Skip to content

Commit 95fccd4

Browse files
fix: generation fails when schemas have an $id field or array of objects (#287)
* fix: is now being handled by changing the value to the classname where appropriate as part of the preprocess hook; items arrays are also working with the changes; updated tests * chore: renamed new custom attribute to be more generic; fixed linting issues; const javaPackage instead of let
1 parent db808d4 commit 95fccd4

8 files changed

+2070
-282
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
node_modules
22
test/temp
3-
coverage
3+
coverage
4+
**/.DS_Store
5+
**/.vscode

hooks/pre-process.js

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,48 @@
11
const ApplicationModel = require('../lib/applicationModel.js');
2+
const _ = require('lodash');
23

3-
module.exports = {
4-
'generate:before': generator => {
5-
generator.asyncapi.allSchemas().forEach((schema, schemaName) => {
6-
// The generator will create file names based on the schema's $id. Instead of guessing what the generator named the file so we can fix it in post,
7-
// ... it's easier to process $id here first. Since we don't use it, removing it is easiest.
8-
if (schema.$id()) {
9-
delete schema._json.$id;
4+
function setSchemaIdsForFileName(asyncapi) {
5+
asyncapi.allSchemas().forEach((schema, schemaName) => {
6+
// If we leave the $id the way it is, the generator will name the schema files what their $id is, which is always a bad idea.
7+
// So we leave it in, but $id is going to be changed to be the class name we want.
8+
// If we remove the $id and there's no x-parser-schema-id, then it wont be returned by allSchemas().
9+
if (schema.$id()) {
10+
// Assuming one of x-parser-schema-id and $id must be present.
11+
let classNameForGenerator;
12+
const parserSchemaId = schema.ext('x-parser-schema-id');
13+
classNameForGenerator = parserSchemaId ? parserSchemaId : _.camelCase(schema.$id().substring(schema.$id().lastIndexOf('/') + 1));
14+
15+
if (classNameForGenerator === 'items') {
16+
const parentSchema = schema.options?.parent;
17+
const parentSchemaItems = parentSchema?.items();
18+
if (parentSchemaItems?._json?.$id === schema.$id()) {
19+
const parentParserSchemaId = parentSchema.ext('x-parser-schema-id');
20+
classNameForGenerator = parentParserSchemaId ? parentParserSchemaId : _.camelCase(parentSchema.$id().substring(parentSchema.$id().lastIndexOf('/') + 1));
21+
// If we come across this schema later in the code generator, we'll know to rename it to its parent because the proper settings will be set in the model class.
22+
schema._json['x-model-class-name'] = classNameForGenerator;
23+
classNameForGenerator += 'Items';
24+
}
1025
}
11-
});
26+
schema._json.$id = classNameForGenerator;
27+
}
28+
});
29+
}
30+
31+
function setSchemaIdsForFileNameIncludingDuplicates(asyncapi) {
32+
// We do this multiple times because allSchemas() returns a list of deduplicated schemas, so if we change the $id of a schema,
33+
// we wont change any of the duplicates. We continue until there are no more duplicates to change.
34+
let numSchemas;
35+
let newNumSchemas;
36+
do {
37+
numSchemas = asyncapi.allSchemas().size;
38+
setSchemaIdsForFileName(asyncapi);
39+
newNumSchemas = asyncapi.allSchemas().size;
40+
} while (numSchemas !== newNumSchemas);
41+
}
1242

43+
module.exports = {
44+
'generate:before': generator => {
45+
setSchemaIdsForFileNameIncludingDuplicates(generator.asyncapi);
1346
ApplicationModel.asyncapi = generator.asyncapi;
1447
}
1548
};

lib/applicationModel.js

+38-67
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class ApplicationModel {
3030
}
3131
// Using x-parser-schema-id didn't work for us, fall back to trying to get at least something using the provided name.
3232
if (!modelClass) {
33-
modelClass = this.modelClassMap[schemaName];
33+
modelClass = this.modelClassMap[schemaName] || this.modelClassMap[_.camelCase(schemaName)];
3434
}
3535
debugApplicationModel(`returning modelClass for caller ${this.caller} ${schemaName}`);
3636
debugApplicationModel(modelClass);
@@ -42,25 +42,26 @@ class ApplicationModel {
4242
}
4343

4444
setupSuperClassMap() {
45-
if (!this.superClassMap) {
46-
this.superClassMap = new Map();
47-
this.anonymousSchemaToSubClassMap = new Map();
48-
debugApplicationModel('-------- SCHEMAS -------------');
49-
debugApplicationModel(ApplicationModel.asyncapi.allSchemas());
50-
ApplicationModel.asyncapi.allSchemas().forEach((schema, schemaName) => {
51-
debugApplicationModel(`${schemaName}:`);
52-
debugApplicationModel(schema);
53-
const allOf = schema.allOf();
54-
if (allOf) {
55-
this.handleAllOfSchema(schema, schemaName, allOf);
56-
}
57-
});
58-
debugApplicationModel('-----------------------------');
59-
debugApplicationModel('superclassMap:');
60-
debugApplicationModel(this.superClassMap);
61-
debugApplicationModel('anonymousSchemaToSubClassMap:');
62-
debugApplicationModel(this.anonymousSchemaToSubClassMap);
45+
if (this.superClassMap) {
46+
return;
6347
}
48+
this.superClassMap = new Map();
49+
this.anonymousSchemaToSubClassMap = new Map();
50+
debugApplicationModel('-------- SCHEMAS -------------');
51+
debugApplicationModel(ApplicationModel.asyncapi.allSchemas());
52+
ApplicationModel.asyncapi.allSchemas().forEach((schema, schemaName) => {
53+
debugApplicationModel(`${schemaName}:`);
54+
debugApplicationModel(schema);
55+
const allOf = schema.allOf();
56+
if (allOf) {
57+
this.handleAllOfSchema(schema, schemaName, allOf);
58+
}
59+
});
60+
debugApplicationModel('-----------------------------');
61+
debugApplicationModel('superclassMap:');
62+
debugApplicationModel(this.superClassMap);
63+
debugApplicationModel('anonymousSchemaToSubClassMap:');
64+
debugApplicationModel(this.anonymousSchemaToSubClassMap);
6465
}
6566

6667
handleAllOfSchema(schema, schemaName, allOfSchema) {
@@ -89,54 +90,19 @@ class ApplicationModel {
8990
}
9091

9192
setupModelClassMap() {
92-
if (!this.modelClassMap) {
93-
this.modelClassMap = new Map();
94-
this.nameToSchemaMap = new Map();
95-
// Register all schemas first, then check the anonymous schemas for duplicates
96-
ApplicationModel.asyncapi.allSchemas().forEach((schema, name) => {
97-
debugApplicationModel(`setupModelClassMap ${name} type ${schema.type()}`);
98-
this.registerSchemaNameToModelClass(schema, name);
99-
this.nameToSchemaMap[name] = schema;
100-
});
101-
102-
ApplicationModel.asyncapi.allSchemas().forEach((schema, schemaName) => {
103-
debugApplicationModel(`setupModelClassMap anonymous schemas ${schemaName} type ${schema.type()}`);
104-
this.registerSchemasInProperties(schema);
105-
this.registerSchemasInAllOf(schema);
106-
});
107-
debugApplicationModel('modelClassMap:');
108-
debugApplicationModel(this.modelClassMap);
109-
}
110-
}
111-
112-
registerSchemasInProperties(schema) {
113-
if (!!Object.keys(schema.properties()).length) {
114-
// Each property name is the name of a schema. It should also have an x-parser-schema-id name. We'll be adding duplicate mappings (two mappings to the same model class) since the anon schemas do have names
115-
Object.keys(schema.properties()).forEach(property => {
116-
const innerSchema = schema.properties()[property];
117-
const innerSchemaParserId = innerSchema.ext('x-parser-schema-id');
118-
const existingModelClass = this.modelClassMap[innerSchemaParserId];
119-
if (existingModelClass) {
120-
this.modelClassMap[property] = existingModelClass;
121-
} else {
122-
this.registerSchemaNameToModelClass(innerSchema, property);
123-
}
124-
});
125-
}
126-
}
127-
128-
registerSchemasInAllOf(schema) {
129-
const allOf = schema.allOf();
130-
debugApplicationModel('allOf:');
131-
debugApplicationModel(allOf);
132-
if (allOf) {
133-
allOf.forEach(innerSchema => {
134-
const name = innerSchema.ext('x-parser-schema-id');
135-
if (this.isAnonymousSchema(name) && innerSchema.type() === 'object') {
136-
this.registerSchemaNameToModelClass(innerSchema, name);
137-
}
138-
});
93+
if (this.modelClassMap) {
94+
return;
13995
}
96+
this.modelClassMap = new Map();
97+
this.nameToSchemaMap = new Map();
98+
// Register all schemas recursively as a flat map of name -> ModelClass
99+
ApplicationModel.asyncapi.allSchemas().forEach((schema, name) => {
100+
debugApplicationModel(`setupModelClassMap ${name} type ${schema.type()}`);
101+
this.registerSchemaNameToModelClass(schema, name);
102+
this.nameToSchemaMap[name] = schema;
103+
});
104+
debugApplicationModel('modelClassMap:');
105+
debugApplicationModel(this.modelClassMap);
140106
}
141107

142108
isAnonymousSchema(schemaName) {
@@ -158,7 +124,12 @@ class ApplicationModel {
158124
modelClass.setCanBeInnerClass(false);
159125
}
160126

161-
const { className, javaPackage } = scsLib.stripPackageName(schemaName);
127+
const classNameAndLocation = scsLib.stripPackageName(schemaName);
128+
let className = classNameAndLocation.className;
129+
const javaPackage = classNameAndLocation.javaPackage;
130+
if (schema._json['x-model-class-name']) {
131+
className = schema._json['x-model-class-name'];
132+
}
162133
modelClass.setJavaPackage(javaPackage);
163134
modelClass.setClassName(className);
164135
debugApplicationModel(`schemaName ${schemaName} className: ${modelClass.getClassName()} super: ${modelClass.getSuperClassName()} javaPackage: ${javaPackage}`);

0 commit comments

Comments
 (0)