gorillib-model 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +12 -0
  3. data/README.md +21 -0
  4. data/Rakefile +15 -0
  5. data/gorillib-model.gemspec +27 -0
  6. data/lib/gorillib/builder.rb +239 -0
  7. data/lib/gorillib/core_ext/datetime.rb +23 -0
  8. data/lib/gorillib/core_ext/exception.rb +153 -0
  9. data/lib/gorillib/core_ext/module.rb +10 -0
  10. data/lib/gorillib/core_ext/object.rb +14 -0
  11. data/lib/gorillib/model/base.rb +273 -0
  12. data/lib/gorillib/model/collection/model_collection.rb +157 -0
  13. data/lib/gorillib/model/collection.rb +200 -0
  14. data/lib/gorillib/model/defaults.rb +115 -0
  15. data/lib/gorillib/model/errors.rb +24 -0
  16. data/lib/gorillib/model/factories.rb +555 -0
  17. data/lib/gorillib/model/field.rb +168 -0
  18. data/lib/gorillib/model/lint.rb +24 -0
  19. data/lib/gorillib/model/named_schema.rb +53 -0
  20. data/lib/gorillib/model/positional_fields.rb +35 -0
  21. data/lib/gorillib/model/schema_magic.rb +163 -0
  22. data/lib/gorillib/model/serialization/csv.rb +60 -0
  23. data/lib/gorillib/model/serialization/json.rb +44 -0
  24. data/lib/gorillib/model/serialization/lines.rb +30 -0
  25. data/lib/gorillib/model/serialization/to_wire.rb +54 -0
  26. data/lib/gorillib/model/serialization/tsv.rb +53 -0
  27. data/lib/gorillib/model/serialization.rb +41 -0
  28. data/lib/gorillib/model/type/extended.rb +83 -0
  29. data/lib/gorillib/model/type/ip_address.rb +153 -0
  30. data/lib/gorillib/model/type/url.rb +11 -0
  31. data/lib/gorillib/model/validate.rb +22 -0
  32. data/lib/gorillib/model/version.rb +5 -0
  33. data/lib/gorillib/model.rb +34 -0
  34. data/spec/builder_spec.rb +193 -0
  35. data/spec/core_ext/datetime_spec.rb +41 -0
  36. data/spec/core_ext/exception.rb +98 -0
  37. data/spec/core_ext/object.rb +45 -0
  38. data/spec/model/collection_spec.rb +290 -0
  39. data/spec/model/defaults_spec.rb +104 -0
  40. data/spec/model/factories_spec.rb +323 -0
  41. data/spec/model/lint_spec.rb +28 -0
  42. data/spec/model/serialization/csv_spec.rb +30 -0
  43. data/spec/model/serialization/tsv_spec.rb +28 -0
  44. data/spec/model/serialization_spec.rb +41 -0
  45. data/spec/model/type/extended_spec.rb +166 -0
  46. data/spec/model/type/ip_address_spec.rb +141 -0
  47. data/spec/model_spec.rb +261 -0
  48. data/spec/spec_helper.rb +15 -0
  49. data/spec/support/capture_output.rb +28 -0
  50. data/spec/support/nuke_constants.rb +9 -0
  51. data/spec/support/shared_context_for_builders.rb +59 -0
  52. data/spec/support/shared_context_for_models.rb +55 -0
  53. data/spec/support/shared_examples_for_factories.rb +71 -0
  54. data/spec/support/shared_examples_for_model_fields.rb +62 -0
  55. data/spec/support/shared_examples_for_models.rb +87 -0
  56. metadata +193 -0
@@ -0,0 +1,273 @@
1
+ module Gorillib
2
+
3
+ # Provides a set of class methods for defining a field schema and instance
4
+ # methods for reading and writing attributes.
5
+ #
6
+ # @example Usage
7
+ # class Person
8
+ # include Gorillib::Model
9
+ #
10
+ # field :name, String, :doc => 'Full name of person'
11
+ # field :height, Float, :doc => 'Height in meters'
12
+ # end
13
+ #
14
+ # person = Person.new
15
+ # person.name = "Bob Dobbs, Jr"
16
+ # puts person #=> #<Person name="Bob Dobbs, Jr">
17
+ #
18
+ module Model
19
+ extend ActiveSupport::Concern
20
+
21
+ def initialize(*args, &block)
22
+ attrs = self.class.attrs_hash_from_args(args)
23
+ receive!(attrs, &block)
24
+ end
25
+
26
+ # Returns a Hash of all attributes
27
+ #
28
+ # @example Get attributes
29
+ # person.attributes # => { :name => "Emmet Brown", :title => "Dr" }
30
+ #
31
+ # @return [{Symbol => Object}] The Hash of all attributes
32
+ def attributes
33
+ self.class.field_names.inject(Hash.new) do |hsh, fn|
34
+ # hsh[fn] = attribute_set?(fn) ? read_attribute(fn) : nil
35
+ hsh[fn] = read_attribute(fn)
36
+ hsh
37
+ end
38
+ end
39
+
40
+ # @return [Array[Object]] all the attributes, in field order, with `nil` where unset
41
+ def attribute_values
42
+ self.class.field_names.map{|fn| read_attribute(fn) }
43
+ end
44
+
45
+ # Returns a Hash of all attributes *that have been set*
46
+ #
47
+ # @example Get attributes (smurfette is unarmed)
48
+ # smurfette.attributes # => { :name => "Smurfette", :weapon => nil }
49
+ # smurfette.compact_attributes # => { :name => "Smurfette" }
50
+ #
51
+ # @return [{Symbol => Object}] The Hash of all *set* attributes
52
+ def compact_attributes
53
+ self.class.field_names.inject(Hash.new) do |hsh, fn|
54
+ hsh[fn] = read_attribute(fn) if attribute_set?(fn)
55
+ hsh
56
+ end
57
+ end
58
+
59
+ #
60
+ # Accept the given attributes, converting each value to the appropriate
61
+ # type, constructing included models and collections, and other triggers as
62
+ # defined.
63
+ #
64
+ # Use `#receive!` to accept 'dirty' data -- from JSON, from a nested hash,
65
+ # or some such. Use `#update_attributes` if your data is already type safe.
66
+ #
67
+ # @param [{Symbol => Object}] hsh The values to receive
68
+ # @return [nil] nothing
69
+ def receive!(hsh={})
70
+ if hsh.respond_to?(:attributes)
71
+ hsh = hsh.compact_attributes
72
+ else
73
+ Gorillib::Model::Validate.hashlike!(hsh){ "attributes for #{self.inspect}" }
74
+ hsh = hsh.dup
75
+ end
76
+ self.class.field_names.each do |field_name|
77
+ if hsh.has_key?(field_name) then val = hsh.delete(field_name)
78
+ elsif hsh.has_key?(field_name.to_s) then val = hsh.delete(field_name.to_s)
79
+ else next ; end
80
+ self.send("receive_#{field_name}", val)
81
+ end
82
+ handle_extra_attributes(hsh)
83
+ nil
84
+ end
85
+
86
+ def handle_extra_attributes(attrs)
87
+ @_extra_attributes ||= Hash.new
88
+ @_extra_attributes.merge!(attrs)
89
+ end
90
+
91
+ #
92
+ # Accept the given attributes, adopting each value directly.
93
+ #
94
+ # Use `#receive!` to accept 'dirty' data -- from JSON, from a nested hash,
95
+ # or some such. Use `#update_attributes` if your data is already type safe.
96
+ #
97
+ # @param [{Symbol => Object}] hsh The values to update with
98
+ # @return [Gorillib::Model] the object itself
99
+ def update_attributes(hsh)
100
+ if hsh.respond_to?(:attributes) then hsh = hsh.attributes ; end
101
+ Gorillib::Model::Validate.hashlike!(hsh){ "attributes for #{self.inspect}" }
102
+ self.class.field_names.each do |field_name|
103
+ if hsh.has_key?(field_name) then val = hsh[field_name]
104
+ elsif hsh.has_key?(field_name.to_s) then val = hsh[field_name.to_s]
105
+ else next ; end
106
+ write_attribute(field_name, val)
107
+ end
108
+ self
109
+ end
110
+
111
+ # Read a value from the model's attributes.
112
+ #
113
+ # @example Reading an attribute
114
+ # person.read_attribute(:name)
115
+ #
116
+ # @param [String, Symbol, #to_s] field_name Name of the attribute to get.
117
+ #
118
+ # @raise [UnknownAttributeError] if the attribute is unknown
119
+ # @return [Object] The value of the attribute, or nil if it is unset
120
+ def read_attribute(field_name)
121
+ attr_name = "@#{field_name}"
122
+ if instance_variable_defined?(attr_name)
123
+ instance_variable_get(attr_name)
124
+ else
125
+ read_unset_attribute(field_name)
126
+ end
127
+ end
128
+
129
+ # Write the value of a single attribute.
130
+ #
131
+ # @example Writing an attribute
132
+ # person.write_attribute(:name, "Benjamin")
133
+ #
134
+ # @param [String, Symbol, #to_s] field_name Name of the attribute to update.
135
+ # @param [Object] val The value to set for the attribute.
136
+ #
137
+ # @raise [UnknownAttributeError] if the attribute is unknown
138
+ # @return [Object] the attribute's value
139
+ def write_attribute(field_name, val)
140
+ instance_variable_set("@#{field_name}", val)
141
+ end
142
+
143
+ # Unset an attribute. Subsequent reads of the attribute will return `nil`,
144
+ # and `attribute_set?` for that field will return false.
145
+ #
146
+ # @example Unsetting an attribute
147
+ # obj.write_attribute(:foo, nil)
148
+ # [ obj.read_attribute(:foo), obj.attribute_set?(:foo) ] # => [ nil, true ]
149
+ # person.unset_attribute(:height)
150
+ # [ obj.read_attribute(:foo), obj.attribute_set?(:foo) ] # => [ nil, false ]
151
+ #
152
+ # @param [String, Symbol, #to_s] field_name Name of the attribute to unset.
153
+ #
154
+ # @raise [UnknownAttributeError] if the attribute is unknown
155
+ # @return [Object] the former value if it was set, nil if it was unset
156
+ def unset_attribute(field_name)
157
+ if instance_variable_defined?("@#{field_name}")
158
+ val = instance_variable_get("@#{field_name}")
159
+ remove_instance_variable("@#{field_name}")
160
+ return val
161
+ else
162
+ return nil
163
+ end
164
+ end
165
+
166
+ # True if the attribute is set.
167
+ #
168
+ # Note that an attribute can have the value nil but be set.
169
+ #
170
+ # @param [String, Symbol, #to_s] field_name Name of the attribute to check.
171
+ #
172
+ # @raise [UnknownAttributeError] if the attribute is unknown
173
+ # @return [true, false]
174
+ def attribute_set?(field_name)
175
+ instance_variable_defined?("@#{field_name}")
176
+ end
177
+
178
+ # Two models are equal if they have the same class and their attributes
179
+ # are equal.
180
+ #
181
+ # @example Compare for equality.
182
+ # model == other
183
+ #
184
+ # @param [Gorillib::Model, Object] other The other model to compare
185
+ #
186
+ # @return [true, false] True if attributes are equal and other is instance of the same Class
187
+ def ==(other)
188
+ return false unless other.instance_of?(self.class)
189
+ attributes == other.attributes
190
+ end
191
+
192
+ # override to_inspectable (not this) in your descendant class
193
+ # @return [String] Human-readable presentation of the attributes
194
+ def inspect
195
+ str = '#<' << self.class.name.to_s
196
+ attrs = to_inspectable
197
+ if attrs.present?
198
+ str << '(' << attrs.map{|attr, val| "#{attr}=#{val.respond_to?(:inspect_compact) ? val.inspect_compact : val.inspect}" }.join(", ") << ')'
199
+ end
200
+ str << '>'
201
+ end
202
+
203
+ def to_s
204
+ inspect
205
+ end
206
+
207
+ def inspect_compact
208
+ str = "#<#{self.class.name.to_s}>"
209
+ end
210
+
211
+ # assembles just the given attributes into the inspect string.
212
+ # @return [String] Human-readable presentation of the attributes
213
+ def to_inspectable
214
+ compact_attributes
215
+ end
216
+
217
+ protected
218
+
219
+ module ClassMethods
220
+
221
+ #
222
+ # A readable handle for this field
223
+ #
224
+ def typename
225
+ @typename ||= ActiveSupport::Inflector.underscore(self.name||'anon').gsub(%r{/}, '.')
226
+ end
227
+
228
+ #
229
+ # Receive external data, type-converting and creating contained models as necessary
230
+ #
231
+ # @return [Gorillib::Model] the new object
232
+ def receive(attrs={}, &block)
233
+ return nil if attrs.nil?
234
+ return attrs if native?(attrs)
235
+ #
236
+ Gorillib::Model::Validate.hashlike!(attrs){ "attributes for #{self.inspect}" }
237
+ klass = attrs.has_key?(:_type) ? Gorillib::Factory(attrs[:_type]) : self
238
+ warn "factory #{klass} is not a type of #{self} as specified in #{attrs}" unless klass <= self
239
+ #
240
+ klass.new(attrs, &block)
241
+ end
242
+
243
+ # A `native` object does not need any transformation; it is accepted directly.
244
+ # By default, an object is native if it `is_a?` this class
245
+ #
246
+ # @param obj [Object] the object that will be received
247
+ # @return [true, false] true if the item does not need conversion
248
+ def native?(obj)
249
+ obj.is_a?(self)
250
+ end
251
+
252
+ # @return Class name and its attributes
253
+ #
254
+ # @example Inspect the model's definition.
255
+ # Person.inspect #=> Person[first_name, last_name]
256
+ def inspect
257
+ "#{self.name || 'anon'}[#{ field_names.join(",") }]"
258
+ end
259
+ def inspect_compact() self.name || inspect ; end
260
+
261
+ end
262
+
263
+ self.included do |base|
264
+ base.instance_eval do
265
+ extend Gorillib::Model::NamedSchema
266
+ extend Gorillib::Model::ClassMethods
267
+ self.meta_module
268
+ @_own_fields ||= {}
269
+ end
270
+ end
271
+
272
+ end
273
+ end
@@ -0,0 +1,157 @@
1
+ module Gorillib
2
+
3
+ #
4
+ # A collection of Models
5
+ #
6
+ # ### Item Type
7
+ #
8
+ # `item_type` is a class attribute -- you can make a "collection of Foo's" by
9
+ # subclassing ModelCollection and set the item item_type at the class level:
10
+ #
11
+ # class ClusterCollection < ModelCollection
12
+ # self.item_type = Cluster
13
+ # end
14
+ #
15
+ #
16
+ #
17
+ # A model collection serializes as an array does, but indexes labelled objects
18
+ # as a hash does.
19
+ #
20
+ #
21
+ class ModelCollection < Gorillib::Collection
22
+ # [Class, #receive] Factory for generating a new collection item.
23
+ class_attribute :item_type, :instance_writer => false
24
+ singleton_class.send(:protected, :item_type=)
25
+
26
+ def initialize(options={})
27
+ @item_type = Gorillib::Factory(options[:item_type]) if options[:item_type]
28
+ super
29
+ end
30
+
31
+ def receive_item(label, *args, &block)
32
+ item = item_type.receive(*args, &block)
33
+ super(label, item)
34
+ rescue StandardError => err ; err.polish("#{item_type} #{label} as #{args.inspect} to #{self}") rescue nil ; raise
35
+ end
36
+
37
+ def update_or_add(label, attrs, &block)
38
+ if label && include?(label)
39
+ item = fetch(label)
40
+ item.receive!(attrs, &block)
41
+ item
42
+ else
43
+ attrs = attrs.attributes if attrs.is_a? Gorillib::Model
44
+ attrs = attrs.merge(key_method => label) if key_method && label
45
+ receive_item(label, attrs, &block)
46
+ end
47
+ rescue StandardError => err ; err.polish("#{item_type} #{label} as #{attrs} to #{self}") rescue nil ; raise
48
+ end
49
+
50
+ # @return [Array] serializable array representation of the collection
51
+ def to_wire(options={})
52
+ to_a.map{|el| el.respond_to?(:to_wire) ? el.to_wire(options) : el }
53
+ end
54
+ # same as #to_wire
55
+ def as_json(*args) to_wire(*args) ; end
56
+ # @return [String] JSON serialization of the collection's array representation
57
+ def to_json(*args) to_wire(*args).to_json(*args) ; end
58
+ end
59
+
60
+ class Collection
61
+
62
+ #
63
+ #
64
+ # class ClusterCollection < ModelCollection
65
+ # self.item_type = Cluster
66
+ # end
67
+ # class Organization
68
+ # field :clusters, ClusterCollection, default: ->{ ClusterCollection.new(common_attrs: { organization: self }) }
69
+ # end
70
+ #
71
+ module CommonAttrs
72
+ extend ActiveSupport::Concern
73
+
74
+ included do
75
+ # [Class, #receive] Attributes to mix in to each added item
76
+ class_attribute :common_attrs, :instance_writer => false
77
+ singleton_class.send(:protected, :common_attrs=)
78
+ self.common_attrs = Hash.new
79
+ end
80
+
81
+ def initialize(options={})
82
+ super
83
+ @common_attrs = self.common_attrs.merge(options[:common_attrs]) if options.include?(:common_attrs)
84
+ end
85
+
86
+ #
87
+ # * a factory-native object: item is updated with common_attrs, then added
88
+ # * raw materials for the object: item is constructed (from the merged attrs and common_attrs), then added
89
+ #
90
+ def receive_item(label, *args, &block)
91
+ attrs = args.extract_options!.merge(common_attrs)
92
+ super(label, *args, attrs, &block)
93
+ end
94
+
95
+ def update_or_add(label, *args, &block)
96
+ attrs = args.extract_options!.merge(common_attrs)
97
+ super(label, *args, attrs, &block)
98
+ end
99
+
100
+ end
101
+
102
+ #
103
+ # @example
104
+ # class Smurf
105
+ # include Gorillib::Model
106
+ # end
107
+ #
108
+ # # Sets the 'village' attribute on each item it receives to the object
109
+ # # this collection belongs to.
110
+ # class SmurfCollection < ModelCollection
111
+ # include Gorillib::Collection::ItemsBelongTo
112
+ # self.item_type = Smurf
113
+ # self.parentage_method = :village
114
+ # end
115
+ #
116
+ # # SmurfVillage makes sure its SmurfCollection knows that it `belongs_to` the village
117
+ # class SmurfVillage
118
+ # include Gorillib::Model
119
+ # field :name, Symbol
120
+ # field :smurfs, SmurfCollection, default: ->{ SmurfCollection.new(belongs_to: self) }
121
+ # end
122
+ #
123
+ # # all the normal stuff works as you'd expect
124
+ # smurf_town = SmurfVillage.new('smurf_town') # #<SmurfVillage name=smurf_town>
125
+ # smurf_town.smurfs # c{ }
126
+ # smurf_town.smurfs.belongs_to # #<SmurfVillage name=smurf_town>
127
+ #
128
+ # # when a new smurf moves to town, it knows what village it belongs_to
129
+ # smurf_town.smurfs.receive_item(:novel_smurf, smurfiness: 10)
130
+ # # => #<Smurf name=:novel_smurf smurfiness=10 village=#<SmurfVillage name=smurf_town>>
131
+ #
132
+ module ItemsBelongTo
133
+ extend ActiveSupport::Concern
134
+ include Gorillib::Collection::CommonAttrs
135
+
136
+ included do
137
+ # [Class, #receive] Name of the attribute to set on
138
+ class_attribute :parentage_method, :instance_writer => false
139
+ singleton_class.send(:protected, :common_attrs=)
140
+ end
141
+
142
+ # add this collection's belongs_to to the common attrs, so that a
143
+ # newly-created object knows its parentage from birth.
144
+ def initialize(*args)
145
+ super
146
+ @common_attrs = self.common_attrs.merge(parentage_method => self.belongs_to)
147
+ end
148
+
149
+ def add(item, *args)
150
+ item.send("#{parentage_method}=", belongs_to)
151
+ super
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,200 @@
1
+ module Gorillib
2
+
3
+ #
4
+ # The Collection class encapsulates the minimum functionality to let you:
5
+ #
6
+ # * store items uniquely, in order added
7
+ # * retrieve items by label
8
+ # * iterate over its values
9
+ #
10
+ # A collection is best used for representing 'plural' properties of models; it
11
+ # is *not* intended to be some radical reimagining of a generic array or
12
+ # hash. We've found its locked-down capabilities to particularly useful for
13
+ # constructing DSLs (Domain-Specific Languages). Collections are *not*
14
+ # intended to be performant: its abstraction layer comes at the price of
15
+ # additional method calls.
16
+ #
17
+ # ### Gated admission
18
+ #
19
+ # Collection provides a well-defended perimeter. Every item added to the
20
+ # collection (whether sent to the initializer, the passes through `add` method
21
+ #
22
+ # ### Familiarity with its contents
23
+ #
24
+ # Typically your model will have a familiar (but not intimate) relationship
25
+ # with its plural property:
26
+ #
27
+ # * items may have some intrinsic, uniquely-identifying feature: a `name`,
28
+ # `id`, or normalized representation. You'd like to be able to add an
29
+ # retrieve them by that intrinsic feature without having to manually juggle
30
+ # the correspondence of labels to intrinsic features.
31
+ #
32
+ # In the case of a ModelCollection,
33
+ #
34
+ # * all its items may share a common type: "a post has many `Comment`s".
35
+ #
36
+ # * a model may want items to hold a reference back to the containing model,
37
+ # or otherwise to share some common attributes. As an example, a `Graph` may
38
+ # have many `Stage` objects; the collection can inform newly-added stages
39
+ # which graph they belong to.
40
+ #
41
+ # ### Barebones enumerable methods
42
+ #
43
+ # The set of methods is purposefully sparse. If you want to use `select`,
44
+ # `invert`, etc, just invoke `to_hash` or `to_a` and work with the copy it
45
+ # gives you.
46
+ #
47
+ # Collection responds to:
48
+ # - receive!, values, to_a, each and each_value;
49
+ # - length, size, empty?, blank?
50
+ # - [], []=, include?, fetch, delete, each_pair, to_hash.
51
+ # - `key_method`: called on items to get their key; `to_key` by default.
52
+ # - `<<`: adds item under its `key_method` key
53
+ # - `receive!`s an array by auto-keying the elements, or a hash by trusting what you give it
54
+ #
55
+ # A ModelCollection adds:
56
+ # - `factory`: generates new items, converts received items
57
+ # - `update_or_create: if absent, creates item with given attributes and
58
+ # `key_method => key`; if present, updates with given attributes.
59
+ #
60
+ #
61
+ class Collection
62
+ # [{Symbol => Object}] The actual store of items -- not for you to mess with
63
+ attr_reader :clxn
64
+ protected :clxn
65
+
66
+ # Object that owns this collection, if any
67
+ attr_reader :belongs_to
68
+
69
+ # [String, Symbol] Method invoked on a new item to generate its collection key; :to_key by default
70
+ class_attribute :key_method, :instance_writer => false
71
+ singleton_class.send(:protected, :key_method=)
72
+
73
+ # include Gorillib::Model
74
+ def initialize(options={})
75
+ @clxn = Hash.new
76
+ @key_method = options[:key_method] if options[:key_method]
77
+ @belongs_to = options[:belongs_to] if options[:belongs_to]
78
+ end
79
+
80
+ # Adds an item in-place. Items added to the collection (via `add`, `[]=`,
81
+ # `initialize`, etc) all pass through the `add` method: you should override
82
+ # this in subclasses to add any gatekeeper behavior.
83
+ #
84
+ # If no label is supplied, we use the result of invoking `key_method` on the
85
+ # item (or raise an error if no label *and* no key_method).
86
+ #
87
+ # It's up to you to ensure that labels make sense; this method doesn't
88
+ # demand the item's key_method match its label.
89
+ #
90
+ # @return [Object] the item
91
+ def add(item, label=nil)
92
+ label ||= label_for(item)
93
+ @clxn[label] = item
94
+ end
95
+
96
+ def label_for(item)
97
+ if key_method.nil? then
98
+ raise ArgumentError, "Can't add things to a #{self.class} without some sort of label: use foo[label] = obj, or set the collection's key_method" ;
99
+ end
100
+ item.public_send(key_method)
101
+ end
102
+
103
+ #
104
+ # Barebones enumerable methods
105
+ #
106
+ # This set of methods is purposefully sparse. If you want to use `select`,
107
+ # `invert`, etc, just invoke `to_hash` or `to_a` and work with the copy it
108
+ # gives you.
109
+ #
110
+
111
+ delegate :[], :fetch, :delete, :include?, :to => :clxn
112
+ delegate :keys, :values, :each_pair, :each_value, :to => :clxn
113
+ delegate :length, :size, :empty?, :blank?, :to => :clxn
114
+
115
+ # @return [Array] an array holding the items
116
+ def to_a ; values ; end
117
+ # @return [{Symbol => Object}] a hash of key=>item pairs
118
+ def to_hash ; clxn.dup ; end
119
+
120
+ # iterate over each value in the collection
121
+ def each(&block); each_value(&block) ; end
122
+
123
+ # Adds item, returning the collection itself.
124
+ # @return [Gorillib::Collection] the collection
125
+ def <<(item)
126
+ add(item)
127
+ self
128
+ end
129
+
130
+ # add item with given label
131
+ def []=(label, item)
132
+ add(item, label)
133
+ end
134
+
135
+ # Receive items in-place, replacing any existing item with that label.
136
+ #
137
+ # Individual items are added using #receive_item -- if you'd like to perform
138
+ # any conversion or modification to items, do it there
139
+ #
140
+ # @param other [{Symbol => Object}, Array<Object>] a hash of key=>item pairs or a list of items
141
+ # @return [Gorillib::Collection] the collection
142
+ def receive!(other)
143
+ if other.respond_to?(:each_pair)
144
+ other.each_pair{|label, item| receive_item(label, item) }
145
+ elsif other.respond_to?(:each)
146
+ other.each{|item| receive_item(nil, item) }
147
+ else
148
+ raise "A collection can only receive something that is enumerable: got #{other.inspect}"
149
+ end
150
+ self
151
+ end
152
+
153
+ # items arriving from the outside world should pass through receive_item,
154
+ # not directly to add.
155
+ def receive_item(label, item)
156
+ add(item, label)
157
+ end
158
+
159
+ # Create a new collection and add the given items to it
160
+ # (if given an existing collection, just returns it directly)
161
+ def self.receive(items, *args)
162
+ return items if native?(items)
163
+ coll = new(*args)
164
+ coll.receive!(items)
165
+ coll
166
+ end
167
+
168
+ # A `native` object does not need any transformation; it is accepted directly.
169
+ # By default, an object is native if it `is_a?` this class
170
+ #
171
+ # @param obj [Object] the object that will be received
172
+ # @return [true, false] true if the item does not need conversion
173
+ def self.native?(obj)
174
+ obj.is_a?(self)
175
+ end
176
+
177
+ # Two collections are equal if they have the same class and their contents are equal
178
+ #
179
+ # @param [Gorillib::Collection, Object] other The other collection to compare
180
+ # @return [true, false] True if attributes are equal and other is instance of the same Class
181
+ def ==(other)
182
+ return false unless other.instance_of?(self.class)
183
+ clxn == other.send(:clxn)
184
+ end
185
+
186
+ # @return [String] string describing the collection's array representation
187
+ def to_s ; to_a.to_s ; end
188
+ # @return [String] string describing the collection's array representation
189
+ def inspect
190
+ key_width = [keys.map{|key| key.to_s.length + 1 }.max.to_i, 45].min
191
+ guts = clxn.map{|key, val| "%-#{key_width}s %s" % ["#{key}:", val.inspect] }.join(",\n ")
192
+ ['c{ ', guts, ' }'].join
193
+ end
194
+
195
+ def inspect_compact
196
+ ['c{ ', keys.join(", "), ' }'].join
197
+ end
198
+
199
+ end
200
+ end