Meta-model and the Generated Domain-Specific Model API

Language

The language used in all examples is the same. The language itself is fairly small: It describes a very simplified lecture schedule. The language intentionally does not use expressions, creating editors for expressions by hand is very cumbersome and at the time where these samples were created modelix has no support for generating these editors for you. The main concepts of the language are:

  • Room: where lecture are held. Each room has a maximum capacity of students, a name and some additional properties.

  • Lecture: Have a name, some description and reference a room where they are held. And also have a schedule which determines when they are held and if they repeat through the whole semester or are a one time lecture.

  • Student: A student with a name and birthday.

  • Assignment: Lecture assignments for a single student.

Some concepts are contained a root node like a Rooms container to make structuring the editors in MPS easier:

domain mermaid
classDiagram
    class Rooms {
      <<root>>
    }
    class Courses {
      <<root>>
    }

    class Students {
      <<root>>
    }

    class LectureAssignments {
      <<root>>
    }

    Rooms *-- Room : 0..n
    Courses *-- Lecture: 0..n
    Lecture .. Room: 1
    Lecture *-- Schedule: 1
    OneOff <|-- Schedule
    Recurring <|-- Schedule
    Schedule *-- DateAndTime:1
    Students *-- Student: 0..n
    Student *-- DateAndTime:born [1]
    LectureAssignments .. Student: 1
    LectureAssignments *-- Assignment: 0..n
    Assignment .. Lecture:1

Generated API

In oder to be able to work with the metamodel / structure of the language outside of MPS we need to generate an API that is usable outside of MPS. This API is generated with the api-gen plugin from modelix. The plugin takes an MPS language definition and exports it into a Java API that wraps around the metamodel independent model-api from modelix. The wrapper is than metamodel specific and give you easy access to the instance of your language.

The generator is configured in the University.Schedule.api with an ApiDefinition. When the model that contains the ApiDefinition is rebuild it will generate Java classes for the languages that are referenced withing it.

You can find the generated code within the repository at mps/solutions/University.Schedule.api/source_gen. The generated Java code is then not compiled within MPS but using a separate gradle build at mps/solutions/University.Schedule.api/build.gradle.kts. The generated code has no dependency into MPS at all but depends on org.modelix.mps.api-gen:runtime which contains a couple of base classes to make the code generator implementation less convoluted and easier to maintain.

For instance accessing the rooms on the Rooms root node and then reading the name of the Room using the modelix model api directly would look like this:

val iNode : INode = ...
iNode.getChildren("rooms").forEach { it.getPropertyValue("name") }

You can see that this is quite error-prone because every access to a child role or property is just a string. If you have a typo or the structure of the language changes you will only notice it at runtime.

Using the generated API the same code looks like this:

val iNode : INode = ...
val rooms = MPSLanguageRegistry.getInstance<Rooms>(iNode)
rooms.children.rooms.forEach { it.properties.name }

The code generator has exported the language definition, and we can use to write type safe code that works with the models. For properties and children we now have attributes in the generated classes and if somebody renames a property or child-role the compiler will tell us. Of course the MPSLanguageRegistry.getInstance<Rooms> would throw an exception if our iNode instance isn’t a Rooms instance.

The generate class for a Room concept:

concept Room extends BaseConcept
             implements INamedConcept

instance can be root: false
alias: <no alias>
short description: <no short description>

properties:
maxPlaces : integer
hasRemoteEquipment : boolean

children:
<< ... >>

references:
<< ... >>
Will look like this:
java
package University.Schedule.structure;

/*Generated by MPS */

import jetbrains.mps.lang.core.structure.BaseConcept;
import jetbrains.mps.lang.core.structure.INamedConcept;
import org.modelix.mps.apigen.runtime.INodeHolder;
import org.jetbrains.annotations.NotNull;
import org.modelix.model.api.INode;
import org.jetbrains.annotations.Nullable;

/**
* Generated for http://127.0.0.1:63320/node?ref=r%3Adfa26643-4653-44bc-9dfe-5a6581bcd381%28University.Schedule.structure%29%2F4128798754188010580
  */
  public class Room extends BaseConcept implements INamedConcept {

public class Properties extends BaseConcept.Properties implements INodeHolder, INamedConcept.Properties {

    @NotNull
    @Override
    public INode getINode() {
      return Room.this.getINode();
    }
    @Nullable
    public Integer getMaxPlaces() {
      String propertyValue = getINode().getPropertyValue("maxPlaces");
      if (propertyValue != null && !(propertyValue.isEmpty())) {
        return Integer.parseInt(propertyValue);
      }
      return null;
    }
    @Nullable
    public Integer setMaxPlaces(Integer value) {
      if (value != null) {
        getINode().setPropertyValue("maxPlaces", Integer.toString(value));
      } else {
        getINode().setPropertyValue("maxPlaces", null);
      }
      return value;
    }
    @Nullable
    public Boolean getHasRemoteEquipment() {
      String propertyValue = getINode().getPropertyValue("hasRemoteEquipment");
      if (propertyValue != null && !(propertyValue.isEmpty())) {
        return Boolean.parseBoolean(propertyValue);
      }
      return null;
    }
    @Nullable
    public Boolean setHasRemoteEquipment(@Nullable Boolean value) {
      if (value != null) {
        getINode().setPropertyValue("hasRemoteEquipment", Boolean.toString(value));
      } else {
        getINode().setPropertyValue("hasRemoteEquipment", null);
      }
      return value;
    }
}
public class Children extends BaseConcept.Children implements INodeHolder, INamedConcept.Children {

    @NotNull
    @Override
    public INode getINode() {
      return Room.this.getINode();
    }
}
public class References extends BaseConcept.References implements INodeHolder, INamedConcept.References {

    @NotNull
    @Override
    public INode getINode() {
      return Room.this.getINode();
    }


}

private final Properties properties;
private final Children children;
private final References references;
public Room(INode node) {
super(node);
this.properties = new Properties();
this.children = new Children();
this.references = new References();
}
public Properties getProperties() {
return this.properties;
}
public Children getChildren() {
return this.children;
}
public References getReferences() {
return this.references;
}
}

Limitations

At the moment it’s not possible to regenerate the API as part of the CI/gradle build, that’s why the generated sources are checked into the repository. This limitation is specific this example and is somehow caused by the MPS build failing to load the right languages during the build. Other projects are successfully using the api-gen code generator within their CI/gradle build. The limitation will get fixed in the future but for now the generated Java code is checked into the repository.