Skip to content

Commit

Permalink
Add ability to declare hash keys in templates as optional.
Browse files Browse the repository at this point in the history
  • Loading branch information
MGPalmer committed Mar 30, 2012
1 parent bf15a70 commit 93fb41e
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 10 deletions.
39 changes: 34 additions & 5 deletions lib/masterplan.rb
Expand Up @@ -38,14 +38,43 @@ def compare_value(template, value, path)
end
end

def compare_hash(template, testee, format, trail = ["root"])
template.stringify_keys!
def compare_hash_keys(template, testee, trail)
mandatory_keys = []
optional_keys = []
template.keys.each do |key|
if key.is_a?(Masterplan::Rule) && key.options["optional"]
optional_keys << key.example_value.to_s
else
mandatory_keys << key.to_s
end
end
failed = false
testee.stringify_keys!
raise FailedError, "keys don't match in #{format_path(trail)}:\nexpected:\t#{template.keys.sort.join(',')}\nreceived:\t#{testee.keys.sort.join(',')}" if template.keys.sort != testee.keys.sort
template.each do |t_key, t_value|
if((mandatory_keys - testee.keys).size > 0) # missing keys
failed = true
else
extra_keys = (testee.keys - mandatory_keys)
if extra_keys.size > 0 && extra_keys.sort != optional_keys.sort
failed = true
end
end
if failed
raise FailedError, "keys don't match in #{format_path(trail)}:\nexpected:\t#{mandatory_keys.sort.join(',')}\nreceived:\t#{testee.keys.sort.join(',')}"
end
end

def compare_hash(template, testee, format, trail = ["root"])
compare_hash_keys(template, testee, trail)
template.each do |t_key_or_rule, t_value|
key_is_optional = t_key_or_rule.is_a?(Masterplan::Rule) && t_key_or_rule.options["optional"]
t_key = if key_is_optional
t_key_or_rule.example_value
else
t_key_or_rule
end
current_path = trail + [t_key]
value = testee[t_key]
compare_value(t_value, value, format_path(current_path))
compare_value(t_value, value, format_path(current_path)) unless key_is_optional and value.nil?
if value && t_value.is_a?(Array)
# all array elements need to be of the same type as the first value in the template
elements_template = t_value.first
Expand Down
3 changes: 3 additions & 0 deletions lib/masterplan/define_rules.rb
Expand Up @@ -10,6 +10,9 @@ module DefineRules
# 'allow_nil': This allows the value to be nil (breaking the first rule)
# 'included_in': Pass an array of values - the value must be one of these
# 'matches': Pass a regexp - the value must match it, and be a String
#
# There is one special rule that only works on hash keys:
# 'optional' : This makes the hash key optional, i.e. no error will occur if the key (and its value) are missing.
def rule(example_value, options = {})
Rule.new(example_value, options)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/masterplan/document.rb
Expand Up @@ -14,7 +14,7 @@ def initialize(hash = {})
def to_hash
result = {}
each do |k, v|
result[k] = self.class.derulerize(v)
result[self.class.derulerize(k)] = self.class.derulerize(v)
end
result
end
Expand Down
3 changes: 2 additions & 1 deletion lib/masterplan/rule.rb
@@ -1,7 +1,7 @@
module Masterplan
class Rule

OPTIONS = ["allow_nil", "compare_each", "included_in", "matches"]
OPTIONS = ["allow_nil", "compare_each", "included_in", "matches", "optional"]

attr_accessor :options, :example_value

Expand All @@ -11,6 +11,7 @@ def initialize(example, options = {})
options['compare_each'] ||= false
options["included_in"] ||= false
options["matches"] ||= false
options["optional"] ||= false
raise ArgumentError, "options can be #{OPTIONS.join(',')}, not #{options.keys.inspect}" unless options.keys.sort == OPTIONS.sort
self.example_value = example
self.options = options
Expand Down
53 changes: 50 additions & 3 deletions spec/masterplan_spec.rb
Expand Up @@ -19,7 +19,11 @@
"material" => "steel",
"scream" => "HAAAAAARGH"
}
]
],
rule("flags", :optional => true) => {
"image" => "jolly roger",
"count" => 1
}
}
})
end
Expand Down Expand Up @@ -77,7 +81,7 @@ def test_value_and_expect(testee, *error_and_descripton)
end.should raise_error(ArgumentError, ":format needs to be one of [:full, :mini] !")
end

it "complains if there are extra keys" do
it "complains if there are extra keys (unless they are optional)" do
test_value_and_expect(
{ :ship => {}, :boat => {} },
Masterplan::FailedError, /expected: ship*\n*received: boat,ship/
Expand All @@ -98,6 +102,45 @@ def test_value_and_expect(testee, *error_and_descripton)
)
end

it "complains if a value is nil when in an optional but given value" do
test_value_and_expect(
{
:ship => {
:parts => [
:name => "Thingy",
:length => 1.0,
:material => "human",
:scream => "UUUUUUUUH"
],
:flags => {
"image" => nil,
"count" => 1
}
}
},
Masterplan::FailedError, /value at 'root'=>'ship'=>'flags'=>'image' \(NilClass\) is not a String/
)
end

it "complains if keys don't match up when in an optional but given value" do
test_value_and_expect(
{
:ship => {
:parts => [
:name => "Thingy",
:length => 1.0,
:material => "human",
:scream => "UUUUUUUUH"
],
:flags => {
"count" => 1
}
}
},
Masterplan::FailedError, /expected: count,image*\n*received: count/
)
end

it "does not complain if a value is nil and the rule allows nil" do
Masterplan.compare(
:scheme => @scheme,
Expand Down Expand Up @@ -165,7 +208,11 @@ def test_value_and_expect(testee, *error_and_descripton)
"length" => nil,
"material" => "steel"
}
]
],
"flags" => {
"image" => "jolly roger",
"count" => 1
}
}
}
end
Expand Down

0 comments on commit 93fb41e

Please sign in to comment.