Expressir Data Model
Purpose
This page explains Expressir’s Ruby data model that represents EXPRESS schemas. Understanding this model is essential for programmatic manipulation of EXPRESS data, writing custom tools, and generating documentation.
References
-
EXPRESS Language - Understanding EXPRESS concepts
-
Parsers - How EXPRESS becomes the data model
-
Guide: Model Traversal - Navigating the model
-
Data Model Reference - Complete API documentation
Concepts
- Model Element
-
Base class for all EXPRESS constructs in Expressir’s object model
- Repository
-
Top-level container holding multiple schemas
- Declaration
-
Named definition (entity, type, function, etc.) within a schema
- Data Type
-
Type specification (primitives, aggregates, constructed types)
- Expression
-
Algorithmic calculation or value reference
- Statement
-
Procedural operation (assignment, if/then, loop, etc.)
- Reference
-
Link to another model element by name
Model Hierarchy
Expressir’s data model follows an object-oriented design with clear class hierarchies:
ModelElement (base class)
├── Repository
│ └── Schema
│ ├── Entity
│ ├── Type
│ ├── Function
│ ├── Procedure
│ ├── Rule
│ ├── Constant
│ ├── SubtypeConstraint
│ └── Interface
├── DataTypes
│ ├── Primitives (String, Integer, Real, Boolean, Logical, Binary, Number)
│ ├── Aggregates (Array, List, Set, Bag)
│ ├── Enumeration
│ ├── Select
│ ├── Generic
│ └── GenericEntity
├── Declarations
│ ├── Attribute
│ ├── DerivedAttribute
│ ├── InverseAttribute
│ ├── Parameter
│ ├── Variable
│ ├── UniqueRule
│ ├── WhereRule
│ └── ...
├── Expressions
│ ├── BinaryExpression
│ ├── UnaryExpression
│ ├── FunctionCall
│ ├── QueryExpression
│ ├── AggregateInitializer
│ └── ...
├── Statements
│ ├── Assignment
│ ├── If
│ ├── Repeat
│ ├── Case
│ ├── Return
│ └── ...
├── References
│ ├── SimpleReference
│ ├── AttributeReference
│ ├── GroupReference
│ └── IndexReference
└── Literals
├── String
├── Integer
├── Real
├── Logical
└── Binary
Key Classes
ModelElement
The base class for all EXPRESS constructs:
module Expressir::Model
class ModelElement
attr_accessor :parent # Parent element in tree
attribute :source, :string # Original EXPRESS text
attribute :untagged_remarks, :string, collection: true
# Navigate to parent and ancestors
def path # Full qualified path (e.g., "schema.entity.attribute")
def find(path) # Find child by path
def children # Direct children
def children_by_id # Children indexed by ID
end
end
Key features:
-
Parent linkage: Every element knows its parent
-
Path navigation: Full qualified paths for reference resolution
-
Child access: Easy traversal of the tree structure
-
Source tracking: Original EXPRESS text available
Repository
Top-level container for schemas:
repository = Expressir::Model::Repository.new
repository.schemas << schema1
repository.schemas << schema2
# Access schemas
repository.schemas.each { |schema| puts schema.id }
# Find elements across schemas
entity = repository.find_entity(qualified_name: "action_schema.action")
type = repository.find_type(qualified_name: "geometry_schema.point")
# Statistics
stats = repository.statistics
# => { total_schemas: 10, total_entities: 150, ... }
# Build indexes for fast lookup
repository.build_indexes
Capabilities:
-
Multi-schema management: Holds multiple schemas
-
Global search: Find entities/types across all schemas
-
Statistics: Aggregate counts and metrics
-
Indexing: Fast lookup with automatic index building
-
Validation: Schema consistency checking
Schema
A named collection of EXPRESS declarations:
schema = Expressir::Model::Declarations::Schema.new(id: "example_schema")
# Access declarations
schema.entities # Array of Entity objects
schema.types # Array of Type objects
schema.functions # Array of Function objects
schema.procedures # Array of Procedure objects
schema.rules # Array of Rule objects
schema.constants # Array of Constant objects
schema.interfaces # Array of Interface objects
# Metadata
schema.file # Source file path
schema.version # Schema version info
schema.remarks # Documentation comments
# Find child element
entity = schema.find("person")
Key attributes:
-
id: Schema name
-
file: Source file path
-
version: SchemaVersion object
-
interfaces: USE FROM / REFERENCE FROM declarations
-
entities, types, functions, etc.: Collections of declarations
Entity
Represents an EXPRESS ENTITY:
entity = Expressir::Model::Declarations::Entity.new(
id: "person",
abstract: false
)
# Attributes
entity.attributes # Explicit attributes
entity.derived_attributes # DERIVE attributes
entity.inverse_attributes # INVERSE attributes
# Inheritance
entity.subtype_of # Array of supertypes
entity.supertype_expression # Supertype expression
# Constraints
entity.unique_rules # UNIQUE rules
entity.where_rules # WHERE rules
entity.informal_propositions # Informal propositions
# Navigation
entity.children # All child elements
entity.parent # Parent schema
Key features:
-
Inheritance support: Subtypes and supertypes
-
Three attribute types: Explicit, derived, inverse
-
Constraint modeling: WHERE and UNIQUE rules
-
Documentation: Remarks and informal propositions
Type
Represents an EXPRESS TYPE:
type = Expressir::Model::Declarations::Type.new(id: "length_measure")
# Underlying type definition
type.underlying_type # DataType object (Select, Enumeration, etc.)
# Constraints
type.where_rules # WHERE constraints
# For enumeration types
if type.underlying_type.is_a?(Expressir::Model::DataTypes::Enumeration)
type.enumeration_items # Array of EnumerationItem objects
end
Type categories:
-
Simple types: Constrained primitives
-
Enumeration: Named values
-
Select: Union types
-
Aggregate: Arrays, lists, sets, bags
-
Generic: Generic and GenericEntity
Attribute
Entity attributes come in three varieties:
Explicit attributes (stored data):
attr = Expressir::Model::Declarations::Attribute.new(
id: "name",
optional: false
)
attr.type = String_type # Type reference
Derived attributes (computed):
derived = Expressir::Model::Declarations::DerivedAttribute.new(
id: "full_name"
)
derived.type = String_type
derived.expression = ... # Calculation expression
Inverse attributes (relationships):
inverse = Expressir::Model::Declarations::InverseAttribute.new(
id: "employees"
)
inverse.type = Set_type
inverse.attribute = "works_for" # Target attribute
Relationships
Parent-Child Relationships
Every element maintains a link to its parent:
# Navigate up
attribute.parent # => Entity
entity.parent # => Schema
schema.parent # => Repository
# Navigate down
schema.entities.first # => Entity
entity.attributes.first # => Attribute
Reference Resolution
References are resolved to their target elements:
# Before resolution: SimpleReference with id
attribute.type # => SimpleReference(id: "length_measure")
# After resolution: Points to actual Type
repository.resolve_all_references
attribute.type.ref # => Type(id: "length_measure")
Working Programmatically
Creating Models from Code
You can construct models programmatically:
# Create repository
repo = Expressir::Model::Repository.new
# Create schema
schema = Expressir::Model::Declarations::Schema.new(id: "my_schema")
repo.schemas << schema
# Create entity
entity = Expressir::Model::Declarations::Entity.new(id: "my_entity")
schema.entities << entity
# Create attribute
attr = Expressir::Model::Declarations::Attribute.new(
id: "my_attribute",
optional: false
)
entity.attributes << attr
# Set attribute type
string_ref = Expressir::Model::References::SimpleReference.new(id: "STRING")
attr.type = string_ref
Parsing EXPRESS to Model
Most commonly, you parse EXPRESS files:
# Parse file to repository
repo = Expressir::Express::Parser.from_file("schema.exp")
# Access elements
schema = repo.schemas.first
entity = schema.entities.find { |e| e.id == "person" }
attribute = entity.attributes.find { |a| a.id == "name" }
# Check type
if attribute.type.is_a?(Expressir::Model::References::SimpleReference)
puts "Type name: #{attribute.type.id}"
end
Model Traversal
Navigate the model tree systematically:
# Visit all entities in repository
repo.schemas.each do |schema|
schema.entities.each do |entity|
puts "Entity: #{entity.path}"
entity.attributes.each do |attr|
puts " Attribute: #{attr.id}"
end
end
end
# Find specific element by path
element = repo.find("action_schema.action.id")
# Search with pattern
entities = repo.schemas.flat_map(&:entities)
person_entities = entities.select { |e| e.id.include?("person") }
Visitor Pattern
For complex traversals, use the visitor pattern:
class MyVisitor < Expressir::Express::Visitor
def visit_entity(entity)
puts "Visiting entity: #{entity.id}"
super # Visit children
end
def visit_attribute(attribute)
puts " Attribute: #{attribute.id}"
end
end
visitor = MyVisitor.new
visitor.visit(repository)
Accessing Elements
By Direct Navigation
# Access through collections
schema = repo.schemas.first
entity = schema.entities.first
attribute = entity.attributes.first
By ID Lookup
# Find by ID in collection
entity = schema.entities.find { |e| e.id == "person" }
# Use children_by_id for faster lookup
entity = schema.children_by_id["person"]
Model Modification
Adding Elements
# Add to collections
schema.entities << new_entity
entity.attributes << new_attribute
# Parent is automatically set
new_entity.parent # => schema
Converting to EXPRESS
Convert model back to EXPRESS text:
# Format entire repository
express = Expressir::Express::Formatter.format(repo)
# Format single schema
express = Expressir::Express::Formatter.format(schema)
# Format with options
formatter = Expressir::Express::Formatter.new(no_remarks: true)
express = formatter.format(schema)
Converting to Liquid
For template-based generation:
# Convert to Liquid drops
repo_drop = repo.to_liquid
schema_drop = schema.to_liquid
# Use in Liquid templates
template = Liquid::Template.parse("{{ schema.id }}")
output = template.render('schema' => schema_drop)
See Liquid guides for details.
Performance Considerations
Indexing
Build indexes for large repositories:
# Build all indexes
repo.build_indexes
# Fast lookups now available
entity = repo.find_entity(qualified_name: "schema.entity")
type = repo.find_type(qualified_name: "schema.type")
Best Practices
- Navigate using relationships
-
Use parent/child links rather than searching
- Use typed collections
-
Access
schema.entitiesrather than filteringschema.children - Build indexes once
-
For repositories with many schemas, build indexes early
- Check types carefully
-
Types can be references, resolved references, or inline definitions
- Preserve parent links
-
When moving elements, update parent correctly
Use visitor pattern: For complex tree traversals, implement a visitor
Common Patterns
Find All Entities Inheriting From Base
def find_subtypes(repo, base_entity_name)
repo.schemas.flat_map(&:entities).select do |entity|
entity.subtype_of&.any? { |st| st.ref&.id == base_entity_name }
end
end
Next Steps
Now that you understand the data model:
- Try it out
-
Parse a schema and explore the model
- Learn traversal
-
Read the model traversal guide
- Study references
-
Review the complete data model reference
- Build tools
-
Create custom analyzers or converters using the model
Bibliography
-
Data Model Reference - Complete API documentation
-
Parsers - How the model is created
-
Model Source Code - Implementation details
-
Ruby API Guides - Working with the model programmatically