google-cloud-firestore 0.22.0 → 0.23.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/README.md +8 -8
  4. data/lib/google-cloud-firestore.rb +1 -1
  5. data/lib/google/cloud/firestore.rb +46 -0
  6. data/lib/google/cloud/firestore/batch.rb +1 -1
  7. data/lib/google/cloud/firestore/client.rb +18 -13
  8. data/lib/google/cloud/firestore/convert.rb +78 -35
  9. data/lib/google/cloud/firestore/credentials.rb +2 -12
  10. data/lib/google/cloud/firestore/document_change.rb +124 -0
  11. data/lib/google/cloud/firestore/document_listener.rb +125 -0
  12. data/lib/google/cloud/firestore/document_reference.rb +35 -0
  13. data/lib/google/cloud/firestore/document_snapshot.rb +91 -9
  14. data/lib/google/cloud/firestore/field_path.rb +23 -13
  15. data/lib/google/cloud/firestore/query.rb +513 -69
  16. data/lib/google/cloud/firestore/query_listener.rb +118 -0
  17. data/lib/google/cloud/firestore/query_snapshot.rb +121 -0
  18. data/lib/google/cloud/firestore/service.rb +8 -0
  19. data/lib/google/cloud/firestore/transaction.rb +2 -2
  20. data/lib/google/cloud/firestore/v1beta1.rb +62 -37
  21. data/lib/google/cloud/firestore/v1beta1/credentials.rb +41 -0
  22. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/common.rb +1 -1
  23. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/document.rb +5 -4
  24. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/firestore.rb +1 -12
  25. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/query.rb +4 -1
  26. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/write.rb +37 -8
  27. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/any.rb +1 -1
  28. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/empty.rb +28 -0
  29. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/timestamp.rb +1 -1
  30. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/wrappers.rb +1 -1
  31. data/lib/google/cloud/firestore/v1beta1/doc/google/rpc/status.rb +1 -1
  32. data/lib/google/cloud/firestore/v1beta1/firestore_client.rb +124 -56
  33. data/lib/google/cloud/firestore/v1beta1/firestore_client_config.json +2 -2
  34. data/lib/google/cloud/firestore/version.rb +1 -1
  35. data/lib/google/cloud/firestore/watch/enumerator_queue.rb +47 -0
  36. data/lib/google/cloud/firestore/watch/inventory.rb +280 -0
  37. data/lib/google/cloud/firestore/watch/listener.rb +298 -0
  38. data/lib/google/cloud/firestore/watch/order.rb +98 -0
  39. data/lib/google/firestore/v1beta1/firestore_services_pb.rb +2 -4
  40. data/lib/google/firestore/v1beta1/query_pb.rb +1 -0
  41. data/lib/google/firestore/v1beta1/write_pb.rb +2 -0
  42. metadata +40 -3
  43. data/lib/google/cloud/firestore/v1beta1/doc/overview.rb +0 -53
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96192127f7ee4dafde2959f7d1f463fb7fa24ab09055e7764e6f83fd0aeda8be
4
- data.tar.gz: 4848935ba22ddc1b1cb9a24825a03802651d9dfc206b3d93a71af91b4e06eb8d
3
+ metadata.gz: 6c249cbd2bcfd378c5ab77ce2b5e717f629e741ca8c30b259897cfcd87ca732a
4
+ data.tar.gz: 90d335ef83762a548eefdc4ed2a35ea5c5c830a1069bd7b969e67a704496cf3b
5
5
  SHA512:
6
- metadata.gz: 523e76c8d3829de28395b9fffd54dc2cd098e08cbc204836ae6836f85f29028b36721b2f66ebb455e5d1895b9d19c1cd1c23f5aee4fffae0842a9df897ce227b
7
- data.tar.gz: 5ec65a1d3ecce996ab3a1d344495df11f84e5cce44d1932c1aea44aa877e5acf7a5e4fec9f7661ff6eb9c49a99db7c2d17109d825a0f5ee2959efe9e6bf73b34
6
+ metadata.gz: 4f8c8534c873bf3b86e8bccce74529c69ffebb5ba14e377ad60e0b32ae319ae22d8b96ff3eece237786952d169f49210168f3243a220e963b363d740c84aa54f
7
+ data.tar.gz: e0ae957a20ae64ae3c26cc6f379c12080fb8d71cf6b30bc0e4717787e7fadf785dcd5694a5d2422477639cb971da33715f21dae5f20613f490dd8a3ab5d9b1ba
data/.yardopts CHANGED
@@ -2,6 +2,7 @@
2
2
  --title=Google Cloud Firestore API
3
3
  --exclude lib/google/firestore/v1beta1
4
4
  --markup markdown
5
+ --markup-provider redcarpet
5
6
 
6
7
  ./lib/**/*.rb
7
8
  -
data/README.md CHANGED
@@ -50,14 +50,14 @@ end
50
50
 
51
51
  ## Supported Ruby Versions
52
52
 
53
- This library is supported on Ruby 2.0+.
54
-
55
- However, Ruby 2.3 or later is strongly recommended, as earlier releases have
56
- reached or are nearing end-of-life. After June 1, 2018, Google will provide
57
- official support only for Ruby versions that are considered current and
58
- supported by Ruby Core (that is, Ruby versions that are either in normal
59
- maintenance or in security maintenance).
60
- See https://www.ruby-lang.org/en/downloads/branches/ for further details.
53
+ This library is supported on Ruby 2.3+.
54
+
55
+ Google provides official support for Ruby versions that are actively supported
56
+ by Ruby Core—that is, Ruby versions that are either in normal maintenance or
57
+ in security maintenance, and not end of life. Currently, this means Ruby 2.3
58
+ and later. Older versions of Ruby _may_ still work, but are unsupported and not
59
+ recommended. See https://www.ruby-lang.org/en/downloads/branches/ for details
60
+ about the Ruby support schedule.
61
61
 
62
62
  [Client Library Documentation]: https://googlecloudplatform.github.io/google-cloud-ruby/#/docs/google-cloud-firestore/latest/google/firestore/v1beta1
63
63
  [Product Documentation]: https://cloud.google.com/firestore
@@ -76,7 +76,7 @@ module Google
76
76
  # present, the default project for the credentials is used.
77
77
  # @param [String, Hash, Google::Auth::Credentials] credentials The path to
78
78
  # the keyfile as a String, the contents of the keyfile as a Hash, or a
79
- # Google::Auth::Credentials object. (See {Datastore::Credentials})
79
+ # Google::Auth::Credentials object. (See {Firestore::Credentials})
80
80
  # @param [String, Array<String>] scope The OAuth 2.0 scopes controlling the
81
81
  # set of resources and operations that the connection can access. See
82
82
  # [Using OAuth 2.0 to Access Google
@@ -390,6 +390,52 @@ module Google
390
390
  # user_ref.update({ nested_field_path => "Pasta" })
391
391
  # ```
392
392
  #
393
+ # ### Listening for changes
394
+ #
395
+ # You can listen to a document reference or a collection reference/query for
396
+ # changes. The current document snapshot or query results snapshot will be
397
+ # yielded first, and each time the contents change.
398
+ #
399
+ # You can use {Firestore::DocumentReference#listen} to be notified of
400
+ # changes to a single document:
401
+ #
402
+ # ```ruby
403
+ # require "google/cloud/firestore"
404
+ #
405
+ # firestore = Google::Cloud::Firestore.new
406
+ #
407
+ # # Get a document reference
408
+ # nyc_ref = firestore.doc "cities/NYC"
409
+ #
410
+ # listener = nyc_ref.listen do |snapshot|
411
+ # puts "The population of #{snapshot[:name]} "
412
+ # puts "is #{snapshot[:population]}."
413
+ # end
414
+ #
415
+ # # When ready, stop the listen operation and close the stream.
416
+ # listener.stop
417
+ # ```
418
+ #
419
+ # You can use {Firestore::Query#listen} to be notified of changes to any
420
+ # document contained in the query:
421
+ #
422
+ # ```ruby
423
+ # require "google/cloud/firestore"
424
+ #
425
+ # firestore = Google::Cloud::Firestore.new
426
+ #
427
+ # # Create a query
428
+ # query = firestore.col(:cities).order(:population, :desc)
429
+ #
430
+ # listener = query.listen do |snapshot|
431
+ # puts "The query snapshot has #{snapshot.docs.count} documents "
432
+ # puts "and has #{snapshot.changes.count} changes."
433
+ # end
434
+ #
435
+ # # When ready, stop the listen operation and close the stream.
436
+ # listener.stop
437
+ # ```
438
+ #
393
439
  # ## Using transactions and batched writes
394
440
  #
395
441
  # Cloud Firestore supports atomic operations for reading and writing data.
@@ -30,7 +30,7 @@ module Google
30
30
  # atomically at a single logical point in time in a database.
31
31
  #
32
32
  # All changes are accumulated in memory until the block passed to
33
- # {Database#batch} completes. Unlike transactions, batches don't lock on
33
+ # {Client#batch} completes. Unlike transactions, batches don't lock on
34
34
  # document reads, should only fail if users provide preconditions, and are
35
35
  # not automatically retried.
36
36
  #
@@ -212,7 +212,7 @@ module Google
212
212
  ##
213
213
  # Creates a field path object representing the sentinel ID of a
214
214
  # document. It can be used in queries to sort or filter by the document
215
- # ID. See {Client#document_id}.
215
+ # ID. See {FieldPath#document_id}.
216
216
  #
217
217
  # @return [FieldPath] The field path object.
218
218
  #
@@ -225,9 +225,8 @@ module Google
225
225
  # cities_col = firestore.col "cities"
226
226
  #
227
227
  # # Create a query
228
- # query = cities_col.start_at("NYC").order(
229
- # Google::Cloud::Firestore::FieldPath.document_id
230
- # )
228
+ # query = cities_col.order(firestore.document_id)
229
+ # .start_at("NYC")
231
230
  #
232
231
  # query.get do |city|
233
232
  # puts "#{city.document_id} has #{city[:population]} residents."
@@ -383,31 +382,37 @@ module Google
383
382
  #
384
383
  def transaction max_retries: nil
385
384
  max_retries = 5 unless max_retries.is_a? Integer
386
- retries = 0
387
- backoff = 1.0
385
+ backoff = { current: 0, delay: 1.0, max: max_retries, mod: 1.3 }
388
386
 
389
387
  transaction = Transaction.from_client self
390
388
  begin
391
389
  yield transaction
392
390
  transaction.commit
393
391
  rescue Google::Cloud::UnavailableError => err
394
- # Re-raise if deadline has passed
395
- raise err if retries >= max_retries
396
- # Sleep with incremental backoff
397
- sleep(backoff *= 1.3)
392
+ # Re-raise if retried more than the max
393
+ raise err if backoff[:current] > backoff[:max]
394
+
395
+ # Sleep with incremental backoff before restarting
396
+ sleep backoff[:delay]
397
+
398
+ # Update increment backoff delay and retry counter
399
+ backoff[:delay] *= backoff[:mod]
400
+ backoff[:current] += 1
401
+
398
402
  # Create new transaction and retry
399
403
  transaction = Transaction.from_client \
400
404
  self, previous_transaction: transaction.transaction_id
401
- retries += 1
402
405
  retry
403
406
  rescue Google::Cloud::InvalidArgumentError => err
404
- # Return if a previous call has succeeded
405
- return nil if retries > 0
407
+ # Return if a previous call was retried but ultimately succeeded
408
+ return nil if backoff[:current] > 0
409
+
406
410
  # Re-raise error.
407
411
  raise err
408
412
  rescue StandardError => err
409
413
  # Rollback transaction when handling unexpected error
410
414
  transaction.rollback rescue nil
415
+
411
416
  # Re-raise error.
412
417
  raise err
413
418
  end
@@ -43,9 +43,9 @@ module Google
43
43
  Time.at timestamp.seconds, Rational(timestamp.nanos, 1000)
44
44
  end
45
45
 
46
- def fields_to_hash fields, context
46
+ def fields_to_hash fields, client
47
47
  Hash[fields.map do |key, value|
48
- [key.to_sym, value_to_raw(value, context)]
48
+ [key.to_sym, value_to_raw(value, client)]
49
49
  end]
50
50
  end
51
51
 
@@ -55,7 +55,7 @@ module Google
55
55
  end]
56
56
  end
57
57
 
58
- def value_to_raw value, context
58
+ def value_to_raw value, client
59
59
  case value.value_type
60
60
  when :null_value
61
61
  nil
@@ -73,13 +73,13 @@ module Google
73
73
  StringIO.new value.bytes_value
74
74
  when :reference_value
75
75
  Google::Cloud::Firestore::DocumentReference.from_path \
76
- value.reference_value, context
76
+ value.reference_value, client
77
77
  when :geo_point_value
78
78
  value.geo_point_value.to_hash
79
79
  when :array_value
80
- value.array_value.values.map { |v| value_to_raw v, context }
80
+ value.array_value.values.map { |v| value_to_raw v, client }
81
81
  when :map_value
82
- fields_to_hash value.map_value.fields, context
82
+ fields_to_hash value.map_value.fields, client
83
83
  end
84
84
  end
85
85
 
@@ -104,9 +104,12 @@ module Google
104
104
  Google::Firestore::V1beta1::Value.new(array_value:
105
105
  Google::Firestore::V1beta1::ArrayValue.new(values: values))
106
106
  elsif Hash === obj
107
- if obj.keys.sort == [:latitude, :longitude]
108
- Google::Firestore::V1beta1::Value.new(geo_point_value:
109
- Google::Type::LatLng.new(obj))
107
+ # keys have been changed to strings before the hash gets here
108
+ geo_pairs = hash_is_geo_point? obj
109
+ if geo_pairs
110
+ Google::Firestore::V1beta1::Value.new(
111
+ geo_point_value: hash_to_geo_point(obj, geo_pairs)
112
+ )
110
113
  else
111
114
  fields = hash_to_fields obj
112
115
  Google::Firestore::V1beta1::Value.new(map_value:
@@ -118,10 +121,30 @@ module Google
118
121
  Google::Firestore::V1beta1::Value.new bytes_value: content
119
122
  else
120
123
  raise ArgumentError,
121
- "A value of type #{obj.class} is not supported."
124
+ "A value of type #{obj.class} is not supported."
122
125
  end
123
126
  end
124
127
 
128
+ def hash_is_geo_point? hash
129
+ return false unless hash.keys.count == 2
130
+
131
+ pairs = hash.map { |k, v| [String(k), v] }.sort
132
+ if pairs.map(&:first) == ["latitude".freeze, "longitude".freeze]
133
+ pairs
134
+ end
135
+ end
136
+
137
+ def hash_to_geo_point hash, pairs = nil
138
+ pairs ||= hash_is_geo_point? hash
139
+
140
+ raise ArgumentError, "value is not a geo point" unless pairs
141
+
142
+ Google::Type::LatLng.new(
143
+ latitude: pairs.first.last,
144
+ longitude: pairs.last.last,
145
+ )
146
+ end
147
+
125
148
  def writes_for_create doc_path, data
126
149
  writes = []
127
150
 
@@ -164,13 +187,15 @@ module Google
164
187
  if merge == true
165
188
  # extract the leaf node field paths from data
166
189
  field_paths = identify_leaf_nodes data
190
+ allow_empty = true
167
191
  else
168
192
  field_paths = Array(merge).map do |field_path|
169
193
  field_path = FieldPath.parse field_path unless field_path.is_a? FieldPath
170
194
  field_path
171
195
  end
196
+ allow_empty = false
172
197
  end
173
- return writes_for_set_merge doc_path, data, field_paths
198
+ return writes_for_set_merge doc_path, data, field_paths, allow_empty
174
199
  end
175
200
 
176
201
  writes = []
@@ -195,9 +220,11 @@ module Google
195
220
  writes
196
221
  end
197
222
 
198
- def writes_for_set_merge doc_path, data, field_paths
223
+ def writes_for_set_merge doc_path, data, field_paths, allow_empty
199
224
  raise ArgumentError, "data is required" unless data.is_a? Hash
200
225
 
226
+ validate_field_paths! field_paths
227
+
201
228
  writes = []
202
229
 
203
230
  # Ensure provided field paths are valid.
@@ -231,22 +258,32 @@ module Google
231
258
  delete_valid_check = delete_valid_check.include? false
232
259
  raise ArgumentError, "deleted field not included in merge" if delete_valid_check
233
260
 
261
+ server_time_paths.select! do |server_time_path|
262
+ field_paths.any? do |field_path|
263
+ server_time_path.formatted_string.start_with? field_path.formatted_string
264
+ end
265
+ end
266
+
234
267
  # Choose only the data there are field paths for
235
268
  field_paths -= delete_paths
236
269
  field_paths -= server_time_paths
237
270
  data = select_by_field_paths data, field_paths
271
+ # Restore delete paths
272
+ field_paths += delete_paths
238
273
 
239
- if data.empty?
240
- if server_time_paths.empty?
274
+ if data.empty? && !allow_empty
275
+ if server_time_paths.empty? && delete_paths.empty?
241
276
  raise ArgumentError, "data required for set with merge"
242
277
  end
243
- else
278
+ end
279
+
280
+ if data.any? || field_paths.any? || (allow_empty && server_time_paths.empty?)
244
281
  writes << Google::Firestore::V1beta1::Write.new(
245
282
  update: Google::Firestore::V1beta1::Document.new(
246
283
  name: doc_path,
247
284
  fields: hash_to_fields(data)),
248
285
  update_mask: Google::Firestore::V1beta1::DocumentMask.new(
249
- field_paths: field_paths.map(&:formatted_string))
286
+ field_paths: field_paths.map(&:formatted_string).sort)
250
287
  )
251
288
  end
252
289
 
@@ -269,18 +306,7 @@ module Google
269
306
  end
270
307
 
271
308
  # Duplicate field paths check
272
- dup_keys = new_data_pairs.map(&:first).map(&:formatted_string)
273
- if dup_keys.size != dup_keys.uniq.size
274
- raise ArgumentError, "duplicate field paths"
275
- end
276
- dup_keys.each do |field_path|
277
- prefix_check = dup_keys.select do |this_path|
278
- this_path.start_with? "#{field_path}."
279
- end
280
- if prefix_check.any?
281
- raise ArgumentError, "one field cannot be a prefix of another"
282
- end
283
- end
309
+ validate_field_paths! new_data_pairs.map(&:first)
284
310
 
285
311
  delete_paths, new_data_pairs = new_data_pairs.partition do |field_path, value|
286
312
  value.is_a?(FieldValue) && value.type == :delete
@@ -301,10 +327,9 @@ module Google
301
327
 
302
328
  data, nested_server_time_paths = remove_field_value_from data, :server_time
303
329
 
304
- server_time_paths = root_server_time_paths + nested_server_time_paths
305
330
  server_time_paths = root_server_time_paths + nested_server_time_paths
306
331
 
307
- field_paths = (field_paths - (field_paths - identify_all_file_paths(data)) + delete_paths).uniq
332
+ field_paths = (field_paths + delete_paths).uniq
308
333
  field_paths.each do |field_path|
309
334
  raise ArgumentError, "empty paths not allowed" if field_path.fields.empty?
310
335
  end
@@ -319,7 +344,7 @@ module Google
319
344
  name: doc_path,
320
345
  fields: hash_to_fields(data)),
321
346
  update_mask: Google::Firestore::V1beta1::DocumentMask.new(
322
- field_paths: field_paths.map(&:formatted_string)),
347
+ field_paths: (field_paths).map(&:formatted_string).sort),
323
348
  current_document: Google::Firestore::V1beta1::Precondition.new(
324
349
  exists: true)
325
350
  )
@@ -464,13 +489,15 @@ module Google
464
489
  tmp_hash = ret_hash
465
490
  prev_hash = ret_hash
466
491
  dup_hash = hash.dup
467
- fields = field_path.fields
492
+ fields = field_path.fields.dup
468
493
  last_field = nil
469
494
 
470
495
  # squash fields until the key exists?
471
- until dup_hash.key? fields.first
472
- fields.unshift "#{fields.shift}.#{fields.shift}"
473
- break if fields.count <= 1
496
+ if fields.count > 1
497
+ until dup_hash.key? fields.first
498
+ fields.unshift "#{fields.shift}.#{fields.shift}"
499
+ break if fields.count <= 1
500
+ end
474
501
  end
475
502
 
476
503
  fields.each do |field|
@@ -482,9 +509,25 @@ module Google
482
509
  dup_hash = dup_hash[field]
483
510
  end
484
511
  prev_hash[last_field] = dup_hash
512
+ prev_hash.delete_if { |_k, v| v.nil? }
485
513
  ret_hash
486
514
  end
487
515
 
516
+ def validate_field_paths! field_paths
517
+ field_paths_strings = field_paths.map(&:formatted_string)
518
+ if field_paths_strings.size != field_paths_strings.uniq.size
519
+ raise ArgumentError, "duplicate field paths"
520
+ end
521
+ field_paths_strings.each do |field_path|
522
+ prefix_check = field_paths_strings.select do |this_path|
523
+ this_path.start_with? "#{field_path}."
524
+ end
525
+ if prefix_check.any?
526
+ raise ArgumentError, "one field cannot be a prefix of another"
527
+ end
528
+ end
529
+ end
530
+
488
531
  def deep_merge_hashes left_hash, right_hash
489
532
  right_hash.each_pair do |key, right_value|
490
533
  left_value = left_hash[key]
@@ -13,7 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
 
16
- require "googleauth"
16
+ require "google/cloud/firestore/v1beta1/credentials"
17
17
 
18
18
  module Google
19
19
  module Cloud
@@ -37,17 +37,7 @@ module Google
37
37
  #
38
38
  # firestore.project_id #=> "my-project"
39
39
  #
40
- class Credentials < Google::Auth::Credentials
41
- SCOPE = ["https://www.googleapis.com/auth/datastore"].freeze
42
- PATH_ENV_VARS = %w[FIRESTORE_CREDENTIALS FIRESTORE_KEYFILE
43
- GOOGLE_CLOUD_CREDENTIALS GOOGLE_CLOUD_KEYFILE
44
- GCLOUD_KEYFILE].freeze
45
- JSON_ENV_VARS = %w[FIRESTORE_CREDENTIALS_JSON FIRESTORE_KEYFILE_JSON
46
- GOOGLE_CLOUD_CREDENTIALS_JSON
47
- GOOGLE_CLOUD_KEYFILE_JSON
48
- GCLOUD_KEYFILE_JSON].freeze
49
- DEFAULT_PATHS = \
50
- ["~/.config/gcloud/application_default_credentials.json"].freeze
40
+ class Credentials < Google::Cloud::Firestore::V1beta1::Credentials
51
41
  end
52
42
  end
53
43
  end
@@ -0,0 +1,124 @@
1
+ # Copyright 2018 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ module Google
17
+ module Cloud
18
+ module Firestore
19
+ ##
20
+ # # DocumentChange
21
+ #
22
+ # A DocumentChange object represents a change to the document matching a
23
+ # query. It contains the document affected and the type of change that
24
+ # occurred (added, modifed, or removed).
25
+ #
26
+ # See {Query#listen} and {QuerySnapshot#changes}.
27
+ #
28
+ # @example
29
+ # require "google/cloud/firestore"
30
+ #
31
+ # firestore = Google::Cloud::Firestore.new
32
+ #
33
+ # # Create a query
34
+ # query = firestore.col(:cities).order(:population, :desc)
35
+ #
36
+ # listener = query.listen do |snapshot|
37
+ # puts "The query snapshot has #{snapshot.docs.count} documents "
38
+ # puts "and has #{snapshot.changes.count} changes."
39
+ # end
40
+ #
41
+ # # When ready, stop the listen operation and close the stream.
42
+ # listener.stop
43
+ #
44
+ class DocumentChange
45
+ ##
46
+ # The document snapshot object for the data.
47
+ #
48
+ # @return [DocumentSnapshot] document snapshot.
49
+ #
50
+ def doc
51
+ @doc
52
+ end
53
+ alias document doc
54
+
55
+ ##
56
+ # The type of change (':added', ':modified', or ':removed').
57
+ #
58
+ # @return [Symbol] The type of change.
59
+ #
60
+ def type
61
+ return :removed if @new_index.nil?
62
+ return :added if @old_index.nil?
63
+ :modified
64
+ end
65
+
66
+ ##
67
+ # Determines whether the document was added.
68
+ #
69
+ # @return [Boolean] Whether the document was added.
70
+ #
71
+ def added?
72
+ type == :added
73
+ end
74
+
75
+ ##
76
+ # Determines whether the document was modified.
77
+ #
78
+ # @return [Boolean] Whether the document was modified.
79
+ #
80
+ def modified?
81
+ type == :modified
82
+ end
83
+
84
+ ##
85
+ # Determines whether the document was removed.
86
+ #
87
+ # @return [Boolean] Whether the document was removed.
88
+ #
89
+ def removed?
90
+ type == :removed
91
+ end
92
+
93
+ ##
94
+ # The index in the documents array prior to the change.
95
+ #
96
+ # @return [Integer, nil] The old index
97
+ #
98
+ def old_index
99
+ @old_index
100
+ end
101
+
102
+ ##
103
+ # The index in the documents array after the change.
104
+ #
105
+ # @return [Integer, nil] The new index
106
+ #
107
+ def new_index
108
+ @new_index
109
+ end
110
+
111
+ ##
112
+ # @private New DocumentChange from a
113
+ # Google::Cloud::Firestore::DocumentSnapshot object.
114
+ def self.from_doc doc, old_index, new_index
115
+ new.tap do |s|
116
+ s.instance_variable_set :@doc, doc
117
+ s.instance_variable_set :@old_index, old_index
118
+ s.instance_variable_set :@new_index, new_index
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end