google-cloud-firestore 0.20.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +8 -0
  3. data/LICENSE +201 -0
  4. data/README.md +30 -0
  5. data/lib/google-cloud-firestore.rb +106 -0
  6. data/lib/google/cloud/firestore.rb +514 -0
  7. data/lib/google/cloud/firestore/batch.rb +462 -0
  8. data/lib/google/cloud/firestore/client.rb +449 -0
  9. data/lib/google/cloud/firestore/collection_reference.rb +249 -0
  10. data/lib/google/cloud/firestore/commit_response.rb +145 -0
  11. data/lib/google/cloud/firestore/convert.rb +561 -0
  12. data/lib/google/cloud/firestore/credentials.rb +35 -0
  13. data/lib/google/cloud/firestore/document_reference.rb +468 -0
  14. data/lib/google/cloud/firestore/document_snapshot.rb +324 -0
  15. data/lib/google/cloud/firestore/field_path.rb +216 -0
  16. data/lib/google/cloud/firestore/field_value.rb +113 -0
  17. data/lib/google/cloud/firestore/generate.rb +35 -0
  18. data/lib/google/cloud/firestore/query.rb +651 -0
  19. data/lib/google/cloud/firestore/service.rb +176 -0
  20. data/lib/google/cloud/firestore/transaction.rb +726 -0
  21. data/lib/google/cloud/firestore/v1beta1.rb +121 -0
  22. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/common.rb +63 -0
  23. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/document.rb +134 -0
  24. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/firestore.rb +584 -0
  25. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/query.rb +215 -0
  26. data/lib/google/cloud/firestore/v1beta1/doc/google/firestore/v1beta1/write.rb +167 -0
  27. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/any.rb +124 -0
  28. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/timestamp.rb +106 -0
  29. data/lib/google/cloud/firestore/v1beta1/doc/google/protobuf/wrappers.rb +89 -0
  30. data/lib/google/cloud/firestore/v1beta1/doc/google/rpc/status.rb +83 -0
  31. data/lib/google/cloud/firestore/v1beta1/doc/overview.rb +53 -0
  32. data/lib/google/cloud/firestore/v1beta1/firestore_client.rb +974 -0
  33. data/lib/google/cloud/firestore/v1beta1/firestore_client_config.json +100 -0
  34. data/lib/google/cloud/firestore/version.rb +22 -0
  35. data/lib/google/firestore/v1beta1/common_pb.rb +44 -0
  36. data/lib/google/firestore/v1beta1/document_pb.rb +49 -0
  37. data/lib/google/firestore/v1beta1/firestore_pb.rb +219 -0
  38. data/lib/google/firestore/v1beta1/firestore_services_pb.rb +87 -0
  39. data/lib/google/firestore/v1beta1/query_pb.rb +103 -0
  40. data/lib/google/firestore/v1beta1/write_pb.rb +73 -0
  41. metadata +251 -0
@@ -0,0 +1,145 @@
1
+ # Copyright 2017 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
+ require "google/cloud/firestore/v1beta1"
17
+ require "google/cloud/firestore/convert"
18
+
19
+ module Google
20
+ module Cloud
21
+ module Firestore
22
+ ##
23
+ # # CommitResponse
24
+ #
25
+ # The response for a commit.
26
+ #
27
+ # @example
28
+ # require "google/cloud/firestore"
29
+ #
30
+ # firestore = Google::Cloud::Firestore.new
31
+ #
32
+ # commit_response = firestore.batch do |b|
33
+ # # Set the data for NYC
34
+ # b.set("cities/NYC", { name: "New York City" })
35
+ #
36
+ # # Update the population for SF
37
+ # b.update("cities/SF", { population: 1000000 })
38
+ #
39
+ # # Delete LA
40
+ # b.delete("cities/LA")
41
+ # end
42
+ #
43
+ # puts commit_response.commit_time
44
+ # commit_response.write_results.each do |write_result|
45
+ # puts write_result.update_time
46
+ # end
47
+ #
48
+ class CommitResponse
49
+ ##
50
+ # @private
51
+ def initialize
52
+ @commit_time = nil
53
+ @write_results = []
54
+ end
55
+
56
+ ##
57
+ # The time at which the commit occurred.
58
+ #
59
+ # @return [Time] The commit time.
60
+ attr_accessor :commit_time
61
+
62
+ ##
63
+ # The result of applying the writes.
64
+ #
65
+ # This i-th write result corresponds to the i-th write in the request.
66
+ #
67
+ # @return [Array<CommitResponse::WriteResult>] The write results.
68
+ attr_accessor :write_results
69
+
70
+ ##
71
+ # @private
72
+ def self.from_grpc grpc, writes
73
+ return new if grpc.nil?
74
+
75
+ commit_time = Convert.timestamp_to_time grpc.commit_time
76
+
77
+ all_write_results = Array(grpc.write_results)
78
+
79
+ write_results = writes.map do |write|
80
+ update_time = nil
81
+ Array(write).count.times do
82
+ write_grpc = all_write_results.shift
83
+ if write_grpc
84
+ update_time ||= Convert.timestamp_to_time write_grpc.update_time
85
+ end
86
+ end
87
+ update_time ||= commit_time
88
+ WriteResult.new.tap do |write_result|
89
+ write_result.instance_variable_set :@update_time, update_time
90
+ end
91
+ end
92
+
93
+ new.tap do |resp|
94
+ resp.instance_variable_set :@commit_time, commit_time
95
+ resp.instance_variable_set :@write_results, write_results
96
+ end
97
+ end
98
+
99
+ ##
100
+ # # WriteResult
101
+ #
102
+ # Represents the result of applying a write.
103
+ #
104
+ # @example
105
+ # require "google/cloud/firestore"
106
+ #
107
+ # firestore = Google::Cloud::Firestore.new
108
+ #
109
+ # commit_response = firestore.batch do |b|
110
+ # # Set the data for NYC
111
+ # b.set("cities/NYC", { name: "New York City" })
112
+ #
113
+ # # Update the population for SF
114
+ # b.update("cities/SF", { population: 1000000 })
115
+ #
116
+ # # Delete LA
117
+ # b.delete("cities/LA")
118
+ # end
119
+ #
120
+ # puts commit_response.commit_time
121
+ # commit_response.write_results.each do |write_result|
122
+ # puts write_result.update_time
123
+ # end
124
+ #
125
+ class WriteResult
126
+ ##
127
+ # @private
128
+ def initialize
129
+ @update_time = nil
130
+ end
131
+
132
+ ##
133
+ # The last update time of the document after applying the write. Not
134
+ # set after a +delete+.
135
+ #
136
+ # If the write did not actually change the document, this will be
137
+ # the previous update_time.
138
+ #
139
+ # @return [Time] The last update time.
140
+ attr_accessor :update_time
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,561 @@
1
+ # Copyright 2017 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
+ require "google/cloud/firestore/v1beta1"
17
+ require "google/cloud/firestore/field_path"
18
+ require "time"
19
+ require "stringio"
20
+
21
+ module Google
22
+ module Cloud
23
+ module Firestore
24
+ ##
25
+ # @private Helper module for converting Protobuf values.
26
+ module Convert
27
+ # rubocop:disable all
28
+ module ClassMethods
29
+ def time_to_timestamp time
30
+ return nil if time.nil?
31
+
32
+ # Force the object to be a Time object.
33
+ time = time.to_time
34
+
35
+ Google::Protobuf::Timestamp.new \
36
+ seconds: time.to_i,
37
+ nanos: time.nsec
38
+ end
39
+
40
+ def timestamp_to_time timestamp
41
+ return nil if timestamp.nil?
42
+
43
+ Time.at timestamp.seconds, Rational(timestamp.nanos, 1000)
44
+ end
45
+
46
+ def fields_to_hash fields, context
47
+ Hash[fields.map do |key, value|
48
+ [key.to_sym, value_to_raw(value, context)]
49
+ end]
50
+ end
51
+
52
+ def hash_to_fields hash
53
+ Hash[hash.map do |key, value|
54
+ [String(key), raw_to_value(value)]
55
+ end]
56
+ end
57
+
58
+ def value_to_raw value, context
59
+ case value.value_type
60
+ when :null_value
61
+ nil
62
+ when :boolean_value
63
+ value.boolean_value
64
+ when :integer_value
65
+ Integer value.integer_value
66
+ when :double_value
67
+ value.double_value
68
+ when :timestamp_value
69
+ timestamp_to_time value.timestamp_value
70
+ when :string_value
71
+ value.string_value
72
+ when :bytes_value
73
+ StringIO.new Base64.decode64 value.bytes_value
74
+ when :reference_value
75
+ Google::Cloud::Firestore::DocumentReference.from_path \
76
+ value.reference_value, context
77
+ when :geo_point_value
78
+ value.geo_point_value.to_hash
79
+ when :array_value
80
+ value.array_value.values.map { |v| value_to_raw v, context }
81
+ when :map_value
82
+ fields_to_hash value.map_value.fields, context
83
+ end
84
+ end
85
+
86
+ def raw_to_value obj
87
+ if NilClass === obj
88
+ Google::Firestore::V1beta1::Value.new null_value: :NULL_VALUE
89
+ elsif TrueClass === obj || FalseClass === obj
90
+ Google::Firestore::V1beta1::Value.new boolean_value: obj
91
+ elsif Integer === obj
92
+ Google::Firestore::V1beta1::Value.new integer_value: obj
93
+ elsif Numeric === obj # Any number not an integer is a double
94
+ Google::Firestore::V1beta1::Value.new double_value: obj.to_f
95
+ elsif Time === obj || DateTime === obj || Date === obj
96
+ Google::Firestore::V1beta1::Value.new \
97
+ timestamp_value: time_to_timestamp(obj.to_time)
98
+ elsif String === obj || Symbol === obj
99
+ Google::Firestore::V1beta1::Value.new string_value: obj.to_s
100
+ elsif Google::Cloud::Firestore::DocumentReference === obj
101
+ Google::Firestore::V1beta1::Value.new reference_value: obj.path
102
+ elsif Array === obj
103
+ values = obj.map { |o| raw_to_value(o) }
104
+ Google::Firestore::V1beta1::Value.new(array_value:
105
+ Google::Firestore::V1beta1::ArrayValue.new(values: values))
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))
110
+ else
111
+ fields = hash_to_fields obj
112
+ Google::Firestore::V1beta1::Value.new(map_value:
113
+ Google::Firestore::V1beta1::MapValue.new(fields: fields))
114
+ end
115
+ elsif obj.respond_to?(:read) && obj.respond_to?(:rewind)
116
+ obj.rewind
117
+ content = obj.read.force_encoding "ASCII-8BIT"
118
+ encoded_content = Base64.strict_encode64 content
119
+ Google::Firestore::V1beta1::Value.new bytes_value: encoded_content
120
+ else
121
+ fail ArgumentError,
122
+ "A value of type #{obj.class} is not supported."
123
+ end
124
+ end
125
+
126
+ def writes_for_create doc_path, data
127
+ writes = []
128
+
129
+ if is_field_value_nested data, :delete
130
+ fail ArgumentError, "DELETE not allowed on create"
131
+ end
132
+ fail ArgumentError, "data is required" unless data.is_a? Hash
133
+
134
+ data, server_time_paths = remove_field_value_from data, :server_time
135
+
136
+ if data.any? || server_time_paths.empty?
137
+ write = Google::Firestore::V1beta1::Write.new(
138
+ update: Google::Firestore::V1beta1::Document.new(
139
+ name: doc_path,
140
+ fields: hash_to_fields(data)),
141
+ current_document: Google::Firestore::V1beta1::Precondition.new(
142
+ exists: false)
143
+ )
144
+ writes << write
145
+ end
146
+
147
+ if server_time_paths.any?
148
+ transform_write = transform_write doc_path, server_time_paths
149
+
150
+ if data.empty?
151
+ transform_write.current_document = \
152
+ Google::Firestore::V1beta1::Precondition.new(exists: false)
153
+ end
154
+
155
+ writes << transform_write
156
+ end
157
+
158
+ writes
159
+ end
160
+
161
+ def writes_for_set doc_path, data, merge: nil
162
+ fail ArgumentError, "data is required" unless data.is_a? Hash
163
+
164
+ if merge
165
+ if merge == true
166
+ # extract the leaf node field paths from data
167
+ field_paths = identify_leaf_nodes data
168
+ else
169
+ field_paths = Array(merge).map do |field_path|
170
+ field_path = FieldPath.parse field_path unless field_path.is_a? FieldPath
171
+ field_path
172
+ end
173
+ end
174
+ return writes_for_set_merge doc_path, data, field_paths
175
+ end
176
+
177
+ writes = []
178
+
179
+ data, delete_paths = remove_field_value_from data, :delete
180
+ if delete_paths.any?
181
+ fail ArgumentError, "DELETE not allowed on set"
182
+ end
183
+
184
+ data, server_time_paths = remove_field_value_from data, :server_time
185
+
186
+ writes << Google::Firestore::V1beta1::Write.new(
187
+ update: Google::Firestore::V1beta1::Document.new(
188
+ name: doc_path,
189
+ fields: hash_to_fields(data))
190
+ )
191
+
192
+ if server_time_paths.any?
193
+ writes << transform_write(doc_path, server_time_paths)
194
+ end
195
+
196
+ writes
197
+ end
198
+
199
+ def writes_for_set_merge doc_path, data, field_paths
200
+ fail ArgumentError, "data is required" unless data.is_a? Hash
201
+
202
+ writes = []
203
+
204
+ # Ensure provided field paths are valid.
205
+ all_valid = identify_leaf_nodes data
206
+ all_valid_check = field_paths.map do |verify_path|
207
+ if all_valid.include?(verify_path)
208
+ true
209
+ else
210
+ found_in_all_valid = all_valid.select do |fp|
211
+ fp.formatted_string.start_with? "#{verify_path.formatted_string}."
212
+ end
213
+ found_in_all_valid.any?
214
+ end
215
+ end
216
+ all_valid_check = all_valid_check.include? false
217
+ fail ArgumentError, "all fields must be in data" if all_valid_check
218
+
219
+ data, delete_paths = remove_field_value_from data, :delete
220
+ data, server_time_paths = remove_field_value_from data, :server_time
221
+
222
+ delete_valid_check = delete_paths.map do |delete_path|
223
+ if field_paths.include?(delete_path)
224
+ true
225
+ else
226
+ found_in_field_paths = field_paths.select do |fp|
227
+ fp.formatted_string.start_with? "#{delete_path.formatted_string}."
228
+ end
229
+ found_in_field_paths.any?
230
+ end
231
+ end
232
+ delete_valid_check = delete_valid_check.include? false
233
+ fail ArgumentError, "deleted field not included in merge" if delete_valid_check
234
+
235
+ # Choose only the data there are field paths for
236
+ field_paths -= delete_paths
237
+ field_paths -= server_time_paths
238
+ data = select_by_field_paths data, field_paths
239
+
240
+ if data.empty?
241
+ if server_time_paths.empty?
242
+ fail ArgumentError, "data required for set with merge"
243
+ end
244
+ else
245
+ writes << Google::Firestore::V1beta1::Write.new(
246
+ update: Google::Firestore::V1beta1::Document.new(
247
+ name: doc_path,
248
+ fields: hash_to_fields(data)),
249
+ update_mask: Google::Firestore::V1beta1::DocumentMask.new(
250
+ field_paths: field_paths.map(&:formatted_string))
251
+ )
252
+ end
253
+
254
+ if server_time_paths.any?
255
+ writes << transform_write(doc_path, server_time_paths)
256
+ end
257
+
258
+ writes
259
+ end
260
+
261
+ def writes_for_update doc_path, data, update_time: nil
262
+ writes = []
263
+
264
+ fail ArgumentError, "data is required" unless data.is_a? Hash
265
+
266
+ # Convert data to use FieldPath
267
+ new_data_pairs = data.map do |key, value|
268
+ key = FieldPath.parse key unless key.is_a? FieldPath
269
+ [key, value]
270
+ end
271
+
272
+ # Duplicate field paths check
273
+ dup_keys = new_data_pairs.map(&:first).map(&:formatted_string)
274
+ if dup_keys.size != dup_keys.uniq.size
275
+ fail ArgumentError, "duplicate field paths"
276
+ end
277
+ dup_keys.each do |field_path|
278
+ prefix_check = dup_keys.select do |this_path|
279
+ this_path.start_with? "#{field_path}."
280
+ end
281
+ if prefix_check.any?
282
+ fail ArgumentError, "one field cannot be a prefix of another"
283
+ end
284
+ end
285
+
286
+ delete_paths, new_data_pairs = new_data_pairs.partition do |field_path, value|
287
+ value.is_a?(FieldValue) && value.type == :delete
288
+ end
289
+
290
+ root_server_time_paths, new_data_pairs = new_data_pairs.partition do |field_path, value|
291
+ value.is_a?(FieldValue) && value.type == :server_time
292
+ end
293
+
294
+ data = build_hash_from_field_paths_and_values new_data_pairs
295
+ field_paths = new_data_pairs.map(&:first)
296
+
297
+ delete_paths.map!(&:first)
298
+ root_server_time_paths.map!(&:first)
299
+
300
+ data, nested_deletes = remove_field_value_from data, :delete
301
+ fail ArgumentError, "DELETE cannot be nested" if nested_deletes.any?
302
+
303
+ data, nested_server_time_paths = remove_field_value_from data, :server_time
304
+
305
+ server_time_paths = root_server_time_paths + nested_server_time_paths
306
+ server_time_paths = root_server_time_paths + nested_server_time_paths
307
+
308
+ field_paths = (field_paths - (field_paths - identify_all_file_paths(data)) + delete_paths).uniq
309
+ field_paths.each do |field_path|
310
+ fail ArgumentError, "empty paths not allowed" if field_path.fields.empty?
311
+ end
312
+
313
+ if data.empty? && delete_paths.empty? && server_time_paths.empty?
314
+ fail ArgumentError, "data is required"
315
+ end
316
+
317
+ if data.any? || delete_paths.any?
318
+ write = Google::Firestore::V1beta1::Write.new(
319
+ update: Google::Firestore::V1beta1::Document.new(
320
+ name: doc_path,
321
+ fields: hash_to_fields(data)),
322
+ update_mask: Google::Firestore::V1beta1::DocumentMask.new(
323
+ field_paths: field_paths.map(&:formatted_string)),
324
+ current_document: Google::Firestore::V1beta1::Precondition.new(
325
+ exists: true)
326
+ )
327
+ if update_time
328
+ write.current_document = \
329
+ Google::Firestore::V1beta1::Precondition.new(
330
+ update_time: time_to_timestamp(update_time))
331
+ end
332
+ writes << write
333
+ end
334
+
335
+ if server_time_paths.any?
336
+ transform_write = transform_write doc_path, server_time_paths
337
+ if data.empty?
338
+ transform_write.current_document = \
339
+ Google::Firestore::V1beta1::Precondition.new(exists: true)
340
+ end
341
+ writes << transform_write
342
+ end
343
+
344
+ writes
345
+ end
346
+
347
+ def write_for_delete doc_path, exists: nil, update_time: nil
348
+ if !exists.nil? && !update_time.nil?
349
+ fail ArgumentError, "cannot specify both exists and update_time"
350
+ end
351
+
352
+ write = Google::Firestore::V1beta1::Write.new(
353
+ delete: doc_path
354
+ )
355
+
356
+ unless exists.nil? && update_time.nil?
357
+ write.current_document = \
358
+ Google::Firestore::V1beta1::Precondition.new({
359
+ exists: exists, update_time: time_to_timestamp(update_time)
360
+ }.delete_if { |_, v| v.nil? })
361
+ end
362
+
363
+ write
364
+ end
365
+
366
+ def is_field_value_nested obj, field_value_type
367
+ return true if obj.is_a?(FieldValue) && obj.type == field_value_type
368
+
369
+ if obj.is_a? Array
370
+ obj.each { |o| val = is_field_value_nested o, field_value_type; return true if val }
371
+ elsif obj.is_a? Hash
372
+ obj.each { |_k, v| val = is_field_value_nested v, field_value_type; return true if val }
373
+ end
374
+ false
375
+ end
376
+
377
+ def remove_field_value_from obj, field_value_type
378
+ return [nil, []] unless obj.is_a? Hash
379
+
380
+ paths = []
381
+ new_pairs = obj.map do |key, value|
382
+ if value.is_a?(FieldValue) && value.type == field_value_type
383
+ paths << [key]
384
+ nil # will be removed by calling compact
385
+ else
386
+ if value.is_a? Hash
387
+ unless value.empty?
388
+ nested_hash, nested_paths = remove_field_value_from value, field_value_type
389
+ if nested_paths.any?
390
+ nested_paths.each do |nested_path|
391
+ paths << (([key] + nested_path.fields).flatten)
392
+ end
393
+ end
394
+ if nested_hash.empty?
395
+ nil # will be removed by calling compact
396
+ else
397
+ [String(key), nested_hash]
398
+ end
399
+ else
400
+ [String(key), value]
401
+ end
402
+ else
403
+ if value.is_a? Array
404
+ if is_field_value_nested value, field_value_type
405
+ fail ArgumentError, "cannot nest #{field_value_type} under arrays"
406
+ end
407
+ end
408
+
409
+ [String(key), value]
410
+ end
411
+ end
412
+ end
413
+
414
+ paths.map! { |path| FieldPath.new *path }
415
+
416
+ # return a new hash and paths
417
+ [Hash[new_pairs.compact], paths]
418
+ end
419
+
420
+ def identify_leaf_nodes hash
421
+ paths = []
422
+
423
+ hash.map do |key, value|
424
+ if value.is_a? Hash
425
+ nested_paths = identify_leaf_nodes value
426
+ nested_paths.each do |nested_path|
427
+ paths << (([key] + nested_path.fields).flatten)
428
+ end
429
+ else
430
+ paths << [key]
431
+ end
432
+ end
433
+
434
+ paths.map { |path| FieldPath.new *path }
435
+ end
436
+
437
+ def identify_all_file_paths hash
438
+ paths = []
439
+
440
+ hash.map do |key, value|
441
+ paths << [key]
442
+
443
+ if value.is_a? Hash
444
+ nested_paths = identify_all_file_paths value
445
+ nested_paths.each do |nested_path|
446
+ paths << (([key] + nested_path.fields).flatten)
447
+ end
448
+ end
449
+ end
450
+
451
+ paths.map { |path| FieldPath.new *path }
452
+ end
453
+
454
+ def select_by_field_paths hash, field_paths
455
+ new_hash = {}
456
+ field_paths.map do |field_path|
457
+ selected_hash = select_field_path hash, field_path
458
+ deep_merge_hashes new_hash, selected_hash
459
+ end
460
+ new_hash
461
+ end
462
+
463
+ def select_field_path hash, field_path
464
+ ret_hash = {}
465
+ tmp_hash = ret_hash
466
+ prev_hash = ret_hash
467
+ dup_hash = hash.dup
468
+ fields = field_path.fields
469
+ last_field = nil
470
+
471
+ # squash fields until the key exists?
472
+ until dup_hash.key? fields.first
473
+ fields.unshift "#{fields.shift}.#{fields.shift}"
474
+ break if fields.count <= 1
475
+ end
476
+
477
+ fields.each do |field|
478
+ prev_hash[last_field] = tmp_hash unless last_field.nil?
479
+ last_field = field
480
+ tmp_hash[field] = {}
481
+ prev_hash = tmp_hash
482
+ tmp_hash = tmp_hash[field]
483
+ dup_hash = dup_hash[field]
484
+ end
485
+ prev_hash[last_field] = dup_hash
486
+ ret_hash
487
+ end
488
+
489
+ def deep_merge_hashes left_hash, right_hash
490
+ right_hash.each_pair do |key, right_value|
491
+ left_value = left_hash[key]
492
+
493
+ if left_value.is_a?(Hash) && right_value.is_a?(Hash)
494
+ left_hash[key] = deep_merge_hashes left_value, right_value
495
+ else
496
+ left_hash[key] = right_value
497
+ end
498
+ end
499
+
500
+ left_hash
501
+ end
502
+
503
+ START_FIELD_PATH_CHARS = /\A[a-zA-Z_]/
504
+ INVALID_FIELD_PATH_CHARS = /[\~\*\/\[\]]/
505
+ ESCAPED_FIELD_PATH = /\A\`(.*)\`\z/
506
+
507
+ def build_hash_from_field_paths_and_values pairs
508
+ pairs.each do |field_path, _value|
509
+ raise ArgumentError unless field_path.is_a? FieldPath
510
+ end
511
+
512
+ dup_hash = {}
513
+
514
+ pairs.each do |field_path, value|
515
+ tmp_dup = dup_hash
516
+ last_field = nil
517
+ field_path.fields.map(&:to_sym).each do |field|
518
+ fail ArgumentError, "empty paths not allowed" if field.empty?
519
+ tmp_dup = tmp_dup[last_field] unless last_field.nil?
520
+ last_field = field
521
+ tmp_dup[field] ||= {}
522
+ end
523
+ tmp_dup[last_field] = value
524
+ end
525
+
526
+ dup_hash
527
+ end
528
+
529
+ def escape_field_path str
530
+ str = String str
531
+
532
+ return "`#{str}`" if INVALID_FIELD_PATH_CHARS.match str
533
+ return "`#{str}`" if str["."] # contains "."
534
+ return str if START_FIELD_PATH_CHARS.match str
535
+
536
+ "`#{str}`"
537
+ end
538
+
539
+ def transform_write doc_path, paths, server_value: :REQUEST_TIME
540
+ field_transforms = paths.map do |path|
541
+ Google::Firestore::V1beta1::DocumentTransform::FieldTransform.new(
542
+ field_path: path.formatted_string,
543
+ set_to_server_value: server_value
544
+ )
545
+ end
546
+
547
+ Google::Firestore::V1beta1::Write.new(
548
+ transform: Google::Firestore::V1beta1::DocumentTransform.new(
549
+ document: doc_path,
550
+ field_transforms: field_transforms
551
+ )
552
+ )
553
+ end
554
+ end
555
+ # rubocop:enable all
556
+
557
+ extend ClassMethods
558
+ end
559
+ end
560
+ end
561
+ end