gorillib 0.4.0pre → 0.4.1pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/CHANGELOG.md +36 -1
  2. data/Gemfile +23 -19
  3. data/Guardfile +1 -1
  4. data/Rakefile +31 -31
  5. data/TODO.md +2 -30
  6. data/VERSION +1 -1
  7. data/examples/builder/ironfan.rb +4 -4
  8. data/gorillib.gemspec +40 -25
  9. data/lib/gorillib/array/average.rb +13 -0
  10. data/lib/gorillib/array/sorted_median.rb +11 -0
  11. data/lib/gorillib/array/sorted_percentile.rb +11 -0
  12. data/lib/gorillib/array/sorted_sample.rb +12 -0
  13. data/lib/gorillib/builder.rb +8 -14
  14. data/lib/gorillib/collection/has_collection.rb +31 -31
  15. data/lib/gorillib/collection/list_collection.rb +58 -0
  16. data/lib/gorillib/collection/model_collection.rb +63 -0
  17. data/lib/gorillib/collection.rb +57 -85
  18. data/lib/gorillib/logger/log.rb +26 -22
  19. data/lib/gorillib/model/base.rb +52 -39
  20. data/lib/gorillib/model/doc_string.rb +15 -0
  21. data/lib/gorillib/model/factories.rb +56 -61
  22. data/lib/gorillib/model/lint.rb +24 -0
  23. data/lib/gorillib/model/serialization.rb +12 -2
  24. data/lib/gorillib/model/validate.rb +2 -2
  25. data/lib/gorillib/pathname.rb +21 -6
  26. data/lib/gorillib/some.rb +2 -0
  27. data/lib/gorillib/type/extended.rb +0 -2
  28. data/lib/gorillib/type/url.rb +9 -0
  29. data/lib/gorillib/utils/console.rb +4 -1
  30. data/notes/HOWTO.md +22 -0
  31. data/notes/bucket.md +155 -0
  32. data/notes/builder.md +170 -0
  33. data/notes/collection.md +81 -0
  34. data/notes/factories.md +86 -0
  35. data/notes/model-overlay.md +209 -0
  36. data/notes/model.md +135 -0
  37. data/notes/structured-data-classes.md +127 -0
  38. data/spec/array/average_spec.rb +24 -0
  39. data/spec/array/sorted_median_spec.rb +18 -0
  40. data/spec/array/sorted_percentile_spec.rb +24 -0
  41. data/spec/array/sorted_sample_spec.rb +28 -0
  42. data/spec/gorillib/builder_spec.rb +46 -28
  43. data/spec/gorillib/collection_spec.rb +195 -10
  44. data/spec/gorillib/model/lint_spec.rb +28 -0
  45. data/spec/gorillib/model/record/factories_spec.rb +27 -13
  46. data/spec/gorillib/model/serialization_spec.rb +3 -5
  47. data/spec/gorillib/model_spec.rb +86 -104
  48. data/spec/spec_helper.rb +2 -1
  49. data/spec/support/gorillib_test_helpers.rb +83 -7
  50. data/spec/support/model_test_helpers.rb +9 -28
  51. metadata +52 -44
  52. data/lib/gorillib/configurable.rb +0 -28
  53. data/spec/gorillib/configurable_spec.rb +0 -62
  54. data/spec/support/shared_examples/included_module.rb +0 -20
@@ -0,0 +1,127 @@
1
+ # gorillib's structured data classes -- Record, Model, Builder and Bucket
2
+
3
+ ## Overview
4
+
5
+ Gorillib provides these general flavors of model:
6
+
7
+ * `Gorillib::Record`: **lightweight structured records**. Easily assemble a dynamic data structure that enables both rich behavior and generic manipulation, serialization and transformation. Especially useful when you just need to pull something from JSON, attach some functionality, and get it back on the wire.
8
+
9
+ ```ruby
10
+ class Place
11
+ include Gorillib::Record
12
+ # fields can be simple...
13
+ field :name, String
14
+ field :country_id, String, :doc => 'Country code (2-letter alpha) containing the place'
15
+ # ... or complext
16
+ field :geo, GeoCoordinates, :doc => 'geographic location of the place'
17
+ end
18
+
19
+ class GeoCoordinates
20
+ include Gorillib::Record
21
+ field :latitude, Float, :doc => 'latitude in decimal degrees; negative numbers are south of the equator'
22
+ field :longitude, Float, :doc => 'longitude in decimal degrees; negative numbers are west of Greenwich'
23
+ end
24
+
25
+ # It's simple to instantiate complex nested data structures
26
+ lunch_spot = Place.receive({ :name => "Torchy's Tacos", :country_id => "us",
27
+ :geo => { :latitude => "30.295", :longitude => "-97.745" }})
28
+ ```
29
+
30
+ * `Gorillib::Model`: **rich structured models** offering predictable magic with a disciplined footprint. Comparable to ActiveRecord or Datamapper, but for a world dominated by JSON+HTTP, not relational databases
31
+
32
+ ```ruby
33
+ class GeoCoordinates
34
+ include Gorillib::Model
35
+ field :latitude, Float, :doc => 'latitude in decimal degrees; negative numbers are south of the equator', :validates => { :numericality => { :>= => -90, :<= => 90 } }
36
+ field :longitude, Float, :doc => 'longitude in decimal degrees; negative numbers are west of Greenwich', :validates => { :numericality => { :>= => -180, :<= => 180 } }
37
+ end
38
+ position = GeoCoordinates.from_tuple(30.295, -97.745)
39
+ # A Gorillib::Model obeys the ActiveModel contract
40
+ GeoCoordinates.model_name.human # => 'Geo coordinates'
41
+ ```
42
+
43
+ * `Gorillib::Builder`: **foundation models for elegant ruby DSLs** (Domain-Specific Languages). Relaxes ruby syntax to enable highly-readable specification of complex behavior.
44
+
45
+ ```ruby
46
+ workflow(:bake_pie) do
47
+ step :make_crust
48
+ step :add_filling
49
+ step :bake
50
+ step :cool
51
+ end
52
+ ```
53
+
54
+ * `Gorillib::Bucket` (?name?): **record-style access to freeform hashes**. Provide a disciplined interface on top of arbitrarily-structured data. Used by [[Configliere]] and others.
55
+
56
+ ```ruby
57
+ # pre-defining a field gives you special powers...
58
+ Settings.define :port, Integer, :doc => 'API server port number', :default => 80
59
+ # but you can still store or read anything you'd like...
60
+ Settings[:shout] = "SAN DIMAS HIGH SCHOOL FOOTBALL RULES"
61
+ # and treat the object as a hash when you'd like to
62
+ conn = Connections.open(Settings.merge(user_overrides))
63
+ ```
64
+
65
+ ## Decisions
66
+
67
+ * **initializer** - *does not* inject an initializer; if you want one, do `alias_method :initialize, :receive!`
68
+ * **frills** --
69
+ - *does inject*: `inspect` on the class, `inspect`, `to_s` on the instance
70
+ - *does inject*: accessors (`foo`, `foo=`) for each field.
71
+ - *does inject*: `==` on the instance
72
+ - *does not define*: `schema`, `initialize`
73
+ * **field defaults** - evaluated on first read, at which point its value is fixed on the record.
74
+
75
+ * **define_metamodel_record** -- visibility=false does not remove an existing method from the metamodel
76
+
77
+ * ?? **hash vs mash** -- does `attributes` return a mash or hash?
78
+ * are these basic functionality:
79
+ - **extra_attributes** -- ??
80
+ - **default** -- ??
81
+
82
+ * Record:
83
+ - class methods -- `field`, `fields`, `field_names`, `has_field?`, `metamodel`, `receive`, `inspect`
84
+ - instance methods -- `read_attribute`, `write_attribute`, `unset_attribute`, `attribute_set?`, `attributes`, `receive!`, `update`, `inspect`, `to_s`, `==`
85
+ - with each attribute -- `receive_foo`, `foo=`, `foo`
86
+
87
+ * Builder:
88
+ - defining classes before they're used
89
+
90
+
91
+ ## Features
92
+
93
+ ### `Record`
94
+
95
+ * `Record::Ordered` -- sort order on fields (and thus record)
96
+ * `Record::FieldAliasing` -- aliases for fields and receivers
97
+ * `Record::Defaults` -- default values
98
+ * `Record::Schema` -- icss schema
99
+ * `Record::HashAccessors` -- adds `[]`, `[]=`, `delete`, `keys`, `#has_key?`, `to_hash`, and `update`. This allows it to be `hashlike`, but you must include that explicitly.
100
+ * `Record::Hashlike` -- mixes in `Record::HashAccessors` and `Gorillib::Hashlike`, making it behave in almost every respect like a hash. Use this when you want something that will *behave* like a hash but *be* a record. If you want something to *be* a hash but *behave* like a record, use the `Gorillib::MashRecord`
101
+
102
+ ### `Builder`
103
+
104
+ * `Builder::GetsetField` --
105
+
106
+ ### `HashRecord`
107
+
108
+ ### `Model`
109
+
110
+ * `Model::Naming`
111
+ * `Model::Conversion` --
112
+
113
+ ### active_model / active_model_lite
114
+
115
+ From `active_model` or `active_model_lite`:
116
+
117
+ * `Model::Callbacks` --
118
+ * `Model::Dirty` --
119
+ * `Model::Serialization` --
120
+ * `Model::Validations` -- implies `Model::Errors`, `Model::Callbacks`
121
+
122
+
123
+ ## Why, in a world with ActiveRecord, Datamapper, Hashie, Struct, ..., do we need yet another damn model framework?
124
+
125
+ ActiveRecord and Datamapper excel in a world where data (and truth) live in the database. ActiveRecord sets the standard for elegant magic, but is fairly heavyweight (I don't want to include a full XML serialization suite just so I can validate records). This often means it's overkill for the myriad flyweight scripts, Goliath apps, and such that we deploy. Datamapper does a remarkable job of delivering power while still being light on its toes, but ultimately is too tightly bound to an ORM view of the world. Hashie, Structs and OStructs behave as both hashes and records. In my experience, this interface is too generous -- their use leads to mealymouthed code.
126
+
127
+ More importantly, our data spends most of its time on the wire or being handled as an opaque blob of data; a good amount of time being handled as a generic bundle of properties; and (though most important) a relatively small amount of time as an active, assertive object. So type conversion and validation are fundamental actions, but shouldn't crud up my critical path or be required. Models should offer predictable and disciplined features, but be accessable as generic bags of facts.
@@ -0,0 +1,24 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+ require 'gorillib/array/average'
3
+
4
+ describe Array do
5
+ describe '#average' do
6
+ context 'on non-float array element' do
7
+ it 'raises error' do
8
+ expect { [0.0, :b, 1.0].average }.should raise_error(ArgumentError)
9
+ end
10
+ end
11
+
12
+ context 'with empty' do
13
+ it 'returns nil' do
14
+ [].average.should be_nil
15
+ end
16
+ end
17
+
18
+ context 'given a numerical array' do
19
+ it 'returns the average of the elements' do
20
+ (1..10).to_a.average.should == 5.5
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+ require 'gorillib/array/sorted_median'
3
+
4
+ describe Array do
5
+ describe '#sorted_median' do
6
+ context 'with empty' do
7
+ it 'returns nil' do
8
+ [].sorted_median.should be_nil
9
+ end
10
+ end
11
+
12
+ context 'given any array' do
13
+ it 'returns the middle element of odd-sized arrays' do
14
+ ("a".."y").to_a.sorted_median.should == "m"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+ require 'gorillib/array/sorted_percentile'
3
+
4
+ describe Array do
5
+ describe '#sorted_percentile' do
6
+ context 'with empty' do
7
+ it 'returns nil' do
8
+ [].sorted_percentile(0.0).should be_nil
9
+ end
10
+ end
11
+
12
+ context 'given any array' do
13
+ it 'returns the element closest to the given percentile' do
14
+ ("a".."y").to_a.sorted_percentile( 0.0).should == "a"
15
+ ("a".."y").to_a.sorted_percentile( 50.0).should == "m"
16
+ ("a".."y").to_a.sorted_percentile(100.0).should == "y"
17
+ end
18
+ end
19
+
20
+ # (Please do not define behavior for two elements equally close to
21
+ # a given percentile.)
22
+
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ require File.expand_path('../spec_helper', File.dirname(__FILE__))
2
+ require 'gorillib/array/sorted_sample'
3
+
4
+ describe Array do
5
+ describe '#sorted_median' do
6
+ context 'with empty' do
7
+ it 'returns an empty array' do
8
+ [].sorted_sample(1).should be_empty
9
+ end
10
+ end
11
+
12
+ context 'given an undersized array' do
13
+ it 'does not return the same element more than once' do
14
+ ("a".."z").to_a.sorted_sample(27).should == ("a".."z").to_a
15
+ end
16
+ end
17
+
18
+ context 'given any array' do
19
+ it ('returns a sample of the given size as close to evenly ' \
20
+ 'distributed over the array as possible') do
21
+ sample = (1..100).to_a.sorted_sample(26)
22
+ deltas = sample[0..-2].zip(sample[1..-1]).map{|a,b| b-a}
23
+ deltas.max.should <= 4
24
+ deltas.min.should >= 3
25
+ end
26
+ end
27
+ end
28
+ end
@@ -3,23 +3,39 @@ require 'spec_helper'
3
3
  # libs under test
4
4
  require 'gorillib/builder'
5
5
  require 'gorillib/builder/field'
6
+ require 'gorillib/collection/model_collection'
6
7
 
7
8
  # testing helpers
8
9
  require 'gorillib/hash/compact'
9
10
  require 'model_test_helpers'
10
11
 
11
12
  describe Gorillib::Builder, :model_spec => true, :builder_spec => true do
12
- let(:example_val ){ mock('example val') }
13
- subject{ car_class }
13
+
14
+ let(:smurf_class) do
15
+ class Gorillib::Test::Smurf
16
+ include Gorillib::Builder
17
+ magic :smurfiness, Integer
18
+ magic :weapon, Symbol
19
+ end
20
+ Gorillib::Test::Smurf
21
+ end
22
+ let(:poppa_smurf ){ smurf_class.receive(:name => 'Poppa Smurf', :smurfiness => 9, :weapon => 'staff') }
23
+ let(:smurfette ){ smurf_class.receive(:name => 'Smurfette', :smurfiness => 11, :weapon => 'charm') }
24
+
25
+ #
26
+ # IT BEHAVES LIKE A MODEL
27
+ # (maybe you wouldn't notice if it was just one little line)
28
+ #
29
+ it_behaves_like 'a model'
14
30
 
15
31
  context 'examples:' do
16
- subject{ car_class }
32
+ let(:subject_class ){ car_class }
17
33
  it 'type-converts values' do
18
- obj = subject.receive( :name => 'wildcat', :make_model => 'Buick Wildcat', :year => "1968", :doors => "2" )
34
+ obj = subject_class.receive( :name => 'wildcat', :make_model => 'Buick Wildcat', :year => "1968", :doors => "2" )
19
35
  obj.attributes.should == { :name => :wildcat, :make_model => 'Buick Wildcat', :year => 1968, :doors => 2, :engine => nil }
20
36
  end
21
37
  it 'handles nested structures' do
22
- obj = subject.receive(
38
+ obj = subject_class.receive(
23
39
  :name => 'wildcat', :make_model => 'Buick Wildcat', :year => "1968", :doors => "2",
24
40
  :engine => { :carburetor => 'edelbrock', :volume => "455", :cylinders => '8' })
25
41
  obj.attributes.values_at(:name, :make_model, :year, :doors).should == [:wildcat, 'Buick Wildcat', 1968, 2 ]
@@ -41,18 +57,20 @@ describe Gorillib::Builder, :model_spec => true, :builder_spec => true do
41
57
  end
42
58
 
43
59
  context 'receive!' do
44
- it 'accepts a configurate block' do
60
+ it 'with a block, instance evals the block' do
45
61
  expect_7 = nil ; expect_obj = nil
46
62
  wildcat.receive!({}){ expect_7 = 7 ; expect_obj = self }
47
63
  expect_7.should == 7 ; expect_obj.should == wildcat
64
+ end
65
+ it 'with a block of arity 1, calls the block passing self' do
48
66
  expect_7 = nil ; expect_obj = nil
49
67
  wildcat.receive!({}){|c| expect_7 = 7 ; expect_obj = c }
50
68
  expect_7.should == 7 ; expect_obj.should == wildcat
51
69
  end
52
70
  end
53
71
 
54
- context ".field" do
55
-
72
+ context ".magic" do
73
+ let(:subject_class){ car_class }
56
74
  context do
57
75
  subject{ car_class.new }
58
76
  let(:sample_val){ 'fiat' }
@@ -61,30 +79,29 @@ describe Gorillib::Builder, :model_spec => true, :builder_spec => true do
61
79
  it("#read_attribute is nil if never set"){ subject.read_attribute(:make_model).should == nil }
62
80
  end
63
81
 
82
+ it "does not create a writer method #foo=" do
83
+ subject_class.should be_method_defined(:doors)
84
+ subject_class.should_not be_method_defined(:doors=)
85
+ end
86
+
64
87
  context 'calling the getset "#foo" method' do
65
88
  subject{ wildcat }
66
89
 
67
90
  it "with no args calls read_attribute(:foo)" do
68
- subject.write_attribute(:doors, example_val)
69
- subject.should_receive(:read_attribute).with(:doors).at_least(:once).and_return(example_val)
70
- subject.doors.should == example_val
91
+ subject.write_attribute(:doors, mock_val)
92
+ subject.should_receive(:read_attribute).with(:doors).at_least(:once).and_return(mock_val)
93
+ subject.doors.should == mock_val
71
94
  end
72
95
  it "with an argument calls write_attribute(:foo)" do
73
96
  subject.write_attribute(:doors, 'gone')
74
- subject.should_receive(:write_attribute).with(:doors, example_val).and_return('returned')
75
- result = subject.doors(example_val)
97
+ subject.should_receive(:write_attribute).with(:doors, mock_val).and_return('returned')
98
+ result = subject.doors(mock_val)
76
99
  result.should == 'returned'
77
100
  end
78
101
  it "with multiple arguments is an error" do
79
102
  expect{ subject.doors(1, 2) }.to raise_error(ArgumentError, "wrong number of arguments (2 for 0..1)")
80
103
  end
81
104
  end
82
-
83
- it "does not create a writer method #foo=" do
84
- subject{ car_class }
85
- subject.should be_method_defined(:doors)
86
- subject.should_not be_method_defined(:doors=)
87
- end
88
105
  end
89
106
 
90
107
  context ".member" do
@@ -95,14 +112,14 @@ describe Gorillib::Builder, :model_spec => true, :builder_spec => true do
95
112
  it("#read_attribute is nil if never set"){ subject.read_attribute(:engine).should == nil }
96
113
 
97
114
  it "calling the getset method #foo with no args calls read_attribute(:foo)" do
98
- wildcat.write_attribute(:doors, example_val)
99
- wildcat.should_receive(:read_attribute).with(:doors).at_least(:once).and_return(example_val)
100
- wildcat.doors.should == example_val
115
+ wildcat.write_attribute(:doors, mock_val)
116
+ wildcat.should_receive(:read_attribute).with(:doors).at_least(:once).and_return(mock_val)
117
+ wildcat.doors.should == mock_val
101
118
  end
102
119
  it "calling the getset method #foo with an argument calls write_attribute(:foo)" do
103
120
  wildcat.write_attribute(:doors, 'gone')
104
- wildcat.should_receive(:write_attribute).with(:doors, example_val).and_return('returned')
105
- result = wildcat.doors(example_val)
121
+ wildcat.should_receive(:write_attribute).with(:doors, mock_val).and_return('returned')
122
+ result = wildcat.doors(mock_val)
106
123
  result.should == 'returned'
107
124
  end
108
125
  it "calling the getset method #foo with multiple arguments is an error" do
@@ -116,10 +133,10 @@ describe Gorillib::Builder, :model_spec => true, :builder_spec => true do
116
133
 
117
134
  context 'collections' do
118
135
  subject{ garage }
119
- let(:sample_val){ Gorillib::Collection.receive([wildcat], car_class, :name) }
136
+ let(:sample_val){ Gorillib::ModelCollection.receive([wildcat], :name, car_class) }
120
137
  let(:raw_val ){ [ wildcat.attributes ] }
121
138
  it_behaves_like "a model field", :cars
122
- it("#read_attribute is an empty collection if never set"){ subject.read_attribute(:cars).should == Gorillib::Collection.new }
139
+ it("#read_attribute is an empty collection if never set"){ subject.read_attribute(:cars).should == Gorillib::ModelCollection.new }
123
140
 
124
141
  it 'a collection holds named objects' do
125
142
  garage.cars.should be_empty
@@ -140,12 +157,13 @@ describe Gorillib::Builder, :model_spec => true, :builder_spec => true do
140
157
 
141
158
  # examine the whole collection
142
159
  garage.cars.keys.should == [:cadzilla, :wildcat, :ford_39]
143
- garage.cars.should == Gorillib::Collection.receive([cadzilla, wildcat, ford_39], car_class, :name)
160
+ garage.cars.should == Gorillib::ModelCollection.receive([cadzilla, wildcat, ford_39], :name, car_class)
144
161
  end
162
+
145
163
  it 'lazily autovivifies collection items' do
146
164
  garage.cars.should be_empty
147
165
  garage.car(:chimera).should be_a(car_class)
148
- garage.cars.should == Gorillib::Collection.receive([{:name => :chimera}], car_class, :name)
166
+ garage.cars.should == Gorillib::ModelCollection.receive([{:name => :chimera}], :name, car_class)
149
167
  end
150
168
 
151
169
  context 'collection getset method' do
@@ -1,20 +1,205 @@
1
1
  require 'spec_helper'
2
2
  #
3
3
  require 'gorillib/model'
4
- require 'gorillib/model/field'
5
- require 'gorillib/model/defaults'
6
- #
4
+ require 'gorillib/collection/model_collection'
5
+ require 'gorillib/collection/list_collection'
7
6
  require 'model_test_helpers'
8
7
 
9
- module Gorillib::Test ; end
10
- module Meta::Gorillib::Test ; end
11
-
12
- describe Gorillib::Collection, :model_spec => true do
13
- it 'needs more tests'
14
- let(:collection_with_mock_clxn) do
8
+ shared_context :collection_spec do
9
+ # a collection with the internal :clxn mocked out, and a method 'innards' to
10
+ # let you access it.
11
+ let(:collection_with_mock_innards) do
15
12
  coll = described_class.new
16
- coll.send(:define_singleton_method, :mock_clxn){ @clxn }
17
13
  coll.send(:instance_variable_set, :@clxn, mock('clxn hash') )
14
+ coll.send(:define_singleton_method, :innards){ @clxn }
15
+ end
16
+ end
17
+
18
+ shared_examples_for 'a collection' do
19
+ subject{ string_collection }
20
+
21
+ context '.receive' do
22
+ it 'makes a new collection, has it #receive! the cargo, returns it' do
23
+ mock_collection = mock('collection')
24
+ mock_cargo = mock('cargo')
25
+ mock_args = [mock, mock]
26
+ described_class.should_receive(:new).with(*mock_args).and_return(mock_collection)
27
+ mock_collection.should_receive(:receive!).with(mock_cargo)
28
+ described_class.receive(mock_cargo, *mock_args).should == mock_collection
29
+ end
30
+ end
31
+
32
+ context 'empty collection' do
33
+ subject{ described_class.new }
34
+ its(:length){ should == 0 }
35
+ its(:size ){ should == 0 }
36
+ its(:empty?){ should be true }
37
+ its(:blank?){ should be true }
38
+ its(:values){ should == [] }
39
+ its(:to_a ){ should == [] }
40
+ end
41
+ context 'non-empty collection' do
42
+ subject{ string_collection }
43
+ its(:length){ should == 4 }
44
+ its(:size ){ should == 4 }
45
+ its(:empty?){ should be false }
46
+ its(:blank?){ should be false }
47
+ end
48
+
49
+ context '#values returns an array' do
50
+ its(:values){ should == %w[wocket in my pocket] }
51
+ end
52
+ context '#to_a returns same as values' do
53
+ its(:to_a ){ should == %w[wocket in my pocket] }
54
+ end
55
+
56
+ context '#each_value' do
57
+ it 'each value in order' do
58
+ result = []
59
+ ret = subject.each_value{|val| result << val.reverse }
60
+ result.should == %w[tekcow ni ym tekcop]
61
+ end
62
+ end
63
+
64
+ # context '#receive (array)', :if => :receives_arrays do
65
+ # it 'adopts the contents of an array' do
66
+ # string_collection.receive!(%w[horton hears a who])
67
+ # string_collection.values.should == %w[wocket in my pocket horton hears a who]
68
+ # end
69
+ # it 'does not adopt duplicates' do
70
+ # string_collection.receive!(%w[red fish blue fish])
71
+ # string_collection.values.should == %w[wocket in my pocket red fish blue]
72
+ # end
73
+ # end
74
+ context '#receive (hash)' do
75
+ it 'adopts the values of a hash' do
76
+ string_collection.receive!({horton: "horton", hears: "hears", a: "a", who: "who" })
77
+ string_collection.values.should == %w[wocket in my pocket horton hears a who]
78
+ end
79
+ it 'does not adopt duplicates' do
80
+ string_collection.receive!({red: 'red', fish: 'fish'})
81
+ string_collection.receive!({blue: 'blue', fish: 'fish'})
82
+ string_collection.values.should == %w[wocket in my pocket red fish blue]
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ shared_examples_for 'a keyed collection' do
89
+ subject{ string_collection }
90
+
91
+ context '#[]' do
92
+ it 'retrieves stored objects' do
93
+ subject[1] = mock_val
94
+ subject[1].should equal(mock_val)
95
+ end
96
+ end
97
+
98
+ context '#fetch' do
99
+ it 'retrieves an object if present' do
100
+ subject[1] = mock_val
101
+ subject.fetch(1).should equal(mock_val)
102
+ end
103
+ it 'if absent and block given: calls block with label, returning its value' do
104
+ got_here = nil
105
+ subject.fetch(69){ got_here = 'yup' ; mock_val }.should equal(mock_val)
106
+ got_here.should == 'yup'
107
+ end
108
+ it 'if absent and no block given: raises an error' do
109
+ ->{ subject.fetch(69) }.should raise_error IndexError, /(key not found: 69|index 69 outside)/
110
+ end
18
111
  end
19
112
 
113
+ context '#delete' do
114
+ it 'retrieves an object if present' do
115
+ subject[1] = mock_val
116
+ subject.delete(1).should equal(mock_val)
117
+ subject.values.should_not include(mock_val)
118
+ end
119
+ it 'if absent and block given: calls block with label, returning its value' do
120
+ got_here = nil
121
+ subject.delete(69){ got_here = 'yup' ; mock_val }.should equal(mock_val)
122
+ got_here.should == 'yup'
123
+ end
124
+ it 'if absent and no block given: returns nil' do
125
+ subject.delete(69).should be nil
126
+ end
127
+ end
128
+ end
129
+
130
+ shared_examples_for 'an auto-keyed collection' do
131
+ subject{ string_collection }
132
+
133
+ it 'retrieves things by their label' do
134
+ string_collection[:pocket].should == "pocket"
135
+ string_collection['pocket'].should == nil
136
+ shouty_collection['POCKET'].should == "pocket"
137
+ shouty_collection['pocket'].should == nil
138
+ end
139
+
140
+ it 'gets label from key if none supplied' do
141
+ string_collection[:marvin].should be nil
142
+ string_collection << 'marvin'
143
+ string_collection[:marvin].should == 'marvin'
144
+ shouty_collection << 'marvin'
145
+ shouty_collection['MARVIN'].should == 'marvin'
146
+ end
147
+
148
+ context '#receive!' do
149
+ it 'extracts labels given an array' do
150
+ subject.receive!(%w[horton hears a who])
151
+ subject[:horton].should == 'horton'
152
+ end
153
+ it 'replaces labels in-place, preserving order' do
154
+ shouty_collection.receive!(%w[in MY pocKET wocKET])
155
+ shouty_collection['WOCKET'].should == 'wocKET'
156
+ shouty_collection.values.should == %w[wocKET in MY pocKET]
157
+ end
158
+ end
159
+
160
+ context '#<<' do
161
+ it 'adds element under its natural label, at end' do
162
+ subject << 'marvin'
163
+ subject.values.last.should == 'marvin'
164
+ subject[:marvin].should == 'marvin'
165
+ end
166
+ it 'replaces duplicate values' do
167
+ val = 'wocKET'
168
+ shouty_collection['WOCKET'].should == 'wocket'
169
+ shouty_collection['WOCKET'].should_not equal(val)
170
+ shouty_collection << val
171
+ shouty_collection['WOCKET'].should == 'wocKET'
172
+ shouty_collection['WOCKET'].should equal(val)
173
+ end
174
+ end
175
+
176
+ end
177
+
178
+ describe 'collections:', :model_spec, :collection_spec do
179
+
180
+ describe Gorillib::ListCollection do
181
+ let(:string_collection){ described_class.receive(%w[wocket in my pocket]) }
182
+ it_behaves_like 'a collection'
183
+ # it_behaves_like 'a keyed collection'
184
+ end
185
+
186
+ describe Gorillib::Collection, :receives_arrays => false do
187
+ let(:string_collection){ described_class.receive({:wocket => 'wocket', :in => 'in', :my => 'my', :pocket => 'pocket'}) }
188
+ let(:shouty_collection){ described_class.receive({'WOCKET' => 'wocket', 'IN' => 'in', 'MY' => 'my', 'POCKET' => 'pocket'}) }
189
+ it_behaves_like 'a collection', :receives_arrays => false
190
+ it_behaves_like 'a keyed collection'
191
+
192
+ # I only want the 'adopts the contents of an array' to run when the
193
+ # it_behaves_like group says it should be part of the specs.
194
+ it "needs travis's rspec help on the 'adopts the contents of an array' spec"
195
+
196
+ end
197
+
198
+ describe Gorillib::ModelCollection do
199
+ let(:string_collection){ described_class.receive(%w[wocket in my pocket], :to_sym, String) }
200
+ let(:shouty_collection){ described_class.receive(%w[wocket in my pocket], :upcase, String) }
201
+ it_behaves_like 'a collection'
202
+ it_behaves_like 'a keyed collection'
203
+ it_behaves_like 'an auto-keyed collection'
204
+ end
20
205
  end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require 'gorillib/model'
3
+ require 'gorillib/model/lint'
4
+
5
+ describe Gorillib::Model::Lint, :model_spec => true do
6
+ subject do
7
+ klass = Class.new{ include Gorillib::Model ; include Gorillib::Model::Lint ; field :bob, Integer }
8
+ klass.new
9
+ end
10
+
11
+ context '#read_attribute' do
12
+ it "raises an error if the field does not exist" do
13
+ ->{ subject.read_attribute(:fnord) }.should raise_error(Gorillib::Model::UnknownFieldError, /unknown field: fnord/)
14
+ end
15
+ end
16
+
17
+ context '#write_attribute' do
18
+ it "raises an error if the field does not exist" do
19
+ ->{ subject.write_attribute(:fnord, 8) }.should raise_error(Gorillib::Model::UnknownFieldError, /unknown field: fnord/)
20
+ end
21
+ end
22
+
23
+ context '#attribute_set?' do
24
+ it "raises an error if the field does not exist" do
25
+ ->{ subject.attribute_set?(:fnord) }.should raise_error(Gorillib::Model::UnknownFieldError, /unknown field: fnord/)
26
+ end
27
+ end
28
+ end