opium 1.1.8 → 1.2.0

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.
@@ -1,3 +1,3 @@
1
1
  module Opium
2
- VERSION = "1.1.8"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -1,15 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Opium::Model::Attributable do
4
- let( :model ) { Class.new { include Opium::Model::Attributable } }
5
-
6
- context 'within an instance' do
7
- subject { model.new }
8
-
9
- it { is_expected.to respond_to( :attributes, :attributes= ) }
10
- it { is_expected.to respond_to( :attributes_to_parse ) }
11
- end
12
-
13
4
  context 'when included in a model' do
14
5
  before do
15
6
  stub_const( 'Book', Class.new do
@@ -107,7 +107,10 @@ describe Opium::Model::Batchable::Batch do
107
107
  context 'when #queue is empty' do
108
108
  before { subject.queue.clear }
109
109
 
110
- it { expect { subject.execute }.to raise_exception }
110
+ # it { expect { subject.execute }.to raise_exception }
111
+
112
+ it { expect { subject.execute }.to_not raise_exception }
113
+ it { expect( subject.owner ).to_not receive(:http_post) }
111
114
  end
112
115
 
113
116
  context 'when #queue has more than MAX_BATCH_SIZE operations' do
@@ -2,4 +2,152 @@ require 'spec_helper'
2
2
 
3
3
  describe Opium::Model::Batchable do
4
4
  it { expect( described_class.constants ).to include( :Batch, :Operation ) }
5
+
6
+ before do
7
+ stub_const( 'Game', Class.new do
8
+ include Opium::Model
9
+ field :title, type: String
10
+ end )
11
+
12
+ stub_request(:get, "https://api.parse.com/1/classes/Game/abcd1234").
13
+ with(headers: {'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
14
+ to_return(:status => 200, :body => { objectId: 'abcd1234', title: 'Never Alone' }.to_json, :headers => {})
15
+
16
+ stub_request(:get, "https://api.parse.com/1/classes/Game/efgh5678").
17
+ with(headers: {'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
18
+ to_return(:status => 200, :body => { objectId: 'efgh5678', title: 'Guacamelee' }.to_json, :headers => {})
19
+ end
20
+
21
+ describe '.batched?' do
22
+ let(:result) { Game.batched? }
23
+
24
+ context 'when there is no batch job' do
25
+ it { expect( result ).to be_falsey }
26
+ end
27
+
28
+ context 'when there is a batch job' do
29
+ before(:each) { Game.create_batch }
30
+ after(:each) { Game.delete_batch }
31
+
32
+ it { expect( result ).to be_truthy }
33
+ end
34
+ end
35
+
36
+ describe '.create_batch' do
37
+ after { Game.delete_batch }
38
+ let(:result) { Game.create_batch }
39
+
40
+ context 'when there is no existing batch job' do
41
+ it { expect { result }.to_not raise_exception }
42
+ it { expect( result ).to be_a Opium::Model::Batchable::Batch }
43
+ it { expect( result.depth ).to be 0 }
44
+ end
45
+
46
+ context 'when there is a current batch job' do
47
+ before { Game.create_batch }
48
+ after { Game.delete_batch }
49
+
50
+ it { expect { result }.to_not raise_exception }
51
+ it( 'increments the depth' ) { expect( result.depth ).to eq 1 }
52
+ end
53
+ end
54
+
55
+ describe '.delete_batch' do
56
+ let(:result) { Game.delete_batch }
57
+
58
+ context 'when there is no existing batch job' do
59
+ it { expect { result }.to raise_exception }
60
+ end
61
+
62
+ context 'when there is a current batch job' do
63
+ before { Game.create_batch }
64
+
65
+ context 'with depth greater than 0' do
66
+ before { Game.create_batch }
67
+ after { Game.delete_batch }
68
+
69
+ it { expect { result }.to_not raise_exception }
70
+ it { expect( result ).to be_a Opium::Model::Batchable::Batch }
71
+ it( 'decrements the depth' ) { expect( result.depth ).to eq 0 }
72
+ end
73
+
74
+ context 'with depth of 0' do
75
+ it { expect { result }.to_not raise_exception }
76
+ it { expect( result ).to be_nil }
77
+ end
78
+ end
79
+ end
80
+
81
+ describe '.current_batch_job' do
82
+ let(:result) { Game.current_batch_job }
83
+
84
+ context 'when there is no batch job' do
85
+ it { expect { result }.to_not raise_exception }
86
+ it { expect( result ).to be_nil }
87
+ end
88
+
89
+ context 'when a batch job has been created' do
90
+ before { Game.create_batch }
91
+ after { Game.delete_batch }
92
+
93
+ it { expect { result }.to_not raise_exception }
94
+ it { expect( result ).to be_a Opium::Model::Batchable::Batch }
95
+ end
96
+ end
97
+
98
+ describe '.batch' do
99
+ let(:batch) { Game.batch( batch_options, &batch_block ) }
100
+ let(:batch_options) { { } }
101
+ let(:batch_block) { -> { } }
102
+
103
+ context 'when given a block' do
104
+ it { expect { batch }.to_not raise_exception }
105
+ it { expect {|b| Game.batch( batch_options, &b ) }.to yield_control.at_least(1).times }
106
+ it 'is batched within the block' do
107
+ Game.batch( batch_options ) do
108
+ expect( Game ).to be_batched
109
+
110
+ Game.batch( batch_options ) do
111
+ expect( Game ).to be_batched
112
+ expect( Game.current_batch_job.depth ).to eq 1
113
+ end
114
+
115
+ expect( Game ).to be_batched
116
+ expect( Game.current_batch_job.depth ).to eq 0
117
+ end
118
+ expect( Game ).to_not be_batched
119
+ end
120
+ end
121
+
122
+ context 'without a block' do
123
+ let(:batch_block) { }
124
+
125
+ it { expect { batch }.to raise_exception }
126
+ end
127
+
128
+ context 'with mode: :mixed' do
129
+ let(:batch_options) { { mode: :mixed } }
130
+ let(:batch_block) do
131
+ -> do
132
+ Game.find('abcd1234').save
133
+ Game.new( title: 'Skyrim' ).save
134
+ Game.find('efgh5678').destroy
135
+ end
136
+ end
137
+
138
+ it { expect { batch }.to_not raise_exception }
139
+
140
+ end
141
+
142
+ context 'with mode: :ordered' do
143
+ let(:batch_options) { { mode: :ordered } }
144
+ let(:batch_block) do
145
+ -> do
146
+ Game.find('abcd1234').save
147
+ Game.new( title: 'Skyrim' ).save
148
+ Game.find('efgh5678').destroy
149
+ end
150
+ end
151
+ end
152
+ end
5
153
  end
@@ -147,6 +147,22 @@ describe Opium::Model::Fieldable do
147
147
  expect( Model.default_attributes ).to include( 'symbolic_with_string_default' => :value, 'symbolic_with_symbol_default' => :value )
148
148
  end
149
149
 
150
+ describe '.has_field?' do
151
+ let(:result) { subject.has_field? field_name }
152
+
153
+ context 'with a valid field_name' do
154
+ let(:field_name) { :symbolic_with_string_default }
155
+
156
+ it { expect( result ).to be_truthy }
157
+ end
158
+
159
+ context 'with an invalid field_name' do
160
+ let(:field_name) { :not_defined }
161
+
162
+ it { expect( result ).to be_falsey }
163
+ end
164
+ end
165
+
150
166
  describe '.field' do
151
167
  subject { Model.new }
152
168
 
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ describe Opium::Model::Reference do
4
+ before do
5
+ stub_const( 'Tree', Class.new do |klass|
6
+ include Opium::Model
7
+ field :trees
8
+
9
+ class << klass
10
+ stub(:model_name).and_return( ActiveModel::Name.new( self, nil, 'Tree' ) )
11
+ end
12
+ end )
13
+ end
14
+
15
+ let(:child) { Tree.new id: 'c1234' }
16
+ let(:metadata) { Opium::Model::Relatable::Metadata.new( Tree, :belongs_to, :parent, class_name: 'Tree' ) }
17
+
18
+ describe '.to_ruby' do
19
+ let(:result) { described_class.to_ruby( subject ) }
20
+
21
+ context 'when given a hash' do
22
+ subject { { metadata: metadata, context: child } }
23
+
24
+ it { expect( result ).to be_a described_class }
25
+ it { expect( result.metadata ).to eq metadata }
26
+ it { expect( result.context ).to eq child }
27
+ it { expect( result ).to be_a Delegator }
28
+ end
29
+
30
+ context 'when given a Reference' do
31
+ subject { described_class.new( metadata, child ) }
32
+
33
+ it { expect( result ).to be_a described_class }
34
+ it { expect( result.metadata ).to eq metadata }
35
+ it { expect( result.context ).to eq child }
36
+ it { expect( result ).to be_a Delegator }
37
+ end
38
+ end
39
+
40
+ describe '.to_parse' do
41
+ end
42
+
43
+ describe '.__getobj__' do
44
+ subject { described_class.new( metadata, child ) }
45
+ let(:result) { subject.__getobj__ }
46
+
47
+ context 'when parse returns no results' do
48
+ before do
49
+ stub_request(:get, "https://api.parse.com/1/classes/Tree?count=1&where=%7B%22trees%22:%7B%22__type%22:%22Pointer%22,%22className%22:%22Tree%22,%22objectId%22:%22c1234%22%7D%7D").
50
+ with(headers: {'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
51
+ to_return(status: 200, body: { results: [], count: 0 }.to_json, headers: { content_type: 'application/json' })
52
+ end
53
+
54
+ it { expect { result }.to_not raise_exception }
55
+ it { expect( result ).to be_nil }
56
+ end
57
+
58
+ context 'when parse returns a result' do
59
+ before do
60
+ stub_request(:get, "https://api.parse.com/1/classes/Tree?count=1&where=%7B%22trees%22:%7B%22__type%22:%22Pointer%22,%22className%22:%22Tree%22,%22objectId%22:%22c1234%22%7D%7D").
61
+ with(headers: {'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
62
+ to_return(status: 200, body: { results: [
63
+ { objectId: 'abcd1234' }
64
+ ], count: 1 }.to_json, headers: { content_type: 'application/json' })
65
+ allow(Tree).to receive(:model_name).and_return( ActiveModel::Name.new( Tree, nil, 'Tree' ) )
66
+ end
67
+
68
+ it { expect { result }.to_not raise_exception }
69
+ it { expect( result ).to_not be_nil }
70
+ it { expect( result ).to be_a Opium::Model }
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe Opium::Model::Relatable::Metadata do
4
+ before do
5
+ stub_const( 'Model', Class.new do |klass|
6
+ stub('model_name').and_return( ActiveModel::Name.new( klass, nil, 'Model' ) )
7
+ end )
8
+ end
9
+
10
+ describe 'inverse_relation_name' do
11
+ let(:result) { subject.inverse_relation_name }
12
+
13
+ context 'when created with an :inverse_of key' do
14
+ subject { described_class.new( Model, :belongs_to, :parent, class_name: 'Model', inverse_of: :children ) }
15
+
16
+ it( 'uses the :inverse_of value' ) { expect( result ).to eq 'children' }
17
+ end
18
+
19
+ context 'when inferred from a :has_many context' do
20
+ subject { described_class.new( Model, :has_many, :articles ) }
21
+
22
+ it( 'uses the singular of its model name' ) { expect( result ).to eq 'model' }
23
+ end
24
+
25
+ context 'when inferred from a :belongs_to context' do
26
+ subject { described_class.new( Model, :belongs_to, :article ) }
27
+
28
+ it( 'uses the plural of its model name' ) { expect( result ).to eq 'models' }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,220 @@
1
+ require 'spec_helper'
2
+
3
+ describe Opium::Model::Relatable do
4
+ before do
5
+ stub_const( 'Game', Class.new do |klass|
6
+ include Opium::Model
7
+ stub(:model_name).and_return( ActiveModel::Name.new( klass, nil, 'Game' ) )
8
+
9
+ field :title, type: String
10
+ has_and_belongs_to_many :players
11
+ end )
12
+
13
+ stub_const( 'Player', Class.new do |klass|
14
+ include Opium::Model
15
+ stub(:model_name).and_return( ActiveModel::Name.new( klass, nil, 'Player' ) )
16
+
17
+ field :tag, type: String
18
+ has_and_belongs_to_many :games
19
+ end )
20
+
21
+ stub_const( 'Article', Class.new do |klass|
22
+ include Opium::Model
23
+
24
+ instance_eval do
25
+ def model_name
26
+ ActiveModel::Name.new( self, nil, 'Article' )
27
+ end
28
+ end
29
+
30
+ field :title, type: String
31
+ has_many :comments
32
+ belongs_to :author, class_name: 'User'
33
+ end )
34
+
35
+ stub_const( 'Comment', Class.new do |klass|
36
+ include Opium::Model
37
+
38
+ instance_eval do
39
+ def model_name
40
+ ActiveModel::Name.new( self, nil, 'Comment' )
41
+ end
42
+ end
43
+
44
+ field :body
45
+ belongs_to :article
46
+ end )
47
+
48
+ stub_const( 'User', Class.new(Opium::User) do |klass|
49
+ stub(:model_name).and_return( ActiveModel::Name.new( klass, nil, 'User' ) )
50
+
51
+ has_many :articles
52
+ has_one :profile
53
+ end )
54
+
55
+ stub_const( 'Profile', Class.new do |klass|
56
+ include Opium::Model
57
+ stub(:model_name).and_return( ActiveModel::Name.new( klass, nil, 'Profile' ) )
58
+
59
+ field :first_name, type: String
60
+ belongs_to :user
61
+ end )
62
+
63
+ stub_const( 'Event', Class.new do |klass|
64
+ include Opium::Model
65
+ stub(:model_name).and_return( ActiveModel::Name.new( klass, nil, 'Event' ) )
66
+
67
+ field :title, type: String
68
+ end )
69
+
70
+ stub_request(:get, "https://api.parse.com/1/classes/Comment?count=1&where=%7B%22$relatedTo%22:%7B%22object%22:%7B%22__type%22:%22Pointer%22,%22className%22:%22Article%22,%22objectId%22:%22abcd1234%22%7D,%22key%22:%22comments%22%7D%7D").
71
+ with(headers: {'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
72
+ to_return(status: 200, body: {
73
+ count: 2,
74
+ results: [
75
+ { objectId: 'c1234', body: 'A Moose once bit my sister...' },
76
+ { objectId: 'c5678', body: 'No realli! She was Karving her initials on the moose' }
77
+ ]
78
+ }.to_json, headers: { content_type: 'application/json' })
79
+
80
+ stub_request(:get, "https://api.parse.com/1/classes/Comment?count=1&where=%7B%22$relatedTo%22:%7B%22object%22:%7B%22__type%22:%22Pointer%22,%22className%22:%22Article%22,%22objectId%22:%22a1234%22%7D,%22key%22:%22comments%22%7D%7D").
81
+ with(headers: {'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
82
+ to_return(status: 200, body: {
83
+ count: 1,
84
+ results: [
85
+ { objectId: 'c2345', body: 'Seems plausible.' }
86
+ ]
87
+ }.to_json, headers: { content_type: 'application/json' })
88
+
89
+ stub_request(:get, "https://api.parse.com/1/classes/Article?count=1&where=%7B%22comments%22:%7B%22__type%22:%22Pointer%22,%22className%22:%22Comment%22,%22objectId%22:%22c1234%22%7D%7D").
90
+ with(headers: {'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
91
+ to_return(status: 200, body: {
92
+ count: 1,
93
+ results: [
94
+ { objectId: 'abcd1234', title: 'Funny Subtitles' }
95
+ ]
96
+ }.to_json, headers: { content_type: 'application/json' })
97
+
98
+ stub_request(:post, "https://api.parse.com/1/classes/Article").
99
+ with(body: "{\"title\":\"A new approach to Sandboxes\"}",
100
+ headers: {'Content-Type'=>'application/json', 'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
101
+ to_return(status: 200, body: { objectId: 'a1234', createdAt: Time.now.utc }.to_json, headers: { content_type: 'appliction/json' })
102
+
103
+ stub_request(:post, "https://api.parse.com/1/classes/Comment").
104
+ with(body: "{\"body\":\"Do. Not. Want.\"}",
105
+ headers: {'Content-Type'=>'application/json', 'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
106
+ to_return(status: 200, body: { objectId: 'c7896', createdAt: Time.now.utc }.to_json, headers: { content_type: 'application/json' })
107
+
108
+ stub_request(:put, "https://api.parse.com/1/classes/Article/a1234").
109
+ with(body: "{\"comments\":{\"__op\":\"AddRelation\",\"objects\":[{\"__type\":\"Pointer\",\"className\":\"Comment\",\"objectId\":\"c7896\"}]}}",
110
+ headers: {'Content-Type'=>'application/json', 'X-Parse-Application-Id'=>'PARSE_APP_ID', 'X-Parse-Rest-Api-Key'=>'PARSE_API_KEY'}).
111
+ to_return(status: 200, body: { updatedAt: Time.now.utc }.to_json, headers: { content_type: 'application/json' })
112
+ end
113
+
114
+ describe '.relations' do
115
+ let(:result) { subject.relations }
116
+
117
+ context 'within a model with multiple relations' do
118
+ subject { User }
119
+
120
+ it { expect { result }.to_not raise_exception }
121
+ it { expect( result ).to be_a Hash }
122
+ it { expect( result.keys ).to include( 'articles', 'profile' ) }
123
+ end
124
+
125
+ context 'within a model with no relations' do
126
+ subject { Event }
127
+
128
+ it { expect { result }.to_not raise_exception }
129
+ it { expect( result ).to be_a Hash }
130
+ it { expect( result ).to be_empty }
131
+ end
132
+ end
133
+
134
+ describe '.has_many' do
135
+ subject { Article }
136
+ let(:result) { subject.relations[relation_name] }
137
+ let(:relation_name) { :comments }
138
+
139
+ it { expect( result ).to be_a Opium::Model::Relatable::Metadata }
140
+
141
+ it { is_expected.to have_field :comments }
142
+ it { expect( subject.fields[:comments].type ).to eq Opium::Model::Relation }
143
+ it { expect( subject.fields[:comments].default ).to_not be_nil }
144
+
145
+ context 'within a new model' do
146
+ subject { Article.new }
147
+
148
+ it { expect( subject.comments ).to_not be_nil }
149
+ it { expect( subject.comments ).to be_a Opium::Model::Relation }
150
+ it('adds a constraint for the owner of the relation') { expect( subject.comments.constraints['where'] ).to include( '$relatedTo' => { 'object' => subject.to_parse, 'key' => 'comments' } ) }
151
+ it { expect( subject.comments ).to be_empty }
152
+ it { expect( subject.comments.owner ).to eq subject }
153
+ end
154
+
155
+ context 'when a model has existing relations' do
156
+ subject { Article.new id: 'abcd1234' }
157
+
158
+ it { expect( subject.comments ).to_not be_nil }
159
+ it { expect( subject.comments ).to be_a Opium::Model::Relation }
160
+ it('adds a constraint for the owner of the relation') { expect( subject.comments.constraints['where'] ).to include( '$relatedTo' => { 'object' => subject.to_parse, 'key' => 'comments' } ) }
161
+ it { expect( subject.comments ).to_not be_empty }
162
+ it { expect( subject.comments.owner ).to eq subject }
163
+ end
164
+ end
165
+
166
+ describe '.has_one' do
167
+ end
168
+
169
+ describe '.belongs_to' do
170
+ subject { Comment }
171
+ let(:result) { subject.relations[relation_name] }
172
+ let(:relation_name) { :article }
173
+
174
+ it { expect( result ).to be_a Opium::Model::Relatable::Metadata }
175
+
176
+ it { is_expected.to have_field :article }
177
+ it { expect( subject.fields[:article].type ).to eq Opium::Model::Reference }
178
+ it { expect( subject.fields[:article].default ).to be_a Hash }
179
+
180
+ context 'within a new model' do
181
+ subject { Comment.new }
182
+
183
+ it { expect( subject.article ).to_not be_nil }
184
+ it { expect( subject.article ).to be_a Opium::Model::Reference }
185
+ it { expect( subject.article.context ).to eq subject }
186
+ it { expect { subject.article.__getobj__ }.to_not raise_exception }
187
+ it { expect( subject.article.__getobj__ ).to be_nil }
188
+ end
189
+
190
+ context 'within an existing model' do
191
+ subject { Comment.new id: 'c1234' }
192
+
193
+ it { expect( subject.article ).to_not be_nil }
194
+ it { expect( subject.article ).to be_a Opium::Model::Reference }
195
+ it { expect( subject.article.context ).to eq subject }
196
+ it( 'loads the proper model id' ) { expect( subject.article.id ).to eq 'abcd1234' }
197
+ it( 'loads a properly typed model') { expect( subject.article.model_name ).to eq 'Article' }
198
+ end
199
+ end
200
+
201
+ describe '.has_and_belongs_to_many' do
202
+ end
203
+
204
+ describe '#save' do
205
+ let(:result) { subject.save }
206
+
207
+ context 'within a model with a has_many relation' do
208
+ subject { Article.new title: 'A new approach to Sandboxes' }
209
+ before { subject.comments.build body: 'Do. Not. Want.' }
210
+
211
+ it { result; expect( subject.errors ).to be_empty }
212
+ it { expect { result }.to_not raise_exception }
213
+ it { expect( result ).to be_truthy }
214
+ it { result && is_expected.to( be_persisted ) }
215
+ it { result && expect( subject.comments ).to( be_a Opium::Model::Relation ) }
216
+ it { result && expect( subject.comments ).to( all( be_a( Opium::Model ) ) ) }
217
+ it { result && expect( subject.comments ).to( all( be_persisted ) ) }
218
+ end
219
+ end
220
+ end