UR::Object::Type::Initializer(3pm) | User Contributed Perl Documentation | UR::Object::Type::Initializer(3pm) |
UR::Object::Type::Initializer - Class definition syntax
UR::Object::Type->define( class_name => 'Namespace::MyClass', id_by => 'my_class_id', has => ['prop_a', 'prop_b'] ); UR::Object::Type->define( class_name => 'Namespace::MyChildClass', is => 'Namespace::MyClass', has => [ 'greeting' => { is => 'String', is_optional => 0, valid_values => ["hello","good morning","good evening"], default_value => 'hello' }, ], ); UR::Object::Type->define( class_name => 'Namespace::Helper', id_by => 'helper_id', has => [ 'my_class_id' => { is => 'String', is_optional => 0 }, 'my_class' => { is => 'Namespace::MyClass', id_by => 'my_class_id' }, 'my_class_a' => { via => 'my_class', to => 'prop_a' }, ], has_optional => [ 'other_attribute' => { is => 'Integer' } ], data_source => 'Namespace::DataSource::DB', table_name => 'HELPERS', ); UR::Object::Type->define( class_name => 'Namespace::Users', id_by => ['uid'], has => [ 'login','passwd','gid','name','home','shell'], data_source => { is => 'UR::DataSource::File', file => '/etc/passwd', column_order => ['login','passwd','uid','gid','name','home','shell', skip_first_line => 0, delimiter => ':' }, id_generator => '-uuid', );
Defining a UR class is like drawing up a blueprint of what a particular kind of object will look like. That blueprint includes the properties these objects will have, what other classes the new class inherits from, and where the source data comes from such as a database or file.
The simplest class definition would look like this:
use UR; class Thing {};
You can create an instance of this class like this:
my $thing_object = Thing->create();
Instances of this class have no properties, no backing storage location, and no inheritance.
Actually, none of those statements are fully true, but we'll come back to that later...
After using UR, or another class that inherits from UR::Namespace, the above "class" syntax above can be used to define a class.
The equivalent, more plain-Perl way to define a class is like this:
UR::Object::Type->define( class_name => 'Thing', # the remainder of the class definition would go here, if there were any );
Classes become instances of another class called UR::Object::Type. It has a property called class_name that contains the package that instances of these objects are blessed into. Class properties are also instances of a class called UR::Object::Property, and those properties have properties (also UR::Object::Properties) that describe it, such as the type of data it holds, and its length. In fact, all the metadata about classes, properties, relationships, inheritance, data sources and contexts are available as instances of metadata classes. You can get information about any class currently available from the command line with the command
ur show properties Thing
with the caveat that "currently available" means you need to be under a namespace directory that contains the class you're describing.
class Vehicle { id_by => 'serial_number', has => ['color', 'weight'], has_optional => ['license_plate'], };
Here we have a basic class definition for a thing we're calling a Vehicle. It has 4 properties: serial_number, color, weight and license_plate. Three of these properties are required, meaning that when you create one, you must give a value for those properties; it is similar to a 'NOT NULL' constraint on a database column. The serial_number property is an ID property of the class, meaning that no two instances (objects) of that class can exist with the same serial_number; it is similar to having a UNIQUE index on that column (or columns) in a database. Not all vehicles have license plates, so it is optional.
After that, you've effectively created five object instances. One UR::Object::Type identified by its class_name being 'Vehicle', and four UR::Object::Property objects identified by the pairs of class_name and property_name. For these four properties, class_name is always 'Vehicle' and property name is one each of serial_number, color, weight and license_plate.
Objects always have one property that is called 'id'. If you have only one property in the id_by section, then the 'id' property is effectively an alias for it. If you have several id_by properties, then the 'id' property becomes an amalgamation of the directly named id properties such that no two objects of that class will have the same 'id'. If there are no id_by properties given (including MyClass above that doesn't have _any_ properties), then an implicit 'id' property will get created. Instances of that class will have an 'id' generated internally by an algorithm. Finally, if the class has more than one ID property, none of them may be called 'id', since that name will be reserved for the amalgamated-value property.
You can control how IDs get autogenerated with the class' id_generator metadata. For classes that save their results in a relational database, it will get new IDs from a sequence (or some equivalent mechanism for databases that do not support sequences) based on the class' table name. If you want to force the system to use some specific sequence, for example if many classes should use the same sequence generator, then put the name of this sequence in.
If the id_generator begins with a dash (-), it indicates a method should be called to generate a new ID. For example, if the name is "-uuid", then the system will call the internal method "$class_meta-"autogenerate_new_object_id_uuid>. nd will make object IDs as hex string UUIDs. The default value is '-urinternal' which makes an ID string composed of the hostname, process ID, the time the program was started and an increasing integer. If id_generator is a subroutine reference, then the sub will be called with the class metaobject and creation BoolExpr passed as parameters.
You'll find that the parser for class definitions is pretty accepting about the kinds of data structures it will take. The first thing after class is used as a string to name the class. The second thing is a hashref containing key/value pairs. If the value part of the pair is a single string, as the id_by is in the Vehicle class definition, then one property is created. If the value portion is an arrayref, then each member of the array creates an additional property.
That same class definition can be made this way:
class Vehicle { id_by => [ serial_number => { is => 'String', len => 25 }, ], has => [ color => { is => 'String' }, weight => { is => 'Number' }, license_plate => { is => 'String', len => 8, is_optional => 1 }, ], };
Here we've more explicitly defined the class' properties by giving them a type. serial_number and license_number are given a maximum length, and license_number is declared as optional. Note that having a 'has_optional' section is the same as explicitly putting 'is_optional => 1' for all those properties. The same shortcut is supported for the other boolean properties of UR::Object::Property, such as is_transient, is_mutable, is_abstract, etc.
The type system is pretty lax in that there's nothing stopping you from using the method for the property to assign a string into a property declared 'Number'. Type, length, is_optional constraints are checked by calling "__errors__()" on the object, and indirectly when data is committed back to its data source.
class Car { is => 'Vehicle', has => [ passenger_count => { is => 'Integer', default_value => 0 }, transmission_type => { is => 'String', valid_values => ['manual','automatic','cvt'] }, ], }; my $car = Car->create(color => 'blue', serial_number => 'abc123', transmission_type => 'manual');
Here we define another class called Car. It inherits from Vehicle, meaning that all the properties that apply to Vehicle instances also apply to Car instances. In addition, Car instances have two new properties. passenger_count has a default value of 0, and transmission_type is constrained to three possible values.
Besides property definitions, there are other things that can be specified in a class definition.
data_source can also be a hashref to define a data source in line with the class definition. See below for more information about "Inline Data Sources".
"ur show properties UR::Object::Property" will print out an exhaustive list of all the properties of a Class Property. A class' properties are declared in the 'id_by' or one of the 'has' sections. Some of the more important ones:
Object properties do not normally hold Perl references to other objects, but you may use 'ARRAY' or 'HASH' here to indicate that the object will store the reference directly. Note that these properties are not usually saveable to outside data sources.
Specifying "calculated_default > 1" is equivalent to,
calculated_default => '__default_' . $prop_name . '__'
and is meant to establish a naming convention without requiring it.
Calculated Properties
Any property can be effectively turned into a calculated property by defining a method with the same name as the property.
Database-backed properties
Some properties are not used to hold actual data, but instead describe some kind of relationship between two classes. For example:
class Person { id_by => 'person_id', has => ['name'], }; class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, ], }; $person = Person->create(person_id => 1, name => 'Bob'); $thing = Thing->create(thing_id => 2, owner_id => 1);
Here, Thing has a property called "owner". It implicitly defines a property called "owner_id". "owner" becomes a read-only property that returns an object of type Person by using the object's value for the "owner_id" property, and looking up a Person object where its ID matches. In the above case, "$thing->owner" will return the same object that $person contains.
Indirect properties can also link classes with multiple ID properties.
class City { id_by => ['name', 'state'] }; class Location { has => [ city => { is => 'String' }, state => { is => 'String' }, cityobj => { is => 'City', id_by => ['city', 'state' ] }, ], };
Note that the order the properties are linked must match in the relationship property's "id_by" and the related class's "id_by"
When one class has a relation property to another, the target class can also define the converse relationship. In this case, OtherClass is the same as the first "Relation Properties" example where the relationship from OtherClass to MyClass, but we also define the relationship in the other direction, from MyClass to OtherClass.
Many Things can point back to the same Person.
class Person { id_by => 'person_id', has => ['name'], has_many => [ things => { is => 'Thing', reverse_as => 'owner' }, ] }; class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, ], };
Note that the value for "reverse_as" needs to be the name of the relation property in the related class that would point back to "me". Yes, it's a bit obtuse, but it's the best we have for now.
When the property of a related object has meaning to another object, that relationship can be defined through an indirect property. Things already have owners, but it is also useful to know a Thing's owner's name.
class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, owner_name => { via => 'owner', to => 'name', default_value => 'No one' }, ], }; $name = $thing->owner_name(); $name eq $person->name; # evaluates to true
The values of indirect properties are not stored in the object. When the property's method is called, it looks up the related object through the accessor named in "via", and on that result, returns whatever the method named in "to" returns.
If one of these Thing objects is created by calling Thing->create(), and no value is specified for owner_id, owner or owner_name, then the system will find a Person object where its 'name' is 'No one' and assign the Thing's owner_id to point to that Person. If no matching Person is found, it will first create one with the name 'No one'.
Sometimes it's useful to have a property that is an alias for another property, perhaps as a refactoring tool or to make the API clearer. The is accomilished by defining an indirect property where the 'via' is __self__.
class Thing { id_by => 'thing_id', has => [ owner => { is => 'Person', id_by => 'owner_id' }, titleholder => { via => '__self__', to => 'owner' }, ] };
In this case, 'titleholder' is an alias for the 'owner' property. titleholder can be called as a method any place owner is a valid method call. BoolExprs may refer to titleholder, but any such references will be rewrittn to 'owner' when they are normalized.
In some cases, objects may be loaded using a parent class, but all the objects are binned into some other subclass.
class Widget { has => [ manufacturer => { is => 'String', valid_values => ['CoolCo','Vectornox'] }, ], is_abstract => 1, sub_classification_method_name => 'subclasser', }; sub Widget::subclasser { my($class,$pending_object) = @_; my $subclass = 'Widget::' . $pending_object->manufacturer; return $subclass; } class Widget::CoolCo { is => 'Widget', has => 'serial_number', }; class Widget::Vextornox { is => 'Widget', has => 'series_string', } my $cool_widget = Widget->create(manufacturer => 'CoolCo'); $cool_widget->isa('Widget::CoolCo'); # evaluates to true $cool_widget->serial_number(12345); # works $cool_widget->series_srting(); # dies
In the class definition for the parent class, Widget, it is marked as being an abstract class, and the sub_classification_method_name specifies the name of a method to call whenever a new Widget object is created or loaded. That method is passed the pre-subclassed object and must return the fully qualified subclass name the object really belongs in. All the objects returned to the caller will be blessed into the appropriate subclass.
Alternatively, a property can be designated to hold the fully qualified subclass name.
class Widget { has => [ subclass_name => { is => 'String', valid_values => ['Widget::CoolCo', 'Widget::Vectornox'] }, ], is_abstract => 1, subclassify_by => 'subclass_name', } my $cool_widget = Widget->create(subclass_name => 'Widget::CoolCo'); $cool_widget = Widget::CoolCo->create(); # subclass_name is automatically "Widget::CoolCo"
These subclass names will be saved to the data source if the class has a data source. Also, when objects of the base class are retrieved with get(), the results will be automatically put in the appropriate child class.
If the data_source of a class definition is a hashref instead of a simple string, that defines an in-line data source. The only required item in that hashref is "is", which declares what class this data source will be created from, such as "UR::DataSource::Oracle" or "UR::DataSource::File". From there, each type of data source will have its own requirements for what is allowed in an inline definition.
For UR::DataSource::RDBMS-derived data sources, it accepts these keys corresponding to the properties of the same name:
server, user, auth, owner
For UR::DataSource::File data sources:
server, file_list, column_order, sort_order, skip_first_line, delimiter, record_separator
In addition, file is a synonym for server.
For UR::DataSource::FileMux data sources:
column_order, sort_order, skip_first_line, delimiter, record_separator, required_for_get, constant_values, file_resolver
In addition, resolve_path_with can replace "file_resolver" and accepts several formats:
Finally, "base_path" and
"resolve_path_with" can be used together.
In this case, resolve_path_with is a listref of property names, base_path is
a string specifying the first part of the pathname. The final path is
created by joining the base_path and all the property's values together with
'/', as in
join('/', $base_path, param1, param2, ..., paramn
)
UR::Object::Type, UR::Object::Property, UR::Manual::Cookbook
2022-01-17 | perl v5.32.1 |