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/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
- to do:
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)' : '')
@@ -1,8 +1,8 @@
1
1
  class Doodle #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 1
5
- TINY = 9
4
+ MINOR = 2
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
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 = { '&' => '&amp;', '"' => '&quot;', '>' => '&gt;', '<' => '&lt;' }
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
+
@@ -82,7 +82,7 @@ describe 'arg_order' do
82
82
  it 'should specify order of positional arguments' do
83
83
  f = Bar.new 1
84
84
  f.value.should_be 1
85
- f.name.should_be "bar"
85
+ f.name.should =~ /bar/
86
86
  end
87
87
  end
88
88
  end
@@ -4,7 +4,7 @@ require 'yaml'
4
4
  describe 'Doodle', 'block initialization of scalar attributes' do
5
5
  temporary_constant :Foo, :Bar, :Farm, :Barn, :Animal do
6
6
  before :each do
7
- class Animal < Doodle
7
+ class ::Animal < Doodle
8
8
  has :species
9
9
  end
10
10
  class Barn < Doodle
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
- it 'should validate ouput from to_yaml' do
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
- yaml = %[
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(yaml))
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
- yaml.should_be "--- !ruby/object:Person\naddress:\n- !ruby/object:AddressLine\n text: Henry Wood House\n- !ruby/object:AddressLine\n text: London\nname: Sean\n"
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? AddressLine }.should_be true
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
+