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.