Skip to content

Commit 582fced

Browse files
fix: unable to get model class when using arrays in schema (#207)
* fix: resolve NPE when attempting to use a null modelClass by trying to use the x-parser-schema-id property of the source schemafirst, then try the property name. * fix: fixed issues with keyword schema naming and writing inner classes that should have been their own class; the schema name is going to be correct more often from using x-schema-parser-id instead when appropriate; broke all args constructor for schemas that are arrays with this commit. * fix: created partial for all args constructor; all args constructor now correctly created for array types * chore: remove code smells; stripPackage function is more cohesive; removed need for a tentative class name. * chore: remove useless modelClass assignment in anonSchema allOf handling * chore: clean up todo; use ternary instead of if statement * update snapshot; unnecessary spacing removed due to removal of comment * chore: fix linting problems
1 parent 0313e3f commit 582fced

11 files changed

+1514
-139
lines changed

filters/all.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -423,12 +423,16 @@ const getMethods = (obj) => {
423423
return [...properties.keys()].filter(item => typeof obj[item] === 'function');
424424
};
425425

426-
function getModelClass(schemaName) {
427-
return applicationModel.getModelClass(schemaName);
426+
function getModelClass(customSchemaObject) {
427+
return applicationModel.getModelClass(customSchemaObject);
428428
}
429-
430429
filter.getModelClass = getModelClass;
431430

431+
function getAnonymousSchemaForRef(schemaName) {
432+
return applicationModel.getAnonymousSchemaForRef(schemaName);
433+
}
434+
filter.getAnonymousSchemaForRef = getAnonymousSchemaForRef;
435+
432436
function getRealPublisher([info, params, channel]) {
433437
return scsLib.getRealPublisher(info, params, channel);
434438
}

hooks/post-process.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
const fs = require('fs');
33
const path = require('path');
44
const ApplicationModel = require('../lib/applicationModel.js');
5+
const _ = require('lodash');
56
const applicationModel = new ApplicationModel('post');
67
// To enable debug logging, set the env var DEBUG="postProcess" with whatever things you want to see.
78
const debugPostProcess = require('debug')('postProcess');
@@ -74,12 +75,14 @@ function processSchema(generator, schemaName, schema, sourcePath, defaultJavaPac
7475
const filePath = path.resolve(sourcePath, fileName);
7576
debugPostProcess(`processSchema ${schemaName}`);
7677
debugPostProcess(schema);
77-
if (schema.type() !== 'object') {
78+
const modelClass = applicationModel.getModelClass({schema, schemaName});
79+
const javaName = modelClass.getClassName();
80+
if ((schema.type() && schema.type() !== 'object') || _.startsWith(javaName, 'Anonymous')) {
7881
debugPostProcess(`deleting ${filePath}`);
79-
fs.unlinkSync(filePath);
82+
if (fs.existsSync(filePath)) {
83+
fs.unlinkSync(filePath);
84+
}
8085
} else {
81-
const modelClass = applicationModel.getModelClass(schemaName);
82-
const javaName = modelClass.getClassName();
8386
const packageDir = getPackageDir(generator, defaultJavaPackageDir, modelClass);
8487
debugPostProcess(`packageDir: ${packageDir}`);
8588

lib/applicationModel.js

+95-25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const ModelClass = require('./modelClass.js');
22
const debugApplicationModel = require('debug')('applicationModel');
3+
const _ = require('lodash');
34
const instanceMap = new Map();
45

56
class ApplicationModel {
@@ -10,12 +11,26 @@ class ApplicationModel {
1011
debugApplicationModel(instanceMap);
1112
}
1213

13-
getModelClass(schemaName) {
14+
getModelClass({schema, schemaName}) {
1415
debugApplicationModel(`getModelClass for caller ${this.caller} schema ${schemaName}`);
1516
this.setupSuperClassMap();
1617
this.setupModelClassMap();
17-
const modelClass = this.modelClassMap[schemaName];
18-
debugApplicationModel(`returning modelClass for caller ${this.caller} ${schemaName}`);
18+
let modelClass;
19+
if (schema) {
20+
const parserSchemaName = schema.ext('x-parser-schema-id');
21+
// Try to use x-parser-schema-id as key
22+
modelClass = this.modelClassMap[parserSchemaName];
23+
if (modelClass && _.startsWith(modelClass.getClassName(), 'Anonymous')) {
24+
// If we translated this schema from the map using an anonymous schema key, we have no idea what the name should be, so we use the one provided directly from the source - not the generator.
25+
// If we translated this schema from the map using a known schema (the name of the schema was picked out correctly by the generator), use that name.
26+
modelClass.setClassName(_.upperFirst(this.isAnonymousSchema(parserSchemaName) ? schemaName : parserSchemaName));
27+
}
28+
}
29+
// Using x-parser-schema-id didn't work for us, fall back to trying to get at least something using the provided name.
30+
if (!modelClass) {
31+
modelClass = this.modelClassMap[schemaName];
32+
}
33+
debugApplicationModel(`returning modelClass for caller ${this.caller} ${schemaName}`);
1934
debugApplicationModel(modelClass);
2035
return modelClass;
2136
}
@@ -66,68 +81,123 @@ class ApplicationModel {
6681
} else {
6782
this.superClassMap[anonymousSchema] = namedSchema;
6883
this.anonymousSchemaToSubClassMap[anonymousSchema] = schemaName;
84+
this.superClassMap[schemaName] = namedSchema;
85+
this.anonymousSchemaToSubClassMap[schemaName] = anonymousSchema;
6986
}
7087
}
7188

7289
setupModelClassMap() {
7390
if (!this.modelClassMap) {
7491
this.modelClassMap = new Map();
92+
this.nameToSchemaMap = new Map();
93+
// Register all schemas first, then check the anonymous schemas for duplicates
94+
ApplicationModel.asyncapi.allSchemas().forEach((schema, name) => {
95+
debugApplicationModel(`setupModelClassMap ${name} type ${schema.type()}`);
96+
this.registerSchemaNameToModelClass(schema, name);
97+
this.nameToSchemaMap[name] = schema;
98+
});
99+
75100
ApplicationModel.asyncapi.allSchemas().forEach((schema, schemaName) => {
76-
debugApplicationModel(`setupModelClassMap ${schemaName} type ${schema.type()}`);
101+
debugApplicationModel(`setupModelClassMap anonymous schemas ${schemaName} type ${schema.type()}`);
102+
this.checkProperties(schema);
103+
77104
const allOf = schema.allOf();
78105
debugApplicationModel('allOf:');
79106
debugApplicationModel(allOf);
80107
if (allOf) {
81108
allOf.forEach(innerSchema => {
82-
const name = innerSchema._json['x-parser-schema-id'];
109+
const name = innerSchema.ext('x-parser-schema-id');
83110
if (this.isAnonymousSchema(name) && innerSchema.type() === 'object') {
84-
this.addSchemaToMap(innerSchema, schemaName);
111+
this.registerSchemaNameToModelClass(innerSchema, schemaName);
85112
}
86113
});
87-
} else {
88-
this.addSchemaToMap(schema, schemaName);
89114
}
90115
});
91116
debugApplicationModel('modelClassMap:');
92117
debugApplicationModel(this.modelClassMap);
93118
}
94119
}
95120

121+
checkProperties(schema) {
122+
if (!!Object.keys(schema.properties()).length) {
123+
// 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
124+
Object.keys(schema.properties()).forEach(property => {
125+
const innerSchema = schema.properties()[property];
126+
const innerSchemaParserId = innerSchema.ext('x-parser-schema-id');
127+
const existingModelClass = this.modelClassMap[innerSchemaParserId];
128+
if (existingModelClass) {
129+
this.modelClassMap[property] = existingModelClass;
130+
} else {
131+
this.registerSchemaNameToModelClass(innerSchema, property);
132+
}
133+
});
134+
}
135+
}
136+
96137
isAnonymousSchema(schemaName) {
97138
return schemaName.startsWith('<');
98139
}
99140

100-
addSchemaToMap(schema, schemaName) {
101-
const modelClass = new ModelClass();
102-
let tentativeClassName = schemaName;
141+
registerSchemaNameToModelClass(schema, schemaName) {
142+
let modelClass = this.modelClassMap[schemaName];
143+
if (!modelClass) {
144+
modelClass = new ModelClass();
145+
}
146+
103147
if (this.isAnonymousSchema(schemaName)) {
104-
// It's an anonymous schema. It might be a subclass...
105-
const subclassName = this.anonymousSchemaToSubClassMap[schemaName];
106-
if (subclassName) {
107-
tentativeClassName = subclassName;
108-
modelClass.setSuperClassName(this.superClassMap[schemaName]);
109-
}
148+
this.handleAnonymousSchemaForAllOf(modelClass, schemaName);
110149
}
111-
// If there is a dot in the schema name, it's probably an Avro schema with a fully qualified name (including the namespace.)
112-
const indexOfDot = schemaName.lastIndexOf('.');
113-
let javaPackage;
114-
if (indexOfDot > 0) {
115-
javaPackage = schemaName.substring(0, indexOfDot);
116-
tentativeClassName = schemaName.substring(indexOfDot + 1);
117-
modelClass.setJavaPackage(javaPackage);
150+
const components = ApplicationModel.asyncapi._json.components;
151+
const nonInnerClassSchemas = Object.keys(components? components.schemas || {} : {});
152+
if (nonInnerClassSchemas.includes(schemaName)) {
153+
modelClass.setCanBeInnerClass(false);
118154
}
119-
modelClass.setClassName(tentativeClassName);
155+
156+
const { className, javaPackage } = this.stripPackageName(schemaName);
157+
modelClass.setJavaPackage(javaPackage);
158+
modelClass.setClassName(className);
120159
debugApplicationModel(`schemaName ${schemaName} className: ${modelClass.getClassName()} super: ${modelClass.getSuperClassName()} javaPackage: ${javaPackage}`);
121160
this.modelClassMap[schemaName] = modelClass;
122161
debugApplicationModel(`Added ${schemaName}`);
123162
debugApplicationModel(modelClass);
124163
}
125164

165+
getAnonymousSchemaForRef(realSchemaName) {
166+
// During our allOf parsing, we found this real schema to anon-schema association
167+
const anonSchema = this.anonymousSchemaToSubClassMap[realSchemaName];
168+
return anonSchema ? this.nameToSchemaMap[anonSchema] : undefined;
169+
}
170+
171+
handleAnonymousSchemaForAllOf(modelClass, schemaName) {
172+
const subclassName = this.anonymousSchemaToSubClassMap[schemaName];
173+
if (subclassName) {
174+
modelClass.setSuperClassName(this.superClassMap[schemaName]);
175+
// Be sure the anonymous modelClass and the named modelClass are updated with the superclass information
176+
// We dont want the anonymous schema because the class name won't be correct if it's a $ref, so if the modelClass exists, update that one, if it doesn't we'll make it
177+
const existingModelClass = this.modelClassMap[subclassName];
178+
if (existingModelClass) {
179+
existingModelClass.setSuperClassName(this.superClassMap[schemaName]);
180+
}
181+
return subclassName;
182+
}
183+
return schemaName;
184+
}
185+
186+
stripPackageName(schemaName) {
187+
// If there is a dot in the schema name, it's probably an Avro schema with a fully qualified name (including the namespace.)
188+
const indexOfDot = schemaName.lastIndexOf('.');
189+
if (indexOfDot > 0) {
190+
return { className: schemaName.substring(indexOfDot + 1), javaPackage: schemaName.substring(0, indexOfDot) };
191+
}
192+
return { className: schemaName, javaPackage: undefined };
193+
}
194+
126195
reset() {
127196
instanceMap.forEach((val) => {
128197
val.superClassMap = null;
129198
val.anonymousSchemaToSubClassMap = null;
130199
val.modelClassMap = null;
200+
val.nameToSchemaMap = null;
131201
});
132202
}
133203
}

lib/modelClass.js

+12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const _ = require('lodash');
22

33
class ModelClass {
4+
constructor() {
5+
this.innerClass = true;
6+
}
7+
48
getClassName() {
59
return this.className;
610
}
@@ -32,6 +36,14 @@ class ModelClass {
3236
fixClassName(originalName) {
3337
return _.upperFirst(_.camelCase(originalName));
3438
}
39+
40+
setCanBeInnerClass(innerClass) {
41+
this.innerClass = innerClass;
42+
}
43+
44+
canBeInnerClass() {
45+
return this.innerClass;
46+
}
3547
}
3648

3749
module.exports = ModelClass;

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"test": "jest --maxWorkers=50% --detectOpenHandles",
1212
"test:watch": "npm run test -- --watch",
1313
"test:watchAll": "npm run test -- --watchAll",
14-
"test:coverage": "npm run test -- --coverage"
14+
"test:coverage": "npm run test -- --coverage",
15+
"test:updateSnapshots": "npm run test -- -u"
1516
},
1617
"keywords": [
1718
"asyncapi",

partials/all-args-constructor

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{%- macro allArgsConstructor(className, properties, indentLevel) -%}
2+
{% set indent1 = indentLevel | indent1 -%}
3+
{% set indent2 = indentLevel | indent2 -%}
4+
{% set indent3 = indentLevel | indent3 -%}
5+
{% set first = true -%}
6+
{%- set hasNoProperties = properties | isEmpty -%}
7+
{%- if not hasNoProperties -%}
8+
{{ indent2 }}public {{ className }} (
9+
{%- for name, prop in properties -%}
10+
{%- set propModelClass = {schema: prop, schemaName: name} | getModelClass %}
11+
{%- set realClassName = propModelClass.getClassName() %}
12+
{%- set variableName = realClassName | identifierName %}
13+
{%- set typeInfo = [name, realClassName, prop] | fixType %}
14+
{%- set type = typeInfo[0] -%}
15+
{%- if first -%}
16+
{%- set first = false -%}
17+
{%- else -%}
18+
, {% endif %}
19+
{{ indent3 }}{{ type }} {{ variableName }}
20+
{%- endfor -%}
21+
) {
22+
{% for name, prop in properties -%}
23+
{%- set propModelClass = {schema: prop, schemaName: name} | getModelClass %}
24+
{%- set realClassName = propModelClass.getClassName() %}
25+
{%- set variableName = realClassName | identifierName -%}
26+
{{ indent3 }}this.{{ variableName }} = {{ variableName }};
27+
{% endfor -%}
28+
{{ indent2 }}}
29+
{%- endif -%}
30+
{% endmacro %}

0 commit comments

Comments
 (0)