Hoodoo Guides: Presenters

Purpose

This Guide describes the Hoodoo presentation layer – a lightweight schema to describe resources for both inbound data validation and outbound data rendering.

Resource definition

A service implements endpoints for one or more resources. The resources are described by your API, with common fields and behaviour mandated by the Hoodoo API Specification.

Any resource definition that uses the Hoodoo presenter layer is defined as a descendant of Hoodoo::Presenters::Base thus:

class Foo < Hoodoo::Presenters::Base
  # ...
end

Namespacing

Often a resource will have at least one corresponding class related to its persistence layer, such as the Active Record Person example in the Active Record Guide. Defining your resource classes inside a module used purely as a namespace is a good idea to avoid confusion over names. For example, there might be a Foo model and resource:

class Foo < ActiveRecord::Base
  # ...
end

module Resources
  class Foo < Hoodoo::Presenters::Base
    # ...
  end
end

Now, Foo will always refer to the model and won’t do anything unexpected such as activating namespace extensions inside Active Record, while Resource::Foo clearly refers to the Hoodoo resource.

The schema

Inside any presenter class is a schema block which uses a presenter DSL to describe its contents. The schema class method is called with a block, inside which the DSL methods are called.

module Resources
  class Foo < Hoodoo::Presenters::Base
    schema do
      # ...
    end
  end
end

Defaults and requirements

Resource fields can have default values assigned; unless specified, the field has no explicit default. For the purposes of validating the contents of a resource, a field may be marked as required; unless so marked, a field may have no value.

Rendering

The render method applies schema defaults to an inbound data set, which in the case of things like the hash field type may be quite complex. The result is a ruby Hash from an input Hash using the schema. For general resource use within a service implementation when a wider request context is available, you should always use the render_in method preferentially.

Rendering rules are:

# Although the DSL accepts Strings or Symbols...

class PresenterClass < Hoodoo::Presenters::Base
  schema do
    object :address do
      text :town
      text :state,   :required => true
      text :country, :default  => 'NZ'
      text :example, :default  => 'nil overrides this default'
    end
  end
end

# ...the renderer only accepts Strings.

data = {
  'address' => {
    'state' => 'Idaho',
    'example' => nil
  }
}

rendered = PresenterClass.render( data )

…yields:

{
  'address' => {
    'state' => 'Idaho',
    'country' => 'NZ',
    'example' => nil
  }
}

Note that for fields like dates or times, Ruby objects are not accepted; you must provide these as Strings in the input data Hash to render and render_in. The Ruby iso8601 method will generate valid results in all cases, but only rounded to integer second accuracy for times; if you want higher precision, see utility method nanosecond_iso8601. For more utility methods related to dates and times, see the Utilities Guide.

Manual outbound validation

You may wish to self-check data you are intending to return as the response to an API call, especially in non-production execution modes – when Service.config.env.production? yields false.

To do this, call validate. An array of zero or more validation errors will be returned. This includes globally defined errors such as generic.required_field_missing if option :required => true is set on a field but a value is not provided for it, along with the various generic.invalid_... errors for fields where the value is not of the expected type.

In the schema DSL, field type string requires a :length => <integer> option and validation will return generic.max_length_exceeded if the data exceeds the allowed length. Within hashes, the key part of the DSL defines specific named keys expected in the hash, while the keys part describes just the generic “shape”/schema of keys (if required) and, like string, lets you specify a :length => <integer> option if you require that keys in the hash do not exceed a certain length.

date and datetime are valid if they are ISO8601 subset dates or datetimes respectively, e.g.

Validation will check for impossible dates (allowing Feb 29 in leap years), and will accept all timezones including Z (Zulu, +00:00 / UTC) for datetimes. See the Utilities Guide for more about the underlying permitted ISO 8601 date/time subsets and their validation methods.

Important: During validation, default specifiers have NO EFFECT since it doesn’t make sense to try and apply inbound data defaults during a validation pass. Defaults are only applied during rendering, allowing your underlying data model to potentially have no defaults, with default values entirely in the application layer should you so wish. Alternatively you could use no resource-layer defaults at all and (say) rely entirely on defaults specified for database columns and/or intermediate Ruby models. In a similar vein, for updates, any required specifiers ARE IGNORED because the typical intent of a PATCH is that any omitted fields in the inbound payload simply mean there is no change being requested for that particular field. It’s only an error to omit a required field when attempting to create a new instance.

Render then validate

Default values for fields are only applied when you render something, not when you validate it. For self-checking outbound data, validating data before it goes through the renderer is wrong, because defaults have not been applied yet. You could incorrectly fail your own outbound data self-check because of an omitted value where you might have over-constrained it as both required, but also having a default which wouldn’t have yet been applied.

Instead, render first. Taking the earlier PresenterClass example for rendering and continuing this with validation:

data = {
  'address' => {
    'state' => 'Idaho',
    'example' => nil
  }
}

rendered = PresenterClass.render( data )
validation_errors = PresenterClass.validate( rendered )

…yields an empty array – there are no errors because the input data is valid. We defined the state field as mandatory though; if omitted:

data = {
  'address' => {
    'example' => nil
  }
}

rendered = PresenterClass.render( data )
validation_errors = PresenterClass.validate( rendered )

…yields:

[
  {
    "code"      => "generic.required_field_missing",
    "message"   => "Field `address.state` is required",
    "reference" => "address.state"
  }
]

Although manual validation of inbound data is an unusual use case – usually you should lean on Hoodoo for this, as described later – if you were doing so as part of a resource implementation class, you would use something along the following lines to add the result of validation to the in-compilation outbound response:

def create( context )

  # ...validate the data `context.request.body` as above, then...

  context.response.errors.merge!( validation_errors )
  return if context.response.halt_processing?

  # ...else continue...

end

The use of errors.merge! arises because the presentation engine’s validation code generates raw Ruby arrays of error data, rather than a higher level Hoodoo::Errors instance. This is not idiomatic; typically we would expect to deal with Hoodoo::Errors instances and use something like context.response.add_errors(errors_object) instead.

Automatic inbound validation

Hoodoo provides support to validate inbound data using the same schema as for resource definition. Although your service implementations are quite at liberty to use presenter methods, or their own ad hoc schemes to validate inbound body data from API calls in creation and update implementations, using Hoodoo’s code where possible is recommended to reduce your maintenance burden and chance of accidental bugs.

The basis of the Hoodoo support is from the following three methods that are used in the interface class in your service that describes your resource’s API behaviour:

Using a whole resource definition

Sometimes, your API will define a resource and all of its fields (apart from the Hoodoo standard common fields) may be writable in a POST creation request and, perhaps, even a PATCH alteration request.

class FooResourceInterface < Hoodoo::Services::Interface
  # ...boilerplate...

  to_create do
    resource PresenterClass
  end
end

This leans on previous examples and gives a class reference to the resource method of the schema. That class is fetched, its own schema examined and this is unpacked “in place”, to become the validation schema for creation actions. In the example above, no on-update schema has been given – you can either copy the to_create block inside a to_update block, use the shorthand method update_same_as_create or define a new to_update block with some schema variation, as described in the next section.

Important: Remember that required flags are deliberately ignored within to_update blocks. Omission of a field is taken to signify that no change in current value is requested.

Using bespoke fields

Sometimes, only some of the fields can be specified for creation and update, but others might arise in the resource representation.

As an extreme example, an ‘Addition’ resource might accept a field add with a value that is an array of integers, but when represented (show / GET) might render as a field sum with a value that is just a single integer giving the sum of the input array.

As a less extreme example, suppose a resource accepts programme_name and programme_code only for creation POSTs; and only allows subsequent modification of programme_name with a PATCH. No matter how the whole Programme resource associated with the ProgrammeInterface given below might look, the validation declaration would be:

class ProgrammeInterface < Hoodoo::Services::Interface
  # ...boilerplate...

  to_create do
    text   :programme_name
    string :programme_code, :length => 32
  end

  to_update do
    text :programme_name
  end
end

Resource and type references

The resource and type parts of the DSL are described in detail in RDoc. Both methods are usually provided with class references directly – e.g. resource( PresenterClass ) – so that the target class is explicit. Historically it has also been possible to pass a Symbol naming a Type or Resource which is then fetched from a hard-coded Hoodoo-derived namespace, but this is deprecated and discouraged.

Full schema example

module Resources
  class Example < Hoodoo::Presenters::Base
    schema do

      integer :quantity, :required => true
      string :client_id, :required => true, :length => 32
      string :status_callback_uri, :required => false, :length => 256

      object :reward, :required => true do
        string :provider_code, :required => true, :length => 32
        string :supplier_code, :required => true, :length => 32
        string :reward_code, :required => true, :length => 32
      end

      object :member, :required => true do
        string :id, :required => true, :length => 32
        string :first_name, :required => true, :length => 128
        string :family_name, :required => true, :length => 128
        date :dob, :required => true
        string :email, :required => true, :length => 128
      end

      object :delivery_target, :required => true do
        string :delivery_type, :required => true, :length => 32
        string :address_1, :length => 128
        string :address_2, :length => 128
        string :address_3, :length => 128
        string :suburb, :length => 128
        string :city_town, :length => 128
        string :region_state, :length => 128
        string :postcode_zip, :length => 128
        string :country_code, :length => 3
      end

      # Field "array_with_any_values" must have an Array value, with
      # any array contents permitted. If the input data has a nil
      # value for this field, then nil would be rendered and it would
      # validate. Add ':required => true' to prohibit this.
      #
      # Example valid input:
      # { 'array_with_values' => [ 'hello', 4, :world ] }
      #
      array :array_with_any_values, :default => [ 1, 2, 3 ]

      # Field "objects_with_two_text_fields" must have an Array value,
      # where values are either nil or an object with two text fields
      # "field_one" and "field_two".
      #
      # Example valid input:
      # { 'array_with_values' => [ { 'field_one' => 'one' },
      #                            { 'field_two' => 'two' } ] }
      #
      array :objects_with_two_text_fields do
        text :field_one
        text :field_two
      end

      # A field "any_allowed_hash" must have a Hash value, with any
      # hash contents permitted. 'default' could be used to provide
      # an entire default Hash value for rendering data with the
      # "any_allowed_hash" field omitted. 'nil' values as for 'array'.
      #
      hash :any_allowed_hash

      # A field "specific_allowed_keys" must have a Hash value, which
      # allows only (none or any of) the two listed key names to be
      # valid. ':default' could be used in the 'key' calls to provide
      # a whole-key default value for an omitted key.
      #
      hash :specific_allowed_keys do
        key :allowed_key_one    # Key has any allowed value
        key :allowed_key_two do # Value must match schema in block
          text :field_one
          integer :field_two, :default => 42
        end
      end

      # As above but any keys can be present in the input data. The
      # ':default' option makes no sense for the 'keys' call and its
      # use is prohibited.
      #
      hash :generic_key_description do
        keys :length => 32 do # Keys must be <=32 chars, values must
                              # match block schema. Block is optional
                              # - if omitted, any values are allowed.
          text :field_one
          integer :field_two
        end
      end

    end # 'schema do'
  end   # 'class Example < Hoodoo::Presenters::Base'
end     # 'module Resources'