From fd46c08c6f02f0e07fde886ec589ba4175b4a9a1 Mon Sep 17 00:00:00 2001 From: Apoorv <130035517+APOORV7G@users.noreply.github.com> Date: Thu, 4 Jun 2026 08:30:04 +0530 Subject: [PATCH 1/2] fix(codegen): update Level enum to include NONE value and adjust JSON schema generation - Added 'NONE' value to the Level enum in the model definition. - Removed $id assignment for inlined map schemas to prevent conflicts in JSON schema generation. - Updated related test snapshots to reflect changes in the Level enum. Signed-off-by: Apoorv <130035517+APOORV7G@users.noreply.github.com> Signed-off-by: Apoorv <130035517+APOORV7G@users.noreply.github.com> --- .../fromcto/jsonschema/jsonschemavisitor.js | 3 +- test/codegen/__snapshots__/codegen.js.snap | 68 +++++++++++-------- test/codegen/fromcto/data/model/hr_base.cto | 4 +- .../fromcto/typescript/typescriptvisitor.js | 4 ++ 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js index e79372c..63be6e7 100644 --- a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js +++ b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js @@ -444,8 +444,9 @@ class JSONSchemaVisitor { let mapKey = mapDeclaration.getModelFile().getType(mapDeclaration.getKey().getType()); let mapValue = mapDeclaration.getModelFile().getType(mapDeclaration.getValue().getType()); + // Do not set $id on inlined map schemas: inherited fields (e.g. Person.nextOfKin + // on Employee, Contractor, Manager) would share the same $id and break Ajv. const jsonSchema = { - $id: field.getFullyQualifiedName(), schema: { title: mapDeclaration.getName(), description : `An instance of ${field.getFullyQualifiedTypeName()}`, diff --git a/test/codegen/__snapshots__/codegen.js.snap b/test/codegen/__snapshots__/codegen.js.snap index a4bb2f0..a34baff 100644 --- a/test/codegen/__snapshots__/codegen.js.snap +++ b/test/codegen/__snapshots__/codegen.js.snap @@ -44,8 +44,10 @@ class \`org.acme.hr.base@1.0.0.Address\` { } \`org.acme.hr.base@1.0.0.Address\` "1" *-- "1" \`org.acme.hr.base@1.0.0.State\` -class \`org.acme.hr.base@1.0.0.Level\` -<< enumeration>> \`org.acme.hr.base@1.0.0.Level\` +class \`org.acme.hr.base@1.0.0.Level\` { +<< enumeration>> + + NONE +} class \`org.acme.hr@1.0.0.pii\` { << concept>> @@ -248,6 +250,7 @@ class org.acme.hr.base_1_0_0.Address { } org.acme.hr.base_1_0_0.Address "1" *-- "1" org.acme.hr.base_1_0_0.State : state class org.acme.hr.base_1_0_0.Level << (E,grey) >> { + + NONE } class org.acme.hr_1_0_0.pii { + Boolean isPii @@ -456,6 +459,7 @@ protocol MyProtocol { } enum Level { + NONE } } @@ -735,6 +739,7 @@ public class Address : Concept { } [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] public enum Level { + NONE, } [System.Text.Json.Serialization.JsonConverter(typeof(TimeJsonConverter))] public readonly record struct Time(System.DateTime Value) @@ -1031,6 +1036,7 @@ type Address struct { } type Level int const ( + NONE Level = 1 + iota ) ", } @@ -1178,7 +1184,7 @@ type Address { country: String! } enum Level { - _: Boolean + NONE } # namespace org.acme.hr@1.0.0 type pii @example @usedBy { @@ -1655,6 +1661,7 @@ package org.acme.hr.base; import com.fasterxml.jackson.annotation.*; @JsonIgnoreProperties({"$class"}) public enum Level { + NONE, } ", } @@ -2429,7 +2436,9 @@ exports[`codegen #formats check we can convert all formats from namespace versio "org.acme.hr.base@1.0.0.Level": { "title": "Level", "description": "An instance of org.acme.hr.base@1.0.0.Level", - "enum": [] + "enum": [ + "NONE" + ] }, "org.acme.hr.base@1.0.0.Time": { "format": "date-time", @@ -2532,7 +2541,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "$ref": "#/definitions/org.acme.hr.base@1.0.0.Address" }, "companyProperties": { - "$id": "org.acme.hr@1.0.0.Company.companyProperties", "schema": { "title": "CompanyProperties", "description": "An instance of org.acme.hr@1.0.0.CompanyProperties", @@ -2546,7 +2554,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio } }, "employeeDirectory": { - "$id": "org.acme.hr@1.0.0.Company.employeeDirectory", "schema": { "title": "EmployeeDirectory", "description": "An instance of org.acme.hr@1.0.0.EmployeeDirectory", @@ -2561,7 +2568,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio } }, "employeeTShirtSizes": { - "$id": "org.acme.hr@1.0.0.Company.employeeTShirtSizes", "schema": { "title": "EmployeeTShirtSizes", "description": "An instance of org.acme.hr.base@1.0.0.EmployeeTShirtSizes", @@ -2576,7 +2582,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio } }, "employeeProfiles": { - "$id": "org.acme.hr@1.0.0.Company.employeeProfiles", "schema": { "title": "EmployeeProfiles", "description": "An instance of org.acme.hr@1.0.0.EmployeeProfiles", @@ -2590,7 +2595,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio } }, "employeeSocialSecurityNumbers": { - "$id": "org.acme.hr@1.0.0.Company.employeeSocialSecurityNumbers", "schema": { "title": "EmployeeSocialSecurityNumbers", "description": "An instance of org.acme.hr@1.0.0.EmployeeSocialSecurityNumbers", @@ -2743,7 +2747,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "$id": "org.acme.hr@1.0.0.Person.nextOfKin", "schema": { "title": "NextOfKin", "description": "An instance of org.acme.hr@1.0.0.NextOfKin", @@ -2850,7 +2853,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "$id": "org.acme.hr@1.0.0.Person.nextOfKin", "schema": { "title": "NextOfKin", "description": "An instance of org.acme.hr@1.0.0.NextOfKin", @@ -2949,7 +2951,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "$id": "org.acme.hr@1.0.0.Person.nextOfKin", "schema": { "title": "NextOfKin", "description": "An instance of org.acme.hr@1.0.0.NextOfKin", @@ -3061,7 +3062,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "$id": "org.acme.hr@1.0.0.Person.nextOfKin", "schema": { "title": "NextOfKin", "description": "An instance of org.acme.hr@1.0.0.NextOfKin", @@ -3242,7 +3242,9 @@ No fields. ### Level (Enumeration) -No values. +| Value | Description | +| ------ | ------------------- | +| NONE | | ## Diagram @@ -3288,8 +3290,10 @@ class \`org.acme.hr.base@1.0.0.Address\` { } \`org.acme.hr.base@1.0.0.Address\` "1" *-- "1" \`org.acme.hr.base@1.0.0.State\` -class \`org.acme.hr.base@1.0.0.Level\` -<< enumeration>> \`org.acme.hr.base@1.0.0.Level\` +class \`org.acme.hr.base@1.0.0.Level\` { +<< enumeration>> + + NONE +} \`\`\` @@ -3736,8 +3740,10 @@ class \`org.acme.hr.base@1.0.0.Address\` { \`org.acme.hr.base@1.0.0.Address\` "1" *-- "1" \`org.acme.hr.base@1.0.0.State\` \`org.acme.hr.base@1.0.0.Address\` --|> \`concerto@1.0.0.Concept\` -class \`org.acme.hr.base@1.0.0.Level\` -<< enumeration>> \`org.acme.hr.base@1.0.0.Level\` +class \`org.acme.hr.base@1.0.0.Level\` { +<< enumeration>> + + NONE +} \`org.acme.hr.base@1.0.0.Level\` --|> \`concerto@1.0.0.Concept\` class \`org.acme.hr@1.0.0.pii\` { @@ -4037,6 +4043,8 @@ exports[`codegen #formats check we can convert all formats from namespace versio + + @@ -4344,7 +4352,9 @@ exports[`codegen #formats check we can convert all formats from namespace versio "org.acme.hr.base@1.0.0.Level": { "title": "Level", "description": "An instance of org.acme.hr.base@1.0.0.Level", - "enum": [] + "enum": [ + "NONE" + ] }, "org.acme.hr.base@1.0.0.Time": { "format": "date-time", @@ -4447,7 +4457,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "$ref": "#/components/schemas/org.acme.hr.base@1.0.0.Address" }, "companyProperties": { - "$id": "org.acme.hr@1.0.0.Company.companyProperties", "schema": { "title": "CompanyProperties", "description": "An instance of org.acme.hr@1.0.0.CompanyProperties", @@ -4461,7 +4470,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio } }, "employeeDirectory": { - "$id": "org.acme.hr@1.0.0.Company.employeeDirectory", "schema": { "title": "EmployeeDirectory", "description": "An instance of org.acme.hr@1.0.0.EmployeeDirectory", @@ -4476,7 +4484,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio } }, "employeeTShirtSizes": { - "$id": "org.acme.hr@1.0.0.Company.employeeTShirtSizes", "schema": { "title": "EmployeeTShirtSizes", "description": "An instance of org.acme.hr.base@1.0.0.EmployeeTShirtSizes", @@ -4491,7 +4498,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio } }, "employeeProfiles": { - "$id": "org.acme.hr@1.0.0.Company.employeeProfiles", "schema": { "title": "EmployeeProfiles", "description": "An instance of org.acme.hr@1.0.0.EmployeeProfiles", @@ -4505,7 +4511,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio } }, "employeeSocialSecurityNumbers": { - "$id": "org.acme.hr@1.0.0.Company.employeeSocialSecurityNumbers", "schema": { "title": "EmployeeSocialSecurityNumbers", "description": "An instance of org.acme.hr@1.0.0.EmployeeSocialSecurityNumbers", @@ -4658,7 +4663,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "$id": "org.acme.hr@1.0.0.Person.nextOfKin", "schema": { "title": "NextOfKin", "description": "An instance of org.acme.hr@1.0.0.NextOfKin", @@ -4765,7 +4769,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "$id": "org.acme.hr@1.0.0.Person.nextOfKin", "schema": { "title": "NextOfKin", "description": "An instance of org.acme.hr@1.0.0.NextOfKin", @@ -4864,7 +4867,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "$id": "org.acme.hr@1.0.0.Person.nextOfKin", "schema": { "title": "NextOfKin", "description": "An instance of org.acme.hr@1.0.0.NextOfKin", @@ -4976,7 +4978,6 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "$id": "org.acme.hr@1.0.0.Person.nextOfKin", "schema": { "title": "NextOfKin", "description": "An instance of org.acme.hr@1.0.0.NextOfKin", @@ -5372,6 +5373,7 @@ class org.acme.hr.base_1_0_0.Address { org.acme.hr.base_1_0_0.Address "1" *-- "1" org.acme.hr.base_1_0_0.State : state org.acme.hr.base_1_0_0.Address --|> concerto_1_0_0.Concept class org.acme.hr.base_1_0_0.Level << (E,grey) >> { + + NONE } org.acme.hr.base_1_0_0.Level --|> concerto_1_0_0.Concept class org.acme.hr_1_0_0.pii { @@ -5529,7 +5531,9 @@ message Address { string zipCode = 5; } -enum Level {} +enum Level { + Level_NONE = 0; +} ", } @@ -6153,6 +6157,8 @@ pub struct Address { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Level { + #[allow(non_camel_case_types)] + NONE, } ", @@ -6889,6 +6895,7 @@ export interface IAddress extends IConcept { } export enum Level { + NONE = 'NONE', } export type Time = Date; @@ -7067,6 +7074,8 @@ declarations: - zipCode: Zip Code of the Address - country: Country of the Address - Level: Level + properties: + - NONE: NONE of the Level - Time: Time - SSN: SSN ", @@ -7344,6 +7353,7 @@ xmlns:concerto="concerto" + diff --git a/test/codegen/fromcto/data/model/hr_base.cto b/test/codegen/fromcto/data/model/hr_base.cto index 7329a38..e02e732 100644 --- a/test/codegen/fromcto/data/model/hr_base.cto +++ b/test/codegen/fromcto/data/model/hr_base.cto @@ -32,7 +32,9 @@ concept Address { o String country } -enum Level {} +enum Level { + o NONE +} scalar Time extends DateTime scalar SSN extends String default="000-00-0000" regex=/(\d{3}-\d{2}-\d{4})+/ diff --git a/test/codegen/fromcto/typescript/typescriptvisitor.js b/test/codegen/fromcto/typescript/typescriptvisitor.js index bdc5d68..efd863a 100644 --- a/test/codegen/fromcto/typescript/typescriptvisitor.js +++ b/test/codegen/fromcto/typescript/typescriptvisitor.js @@ -38,6 +38,10 @@ describe('TypescriptVisitor', function () { typescriptVisitor = new TypescriptVisitor(); mockFileWriter = sinon.createStubInstance(FileWriter); }); + // Leaked Sinon stub doesn't break hr_integration tests + afterEach(() => { + sandbox.restore(); + }); describe('visit', () => { let param; From 2d590f401daf9f096a81b739ffae36f52eb90280 Mon Sep 17 00:00:00 2001 From: Apoorv <130035517+APOORV7G@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:06:25 +0530 Subject: [PATCH 2/2] test(codegen): add tests for non-empty and empty enum handling in JSON schema generation - Introduced tests to verify the correct handling of non-empty enums, ensuring they emit valid values and compile with Ajv. - Added a test to confirm that schemas with empty enums fail Ajv compilation as expected. - Updated the JSON schema visitor to accommodate these new test cases. Signed-off-by: Apoorv <130035517+APOORV7G@users.noreply.github.com> --- .../fromcto/jsonschema/jsonschemavisitor.js | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/codegen/fromcto/jsonschema/jsonschemavisitor.js b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js index f148a56..6b21eae 100644 --- a/test/codegen/fromcto/jsonschema/jsonschemavisitor.js +++ b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js @@ -251,6 +251,30 @@ concept SomeOtherThing identified { } `; +const MODEL_NON_EMPTY_ENUMS = ` +namespace test@1.0.0 + +enum Level { + o NONE +} + +enum Role { + o ADMIN + o USER +} + +map Tags { + o String + o String +} +`; + +const MODEL_EMPTY_ENUM = ` +namespace test@1.0.0 + +enum Level {} +`; + describe('JSONSchema (samples)', function () { describe('samples', () => { @@ -344,6 +368,52 @@ describe('JSONSchema (samples)', function () { expect(schema.properties.someId.format).to.be.undefined; }); + it('should emit non-empty enum arrays and compile with Ajv', () => { + const modelManager = new ModelManager(); + modelManager.addCTOModel(MODEL_NON_EMPTY_ENUMS); + const visitor = new JSONSchemaVisitor(); + const schema = modelManager.accept(visitor, {}); + + const enumDefinitions = Object.entries(schema.definitions) + .filter(([, definition]) => Array.isArray(definition.enum)); + + expect(enumDefinitions.length).to.equal(2); + + enumDefinitions.forEach(([fqn, definition]) => { + expect( + definition.enum, + `${fqn} must declare at least one enum value` + ).to.have.lengthOf.at.least(1); + definition.enum.forEach((value, index) => { + expect( + value, + `${fqn} enum[${index}] must not be null or undefined` + ).to.not.be.oneOf([null, undefined]); + }); + }); + + expect(schema.definitions['test@1.0.0.Level'].enum).to.include('NONE'); + expect(schema.definitions['test@1.0.0.Role'].enum).to.include('ADMIN', 'USER'); + + const ajv = new Ajv({ strict: false }); + ajv.compile(schema); + + // maps are handled inline on fields; visiting a map type alone does nothing + expect(visitor.visit(modelManager.getType('test@1.0.0.Tags'), {})).to.be.undefined; + }); + + it('should fail Ajv compile when generated schema contains an empty enum', () => { + const modelManager = new ModelManager(); + modelManager.addCTOModel(MODEL_EMPTY_ENUM); + const visitor = new JSONSchemaVisitor(); + const schema = modelManager.accept(visitor, {}); + + expect(schema.definitions['test@1.0.0.Level'].enum).to.deep.equal([]); + + const ajv = new Ajv({ strict: false }); + expect(() => ajv.compile(schema)).to.throw(/fewer than 1 item/); + }); + it('should generate for Map of type ', () => { const modelManager = new ModelManager(); modelManager.addCTOModel( MODEL_MAP_STRING );