doodle 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. data/History.txt +24 -0
  2. data/Manifest.txt +26 -1
  3. data/README.txt +9 -8
  4. data/lib/doodle.rb +43 -1496
  5. data/lib/doodle/app.rb +6 -0
  6. data/lib/doodle/attribute.rb +165 -0
  7. data/lib/doodle/base.rb +180 -0
  8. data/lib/doodle/collector-1.9.rb +72 -0
  9. data/lib/doodle/collector.rb +191 -0
  10. data/lib/doodle/comparable.rb +8 -0
  11. data/lib/doodle/conversion.rb +80 -0
  12. data/lib/doodle/core.rb +42 -0
  13. data/lib/doodle/datatype-holder.rb +39 -0
  14. data/lib/doodle/debug.rb +20 -0
  15. data/lib/doodle/deferred.rb +13 -0
  16. data/lib/doodle/equality.rb +21 -0
  17. data/lib/doodle/exceptions.rb +29 -0
  18. data/lib/doodle/factory.rb +91 -0
  19. data/lib/doodle/getter-setter.rb +154 -0
  20. data/lib/doodle/info.rb +298 -0
  21. data/lib/doodle/inherit.rb +40 -0
  22. data/lib/doodle/json.rb +38 -0
  23. data/lib/doodle/marshal.rb +16 -0
  24. data/lib/doodle/normalized_array.rb +512 -0
  25. data/lib/doodle/normalized_hash.rb +356 -0
  26. data/lib/doodle/ordered-hash.rb +8 -0
  27. data/lib/doodle/singleton.rb +23 -0
  28. data/lib/doodle/smoke-and-mirrors.rb +23 -0
  29. data/lib/doodle/to_hash.rb +17 -0
  30. data/lib/doodle/utils.rb +173 -11
  31. data/lib/doodle/validation.rb +122 -0
  32. data/lib/doodle/version.rb +1 -1
  33. data/lib/molic_orderedhash.rb +24 -10
  34. data/spec/assigned_spec.rb +45 -0
  35. data/spec/attributes_spec.rb +7 -7
  36. data/spec/collector_spec.rb +100 -13
  37. data/spec/doodle_context_spec.rb +5 -5
  38. data/spec/from_spec.rb +43 -3
  39. data/spec/json_spec.rb +232 -0
  40. data/spec/member_init_spec.rb +11 -11
  41. data/spec/modules_spec.rb +4 -4
  42. data/spec/multi_collector_spec.rb +91 -0
  43. data/spec/must_spec.rb +32 -0
  44. data/spec/spec_helper.rb +14 -4
  45. data/spec/specialized_attribute_class_spec.rb +2 -2
  46. data/spec/typed_collector_spec.rb +57 -0
  47. data/spec/xml_spec.rb +8 -8
  48. metadata +33 -3
@@ -0,0 +1,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
@@ -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