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