Skip to content

Instantly share code, notes, and snippets.

@raphael
Created January 27, 2011 21:19
Show Gist options
  • Save raphael/799298 to your computer and use it in GitHub Desktop.
Save raphael/799298 to your computer and use it in GitHub Desktop.
require 'rubygems'
require 'ruote'
require 'flexmock'
require 'spec'
# Workitem fields validation
# This participant validates the fields of the current workitem
# Especially useful to validate the initial workitem
# An input definition may include the following:
# - name: Input name, compulsory
# - kind: Input type, compulsory
# - obligation: Whether input is required or optional, required by default.
# - default: Input default value, optional.
#
# The kind of an input consists of a string corresponding to the Ruby class
# name, one of:
# - String
# - Fixnum
# - Bool (note: not a ruby class per se)
# - DateTime
# - Array<KIND> (KIND must recursively take on the values in this list)
# - Hash<KIND, KIND) (same note as above)
#
# The participant expects the 'fields_definitions' field to contain the
# definitions as hashes indexed by field name
#
# e.g.:
# fields_definitions 'names' => [ 'Array<String>', :required ],
# 'operation' => [ 'String', :default => 'run' ],
# 'operation_timeout' => [ 'Fixnum', :default => 300 ],
# 'arguments' => [ 'Hash<String, String>', :default => {} ],
# 'result_field' => [ 'String', :default => '__result__' ]
# error '${f:fields_errors}', :if => '${f:fields_errors}'
#
class FieldsValidationParticipant
include Ruote::LocalParticipant
# Lookup fields validations and iterate through to validate
#
# === Input Workitem Fields
# fields_definitions<Hash>:: Hash of fields definition indexed by name
#
# === Ouput Workitem Fields
# fields_errors<Array>:: List of error messages, empty if no error
def consume(workitem)
defs = workitem.fields['fields_definitions'] || {}
errors = []
# First validate
defs.each { |name, val| errors += validate(name, val, workitem.lookup(name)) }
workitem.set_field('fields_errors', errors)
# Then set default values
defaults = defs.select { |_, val| val.is_a?(Hash) && val.include?(:default) }
defaults.each { |name, val| workitem.set_field(name, val[:default]) unless workitem.lookup(name) }
reply_to_engine(workitem)
end
protected
# Validate a given workitem field against its input definition
#
# === Argumnets
# name<String>:: Name of field
# definition<Array>:: Input definition
# field<Object>:: Actual field value
#
# === Return
# errors<Array>:: List of error messages or empty array if no error
def validate(name, definition, field)
errors = []
if field.nil?
if definition[1] == :required
errors << "Missing required workitem field #{name.inspect}"
end
else
errors += check_kind(field, definition[0], name)
end
errors
end
# Recursively check that the kind of given field matches given kind
#
# === Arguments
# field<Object>:: Actual workitem field being checked
# kind<String>:: Kind 'field' should match
# name<String>:: Name of field for error message
#
# === Return
# errors<Array>:: List of errors or empty array if no error
def check_kind(field, kind, name)
errors = []
if kind =~ /^Array<(.*)>$/
inner = Regexp.last_match[1]
errors += check_kind(field, 'Array', name)
if errors.empty?
field.each_index do |i|
errors += check_kind(field[i], inner, "element at index #{i} of #{name}")
end
end
elsif kind =~ /^Hash<(.*), *(.*)>$/
inner_key = Regexp.last_match[1]
inner_val = Regexp.last_match[2]
errors += check_kind(field, 'Hash', name)
if errors.empty?
field.each do |k ,v|
errors += check_kind(k, inner_key, "key #{k.inspect} of #{name}")
errors += check_kind(v, inner_val, "value at key #{k.inspect} of #{name}")
end
end
else
if field.class.to_s != kind
errors << "Workitem field #{name.inspect} kind is invalid (should be " +
"#{kind.inspect} but is #{field.class.to_s.inspect})"
end
end
errors
end
end
config = Spec::Runner.configuration
config.mock_with :flexmock
describe Maestro::FieldsValidationParticipant do
# Create test workitem
def new_workitem
workitem = Ruote::Workitem.new('fields' =>
{ 'fields_definitions' => { 'required_field' => [ 'String', :required ],
'optional_field' => [ 'String', :optional ],
'array_field' => [ 'Array<String>', :required ],
'hash_field' => [ 'Hash<String, Fixnum>', :required ] },
'required_field' => 'I\'m here',
'array_field' => [ 'I\'m a string' ],
'hash_field' => { 'key' => 42 }})
end
before(:each) do
@workitem = nil
@validator = Maestro::FieldsValidationParticipant.new
flexmock(@validator).should_receive(:reply_to_engine).and_return { |wi| @workitem = wi }
end
it 'should work with no definition' do
workitem = Ruote::Workitem.new('fields' => {'a' => 'b'})
@validator.consume(workitem)
@workitem.should_not be_nil
@workitem.to_h.should == workitem.to_h
end
it 'should validate required inputs' do
workitem = new_workitem
@validator.consume(workitem)
@workitem.should_not be_nil
@workitem.to_h.should == workitem.to_h
end
it 'should invalidate missing inputs' do
workitem = new_workitem
workitem.set_field('required_field', nil)
@validator.consume(workitem)
@workitem.should_not be_nil
@workitem.lookup('fields_errors').should_not be_nil
@workitem.lookup('fields_errors').class.should == Array
@workitem.lookup('fields_errors').size.should == 1
@workitem.lookup('fields_errors')[0].should =~ /^Missing required workitem field/
end
it 'should invalidate incorrect kinds' do
workitem = new_workitem
workitem.set_field('optional_field', 42)
@validator.consume(workitem)
@workitem.should_not be_nil
@workitem.lookup('fields_errors').should_not be_nil
@workitem.lookup('fields_errors').class.should == Array
@workitem.lookup('fields_errors').size.should == 1
@workitem.lookup('fields_errors')[0].should =~ /kind is invalid/
end
it 'should invalidate multiple errors' do
workitem = new_workitem
workitem.set_field('optional_field', 42)
workitem.set_field('required_field', 42)
@validator.consume(workitem)
@workitem.should_not be_nil
@workitem.lookup('fields_errors').should_not be_nil
@workitem.lookup('fields_errors').class.should == Array
@workitem.lookup('fields_errors').size.should == 2
@workitem.lookup('fields_errors').all? { |e| e.should =~ /kind is invalid/ }
end
it 'should invalidate incorrect array kind' do
workitem = new_workitem
workitem.set_field('array_field', [ 42 ])
@validator.consume(workitem)
@workitem.should_not be_nil
@workitem.lookup('fields_errors').should_not be_nil
@workitem.lookup('fields_errors').class.should == Array
@workitem.lookup('fields_errors').size.should == 1
@workitem.lookup('fields_errors')[0].should =~ /kind is invalid/
end
it 'should invalidate incorrect hash key kind' do
workitem = new_workitem
workitem.set_field('hash_field', [ 42 => 42 ])
@validator.consume(workitem)
@workitem.should_not be_nil
@workitem.lookup('fields_errors').should_not be_nil
@workitem.lookup('fields_errors').class.should == Array
@workitem.lookup('fields_errors').size.should == 1
@workitem.lookup('fields_errors')[0].should =~ /kind is invalid/
end
it 'should invalidate incorrect hash value kind' do
workitem = new_workitem
workitem.set_field('hash_field', [ 'key' => 'not_an_int' ])
@validator.consume(workitem)
@workitem.should_not be_nil
@workitem.lookup('fields_errors').should_not be_nil
@workitem.lookup('fields_errors').class.should == Array
@workitem.lookup('fields_errors').size.should == 1
@workitem.lookup('fields_errors')[0].should =~ /kind is invalid/
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment