Oval - Options Validator

Build Status Coverage Status Code Climate

Table of Contents

  1. Overview
  2. Module Description
  3. Usage
  4. Reference
  5. Limitations

Overview

Validate option hashes when passed to methods.

[Table of Contents]

Module Description

Using hashes to pass options to methods is a very common ruby practice. With Oval method authors may restrict callers to pass only declared options that meet requirements described in a hash declaration.

The shape of acceptable hashes is described by a simple grammar. The validation is then carried out by a recursive-descent parser that matches the actual values provided by caller against declarators that comprise the hash declaration.

A declaration consists of terminal and non-terminal declarators. Non-terminal declarators are created by methods of Oval module which have names starting with ov_ prefix. All other values (such as :symbol, 'string', nil, or Class) are terminals. Terminals use == operator to match the values provided by caller. Non-terminal use its own logic introducing more elaborate matching criteria (see for example ov_collection).

Oval raises Oval::DeclError if the declaration is not well-formed, that is if the description of options shape is erroneous. This is raised from the point of declaration. Other, more common exception is the Oval::ValueError which is raised each time the validation fails. This one is raised from within a method which takes the options as an argument.

[Table of Contents]

Usage

The usage is basically a two-step procedure. The first step is to declare options shape. This would create a validator object. The second step is to validate options within a method using the previously constructed validator. For simple hashes the entire construction may fit to a single line. Let's start with such a simple example.

Example 1: Declaring Simple Options

The method foo in the following code accepts only {} and {:foo => value} as ops, where value is arbitrary:

# Options validator
require 'oval'
class C
  extend Oval
  def self.foo(ops = {})
    ov_options[ :foo => ov_anything ].validate(ops, 'ops')
  end
end

What does it do? Just try it out:

C.foo # should pass
C.foo :foo => 10 # should pass
C.foo :foo => 10, :bar => 20 # Oval::ValueError "Invalid option :bar for ops. Allowed options are :foo"

Options are declared with ov_xxx declarators. The ov_options method should always be at the top level. Then all the allowed options should be listed inside of [] square brackets. Keys may be any values convertible to strings (i.e. a key given in declaration must respond_to? :to_s). Values are declared recursively using ov_xxx declarators or terminal declarators (any other ruby values).

In Example 1 we have declared options inside of a method for simplicity. This isn't an optimal technique. Usually options' declaration remains same for the entire lifetime of an application, so it is unnecessary to recreate the declaration each time function is called. In other words, we should move the declaration outside of the method, convert it to a singleton and only validate options inside of a function. For that purpose, the Example 1 could be modified to the following form

Example 2: Separating declaration from validation

In this example we separate options declaration from the validation to reduce costs related to options declaration:

# Options validator
require 'oval'
class C
  extend Oval
  # create a singleton declaration ov
  def self.ov
    @ov ||= ov_options[ :foo => ov_anything ]
  end
  # use ov to validate ops
  def self.foo(ops = {})
    ov.validate(ops, 'ops')
  end
end

[Table of Contents]

Reference

Declarators

A declaration of options consists entirely of what we call here declarators. The ov_options should be used as a root of every declaration (starting symbol in grammar terms). It accepts a Hash of the form

{optname1 => optdecl1, optname2 => optdecl2, ... }

as an argument. The optname# is an option name, and optdecl# is a declarator restricting the option's value. Each option name (key) must be convertible to a String. Option value declarators are non-terminal declarators (defined later in this section) or terminals (any other ruby values). The simple declaration

ov_options[ :foo => :bar ]

uses only terminals inside of ov_options and literary permits only the {:foo => :bar} or the empty hash {} as options (and nothing else). This is how terminal declarators (:foo and :bar in this example) work. More freedom may be introduced with non-terminal declarators, for example:

ov_options[ :foo => ov_anything ]

defines an option :foo which accepts any value. In what follows, we'll document all the core non-terminal declarators implemented in Oval.

ov_anything

  • Declaration
  ov_anything

or

  ov_anything[]
  • Validation - permits any value
  • Example
  ov = ov_options[ :bar => ov_anything ]
  def foo(ops = {})
    ov.validate(ops, 'ops')
  end

ov_collection

  • Declaration
  ov_collection[ class_decl, item_decl ]
  • Validation - permits only collections of type class_decl with items matching item_decl declaration
  • Allowed values for class_decl are:
    • Hash or Array or any subclass of Hash or Array,
    • ov_subclass_of[klass] where klass is Hash or Array or a subclass of any of them.
  • Allowed values for item_decl:
    • if class_decl is Array-like, then any value is allowed as item_decl,
    • if class_decl is Hash-like, then item_decl should be a one-element Hash in form { key_decl => val_decl }.
  • Example
  ov = ov_options[
    :bar => ov_collection[ Hash, { instance_of[Symbol] => anything } ],
    :geez => ov_collection [ Array, instance_of[String] ]
  ]
  def foo(ops = {})
    ov.validate(ops, 'ops')
  end

ov_instance_of

  • Declaration
  ov_instance_of[klass]
  • Validation - permits only instances of a given class klass
  • Allowed values for klass - only class names, for example String, Hash, etc.
  • Example
  ov = ov_options[ :bar => ov_instance_of[String] ]
  def foo(ops = {})
    ov.validate(ops,'ops')
  end

ov_kind_of

  • Declaration
  ov_kind_of[klass]
  • Validation - permits only values that are a kind of given class klass
  • Allowed values for klass - only class names, for example String, Hash, etc.
  • Example
  ov = ov_options[ :bar => ov_kind_of[Numeric] ]
  def foo(ops = {})
    ov.validate(ops,'ops')
  end

ov_one_of

  • Declaration
  ov_one_of[decl1,decl2,...]
  • Validation - permits only values matching one of declarations decl, decl2, ...
  • Example
  ov = ov_options[ 
    :bar => ov_one_of[ ov_instance_of[String], ov_kind_of[Numeric], nil ]
  ]
  def foo(ops = {})
    ov.validate(ops,'ops')
  end

ov_options

  • Declaration
  ov_options[ optkey_decl1 => optval_decl1, ... ]
  • Validation - permits only declared options and their values.
  • Allowed values for optkey_declN - anything that is convertible to string (namely, anything that responds to to_s method).
  • Example:
  ov = ov_options[ 
    :bar => ov_anything,
    :geez => ov_instance_of[String],
    # ...
  ]
  def foo(ops = {})
    ov.validate(ops,'ops')
  end 

ov_subclass_of

  • Declaration
  ov_subclass_of[klass]
  • Validation - permits only subclasses of klass
  • Allowed values for klass - only class names, for example String, Hash, etc.
  • Example
  ov = ov_options[ :bar => ov_subclass_of[Numeric] ]
  def foo(ops = {})
    ov.validate(ops,'ops')
  end

API Reference

API reference may be generated with

bundle exec rake yard

The generated documentation goes to doc/ directory. Note that this works only under ruby >= 1.9.

The API documentation is also available online.

[Table of Contents]

Limitations

  • API documentation is currently very poor,

[Table of Contents]