Object Constraint Language (OCL)¶
This page walks through a complete OCL example end-to-end: building a small domain model, parsing OCL constraints into ASTs, attaching preconditions and postconditions to a method, and inspecting the parsed AST.
For language reference, see OCL specification.
Domain model¶
Take a tiny banking model — an Account class with a balance and an
active flag, and a deposit operation:
from besser.BUML.metamodel.structural import (
DomainModel, Class, Property, Method, Parameter, IntegerType, BooleanType,
)
account = Class("Account", attributes={
Property("balance", IntegerType),
Property("is_active", BooleanType),
})
model = DomainModel("BankingModel", types={account})
deposit = Method(
name="deposit",
parameters=[Parameter("amount", IntegerType)],
type=IntegerType,
)
Parsing constraints¶
Use parse_ocl() to turn OCL source
text into an OCLConstraint. The returned constraint exposes both
the source-text form (.expression) and the parsed AST (.ast):
from besser.BUML.notations.ocl.api import parse_ocl
invariant = parse_ocl(
"context Account inv: self.balance >= 0",
model,
context_class=account,
)
print(invariant.expression)
# 'self.balance >= 0'
print(type(invariant.ast).__name__, invariant.ast.operation)
# OperationCallExpression >=
Preconditions and postconditions on the method¶
Method contracts are anchored on the Method itself via the
pre and post first-class fields:
pre = parse_ocl(
"context Account::deposit(amount: Integer) pre: self.is_active and amount > 0",
model, context_class=account,
)
post = parse_ocl(
"context Account::deposit(amount: Integer) post: self.balance >= 0",
model, context_class=account,
)
pre.name, post.name = "deposit_pre_active_and_positive", "deposit_post_nonneg"
deposit.add_pre(pre)
deposit.add_post(post)
[c.name for c in deposit.pre] # ['deposit_pre_active_and_positive']
[c.name for c in deposit.post] # ['deposit_post_nonneg']
The full BOCL header (context Class::method(params) pre|post:) is required
when calling parse_ocl directly with a precondition or postcondition —
context_class must also be passed explicitly because the parser’s
auto-detect regex only handles the simpler invariant header shape.
add_pre / add_post raise ValueError if a precondition or
postcondition with the same name already exists on the method.
Normalization and pretty-printing¶
Sugar operators like implies can be rewritten into a smaller core
(not / or) using the normalizer — handy when you only want to
implement semantics for a minimal set of operators in your consumer:
from besser.BUML.notations.ocl.normalization.normalize import normalize
from besser.BUML.notations.ocl.pretty_printer import pretty_print
c = parse_ocl(
"context Account inv: self.is_active implies self.balance >= 0",
model, context_class=account,
)
print(pretty_print(c.ast))
# 'self.is_active implies self.balance >= 0'
c_norm = normalize(c, model)
print(pretty_print(c_norm.ast))
# 'not self.is_active or self.balance >= 0'
Validating with the B-OCL Interpreter¶
To evaluate parsed OCL constraints against an object model, use the
external B-OCL Interpreter.
The interpreter consumes the same OCLConstraint objects produced
by parse_ocl.