Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 52 additions & 33 deletions lib/codegen/fromcto/csharp/csharpvisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ class CSharpVisitor {
return this.visitEnumDeclaration(thing, parameters);
} else if (thing.isClassDeclaration?.()) {
return this.visitClassDeclaration(thing, parameters);
} else if (thing.isMapDeclaration?.()) {
return;
} else if (thing.isMapDeclaration?.()) {
return this.visitMapDeclaration(thing, parameters);
} else if (thing.isTypeScalar?.()) {
return this.visitScalarField(thing, parameters);
} else if (thing.isField?.()) {
Expand Down Expand Up @@ -264,6 +264,53 @@ class CSharpVisitor {
return dotnetNs;
}

/**
* Resolve the C# key and value types for a MapDeclaration.
* Handles primitives, scalars (via csharpScalarPrimitives), and concept types.
* @param {MapDeclaration} mapDeclaration - the map declaration to resolve types for
* @returns {{ keyType: string, valueType: string }} the resolved C# key and value type strings
* @private
*/
resolveMapTypes(mapDeclaration) {
const mapKeyType = mapDeclaration.getKey().getType();
const mapValueType = mapDeclaration.getValue().getType();

let keyType;
if (ModelUtil.isPrimitiveType(mapKeyType)) {
keyType = this.toCSharpType(mapKeyType);
} else if (ModelUtil.isScalar(mapDeclaration.getKey())) {
const scalarDecl = mapDeclaration.getModelFile().getType(mapKeyType);
keyType = this.toCSharpType(scalarDecl.getType());
}

let valueType;
if (ModelUtil.isPrimitiveType(mapValueType)) {
valueType = this.toCSharpType(mapValueType);
} else if (ModelUtil.isScalar(mapDeclaration.getValue())) {
const scalarDecl = mapDeclaration.getModelFile().getType(mapValueType);
valueType = this.toCSharpType(scalarDecl.getType());
} else {
valueType = mapValueType;
}

return { keyType, valueType };
}

/**
* Visitor design pattern
* @param {MapDeclaration} mapDeclaration - the object being visited
* @param {Object} parameters - the parameter
* @return {Object} the result of visiting or null
* @private
*/
visitMapDeclaration(mapDeclaration, parameters) {
const { keyType, valueType } = this.resolveMapTypes(mapDeclaration);
const name = mapDeclaration.getName();
const identifier = this.toCSharpIdentifier(undefined, name, parameters);
parameters.fileWriter.writeLine(0, `public class ${identifier} : Dictionary<${keyType}, ${valueType}> {}`);
return null;
}

/**
* Visitor design pattern
* @param {ScalarDeclaration} scalarDeclaration - the object being visited
Expand Down Expand Up @@ -313,37 +360,9 @@ class CSharpVisitor {
// write Map field
if (ModelUtil.isMap?.(field)) {
const mapDeclaration = field.getModelFile().getType(field.getType());
const mapKeyType = mapDeclaration.getKey().getType();
const mapValueType = mapDeclaration.getValue().getType();

let keyType;
let valueType;

// Map Key
if (ModelUtil.isPrimitiveType(mapKeyType)) {
keyType = this.toCSharpType(mapKeyType);
}
else if (ModelUtil.isScalar(mapDeclaration.getKey())) {
const scalarDeclaration = mapDeclaration.getModelFile().getType(mapDeclaration.getKey().getType());
const keyFQN = ModelUtil.removeNamespaceVersionFromFullyQualifiedName(scalarDeclaration.getFullyQualifiedName());
const scalarType = keyFQN === 'concerto.scalar.UUID' ? keyFQN : scalarDeclaration.getType();
keyType = this.toCSharpType(scalarType);
}

// Map Value
if (ModelUtil.isPrimitiveType(mapValueType)) {
valueType = this.toCSharpType(mapValueType);
}
else if (ModelUtil.isScalar(mapDeclaration.getValue())) {
const scalarDeclaration = mapDeclaration.getModelFile().getType(mapDeclaration.getValue().getType());
const keyFQN = ModelUtil.removeNamespaceVersionFromFullyQualifiedName(scalarDeclaration.getFullyQualifiedName());
const scalarType = keyFQN === 'concerto.scalar.UUID' ? keyFQN : scalarDeclaration.getType();
valueType = this.toCSharpType(scalarType);
} else {
valueType = mapValueType;
}

parameters.fileWriter.writeLine(1, `public Dictionary<${keyType}, ${valueType}> ${field.getName()} { get; set; }`);
const { keyType, valueType } = this.resolveMapTypes(mapDeclaration);
const nullable = field.isOptional() ? '?' : '';
parameters.fileWriter.writeLine(1, `public Dictionary<${keyType}, ${valueType}>${nullable} ${field.getName()} { get; set; }`);
return null;
}

Expand Down
17 changes: 12 additions & 5 deletions test/codegen/__snapshots__/codegen.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ public enum TShirtSizeType {
MEDIUM,
LARGE,
}
public class EmployeeTShirtSizes : Dictionary<string, TShirtSizeType> {}
[AccordProject.Concerto.Type(Namespace = "org.acme.hr.base", Version = "1.0.0", Name = "Address")]
[System.Text.Json.Serialization.JsonConverter(typeof(AccordProject.Concerto.ConcertoConverterFactorySystem))]
public class Address : Concept {
Expand Down Expand Up @@ -771,6 +772,12 @@ public class Info : Concept {
public override string _class { get; } = "org.acme.hr@1.0.0.Info";
public string name { get; set; }
}
public class CompanyProperties : Dictionary<string, string> {}
public class EmployeeLoginTimes : Dictionary<string, System.DateTime> {}
public class EmployeeSocialSecurityNumbers : Dictionary<string, string> {}
public class NextOfKin : Dictionary<string, string> {}
public class EmployeeProfiles : Dictionary<string, Concept> {}
public class EmployeeDirectory : Dictionary<string, Employee> {}
[AccordProject.Concerto.Type(Namespace = "org.acme.hr", Version = "1.0.0", Name = "Company")]
[System.Text.Json.Serialization.JsonConverter(typeof(AccordProject.Concerto.ConcertoConverterFactorySystem))]
public class Company : Concept {
Expand All @@ -779,11 +786,11 @@ public class Company : Concept {
[System.ComponentModel.DataAnnotations.RegularExpression(@"abc.*", ErrorMessage = "Invalid characters")]
public string name { get; set; }
public org.acme.hr.base.Address headquarters { get; set; }
public Dictionary<string, string> companyProperties { get; set; }
public Dictionary<string, Employee> employeeDirectory { get; set; }
public Dictionary<string, TShirtSizeType> employeeTShirtSizes { get; set; }
public Dictionary<string, Concept> employeeProfiles { get; set; }
public Dictionary<string, string> employeeSocialSecurityNumbers { get; set; }
public Dictionary<string, string>? companyProperties { get; set; }
public Dictionary<string, Employee>? employeeDirectory { get; set; }
public Dictionary<string, TShirtSizeType>? employeeTShirtSizes { get; set; }
public Dictionary<string, Concept>? employeeProfiles { get; set; }
public Dictionary<string, string>? employeeSocialSecurityNumbers { get; set; }
}
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))]
public enum Department {
Expand Down
99 changes: 99 additions & 0 deletions test/codegen/fromcto/csharp/csharpvisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -1783,6 +1783,57 @@ public class SampleModel : Concept {
});
});

describe('map field integration (HR model)', () => {
let modelManager;
let fileWriter;

beforeEach(() => {
sandbox.restore();
modelManager = new ModelManager({ strict: true });
modelManager.addCTOModel(
fs.readFileSync(path.resolve(__dirname, '../data/model/hr_base.cto'), 'utf-8'),
'hr_base.cto'
);
modelManager.addCTOModel(
fs.readFileSync(path.resolve(__dirname, '../data/model/hr.cto'), 'utf-8'),
'hr.cto'
);
fileWriter = new InMemoryWriter();
});

it('should emit map declarations as Dictionary subclasses', () => {
csharpVisitor.visit(modelManager, { fileWriter });
const hrFile = fileWriter.getFilesInMemory().get('org.acme.hr@1.0.0.cs');
const basFile = fileWriter.getFilesInMemory().get('org.acme.hr.base@1.0.0.cs');

// hr.cto map declarations
hrFile.should.match(/public class CompanyProperties : Dictionary<string, string> \{\}/);
hrFile.should.match(/public class EmployeeLoginTimes : Dictionary<string, System\.DateTime> \{\}/);
hrFile.should.match(/public class EmployeeSocialSecurityNumbers : Dictionary<string, string> \{\}/);
hrFile.should.match(/public class NextOfKin : Dictionary<string, string> \{\}/);
hrFile.should.match(/public class EmployeeProfiles : Dictionary<string, Concept> \{\}/);
hrFile.should.match(/public class EmployeeDirectory : Dictionary<string, Employee> \{\}/);

// hr_base.cto map declaration (SSN scalar key → enum value)
basFile.should.match(/public class EmployeeTShirtSizes : Dictionary<string, TShirtSizeType> \{\}/);
});

it('should emit map fields as Dictionary<K,V> typed properties', () => {
csharpVisitor.visit(modelManager, { fileWriter });
const hrFile = fileWriter.getFilesInMemory().get('org.acme.hr@1.0.0.cs');

// Company fields
hrFile.should.match(/public Dictionary<string, string>\? companyProperties \{ get; set; \}/);
hrFile.should.match(/public Dictionary<string, TShirtSizeType>\? employeeTShirtSizes \{ get; set; \}/);
hrFile.should.match(/public Dictionary<string, string>\? employeeSocialSecurityNumbers \{ get; set; \}/);
hrFile.should.match(/public Dictionary<string, Concept>\? employeeProfiles \{ get; set; \}/);
hrFile.should.match(/public Dictionary<string, Employee>\? employeeDirectory \{ get; set; \}/);

// Person field (scalar-key map, non-optional)
hrFile.should.match(/public Dictionary<string, string> nextOfKin \{ get; set; \}/);
});
});

describe('visitEnumValueDeclaration', () => {
it('should write a line with the name of the enum value', () => {
let param = {
Expand Down Expand Up @@ -1878,6 +1929,54 @@ public class SampleModel : Concept {
});
});

describe('visitMapDeclaration', () => {
let param;
let mockMapDeclaration;

beforeEach(() => {
param = { fileWriter: mockFileWriter };
mockMapDeclaration = sinon.createStubInstance(MapDeclaration);
mockMapDeclaration.getName.returns('PhoneBook');
mockMapDeclaration.isMapDeclaration.returns(true);
});

it('should emit a Dictionary subclass for a primitive key and primitive value', () => {
const modelFile = sinon.createStubInstance(ModelFile);
mockMapDeclaration.getModelFile.returns(modelFile);
sandbox.stub(ModelUtil, 'isPrimitiveType').callsFake(t => t === 'String');
sandbox.stub(ModelUtil, 'isScalar').returns(false);
mockMapDeclaration.getKey.returns({ getType: () => 'String' });
mockMapDeclaration.getValue.returns({ getType: () => 'String' });

csharpVisitor.visitMapDeclaration(mockMapDeclaration, param);
param.fileWriter.writeLine.withArgs(0, 'public class PhoneBook : Dictionary<string, string> {}').calledOnce.should.be.ok;
});

it('should emit a Dictionary subclass for a primitive key and concept value', () => {
const modelFile = sinon.createStubInstance(ModelFile);
mockMapDeclaration.getModelFile.returns(modelFile);
sandbox.stub(ModelUtil, 'isPrimitiveType').callsFake(t => t === 'String');
sandbox.stub(ModelUtil, 'isScalar').returns(false);
mockMapDeclaration.getKey.returns({ getType: () => 'String' });
mockMapDeclaration.getValue.returns({ getType: () => 'Person' });

csharpVisitor.visitMapDeclaration(mockMapDeclaration, param);
param.fileWriter.writeLine.withArgs(0, 'public class PhoneBook : Dictionary<string, Person> {}').calledOnce.should.be.ok;
});

it('should emit a Dictionary subclass for a primitive key and DateTime value', () => {
const modelFile = sinon.createStubInstance(ModelFile);
mockMapDeclaration.getModelFile.returns(modelFile);
sandbox.stub(ModelUtil, 'isPrimitiveType').returns(true);
sandbox.stub(ModelUtil, 'isScalar').returns(false);
mockMapDeclaration.getKey.returns({ getType: () => 'String' });
mockMapDeclaration.getValue.returns({ getType: () => 'DateTime' });

csharpVisitor.visitMapDeclaration(mockMapDeclaration, param);
param.fileWriter.writeLine.withArgs(0, 'public class PhoneBook : Dictionary<string, System.DateTime> {}').calledOnce.should.be.ok;
});
});

describe('toCSharpType', () => {
it('should return System.DateTime for DateTime', () => {
csharpVisitor.toCSharpType('DateTime').should.deep.equal('System.DateTime');
Expand Down
Loading