├── .github └── workflows │ ├── CI.yml │ └── CODE_SCANNING.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── descriptor.md ├── eslint.config.js ├── lib ├── base.js ├── descriptor-builder.js ├── factory.js ├── index.js ├── moddle.js ├── ns.js ├── properties.js ├── registry.js └── types.js ├── package-lock.json ├── package.json ├── resources └── schema │ └── moddle.json ├── rollup.config.js └── test ├── expect.js ├── fixtures └── model │ ├── datatype-external.json │ ├── datatype.json │ ├── extension │ ├── base.json │ └── custom.json │ ├── meta.json │ ├── noalias.json │ ├── properties-extended.json │ ├── properties.json │ ├── redefines │ └── base.json │ ├── replaces │ └── base.json │ ├── schema-meta.json │ ├── self-extend.json │ └── shadow.json ├── helper.js ├── integration └── distro.cjs ├── matchers.js └── spec ├── extension.js ├── meta.js ├── moddle.js ├── ns.js ├── properties.js ├── schema.js └── types.js /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | jobs: 4 | build: 5 | 6 | strategy: 7 | matrix: 8 | os: [ ubuntu-latest ] 9 | node-version: [ 20 ] 10 | 11 | runs-on: ${{ matrix.os }} 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | cache: 'npm' 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Build 24 | run: npm run all -------------------------------------------------------------------------------- /.github/workflows/CODE_SCANNING.yml: -------------------------------------------------------------------------------- 1 | name: CODE_SCANNING 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | pull_request: 7 | branches: [ master, develop ] 8 | paths-ignore: 9 | - '**/*.md' 10 | 11 | jobs: 12 | codeql_build: 13 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 14 | runs-on: ubuntu-latest 15 | 16 | permissions: 17 | # required for all workflows 18 | security-events: write 19 | 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | # Initializes the CodeQL tools for scanning. 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v2 27 | with: 28 | languages: javascript 29 | config: | 30 | paths-ignore: 31 | - '**/test' 32 | 33 | - name: Perform CodeQL Analysis 34 | uses: github/codeql-action/analyze@v2 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | tmp/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to [moddle](https://github.com/bpmn-io/moddle) are documented here. We use [semantic versioning](http://semver.org/) for releases. 4 | 5 | ## Unreleased 6 | 7 | ___Note:__ Yet to be released changes appear here._ 8 | 9 | ## 7.2.0 10 | 11 | * `FEAT`: expose moddle schema via well-known folder ([#64](https://github.com/bpmn-io/moddle/pull/64)) 12 | * `DOCS`: simplify `$schema` related documentation 13 | 14 | ## 7.1.0 15 | 16 | * `FEAT`: provide a JSON schema to validate moddle schema ([#62](https://github.com/bpmn-io/moddle/issues/62)) 17 | 18 | ## 7.0.0 19 | 20 | * `FEAT`: add `exports` configuration 21 | * `FEAT`: add moddle descriptor JSON schema ([#57](https://github.com/bpmn-io/moddle/pull/57)) 22 | * `FIX`: remove broken `main` configuration 23 | * `CHORE`: drop `UMD` distribution 24 | * `DEPS`: update to `min-dash@4.2.1` 25 | 26 | ### Breaking Changes 27 | 28 | * UMD distribution no longer bundled. The module is now available as an ES module. 29 | 30 | ## 6.2.3 31 | 32 | * `FIX`: mark accessors as non-enumerable ([#55](https://github.com/bpmn-io/moddle/pull/55)) 33 | 34 | ## 6.2.2 35 | 36 | * `FIX`: add accessors for elements created by `createAny` ([#54](https://github.com/bpmn-io/moddle/pull/54)) 37 | 38 | ## 6.2.1 39 | 40 | * `FIX`: allow self-extension using local name ([#52](https://github.com/bpmn-io/moddle/pull/52)) 41 | 42 | ## 6.2.0 43 | 44 | * `FEAT`: add ability to configure moddle ([#48](https://github.com/bpmn-io/moddle/pull/48)) 45 | * `FEAT`: add ability to explicitly mark property as global ([#48](https://github.com/bpmn-io/moddle/pull/48)) 46 | * `FEAT`: add `strict` mode / ability to log unknown property access ([#48](https://github.com/bpmn-io/moddle/pull/48)) 47 | 48 | ## 6.1.0 49 | 50 | * `FEAT`: improve error thrown on trait introspection ([#38](https://github.com/bpmn-io/moddle/issues/38), [#46](https://github.com/bpmn-io/moddle/pull/46)) 51 | * `FIX`: correctly handle `inherits` flag with multiple parents ([#47](https://github.com/bpmn-io/moddle/pull/47)) 52 | 53 | ## 6.0.0 54 | 55 | * `DEPS`: bump to `min-dash@4` 56 | * `CHORE`: turn into ES module 57 | 58 | ## 5.0.4 59 | 60 | * `FIX`: guard against `ModdleElement#set` miss-use ([#43](https://github.com/bpmn-io/moddle/issues/43)) 61 | 62 | ## 5.0.3 63 | 64 | * `FIX`: use getters for read-only properties ([#40](https://github.com/bpmn-io/moddle/pull/40)) 65 | * `CHORE`: add prepare script ([#33](https://github.com/bpmn-io/moddle/pull/33))) 66 | 67 | ## 5.0.2 68 | 69 | * `FIX`: make `Any` type `$instanceOf` member non-enumerable 70 | 71 | ## 5.0.1 72 | 73 | * `CHORE`: cleanup pre-built distribution 74 | 75 | ## 5.0.0 76 | 77 | * `CHORE`: expose `{ Moddle }` and utilities 78 | * `CHORE`: provide pre-packaged distribution 79 | 80 | ### Breaking Changes 81 | 82 | * We expose `Moddle` as a named export now. 83 | * We do not publish `lib` folder anymore, destructure the provided default export. 84 | * No need for `esm` to consume the library anymore. 85 | 86 | ## 4.1.0 87 | 88 | * `CHORE`: bump utility toolbelt 89 | 90 | ## 4.0.0 91 | 92 | ### Breaking Changes 93 | 94 | * `FEAT`: migrate to ES modules. Use `esm` or a ES module aware transpiler to consume this library. 95 | 96 | ## 3.0.0 97 | 98 | * `FEAT`: drop lodash in favor of [min-dash](https://github.com/bpmn-io/min-dash) 99 | 100 | ## ... 101 | 102 | Check `git log` for earlier history. 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present Camunda Services GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moddle 2 | 3 | [![CI](https://github.com/bpmn-io/moddle/workflows/CI/badge.svg)](https://github.com/bpmn-io/moddle/actions?query=workflow%3ACI) 4 | 5 | A utility library for working with meta-model based data structures. 6 | 7 | 8 | ## What is it good for? 9 | 10 | [moddle](https://github.com/bpmn-io/moddle) offers you a concise way to define [meta models](https://en.wikipedia.org/wiki/Metamodeling) in JavaScript. You can use these models to consume documents, create model elements, and perform model validation. 11 | 12 | 13 | ### Define a schema 14 | 15 | You start by creating a [moddle schema](./docs/descriptor.md). It is a [JSON](http://json.org/) file which describes types, their properties, and relationships: 16 | 17 | ```json 18 | { 19 | "$schema": "https://unpkg.com/moddle/resources/schema/moddle.json", 20 | "name": "Cars", 21 | "uri": "http://cars", 22 | "prefix": "c", 23 | "types": [ 24 | { 25 | "name": "Base", 26 | "properties": [ 27 | { "name": "id", "type": "String", "isAttr": true } 28 | ] 29 | }, 30 | { 31 | "name": "Root", 32 | "superClass": [ "Base" ], 33 | "properties": [ 34 | { "name": "cars", "type": "Car", "isMany": true } 35 | ] 36 | }, 37 | { 38 | "name": "Car", 39 | "superClass": [ "Base" ], 40 | "properties": [ 41 | { "name": "name", "type": "String", "isAttr": true, "default": "No Name" }, 42 | { "name": "power", "type": "Integer", "isAttr": true }, 43 | { "name": "similar", "type": "Car", "isMany": true, "isReference": true }, 44 | { "name": "trunk", "type": "Element", "isMany": true } 45 | ] 46 | } 47 | ] 48 | } 49 | ``` 50 | 51 | You may attach the provided [JSON schema](./resources/schema/moddle.json) to get your moddle descriptor validated by code editor. 52 | 53 | 54 | ### Instantiate moddle 55 | 56 | You can instantiate a moddle instance with a set of defined schemas: 57 | 58 | ```javascript 59 | import { Moddle } from 'moddle'; 60 | 61 | var cars = new Moddle([ carsJSON ]); 62 | ``` 63 | 64 | 65 | ### Create objects 66 | 67 | Use a [moddle](https://github.com/bpmn-io/moddle) instance to create objects of your defined types: 68 | 69 | ```javascript 70 | var taiga = cars.create('c:Car', { name: 'Taiga' }); 71 | 72 | console.log(taiga); 73 | // { $type: 'c:Car', name: 'Taiga' }; 74 | 75 | 76 | var cheapCar = cars.create('c:Car'); 77 | 78 | console.log(cheapCar.name); 79 | // "No Name" 80 | 81 | 82 | // really? 83 | cheapCar.get('similar').push(taiga); 84 | ``` 85 | 86 | 87 | ### Introspect things 88 | 89 | Then again, given the knowledge [moddle](https://github.com/bpmn-io/moddle) has, you can perform deep introspection: 90 | 91 | ```javascript 92 | var carDescriptor = cheapCar.$descriptor; 93 | 94 | console.log(carDescriptor.properties); 95 | // [ { name: 'id', type: 'String', ... }, { name: 'name', type: 'String', ...} ... ] 96 | ``` 97 | 98 | 99 | ### Access extensions 100 | 101 | moddle is friendly towards extensions and keeps unknown _any_ properties around: 102 | 103 | ```javascript 104 | taiga.set('specialProperty', 'not known to moddle'); 105 | 106 | console.log(taiga.get('specialProperty')); 107 | // 'not known to moddle' 108 | ``` 109 | 110 | It also allows you to create _any_ elements for namespaces that you did not explicitly define: 111 | 112 | ```javascript 113 | var screwdriver = cars.createAny('tools:Screwdriver', 'http://tools', { 114 | make: 'ScrewIt!' 115 | }); 116 | 117 | car.trunk.push(screwdriver); 118 | ``` 119 | 120 | 121 | ### There is more 122 | 123 | Have a look at our [test coverage](https://github.com/bpmn-io/moddle/blob/master/test/spec) to learn about everything that is currently supported. 124 | 125 | 126 | ## Resources 127 | 128 | * [Issues](https://github.com/bpmn-io/moddle/issues) 129 | * [Examples](https://github.com/bpmn-io/moddle/tree/master/test/fixtures/model) 130 | * [Documentation](https://github.com/bpmn-io/moddle/tree/master/docs) 131 | 132 | 133 | ## Related 134 | 135 | * [moddle-xml](https://github.com/bpmn-io/moddle-xml): read xml documents based on moddle descriptors 136 | 137 | 138 | ## License 139 | 140 | MIT 141 | -------------------------------------------------------------------------------- /docs/descriptor.md: -------------------------------------------------------------------------------- 1 | # moddle Descriptor 2 | 3 | The moddle descriptor is a JSON file that describes elements, their properties and relationships. 4 | 5 | 6 | ## Package Definition 7 | 8 | The root of a descriptor file is a package definition. 9 | 10 | ```json 11 | { 12 | "name": "SamplePackage", 13 | "prefix": "s", 14 | "types": [], 15 | "enumerations": [] 16 | } 17 | ``` 18 | 19 | 20 | #### Notes 21 | 22 | The `prefix` uniquely identifies elements in a package if more multiple packages are in place. 23 | 24 | The `types` collection contains all known types. 25 | 26 | The `enumerations` and `associations` properties are reserved for future use. 27 | 28 | 29 | ## Type Definition 30 | 31 | A type is a moddle element with a (package-) unique name and a list of properties. 32 | 33 | ```json 34 | { 35 | "name": "Base", 36 | "properties": [ 37 | { "name": "id", "type": "Number" }, 38 | ... 39 | ] 40 | } 41 | ``` 42 | 43 | 44 | ### Inheritance 45 | 46 | Types can inherit from one or more super types by specifying the `superClass` property. 47 | 48 | ```json 49 | { 50 | "name": "Root", 51 | "superClass": [ "Base" ] 52 | } 53 | ``` 54 | 55 | By inheriting from a super type, a type inherits all properties declared in the super type hierarchy. 56 | 57 | Inherited properties will appear before own properties based on the order they are declared in the type hierarchy. 58 | 59 | 60 | ### Extending existing Types 61 | 62 | Some meta-models require it to plug-in new properties that to certain existing model elements. This can be acomplished using the `extends` field. Consider the following type definition: 63 | 64 | ```json 65 | { 66 | "name": "BetterRoot", 67 | "extends": [ "Root" ], 68 | "properties": [ 69 | { "name": "id", "type": "Number" } 70 | ] 71 | } 72 | ``` 73 | 74 | With this model definition, every instance of `Root` will automatically have another property `BetterRoot#id` added. At the same time, instances of root will be instances of `BetterRoot`, too. 75 | 76 | This extension is _implicit_ when compared to [inheritance](#inheritance). In the inheritance case one would need to instantiate `BetterRoot`, to actually get the new property `id`. Extending allows us to simply instantiate `Root` with the additional property defined for it. 77 | 78 | 79 | ## Property Definition 80 | 81 | A property has a name, a type as well as a number of additional qualifiers and is added to a types `properties` list. 82 | 83 | ```json 84 | { 85 | "name": "stringProperty", 86 | "type": "String" 87 | } 88 | ``` 89 | 90 | The `type` attribute may reference simple types such as `String`, `Boolean`, `Integer` or `Real` or any custom defined type. 91 | 92 | 93 | ### Qualifiers 94 | 95 | Qualifiers can be used to further define a property. 96 | 97 | | Qualifier | Values | Description | 98 | | ------------- | ------------- | ----- | 99 | | `isMany=false` | `Boolean` | collection (i.e. list like) property | 100 | | `isReference=false` | `Boolean` | reference to another object via its `id` property | 101 | | `default` | simple type | the default value to set if non is defined | 102 | | `redefines` | `String` (identifier) | redefines the property inherited from a super type, overriding `name`, `type` and qualifiers | 103 | 104 | 105 | ## Cross Package Referencing 106 | 107 | Across packages, elements may be referenced via `packagePrefix:packageLocalName`. 108 | 109 | 110 | ### Example 111 | 112 | Given we got two packages, a _base_ package, and a _domain_ package that builds on top of it. 113 | 114 | ```json 115 | { 116 | "$schema": "https://unpkg.com/moddle/resources/schema/moddle.json", 117 | "name": "BasePackage", 118 | "prefix": "b", 119 | "types": [ 120 | { "name": "Base" }, 121 | { 122 | "name": "BaseWithId", 123 | "superClass": [ "Base" ], 124 | "properties": [ { "name": "id", "type": "String" } ] 125 | } 126 | ] 127 | } 128 | ``` 129 | 130 | The domain package may define its own types on top of the base package by referencing properties and types defined in the base package via their name, prefixed with `b:`. 131 | 132 | ```json 133 | { 134 | "$schema": "https://unpkg.com/moddle/resources/schema/moddle.json", 135 | "name": "DomainPackage", 136 | "prefix": "d", 137 | "types": [ 138 | { 139 | "name": "Base", 140 | "superClass": [ "b:BaseWithId" ], 141 | "properties": [ 142 | { 143 | "name": "id", 144 | "type": "Integer", 145 | "redefines": "b:BaseWithId#id" 146 | } 147 | ] 148 | }, 149 | { 150 | "name": "Root", 151 | "properties": [ 152 | { "name": "elements", "type": "b:Base", "isMany": true } 153 | ] 154 | } 155 | ] 156 | } 157 | ``` 158 | 159 | To instantiate the domain package as part of a moddle instance, the base package must be provided, too. 160 | 161 | 162 | ### External Links 163 | 164 | Valid locations for externally defined types and properties are 165 | 166 | * a types `superClass` attribute 167 | * a properties `type` attribute 168 | * a properties `redefines` attribute (e.g. to redefine a property inherited from an externally defined type) 169 | 170 | 171 | ## Serializing to XML 172 | 173 | Reading and writing XML from moddle is possible via [moddle-xml](https://github.com/bpmn-io/moddle-xml). It requires [additional meta-data](https://github.com/bpmn-io/moddle-xml/blob/master/docs/descriptor-xml.md) to be specified in your moddle descriptor. 174 | 175 | 176 | ## Validating the Schema 177 | 178 | A [JSON schema](../resources/schema/moddle.json) is available, include it via the `$schema` attribute in your moddle descriptor and it will be picked up by code editor: 179 | 180 | ```json 181 | { 182 | "$schema": "https://unpkg.com/moddle/resources/schema/moddle.json", 183 | ... 184 | } 185 | ``` 186 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import bpmnIoPlugin from 'eslint-plugin-bpmn-io'; 2 | 3 | const files = { 4 | lib: [ 5 | 'lib/**/*.js' 6 | ], 7 | test: [ 8 | 'test/**/*.js', 9 | 'test/**/*.cjs' 10 | ], 11 | ignored: [ 12 | 'dist' 13 | ] 14 | }; 15 | 16 | export default [ 17 | { 18 | 'ignores': files.ignored 19 | }, 20 | 21 | // build 22 | ...bpmnIoPlugin.configs.node.map(config => { 23 | 24 | return { 25 | ...config, 26 | ignores: files.lib 27 | }; 28 | }), 29 | 30 | // lib + test 31 | ...bpmnIoPlugin.configs.recommended.map(config => { 32 | 33 | return { 34 | ...config, 35 | files: files.lib 36 | }; 37 | }), 38 | 39 | // test 40 | ...bpmnIoPlugin.configs.mocha.map(config => { 41 | 42 | return { 43 | ...config, 44 | files: files.test 45 | }; 46 | }) 47 | ]; -------------------------------------------------------------------------------- /lib/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Moddle base element. 3 | */ 4 | export default function Base() { } 5 | 6 | Base.prototype.get = function(name) { 7 | return this.$model.properties.get(this, name); 8 | }; 9 | 10 | Base.prototype.set = function(name, value) { 11 | this.$model.properties.set(this, name, value); 12 | }; -------------------------------------------------------------------------------- /lib/descriptor-builder.js: -------------------------------------------------------------------------------- 1 | import { 2 | pick, 3 | assign, 4 | forEach, 5 | bind 6 | } from 'min-dash'; 7 | 8 | import { 9 | parseName as parseNameNs 10 | } from './ns.js'; 11 | 12 | 13 | /** 14 | * A utility to build element descriptors. 15 | */ 16 | export default function DescriptorBuilder(nameNs) { 17 | this.ns = nameNs; 18 | this.name = nameNs.name; 19 | this.allTypes = []; 20 | this.allTypesByName = {}; 21 | this.properties = []; 22 | this.propertiesByName = {}; 23 | } 24 | 25 | 26 | DescriptorBuilder.prototype.build = function() { 27 | return pick(this, [ 28 | 'ns', 29 | 'name', 30 | 'allTypes', 31 | 'allTypesByName', 32 | 'properties', 33 | 'propertiesByName', 34 | 'bodyProperty', 35 | 'idProperty' 36 | ]); 37 | }; 38 | 39 | /** 40 | * Add property at given index. 41 | * 42 | * @param {Object} p 43 | * @param {Number} [idx] 44 | * @param {Boolean} [validate=true] 45 | */ 46 | DescriptorBuilder.prototype.addProperty = function(p, idx, validate) { 47 | 48 | if (typeof idx === 'boolean') { 49 | validate = idx; 50 | idx = undefined; 51 | } 52 | 53 | this.addNamedProperty(p, validate !== false); 54 | 55 | var properties = this.properties; 56 | 57 | if (idx !== undefined) { 58 | properties.splice(idx, 0, p); 59 | } else { 60 | properties.push(p); 61 | } 62 | }; 63 | 64 | 65 | DescriptorBuilder.prototype.replaceProperty = function(oldProperty, newProperty, replace) { 66 | var oldNameNs = oldProperty.ns; 67 | 68 | var props = this.properties, 69 | propertiesByName = this.propertiesByName, 70 | rename = oldProperty.name !== newProperty.name; 71 | 72 | if (oldProperty.isId) { 73 | if (!newProperty.isId) { 74 | throw new Error( 75 | 'property <' + newProperty.ns.name + '> must be id property ' + 76 | 'to refine <' + oldProperty.ns.name + '>'); 77 | } 78 | 79 | this.setIdProperty(newProperty, false); 80 | } 81 | 82 | if (oldProperty.isBody) { 83 | 84 | if (!newProperty.isBody) { 85 | throw new Error( 86 | 'property <' + newProperty.ns.name + '> must be body property ' + 87 | 'to refine <' + oldProperty.ns.name + '>'); 88 | } 89 | 90 | // TODO: Check compatibility 91 | this.setBodyProperty(newProperty, false); 92 | } 93 | 94 | // validate existence and get location of old property 95 | var idx = props.indexOf(oldProperty); 96 | if (idx === -1) { 97 | throw new Error('property <' + oldNameNs.name + '> not found in property list'); 98 | } 99 | 100 | // remove old property 101 | props.splice(idx, 1); 102 | 103 | // replacing the named property is intentional 104 | // 105 | // * validate only if this is a "rename" operation 106 | // * add at specific index unless we "replace" 107 | // 108 | this.addProperty(newProperty, replace ? undefined : idx, rename); 109 | 110 | // make new property available under old name 111 | propertiesByName[oldNameNs.name] = propertiesByName[oldNameNs.localName] = newProperty; 112 | }; 113 | 114 | 115 | DescriptorBuilder.prototype.redefineProperty = function(p, targetPropertyName, replace) { 116 | 117 | var nsPrefix = p.ns.prefix; 118 | var parts = targetPropertyName.split('#'); 119 | 120 | var name = parseNameNs(parts[0], nsPrefix); 121 | var attrName = parseNameNs(parts[1], name.prefix).name; 122 | 123 | var redefinedProperty = this.propertiesByName[attrName]; 124 | if (!redefinedProperty) { 125 | throw new Error('refined property <' + attrName + '> not found'); 126 | } else { 127 | this.replaceProperty(redefinedProperty, p, replace); 128 | } 129 | 130 | delete p.redefines; 131 | }; 132 | 133 | DescriptorBuilder.prototype.addNamedProperty = function(p, validate) { 134 | var ns = p.ns, 135 | propsByName = this.propertiesByName; 136 | 137 | if (validate) { 138 | this.assertNotDefined(p, ns.name); 139 | this.assertNotDefined(p, ns.localName); 140 | } 141 | 142 | propsByName[ns.name] = propsByName[ns.localName] = p; 143 | }; 144 | 145 | DescriptorBuilder.prototype.removeNamedProperty = function(p) { 146 | var ns = p.ns, 147 | propsByName = this.propertiesByName; 148 | 149 | delete propsByName[ns.name]; 150 | delete propsByName[ns.localName]; 151 | }; 152 | 153 | DescriptorBuilder.prototype.setBodyProperty = function(p, validate) { 154 | 155 | if (validate && this.bodyProperty) { 156 | throw new Error( 157 | 'body property defined multiple times ' + 158 | '(<' + this.bodyProperty.ns.name + '>, <' + p.ns.name + '>)'); 159 | } 160 | 161 | this.bodyProperty = p; 162 | }; 163 | 164 | DescriptorBuilder.prototype.setIdProperty = function(p, validate) { 165 | 166 | if (validate && this.idProperty) { 167 | throw new Error( 168 | 'id property defined multiple times ' + 169 | '(<' + this.idProperty.ns.name + '>, <' + p.ns.name + '>)'); 170 | } 171 | 172 | this.idProperty = p; 173 | }; 174 | 175 | DescriptorBuilder.prototype.assertNotTrait = function(typeDescriptor) { 176 | 177 | const _extends = typeDescriptor.extends || []; 178 | 179 | if (_extends.length) { 180 | throw new Error( 181 | `cannot create <${ typeDescriptor.name }> extending <${ typeDescriptor.extends }>` 182 | ); 183 | } 184 | }; 185 | 186 | DescriptorBuilder.prototype.assertNotDefined = function(p, name) { 187 | var propertyName = p.name, 188 | definedProperty = this.propertiesByName[propertyName]; 189 | 190 | if (definedProperty) { 191 | throw new Error( 192 | 'property <' + propertyName + '> already defined; ' + 193 | 'override of <' + definedProperty.definedBy.ns.name + '#' + definedProperty.ns.name + '> by ' + 194 | '<' + p.definedBy.ns.name + '#' + p.ns.name + '> not allowed without redefines'); 195 | } 196 | }; 197 | 198 | DescriptorBuilder.prototype.hasProperty = function(name) { 199 | return this.propertiesByName[name]; 200 | }; 201 | 202 | DescriptorBuilder.prototype.addTrait = function(t, inherited) { 203 | 204 | if (inherited) { 205 | this.assertNotTrait(t); 206 | } 207 | 208 | var typesByName = this.allTypesByName, 209 | types = this.allTypes; 210 | 211 | var typeName = t.name; 212 | 213 | if (typeName in typesByName) { 214 | return; 215 | } 216 | 217 | forEach(t.properties, bind(function(p) { 218 | 219 | // clone property to allow extensions 220 | p = assign({}, p, { 221 | name: p.ns.localName, 222 | inherited: inherited 223 | }); 224 | 225 | Object.defineProperty(p, 'definedBy', { 226 | value: t 227 | }); 228 | 229 | var replaces = p.replaces, 230 | redefines = p.redefines; 231 | 232 | // add replace/redefine support 233 | if (replaces || redefines) { 234 | this.redefineProperty(p, replaces || redefines, replaces); 235 | } else { 236 | if (p.isBody) { 237 | this.setBodyProperty(p); 238 | } 239 | if (p.isId) { 240 | this.setIdProperty(p); 241 | } 242 | this.addProperty(p); 243 | } 244 | }, this)); 245 | 246 | types.push(t); 247 | typesByName[typeName] = t; 248 | }; -------------------------------------------------------------------------------- /lib/factory.js: -------------------------------------------------------------------------------- 1 | import { 2 | forEach, 3 | bind 4 | } from 'min-dash'; 5 | 6 | import Base from './base.js'; 7 | 8 | /** 9 | * A model element factory. 10 | * 11 | * @param {Moddle} model 12 | * @param {Properties} properties 13 | */ 14 | export default function Factory(model, properties) { 15 | this.model = model; 16 | this.properties = properties; 17 | } 18 | 19 | 20 | Factory.prototype.createType = function(descriptor) { 21 | 22 | var model = this.model; 23 | 24 | var props = this.properties, 25 | prototype = Object.create(Base.prototype); 26 | 27 | // initialize default values 28 | forEach(descriptor.properties, function(p) { 29 | if (!p.isMany && p.default !== undefined) { 30 | prototype[p.name] = p.default; 31 | } 32 | }); 33 | 34 | props.defineModel(prototype, model); 35 | props.defineDescriptor(prototype, descriptor); 36 | 37 | var name = descriptor.ns.name; 38 | 39 | /** 40 | * The new type constructor 41 | */ 42 | function ModdleElement(attrs) { 43 | props.define(this, '$type', { value: name, enumerable: true }); 44 | props.define(this, '$attrs', { value: {} }); 45 | props.define(this, '$parent', { writable: true }); 46 | 47 | forEach(attrs, bind(function(val, key) { 48 | this.set(key, val); 49 | }, this)); 50 | } 51 | 52 | ModdleElement.prototype = prototype; 53 | 54 | ModdleElement.hasType = prototype.$instanceOf = this.model.hasType; 55 | 56 | // static links 57 | props.defineModel(ModdleElement, model); 58 | props.defineDescriptor(ModdleElement, descriptor); 59 | 60 | return ModdleElement; 61 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | export { 2 | default as Moddle 3 | } from './moddle.js'; 4 | 5 | export { 6 | parseName as parseNameNS 7 | } from './ns.js'; 8 | 9 | export { 10 | isBuiltIn as isBuiltInType, 11 | isSimple as isSimpleType, 12 | coerceType 13 | } from './types.js'; -------------------------------------------------------------------------------- /lib/moddle.js: -------------------------------------------------------------------------------- 1 | import { 2 | isString, 3 | isObject, 4 | forEach, 5 | set 6 | } from 'min-dash'; 7 | 8 | import Factory from './factory.js'; 9 | import Registry from './registry.js'; 10 | import Properties from './properties.js'; 11 | 12 | import { 13 | parseName as parseNameNs 14 | } from './ns.js'; 15 | 16 | 17 | // Moddle implementation ///////////////////////////////////////////////// 18 | 19 | /** 20 | * @class Moddle 21 | * 22 | * A model that can be used to create elements of a specific type. 23 | * 24 | * @example 25 | * 26 | * var Moddle = require('moddle'); 27 | * 28 | * var pkg = { 29 | * name: 'mypackage', 30 | * prefix: 'my', 31 | * types: [ 32 | * { name: 'Root' } 33 | * ] 34 | * }; 35 | * 36 | * var moddle = new Moddle([pkg]); 37 | * 38 | * @param {Array} packages the packages to contain 39 | * 40 | * @param { { strict?: boolean } } [config] moddle configuration 41 | */ 42 | export default function Moddle(packages, config = {}) { 43 | 44 | this.properties = new Properties(this); 45 | 46 | this.factory = new Factory(this, this.properties); 47 | this.registry = new Registry(packages, this.properties); 48 | 49 | this.typeCache = {}; 50 | 51 | this.config = config; 52 | } 53 | 54 | 55 | /** 56 | * Create an instance of the specified type. 57 | * 58 | * @method Moddle#create 59 | * 60 | * @example 61 | * 62 | * var foo = moddle.create('my:Foo'); 63 | * var bar = moddle.create('my:Bar', { id: 'BAR_1' }); 64 | * 65 | * @param {String|Object} descriptor the type descriptor or name know to the model 66 | * @param {Object} attrs a number of attributes to initialize the model instance with 67 | * @return {Object} model instance 68 | */ 69 | Moddle.prototype.create = function(descriptor, attrs) { 70 | var Type = this.getType(descriptor); 71 | 72 | if (!Type) { 73 | throw new Error('unknown type <' + descriptor + '>'); 74 | } 75 | 76 | return new Type(attrs); 77 | }; 78 | 79 | 80 | /** 81 | * Returns the type representing a given descriptor 82 | * 83 | * @method Moddle#getType 84 | * 85 | * @example 86 | * 87 | * var Foo = moddle.getType('my:Foo'); 88 | * var foo = new Foo({ 'id' : 'FOO_1' }); 89 | * 90 | * @param {String|Object} descriptor the type descriptor or name know to the model 91 | * @return {Object} the type representing the descriptor 92 | */ 93 | Moddle.prototype.getType = function(descriptor) { 94 | 95 | var cache = this.typeCache; 96 | 97 | var name = isString(descriptor) ? descriptor : descriptor.ns.name; 98 | 99 | var type = cache[name]; 100 | 101 | if (!type) { 102 | descriptor = this.registry.getEffectiveDescriptor(name); 103 | type = cache[name] = this.factory.createType(descriptor); 104 | } 105 | 106 | return type; 107 | }; 108 | 109 | 110 | /** 111 | * Creates an any-element type to be used within model instances. 112 | * 113 | * This can be used to create custom elements that lie outside the meta-model. 114 | * The created element contains all the meta-data required to serialize it 115 | * as part of meta-model elements. 116 | * 117 | * @method Moddle#createAny 118 | * 119 | * @example 120 | * 121 | * var foo = moddle.createAny('vendor:Foo', 'http://vendor', { 122 | * value: 'bar' 123 | * }); 124 | * 125 | * var container = moddle.create('my:Container', 'http://my', { 126 | * any: [ foo ] 127 | * }); 128 | * 129 | * // go ahead and serialize the stuff 130 | * 131 | * 132 | * @param {String} name the name of the element 133 | * @param {String} nsUri the namespace uri of the element 134 | * @param {Object} [properties] a map of properties to initialize the instance with 135 | * @return {Object} the any type instance 136 | */ 137 | Moddle.prototype.createAny = function(name, nsUri, properties) { 138 | 139 | var nameNs = parseNameNs(name); 140 | 141 | var element = { 142 | $type: name, 143 | $instanceOf: function(type) { 144 | return type === this.$type; 145 | }, 146 | get: function(key) { 147 | return this[key]; 148 | }, 149 | set: function(key, value) { 150 | set(this, [ key ], value); 151 | } 152 | }; 153 | 154 | var descriptor = { 155 | name: name, 156 | isGeneric: true, 157 | ns: { 158 | prefix: nameNs.prefix, 159 | localName: nameNs.localName, 160 | uri: nsUri 161 | } 162 | }; 163 | 164 | this.properties.defineDescriptor(element, descriptor); 165 | this.properties.defineModel(element, this); 166 | this.properties.define(element, 'get', { enumerable: false, writable: true }); 167 | this.properties.define(element, 'set', { enumerable: false, writable: true }); 168 | this.properties.define(element, '$parent', { enumerable: false, writable: true }); 169 | this.properties.define(element, '$instanceOf', { enumerable: false, writable: true }); 170 | 171 | forEach(properties, function(a, key) { 172 | if (isObject(a) && a.value !== undefined) { 173 | element[a.name] = a.value; 174 | } else { 175 | element[key] = a; 176 | } 177 | }); 178 | 179 | return element; 180 | }; 181 | 182 | /** 183 | * Returns a registered package by uri or prefix 184 | * 185 | * @return {Object} the package 186 | */ 187 | Moddle.prototype.getPackage = function(uriOrPrefix) { 188 | return this.registry.getPackage(uriOrPrefix); 189 | }; 190 | 191 | /** 192 | * Returns a snapshot of all known packages 193 | * 194 | * @return {Object} the package 195 | */ 196 | Moddle.prototype.getPackages = function() { 197 | return this.registry.getPackages(); 198 | }; 199 | 200 | /** 201 | * Returns the descriptor for an element 202 | */ 203 | Moddle.prototype.getElementDescriptor = function(element) { 204 | return element.$descriptor; 205 | }; 206 | 207 | /** 208 | * Returns true if the given descriptor or instance 209 | * represents the given type. 210 | * 211 | * May be applied to this, if element is omitted. 212 | */ 213 | Moddle.prototype.hasType = function(element, type) { 214 | if (type === undefined) { 215 | type = element; 216 | element = this; 217 | } 218 | 219 | var descriptor = element.$model.getElementDescriptor(element); 220 | 221 | return (type in descriptor.allTypesByName); 222 | }; 223 | 224 | /** 225 | * Returns the descriptor of an elements named property 226 | */ 227 | Moddle.prototype.getPropertyDescriptor = function(element, property) { 228 | return this.getElementDescriptor(element).propertiesByName[property]; 229 | }; 230 | 231 | /** 232 | * Returns a mapped type's descriptor 233 | */ 234 | Moddle.prototype.getTypeDescriptor = function(type) { 235 | return this.registry.typeMap[type]; 236 | }; -------------------------------------------------------------------------------- /lib/ns.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses a namespaced attribute name of the form (ns:)localName to an object, 3 | * given a default prefix to assume in case no explicit namespace is given. 4 | * 5 | * @param {String} name 6 | * @param {String} [defaultPrefix] the default prefix to take, if none is present. 7 | * 8 | * @return {Object} the parsed name 9 | */ 10 | export function parseName(name, defaultPrefix) { 11 | var parts = name.split(/:/), 12 | localName, prefix; 13 | 14 | // no prefix (i.e. only local name) 15 | if (parts.length === 1) { 16 | localName = name; 17 | prefix = defaultPrefix; 18 | } 19 | 20 | // prefix + local name 21 | else if (parts.length === 2) { 22 | localName = parts[1]; 23 | prefix = parts[0]; 24 | } 25 | 26 | else { 27 | throw new Error('expected or , got ' + name); 28 | } 29 | 30 | name = (prefix ? prefix + ':' : '') + localName; 31 | 32 | return { 33 | name: name, 34 | prefix: prefix, 35 | localName: localName 36 | }; 37 | } -------------------------------------------------------------------------------- /lib/properties.js: -------------------------------------------------------------------------------- 1 | import { 2 | assign, 3 | isString 4 | } from 'min-dash'; 5 | 6 | /** 7 | * A utility that gets and sets properties of model elements. 8 | * 9 | * @param {Model} model 10 | */ 11 | export default function Properties(model) { 12 | this.model = model; 13 | } 14 | 15 | 16 | /** 17 | * Sets a named property on the target element. 18 | * If the value is undefined, the property gets deleted. 19 | * 20 | * @param {Object} target 21 | * @param {String} name 22 | * @param {Object} value 23 | */ 24 | Properties.prototype.set = function(target, name, value) { 25 | 26 | if (!isString(name) || !name.length) { 27 | throw new TypeError('property name must be a non-empty string'); 28 | } 29 | 30 | var property = this.getProperty(target, name); 31 | 32 | var propertyName = property && property.name; 33 | 34 | if (isUndefined(value)) { 35 | 36 | // unset the property, if the specified value is undefined; 37 | // delete from $attrs (for extensions) or the target itself 38 | if (property) { 39 | delete target[propertyName]; 40 | } else { 41 | delete target.$attrs[stripGlobal(name)]; 42 | } 43 | } else { 44 | 45 | // set the property, defining well defined properties on the fly 46 | // or simply updating them in target.$attrs (for extensions) 47 | if (property) { 48 | if (propertyName in target) { 49 | target[propertyName] = value; 50 | } else { 51 | defineProperty(target, property, value); 52 | } 53 | } else { 54 | target.$attrs[stripGlobal(name)] = value; 55 | } 56 | } 57 | }; 58 | 59 | /** 60 | * Returns the named property of the given element 61 | * 62 | * @param {Object} target 63 | * @param {String} name 64 | * 65 | * @return {Object} 66 | */ 67 | Properties.prototype.get = function(target, name) { 68 | 69 | var property = this.getProperty(target, name); 70 | 71 | if (!property) { 72 | return target.$attrs[stripGlobal(name)]; 73 | } 74 | 75 | var propertyName = property.name; 76 | 77 | // check if access to collection property and lazily initialize it 78 | if (!target[propertyName] && property.isMany) { 79 | defineProperty(target, property, []); 80 | } 81 | 82 | return target[propertyName]; 83 | }; 84 | 85 | 86 | /** 87 | * Define a property on the target element 88 | * 89 | * @param {Object} target 90 | * @param {String} name 91 | * @param {Object} options 92 | */ 93 | Properties.prototype.define = function(target, name, options) { 94 | 95 | if (!options.writable) { 96 | 97 | var value = options.value; 98 | 99 | // use getters for read-only variables to support ES6 proxies 100 | // cf. https://github.com/bpmn-io/internal-docs/issues/386 101 | options = assign({}, options, { 102 | get: function() { return value; } 103 | }); 104 | 105 | delete options.value; 106 | } 107 | 108 | Object.defineProperty(target, name, options); 109 | }; 110 | 111 | 112 | /** 113 | * Define the descriptor for an element 114 | */ 115 | Properties.prototype.defineDescriptor = function(target, descriptor) { 116 | this.define(target, '$descriptor', { value: descriptor }); 117 | }; 118 | 119 | /** 120 | * Define the model for an element 121 | */ 122 | Properties.prototype.defineModel = function(target, model) { 123 | this.define(target, '$model', { value: model }); 124 | }; 125 | 126 | /** 127 | * Return property with the given name on the element. 128 | * 129 | * @param {any} target 130 | * @param {string} name 131 | * 132 | * @return {object | null} property 133 | */ 134 | Properties.prototype.getProperty = function(target, name) { 135 | 136 | var model = this.model; 137 | 138 | var property = model.getPropertyDescriptor(target, name); 139 | 140 | if (property) { 141 | return property; 142 | } 143 | 144 | if (name.includes(':')) { 145 | return null; 146 | } 147 | 148 | const strict = model.config.strict; 149 | 150 | if (typeof strict !== 'undefined') { 151 | const error = new TypeError(`unknown property <${ name }> on <${ target.$type }>`); 152 | 153 | if (strict) { 154 | throw error; 155 | } else { 156 | 157 | // eslint-disable-next-line no-undef 158 | typeof console !== 'undefined' && console.warn(error); 159 | } 160 | } 161 | 162 | return null; 163 | }; 164 | 165 | function isUndefined(val) { 166 | return typeof val === 'undefined'; 167 | } 168 | 169 | function defineProperty(target, property, value) { 170 | Object.defineProperty(target, property.name, { 171 | enumerable: !property.isReference, 172 | writable: true, 173 | value: value, 174 | configurable: true 175 | }); 176 | } 177 | 178 | function stripGlobal(name) { 179 | return name.replace(/^:/, ''); 180 | } -------------------------------------------------------------------------------- /lib/registry.js: -------------------------------------------------------------------------------- 1 | import { 2 | assign, 3 | forEach, 4 | bind 5 | } from 'min-dash'; 6 | 7 | import { 8 | isBuiltIn as isBuiltInType 9 | } from './types.js'; 10 | 11 | import DescriptorBuilder from './descriptor-builder.js'; 12 | 13 | import { 14 | parseName as parseNameNs 15 | } from './ns.js'; 16 | 17 | 18 | /** 19 | * A registry of Moddle packages. 20 | * 21 | * @param {Array} packages 22 | * @param {Properties} properties 23 | */ 24 | export default function Registry(packages, properties) { 25 | this.packageMap = {}; 26 | this.typeMap = {}; 27 | 28 | this.packages = []; 29 | 30 | this.properties = properties; 31 | 32 | forEach(packages, bind(this.registerPackage, this)); 33 | } 34 | 35 | 36 | Registry.prototype.getPackage = function(uriOrPrefix) { 37 | return this.packageMap[uriOrPrefix]; 38 | }; 39 | 40 | Registry.prototype.getPackages = function() { 41 | return this.packages; 42 | }; 43 | 44 | 45 | Registry.prototype.registerPackage = function(pkg) { 46 | 47 | // copy package 48 | pkg = assign({}, pkg); 49 | 50 | var pkgMap = this.packageMap; 51 | 52 | ensureAvailable(pkgMap, pkg, 'prefix'); 53 | ensureAvailable(pkgMap, pkg, 'uri'); 54 | 55 | // register types 56 | forEach(pkg.types, bind(function(descriptor) { 57 | this.registerType(descriptor, pkg); 58 | }, this)); 59 | 60 | pkgMap[pkg.uri] = pkgMap[pkg.prefix] = pkg; 61 | this.packages.push(pkg); 62 | }; 63 | 64 | 65 | /** 66 | * Register a type from a specific package with us 67 | */ 68 | Registry.prototype.registerType = function(type, pkg) { 69 | 70 | type = assign({}, type, { 71 | superClass: (type.superClass || []).slice(), 72 | extends: (type.extends || []).slice(), 73 | properties: (type.properties || []).slice(), 74 | meta: assign(({}, type.meta || {})) 75 | }); 76 | 77 | var ns = parseNameNs(type.name, pkg.prefix), 78 | name = ns.name, 79 | propertiesByName = {}; 80 | 81 | // parse properties 82 | forEach(type.properties, bind(function(p) { 83 | 84 | // namespace property names 85 | var propertyNs = parseNameNs(p.name, ns.prefix), 86 | propertyName = propertyNs.name; 87 | 88 | // namespace property types 89 | if (!isBuiltInType(p.type)) { 90 | p.type = parseNameNs(p.type, propertyNs.prefix).name; 91 | } 92 | 93 | assign(p, { 94 | ns: propertyNs, 95 | name: propertyName 96 | }); 97 | 98 | propertiesByName[propertyName] = p; 99 | }, this)); 100 | 101 | // update ns + name 102 | assign(type, { 103 | ns: ns, 104 | name: name, 105 | propertiesByName: propertiesByName 106 | }); 107 | 108 | forEach(type.extends, bind(function(extendsName) { 109 | var extendsNameNs = parseNameNs(extendsName, ns.prefix); 110 | 111 | var extended = this.typeMap[extendsNameNs.name]; 112 | 113 | extended.traits = extended.traits || []; 114 | extended.traits.push(name); 115 | }, this)); 116 | 117 | // link to package 118 | this.definePackage(type, pkg); 119 | 120 | // register 121 | this.typeMap[name] = type; 122 | }; 123 | 124 | 125 | /** 126 | * Traverse the type hierarchy from bottom to top, 127 | * calling iterator with (type, inherited) for all elements in 128 | * the inheritance chain. 129 | * 130 | * @param {Object} nsName 131 | * @param {Function} iterator 132 | * @param {Boolean} [trait=false] 133 | */ 134 | Registry.prototype.mapTypes = function(nsName, iterator, trait) { 135 | 136 | var type = isBuiltInType(nsName.name) ? { name: nsName.name } : this.typeMap[nsName.name]; 137 | 138 | var self = this; 139 | 140 | /** 141 | * Traverse the selected super type or trait 142 | * 143 | * @param {String} cls 144 | * @param {Boolean} [trait=false] 145 | */ 146 | function traverse(cls, trait) { 147 | var parentNs = parseNameNs(cls, isBuiltInType(cls) ? '' : nsName.prefix); 148 | self.mapTypes(parentNs, iterator, trait); 149 | } 150 | 151 | /** 152 | * Traverse the selected trait. 153 | * 154 | * @param {String} cls 155 | */ 156 | function traverseTrait(cls) { 157 | return traverse(cls, true); 158 | } 159 | 160 | /** 161 | * Traverse the selected super type 162 | * 163 | * @param {String} cls 164 | */ 165 | function traverseSuper(cls) { 166 | return traverse(cls, false); 167 | } 168 | 169 | if (!type) { 170 | throw new Error('unknown type <' + nsName.name + '>'); 171 | } 172 | 173 | forEach(type.superClass, trait ? traverseTrait : traverseSuper); 174 | 175 | // call iterator with (type, inherited=!trait) 176 | iterator(type, !trait); 177 | 178 | forEach(type.traits, traverseTrait); 179 | }; 180 | 181 | 182 | /** 183 | * Returns the effective descriptor for a type. 184 | * 185 | * @param {String} type the namespaced name (ns:localName) of the type 186 | * 187 | * @return {Descriptor} the resulting effective descriptor 188 | */ 189 | Registry.prototype.getEffectiveDescriptor = function(name) { 190 | 191 | var nsName = parseNameNs(name); 192 | 193 | var builder = new DescriptorBuilder(nsName); 194 | 195 | this.mapTypes(nsName, function(type, inherited) { 196 | builder.addTrait(type, inherited); 197 | }); 198 | 199 | var descriptor = builder.build(); 200 | 201 | // define package link 202 | this.definePackage(descriptor, descriptor.allTypes[descriptor.allTypes.length - 1].$pkg); 203 | 204 | return descriptor; 205 | }; 206 | 207 | 208 | Registry.prototype.definePackage = function(target, pkg) { 209 | this.properties.define(target, '$pkg', { value: pkg }); 210 | }; 211 | 212 | 213 | 214 | // helpers //////////////////////////// 215 | 216 | function ensureAvailable(packageMap, pkg, identifierKey) { 217 | 218 | var value = pkg[identifierKey]; 219 | 220 | if (value in packageMap) { 221 | throw new Error('package with ' + identifierKey + ' <' + value + '> already defined'); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in moddle types 3 | */ 4 | var BUILTINS = { 5 | String: true, 6 | Boolean: true, 7 | Integer: true, 8 | Real: true, 9 | Element: true 10 | }; 11 | 12 | /** 13 | * Converters for built in types from string representations 14 | */ 15 | var TYPE_CONVERTERS = { 16 | String: function(s) { return s; }, 17 | Boolean: function(s) { return s === 'true'; }, 18 | Integer: function(s) { return parseInt(s, 10); }, 19 | Real: function(s) { return parseFloat(s); } 20 | }; 21 | 22 | /** 23 | * Convert a type to its real representation 24 | */ 25 | export function coerceType(type, value) { 26 | 27 | var converter = TYPE_CONVERTERS[type]; 28 | 29 | if (converter) { 30 | return converter(value); 31 | } else { 32 | return value; 33 | } 34 | } 35 | 36 | /** 37 | * Return whether the given type is built-in 38 | */ 39 | export function isBuiltIn(type) { 40 | return !!BUILTINS[type]; 41 | } 42 | 43 | /** 44 | * Return whether the given type is simple 45 | */ 46 | export function isSimple(type) { 47 | return !!TYPE_CONVERTERS[type]; 48 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moddle", 3 | "version": "7.2.0", 4 | "description": "A library for importing meta-model based file formats into JS", 5 | "scripts": { 6 | "all": "run-s lint test test:schema", 7 | "lint": "eslint .", 8 | "pretest": "run-s build", 9 | "dev": "npm test -- --watch", 10 | "test": "mocha --reporter=spec --recursive test", 11 | "test:schema": "ajv -s resources/schema/moddle.json -d 'test/fixtures/**/*.json'", 12 | "build": "rollup -c", 13 | "prepare": "run-s build" 14 | }, 15 | "type": "module", 16 | "exports": { 17 | ".": { 18 | "import": "./dist/index.js", 19 | "require": "./dist/index.cjs" 20 | }, 21 | "./package.json": "./package.json" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/bpmn-io/moddle" 26 | }, 27 | "keywords": [ 28 | "model", 29 | "meta-model", 30 | "xml", 31 | "xsd", 32 | "import", 33 | "export" 34 | ], 35 | "author": { 36 | "name": "Nico Rehwaldt", 37 | "url": "https://github.com/Nikku" 38 | }, 39 | "contributors": [ 40 | { 41 | "name": "bpmn.io contributors", 42 | "url": "https://github.com/bpmn-io" 43 | } 44 | ], 45 | "license": "MIT", 46 | "sideEffects": false, 47 | "devDependencies": { 48 | "ajv": "^8.17.1", 49 | "ajv-cli": "^5.0.0", 50 | "chai": "^4.5.0", 51 | "eslint": "^9.17.0", 52 | "eslint-plugin-bpmn-io": "^2.0.2", 53 | "fast-glob": "^3.3.3", 54 | "mocha": "^10.8.2", 55 | "npm-run-all": "^4.1.2", 56 | "rollup": "^4.30.1" 57 | }, 58 | "dependencies": { 59 | "min-dash": "^4.2.1" 60 | }, 61 | "files": [ 62 | "dist", 63 | "resources/schema/moddle.json" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /resources/schema/moddle.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "https://bpmn.io/20241206/moddle-package.schema.json", 4 | "title": "Moddle Package Schema", 5 | "type": "object", 6 | "required": [ 7 | "name", 8 | "prefix", 9 | "uri", 10 | "types" 11 | ], 12 | "properties": { 13 | "name": { 14 | "type": "string", 15 | "description": "Name of the package." 16 | }, 17 | "$schema": { 18 | "type": "string", 19 | "description": "Reference to this schema" 20 | }, 21 | "prefix": { 22 | "type": "string", 23 | "description": "The prefix uniquely identifies elements in a package if more multiple packages are in place." 24 | }, 25 | "uri": { 26 | "type": "string", 27 | "description": "The uri field in a package definition defines the associated XML namespace URI." 28 | }, 29 | "types": { 30 | "type": "array", 31 | "description": "List of types belonging to this package.", 32 | "items": { 33 | "type": "object", 34 | "required": [ 35 | "name" 36 | ], 37 | "properties": { 38 | "name": { 39 | "type": "string", 40 | "description": "Name of the defined type." 41 | }, 42 | "superClass": { 43 | "type": "array", 44 | "items": { 45 | "type": "string", 46 | "description": "Name of the super type." 47 | } 48 | }, 49 | "isAbstract": { 50 | "type": "boolean", 51 | "description": "Can this type be instantiated." 52 | }, 53 | "properties": { 54 | "type": "array", 55 | "items": { 56 | "type": "object", 57 | "properties": { 58 | "name": { 59 | "type": "string", 60 | "description": "Name of the property." 61 | }, 62 | "type": { 63 | "type": "string", 64 | "description": "Type of the property." 65 | }, 66 | "isAttr": { 67 | "type": "boolean", 68 | "description": "Should the property be serialized as XML attribute." 69 | }, 70 | "isBody": { 71 | "type": "boolean", 72 | "description": "Should the property be serialized as body element." 73 | }, 74 | "isId": { 75 | "type": "boolean", 76 | "description": "Is current property map to XML node id." 77 | }, 78 | "isMany": { 79 | "type": "boolean", 80 | "description": "Is the property an array or a single." 81 | }, 82 | "isReference": { 83 | "type": "boolean", 84 | "description": "Is the property referencing to another element" 85 | }, 86 | "default": { 87 | "description": "Provides default for the property.", 88 | "oneOf": [ 89 | { 90 | "type": "boolean" 91 | }, 92 | { 93 | "type": "number" 94 | }, 95 | { 96 | "type": "string" 97 | } 98 | ] 99 | }, 100 | "redefines": { 101 | "type": "string", 102 | "description": "Property name for the redefinition of existing property" 103 | }, 104 | "replaces": { 105 | "type": "string", 106 | "description": "Property name for the replacement of existing property" 107 | }, 108 | "xml": { 109 | "type": "object", 110 | "description": "Defines XML serialization details", 111 | "properties": { 112 | "serialize": { 113 | "type": "string", 114 | "description": "Provides XML serialization type, e.g. xsi:type" 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | "xml": { 125 | "type": "object", 126 | "description": "Defines XML serialization details.", 127 | "properties": { 128 | "tagAlias": { 129 | "const": "lowerCase" 130 | }, 131 | "typePrefix": { 132 | "type": "string" 133 | } 134 | } 135 | }, 136 | "associations": { 137 | "type": "array", 138 | "description": "The associations property is reserved for future use." 139 | }, 140 | "enumerations": { 141 | "description": "The enumerations property is reserved for future use.", 142 | "type": "array", 143 | "items": { 144 | "type": "object", 145 | "properties": { 146 | "name": { 147 | "type": "string" 148 | }, 149 | "literalValues": { 150 | "type": "array", 151 | "items": { 152 | "type": "object", 153 | "properties": { 154 | "name": { 155 | "type": "string" 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | 4 | const pkg = JSON.parse(fs.readFileSync('./package.json')); 5 | 6 | const pkgExports = pkg.exports['.']; 7 | 8 | function pgl(plugins = []) { 9 | return plugins; 10 | } 11 | 12 | const srcEntry = 'lib/index.js'; 13 | 14 | export default [ 15 | { 16 | input: srcEntry, 17 | output: [ 18 | { file: pkgExports.require, format: 'cjs', sourcemap: true }, 19 | { file: pkgExports.import, format: 'es', sourcemap: true } 20 | ], 21 | external: [ 22 | 'min-dash' 23 | ], 24 | plugins: pgl() 25 | } 26 | ]; -------------------------------------------------------------------------------- /test/expect.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | 3 | import Matchers from './matchers.js'; 4 | 5 | // add matchers 6 | chai.use(Matchers); 7 | 8 | // expose chai expect 9 | export { 10 | expect as default 11 | } from 'chai'; -------------------------------------------------------------------------------- /test/fixtures/model/datatype-external.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DataTypes 2", 3 | "uri": "http://datatypes2", 4 | "prefix": "do", 5 | "types": [ 6 | { 7 | "name": "Rect", 8 | "properties": [ 9 | { "name": "x", "type": "Integer", "isAttr": true } 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /test/fixtures/model/datatype.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DataTypes", 3 | "uri": "http://datatypes", 4 | "prefix": "dt", 5 | "types": [ 6 | { 7 | "name": "Root", 8 | "properties": [ 9 | { "name": "bounds", "serialize": "xsi:type", "type": "Rect" }, 10 | { "name": "otherBounds", "type": "do:Rect", "serialize": "xsi:type", "isMany": true } 11 | ] 12 | }, 13 | { 14 | "name": "Rect", 15 | "properties": [ 16 | { "name": "y", "type": "Integer", "isAttr": true } 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /test/fixtures/model/extension/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Base", 3 | "uri": "http://base", 4 | "prefix": "b", 5 | "types": [ 6 | { 7 | "name": "Root", 8 | "properties": [ 9 | { "name": "own", "type": "Own" }, 10 | { "name": "ownAttr", "type": "String", "isAttr": true }, 11 | { "name": "generic", "type": "Element" }, 12 | { "name": "genericCollection", "type": "Element", "isMany": true } 13 | ] 14 | }, 15 | { 16 | "name": "Own", 17 | "properties": [ 18 | { "name": "count", "type": "Integer", "isAttr": true } 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /test/fixtures/model/extension/custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Custom", 3 | "uri": "http://custom", 4 | "prefix": "c", 5 | "types": [ 6 | { 7 | "name": "CustomRoot", 8 | "superClass": [ "Base" ], 9 | "extends": [ "b:Root" ], 10 | "properties": [ 11 | { "name": "customAttr", "type": "Integer", "isAttr": true }, 12 | { "name": "generic", "type": "CustomGeneric", "redefines": "b:Root#generic" } 13 | ] 14 | }, 15 | { 16 | "name": "Base", 17 | "properties": [ 18 | { "name": "customBaseAttr", "type": "Integer", "isAttr": true } 19 | ] 20 | }, 21 | { 22 | "name": "CustomGeneric", 23 | "superClass": [ "Element" ], 24 | "properties": [ 25 | { "name": "count", "type": "Integer", "isAttr": true } 26 | ] 27 | }, 28 | { 29 | "name": "Property", 30 | "superClass": [ "Element" ], 31 | "properties": [ 32 | { "name": "key", "type": "String", "isAttr": true }, 33 | { "name": "value", "type": "String", "isAttr": true } 34 | ] 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /test/fixtures/model/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cars", 3 | "uri": "http://cars", 4 | "prefix": "c", 5 | "types": [ 6 | { 7 | "name": "Car", 8 | "properties": [ 9 | { "name": "name", "type": "String", "isAttr": true, "default": "No Name" } 10 | ], 11 | "meta": { 12 | "owners": [ 13 | "the pope", 14 | "donald trump" 15 | ] 16 | } 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/model/noalias.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NoAlias", 3 | "uri": "http://noalias", 4 | "prefix": "na", 5 | "types": [ 6 | { 7 | "name": "Root", 8 | "properties": [ 9 | { "name": "id", "type": "Integer", "isAttr": true, "isId": true } 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /test/fixtures/model/properties-extended.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Extended", 3 | "uri": "http://extended", 4 | "prefix": "ext", 5 | "types": [ 6 | { 7 | "name": "ExtendedComplex", 8 | "superClass": [ "props:ComplexCount" ], 9 | "properties": [ 10 | { "name": "numCount", "type": "Integer", "isAttr": true, "redefines": "props:Complex#count" } 11 | ] 12 | }, 13 | { 14 | "name": "Root", 15 | "superClass": [ "props:Root" ], 16 | "properties": [ 17 | { "name": "elements", "type": "Base", "isMany": true } 18 | ] 19 | }, 20 | { 21 | "name": "Base" 22 | }, 23 | { 24 | "name": "CABSBase" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /test/fixtures/model/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Properties", 3 | "uri": "http://properties", 4 | "prefix": "props", 5 | "types": [ 6 | { 7 | "name": "Complex", 8 | "properties": [ 9 | { "name": "id", "type": "String", "isAttr": true, "isId": true } 10 | ] 11 | }, 12 | { 13 | "name": "ComplexAttrs", 14 | "superClass": [ "Complex" ], 15 | "properties": [ 16 | { "name": "attrs", "type": "Attributes", "serialize": "xsi:type" } 17 | ] 18 | }, 19 | { 20 | "name": "ComplexAttrsCol", 21 | "superClass": [ "Complex" ], 22 | "properties": [ 23 | { "name": "attrs", "type": "Attributes", "isMany": true, "serialize": "xsi:type" } 24 | ] 25 | }, 26 | { 27 | "name": "ComplexCount", 28 | "superClass": [ "Complex" ], 29 | "properties": [ 30 | { "name": "count", "type": "Integer", "isAttr": true } 31 | ] 32 | }, 33 | { 34 | "name": "ComplexNesting", 35 | "superClass": [ "Complex" ], 36 | "properties": [ 37 | { "name": "nested", "type": "Complex", "isMany": true } 38 | ] 39 | }, 40 | { 41 | "name": "SimpleBody", 42 | "superClass": [ "Base" ], 43 | "properties": [ 44 | { 45 | "name": "body", 46 | "type": "String", 47 | "isBody": true 48 | } 49 | ] 50 | }, 51 | { 52 | "name": "SimpleBodyProperties", 53 | "superClass": [ "Base" ], 54 | "properties": [ 55 | { 56 | "name": "intValue", 57 | "type": "Integer" 58 | }, 59 | { 60 | "name": "boolValue", 61 | "type": "Boolean" 62 | }, 63 | { 64 | "name": "str", 65 | "type": "String", 66 | "isMany": true 67 | } 68 | ] 69 | }, 70 | { 71 | "name": "Base" 72 | }, 73 | { 74 | "name": "BaseWithId", 75 | "superClass": [ "Base" ], 76 | "properties": [ 77 | { "name": "id", "type": "String", "isAttr": true, "isId": true } 78 | ] 79 | }, 80 | { 81 | "name": "BaseWithNumericId", 82 | "superClass": [ "BaseWithId" ], 83 | "properties": [ 84 | { "name": "idNumeric", "type": "Integer", "isAttr": true, "redefines": "BaseWithId#id", "isId": true } 85 | ] 86 | }, 87 | { 88 | "name": "Attributes", 89 | "superClass": [ "BaseWithId" ], 90 | "properties": [ 91 | { 92 | "name": "realValue", 93 | "type": "Real", 94 | "isAttr": true 95 | }, 96 | { 97 | "name": "integerValue", 98 | "type": "Integer", 99 | "isAttr": true 100 | }, 101 | { 102 | "name": "booleanValue", 103 | "type": "Boolean", 104 | "isAttr": true 105 | }, 106 | { 107 | "name": "defaultBooleanValue", 108 | "type": "Boolean", 109 | "isAttr": true, 110 | "default": true 111 | } 112 | ] 113 | }, 114 | { 115 | "name": "SubAttributes", 116 | "superClass": [ "Attributes" ] 117 | }, 118 | { 119 | "name": "Root", 120 | "properties": [ 121 | { 122 | "name": "any", 123 | "type": "Base", 124 | "isMany": true 125 | } 126 | ] 127 | }, 128 | { 129 | "name": "Embedding", 130 | "superClass": [ "BaseWithId" ], 131 | "properties": [ 132 | { 133 | "name": "embeddedComplex", 134 | "type": "Complex" 135 | } 136 | ] 137 | }, 138 | { 139 | "name": "ReferencingSingle", 140 | "superClass": [ "BaseWithId" ], 141 | "properties": [ 142 | { 143 | "name": "referencedComplex", 144 | "type": "Complex", 145 | "isReference": true, 146 | "isAttr": true 147 | } 148 | ] 149 | }, 150 | { 151 | "name": "ReferencingCollection", 152 | "superClass": [ "BaseWithId" ], 153 | "properties": [ 154 | { 155 | "name": "references", 156 | "type": "Complex", 157 | "isReference": true, 158 | "isMany": true 159 | } 160 | ] 161 | }, 162 | { 163 | "name": "ContainedCollection", 164 | "superClass": [ "BaseWithId" ], 165 | "properties": [ 166 | { 167 | "name": "children", 168 | "type": "Complex", 169 | "isMany": true 170 | } 171 | ] 172 | }, 173 | { 174 | "name": "MultipleSuper", 175 | "superClass": [ 176 | "Base", 177 | "BaseWithId", 178 | "SimpleBody" 179 | ] 180 | } 181 | ] 182 | } -------------------------------------------------------------------------------- /test/fixtures/model/redefines/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Redefines Base", 3 | "uri": "http://redefine", 4 | "prefix": "b", 5 | "types": [ 6 | { 7 | "name": "Base", 8 | "properties": [ 9 | { "name": "id", "type": "Integer" }, 10 | { "name": "name", "type": "String" } 11 | ] 12 | }, 13 | { 14 | "name": "Extension", 15 | "superClass": [ "Base" ], 16 | "properties": [ 17 | { "name": "value", "type": "String" }, 18 | { "name": "id", "type": "Integer", "redefines": "Base#id", "isId": true } 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /test/fixtures/model/replaces/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Replaces Base", 3 | "uri": "http://replaces", 4 | "prefix": "b", 5 | "types": [ 6 | { 7 | "name": "Base", 8 | "properties": [ 9 | { "name": "id", "type": "Integer", "isId": true }, 10 | { "name": "name", "type": "String" } 11 | ] 12 | }, 13 | { 14 | "name": "Extension", 15 | "superClass": [ "Base" ], 16 | "properties": [ 17 | { "name": "value", "type": "String" }, 18 | { "name": "id", "type": "Integer", "replaces": "Base#id", "isId": true } 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /test/fixtures/model/schema-meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../../docs/moddle.json", 3 | "meta": { 4 | "is": "allowed" 5 | }, 6 | "name": "Cars", 7 | "uri": "http://cars", 8 | "prefix": "c", 9 | "types": [ 10 | { 11 | "name": "Car", 12 | "properties": [ 13 | { 14 | "name": "name", 15 | "type": "String", 16 | "isAttr": true, 17 | "default": "No Name" 18 | } 19 | ], 20 | "meta": { 21 | "owners": [ 22 | "the pope", 23 | "donald trump" 24 | ] 25 | } 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /test/fixtures/model/self-extend.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Self Extend", 3 | "uri": "http://self-extend", 4 | "prefix": "se", 5 | "types": [ 6 | { 7 | "name": "Rect" 8 | }, 9 | { 10 | "name": "ExtendedRect", 11 | "extends": [ "Rect" ] 12 | }, 13 | { 14 | "name": "OtherExtendedRect", 15 | "extends": [ "se:Rect" ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /test/fixtures/model/shadow.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Shadow", 3 | "uri": "http://shadow", 4 | "prefix": "s", 5 | "types": [ 6 | { 7 | "name": "Element", 8 | "properties": [] 9 | }, 10 | { 11 | "name": "NamedElement", 12 | "superClass": [ "s:Element" ] 13 | }, 14 | { 15 | "name": "ExtendsBuiltinElement", 16 | "superClass": [ 17 | "Element" 18 | ] 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | import { 4 | map 5 | } from 'min-dash'; 6 | 7 | import { Moddle } from 'moddle'; 8 | 9 | import expect from './expect.js'; 10 | 11 | 12 | export function readFile(filename) { 13 | return fs.readFileSync(filename, { encoding: 'UTF-8' }); 14 | } 15 | 16 | export function createModelBuilder(base) { 17 | 18 | var cache = {}; 19 | 20 | if (!base) { 21 | throw new Error('[test-util] must specify a base directory'); 22 | } 23 | 24 | function createModel(packageNames, config = {}) { 25 | 26 | var packages = map(packageNames, function(f) { 27 | var pkg = cache[f]; 28 | var file = base + f + '.json'; 29 | 30 | if (!pkg) { 31 | try { 32 | pkg = cache[f] = JSON.parse(readFile(base + f + '.json')); 33 | } catch (e) { 34 | throw new Error('[Helper] failed to parse <' + file + '> as JSON: ' + e.message); 35 | } 36 | } 37 | 38 | return pkg; 39 | }); 40 | 41 | return new Moddle(packages, config); 42 | } 43 | 44 | return createModel; 45 | } 46 | 47 | /** 48 | * @param {Object} descriptor 49 | * @param {string[]} expectedPropertyNames 50 | */ 51 | export function expectOrderedProperties(descriptor, expectedPropertyNames) { 52 | var propertyNames = descriptor.properties.map(function(p) { 53 | return p.name; 54 | }); 55 | 56 | // then 57 | expect(propertyNames).to.eql(expectedPropertyNames); 58 | } 59 | 60 | /** 61 | * @param {Moddle} model 62 | * @param {string} typeName 63 | * 64 | * @return {Object} descriptor 65 | */ 66 | export function getEffectiveDescriptor(model, typeName) { 67 | var Type = model.getType(typeName); 68 | 69 | return model.getElementDescriptor(Type); 70 | } -------------------------------------------------------------------------------- /test/integration/distro.cjs: -------------------------------------------------------------------------------- 1 | const { 2 | expect 3 | } = require('chai'); 4 | 5 | const pkg = require('../../package.json'); 6 | const pkgExports = pkg.exports['.']; 7 | 8 | 9 | describe('integration', function() { 10 | 11 | describe('distro', function() { 12 | 13 | it('should expose CJS bundle', function() { 14 | 15 | const { 16 | Moddle, 17 | isSimpleType, 18 | isBuiltInType, 19 | parseNameNS, 20 | coerceType 21 | } = require('../../' + pkgExports.require); 22 | 23 | expect(new Moddle()).to.exist; 24 | 25 | expect(isSimpleType).to.exist; 26 | expect(isBuiltInType).to.exist; 27 | expect(parseNameNS).to.exist; 28 | expect(coerceType).to.exist; 29 | }); 30 | 31 | }); 32 | 33 | }); -------------------------------------------------------------------------------- /test/matchers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {import('chai')} chai 3 | * @param {import('chai').util} utils 4 | */ 5 | export default function(chai, utils) { 6 | 7 | utils.addMethod(chai.Assertion.prototype, 'jsonEqual', function(comparison) { 8 | 9 | var actual = JSON.stringify(this._obj); 10 | var expected = JSON.stringify(comparison); 11 | 12 | this.assert( 13 | actual == expected, 14 | 'expected #{this} to deep equal #{act}', 15 | 'expected #{this} not to deep equal #{act}', 16 | comparison, // expected 17 | this._obj, // actual 18 | true // show diff 19 | ); 20 | }); 21 | } -------------------------------------------------------------------------------- /test/spec/extension.js: -------------------------------------------------------------------------------- 1 | import expect from '../expect.js'; 2 | 3 | import { 4 | createModelBuilder 5 | } from '../helper.js'; 6 | 7 | 8 | describe('extension', function() { 9 | 10 | var createModel = createModelBuilder('test/fixtures/model/'); 11 | 12 | 13 | describe('types', function() { 14 | 15 | describe('built-in shadowing', function() { 16 | 17 | // given 18 | var model = createModel([ 'shadow' ]); 19 | 20 | 21 | it('should shadow ', function() { 22 | 23 | // when 24 | var element = model.create('s:Element'); 25 | 26 | // then 27 | expect(element).to.exist; 28 | expect(element.$instanceOf('s:Element')).to.be.true; 29 | }); 30 | 31 | 32 | it('should shadow in inheritance hierarchy', function() { 33 | 34 | // when 35 | var element = model.create('s:NamedElement'); 36 | 37 | // then 38 | expect(element).to.exist; 39 | expect(element.$instanceOf('s:Element')).to.be.true; 40 | expect(element.$instanceOf('s:NamedElement')).to.be.true; 41 | }); 42 | 43 | 44 | it('should provide built-in type', function() { 45 | 46 | // when 47 | var element = model.create('s:ExtendsBuiltinElement'); 48 | 49 | // then 50 | expect(element).to.exist; 51 | expect(element.$instanceOf('Element')).to.be.true; 52 | expect(element.$instanceOf('s:ExtendsBuiltinElement')).to.be.true; 53 | }); 54 | 55 | }); 56 | 57 | }); 58 | 59 | 60 | describe('extension', function() { 61 | 62 | var model = createModel([ 'extension/base', 'extension/custom' ]); 63 | 64 | 65 | describe('trait', function() { 66 | 67 | it('should not provide meta-data', function() { 68 | 69 | expect(() => { 70 | model.getType('c:CustomRoot'); 71 | }).to.throw(/cannot create extending /); 72 | }); 73 | 74 | 75 | describe('descriptor', function() { 76 | 77 | it('should indicate non-inherited', function() { 78 | 79 | // given 80 | var ComplexType = model.getType('b:Root'); 81 | 82 | // when 83 | var descriptor = model.getElementDescriptor(ComplexType), 84 | customAttrDescriptor = descriptor.propertiesByName['customAttr'], 85 | customBaseAttrDescriptor = descriptor.propertiesByName['customBaseAttr'], 86 | ownAttrDescriptor = descriptor.propertiesByName['ownAttr']; 87 | 88 | // then 89 | expect(customAttrDescriptor.inherited).to.be.false; 90 | expect(customBaseAttrDescriptor.inherited).to.be.false; 91 | expect(ownAttrDescriptor.inherited).to.be.true; 92 | }); 93 | 94 | }); 95 | 96 | 97 | it('should plug-in into type hierarchy', function() { 98 | 99 | var root = model.create('b:Root'); 100 | 101 | // then 102 | expect(root.$instanceOf('c:CustomRoot')).to.be.true; 103 | }); 104 | 105 | 106 | it('should add custom attribute', function() { 107 | 108 | // when 109 | var root = model.create('b:Root', { 110 | customAttr: -1 111 | }); 112 | 113 | // then 114 | expect(root.customAttr).to.eql(-1); 115 | }); 116 | 117 | 118 | it('should refine property', function() { 119 | 120 | // given 121 | var Type = model.getType('b:Root'); 122 | 123 | // when 124 | var genericProperty = Type.$descriptor.propertiesByName['generic']; 125 | 126 | // then 127 | expect(genericProperty.type).to.eql('c:CustomGeneric'); 128 | }); 129 | 130 | 131 | it('should use refined property', function() { 132 | 133 | var customGeneric = model.create('c:CustomGeneric', { count: 100 }); 134 | 135 | // when 136 | var root = model.create('b:Root', { 137 | generic: customGeneric 138 | }); 139 | 140 | // then 141 | expect(root.generic).to.eql(customGeneric); 142 | }); 143 | 144 | }); 145 | 146 | 147 | describe('types', function() { 148 | 149 | it('should provide custom types', function() { 150 | 151 | var property = model.create('c:Property'); 152 | 153 | // then 154 | expect(property.$instanceOf('c:Property')).to.be.true; 155 | }); 156 | 157 | }); 158 | 159 | 160 | describe('generic', function() { 161 | 162 | it('should extend Element', function() { 163 | 164 | // when 165 | var customGeneric = model.create('c:CustomGeneric', { count: 100 }); 166 | 167 | // then 168 | expect(customGeneric.$instanceOf('Element')).to.be.true; 169 | }); 170 | 171 | 172 | it('should be part of generic collection', function() { 173 | 174 | var customProperty = model.create('c:Property', { key: 'foo', value: 'bar' }); 175 | 176 | // when 177 | var root = model.create('b:Root', { 178 | genericCollection: [ customProperty ] 179 | }); 180 | 181 | // then 182 | expect(root.genericCollection).to.eql([ customProperty ]); 183 | }); 184 | 185 | }); 186 | 187 | }); 188 | 189 | 190 | describe('property replacement', function() { 191 | 192 | var model = createModel([ 'replaces/base' ]); 193 | 194 | it('should replace in descriptor', function() { 195 | 196 | // given 197 | var Extension = model.getType('b:Extension'); 198 | 199 | // when 200 | var descriptor = model.getElementDescriptor(Extension), 201 | propertyNames = descriptor.properties.map(function(p) { 202 | return p.name; 203 | }); 204 | 205 | // then 206 | expect(propertyNames).to.eql([ 207 | 'name', 208 | 'value', 209 | 'id' 210 | ]); 211 | 212 | expect(descriptor.propertiesByName['b:id'].type).to.eql('Integer'); 213 | expect(descriptor.propertiesByName['id'].type).to.eql('Integer'); 214 | }); 215 | 216 | }); 217 | 218 | 219 | describe('property redefinition', function() { 220 | 221 | var model = createModel([ 'redefines/base' ]); 222 | 223 | it('should redefine in descriptor', function() { 224 | 225 | // given 226 | var Extension = model.getType('b:Extension'); 227 | 228 | // when 229 | var descriptor = model.getElementDescriptor(Extension), 230 | propertyNames = descriptor.properties.map(function(p) { 231 | return p.name; 232 | }); 233 | 234 | // then 235 | expect(propertyNames).to.eql([ 236 | 'id', 237 | 'name', 238 | 'value' 239 | ]); 240 | 241 | expect(descriptor.propertiesByName['b:id'].type).to.eql('Integer'); 242 | expect(descriptor.propertiesByName['id'].type).to.eql('Integer'); 243 | }); 244 | 245 | }); 246 | 247 | 248 | describe('extension - self extend', function() { 249 | 250 | it('should self-extend', async function() { 251 | 252 | // when 253 | var model = createModel([ 'self-extend' ]); 254 | 255 | var element = model.create('se:Rect'); 256 | 257 | // then 258 | expect(element.$instanceOf('se:ExtendedRect')).to.be.true; 259 | expect(element.$instanceOf('se:OtherExtendedRect')).to.be.true; 260 | }); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /test/spec/meta.js: -------------------------------------------------------------------------------- 1 | import expect from '../expect.js'; 2 | 3 | import { 4 | createModelBuilder 5 | } from '../helper.js'; 6 | 7 | 8 | describe('meta', function() { 9 | 10 | var createModel = createModelBuilder('test/fixtures/model/'); 11 | var model = createModel([ 'meta' ]); 12 | 13 | 14 | it('should have the "meta" attribute', function() { 15 | 16 | // when 17 | var meta = model.getTypeDescriptor('c:Car').meta; 18 | 19 | // then 20 | expect(meta).to.exist; 21 | expect(meta).to.be.an('object'); 22 | }); 23 | 24 | 25 | it('should have a "owners" property inside "meta"', function() { 26 | 27 | // when 28 | var meta = model.getTypeDescriptor('c:Car').meta; 29 | 30 | // then 31 | expect(meta.owners).to.exist; 32 | expect(meta.owners).to.eql([ 'the pope', 'donald trump' ]); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /test/spec/moddle.js: -------------------------------------------------------------------------------- 1 | import expect from '../expect.js'; 2 | 3 | import { 4 | createModelBuilder 5 | } from '../helper.js'; 6 | 7 | 8 | describe('moddle', function() { 9 | 10 | var createModel = createModelBuilder('test/fixtures/model/'); 11 | 12 | describe('base', function() { 13 | 14 | var model = createModel([ 'properties' ]); 15 | 16 | 17 | it('should provide types', function() { 18 | 19 | // when 20 | var ComplexType = model.getType('props:Complex'); 21 | var SimpleBody = model.getType('props:SimpleBody'); 22 | var Attributes = model.getType('props:Attributes'); 23 | 24 | // then 25 | expect(ComplexType).to.exist; 26 | expect(SimpleBody).to.exist; 27 | expect(Attributes).to.exist; 28 | }); 29 | 30 | 31 | it('should provide packages by prefix', function() { 32 | 33 | // when 34 | var propertiesPackage = model.getPackage('props'); 35 | 36 | // then 37 | expect(propertiesPackage).to.exist; 38 | expect(propertiesPackage.name).to.equal('Properties'); 39 | expect(propertiesPackage.uri).to.equal('http://properties'); 40 | expect(propertiesPackage.prefix).to.equal('props'); 41 | }); 42 | 43 | 44 | it('should provide packages by uri', function() { 45 | 46 | // when 47 | var propertiesPackage = model.getPackage('http://properties'); 48 | 49 | // then 50 | expect(propertiesPackage).to.exist; 51 | expect(propertiesPackage.name).to.equal('Properties'); 52 | expect(propertiesPackage.uri).to.equal('http://properties'); 53 | expect(propertiesPackage.prefix).to.equal('props'); 54 | }); 55 | 56 | 57 | it('should provide type descriptor', function() { 58 | 59 | // given 60 | var expectedDescriptorNs = { name: 'props:Complex', prefix: 'props', localName: 'Complex' }; 61 | 62 | var expectedDescriptorProperties = [ 63 | { 64 | name: 'id', 65 | type: 'String', 66 | isAttr: true, 67 | isId: true, 68 | ns: { name: 'props:id', prefix: 'props', localName: 'id' }, 69 | inherited: true 70 | } 71 | ]; 72 | 73 | var expectedDescriptorPropertiesByName = { 74 | 75 | 'id': { 76 | name: 'id', 77 | type: 'String', 78 | isAttr: true, 79 | isId: true, 80 | ns: { name: 'props:id', prefix: 'props', localName: 'id' }, 81 | inherited: true 82 | }, 83 | 'props:id': { 84 | name: 'id', 85 | type: 'String', 86 | isAttr: true, 87 | isId: true, 88 | ns: { name: 'props:id', prefix: 'props', localName: 'id' }, 89 | inherited: true 90 | } 91 | }; 92 | 93 | // when 94 | var ComplexType = model.getType('props:Complex'); 95 | 96 | var descriptor = model.getElementDescriptor(ComplexType); 97 | 98 | // then 99 | expect(descriptor).to.exist; 100 | expect(descriptor.name).to.equal('props:Complex'); 101 | 102 | expect(descriptor.ns).to.jsonEqual(expectedDescriptorNs); 103 | expect(descriptor.properties).to.jsonEqual(expectedDescriptorProperties); 104 | expect(descriptor.propertiesByName).to.jsonEqual(expectedDescriptorPropertiesByName); 105 | }); 106 | 107 | 108 | it('should provide type descriptor via $descriptor property', function() { 109 | 110 | // given 111 | var ComplexType = model.getType('props:Complex'); 112 | var expectedDescriptor = model.getElementDescriptor(ComplexType); 113 | 114 | // when 115 | var descriptor = ComplexType.$descriptor; 116 | 117 | // then 118 | expect(descriptor).to.equal(expectedDescriptor); 119 | }); 120 | 121 | 122 | it('should provide model via $model property', function() { 123 | 124 | // given 125 | var ComplexType = model.getType('props:Complex'); 126 | 127 | // when 128 | var foundModel = ComplexType.$model; 129 | 130 | // then 131 | expect(foundModel).to.equal(model); 132 | }); 133 | 134 | 135 | describe('create', function() { 136 | 137 | it('should provide meta-data', function() { 138 | 139 | // when 140 | var instance = model.create('props:BaseWithNumericId'); 141 | 142 | // then 143 | expect(instance.$descriptor).to.exist; 144 | expect(instance.$type).to.equal('props:BaseWithNumericId'); 145 | }); 146 | 147 | }); 148 | 149 | 150 | describe('createAny', function() { 151 | 152 | it('should provide attrs + basic meta-data', function() { 153 | 154 | // when 155 | var anyInstance = model.createAny('other:Foo', 'http://other', { 156 | bar: 'BAR' 157 | }); 158 | 159 | // then 160 | expect(anyInstance).to.jsonEqual({ 161 | $type: 'other:Foo', 162 | bar: 'BAR' 163 | }); 164 | 165 | expect(anyInstance.$instanceOf('other:Foo')).to.be.true; 166 | }); 167 | 168 | 169 | it('should provide ns meta-data', function() { 170 | 171 | // when 172 | var anyInstance = model.createAny('other:Foo', 'http://other', { 173 | bar: 'BAR' 174 | }); 175 | 176 | // then 177 | expect(anyInstance.$descriptor).to.jsonEqual({ 178 | name: 'other:Foo', 179 | isGeneric: true, 180 | ns: { prefix : 'other', localName : 'Foo', uri : 'http://other' } 181 | }); 182 | }); 183 | 184 | 185 | it('should return non-enumerable special props', function() { 186 | 187 | // given 188 | var anyInstance = model.createAny('other:Foo', 'http://other', { 189 | bar: 'BAR' 190 | }); 191 | 192 | // assume 193 | expect(anyInstance).not.to.have.keys([ 194 | '$parent', 195 | '$instanceOf' 196 | ]); 197 | 198 | // when 199 | anyInstance.$parent = 'foo'; 200 | anyInstance.$instanceOf = 'bar'; 201 | 202 | // then 203 | expect(anyInstance).not.to.have.keys([ 204 | '$parent', 205 | '$instanceOf' 206 | ]); 207 | }); 208 | 209 | 210 | it('should have getters', function() { 211 | 212 | // when 213 | var anyInstance = model.createAny('other:Foo', 'http://other', { 214 | bar: 'BAR' 215 | }); 216 | 217 | // then 218 | expect(anyInstance.get('bar')).to.eql('BAR'); 219 | }); 220 | 221 | 222 | it('should have setters', function() { 223 | 224 | // given 225 | var anyInstance = model.createAny('other:Foo', 'http://other'); 226 | 227 | // when 228 | anyInstance.set('bar', 'BAR'); 229 | 230 | // then 231 | expect(anyInstance.get('bar')).to.eql('BAR'); 232 | }); 233 | 234 | 235 | it('should prevent prototype pollution', function() { 236 | 237 | // given 238 | var anyInstance = model.createAny('other:Foo', 'http://other'); 239 | 240 | // when 241 | 242 | expect(function() { 243 | anyInstance.set('__proto__', { hacked() { console.log('hacked'); } }); 244 | }).to.throw('illegal key: __proto__'); 245 | 246 | }); 247 | 248 | 249 | it('should NOT allow array as key', function() { 250 | 251 | // given 252 | var anyInstance = model.createAny('other:Foo', 'http://other'); 253 | 254 | // when 255 | expect(function() { 256 | anyInstance.set([ 'path', 'to', 'key' ], 'value'); 257 | }).to.throw('illegal key type: object. Key should be of type number or string.'); 258 | }); 259 | 260 | 261 | it('should mark accessors as special props', function() { 262 | 263 | // given 264 | var anyInstance = model.createAny('other:Foo', 'http://other', { 265 | bar: 'BAR' 266 | }); 267 | 268 | // then 269 | expect(anyInstance).not.to.have.keys([ 270 | 'get', 271 | 'set' 272 | ]); 273 | }); 274 | 275 | }); 276 | 277 | 278 | describe('getType', function() { 279 | 280 | it('should provide instantiatable type', function() { 281 | 282 | // when 283 | var SimpleBody = model.getType('props:SimpleBody'); 284 | 285 | var instance = new SimpleBody({ body: 'BAR' }); 286 | 287 | // then 288 | expect(instance instanceof SimpleBody).to.be.true; 289 | expect(instance.body).to.eql('BAR'); 290 | }); 291 | 292 | }); 293 | 294 | 295 | describe('instance', function() { 296 | 297 | it('should query types via $instanceOf', function() { 298 | 299 | // given 300 | var instance = model.create('props:BaseWithNumericId'); 301 | 302 | // then 303 | expect(instance.$instanceOf('props:BaseWithNumericId')).to.equal(true); 304 | expect(instance.$instanceOf('props:Base')).to.equal(true); 305 | }); 306 | 307 | 308 | it('should provide $type in instance', function() { 309 | 310 | // given 311 | var SimpleBody = model.getType('props:SimpleBody'); 312 | 313 | // when 314 | var instance = new SimpleBody(); 315 | 316 | // then 317 | expect(instance.$type).to.equal('props:SimpleBody'); 318 | }); 319 | 320 | 321 | it('should provide $descriptor in instance', function() { 322 | 323 | // given 324 | var SimpleBody = model.getType('props:SimpleBody'); 325 | 326 | // when 327 | var instance = new SimpleBody(); 328 | 329 | // then 330 | expect(instance.$descriptor).to.eql(SimpleBody.$descriptor); 331 | }); 332 | 333 | }); 334 | 335 | 336 | describe('helpers', function() { 337 | 338 | it('should get property descriptor', function() { 339 | 340 | // given 341 | var SimpleBody = model.getType('props:SimpleBody'); 342 | 343 | var instance = new SimpleBody(); 344 | 345 | // when 346 | var body = model.getPropertyDescriptor(instance, 'props:body'); 347 | 348 | // then 349 | expect(body).to.include.keys([ 'name', 'type', 'isBody', 'ns' ]); 350 | }); 351 | 352 | 353 | it('should get type descriptor', function() { 354 | 355 | // when 356 | var simpleBody = model.getTypeDescriptor('props:SimpleBody'); 357 | 358 | // then 359 | expect(simpleBody).to.include.keys([ 'name', 'superClass', 'properties' ]); 360 | }); 361 | 362 | }); 363 | 364 | }); 365 | 366 | 367 | describe('error handling', function() { 368 | 369 | it('should handle package redefinition', function() { 370 | 371 | // given 372 | function create() { 373 | 374 | // when 375 | createModel([ 'properties', 'properties' ]); 376 | } 377 | 378 | // then 379 | expect(create).to.throw(/package with prefix already defined/); 380 | 381 | }); 382 | 383 | }); 384 | 385 | 386 | describe('property access', function() { 387 | 388 | const moddle = createModel([ 389 | 'properties', 390 | 'properties-extended' 391 | ]); 392 | 393 | 394 | describe('typed', function() { 395 | 396 | it('should access basic', function() { 397 | 398 | // when 399 | const element = moddle.create('props:ComplexCount', { 400 | count: 10 401 | }); 402 | 403 | // then 404 | expect(element.get('count')).to.eql(10); 405 | expect(element.get('props:count')).to.eql(10); 406 | 407 | // available under base name 408 | expect(element.count).to.exist; 409 | }); 410 | 411 | 412 | it('should access refined property, created via base name', function() { 413 | 414 | // when 415 | const element = moddle.create('ext:ExtendedComplex', { 416 | count: 10 417 | }); 418 | 419 | // then 420 | expect(element.get('numCount')).to.eql(10); 421 | expect(element.get('ext:numCount')).to.eql(10); 422 | expect(element.get('count')).to.eql(10); 423 | expect(element.get('props:count')).to.eql(10); 424 | 425 | // available under refined name 426 | expect(element.numCount).to.eql(10); 427 | expect(element.count).not.to.exist; 428 | }); 429 | 430 | 431 | it('should access refined property, created via refined name', function() { 432 | 433 | // when 434 | const element = moddle.create('ext:ExtendedComplex', { 435 | numCount: 10 436 | }); 437 | 438 | // then 439 | expect(element.get('numCount')).to.eql(10); 440 | expect(element.get('ext:numCount')).to.eql(10); 441 | expect(element.get('count')).to.eql(10); 442 | expect(element.get('props:count')).to.eql(10); 443 | 444 | // available under refined name 445 | expect(element.numCount).to.eql(10); 446 | expect(element.count).not.to.exist; 447 | }); 448 | 449 | 450 | it('should access global name', function() { 451 | 452 | // when 453 | const element = moddle.create('props:ComplexCount', { 454 | ':xmlns': 'http://foo' 455 | }); 456 | 457 | // then 458 | expect(element.get(':xmlns')).to.eql('http://foo'); 459 | expect(element.get('xmlns')).to.eql('http://foo'); 460 | 461 | // available as extension attribute 462 | expect(element.$attrs).to.have.property('xmlns'); 463 | }); 464 | 465 | 466 | it('should access global name (no prefix)', function() { 467 | 468 | // when 469 | const element = moddle.create('props:ComplexCount', { 470 | 'xmlns': 'http://foo' 471 | }); 472 | 473 | // then 474 | expect(element.get(':xmlns')).to.eql('http://foo'); 475 | expect(element.get('xmlns')).to.eql('http://foo'); 476 | 477 | // available as extension attribute 478 | expect(element.$attrs).to.have.property('xmlns'); 479 | }); 480 | 481 | }); 482 | 483 | 484 | describe('any', function() { 485 | 486 | it('should access property', function() { 487 | 488 | // when 489 | const element = moddle.createAny('foo:Bar', 'http://tata', { 490 | count: 10 491 | }); 492 | 493 | // then 494 | expect(element['count']).to.eql(10); 495 | }); 496 | 497 | }); 498 | 499 | }); 500 | 501 | 502 | describe('property access (lax)', function() { 503 | 504 | const moddle = createModel([ 505 | 'properties' 506 | ], { 507 | strict: false 508 | }); 509 | 510 | 511 | describe('typed', function() { 512 | 513 | it('should access unknown attribute', function() { 514 | 515 | // when 516 | const element = moddle.create('props:ComplexCount', { 517 | foo: 'bar' 518 | }); 519 | 520 | // then 521 | expect(element.get('foo')).to.eql('bar'); 522 | }); 523 | 524 | }); 525 | 526 | }); 527 | 528 | 529 | describe('property access (strict)', function() { 530 | 531 | const moddle = createModel([ 532 | 'properties' 533 | ], { 534 | strict: true 535 | }); 536 | 537 | 538 | it('should configure in strict mode', function() { 539 | 540 | // then 541 | expect(moddle.config.strict).to.be.true; 542 | }); 543 | 544 | 545 | describe('typed', function() { 546 | 547 | it('should access basic', function() { 548 | 549 | // when 550 | const element = moddle.create('props:ComplexCount', { 551 | count: 10 552 | }); 553 | 554 | // then 555 | expect(element.get('count')).to.eql(10); 556 | expect(element.get('props:count')).to.eql(10); 557 | 558 | // available under base name 559 | expect(element.count).to.exist; 560 | }); 561 | 562 | 563 | it('should access global name', function() { 564 | 565 | // when 566 | const element = moddle.create('props:ComplexCount', { 567 | ':xmlns': 'http://foo' 568 | }); 569 | 570 | // then 571 | expect(element.get(':xmlns')).to.eql('http://foo'); 572 | 573 | expect(() => { 574 | element.get('xmlns'); 575 | }).to.throw(/unknown property on /); 576 | 577 | // available as extension attribute 578 | expect(element.$attrs).to.have.property('xmlns'); 579 | }); 580 | 581 | 582 | it('fail accessing unknown property', function() { 583 | 584 | // when 585 | const element = moddle.create('props:ComplexCount'); 586 | 587 | // then 588 | expect(() => { 589 | element.get('foo'); 590 | }).to.throw(/unknown property on /); 591 | 592 | expect(() => { 593 | element.set('foo', 10); 594 | }).to.throw(/unknown property on /); 595 | }); 596 | 597 | 598 | it('fail instantiating with unknown property', function() { 599 | 600 | // then 601 | expect(() => { 602 | moddle.create('props:ComplexCount', { 603 | foo: 10 604 | }); 605 | }).to.throw(/unknown property on /); 606 | }); 607 | 608 | }); 609 | 610 | }); 611 | 612 | }); 613 | -------------------------------------------------------------------------------- /test/spec/ns.js: -------------------------------------------------------------------------------- 1 | import expect from '../expect.js'; 2 | 3 | import { 4 | parseName 5 | } from '../../lib/ns.js'; 6 | 7 | 8 | describe('ns', function() { 9 | 10 | describe('parseName', function() { 11 | 12 | it('should parse namespaced name', function() { 13 | expect(parseName('asdf:bar')).to.jsonEqual({ 14 | name: 'asdf:bar', 15 | prefix: 'asdf', 16 | localName: 'bar' 17 | }); 18 | }); 19 | 20 | 21 | it('should parse localName (with default ns)', function() { 22 | expect(parseName('bar', 'asdf')).to.jsonEqual({ 23 | name: 'asdf:bar', 24 | prefix: 'asdf', 25 | localName: 'bar' 26 | }); 27 | }); 28 | 29 | 30 | it('should parse non-ns name', function() { 31 | expect(parseName('bar')).to.jsonEqual({ 32 | name: 'bar', 33 | prefix: undefined, 34 | localName: 'bar' 35 | }); 36 | }); 37 | 38 | 39 | it('should handle invalid input', function() { 40 | expect(function() { 41 | parseName('asdf:foo:bar'); 42 | }).to.throw(); 43 | }); 44 | 45 | }); 46 | 47 | }); -------------------------------------------------------------------------------- /test/spec/properties.js: -------------------------------------------------------------------------------- 1 | import { forEach } from 'min-dash'; 2 | 3 | import expect from '../expect.js'; 4 | 5 | import { 6 | createModelBuilder, 7 | getEffectiveDescriptor 8 | } from '../helper.js'; 9 | 10 | 11 | describe('properties', function() { 12 | 13 | var createModel = createModelBuilder('test/fixtures/model/'); 14 | var model = createModel([ 'properties', 'properties-extended' ]); 15 | 16 | 17 | describe('descriptor', function() { 18 | 19 | it('should provide id property', function() { 20 | 21 | // when 22 | var Complex = model.getType('props:Complex'); 23 | 24 | var descriptor = model.getElementDescriptor(Complex); 25 | var idProperty = descriptor.propertiesByName.id; 26 | 27 | // then 28 | expect(idProperty).to.exist; 29 | expect(idProperty.isId).to.be.true; 30 | 31 | expect(descriptor.idProperty).to.eql(idProperty); 32 | }); 33 | 34 | 35 | it('should provide body property', function() { 36 | 37 | // when 38 | var SimpleBody = model.getType('props:SimpleBody'); 39 | 40 | var descriptor = model.getElementDescriptor(SimpleBody); 41 | var bodyProperty = descriptor.propertiesByName.body; 42 | 43 | // then 44 | expect(bodyProperty).to.exist; 45 | expect(bodyProperty.isBody).to.be.true; 46 | 47 | expect(descriptor.bodyProperty).to.eql(bodyProperty); 48 | }); 49 | 50 | 51 | it('should NOT provide default id', function() { 52 | 53 | // when 54 | var SimpleBody = model.getType('props:SimpleBody'); 55 | 56 | var descriptor = model.getElementDescriptor(SimpleBody); 57 | var idProperty = descriptor.propertiesByName.id; 58 | 59 | // then 60 | expect(idProperty).not.to.exist; 61 | }); 62 | 63 | 64 | describe('should inherit', function() { 65 | 66 | it('single parent', function() { 67 | 68 | // when 69 | var descriptor = getEffectiveDescriptor(model, 'ext:Root'); 70 | 71 | var propertiesByName = descriptor.propertiesByName; 72 | 73 | // then 74 | expect(propertiesByName).to.include.keys([ 75 | 'any', 76 | 'elements' 77 | ]); 78 | 79 | // then 80 | expect(propertiesByName['elements']).to.have.property('inherited', true); 81 | expect(propertiesByName['any']).to.have.property('inherited', true); 82 | }); 83 | 84 | 85 | it('multiple parents', function() { 86 | 87 | // when 88 | var descriptor = getEffectiveDescriptor(model, 'props:MultipleSuper'); 89 | 90 | var propertiesByName = descriptor.propertiesByName; 91 | 92 | // then 93 | expect(propertiesByName).to.include.keys([ 94 | 'id', 95 | 'body' 96 | ]); 97 | 98 | // then 99 | expect(propertiesByName['id']).to.have.property('inherited', true); 100 | expect(propertiesByName['body']).to.have.property('inherited', true); 101 | }); 102 | 103 | }); 104 | 105 | }); 106 | 107 | 108 | describe('instance', function() { 109 | 110 | it('should set simple properties in constructor', function() { 111 | 112 | // when 113 | var attributes = model.create('props:Attributes', { 114 | id: 'ATTR_1', 115 | booleanValue: false, 116 | integerValue: -1000 117 | }); 118 | 119 | // then 120 | // expect constructor to have set values 121 | expect(attributes.id).to.equal('ATTR_1'); 122 | expect(attributes.booleanValue).to.equal(false); 123 | expect(attributes.integerValue).to.equal(-1000); 124 | }); 125 | 126 | 127 | describe('should set collection properties in constructor', function() { 128 | 129 | it('referencing', function() { 130 | 131 | // given 132 | var reference1 = model.create('props:ComplexCount'); 133 | var reference2 = model.create('props:ComplexNesting'); 134 | 135 | // when 136 | var referencingCollection = model.create('props:ReferencingCollection', { 137 | references: [ reference1, reference2 ] 138 | }); 139 | 140 | // then 141 | expect(referencingCollection.references).to.jsonEqual([ reference1, reference2 ]); 142 | 143 | // TODO: validate not parent -> child relationship 144 | }); 145 | 146 | 147 | it('containment', function() { 148 | 149 | // given 150 | var child1 = model.create('props:ComplexCount'); 151 | var child2 = model.create('props:ComplexNesting'); 152 | 153 | // when 154 | var containedCollection = model.create('props:ContainedCollection', { 155 | children: [ child1, child2 ] 156 | }); 157 | 158 | // then 159 | expect(containedCollection.children).to.jsonEqual([ child1, child2 ]); 160 | 161 | // TODO: establish parent relationship 162 | }); 163 | 164 | }); 165 | 166 | 167 | describe('should provide default values', function() { 168 | 169 | it('local', function() { 170 | 171 | // given 172 | var Attributes = model.getType('props:Attributes'); 173 | 174 | // when 175 | var instance = new Attributes(); 176 | 177 | // then 178 | expect(instance.defaultBooleanValue).to.equal(true); 179 | }); 180 | 181 | 182 | it('inherited', function() { 183 | 184 | // given 185 | var SubAttributes = model.getType('props:SubAttributes'); 186 | 187 | // when 188 | var instance = new SubAttributes(); 189 | 190 | // then 191 | expect(instance.defaultBooleanValue).to.equal(true); 192 | }); 193 | 194 | }); 195 | 196 | 197 | it.skip('should set collection properties in constructor'); 198 | 199 | 200 | it('should lazy init collection properties', function() { 201 | 202 | // given 203 | var Root = model.getType('props:Root'); 204 | var instance = new Root(); 205 | 206 | // assume 207 | expect(instance.any).not.to.exist; 208 | 209 | // when 210 | var any = instance.get('props:any'); 211 | 212 | // then 213 | expect(any).to.eql([]); 214 | expect(instance.any).to.equal(any); 215 | }); 216 | 217 | 218 | describe('set', function() { 219 | 220 | it('should set property', function() { 221 | 222 | // given 223 | var instance = model.create('props:Attributes'); 224 | 225 | // when 226 | instance.set('id', 'ATTR_1'); 227 | 228 | // then 229 | expect(instance.id).to.equal('ATTR_1'); 230 | }); 231 | 232 | 233 | it('should set property (ns)', function() { 234 | 235 | // given 236 | var instance = model.create('props:Attributes'); 237 | 238 | // when 239 | instance.set('props:booleanValue', true); 240 | instance.set('props:integerValue', -1000); 241 | 242 | // then 243 | expect(instance.booleanValue).to.equal(true); 244 | expect(instance.integerValue).to.equal(-1000); 245 | }); 246 | 247 | 248 | it('should set extension property', function() { 249 | 250 | // given 251 | var instance = model.create('props:Attributes'); 252 | 253 | // when 254 | instance.set('foo', 'bar'); 255 | 256 | // then 257 | expect(instance.$attrs).to.have.property('foo', 'bar'); 258 | expect(instance).not.to.have.keys('foo'); 259 | }); 260 | 261 | 262 | it('should set extension property (ns)', function() { 263 | 264 | // given 265 | var instance = model.create('props:Attributes'); 266 | 267 | // when 268 | instance.set('namespace:foo', 'bar'); 269 | 270 | // then 271 | expect(instance.$attrs).to.have.property('namespace:foo', 'bar'); 272 | expect(instance).not.to.have.keys('foo', 'namespace:foo'); 273 | }); 274 | 275 | 276 | it('should reject empty string as property name', function() { 277 | 278 | // given 279 | var instance = model.create('props:Attributes'); 280 | 281 | // when 282 | var set = function() { 283 | instance.set('', 'foo'); 284 | }; 285 | 286 | // then 287 | expect(set).to.throw(TypeError, 'property name must be a non-empty string'); 288 | }); 289 | 290 | 291 | forEach([ 292 | false, 293 | true, 294 | undefined, 295 | null, 296 | NaN, 297 | Function, 298 | 0, 299 | 1, 300 | Infinity 301 | ], function(invalidName) { 302 | 303 | it('should reject non-string property name <' + invalidName + '>', function() { 304 | 305 | // given 306 | var instance = model.create('props:Attributes'); 307 | 308 | // when 309 | var set = function() { 310 | instance.set(invalidName, 'foo'); 311 | }; 312 | 313 | // then 314 | expect(set).to.throw(TypeError, 'property name must be a non-empty string'); 315 | }); 316 | 317 | }); 318 | }); 319 | 320 | 321 | describe('update', function() { 322 | 323 | it('should update property', function() { 324 | 325 | // given 326 | var attributes = model.create('props:Attributes', { id: 'ATTR_1' }); 327 | 328 | // when 329 | attributes.set('id', 'ATTR_23'); 330 | 331 | // then 332 | expect(attributes.id).to.equal('ATTR_23'); 333 | }); 334 | 335 | 336 | it('should update property (ns)', function() { 337 | 338 | // given 339 | var attributes = model.create('props:Attributes', { 'props:integerValue': -1000 }); 340 | 341 | // when 342 | attributes.set('props:integerValue', 1024); 343 | 344 | // then 345 | expect(attributes.integerValue).to.equal(1024); 346 | }); 347 | 348 | 349 | it('should update extension property', function() { 350 | 351 | // given 352 | var attributes = model.create('props:Attributes', { 'foo': 'bar' }); 353 | 354 | // when 355 | attributes.set('foo', 'baz'); 356 | 357 | // then 358 | expect(attributes.$attrs.foo).to.equal('baz'); 359 | }); 360 | 361 | 362 | it('should update extension property (ns)', function() { 363 | 364 | // given 365 | var attributes = model.create('props:Attributes', { 'foo:bar': 'baz' }); 366 | 367 | // when 368 | attributes.set('foo:bar', 'qux'); 369 | 370 | // then 371 | expect(attributes.$attrs).to.have.property('foo:bar', 'qux'); 372 | }); 373 | 374 | }); 375 | 376 | 377 | describe('unset', function() { 378 | 379 | it('should unset property', function() { 380 | 381 | // given 382 | var attributes = model.create('props:Attributes', { id: 'ATTR_1' }); 383 | 384 | // assume 385 | expect(attributes.id).to.equal('ATTR_1'); 386 | 387 | // when 388 | attributes.set('id', undefined); 389 | 390 | // then 391 | expect(attributes).not.to.have.property('id'); 392 | }); 393 | 394 | 395 | it('should unset property (ns)', function() { 396 | 397 | // given 398 | var attributes = model.create('props:Attributes', { 399 | 'props:integerValue': -1000 400 | }); 401 | 402 | // assume 403 | expect(attributes.integerValue).to.equal(-1000); 404 | 405 | // when 406 | attributes.set('props:integerValue', undefined); 407 | 408 | // then 409 | expect(attributes).not.to.have.keys('integerValue', 'props:integerValue'); 410 | }); 411 | 412 | 413 | it('should unset extension property', function() { 414 | 415 | // given 416 | var attributes = model.create('props:Attributes', { 417 | 'foobar': 42 418 | }); 419 | 420 | // assume 421 | expect(attributes.$attrs.foobar).to.equal(42); 422 | 423 | // when 424 | attributes.set('foobar', undefined); 425 | 426 | // then 427 | expect(attributes.$attrs).not.to.have.keys('foobar'); 428 | }); 429 | 430 | 431 | it('should unset extension property (ns)', function() { 432 | 433 | // given 434 | var attributes = model.create('props:Attributes', { 435 | 'foo:bar': 42 436 | }); 437 | 438 | // assume 439 | expect(attributes.$attrs['foo:bar']).to.equal(42); 440 | 441 | // when 442 | attributes.set('foo:bar', undefined); 443 | 444 | // then 445 | expect(attributes.$attrs).not.to.have.keys('foo:bar'); 446 | }); 447 | 448 | }); 449 | 450 | }); 451 | 452 | 453 | describe('should redefine property', function() { 454 | 455 | it('descriptor', function() { 456 | 457 | // given 458 | 459 | // when 460 | var BaseWithId = model.getType('props:BaseWithId'); 461 | var BaseWithNumericId = model.getType('props:BaseWithNumericId'); 462 | 463 | var baseDescriptor = BaseWithId.$descriptor; 464 | var redefinedDescriptor = BaseWithNumericId.$descriptor; 465 | 466 | var originalIdProperty = baseDescriptor.propertiesByName.id; 467 | 468 | var refinedIdProperty = redefinedDescriptor.propertiesByName.id; 469 | var numericIdProperty = redefinedDescriptor.propertiesByName.idNumeric; 470 | 471 | // then 472 | expect(refinedIdProperty).not.to.jsonEqual(originalIdProperty); 473 | 474 | expect(refinedIdProperty).to.exist; 475 | expect(refinedIdProperty).to.eql(numericIdProperty); 476 | }); 477 | 478 | 479 | describe('instance', function() { 480 | 481 | it('init in constructor', function() { 482 | 483 | // given 484 | var BaseWithNumericId = model.getType('props:BaseWithNumericId'); 485 | 486 | // when 487 | var instance = new BaseWithNumericId({ 'id': 1000 }); 488 | 489 | // then 490 | expect(instance.idNumeric).to.equal(1000); 491 | }); 492 | 493 | 494 | it('access via #get', function() { 495 | 496 | // given 497 | var BaseWithNumericId = model.getType('props:BaseWithNumericId'); 498 | 499 | // when 500 | var instance = new BaseWithNumericId({ 'id': 1000 }); 501 | 502 | // then 503 | expect(instance.get('props:idNumeric')).to.equal(1000); 504 | }); 505 | 506 | 507 | it('access via #get + original name', function() { 508 | 509 | // given 510 | var BaseWithNumericId = model.getType('props:BaseWithNumericId'); 511 | 512 | // when 513 | var instance = new BaseWithNumericId({ 'id': 1000 }); 514 | 515 | // then 516 | expect(instance.get('props:id')).to.equal(1000); 517 | }); 518 | 519 | 520 | it('should return $attrs property on non-metamodel defined property access', function() { 521 | 522 | // given 523 | var BaseWithNumericId = model.getType('props:BaseWithNumericId'); 524 | 525 | // when 526 | var instance = new BaseWithNumericId({ 'id': 1000 }); 527 | 528 | instance.$attrs.unknown = 'UNKNOWN'; 529 | 530 | // then 531 | expect(instance.get('unknown')).to.eql('UNKNOWN'); 532 | }); 533 | 534 | }); 535 | 536 | }); 537 | 538 | 539 | describe('integration', function() { 540 | 541 | var proxyConfig = { 542 | get(target, property) { 543 | return new Proxy(target[property], proxyConfig); 544 | } 545 | }; 546 | 547 | it('should support proxies', function() { 548 | var Complex = model.getType('props:Complex'); 549 | 550 | // when 551 | var proxy = new Proxy(Complex, proxyConfig); 552 | 553 | // then 554 | expect(proxy.$descriptor).to.exist; 555 | }); 556 | 557 | }); 558 | 559 | }); 560 | -------------------------------------------------------------------------------- /test/spec/schema.js: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import FastGlob from 'fast-glob'; 3 | 4 | import { readFile } from '../helper.js'; 5 | import expect from '../expect.js'; 6 | 7 | 8 | describe('JSON schema', function() { 9 | 10 | let validator; 11 | 12 | before(function() { 13 | const schema = readFile('resources/schema/moddle.json'); 14 | 15 | validator = new Ajv().compile(JSON.parse(schema)); 16 | }); 17 | 18 | 19 | for (const file of FastGlob.globSync('test/fixtures/model/**/*.json')) { 20 | 21 | it(`should validate fixture: ${file}`, function() { 22 | 23 | // given 24 | const model = JSON.parse(readFile(file)); 25 | 26 | // then 27 | expect(validator(model)).to.be.true; 28 | }); 29 | 30 | } 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /test/spec/types.js: -------------------------------------------------------------------------------- 1 | import expect from '../expect.js'; 2 | 3 | import { 4 | coerceType 5 | } from '../../lib/types.js'; 6 | 7 | 8 | describe('Types', function() { 9 | 10 | describe('coerceType', function() { 11 | 12 | it('should convert Real', function() { 13 | expect(coerceType('Real', '420')).to.eql(420.0); 14 | }); 15 | 16 | 17 | it('should convert Real (-0.01)', function() { 18 | expect(coerceType('Real', '-0.01')).to.eql(-0.01); 19 | }); 20 | 21 | 22 | it('should convert Boolean (true)', function() { 23 | expect(coerceType('Boolean', 'true')).to.equal(true); 24 | }); 25 | 26 | 27 | it('should convert Boolean (false)', function() { 28 | expect(coerceType('Boolean', 'false')).to.equal(false); 29 | }); 30 | 31 | 32 | it('should convert Integer', function() { 33 | expect(coerceType('Integer', '12012')).to.equal(12012); 34 | }); 35 | 36 | 37 | it('should NOT convert complex', function() { 38 | var complexElement = { a: 'A' }; 39 | expect(coerceType('Element', complexElement)).to.equal(complexElement); 40 | }); 41 | 42 | }); 43 | 44 | }); 45 | 46 | 47 | --------------------------------------------------------------------------------