gorillib-model 0.0.1

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 (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