spase-model-tools
Version:
Tools to generate information model PDF, JSON and XSD files.
668 lines (573 loc) • 21.6 kB
JavaScript
#!/usr/bin/env node
"use strict";
/**
* Read information model specification file in JSON format and generate an XML schema document (XSD).
*
* @author Todd King
**/
const fs = require('fs');
const yargs = require('yargs');
const path = require('path');
const lineByLine = require('n-readlines');
const htmlEncode = require('js-htmlencode').htmlEncode;
var options = yargs
.version('1.0.2')
.usage('Read information model specification file in JSON format and generate an XML schema document (XSD).')
.usage('$0 [args] <folder>')
.example('$0 -d example.json', 'Read the information model specification file "example.json"')
.epilog('copyright 2020')
.showHelpOnFail(false, "Specify --help for available options")
.help('h')
// version
.options({
// Verbose flag
'v' : {
alias: 'verbose',
describe : 'show information while processing files',
type: 'boolean',
default: false
},
// help text
'h' : {
alias : 'help',
description: 'show information about the app.'
},
// Config
'c' : {
alias: 'config',
describe : 'Config file.',
type: 'string',
default: 'config.json'
},
// Data
'd' : {
alias: 'data',
describe : 'File containing data.',
type: 'string',
default: 'data/data.json'
},
// base element name
'b' : {
alias : 'base',
description: 'Base element name of document.',
default: "Spase"
},
// Output file
'o' : {
alias: 'output',
describe : 'Output file name for generated JSON file.',
type: 'string',
default: null
},
})
.argv
;
var args = options._; // Unprocessed command line arguments
var outputFile = null; // None defined.
// Output
if(options.output) {
outputFile = fs.createWriteStream(options.output);
}
var model = readModelSpec(options.data);
// outputWrite(0, "Base: " + options.base);
// outputWrite(0, JSON.stringify(model.dictionary[options.base], null, 3));
// outputWrite(0, JSON.stringify(model.ontology[options.base], null, 3));
makeXSD(model);
outputEnd();
/**
* Write to output file if defined, otherwise to console.log()
**/
function outputWrite(indent, str) {
var prefix = "";
for(var i = 0; i < indent; i++) { prefix += " "; }
if(outputFile == null) {
var prefix = "";
for(var i = 0; i < indent; i++) { prefix += " "; }
console.log(prefix + str);
} else {
outputFile.write(prefix + str + "\n");
}
}
/**
* Concludes output.
**/
function outputEnd() {
if(outputFile != null) {
outputFile.end();
}
}
/**
* Close an output file if one is assigned.
**/
var outputEnd = function() {
if(outputFile) { outputFile.end(); outputFile = null }
}
function today() {
var stamp = new Date();
var dd = String(stamp.getDate()).padStart(2, '0');
var mm = String(stamp.getMonth() + 1).padStart(2, '0'); //January is 0!
var yyyy = stamp.getFullYear();
stamp = yyyy + '-' + mm + '-' + dd;
return(stamp);
}
function readModelSpec(pathname) {
var data=fs.readFileSync(pathname, 'utf8');
var model=JSON.parse(data);
return model;
}
function makeXSD(model) {
outputWrite(0, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
outputWrite(0, "<!-- Automatically created based on the specification available at http://www.spase-group.org/model -->");
if( model.extend.length != 0 ) { // Indicate what it extends
outputWrite(0, "<!-- Extends the schema contained in \"" + model.extend + "\" -->");
}
outputWrite(0, "<!-- Version: " + model.version + " -->");
outputWrite(0, "<!-- Generated: " + today() + " -->");
outputWrite(0, "<xsd:schema");
outputWrite(0, " targetNamespace=\"" + model.schemaurl + "\"");
outputWrite(0, " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"");
outputWrite(0, " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
outputWrite(0, " xmlns:vc=\"http://www.w3.org/2007/XMLSchema-versioning\"");
outputWrite(0, " xmlns:" + model.namespace + "=\"" + model.schemaurl + "\"");
outputWrite(0, " elementFormDefault=\"qualified\"");
outputWrite(0, " attributeFormDefault=\"unqualified\"");
outputWrite(0, " vc:minVersion=\"1.1\"");
outputWrite(0, " version=\"" + model.version + "\"");
outputWrite(0, ">");
outputWrite(0, "");
if(model.extend.length == 0 ) { // Only include in a base schema
outputWrite(1, "<!-- Document root element -->");
outputWrite(1, "<xsd:element name=\"Spase\" type=\"spase:Spase\" />");
outputWrite(0, "");
}
makeTree(model, "Spase", true);
makeGroup(model);
makeDictionary(model);
makeLists(1, model);
if(model.extend.length == 0 ) { // Only include in a base schema
makeTypes(model);
}
outputWrite(0, "</xsd:schema>");
}
function makeTree(model, term, addLang) {
if(options.verbose) console.log("*** Generating schema ***");
if( model.extend.length != 0 ) { // Override if not in a base schema
outputWrite(1, "<!-- \"override\" does an implicit \"include\" of the referenced schema, then redfines the element -->");
outputWrite(1, "<xsd:override schemaLocation=\"" + model.extend + "\">");
}
makeBranch(model, term, addLang);
makeExtension(model, addLang);
if( model.extend.length != 0 ) { // Override if not in a base schema
outputWrite(1, "</xsd:override>");
outputWrite(0, "");
}
// Remove book keeping elements from ontology
var keys = Object.keys(model.ontology);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var term = model.ontology[key];
if(term['_written']) delete term['_written']; // Remove
}
}
function makeBranch(model, term, addLang) {
if(options.verbose) console.log(" Element: " + term);
var item = model.ontology[term];
if(item['_written']) return; // Don't write twice
item['_written'] = true;
try {
outputWrite(1, "<xsd:complexType name=\"" + getXSLName(term) + "\">");
addAnnotation(2, model.dictionary[term].definition);
outputWrite(2, "<xsd:sequence>");
} catch(e) {
console.log("Processing term: " + term);
console.log(e.message);
}
var currentGroup = "";
var inc = 0;
var inChoice = false;
var keys = Object.keys(model.ontology[term]);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if(key == '_written') continue; // Used to control output
var member = item[key];
if (currentGroup.length > 0) { // In a choice
if( member.group != currentGroup) { // Close group
outputWrite(3, "</xsd:choice>");
inc--;
inChoice = false;
currentGroup = "";
}
}
if (member.group.length > 0) { // part of a choice
if( member.group != currentGroup) { // Start a new choice
outputWrite(3, "<xsd:choice "
+ getXSLOccurrence(member.occurrence)
+ ">");
inc++;
inChoice = true;
}
currentGroup = member.group;
}
var type = member.element;
if( ! model.dictionary[member.element]) {
console.log("Definition of '" + member.element + "' missing from dictionary.");
}
if(model.dictionary[member.element].type == 'Enumeration') {
type = model.dictionary[member.element].list;
}
var occur = "";
if(!inChoice) occur = " " + getXSLOccurrence(member.occurrence);
outputWrite(
3 + inc,
"<xsd:element name=\"" + getXSLName(member.element) + "\""
+ " type=\"" + model.namespace + ":" + getXSLName(type) + "\""
+ occur
+ " />"
);
}
// Wrap up sequence/complexType
if (currentGroup.length > 0) { // In a choice
outputWrite(3, "</xsd:choice>");
}
outputWrite(2, "</xsd:sequence>");
if (addLang) {
outputWrite(3,
"<xsd:attribute name=\"lang\" type=\"xsd:string\" default=\"en\"/>");
}
outputWrite(1, "</xsd:complexType>");
// Now output complexTypes used by this object
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if(model.ontology[key]) makeBranch(model, key, false); // We don't automatically add "lang" to all subelements
}
}
function makeExtension(model, addLang) {
var term = 'Extension';
// "Extension" was introduced in 1.2.0 - Ignore error if 1.1.0
if(model.version.localeCompare("1.2.0") >= 0) {
try {
outputWrite(0, "");
outputWrite(1, "<xsd:complexType name=\"" + getXSLName(term) + "\">");
addAnnotation(2, model.dictionary[term].definition);
outputWrite(2, "<xsd:sequence>");
outputWrite(3, "<xsd:any minOccurs=\"0\" maxOccurs=\"unbounded\" processContents=\"lax\" />");
outputWrite(2, "</xsd:sequence>");
if (addLang) {
outputWrite(3, "<xsd:attribute name=\"lang\" type=\"xsd:string\" default=\"en\"/>");
}
outputWrite(1, "</xsd:complexType>");
} catch(e) {
console.log("Processing term: " + term);
console.log(e.message);
}
}
}
function makeGroup(model) {
// Generate types for each list
var currentGroup = "";
if(options.verbose) console.log("*** Generating groups ***");
outputWrite(0, "<!-- ================================");
outputWrite(0, " Groups");
outputWrite(0, " ================================ -->");
var keys = Object.keys(model.ontology);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var container = model.ontology[key];
var items = Object.keys(container);
for (var j = 0; j < items.length; j++) {
var item = items[j];
var group = container[item].group;
if(group.length == 0) continue;
if (currentGroup.length > 0) { // In a choice
if( group != currentGroup) { // Close group
outputWrite(2, "</xsd:sequence>");
outputWrite(1, "</xsd:group>");
}
}
if( group != currentGroup) { // Start a new choice
if(options.verbose) console.log(" Group: " + group);
outputWrite(1, "<xsd:group name=\"" + group + "\">");
outputWrite(2, "<xsd:sequence>");
}
currentGroup = group;
var term = container[item].element;
outputWrite(3, "<xsd:element name=\"" + getXSLName(term) + "\""
+ " type=\"" + model.namespace + ":" + getXSLName(term) + "\""
+ " " + getXSLOccurrence(container[item].occurrence)
+ " />");
}
}
// If group was started - finish it
if (currentGroup.length > 0) { // Indicates member of group - text after ">" is group name
outputWrite(2, "</xsd:sequence>");
outputWrite(1, "</xsd:group>");
}
}
function makeDictionary(model) {
if(options.verbose) console.log("*** Generating dictionary ***");
outputWrite(0, "<!-- ================================");
outputWrite(0, " Dictionary Terms");
outputWrite(0, " ================================ -->");
// Generate types for each list
var keys = Object.keys(model.dictionary);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var term = model.dictionary[key].term;
var type = model.dictionary[key].type;
var desc = model.dictionary[key].definition;
// Version and Extension are special instances and handled elsewhere.
if(term == "Version") continue;
if(term == "Extension") continue;
if(options.verbose) console.log(" Term: " + key);
if (type == "Item") { // An item appears in enumerations
// Do nothing
} else if (type == "Enumeration") { // Handled separately
// Do nothing
} else if (type == "Container") { // A set of elements as defined in the ontology
// Do nothing
} else if(type == "Boundary") { // Complex content
outputWrite(1, "<xsd:complexType name=\"" + getXSLName(term) + "\">");
addAnnotation(2, desc);
outputWrite(2, "<xsd:complexContent>");
outputWrite(3, "<xsd:restriction base=\"" + getXSLType(type, model.namespace, term) + "\""
+ " />");
outputWrite(1, "</xsd:complexContent>");
outputWrite(1, "</xsd:complexType>");
} else if(type == "Value") { // Simple content
outputWrite(1, "<xsd:complexType name=\"" + getXSLName(term) + "\">");
addAnnotation(2, desc);
outputWrite(2, "<xsd:simpleContent>");
outputWrite(3, "<xsd:restriction base=\"" + getXSLType(type, model.namespace, term) + "\""
+ " />");
outputWrite(1, "</xsd:simpleContent>");
outputWrite(1, "</xsd:complexType>");
} else { // Simple base type
if(type.startsWith("+")) { // Special case - use another class
// don't write
} else {
outputWrite(1, "<xsd:simpleType name=\"" + getXSLName(term) + "\">");
addAnnotation(2, desc);
outputWrite(2, "<xsd:restriction base=\"" + getXSLType(type, model.namespace, term) + "\""
+ " />");
outputWrite(1, "</xsd:simpleType>");
}
}
}
}
/**
* Generate XML schema description of every list item
**/
function makeLists(indent, model) {
if(options.verbose) console.log("*** Generating enumeration lists ***");
outputWrite(0, "<!-- ================================");
outputWrite(0, " Lists");
outputWrite(0, " ================================ -->");
if(model.extend.length == 0) { // Only include in a base schema
// Generate enumeration for version
outputWrite(0, "<!-- ==========================");
outputWrite(0, " Version");
outputWrite(0, " ========================== -->");
outputWrite(indent, "<xsd:simpleType name=\"Version\">");
addAnnotation(indent + 1, "Version number.");
outputWrite(indent + 1, "<xsd:restriction base=\"xsd:string\">");
outputWrite(indent + 2, "<xsd:enumeration value=\"" + model.version + "\" />");
outputWrite(indent + 1, "</xsd:restriction>");
outputWrite(indent + 1, "</xsd:simpleType>");
}
// Generate types for each list
var keys = Object.keys(model.list);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var listName = getXSLName(model.list[key].name);
var desc = model.list[key].definition;
if (model.list[key].type == "Open") {
desc = "Open List. See: " + model.list[key].reference;
}
if(options.verbose) console.log(" List: " + key);
outputWrite(0, "<!-- ==========================");
outputWrite(0, " List: " + model.list[key].name);
outputWrite(0, "");
outputWrite(0, " " + model.list[key].definition);
outputWrite(0, " ========================== -->");
if (model.list[key].type == "Open") {
outputWrite(indent, "<xsd:element name=\"" + listName + "\" type=\"xsd:string\">");
addAnnotation(indent + 1, model.list[key].definition);
outputWrite(indent, "</xsd:element>");
} else if (model.list[key].type == "Union") {
outputWrite(indent, "<xsd:simpleType name=\"" + listName + "\">");
addAnnotation(indent + 1, model.list[key].definition);
outputWrite(indent, "<xsd:union");
makeEnumUnion(indent, model.namespace + ":", model.list[key].reference);
outputWrite(indent, "/>");
outputWrite(indent, "</xsd:simpleType>");
} else { // Closed
outputWrite(indent, "<xsd:simpleType name=\"" + listName + "\">");
addAnnotation(indent + 1, model.list[key].definition);
outputWrite(indent + 1, "<xsd:restriction base=\"xsd:string\">");
makeEnum(indent + 2, model, "", model.list[key].name);
outputWrite(indent + 1, "</xsd:restriction>");
outputWrite(indent, "</xsd:simpleType>");
}
}
}
/**
* Generate XML schema description of non-standard data types
**/
function makeTypes(model) {
if(options.verbose) console.log("*** Generating types ***");
outputWrite(0, "<!-- ================================");
outputWrite(0, " Types");
outputWrite(0, " ================================ -->");
outputWrite(0, "");
if(options.verbose) console.log(" Type: Sequence");
if(model.type["Sequence"]) { // Introduced in version 1.2.0
outputWrite(1, "<xsd:simpleType name=\"typeSequence\">");
outputWrite(2, "<xsd:annotation>");
outputWrite(3, "<xsd:documentation xml:lang=\"en\">");
addAnnotation(4, model.type["Sequence"].definition);
outputWrite(3, "</xsd:documentation>");
outputWrite(2, "</xsd:annotation>");
outputWrite(2, "<xsd:list itemType=\"xsd:integer\"/>");
outputWrite(1, "</xsd:simpleType>");
}
outputWrite(0, "");
if(options.verbose) console.log(" Type: ID");
if(model.type["ID"]) { // Introduced in version 2.2.3
outputWrite(1, "<xsd:simpleType name=\"typeID\">");
outputWrite(2, "<xsd:annotation>");
outputWrite(3, "<xsd:documentation xml:lang=\"en\">");
addAnnotation(4, model.type["ID"].definition);
outputWrite(3, "</xsd:documentation>");
outputWrite(2, "</xsd:annotation>");
outputWrite(2, "<xsd:restriction base=\"xsd:string\">");
outputWrite(3, "<xsd:pattern value=\"[^:]+://[^/]+/.+\"/>");
outputWrite(2, "</xsd:restriction>");
outputWrite(1, "</xsd:simpleType>");
}
}
/**
* Generate XML schema complextType for a type defined with an ontology
**/
function defineType(model, name) {
outputWrite(0, "");
outputWrite(1, "<xsd:complexType name=\"type" + name + "\">");
outputWrite(2, "<xsd:annotation>");
outputWrite(3, "<xsd:documentation xml:lang=\"en\">");
outputWrite(4, model.dictionary[name].definition);
outputWrite(3, "</xsd:documentation>");
outputWrite(2, "</xsd:annotation>");
outputWrite(2, "<xsd:sequence>");
// Generate types for each list
var list = model.ontology[name];
var keys = Object.keys(list);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
var elem = getXSLName(key);
var type = getXSLName(model.dictionary[key].type);
if(type.isEmpty()) type = model.namespace + ":" + key ;
outputWrite(3, "<xsd:element name=\"" + elem + "\" type=\"" + type + "\" " + getXSLOccurrence(model.ontology[key].occurrence) + " />" );
}
outputWrite(2, "</xsd:sequence>");
outputWrite(1, "</xsd:complexType>");
}
/**
* Create an enumeration list.
**/
function makeEnum(indent, model, prefix, list) {
var buffer = "";
if( ! model.member[list]) {
console.log("List '" + list + "' has no members defined.");
return;
}
try {
var keys = Object.keys(model.member[list]);
var term = "";
for (var i = 0; i < keys.length; i++) {
term = keys[i];
buffer = prefix;
if (prefix.length > 0) buffer += ".";
buffer += getXSLName(term);
outputWrite(indent + 1, "<xsd:enumeration value=\"" + buffer + "\">");
if( ! model.dictionary[term]) console.log("Error in list '" + list + "' - member '" + term +"' is not defined.");
addAnnotation(indent + 2, model.dictionary[term].definition);
outputWrite(indent + 1, "</xsd:enumeration>");
if (model.member[term]) { // Nested Enumeration
makeEnum(indent + 1, model, buffer, term);
}
}
} catch(e) {
console.log("Processing term '" + term + "' in list '" + list + "'");
console.log(e.message);
}
}
/**
* Create an enumeration list.
**/
function makeEnumUnion(indent, prefix, list) {
var part = list.split(",");
console.log("makeEnumUnion");
console.log(" prefix: " + prefix);
console.log(" list: " + list);
var delim = "";
var enumList = "memberTypes=\"";
for(var i = 0; i < part.length; i++) {
var p = part[i];
if(p.indexOf(":") == -1) enumList += delim + prefix + p.trim(); // No prefix - add namespace
else enumList += delim + p.trim(); // Namespace already defined
delim = " ";
}
enumList += "\"";
console.log(enumList);
outputWrite(indent+1, enumList);
}
function getXSLName(term) {
// Strip spaces, dashes and single quotes
if( ! term ) return "";
var buffer = "";
buffer = term.replace(/-/g, "");
buffer = buffer.replace(/'/g, "");
buffer = buffer.replace(/ /g, "");
return buffer;
}
function getElementGroup(term) {
var buffer = "";
return buffer;
}
function addAnnotation(indent, desc) {
outputWrite(indent, "<xsd:annotation>");
outputWrite(indent + 1, "<xsd:documentation xml:lang=\"en\">");
outputWrite(indent + 1, htmlEncode(desc));
outputWrite(indent + 1, "</xsd:documentation>");
outputWrite(indent, "</xsd:annotation>");
}
function getXSLType(type, namespace, name) {
// XML Schema internal types
if (type == "Container") return namespace + ":" + name;
// XML Schema built-in types
if (type == "Count") return "xsd:integer";
if (type == "DateTime") return "xsd:dateTime";
if (type == "Duration") return "xsd:duration";
if (type == "Numeric") return "xsd:double";
if (type == "Text") return "xsd:string";
if (type == "URL") return "xsd:anyURI";
// Internally defined types
if (type == "Boundary") return "spase:typeBoundary";
if (type == "Value") return "spase:typeValue";
if (type == "Sequence") return "spase:typeSequence";
if (type == "StringSequence") return "spase:typeStringSequence";
if (type == "FloatSequence") return "spase:typeFloatSequence";
if (type == "ID") return "spase:typeID";
// Defined differently prior to version 1.2.0
// Date was a xsd:dateTime and Time was xsd:duration.
if (type == "Date") return "xsd:date";
if (type == "Time") return "xsd:time";
return "xsd:string"; // Default
}
function getXSLOccurrence(occur) {
occur = occur.trim();
if (occur == "0") return "minOccurs=\"0\" maxOccurs=\"1\""; // Optional
if (occur == "1") return "minOccurs=\"1\" maxOccurs=\"1\""; // One only
if (occur == "+") return "minOccurs=\"1\" maxOccurs=\"unbounded\""; // At least one, perhaps many
if (occur, "*") return "minOccurs=\"0\" maxOccurs=\"unbounded\""; // Any number
return ""; // Default
}