Skip to content

Commit da7d206

Browse files
authored
Merge pull request #3 from FlowFuse/simple-jsonschema-generator
Add simple JSONSchema generator
2 parents f5b94a2 + 341f22b commit da7d206

File tree

4 files changed

+297
-11
lines changed

4 files changed

+297
-11
lines changed

lib/schema-generator/index.js

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Get the type description of a value.
2+
// This only needs to handle JSON-compatible values:
3+
// - object
4+
// - array
5+
// - string
6+
// - number
7+
// - boolean
8+
// - null
9+
function getObjectType (obj) {
10+
let type = typeof obj
11+
if (type === 'object') {
12+
if (Array.isArray(obj)) {
13+
type = 'array'
14+
} else if (obj === null) {
15+
type = 'null'
16+
}
17+
}
18+
return type
19+
}
20+
21+
function handleArray (obj) {
22+
const schema = {
23+
type: 'array'
24+
}
25+
// Check types of array values.
26+
let arrayType
27+
let multipleTypes = false
28+
let itemsSchema
29+
for (let i = 0; i < obj.length; i++) {
30+
const elementSchema = generateSchema(obj[i])
31+
const elementType = elementSchema.type
32+
if (i > 0 && elementType !== arrayType) {
33+
// Mixed fundamental types in the array.
34+
multipleTypes = true
35+
// For now, we just skip trying to representing multiple types
36+
break
37+
} else {
38+
arrayType = elementType
39+
if (elementType === 'object') {
40+
if (!itemsSchema) {
41+
itemsSchema = elementSchema
42+
} else {
43+
// Merge the properties of multiple objects rather than
44+
// try to create oneOf type schemas
45+
const keys = Object.keys(elementSchema.properties)
46+
keys.forEach(key => {
47+
if (!Object.hasOwn(itemsSchema.properties, key)) {
48+
itemsSchema.properties[key] = elementSchema.properties[key]
49+
}
50+
})
51+
}
52+
} else if (elementType === 'array') {
53+
if (!itemsSchema) {
54+
itemsSchema = elementSchema
55+
} else {
56+
// TODO: Check if they match
57+
}
58+
} else {
59+
itemsSchema = generateSchema(obj[i])
60+
}
61+
}
62+
}
63+
if (!multipleTypes && arrayType) {
64+
schema.items = itemsSchema
65+
}
66+
return schema
67+
}
68+
69+
function handleObject (obj) {
70+
const schema = {
71+
type: 'object',
72+
properties: {}
73+
}
74+
for (const [key, value] of Object.entries(obj)) {
75+
schema.properties[key] = generateSchema(value)
76+
}
77+
78+
return schema
79+
}
80+
81+
/**
82+
* Generates a JSONSchema doc for the provided value.
83+
* This assumes the value comes from a JSON-parsed string - so only handles the
84+
* strict subset of valid JSON types.
85+
* Limitations:
86+
* - Arrays of mixed types - doesn't include type information
87+
* - Arrays of Objects with different properties - returns the set of all properties seen
88+
* - If a property has different types in different objects, the first type is returned
89+
* @param {*} obj The JSON-parsed value to generate a schema for
90+
* @returns The JSONSchema for the provided value
91+
*/
92+
function generateSchema (obj) {
93+
const type = getObjectType(obj)
94+
switch (type) {
95+
case 'object':
96+
return handleObject(obj)
97+
case 'array':
98+
return handleArray(obj)
99+
default:
100+
return {
101+
type
102+
}
103+
}
104+
}
105+
module.exports = {
106+
generateSchema
107+
}

package-lock.json

+80-10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"url": "git+https://github.com/FlowFuse/mqtt-schema-agent.git"
1313
},
1414
"scripts": {
15-
"test": "mocha 'test/unit/**/*_spec.js'",
15+
"test": "mocha 'test/**/*_spec.js' --timeout 10000 --node-option=unhandled-rejections=strict",
1616
"lint": "eslint -c .eslintrc \"*.js\" \"lib/**/*.js\" \"test/**/*.js\"",
1717
"lint:fix": "eslint -c .eslintrc \"*.js\" \"lib/**/*.js\" \"test/**/*.js\" --fix"
1818
},
@@ -34,6 +34,7 @@
3434
"mqtt": "^5.10.3"
3535
},
3636
"devDependencies": {
37+
"ajv": "8.17.1",
3738
"aedes": "^0.49.0",
3839
"eslint": "^8.48.0",
3940
"eslint-config-standard": "^17.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
const { generateSchema } = require('../../../lib/schema-generator/index')
2+
const Ajv = require('ajv')
3+
const validator = new Ajv()
4+
5+
const TESTCASES = [
6+
'abc',
7+
123,
8+
false,
9+
true,
10+
null,
11+
{},
12+
{ a: 1, b: 2, c: null, d: [1, 2, 3] },
13+
{ a: { b: { c: 1 } } },
14+
[],
15+
[1, 2, 3],
16+
['a', 'b', 'c'],
17+
[true, 'a', 1],
18+
[{ a: 1 }, { b: 'abc' }],
19+
{
20+
Account: {
21+
'Account Name': 'Firefly',
22+
Order: [
23+
{
24+
OrderID: 'order103',
25+
Product: [
26+
{
27+
'Product Name': 'Bowler Hat',
28+
ProductID: 858383,
29+
SKU: '0406654608',
30+
Description: {
31+
Colour: 'Purple',
32+
Width: 300,
33+
Height: 200,
34+
Depth: 210,
35+
Weight: 0.75
36+
},
37+
Price: 34.45,
38+
Quantity: 2
39+
},
40+
{
41+
'Product Name': 'Trilby hat',
42+
ProductID: 858236,
43+
SKU: '0406634348',
44+
Description: {
45+
Colour: 'Orange',
46+
Width: 300,
47+
Height: 200,
48+
Depth: 210,
49+
Weight: 0.6
50+
},
51+
Price: 21.67,
52+
Quantity: 1
53+
}
54+
]
55+
},
56+
{
57+
OrderID: 'order104',
58+
Product: [
59+
{
60+
'Product Name': 'Bowler Hat',
61+
ProductID: 858383,
62+
SKU: '040657863',
63+
Description: {
64+
Colour: 'Purple',
65+
Width: 300,
66+
Height: 200,
67+
Depth: 210,
68+
Weight: 0.75
69+
},
70+
Price: 34.45,
71+
Quantity: 4
72+
},
73+
{
74+
ProductID: 345664,
75+
SKU: '0406654603',
76+
'Product Name': 'Cloak',
77+
Description: {
78+
Colour: 'Black',
79+
Width: 30,
80+
Height: 20,
81+
Depth: 210,
82+
Weight: 2
83+
},
84+
Price: 107.99,
85+
Quantity: 1
86+
}
87+
]
88+
}
89+
]
90+
}
91+
}
92+
]
93+
94+
describe('Schema Generator', function () {
95+
TESTCASES.forEach((value, index) => {
96+
it('generates valid schema ' + index, function () {
97+
const schema = generateSchema(value)
98+
const validate = validator.compile(schema)
99+
const valid = validate(value)
100+
if (!valid) {
101+
console.log(`Value: ${JSON.stringify(value)}`)
102+
console.log(`Schema: ${JSON.stringify(schema)}`)
103+
console.log(`Errors: ${validate.errors}`)
104+
valid.should.be.true()
105+
}
106+
})
107+
})
108+
})

0 commit comments

Comments
 (0)