Skip to content

Commit fbaf3de

Browse files
Added schema bundler and updated readme.
Signed-off-by: Steve Springett <[email protected]>
1 parent dae213a commit fbaf3de

File tree

3 files changed

+226
-6
lines changed

3 files changed

+226
-6
lines changed

schema/2.0/README.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
11
# CycloneDX 2.0 Schemas
22

3-
This directory contains the official JSON Schema definitions for CycloneDX 2.0, as standardised in [ECMA-424](https://ecma-international.org/publications-and-standards/standards/ecma-424/). These schemas constitute the normative implementation of the CycloneDX specification and are intended for use in validation, tooling, and data exchange.
3+
This directory contains the official JSON Schema definitions for CycloneDX 2.0, as standardised in
4+
[ECMA-424](https://ecma-international.org/publications-and-standards/standards/ecma-424/).
5+
These schemas constitute the normative implementation of the CycloneDX specification and are intended for use in
6+
validation, tooling, and data exchange.
47

58
## Schema Overview
69

710
| File | Description |
811
|--------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
912
| [`cyclonedx-2.0.schema.json`](./cyclonedx-2.0.schema.json) | The normative schema for CycloneDX Bill of Materials (BOM) documents. This schema references modular models and defines the complete structure for expressing inventories and metadata. |
13+
| [`cyclonedx-2.0-bundled.schema.json`](./cyclonedx-2.0-bundled.schema.json) | A fully resolved version of the BOM schema with all external model references inlined. Useful for systems that require a self-contained schema. |
1014
| [`cyclonedx-api-2.0.schema.json`](./cyclonedx-api-2.0.schema.json) | The normative API-focused schema. It reuses CycloneDX models but is structured for compatibility with request/response patterns in service architectures. |
11-
| [`cyclonedx-combined-2.0.schema.json`](./cyclonedx-combined-2.0.schema.json) | A fully resolved version of the BOM schema with all external model references inlined. Useful for systems that require a self-contained schema. |
12-
| [`cyclonedx-api-combined-2.0.schema.json`](./cyclonedx-api-combined-2.0.schema.json) | The combined version of the API schema with all model definitions embedded. Suitable for use in tools or validators that do not support `$ref` resolution. |
15+
| [`cyclonedx-api-2.0-bundled.schema.json`](./cyclonedx-api-2.0-bundled.schema.json) | The combined version of the API schema with all model definitions embedded. Suitable for use in tools or validators that do not support `$ref` resolution. |
1316

1417
## Modularity and Model Composition
1518

16-
CycloneDX 2.0 is defined as a modular specification. All core concepts—such as components, services, vulnerabilities, licensing, and AI/ML metadata—are encapsulated in reusable model definitions located in the [`model/`](./model) directory.
19+
CycloneDX 2.0 is defined as a modular specification. All core concepts—such as components, services, vulnerabilities,
20+
licensing, and AI/ML metadata, are encapsulated in reusable model definitions located in the [`model/`](./model) directory.
1721

1822
This modular architecture promotes:
1923

2024
- **Consistency** across multiple schema contexts
2125
- **Reusability** of models within and beyond CycloneDX
2226
- **Clarity and maintainability** for implementers
2327

24-
## Combined Schemas
28+
## Bundled Schemas
2529

26-
The `*-combined` schema files are auto-generated from the normative schemas by resolving all references. These are provided for convenience and do not supersede the authoritative pre-defined schemas.
30+
The `*-bundled` schema files are auto-generated from the normative schemas by resolving all references.
31+
These are provided for convenience and do not supersede the authoritative pre-defined schemas.
2732

2833
## Related Resources
2934

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs').promises;
4+
const path = require('path');
5+
6+
/**
7+
* Recursively walks through an object and rewrites $ref paths
8+
*/
9+
function rewriteRefs(obj, schemaFiles, defsKeyword, currentSchemaName) {
10+
if (typeof obj !== 'object' || obj === null) {
11+
return obj;
12+
}
13+
14+
if (Array.isArray(obj)) {
15+
return obj.map(item => rewriteRefs(item, schemaFiles, defsKeyword, currentSchemaName));
16+
}
17+
18+
const newObj = {};
19+
for (const [key, value] of Object.entries(obj)) {
20+
if (key === '$ref' && typeof value === 'string') {
21+
// Case 1: Reference to another schema file (external reference)
22+
const fileMatch = value.match(/^(.+\.schema\.json)(#.*)?$/);
23+
if (fileMatch) {
24+
const filename = fileMatch[1];
25+
const fragment = fileMatch[2] || '';
26+
27+
const basename = path.basename(filename);
28+
const schemaName = basename.replace('.schema.json', '');
29+
30+
// Rewrite to point to the bundled schema's definitions
31+
newObj[key] = `#/${defsKeyword}/${schemaName}${fragment}`;
32+
}
33+
// Case 2: Internal reference within the same schema (starts with #)
34+
else if (value.startsWith('#')) {
35+
// Rewrite to be relative to the current schema's location in the bundle
36+
newObj[key] = `#/${defsKeyword}/${currentSchemaName}${value.substring(1)}`;
37+
}
38+
// Case 3: Other references (URLs, etc.) - leave as-is
39+
else {
40+
newObj[key] = value;
41+
}
42+
} else {
43+
newObj[key] = rewriteRefs(value, schemaFiles, defsKeyword, currentSchemaName);
44+
}
45+
}
46+
return newObj;
47+
}
48+
49+
async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
50+
try {
51+
const absoluteModelsDir = path.resolve(modelsDirectory);
52+
const absoluteRootPath = path.resolve(rootSchemaPath);
53+
54+
// Verify paths exist
55+
await fs.access(absoluteModelsDir);
56+
await fs.access(absoluteRootPath);
57+
58+
const rootSchemaFilename = path.basename(absoluteRootPath);
59+
const rootSchemaDir = path.dirname(absoluteRootPath);
60+
61+
console.log(`Models directory: ${absoluteModelsDir}`);
62+
console.log(`Root schema: ${absoluteRootPath}`);
63+
64+
// Generate output filename by inserting "-bundled" before ".schema.json"
65+
const outputFilename = rootSchemaFilename.replace('.schema.json', '-bundled.schema.json');
66+
const outputPath = path.join(rootSchemaDir, outputFilename);
67+
68+
console.log(`Output: ${outputPath}\n`);
69+
70+
// Read all schema files in the models directory
71+
const files = await fs.readdir(absoluteModelsDir);
72+
const schemaFiles = files.filter(file => file.endsWith('.schema.json') && !file.includes('-bundled'));
73+
74+
console.log(`Found ${schemaFiles.length} schema files in models directory`);
75+
76+
// Read all schemas from models directory
77+
const schemas = {};
78+
let detectedSchemaVersion = null;
79+
80+
for (const file of schemaFiles) {
81+
const schemaPath = path.join(absoluteModelsDir, file);
82+
console.log(` Reading ${file}...`);
83+
84+
const content = await fs.readFile(schemaPath, 'utf8');
85+
const schema = JSON.parse(content);
86+
87+
// Detect the $schema version from the first schema that has it
88+
if (!detectedSchemaVersion && schema.$schema) {
89+
detectedSchemaVersion = schema.$schema;
90+
}
91+
92+
const schemaName = path.basename(file, '.schema.json');
93+
schemas[schemaName] = schema;
94+
}
95+
96+
// Read the root schema
97+
console.log(`\nReading root schema...`);
98+
const rootContent = await fs.readFile(absoluteRootPath, 'utf8');
99+
const rootSchema = JSON.parse(rootContent); // Fixed: was 'content', should be 'rootContent'
100+
const rootSchemaName = path.basename(rootSchemaFilename, '.schema.json');
101+
102+
// Add root schema to the schemas collection
103+
schemas[rootSchemaName] = rootSchema;
104+
105+
// Use detected version from root schema if available
106+
if (rootSchema.$schema) {
107+
detectedSchemaVersion = rootSchema.$schema;
108+
}
109+
110+
// Use detected version, or provided option, or default to 2020-12
111+
const schemaVersion = options.schemaVersion ||
112+
detectedSchemaVersion ||
113+
'https://json-schema.org/draft/2020-12/schema';
114+
115+
// Determine which keyword to use based on schema version
116+
const isDraft2019OrLater = schemaVersion.includes('2019-09') ||
117+
schemaVersion.includes('2020-12') ||
118+
schemaVersion.includes('/next');
119+
const defsKeyword = isDraft2019OrLater ? '$defs' : 'definitions';
120+
121+
console.log(`\nUsing schema version: ${schemaVersion}`);
122+
console.log(`Using keyword: ${defsKeyword}`);
123+
console.log('Rewriting $ref pointers...');
124+
125+
// Rewrite all $refs in all schemas
126+
const rewrittenDefinitions = {};
127+
for (const [name, schema] of Object.entries(schemas)) {
128+
console.log(` Rewriting refs in ${name}...`);
129+
rewrittenDefinitions[name] = rewriteRefs(schema, [...schemaFiles, rootSchemaFilename], defsKeyword, name);
130+
}
131+
132+
// Get the rewritten root schema
133+
const rootSchemaRewritten = rewrittenDefinitions[rootSchemaName];
134+
135+
// Build the final schema with root schema properties at the top level
136+
const finalSchema = {
137+
...rootSchemaRewritten,
138+
"$schema": schemaVersion,
139+
[defsKeyword]: rewrittenDefinitions
140+
};
141+
142+
// Optionally validate with AJV
143+
if (options.validate) {
144+
console.log('\nValidating with AJV...');
145+
const Ajv = require('ajv');
146+
const ajv = new Ajv({
147+
strict: false,
148+
allowUnionTypes: true
149+
});
150+
151+
try {
152+
ajv.compile(finalSchema);
153+
console.log('✓ Schema validation passed');
154+
} catch (validationErr) {
155+
console.warn('⚠ Schema validation warning:', validationErr.message);
156+
}
157+
}
158+
159+
await fs.writeFile(outputPath, JSON.stringify(finalSchema, null, 2));
160+
161+
console.log(`\n✓ Bundled ${Object.keys(schemas).length} schemas to ${outputFilename}`);
162+
163+
// Show file size
164+
const stats = await fs.stat(outputPath);
165+
const sizeKB = (stats.size / 1024).toFixed(2);
166+
console.log(` File size: ${sizeKB} KB`);
167+
168+
return finalSchema;
169+
170+
} catch (err) {
171+
console.error('Error processing schemas:', err.message);
172+
throw err;
173+
}
174+
}
175+
176+
// CLI usage
177+
if (require.main === module) {
178+
const [,, modelsDirectory, rootSchemaPath] = process.argv;
179+
180+
if (!modelsDirectory || !rootSchemaPath) {
181+
console.log('Usage: node bundle-schemas.js <models-directory> <root-schema-path>');
182+
console.log('');
183+
console.log('Example:');
184+
console.log(' node bundle-schemas.js \\');
185+
console.log(' ./schema/2.0/model \\');
186+
console.log(' ./schema/2.0/cyclonedx-2.0.schema.json');
187+
console.log('');
188+
console.log('This will create: ./schema/2.0/cyclonedx-2.0-bundled.schema.json');
189+
process.exit(1);
190+
}
191+
192+
bundleSchemas(modelsDirectory, rootSchemaPath, { validate: true })
193+
.catch(err => process.exit(1));
194+
}
195+
196+
module.exports = { bundleSchemas };

tools/src/main/js/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "schema-bundler",
3+
"version": "1.0.0",
4+
"description": "Bundle multiple JSON schemas into a single file",
5+
"main": "bundle-schemas.js",
6+
"scripts": {
7+
"bundle": "node bundle-schemas.js"
8+
},
9+
"bin": {
10+
"bundle-schemas": "./bundle-schemas.js"
11+
},
12+
"keywords": ["json-schema", "ajv", "bundle"],
13+
"author": "",
14+
"license": "MIT",
15+
"dependencies": {
16+
"@apidevtools/json-schema-ref-parser": "^11.0.0",
17+
"ajv": "^8.12.0"
18+
}
19+
}

0 commit comments

Comments
 (0)