Puppet Internals - Using Model Meta Data
In this post about modeling with Ecore and RGen I will explain the operations that allows us to navigate the model, and how to generically get and set elements / values in the model, as well as how
to get to the meta-model from model objects.
e-ness
As you will see, methods that operate on, or make use of Ecore meta data are typically named
with an initial lower case 'e'. They also use Camel Cased names to make these operations as similar as possible to the corresponding operations in EMF / Java. Whenever you see such 'e' methods, they are about the Ecore aspects of the object (instance), or its class (access to the meta-model). In everyday speak we can talk about this as the object's e-ness (since it is much easier to say than "the meta aspects of..."
In this post I will show the various basic 'e' operations with examples. If you want to
get the source of the model used in the examples, the final version is available in this gist.
Getting Content
One of the first things you typically want to do is to get the content of a model generically
(that is, without any explicit knowledge about a particular model).
First, we need to make a couple of adjustments to the example model I showed in the previous post. RGen comes with some built in navigations, but not all that are available in the EMF (Java) implementation of Ecore. So, we have a module in Puppet that should be included to get
full support.
We need to add:
require 'puppet'
require 'puppet/pops'
And then in our base class (that all our domain model classes inherit form) define it like this:
class MyModelElement < RGen::MetamodelBuilder::MMBase
include Puppet::Pops::Containment
abstract
end
Getting All Contents
A common operation is to iterate over all containments in an object. This is done with the method eAllContents
which is typically called with a block receiving each contained element.
It can also be called without a block to get a Ruby Enumerable
.
The eAllContents
method does not include the object it is invoked on, only its content is included
in the enumeration. It visits all contained recursively in depth first order where parents appear
before children.
We can try this out with the Car model.
engine = MyModel::CombustionEngine.new
car = MyModel::Car.new
car.engine = engine
car.eAllContents.map {|element| "#{element}" }
# => ["#<MyModel::CombustionEngine:0x007ff8a1b606d8>"]
In the Puppet implementation of the future parer, the ability to iterate over all contents is
used when validating models. In particular the model produced by the parser.
Getting All Containers
If we have a model element, and would like to traverse all of its containers (until we reach the
root) we use the eAllContainers
. If we just want the immediate container we use eContainer
.
# continuation of the previous example
engine.eContainer # MyModel::Car
engine.eAllContainers.to_a # [MyModel::Car]
Not so exiting, but if we add a Garage
that can contain cars - i.e. by adding this to the model:
class Garage < MyModelElement
contains_many_uni 'cars', Car
end
And then add our car to the garage.
garage = MyModel::Garage.new
garage.addCars(car)
engine.eAllContainers.to_a # [MyModel::Car, MyModel::Garage]
# and just to check what happens if we get all contents in the garage
garage.eAllContents.to_a # [MyModel::Car, MyModel::CombustionEngine]
In the Puppet implementation of the future parser, the ability to search up the containment
chain is used in validation (some object must be contained by top level constructs), and
in order to find information such as a source text location index, and to find the loader that
loaded the code (which is recorded at the root of the model).
Where am I? What is my role?
It is often useful to ask:
- Where is this object contained?
- What is this object's role in that container?
In the sample car model we have right now, this is not so valuable, since we do not have anything
that can be contained in multiple places. So to make this a bit more interesting we can add the
following to the model
class Car < MyModelElement
# as before AND...
has_attr 'reg_nbr', String, :defaultValueLiteral => 'UNREGISTERED'
contains_one_uni 'left_front', Wheel
contains_one_uni 'right_front', Wheel
contains_one_uni 'left_rear', Wheel
contains_one_uni, 'right_rear', Wheel
end
RimTypeEnum = RGen::MetamodelBuilder::DataTypes::Enum.new([:black, :silver])
class Wheel < MyModelElement
has_attr 'rim', RimTypeEnum
has_attr 'rim_serial', String
end
The above example also shows how to set a default value for an attribute. Default values
can only be used with single valued attributes. All other have an empty Array
as their default.
Now we can create wheels and assign to the car. This also demonstrates that it is possible
to give values to features in a Hash
when creating the instance:
car = MyModel::Car.new(:reg_nbr => 'ABC123')
car.left_front = w1 = MyModel::Wheel.new(:rim_serial => '1', :rim => :silver)
car.right_front = w2 = MyModel::Wheel.new(:rim_serial => '2', :rim => :silver)
car.left_rear = w3 = MyModel::Wheel.new(:rim_serial => '3', :rim => :silver)
car.right_rear = w4 = MyModel::Wheel.new(:rim_serial => '4', :rim => :silver)
And now we can start asking questions:
w1.eContainer.reg_nbr # => 'ABC123'
w1.eContainingFeature # => :left_front
w2.eContainer.reg_nbr # => 'ABC123'
w2.eContainingFeature # => :right_front
In the Puppet implementation these operations are typically used when generating error messages.
An error is found in some deeply nested / contained element and a message should inform the
user about where it is being used / the role it plays. Sometimes also used in validation when
something is valid or not depending on the role it plays in its container.
Generic Get
Features can be read generically with the method getGeneric
. Using the wheel example above:
car.getGeneric(:left_front).rim_serial # => '1'
w1.getGeneric(:rim) # => :silver
There is also a getGenericAsArray
which returns the value in an Array
, if it is not already
one (which it always is when the feature is multi-valued).
The method eIsSet
can be used to test if a feature has been set. This returns true
if
the value has any other value than the feature's default value (and if default value is not
defined, if the feature is nil).
car2 = MyModel::Car.new
car2.reg_nbr # => 'UNREGISTERED'
car2.eIsSet(:reg_nbr) # => false
car2.reg_nbr = 'XYZ123'
car2.eIsSet(:reg_nbr) # => true
This means we can define the value that represents "missing value" per single valued feature, and
as you will see below we can reset the value to the default.
At present we do not make use of the generic operations anywhere in the Puppet implementation as the validation logic is
aware of the individual classes. We may make use of it to add additional generic validation that
is driven by meta data alone. We will probably also use this when we get to the catalog and resource
type models.
Generic Mutating Operations
As you may have guessed, it is also possible to generically modify objects. The methods are:
setGeneric(name, value)
addGeneric(name, value)
removeGeneric(name, value)
eUnset(name)
- sets feature to its default value (or nil/empty list if there is no default)
Here is an example using eUnset
to return the car to the default for its reg_nbr
:
carxyz123 = MyModel::Car.new(:reg_nbr => 'XYZ123')
carxyz123.eIsSet(:reg_nbr) # => true
carxyz.reg_nbr # => 'XYZ123'
carxyz.eUnset(:reg_nbr)
carxyz123.eIsSet(:reg_nbr) # => false
carxyz.reg_nbr # => 'UNREGISTERED'
So, what is missing from the picture?
You may have noticed it already. We did add an eAllContents
, and eAllContainers
to each
model class by including the Puppet::Pops::Containment
module, but these only operate on
containment references.
- How can you get all features including attributes and regular references?
- Why are these not directly available on all model objects as individual methods?
The reasons for the design are that it is very common to navigate to the container, or to contained children e.g. for validation purposes, but these operations typically result ending up in logic that has specific knowledge about a particular class, and there we are in a context where we already know about all of the attributes and references and we can just use them directly.
While it would be possible to provide direct access to almost all e-ness methods directly, there is a limit to how much bloat we want in each class. Therefore, all other e-ness operations we may want to
perform has to be written in a slightly more round-about fashion where we navigate to the meta model and get the information there ourselves.
All the information we need is available in the meta-model, and the following sections
show how this is done.
Getting the Meta Model
We can get the entire meta-model from the Ruby module by calling the method ecore
:
MyModel.ecore # => ECore::EPackage
From there we can navigate the contents of the entire package. As an example, one of
the methods, eAllClasses
, gives us all defined classes. Here is what happens if you
try this in irb
on MyModel
(notice the map to each class' name to get something meaningful
as output):
MyModel.ecore.eAllClasses.map {|ec| ec.name }
=> ["MyModelElement", "Engine", "CombustionEngine", "ElectricalEngine", "Car",
"ServiceOrder", "Garage", "Wheel"]
We can also get to the meta model element for each individual class in our model by calling the
method ecore
on that object's class. (We can not use this on the values of attributes since
they are basic Ruby types, and thus do not have any e-ness).
car = MyModel::Car.new
car.class.ecore # => RGen::ECore::EClass
Oh, look we got something back called an EClass
which is the meta-model representation of a class.
The EClass
has many useful methods to get information about the class, its attributes, references,
containments etc. As an example, the method eAllAttributes
returns an Enumerable
with ECore::EAttribute
elements that describes all of the attributes for the class and all of its superclasses.
If you thought it just started to get interesting, don't worry, I will come back with more about about the ECore
model in the next post.
In this Post
In this post you have seen how a model can be navigated, and how we can find the meta-data for elements in the model. In the next post I am going to dive deeper into the Ecore model itself.