UNPKG

uml-model

Version:

Simple UML model with manipulation API

1,240 lines (1,146 loc) 41.1 kB
/** UML Model * editing features: * @function{detach}: replace all references to <this> with a reference to a new MissingElement. * @function{remove(X)}: delete <X>'s entry in <this>. * - update(X, Y): replace all uses of <X> in <this> with <Y>. * * rendering bugs: * 1. junky HTML -- mixes BLOCK and FLOW * 2. doesn't display multi-generaltional inheritance */ function UmlModel (modelOptions = {}, $ = null) { if (typeof UmlModel.singleton === 'object') return UmlModel.singleton const AGGREGATION_shared = 'AGGREGATION_shared' const AGGREGATION_composite = 'AGGREGATION_composite' const XSD = 'http://www.w3.org/2001/XMLSchema#' const jsonCycles = require('circular-json') /** render members of a Model or a Package */ function renderElement (renderTitle, list, renderMember, cssClass) { let expandPackages = $('<img/>', { src: 'plusbox.gif' }) let elements = $('<ul/>') let packages = $('<div/>').addClass('uml ' + cssClass).append( expandPackages, $('<span/>').text(cssClass).addClass('type', cssClass), renderTitle(this), $('<span/>').text(list.length).addClass('length'), elements ).addClass(COLLAPSED).on('click', evt => { if (packages.hasClass(COLLAPSED)) { elements.append(list.map( elt => { try { return $('<li/>').append(renderMember(elt)) } catch (e) { console.warn([e, elt]) return $('<li/>').addClass('error').append(e) } } )) packages.removeClass(COLLAPSED).addClass(EXPANDED) expandPackages.attr('src', 'minusbox.gif') } else { elements.empty() packages.removeClass(EXPANDED).addClass(COLLAPSED) expandPackages.attr('src', 'plusbox.gif') } return false }) return packages } function objDiffs (l, r, render, seen) { if (l === null && r === null || l instanceof Object && Object.keys(l).length === 0 && r === null || r instanceof Object && Object.keys(r).length === 0 && l === null) { return [] } if (!(l instanceof Object) || !(r instanceof Object)) { throw Error('invocation error: objDiffs called with non-object') } if (l.constructor === Array || r.constructor === Array) { throw Error('invocation error: objDiffs called with Array') } return [] .concat(Object.keys(l).reduce( (acc, x) => x in r ? acc : acc.concat(render(x + ' missing in left')), [] )) .concat(Object.keys(r).reduce( (acc, x) => x in l ? acc : acc.concat(render(x + ' missing in right')), [] )) .concat(Object.keys(l).reduce( (acc, x) => x in r ? acc.concat(x) : acc, [] ).reduce( (acc, x) => acc.concat(l[x].diffs(r[x], render, seen)), [] )) } function hashById (list) { return list.reduce( (acc, x) => add(acc, x.id, x), {} ) function add (obj, key, val) { if (key in obj) { throw Error('key ' + key + ' already in ' + obj) } let ret = Object.assign({}, obj) ret[key] = val return ret } } function revisiting (seen, l, r) { // console.warn(seen.length ? seen[seen.length - 2].id : 'top', l.id) if (seen.indexOf(l) !== -1 ) { return true } else { seen.push(l) seen.push(r) return false } } function testRTTI (topic, l, r, render) { return l.rtti === r.rtti ? [] : [render(topic + ' rtti ' + l.rtti + ' != ' + r.rtti)] } function compareList (topic, l, r, render) { let ret = [] for (let i = 0; i < l.length || i < r.length; ++i) { if (l[i] !== r[i]) { ret.push(render(topic + '\'s' + i + 'th entry is ' + l[i] + ' not ' + r[i])) } } return ret } const COLLAPSED = 'collapsed', EXPANDED = 'expanded' class Model { constructor (id, name, source, elements, missingElements) { Object.assign(this, { get rtti () { return 'Model' }, id, name, source, elements, missingElements, // getClasses: function () { // return elements.reduce( // (acc, pkg) => acc.concat(pkg.list('Class')), [] // ) // } getClasses: function () { return this.elements.reduce( (acc, pkg) => acc.concat(pkg.list('Class')), [] ) }, getDatatypes: function () { return this.elements.reduce( (acc, pkg) => acc.concat(pkg.list('Datatype')), [] ) }, getEnumerations: function () { return this.elements.reduce( (acc, pkg) => acc.concat(pkg.list('Enumeration')), [] ) }, getPrimitiveTypes: function () { return this.elements.reduce( (acc, pkg) => acc.concat(pkg.list('PrimitiveType')), [] ) }, getProperties: function () { let ret = {} this.getClasses().concat(this.getDatatypes()).forEach(klass => { klass.properties.forEach( property => { if (!(property.name in ret)) { ret[property.name] = { uses: [] } } ret[property.name].uses.push({ klass, property }) } ) }) return ret } }) } diffs (other, render = s => s, seen = []) { if (revisiting(seen, this, other)) { return [] } let topic = 'Model' return testRTTI(topic, this, other, render) .concat(objDiffs(hashById(this.elements), hashById(other.elements), render, seen)) .concat(objDiffs(this.missingElements, other.missingElements, render, seen)) } render () { let ret = $('<div/>').addClass('uml model ' + EXPANDED) let sourceString = [this.source.resource, this.source.method, this.source.timestamp].join(' ') let renderTitle = _ => [ $('<span/>').text(this.source.resource).addClass('name'), ' ', this.source.method, ' ', this.source.timestamp ] let packages = renderElement(renderTitle, this.elements, elt => elt.render(), 'model') ret.append(packages) return ret } list (rtti) { return this.elements.reduce( (acc, elt) => { let add = [] if (!rtti || elt.rtti === rtti) { add = add.concat([elt]) } if (elt.rtti === 'Package') { add = add.concat(elt.list(rtti)) } return acc.concat(add) }, [] ) } toShExJ (options = {}) { return { "@context": "http://www.w3.org/ns/shex.jsonld", "type": "Schema", "shapes": this.elements.reduce( (acc, pkg) => acc.concat(pkg.toShExJ([], options)), [] ) } } } class Packagable { constructor (id, references, name, parent, comments) { Object.assign(this, { id, references, name }) if (parent) { Object.assign(this, { parent }) } if (comments) { Object.assign(this, { comments }) } } diffs (other, render = s => s, seen = []) { let topic = this.rtti + ' ' + this.id return testRTTI(topic, this, other, render) .concat(objDiffs(hashById(this.references), hashById(other.references), render, seen)) .concat(this.name !== other.name ? [ render(topic + ' name:' + other.name + ' doesn\'t match ' + this.name) ] : []) .concat(this.parent ? this.parent.diffs(other.parent, render, seen).map( d => render(topic + d) ) : []) .concat(this.comments || other.comments ? compareList(topic, this.comments, other.comments, render) : []) } remove (missingElements) { let from = this.references.find(ref => ref instanceof Package || ref instanceof Model) // parent.elements let fromIndex = from ? from.elements.indexOf(this) : -1 if (fromIndex === -1) { // throw Error('detach package: ' + this.id + ' not found in parent ' + from.id) } else { from.elements.splice(fromIndex, 1) } let refIndex = from ? this.references.indexOf(from) : -1 if (refIndex === -1) { // throw Error('detach package: ' + this.id + ' has no reference to parent ' + from.id) } else { this.references.splice(refIndex, 1) } this.detach(missingElements) } detach (missingElements) { if (this.references.length === 0) { // no refs so no MissingElemennt return } if (this.id in missingElements) { throw Error(this.rtti + ' ' + this.id + ' already listed in missingElements') } let missingElt = new MissingElement(this.id, this.references) missingElements[this.id] = missingElt this.references.forEach( ref => ref.update(this, missingElt) ) if (this.references.length === 0) { delete missingElements[this.id] } } render () { let ret = $('<div/>').addClass('uml model ' + EXPANDED) ret.append('render() not implemented on: ' + Object.keys(this).join(' | ')) return ret } renderTitle () { return $('<span/>').text(this.name).addClass('name') } } class Package extends Packagable { constructor (id, reference, name, elements, parent, comments) { // pass the same falsy reference value to Packagable super(id, reference ? [reference] : reference, name, parent, comments) Object.assign(this, { get rtti () { return 'Package' }, elements }) } diffs (other, render = s => s, seen = []) { if (revisiting(seen, this, other)) { return [] } return super.diffs(other, render, seen) .concat(objDiffs(hashById(this.elements), hashById(other.elements), render, seen)) } update (from, to) { let idx = this.elements.indexOf(from) if (idx === -1) { throw Error('update package: ' + from.id + ' not found in elements') } this.elements[idx] = to } remove (missingElements/*, rtti*/) { this.elements.forEach( doomed => { let idx = doomed.references.indexOf(this) if (idx === -1) { // throw Error('detach package: ' + this.id + ' not found in references of child ' + doomed.id) } else { doomed.references.splice(idx, 1) // detach package from references ?? redundant against Packagable.remove this.references.find(Package || Model) } doomed.remove(missingElements) } ) super.remove(missingElements) /* let doomed = this.list(rtti) console.log('detach', doomed) this.list(rtti).forEach( doomed => doomed.detach(missingElements) ) */ } render () { let ret = $('<div/>').addClass('uml package ' + EXPANDED) let packages = renderElement(_ => this.renderTitle(), this.elements, elt => elt.render(), 'package') ret.append(packages) return ret } list (rtti) { return this.elements.reduce( (acc, elt) => { let add = [] if (!rtti || elt.rtti === rtti) { add = add.concat([elt]) } if (elt.rtti === 'Package') { add = add.concat(elt.list(rtti)) } return acc.concat(add) }, [] ) } toShExJ (parents = [], options = {}) { return this.elements.reduce( (acc, elt) => acc.concat(elt.toShExJ(parents.concat(this.name), options)), [] ) } } class Enumeration extends Packagable { constructor (id, references, name, values, parent, comments) { super(id, references, name, parent, comments) Object.assign(this, { get rtti () { return 'Enumeration' }, values }) } diffs (other, render = s => s, seen = []) { if (revisiting(seen, this, other)) { return [] } let topic = 'Enumeration ' + this.id if (this.id !== other.id) { return [render(topic + ' doesn\'t match id ' + other.id)] } return super.diffs(other, render, seen) .concat(compareList(topic, this.values, other.values, render)) // let ret = super.diffs(other, render, seen) // for (let i = 0; i < this.values.length && i < other.values.length; ++i) { // if (this.values[i] !== other.values[i]) { // ret.push(render(topic + '\'s' + i + 'th entry is ' + this.values[i] + ' not ' + other.values[i])) // } // } // return ret } render () { let ret = $('<div/>').addClass('uml enumeration ' + EXPANDED) let packages = renderElement(_ => this.renderTitle(), this.values, elt => elt, 'enumeration') ret.append(packages) return ret } summarize () { return $('<span/>').addClass('uml enumeration').append( $('<span/>').text('enumeration').addClass('type enumeration'), $('<span/>').text(this.name).addClass('name'), $('<span/>').text(this.values.length).addClass('length') ) } toShExJ (parents = [], options = {}) { let ret = { "id": options.iri(this.name, this), "type": "NodeConstraint", "values": this.values.map( v => options.iri(v, this) ) } if (options.annotations) { let toAdd = options.annotations(this) if (toAdd && toAdd.length) { ret.annotations = toAdd } } return ret } } class PrimitiveType extends Packagable { // Parent may be null for automatic primitiveTypes generated by e.g. XSD hrefs. constructor (id, references, name, external, parent, comments) { super(id, references, name, parent, comments) Object.assign(this, { get rtti () { return 'PrimitiveType' }, external }) } diffs (other, render = s => s, seen = []) { if (revisiting(seen, this, other)) { return [] } let topic = 'PrimitiveType ' + this.id if (this.id !== other.id) { return [render(topic + ' doesn\'t match id ' + other.id)] } return super.diffs(other, render, seen) } render () { return $('<div/>').addClass('uml primitiveType ' + EXPANDED).append( renderElement(_ => this.renderTitle(), [], () => null, 'primitiveType') ) } summarize () { return $('<span/>').addClass('uml primitiveType').append( $('<span/>').text('primitiveType').addClass('type primitiveType'), $('<span/>').text(this.name).addClass('name') ) } toShExJ (parents = [], options = {}) { let ret = { "id": options.iri(this.name, this), "type": "NodeConstraint" } // Calling program encouraged to add xmlPrimitiveType attributes. if (this.xmlPrimitiveType) { ret.datatype = this.xmlPrimitiveType } else { ret.nodeKind = 'Literal' } // Should they also add facets? if (options.annotations) { let toAdd = options.annotations(this) if (toAdd && toAdd.length) { ret.annotations = toAdd } } return ret } } class Classifier extends Packagable { constructor (id, references, name, generalizations, properties, isAbstract, parent, comments) { super(id, references, name, parent, comments) Object.assign(this, { generalizations, properties, isAbstract }) } diffs (other, render = s => s, seen = []) { if (revisiting(seen, this, other)) { return [] } if (this.id !== other.id) { return [render(this.rtti() + ' doesn\'t match id ' + other.id)] } return super.diffs(other, render, seen) .concat(objDiffs(hashById(this.generalizations), hashById(other.generalizations), render, seen)) .concat(objDiffs(hashById(this.properties), hashById(other.properties), render, seen)) .concat(this.aggregation !== other.aggregation ? [ render(this.rtti() + ' aggregation:' + other.aggregation + ' doesn\'t match ' + this.aggregation) ] : []) } update (from, to) { let idx = this.generalizations.indexOf(from) if (idx === -1) { throw Error('update package: ' + from.id + ' not found in elements') } this.generalizations[idx] = to } remove (missingElements) { this.properties.forEach( prop => prop.remove(missingElements) ) super.remove(missingElements) } render () { let ret = $('<div/>').addClass('uml class ' + EXPANDED) let renderTitle = _ => [ $('<span/>').text(this.name).addClass('name') ].concat((this.generalizations || []).reduce( (acc, gen) => acc.concat([' ⊃', gen.summarize()]), [] )) let packages = renderElement(renderTitle, this.properties, property => { return property.renderProp() }, 'class') ret.append(packages) return ret } summarize () { let expandPackages = $('<img/>', { src: 'plusbox.gif' }) let elements = $('<ul/>') let packages = $('<span/>').addClass('uml class object').append( expandPackages, $('<span/>').text('class').addClass('type class'), $('<span/>').text(this.name).addClass('name'), $('<span/>').text(this.properties.length).addClass('length'), elements ).addClass(COLLAPSED).on('click', evt => { if (packages.hasClass(COLLAPSED)) { elements.append(this.properties.map( elt => $('<li/>').append(elt.renderProp()) )) packages.removeClass(COLLAPSED).addClass(EXPANDED) expandPackages.attr('src', 'minusbox.gif') } else { elements.empty() packages.removeClass(EXPANDED).addClass(COLLAPSED) expandPackages.attr('src', 'plusbox.gif') } return false }) return packages } toShExJ (parents = [], options = {}) { let shape = { "type": "Shape" } if (options.closedShapes) { shape.closed = true } if ('generalizations' in this && this.generalizations.length > 0) { shape.extends = this.generalizations.map(c => options.iri(c.name)) } let ret = { "id": options.iri(this.name, this), "type": "ShapeDecl", "abstract": this.isAbstract, "shapeExpr": shape } if (this.properties.length > 0) { let conjuncts = this.properties.map( p => p.propToShExJ(options) ) if (conjuncts.length === 1) { shape.expression = conjuncts[0] } else { shape.expression = { "type": "EachOf", "expressions": conjuncts } } } if (options.annotations) { let toAdd = options.annotations(this) if (toAdd && toAdd.length) { shape.annotations = toAdd } } return ret } } class Class extends Classifier { constructor (id, references, name, generalizations, properties, isAbstract, parent, comments) { super(id, references, name, generalizations, properties, isAbstract, parent, comments) Object.assign(this, { get rtti () { return 'Class' } }) } } class Datatype extends Classifier { constructor (id, references, name, generalizations, properties, isAbstract, parent, comments) { super(id, references, name, generalizations, properties, isAbstract, parent, comments) Object.assign(this, { get rtti () { return 'Datatype' } }) } } class Property { constructor (id, inClassifier, name, type, lower, upper, association, aggregation, comments) { Object.assign(this, { get rtti () { return 'Property' }, id, inClassifier, name, type, lower, upper, association, aggregation }) if (comments && comments.length) { this.comments = comments } } diffs (other, render = s => s, seen = []) { if (revisiting(seen, this, other)) { return [] } let topic = 'Property ' + this.id if (this.id !== other.id) { return [render(topic + ' doesn\'t match id ' + other.id)] } return testRTTI(topic, this, other, render) .concat(this.inClassifier.diffs(other.inClassifier, render, seen).map( d => render(topic + d) )) .concat(this.name !== other.name ? [ render(topic + ' name:' + other.name + ' doesn\'t match ' + this.name) ] : []) // This takes forever for even a small-ish model. .concat(this.type.diffs(other.type, render, seen).map( d => render(topic + d) )) .concat(this.lower !== other.lower ? [ render(topic + ' lower:' + other.lower + ' doesn\'t match ' + this.lower) ] : []) .concat(this.upper !== other.upper ? [ render(topic + ' upper:' + other.upper + ' doesn\'t match ' + this.upper) ] : []) .concat(this.assocation ? this.assocation.diffs(other.assocation, render, seen).map( d => render(topic + d) ) : []) .concat(this.aggregation !== other.aggregation ? [ render(topic + ' aggregation:' + other.aggregation + ' doesn\'t match ' + this.aggregation) ] : []) } update (from, to) { if (this.type !== from) { throw Error('update property: ' + from.id + ' not property type') } this.type = to } remove (missingElements) { let idx = this.type.references.indexOf(this) if (idx === -1) { throw Error('property type ' + this.type.id + ' does not list ' + this.id + ' in references') } this.type.references.splice(idx, 1) // detach prop from references. idx = this.inClassifier.properties.indexOf(this) if (idx === -1) { throw Error('Property ' + this.id + ' does not appear in Class ' + this.inClassifier.id) } this.inClassifier.properties.splice(idx, 1) // detach prop from references. } renderProp () { return $('<span/>').append( this.name, this.type.summarize() ) } propToShExJ (options) { let valueExpr = this.type.rtti === 'PrimitiveType' && this.type.external === true ? !modelOptions.anyURIasDataProperty && this.type.name === XSD + 'anyURI' ? { "type": "NodeConstraint", "nodeKind": 'iri' } : { "type": "NodeConstraint", "datatype": this.type.name } : options.iri(this.type.name, this) let ret = { "type": "TripleConstraint", "predicate": options.iri(this.name, this), "valueExpr": valueExpr } if (this.lower !== undefined) { ret.min = parseInt(this.lower) } if (this.upper !== undefined) { ret.max = this.upper === '*' ? -1 : parseInt(this.upper) } if (options.annotations) { let toAdd = options.annotations(this) if (toAdd && toAdd.length) { ret.annotations = toAdd } } return ret } } class Import { constructor (id, target, reference) { Object.assign(this, { get rtti () { return 'Import' }, id, target, reference }) } diffs (other, render = s => s, seen = []) { if (revisiting(seen, this, other)) { return [] } let topic = 'Import ' + this.id return testRTTI(topic, this, other, render) .concat(this.target.diffs(other.target, render, seen)) .concat(this.reference.diffs(other.reference, render, seen)) } update (from, to) { if (this.target !== from) { throw Error('update import: ' + from.id + ' not import type') } this.target = to } toShExJ (parents = [], options = {}) { return [] } render () { let ret = $('<div/>').addClass('uml model ' + EXPANDED) ret.append( $('<span/>').text('import').addClass('type import'), '→', this.target.render()) return ret } } class MissingElement { constructor (id, references = []) { Object.assign(this, { get rtti () { return 'MissingElement' }, id, references }) } diffs (other, render = s => s, seen = []) { if (revisiting(seen, this, other)) { return [] } let topic = 'MissingElement ' + this.id return testRTTI(topic, this, other, render) .concat(objDiffs(hashById(this.references), hashById(other.references), render, seen)) } render () { return $('<span/>').addClass('uml missing').append( '☣', $('<span/>').text('missing').addClass('type missing'), $('<span/>').text(this.id).addClass('name') ) } summarize () { return $('<span/>').addClass('uml missing').append( $('<span/>').text('missing').addClass('type missing'), $('<span/>').text(this.id).addClass('name') ) } toShExJ (parents = [], options = {}) { console.warn('toShExJ: no definition for ' + this.id + ' referenced by ' + this.references.map( ref => ref.id ).join(', ')) return [] } } function fromJSON (from, options = {}) { if (!options.missing) { options.missing = (obj, key, target) => { throw Error(obj.id + '[' + key + '] references unknown object ' + target) } } let M = this from = JSON.parse(from) let objs = {} function makeObjs (obj) { if (!!obj && typeof obj !== 'string') { if (obj.id) { objs[obj.id] = new M[obj.rtti]() } Object.keys(obj).forEach(k => { makeObjs(obj[k]) }) } } makeObjs(from) let myMissing = { } function populate (obj) { Object.keys(obj).forEach(k => { let v = obj[k] if (!!v && typeof v !== 'string') { populate(v) if (obj.rtti === 'Property' && k === 'type' && obj.external) { let prop = objs[obj.id] if (v._idref in objs) { objs[v._idref].references.push(prop) } else { objs[v._idref] = new M.PrimitiveType(v._idref, [prop], v._idref, prop, []) } } if (v._idref) { if (!(v._idref in objs)) { myMissing[v._idref] = objs[v._idref] = options.missing(obj, k, v._idref) } if (v._idref in myMissing) { myMissing[v._idref].references.push(objs[obj.id] || obj) } v = obj[k] = objs[v._idref] } if (v.id) { obj[k] = Object.assign(objs[v.id], v) } } }) } populate(from) if (from.id) { from = Object.assign(objs[from.id], from) } return from // Below is an attempt to use a single pass in circular-json's parse callback. // It results in an incomplete substitution, possibly because target objects // get replaced before substitution. let needed = { } let known = { } let ret = jsonCycles.parse(from, function (key, value) { let references = { 'Model': ['elements'], 'Package': ['references', 'parent', 'elements'], 'Import': ['target', 'reference'], 'Property': ['type', 'inClassifier'], 'Enumeration': ['references', 'parent'], 'PrimitiveType': ['references', 'parent'], 'Class': ['references', 'parent', 'generalizations', 'properties'] } let keys = references[this.rtti] || [] // if (this.rtti === 'Property' && key === 'type' || // this.rtti === 'Class' && key === 'properties' || // this.rtti === 'pak1' && key === 'elements' || // this.rtti === 'Model' && key === 'elements') { // console.log('this:', this, '\n', 'key:', key, '\n', 'value:', value, '\n') // } if (keys.indexOf(key) !== -1) { if (typeof value === 'object' && value.constructor === Array) { return value.map( (ent, idx) => resolve(value, idx, ent) ) } else { return resolve(this, key, value) } } return value function resolve (obj, key, value) { let idref = value._idref let id = value.id if (idref) { if (idref in known) { value = known[idref] } else { if (!(idref in needed)) { needed[idref] = [] } else if (needed.idref.length === 0) { throw Error('it seems ' + idref + ' was previously resolved') } needed[idref].push({ obj:obj, key, idref: idref }) } } else { if (id in known) { throw Error('duplicate definition of ' + id) } known[id] = value // known[id] = Object.assign(new UmlModel[value.rtti], value) // Object.keys(needed).forEach(nid => { // let nz = needed[nid]; // nz.forEach(n => { // if (n.obj === value) { // n.obj = known[id] // // console.log(nid, n) // } // }) // }) // value = known[id] // if (id in needed) { // needed[id].forEach(n => { // n.obj[n.key] = value // }) // // could just delete needed[id] but curious about resolutions // needed[id].length = 0 // } } return value } }) // ret = known[ret.id] = Object.assign(new UmlModel[ret.rtti], ret) ret = known[ret.id] = ret // trim to just id=ret.id Object.keys(needed).forEach(id => { let nz = needed[id] if (!(id in known)) { throw Error('no definition for ' + id + ' needed in ' + nz.length + ' place(s)') } nz.forEach(n => { n.obj[n.key] = known[id] }) }) /* let ret = jsonCycles.parse(JSON.stringify(j), function (key, value) { let references = { 'Model': ['elements'], 'Package': ['references', 'parent', 'elements'], 'Import': ['target', 'reference'], 'Property': ['type', 'inClassifier'], 'Enumeration': ['references', 'parent'], 'PrimitiveType': ['references', 'parent'], 'Class': ['references', 'parent', 'generalizations', 'properties'] } let keys = references[this.rtti] || [] console.log('this:', this, '\n', 'key:', key, '\n', 'value:', value, '\n') if (keys.indexOf(key) !== -1) { if (typeof value === 'object' && value.constructor === Array) { return value.map( (ent, idx) => resolve(value, idx, ent) ) } else { return resolve(this, key, value) } } return value function resolve (obj, key, value) { let idref = value._idref let id = value.id if (idref) { if (idref in known) { value = known[idref] } else { if (!(idref in needed)) { needed[idref] = [] } else if (needed.idref.length === 0) { throw Error('it seems ' + idref + ' was previously resolved') } needed[idref].push({ obj:obj, key, idref: idref }) } } else { if (id in known) { throw Error('duplicate definition of ' + id) } // known[id] = Object.assign(new UmlModel[value.rtti], value) known[id] = new UmlModel[value.rtti] Object.keys(value).forEach(k => { known[id][k] = value[k] }) Object.keys(needed).forEach(nid => { let nz = needed[nid]; nz.forEach(n => { if (n.obj === value) { n.obj = known[id] // console.log(nid, n) } }) }) value = known[id] if (id in needed) { needed[id].forEach(n => { n.obj[n.key] = value }) // could just delete needed[id] but curious about resolutions needed[id].length = 0 } } return value } }) ret = known[ret.id] = Object.assign(new UmlModel[ret.rtti], ret) // trim to just id=ret.id Object.keys(needed).forEach(id => { let nz = needed[id] if (!(id in known)) { throw Error('no definition for ' + id + ' needed in ' + nz.length + ' place(s)') } nz.forEach(n => { n.obj[n.key] = known[id] }) }) */ return ret } function fromJSON999 (obj) { let packages = {} let enums = {} let classes = {} let datatypes = {} let primitiveTypes = {} let associations = {} let imports = {} let missingElements = {} let ret = new UmlModel.Model( xmiGraph.source, null, missingElements ) ret.elements = Object.keys(xmiGraph.packageHierarchy.roots).map( packageId => createPackage(packageId, ret) ) return ret function mapElementByXmiReference (xmiRef, reference) { switch (xmiRef.type) { case 'import': return followImport(xmiRef.id, reference) case 'package': return createPackage(xmiRef.id, reference) case 'enumeration': return createEnumeration(xmiRef.id, reference) case 'primitiveType': return createPrimitiveType(xmiRef.id, reference) case 'class': return createClass(xmiRef.id, reference) default: throw Error('mapElementByXmiReference: unknown reference type in ' + JSON.stringify(xmiRef)) } } function followImport (importId, reference) { if (importId in imports) { throw Error('import id "' + importId + '" already used for ' + JSON.stringify(imports[importId])) // imports[importId].references.push(reference) // return imports[importId] } const importRecord = xmiGraph.imports[importId] // let ref = createdReferencedValueType(importRecord.idref) // let ret = imports[importId] = new UmlModel.Import(importId, ref) let ret = imports[importId] = new UmlModel.Import(importId, null, reference) ret.target = createdReferencedValueType(importRecord.idref, ret) return ret // imports[importId] = createdReferencedValueType(importRecord.idref) // imports[importId].importId = importId // write down that it's an import for round-tripping // return imports[importId] } function createdReferencedValueType (target, reference) { if (target in xmiGraph.packages) { return createPackage(target, reference) } if (target in xmiGraph.enums) { return createEnumeration(target, reference) } if (target in xmiGraph.primitiveTypes) { return createPrimitiveType(target, reference) } if (target in xmiGraph.classes) { return createClass(target, reference) } if (target in xmiGraph.datatypes) { return createDatatype(target, reference) } return missingElements[target] = createMissingElement(target, reference) } function mapElementByIdref (propertyRecord, reference) { if (propertyRecord.href) { if (propertyRecord.href in primitiveTypes) { primitiveTypes[propertyRecord.href].references.push(reference) return primitiveTypes[propertyRecord.href] } return primitiveTypes[propertyRecord.href] = new UmlModel.PrimitiveType(propertyRecord.href, [reference], propertyRecord.href, null, propertyRecord.comments) } return createdReferencedValueType(propertyRecord.idref, reference) } function createPackage (packageId, reference) { if (packageId in packages) { throw Error('package id "' + packageId + '" already used for ' + JSON.stringify(packages[packageId])) } const packageRecord = xmiGraph.packages[packageId] let ret = packages[packageId] = new UmlModel.Package(packageId, reference, packageRecord.name, null, reference, packageRecord.comments) ret.elements = packageRecord.elements.map( xmiReference => mapElementByXmiReference(xmiReference, ret) ) return ret } function createEnumeration (enumerationId, reference) { if (enumerationId in enums) { enums[enumerationId].references.push(reference) return enums[enumerationId] } const enumerationRecord = xmiGraph.enums[enumerationId] return enums[enumerationId] = new UmlModel.Enumeration(enumerationId, [reference], enumerationRecord.name, enumerationRecord.values, reference, enumerationRecord.comments) } function createPrimitiveType (primitiveTypeId, reference) { if (primitiveTypeId in primitiveTypes) { primitiveTypes[primitiveTypeId].references.push(reference) return primitiveTypes[primitiveTypeId] } const primitiveTypeRecord = xmiGraph.primitiveTypes[primitiveTypeId] return primitiveTypes[primitiveTypeId] = new UmlModel.PrimitiveType(primitiveTypeId, [reference], primitiveTypeRecord.name, reference, primitiveTypeRecord.comments) } function createClass (classId, reference) { if (classId in classes) { classes[classId].references.push(reference) return classes[classId] } const classRecord = xmiGraph.classes[classId] let ret = classes[classId] = new UmlModel.Class(classId, [reference], classRecord.name, classRecord.superClasses, [], classRecord.isAbstract, reference, classRecord.comments) // avoid cycles like Identifiable { basedOn Identifiable } ret.properties = classRecord.properties.map( propertyRecord => createProperty(propertyRecord, ret)) return ret } function createMissingElement (missingElementId, reference) { if (missingElementId in missingElements) { missingElements[missingElementId].references.push(reference) return missingElements[missingElementId] } return missingElements[missingElementId] = new UmlModel.MissingElement(missingElementId, [reference]) } function createProperty (propertyRecord, inClassifier) { let ret = new UmlModel.Property(propertyRecord.id, inClassifier, propertyRecord.name, null, // so we can pass the Property to unresolved types propertyRecord.lower, propertyRecord.upper, propertyRecord.association, propertyRecord.aggregation, propertyRecord.comments) ret.type = mapElementByIdref(propertyRecord, ret) return ret } } function toJSON (term, options = { fixed: 0}) { return jsonCycles.stringify(term, function (key, value) { let references = { 'Package': ['references', 'parent'], 'Import': ['target', 'reference'], 'Property': ['type', 'inClassifier'], 'Enumeration': ['references', 'parent'], 'PrimitiveType': ['references', 'parent'], 'Class': ['references', 'parent', 'generalizations'] } let keys = references[this.rtti] || [] if (keys.indexOf(key) !== -1) { if (typeof value === 'object' && value.constructor === Array) { return value.map( ent => { ++options.fixed return { _idref: ent.id } } ) } else { ++options.fixed return { _idref: this[key].id } } } return value }, 2, true) } class Point { constructor (x, y) { Object.assign(this, {x, y}) } foo () { return 'foo' } bar () { return 'bar' } } return UmlModel.singleton = { Model, Property, Class, Package, Enumeration, PrimitiveType, Import, MissingElement, // Association, Aggregation: { shared: AGGREGATION_shared, composite: AGGREGATION_composite }, fromJSON, toJSON, Point } } module.exports = UmlModel