gorillib 0.4.1pre → 0.4.2pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/.gitignore +13 -10
  2. data/.rspec +1 -1
  3. data/.yardopts +1 -0
  4. data/CHANGELOG.md +47 -0
  5. data/Gemfile +22 -19
  6. data/Guardfile +23 -9
  7. data/README.md +12 -12
  8. data/Rakefile +29 -40
  9. data/VERSION +1 -1
  10. data/examples/benchmark/factories_benchmark.rb +87 -0
  11. data/examples/builder/ironfan.rb +1 -19
  12. data/examples/hash/slicing_methods.rb +101 -0
  13. data/gorillib.gemspec +36 -35
  14. data/lib/gorillib/array/deep_compact.rb +4 -3
  15. data/lib/gorillib/array/simple_statistics.rb +76 -0
  16. data/lib/gorillib/base.rb +0 -1
  17. data/lib/gorillib/builder.rb +15 -30
  18. data/lib/gorillib/collection.rb +159 -57
  19. data/lib/gorillib/collection/model_collection.rb +136 -43
  20. data/lib/gorillib/datetime/parse.rb +4 -2
  21. data/lib/gorillib/{array → deprecated/array}/average.rb +0 -0
  22. data/lib/gorillib/{array → deprecated/array}/random.rb +2 -1
  23. data/lib/gorillib/{array → deprecated/array}/sorted_median.rb +0 -0
  24. data/lib/gorillib/{array → deprecated/array}/sorted_percentile.rb +0 -0
  25. data/lib/gorillib/deprecated/array/sorted_sample.rb +13 -0
  26. data/lib/gorillib/{metaprogramming → deprecated/metaprogramming}/aliasing.rb +0 -0
  27. data/lib/gorillib/enumerable/sum.rb +3 -3
  28. data/lib/gorillib/exception/raisers.rb +92 -22
  29. data/lib/gorillib/factories.rb +550 -0
  30. data/lib/gorillib/hash/mash.rb +15 -58
  31. data/lib/gorillib/hashlike/deep_compact.rb +2 -2
  32. data/lib/gorillib/hashlike/slice.rb +55 -40
  33. data/lib/gorillib/model.rb +5 -3
  34. data/lib/gorillib/model/base.rb +33 -119
  35. data/lib/gorillib/model/defaults.rb +58 -14
  36. data/lib/gorillib/model/errors.rb +10 -0
  37. data/lib/gorillib/model/factories.rb +1 -367
  38. data/lib/gorillib/model/field.rb +40 -18
  39. data/lib/gorillib/model/fixup.rb +16 -0
  40. data/lib/gorillib/model/positional_fields.rb +35 -0
  41. data/lib/gorillib/model/schema_magic.rb +162 -0
  42. data/lib/gorillib/model/serialization.rb +1 -2
  43. data/lib/gorillib/model/serialization/csv.rb +59 -0
  44. data/lib/gorillib/pathname.rb +19 -8
  45. data/lib/gorillib/some.rb +2 -0
  46. data/lib/gorillib/string/constantize.rb +17 -10
  47. data/lib/gorillib/string/inflector.rb +11 -7
  48. data/lib/gorillib/type/boolean.rb +40 -0
  49. data/lib/gorillib/type/extended.rb +76 -40
  50. data/lib/gorillib/type/url.rb +6 -4
  51. data/lib/gorillib/utils/console.rb +1 -18
  52. data/lib/gorillib/utils/edge_cases.rb +18 -0
  53. data/spec/examples/builder/ironfan_spec.rb +5 -10
  54. data/spec/gorillib/array/compact_blank_spec.rb +36 -21
  55. data/spec/gorillib/array/simple_statistics_spec.rb +143 -0
  56. data/spec/gorillib/builder_spec.rb +16 -20
  57. data/spec/gorillib/collection_spec.rb +131 -35
  58. data/spec/gorillib/exception/raisers_spec.rb +39 -0
  59. data/spec/gorillib/hash/deep_compact_spec.rb +3 -3
  60. data/spec/gorillib/model/{record/defaults_spec.rb → defaults_spec.rb} +5 -1
  61. data/spec/gorillib/model/factories_spec.rb +335 -0
  62. data/spec/gorillib/model/{record/overlay_spec.rb → overlay_spec.rb} +0 -0
  63. data/spec/gorillib/model/serialization_spec.rb +2 -2
  64. data/spec/gorillib/model_spec.rb +19 -18
  65. data/spec/gorillib/pathname_spec.rb +7 -7
  66. data/spec/gorillib/string/truncate_spec.rb +3 -13
  67. data/spec/gorillib/type/extended_spec.rb +50 -2
  68. data/spec/gorillib/utils/capture_output_spec.rb +1 -1
  69. data/spec/spec_helper.rb +10 -7
  70. data/spec/support/factory_test_helpers.rb +76 -0
  71. data/spec/support/gorillib_test_helpers.rb +36 -24
  72. data/spec/support/model_test_helpers.rb +39 -2
  73. metadata +86 -51
  74. data/lib/alt/kernel/call_stack.rb +0 -56
  75. data/lib/gorillib/array/sorted_sample.rb +0 -12
  76. data/lib/gorillib/builder/field.rb +0 -5
  77. data/lib/gorillib/collection/has_collection.rb +0 -31
  78. data/lib/gorillib/collection/list_collection.rb +0 -58
  79. data/lib/gorillib/exception/confidence.rb +0 -17
  80. data/lib/gorillib/io/system_helpers.rb +0 -30
  81. data/lib/gorillib/model/record_schema.rb +0 -9
  82. data/lib/gorillib/utils/stub_module.rb +0 -33
  83. data/spec/array/average_spec.rb +0 -24
  84. data/spec/array/sorted_median_spec.rb +0 -18
  85. data/spec/array/sorted_percentile_spec.rb +0 -24
  86. data/spec/array/sorted_sample_spec.rb +0 -28
  87. data/spec/gorillib/metaprogramming/aliasing_spec.rb +0 -180
  88. data/spec/gorillib/model/record/factories_spec.rb +0 -335
  89. data/spec/support/kcode_test_helper.rb +0 -16
@@ -4,73 +4,116 @@ require 'gorillib/metaprogramming/class_attribute'
4
4
  module Gorillib
5
5
 
6
6
  #
7
- # A generic collection stores objects uniquely, in the order added. It responds to:
7
+ # The Collection class encapsulates the minimum functionality to let you:
8
+ #
9
+ # * store items uniquely, in order added
10
+ # * retrieve items by label
11
+ # * iterate over its values
12
+ #
13
+ # A collection is best used for representing 'plural' properties of models; it
14
+ # is *not* intended to be some radical reimagining of a generic array or
15
+ # hash. We've found its locked-down capabilities to particularly useful for
16
+ # constructing DSLs (Domain-Specific Languages). Collections are *not*
17
+ # intended to be performant: its abstraction layer comes at the price of
18
+ # additional method calls.
19
+ #
20
+ # ### Gated admission
21
+ #
22
+ # Collection provides a well-defended perimeter. Every item added to the
23
+ # collection (whether sent to the initializer, the passes through `add` method
24
+ #
25
+ # ### Familiarity with its contents
26
+ #
27
+ # Typically your model will have a familiar (but not intimate) relationship
28
+ # with its plural property:
29
+ #
30
+ # * items may have some intrinsic, uniquely-identifying feature: a `name`,
31
+ # `id`, or normalized representation. You'd like to be able to add an
32
+ # retrieve them by that intrinsic feature without having to manually juggle
33
+ # the correspondence of labels to intrinsic features.
34
+ #
35
+ # In the case of a ModelCollection,
36
+ #
37
+ # * all its items may share a common type: "a post has many `Comment`s".
38
+ #
39
+ # * a model may want items to hold a reference back to the containing model,
40
+ # or otherwise to share some common attributes. As an example, a `Graph` may
41
+ # have many `Stage` objects; the collection can inform newly-added stages
42
+ # which graph they belong to.
43
+ #
44
+ # ### Barebones enumerable methods
45
+ #
46
+ # The set of methods is purposefully sparse. If you want to use `select`,
47
+ # `invert`, etc, just invoke `to_hash` or `to_a` and work with the copy it
48
+ # gives you.
49
+ #
50
+ # Collection responds to:
8
51
  # - receive!, values, to_a, each and each_value;
9
52
  # - length, size, empty?, blank?
10
- #
11
- # A Collection additionally lets you store and retrieve things by label:
12
53
  # - [], []=, include?, fetch, delete, each_pair, to_hash.
54
+ # - `key_method`: called on items to get their key; `to_key` by default.
55
+ # - `<<`: adds item under its `key_method` key
56
+ # - `receive!`s an array by auto-keying the elements, or a hash by trusting what you give it
13
57
  #
14
58
  # A ModelCollection adds:
15
- # - `key_method`: called on objects to get their key; `to_key` by default.
16
- # - `factory`: generates new objects, converts received objects
17
- # - `<<`: adds object under its `key_method` key
18
- # - `receive!`s an array by auto-keying the elements, or a hash by trusting what you give it
19
- # - `update_or_create: if absent, creates object with given attributes and
59
+ # - `factory`: generates new items, converts received items
60
+ # - `update_or_create: if absent, creates item with given attributes and
20
61
  # `key_method => key`; if present, updates with given attributes.
21
62
  #
22
- class GenericCollection
23
- # [{Symbol => Object}] The actual store of items, but not for you to mess with
63
+ #
64
+ class Collection
65
+ # [{Symbol => Object}] The actual store of items -- not for you to mess with
24
66
  attr_reader :clxn
25
67
  protected :clxn
26
68
 
27
- def self.receive(items, *args)
28
- coll = new(*args)
29
- coll.receive!(items)
30
- coll
31
- end
69
+ # Object that owns this collection, if any
70
+ attr_reader :belongs_to
32
71
 
33
- delegate :length, :size, :empty?, :blank?, :to => :clxn
72
+ # [String, Symbol] Method invoked on a new item to generate its collection key; :to_key by default
73
+ class_attribute :key_method, :instance_writer => false
74
+ singleton_class.send(:protected, :key_method=)
34
75
 
35
- # Two collections are equal if they have the same class and their contents are equal
36
- #
37
- # @param [Gorillib::Collection, Object] other The other collection to compare
38
- # @return [true, false] True if attributes are equal and other is instance of the same Class
39
- def ==(other)
40
- return false unless other.instance_of?(self.class)
41
- clxn == other.send(:clxn)
76
+ # include Gorillib::Model
77
+ def initialize(options={})
78
+ @clxn = Hash.new
79
+ @key_method = options[:key_method] if options[:key_method]
80
+ @belongs_to = options[:belongs_to] if options[:belongs_to]
42
81
  end
43
82
 
44
- public
45
-
46
- # @return [String] string describing the collection's array representation
47
- def to_s ; to_a.to_s ; end
48
- # @return [String] string describing the collection's array representation
49
- def inspect(detailed=true)
50
- if detailed then guts = clxn.map{|key, val| "%-15s %s" % ["#{key}:", val.inspect] }.join(",\n ")
51
- else guts = keys.join(", ") ; end
52
- ["c{ ", guts, " }"].join
53
- end
54
- # @return [Array] serializable array representation of the collection
55
- def to_wire(options={})
56
- to_a.map{|el| el.respond_to?(:to_wire) ? el.to_wire(options) : el }
83
+ # Adds an item in-place. Items added to the collection (via `add`, `[]=`,
84
+ # `initialize`, etc) all pass through the `add` method: you should override
85
+ # this in subclasses to add any gatekeeper behavior.
86
+ #
87
+ # If no label is supplied, we use the result of invoking `key_method` on the
88
+ # item (or raise an error if no label *and* no key_method).
89
+ #
90
+ # It's up to you to ensure that labels make sense; this method doesn't
91
+ # demand the item's key_method match its label.
92
+ #
93
+ # @return [Object] the item
94
+ def add(item, label=nil)
95
+ label ||= label_for(item)
96
+ @clxn[label] = item
57
97
  end
58
- # same as #to_wire
59
- def as_json(*args) to_wire(*args) ; end
60
- # @return [String] JSON serialization of the collection's array representation
61
- def to_json(*args) to_wire(*args).to_json(*args) ; end
62
98
 
63
- end
64
-
65
- class Collection < Gorillib::GenericCollection
66
- def initialize
67
- @clxn = Hash.new
99
+ def label_for(item)
100
+ if key_method.nil? then
101
+ 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" ;
102
+ end
103
+ item.public_send(key_method)
68
104
  end
69
105
 
70
- delegate :[], :[]=, :fetch, :delete, :to => :clxn
71
- delegate :values, :to => :clxn
72
- delegate :keys, :each_pair, :each_value, :to => :clxn
73
- delegate :include?, :to => :clxn
106
+ #
107
+ # Barebones enumerable methods
108
+ #
109
+ # This set of methods is purposefully sparse. If you want to use `select`,
110
+ # `invert`, etc, just invoke `to_hash` or `to_a` and work with the copy it
111
+ # gives you.
112
+ #
113
+
114
+ delegate :[], :fetch, :delete, :include?, :to => :clxn
115
+ delegate :keys, :values, :each_pair, :each_value, :to => :clxn
116
+ delegate :length, :size, :empty?, :blank?, :to => :clxn
74
117
 
75
118
  # @return [Array] an array holding the items
76
119
  def to_a ; values ; end
@@ -80,21 +123,80 @@ module Gorillib
80
123
  # iterate over each value in the collection
81
124
  def each(&block); each_value(&block) ; end
82
125
 
83
- # Add the new items in-place; given items clobber existing items
126
+ # Adds item, returning the collection itself.
127
+ # @return [Gorillib::Collection] the collection
128
+ def <<(item)
129
+ add(item)
130
+ self
131
+ end
132
+
133
+ # add item with given label
134
+ def []=(label, item)
135
+ add(item, label)
136
+ end
137
+
138
+ # Receive items in-place, replacing any existing item with that label.
139
+ #
140
+ # Individual items are added using #receive_item -- if you'd like to perform
141
+ # any conversion or modification to items, do it there
142
+ #
84
143
  # @param other [{Symbol => Object}, Array<Object>] a hash of key=>item pairs or a list of items
85
144
  # @return [Gorillib::Collection] the collection
86
145
  def receive!(other)
87
- clxn.merge!( convert_collection(other) )
146
+ if other.respond_to?(:each_pair)
147
+ other.each_pair{|label, item| receive_item(label, item) }
148
+ elsif other.respond_to?(:each)
149
+ other.each{|item| receive_item(nil, item) }
150
+ else
151
+ raise "A collection can only receive something that is enumerable: got #{other.inspect}"
152
+ end
88
153
  self
89
154
  end
90
155
 
91
- protected
156
+ # items arriving from the outside world should pass through receive_item,
157
+ # not directly to add.
158
+ def receive_item(label, item)
159
+ add(item, label)
160
+ end
161
+
162
+ # Create a new collection and add the given items to it
163
+ # (if given an existing collection, just returns it directly)
164
+ def self.receive(items, *args)
165
+ return items if native?(items)
166
+ coll = new(*args)
167
+ coll.receive!(items)
168
+ coll
169
+ end
170
+
171
+ # A `native` object does not need any transformation; it is accepted directly.
172
+ # By default, an object is native if it `is_a?` this class
173
+ #
174
+ # @param obj [Object] the object that will be received
175
+ # @return [true, false] true if the item does not need conversion
176
+ def self.native?(obj)
177
+ obj.is_a?(self)
178
+ end
179
+
180
+ # Two collections are equal if they have the same class and their contents are equal
181
+ #
182
+ # @param [Gorillib::Collection, Object] other The other collection to compare
183
+ # @return [true, false] True if attributes are equal and other is instance of the same Class
184
+ def ==(other)
185
+ return false unless other.instance_of?(self.class)
186
+ clxn == other.send(:clxn)
187
+ end
188
+
189
+ # @return [String] string describing the collection's array representation
190
+ def to_s ; to_a.to_s ; end
191
+ # @return [String] string describing the collection's array representation
192
+ def inspect
193
+ key_width = [keys.map{|key| key.to_s.length + 1 }.max.to_i, 45].min
194
+ guts = clxn.map{|key, val| "%-#{key_width}s %s" % ["#{key}:", val.inspect] }.join(",\n ")
195
+ ['c{ ', guts, ' }'].join
196
+ end
92
197
 
93
- # - if the given collection responds_to `to_hash`, it is received into the internal collection; each hash key *must* match the id of its value or results are undefined.
94
- # - otherwise, it receives a hash generates from the id/value pairs of each object in the given collection.
95
- def convert_collection(cc)
96
- return cc.to_hash if cc.respond_to?(:to_hash)
97
- raise "a #{self.class} can only receive a hash with explicitly-labelled contents."
198
+ def inspect_compact
199
+ ['c{ ', keys.join(", "), ' }'].join
98
200
  end
99
201
 
100
202
  end
@@ -1,62 +1,155 @@
1
1
  module Gorillib
2
- class ModelCollection < Gorillib::Collection
3
- # [String, Symbol] Method invoked on a new item to generate its collection key; :to_key by default
4
- attr_accessor :key_method
5
- # The default `key_method` invoked on a new item to generate its collection key
6
- DEFAULT_KEY_METHOD = :to_key
7
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
8
22
  # [Class, #receive] Factory for generating a new collection item.
9
- class_attribute :factory, :instance_writer => false
10
- singleton_class.class_eval{ protected :factory= }
11
-
12
- def initialize(key_meth=nil, obj_factory=nil)
13
- @factory = Gorillib::Factory(obj_factory) if obj_factory
14
- @clxn = Hash.new
15
- @key_method = key_meth || DEFAULT_KEY_METHOD
16
- end
23
+ class_attribute :item_type, :instance_writer => false
24
+ singleton_class.send(:protected, :item_type=)
17
25
 
18
- # Adds an item in-place
19
- # @return [Gorillib::Collection] the collection
20
- def <<(val)
21
- receive! [val]
22
- self
26
+ def initialize(options={})
27
+ @item_type = Gorillib::Factory(options[:item_type]) if options[:item_type]
28
+ super
23
29
  end
24
30
 
25
- def create(*args, &block)
26
- item = factory.receive(*args)
27
- self << item
28
- item
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
29
35
  end
30
36
 
31
- def update_or_create(key, *args, &block)
32
- if include?(key)
33
- obj = fetch(key)
34
- obj.receive!(*args, &block)
35
- obj
37
+ def update_or_add(label, attrs, &block)
38
+ if label && include?(label)
39
+ item = fetch(label)
40
+ item.receive!(attrs, &block)
41
+ item
36
42
  else
37
- attrs = args.extract_options!.merge(key_method => key)
38
- create(*args, attrs, &block)
43
+ attrs = attrs.merge(key_method => label) if key_method && label
44
+ receive_item(label, attrs, &block)
39
45
  end
46
+ rescue StandardError => err ; err.polish("#{item_type} #{label} as #{attrs} to #{self}") rescue nil ; raise
47
+ end
48
+
49
+ # @return [Array] serializable array representation of the collection
50
+ def to_wire(options={})
51
+ to_a.map{|el| el.respond_to?(:to_wire) ? el.to_wire(options) : el }
40
52
  end
53
+ # same as #to_wire
54
+ def as_json(*args) to_wire(*args) ; end
55
+ # @return [String] JSON serialization of the collection's array representation
56
+ def to_json(*args) to_wire(*args).to_json(*args) ; end
57
+ end
58
+
59
+ class Collection
60
+
61
+ #
62
+ #
63
+ # class ClusterCollection < ModelCollection
64
+ # self.item_type = Cluster
65
+ # end
66
+ # class Organization
67
+ # field :clusters, ClusterCollection, default: ->{ ClusterCollection.new(common_attrs: { organization: self }) }
68
+ # end
69
+ #
70
+ module CommonAttrs
71
+ extend Gorillib::Concern
72
+
73
+ included do
74
+ # [Class, #receive] Attributes to mix in to each added item
75
+ class_attribute :common_attrs, :instance_writer => false
76
+ singleton_class.send(:protected, :common_attrs=)
77
+ self.common_attrs = Hash.new
78
+ end
41
79
 
42
- protected
80
+ def initialize(options={})
81
+ super
82
+ @common_attrs = self.common_attrs.merge(options[:common_attrs]) if options.include?(:common_attrs)
83
+ end
84
+
85
+ #
86
+ # * a factory-native object: item is updated with common_attrs, then added
87
+ # * raw materials for the object: item is constructed (from the merged attrs and common_attrs), then added
88
+ #
89
+ def receive_item(label, *args, &block)
90
+ attrs = args.extract_options!.merge(common_attrs)
91
+ super(label, *args, attrs, &block)
92
+ end
93
+
94
+ def update_or_add(label, *args, &block)
95
+ attrs = args.extract_options!.merge(common_attrs)
96
+ super(label, *args, attrs, &block)
97
+ end
43
98
 
44
- def convert_value(val)
45
- return val unless factory
46
- return nil if val.nil?
47
- factory.receive(val)
48
99
  end
49
100
 
50
- # - if the given collection responds_to `to_hash`, it is received into the internal collection; each hash key *must* match the id of its value or results are undefined.
51
- # - otherwise, it receives a hash generates from the id/value pairs of each object in the given collection.
52
- def convert_collection(cc)
53
- return cc.to_hash if cc.respond_to?(:to_hash)
54
- cc.inject({}) do |acc, val|
55
- val = convert_value(val)
56
- key = val.public_send(key_method)
57
- acc[key] = val
58
- acc
101
+ #
102
+ # @example
103
+ # class Smurf
104
+ # include Gorillib::Model
105
+ # end
106
+ #
107
+ # # Sets the 'village' attribute on each item it receives to the object
108
+ # # this collection belongs to.
109
+ # class SmurfCollection < ModelCollection
110
+ # include Gorillib::Collection::ItemsBelongTo
111
+ # self.item_type = Smurf
112
+ # self.parentage_method = :village
113
+ # end
114
+ #
115
+ # # SmurfVillage makes sure its SmurfCollection knows that it `belongs_to` the village
116
+ # class SmurfVillage
117
+ # include Gorillib::Model
118
+ # field :name, Symbol
119
+ # field :smurfs, SmurfCollection, default: ->{ SmurfCollection.new(belongs_to: self) }
120
+ # end
121
+ #
122
+ # # all the normal stuff works as you'd expect
123
+ # smurf_town = SmurfVillage.new('smurf_town') # #<SmurfVillage name=smurf_town>
124
+ # smurf_town.smurfs # c{ }
125
+ # smurf_town.smurfs.belongs_to # #<SmurfVillage name=smurf_town>
126
+ #
127
+ # # when a new smurf moves to town, it knows what village it belongs_to
128
+ # smurf_town.smurfs.receive_item(:novel_smurf, smurfiness: 10)
129
+ # # => #<Smurf name=:novel_smurf smurfiness=10 village=#<SmurfVillage name=smurf_town>>
130
+ #
131
+ module ItemsBelongTo
132
+ extend Gorillib::Concern
133
+ include Gorillib::Collection::CommonAttrs
134
+
135
+ included do
136
+ # [Class, #receive] Name of the attribute to set on
137
+ class_attribute :parentage_method, :instance_writer => false
138
+ singleton_class.send(:protected, :common_attrs=)
59
139
  end
140
+
141
+ # add this collection's belongs_to to the common attrs, so that a
142
+ # newly-created object knows its parentage from birth.
143
+ def initialize(*args)
144
+ super
145
+ @common_attrs = self.common_attrs.merge(parentage_method => self.belongs_to)
146
+ end
147
+
148
+ def add(item, *args)
149
+ item.send("#{parentage_method}=", belongs_to)
150
+ super
151
+ end
152
+
60
153
  end
61
154
 
62
155
  end