gorillib 0.4.1pre → 0.4.2pre

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