doodle 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +29 -10
- data/Manifest.txt +12 -0
- data/README.txt +5 -4
- data/lib/doodle.rb +227 -112
- data/lib/doodle/app.rb +25 -21
- data/lib/doodle/version.rb +2 -2
- data/lib/doodle/xml.rb +175 -0
- data/spec/arg_order_spec.rb +1 -1
- data/spec/block_init_spec.rb +1 -1
- data/spec/bugs_spec.rb +61 -29
- data/spec/collector_spec.rb +92 -25
- data/spec/defaults_spec.rb +13 -0
- data/spec/doodle_context_spec.rb +1 -1
- data/spec/doodle_spec.rb +4 -14
- data/spec/equality_spec.rb +118 -0
- data/spec/modules_spec.rb +67 -0
- data/spec/serialization_spec.rb +3 -3
- data/spec/spec.template +16 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/to_hash_spec.rb +12 -2
- data/spec/validation2_spec.rb +10 -10
- data/spec/validation_spec.rb +1 -1
- data/spec/xml_spec.rb +219 -0
- metadata +17 -2
data/lib/doodle/app.rb
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# -*- mode: ruby; -*-
|
2
|
-
# command line option handling DSL implemented using Doodle
|
3
2
|
# Sean O'Halpin, 2008-09-29
|
4
3
|
|
5
4
|
=begin
|
6
5
|
|
7
|
-
|
6
|
+
TODO:
|
8
7
|
- add file mode to filename (or have separate file type)
|
9
8
|
- use PathName?
|
10
9
|
- sort out lists of things (with types)
|
11
10
|
- apply match ~before~ from
|
11
|
+
- add match to core doodle (as 'pattern' a la XMLSchema?)
|
12
12
|
- handle config files
|
13
13
|
|
14
14
|
=end
|
@@ -25,13 +25,16 @@ require 'pp'
|
|
25
25
|
# confusing :)
|
26
26
|
|
27
27
|
class Doodle
|
28
|
+
# command line option handling DSL implemented using Doodle
|
28
29
|
class App < Doodle
|
29
30
|
# specialised classes for handling attributes
|
31
|
+
|
32
|
+
# replace the full directory path with ./ where appropriate
|
30
33
|
def self.tidy_dir(path)
|
31
|
-
path.to_s.gsub(Regexp.new("^#{ Dir.pwd }/"), './')
|
34
|
+
path.to_s.gsub(Regexp.new("^#{ Regexp.escape(Dir.pwd) }/"), './')
|
32
35
|
end
|
33
36
|
|
34
|
-
# generic option
|
37
|
+
# class representing a generic option
|
35
38
|
class Option < Doodle::DoodleAttribute
|
36
39
|
doodle do
|
37
40
|
string :flag, :max => 1, :doc => "one character abbreviation" do
|
@@ -58,18 +61,20 @@ class Doodle
|
|
58
61
|
end
|
59
62
|
end
|
60
63
|
# specialied Filename attribute
|
64
|
+
# - :existing => true|false (default = false)
|
61
65
|
class Filename < Option
|
62
66
|
doodle do
|
63
67
|
boolean :existing, :default => false, :doc => "set to true if file must exist"
|
64
68
|
end
|
65
69
|
end
|
66
70
|
|
71
|
+
# regular expression for ISO date
|
67
72
|
RX_ISODATE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(\.\d+)? ?Z$/
|
68
|
-
|
73
|
+
|
69
74
|
# App directives
|
70
75
|
class << self
|
71
76
|
public
|
72
|
-
|
77
|
+
|
73
78
|
has :script_name, :default => File.basename($0)
|
74
79
|
has :doc, :default => $0
|
75
80
|
has :usage do
|
@@ -97,7 +102,7 @@ class Doodle
|
|
97
102
|
#p [:option, args, :optional, optional?]
|
98
103
|
key_values, args = args.partition{ |x| x.kind_of?(Hash)}
|
99
104
|
key_values = key_values.inject({ }){ |hash, kv| hash.merge(kv)}
|
100
|
-
|
105
|
+
|
101
106
|
errors = []
|
102
107
|
|
103
108
|
# handle optional/required flipflop
|
@@ -117,7 +122,7 @@ class Doodle
|
|
117
122
|
else
|
118
123
|
key_values.delete(:required)
|
119
124
|
required = { }
|
120
|
-
end
|
125
|
+
end
|
121
126
|
args = [{ :using => Option }.merge(required).merge(key_values), *args]
|
122
127
|
da = has(*args, &block)
|
123
128
|
if errors.size > 0
|
@@ -143,12 +148,12 @@ class Doodle
|
|
143
148
|
end
|
144
149
|
da
|
145
150
|
end
|
146
|
-
# expect string
|
151
|
+
# expect a string
|
147
152
|
def string(*args, &block)
|
148
153
|
args = [{ :using => Option, :kind => String }, *args]
|
149
154
|
da = option(*args, &block)
|
150
155
|
end
|
151
|
-
# expect symbol
|
156
|
+
# expect a symbol (and convert from String)
|
152
157
|
def symbol(*args, &block)
|
153
158
|
args = [{ :using => Option, :kind => Symbol }, *args]
|
154
159
|
da = option(*args, &block)
|
@@ -158,7 +163,7 @@ class Doodle
|
|
158
163
|
end
|
159
164
|
end
|
160
165
|
end
|
161
|
-
# expect filename
|
166
|
+
# expect a filename - set <tt>:existing => true</tt> to specify that the file must exist
|
162
167
|
# filename :input, :existing => true, :flag => "i", :doc => "input file name"
|
163
168
|
def filename(*args, &block)
|
164
169
|
args = [{ :using => Filename, :kind => String }, *args ]
|
@@ -171,9 +176,9 @@ class Doodle
|
|
171
176
|
end
|
172
177
|
end
|
173
178
|
end
|
174
|
-
# expect on/off flag, e.g. -b
|
175
|
-
# doesn't take any arguments (mere presence sets it to true)
|
176
|
-
# booleans are false by default
|
179
|
+
# expect an on/off flag, e.g. -b
|
180
|
+
# - doesn't take any arguments (mere presence sets it to true)
|
181
|
+
# - booleans are false by default
|
177
182
|
def boolean(*args, &block)
|
178
183
|
args = [{ :using => Option, :default => false, :arity => 0}, *args]
|
179
184
|
da = option(*args, &block)
|
@@ -198,7 +203,7 @@ class Doodle
|
|
198
203
|
end
|
199
204
|
end
|
200
205
|
# whole number, e.g. -n 10
|
201
|
-
# you can use, e.g. :values => [1,2,3] or :values => (0..99) to restrict valid values
|
206
|
+
# - you can use, e.g. :values => [1,2,3] or :values => (0..99) to restrict the range of valid values
|
202
207
|
def integer(*args, &block)
|
203
208
|
args = [{ :using => Option, :kind => Integer }, *args]
|
204
209
|
da = option(*args, &block)
|
@@ -230,7 +235,7 @@ class Doodle
|
|
230
235
|
end
|
231
236
|
end
|
232
237
|
# utcdate: -d 2008-09-28T21:41:29Z
|
233
|
-
# actually uses Time (so restricted range)
|
238
|
+
# - actually uses Time (so restricted range)
|
234
239
|
def utcdate(*args, &block)
|
235
240
|
args = [{ :using => Option, :kind => Time }, *args]
|
236
241
|
da = option(*args, &block)
|
@@ -244,7 +249,7 @@ class Doodle
|
|
244
249
|
end
|
245
250
|
end
|
246
251
|
|
247
|
-
# use this to include 'standard' flags
|
252
|
+
# use this to include 'standard' flags: help (-h, --help), verbose (-v, --verbose) and debug (-d, --debug)
|
248
253
|
def std_flags
|
249
254
|
boolean :help, :flag => "h", :doc => "display this help"
|
250
255
|
boolean :verbose, :flag => "v", :doc => "verbose output"
|
@@ -252,7 +257,7 @@ class Doodle
|
|
252
257
|
end
|
253
258
|
|
254
259
|
has :exit_status, :default => 0
|
255
|
-
|
260
|
+
|
256
261
|
# call App.run to start your application (calls instance.run)
|
257
262
|
def run(argv = ARGV)
|
258
263
|
begin
|
@@ -267,15 +272,13 @@ class Doodle
|
|
267
272
|
exit_status 1
|
268
273
|
end
|
269
274
|
puts "\nERROR: #{e}"
|
270
|
-
puts
|
271
|
-
puts help_text
|
272
275
|
ensure
|
273
276
|
exit(exit_status)
|
274
277
|
end
|
275
278
|
end
|
276
279
|
|
277
280
|
private
|
278
|
-
|
281
|
+
|
279
282
|
# helpers
|
280
283
|
def flag_to_attribute(flag)
|
281
284
|
a = doodle.attributes.select do |key, attr|
|
@@ -426,6 +429,7 @@ class Doodle
|
|
426
429
|
args + options
|
427
430
|
end
|
428
431
|
public
|
432
|
+
# defines the help text displayed when option --help passed
|
429
433
|
def help_text
|
430
434
|
format_block = proc {|key, flag, doc, required, kind|
|
431
435
|
sprintf(" %-3s %-14s %-10s %s %s", flag, key, kind, doc, required ? '(REQUIRED)' : '')
|
data/lib/doodle/version.rb
CHANGED
data/lib/doodle/xml.rb
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
# some prob with different versions of libxml on different platforms
|
3
|
+
# begin
|
4
|
+
# require 'nokogiri'
|
5
|
+
# rescue LoadError
|
6
|
+
require 'rexml/document'
|
7
|
+
# end
|
8
|
+
|
9
|
+
class Doodle
|
10
|
+
module EscapeXML
|
11
|
+
ESCAPE = { '&' => '&', '"' => '"', '>' => '>', '<' => '<' }
|
12
|
+
|
13
|
+
def self.escape(s)
|
14
|
+
s.to_s.gsub(/[&"><]/) { |special| ESCAPE[special] }
|
15
|
+
end
|
16
|
+
def self.unescape(s)
|
17
|
+
ESCAPE.inject(s.to_s) do |str, (k, v)|
|
18
|
+
# don't use gsub! here - don't want to modify argument
|
19
|
+
str.gsub(v, k)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# adds to_xml and from_xml methods for serializing and deserializing
|
25
|
+
# Doodle object graphs to and from XML
|
26
|
+
#
|
27
|
+
# works for me but YMMV
|
28
|
+
module XML
|
29
|
+
include Utils
|
30
|
+
class Document < Doodle
|
31
|
+
include Doodle::XML
|
32
|
+
end
|
33
|
+
|
34
|
+
# adapter module for REXML
|
35
|
+
module REXMLAdapter
|
36
|
+
|
37
|
+
# return the parsed xml DOM
|
38
|
+
def parse_xml(xml)
|
39
|
+
REXML::Document.new(xml)
|
40
|
+
end
|
41
|
+
|
42
|
+
# test whether a node is a text node
|
43
|
+
def text_node?(node)
|
44
|
+
node.kind_of?(::REXML::Text)
|
45
|
+
end
|
46
|
+
|
47
|
+
# get the first XML element in the document
|
48
|
+
def get_root(doc)
|
49
|
+
# skip :REXML::XMLDecl
|
50
|
+
# REXML children does not properly implement shift (or pop)
|
51
|
+
root = doc.children.find { |el, i| el.kind_of?(REXML::Element) }
|
52
|
+
if root.nil?
|
53
|
+
raise ArgumentError, "XML document does not contain any elements"
|
54
|
+
else
|
55
|
+
root
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# adapter module for Nokogiri
|
61
|
+
module NokogiriAdapter
|
62
|
+
|
63
|
+
# return the parsed xml DOM
|
64
|
+
def parse_xml(xml)
|
65
|
+
Nokogiri::XML(xml)
|
66
|
+
end
|
67
|
+
|
68
|
+
# test whether a node is a text node
|
69
|
+
def text_node?(node)
|
70
|
+
node.name == "text"
|
71
|
+
end
|
72
|
+
|
73
|
+
# get the first XML element in the document
|
74
|
+
def get_root(doc)
|
75
|
+
doc.children.first
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
if Object.const_defined?(:Nokogiri)
|
80
|
+
extend NokogiriAdapter
|
81
|
+
else
|
82
|
+
extend REXMLAdapter
|
83
|
+
end
|
84
|
+
|
85
|
+
class << self
|
86
|
+
# parse XML +str+ into a Doodle object graph, using +ctx+ as the
|
87
|
+
# root namespace (can be module or class)
|
88
|
+
#
|
89
|
+
# this is the entry point - most of the heavy lifting is done by
|
90
|
+
# +from_xml_elem+
|
91
|
+
def from_xml(ctx, str)
|
92
|
+
doc = parse_xml(str)
|
93
|
+
root = get_root(doc)
|
94
|
+
from_xml_elem(ctx, root)
|
95
|
+
end
|
96
|
+
|
97
|
+
# helper function to handle recursion
|
98
|
+
def from_xml_elem(ctx, root)
|
99
|
+
attributes = root.attributes.inject({ }) { |hash, (k, v)| hash[k] = EscapeXML.unescape(v.to_s); hash}
|
100
|
+
text, children = root.children.partition{ |x| text_node?(x) }
|
101
|
+
text = text.map{ |x| x.to_s}.reject{ |s| s =~ /^\s*$/}.join('')
|
102
|
+
oroot = Utils.const_lookup(root.name, ctx).new(text, attributes) {
|
103
|
+
from_xml_elem(root)
|
104
|
+
}
|
105
|
+
oroot
|
106
|
+
end
|
107
|
+
private :from_xml_elem
|
108
|
+
end
|
109
|
+
|
110
|
+
def from_xml_elem(parent)
|
111
|
+
children = parent.children.reject{ |x| XML.text_node?(x) }
|
112
|
+
children.each do |child|
|
113
|
+
text = child.children.select{ |x| XML.text_node?(x) }.map{ |x| x.to_s}.reject{ |s| s =~ /^\s*$/}.join('')
|
114
|
+
object = const_lookup(child.name)
|
115
|
+
method = Doodle::Utils.snake_case(Utils.normalize_const(child.name))
|
116
|
+
attributes = child.attributes.inject({ }) { |hash, (k, v)| hash[k] = EscapeXML.unescape(v.to_s); hash}
|
117
|
+
send(method, object.new(text, attributes) {
|
118
|
+
from_xml_elem(child)
|
119
|
+
})
|
120
|
+
end
|
121
|
+
#parent
|
122
|
+
self
|
123
|
+
end
|
124
|
+
private :from_xml_elem
|
125
|
+
|
126
|
+
# override this to define a tag name for output - the default is
|
127
|
+
# to use the classname (wthout namespacing)
|
128
|
+
def tag
|
129
|
+
#self.class.to_s.split(/::/)[-1].downcase
|
130
|
+
self.class.to_s.split(/::/)[-1]
|
131
|
+
end
|
132
|
+
|
133
|
+
# override this to define a specialised attributes format
|
134
|
+
def format_attributes(attributes)
|
135
|
+
if attributes.size > 0
|
136
|
+
" " + attributes.map{ |k, v| %[#{ k }="#{ v }"]}.join(" ")
|
137
|
+
else
|
138
|
+
""
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# override this to define a specialised tag format
|
143
|
+
def format_tag(tag, attributes, body)
|
144
|
+
if body.size > 0
|
145
|
+
["<#{tag}#{format_attributes(attributes)}>", body, "</#{tag}>"]
|
146
|
+
else
|
147
|
+
["<#{tag}#{format_attributes(attributes)} />"]
|
148
|
+
end.join('')
|
149
|
+
end
|
150
|
+
|
151
|
+
# output Doodle object graph as xml
|
152
|
+
def to_xml
|
153
|
+
body = []
|
154
|
+
attributes = []
|
155
|
+
self.doodle.attributes.map do |k, attr|
|
156
|
+
next if self.default?(k)
|
157
|
+
# arbitrary
|
158
|
+
if k == :_text_
|
159
|
+
body << self._text_
|
160
|
+
next
|
161
|
+
end
|
162
|
+
v = send(k)
|
163
|
+
if v.kind_of?(Doodle)
|
164
|
+
body << v.to_xml
|
165
|
+
elsif v.kind_of?(Array)
|
166
|
+
body << v.map{ |x| x.to_xml }
|
167
|
+
else
|
168
|
+
attributes << [k, EscapeXML.escape(v)]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
format_tag(tag, attributes, body)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
data/spec/arg_order_spec.rb
CHANGED
data/spec/block_init_spec.rb
CHANGED
data/spec/bugs_spec.rb
CHANGED
@@ -24,7 +24,7 @@ end
|
|
24
24
|
describe 'Doodle', 'loading good data from yaml' do
|
25
25
|
temporary_constant :Foo do
|
26
26
|
before :each do
|
27
|
-
class Foo < Doodle
|
27
|
+
class ::Foo < Doodle
|
28
28
|
has :date, :kind => Date do
|
29
29
|
from String do |s|
|
30
30
|
Date.parse(s)
|
@@ -57,7 +57,7 @@ end
|
|
57
57
|
describe 'Doodle', 'loading bad data from yaml' do
|
58
58
|
temporary_constant :Foo do
|
59
59
|
before :each do
|
60
|
-
class Foo < Doodle
|
60
|
+
class ::Foo < Doodle
|
61
61
|
has :date, :kind => Date do
|
62
62
|
from String do |s|
|
63
63
|
Date.parse(s)
|
@@ -83,7 +83,7 @@ end
|
|
83
83
|
describe 'Doodle', 'loading bad data from yaml with default defined' do
|
84
84
|
temporary_constant :Foo do
|
85
85
|
before :each do
|
86
|
-
class Foo < Doodle
|
86
|
+
class ::Foo < Doodle
|
87
87
|
has :date, :kind => Date do
|
88
88
|
default Date.today
|
89
89
|
from String do |s|
|
@@ -110,7 +110,7 @@ end
|
|
110
110
|
describe Doodle, 'class attributes:' do
|
111
111
|
temporary_constant :Foo do
|
112
112
|
before :each do
|
113
|
-
class Foo < Doodle
|
113
|
+
class ::Foo < Doodle
|
114
114
|
has :ivar
|
115
115
|
class << self
|
116
116
|
has :cvar
|
@@ -127,19 +127,20 @@ end
|
|
127
127
|
describe Doodle, 'initializing from hashes and yaml' do
|
128
128
|
temporary_constants :AddressLine, :Person do
|
129
129
|
before :each do
|
130
|
-
class AddressLine < Doodle
|
130
|
+
class ::AddressLine < Doodle
|
131
131
|
has :text, :kind => String
|
132
132
|
end
|
133
133
|
|
134
|
-
class Person < Doodle
|
134
|
+
class ::Person < Doodle
|
135
135
|
has :name, :kind => String
|
136
|
-
has :address, :collect => { :line => AddressLine }
|
136
|
+
has :address, :collect => { :line => ::AddressLine }
|
137
137
|
end
|
138
138
|
end
|
139
139
|
|
140
|
-
|
140
|
+
# TODO: this is a bit of a mess - split into separate specs and clarify what I'm expecting
|
141
|
+
it 'should validate output from to_yaml' do
|
141
142
|
|
142
|
-
|
143
|
+
source_yaml = %[
|
143
144
|
---
|
144
145
|
:address:
|
145
146
|
- Henry Wood House
|
@@ -147,23 +148,26 @@ describe Doodle, 'initializing from hashes and yaml' do
|
|
147
148
|
:name: Sean
|
148
149
|
]
|
149
150
|
|
150
|
-
person = Person(YAML.load(
|
151
|
+
person = Person(YAML.load(source_yaml))
|
151
152
|
yaml = person.to_yaml
|
152
153
|
# be careful here - Ruby yaml is finicky (spaces after class names)
|
153
154
|
yaml = yaml.gsub(/\s*\n/m, "\n")
|
154
|
-
# yaml.should_be %[--- !ruby/object:Person
|
155
|
-
# address:
|
156
|
-
# - !ruby/object:AddressLine
|
157
|
-
# text: Henry Wood House
|
158
|
-
# - !ruby/object:AddressLine
|
159
|
-
# text: London
|
160
|
-
# name: Sean
|
161
|
-
# ]
|
162
|
-
|
163
|
-
|
155
|
+
# yaml.should_be %[--- !ruby/object:Person
|
156
|
+
# address:
|
157
|
+
# - !ruby/object:AddressLine
|
158
|
+
# text: Henry Wood House
|
159
|
+
# - !ruby/object:AddressLine
|
160
|
+
# text: London
|
161
|
+
# name: Sean
|
162
|
+
# ]
|
163
|
+
loaded = YAML::load(source_yaml)
|
164
|
+
loaded[:name].should_be person.name
|
165
|
+
loaded[:address].should_be person.address.map{|x| x.text}
|
166
|
+
# want to compare yaml output with this but different order for every version of ruby, jruby, etc.
|
167
|
+
# "--- !ruby/object:Person\naddress:\n- !ruby/object:AddressLine\n text: Henry Wood House\n- !ruby/object:AddressLine\n text: London\nname: Sean\n"
|
164
168
|
person = YAML.load(yaml)
|
165
169
|
proc { person.validate! }.should_not raise_error
|
166
|
-
person.address.all?{ |x| x.kind_of?
|
170
|
+
person.address.all?{ |x| x.kind_of?(AddressLine) }.should_be true
|
167
171
|
|
168
172
|
end
|
169
173
|
end
|
@@ -172,20 +176,20 @@ end
|
|
172
176
|
describe 'Doodle', 'hiding @__doodle__' do
|
173
177
|
temporary_constant :Foo, :Bar, :DString, :DHash, :DArray do
|
174
178
|
before :each do
|
175
|
-
class Foo < Doodle
|
179
|
+
class ::Foo < Doodle
|
176
180
|
has :var1, :kind => Integer
|
177
181
|
end
|
178
|
-
class Bar
|
182
|
+
class ::Bar
|
179
183
|
include Doodle::Core
|
180
184
|
has :var2, :kind => Integer
|
181
185
|
end
|
182
|
-
class DString < String
|
186
|
+
class ::DString < String
|
183
187
|
include Doodle::Core
|
184
188
|
end
|
185
|
-
class DHash < Hash
|
189
|
+
class ::DHash < Hash
|
186
190
|
include Doodle::Core
|
187
191
|
end
|
188
|
-
class DArray < Array
|
192
|
+
class ::DArray < Array
|
189
193
|
include Doodle::Core
|
190
194
|
end
|
191
195
|
end
|
@@ -232,15 +236,15 @@ end
|
|
232
236
|
describe 'Doodle', 'initalizing class level collectors' do
|
233
237
|
temporary_constant :Menu, :KeyedMenu, :Item, :SubMenu do
|
234
238
|
before :each do
|
235
|
-
class Item < Doodle
|
239
|
+
class ::Item < Doodle
|
236
240
|
has :title
|
237
241
|
end
|
238
|
-
class Menu < Doodle
|
242
|
+
class ::Menu < Doodle
|
239
243
|
class << self
|
240
244
|
has :items, :collect => Item
|
241
245
|
end
|
242
246
|
end
|
243
|
-
class KeyedMenu < Doodle
|
247
|
+
class ::KeyedMenu < Doodle
|
244
248
|
class << self
|
245
249
|
has :items, :collect => Item, :key => :title
|
246
250
|
end
|
@@ -322,3 +326,31 @@ describe 'Doodle', 'validating required attributes after default attributes' do
|
|
322
326
|
end
|
323
327
|
end
|
324
328
|
end
|
329
|
+
|
330
|
+
describe Doodle, 'if default specified before required attributes, they are ignored if defined in block' do
|
331
|
+
temporary_constant :Address do
|
332
|
+
before :each do
|
333
|
+
class Address < Doodle
|
334
|
+
has :where, :default => "home"
|
335
|
+
has :city
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
it 'should raise an error that required attributes have not been set' do
|
340
|
+
proc {
|
341
|
+
Address do
|
342
|
+
city "London"
|
343
|
+
end
|
344
|
+
}.should_not raise_error
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'should define required attributes' do
|
348
|
+
a = Address do
|
349
|
+
city "London"
|
350
|
+
end
|
351
|
+
a.city.should_be "London"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
356
|
+
|