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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +24 -0
  4. data/.travis.yml +17 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +11 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +71 -0
  9. data/Rakefile +10 -0
  10. data/lib/generators/opium/config_generator.rb +15 -0
  11. data/lib/generators/opium/model_generator.rb +33 -0
  12. data/lib/generators/opium/templates/config.yml +27 -0
  13. data/lib/generators/opium/templates/model.rb +10 -0
  14. data/lib/opium/config.rb +44 -0
  15. data/lib/opium/extensions/array.rb +10 -0
  16. data/lib/opium/extensions/boolean.rb +13 -0
  17. data/lib/opium/extensions/date.rb +18 -0
  18. data/lib/opium/extensions/date_time.rb +18 -0
  19. data/lib/opium/extensions/false_class.rb +7 -0
  20. data/lib/opium/extensions/float.rb +13 -0
  21. data/lib/opium/extensions/geo_point.rb +37 -0
  22. data/lib/opium/extensions/hash.rb +43 -0
  23. data/lib/opium/extensions/integer.rb +13 -0
  24. data/lib/opium/extensions/numeric.rb +7 -0
  25. data/lib/opium/extensions/object.rb +15 -0
  26. data/lib/opium/extensions/pointer.rb +20 -0
  27. data/lib/opium/extensions/regexp.rb +12 -0
  28. data/lib/opium/extensions/string.rb +20 -0
  29. data/lib/opium/extensions/time.rb +19 -0
  30. data/lib/opium/extensions/true_class.rb +7 -0
  31. data/lib/opium/extensions.rb +16 -0
  32. data/lib/opium/model/attributable.rb +37 -0
  33. data/lib/opium/model/callbacks.rb +38 -0
  34. data/lib/opium/model/connectable.rb +155 -0
  35. data/lib/opium/model/criteria.rb +123 -0
  36. data/lib/opium/model/dirty.rb +35 -0
  37. data/lib/opium/model/field.rb +31 -0
  38. data/lib/opium/model/fieldable.rb +57 -0
  39. data/lib/opium/model/findable.rb +20 -0
  40. data/lib/opium/model/kaminari/queryable.rb +46 -0
  41. data/lib/opium/model/kaminari/scopable.rb +15 -0
  42. data/lib/opium/model/kaminari.rb +4 -0
  43. data/lib/opium/model/persistable.rb +153 -0
  44. data/lib/opium/model/queryable.rb +150 -0
  45. data/lib/opium/model/scopable.rb +58 -0
  46. data/lib/opium/model/serialization.rb +13 -0
  47. data/lib/opium/model.rb +47 -0
  48. data/lib/opium/railtie.rb +14 -0
  49. data/lib/opium/user.rb +44 -0
  50. data/lib/opium/version.rb +3 -0
  51. data/lib/opium.rb +9 -0
  52. data/opium.gemspec +40 -0
  53. data/spec/opium/config/opium.yml +5 -0
  54. data/spec/opium/config_spec.rb +56 -0
  55. data/spec/opium/extensions/array_spec.rb +34 -0
  56. data/spec/opium/extensions/boolean_spec.rb +28 -0
  57. data/spec/opium/extensions/date_spec.rb +55 -0
  58. data/spec/opium/extensions/date_time_spec.rb +55 -0
  59. data/spec/opium/extensions/float_spec.rb +42 -0
  60. data/spec/opium/extensions/geo_point_spec.rb +55 -0
  61. data/spec/opium/extensions/hash_spec.rb +76 -0
  62. data/spec/opium/extensions/integer_spec.rb +42 -0
  63. data/spec/opium/extensions/object_spec.rb +24 -0
  64. data/spec/opium/extensions/pointer_spec.rb +28 -0
  65. data/spec/opium/extensions/regexp_spec.rb +23 -0
  66. data/spec/opium/extensions/string_spec.rb +65 -0
  67. data/spec/opium/extensions/time_spec.rb +55 -0
  68. data/spec/opium/model/attributable_spec.rb +45 -0
  69. data/spec/opium/model/callbacks_spec.rb +59 -0
  70. data/spec/opium/model/connectable_spec.rb +218 -0
  71. data/spec/opium/model/criteria_spec.rb +285 -0
  72. data/spec/opium/model/dirty_spec.rb +39 -0
  73. data/spec/opium/model/fieldable_spec.rb +133 -0
  74. data/spec/opium/model/findable_spec.rb +57 -0
  75. data/spec/opium/model/kaminari/queryable_spec.rb +22 -0
  76. data/spec/opium/model/kaminari/scopable_spec.rb +20 -0
  77. data/spec/opium/model/kaminari_spec.rb +104 -0
  78. data/spec/opium/model/persistable_spec.rb +367 -0
  79. data/spec/opium/model/queryable_spec.rb +338 -0
  80. data/spec/opium/model/scopable_spec.rb +115 -0
  81. data/spec/opium/model/serialization_spec.rb +51 -0
  82. data/spec/opium/model_spec.rb +49 -0
  83. data/spec/opium/user_spec.rb +195 -0
  84. data/spec/opium_spec.rb +5 -0
  85. data/spec/spec_helper.rb +25 -0
  86. 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