2011-04-01

Dynamic Assets: Part I - Definition

I am currently working on a feature of a project that allows users to track assets that exist at a location. The interesting part of the feature is that an asset can be almost anything, and that the properties of a single instance of an asset may have little or no overlap with the properties of a different asset.

Properties in this context refer not just to the attributes of an asset, but also to how those attributes are displayed, entered by the user when adding/editing an asset, and validated when the user is saving an asset. Assets can have sub-assets, forming a tree. And the most interesting wrinkle: users should be able to define their own assets, with properties' data-types, display and validation all user-controlled.

I'm not going to talk about how the assets are stored (until a few days ago it was a toss-up between schema-less MySQL or MongoDB.) Instead, what follows is series of posts describing our solution for defining an asset, displaying the add and edit forms and validating an asset. This first post covers how asset types are defined.

An asset is basically a bag of properties, and each property has attributes that define what values it can hold and hints about how it should be presented to the user when displaying or editing.

For example: the plumbing system in your home may have properties like "installation date" or "water source" (where water source may be either "city" or "well".) The system may also have sub-assets, like "water heater", which has properties like "type" (gas, electric, solar) and "last maintenance date". There might also be a list of "showers", which are also sub-assets, and have properties like "location", "size" and "needs to be cleaned".

Our first pass was to define a class for each asset type, which would include the type's properties and how those properties should be displayed. The problem with this solution was two-fold: first, we don't currently know what properties each of our assets will or won't need to have (so an asset type's schema will be in flux); and second, we don't want our users to have to write code in order to define their own asset types. In fact, we don't know all the assets we will need to track for our own purposes yet.

Our solution was to store asset definitions as data instead of code. Then, all we need is a mechanism that reads a definition and builds a dynamic "class-less" asset object. The definition syntax is JSON, though the code that actually constructs concrete Asset instances doesn't care how the definitions are stored.

Here is some of the definition for the above plumbing system (I've intentionally added some constraints to demonstrate other bits of the definition syntax):
{
  "plumbing" : {
    "type" : "plumbing",
    "display" : "Home Plumbing",
    "instance_name" : "Installed %installation_date%",
    "fields" : {
      "water_source" : {
        "type" : "string",
        "options" : ["city", "well"],
        "other" : "Where does the water come from",
        "default" : "city"
      },
      "installation_date" : {
        "type" : "date",
        "required" : true
      },
      "water_heater" : {
        "type" : "subasset",
        "options" : ["gas_heater", "electric_heater"],
      },
      "showers" : {
        "type" : "subasset",
        "options" : ["shower"],
        "collection" : true,
        "max" : 5
      }
    }
  },

  "another_type" : {
    ...
  },
  ...
}
All assets must have a "type", which must be unique among all asset definitions. "display" is an optional field; if not specified, the display is the "type" string upper camel-cased and with underscores replaced with spaces (e. g. "plumbing_system" becomes "Plumbing System"). The display is used to identify an asset instance's type to the user, mainly on entry/edit forms and reporting.

The "instance_name" is the formatting string used to present a specific asset to the user. Wrapping a field name in %'s will substitute the actual value of that field when displaying the asset to the user. In the above example, if a plumbing asset has an installation date of "6/12/2009", then the asset would be displayed to the user as "Installed 6/12/2009". If no instance_name is given, the instance name defaults to the asset's id.

The rest of the asset is a list of fields, indexed by the internal field name, and each having some combination of descriptive attributes. The "type" attribute is the only required attribute for a field, and must have one of the values "string", "int", "boolean", "datetime", "date", "float" or "subasset". The difference between date and datetime is purely in the way that they are presented to the user (and how granularly the value is stored: dates are always rounded to midnight.)

"required" is a boolean flag indicating whether the asset can be saved without a value in that field or not. "hidden" is another boolean flag, specifying if the field should be presented to the user. We have to be careful that if a field is required and hidden the application must provide a value. A good way to do that is with the "default" attribute, which specifies the default value for that field. If the field is not hidden, the default value will be pre-entered or selected for the user on an "Add Asset" form.

The "options" attribute lists the valid values for a field. The exception is for "subasset" fields, in which case options is a list of valid subasset types (in our plumbing example, gas heaters and electric heaters could be their own asset types with their own set of fields, and an instance of either would be a valid value for the "water_heater" field.) If no type options are listed for a subasset field, any asset type is assumed to be valid.

If a user should be allowed to enter their own value in addition to the, then the "other" attribute can be used. If "other" is a boolean true, then an "Other" option will be added to the list of options, and the user will be able to enter whatever value they like. "other" can also be a string, in which case it is the text to display as the "Other" option, and the label on whatever form field is used to capture the user's input.

Some fields hold multiple values. In these cases, the "collection" attribute is set to boolean true. By default, the same value can appear in the collection multiple times. If that is not desired, the "unique" attribute can be used to validate that each value appears only once. Using combinations of "collection", "unique" and "options" can drive some pretty complex form behaviors (which will be shown in a future post.)

There are also "max" and "min" attributes. On a collection, the value of the attributes are integers specifying how many values are allowed to be in that collection. Otherwise, they represent upper and lower bounds on integer, float, date and datetime values.

The definition syntax so far gives us all the flexibility we need to define our project's known asset types, and we believe it will be useful in the future for easily adding new types and allowing our users to define their own assets types (probably through a form that translates into the JSON definition.)

Part II will be an overview of we actually use these definitions to form "class-less" asset objects on-the-fly.

3 comments:

  1. This looks pretty interesting! Sounds like this approach will work fine, unless/until you start needing to define different behaviors for asset types. Then what?

    ReplyDelete
  2. In a post coming soon, I'll show how we construct these things. The short answer is that Assets don't really have behaviors; they just have properties that define their values and how they should be displayed.

    The longer answer is that we could have a specific class for an Asset type that needs special behaviors. The class would then be responsible for building itself correctly, and the factory that creates Assets for us would need to know to instantiate that class instead of the generic Asset class.

    ReplyDelete