google-cloud-firestore 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
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