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
135 changes: 77 additions & 58 deletions lib/codegen/fromcto/csharp/csharpvisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,8 @@ class CSharpVisitor {

parameters.fileWriter.writeLine(0, `namespace ${dotNetNamespace};`);

if (modelFile.getAllDeclarations().some(d => d.isMapDeclaration?.())) {
parameters.fileWriter.writeLine(0, 'using System.Collections.Generic;');
}
parameters.fileWriter.writeLine(0, 'using System.Collections.Generic;');
parameters.fileWriter.writeLine(0, 'using System.ComponentModel.DataAnnotations;');
Comment thread
muhabdulkadir marked this conversation as resolved.

modelFile.getImports()
.map(importString => ModelUtil.getNamespace(importString))
Expand Down Expand Up @@ -435,24 +434,12 @@ class CSharpVisitor {
const fqn = ModelUtil.removeNamespaceVersionFromFullyQualifiedName(scalarDecl.getFullyQualifiedName());
return fqn === 'concerto.scalar.UUID' ? 'System.Guid' : this.toCSharpIdentifier(undefined, scalarDecl.getName(), parameters);
}
return this.toCSharpIdentifier(undefined, typeName, parameters);
}

/**
* Returns true if the map key/value side is a scalar that declares validators.
* Used to decide whether to emit [ValidateComplexType] on the map property.
* @param {MapKeyType|MapValueType} side - key or value side of the map
* @param {ModelFile} modelFile - the model file containing the map declaration
* @returns {boolean} - true if the map side is a scalar with validators, false otherwise
* @private
*/
mapSideHasValidators(side, modelFile) {
const typeName = side.getType();
if (!ModelUtil.isPrimitiveType(typeName) && ModelUtil.isScalar(side)) {
const scalarDecl = modelFile.getType(typeName);
return !!scalarDecl?.getValidator?.();
}
return false;
// concept or relationship — reuse getDotNetNamespaceOfType which returns '' for
// same-namespace types and 'Some.Ns.' for cross-namespace types.
const typeFqn = modelFile.getType(typeName)?.getFullyQualifiedName();
const canonicalName = typeFqn ? ModelUtil.getShortName(typeFqn) : typeName;
const ns = this.getDotNetNamespaceOfType(typeFqn, side.getParent(), parameters);
return `${ns}${this.toCSharpIdentifier(undefined, canonicalName, parameters)}`;
}

/**
Expand Down Expand Up @@ -532,52 +519,57 @@ class CSharpVisitor {

// Check the underlying field type for validator applicability (handles scalar aliases)
const rawFieldType = this.getFieldType(field);
if (rawFieldType === 'String') {
const validator = field.getValidator();

if(validator) {
if(validator.getMinLength()) {
parameters.fileWriter.writeLine(1, `[System.ComponentModel.DataAnnotations.MinLength(${validator.getMinLength()})]`);
}
if(validator.getMaxLength()) {
parameters.fileWriter.writeLine(1, `[System.ComponentModel.DataAnnotations.MaxLength(${validator.getMaxLength()})]`);
}
if (validator.getRegex()) {
let regexVal = validator.getRegex().source;
parameters.fileWriter.writeLine(1, `[System.ComponentModel.DataAnnotations.RegularExpression(@"${regexVal}", ErrorMessage = "Invalid characters")]`);
// Only emit DataAnnotation validators for raw primitive-typed properties.
// Scalar-typed properties (externalFieldType set) already carry their validators
// on the scalar struct's Value property — emitting them here would produce
// attributes like [MinLength] on a struct type, which throws at runtime.
if (!externalFieldType) {
if (rawFieldType === 'String') {
const validator = field.getValidator();

if(validator) {
if(validator.getMinLength()) {
parameters.fileWriter.writeLine(1, `[System.ComponentModel.DataAnnotations.MinLength(${validator.getMinLength()})]`);
}
if(validator.getMaxLength()) {
parameters.fileWriter.writeLine(1, `[System.ComponentModel.DataAnnotations.MaxLength(${validator.getMaxLength()})]`);
}
if (validator.getRegex()) {
let regexVal = validator.getRegex().source;
parameters.fileWriter.writeLine(1, `[System.ComponentModel.DataAnnotations.RegularExpression(@"${regexVal}", ErrorMessage = "Invalid characters")]`);
}
}
}
} else if (['Integer', 'Long', 'Double'].includes(rawFieldType)) {
const validator = field.getValidator();
if (validator) {
const lower = validator.getLowerBound();
const upper = validator.getUpperBound();
if (lower != null || upper != null) {
const csTypeMap = { Integer: 'int', Long: 'long', Double: 'double' };
const defaultMin = { Integer: '-2147483648', Long: '-9223372036854775808', Double: '-1.7976931348623157E+308' };
const defaultMax = { Integer: '2147483647', Long: '9223372036854775807', Double: '1.7976931348623157E+308' };
const csType = csTypeMap[rawFieldType];
const lo = lower != null ? String(lower) : defaultMin[rawFieldType];
const hi = upper != null ? String(upper) : defaultMax[rawFieldType];
parameters.fileWriter.writeLine(1, `[System.ComponentModel.DataAnnotations.Range(typeof(${csType}), "${lo}", "${hi}")]`);
} else if (['Integer', 'Long', 'Double'].includes(rawFieldType)) {
const validator = field.getValidator();
if (validator) {
const lower = validator.getLowerBound();
const upper = validator.getUpperBound();
if (lower != null || upper != null) {
const csTypeMap = { Integer: 'int', Long: 'long', Double: 'double' };
const defaultMin = { Integer: '-2147483648', Long: '-9223372036854775808', Double: '-1.7976931348623157E+308' };
const defaultMax = { Integer: '2147483647', Long: '9223372036854775807', Double: '1.7976931348623157E+308' };
const csType = csTypeMap[rawFieldType];
const lo = lower != null ? String(lower) : defaultMin[rawFieldType];
const hi = upper != null ? String(upper) : defaultMax[rawFieldType];
parameters.fileWriter.writeLine(1, `[System.ComponentModel.DataAnnotations.Range(typeof(${csType}), "${lo}", "${hi}")]`);
}
}
} else if (!field.isPrimitive()) {
const fqn = this.getDotNetNamespaceOfType(field.getFullyQualifiedTypeName(), field.getParent(), parameters);
fieldType = `${fqn}${ModelUtil.getShortName(field.getFullyQualifiedTypeName())}`;
}
} else if (!externalFieldType && !field.isPrimitive()) {
let fqn = this.getDotNetNamespaceOfType(field.getFullyQualifiedTypeName(), field.getParent(), parameters);
const modelFile = field.getModelFile();
if (modelFile?.isImportedType(fieldType)) {
fieldType = modelFile.getImportedType(fieldType);
}
fieldType = `${fqn}${fieldType}`;
}

let nullableType = '';
if(field.isOptional() || isOptional){
nullableType = '?';
}

const rawDefault = externalFieldType !== undefined ? scalarDefaultValue : field.getDefaultValue();
const csDefault = this.formatDefaultLiteral(rawDefault, rawFieldType, !!externalFieldType);
// Arrays have no per-element default initializer; scalar defaults must not bleed into T[] = new(x).
const rawDefault = field.isArray()
? null
: (externalFieldType !== undefined ? scalarDefaultValue : field.getDefaultValue());
const csDefault = this.formatDefaultLiteral(rawDefault, rawFieldType, !!externalFieldType, field, fieldType);
const getset = csDefault != null ? `{ get; set; } = ${csDefault};` : '{ get; set; }';

const lines = this.toCSharpProperty(
Expand Down Expand Up @@ -650,6 +642,14 @@ class CSharpVisitor {
// if it's scalar type, remove namespace and version from fqn
type = ModelUtil.removeNamespaceVersionFromFullyQualifiedName(qualifiedType);
}
} else {
// Qualify the type when the relationship target is in a different namespace
const fqn = this.getDotNetNamespaceOfType(
relationship.getFullyQualifiedTypeName(),
relationship.getParent(),
parameters
);
type = `${fqn}${ModelUtil.getShortName(relationship.getFullyQualifiedTypeName())}`;
}

// we export all relationships
Expand All @@ -673,13 +673,32 @@ class CSharpVisitor {
* @param {*} value - the raw default value from getDefaultValue()
* @param {string} concertoType - the underlying Concerto primitive type
* @param {boolean} isScalar - true when the property type is a scalar struct
* @param {Field} [field] - the field for context when handling enum defaults (optional)
* @param {string} [resolvedFieldType] - resolved C# property type used for enum qualification
* @returns {string|null} C# literal string, or null if no default
* @private
*/
formatDefaultLiteral(value, concertoType, isScalar) {
formatDefaultLiteral(value, concertoType, isScalar, field, resolvedFieldType) {
if (value == null) {return null;}
// Pre-computed C# literal (e.g. UUID default needs System.Guid.Parse, not a bare string)
if (value?.__csharpLiteral) {return value.__csharpLiteral;}

// Handle enum types: emit the enum member access expression (e.g. PartySide.External) rather than a raw literal (e.g. "EXTERNAL")
if (field && !isScalar) {
try {
const fieldType = field.getModelFile().getType(field.getType());
if (fieldType?.isEnum?.()) {
// Enum value found; emit Type.Member
// e.g., "EXTERNAL" -> "PartySide.External"
const enumMemberName = camelCase(String(value), { pascalCase: true });
const enumTypeName = resolvedFieldType || this.toCSharpIdentifier(undefined, field.getType());
return `${enumTypeName}.${enumMemberName}`;
}
} catch (e) {
// Type resolution failed; fall through to default handling
}
}

const rawLiteral = concertoType === 'String' ? `"${value}"` : String(value);
return isScalar ? `new(${rawLiteral})` : rawLiteral;
}
Expand Down
38 changes: 19 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"author": "accordproject.org",
"license": "Apache-2.0",
"devDependencies": {
"@accordproject/concerto-cto": "4.1.2",
"@accordproject/concerto-cto": "4.1.3",
"@babel/preset-env": "7.16.11",
"@types/webgl-ext": "0.0.37",
"babel-loader": "8.2.3",
Expand Down Expand Up @@ -88,9 +88,9 @@
"pluralize": "8.0.0"
},
"peerDependencies": {
"@accordproject/concerto-core": "^4.1.2",
"@accordproject/concerto-util": "^4.1.2",
"@accordproject/concerto-vocabulary": "^4.1.2"
"@accordproject/concerto-core": "^4.1.3",
"@accordproject/concerto-util": "^4.1.3",
"@accordproject/concerto-vocabulary": "^4.1.3"
},
"license-check-and-add-config": {
"folder": "./lib",
Expand Down
7 changes: 6 additions & 1 deletion test/codegen/__snapshots__/codegen.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,8 @@ exports[`codegen #formats check we can convert all formats from namespace versio
{
"key": "concerto.decorator@1.0.0.cs",
"value": "namespace concerto.decorator;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using AccordProject.Concerto;
[AccordProject.Concerto.Type(Namespace = "concerto.decorator", Version = "1.0.0", Name = "Decorator")]
[System.Text.Json.Serialization.JsonConverter(typeof(AccordProject.Concerto.ConcertoConverterFactorySystem))]
Expand All @@ -644,6 +646,8 @@ exports[`codegen #formats check we can convert all formats from namespace versio
{
"key": "concerto@1.0.0.cs",
"value": "namespace AccordProject.Concerto;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using concerto.decorator;
[AccordProject.Concerto.Type(Namespace = "concerto", Version = "1.0.0", Name = "Concept")]
[System.Text.Json.Serialization.JsonConverter(typeof(AccordProject.Concerto.ConcertoConverterFactorySystem))]
Expand Down Expand Up @@ -696,6 +700,7 @@ exports[`codegen #formats check we can convert all formats from namespace versio
"key": "org.acme.hr.base@1.0.0.cs",
"value": "namespace org.acme.hr.base;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using AccordProject.Concerto;
[AccordProject.Concerto.Type(Namespace = "org.acme.hr.base", Version = "1.0.0", Name = "Category")]
[System.Text.Json.Serialization.JsonConverter(typeof(AccordProject.Concerto.ConcertoConverterFactorySystem))]
Expand Down Expand Up @@ -778,6 +783,7 @@ exports[`codegen #formats check we can convert all formats from namespace versio
"key": "org.acme.hr@1.0.0.cs",
"value": "namespace org.acme.hr;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using org.acme.hr.base;
using concerto.decorator;
using AccordProject.Concerto;
Expand Down Expand Up @@ -865,7 +871,6 @@ public abstract class Person : Participant {
public string lastName { get; set; }
public string? middleNames { get; set; }
public org.acme.hr.base.Address homeAddress { get; set; }
[System.ComponentModel.DataAnnotations.RegularExpression(@"(\\d{3}-\\d{2}-\\d{4})+", ErrorMessage = "Invalid characters")]
public SSN ssn { get; set; } = new("000-00-0000");
public double height { get; set; }
public System.DateTime dob { get; set; }
Expand Down
4 changes: 2 additions & 2 deletions test/codegen/fromJsonSchema/cto/data/concertoModel.cto
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ concept Root {
o DateTime dob
o DateTime graduationDate optional
o Integer age
o Double height range=[50,]
o Double height range=[50.0,]
o String favouriteFood optional
o Root$_properties$_children[] children
o Root$_properties$_company company
Expand Down Expand Up @@ -97,7 +97,7 @@ concept Pet {

concept Stuff {
o String sku
o Double price range=[,99999999]
o Double price range=[,99999999.0]
o Pet product
}

Expand Down
4 changes: 2 additions & 2 deletions test/codegen/fromJsonSchema/cto/jsonSchemaVisitor.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,8 @@ concept veggie {
concept geographical_location {
o String name default="home" regex=/[\\w\\s]+/ optional
o Double latitude
o Double longitude range=[-180,180]
o Double elevation range=[-11034,] optional
o Double longitude range=[-180.0,180.0]
o Double elevation range=[-11034.0,] optional
o Integer yearDiscovered range=[,2022] optional
}`);
});
Expand Down
Loading
Loading