doodle 0.1.9 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|