diff --git a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js index 7c11e79..e6b7437 100644 --- a/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js +++ b/lib/codegen/fromcto/jsonschema/jsonschemavisitor.js @@ -136,6 +136,28 @@ class JSONSchemaVisitor { return jsonSchema; } + /** + * Get the schema for the key or value type of a map declaration, the same way as for a + * field of that type: a scalar contributes its base type and validators, and a class + * declaration is referenced by its fully qualified name, from the namespace it is declared in. + * @param {Object} [declaration] - the declaration of the type, if it is not a primitive + * @param {String} typeName - the type name used in the map declaration + * @param {Object} parameters - the parameters + * @return {Object} the JSON schema for the map key or value + * @private + */ + getMapEntrySchema(declaration, typeName, parameters) { + if (declaration?.isClassDeclaration?.()) { + const refRoot = parameters.refRoot ? parameters.refRoot : '#/definitions'; + return { $ref: `${refRoot}/${declaration.getFullyQualifiedName()}` }; + } + if (declaration?.isScalarDeclaration?.()) { + const isScalarUUID = ModelUtil.removeNamespaceVersionFromFullyQualifiedName(declaration.getFullyQualifiedName()) === 'concerto.scalar.UUID'; + return this.getFieldOrScalarDeclarationValidatorsForSchema(declaration.getType(), isScalarUUID, declaration.getValidator()); + } + return this.getFieldOrScalarDeclarationValidatorsForSchema(typeName); + } + /** * Returns true if the class declaration contains recursive references. * @@ -441,50 +463,31 @@ class JSONSchemaVisitor { } else if (ModelUtil.isMap?.(field)) { const mapDeclaration = field.getParent().getModelFile().getModelManager().getType(field.getFullyQualifiedTypeName()); - let mapKey = mapDeclaration.getModelFile().getType(mapDeclaration.getKey().getType()); - let mapValue = mapDeclaration.getModelFile().getType(mapDeclaration.getValue().getType()); + const keyType = mapDeclaration.getKey().getType(); + const valueType = 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 = { - schema: { - title: mapDeclaration.getName(), - description : `An instance of ${field.getFullyQualifiedTypeName()}`, - type: 'object', - propertyNames: { - type: 'string' - }, - additionalProperties: { - type: this.getFieldOrScalarDeclarationValidatorsForSchema(mapDeclaration.getValue().getType()).type - } - } + // The schema is returned as the property's schema itself, like every other field type. + const mapSchema = { + title: mapDeclaration.getName(), + description : `An instance of ${field.getFullyQualifiedTypeName()}`, + type: 'object', + propertyNames: this.getMapEntrySchema(mapDeclaration.getModelFile().getType(keyType), keyType, parameters), + additionalProperties: this.getMapEntrySchema(mapDeclaration.getModelFile().getType(valueType), valueType, parameters) }; - if (mapKey.isScalarDeclaration?.() && mapKey.getValidator()?.getRegex()) { - jsonSchema.schema.propertyNames.pattern = String(mapKey.getValidator().getRegex()); - - } - - if (mapValue.isScalarDeclaration?.() && mapValue.getValidator()?.getRegex()) { - jsonSchema.schema.additionalProperties.pattern = String(mapValue.getValidator().getRegex()); - } - - // if its a ClassDeclaration, add reference to its schema. - if (mapValue?.isClassDeclaration?.()) { - jsonSchema.schema.additionalProperties = {$ref: `#/definitions/${field.getParent().getModelFile().getNamespace().concat(`.${mapDeclaration.getValue().getType()}`)}`}; - } - const sizeValidator = field.getSizeValidator?.(); if (sizeValidator) { if (sizeValidator.getMinSize() !== null) { - jsonSchema.schema.minProperties = sizeValidator.getMinSize(); + mapSchema.minProperties = sizeValidator.getMinSize(); } if (sizeValidator.getMaxSize() !== null) { - jsonSchema.schema.maxProperties = sizeValidator.getMaxSize(); + mapSchema.maxProperties = sizeValidator.getMaxSize(); } } - return jsonSchema; + return mapSchema; // Not primitive, so must be a class or enumeration! } else { // Look up the type of the property. diff --git a/test/codegen/__snapshots__/codegen.js.snap b/test/codegen/__snapshots__/codegen.js.snap index c5b08e3..921f978 100644 --- a/test/codegen/__snapshots__/codegen.js.snap +++ b/test/codegen/__snapshots__/codegen.js.snap @@ -2518,71 +2518,61 @@ exports[`codegen #formats check we can convert all formats from namespace versio "$ref": "#/definitions/org.acme.hr.base@1.0.0.Address" }, "companyProperties": { - "schema": { - "title": "CompanyProperties", - "description": "An instance of org.acme.hr@1.0.0.CompanyProperties", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } + "title": "CompanyProperties", + "description": "An instance of org.acme.hr@1.0.0.CompanyProperties", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } }, "employeeDirectory": { - "schema": { - "title": "EmployeeDirectory", - "description": "An instance of org.acme.hr@1.0.0.EmployeeDirectory", - "type": "object", - "propertyNames": { - "type": "string", - "pattern": "/(\\\\d{3}-\\\\d{2}-\\\\d{4})+/" - }, - "additionalProperties": { - "$ref": "#/definitions/org.acme.hr@1.0.0.Employee" - } + "title": "EmployeeDirectory", + "description": "An instance of org.acme.hr@1.0.0.EmployeeDirectory", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "(\\\\d{3}-\\\\d{2}-\\\\d{4})+" + }, + "additionalProperties": { + "$ref": "#/definitions/org.acme.hr@1.0.0.Employee" } }, "employeeTShirtSizes": { - "schema": { - "title": "EmployeeTShirtSizes", - "description": "An instance of org.acme.hr.base@1.0.0.EmployeeTShirtSizes", - "type": "object", - "propertyNames": { - "type": "string", - "pattern": "/(\\\\d{3}-\\\\d{2}-\\\\d{4})+/" - }, - "additionalProperties": { - "$ref": "#/definitions/org.acme.hr@1.0.0.TShirtSizeType" - } + "title": "EmployeeTShirtSizes", + "description": "An instance of org.acme.hr.base@1.0.0.EmployeeTShirtSizes", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "(\\\\d{3}-\\\\d{2}-\\\\d{4})+" + }, + "additionalProperties": { + "$ref": "#/definitions/org.acme.hr.base@1.0.0.TShirtSizeType" } }, "employeeProfiles": { - "schema": { - "title": "EmployeeProfiles", - "description": "An instance of org.acme.hr@1.0.0.EmployeeProfiles", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/definitions/org.acme.hr@1.0.0.Concept" - } + "title": "EmployeeProfiles", + "description": "An instance of org.acme.hr@1.0.0.EmployeeProfiles", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/definitions/concerto@1.0.0.Concept" } }, "employeeSocialSecurityNumbers": { - "schema": { - "title": "EmployeeSocialSecurityNumbers", - "description": "An instance of org.acme.hr@1.0.0.EmployeeSocialSecurityNumbers", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "SSN", - "pattern": "/(\\\\d{3}-\\\\d{2}-\\\\d{4})+/" - } + "title": "EmployeeSocialSecurityNumbers", + "description": "An instance of org.acme.hr@1.0.0.EmployeeSocialSecurityNumbers", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "pattern": "(\\\\d{3}-\\\\d{2}-\\\\d{4})+" } } }, @@ -2724,16 +2714,14 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "schema": { - "title": "NextOfKin", - "description": "An instance of org.acme.hr@1.0.0.NextOfKin", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "KinTelephone" - } + "title": "NextOfKin", + "description": "An instance of org.acme.hr@1.0.0.NextOfKin", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } } }, @@ -2830,16 +2818,14 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "schema": { - "title": "NextOfKin", - "description": "An instance of org.acme.hr@1.0.0.NextOfKin", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "KinTelephone" - } + "title": "NextOfKin", + "description": "An instance of org.acme.hr@1.0.0.NextOfKin", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } } }, @@ -2928,16 +2914,14 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "schema": { - "title": "NextOfKin", - "description": "An instance of org.acme.hr@1.0.0.NextOfKin", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "KinTelephone" - } + "title": "NextOfKin", + "description": "An instance of org.acme.hr@1.0.0.NextOfKin", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } } }, @@ -3039,16 +3023,14 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "schema": { - "title": "NextOfKin", - "description": "An instance of org.acme.hr@1.0.0.NextOfKin", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "KinTelephone" - } + "title": "NextOfKin", + "description": "An instance of org.acme.hr@1.0.0.NextOfKin", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } } }, @@ -4424,71 +4406,61 @@ exports[`codegen #formats check we can convert all formats from namespace versio "$ref": "#/components/schemas/org.acme.hr.base@1.0.0.Address" }, "companyProperties": { - "schema": { - "title": "CompanyProperties", - "description": "An instance of org.acme.hr@1.0.0.CompanyProperties", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } + "title": "CompanyProperties", + "description": "An instance of org.acme.hr@1.0.0.CompanyProperties", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } }, "employeeDirectory": { - "schema": { - "title": "EmployeeDirectory", - "description": "An instance of org.acme.hr@1.0.0.EmployeeDirectory", - "type": "object", - "propertyNames": { - "type": "string", - "pattern": "/(\\\\d{3}-\\\\d{2}-\\\\d{4})+/" - }, - "additionalProperties": { - "$ref": "#/definitions/org.acme.hr@1.0.0.Employee" - } + "title": "EmployeeDirectory", + "description": "An instance of org.acme.hr@1.0.0.EmployeeDirectory", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "(\\\\d{3}-\\\\d{2}-\\\\d{4})+" + }, + "additionalProperties": { + "$ref": "#/components/schemas/org.acme.hr@1.0.0.Employee" } }, "employeeTShirtSizes": { - "schema": { - "title": "EmployeeTShirtSizes", - "description": "An instance of org.acme.hr.base@1.0.0.EmployeeTShirtSizes", - "type": "object", - "propertyNames": { - "type": "string", - "pattern": "/(\\\\d{3}-\\\\d{2}-\\\\d{4})+/" - }, - "additionalProperties": { - "$ref": "#/definitions/org.acme.hr@1.0.0.TShirtSizeType" - } + "title": "EmployeeTShirtSizes", + "description": "An instance of org.acme.hr.base@1.0.0.EmployeeTShirtSizes", + "type": "object", + "propertyNames": { + "type": "string", + "pattern": "(\\\\d{3}-\\\\d{2}-\\\\d{4})+" + }, + "additionalProperties": { + "$ref": "#/components/schemas/org.acme.hr.base@1.0.0.TShirtSizeType" } }, "employeeProfiles": { - "schema": { - "title": "EmployeeProfiles", - "description": "An instance of org.acme.hr@1.0.0.EmployeeProfiles", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/definitions/org.acme.hr@1.0.0.Concept" - } + "title": "EmployeeProfiles", + "description": "An instance of org.acme.hr@1.0.0.EmployeeProfiles", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/concerto@1.0.0.Concept" } }, "employeeSocialSecurityNumbers": { - "schema": { - "title": "EmployeeSocialSecurityNumbers", - "description": "An instance of org.acme.hr@1.0.0.EmployeeSocialSecurityNumbers", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "SSN", - "pattern": "/(\\\\d{3}-\\\\d{2}-\\\\d{4})+/" - } + "title": "EmployeeSocialSecurityNumbers", + "description": "An instance of org.acme.hr@1.0.0.EmployeeSocialSecurityNumbers", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string", + "pattern": "(\\\\d{3}-\\\\d{2}-\\\\d{4})+" } } }, @@ -4630,16 +4602,14 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "schema": { - "title": "NextOfKin", - "description": "An instance of org.acme.hr@1.0.0.NextOfKin", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "KinTelephone" - } + "title": "NextOfKin", + "description": "An instance of org.acme.hr@1.0.0.NextOfKin", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } } }, @@ -4736,16 +4706,14 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "schema": { - "title": "NextOfKin", - "description": "An instance of org.acme.hr@1.0.0.NextOfKin", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "KinTelephone" - } + "title": "NextOfKin", + "description": "An instance of org.acme.hr@1.0.0.NextOfKin", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } } }, @@ -4834,16 +4802,14 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "schema": { - "title": "NextOfKin", - "description": "An instance of org.acme.hr@1.0.0.NextOfKin", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "KinTelephone" - } + "title": "NextOfKin", + "description": "An instance of org.acme.hr@1.0.0.NextOfKin", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } } }, @@ -4945,16 +4911,14 @@ exports[`codegen #formats check we can convert all formats from namespace versio "type": "string" }, "nextOfKin": { - "schema": { - "title": "NextOfKin", - "description": "An instance of org.acme.hr@1.0.0.NextOfKin", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "KinTelephone" - } + "title": "NextOfKin", + "description": "An instance of org.acme.hr@1.0.0.NextOfKin", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" } } }, diff --git a/test/codegen/fromcto/jsonschema/jsonschemavisitor.js b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js index 5ff763f..cd392a6 100644 --- a/test/codegen/fromcto/jsonschema/jsonschemavisitor.js +++ b/test/codegen/fromcto/jsonschema/jsonschemavisitor.js @@ -421,11 +421,11 @@ describe('JSONSchema (samples)', function () { modelManager.addCTOModel( MODEL_MAP_STRING ); const visitor = new JSONSchemaVisitor(); const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Foo'}); - expect(schema.properties.dictionary.schema.title).equal('Dictionary'); - expect(schema.properties.dictionary.schema.description).equal('An instance of test@1.0.0.Dictionary'); - expect(schema.properties.dictionary.schema.type).equal('object'); - expect(schema.properties.dictionary.schema.propertyNames.type).equal('string'); - expect(schema.properties.dictionary.schema.additionalProperties.type).equal('string'); + expect(schema.properties.dictionary.title).equal('Dictionary'); + expect(schema.properties.dictionary.description).equal('An instance of test@1.0.0.Dictionary'); + expect(schema.properties.dictionary.type).equal('object'); + expect(schema.properties.dictionary.propertyNames.type).equal('string'); + expect(schema.properties.dictionary.additionalProperties.type).equal('string'); }); @@ -434,11 +434,11 @@ describe('JSONSchema (samples)', function () { modelManager.addCTOModel( MODEL_MAP_COMPLEX ); const visitor = new JSONSchemaVisitor(); const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Foo'}); - expect(schema.properties.dictionary.schema.title).equal('Dictionary'); - expect(schema.properties.dictionary.schema.description).equal('An instance of test@1.0.0.Dictionary'); - expect(schema.properties.dictionary.schema.type).equal('object'); - expect(schema.properties.dictionary.schema.propertyNames.type).equal('string'); - expect(schema.properties.dictionary.schema.additionalProperties.$ref).equal('#/definitions/test@1.0.0.Person'); + expect(schema.properties.dictionary.title).equal('Dictionary'); + expect(schema.properties.dictionary.description).equal('An instance of test@1.0.0.Dictionary'); + expect(schema.properties.dictionary.type).equal('object'); + expect(schema.properties.dictionary.propertyNames.type).equal('string'); + expect(schema.properties.dictionary.additionalProperties.$ref).equal('#/definitions/test@1.0.0.Person'); }); it('should generate for Map of type ', () => { @@ -446,11 +446,11 @@ describe('JSONSchema (samples)', function () { modelManager.addCTOModel( MODEL_MAP_NUMBER ); const visitor = new JSONSchemaVisitor(); const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Foo'}); - expect(schema.properties.dictionary.schema.title).equal('Dictionary'); - expect(schema.properties.dictionary.schema.description).equal('An instance of test@1.0.0.Dictionary'); - expect(schema.properties.dictionary.schema.type).equal('object'); - expect(schema.properties.dictionary.schema.propertyNames.type).equal('string'); - expect(schema.properties.dictionary.schema.additionalProperties.type).equal('number'); + expect(schema.properties.dictionary.title).equal('Dictionary'); + expect(schema.properties.dictionary.description).equal('An instance of test@1.0.0.Dictionary'); + expect(schema.properties.dictionary.type).equal('object'); + expect(schema.properties.dictionary.propertyNames.type).equal('string'); + expect(schema.properties.dictionary.additionalProperties.type).equal('number'); }); @@ -459,11 +459,111 @@ describe('JSONSchema (samples)', function () { modelManager.addCTOModel( MODEL_MAP_BOOLEAN ); const visitor = new JSONSchemaVisitor(); const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Foo'}); - expect(schema.properties.dictionary.schema.title).equal('Dictionary'); - expect(schema.properties.dictionary.schema.description).equal('An instance of test@1.0.0.Dictionary'); - expect(schema.properties.dictionary.schema.type).equal('object'); - expect(schema.properties.dictionary.schema.propertyNames.type).equal('string'); - expect(schema.properties.dictionary.schema.additionalProperties.type).equal('boolean'); + expect(schema.properties.dictionary.title).equal('Dictionary'); + expect(schema.properties.dictionary.description).equal('An instance of test@1.0.0.Dictionary'); + expect(schema.properties.dictionary.type).equal('object'); + expect(schema.properties.dictionary.propertyNames.type).equal('string'); + expect(schema.properties.dictionary.additionalProperties.type).equal('boolean'); + }); + + it('should validate map values, and compile with strict Ajv', () => { + const modelManager = new ModelManager(); + modelManager.addCTOModel( MODEL_MAP_STRING ); + const visitor = new JSONSchemaVisitor(); + const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Foo'}); + + // The map schema is the property schema itself, not wrapped in a `schema` key, + // which is not a JSON Schema keyword and was ignored by validators. + expect(schema.properties.dictionary).to.not.have.property('schema'); + + const validate = new Ajv({ strict: true }).compile(schema); + const foo = dictionary => ({ $class: 'test@1.0.0.Foo', dictionary }); + validate(foo({ colour: 'red' })).should.equal(true); + validate(foo({ colour: 42 })).should.equal(false); + validate(foo({ size: { nested: true } })).should.equal(false); + validate(foo('not a map')).should.equal(false); + }); + + it('should apply the base type and validators of scalar map keys and values', () => { + const modelManager = new ModelManager(); + modelManager.addCTOModel( UUID_SCALAR ); + modelManager.addCTOModel(` +namespace test@1.0.0 + +import concerto.scalar@1.0.0.{ UUID } + +scalar SSN extends String regex=/\\d{3}-\\d{2}-\\d{4}/ +scalar Time extends DateTime + +map Owners { + o SSN + o UUID +} + +map LoginTimes { + o String + o Time +} + +concept Foo { + o Owners owners + o LoginTimes loginTimes +} +`); + const visitor = new JSONSchemaVisitor(); + const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Foo'}); + + expect(schema.properties.owners.propertyNames).to.deep.equal({ type: 'string', pattern: '\\d{3}-\\d{2}-\\d{4}' }); + expect(schema.properties.owners.additionalProperties).to.deep.equal({ type: 'string', format: 'uuid' }); + expect(schema.properties.loginTimes.additionalProperties).to.deep.equal({ format: 'date-time', type: 'string' }); + + const ajv = new Ajv({ strict: true }); + ajv.addFormat('uuid', /^[0-9a-f-]{36}$/i); + ajv.addFormat('date-time', true); + const validate = ajv.compile(schema); + const foo = owners => ({ $class: 'test@1.0.0.Foo', owners, loginTimes: {} }); + validate(foo({ '123-45-6789': '9fd1c6a4-6b0e-4f3a-9c8e-1d2b3c4d5e6f' })).should.equal(true); + validate(foo({ 'not-an-ssn': '9fd1c6a4-6b0e-4f3a-9c8e-1d2b3c4d5e6f' })).should.equal(false); + validate(foo({ '123-45-6789': 'not-a-uuid' })).should.equal(false); + }); + + it('should pass an unrecognised type name through as the schema type', () => { + // Every caller passes a primitive type name; this is the fallback for anything else. + const visitor = new JSONSchemaVisitor(); + visitor.getFieldOrScalarDeclarationValidatorsForSchema('Unknown').should.deep.equal({ type: 'Unknown' }); + }); + + it('should reference map values declared in another namespace, under refRoot', () => { + const modelManager = new ModelManager(); + modelManager.addCTOModel(` +namespace other@1.0.0 + +concept Person { + o String name +} + +map People { + o String + o Person +} +`, 'other.cto'); + modelManager.addCTOModel(` +namespace test@1.0.0 + +import other@1.0.0.{ People } + +concept Foo { + o People people +} +`, 'test.cto'); + const visitor = new JSONSchemaVisitor(); + + const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Foo'}); + expect(schema.properties.people.additionalProperties.$ref).equal('#/definitions/other@1.0.0.Person'); + new Ajv({ strict: true }).compile(schema); + + const openApiStyle = modelManager.accept(visitor, { rootType: 'test@1.0.0.Foo', refRoot: '#/components/schemas' }); + expect(openApiStyle.properties.people.additionalProperties.$ref).equal('#/components/schemas/other@1.0.0.Person'); }); it('should use min max length for string fields when length validator is configured', () => { @@ -660,15 +760,15 @@ concept Test { const visitor = new JSONSchemaVisitor(); const schema = modelManager.accept(visitor, { rootType: 'test@1.0.0.Test' }); - expect(schema.properties.both.schema.minProperties).equal(1); - expect(schema.properties.both.schema.maxProperties).equal(20); - expect(schema.properties.both.schema.type).equal('object'); + expect(schema.properties.both.minProperties).equal(1); + expect(schema.properties.both.maxProperties).equal(20); + expect(schema.properties.both.type).equal('object'); - expect(schema.properties.minOnly.schema.minProperties).equal(3); - expect(schema.properties.minOnly.schema).to.not.have.property('maxProperties'); + expect(schema.properties.minOnly.minProperties).equal(3); + expect(schema.properties.minOnly).to.not.have.property('maxProperties'); - expect(schema.properties.maxOnly.schema).to.not.have.property('minProperties'); - expect(schema.properties.maxOnly.schema.maxProperties).equal(10); + expect(schema.properties.maxOnly).to.not.have.property('minProperties'); + expect(schema.properties.maxOnly.maxProperties).equal(10); }); it('should generate minItems/maxItems for array fields with size validators', () => { diff --git a/test/verification/cases.js b/test/verification/cases.js index b253ac4..bec47e8 100644 --- a/test/verification/cases.js +++ b/test/verification/cases.js @@ -63,7 +63,7 @@ const CASES = [ graphql: 'map types emit invalid GraphQL SDL (key/value field syntax)', rust: 'map declarations reference scalar key/value types (e.g. SSN, Time) that the Rust visitor does not emit, so cargo check fails', avro: 'versioned namespaces (e.g. concerto.decorator@1.0.0) are illegal Avro identifiers (segment "0")', - openapi: 'JSON Schema $decorators and invalid schema nesting fail OpenAPI struct validation', + openapi: 'JSON Schema $decorators and map propertyNames fail OpenAPI struct validation, and some $refs do not resolve', golang: 'GoLangVisitor emits bare package-name imports (e.g. import "concerto_decorator_1_0_0") instead of Go module-relative import paths, so any cross-namespace model fails go build', }, }, @@ -83,7 +83,7 @@ const CASES = [ golang: 'GoLangVisitor emits bare package-name imports (e.g. import "concerto_decorator_1_0_0") instead of Go module-relative import paths, so any cross-namespace model fails go build', protobuf: 'Relationships are not supported in protobuf', graphql: 'Relationships are not supported in graphql visitor', - openapi: 'Map schema nesting (schema property) fails OpenAPI struct validation', + openapi: 'OpenAPI 3.0 schema objects do not allow propertyNames, which map key schemas use', }, }, {