parse-stack 1.5.3 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/parse-ruby-sdk.png +0 -0
  3. data/Changes.md +25 -1
  4. data/Gemfile.lock +4 -4
  5. data/README.md +37 -31
  6. data/bin/console +3 -0
  7. data/lib/parse/api/all.rb +2 -1
  8. data/lib/parse/api/apps.rb +12 -0
  9. data/lib/parse/api/config.rb +5 -1
  10. data/lib/parse/api/files.rb +1 -0
  11. data/lib/parse/api/hooks.rb +1 -0
  12. data/lib/parse/api/objects.rb +4 -1
  13. data/lib/parse/api/push.rb +1 -0
  14. data/lib/parse/api/{schemas.rb → schema.rb} +7 -0
  15. data/lib/parse/api/server.rb +44 -0
  16. data/lib/parse/api/sessions.rb +1 -0
  17. data/lib/parse/api/users.rb +4 -1
  18. data/lib/parse/client.rb +109 -73
  19. data/lib/parse/client/authentication.rb +2 -1
  20. data/lib/parse/client/batch.rb +9 -1
  21. data/lib/parse/client/body_builder.rb +16 -1
  22. data/lib/parse/client/caching.rb +15 -13
  23. data/lib/parse/client/protocol.rb +27 -15
  24. data/lib/parse/client/response.rb +26 -8
  25. data/lib/parse/model/acl.rb +1 -1
  26. data/lib/parse/model/associations/belongs_to.rb +18 -19
  27. data/lib/parse/model/associations/collection_proxy.rb +6 -0
  28. data/lib/parse/model/associations/has_many.rb +5 -6
  29. data/lib/parse/model/bytes.rb +4 -1
  30. data/lib/parse/model/classes/user.rb +46 -44
  31. data/lib/parse/model/core/actions.rb +508 -460
  32. data/lib/parse/model/core/builder.rb +75 -0
  33. data/lib/parse/model/core/errors.rb +9 -0
  34. data/lib/parse/model/core/fetching.rb +42 -38
  35. data/lib/parse/model/core/properties.rb +46 -27
  36. data/lib/parse/model/core/querying.rb +231 -228
  37. data/lib/parse/model/core/schema.rb +76 -74
  38. data/lib/parse/model/date.rb +10 -2
  39. data/lib/parse/model/file.rb +16 -2
  40. data/lib/parse/model/geopoint.rb +9 -2
  41. data/lib/parse/model/model.rb +38 -7
  42. data/lib/parse/model/object.rb +60 -19
  43. data/lib/parse/model/pointer.rb +22 -1
  44. data/lib/parse/model/push.rb +6 -2
  45. data/lib/parse/query.rb +57 -11
  46. data/lib/parse/query/constraint.rb +5 -2
  47. data/lib/parse/query/constraints.rb +588 -589
  48. data/lib/parse/query/ordering.rb +2 -2
  49. data/lib/parse/stack.rb +1 -0
  50. data/lib/parse/stack/version.rb +1 -1
  51. data/lib/parse/webhooks.rb +30 -29
  52. data/lib/parse/webhooks/payload.rb +181 -168
  53. data/lib/parse/webhooks/registration.rb +1 -1
  54. data/parse-stack.gemspec +9 -9
  55. metadata +9 -12
@@ -5,18 +5,30 @@
5
5
  module Parse
6
6
  # Set of Parse protocol constants.
7
7
  module Protocol
8
- HOST = 'api.parse.com'
9
- SERVER_URL = 'https://api.parse.com/1/'
10
- APP_ID = 'X-Parse-Application-Id'
11
- API_KEY = 'X-Parse-REST-API-Key'
12
- MASTER_KEY = 'X-Parse-Master-Key'
13
- SESSION_TOKEN = 'X-Parse-Session-Token'
14
- REVOCABLE_SESSION = 'X-Parse-Revocable-Session'
15
- EMAIL = 'X-Parse-Email'
16
- PASSWORD = 'X-Parse-Password'
17
- INSTALLATION_ID = 'Parse-Installation-Id'
18
- CONTENT_TYPE = 'Content-Type'
19
- CONTENT_TYPE_FORMAT = 'application/json; charset=utf-8'
8
+ # The default server url, based on the hosted Parse platform.
9
+ SERVER_URL = 'https://api.parse.com/1/'.freeze
10
+ # The default legacy server url, based on the hosted Parse platform.
11
+ LEGACY_SERVER_URL = 'https://api.parse.com/1/'.freeze
12
+ # The request header field to send the application Id.
13
+ APP_ID = 'X-Parse-Application-Id'
14
+ # The request header field to send the REST API key.
15
+ API_KEY = 'X-Parse-REST-API-Key'
16
+ # The request header field to send the Master key.
17
+ MASTER_KEY = 'X-Parse-Master-Key'
18
+ # The request header field to send the revocable Session key.
19
+ SESSION_TOKEN = 'X-Parse-Session-Token'
20
+ # The request header field to request a revocable session token.
21
+ REVOCABLE_SESSION = 'X-Parse-Revocable-Session'
22
+ # The request header field to send the installation id.
23
+ INSTALLATION_ID = 'Parse-Installation-Id'
24
+ # The request header field to send an email when authenticating with Parse hosted platform.
25
+ EMAIL = 'X-Parse-Email'
26
+ # The request header field to send the password when authenticating with the Parse hosted platform.
27
+ PASSWORD = 'X-Parse-Password'
28
+ # The request header field for the Content type.
29
+ CONTENT_TYPE = 'Content-Type'
30
+ # The default content type format for sending API requests.
31
+ CONTENT_TYPE_FORMAT = 'application/json; charset=utf-8'
20
32
  end
21
33
 
22
34
  # All Parse error codes.
@@ -67,8 +79,8 @@ module Parse
67
79
  # LinkedIdMissing 250 Error code indicating that a user cannot be linked to an account because that account's id could not be found.
68
80
  # InvalidLinkedSession 251 Error code indicating that a user with a linked (e.g. Facebook) account has an invalid session.
69
81
  # UnsupportedService 252 Error code indicating that a service being linked (e.g. Facebook or Twitter) is unsupported.
70
- module ErrorCodes
71
-
72
- end
82
+ # module ErrorCodes
83
+ #
84
+ # end
73
85
 
74
86
  end
@@ -3,30 +3,47 @@
3
3
 
4
4
  require 'active_support'
5
5
  require 'active_support/json'
6
- # This is the model that represents a response from Parse. A Response can also
7
- # be a set of responses (from a Batch response).
6
+
8
7
  module Parse
9
8
 
10
- class ResponseError < StandardError; end;
9
+ # Represents a response from Parse server. A response can also
10
+ # be a set of responses (from a Batch response).
11
11
  class Response
12
12
  include Enumerable
13
13
 
14
+ # Code for an unknown error.
14
15
  ERROR_INTERNAL = 1
16
+ # Code when the server returns a 500 or is non-responsive.
15
17
  ERROR_SERVICE_UNAVAILABLE = 2
18
+ # Code when the request times out.
16
19
  ERROR_TIMEOUT = 124
20
+ # Code when the requests per second limit as been exceeded.
17
21
  ERROR_EXCEEDED_BURST_LIMIT = 155
22
+ # Code when a requested record is not found.
18
23
  ERROR_OBJECT_NOT_FOUND = 101
24
+ # Code when the username is missing in request.
19
25
  ERROR_USERNAME_MISSING = 200
26
+ # Code when the password is missing in request.
20
27
  ERROR_PASSWORD_MISSING = 201
28
+ # Code when the username is already in the system.
21
29
  ERROR_USERNAME_TAKEN = 202
30
+ # Code when the email is already in the system.
22
31
  ERROR_EMAIL_TAKEN = 203
32
+ # Code when the email is not found
23
33
  ERROR_EMAIL_NOT_FOUND = 205
34
+ # Code when the email is invalid
24
35
  ERROR_EMAIL_INVALID = 125
25
36
 
26
- ERROR = "error"
27
- CODE = "code"
28
- RESULTS = "results"
29
- COUNT = "count"
37
+ # The field name for the error.
38
+ ERROR = "error".freeze
39
+ # The field name for the success.
40
+ SUCCESS = "success".freeze
41
+ # The field name for the error code.
42
+ CODE = "code".freeze
43
+ # The field name for the results of the request.
44
+ RESULTS = "results".freeze
45
+ # The field name for the count result in a count response.
46
+ COUNT = "count".freeze
30
47
 
31
48
  # @!attribute [rw] parse_class
32
49
  # @return [String] the Parse class for this request
@@ -85,7 +102,7 @@ module Parse
85
102
  # if batch response, generate array based on the response hash.
86
103
  @result.map do |r|
87
104
  next r unless r.is_a?(Hash)
88
- hash = r["success"] || r[ERROR]
105
+ hash = r[SUCCESS] || r[ERROR]
89
106
  Parse::Response.new hash
90
107
  end
91
108
  end
@@ -145,6 +162,7 @@ module Parse
145
162
  self
146
163
  end
147
164
 
165
+ # @!visibility private
148
166
  def inspect
149
167
  if error?
150
168
  "#<#{self.class} @code=#{code} @error='#{error}'>"
@@ -100,7 +100,7 @@ module Parse
100
100
  def permissions
101
101
  @permissions ||= {}
102
102
  end
103
-
103
+ # The key field value for public permissions.
104
104
  PUBLIC = "*"
105
105
 
106
106
  # Create a new ACL with default Public read/write permissions and any
@@ -57,23 +57,6 @@ module Parse
57
57
  # A hash mapping of all belongs_to associations for this model.
58
58
  # @return [Hash]
59
59
 
60
- # @!method key?
61
- # A dynamically generated method based on the value of `key` passed to the
62
- # belongs_to method, which returns true if this instance has a pointer for
63
- # this field.
64
- # @example
65
- #
66
- # class Post < Parse::Object
67
- # belongs_to :author # generates 'author?'
68
- # end
69
- #
70
- # post = Post.new
71
- # post.author? # => false
72
- # post.author = Author.new
73
- # post.author? # => true
74
- # @return [Boolean] true if field contains a Parse::Pointer or subclass.
75
-
76
-
77
60
  # @!method self.belongs_to(key, opts = {})
78
61
  # Creates a one-to-one association with another Parse model.
79
62
  # @param [Symbol] key The singularized version of the foreign class and the name of the
@@ -106,6 +89,22 @@ module Parse
106
89
  # @return [Parse::Object] a Parse::Object subclass when using the accessor
107
90
  # when fetching the association.
108
91
 
92
+ # @!method key?
93
+ # A dynamically generated method based on the value of `key` passed to the
94
+ # belongs_to method, which returns true if this instance has a pointer for
95
+ # this field.
96
+ # @example
97
+ #
98
+ # class Post < Parse::Object
99
+ # belongs_to :author # generates 'author?'
100
+ # end
101
+ #
102
+ # post = Post.new
103
+ # post.author? # => false
104
+ # post.author = Author.new
105
+ # post.author? # => true
106
+ # @return [Boolean] true if field contains a Parse::Pointer or subclass.
107
+
109
108
  # @!visibility private
110
109
  def self.included(base)
111
110
  base.extend(ClassMethods)
@@ -130,10 +129,10 @@ module Parse
130
129
  set_attribute_method = :"#{key}_set_attribute!"
131
130
 
132
131
  if self.fields[key].present? && Parse::Properties::BASE_FIELD_MAP[key].nil?
133
- raise Parse::Properties::DefinitionError, "Belongs relation #{self}##{key} already defined with type #{klassName}"
132
+ raise ArgumentError, "Belongs relation #{self}##{key} already defined with type #{klassName}"
134
133
  end
135
134
  if self.fields[parse_field].present?
136
- raise Parse::Properties::DefinitionError, "Alias belongs_to #{self}##{parse_field} conflicts with previously defined property."
135
+ raise ArgumentError, "Alias belongs_to #{self}##{parse_field} conflicts with previously defined property."
137
136
  end
138
137
  # store this attribute in the attributes hash with the proper remote column name.
139
138
  # we know the type is pointer.
@@ -113,6 +113,7 @@ module Parse
113
113
  # @!attribute [rw] collection
114
114
  # Set the internal collection of items without dirty tracking or
115
115
  # change notifications.
116
+ # @return [Array] the collection
116
117
  def set_collection!(list)
117
118
  @collection = list
118
119
  end
@@ -230,6 +231,7 @@ module Parse
230
231
  collection.count
231
232
  end
232
233
 
234
+ # @return [Hash] a JSON representation
233
235
  def as_json(*args)
234
236
  collection.as_json(args)
235
237
  end
@@ -253,21 +255,25 @@ module Parse
253
255
  forward "#{@key}_will_change!"
254
256
  end
255
257
 
258
+ # Alias for Array#each
256
259
  def each
257
260
  return collection.enum_for(:each) unless block_given?
258
261
  collection.each &Proc.new
259
262
  end
260
263
 
264
+ # Alias for Array#map
261
265
  def map
262
266
  return collection.enum_for(:map) unless block_given?
263
267
  collection.map &Proc.new
264
268
  end
265
269
 
270
+ # Alias for Array#select
266
271
  def select
267
272
  return collection.enum_for(:select) unless block_given?
268
273
  collection.select &Proc.new
269
274
  end
270
275
 
276
+ # @!visibility private
271
277
  def inspect
272
278
  "#<#{self.class} changed?=#{changed?} @collection=#{@collection.inspect} >"
273
279
  end
@@ -366,15 +366,15 @@ module Parse
366
366
  results.send(m, *args, &chained_block)
367
367
  end
368
368
 
369
- query.define_singleton_method(:to_s) { self.results.to_s }
370
- query.define_singleton_method(:inspect) { self.results.to_a.inspect }
371
-
369
+ Parse::Query.apply_auto_introspection!(query)
370
+
372
371
  return query if block.nil?
373
372
  query.results(&block)
374
373
  end
375
374
 
376
375
  end
377
376
 
377
+ # Define a one-to-many or many-to-many association between the local model and a foreign class.
378
378
  def has_many(key, scope = nil, **opts)
379
379
  opts[:through] ||= :query
380
380
 
@@ -398,10 +398,10 @@ module Parse
398
398
 
399
399
  # verify that the user did not duplicate properties or defined different properties with the same name
400
400
  if self.fields[key].present? && Parse::Properties::BASE_FIELD_MAP[key].nil?
401
- raise Parse::Properties::DefinitionError, "Has_many property #{self}##{key} already defined with type #{klassName}"
401
+ raise ArgumentError, "Has_many property #{self}##{key} already defined with type #{klassName}"
402
402
  end
403
403
  if self.fields[parse_field].present?
404
- raise Parse::Properties::DefinitionError, "Alias has_many #{self}##{parse_field} conflicts with previously defined property."
404
+ raise ArgumentError, "Alias has_many #{self}##{parse_field} conflicts with previously defined property."
405
405
  end
406
406
  # validations
407
407
  validates_presence_of(key) if opts[:required]
@@ -545,7 +545,6 @@ module Parse
545
545
  end
546
546
 
547
547
  # @return [Boolean] true if there are pending relational changes for
548
- # associations defined using Parse Relations.
549
548
  def relation_changes?
550
549
  changed.any? { |key| relations[key.to_sym] }
551
550
  end
@@ -11,6 +11,7 @@ module Parse
11
11
 
12
12
  # Support for the Bytes type in Parse
13
13
  class Bytes < Model
14
+ # The default attributes in a Parse Bytes hash.
14
15
  ATTRIBUTES = {__type: :string, base64: :string }.freeze
15
16
  # @return [String] the base64 string representing the content
16
17
  attr_accessor :base64
@@ -26,7 +27,9 @@ module Parse
26
27
  @base64 = (bytes.is_a?(Bytes) ? bytes.base64 : bytes).dup
27
28
  end
28
29
 
29
- # @return [Hash]
30
+ # @!attribute attributes
31
+ # Supports for mass assignment of values and encoding to JSON.
32
+ # @return [ATTRIBUTES]
30
33
  def attributes
31
34
  ATTRIBUTES
32
35
  end
@@ -3,20 +3,22 @@
3
3
 
4
4
  require_relative '../object'
5
5
  module Parse
6
- # 200 Error code indicating that the username is missing or empty.
7
- class UsernameMissingError < StandardError; end;
8
- # 201 Error code indicating that the password is missing or empty.
9
- class PasswordMissingError < StandardError; end;
10
- # Error code 202: indicating that the username has already been taken.
11
- class UsernameTakenError < StandardError; end;
12
- # 203 Error code indicating that the email has already been taken.
13
- class EmailTakenError < StandardError; end;
14
- # 204 Error code indicating that the email is missing, but must be specified.
15
- class EmailMissing < StandardError; end;
16
- # 205 Error code indicating that a user with the specified email was not found.
17
- class EmailNotFound < StandardError; end;
18
- # 125 Error code indicating that the email address was invalid.
19
- class InvalidEmailAddress < StandardError; end;
6
+ class Error
7
+ # 200 Error code indicating that the username is missing or empty.
8
+ class UsernameMissingError < Error; end;
9
+ # 201 Error code indicating that the password is missing or empty.
10
+ class PasswordMissingError < Error; end;
11
+ # Error code 202: indicating that the username has already been taken.
12
+ class UsernameTakenError < Error; end;
13
+ # 203 Error code indicating that the email has already been taken.
14
+ class EmailTakenError < Error; end;
15
+ # 204 Error code indicating that the email is missing, but must be specified.
16
+ class EmailMissing < Error; end;
17
+ # 205 Error code indicating that a user with the specified email was not found.
18
+ class EmailNotFound < Error; end;
19
+ # 125 Error code indicating that the email address was invalid.
20
+ class InvalidEmailAddress < Error; end;
21
+ end
20
22
 
21
23
  # The main class representing the _User table in Parse. A user can either be signed up or anonymous.
22
24
  # All users need to have a username and a password, with email being optional but globally unique if set.
@@ -168,20 +170,20 @@ module Parse
168
170
  # Adds the third-party authentication data to for a given service.
169
171
  # @param service_name [Symbol] The name of the service (ex. :facebook)
170
172
  # @param data [Hash] The body of the OAuth data. Dependent on each service.
171
- # @raise [ResponseError] If user was not successfully linked
173
+ # @raise [Parse::Client::ResponseError] If user was not successfully linked
172
174
  def link_auth_data!(service_name, **data)
173
175
  response = client.set_service_auth_data(id, service_name, data)
174
- raise Parse::ResponseError, response if response.error?
176
+ raise Parse::Client::ResponseError, response if response.error?
175
177
  apply_attributes!(response.result)
176
178
  end
177
179
 
178
180
  # Removes third-party authentication data for this user
179
181
  # @param service_name [Symbol] The name of the third-party service (ex. :facebook)
180
- # @raise [ResponseError] If user was not successfully unlinked
182
+ # @raise [Parse::Client::ResponseError] If user was not successfully unlinked
181
183
  # @return [Boolean] True/false if successful.
182
184
  def unlink_auth_data!(service_name)
183
185
  response = client.set_service_auth_data(id, service_name, nil)
184
- raise Parse::ResponseError, response if response.error?
186
+ raise Parse::Client::ResponseError, response if response.error?
185
187
  apply_attributes!(response.result)
186
188
  end
187
189
 
@@ -208,21 +210,21 @@ module Parse
208
210
 
209
211
  # You may set a password for this user when you are creating them. Parse never returns a
210
212
  # @param passwd The user's password to be used for signing up.
211
- # @raise [Parse::UsernameMissingError] If username is missing.
212
- # @raise [Parse::PasswordMissingError] If password is missing.
213
- # @raise [Parse::UsernameTakenError] If the username has already been taken.
214
- # @raise [Parse::EmailTakenError] If the email has already been taken (or exists in the system).
215
- # @raise [Parse::InvalidEmailAddress] If the email is invalid.
216
- # @raise [Parse::ResponseError] An unknown error occurred.
213
+ # @raise [Parse::Error::UsernameMissingError] If username is missing.
214
+ # @raise [Parse::Error::PasswordMissingError] If password is missing.
215
+ # @raise [Parse::Error::UsernameTakenError] If the username has already been taken.
216
+ # @raise [Parse::Error::EmailTakenError] If the email has already been taken (or exists in the system).
217
+ # @raise [Parse::Error::InvalidEmailAddress] If the email is invalid.
218
+ # @raise [Parse::Client::ResponseError] An unknown error occurred.
217
219
  # @return [Boolean] True if signup it was successful. If it fails an exception is thrown.
218
220
  def signup!(passwd = nil)
219
221
  self.password = passwd || password
220
222
  if username.blank?
221
- raise Parse::UsernameMissingError, "Signup requires a username."
223
+ raise Parse::Error::UsernameMissingError, "Signup requires a username."
222
224
  end
223
225
 
224
226
  if password.blank?
225
- raise Parse::PasswordMissingError, "Signup requires a password."
227
+ raise Parse::Error::PasswordMissingError, "Signup requires a password."
226
228
  end
227
229
 
228
230
  signup_attrs = attribute_updates
@@ -238,17 +240,17 @@ module Parse
238
240
 
239
241
  case response.code
240
242
  when Parse::Response::ERROR_USERNAME_MISSING
241
- raise Parse::UsernameMissingError, response
243
+ raise Parse::Error::UsernameMissingError, response
242
244
  when Parse::Response::ERROR_PASSWORD_MISSING
243
- raise Parse::PasswordMissingError, response
245
+ raise Parse::Error::PasswordMissingError, response
244
246
  when Parse::Response::ERROR_USERNAME_TAKEN
245
- raise Parse::UsernameTakenError, response
247
+ raise Parse::Error::UsernameTakenError, response
246
248
  when Parse::Response::ERROR_EMAIL_TAKEN
247
- raise Parse::EmailTakenError, response
249
+ raise Parse::Error::EmailTakenError, response
248
250
  when Parse::Response::ERROR_EMAIL_INVALID
249
- raise Parse::InvalidEmailAddress, response
251
+ raise Parse::Error::InvalidEmailAddress, response
250
252
  end
251
- raise Parse::ResponseError, response
253
+ raise Parse::Client::ResponseError, response
252
254
  end
253
255
 
254
256
  # Login and get a session token for this user.
@@ -290,12 +292,12 @@ module Parse
290
292
  # Creates a new Parse::User given a hash that maps to the fields defined in your Parse::User collection.
291
293
  # @param body [Hash] The hash containing the Parse::User fields. The field `username` and `password` are required.
292
294
  # @option opts [Boolean] :master_key Whether the master key should be used for this request.
293
- # @raise [UsernameMissingError] If username is missing.
294
- # @raise [PasswordMissingError] If password is missing.
295
- # @raise [UsernameTakenError] If the username has already been taken.
296
- # @raise [EmailTakenError] If the email has already been taken (or exists in the system).
297
- # @raise [InvalidEmailAddress] If the email is invalid.
298
- # @raise [ResponseError] An unknown error occurred.
295
+ # @raise [Parse::Error::UsernameMissingError] If username is missing.
296
+ # @raise [Parse::Error::PasswordMissingError] If password is missing.
297
+ # @raise [Parse::Error::UsernameTakenError] If the username has already been taken.
298
+ # @raise [Parse::Error::EmailTakenError] If the email has already been taken (or exists in the system).
299
+ # @raise [Parse::Error::InvalidEmailAddress] If the email is invalid.
300
+ # @raise [Parse::Client::ResponseError] An unknown error occurred.
299
301
  # @return [User] Returns a successfully created Parse::User.
300
302
  def self.create(body, **opts)
301
303
  response = client.create_user(body, opts: opts)
@@ -305,15 +307,15 @@ module Parse
305
307
 
306
308
  case response.code
307
309
  when Parse::Response::ERROR_USERNAME_MISSING
308
- raise Parse::UsernameMissingError, response
310
+ raise Parse::Error::UsernameMissingError, response
309
311
  when Parse::Response::ERROR_PASSWORD_MISSING
310
- raise Parse::PasswordMissingError, response
312
+ raise Parse::Error::PasswordMissingError, response
311
313
  when Parse::Response::ERROR_USERNAME_TAKEN
312
- raise Parse::UsernameTakenError, response
314
+ raise Parse::Error::UsernameTakenError, response
313
315
  when Parse::Response::ERROR_EMAIL_TAKEN
314
- raise Parse::EmailTakenError, response
316
+ raise Parse::Error::EmailTakenError, response
315
317
  end
316
- raise Parse::ResponseError, response
318
+ raise Parse::Client::ResponseError, response
317
319
  end
318
320
 
319
321
  # Automatically and implicitly signup a user if it did not already exists and
@@ -372,7 +374,7 @@ module Parse
372
374
  # @see #session!
373
375
  def self.session(token, opts = {})
374
376
  self.session! token, opts
375
- rescue InvalidSessionTokenError => e
377
+ rescue Parse::Error::InvalidSessionTokenError => e
376
378
  nil
377
379
  end
378
380