Masterplan

Masterplan is a library for comparing Ruby data structures against predefined templates.

At Travel IQ, this is used to define a canonical definition of the structure and format of the requests and responses of our APIs. In short, you get something that is similar to a XML Scheme - a template against which your data can be compared, to ensure its correctness programmatically. Only without the XML part.

Examples make this easier to explain. Say you have a webservice or class that produces a Ruby data structure like this:

{
  :airports => [
    {
      :name => "Tegel Airport",
      :code => "TXL",
      :latitude => 2.35454,
      :longitude => 54.67867
    },
    {
      :name => "Schönefeld Airport",
      :code => "SXF",
      :latitude => 5.35454,
      :longitude => 34.67867
    }
  ]
}

and so on and so forth. In your tests of this service, you want to make sure that this structure follows certain rules:

  • It’s a Hash with one key, :airports

  • The value is an Array, and it can be empty, but not null

  • Each entry is a hash

  • Each hash has the keys :name, :code, :latitude, :longitude

  • No value of these keys is ever null

  • Name and code are strings

  • The strings aren’t empty

  • The code is three characters long and consists of uppercase letters

  • Latitude and Longitude are floats

In a more XML-centric world, you would define an XML (or Relax-NG etc.) Scheme that defines all these rules and then use that to validate your output. But we also want to deliver JSON…so we’ll define the rules as a Masterplan::Document, and validate the data while it’s still Ruby, and say that as long as the source data is correct, the representation in JSON or XML will also be correct:

include Masterplan::DefineRules

doc = Masterplan::Document.new(
  :airports => [
    {
      :name => "Tegel Airport",
      :code => rule("TXL", :matches => /[A-Z]{3}/),
      :latitude => 2.35454,
      :longitude => 54.67867
    },
    {
      :name => "Schönefeld Airport",
      :code => "SXF",
      :latitude => 5.35454,
      :longitude => 34.67867
    }
  ]
)

It doesn’t look much different from the example, but there are a lot of rules built-in. You can now use the doc object to check your data against the template:

Masterplan.compare(:scheme => doc, :to => [{:example => :data}])

And it will throw a Masterplan::FailedError exception, and print out debugging data:

>> Masterplan.compare(:scheme => doc, :to => {:example => :data})
Masterplan::FailedError: keys don't match in 'root':
expected:	airports
received:	example

Expected:
{"airports"=>
  [{:latitude=>2.35454,
    :longitude=>54.67867,
    :code=>
     #<Masterplan::Rule:0x6d0bd70
      @example_value="TXL",
      @options=
       {"compare_each"=>false,
        "allow_nil"=>false,
        "included_in"=>false,
        "matches"=>/[A-Z]{3}/}>,
    :name=>"Tegel Airport"},
   {:latitude=>5.35454,
    :longitude=>34.67867,
    :code=>"SXF",
    :name=>"Schönefeld Airport"}]}

but was:
{"example"=>:data}

Another example:

>> Masterplan.compare(:scheme => doc, :to => {:airports => [{:name => "Bla", :latitude => 1.1, :longitude => 2.3, :code => "XXx"}]})
Masterplan::FailedError: value at 'root'=>'airports'=>'0'=>'code' "XXx" (String) does not match /[A-Z]{3}/ !

Expected:
{"name"=>"Tegel Airport",
 "latitude"=>2.35454,
 "code"=>
  #<Masterplan::Rule:0x6d0bd70
   @example_value="TXL",
   @options=
    {"compare_each"=>false,
     "allow_nil"=>false,
     "included_in"=>false,
     "matches"=>/[A-Z]{3}/}>,
 "longitude"=>54.67867}

but was:
{"name"=>"Bla", "latitude"=>1.1, "code"=>"XXx", "longitude"=>2.3}

The implicit rules are:

  • Each object in the data needs to be of the same class as in the template

  • hash keys must match up

  • The first element of an Array in the template is used as the template for all elements in the data. That’s why we didn’t have to restate the custom rule about the code in the above example, as only the “Tegel Airport” hash is used for all checks.

You can add extra rules with the rule method - see Masterplan::DefineRules#rule for details.

You can control the verbosity of the output with the :format option:

>> Masterplan.compare(:scheme => doc, :to => {:example => :data}, :format => :mini)

Valid values are :full (the default), or :mini. :mini produces only a one-line output (leaving out the “Expected…but was” part).

There is also an added assertion for unit tests or specs:

assert_masterplan(doc, [{:example => :data})

Use schemes as examples in documentation

A problem with webservices is that you need to keep the documentation up to date - something that is easily forgotten. If you have a masterplan document, you can use it not only as the template, but also as an example in, say, online documentation:

<pre>
  <%= JSON.dump(doc.to_hash) %>
</pre>

The to_hash method removes the Masterplan::Rule objects for clean output.

Caveat

Note that for the moment, schemes, i.e. the outermost object, can only be hashes.

Installation

(sudo) gem install masterplan

(Note that the latest version is probably not available as a gem directly).

Or, install latest version from Github with bundler by adding this to your Gemfile:

gem 'masterplan', :git => 'git://github.com/traveliq/masterplan.git'

Authors

Martin Tepper (monogreen.de), Holger Pillmann ([email protected]), Dr. Florian Odronitz ([email protected])

Contact

For questions, contact the authors or [email protected]

Contributing to masterplan

  • Check out the latest master to make sure the feature hasn’t been implemented or the bug hasn’t been fixed yet

  • Check out the issue tracker to make sure someone already hasn’t requested it and/or contributed it

  • Fork the project

  • Start a feature/bugfix branch

  • Commit and push until you are happy with your contribution

  • Make sure to add tests for it. This is important so I don’t break it in a future version unintentionally.

  • Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.

Copyright © 2011 www.travel-iq.com. See LICENSE.txt for further details.