diff --git a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js
index e79372cc..63be6e76 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 a4bb2f08..a34bafff 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 7329a382..e02e7321 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/jsonschema/jsonschemavisitor.js b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js
index f148a566..6b21eaeb 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 );
diff --git a/test/codegen/fromcto/typescript/typescriptvisitor.js b/test/codegen/fromcto/typescript/typescriptvisitor.js
index bdc5d687..efd863a2 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;