doodle 0.2.2 → 0.2.3

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