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:
result
contains the actual result of the evaluation run as a boolean valuenamesOfFailedInvs
contains the names of failed invariants oranonymous
if none has been providedevaluatedContexts
contains allContextExpressions
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:
getResult() : boolean
: Returns the result of each and every OCL rule that has been run for the given object.getNamesOfFailedInvs() : Array<string>
: Returns the names of invariants that have evaluated tofalse
.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.