NAV Navbar
TypeScript JavaScript

OCL.js

Travis Status devDependency Status License: MIT npm (scoped)

The Object Constraint Language (OCL) is a language for describing rules that apply to MOF conform modelling languages like UML. The OCL is a text based language that provides constraint and object query expressions that cannot be expressed by a meta modelling language.

The Object-Constraint-Language is used to add additional constraints to a given modelling language. Since there do exist implementations in Java (e.g. Eclipse Modelling Framework (EMF)) but not JavaScript, this is a basic OCL implementation in JavaScript.

This library does not claim to be fully compliant to the given OMG OCL definition and might have slight differences.

Usage

Installation

$ npm install @stekoe/ocl.js --save

OCL.js is entirely written in JavaScript and is published to npm so you can either install it with npm or yarn. It is designed to run either in an node environment or in the browser.

As alternative, you can also download the ocl.min.js file from GitHub and include that to your code.

Example: Basic usage

import { OclEngine } from '@stekoe/ocl.js';

class Person {
  private parents = [];
}

// Define OCL rule
const myOclExpression = `
    context Person
        inv: self.parents->forAll(p | p <> self)
`;

// Instantiate the OclEngine here
const oclEngine = OclEngine.create();

// Add your first OCL expression here
oclEngine.addOclExpression(myOclExpression);

// Evaluate an object obj against all know OCL expressions
const oclResult = oclEngine.evaluate(new Person());

// Prints 'true' to console!
console.log(oclResult.getResult());
const OclEngine = require("@stekoe/ocl.js").OclEngine;

// A simple class that represents a person
class Person {
  constructor() {
    this.parents = [];
  }
}

// Define OCL rule
const myOclExpression = `
    context Person
        inv: self.parents->forAll(p | p <> self)
`;

// Instantiate the OclEngine here
const oclEngine = OclEngine.create();

// Add your first OCL expression here
oclEngine.addOclExpression(myOclExpression);

// Evaluate an object obj against all know OCL expressions
const oclResult = oclEngine.evaluate(new Person());

// Prints 'true' to console!
console.log(oclResult.getResult());

When adding OCL.js via npm, you can start using it via importing the OCLEngine that is provided by “@stekoe/ocl.js”.

The resulting oclResult object contains three fields:

  1. result contains the actual result of the evaluation run as a boolean value
  2. namesOfFailedInvs contains the names of failed invariants or anonymous if none has been provided
  3. evaluatedContexts contains all ContextExpressions that have been evaluated

API

create() : OclEngine

const oclEngine = OclEngine.create();

Instead of using the constructor, this function can be used to create a new OclEngine instance.

setTypeDeterminer(fn : Function) : void

class MyCustomType {
  private _type: string;
}

oclEngine.setTypeDeterminer(obj => obj._type);

In order to let OCL.js determine the correct object instance for a context, there is a basic type detection implemented by default. The basic implementation tries to determine the type of the given object by accessing the property typeName. If that property is not available, the name of the constructor is about to be determined. If there is no type information available, Object will be returned as most abstract type.

There might be cases where the build-in type detection function is not sufficient, hence it is possible to provide a custom function for that. Assumed that the discriminator for an object type is part of the given instance (e.g. a field called _type) then a custom function which determines the actual type can be provided.

Let’s say there is a class MyCustomType that contains the field _type as discriminator.

Whenever the OCL engine has to check the context, the custom function that is passed to setTypeDeterminer will be called.

registerTypes(list : Map) : void

const customTypes = {
  "Person": class Person { /* implementation */ } 
}

oclEngine.registerTypes(customTypes);

In order to register custom types, this function takes a map as argument. The key of the map is used to lookup a specific type even if the code is minified. By adding custom types, ocl functions like oclIsTypeOf and oclIsKindOf work more properly.

registerEnum

const Gender = {
    FEMALE: 'female',
    MALE: 'male',
    OTHER: 'other'
}

oclEngine.registerEnum('Gender', Gender);

Whenever the use of enumerations is necessary, the enumeration has to be registered to the OCL Engine in order to let it parse the correct values.

Registering an enumeration is possible via the function registerEnum(name : string, values : object). Let's assume one has defined an enumeration for Gender, it has to be registered to the OCL Engine as follows:

addOclExpression(oclExpression : string, labels? : Array) : OclEngine

const oclExpression = `
    context Person inv: self.name->notEmpty()
`;
oclEngine.addOclExpression(oclExpression);
const oclExpression = `
    context Person inv: self.name->notEmpty()
    context Person inv: self.age > 0
`;
oclEngine.addOclExpression(oclExpression);

Via this function, new OCL expressions can be added to the engine. It is possible to define one context per function call or multiple contexts at one.

The second argument label can be used to tag OCL expressions. This is useful when just a subset of rules should be evaluated. The rules that should be used during evaluation phase can be specified when calling evaluate function.

addOclExpressions(oclExpressions : Array, labels? : Array) : OclEngine

const oclExpressions = [
  'context Person inv: self.name->notEmpty()',
  'context Person inv: self.age > 0'
];
oclEngine.addOclExpression(oclExpressions);

Instead of passing a big blob of text, containing all OCL rules, it is also possible to pass in an array of OCL expressions. For each OCL expression in the array, the function addOclExpression will be called.

removeOclExpression(oclExpression : string) : OclEngine

Removes an already registered OCL rule.

clearAllOclExpressions : OclEngine

Removes all registered OCL rules.

evaluate(obj : any, labels? : Array) : OclResult

This function runs the evaluation process for the given object obj. The OCL engine tries to determine the type of the given object and then runs all context definitions / rules that match the type. As a result an object is returned that offers three methods:

  1. getResult() : boolean: Returns the result of each and every OCL rule that has been run for the given object.
  2. getNamesOfFailedInvs() : Array<string>: Returns the names of invariants that have evaluated to false.
  3. getEvaluatedContexts() : Array<ContextExpression>: Returns all the context expressions that have been evaluated.

createQuery(query : string) : Expression

const expression = oclEngine.createQuery('self->any(i | i < 1)');
const result = oclEngine.evaluateQuery([1.2, 2.3, 5.2, 0.9], expression);
// result will now be [0.9]

Even though the OCL is created to check for constraints, a big part of it is about querying objects and object properties. Hence, OCL.js can also be used to query objects, as well, without having the need to run an evaluation.

evaluateQuery(obj : any, oclExpression : Expression) : any

Runs the given oclExpression on the object obj.

Examples

Despite the examples shown below, there are some examples included in the OCL.js respository. You can find examples for either Node.js or webpack.

Company

export class Company {
    name : String;
    employee: Person[];
    manager: Person;
}

export class Person {
    name : String;
    age : Number;
    isUnemployed: boolean;
}
import { OclEngine } from "@stekoe/ocl.js"
import { Company, Person } from "./company.js"

const oclEngine = OclEngine.create();

oclEngine.addOclExpression(`
    -- No one should work that long...
    context Company inv:
        self.employee->forAll(p : Person | p.age <= 65 )

    -- If a company has a manager, 
    -- the company has at least one employee.
    context Company
        inv: self.manager.isUnemployed = false
        inv: self.employee->notEmpty()
`);

let company = new Company();
company.employee.push(new Person());

const oclResult = oclEngine.evaluate(company);

Person

import { OclEngine } from "@stekoe/ocl.js"

class Person {
    name : String;
    age : Number;
    children : Person[];
    isMarried : Boolean;
    husband : Person;
    wife : Person;
}

const oclResult = OclEngine.create()
  .addOclExpression(`
      -- Check that underage persons are not married
      context Person inv:
          age < 18 implies isMarried = false

      -- Check that each and every children is younger than 18 years old
      context Person inv:
          children->select(age >= 18)->isEmpty()

      -- If a person is married, wife or husband has to be at least 18 years old
      context Person inv:
          self.wife->notEmpty() implies self.wife.age >= 18 and
          self.husband->notEmpty() implies self.husband.age >= 18

      -- If there are children, one should be named Stephan ;)
      context Person inv:
          self.children->exists(child | child.name = 'Stephan')
  `)
  .evaluate(new Person());

Implemented Language Features

Collection

AnyExpression

any(expr : OclExpression) : T
self.collection->any(i < 2)

Returns the first element that validates the given expression.

AppendExpression

append(elem : T) : Collection<T>
self.collection->append("string")

Appends the given element to the given collection and returns the extended collection.

AsSetExpression

asSet() : Collection
self.collection->asSet()

Returns the given collection as set, containing unique entries.

AtExpression

at(index : Number) : T
self.collection->at(2)

Returns the element of the collection at index index. Index starts at 1.

CollectExpression

When we want to specify a collection that is derived from some other collection, but which contains different objects from the original collection (i.e., it is not a sub-collection), we can use a collect operation. The collect operation uses the same syntax as the select and reject.

collect(expr : OclExpression) : Collection
self.children->collect(age)

ExistsExpression

exists(expr : OclExpression) : Boolean
self.collection->exists(i | i < 2)

Operation which checks whether a collection contains an element specified by expr.

FirstExpression

collection->first() : T
self.collection->first()

Returns the first element of the collection.

ForAllExpression

Many times a constraint is needed on all elements of a collection. The forAll operation in OCL allows specifying a Boolean expression, which must hold for all objects in a collection.

forAll(expr : oclExpression)

IsEmptyExpression

isEmpty() : Boolean
self.cars->isEmpty()

Returns true if self is empty, false otherwise.

IsUniqueExpression

isUnique(expr : oclExpression) : boolean
self.collection->isUnique(self > 3)

Returns true if the given expr evaluated on the body returns only different values.

LastExpression

last() : T
self.collection->last()

Returns the last element of the collection.

NotEmptyExpression

notEmpty() : Boolean
self.cars->notEmpty()

Returns true if self is not empty, false otherwise.

OneExpression

one(expr : oclExpression) : boolean
self.collection->one(age < 18)

Returns true of there is exactly one element matching the given expression, false otherwise.

RejectExpression

The reject operation specifies a subset of a collection. A reject is an operation on a collection and is specified using the arrow-syntax. This results in a collection that removes all the elements from collection for which the boolean-expression evaluates to true. To find the result of this expression, for each element in collection the expression boolean-expression is evaluated. If this evaluates to true, the element is excluded in the result collection, otherwise not.

reject(expr : oclExpression) : Collection
self.customer->reject(underage)

SelectExpression

The select operation specifies a subset of a collection. A select is an operation on a collection and is specified using the arrow-syntax. This results in a collection that contains all the elements from collection for which the boolean-expression evaluates to true. To find the result of this expression, for each element in collection the expression boolean-expression is evaluated. If this evaluates to true, the element is included in the result collection, otherwise not.

select(expr : oclExpression) : Collection
self.collection->select(item | item.name = "random")

SizeExpression

size() : Number
self.collection->size()

Returns the size of the given collection.

SumExpression

sum() : Number
self.jobs.salary->sum()

Returns the sum of all elements contained in self if they support the '+' operation.

UnionExpression

union(c : Collection) : Collection
self.collection->union(self.anotherCollection)

Returns a collection containing all elements of self and all elements of the passed in collection.

Context

ClassifierContextExpression

context <Type> (inv|def)

Define invariants and definitions on a given types

OperationContextExpression

context Person::kill() (pre|post)
context Person::setAge(age: number)
   pre: age > 0

The Operation Context Expression allows to define pre and or post conditions of functions.

PropertyContextExpression

context Person::age (init|derive)

A PropertyContextDefinition allows to initialize or derive a value for the targeted property.

Expressions

DefExpression

The Let expression allows a variable to be used in one OCL expression. To enable reuse of variables/operations over multiple OCL expressions one can use a Constraint with the stereotype «definition», in which helper variables/operations are defined. This «definition» Constraint must be attached to a Classifier and may only contain variable and/or operation definitions, nothing else. All variables and operations defined in the «definition» constraint are known in the same context as where any property of the Classifier can be used. Such variables and operations are attributes and operations with stereotype «OclHelper» of the classifier. They are used in an OCL expression in exactly the same way as normal attributes or operations are used. The syntax of the attribute or operation definitions is similar to the Let expression, but each attribute and operation definition is prefixed with the keyword ‘def’ as shown below.

context Person def:
income : Integer = self.job.salary->sum()

DeriveExpression

context Person::income : Integer
derive:  if underAge
 then (parents.income->sum() * 1/100).round()
 else job.salary->sum()
endif

A derived value expression is an expression that may be linked to a property.

EnumerationExpression

Resolves enumeration values.

IfExpression

The IfExpression allows to execute a statement if the given condition is truthy. Otherwise the else part is taken.

InitExpression

InvariantExpression

The OCL expression can be part of an Invariant which is a Constraint stereotyped as an «invariant». When the invariant is associated with a Classifier, the latter is referred to as a “type” in this clause. An OCL expression is an invariant of the type and must be true for all instances of that type at any time. (Note that all OCL expressions that express invariants are of the type Boolean.)

context Person inv:
self.age > 0

NativeJsFunctionCallExpression

OclIsKindOfExpression

oclIsKindOf(type : T) : Boolean

Checks if self is an instance of the class identified by the name

OclIsTypeOfExpression

oclIsTypeOf(s : String) : Boolean

Checks if self is an instance of exact the class identified by the name

OclIsUndefinedExpression

oclIsUndefined() : Boolean

Checks if self is not defined

OperationCallExpression

PackageDeclaration

In order to group and organise OCL constraints, packages can be used.

PostExpression

A condition that has to be fulfilled after the operation addressed by the parent OperationCallExpression has been executed.

PreExpression

A condition that has to be fulfilled before executing the operation addressed by the parent OperationCallExpression.

VariableExpression

Resolve variables. Simple values are returned as is (e.g. self.age: number), collections are aggregated.

Gate

AndExpression

false and true
A B A and B
false false false
false true false
true false false
true true true

ImpliesExpression

false implies true
A B A implies B
false false true
false true true
true false false
true true true

NotExpression

not false
A NOT A
true false
false true

OrExpression

false or true
A B A or B
false false false
false true true
true false true
true true true

XorExpression

false xor true
A B A xor B
false false false
false true true
true false true
true true false

Literal

BooleanExpression

NilExpression

NumberExpression

StringExpression

Math

AbsExpression

Number::abs () : Number
-2.abs() = 2

Returns the absolute value of self.

AdditionExpression

Symbol: +
1 + 2

Addition

DivExpression

Number::div ( i : Number ) : Number
3 div 2 = 1

Returns the integer quotient of the division of self by i.

DivideExpression

Symbol: /
17 / 2

Division

MaxExpression

Number::max ( i : Number ) : Number
6.max(3) = 6

Returns the greatest number of self and i.

MinExpression

Number::min ( i : Number ) : Number
6.max(3) = 3

Returns the lowest number of self and i.

ModuloExpression

Number::mod ( i : Number ) : Number
4 mod 2 = 0

Returns the number remainder of the division of self by i.

MultiplyExpression

Symbol: *
1 * 2

Multiply

PowerExpression

Symbol: ^
4 ^ 2

Power

RoundExpression

Number::round () : Number

Returns the nearest number to self.

SqrtExpression

Number::sqrt () : Number
9.sqrt() = 3

Returns the square root of self.

SubstractionExpression

Symbol: -
1 - 2

Substraction

String

ConcatExpression

String::concat (s : String) : String
self.name.concat("string")

Returns a string that is concatenated using source and body

IndexOfExpression

String::indexOf (s : String) : Number
self.name.indexOf("string")

Returns the index of the given string in self or 0 if it is not condained.

SubstringExpression

String::substring (start : Number, end : Number) : String
self.name.substring(0,2)

Returns a string containing all characters from self starting from index start up to index end included. Both start and end parameters should be contained between 1 and self.size() included. start cannot be greater than end.

ToIntegerExpression

String::toInteger () : Number
"3.414".toInteger()

Tries to convert a string to a number.

ToLowerCaseExpression

String:: toLowerCase () : String
self.name.toLowerCase()

Returns self as lower case string.

ToRealExpression

String:: toReal () : Number
"3.414".toReal()

Tries to convert a string to a number.

ToUpperCaseExpression

String:: toUpperCase () : String
self.name.toUpperCase()

Returns self into upper case string.