Skip to content

Metamodel Provider

The IMetamodelProvider interface enables integration of ocl.js with runtime metamodels where type hierarchies are not based on JavaScript prototype chains. This is particularly useful for modeling platforms, dynamic object systems, and environments where type information is external to the objects themselves.

When to Use a Metamodel Provider

Consider using a metamodel provider when:

  • Working with plain objects: Your objects don't use JavaScript classes or prototypes
  • External type systems: Type information is stored separately from the objects (e.g., in a metamodel registry)
  • Dynamic metamodels: Types and their hierarchies can change at runtime
  • Modeling platforms: You're integrating with EMF/Ecore-style modeling systems
  • Custom type hierarchies: Your inheritance relationships aren't expressed through JavaScript's prototype chain

Basic Usage

Setting Up a Provider

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

// Example: Objects with a _type property
interface TypedObject {
  _type: string;
  [key: string]: unknown;
}

// Define your metamodel provider
const provider: IMetamodelProvider = {
  getTypeName(obj: unknown): string | undefined {
    if (typeof obj === 'object' && obj !== null && '_type' in obj) {
      return (obj as TypedObject)._type;
    }
    return undefined;
  },

  isKindOf(obj: unknown, typeName: string): boolean {
    const objType = this.getTypeName(obj);
    if (!objType) return false;
    
    // Check if objType is typeName or a subtype of typeName
    // This example uses a simple hierarchy
    const hierarchy: Record<string, string[]> = {
      'Entity': [],
      'NamedElement': ['Entity'],
      'Person': ['NamedElement', 'Entity'],
      'Employee': ['Person', 'NamedElement', 'Entity']
    };
    
    return objType === typeName || (hierarchy[objType]?.includes(typeName) ?? false);
  },

  isTypeOf(obj: unknown, typeName: string): boolean {
    return this.getTypeName(obj) === typeName;
  }
};

// Use the provider with OclEngine
const engine = OclEngine.create()
  .setMetamodelProvider(provider)
  .addOclExpression('context Person inv: self.oclIsKindOf(NamedElement)');

// Evaluate an object
const person = {
  _type: 'Person',
  name: 'John Doe',
  age: 30
};

const result = engine.evaluate(person);
console.log(result.getResult()); // true

Advanced Example: External Metamodel Registry

For more complex scenarios, you might have an external metamodel registry that manages type definitions and relationships:

typescript
// Metamodel registry that manages type definitions
class MetamodelRegistry {
  private types = new Map<string, TypeDefinition>();

  registerType(name: string, superTypes: string[] = []) {
    this.types.set(name, { name, superTypes });
  }

  isSubtypeOf(typeName: string, superTypeName: string): boolean {
    if (typeName === superTypeName) return true;
    
    const typeDef = this.types.get(typeName);
    if (!typeDef) return false;
    
    // Check direct supertypes
    if (typeDef.superTypes.includes(superTypeName)) return true;
    
    // Check transitive supertypes
    return typeDef.superTypes.some(st => this.isSubtypeOf(st, superTypeName));
  }

  getType(typeName: string): TypeDefinition | undefined {
    return this.types.get(typeName);
  }
}

interface TypeDefinition {
  name: string;
  superTypes: string[];
}

// Initialize the registry
const registry = new MetamodelRegistry();
registry.registerType('Entity', []);
registry.registerType('NamedElement', ['Entity']);
registry.registerType('Person', ['NamedElement']);
registry.registerType('Employee', ['Person']);

// Create a provider that uses the registry
const registryProvider: IMetamodelProvider = {
  getTypeName(obj: unknown): string | undefined {
    if (typeof obj === 'object' && obj !== null && '__typename' in obj) {
      return (obj as Record<string, unknown>).__typename as string;
    }
    return undefined;
  },

  isKindOf(obj: unknown, typeName: string): boolean {
    const objType = this.getTypeName(obj);
    if (!objType) return false;
    return registry.isSubtypeOf(objType, typeName);
  },

  isTypeOf(obj: unknown, typeName: string): boolean {
    return this.getTypeName(obj) === typeName;
  }
};

// Use with OclEngine
const engine = OclEngine.create()
  .setMetamodelProvider(registryProvider);

EMF/Ecore Integration Example

For modeling systems like Eclipse Modeling Framework (EMF), you can create a provider that integrates with the metamodel:

typescript
// Simplified EMF-style example
interface EObject {
  eClass(): EClass;
  eGet(feature: string): unknown;
}

interface EClass {
  getName(): string;
  getESuperTypes(): EClass[];
}

const emfProvider: IMetamodelProvider = {
  getTypeName(obj: unknown): string | undefined {
    if (this.isEObject(obj)) {
      return obj.eClass().getName();
    }
    return undefined;
  },

  isKindOf(obj: unknown, typeName: string): boolean {
    if (!this.isEObject(obj)) return false;
    
    const eClass = obj.eClass();
    return this.eClassMatches(eClass, typeName);
  },

  isTypeOf(obj: unknown, typeName: string): boolean {
    if (!this.isEObject(obj)) return false;
    return obj.eClass().getName() === typeName;
  },

  // Helper methods
  isEObject(obj: unknown): obj is EObject {
    return typeof obj === 'object' && obj !== null && 'eClass' in obj;
  },

  eClassMatches(eClass: EClass, typeName: string): boolean {
    if (eClass.getName() === typeName) return true;
    
    return eClass.getESuperTypes().some(superType => 
      this.eClassMatches(superType, typeName)
    );
  }
};

Comparison with setTypeDeterminer

OCL.js provides two ways to customize type resolution:

setTypeDeterminer()

Simple callback for type name resolution only:

typescript
engine.setTypeDeterminer((obj) => {
  return obj._type || obj.constructor.name;
});

Use when:

  • You only need to determine type names
  • No custom type hierarchy support needed
  • Simple, single-purpose type resolution

setMetamodelProvider()

Full metamodel integration with type hierarchy support:

typescript
engine.setMetamodelProvider({
  getTypeName(obj) { /* ... */ },
  isKindOf(obj, typeName) { /* ... */ },
  isTypeOf(obj, typeName) { /* ... */ }
});

Use when:

  • You need full type hierarchy support
  • Working with external metamodels
  • Need oclIsKindOf and oclIsTypeOf operations
  • Type relationships are not based on prototypes

OCL Operations Enabled

Setting a metamodel provider enables proper support for:

  • oclIsKindOf(type): Checks if object is of the specified type or any subtype
  • oclIsTypeOf(type): Checks if object is exactly of the specified type
  • Context matching: Automatically matches objects to OCL context declarations

API Reference

See the detailed API documentation:

Released under the MIT License.