Module: Hoodoo::Presenters::BaseDSL

Included in:
Array, Hash, Object
Defined in:
lib/hoodoo/presenters/base_dsl.rb

Overview

A mixin to be used by any presenter that wants to support the Hoodoo::Presenters family of schema DSL methods. See e.g. Hoodoo::Presenters::Base. Mixed in by e.g. Hoodoo::Presenters::Object so that an instance can nest definitions of fields inside itself using this DSL.

Instance Method Summary collapse

Instance Method Details

#array(name, options = {}, &block) ⇒ Object

Define a JSON array with the supplied name and options. If there is a block provided, then more DSL calls inside the block define how each array entry must look; otherwise array entries are not validated / are undefined unless the :type option is specified (see below).

When an array uses :required => true, this only says that at least an empty array must be present, nothing more. If the array uses a block with fields that themselves are required, then this is only checked for if the array contains one or more entries (and is checked for each of those entries).

name

The JSON key

options

A Hash of options, e.g. :required => true

&block

Optional block declaring the fields of each array item

Array entries are normally either unvalidated, or describe complex types via a block. For simple fields, pass a :type option to declare that array entries must be of supported types as follows:

:array

Hoodoo::Presenters::Array (see #array)

:boolean

Hoodoo::Presenters::Boolean (see #boolean)

:date

Hoodoo::Presenters::Date (see #date)

:date_time

Hoodoo::Presenters::DateTime (see #datetime)

:decimal

Hoodoo::Presenters::Decimal (see #decimal)

:enum

Hoodoo::Presenters::Enum (see #enum)

:float

Hoodoo::Presenters::Float (see #float)

:integer

Hoodoo::Presenters::Integer (see #integer)

:string

Hoodoo::Presenters::String (see #string)

:tags

Hoodoo::Presenters::Tags (see #tags)

:text

Hoodoo::Presenters::Text (see #text)

:uuid

Hoodoo::Presenters::UUID (see #uuid)

Some of these types require additional parameters, such as :precision for Hoodoo::Presenters::Decimal or :from for Hoodoo::Presenters::Enum. For any options that are to apply to the the new Array simple type fields, prefix the option with the string field_ - for example, :field_precision => 2.

It does not make sense to attempt to apply field defaults to simple type array entries via :field_default; don’t do this.

In the case of :type => :array, the declaring Array is saying that its entries are themselves individually Arrays. This means that validation will ensure and rendering will assume that each of the parent Array entries are themselves Arrays, but will not validte the child Array contents any further. It is not possible to declare an Array with a child Array that has further children, or has child-level validation; instead you would need to use the block syntax, so that the child Array was associated to some named key in the arising Object/Hash making up each of the parent entries.

Block syntax example

Mandatory JSON field “currencies” would lead to an array where each array entry contains the fields defined by Hoodoo::Data::Types::Currency along with an up-to-32 character string with field name “notes”, that field also being required. Whether or not the fields of the referenced Currency type are needed is up to the definition of that type. See #type for more information.

class VeryWealthy < Hoodoo::Presenters::Base
  schema do
    array :currencies, :required => true do
      type :Currency
      string :notes, :required => true, :length => 32
    end
  end
end

Simple type syntax without field options

An optional Array which consists of simple UUIDs as its entries:

class UUIDCollection < Hoodoo::Presenters::Base
  schema do
    array :uuids, :type => :uuid
  end
end

# E.g.:
#
# {
#   "uuids" => [ "...uuid...", "...uuid...", ... ]
# }

Validation of data intended to be rendered through such a schema declaration would make sure that each array entry was UUID-like.

Simple type syntax with field options

An optional Array which consists of Decimals with precision 2:

class DecimalCollection < Hoodoo::Presenters::Base
  schema do
    array :numbers, :type => :decimal, :field_precision => 2
  end
end

# E.g.:
#
# {
#   "numbers" => [ BigDecimal.new( '2.2511' ) ]
# }


167
168
169
170
# File 'lib/hoodoo/presenters/base_dsl.rb', line 167

def array( name, options = {}, &block )
  ary = property( name, Hoodoo::Presenters::Array, options, &block )
  internationalised() if ary.is_internationalised?()
end

#boolean(name, options = {}) ⇒ Object

Define a JSON boolean with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true



374
375
376
# File 'lib/hoodoo/presenters/base_dsl.rb', line 374

def boolean( name, options = {} )
  property( name, Hoodoo::Presenters::Boolean, options )
end

#date(name, options = {}) ⇒ Object

Define a JSON date with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true



383
384
385
# File 'lib/hoodoo/presenters/base_dsl.rb', line 383

def date( name, options = {} )
  property( name, Hoodoo::Presenters::Date, options )
end

#datetime(name, options = {}) ⇒ Object

Define a JSON datetime with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true



392
393
394
# File 'lib/hoodoo/presenters/base_dsl.rb', line 392

def datetime( name, options = {} )
  property( name, Hoodoo::Presenters::DateTime, options )
end

#decimal(name, options = {}) ⇒ Object

Define a JSON decimal with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true and mandatory :precision => [decimal-precision-number]



365
366
367
# File 'lib/hoodoo/presenters/base_dsl.rb', line 365

def decimal( name, options = {} )
  property( name, Hoodoo::Presenters::Decimal, options )
end

#enum(name, options = {}) ⇒ Object

Define a JSON string which can only have a restricted set of exactly matched values, with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true and mandatory :from => [array-of-allowed-strings-or-symbols]



414
415
416
# File 'lib/hoodoo/presenters/base_dsl.rb', line 414

def enum( name, options = {} )
  property( name, Hoodoo::Presenters::Enum, options )
end

#float(name, options = {}) ⇒ Object

Define a JSON float with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true



355
356
357
# File 'lib/hoodoo/presenters/base_dsl.rb', line 355

def float( name, options = {} )
  property( name, Hoodoo::Presenters::Float, options )
end

#hash(name, options = {}, &block) ⇒ Object

Define a JSON object with the supplied name and optional constraints on properties (like hash keys) and property values (like hash values) that the object may contain, in abstract terms.

name

The JSON key

options

A Hash of options, e.g. :required => true

&block

Optional block declaring the fields making up the nested hash

Block-based complex type examples

Example 1

A Hash where keys must be <= 16 characters long and values must match a Hoodoo::Data::Types::Currency type (with the default Hoodoo::Data::Types namespace use arising from the Symbol passed to the #type method).

class CurrencyHash < Hoodoo::Presenters::Base
  schema do
    hash :currencies do
      keys :length => 16 do
        type :Currency
      end
    end
  end
end

See Hoodoo::Presenters::Hash#keys for more information and examples.

Example 2

A Hash where keys must be ‘one’ or ‘two’, each with a value matching the given schema. Here, the example assumes that a subclass of Hoodoo::Presenters::Base has been defined under the name of SomeNamespace::Types::Currency, since this is passed as a class reference to the #type method.

class AltCurrencyHash < Hoodoo::Presenters::Base
  schema do
    hash :currencies do
      key :one do
        type SomeNamespace::Types::Currency
      end

      key :two do
        text :title
        text :description
      end
    end
  end
end

See Hoodoo::Presenters::Hash#key for more information and examples.

Simple types

As with #array, simple types can be declared for Hash key values by passing a :type option to Hoodoo::Presenters::Hash#key or Hoodoo::Presenters::Hash#keys. See the #array documentation for a list of permitted types.

For individual specific keys in Hoodoo::Presenters::Hash#key, it does make sense sometimes to specify field defaults using either a :default or :field_default key (they are synonyms). For arbitrary keys via Hoodoo::Presenters::Hash#keys the situation is the same as with array entries and it does not make sense to specify field defaults.

Simple type example

class Person < Hoodoo::Presenters::Base
  schema do
    hash :name do
      key :first, :type => :text
      key :last,  :type => :text
    end

    hash :address do
      keys :type => :text
    end

    hash :identifiers, :required => true do
      keys :length => 8, :type => :string, :field_length => 32
    end
  end
end

The optional Hash called name has two optional keys which must be called first or last and have values that conform to Hoodoo::Presenters::Text.

The optional Hash called address has arbitrarily named unbounded length keys which where present must conform to Hoodoo::Presenters::Text.

The required Hash called identifiers hash arbitrarily named keys with a maximum length of 8 characters which must have values that conform to Hoodoo::Presenters::String and are each no more than 32 characters long.

Therefore the following payload is valid:

data = {
  "name" => {
    "first" => "Test",
    "last" => "Testy"
  },
  "address" => {
    "road" => "1 Test Street",
    "city" => "Testville",
    "post_code" => "T01 C41"
  },
  "identifiers" => {
    "primary" => "9759c77d188f4bfe85959738dc6f8505",
    "postgres" => "1442"
  }
}

Person.validate( data )
# => []

The following example contains numerous mistakes:

data = {
  "name" => {
    "first" => "Test",
    "surname" => "Testy" # Invalid key name
  },
  "address" => {
    "road" => "1 Test Street",
    "city" => "Testville",
    "zip" => 90421 # Integer, not Text
  },
  "identifiers" => {
    "primary" => "9759c77d188f4bfe85959738dc6f8505_441", # Value too long
    "postgresql" => "1442" # Key name too long
  }
}

Person.validate( data )
# => [{"code"=>"generic.invalid_hash",
#      "message"=>"Field `name` is an invalid hash due to unrecognised keys `surname`",
#      "reference"=>"name"},
#     {"code"=>"generic.invalid_string",
#      "message"=>"Field `address.zip` is an invalid string",
#      "reference"=>"address.zip"},
#     {"code"=>"generic.invalid_string",
#      "message"=>"Field `identifiers.primary` is longer than maximum length `32`",
#      "reference"=>"identifiers.primary"},
#     {"code"=>"generic.invalid_string",
#      "message"=>"Field `identifiers.postgresql` is longer than maximum length `8`",
#      "reference"=>"identifiers.postgresql"}]


326
327
328
329
# File 'lib/hoodoo/presenters/base_dsl.rb', line 326

def hash( name, options = {}, &block )
  hash = property( name, Hoodoo::Presenters::Hash, options, &block )
  internationalised() if hash.is_internationalised?()
end

#integer(name, options = {}) ⇒ Object

Define a JSON integer with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true



336
337
338
# File 'lib/hoodoo/presenters/base_dsl.rb', line 336

def integer( name, options = {} )
  property( name, Hoodoo::Presenters::Integer, options )
end

#internationalised(options = nil) ⇒ Object

Declares that this Type or Resource contains fields which will may carry human-readable data subject to platform interntionalisation rules. A Resource which is internationalised automatically gains a language field (part of the Platform API’s Common Fields) used in resource representations. A Type which is internationalised gains nothing until it is cross-referenced by a Resource definion, at which point the cross-referencing resource becomes itself implicitly internationalised (so it “taints” the resource). For cross-referencing, see #type.

options

Optional options hash. No options currently defined.

Example - a Member resource with internationalised fields such as the member’s name:

class Member < Hoodoo::Presenters::Base
  schema do

    # Say that Member will contain at least one field that holds
    # human readable data, causing the Member to be subject to
    # internationalisation rules.

    internationalised

    # Declare fields as normal, for example...

    text :name

  end
end


683
684
685
686
# File 'lib/hoodoo/presenters/base_dsl.rb', line 683

def internationalised( options = nil )
  options ||= {}
  @internationalised = true
end

#is_internationalised?Boolean

An enquiry method related to, but not part of the DSL; returns true if the schema instance is internationalised, else false.

Returns:



691
692
693
# File 'lib/hoodoo/presenters/base_dsl.rb', line 691

def is_internationalised?
  !! @internationalised
end

#object(name, options = {}, &block) ⇒ Object

Define a JSON object with the supplied name and options.

name

The JSON key.

options

Optional Hash of options, e.g. :required => true

&block

Block declaring the fields making up the nested object

Example - mandatory JSON field “currencies” would lead to an object which had the same fields as Hoodoo::Data::Types::Currency along with an up-to-32 character string with field name “notes”, that field also being required. Whether or not the fields of the referenced Currency type are needed is up to the definition of that type. See #type for more information.

class Wealthy < Hoodoo::Presenters::Base
  schema do
    object :currencies, :required => true do
      type :Currency
      string :notes, :required => true, :length => 32
    end
  end
end

Raises:

  • (ArgumentError)


44
45
46
47
48
49
# File 'lib/hoodoo/presenters/base_dsl.rb', line 44

def object( name, options = {}, &block )
  raise ArgumentError.new( 'Hoodoo::Presenters::Base#Object must have block' ) unless block_given?

  obj = property( name, Hoodoo::Presenters::Object, options, &block )
  internationalised() if obj.is_internationalised?()
end

#resource(resource_info, options = nil) ⇒ Object

Declare that a resource of a given name is included at this point. This is only normally done within the description of the schema for an interface. The fields of the given named resource are considered to be defined inline at the point of declaration - essentially, it’s macro expansion.

resource_info

The Hoodoo::Presenters::Base subclass for the Resource in question, e.g. Product. The deprecated form of this interface takes the name of the type to nest as a symbol, e.g. :Product, in which case the Resource must be declared within nested modules Hoodoo::Data::Types.

options

Optional options hash. No options currently defined.

Example - an iterface takes an Outlet resource in its create action.

class Outlet < Hoodoo::Presenters::Base
  schema do
    internationalised

    text :name
    uuid :participant_id, :resource => :Participant, :required => true
    uuid :calculator_id,  :resource => :Calculator
  end
end

class OutletInterface < Hoodoo::Services::Interface
  to_create do
    resource Outlet
  end
end

It doesn’t make sense to mark a resource ‘field’ as :required in the options since the declaration just expands to the contents of the referenced resource and it is the definition of that resource that determines whether or not its various field(s) are optional / required. That is, the following two declarations behave identically:

resource Outlet

resource Outlet, :required => true # Pointless option!


636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
# File 'lib/hoodoo/presenters/base_dsl.rb', line 636

def resource( resource_info, options = nil )
  options ||= {}

  if resource_info.is_a?( Class ) && resource_info < Hoodoo::Presenters::Base
    klass = resource_info
  else
    begin
      klass = Hoodoo::Data::Resources.const_get( resource_info )
    rescue
      raise "Hoodoo::Presenters::Base\#resource: Unrecognised resource name '#{ resource_info }'"
    end
  end

  self.instance_exec( &klass.get_schema_definition() )
end

#string(name, options = {}) ⇒ Object

Define a JSON string with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true and mandatory :length => [max-length-in-chars]



346
347
348
# File 'lib/hoodoo/presenters/base_dsl.rb', line 346

def string( name, options = {} )
  property( name, Hoodoo::Presenters::String, options )
end

#tags(field_name, options = nil) ⇒ Object

Declares that this Type or Resource has a string field of unlimited length that contains comma-separated tag strings.

field_name

Name of the field that will hold the tags.

options

Optional options hash. See Hoodoo::Presenters::BaseDSL.

Example - a Product resource which supports product tagging:

class Product < Hoodoo::Presenters::Base
  schema do
    internationalised

    text :name
    text :description
    string :sku, :length => 64
    tags :tags
  end
end


437
438
439
440
# File 'lib/hoodoo/presenters/base_dsl.rb', line 437

def tags( field_name, options = nil )
  options ||= {}
  property( field_name, Hoodoo::Presenters::Tags, options )
end

#text(name, options = {}) ⇒ Object

Define a JSON string of unlimited length with the supplied name and options.

name

The JSON key

options

A Hash of options, e.g. :required => true



402
403
404
# File 'lib/hoodoo/presenters/base_dsl.rb', line 402

def text( name, options = {} )
  property( name, Hoodoo::Presenters::Text, options )
end

#type(type_info, options = nil) ⇒ Object

Declare that a nested type of a given name is included at this point. This is only normally done within an array or object declaration. The fields of the given named type are considered to be defined inline at the point of declaration - essentially, it’s macro expansion.

type_info

The Hoodoo::Presenters::Base subclass for the Type in question, e.g. BasketItem. The deprecated form of this interface takes the name of the type to nest as a symbol, e.g. :BasketItem, in which case the Type must be declared within nested modules Hoodoo::Data::Types.

options

Optional options hash. No options currently defined.

It doesn’t make sense to mark a type ‘field’ as :required in the options since the declaration just expands to the contents of the referenced type and it is the definition of that type that determines whether or not its various field(s) are optional or required.

Example 1 - a basket includes an array of the Type described by class BasketItem:

class Basket < Hoodoo::Presenters::Base
  schema do
    array :items do
      type BasketItem
    end
  end
end

A fragment of JSON for a basket might look like this:

{
  "items": [
    {
       // (First BasketItem's fields)
    },
    {
       // (First BasketItem's fields)
    },
    // etc.
  ]
}

Example 2 - a basket item refers to a product description by having its fields inline. So suppose we have this:

class Product < Hoodoo::Presenters::Base
  schema do
    internationalised
    text :name
    text :description
  end
end

class BasketItem < Hoodoo::Presenters::Base
  schema do
    object :product_data do
      type Product
    end
  end
end

…then this would be a valid BasketItem fragment of JSON:

{
  "product_data": {
    "name": "Washing powder",
    "description": "Washes whiter than white!"
  }
}

It is also possible to use this mechanism for inline expansions when you have, say, a Resource defined entirely in terms of something reused elsewhere as a Type. For example, suppose the product/basket information from above included information on a Currency that was used for payment. It might reuse a Type; meanwhile we might have a resource for managing Currencies, defined entirely through that Type:

class Currency < Hoodoo::Presenters::Base
  schema do
    string :curency_code, :required => true, :length => 8
    string :symbol, :length => 16
    integer :multiplier, :default => 100
    array :qualifiers do
      string :qualifier, :length => 32
    end
  end
end

resource :Currency do
  schema do
    type Currency # Fields are *inline*
  end
end

This means that the Resource of Currency has exactly the same fields as the Type of Currency. The Resource could define other fields too, though this would be risky as the Type might gain same-named fields in future, leading to undefined behaviour. At such a time, a degree of cut-and-paste and removing the type call from the Resource definition would probably be wise.



577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
# File 'lib/hoodoo/presenters/base_dsl.rb', line 577

def type( type_info, options = nil )
  options ||= {}

  if type_info.is_a?( Class ) && type_info < Hoodoo::Presenters::Base
    klass = type_info
  else
    begin
      klass = Hoodoo::Data::Types.const_get( type_info )
    rescue
      raise "Hoodoo::Presenters::Base\#type: Unrecognised type name '#{ type_info }'"
    end
  end

  self.instance_exec( &klass.get_schema_definition() )
end

#uuid(field_name, options = nil) ⇒ Object

Declares that this Type or Resource _refers to_ another Resource instance via its UUID. There’s no need to declare the presence of the UUID field _for the instance itself_ on all resource definitions as that’s implicit; this #uuid method is just for relational information (AKA associations).

field_name

Name of the field that will hold the UUID.

options

Options hash. See below.

In addition to standard options from Hoodoo::Presenters::BaseDSL, extra option keys and values are:

:resource

The name of a resource (as a symbol, e.g. :Product) that the UUID should refer to. Implementations may use this to validate that the resource, where a UUID is provided, really is for a Product instance and not something else. Optional.

Example - a basket item that refers to an integer quantity of some specific Product resource instance:

class BasketItem < Hoodoo::Presenters::Base
  schema do
    integer :quantity, :required => true
    uuid :product_id, :resource => :Product
  end
end


470
471
472
473
# File 'lib/hoodoo/presenters/base_dsl.rb', line 470

def uuid( field_name, options = nil )
  options ||= {}
  property(field_name, Hoodoo::Presenters::UUID, options)
end