masterplan 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +30 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +181 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/lib/masterplan.rb +94 -0
- data/lib/masterplan/define_rules.rb +26 -0
- data/lib/masterplan/document.rb +37 -0
- data/lib/masterplan/rule.rb +99 -0
- data/lib/unit_test_extensions.rb +9 -0
- data/masterplan.gemspec +72 -0
- data/spec/masterplan_spec.rb +66 -0
- data/spec/spec_helper.rb +12 -0
- metadata +161 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (2.3.10)
|
5
|
+
diff-lcs (1.1.2)
|
6
|
+
git (1.2.5)
|
7
|
+
jeweler (1.5.2)
|
8
|
+
bundler (~> 1.0.0)
|
9
|
+
git (>= 1.2.5)
|
10
|
+
rake
|
11
|
+
rake (0.8.7)
|
12
|
+
rcov (0.9.9)
|
13
|
+
rspec (2.3.0)
|
14
|
+
rspec-core (~> 2.3.0)
|
15
|
+
rspec-expectations (~> 2.3.0)
|
16
|
+
rspec-mocks (~> 2.3.0)
|
17
|
+
rspec-core (2.3.1)
|
18
|
+
rspec-expectations (2.3.0)
|
19
|
+
diff-lcs (~> 1.1.2)
|
20
|
+
rspec-mocks (2.3.0)
|
21
|
+
|
22
|
+
PLATFORMS
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
activesupport (~> 2.3.5)
|
27
|
+
bundler (~> 1.0.0)
|
28
|
+
jeweler (~> 1.5.2)
|
29
|
+
rcov
|
30
|
+
rspec (~> 2.3.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Martin Tepper
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
= Masterplan
|
2
|
+
|
3
|
+
Masterplan is a library for comparing Ruby data structures against predefined templates.
|
4
|
+
|
5
|
+
At Travel IQ, this is used to define a canonical definition of the structure and format of the requests and responses of our APIs.
|
6
|
+
In short, you get something that is similar to a XML Scheme - a template against which your data can be compared, to
|
7
|
+
ensure its correctness programmatically. Only without the XML part.
|
8
|
+
|
9
|
+
Examples make this easier to explain. Say you have a webservice or class that produces
|
10
|
+
a Ruby data structure like this:
|
11
|
+
|
12
|
+
{
|
13
|
+
:airports => [
|
14
|
+
{
|
15
|
+
:name => "Tegel Airport",
|
16
|
+
:code => "TXL",
|
17
|
+
:latitude => 2.35454,
|
18
|
+
:longitude => 54.67867
|
19
|
+
},
|
20
|
+
{
|
21
|
+
:name => "Schönefeld Airport",
|
22
|
+
:code => "SXF",
|
23
|
+
:latitude => 5.35454,
|
24
|
+
:longitude => 34.67867
|
25
|
+
}
|
26
|
+
]
|
27
|
+
}
|
28
|
+
|
29
|
+
and so on and so forth. In your tests of this service, you want to make sure that this
|
30
|
+
structure follows certain rules:
|
31
|
+
|
32
|
+
* It's a Hash with one key, :airports
|
33
|
+
* The value is an Array, and it can be empty, but not null
|
34
|
+
* Each entry is a hash
|
35
|
+
* Each hash has the keys :name, :code, :latitude, :longitude
|
36
|
+
* No value of these keys is ever null
|
37
|
+
* Name and code are strings
|
38
|
+
* The strings aren't empty
|
39
|
+
* The code is three characters long and consists of uppercase letters
|
40
|
+
* Latitude and Longitude are floats
|
41
|
+
|
42
|
+
In a more XML-centric world, you would define an XML (or Relax-NG etc.) Scheme that defines all
|
43
|
+
these rules and then use that to validate your output. But we also want to deliver JSON...so we'll define
|
44
|
+
the rules as a Masterplan::Document, and validate the data while it's still Ruby, and say that as long
|
45
|
+
as the source data is correct, the representation in JSON or XML will also be correct:
|
46
|
+
|
47
|
+
include Masterplan::DefineRules
|
48
|
+
|
49
|
+
doc = Masterplan::Document.new(
|
50
|
+
:airports => [
|
51
|
+
{
|
52
|
+
:name => "Tegel Airport",
|
53
|
+
:code => rule("TXL", :matches => /[A-Z]{3}/),
|
54
|
+
:latitude => 2.35454,
|
55
|
+
:longitude => 54.67867
|
56
|
+
},
|
57
|
+
{
|
58
|
+
:name => "Schönefeld Airport",
|
59
|
+
:code => "SXF",
|
60
|
+
:latitude => 5.35454,
|
61
|
+
:longitude => 34.67867
|
62
|
+
}
|
63
|
+
]
|
64
|
+
)
|
65
|
+
|
66
|
+
It doesn't look much different from the example, but there are a lot of rules built-in.
|
67
|
+
You can now use the doc object to check your data against the template:
|
68
|
+
|
69
|
+
Masterplan.compare(:scheme => doc, :to => [{:example => :data}])
|
70
|
+
|
71
|
+
And it will throw a Masterplan::FailedError exception, and print out debugging data:
|
72
|
+
|
73
|
+
>> Masterplan.compare(:scheme => doc, :to => {:example => :data})
|
74
|
+
Masterplan::FailedError: keys don't match in 'root':
|
75
|
+
expected: airports
|
76
|
+
received: example
|
77
|
+
|
78
|
+
Expected:
|
79
|
+
{"airports"=>
|
80
|
+
[{:latitude=>2.35454,
|
81
|
+
:longitude=>54.67867,
|
82
|
+
:code=>
|
83
|
+
#<Masterplan::Rule:0x6d0bd70
|
84
|
+
@example_value="TXL",
|
85
|
+
@options=
|
86
|
+
{"compare_each"=>false,
|
87
|
+
"allow_nil"=>false,
|
88
|
+
"included_in"=>false,
|
89
|
+
"matches"=>/[A-Z]{3}/}>,
|
90
|
+
:name=>"Tegel Airport"},
|
91
|
+
{:latitude=>5.35454,
|
92
|
+
:longitude=>34.67867,
|
93
|
+
:code=>"SXF",
|
94
|
+
:name=>"Schönefeld Airport"}]}
|
95
|
+
|
96
|
+
|
97
|
+
but was:
|
98
|
+
{"example"=>:data}
|
99
|
+
|
100
|
+
Another example:
|
101
|
+
|
102
|
+
>> Masterplan.compare(:scheme => doc, :to => {:airports => [{:name => "Bla", :latitude => 1.1, :longitude => 2.3, :code => "XXx"}]})
|
103
|
+
Masterplan::FailedError: value at 'root'=>'airports'=>'0'=>'code' "XXx" (String) does not match /[A-Z]{3}/ !
|
104
|
+
|
105
|
+
Expected:
|
106
|
+
{"name"=>"Tegel Airport",
|
107
|
+
"latitude"=>2.35454,
|
108
|
+
"code"=>
|
109
|
+
#<Masterplan::Rule:0x6d0bd70
|
110
|
+
@example_value="TXL",
|
111
|
+
@options=
|
112
|
+
{"compare_each"=>false,
|
113
|
+
"allow_nil"=>false,
|
114
|
+
"included_in"=>false,
|
115
|
+
"matches"=>/[A-Z]{3}/}>,
|
116
|
+
"longitude"=>54.67867}
|
117
|
+
|
118
|
+
|
119
|
+
but was:
|
120
|
+
{"name"=>"Bla", "latitude"=>1.1, "code"=>"XXx", "longitude"=>2.3}
|
121
|
+
|
122
|
+
The implicit rules are:
|
123
|
+
|
124
|
+
* Each object in the data needs to be of the same class as in the template
|
125
|
+
* hash keys must match up
|
126
|
+
* The first element of an Array in the template is used as the template for all elements in the data. That's why we didn't
|
127
|
+
have to restate the custom rule about the code in the above example, as only the "Tegel Airport" hash is used for all checks.
|
128
|
+
|
129
|
+
You can add extra rules with the rule method - see Masterplan::DefineRules#rule for details.
|
130
|
+
|
131
|
+
There is also an added assertion for unit tests or specs:
|
132
|
+
|
133
|
+
assert_masterplan(doc, [{:example => :data})
|
134
|
+
|
135
|
+
== Use schemes as examples in documentation
|
136
|
+
|
137
|
+
A problem with webservices is that you need to keep the documentation up to date - something
|
138
|
+
that is easily forgotten. If you have a masterplan document, you can use it not only as the template,
|
139
|
+
but also as an example in, say, online documentation:
|
140
|
+
|
141
|
+
<pre>
|
142
|
+
<%= JSON.dump(doc.to_hash) %>
|
143
|
+
</pre>
|
144
|
+
|
145
|
+
The to_hash method removes the Masterplan::Rule objects for clean output.
|
146
|
+
|
147
|
+
== Caveat
|
148
|
+
|
149
|
+
Note that for the moment, schemes, i.e. the outermost object, can only be hashes.
|
150
|
+
|
151
|
+
== Installation
|
152
|
+
|
153
|
+
Currently, you can only install from Github. Add this to your Gemfile:
|
154
|
+
|
155
|
+
gem 'masterplan', :git => 'git://github.com/traveliq/masterplan.git'
|
156
|
+
|
157
|
+
If you don't use bundler, you're on your own, sorry.
|
158
|
+
|
159
|
+
== Authors
|
160
|
+
|
161
|
+
Martin Tepper (monogreen.de), Holger Pillmann (holger.pillmann@gmail.com), Dr. Florian Odronitz (odo@mac.com)
|
162
|
+
|
163
|
+
== Contact
|
164
|
+
|
165
|
+
For questions, contact the authors or developer@traveliq.net
|
166
|
+
|
167
|
+
== Contributing to masterplan
|
168
|
+
|
169
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
170
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
171
|
+
* Fork the project
|
172
|
+
* Start a feature/bugfix branch
|
173
|
+
* Commit and push until you are happy with your contribution
|
174
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
175
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
176
|
+
|
177
|
+
== Copyright
|
178
|
+
|
179
|
+
Copyright (c) 2011 www.travel-iq.com. See LICENSE.txt for
|
180
|
+
further details.
|
181
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "masterplan"
|
16
|
+
gem.homepage = "http://github.com/traveliq/masterplan"
|
17
|
+
gem.license = "MIT"
|
18
|
+
gem.summary = %Q{Masterplan is a library for comparing Ruby data structures against predefined templates.}
|
19
|
+
gem.description = %Q{Please see the README}
|
20
|
+
gem.email = "developer@traveliq.net"
|
21
|
+
gem.authors = ["Martin Tepper"]
|
22
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
23
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
24
|
+
# gem.add_runtime_dependency 'jabber4r', '> 0.1'
|
25
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rspec/core'
|
30
|
+
require 'rspec/core/rake_task'
|
31
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
32
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
36
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
37
|
+
spec.rcov = true
|
38
|
+
end
|
39
|
+
|
40
|
+
task :default => :spec
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
45
|
+
|
46
|
+
rdoc.rdoc_dir = 'rdoc'
|
47
|
+
rdoc.title = "masterplan #{version}"
|
48
|
+
rdoc.rdoc_files.include('README*')
|
49
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
50
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.0
|
data/lib/masterplan.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
require 'active_support/version'
|
3
|
+
if ActiveSupport::VERSION::STRING >= "3.0.0"
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
end
|
6
|
+
require 'test/unit/assertions'
|
7
|
+
require 'masterplan'
|
8
|
+
require 'masterplan/rule'
|
9
|
+
require 'masterplan/document'
|
10
|
+
require 'masterplan/define_rules'
|
11
|
+
require 'unit_test_extensions'
|
12
|
+
module Masterplan
|
13
|
+
|
14
|
+
class FailedError < Test::Unit::AssertionFailedError
|
15
|
+
attr_accessor :printed
|
16
|
+
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
|
20
|
+
def compare(options = {:scheme => {}, :to => {}})
|
21
|
+
scheme = options[:scheme]
|
22
|
+
testee = options[:to]
|
23
|
+
raise ArgumentError, ":to needs to be a hash !" unless testee.is_a?(Hash)
|
24
|
+
raise ArgumentError, ":scheme needs to be a Masterplan::Document !" unless scheme.is_a?(Document)
|
25
|
+
compare_hash(scheme, testee)
|
26
|
+
true
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def compare_value(template, value, path)
|
32
|
+
if template.is_a?(Rule)
|
33
|
+
template.masterplan_compare(value, path)
|
34
|
+
else
|
35
|
+
Rule.check_class_equality!(template, value, path)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def compare_hash(template, testee, trail = ["root"])
|
40
|
+
template.stringify_keys!
|
41
|
+
testee.stringify_keys!
|
42
|
+
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
|
43
|
+
template.each do |t_key, t_value|
|
44
|
+
current_path = trail + [t_key]
|
45
|
+
value = testee[t_key]
|
46
|
+
compare_value(t_value, value, format_path(current_path))
|
47
|
+
if value && t_value.is_a?(Array)
|
48
|
+
# all array elements need to be of the same type as the first value in the template
|
49
|
+
elements_template = t_value.first
|
50
|
+
value.each_with_index do |elements_value, index|
|
51
|
+
array_path = current_path + [index]
|
52
|
+
compare_value(elements_template, elements_value, format_path(array_path))
|
53
|
+
if elements_value.is_a?(Hash)
|
54
|
+
compare_hash(elements_template, elements_value, array_path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
if value.is_a?(Array) && t_value.is_a?(Rule) && t_value.options['compare_each']
|
59
|
+
value.each_with_index do |elements_value, index|
|
60
|
+
elements_template = t_value.example_value[index]
|
61
|
+
array_path = current_path + [index]
|
62
|
+
compare_value(elements_template, elements_value, format_path(array_path))
|
63
|
+
if elements_value.is_a?(Hash)
|
64
|
+
compare_hash(elements_template, elements_value, array_path)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
if value.is_a?(Hash)
|
69
|
+
if t_value.is_a?(Masterplan::Rule)
|
70
|
+
compare_value(t_value, value, current_path)
|
71
|
+
compare_hash(t_value.example_value, value, current_path)
|
72
|
+
else
|
73
|
+
compare_hash(t_value, value, current_path)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
rescue Masterplan::FailedError => e
|
79
|
+
raise e if e.printed
|
80
|
+
|
81
|
+
error = Masterplan::FailedError.new
|
82
|
+
error.printed = true
|
83
|
+
|
84
|
+
expected = PP.pp(template, '')
|
85
|
+
outcome = PP.pp(testee, '')
|
86
|
+
|
87
|
+
raise error, "#{e.message}\n\nExpected:\n#{expected}\n\nbut was:\n#{outcome}", caller
|
88
|
+
end
|
89
|
+
|
90
|
+
def format_path(trail)
|
91
|
+
"'" + trail.join("'=>'") + "'"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Masterplan
|
2
|
+
|
3
|
+
# Include this module into whatever code generates Masterplan::Documents - you get
|
4
|
+
# methods that make it easier to generate Masterplan::Rule objects.
|
5
|
+
module DefineRules
|
6
|
+
|
7
|
+
# This turns the supplied +example_value+ (any object) into an object that carries rules about itself with it.
|
8
|
+
# The rules will be applied when a template is compared with assert_masterplan. Rules are:
|
9
|
+
# (default): This always applies - the value must be of the same class as the +example_value+
|
10
|
+
# 'allow_nil': This allows the value to be nil (breaking the first rule)
|
11
|
+
# 'included_in': Pass an array of values - the value must be one of these
|
12
|
+
# 'matches': Pass a regexp - the value must match it, and be a String
|
13
|
+
def rule(example_value, options = {})
|
14
|
+
Rule.new(example_value, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
#for iterating over each example in an array intead of using only the first to compare the data array with
|
18
|
+
def iterating_rule(example_value, options = {})
|
19
|
+
if example_value
|
20
|
+
Rule.new(example_value, :compare_each => true)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Masterplan
|
2
|
+
class Document < Hash
|
3
|
+
|
4
|
+
def initialize(hash = {})
|
5
|
+
raise ArgumentError, "Can only work with a Hash, not a #{hash.class.name} !" unless hash.is_a?(Hash)
|
6
|
+
hash.each do |k, v|
|
7
|
+
self[k] = v
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Turns a Masterplan::Document into a plain Hash - this removes all special
|
12
|
+
# objects like Masterplan::Rule and replaces them with their example values, so
|
13
|
+
# the result can be used as documentation.
|
14
|
+
def to_hash
|
15
|
+
result = {}
|
16
|
+
each do |k, v|
|
17
|
+
result[k] = self.class.derulerize(v)
|
18
|
+
end
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def self.derulerize(object)
|
25
|
+
case object
|
26
|
+
when Hash
|
27
|
+
new(object).to_hash
|
28
|
+
when Array
|
29
|
+
object.map { |e| derulerize(e) }
|
30
|
+
when Masterplan::Rule
|
31
|
+
derulerize(object.example_value)
|
32
|
+
else
|
33
|
+
object
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Masterplan
|
2
|
+
class Rule
|
3
|
+
|
4
|
+
OPTIONS = ["allow_nil", "compare_each", "included_in", "matches"]
|
5
|
+
|
6
|
+
attr_accessor :options, :example_value
|
7
|
+
|
8
|
+
def initialize(example, options = {})
|
9
|
+
options.stringify_keys!
|
10
|
+
options['allow_nil'] ||= false
|
11
|
+
options['compare_each'] ||= false
|
12
|
+
options["included_in"] ||= false
|
13
|
+
options["matches"] ||= false
|
14
|
+
raise ArgumentError, "options can be #{OPTIONS.join(',')}, not #{options.keys.inspect}" unless options.keys.sort == OPTIONS.sort
|
15
|
+
self.example_value = example
|
16
|
+
self.options = options
|
17
|
+
self.masterplan_compare(example, "initialization of rule")
|
18
|
+
end
|
19
|
+
|
20
|
+
def masterplan_compare(value, path)
|
21
|
+
# puts "#{path} #{@masterplan_rule_options.inspect}"
|
22
|
+
# puts self.inspect
|
23
|
+
# puts value.inspect
|
24
|
+
# puts @masterplan_rule_options["included_in"].inspect
|
25
|
+
# puts !@masterplan_rule_options["included_in"].include?(value) if @masterplan_rule_options["included_in"]
|
26
|
+
return true if masterplan_check_allowed_nil!(value, path)
|
27
|
+
return true if masterplan_check_included_in!(value, path)
|
28
|
+
return true if masterplan_check_matches!(value, path)
|
29
|
+
return true if masterplan_check_class_equality!(value, path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.check_class_equality!(template, value, path)
|
33
|
+
|
34
|
+
value_klass = case value
|
35
|
+
when Document
|
36
|
+
Hash
|
37
|
+
when Rule
|
38
|
+
value.example_value.class
|
39
|
+
else
|
40
|
+
value.class
|
41
|
+
end
|
42
|
+
template_klass = case template
|
43
|
+
when Document
|
44
|
+
Hash
|
45
|
+
when Rule
|
46
|
+
template.example_value.class
|
47
|
+
else
|
48
|
+
template.class
|
49
|
+
end
|
50
|
+
if template_klass != value_klass
|
51
|
+
raise FailedError, "value at #{path} (#{value_klass}) is not a #{template_klass} !"
|
52
|
+
else
|
53
|
+
true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def masterplan_check_class_equality!(value, path)
|
60
|
+
self.class.check_class_equality!(self, value, path)
|
61
|
+
end
|
62
|
+
|
63
|
+
def masterplan_check_allowed_nil!(value, path)
|
64
|
+
if options['allow_nil']
|
65
|
+
if value.nil?
|
66
|
+
true
|
67
|
+
else
|
68
|
+
false
|
69
|
+
end
|
70
|
+
else
|
71
|
+
false
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def masterplan_check_included_in!(value, path)
|
76
|
+
if options["included_in"]
|
77
|
+
unless options["included_in"].include?(value)
|
78
|
+
raise Masterplan::FailedError, "value at #{path} #{value.inspect} (#{value.class}) is not one of #{options["included_in"].inspect} !"
|
79
|
+
else
|
80
|
+
true
|
81
|
+
end
|
82
|
+
else
|
83
|
+
false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def masterplan_check_matches!(value, path)
|
88
|
+
if options["matches"]
|
89
|
+
if value !~ options["matches"]
|
90
|
+
raise Masterplan::FailedError, "value at #{path} #{value.inspect} (#{value.class}) does not match #{options["matches"].inspect} !"
|
91
|
+
else
|
92
|
+
true
|
93
|
+
end
|
94
|
+
else
|
95
|
+
false
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/masterplan.gemspec
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{masterplan}
|
8
|
+
s.version = "0.3.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Martin Tepper"]
|
12
|
+
s.date = %q{2011-02-02}
|
13
|
+
s.description = %q{Please see the README}
|
14
|
+
s.email = %q{developer@traveliq.net}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".rspec",
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE.txt",
|
25
|
+
"README.rdoc",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"lib/masterplan.rb",
|
29
|
+
"lib/masterplan/define_rules.rb",
|
30
|
+
"lib/masterplan/document.rb",
|
31
|
+
"lib/masterplan/rule.rb",
|
32
|
+
"lib/unit_test_extensions.rb",
|
33
|
+
"masterplan.gemspec",
|
34
|
+
"spec/masterplan_spec.rb",
|
35
|
+
"spec/spec_helper.rb"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://github.com/traveliq/masterplan}
|
38
|
+
s.licenses = ["MIT"]
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
s.rubygems_version = %q{1.3.7}
|
41
|
+
s.summary = %q{Masterplan is a library for comparing Ruby data structures against predefined templates.}
|
42
|
+
s.test_files = [
|
43
|
+
"spec/masterplan_spec.rb",
|
44
|
+
"spec/spec_helper.rb"
|
45
|
+
]
|
46
|
+
|
47
|
+
if s.respond_to? :specification_version then
|
48
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
49
|
+
s.specification_version = 3
|
50
|
+
|
51
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
52
|
+
s.add_runtime_dependency(%q<activesupport>, ["~> 2.3.5"])
|
53
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
|
54
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
55
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
|
56
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<activesupport>, ["~> 2.3.5"])
|
59
|
+
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
60
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
61
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
62
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
63
|
+
end
|
64
|
+
else
|
65
|
+
s.add_dependency(%q<activesupport>, ["~> 2.3.5"])
|
66
|
+
s.add_dependency(%q<rspec>, ["~> 2.3.0"])
|
67
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
68
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
|
69
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
include Masterplan::DefineRules
|
4
|
+
|
5
|
+
describe "Masterplan" do
|
6
|
+
before(:each) do
|
7
|
+
@scheme = Masterplan::Document.new({
|
8
|
+
"ship" => {
|
9
|
+
"parts" => [
|
10
|
+
{
|
11
|
+
"name" => "Mast",
|
12
|
+
"length" => rule(12.3, :allow_nil => true),
|
13
|
+
"material" => rule("wood", :included_in => ['wood', 'steel', 'human'])
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"name" => "Rudder",
|
17
|
+
"length" => nil,
|
18
|
+
"material" => "steel"
|
19
|
+
}
|
20
|
+
]
|
21
|
+
}
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "Testing with #compare" do
|
26
|
+
|
27
|
+
it "returns true for a valid document, treating symbols and strings alike" do
|
28
|
+
Masterplan.compare(
|
29
|
+
:scheme => @scheme,
|
30
|
+
:to => {
|
31
|
+
:ship => {
|
32
|
+
:parts => [
|
33
|
+
:name => "Thingy",
|
34
|
+
:length => 1.0,
|
35
|
+
:material => "human"
|
36
|
+
]
|
37
|
+
}
|
38
|
+
}
|
39
|
+
).should be_true
|
40
|
+
end
|
41
|
+
it "complains if a key is missing" do
|
42
|
+
lambda do
|
43
|
+
Masterplan.compare(
|
44
|
+
:scheme => @scheme,
|
45
|
+
:to => {
|
46
|
+
:tank => {}
|
47
|
+
}
|
48
|
+
)
|
49
|
+
end.should raise_error(Masterplan::FailedError, /expected: ship*\n*received: tank/)
|
50
|
+
end
|
51
|
+
it "complains if not given a Masterplan::Document"
|
52
|
+
it "complains if there are extra keys"
|
53
|
+
it "complains if a value is of the wrong class"
|
54
|
+
it "complains if a value is nil"
|
55
|
+
it "does not complain if a value is nil and the rule allows nil"
|
56
|
+
it "complains if a value does not match the regexp rule"
|
57
|
+
it "complains if a value is not included in the rule list"
|
58
|
+
it "checks all values of value arrays, but only against the first array value of the scheme"
|
59
|
+
it "checks all array values one-to-one if the compare_each rule is used"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "convertsinto plain example hashes"
|
63
|
+
it "doesn't create a Document out of anything other than a Hash"
|
64
|
+
it "checks that the examples of rules obey the rules"
|
65
|
+
it "has a unit test extension method"
|
66
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'masterplan'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: masterplan
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 19
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Martin Tepper
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-02-02 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
hash: 9
|
28
|
+
segments:
|
29
|
+
- 2
|
30
|
+
- 3
|
31
|
+
- 5
|
32
|
+
version: 2.3.5
|
33
|
+
requirement: *id001
|
34
|
+
prerelease: false
|
35
|
+
type: :runtime
|
36
|
+
name: activesupport
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 2
|
46
|
+
- 3
|
47
|
+
- 0
|
48
|
+
version: 2.3.0
|
49
|
+
requirement: *id002
|
50
|
+
prerelease: false
|
51
|
+
type: :development
|
52
|
+
name: rspec
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ~>
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 23
|
60
|
+
segments:
|
61
|
+
- 1
|
62
|
+
- 0
|
63
|
+
- 0
|
64
|
+
version: 1.0.0
|
65
|
+
requirement: *id003
|
66
|
+
prerelease: false
|
67
|
+
type: :development
|
68
|
+
name: bundler
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 7
|
76
|
+
segments:
|
77
|
+
- 1
|
78
|
+
- 5
|
79
|
+
- 2
|
80
|
+
version: 1.5.2
|
81
|
+
requirement: *id004
|
82
|
+
prerelease: false
|
83
|
+
type: :development
|
84
|
+
name: jeweler
|
85
|
+
- !ruby/object:Gem::Dependency
|
86
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ">="
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
hash: 3
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
version: "0"
|
95
|
+
requirement: *id005
|
96
|
+
prerelease: false
|
97
|
+
type: :development
|
98
|
+
name: rcov
|
99
|
+
description: Please see the README
|
100
|
+
email: developer@traveliq.net
|
101
|
+
executables: []
|
102
|
+
|
103
|
+
extensions: []
|
104
|
+
|
105
|
+
extra_rdoc_files:
|
106
|
+
- LICENSE.txt
|
107
|
+
- README.rdoc
|
108
|
+
files:
|
109
|
+
- .document
|
110
|
+
- .rspec
|
111
|
+
- Gemfile
|
112
|
+
- Gemfile.lock
|
113
|
+
- LICENSE.txt
|
114
|
+
- README.rdoc
|
115
|
+
- Rakefile
|
116
|
+
- VERSION
|
117
|
+
- lib/masterplan.rb
|
118
|
+
- lib/masterplan/define_rules.rb
|
119
|
+
- lib/masterplan/document.rb
|
120
|
+
- lib/masterplan/rule.rb
|
121
|
+
- lib/unit_test_extensions.rb
|
122
|
+
- masterplan.gemspec
|
123
|
+
- spec/masterplan_spec.rb
|
124
|
+
- spec/spec_helper.rb
|
125
|
+
has_rdoc: true
|
126
|
+
homepage: http://github.com/traveliq/masterplan
|
127
|
+
licenses:
|
128
|
+
- MIT
|
129
|
+
post_install_message:
|
130
|
+
rdoc_options: []
|
131
|
+
|
132
|
+
require_paths:
|
133
|
+
- lib
|
134
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
135
|
+
none: false
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
hash: 3
|
140
|
+
segments:
|
141
|
+
- 0
|
142
|
+
version: "0"
|
143
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
144
|
+
none: false
|
145
|
+
requirements:
|
146
|
+
- - ">="
|
147
|
+
- !ruby/object:Gem::Version
|
148
|
+
hash: 3
|
149
|
+
segments:
|
150
|
+
- 0
|
151
|
+
version: "0"
|
152
|
+
requirements: []
|
153
|
+
|
154
|
+
rubyforge_project:
|
155
|
+
rubygems_version: 1.3.7
|
156
|
+
signing_key:
|
157
|
+
specification_version: 3
|
158
|
+
summary: Masterplan is a library for comparing Ruby data structures against predefined templates.
|
159
|
+
test_files:
|
160
|
+
- spec/masterplan_spec.rb
|
161
|
+
- spec/spec_helper.rb
|