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
data/lib/doodle/info.rb
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
class Doodle
|
2
|
+
|
3
|
+
# place to stash bookkeeping info
|
4
|
+
class DoodleInfo
|
5
|
+
attr_accessor :this
|
6
|
+
attr_accessor :local_attributes
|
7
|
+
attr_accessor :local_validations
|
8
|
+
attr_accessor :local_conversions
|
9
|
+
attr_accessor :validation_on
|
10
|
+
attr_accessor :arg_order
|
11
|
+
attr_accessor :errors
|
12
|
+
attr_accessor :parent
|
13
|
+
|
14
|
+
# takes one param - the object being doodled
|
15
|
+
def initialize(object)
|
16
|
+
@this = object
|
17
|
+
@local_attributes = Doodle::OrderedHash.new
|
18
|
+
@local_validations = []
|
19
|
+
@validation_on = true
|
20
|
+
@local_conversions = {}
|
21
|
+
@arg_order = []
|
22
|
+
@errors = []
|
23
|
+
#@parent = nil
|
24
|
+
@parent = Doodle.parent
|
25
|
+
end
|
26
|
+
|
27
|
+
# hide from inspect
|
28
|
+
m = instance_method(:inspect)
|
29
|
+
define_method :__inspect__ do
|
30
|
+
m.bind(self).call
|
31
|
+
end
|
32
|
+
|
33
|
+
# hide from inspect
|
34
|
+
def inspect
|
35
|
+
''
|
36
|
+
end
|
37
|
+
|
38
|
+
# handle errors either by collecting in :errors or raising an exception
|
39
|
+
def handle_error(name, *args)
|
40
|
+
# don't include duplicates (FIXME: hacky - shouldn't have duplicates in the first place)
|
41
|
+
if !errors.include?([name, *args])
|
42
|
+
errors << [name, *args]
|
43
|
+
end
|
44
|
+
if Doodle.raise_exception_on_error
|
45
|
+
raise(*args)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# provide an alternative inheritance chain that works for singleton
|
50
|
+
# classes as well as modules, classes and instances
|
51
|
+
def parents
|
52
|
+
anc = if @this.respond_to?(:ancestors)
|
53
|
+
if @this.ancestors.include?(@this)
|
54
|
+
@this.ancestors[1..-1]
|
55
|
+
else
|
56
|
+
# singletons have no doodle_parents (they're orphans)
|
57
|
+
[]
|
58
|
+
end
|
59
|
+
else
|
60
|
+
@this.class.ancestors
|
61
|
+
end
|
62
|
+
anc.select{|x| x.kind_of?(Class)}
|
63
|
+
end
|
64
|
+
|
65
|
+
# send message to all doodle_parents and collect results
|
66
|
+
def collect_inherited(message)
|
67
|
+
result = []
|
68
|
+
parents.each do |klass|
|
69
|
+
if klass.respond_to?(:doodle) && klass.doodle.respond_to?(message)
|
70
|
+
result.unshift(*klass.doodle.__send__(message))
|
71
|
+
else
|
72
|
+
break
|
73
|
+
end
|
74
|
+
end
|
75
|
+
result
|
76
|
+
end
|
77
|
+
private :collect_inherited
|
78
|
+
|
79
|
+
# collect results of calling method which returns a hash
|
80
|
+
# - if tf == true, returns all inherited attributes
|
81
|
+
# - if tf == false, returns only those attributes defined in the current object/class
|
82
|
+
def handle_inherited_hash(tf, method)
|
83
|
+
if tf
|
84
|
+
collect_inherited(method).inject(Doodle::OrderedHash.new){ |hash, item|
|
85
|
+
hash.merge(Doodle::OrderedHash[*item])
|
86
|
+
}.merge(@this.doodle.__send__(method))
|
87
|
+
else
|
88
|
+
@this.doodle.__send__(method)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
private :handle_inherited_hash
|
92
|
+
|
93
|
+
# returns array of Attributes
|
94
|
+
# - if tf == true, returns all inherited attributes
|
95
|
+
# - if tf == false, returns only those attributes defined in the current object/class
|
96
|
+
def attributes(tf = true)
|
97
|
+
results = handle_inherited_hash(tf, :local_attributes)
|
98
|
+
# if an instance, include the singleton_class attributes
|
99
|
+
#p [:attributes, @this, @this.singleton_class, @this.singleton_class.methods(false), results]
|
100
|
+
if !@this.kind_of?(Class) && @this.singleton_class.doodle.respond_to?(:attributes)
|
101
|
+
results = results.merge(@this.singleton_class.doodle.attributes)
|
102
|
+
end
|
103
|
+
results
|
104
|
+
end
|
105
|
+
|
106
|
+
# return class level attributes
|
107
|
+
def class_attributes
|
108
|
+
attrs = Doodle::OrderedHash.new
|
109
|
+
if @this.kind_of?(Class)
|
110
|
+
attrs = collect_inherited(:class_attributes).inject(Doodle::OrderedHash.new){ |hash, item|
|
111
|
+
hash.merge(Doodle::OrderedHash[*item])
|
112
|
+
}.merge(@this.singleton_class.doodle.respond_to?(:attributes) ? @this.singleton_class.doodle.attributes : { })
|
113
|
+
attrs
|
114
|
+
else
|
115
|
+
@this.class.doodle.class_attributes
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# access list of validations
|
120
|
+
#
|
121
|
+
# note: validations are handled differently to attributes and
|
122
|
+
# conversions because ~all~ validations apply (so are stored as an
|
123
|
+
# array), whereas attributes and conversions are keyed by name and
|
124
|
+
# kind respectively, so only the most recent applies
|
125
|
+
#
|
126
|
+
def validations(tf = true)
|
127
|
+
if tf
|
128
|
+
local_validations + collect_inherited(:local_validations)
|
129
|
+
else
|
130
|
+
local_validations
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# find an attribute
|
135
|
+
def lookup_attribute(name)
|
136
|
+
# (look at singleton attributes first)
|
137
|
+
# fixme[this smells like a hack to me]
|
138
|
+
if @this.class == Class
|
139
|
+
class_attributes[name]
|
140
|
+
else
|
141
|
+
attributes[name]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# returns hash of conversions
|
146
|
+
# - if tf == true, returns all inherited conversions
|
147
|
+
# - if tf == false, returns only those conversions defined in the current object/class
|
148
|
+
def conversions(tf = true)
|
149
|
+
handle_inherited_hash(tf, :local_conversions)
|
150
|
+
end
|
151
|
+
|
152
|
+
# return hash of key => value pairs of initial values (where defined)
|
153
|
+
# - if tf == true, returns all inherited initial values
|
154
|
+
# - if tf == false, returns only those initial values defined in current object/class
|
155
|
+
def initial_values(tf = true)
|
156
|
+
attributes(tf).select{|n, a| a.init_defined? }.inject({}) {|hash, (n, a)|
|
157
|
+
#p [:initial_values, a.name]
|
158
|
+
hash[n] = case a.init
|
159
|
+
when NilClass, TrueClass, FalseClass, Fixnum, Float, Bignum, Symbol
|
160
|
+
# uncloneable values
|
161
|
+
#p [:initial_values, :special, a.name, a.init]
|
162
|
+
a.init
|
163
|
+
when DeferredBlock
|
164
|
+
#p [:initial_values, self, DeferredBlock, a.name]
|
165
|
+
begin
|
166
|
+
@this.instance_eval(&a.init.block)
|
167
|
+
rescue Object => e
|
168
|
+
#p [:exception_in_deferred_block, e]
|
169
|
+
raise
|
170
|
+
end
|
171
|
+
else
|
172
|
+
if a.init.kind_of?(Class)
|
173
|
+
#p [:initial_values, :class]
|
174
|
+
a.init.new
|
175
|
+
else
|
176
|
+
#p [:initial_values, :clone, a.name]
|
177
|
+
begin
|
178
|
+
a.init.clone
|
179
|
+
rescue Exception => e
|
180
|
+
warn "tried to clone #{a.init.class} in :init option (#{e})"
|
181
|
+
#p [:initial_values, :exception, a.name, e]
|
182
|
+
a.init
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
hash
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
# turn off validation, execute block, then set validation to same
|
191
|
+
# state as it was before +defer_validation+ was called - can be nested
|
192
|
+
def defer_validation(&block)
|
193
|
+
#p [:defer_validation, self.validation_on, @this]
|
194
|
+
old_validation = self.validation_on
|
195
|
+
self.validation_on = false
|
196
|
+
v = nil
|
197
|
+
begin
|
198
|
+
v = @this.instance_eval(&block)
|
199
|
+
ensure
|
200
|
+
self.validation_on = old_validation
|
201
|
+
end
|
202
|
+
@this.validate!(false)
|
203
|
+
v
|
204
|
+
end
|
205
|
+
|
206
|
+
# helper function to initialize from hash - this is safe to use
|
207
|
+
# after initialization (validate! is called if this method is
|
208
|
+
# called after initialization)
|
209
|
+
def update(*args, &block)
|
210
|
+
# p [:doodle_initialize_from_hash, :args, *args]
|
211
|
+
defer_validation do
|
212
|
+
# hash initializer
|
213
|
+
# separate into array of hashes of form [{:k1 => v1}, {:k2 => v2}] and positional args
|
214
|
+
key_values, args = args.partition{ |x| x.kind_of?(Hash)}
|
215
|
+
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args] }
|
216
|
+
#!p [self.class, :doodle_initialize_from_hash, :key_values, key_values, :args, args]
|
217
|
+
|
218
|
+
# set up initial values with ~clones~ of specified values (so not shared between instances)
|
219
|
+
#init_values = initial_values
|
220
|
+
#!p [:init_values, init_values]
|
221
|
+
|
222
|
+
# match up positional args with attribute names (from arg_order) using idiom to create hash from array of assocs
|
223
|
+
#arg_keywords = init_values.merge(Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))])
|
224
|
+
arg_keywords = Hash[*(Utils.flatten_first_level(self.class.arg_order[0...args.size].zip(args)))]
|
225
|
+
#!p [self.class, :doodle_initialize_from_hash, :arg_keywords, arg_keywords]
|
226
|
+
|
227
|
+
# merge all hash args into one
|
228
|
+
key_values = key_values.inject(arg_keywords) { |hash, item|
|
229
|
+
#!p [self.class, :doodle_initialize_from_hash, :merge, hash, item]
|
230
|
+
hash.merge(item)
|
231
|
+
}
|
232
|
+
#!p [self.class, :doodle_initialize_from_hash, :key_values2, key_values]
|
233
|
+
|
234
|
+
# convert keys to symbols (note not recursively - only first level == doodle keywords)
|
235
|
+
Doodle::Utils.symbolize_keys!(key_values)
|
236
|
+
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :key_values2, key_values, :args2, args] }
|
237
|
+
#!p [self.class, :doodle_initialize_from_hash, :key_values3, key_values]
|
238
|
+
|
239
|
+
# create attributes
|
240
|
+
key_values.keys.each do |key|
|
241
|
+
#DBG: Doodle::Debug.d { [self.class, :doodle_initialize_from_hash, :setting, key, key_values[key]] }
|
242
|
+
#p [self.class, :doodle_initialize_from_hash, :setting, key, key_values[key]]
|
243
|
+
#p [:update, :setting, key, key_values[key], __doodle__.validation_on]
|
244
|
+
if respond_to?(key)
|
245
|
+
__send__(key, key_values[key])
|
246
|
+
else
|
247
|
+
# raise error if not defined
|
248
|
+
__doodle__.handle_error key, Doodle::UnknownAttributeError, "unknown attribute '#{key}' => #{key_values[key].inspect} for #{self} #{doodle.attributes.map{ |k,v| k.inspect}.join(', ')}", Doodle::Utils.doodle_caller
|
249
|
+
end
|
250
|
+
end
|
251
|
+
# do init_values after user supplied values so init blocks can
|
252
|
+
# depend on user supplied values
|
253
|
+
# - don't reset values which are supplied in args
|
254
|
+
#p [:getting_init_values, instance_variables]
|
255
|
+
__doodle__.initial_values.each do |key, value|
|
256
|
+
if !key_values.key?(key) && respond_to?(key)
|
257
|
+
#p [:initial_values, key, value]
|
258
|
+
__send__(key, value)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
if block_given?
|
262
|
+
#p [:update, block, __doodle__.validation_on]
|
263
|
+
#p [:this, self]
|
264
|
+
instance_eval(&block)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
@this
|
268
|
+
end
|
269
|
+
|
270
|
+
# returns array of values (including defaults)
|
271
|
+
# - if tf == true, returns all inherited values (default)
|
272
|
+
# - if tf == false, returns only those values defined in current object
|
273
|
+
def values(tf = true)
|
274
|
+
attributes(tf).map{ |k, a| @this.send(k)}
|
275
|
+
end
|
276
|
+
|
277
|
+
# returns array of attribute names
|
278
|
+
# - if tf == true, returns all inherited attribute names (default)
|
279
|
+
# - if tf == false, returns only those attribute names defined in current object
|
280
|
+
def keys(tf = true)
|
281
|
+
attributes(tf).keys
|
282
|
+
end
|
283
|
+
|
284
|
+
# returns array of [key, value] pairs including default values
|
285
|
+
# - if tf == true, returns all inherited [key, value] pairs (default)
|
286
|
+
# - if tf == false, returns only those [key, value] pairs defined in current object
|
287
|
+
def key_values(tf = true)
|
288
|
+
keys(tf).zip(values(tf))
|
289
|
+
end
|
290
|
+
|
291
|
+
# returns array of [key, value] pairs excluding default values
|
292
|
+
# - if tf == true, returns all inherited [key, value] pairs (default)
|
293
|
+
# - if tf == false, returns only those [key, value] pairs defined in current object
|
294
|
+
def key_values_without_defaults(tf = true)
|
295
|
+
keys(tf).reject{|k| @this.default?(k) }.map{ |k, a| [k, @this.send(k)]}
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Doodle
|
2
|
+
# = inherit
|
3
|
+
# the intent of inherit is to provide a way to create directives
|
4
|
+
# that affect all members of a class 'family' without having to
|
5
|
+
# modify Module, Class or Object - in some ways, it's similar to Ara
|
6
|
+
# Howard's mixable[http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/197296]
|
7
|
+
# though not as tidy :S
|
8
|
+
#
|
9
|
+
# this works down to third level <tt>class << self</tt> - in practice, this is
|
10
|
+
# perfectly good - it would be great to have a completely general
|
11
|
+
# solution but I doubt whether the payoff is worth the effort
|
12
|
+
|
13
|
+
module Inherited
|
14
|
+
def self.included(other)
|
15
|
+
other.extend(Inherited)
|
16
|
+
other.send(:include, Factory)
|
17
|
+
end
|
18
|
+
|
19
|
+
# fake module inheritance chain
|
20
|
+
def inherit(other, &block)
|
21
|
+
# include in instance method chain
|
22
|
+
include other
|
23
|
+
include Inherited
|
24
|
+
|
25
|
+
sc = class << self; self; end
|
26
|
+
sc.module_eval {
|
27
|
+
# class method chain
|
28
|
+
include other
|
29
|
+
# singleton method chain
|
30
|
+
extend other
|
31
|
+
# ensure that subclasses also inherit this module
|
32
|
+
define_method :inherited do |klass|
|
33
|
+
#p [:inherit, :inherited, klass]
|
34
|
+
klass.__send__(:inherit, other) # n.b. closure
|
35
|
+
end
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/doodle/json.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
class Doodle
|
5
|
+
module JSON
|
6
|
+
module InstanceMethods
|
7
|
+
def to_json(*a)
|
8
|
+
# don't include default values
|
9
|
+
values = doodle.key_values_without_defaults
|
10
|
+
value_hash = Hash[*Doodle::Utils.flatten_first_level(values)]
|
11
|
+
{
|
12
|
+
'json_class' => self.class.name,
|
13
|
+
'data' => value_hash,
|
14
|
+
}.to_json(*a)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
module ClassMethods
|
18
|
+
def json_create(o)
|
19
|
+
#pp [:json_create, o]
|
20
|
+
const = Doodle::Utils.const_resolve(o['json_class'])
|
21
|
+
const.new(o['data'])
|
22
|
+
end
|
23
|
+
def from_json(src)
|
24
|
+
v = ::JSON::parse(src)
|
25
|
+
if v.kind_of?(Hash)
|
26
|
+
new(v)
|
27
|
+
else
|
28
|
+
v
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
def self.included(other)
|
33
|
+
other.module_eval { include InstanceMethods }
|
34
|
+
other.extend(ClassMethods)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
include Doodle::JSON
|
38
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Doodle
|
2
|
+
module ModMarshal
|
3
|
+
# helper for Marshal.dump
|
4
|
+
def marshal_dump
|
5
|
+
# note: perhaps should also dump singleton attribute definitions?
|
6
|
+
instance_variables.map{|x| [x, instance_variable_get(x)] }
|
7
|
+
end
|
8
|
+
# helper for Marshal.load
|
9
|
+
def marshal_load(data)
|
10
|
+
data.each do |name, value|
|
11
|
+
instance_variable_set(name, value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,512 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module ArraySentence
|
4
|
+
def join_with(sep = ', ', last = ' and ')
|
5
|
+
if self.size > 1
|
6
|
+
self[0..-2].join(sep) + last + self[-1].to_s
|
7
|
+
else
|
8
|
+
self[0].to_s
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module NormalizedArrayMethods
|
14
|
+
module InstanceMethods
|
15
|
+
include ArraySentence
|
16
|
+
|
17
|
+
# def &(other)
|
18
|
+
# super
|
19
|
+
# end
|
20
|
+
|
21
|
+
# def *(other)
|
22
|
+
# super
|
23
|
+
# end
|
24
|
+
|
25
|
+
# def +(other)
|
26
|
+
# super
|
27
|
+
# end
|
28
|
+
|
29
|
+
# def -(other)
|
30
|
+
# super
|
31
|
+
# end
|
32
|
+
|
33
|
+
def initialize(*args, &block)
|
34
|
+
#p [:init, 1, args, block]
|
35
|
+
if block_given?
|
36
|
+
#p [:init, 2]
|
37
|
+
original_block = block
|
38
|
+
block = proc { |index|
|
39
|
+
#p [:init, 3, index]
|
40
|
+
normalize_value(original_block[normalize_index(index)])
|
41
|
+
}
|
42
|
+
#p [:init, 4, block]
|
43
|
+
end
|
44
|
+
replace(super(*args, &block))
|
45
|
+
end
|
46
|
+
|
47
|
+
# def initialize(*args, &block)
|
48
|
+
# replace(super)
|
49
|
+
# end
|
50
|
+
|
51
|
+
def normalize_index(index)
|
52
|
+
index
|
53
|
+
end
|
54
|
+
|
55
|
+
def normalize_value(value)
|
56
|
+
p [self.class, :normalize_value, value]
|
57
|
+
value
|
58
|
+
end
|
59
|
+
|
60
|
+
# convenience method to check indices
|
61
|
+
def normalize_indices(*indices)
|
62
|
+
indices.map{ |index| normalize_index(index) }
|
63
|
+
end
|
64
|
+
|
65
|
+
# convenience method to check values
|
66
|
+
def normalize_values(*values)
|
67
|
+
if values.empty?
|
68
|
+
values = self
|
69
|
+
end
|
70
|
+
values.map{ |value| normalize_value(value) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def <<(value)
|
74
|
+
super(normalize_value(value))
|
75
|
+
end
|
76
|
+
|
77
|
+
# def <=>(other)
|
78
|
+
# super(normalize_values(*other))
|
79
|
+
# end
|
80
|
+
|
81
|
+
# def ==(other)
|
82
|
+
# super(normalize_values(*other))
|
83
|
+
# end
|
84
|
+
|
85
|
+
def [](index)
|
86
|
+
super(normalize_index(index))
|
87
|
+
end
|
88
|
+
|
89
|
+
def []=(index, value)
|
90
|
+
super(normalize_index(index), normalize_value(value))
|
91
|
+
end
|
92
|
+
|
93
|
+
# def assoc(key)
|
94
|
+
# super
|
95
|
+
# end
|
96
|
+
|
97
|
+
def at(index)
|
98
|
+
super(normalize_index(index))
|
99
|
+
end
|
100
|
+
|
101
|
+
# def clear()
|
102
|
+
# super
|
103
|
+
# end
|
104
|
+
|
105
|
+
# def collect(&block)
|
106
|
+
# #FIXME
|
107
|
+
# end
|
108
|
+
|
109
|
+
def collect!(&block)
|
110
|
+
super() {|x| normalize_value(block.call(x))}
|
111
|
+
end
|
112
|
+
|
113
|
+
# def compact()
|
114
|
+
# super
|
115
|
+
# end
|
116
|
+
|
117
|
+
# def compact!()
|
118
|
+
# super
|
119
|
+
# end
|
120
|
+
|
121
|
+
def concat(other)
|
122
|
+
super(normalize_values(*other))
|
123
|
+
end
|
124
|
+
|
125
|
+
def delete(value)
|
126
|
+
super(normalize_value(value))
|
127
|
+
end
|
128
|
+
|
129
|
+
def delete_at(index)
|
130
|
+
super(normalize_index(index))
|
131
|
+
end
|
132
|
+
|
133
|
+
# def delete_if(&block)
|
134
|
+
# super
|
135
|
+
# end
|
136
|
+
|
137
|
+
# def each(&block)
|
138
|
+
# super
|
139
|
+
# end
|
140
|
+
|
141
|
+
# def each_index(&block)
|
142
|
+
# super
|
143
|
+
# end
|
144
|
+
|
145
|
+
# def empty?()
|
146
|
+
# super
|
147
|
+
# end
|
148
|
+
|
149
|
+
# def eql?(other)
|
150
|
+
# super(normalize_values(*other))
|
151
|
+
# end
|
152
|
+
|
153
|
+
def fetch(index)
|
154
|
+
super(normalize_index(index))
|
155
|
+
end
|
156
|
+
|
157
|
+
# array.fill(obj) -> array
|
158
|
+
# array.fill(obj, start [, length]) -> array
|
159
|
+
# array.fill(obj, range ) -> array
|
160
|
+
# array.fill {|index| block } -> array
|
161
|
+
# array.fill(start [, length] ) {|index| block } -> array
|
162
|
+
# array.fill(range) {|index| block } -> array
|
163
|
+
def fill(*args, &block)
|
164
|
+
# TODO: check value
|
165
|
+
# check start, length
|
166
|
+
replace(super)
|
167
|
+
end
|
168
|
+
|
169
|
+
def first(count = 1)
|
170
|
+
# TODO: check count
|
171
|
+
super
|
172
|
+
end
|
173
|
+
|
174
|
+
# def flatten()
|
175
|
+
# super
|
176
|
+
# end
|
177
|
+
|
178
|
+
# def flatten!()
|
179
|
+
# super
|
180
|
+
# end
|
181
|
+
|
182
|
+
# def frozen?()
|
183
|
+
# super
|
184
|
+
# end
|
185
|
+
|
186
|
+
# def hash()
|
187
|
+
# super
|
188
|
+
# end
|
189
|
+
|
190
|
+
def include?(value)
|
191
|
+
super(normalize_value(value))
|
192
|
+
end
|
193
|
+
|
194
|
+
def index(value)
|
195
|
+
super(normalize_value(value))
|
196
|
+
end
|
197
|
+
|
198
|
+
def insert(index, *values)
|
199
|
+
super(normalize_index(index), normalize_values(*values))
|
200
|
+
end
|
201
|
+
|
202
|
+
# def inspect()
|
203
|
+
# super
|
204
|
+
# end
|
205
|
+
|
206
|
+
# def join(sep = $,)
|
207
|
+
# super
|
208
|
+
# end
|
209
|
+
|
210
|
+
def last(count = 1)
|
211
|
+
# TODO: check count
|
212
|
+
super
|
213
|
+
end
|
214
|
+
|
215
|
+
# def length()
|
216
|
+
# super
|
217
|
+
# end
|
218
|
+
|
219
|
+
# def map(&block)
|
220
|
+
# super
|
221
|
+
# end
|
222
|
+
|
223
|
+
def map!(&block)
|
224
|
+
collect!(&block)
|
225
|
+
end
|
226
|
+
|
227
|
+
# def nitems()
|
228
|
+
# super
|
229
|
+
# end
|
230
|
+
|
231
|
+
# def pack()
|
232
|
+
# super
|
233
|
+
# end
|
234
|
+
|
235
|
+
# def pop()
|
236
|
+
# super
|
237
|
+
# end
|
238
|
+
|
239
|
+
def push(*values)
|
240
|
+
super(normalize_values(*values))
|
241
|
+
end
|
242
|
+
|
243
|
+
# def rassoc(key)
|
244
|
+
# super
|
245
|
+
# end
|
246
|
+
|
247
|
+
# def reject(&block)
|
248
|
+
# super
|
249
|
+
# end
|
250
|
+
|
251
|
+
def reject!(&block)
|
252
|
+
super() {|x| normalize_value(block.call(x))}
|
253
|
+
end
|
254
|
+
|
255
|
+
def replace(other)
|
256
|
+
#p [:replace, other]
|
257
|
+
super(other.to_enum(:each_with_index).map{ |v, i|
|
258
|
+
#p [:replace_norm, i, v]
|
259
|
+
normalize_index(i);
|
260
|
+
normalize_value(v)
|
261
|
+
})
|
262
|
+
end
|
263
|
+
|
264
|
+
# def reverse()
|
265
|
+
# super
|
266
|
+
# end
|
267
|
+
|
268
|
+
# def reverse!()
|
269
|
+
# super
|
270
|
+
# end
|
271
|
+
|
272
|
+
# def reverse_each(&block)
|
273
|
+
# super
|
274
|
+
# end
|
275
|
+
|
276
|
+
def rindex(value)
|
277
|
+
super(normalize_value(value))
|
278
|
+
end
|
279
|
+
|
280
|
+
# def select(&block)
|
281
|
+
# super
|
282
|
+
# end
|
283
|
+
|
284
|
+
# def shift()
|
285
|
+
# super
|
286
|
+
# end
|
287
|
+
|
288
|
+
# alias :size :length
|
289
|
+
|
290
|
+
# array[index] -> obj or nil
|
291
|
+
# array[start, length] -> an_array or nil
|
292
|
+
# array[range] -> an_array or nil
|
293
|
+
# array.slice(index) -> obj or nil
|
294
|
+
# array.slice(start, length) -> an_array or nil
|
295
|
+
# array.slice(range) -> an_array or nil
|
296
|
+
def slice(*args)
|
297
|
+
# TODO: check indices
|
298
|
+
super
|
299
|
+
end
|
300
|
+
|
301
|
+
def slice!(*args)
|
302
|
+
# TODO: check indices
|
303
|
+
super
|
304
|
+
end
|
305
|
+
|
306
|
+
# def sort(&block)
|
307
|
+
# super
|
308
|
+
# end
|
309
|
+
|
310
|
+
# def sort!(&block)
|
311
|
+
# super
|
312
|
+
# end
|
313
|
+
|
314
|
+
# def to_a()
|
315
|
+
# super
|
316
|
+
# end
|
317
|
+
|
318
|
+
# def to_ary()
|
319
|
+
# super
|
320
|
+
# end
|
321
|
+
|
322
|
+
# def to_s()
|
323
|
+
# super
|
324
|
+
# end
|
325
|
+
|
326
|
+
# def transpose()
|
327
|
+
# super
|
328
|
+
# end
|
329
|
+
|
330
|
+
# def uniq()
|
331
|
+
# super
|
332
|
+
# end
|
333
|
+
|
334
|
+
# def uniq!()
|
335
|
+
# super
|
336
|
+
# end
|
337
|
+
|
338
|
+
def unshift(*values)
|
339
|
+
super(normalize_values(*values))
|
340
|
+
end
|
341
|
+
|
342
|
+
def values_at(*indices)
|
343
|
+
super(normalize_indices(*indices))
|
344
|
+
end
|
345
|
+
# deprecated - use values_at
|
346
|
+
alias :indexes :values_at
|
347
|
+
alias :indices :values_at
|
348
|
+
|
349
|
+
# should this be normalized?
|
350
|
+
def zip(other)
|
351
|
+
super(normalize_values(*other))
|
352
|
+
end
|
353
|
+
|
354
|
+
# def |(other)
|
355
|
+
# super(other.map{ |x| normalize_value(x)})
|
356
|
+
# end
|
357
|
+
|
358
|
+
end
|
359
|
+
|
360
|
+
module ClassMethods
|
361
|
+
def [](*args)
|
362
|
+
super.normalize_values
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
class NormalizedArray < Array
|
368
|
+
include NormalizedArrayMethods::InstanceMethods
|
369
|
+
extend NormalizedArrayMethods::ClassMethods
|
370
|
+
end
|
371
|
+
|
372
|
+
if __FILE__ == $0
|
373
|
+
require 'rubygems'
|
374
|
+
require 'assertion'
|
375
|
+
|
376
|
+
class StringArray < NormalizedArray
|
377
|
+
def normalize_value(v)
|
378
|
+
p [self.class, :normalize_value, v]
|
379
|
+
v.to_s
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
def BoundedArray(upper_bound)
|
384
|
+
typed_class = Class.new(NormalizedArray) do
|
385
|
+
define_method :normalize_index do |index|
|
386
|
+
raise IndexError, "index #{index} out of range" if !(0..upper_bound).include?(index)
|
387
|
+
index
|
388
|
+
end
|
389
|
+
end
|
390
|
+
typed_class
|
391
|
+
end
|
392
|
+
|
393
|
+
def TypedArray(*klasses)
|
394
|
+
typed_class = Class.new(NormalizedArray) do
|
395
|
+
define_method :normalize_value do |v|
|
396
|
+
if !klasses.any?{ |klass| v.kind_of?(klass) }
|
397
|
+
raise TypeError, "#{self.class}: #{v.class}(#{v.inspect}) is not a kind of #{klasses.map{ |c| c.to_s }.join(', ')}", [caller[-1]]
|
398
|
+
end
|
399
|
+
v
|
400
|
+
end
|
401
|
+
end
|
402
|
+
typed_class
|
403
|
+
end
|
404
|
+
TypedStringArray = TypedArray(String)
|
405
|
+
|
406
|
+
# na = Array.new(4) { |i|
|
407
|
+
# p [:in_block, i]
|
408
|
+
# 42
|
409
|
+
# }
|
410
|
+
# p na[1]
|
411
|
+
# p na[1] == 42
|
412
|
+
|
413
|
+
sa = StringArray.new(3) { |i|
|
414
|
+
p [:in_block, i]
|
415
|
+
42
|
416
|
+
}
|
417
|
+
assert { sa[1] == "42" }
|
418
|
+
assert_error { sa[1] == 42 }
|
419
|
+
assert_error { sa.values == ["42"] * 3 }
|
420
|
+
assert_error { sa.values == [42] }
|
421
|
+
|
422
|
+
sa = nil
|
423
|
+
assert_ok { sa = StringArray.new([1,2,3]) }
|
424
|
+
assert { sa == ["1", "2", "3"] }
|
425
|
+
assert { (sa | ["4", "5", "6"]) == ["1", "2", "3", "4", "5", "6"] }
|
426
|
+
assert { (sa | [4, 5, 6]) == ["1", "2", "3", 4, 5, 6] }
|
427
|
+
assert { (sa | [4, 5, 6]).class == Array }
|
428
|
+
|
429
|
+
# equality
|
430
|
+
assert { sa == ["1", "2", "3"] }
|
431
|
+
|
432
|
+
## -FIXME: not sure about this coercion-
|
433
|
+
assert_error { sa.eql?( [1, 2, 3] )}
|
434
|
+
|
435
|
+
## -FIXME: equality should not be non-commutative-
|
436
|
+
assert_error { sa == [1, 2, 3] }
|
437
|
+
assert_error { [1, 2, 3] == sa }
|
438
|
+
|
439
|
+
BoundedArray4 = BoundedArray(4)
|
440
|
+
ca = BoundedArray4.new([1,2,3])
|
441
|
+
assert { ca == [1, 2, 3] }
|
442
|
+
assert { ca[4] = 42 }
|
443
|
+
expect_error(IndexError) { ca[5] = 42 }
|
444
|
+
expect_error('out of range') { ca[5] = 42 }
|
445
|
+
expect_error { ca[5] = 42 }
|
446
|
+
#expect_ok { false }
|
447
|
+
#expect_error { true }
|
448
|
+
#assert_ok { true }
|
449
|
+
|
450
|
+
assert_ok { ca = BoundedArray4.new([1,2,3,4,5]) }
|
451
|
+
assert_error { ca = BoundedArray4.new([1,2,3,4,5,6]) }
|
452
|
+
|
453
|
+
assert_error { sa = TypedStringArray.new([1,2,3]) }
|
454
|
+
assert_ok { sa = TypedStringArray.new(["1","2","3"]) }
|
455
|
+
|
456
|
+
# ca = BoundedArray.new([1,2,3,4,5])
|
457
|
+
# pp ca
|
458
|
+
|
459
|
+
TypedIntegerArray = TypedArray(Integer)
|
460
|
+
ia = TypedIntegerArray.new([1,2,3])
|
461
|
+
expect_error { ia[1] = "hello" }
|
462
|
+
|
463
|
+
StringOrIntegerArray = TypedArray(Integer, String)
|
464
|
+
ma = nil
|
465
|
+
expect_ok { ma = StringOrIntegerArray.new([1,"2",3]) }
|
466
|
+
expect_ok { ma[0] = "hello" }
|
467
|
+
expect_error { ma[1] = Date.new }
|
468
|
+
|
469
|
+
assert {
|
470
|
+
ma = [].extend(ArraySentence)
|
471
|
+
ma.join_with(', ', ' and ') == ""
|
472
|
+
}
|
473
|
+
|
474
|
+
assert {
|
475
|
+
ma = [nil].extend(ArraySentence)
|
476
|
+
ma.join_with(', ', ' and ') == ""
|
477
|
+
}
|
478
|
+
|
479
|
+
assert {
|
480
|
+
ma = [nil, nil].extend(ArraySentence)
|
481
|
+
ma.join_with(', ', ' and ') == " and "
|
482
|
+
}
|
483
|
+
|
484
|
+
assert {
|
485
|
+
ma = [nil, nil, nil].extend(ArraySentence)
|
486
|
+
ma.join_with(', ', ' and ') == ", and "
|
487
|
+
}
|
488
|
+
|
489
|
+
assert {
|
490
|
+
ma = [1].extend(ArraySentence)
|
491
|
+
ma.join_with(', ', ' and ') == "1"
|
492
|
+
}
|
493
|
+
|
494
|
+
assert {
|
495
|
+
ma = [1, 2].extend(ArraySentence)
|
496
|
+
ma.join_with(', ', ' and ') == "1 and 2"
|
497
|
+
}
|
498
|
+
|
499
|
+
assert {
|
500
|
+
ma = [1, 2, 3].extend(ArraySentence)
|
501
|
+
ma.join_with(', ', ' or ') == "1, 2 or 3"
|
502
|
+
}
|
503
|
+
|
504
|
+
assert {
|
505
|
+
ma = [1, 2, 3].extend(ArraySentence)
|
506
|
+
ma.join_with(', ', ' and ') == "1, 2 and 3"
|
507
|
+
}
|
508
|
+
|
509
|
+
expect_error("Fixnum(1) is not a kind of String") { TypedStringArray[1,2,3] }
|
510
|
+
expect_error(TypeError) { TypedStringArray[1,2,3] }
|
511
|
+
|
512
|
+
end
|