opium 1.5.4 → 1.5.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5e532f3a073fd1b08d3b659faf9a2012b75de256
4
- data.tar.gz: c4df755ca1dd1d8f3af4822740e8bb086197679b
3
+ metadata.gz: b53eb8b7bd8065147581a1ad93cc314190142304
4
+ data.tar.gz: 19898ad35c81040d44ba9765cf66319c1a4a6bc7
5
5
  SHA512:
6
- metadata.gz: 1caaf649218ecbc0ac3e69ae32bc5d8f087b2189ff9280f3b335010ae4173ee5dc7d74b7c9df987607205a1a6d667dde62df44e11d51c115d71e8f4c2ca7e3f9
7
- data.tar.gz: fa9d3fdf08b4c3c635fd579529540aade5b3c7bcb5ceea1cc4fb56ad8e27edf5825aa37cfa767313625c31646baa7ae91f6414bba93711c92c0a97bdeaf5ed96
6
+ metadata.gz: b3979d70c2107f4865da5804cddf925c7731c098f6f0dfab6180cc2388d598021937d6d441752f7a2fb8d795855357f9fe0c0b4a50bd937cd6d1bc5fb3c4114c
7
+ data.tar.gz: 1351e6632df54fdf436c0c1358387d24629022fe47e00afcfd9c5efed9958c252a041cc61058ea05cdb5054dda64c205dfd72de69f57271614c737200a9cba5f
@@ -6,6 +6,7 @@ rvm:
6
6
  - 2.2.3
7
7
  - 2.2.4
8
8
  - 2.3.1
9
+ - 2.3.4
9
10
 
10
11
  before_install:
11
12
  - gem update bundler
@@ -1,3 +1,8 @@
1
+ ## 1.5.5
2
+ ### Resolved Issues
3
+ - Attributable no longer stores unknown fields within the attributes hash of the object; rather, a new accessor is created on the fly for the class to store that data. This should prevent serialization from breaking on unknown fields with more recent versions of ActiveModel.
4
+ - User should not be storing any data other than its known fields. This was primarily an issue with the find by session token endpoint, which is now mysteriously embedding `__type` and `className` into the results.
5
+
1
6
  ## 1.5.4
2
7
  ### Resolved Issues
3
8
  - #55: Opium::Installation was not properly using the master key for performing queries. Parse requires the presence of the master key to perform any sort of non-id query on installations.
data/README.md CHANGED
@@ -465,10 +465,22 @@ player.high_scores.include?( score3 ) # true, as the above setter updates play
465
465
 
466
466
  ### Querying data
467
467
 
468
+ Models can be searched for via one of two different methods: by their associated `id` field value, or by criteria defining the attributes a model must have to be part of a match set.
469
+
468
470
  #### Find by id
469
471
 
472
+ IDs in Parse should be unique within a given model class. As such, finding by id always returns, at most, one result: the model instance associated with the provided id.
473
+
474
+ `Opium::Model` provides a single class method, `find` for performing an id search. It accepts a single parameter, `id`, being a string representing the id to look for. Should a matching model exist, it will be the return value of the method; otherwise, an exception is raised. Any class which mixes in `Opium::Model` has access to this method.
475
+
476
+ ```ruby
477
+ score = HighScore.find( 'd3ADb3Ef' )
478
+ ```
479
+
470
480
  #### Criteria & Scopes
471
481
 
482
+ While `find` is useful if a model id is already available, it is more often desirable to perform a query across more and varied content stored within the model. This is where criteria come into play.
483
+
472
484
  #### Kaminari support
473
485
 
474
486
  Opium comes with support for [Kaminari](https://rubygems.org/gems/kaminari). To ensure that Opium loads itself correctly, please specify it _after_ Kaminari within your Gemfile:
@@ -507,6 +519,18 @@ Note that note all of these data are supported on all platforms.
507
519
 
508
520
  Once the push has be configured as desired, it can be sent out by triggering the `create` method, which will either raise an error on failure or return true on success. Note that a truthy return value does not necessarily indicate that Parse has successfully sent any notifications; rather it merely indicates that Parse has successfully received the push request and did not find anything egregious in it.
509
521
 
522
+ In the following example, a push is scheduled for a day in the future which targets some channels.
523
+
524
+ ```ruby
525
+ p = Opium::Push.new(
526
+ alert: 'Be sure to watch our eSports tournament on Twitch!',
527
+ push_at: 1.day.from_now,
528
+ expiration_interval: 1.day,
529
+ channels: %w[ Gaming eSports Tournaments ]
530
+ )
531
+ p.create
532
+ ```
533
+
510
534
  ### Advanced Targeting
511
535
 
512
536
  ## Contributing
@@ -2,40 +2,39 @@ module Opium
2
2
  module Model
3
3
  module Attributable
4
4
  extend ActiveSupport::Concern
5
-
5
+
6
6
  included do
7
7
  include ActiveModel::ForbiddenAttributesProtection
8
8
  end
9
-
9
+
10
10
  def initialize( attributes = {} )
11
11
  super( self.class.default_attributes( self ).merge attributes )
12
12
  end
13
-
13
+
14
14
  def attributes
15
15
  @attributes ||= {}.with_indifferent_access
16
16
  end
17
-
17
+
18
18
  def attributes=(values)
19
19
  sanitize_for_mass_assignment( rubyize_field_names( values ) ).each do |k, v|
20
20
  field_info, setter = self.class.fields[k], :"#{k}="
21
- if field_info.present? || self.respond_to?( setter )
22
- send( setter, v )
23
- else
24
- attributes[k] = v
21
+ unless field_info.present? || self.respond_to?( setter )
22
+ self.class.send(:attr_accessor, k)
25
23
  end
24
+ send( setter, v )
26
25
  end
27
26
  end
28
-
27
+
29
28
  def attributes_to_parse( options = {} )
30
29
  options[:except] ||= self.class.fields.values.select {|f| f.readonly? || f.virtual? }.map {|f| f.name} if options[:not_readonly]
31
30
  Hash[*self.as_json( options ).flat_map {|k, v| [self.class.fields[k].name_to_parse, self.class.fields[k].type.to_parse(v)]}]
32
31
  end
33
-
32
+
34
33
  private
35
-
34
+
36
35
  def rubyize_field_names( hash )
37
36
  hash.transform_keys {|k| self.class.ruby_canonical_field_names[k] || k}
38
37
  end
39
38
  end
40
39
  end
41
- end
40
+ end
@@ -1,17 +1,17 @@
1
1
  module Opium
2
2
  class User
3
3
  include Opium::Model
4
-
4
+
5
5
  field :username, type: String
6
6
  field :password, type: String
7
7
  field :email, type: String
8
8
  field :email_verified, type: Boolean, readonly: true
9
9
  field :session_token, type: String, readonly: true
10
-
10
+
11
11
  no_object_prefix!
12
12
  requires_heightened_privileges!
13
13
  add_header_to [:put, :delete], :x_parse_session_token, :session_token.to_proc
14
-
14
+
15
15
  class << self
16
16
  # Note that this will eat any ParseErrors which get raised, and not perform any logging.
17
17
  def authenticate( username, password )
@@ -19,26 +19,27 @@ module Opium
19
19
  rescue Opium::Model::Connectable::ParseError => e
20
20
  nil
21
21
  end
22
-
22
+
23
23
  def authenticate!( username, password )
24
24
  new( as_resource('login') { http_get query: { username: username, password: password } } )
25
25
  end
26
-
26
+
27
27
  def find_by_session_token( token )
28
- new( http_get id: 'me', headers: { x_parse_session_token: token } )
28
+ data = http_get id: 'me', headers: { x_parse_session_token: token }
29
+ new( data.slice( *fields.keys ) )
29
30
  end
30
31
  end
31
-
32
+
32
33
  def reset_password
33
34
  reset_password!
34
35
  rescue => e
35
36
  self.errors.add( :email, e.to_s )
36
37
  false
37
38
  end
38
-
39
+
39
40
  def reset_password!
40
41
  fail KeyError, 'an email address is required to reset password' unless email
41
42
  self.class.as_resource('requestPasswordReset') { self.class.http_post data: email }.empty?
42
43
  end
43
44
  end
44
- end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module Opium
2
- VERSION = "1.5.4"
2
+ VERSION = "1.5.5"
3
3
  end
@@ -10,41 +10,48 @@ describe Opium::Model::Attributable do
10
10
  attr_accessor :not_a_field
11
11
  end )
12
12
  end
13
-
13
+
14
14
  subject { Book.new( title: 'Little Brother', id: 'abc123', created_at: Time.now, genre: :sci_fi ) }
15
-
15
+
16
16
  describe '#attributes_to_parse' do
17
17
  context 'when called with no parameters' do
18
18
  it 'has all fields present, with names and values converted to parse' do
19
19
  expect( subject.attributes_to_parse ).to eq( 'objectId' => 'abc123', 'title' => 'Little Brother', 'genre' => 'sci_fi', 'createdAt' => subject.created_at.to_parse, 'updatedAt' => nil )
20
20
  end
21
21
  end
22
-
22
+
23
23
  context 'when called with except' do
24
24
  it 'excludes the excepted fields' do
25
25
  expect( subject.attributes_to_parse( except: [:id, :updated_at] ) ).to eq( 'title' => 'Little Brother', 'genre' => 'sci_fi', 'createdAt' => subject.created_at.to_parse )
26
26
  end
27
27
  end
28
-
28
+
29
29
  context 'when called with not_readonly' do
30
30
  it 'excludes all readonly fields' do
31
31
  expect( subject.attributes_to_parse( not_readonly: true ) ).to eq( 'title' => 'Little Brother', 'genre' => 'sci_fi' )
32
32
  end
33
33
  end
34
34
  end
35
-
35
+
36
36
  describe '#attributes=' do
37
- it 'stores unrecognized fields in the attributes hash' do
37
+ it 'does not store unrecognized fields in the attributes hash' do
38
38
  expect { subject.attributes = { unknownField: 42 } }.to_not raise_exception
39
- expect( subject.attributes ).to have_key('unknownField')
40
- expect( subject.attributes['unknownField'] ).to eq 42
39
+ expect( subject.attributes ).to_not have_key('unknownField')
40
+ expect( subject.attributes['unknownField'] ).to_not eq 42
41
41
  end
42
-
42
+
43
43
  it 'calls relevant setters rather for non fields if they exist' do
44
44
  expect { subject.attributes = { not_a_field: 42 } }.to_not raise_exception
45
45
  expect( subject.not_a_field ).to eq 42
46
46
  expect( subject.attributes ).to_not have_key('not_a_field')
47
47
  end
48
+
49
+ it 'creates an accessor for unknown fields' do
50
+ expect { subject.attributes = { unknown: 42 } }.to_not raise_exception
51
+ expect( subject ).to respond_to( :unknown, :unknown= )
52
+ expect( subject.unknown ).to eq 42
53
+ expect( subject.attributes ).to_not have_key('unknown')
54
+ end
48
55
  end
49
56
  end
50
- end
57
+ end
@@ -8,12 +8,12 @@ describe Opium::Model::Serialization do
8
8
  field :price
9
9
  end
10
10
  end
11
-
11
+
12
12
  it { model.should respond_to( :include_root_in_json ) }
13
13
  it "include_root_in_json should default to false" do
14
14
  model.include_root_in_json.should == false
15
15
  end
16
-
16
+
17
17
  describe "instance" do
18
18
  describe "with no data" do
19
19
  let( :params ) { { "id" => nil, "created_at" => nil, "updated_at" => nil, "name" => nil, "price" => nil } }
@@ -25,7 +25,7 @@ describe Opium::Model::Serialization do
25
25
  end
26
26
  end
27
27
  end
28
-
28
+
29
29
  describe "with partial data" do
30
30
  let( :params ) { { "name" => "test", "price" => nil } }
31
31
  subject { model.new( name: "test" ) }
@@ -36,7 +36,7 @@ describe Opium::Model::Serialization do
36
36
  end
37
37
  end
38
38
  end
39
-
39
+
40
40
  describe "with full data" do
41
41
  let( :params ) { { "name" => "test", "price" => 75.0 } }
42
42
  subject { model.new( params ) }
@@ -47,5 +47,20 @@ describe Opium::Model::Serialization do
47
47
  end
48
48
  end
49
49
  end
50
+
51
+ describe 'with non field data' do
52
+ let( :params ) { {'name' => 'test', 'price' => 75.0, 'extra' => true} }
53
+ subject { model.new( params ) }
54
+ it { expect { subject.as_json }.to_not raise_exception }
55
+
56
+ describe '#to_json' do
57
+ let(:result) { JSON.parse(subject.to_json) }
58
+
59
+ it 'only has field values' do
60
+ expect( result ).to include( 'name', 'price' )
61
+ expect( result ).to_not include( 'extra' )
62
+ end
63
+ end
64
+ end
50
65
  end
51
- end
66
+ end
@@ -3,17 +3,17 @@ require 'spec_helper'
3
3
  describe Opium::User do
4
4
  before do
5
5
  stub_request(:get, 'https://api.parse.com/1/login?password=swordfish&username=username').to_return(
6
- status: 200,
7
- body: {
8
- username: 'username',
9
- createdAt: '2014-11-01T12:00:30Z',
6
+ status: 200,
7
+ body: {
8
+ username: 'username',
9
+ createdAt: '2014-11-01T12:00:30Z',
10
10
  updatedAt: '2015-02-10T16:37:23Z',
11
11
  objectId: 'abcd1234',
12
12
  sessionToken: 'super-secret-session-id'
13
13
  }.to_json,
14
14
  headers: { 'Content-Type' => 'application/json' }
15
15
  )
16
-
16
+
17
17
  stub_request(:get, 'https://api.parse.com/1/login?password=deadbeef&username=username').to_return(
18
18
  status: 404,
19
19
  body: {
@@ -22,145 +22,148 @@ describe Opium::User do
22
22
  }.to_json,
23
23
  headers: { 'Content-Type' => 'application/json' }
24
24
  )
25
-
25
+
26
26
  stub_request(:post, "https://api.parse.com/1/requestPasswordReset").with(
27
27
  body: "{\"data\":\"user@email.com\"}",
28
28
  headers: { 'Content-Type'=>'application/json' }
29
29
  ).to_return(
30
30
  status: 200,
31
- body: "{}",
31
+ body: "{}",
32
32
  headers: {'Content-Type' => 'application/json'}
33
33
  )
34
-
34
+
35
35
  stub_request(:get, "https://api.parse.com/1/users/me").
36
36
  with( headers: {'X-Parse-Session-Token'=>'super-secret-session-id'} ).
37
37
  to_return(
38
- :status => 200,
38
+ :status => 200,
39
39
  :body => {
40
- username: 'username',
41
- createdAt: '2014-11-01T12:00:30Z',
40
+ username: 'username',
41
+ createdAt: '2014-11-01T12:00:30Z',
42
42
  updatedAt: '2015-02-10T16:37:23Z',
43
- objectId: 'abcd1234'
44
- }.to_json,
43
+ objectId: 'abcd1234',
44
+ __type: 'Object',
45
+ className: '_User'
46
+ }.to_json,
45
47
  headers: { 'Content-Type' => 'application/json' }
46
48
  )
47
-
49
+
48
50
  stub_request(:get, "https://api.parse.com/1/users/me").
49
51
  with( headers: {'X-Parse-Session-Token'=>'never-been-given-out'}).
50
52
  to_return(
51
- status: 404,
53
+ status: 404,
52
54
  body: {
53
55
  code: 101,
54
56
  error: 'invalid session'
55
- }.to_json,
57
+ }.to_json,
56
58
  headers: { 'Content-Type' => 'application/json' }
57
59
  )
58
60
  end
59
-
61
+
60
62
  it { described_class.should respond_to( :authenticate, :authenticate! ).with(2).arguments }
61
63
  it { described_class.should respond_to( :find_by_session_token ).with(1).argument }
62
64
  it { described_class.should respond_to( :object_prefix ) }
63
-
65
+
64
66
  it { should be_an( Opium::Model ) }
65
-
67
+
66
68
  [:username, :password, :email, :email_verified, :session_token].each do |field_name|
67
69
  it { described_class.fields.should have_key( field_name ) }
68
70
  end
69
-
71
+
70
72
  it { described_class.fields[:session_token].should be_readonly }
71
73
  it { expect( described_class.fields[:email_verified] ).to be_readonly }
72
74
  it { described_class.object_prefix.should be_empty }
73
75
  it { described_class.resource_name.should == 'users' }
74
76
  it { expect( described_class.requires_heightened_privileges? ).to be_truthy }
75
-
77
+
76
78
  describe '.authenticate' do
77
79
  context 'with a good login' do
78
80
  it 'should return an Opium::User matching the credentials' do
79
81
  expect { described_class.authenticate( 'username', 'swordfish' ) }.to_not raise_exception
80
-
82
+
81
83
  login = described_class.authenticate( 'username', 'swordfish' )
82
-
83
- login.should_not be_nil
84
- login.should be_a( described_class )
85
- login.username.should == 'username'
84
+
85
+ login.should_not be_nil
86
+ login.should be_a( described_class )
87
+ login.username.should == 'username'
86
88
  login.session_token.should_not be_nil
87
89
  end
88
90
 
89
91
  end
90
-
92
+
91
93
  context 'with a bad login' do
92
94
  let(:login) { described_class.authenticate( 'username', 'deadbeef' ) }
93
-
95
+
94
96
  it { expect { login }.to_not raise_exception }
95
97
  it { login.should be_nil }
96
98
  end
97
99
  end
98
-
100
+
99
101
  describe '.authenticate!' do
100
102
  context 'with a good login' do
101
103
  it { expect { described_class.authenticate!( 'username', 'swordfish' ) }.to_not raise_exception }
102
104
  end
103
-
105
+
104
106
  context 'with a bad login' do
105
107
  it { expect { described_class.authenticate!( 'username', 'deadbeef' ) }.to raise_exception }
106
108
  end
107
109
  end
108
-
110
+
109
111
  describe '.find_by_session_token' do
110
112
  context 'without a token' do
111
113
  it { expect { described_class.find_by_session_token( nil ) }.to raise_exception }
112
114
  end
113
-
115
+
114
116
  context 'with a valid token' do
115
117
  let(:current_user) { described_class.find_by_session_token( 'super-secret-session-id' ) }
116
-
118
+
117
119
  it { expect { current_user }.to_not raise_exception }
118
120
  it { current_user.should be_a( described_class ) }
121
+ it { expect( current_user ).to_not respond_to( :__type, :className ) }
119
122
  end
120
-
123
+
121
124
  context 'with an invalid token' do
122
125
  it { expect { described_class.find_by_session_token( 'never-been-given-out' ) }.to raise_exception(Opium::Model::Connectable::ParseError) }
123
126
  end
124
127
  end
125
-
128
+
126
129
  context 'with a logged in user' do
127
130
  subject { Opium::User.new( id: 'abcd1234', session_token: 'super-secret-session-id', usernmae: 'user', email: 'user@email.com' ) }
128
-
131
+
129
132
  it { should respond_to( :reset_password, :reset_password! ) }
130
-
133
+
131
134
  describe '#reset_password' do
132
135
  context 'without an email' do
133
136
  before { subject.email = nil }
134
137
  after { subject.email = 'user@example.com' }
135
-
138
+
136
139
  it { expect { subject.reset_password }.to_not raise_exception }
137
- it do
140
+ it do
138
141
  subject.reset_password.should == false
139
142
  subject.errors.should_not be_empty
140
143
  end
141
144
  end
142
-
145
+
143
146
  context 'with an email' do
144
147
  it { expect { subject.reset_password }.to_not raise_exception }
145
148
  it { subject.reset_password.should == true }
146
149
  end
147
150
  end
148
-
151
+
149
152
  describe '#reset_password!' do
150
153
  describe 'without an email' do
151
154
  before { subject.email = nil }
152
155
  after { subject.email = 'user@example.com' }
153
-
156
+
154
157
  it { expect { subject.reset_password! }.to raise_exception }
155
158
  end
156
-
159
+
157
160
  context 'with an email' do
158
161
  it { expect { subject.reset_password! }.to_not raise_exception }
159
162
  it { subject.reset_password!.should == true }
160
163
  end
161
164
  end
162
165
  end
163
-
166
+
164
167
  shared_examples_for 'a varying privileges user' do |method, header_target|
165
168
  describe "##{method}" do
166
169
  let( :request ) { subject.send( method, sent_headers: true ) }
@@ -168,47 +171,47 @@ describe Opium::User do
168
171
  request
169
172
  subject.send( :sent_headers )
170
173
  end
171
-
174
+
172
175
  context 'with a session token' do
173
176
  subject { Opium::User.new( id: 'abcd1234', session_token: 'super-secret-session-id', usernmae: 'user', email: 'user@email.com' ) }
174
-
177
+
175
178
  it { expect( Opium::User.get_header_for( *header_target, subject ) ).to_not be_empty }
176
-
179
+
177
180
  it { expect( sent_headers ).to be_a( Hash ) }
178
181
  it { expect( sent_headers.keys ).to include( 'X-Parse-Application-Id', 'X-Parse-Rest-Api-Key', 'X-Parse-Session-Token' ) }
179
182
  it { expect( sent_headers.keys ).to_not include( 'X-Parse-Master-Key' ) }
180
183
  end
181
-
184
+
182
185
  context 'without a session token' do
183
186
  subject { Opium::User.new( id: 'abcd1234', session_token: nil, username: 'user', email: 'user@email.com' ) }
184
-
187
+
185
188
  it { expect( Opium::User.get_header_for( *header_target, subject ) ).to be_empty }
186
-
189
+
187
190
  it { expect( sent_headers ).to be_a( Hash ) }
188
191
  it { expect( sent_headers.keys ).to include( 'X-Parse-Application-Id', 'X-Parse-Master-Key' ) }
189
192
  it { expect( sent_headers.keys ).to_not include( 'X-Parse-Rest-Api-Key, X-Parse-Session-Token' ) }
190
193
  end
191
194
  end
192
195
  end
193
-
196
+
194
197
  it_behaves_like 'a varying privileges user', :save, [:put, :update]
195
198
  it_behaves_like 'a varying privileges user', :delete, [:delete, :delete]
196
-
199
+
197
200
  context 'within a subclass' do
198
201
  before do
199
202
  stub_const( 'SpecialUser', Class.new(Opium::User) do
200
203
  field :has_web_access, type: Opium::Boolean
201
204
  end )
202
205
  end
203
-
206
+
204
207
  subject { SpecialUser }
205
-
208
+
206
209
  it { is_expected.to be <= Opium::User }
207
210
  it { is_expected.to respond_to( :field, :fields ) }
208
211
  it { expect( subject.fields.keys ).to include( 'username', 'password', 'email', 'has_web_access' ) }
209
-
212
+
210
213
  it { expect( subject ).to have_heightened_privileges }
211
214
  it { expect( subject.object_prefix ).to be_empty }
212
215
  it { expect( subject.resource_name ).to eq 'users' }
213
216
  end
214
- end
217
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: opium
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.4
4
+ version: 1.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Bowers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-03-01 00:00:00.000000000 Z
11
+ date: 2017-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -375,7 +375,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
375
375
  version: '0'
376
376
  requirements: []
377
377
  rubyforge_project:
378
- rubygems_version: 2.5.1
378
+ rubygems_version: 2.6.11
379
379
  signing_key:
380
380
  specification_version: 4
381
381
  summary: An Object Parse.com Mapping technology for defining models.