opium 1.0.0.beta
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.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +24 -0
- data/.travis.yml +17 -0
- data/Gemfile +4 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +71 -0
- data/Rakefile +10 -0
- data/lib/generators/opium/config_generator.rb +15 -0
- data/lib/generators/opium/model_generator.rb +33 -0
- data/lib/generators/opium/templates/config.yml +27 -0
- data/lib/generators/opium/templates/model.rb +10 -0
- data/lib/opium/config.rb +44 -0
- data/lib/opium/extensions/array.rb +10 -0
- data/lib/opium/extensions/boolean.rb +13 -0
- data/lib/opium/extensions/date.rb +18 -0
- data/lib/opium/extensions/date_time.rb +18 -0
- data/lib/opium/extensions/false_class.rb +7 -0
- data/lib/opium/extensions/float.rb +13 -0
- data/lib/opium/extensions/geo_point.rb +37 -0
- data/lib/opium/extensions/hash.rb +43 -0
- data/lib/opium/extensions/integer.rb +13 -0
- data/lib/opium/extensions/numeric.rb +7 -0
- data/lib/opium/extensions/object.rb +15 -0
- data/lib/opium/extensions/pointer.rb +20 -0
- data/lib/opium/extensions/regexp.rb +12 -0
- data/lib/opium/extensions/string.rb +20 -0
- data/lib/opium/extensions/time.rb +19 -0
- data/lib/opium/extensions/true_class.rb +7 -0
- data/lib/opium/extensions.rb +16 -0
- data/lib/opium/model/attributable.rb +37 -0
- data/lib/opium/model/callbacks.rb +38 -0
- data/lib/opium/model/connectable.rb +155 -0
- data/lib/opium/model/criteria.rb +123 -0
- data/lib/opium/model/dirty.rb +35 -0
- data/lib/opium/model/field.rb +31 -0
- data/lib/opium/model/fieldable.rb +57 -0
- data/lib/opium/model/findable.rb +20 -0
- data/lib/opium/model/kaminari/queryable.rb +46 -0
- data/lib/opium/model/kaminari/scopable.rb +15 -0
- data/lib/opium/model/kaminari.rb +4 -0
- data/lib/opium/model/persistable.rb +153 -0
- data/lib/opium/model/queryable.rb +150 -0
- data/lib/opium/model/scopable.rb +58 -0
- data/lib/opium/model/serialization.rb +13 -0
- data/lib/opium/model.rb +47 -0
- data/lib/opium/railtie.rb +14 -0
- data/lib/opium/user.rb +44 -0
- data/lib/opium/version.rb +3 -0
- data/lib/opium.rb +9 -0
- data/opium.gemspec +40 -0
- data/spec/opium/config/opium.yml +5 -0
- data/spec/opium/config_spec.rb +56 -0
- data/spec/opium/extensions/array_spec.rb +34 -0
- data/spec/opium/extensions/boolean_spec.rb +28 -0
- data/spec/opium/extensions/date_spec.rb +55 -0
- data/spec/opium/extensions/date_time_spec.rb +55 -0
- data/spec/opium/extensions/float_spec.rb +42 -0
- data/spec/opium/extensions/geo_point_spec.rb +55 -0
- data/spec/opium/extensions/hash_spec.rb +76 -0
- data/spec/opium/extensions/integer_spec.rb +42 -0
- data/spec/opium/extensions/object_spec.rb +24 -0
- data/spec/opium/extensions/pointer_spec.rb +28 -0
- data/spec/opium/extensions/regexp_spec.rb +23 -0
- data/spec/opium/extensions/string_spec.rb +65 -0
- data/spec/opium/extensions/time_spec.rb +55 -0
- data/spec/opium/model/attributable_spec.rb +45 -0
- data/spec/opium/model/callbacks_spec.rb +59 -0
- data/spec/opium/model/connectable_spec.rb +218 -0
- data/spec/opium/model/criteria_spec.rb +285 -0
- data/spec/opium/model/dirty_spec.rb +39 -0
- data/spec/opium/model/fieldable_spec.rb +133 -0
- data/spec/opium/model/findable_spec.rb +57 -0
- data/spec/opium/model/kaminari/queryable_spec.rb +22 -0
- data/spec/opium/model/kaminari/scopable_spec.rb +20 -0
- data/spec/opium/model/kaminari_spec.rb +104 -0
- data/spec/opium/model/persistable_spec.rb +367 -0
- data/spec/opium/model/queryable_spec.rb +338 -0
- data/spec/opium/model/scopable_spec.rb +115 -0
- data/spec/opium/model/serialization_spec.rb +51 -0
- data/spec/opium/model_spec.rb +49 -0
- data/spec/opium/user_spec.rb +195 -0
- data/spec/opium_spec.rb +5 -0
- data/spec/spec_helper.rb +25 -0
- metadata +400 -0
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe Opium::Model::Connectable do
|
4
|
+
before do
|
5
|
+
stub_const( 'Model', Class.new do |klass|
|
6
|
+
include Opium::Model::Connectable
|
7
|
+
stub('model_name').and_return(ActiveModel::Name.new(klass, nil, 'Model'))
|
8
|
+
end )
|
9
|
+
end
|
10
|
+
|
11
|
+
after do
|
12
|
+
Opium::Model::Criteria.models.clear
|
13
|
+
end
|
14
|
+
|
15
|
+
subject { Model }
|
16
|
+
let( :response ) { double('Response').tap {|r| allow(r).to receive(:body) } }
|
17
|
+
|
18
|
+
it { is_expected.to respond_to( :connection, :reset_connection! ) }
|
19
|
+
it { is_expected.to respond_to( :object_prefix, :no_object_prefix! ) }
|
20
|
+
it { is_expected.to respond_to( :as_resource, :resource_name ).with(1).argument }
|
21
|
+
it { is_expected.to respond_to( :http_get, :http_post, :http_delete ).with(1).argument }
|
22
|
+
it { is_expected.to respond_to( :http_put ).with(2).arguments }
|
23
|
+
it { is_expected.to respond_to( :requires_heightened_privileges!, :requires_heightened_privileges? ) }
|
24
|
+
|
25
|
+
describe '.object_prefix' do
|
26
|
+
it { expect( subject.object_prefix ).to eq 'classes' }
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.reset_connection!' do
|
30
|
+
it { expect { subject.reset_connection! }.to change( subject, :connection ) }
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '.no_object_prefix!' do
|
34
|
+
after do
|
35
|
+
Model.instance_variable_set :@object_prefix, nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'has an empty object_prefix' do
|
39
|
+
expect { subject.no_object_prefix! }.to change( subject, :object_prefix ).from( 'classes' ).to( '' )
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '.as_resource' do
|
44
|
+
it { expect { subject.as_resource }.to raise_exception(ArgumentError) }
|
45
|
+
it do
|
46
|
+
expect {|b| subject.as_resource( :masked, &b ) }.to yield_control
|
47
|
+
end
|
48
|
+
|
49
|
+
it { expect { subject.as_resource( :masked ) {} }.to_not change( subject, :resource_name ) }
|
50
|
+
|
51
|
+
it 'changes the .resource_name within a block' do
|
52
|
+
subject.as_resource( :masked ) do
|
53
|
+
expect( subject.resource_name ).to eq 'masked'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe '.resource_name' do
|
59
|
+
it 'is based off the class name' do
|
60
|
+
subject.resource_name.should == 'classes/Model'
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'is able to include a resource id' do
|
64
|
+
subject.resource_name('abc123').should == 'classes/Model/abc123'
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'demodulizes the model_name' do
|
68
|
+
namespaced_model = subject
|
69
|
+
namespaced_model.stub(:model_name).and_return(ActiveModel::Name.new(subject, nil, 'Namespace::Model'))
|
70
|
+
namespaced_model.should_receive(:resource_name).and_call_original
|
71
|
+
namespaced_model.model_name.name.should == 'Namespace::Model'
|
72
|
+
namespaced_model.resource_name.should == 'classes/Model'
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
shared_examples_for 'a sent-headers response' do |method, *args|
|
77
|
+
let(:options) { args.last.is_a?(Hash) ? args.pop : {} }
|
78
|
+
let(:params) { args.push options.merge( sent_headers: true ) }
|
79
|
+
let(:request) { subject.send( :"http_#{method}", *params ) }
|
80
|
+
|
81
|
+
it { expect { request }.to_not raise_exception }
|
82
|
+
|
83
|
+
it { expect( request ).to be_a(Hash) }
|
84
|
+
|
85
|
+
if [:put, :post].include? method
|
86
|
+
context 'when including a JSON encoded body' do
|
87
|
+
it { expect( request ).to have_key 'Content-Type' }
|
88
|
+
it { expect( request['Content-Type'] ).to eq 'application/json' }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'when not .requires_heightened_prvileges?' do
|
93
|
+
before { subject.instance_variable_set :@requires_heightened_privileges, nil }
|
94
|
+
|
95
|
+
it { expect( subject.requires_heightened_privileges? ).to eq false }
|
96
|
+
|
97
|
+
it { expect( request.keys ).to include( 'X-Parse-Application-Id', 'X-Parse-Rest-Api-Key' ) }
|
98
|
+
it { expect( request.keys ).to_not include( 'X-Parse-Master-Key', 'X-Parse-Session-Token' ) }
|
99
|
+
end
|
100
|
+
|
101
|
+
unless method == :get
|
102
|
+
context 'when .requires_heightened_privileges?' do
|
103
|
+
subject do
|
104
|
+
Model.requires_heightened_privileges!
|
105
|
+
Model.send( :"http_#{method}", *params )
|
106
|
+
end
|
107
|
+
|
108
|
+
after { Model.instance_variable_set :@requires_heightened_privileges, nil }
|
109
|
+
|
110
|
+
it { expect( subject.keys ).to include( 'X-Parse-Application-Id', 'X-Parse-Master-Key' ) }
|
111
|
+
it { expect( subject.keys ).to_not include( 'X-Parse-Rest-Api-Key', 'X-Parse-Session-Token' ) }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe '.http_get' do
|
117
|
+
before do
|
118
|
+
stub_request( :get, %r{https://api.parse.com/1/classes/Model(\?.+)?} ).
|
119
|
+
to_return( status: 200, body: { objectId: 'abc123' }.to_json, headers: { 'Content-Type' => 'application/json' } )
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'executes a :get on :connection' do
|
123
|
+
subject.connection.should_receive(:get) { response }.with( 'classes/Model' )
|
124
|
+
subject.http_get
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'uses a resource id if passed an :id option' do
|
128
|
+
subject.connection.should_receive(:get) { response }.with( 'classes/Model/abcd1234' )
|
129
|
+
subject.http_get id: 'abcd1234'
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'returns a raw response if passed :raw_response' do
|
133
|
+
subject.http_get( raw_response: true ).should be_a( Faraday::Response )
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'sets params for everything within the :query hash' do
|
137
|
+
response = subject.http_get query: { keys: 'title,price', order: '-title', limit: 5 }, raw_response: true
|
138
|
+
request_params = response.env.url.query.split('&').map {|p| p.split('=')}
|
139
|
+
request_params.should =~ { 'keys' => 'title%2Cprice', 'order' => '-title', 'limit' => '5' }
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'special cases a :query key of "where", making it json encoded' do
|
143
|
+
criteria = { price: { '$lte' => 5 } }
|
144
|
+
response = subject.http_get query: { where: criteria }, raw_response: true
|
145
|
+
query = URI.decode response.env.url.query
|
146
|
+
query.should == "where=#{criteria.to_json}"
|
147
|
+
end
|
148
|
+
|
149
|
+
it_behaves_like 'a sent-headers response', :get
|
150
|
+
end
|
151
|
+
|
152
|
+
describe '.http_post' do
|
153
|
+
it 'executes a :post on :connection' do
|
154
|
+
subject.connection.should_receive(:post) { response }
|
155
|
+
subject.http_post( {} )
|
156
|
+
end
|
157
|
+
|
158
|
+
it_behaves_like 'a sent-headers response', :post, {}, {}
|
159
|
+
end
|
160
|
+
|
161
|
+
describe '.http_put' do
|
162
|
+
it 'executes a :put on :connection' do
|
163
|
+
subject.connection.should_receive(:put) { response }
|
164
|
+
subject.http_put( 'abcd1234', {} )
|
165
|
+
end
|
166
|
+
|
167
|
+
it_behaves_like 'a sent-headers response', :put, 'abcd1234', {}, {}
|
168
|
+
end
|
169
|
+
|
170
|
+
describe '.http_delete' do
|
171
|
+
it 'executes a :delete on :connection' do
|
172
|
+
subject.connection.should_receive(:delete) { response }
|
173
|
+
subject.http_delete( 'abcd1234' )
|
174
|
+
end
|
175
|
+
|
176
|
+
it_behaves_like 'a sent-headers response', :delete, 'abcd1234'
|
177
|
+
end
|
178
|
+
|
179
|
+
describe '.requires_heightened_privileges!' do
|
180
|
+
shared_examples_for 'it has heightened privileges on' do |method, *args|
|
181
|
+
before do
|
182
|
+
resource = args.first.is_a?(Hash) ? '' : "/#{args.first}"
|
183
|
+
to_send = method == :delete ? {} : {body: "{}"}
|
184
|
+
headers = method == :delete ? {} : {content_type: 'application/json'}
|
185
|
+
stub_request(method, "https://api.parse.com/1/classes/Model#{resource}").
|
186
|
+
with(
|
187
|
+
to_send.merge(
|
188
|
+
headers: headers.merge( x_parse_application_id: 'PARSE_APP_ID', x_parse_master_key: 'PARSE_MASTER_KEY' )
|
189
|
+
)
|
190
|
+
).to_return(:status => 200, :body => "{}", :headers => {content_type: 'application/json'})
|
191
|
+
end
|
192
|
+
|
193
|
+
it "causes :http_#{method} to add a master-key header" do
|
194
|
+
subject.requires_heightened_privileges!
|
195
|
+
expect { subject.send( :"http_#{method}", *args ) }.to_not raise_exception
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
after { subject.instance_variable_set :@requires_heightened_privileges, nil }
|
200
|
+
|
201
|
+
it { subject.requires_heightened_privileges!.should == true }
|
202
|
+
|
203
|
+
it_behaves_like 'it has heightened privileges on', :post, {}
|
204
|
+
it_behaves_like 'it has heightened privileges on', :put, 'abcd1234', {}
|
205
|
+
it_behaves_like 'it has heightened privileges on', :delete, 'abcd1234'
|
206
|
+
end
|
207
|
+
|
208
|
+
describe '.requires_heightened_privileges?' do
|
209
|
+
after { subject.instance_variable_set :@requires_heightened_privileges, nil }
|
210
|
+
|
211
|
+
it { subject.requires_heightened_privileges?.should == false }
|
212
|
+
|
213
|
+
it 'is true if privileges have been raised' do
|
214
|
+
subject.requires_heightened_privileges!
|
215
|
+
subject.requires_heightened_privileges?.should == true
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,285 @@
|
|
1
|
+
require 'spec_helper.rb'
|
2
|
+
|
3
|
+
describe Opium::Model::Criteria do
|
4
|
+
before do
|
5
|
+
stub_const( 'Game', Class.new do |klass|
|
6
|
+
include Opium::Model
|
7
|
+
field :title, type: String
|
8
|
+
field :price, type: Float
|
9
|
+
|
10
|
+
stub('model_name').and_return(ActiveModel::Name.new(klass, nil, 'Game'))
|
11
|
+
end )
|
12
|
+
|
13
|
+
stub_request( :get, 'https://api.parse.com/1/classes/Game?count=1' ).with( body: {} ).
|
14
|
+
to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, body: {
|
15
|
+
count: 10,
|
16
|
+
results: [
|
17
|
+
{ objectId: 'abcd1234', createdAt: Time.now - 50000, title: 'Skyrim', price: 45.99 },
|
18
|
+
{ objectId: 'efgh5678', createdAt: Time.now - 10000, title: 'Terraria', price: 15.99 }
|
19
|
+
]
|
20
|
+
}.to_json )
|
21
|
+
end
|
22
|
+
|
23
|
+
after do
|
24
|
+
Opium::Model::Criteria.models.clear
|
25
|
+
end
|
26
|
+
|
27
|
+
subject { Opium::Model::Criteria.new( 'Object' ) }
|
28
|
+
|
29
|
+
it { is_expected.to be_a( Opium::Model::Queryable::ClassMethods ) }
|
30
|
+
it { is_expected.to be_an( Enumerable ) }
|
31
|
+
it { is_expected.to respond_to( :chain ) }
|
32
|
+
it { is_expected.to respond_to( :constraints, :variables ) }
|
33
|
+
it { is_expected.to respond_to( :update_constraint, :update_variable ).with(2).arguments }
|
34
|
+
it { is_expected.to respond_to( :constraints?, :variables? ) }
|
35
|
+
it { is_expected.to respond_to( :model, :model_name ) }
|
36
|
+
it { is_expected.to respond_to( :empty? ) }
|
37
|
+
it { is_expected.to respond_to( :to_parse ) }
|
38
|
+
it { is_expected.to respond_to( :each ) }
|
39
|
+
it { is_expected.to respond_to( :to_a ) }
|
40
|
+
it { is_expected.to respond_to( :count, :total_count ) }
|
41
|
+
|
42
|
+
describe '#chain' do
|
43
|
+
it 'returns a copy of the object' do
|
44
|
+
result = subject.chain
|
45
|
+
result.should be_a( Opium::Model::Criteria )
|
46
|
+
result.should == subject
|
47
|
+
result.should_not equal( subject )
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#update_constraint' do
|
52
|
+
it 'chains the criteria and alter the specified constraint on the copy' do
|
53
|
+
result = subject.update_constraint( :order, ['title', 1] )
|
54
|
+
result.should be_a( Opium::Model::Criteria )
|
55
|
+
result.should_not equal( subject )
|
56
|
+
result.should_not == subject
|
57
|
+
result.constraints.should have_key( :order )
|
58
|
+
result.constraints[:order].should == ['title', 1]
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'merges hash-valued constraints' do
|
62
|
+
subject.constraints['where'] = { score: { '$lte' => 321 } }
|
63
|
+
result = subject.update_constraint( 'where', price: { '$gte' => 123 } )
|
64
|
+
result.constraints['where'].should =~ { 'score' => { '$lte' => 321 }, 'price' => { '$gte' => 123 } }
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'deep merges hash-valued constraints' do
|
68
|
+
subject.constraints['where'] = { score: { '$lte' => 321 } }
|
69
|
+
result = subject.update_constraint( 'where', score: { '$gte' => 123 } )
|
70
|
+
result.constraints['where'].should =~ { 'score' => { '$lte' => 321, '$gte' => 123 } }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#update_variable' do
|
75
|
+
it 'chains the criteria and alter the specified instance variable on the copy' do
|
76
|
+
result = subject.update_variable( :cache, true )
|
77
|
+
result.should be_an( Opium::Model::Criteria )
|
78
|
+
result.should_not equal( subject )
|
79
|
+
result.should_not == subject
|
80
|
+
result.constraints.should_not have_key( :cache )
|
81
|
+
result.variables.should have_key( :cache )
|
82
|
+
result.variables[:cache].should == true
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe '#==' do
|
87
|
+
let( :first ) { Opium::Model::Criteria.new( 'Object' ).update_constraint( :order, ['title', 1] ) }
|
88
|
+
let( :second ) { Opium::Model::Criteria.new( 'Object' ).update_constraint( :order, ['title', 1] ) }
|
89
|
+
|
90
|
+
it 'should not affect :equal?' do
|
91
|
+
first.should_not equal( second )
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'is based on the criteria constraints' do
|
95
|
+
first.should == second
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'is based on the criteria variables' do
|
99
|
+
third = first.update_variable( :cache, true )
|
100
|
+
second.should_not == third
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#criteria' do
|
105
|
+
subject { Opium::Model::Criteria.new( 'Object' ).update_constraint( :order, ['title', 1] ) }
|
106
|
+
|
107
|
+
it 'is == to self' do
|
108
|
+
subject.criteria.should == subject
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'is not a duplicate of self' do
|
112
|
+
subject.criteria.should equal( subject )
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe '#model' do
|
117
|
+
subject { Opium::Model::Criteria.new( 'Game' ) }
|
118
|
+
|
119
|
+
it 'is the constantized version of :model_name' do
|
120
|
+
subject.model_name.should == 'Game'
|
121
|
+
subject.model.should == Game
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe '#empty?' do
|
126
|
+
before do
|
127
|
+
stub_request(:get, "https://api.parse.com/1/classes/Game?count=1&where=%7B%22price%22:%7B%22$gte%22:9000.0%7D%7D").
|
128
|
+
to_return(
|
129
|
+
status: 200,
|
130
|
+
body: {
|
131
|
+
count: 0,
|
132
|
+
results: []
|
133
|
+
}.to_json,
|
134
|
+
headers: { content_type: 'application/json' }
|
135
|
+
)
|
136
|
+
end
|
137
|
+
|
138
|
+
subject { Game.criteria }
|
139
|
+
|
140
|
+
it { expect { subject.empty? }.to_not raise_exception }
|
141
|
+
it do
|
142
|
+
expect( subject ).to receive(:count).once.and_call_original
|
143
|
+
subject.empty?
|
144
|
+
end
|
145
|
+
|
146
|
+
it 'returns true if count is 0' do
|
147
|
+
subject.gte( price: 9000.0 ).empty?.should == true
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'returns false if count is not 0' do
|
151
|
+
subject.criteria.empty? == false
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
describe '#constraints?' do
|
156
|
+
it 'returns true if count is not the only constraint' do
|
157
|
+
Game.limit( 10 ).constraints?.should == true
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'returns false if count is the only constraint' do
|
161
|
+
Game.criteria.constraints?.should == false
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe '#each' do
|
166
|
+
subject { Game.criteria }
|
167
|
+
|
168
|
+
context 'without a block' do
|
169
|
+
it 'returns an Enumerator' do
|
170
|
+
subject.each.should be_a( Enumerator )
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context 'with a block' do
|
175
|
+
it "calls its :model's :http_get" do
|
176
|
+
subject.model.should receive(:http_get).with(query: subject.constraints)
|
177
|
+
subject.each {|model| }
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'yields to its block any results it finds' do
|
181
|
+
expect {|b| subject.each &b }.to yield_control.twice
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'yields to its block Opium::Model objects (Game in context)' do
|
185
|
+
expect {|b| subject.each &b }.to yield_successive_args(Opium::Model, Opium::Model)
|
186
|
+
expect {|b| subject.each &b }.to yield_successive_args(Game, Game)
|
187
|
+
end
|
188
|
+
|
189
|
+
it "calls its :model's :http_get when counting" do
|
190
|
+
subject.model.should receive(:http_get).with(query: subject.constraints).twice
|
191
|
+
subject.each {|model| }
|
192
|
+
subject.each.count
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
context 'when #cached?' do
|
197
|
+
subject { Game.criteria.cache }
|
198
|
+
|
199
|
+
it 'calls its :model\'s :http_get only once' do
|
200
|
+
subject.model.should receive(:http_get).with(query: subject.constraints).once
|
201
|
+
subject.each {|model| }
|
202
|
+
subject.each {|model| }
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'yields to its block any results it finds' do
|
206
|
+
expect {|b| subject.each &b }.to yield_control.twice
|
207
|
+
expect {|b| subject.each &b }.to yield_control.twice
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'yields to its block Opium::Model objects (Game in context)' do
|
211
|
+
expect {|b| subject.each &b }.to yield_successive_args(Opium::Model, Opium::Model)
|
212
|
+
expect {|b| subject.each &b }.to yield_successive_args(Game, Game)
|
213
|
+
end
|
214
|
+
|
215
|
+
it "does not call its :model's :http_get when counting" do
|
216
|
+
subject.model.should receive(:http_get).with(query: subject.constraints).once
|
217
|
+
subject.each {|model| }
|
218
|
+
subject.each.count
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
describe '#uncache' do
|
224
|
+
subject { Game.criteria.cache }
|
225
|
+
|
226
|
+
it 'causes #each to call its :model\'s :http_get twice' do
|
227
|
+
subject.model.should receive(:http_get).with(query: subject.constraints).twice
|
228
|
+
subject.each {|model| }
|
229
|
+
subject.uncache.each {|model| }
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'deletes its @cache' do
|
233
|
+
subject.each {|model| }
|
234
|
+
subject.uncache.instance_variable_get(:@cache).should be_nil
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe '#count' do
|
239
|
+
subject { Game.criteria }
|
240
|
+
|
241
|
+
it { expect { subject.count }.to_not raise_exception }
|
242
|
+
it do
|
243
|
+
expect( subject ).to receive(:each).and_call_original
|
244
|
+
subject.count
|
245
|
+
end
|
246
|
+
|
247
|
+
it 'equals the number of items from #each' do
|
248
|
+
expect( subject.count ).to be == 2
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe '#total_count' do
|
253
|
+
subject { Game.criteria }
|
254
|
+
|
255
|
+
it { expect { subject.total_count }.to_not raise_exception }
|
256
|
+
it do
|
257
|
+
expect( subject ).to receive(:each).and_call_original
|
258
|
+
subject.total_count
|
259
|
+
end
|
260
|
+
|
261
|
+
it "equals the 'count' result returned from parse" do
|
262
|
+
expect( subject.total_count ).to be == 10
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
describe '#to_parse' do
|
267
|
+
subject { Game.criteria }
|
268
|
+
|
269
|
+
it { expect( subject.to_parse ).to be_a( Hash ) }
|
270
|
+
|
271
|
+
it 'has a "query" key, if a "where" constraint exists, containing a "where" and a "className"' do
|
272
|
+
Game.between( price: 5..10 ).to_parse.tap do |criteria|
|
273
|
+
criteria.should have_key( 'query' )
|
274
|
+
criteria['query'].should =~ { 'where' => { 'price' => { '$gte' => 5, '$lte' => 10 } }, 'className' => 'Game' }
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
it 'should have a "key" key, if a "keys" constraint exists' do
|
279
|
+
Game.keys( :price ).to_parse.tap do |criteria|
|
280
|
+
criteria.should have_key( 'key' )
|
281
|
+
criteria['key'].should == 'price'
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Opium::Model::Dirty do
|
4
|
+
let( :model ) { Class.new { def initialize(a = {}); end; def save(o = {}); end; include Opium::Model::Dirty; } }
|
5
|
+
|
6
|
+
describe 'instance' do
|
7
|
+
subject { model.new }
|
8
|
+
|
9
|
+
it { should respond_to( :changed?, :changed, :changed_attributes ) }
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'when included in a model' do
|
13
|
+
before do
|
14
|
+
stub_const( 'Game', Class.new do
|
15
|
+
include Opium::Model
|
16
|
+
field :title, type: String
|
17
|
+
end )
|
18
|
+
stub_request(:post, "https://api.parse.com/1/classes/Game").with(
|
19
|
+
body: "{\"title\":\"Skyrim\"}",
|
20
|
+
headers: {'Content-Type'=>'application/json'}
|
21
|
+
).to_return(
|
22
|
+
body: { objectId: 'abcd1234', createdAt: Time.now.to_s }.to_json,
|
23
|
+
status: 200,
|
24
|
+
headers: { 'Content-Type' => 'application/json', Location: 'https://api.parse.com/1/classes/Game/abcd1234' }
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
subject { Game.new( title: 'Skyrim' ) }
|
29
|
+
|
30
|
+
it 'when instantiated, should not be changed' do
|
31
|
+
subject.should_not be_changed
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'when saved, should receive #changes_applied' do
|
35
|
+
subject.should receive(:changes_applied)
|
36
|
+
subject.save
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Opium::Model::Fieldable do
|
4
|
+
describe "in a model without explicit fields" do
|
5
|
+
let( :model ) { Class.new { include Opium::Model } }
|
6
|
+
|
7
|
+
it { model.should respond_to( :fields ).with(0).arguments }
|
8
|
+
it "should have #fields [:id, :created_at, :updated_at]" do
|
9
|
+
model.fields.should be_a_kind_of( Hash )
|
10
|
+
model.fields.should_not be_empty
|
11
|
+
model.fields.keys.should =~ %w[id created_at updated_at]
|
12
|
+
model.fields.values.each do |f|
|
13
|
+
f.should be_a_kind_of( Opium::Model::Field )
|
14
|
+
f.should be_readonly
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "in a model with fields" do
|
20
|
+
let( :model ) do
|
21
|
+
Class.new do
|
22
|
+
include Opium::Model
|
23
|
+
field :name, type: String, default: "default"
|
24
|
+
field :price, type: Float, default: -> { 5.0 * 2 }
|
25
|
+
field :no_cast
|
26
|
+
field :cannot_be_directly_changed, readonly: true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it { model.should respond_to( :field ).with(2).arguments }
|
31
|
+
it { model.should respond_to( :fields ).with(0).arguments }
|
32
|
+
it { model.should respond_to( :ruby_canonical_field_names ) }
|
33
|
+
it { model.should respond_to( :parse_canonical_field_names ) }
|
34
|
+
|
35
|
+
it "should have #fields for every #field" do
|
36
|
+
model.fields.should be_a_kind_of( Hash )
|
37
|
+
model.fields.should_not be_empty
|
38
|
+
model.fields.keys.should =~ %w[name price no_cast cannot_be_directly_changed id created_at updated_at]
|
39
|
+
model.fields.values.each do |f|
|
40
|
+
f.should_not be_nil
|
41
|
+
f.should be_a_kind_of( Opium::Model::Field )
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "each #fields should have a #name, #as, #type, #default, #readonly, #readonly?" do
|
46
|
+
model.fields.values.each do |f|
|
47
|
+
f.should respond_to(:name, :as, :type, :default, :readonly, :readonly?)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "each #fields should have the type they were defined with" do
|
52
|
+
expected = {name: String, price: Float, no_cast: Object, cannot_be_directly_changed: Object, id: String, created_at: DateTime, updated_at: DateTime}
|
53
|
+
model.fields.values.each do |f|
|
54
|
+
f.type.should == expected[ f.name.to_sym ]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "each #fields should have the default they were defined with" do
|
59
|
+
expected = {name: "default", price: 10.0, no_cast: nil}
|
60
|
+
model.fields.values.each do |f|
|
61
|
+
f.default.should == expected[ f.name.to_sym ]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it "each #fields should convert ruby names to parse names" do
|
66
|
+
expected = {name: "name", price: "price", no_cast: "noCast", cannot_be_directly_changed: "cannotBeDirectlyChanged", id: "objectId", created_at: "createdAt", updated_at: "updatedAt"}
|
67
|
+
model.fields.values.each do |f|
|
68
|
+
f.name_to_parse.should == expected[ f.name.to_sym ]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should return the canonical ruby form of a given field name" do
|
73
|
+
expected = {name: 'name', price: 'price', noCast: 'no_cast', cannotBeDirectlyChanged: 'cannot_be_directly_changed', objectId: 'id', createdAt: 'created_at', updatedAt: 'updated_at'}
|
74
|
+
expected.each do |field_alias, expected_field_name|
|
75
|
+
model.ruby_canonical_field_names[field_alias].should == expected_field_name
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should return the canonical parse form of a given field name" do
|
80
|
+
expected = {name: 'name', price: 'price', no_cast: 'noCast', cannot_be_directly_changed: 'cannotBeDirectlyChanged', id: 'objectId', created_at: 'createdAt', updated_at: 'updatedAt'}
|
81
|
+
expected.each do |field_alias, expected_field_name|
|
82
|
+
model.parse_canonical_field_names[field_alias].should == expected_field_name
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it { model.should respond_to( :default_attributes ) }
|
87
|
+
|
88
|
+
it "default_attributes should return its #fields default" do
|
89
|
+
expected = {"name" => "default", "price" => 10.0, "no_cast" => nil, "cannot_be_directly_changed" => nil, "id" => nil, "created_at" => nil, "updated_at" => nil}
|
90
|
+
model.default_attributes.should == expected
|
91
|
+
end
|
92
|
+
|
93
|
+
describe "instance" do
|
94
|
+
subject { model.new }
|
95
|
+
|
96
|
+
it "should not have setters for readonly fields" do
|
97
|
+
should_not respond_to( :cannot_be_directly_changed= )
|
98
|
+
end
|
99
|
+
|
100
|
+
{name: "42", price: 42.0}.each do |field_name, expected_value|
|
101
|
+
it "should have a getter and setter for its fields" do
|
102
|
+
should respond_to( field_name ).with(0).arguments
|
103
|
+
should respond_to( :"#{field_name}=" ).with(1).argument
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should have a dirty tracking method for its fields" do
|
107
|
+
should respond_to( :"#{field_name}_will_change!" )
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should receive a dirty tracking update when the setter is called with a new value" do
|
111
|
+
subject.should_receive :"#{field_name}_will_change!"
|
112
|
+
subject.send(:"#{field_name}=", "changed!")
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should not receive a dirty tracking update when the setter is called with the current value" do
|
116
|
+
subject.send(:"#{field_name}=", "current")
|
117
|
+
subject.should_not_receive :"#{field_name}_will_change!"
|
118
|
+
subject.send(:"#{field_name}=", "current")
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should call the :to_ruby conversion method on the field type on setting" do
|
122
|
+
model.fields[field_name].type.should_receive(:to_ruby).at_least(:once)
|
123
|
+
subject.send(:"#{field_name}=", "42")
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should convert the value passed to its setter to the field's type" do
|
127
|
+
subject.send(:"#{field_name}=", "42")
|
128
|
+
subject.send(:"#{field_name}").should == expected_value
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|