Issue with Declarative Object Definition

The FreeIPA UI approach attempts to use a  Domain Specific languages for UI definition.  While I like how it has emerged thus far, I don’t feel we are yet where we need it to be.

In the current incarnation, the definition of the UI for an entity to look like this:


IPA.entity_factories.group =  function () {
  return IPA.entity({
        'name': 'group'
    }).
        facet(
            IPA.search_facet().
                column({name: 'cn'}).
                column({name: 'gidnumber'}).
                column({name: 'description'}).
                dialog(
                    IPA.add_dialog({
                        'name': 'add',
                        'title': IPA.messages.objects.group.add
                    }).
                        field(IPA.text_widget({name: 'cn', undo: false})).
                        field(IPA.text_widget({name: 'description', undo: false})).
                        field(IPA.checkbox_widget({
                            name: 'posix',
                            label: IPA.messages.objects.group.posix,
                            undo: false,
                            checked: 'checked'})).
                        field(IPA.text_widget({name: 'gidnumber', undo: false})))).
        facet(
            IPA.details_facet().
                section(
                    IPA.stanza({
                        name: 'details',
                        label: IPA.messages.objects.group.details
                    }).
                        input({name: 'cn' }).
                        input({name: 'description'}).
                        input({name: 'gidnumber' }))).
        facet(
            IPA.group_member_user_facet({
                'name': 'member_user'
            })).
        facet(
            IPA.association_facet({
                name: 'memberof_group',
                associator: IPA.serial_associator
            })).
        facet(
            IPA.association_facet({
                name: 'memberof_netgroup',
                associator: IPA.serial_associator
            })).
        facet(
            IPA.association_facet({
                name: 'memberof_role',
                associator: IPA.serial_associator
            })).
        standard_associations();
};

There are a couple things I want to improve, but have not yet hit exactly how to solve them.

  1. Split the UI Toolkit into a generic, reusable part and the aspect that is specific to FreeIPA
  2. Simplify the object lifecycle.

A widget in the IPA code currently has the following methods  as part of its contract:

  • construction code:  The initial function called to create the object
  • init():  called after all dependencies have been filled
  • validate():  once a field jhas been edited, check to see if it still matches the pattern accepted by the server

This pattern is followed by entities and other visible components.

What I would like to do is to collapse the constructor and init code into a single function call.  The reason that they are currently split is that we were creating all of our objects up front, before we got the Internationalized strings back from the server.  The solution to that was to register the functions early, and to call the constructors after the message came back from the server.

However, the late init is currently required in the declarative code above.   A call to IPA.association_facet or IPA.stanza does not have the context of the initial entity.  In order to populate the label and validation code for the widgets we need to know which entity we are working with.  So these objects hold off on defining these things until later.  The sequence is :

  1. create entity
  2. create subordinate object
  3. add subordinate object to entity
  4. entity tells subordinate object “your entity_name is ‘group'”
  5. entity calls init on the subordinate object
  6. subordinate object calls init on its subordinates.

It is interesting to note that, while the subordinate objects need to know the name of the entity they are working with, they don’t need to keep a pointer to their parent:  all behavior is driven from the top of the tree on down.

I’d like to keep this approach, but here we hit something as fundamental as,Gödel’s incompleteness theorems. You have a choice:  in a declarative statement, you can either return the context of the parent or you have to explicitly return to it from the child.  For example, when adding a section to an entity,  you can either do:

 

that.add_facet = function(facet){
      that.facets.push(section);
      return that;
}

or you can do:

that.facet = function(spec){
     that.sections.push(IPA.facet(spec));
     return facet;
}

Notice the difference of the the value returned.   The first function allows you to add multiple facets to the entity like this:

  return IPA.entity().
      add_facet(facet1).
      add_facet(facet2).
      add_facet(facet1);

The drawback is the you cannot chain the construction of subordinate objects to the facets.  With the second form, you can:

 return IPA.entity().
      facet({property:'value'}).
      section(key:'value'}).
      field({name:'field1'});

The major limitation here is that you can only add one thing. Another minor drawback is that things don’t nest as nicely.
If objects keep pointers to their parents, you can do an explicit return to parent scope. This is the equivalent of a closed parenthesis in a programming language:

 return IPA.entity().
      facet({name:'facet1'}).
          section(key:'value2'}).
      parent().
      facet({name:'facet2'}).
            section(name:'section1'}).
                field({name:'field1'}).parent().
                field({name:'field2'}).parent().
            parent().
            section(name:'section2'}).
                field({name:'field3'}).parent().
                field({name:'field4'}).parent().
            parent().
      parent();

Note that this messes up the automated indentation of your language, which might be a fatal flaw if you are programming in Python.

Another option is to extract the state into some other object, along the lines of the builder pattern.

var builder = IPA.builder();
builder.entity = "group";

 return builder.entity().
      facet(builder.details_facet({name:'facet1'}).
                 section(
                      builder.section(key:'value2'})).
                          field()
      facet(builder.facet({name:'facet2'}).
            section(builder.section({name:'section1'}).
                field({name:'field1'}).parent().
                field({name:'field2'}).parent()
       ));

The close parenthesis mean the end of the subordinate scopes.

The shortcoming of this approach is that every object you want to create has to be added to your builder, leading to a lot of duplicated function calls.  Well, I guess that is only true of objects that require special treatment, but that is starting to look like it is most objects.

What we want is a decorator, something that can take a call

Something.text_widget({name:’somename’});

and inject the code into the spec:

spec.label = Something.label_factory.get(current_entity.name,. field.name);
spec.format = Something.format_factory.get(current_entity.name,. field.name);

and then pass on the call:

return IPA.text_widget(spec);

It is tempting to think that we could do this in Javascript, by replacing a function pointer with a wrapper.  The problem is “Least surprise.”  As people will look at the code to debug what is happending, and not understand how this magic value gets injected.

It might be tempting to go to a completely declarative approach and say that the object definitions will be done in JSON, parsed, and that value is what will be used.  IPA  started off with this approach early on.  We moved away from it for  two reasons:  first, the added level indirection made debugging more complex.    Second, it made it harder to add custom behavior to an entity.

The builder approach is looking better all the time.  If we want to stick with the builder approach, what we should probably do is provide a way to bulk register functions that do little more than wrap the widgets with the functionality defined.  The API for this would be a lng the lines of:

SOMETHING.register_widgets([IPA.text_widget, IPA.radio_widget],  function(spec){....});

 

The indefatigable Endi Dewata came up with one better:

IPA.entity = function() {

     // create entity with I18n messages
     var that = ...

     // context variables
     that.facet = null;
     that.section = null;
     that.column = null;
     that.field = null;
     that.dialog = null;

     that.search_facet = function() {

         // create search facet with i18n messages
         that.facet = ...

         // add search facet to the entity
         that.add_facet(that.facet);

         //return entity
         return that;
     };

     that.details_facet = function() {

         // create details facet with i18n messages
         that.facet = ...

         // add details facet to the entity
         that.add_facet(that.facet);

         //return entity
         return that;
     };

     that.column = function() {

         // create column with i18n messages
         that.column = ...

         // add column to the latest facet
         that.facet.add_column(column);

         //return entity
         return that;
     };

     that.section = function() {

         // create section with i18n messages
         that.section = ...

         // add section to the latest facet
         that.facet.add_section(that.section);

         //return entity
         return that;
     };

     that.details_text = function() {

         // create text field with i18n messages
         that.field = ...

         // add text field to the latest facet
         that.section.add_field(that.field);

         // return entity
         return that;
     };
};

This way you can chain the construction of subordinate objects. You don’t even need the parent() method. And it’s cleaner than using a separate builder object.


IPA.user = function() {

     return IPA.entity({ name: 'user' }).
         search_facet().
             column({ name: 'uid' }).
             column({ name: 'cn' }).
             adder_dialog().
                 dialog_text({ name: 'uid' }).
                 dialog_text({ name: 'cn' }).
         details_facet().
             section().
                 details_text({ name: 'uid' }).
                 details_text({ name: 'cn' }).
         association_facets();
};

I think this last is a lot closer to what we are going to go with. I might still extract the builder into a separate object from the actual entity. I would definitely do it in a language like C++ or Java, where the language enforces information hiding etc. As I can see it, the only drawback to Endi’s approach is that you lose the nesting of parenthesis.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.