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.
- data/History.txt +24 -0
- data/Manifest.txt +26 -1
- data/README.txt +9 -8
- data/lib/doodle.rb +43 -1496
- data/lib/doodle/app.rb +6 -0
- data/lib/doodle/attribute.rb +165 -0
- data/lib/doodle/base.rb +180 -0
- data/lib/doodle/collector-1.9.rb +72 -0
- data/lib/doodle/collector.rb +191 -0
- data/lib/doodle/comparable.rb +8 -0
- data/lib/doodle/conversion.rb +80 -0
- data/lib/doodle/core.rb +42 -0
- data/lib/doodle/datatype-holder.rb +39 -0
- data/lib/doodle/debug.rb +20 -0
- data/lib/doodle/deferred.rb +13 -0
- data/lib/doodle/equality.rb +21 -0
- data/lib/doodle/exceptions.rb +29 -0
- data/lib/doodle/factory.rb +91 -0
- data/lib/doodle/getter-setter.rb +154 -0
- data/lib/doodle/info.rb +298 -0
- data/lib/doodle/inherit.rb +40 -0
- data/lib/doodle/json.rb +38 -0
- data/lib/doodle/marshal.rb +16 -0
- data/lib/doodle/normalized_array.rb +512 -0
- data/lib/doodle/normalized_hash.rb +356 -0
- data/lib/doodle/ordered-hash.rb +8 -0
- data/lib/doodle/singleton.rb +23 -0
- data/lib/doodle/smoke-and-mirrors.rb +23 -0
- data/lib/doodle/to_hash.rb +17 -0
- data/lib/doodle/utils.rb +173 -11
- data/lib/doodle/validation.rb +122 -0
- data/lib/doodle/version.rb +1 -1
- data/lib/molic_orderedhash.rb +24 -10
- data/spec/assigned_spec.rb +45 -0
- data/spec/attributes_spec.rb +7 -7
- data/spec/collector_spec.rb +100 -13
- data/spec/doodle_context_spec.rb +5 -5
- data/spec/from_spec.rb +43 -3
- data/spec/json_spec.rb +232 -0
- data/spec/member_init_spec.rb +11 -11
- data/spec/modules_spec.rb +4 -4
- data/spec/multi_collector_spec.rb +91 -0
- data/spec/must_spec.rb +32 -0
- data/spec/spec_helper.rb +14 -4
- data/spec/specialized_attribute_class_spec.rb +2 -2
- data/spec/typed_collector_spec.rb +57 -0
- data/spec/xml_spec.rb +8 -8
- 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,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
|
data/lib/doodle/utils.rb
CHANGED
@@ -1,13 +1,175 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
#
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|