gorillib 0.4.0pre → 0.4.1pre
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +36 -1
- data/Gemfile +23 -19
- data/Guardfile +1 -1
- data/Rakefile +31 -31
- data/TODO.md +2 -30
- data/VERSION +1 -1
- data/examples/builder/ironfan.rb +4 -4
- data/gorillib.gemspec +40 -25
- data/lib/gorillib/array/average.rb +13 -0
- data/lib/gorillib/array/sorted_median.rb +11 -0
- data/lib/gorillib/array/sorted_percentile.rb +11 -0
- data/lib/gorillib/array/sorted_sample.rb +12 -0
- data/lib/gorillib/builder.rb +8 -14
- data/lib/gorillib/collection/has_collection.rb +31 -31
- data/lib/gorillib/collection/list_collection.rb +58 -0
- data/lib/gorillib/collection/model_collection.rb +63 -0
- data/lib/gorillib/collection.rb +57 -85
- data/lib/gorillib/logger/log.rb +26 -22
- data/lib/gorillib/model/base.rb +52 -39
- data/lib/gorillib/model/doc_string.rb +15 -0
- data/lib/gorillib/model/factories.rb +56 -61
- data/lib/gorillib/model/lint.rb +24 -0
- data/lib/gorillib/model/serialization.rb +12 -2
- data/lib/gorillib/model/validate.rb +2 -2
- data/lib/gorillib/pathname.rb +21 -6
- data/lib/gorillib/some.rb +2 -0
- data/lib/gorillib/type/extended.rb +0 -2
- data/lib/gorillib/type/url.rb +9 -0
- data/lib/gorillib/utils/console.rb +4 -1
- data/notes/HOWTO.md +22 -0
- data/notes/bucket.md +155 -0
- data/notes/builder.md +170 -0
- data/notes/collection.md +81 -0
- data/notes/factories.md +86 -0
- data/notes/model-overlay.md +209 -0
- data/notes/model.md +135 -0
- data/notes/structured-data-classes.md +127 -0
- data/spec/array/average_spec.rb +24 -0
- data/spec/array/sorted_median_spec.rb +18 -0
- data/spec/array/sorted_percentile_spec.rb +24 -0
- data/spec/array/sorted_sample_spec.rb +28 -0
- data/spec/gorillib/builder_spec.rb +46 -28
- data/spec/gorillib/collection_spec.rb +195 -10
- data/spec/gorillib/model/lint_spec.rb +28 -0
- data/spec/gorillib/model/record/factories_spec.rb +27 -13
- data/spec/gorillib/model/serialization_spec.rb +3 -5
- data/spec/gorillib/model_spec.rb +86 -104
- data/spec/spec_helper.rb +2 -1
- data/spec/support/gorillib_test_helpers.rb +83 -7
- data/spec/support/model_test_helpers.rb +9 -28
- metadata +52 -44
- data/lib/gorillib/configurable.rb +0 -28
- data/spec/gorillib/configurable_spec.rb +0 -62
- 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
|
-
|
13
|
-
|
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
|
-
|
32
|
+
let(:subject_class ){ car_class }
|
17
33
|
it 'type-converts values' do
|
18
|
-
obj =
|
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 =
|
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 '
|
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 ".
|
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,
|
69
|
-
subject.should_receive(:read_attribute).with(:doors).at_least(:once).and_return(
|
70
|
-
subject.doors.should ==
|
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,
|
75
|
-
result = subject.doors(
|
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,
|
99
|
-
wildcat.should_receive(:read_attribute).with(:doors).at_least(:once).and_return(
|
100
|
-
wildcat.doors.should ==
|
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,
|
105
|
-
result = wildcat.doors(
|
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::
|
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::
|
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::
|
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::
|
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/
|
5
|
-
require 'gorillib/
|
6
|
-
#
|
4
|
+
require 'gorillib/collection/model_collection'
|
5
|
+
require 'gorillib/collection/list_collection'
|
7
6
|
require 'model_test_helpers'
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|