@schematize/metamodel
v0.4.6
Published
Schematize Metamodel Library
Readme
Metamodel
@schematize/metamodel is a javascript implementation of the "UML Metamodel".
UML
Let's first talk about the "UML" in UML Metamodel.
UML stands for "Unified Modeling Language". UML is an international standard managed by the Object Management Group (OMG). UML has also been published by the International Organization for Standardization (ISO) and the International Electrotechnical Commission (IEC) as the ISO/IEC 19501 standard. (See ISO/IEC 19505-1:2012)
"The objective of UML is to provide system architects, software engineers, and software developers with tools for analysis, design, and implementation of software-based systems as well as for modeling business and similar processes."
In short, UML is a modeling language standard for describing systems and processes.
Metamodel
Okay, what about the "Metamodel" portion of UML Metamodel?
The prefix "meta" usually means "about itself" or "above" or "beyond". You may have heard the phrase "Metadata is data about data" or "data that describes data". Well, a Metamodel is a model of models.
To put it all together. The UML Metamodel is a model for modeling models. To take it full circle, since the UML Metamodel is a model, it models itself. 🤯 Inception!
This library uses and implements the UML Metamodel described here: https://www.omg.org/spec/UML/2.5.1.
Side-note
Schemas and Models are a core concept of systems architecture, and a rock-solid stable foundation upon which to build everything. Schematizing something is the act or process of reducing to a schema or model. Hence the reason behind the name name "Schematize". 😉 It all started here.
Features
Ability to model models
Classical and/or Prototypal inheritance pattern support
OOP and/or Functional programming patterns
Performance
No external dependencies (only 2 @schematize dependencies)
Cross-platform.
- Modern Browsers (Chrome, Safari, Firefox, Edge)
- Node
- Deno
Semantic mapping of API names to real programming concepts (Class really means Class, etc.)
typeof (new Metamodel.StructuredClassifiers.Class()) === 'function';- Inheritance works through prototypal inheritance just as it does in plain javascript.
const Class = Metamodel.StructuredClassifiers.Class;
class MyClass extends Class.prototype {}
const instance = new MyClass();
console.log(instance instanceof MyClass); // true
console.log(instance instanceof Class.prototype); // trueinstanceofworks the same as it would in plain javascript.
(new MyClass() instanceof MyClass) === true;- Supports composition while also supporting inheritance. Pick whatever style fits the job.
const Class = Metamodel.StructuredClassifiers.Class;
const Interface = Metamodel.SimpleClassifiers.Interface;
// Inheritance
const Person = new Class({
name: 'Person'
});
const Employee = new Class({
name: 'Employee',
generalization: [{ general: Person }] // extends Person
});
const employee = new Employee({ name: 'Ben' });
console.log(employee instanceof Employee); // true
console.log(employee instanceof Person); // true
// Composition
const ICanFly = new Interface({
name: 'ICanFly',
});
const ICanSwim = new Interface({
name: 'ICanSwim',
});
const Penguin = new Class({
name: 'Penguin',
interfaceRealization: [{ contract: ICanSwim}]
});
const Parrot = new Class({
name: 'Parrot',
interfaceRealization: [{ contract: ICanFly}]
});
const Duck = new Class({
name: 'Duck',
interfaceRealization: [{ contract: ICanFly }, { contract: ICanSwim}]
});
const penguin = new Penguin({});
const parrot = new Parrot({});
const duck = new Duck({});
console.log(penguin instanceof ICanFly); // false
console.log(penguin instanceof ICanSwim); // true
console.log(parrot instanceof ICanFly); // true
console.log(parrot instanceof ICanSwim); // false
console.log(duck instanceof ICanFly); // true
console.log(duck instanceof ICanSwim); // true- Types (Classes, Interfaces, DataTypes, Enumerations, etc.) as well as their respective instances can all be created with or without the
newkeyword.
// with new
const MyClass = new Metamodel.StructuredClassifiers.Class();
// without new
const MyClass = Metamodel.StructuredClassifiers.Class();- Types (Classes, Interfaces, DataTypes, Enumerations, etc.) are first class citizens and can be treated as objects, instances, or types in their respective contexts.
const MyClass = new Metamodel.StructuredClassifiers.Class();
// acting as an instance of Metamodel.StructuredClassifiers.Class
MyClass.property = 'object context';
// acting as a class that can create instances
const instance = MyClass({});- Built on top of @schematize/instance.js Allows ability to hook into events and take further action (saving or updating state, persisting, doing additional logic)
const MyClass = new Metamodel.StructuredClassifiers.Class();
const instance = new MyClass({});
instance.on('set', (evt) => {
console.log(evt.detail.property + ':' + evt.detail.value);
});
instance.set('name', 'theValue'); // logs: 'name:theValue'- Allows the ability to mimic core library behavior at the meta level.
const ClassCreator = new Metamodel.StructuredClassifiers.Class({
name: 'ClassCreator',
metaclass: 'Class'
});
const AnotherClass = new ClassCreator({
name: 'AnotherClass',
});
const instance = new AnotherClass({});- Exposes the ability to extend functionality in whatever way makes most sense, either through extending prototypes or by adding event listening functionality.
// Extension by prototype inheritance
Class.prototype.save = function () {
// implement code here that all Class instances will inherit.
}
// Extension by event listeners
Class.prototype.on('set', (evt) => {
// implement code that does something here
});- Implements EventTarget Interface as well as other common patters for listening to and firing events. (More details below)
instance.addEventListener('set', (evt) => {
// evt.detail
});- Feels like you are using plain javascript Functions, Classes, and Objects. No strange patters of creating instances, class extension, etc. that are not already native patterns in Javascript.
// Classes and Functions in Plain JS
class JSClass {
}
function JSFunctionConstructor() {
}
// Classes and Functions in Schematize.js
const MetamodelClass = Metamodel.StructuredClassifiers.Class({
name: 'MetamodelClass'
});
// Objects in Plain JS
let jsClassInstance = new JSClass();
let jsFunctionInstance = new JSFunction();
jsClassInstance.property = 'value';
// Objects in Schematize.js
let schematizeInstance = new MetamodelClass();
schematizeInstance.property = 'value';
- Schematize.js plays nicely with plain javascript Functions, Classes, and Objects. You aren't locked into all one or the other.
// use your native JS Class and extend a class build from Schematize.js.
const MetamodelClass = Metamodel.StructuredClassifiers.Class({
name: 'MetamodelClass'
});
class JSClass extends MetamodelClass {}
let instance = new JSClass();
instance instanceof MetamodelClass; // trueAbility to automatically handle any of these types of associations:
- one-to-one (1..1)
- one-to-many (1..*)
- many-to-many (*..*)
- and any other variation of those and variation in multiplicity upper or lower values. (0..1, 0..*, 2..2, 2..*, etc.)
Automatic "magic" handling of opposite ends of an association.
// assuming there is a one-to-many Association between Employee and Assignment
// Employee [1] <==> [*] Assignment
const employee = new Employee();
const assignment1 = new Assignment();
// (assuming proxies, but can use .set function and will work as well)
assignment1.employee = employee;
// employee.assignments Collection now automatically contains the assignment
// without explicit assignment
const assignment2 = new Assignment();
employee.assignments.push(assignment2);
// assignment2.employee now automatically contains the employee without explicit
// assignment- Automatic "linking" of instances by simply creating an instance of an Association (or a link):
// assuming the above model with this association definition (not complete):
let A_employee_assignment = new Association({
memberEnds: [{
__id__: 'PROPERTY_EMPLOYEE_ABC',
class: Assignment,
name: 'employee',
type: Employee,
lowerValue: new UnlimitedNatural(1),
upperValue: new UnlimitedNatural(1) // 0..*
}, {
__id__: 'PROPERTY_ASSIGNMENTS_DEF',
class: Employee,
name: 'assignments',
type: Assignment,
lowerValue: new UnlimitedNatural(0),
upperValue: new UnlimitedNatural('*') // 0..*
}]
});
// ...
let employee = Employee({ __id__: 'EMPLOYEE_123' });
let assignment = Assignment({ __id__: 'ASSIGNMENT_456' });
// creating an instance of the Association (or generally what's referred to as
// a "link") automatically assigns them together
new A_employee_assignment({
PROPERTY_EMPLOYEE_ABC: 'EMPLOYEE_123',
PROPERTY_ASSIGNMENTS_DEF: 'ASSIGNMENT_456',
});
// employee.assignments Collection now automatically contains the assignment
// without explicit assignment
// assignment.employee also automatically is set to the employee instance (along the same lines) Bi-directional reference support for traversing both directions of an association/relationship between instances.
Automatic coersion attempts based on the property types.
Automatic type checking and validation
Ability to serialize an entire model to JSON, store it, and then later "resurrect" it back into existence dynamically to be able to continue to create instances, links, etc.
Just as Instance.js allows, automatic "magical" handling of all of the above functionality when using plain object access, assignment, or deletion just like you would with plain objects or arrays.
Dependencies
@schematize/instance.js: Core instance management@schematize/refs: JavaScript reference implementations
Installation
npm install @schematize/metamodelUsage
Include script
Start by including the script that suits your needs and environment:
Browser global
<script src="node_modules/@schematize/metamodel/dist/iife/Metamodel.min.js"></script>
<script>console.log(Metamodel);</script>Browser module
import Metamodel from 'node_modules/@schematize/metamodel/dist/es/Metamodel.min.js';Node require
const Metamodel = require('@schematize/metamodel');Node module
import Metamodel from '@schematize/metamodel';Full UML Metamodel Functionality
From there, you will need to "resurrect" a UML model into existence (basically hydrate it) using a interchange JSON file or string. For this you can check out the @schematize/model-json repository. Here is an example:
// For Metamodel to work, you must use at bare-minimum UML and PrimitiveTypes
import {
jsonUML,
jsonPrimitiveTypes
} from '@schematize/model-json';
// Resurrect a UML metamodel from JSON
const [
UML,
PrimitiveTypes
] = Metamodel.resurrect(
jsonUML,
jsonPrimitiveTypes
);
// optionally assign Metamodel Core properties to the UML object
Object.assign(UML, Metamodel);
// UML is the Metamodel
Metamodel = UML;
// Access metamodel elements through the UML Metamodel package
const Class = Metamodel.StructuredClassifiers.Class;
const Association = Metamodel.StructuredClassifiers.Association;
const Property = Metamodel.Classification.Property;
const Package = Metamodel.Packages.Package;What's cool about this is it also means that you can pass your own custom UML model json interchange if you wish. As long as it adheres to the core UML Metamodel, your modifed Metamodel will be "resurrected" into existence and contain all of your modifications. Meta-meta-meta programming. 😱
Essential Functionality only
If you are only interested in the essential functionality of the Metamodel library without the UML Metamodel, instead of import Metamodel you only need to import Essential from src/Essential.mjs.
// This will set up all of the essential Metamodel event listeners, and add
// functionality to Instance.prototype, Collection.prototype, etc. Even if
// you are not using Essential directly, this call is still necessary.
import * as Essential from '@schematize/metamodel/src/Essential.mjs';
import Class_prototype from '@schematize/metamodel/src/Class/prototype.mjs';
// build your model using regular ES5 function constructors or ES6 classes
class MyClass extends Class_prototype {
constructor(properties = {}) {
properties.__type__ = MyClass;
return super(properties);
}
static get ownedAttribute () {
return [{
class: MyClass,
name: `myStringProperty`,
type: String,
}, {
class: MyClass,
name: `myBooleanProperty`,
type: Number,
}];
}
}
// use your model
let instance = MyClass();This is very useful and also performance optimized.
⚠️ The rest of this documentation assumes you are using the "Full UML Metamodel Functionality".
Create a new Class
There are 2 ways to create a new Class:
1) Instantiate a new Metamodel Class
const Class = Metamodel.StructuredClassifiers.Class;
const Association = Metamodel.StructuredClassifiers.Association;
let
Account,
Transaction;
Account = new Class({
name: 'Account',
ownedAttribute: [{
id: 2,
name: 'accountNumber',
type: Number,
}, {
name: 'name',
type: String,
}, {
name: 'active',
type: Boolean,
}, {
// Account has 0 to * (many) of the type "Transaction" which form the
// association "Account has many Transactions"
name: 'transactions',
type: Transaction,
lowerValue: 0,
upperValue: Infinity,
association: {
id: 3,
name: 'Account has many Transactions'
}
}],
});
Transaction = new Class({
name: 'Transaction',
ownedAttribute: [{
name: 'accountNumber',
type: Number,
}, {
name: 'date',
type: Date,
}, {
// Transaction has a min of 1 to a max of 1 of the type "Account" which
// form the association "Account has many Transactions"
name: 'account',
type: Account,
lowerValue: 1,
upperValue: 1,
association: {
id: 3,
}
}],
});
// an account instance
const account = new Account({
accountNumber: 1594905,
name: 'first thing',
active: true,
});
// a trasnaction instance
const transaction = new Transaction({
accountNumber: 1594905,
date: new Date('2019-03-15 12:13:03')
});
2) Point your Class's prototype chain at Metamodel.StructuredClassifiers.Class.prototype
const Class = Metamodel.StructuredClassifiers.Class;
const Association = Metamodel.StructuredClassifiers.Association;
const A_account_transaction = function A_account_transaction (properties = {}) {
properties.__type__ = properties.__type__ || A_account_transaction;
return Association.prototype(properties, this);
};
A_account_transaction.prototype = Object.create(Association.prototype.prototype);
A_account_transaction.prototype.constructor = A_account_transaction;
//--------------------------------------------
// You can do this by using ES5 class syntax:
const Account = function Account (properties) {
// explicitly set type
properties.__type__ = Account;
return Class.prototype(properties, this);
};
Account.prototype = Object.create(Class.prototype.prototype);
Account.prototype.constructor = Account;
Account.ownedAttribute = [{
class: Account,
name: 'accountNumber',
type: Number,
}, {
class: Account,
name: 'name',
type: String,
}, {
class: Account,
name: 'active',
type: Boolean
}, {
class: Account,
// transactions
name: 'transactions',
// type
type: Transaction,
// has 0
lowerValue: 0,
// to many
upperValue: Infinity,
// to form this relationships
association: AccountTransactionAssociation,
}];
//-----------------------------------------------
// Or you can do this by using ES6 class syntax:
class Transaction extends Schematize.Class.prototype {
constructor(properties) {
properties.__type__ = Transaction;
return super(properties);
}
static get ownedAttributes () {
return [
{
class: Transaction,
name: 'accountNumber',
type: Number,
},
{
class: Transaction,
name: 'date',
type: Date,
},
{
// this class
class: Transaction,
// account
name: 'account',
// type
type: Account,
// belongs to 1 (required to be tied to at least 1)
lowerValue: 1,
// to maximum of 1
upperValue: 1,
// to form this relationships
association: AccountTransactionAssociation,
}
];
}
}
A_account_transaction.memberEnd = [
Account.ownedAttribute[2],
Transaction.ownedAttribute[2]
];
const account = new Account({
accountNumber: 1594905,
name: 'Account Name',
active: true,
});
const transaction = new Transaction({
accountNumber: 1594905,
date: new Date('2019-03-15 12:13:03')
});@schematize/instance.js
The Schematize Metamodel library is built on top of the Schematize Instance.js library. Instance is the base class for all Classes and instances built with Metamodel. It therefore inherits all of the same functionality and is built on the same principles. Also, fun fact, Metamodel.StructuredClassifiers.Class.prototype.prototype IS Instance.prototype (described later).
Please see the README.md documentation there (https://www.npmjs.com/package/@schematize/instance.js) for the following:
__type__the "type" of an instance...closely relates to the Metamodel.CommonStructure.Type Metaclass.__id__the unique identifier given to every instanceEventTargetand it's corresponding methods:addEventListener,removeEventListener,dispatchEvent__dispatch__and how to configure event capturing/bubbling/propogation.Instancethe core base constructor function and the events it dispatches when creating instances:beforeInstance- event fired before any instance gets fully instantiatedinstance- after an instance is created, but before property assignment
Instance.prototypemethods which are inherited by all instances in Metamodel including:get- getting a value from a property of an instanceset- setting a value to a property of an instancedeleteProperty- deleting a property of an instancedefineProperty- defining a property on an instance (using standard Javascript descriptor syntax)assign- assigning values to multiple properties of an instancedestroy- destroying an instance
Instance Cache- the way single instances are kept and referenced everywhereBackrefs- the way deferred reference resolution worksProxies- internally how you can make simple operations like assigning, accessing, or deleting properties "magic" where they automatically trigger events and other functionality.Collections- An extension of native Arrays that can be observed, listened to, extended, and even proxiedCollection.prototypemethods which are used extensively in this library and for triggering change detection and other extended use cases:Collection.prototype.append- same asArray.prototype.push, but betterCollection.prototype.prepend- same asArray.prototype.unshift, but betterCollection.prototype.removeLast- same asArray.prototype.pop, but betterCollection.prototype.removeFirst- same asArray.prototype.shift, but betterCollection.prototype.replace- same asArray.prototype.splice, but betterCollection.prototype.flip- same asArray.prototype.reverse, but betterCollection.prototype.order- same asArray.prototype.sort, but better
Metamodel Functionality
TODO: Auto-setting of the opposite TODO: Auto-linking via Associations TODO: etc.
Prototype Structure
Metamodel Javascript Prototype Structure
I feel it is very important to explain how the prototype model works. This will help you understand in more depth how inheritance works and lay the foundation for how to inherit, extend, of manipulate the library as you see fit.
The built-in javascript objects inherit according to this diagram.
Please note that Function.prototype is actually a "function" type:
typeof Function.prototype; // "function"
typeof Object.prototype; // "object"If you run this is any javascript environment (node, browser, etc.) you should see the following:
Object.getPrototypeOf(Function) === Function.prototype; // true
Function.__proto__ === Function.prototype; // true (same as above)
Object.getPrototypeOf(Object) === Function.prototype; // true
Object.__proto__ === Function.prototype; // true (same as above)
Object.getPrototypeOf(Function.prototype) === Object.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true (same as above)The beauty of this structure is that Object and Function are both instances of themselves.
Object instanceof Function; // true
Function instanceof Function; // true
Object instanceof Object; // true
Function instanceof Object; // trueThe UML Metamodel prototype model is similar.
Note that Class.prototype is also has a type of "function":
const Class = Metamodel.StructuredClassifiers.Class;
typeof Class.prototype; // "function"
typeof Object.prototype; // "object"Also note that Class.prototype.prototype is the Instance.prototype, but Instance.prototype.constructor is not Class.prototype.
const Class = Metamodel.StructuredClassifiers.Class;
typeof Class.prototype.prototype === Instance.prototype; // true
typeof Instance.prototype.constructor === Class.prototype; // falseOkay, so this feels strange right? Class.prototype.prototpe? Why two prototypes?
This is because fundamentally to Javascript, whenever you invoke a function constructor with the new keyword (or in other words, when you instantiate an instance of a class in javascript), a new object is created which points the __proto__ at the .prototype of the function constructor.
Without being comprehensive, here are a few examples of how the UML Metamodel Classes are similar to the plain built-ins:
const Element = Metamodel.CommonStructure.Element;
const Class = Metamodel.StructuredClassifiers.Class;
Object.getPrototypeOf(Element) === Class.prototype; // true
Element.__proto__ === Class.prototype; // true (same as above)
Object.getPrototypeOf(Class) === Class.prototype; // true
Class.__proto__ === Class.prototype; // true (same as above)
// etc.Again, the beauty of this structure is that Element, Class, etc. are all instances of themselves.
const Element = Metamodel.CommonStructure.Element;
const Class = Metamodel.StructuredClassifiers.Class;
Element instanceof Class; // true
Class instanceof Class; // true
Element instanceof Element; // true
Class instanceof Element; // trueThis is how we acheive the goal of being self-describing, just like UML itself is.
Prototype Hierarchy
With that foundation, here is prototype hierarchy of some of the most common Classes in the UML Metamodel:
And here is the same look, at the Class Hierarchy:
