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/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
+