doodle 0.2.2 → 0.2.3

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.
Files changed (48) hide show
  1. data/History.txt +24 -0
  2. data/Manifest.txt +26 -1
  3. data/README.txt +9 -8
  4. data/lib/doodle.rb +43 -1496
  5. data/lib/doodle/app.rb +6 -0
  6. data/lib/doodle/attribute.rb +165 -0
  7. data/lib/doodle/base.rb +180 -0
  8. data/lib/doodle/collector-1.9.rb +72 -0
  9. data/lib/doodle/collector.rb +191 -0
  10. data/lib/doodle/comparable.rb +8 -0
  11. data/lib/doodle/conversion.rb +80 -0
  12. data/lib/doodle/core.rb +42 -0
  13. data/lib/doodle/datatype-holder.rb +39 -0
  14. data/lib/doodle/debug.rb +20 -0
  15. data/lib/doodle/deferred.rb +13 -0
  16. data/lib/doodle/equality.rb +21 -0
  17. data/lib/doodle/exceptions.rb +29 -0
  18. data/lib/doodle/factory.rb +91 -0
  19. data/lib/doodle/getter-setter.rb +154 -0
  20. data/lib/doodle/info.rb +298 -0
  21. data/lib/doodle/inherit.rb +40 -0
  22. data/lib/doodle/json.rb +38 -0
  23. data/lib/doodle/marshal.rb +16 -0
  24. data/lib/doodle/normalized_array.rb +512 -0
  25. data/lib/doodle/normalized_hash.rb +356 -0
  26. data/lib/doodle/ordered-hash.rb +8 -0
  27. data/lib/doodle/singleton.rb +23 -0
  28. data/lib/doodle/smoke-and-mirrors.rb +23 -0
  29. data/lib/doodle/to_hash.rb +17 -0
  30. data/lib/doodle/utils.rb +173 -11
  31. data/lib/doodle/validation.rb +122 -0
  32. data/lib/doodle/version.rb +1 -1
  33. data/lib/molic_orderedhash.rb +24 -10
  34. data/spec/assigned_spec.rb +45 -0
  35. data/spec/attributes_spec.rb +7 -7
  36. data/spec/collector_spec.rb +100 -13
  37. data/spec/doodle_context_spec.rb +5 -5
  38. data/spec/from_spec.rb +43 -3
  39. data/spec/json_spec.rb +232 -0
  40. data/spec/member_init_spec.rb +11 -11
  41. data/spec/modules_spec.rb +4 -4
  42. data/spec/multi_collector_spec.rb +91 -0
  43. data/spec/must_spec.rb +32 -0
  44. data/spec/spec_helper.rb +14 -4
  45. data/spec/specialized_attribute_class_spec.rb +2 -2
  46. data/spec/typed_collector_spec.rb +57 -0
  47. data/spec/xml_spec.rb +8 -8
  48. metadata +33 -3
@@ -0,0 +1,356 @@
1
+ # NormalizedHash - ensure hash keys and/or values are normalized to
2
+ # particular type
3
+ #
4
+ # To use, derive a subclass from NormalizeKeyHash and provide you own
5
+ # normalize_key(key) method
6
+ #
7
+ # See StringKeyHash and SymbolKeyHash for examples
8
+ #
9
+ # Sean O'Halpin, 2004..2009
10
+
11
+ module ModNormalizedHash
12
+ module InstanceMethods
13
+ def initialize(arg = {}, &block)
14
+ if block_given?
15
+ original_block = block
16
+ # this is unfortunate
17
+ block = proc { |h, k|
18
+ #p [:block, h, k]
19
+ res = normalize_value(original_block[h, normalize_key(k)])
20
+ #p [:block_self, self, res]
21
+ each do |k, v|
22
+ #p [:init_block, k, v]
23
+ self[k] = normalize_value(v)
24
+ end
25
+ #p [:block_res, self, res]
26
+ res
27
+ }
28
+ end
29
+ if arg.is_a?(Hash)
30
+ super(&block)
31
+ update(arg)
32
+ else
33
+ super(arg, &block)
34
+ end
35
+ end
36
+
37
+ def default(k)
38
+ super(normalize_key(k))
39
+ end
40
+
41
+ def default=(value)
42
+ super(normalize_value(value))
43
+ end
44
+
45
+ def store(k,v)
46
+ super(normalize_key(k), normalize_value(v))
47
+ end
48
+
49
+ def fetch(k)
50
+ super(normalize_key(k))
51
+ end
52
+
53
+ # Note that invert returns a new +Hash+. This is by design. If you want the new hash to have the same properties as its source,
54
+ # use something like:
55
+ #
56
+ # h = StringKeyHash.new(h.invert)
57
+ #
58
+ # def invert
59
+ # super
60
+ # end
61
+
62
+ def delete(k)
63
+ super(normalize_key(k))
64
+ end
65
+
66
+ def [](k)
67
+ super(normalize_key(k))
68
+ end
69
+
70
+ def []=(k,v)
71
+ super(normalize_key(k), normalize_value(v))
72
+ end
73
+
74
+ def key?(k)
75
+ super(normalize_key(k))
76
+ end
77
+ alias :has_key? :key?
78
+ alias :member? :has_key?
79
+ alias :include? :has_key?
80
+
81
+ def has_value?(v)
82
+ super(normalize_value(v))
83
+ end
84
+ alias :value? :has_value?
85
+
86
+ def update(other, &block)
87
+ if block_given?
88
+ # {|key, oldval, newval| block}
89
+ super(other) { |key, oldval, newval|
90
+ normalize_value(block.call(key, oldval, newval))
91
+ }
92
+ else
93
+ other.each do |k,v|
94
+ store(k,v)
95
+ end
96
+ end
97
+ end
98
+ alias :merge! :update
99
+
100
+ def merge(other)
101
+ self.dup.update(other)
102
+ end
103
+
104
+ def values_at(*keys)
105
+ super(*keys.map{ |k| normalize_key(k)})
106
+ end
107
+ alias :indices :values_at # deprecated
108
+ alias :indexes :values_at # deprecated
109
+
110
+ def replace(other)
111
+ self.clear
112
+ update(other)
113
+ end
114
+
115
+ def index(value)
116
+ super(normalize_value(v))
117
+ end
118
+
119
+ # implemented in super
120
+ # def clear
121
+ # super
122
+ # end
123
+
124
+ # def default_proc
125
+ # super
126
+ # end
127
+
128
+ # def delete_if(&block)
129
+ # super
130
+ # end
131
+
132
+ # def each(&block)
133
+ # super
134
+ # end
135
+
136
+ # def each_key(&block)
137
+ # super
138
+ # end
139
+
140
+ # def each_pair(&block)
141
+ # super
142
+ # end
143
+
144
+ # def each_value(&block)
145
+ # super
146
+ # end
147
+
148
+ # def empty?
149
+ # super
150
+ # end
151
+
152
+ # def invert
153
+ # super
154
+ # end
155
+
156
+ # def keys
157
+ # super
158
+ # end
159
+
160
+ # def length
161
+ # super
162
+ # end
163
+ # alias :size :length
164
+
165
+ # def rehash
166
+ # super
167
+ # end
168
+
169
+ # def reject!(&block)
170
+ # super
171
+ # end
172
+
173
+ # def shift
174
+ # super
175
+ # end
176
+
177
+ # def to_hash
178
+ # super
179
+ # end
180
+
181
+ # def values
182
+ # super
183
+ # end
184
+ end
185
+
186
+ module ClassMethods
187
+ def [](*args)
188
+ new(Hash[*args])
189
+ end
190
+ end
191
+
192
+ # in normal usage, these are the only methods you should need to override
193
+ module OverrideMethods
194
+ # override this method to normalize key, e.g. to normalize keys to
195
+ # strings:
196
+ #
197
+ # def normalize_key(k)
198
+ # k.to_s
199
+ # end
200
+ def normalize_key(k)
201
+ k
202
+ end
203
+
204
+ # override this method to normalize value, e.g. to normalize
205
+ # values to strings:
206
+ #
207
+ # def normalize_value(v)
208
+ # v.to_s
209
+ # end
210
+ def normalize_value(v)
211
+ v
212
+ end
213
+ end
214
+
215
+ end
216
+
217
+ # Note that some methods return a new +Hash+ not an object of your
218
+ # subclass. This is by design (i.e. it's how ruby works). If you want
219
+ # the new hash to have the same properties as its source, use
220
+ # something like:
221
+ #
222
+ # h = StringKeyHash.new(h.invert)
223
+ #
224
+ # The methods are:
225
+ #
226
+ # invert => Hash
227
+ # select, reject => Hash in 1.9
228
+ class NormalizedHash < Hash
229
+ include ModNormalizedHash::InstanceMethods
230
+ include ModNormalizedHash::OverrideMethods
231
+ extend ModNormalizedHash::ClassMethods
232
+ end
233
+
234
+ class SymbolKeyHash < NormalizedHash
235
+ def normalize_key(k)
236
+ k.to_s.to_sym
237
+ end
238
+ end
239
+
240
+ class StringKeyHash < NormalizedHash
241
+ def normalize_key(k)
242
+ #p [:normalizing, k]
243
+ k.to_s
244
+ end
245
+ end
246
+
247
+ class StringHash < StringKeyHash
248
+ def normalize_value(v)
249
+ v.to_s
250
+ end
251
+ end
252
+
253
+ module ModNormalizedHash
254
+ module ClassMethods
255
+ def MultiTypedHash(*klasses, &block)
256
+ typed_class = Class.new(NormalizedHash) do
257
+ # note: cannot take a block
258
+ if block_given?
259
+ define_method :normalize_value, &block
260
+ else
261
+ define_method :normalize_value do |v|
262
+ if !klasses.any?{ |klass| v.kind_of?(klass) }
263
+ raise TypeError, "#{self.class}: #{v.class}(#{v.inspect}) is not a kind of #{klasses.map{ |c| c.to_s }.join(', ')}", [caller[-1]]
264
+ end
265
+ v
266
+ end
267
+ end
268
+ end
269
+ typed_class
270
+ end
271
+ end
272
+ end
273
+
274
+ if __FILE__ == $0
275
+ require 'rubygems'
276
+ require 'assertion'
277
+ require 'date'
278
+
279
+ sh = StringKeyHash.new { |h,k| h[k] = 42 }
280
+ assert { sh[:a] == 42 }
281
+ assert { sh["a"] == 42 }
282
+ assert { sh.keys == ["a"] }
283
+
284
+ sh = StringKeyHash.new( { :a => 2 } )
285
+ assert { sh.keys == ["a"] }
286
+
287
+ yh = SymbolKeyHash.new { |h,k| h[k] = 42 }
288
+ assert { yh[:a] == 42 }
289
+ assert { yh["a"] == 42 }
290
+ assert { yh.keys == [:a] }
291
+
292
+ yh = SymbolKeyHash.new( { :a => 2 } )
293
+ assert { yh.keys == [:a] }
294
+
295
+ bh = StringKeyHash.new( { :a => 2 } ) { |h,k| h[k] = 42}
296
+ assert { bh[:b] == 42 }
297
+ assert { bh.keys.sort == ["a", "b"] }
298
+
299
+ sh = StringHash.new( { :a => 2 } ) { |h,k| h[k] = 42}
300
+ assert { sh[:b] == "42" }
301
+ assert { sh.keys.sort == ["a", "b"] }
302
+ assert { sh.values.sort == ["2", "42"] }
303
+
304
+ skh = SymbolKeyHash.new( { :a => 2 } ) { |h,k| h[k] = 42}
305
+ assert { skh.values == [2] }
306
+ skh['b'] = 42
307
+ assert { skh.key?(:a) && skh.key?(:b) }
308
+ #p skh.invert.keys
309
+ #p skh.invert
310
+ #p skh.invert.class
311
+ assert { skh.invert.keys == [2, 42] }
312
+ nskh = StringKeyHash.new(skh.invert)
313
+ #p nskh
314
+ assert { nskh.keys.sort == ["2", "42"] }
315
+ nsh = StringHash.new(skh.invert)
316
+ #p nsh
317
+ assert { nsh.keys.sort == ["2", "42"] }
318
+ assert { nsh.values.sort == ["a", "b"] }
319
+
320
+ StringIntegerHash = NormalizedHash::MultiTypedHash(String, Integer)
321
+
322
+ expect_ok { sih = StringIntegerHash[:a => 1, :b => "Hello"] }
323
+ expect_error(TypeError) { sih = StringIntegerHash[:a => 1, :b => Date.new] }
324
+
325
+ expect_ok {
326
+ sih = StringIntegerHash.new
327
+ sih[:a] = 1
328
+ sih[:b] = "hello"
329
+ }
330
+
331
+ expect_error(TypeError) {
332
+ sih = StringIntegerHash.new
333
+ sih[:c] = Date.new
334
+ }
335
+
336
+ ReverseStringHash = NormalizedHash::MultiTypedHash() do |v|
337
+ if v.kind_of?(String)
338
+ v.reverse
339
+ else
340
+ v
341
+ end
342
+ end
343
+
344
+ assert {
345
+ sih = ReverseStringHash[:a => 123, :b => "Hello"]
346
+ sih[:a] == 123 && sih[:b] == "Hello".reverse
347
+ }
348
+
349
+ assert {
350
+ sih = ReverseStringHash.new
351
+ sih[:a] = 123
352
+ sih[:b] = "Hello"
353
+ sih[:a] == 123 && sih[:b] == "Hello".reverse
354
+ }
355
+
356
+ end
@@ -0,0 +1,8 @@
1
+ if RUBY_VERSION < '1.9.0'
2
+ require 'molic_orderedhash' # TODO: replace this with own (required functions only) version
3
+ else
4
+ # 1.9+ hashes are ordered by default
5
+ class Doodle
6
+ OrderedHash = ::Hash
7
+ end
8
+ end
@@ -0,0 +1,23 @@
1
+ class Doodle
2
+ # provides more direct access to the singleton class and a way to
3
+ # treat singletons, Modules and Classes equally in a meta context
4
+ module Singleton
5
+ # return the 'singleton class' of an object, optionally executing
6
+ # a block argument in the (module/class) context of that object
7
+ def singleton_class(&block)
8
+ sc = class << self; self; end
9
+ sc.module_eval(&block) if block_given?
10
+ sc
11
+ end
12
+ # evaluate in class context of self, whether Class, Module or singleton
13
+ def sc_eval(*args, &block)
14
+ if self.kind_of?(Module)
15
+ klass = self
16
+ else
17
+ klass = self.singleton_class
18
+ end
19
+ klass.module_eval(*args, &block)
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,23 @@
1
+ class Doodle
2
+
3
+ # what it says on the tin :) various hacks to hide @__doodle__ variable
4
+ module SmokeAndMirrors
5
+
6
+ # redefine instance_variables to ignore our private @__doodle__ variable
7
+ # (hack to fool yaml and anything else that queries instance_variables)
8
+ meth = Object.instance_method(:instance_variables)
9
+ define_method :instance_variables do
10
+ meth.bind(self).call.reject{ |x| x.to_s =~ /@__doodle__/}
11
+ end
12
+
13
+ # hide @__doodle__ from inspect
14
+ def inspect
15
+ super.gsub(/\s*@__doodle__=,/,'').gsub(/,?\s*@__doodle__=/,'')
16
+ end
17
+
18
+ # fix for pp
19
+ def pretty_print(q)
20
+ q.pp_object(self)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ require 'yaml'
2
+ class Doodle
3
+ module ToHash
4
+ # create 'pure' hash of scalars only from attributes - hacky but works (kinda)
5
+ def to_hash
6
+ Doodle::Utils.symbolize_keys!(YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }, true)
7
+ #begin
8
+ # YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }
9
+ #rescue Object => e
10
+ # doodle.attributes.inject({}) {|hash, (name, attribute)| hash[name] = send(name); hash}
11
+ #end
12
+ end
13
+ def to_string_hash
14
+ Doodle::Utils.stringify_keys!(YAML::load(to_yaml.gsub(/!ruby\/object:.*$/, '')) || { }, true)
15
+ end
16
+ end
17
+ end
@@ -1,13 +1,175 @@
1
- # doodle/utils.rb
2
- # - not part of main lib but useful tidbits
3
- # Sean O'Halpin, 2008-04-17
4
- require 'pp'
5
-
6
- def try(&block)
7
- begin
8
- block.call
9
- rescue Exception => e
10
- e
1
+ # Set of utility functions to avoid monkeypatching base classes
2
+ class Doodle
3
+ # Set of utility functions to avoid monkeypatching base classes
4
+ module Utils
5
+ module ClassMethods
6
+
7
+ # unnest arrays by one level of nesting, e.g. [1, [[2], 3]] =>
8
+ # [1, [2], 3].
9
+ def flatten_first_level(enum)
10
+ enum.inject([]) {|arr, i|
11
+ if i.kind_of?(Array)
12
+ arr.push(*i)
13
+ else
14
+ arr.push(i)
15
+ end
16
+ }
17
+ end
18
+
19
+ # convert a CamelCasedWord to a snake_cased_word
20
+ # based on version in facets/string/case.rb, line 80
21
+ def snake_case(camel_cased_word)
22
+ # if all caps, just downcase it
23
+ if camel_cased_word =~ /^[A-Z]+$/
24
+ camel_cased_word.downcase
25
+ else
26
+ camel_cased_word.to_s.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
27
+ end
28
+ end
29
+ alias :snakecase :snake_case
30
+
31
+ # resolve a constant of the form Some::Class::Or::Module -
32
+ # doesn't work with constants defined in anonymous
33
+ # classes/modules
34
+ def const_resolve(constant)
35
+ constant.to_s.split(/::/).reject{|x| x.empty?}.inject(Object) { |prev, this| prev.const_get(this) }
36
+ end
37
+
38
+ # deep copy of object (unlike shallow copy dup or clone)
39
+ def deep_copy(obj)
40
+ ::Marshal.load(::Marshal.dump(obj))
41
+ end
42
+
43
+ # normalize hash keys using method (e.g. +:to_sym+, +:to_s+)
44
+ #
45
+ # [+hash+] target hash to update
46
+ # [+recursive+] recurse into child hashes if +true+ (default is not to recurse)
47
+ # [+method+] method to apply to key (default is +:to_sym+)
48
+ def normalize_keys!(hash, recursive = false, method = :to_sym)
49
+ if hash.kind_of?(Hash)
50
+ hash.keys.each do |key|
51
+ normalized_key = key.respond_to?(method) ? key.send(method) : key
52
+ v = hash.delete(key)
53
+ if recursive
54
+ if v.kind_of?(Hash)
55
+ v = normalize_keys!(v, recursive, method)
56
+ elsif v.kind_of?(Array)
57
+ v = v.map{ |x| normalize_keys!(x, recursive, method) }
58
+ end
59
+ end
60
+ hash[normalized_key] = v
61
+ end
62
+ end
63
+ hash
64
+ end
65
+
66
+ # normalize hash keys using method (e.g. :to_sym, :to_s)
67
+ # - returns copy of hash
68
+ # - optionally recurse into child hashes
69
+ # see #normalize_keys! for details
70
+ def normalize_keys(hash, recursive = false, method = :to_sym)
71
+ if recursive
72
+ h = deep_copy(hash)
73
+ else
74
+ h = hash.dup
75
+ end
76
+ normalize_keys!(h, recursive, method)
77
+ end
78
+
79
+ # convert keys to symbols
80
+ # - updates target hash in place
81
+ # - optionally recurse into child hashes
82
+ def symbolize_keys!(hash, recursive = false)
83
+ normalize_keys!(hash, recursive, :to_sym)
84
+ end
85
+
86
+ # convert keys to symbols
87
+ # - returns copy of hash
88
+ # - optionally recurse into child hashes
89
+ def symbolize_keys(hash, recursive = false)
90
+ normalize_keys(hash, recursive, :to_sym)
91
+ end
92
+
93
+ # convert keys to strings
94
+ # - updates target hash in place
95
+ # - optionally recurse into child hashes
96
+ def stringify_keys!(hash, recursive = false)
97
+ normalize_keys!(hash, recursive, :to_s)
98
+ end
99
+
100
+ # convert keys to strings
101
+ # - returns copy of hash
102
+ # - optionally recurse into child hashes
103
+ def stringify_keys(hash, recursive = false)
104
+ normalize_keys(hash, recursive, :to_s)
105
+ end
106
+
107
+ # simple (!) pluralization - if you want fancier, override this method
108
+ def pluralize(string)
109
+ s = string.to_s
110
+ if s =~ /s$/
111
+ s + 'es'
112
+ else
113
+ s + 's'
114
+ end
115
+ end
116
+
117
+ # caller
118
+ def doodle_caller
119
+ if $DEBUG
120
+ caller
121
+ else
122
+ [caller[-1]]
123
+ end
124
+ end
125
+
126
+ # execute block - catch any exceptions and return as value
127
+ def try(&block)
128
+ begin
129
+ block.call
130
+ rescue Exception => e
131
+ e
132
+ end
133
+ end
134
+
135
+ # normalize a name to contain only those characters which are
136
+ # valid for a Ruby constant
137
+ def normalize_const(const)
138
+ const.to_s.gsub(/[^A-Za-z_0-9]/, '')
139
+ end
140
+
141
+ # lookup a constant along the module nesting path
142
+ def const_lookup(const, context = self)
143
+ #p [:const_lookup, const, context]
144
+ const = Utils.normalize_const(const)
145
+ result = nil
146
+ if !context.kind_of?(Module)
147
+ context = context.class
148
+ end
149
+ klasses = context.to_s.split(/::/)
150
+ #p klasses
151
+
152
+ path = []
153
+ 0.upto(klasses.size - 1) do |i|
154
+ path << Doodle::Utils.const_resolve(klasses[0..i].join('::'))
155
+ end
156
+ path = (path.reverse + context.ancestors).flatten
157
+ #p [:const, context, path]
158
+ path.each do |ctx|
159
+ #p [:checking, ctx]
160
+ if ctx.const_defined?(const)
161
+ result = ctx.const_get(const)
162
+ break
163
+ end
164
+ end
165
+ if result.nil?
166
+ raise NameError, "Uninitialized constant #{const} in context #{context}"
167
+ else
168
+ result
169
+ end
170
+ end
171
+ end
172
+ extend ClassMethods
173
+ include ClassMethods
11
174
  end
12
175
  end
13
-