Created
January 27, 2011 21:19
-
-
Save raphael/799298 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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