Skip to content

Commit de53bed

Browse files
fix: a schema using allOf, with a reference to another schema, should create a subclass (#175)
* Added allOf subclass support. * Add a technical requirements section to the README to note the required generator version. * Fixed sonarcloud issues. * Improvement based on peer review. Co-authored-by: Michael Davis <[email protected]>
1 parent 4a221d6 commit de53bed

File tree

12 files changed

+484
-168
lines changed

12 files changed

+484
-168
lines changed

filters/all.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const generatorFilters = require('@asyncapi/generator-filters');
44
const _ = require('lodash');
55
const ScsLib = require('../lib/scsLib.js');
66
const scsLib = new ScsLib();
7+
const ApplicationModel = require('../lib/applicationModel.js');
8+
const applicationModel = new ApplicationModel('all');
79
// To enable debug logging, set the env var DEBUG="type function" with whatever things you want to see.
810
const debugDynamic = require('debug')('dynamic');
911
const debugFunction = require('debug')('function');
@@ -421,6 +423,12 @@ const getMethods = (obj) => {
421423
return [...properties.keys()].filter(item => typeof obj[item] === 'function');
422424
};
423425

426+
function getModelClass(schemaName) {
427+
return applicationModel.getModelClass(schemaName);
428+
}
429+
430+
filter.getModelClass = getModelClass;
431+
424432
function getRealPublisher([info, params, channel]) {
425433
const pub = scsLib.getRealPublisher(info, params, channel);
426434
return pub;

hooks/post-process.js

Lines changed: 101 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,25 @@
11
// vim: set ts=2 sw=2 sts=2 expandtab :
22
const fs = require('fs');
33
const path = require('path');
4-
const ScsLib = require('../lib/scsLib.js');
5-
const scsLib = new ScsLib();
4+
const ApplicationModel = require('../lib/applicationModel.js');
5+
const applicationModel = new ApplicationModel('post');
66
// To enable debug logging, set the env var DEBUG="postProcess" with whatever things you want to see.
77
const debugPostProcess = require('debug')('postProcess');
8-
98
const sourceHead = '/src/main/java/';
109

1110
module.exports = {
1211
'generate:after': generator => {
1312
const asyncapi = generator.asyncapi;
14-
let sourcePath = generator.targetDir + sourceHead;
15-
const info = asyncapi.info();
16-
let javaPackage = generator.templateParams['javaPackage'];
17-
const extensions = info.extensions();
18-
let overridePath;
19-
20-
if (!javaPackage && info && extensions) {
21-
javaPackage = extensions['x-java-package'];
22-
}
13+
const sourcePath = generator.targetDir + sourceHead;
2314

24-
if (javaPackage) {
25-
debugPostProcess(`package: ${javaPackage}`);
26-
overridePath = `${generator.targetDir + sourceHead + javaPackage.replace(/\./g, '/')}/`;
27-
}
28-
29-
asyncapi.allSchemas().forEach((value, key, map) => {
30-
processSchema(key, value);
31-
});
15+
// NEW
3216

33-
if (javaPackage) {
34-
debugPostProcess(`Moving files from ${sourcePath} to ${overridePath}`);
35-
let first = true;
36-
fs.readdirSync(sourcePath).forEach(file => {
37-
if (!fs.lstatSync(path.resolve(sourcePath, file)).isDirectory()) {
38-
if (first) {
39-
first = false;
40-
debugPostProcess(`Making ${overridePath}`);
41-
fs.mkdirSync(overridePath, { recursive: true });
42-
}
43-
44-
debugPostProcess(`Copying ${file}`);
45-
moveFile(sourcePath, overridePath, file);
46-
}
47-
});
48-
sourcePath = overridePath;
49-
}
17+
const defaultJavaPackage = getDefaultJavaPackage(generator);
18+
const defaultJavaPackageDir = getDefaultJavaPackageDir(generator, defaultJavaPackage);
19+
20+
asyncapi.allSchemas().forEach((schema, schemaName) => {
21+
processSchema(generator, schemaName, schema, sourcePath, defaultJavaPackageDir);
22+
});
5023

5124
// Rename the pom file if necessary, and only include Application.java when an app is requested.
5225
const artifactType = generator.templateParams['artifactType'];
@@ -58,67 +31,104 @@ module.exports = {
5831
} else {
5932
fs.renameSync(path.resolve(generator.targetDir, 'pom.app'), path.resolve(generator.targetDir, 'pom.xml'));
6033
fs.unlinkSync(path.resolve(generator.targetDir, 'pom.lib'));
34+
if (defaultJavaPackageDir) {
35+
moveFile(sourcePath, defaultJavaPackageDir, 'Application.java');
36+
}
6137
}
38+
applicationModel.reset(); // Must clear its cache for when we run the jest tests.
39+
}
40+
};
6241

63-
// This renames schema objects ensuring they're proper Java class names. It also removes files that are schemas of simple types.
42+
function getDefaultJavaPackage(generator) {
43+
const asyncapi = generator.asyncapi;
44+
const info = asyncapi.info();
45+
let javaPackage = generator.templateParams['javaPackage'];
46+
const extensions = info.extensions();
6447

65-
function processSchema(schemaName, schema) {
66-
if (schemaName.startsWith('<')) {
67-
debugPostProcess(`found an anonymous schema ${schemaName}`);
68-
schemaName = schemaName.replace('<', '');
69-
schemaName = schemaName.replace('>', '');
70-
}
48+
if (!javaPackage && info && extensions) {
49+
javaPackage = extensions['x-java-package'];
50+
}
7151

72-
// First see if we need to move it to a different package based on its namespace.
73-
// This mainly applies to Avro files which have the fully qualified name.
74-
let newSourceDir = sourcePath;
75-
const generatedFileName = `${schemaName}.java`;
76-
let desiredClassName = scsLib.getClassName(schemaName);
77-
78-
const indexOfDot = schemaName.lastIndexOf('.');
79-
if (indexOfDot > 0) {
80-
const newPackage = schemaName.substring(0, indexOfDot);
81-
const className = schemaName.substring(indexOfDot + 1);
82-
debugPostProcess(`package: ${newPackage} className: ${className}`);
83-
newSourceDir = `${generator.targetDir + sourceHead + newPackage.replace(/\./g, '/')}/`;
84-
moveFile(sourcePath, newSourceDir, generatedFileName);
85-
desiredClassName = scsLib.getClassName(className);
86-
}
52+
debugPostProcess(`getDefaultJavaPackage: ${javaPackage}`);
53+
return javaPackage;
54+
}
8755

88-
const oldPath = path.resolve(newSourceDir, generatedFileName);
89-
debugPostProcess(`old path: ${oldPath}`);
90-
91-
if (fs.existsSync(oldPath)) {
92-
const schemaType = schema.type();
93-
debugPostProcess(`Old path exists. schemaType: ${schemaType}`);
94-
if (schemaType === 'object' || schemaType === 'enum') {
95-
const javaName = scsLib.getClassName(schemaName);
96-
debugPostProcess(`desiredClassName: ${desiredClassName} schemaName: ${schemaName}`);
97-
98-
if (javaName !== schemaName) {
99-
const newPath = path.resolve(newSourceDir, `${desiredClassName}.java`);
100-
fs.renameSync(oldPath, newPath);
101-
debugPostProcess(`Renamed class file ${schemaName} to ${desiredClassName}`);
102-
}
103-
} else {
104-
// In this case it's an anonymous schema for a primitive type or something.
105-
debugPostProcess(`deleting ${oldPath}`);
106-
fs.unlinkSync(oldPath);
107-
}
108-
}
56+
function getDefaultJavaPackageDir(generator, defaultJavaPackage) {
57+
let defaultPackageDir;
58+
59+
if (defaultJavaPackage) {
60+
const packageDir = packageToPath(defaultJavaPackage);
61+
defaultPackageDir = `${generator.targetDir}${sourceHead}${packageDir}`;
62+
}
63+
64+
debugPostProcess(`getDefaultJavaPackageDir: ${defaultPackageDir}`);
65+
return defaultPackageDir;
66+
}
67+
68+
function packageToPath(javaPackage) {
69+
return javaPackage.replace(/\./g, '/');
70+
}
71+
72+
function processSchema(generator, schemaName, schema, sourcePath, defaultJavaPackageDir) {
73+
const fileName = getFileName(schemaName);
74+
const filePath = path.resolve(sourcePath, fileName);
75+
debugPostProcess(`processSchema ${schemaName}`);
76+
debugPostProcess(schema);
77+
if (schema.type() !== 'object') {
78+
debugPostProcess(`deleting ${filePath}`);
79+
fs.unlinkSync(filePath);
80+
} else {
81+
const modelClass = applicationModel.getModelClass(schemaName);
82+
const javaName = modelClass.getClassName();
83+
const packageDir = getPackageDir(generator, defaultJavaPackageDir, modelClass);
84+
debugPostProcess(`packageDir: ${packageDir}`);
85+
86+
if (packageDir) {
87+
moveFile(sourcePath, packageDir, fileName);
10988
}
11089

111-
function moveFile(oldDirectory, newDirectory, fileName) {
112-
if (!fs.existsSync(newDirectory)) {
113-
fs.mkdirSync(newDirectory, { recursive: true });
114-
debugPostProcess(`Made directory ${newDirectory}`);
115-
}
116-
const oldPath = path.resolve(oldDirectory, fileName);
117-
const newPath = path.resolve(newDirectory, fileName);
118-
fs.copyFileSync(oldPath, newPath);
119-
fs.unlinkSync(oldPath);
120-
debugPostProcess(`Moved ${fileName} from ${oldPath} to ${newPath}`);
90+
debugPostProcess(`javaName: ${javaName} schemaName: ${schemaName}`);
91+
if (javaName !== schemaName) {
92+
const currentPath = packageDir || sourcePath;
93+
const newPath = path.resolve(currentPath, `${javaName}.java`);
94+
const oldPath = path.resolve(currentPath, fileName);
95+
fs.renameSync(oldPath, newPath);
96+
debugPostProcess(`Renamed class file ${schemaName} to ${javaName}`);
12197
}
12298
}
123-
};
99+
}
100+
101+
function getFileName(schemaName) {
102+
const trimmedSchemaName = trimSchemaName(schemaName);
103+
return `${trimmedSchemaName}.java`;
104+
}
105+
106+
function trimSchemaName(schemaName) {
107+
let trimmedSchemaName = schemaName;
108+
if (schemaName.startsWith('<')) {
109+
trimmedSchemaName = schemaName.replace('<', '');
110+
trimmedSchemaName = trimmedSchemaName.replace(/>$/, '');
111+
}
112+
return trimmedSchemaName;
113+
}
114+
115+
function getPackageDir(generator, defaultJavaPackageDir, modelClass) {
116+
const fileSpecificPackage = modelClass.getJavaPackage();
117+
if (fileSpecificPackage) {
118+
const packagePath = packageToPath(fileSpecificPackage);
119+
return `${generator.targetDir}${sourceHead}${packagePath}`;
120+
}
121+
return defaultJavaPackageDir;
122+
}
124123

124+
function moveFile(oldDirectory, newDirectory, fileName) {
125+
if (!fs.existsSync(newDirectory)) {
126+
fs.mkdirSync(newDirectory, { recursive: true });
127+
debugPostProcess(`Made directory ${newDirectory}`);
128+
}
129+
const oldPath = path.resolve(oldDirectory, fileName);
130+
const newPath = path.resolve(newDirectory, fileName);
131+
fs.copyFileSync(oldPath, newPath);
132+
fs.unlinkSync(oldPath);
133+
debugPostProcess(`Moved ${fileName} from ${oldPath} to ${newPath}`);
134+
}

hooks/pre-process.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const ApplicationModel = require('../lib/applicationModel.js');
2+
3+
module.exports = {
4+
'generate:before': generator => {
5+
ApplicationModel.asyncapi = generator.asyncapi;
6+
}
7+
};

lib/applicationModel.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
const ModelClass = require('./modelClass.js');
2+
const debugApplicationModel = require('debug')('applicationModel');
3+
const instanceMap = new Map();
4+
5+
class ApplicationModel {
6+
constructor(caller) {
7+
this.caller = caller;
8+
debugApplicationModel(`constructor for ${caller} ++++++++++++++++++++++++++++++++++++++++++`);
9+
instanceMap.set(caller, this);
10+
debugApplicationModel(instanceMap);
11+
}
12+
13+
getModelClass(schemaName) {
14+
debugApplicationModel(`getModelClass for caller ${this.caller} schema ${schemaName}`);
15+
this.setupSuperClassMap();
16+
this.setupModelClassMap();
17+
const modelClass = this.modelClassMap[schemaName];
18+
debugApplicationModel(`returning modelClass for caller ${this.caller} ${schemaName}`);
19+
debugApplicationModel(modelClass);
20+
return modelClass;
21+
}
22+
23+
getSchema(schemaName) {
24+
return ApplicationModel.asyncapi.components().schema(schemaName);
25+
}
26+
27+
setupSuperClassMap() {
28+
if (!this.superClassMap) {
29+
this.superClassMap = new Map();
30+
this.anonymousSchemaToSubClassMap = new Map();
31+
debugApplicationModel('-------- SCHEMAS -------------');
32+
debugApplicationModel(ApplicationModel.asyncapi.allSchemas());
33+
ApplicationModel.asyncapi.allSchemas().forEach((schema, schemaName) => {
34+
debugApplicationModel(`${schemaName}:`);
35+
debugApplicationModel(schema);
36+
const allOf = schema.allOf();
37+
if (allOf) {
38+
this.handleAllOfSchema(schema, schemaName, allOf);
39+
}
40+
});
41+
debugApplicationModel('-----------------------------');
42+
debugApplicationModel('superclassMap:');
43+
debugApplicationModel(this.superClassMap);
44+
debugApplicationModel('anonymousSchemaToSubClassMap:');
45+
debugApplicationModel(this.anonymousSchemaToSubClassMap);
46+
}
47+
}
48+
49+
handleAllOfSchema(schema, schemaName, allOfSchema) {
50+
let anonymousSchema;
51+
let namedSchema;
52+
allOfSchema.forEach(innerSchema => {
53+
debugApplicationModel('=== allOf inner schema: ===');
54+
debugApplicationModel(innerSchema);
55+
debugApplicationModel('===========================');
56+
const name = innerSchema._json['x-parser-schema-id'];
57+
if (this.isAnonymousSchema(name)) {
58+
anonymousSchema = name;
59+
} else {
60+
namedSchema = name;
61+
}
62+
});
63+
if (!anonymousSchema || !namedSchema) {
64+
console.log('Warning: Unable to find both an anonymous and a named schema in an allOf schema.');
65+
console.log(schema);
66+
} else {
67+
this.superClassMap[anonymousSchema] = namedSchema;
68+
this.anonymousSchemaToSubClassMap[anonymousSchema] = schemaName;
69+
}
70+
}
71+
72+
setupModelClassMap() {
73+
if (!this.modelClassMap) {
74+
this.modelClassMap = new Map();
75+
ApplicationModel.asyncapi.allSchemas().forEach((schema, schemaName) => {
76+
debugApplicationModel(`setupModelClassMap ${schemaName} type ${schema.type()}`);
77+
const allOf = schema.allOf();
78+
debugApplicationModel('allOf:');
79+
debugApplicationModel(allOf);
80+
if (allOf) {
81+
allOf.forEach(innerSchema => {
82+
const name = innerSchema._json['x-parser-schema-id'];
83+
if (this.isAnonymousSchema(name) && innerSchema.type() === 'object') {
84+
this.addSchemaToMap(innerSchema, schemaName);
85+
}
86+
});
87+
} else {
88+
this.addSchemaToMap(schema, schemaName);
89+
}
90+
});
91+
debugApplicationModel('modelClassMap:');
92+
debugApplicationModel(this.modelClassMap);
93+
}
94+
}
95+
96+
isAnonymousSchema(schemaName) {
97+
return schemaName.startsWith('<');
98+
}
99+
100+
addSchemaToMap(schema, schemaName) {
101+
const modelClass = new ModelClass();
102+
let tentativeClassName = schemaName;
103+
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+
}
110+
}
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);
118+
}
119+
modelClass.setClassName(tentativeClassName);
120+
debugApplicationModel(`schemaName ${schemaName} className: ${modelClass.getClassName()} super: ${modelClass.getSuperClassName()} javaPackage: ${javaPackage}`);
121+
this.modelClassMap[schemaName] = modelClass;
122+
debugApplicationModel(`Added ${schemaName}`);
123+
debugApplicationModel(modelClass);
124+
}
125+
126+
reset() {
127+
instanceMap.forEach((val) => {
128+
val.superClassMap = null;
129+
val.anonymousSchemaToSubClassMap = null;
130+
val.modelClassMap = null;
131+
});
132+
}
133+
}
134+
135+
module.exports = ApplicationModel;

0 commit comments

Comments
 (0)