SimpleMapper: A Non-Relational Object Mapper

The simple data structures expressed through JSON are becoming more relevant every day. We use them to exchange information between services via Thrift, from server to HTTP client for RESTful services, or from server to HTTP client for Ajax-heavy web applications.

While JSON structures map nicely to core Ruby objects (Hash, Array, etc.), those structures themselves do not provide business logic specific to an application’s problem domain. Additionally, it is increasingly the case that the JSON object structure is the canonical representation your application receives for a given entity; a Thrift client, for instance, will send and receive JSON to the corresponding service.

The object-relational mapper (ORM) traditionally addresses the need for business logic and domain-specific semantics at the application tier when working with a provider of structured data (i.e. a SQL-compliant relational database). SimpleMapper attempts to provide something somewhat analogous to the ORM, but with JSON-based or “simple” data structures as the foundation for structuring data, rather than a relational model of classes/relations and their constraints, references, etc.

What?

Say you need a service that serves and accepts JSON structures representing “users” (because, really, who doesn’t need that service?). You might see data structures like this (in JSON):

{
    id:            '348179ce-4d38-11df-8f4f-cd459e8422de',
    registered_at: '2010-04-01 09:22:17-0400',
    email:         '[email protected]',
    title:         'Mr.',
    first_name:    'Hot',
    last_name:     'Pantz',
    address:       {
        address:  'One My Way',
        address2: 'Not That Way',
        city:     'New York',
        state:    'NY',
        postal:   '10010'
    }
}

That’s not a particularly complex data structure, but let’s note a few things:

  • The :id appears to be a GUID. Fun.

  • The :registered_at is a timestamp with a particular format. Also fun.

  • There’s an :email address. More fun.

All of those things and their noted funness would benefit from business logic for validation purposes, encapsulation, etc. Our OOPified brains long for these structures to map to an object.

So, go ahead.

class User
  # Get our attribute mapping magic
  include SimpleMapper::Attributes

  # Define our typed attributes
  maps :id,            :type => :simple_uuid, :default => :from_type
  maps :registered_at, :type => :timestamp,   :default => :from_type

  # Simple string attributes don't need a type
  maps :email
  maps :title
  maps :first_name
  maps :last_name

  # nested attribute for the address.
  maps :address do
    # This block is evaluated in the context of new
    # class that has the SimpleMapper behaviors
    [:address, :address2, :city, :state, :postal].each {|attr| maps attr}

    # How about a to_s that represents the full address as one string?
    def to_s
      "#{address}; #{address2}; #{city}, #{state} #{postal}"
    end
  end
end

Now you have a class that describes the data structure you’re working with. What now?

You can create new objects and spit out the simple structure.

user = User.new(:email      => '[email protected]',
                :title      => 'Mr.',
                :first_name => 'Hot',
                :last_name  => 'Pantz',
                :address    => {:address  => 'One My Way',
                                :address2 => 'Not That Way',
                                :city     => 'New York',
                                :state    => 'NY',
                                :postal   => '10010'})
# the :simple_uuid type gives GUIDs, with a :default of :from_type
# meaning it'll autopopulate
# This prints some GUID like '348179ce-4d38-11df-8f4f-cd459e8422de':
puts user.id

# And the :default of :from_type on a :timestamp type gets the current date/time.
# This will print 'DateTime'
puts user.registered_at.class
# This will print it using DateTime's default :to_s format
# like '2010-0421T07:41:46-04:00'
puts user.registered_at

# This will print out 'One My Way; Not That Way; New York, NY 10010':
puts user.address

# The :to_simple method dumps the structure out in simple object format,
# which is readily JSON-ifiable.  However, it'll enforce defaults and type
# formatting and such, so that things like timestamps will be stringified with
# the correct format.
user.to_simple

# Results in a structure like:
{
    :id            => '348179ce-4d38-11df-8f4f-cd459e8422de',
    :registered_at => '2010-04-21 07:41:46-04:00',
    :email         => '[email protected]',
    :title         => 'Mr.',
    :first_name    => 'Hot',
    :last_name     => 'Pantz',
    :address       => {
        :address  => 'One My Way',
        :address2 => 'Not That Way',
        :city     => 'New York',
        :state    => 'NY',
        :postal   => '10010',
    },
}

So, the :new constructor and the :to_simple method give you input and output from/to simple structures, while the SimpleMapper::Attributes module gives you semantics for defining higher-level classes on top of those simple structures.

What if my service needs to have something analogous to an update, such that I only get the simple structure for attributes that were changed?

user.last_name = 'Pantalonez'
user.to_simple(:changed => true)
# Results in:
# { :last_name => 'Pantalonez' }

The :changed option indicates that we only want attributes that were altered since the instance was instantiated. It doesn’t care if the values actually differ from the original, it only cares if the attribute was assigned since creation.

Similarly, you can provide a :defined option to the :to_simple invocation, and you’ll only get attributes in the resulting structure that have a non-nil value. This is useful if you’re ultimately dealing with a data source that manages such things itself (like allowing NULL on a particular database column) or is sparse and would thus prefer to not have any entry for an undefined value at all (like Cassandra, MongoDB, etc.)

What’s Coming

SimpleMapper is young as of this writing. There’s a basic type system with defaults. Support for nested structures is pretty simple, as shown above.

Features expected in the near future include:

  • collections: deal with an attribute that is a collection

  • pattern-based collections: group all key/value pairs into a single collection attribute for any keys that match a developer-specific pattern

  • ActiveModel compliance to allow validation, callbacks, etc.