fmrest 0.7.1 → 0.8.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94745e25176074d96540a4a7659dce7ee15d6ea88867a58c35c3f38af8cd466f
4
- data.tar.gz: 512669f188c9abe0c403318d50da19ac5376b6cc6bed74880eff9aebb5d642af
3
+ metadata.gz: 5873c0192a2b166cfef71b33c521665dd6953660bd9d23d3ef728b89c55e886f
4
+ data.tar.gz: 7615faa115cc7506d3a472d2fac70ea293bd3dbec1e03bb3c19f0faca51b7c9d
5
5
  SHA512:
6
- metadata.gz: 620ee792b4b585d80358857d5464870c3384211d4311385a034ff6b941c40672d48bfa9f42b48935885131c2fe579d8a51b7e84544dbe08483d467d27966ba0c
7
- data.tar.gz: e422690817ba000c9bbecd828a0c58118e223238ba44b76e86a6160cd87cb854cad0a584efd7932237cb58f125a5e567f292c0849022b897dc3f4c89ab66d96d
6
+ metadata.gz: b627994fa66842be8200692895077649837a7b343c337aaf5762926c633de02baf14ba6428cc33b8b3cc0d0314d7d62a4f6cf16173edb09d32ba8218c533d7d6
7
+ data.tar.gz: 2cd9e7f7003f9ec05120587f50ee892076d8e02045a6b18e9311276792bf831dee6c4434969990b17f4f5898f95040da0614aabbdb5940fa8906f3c507f7f0e8
@@ -1,5 +1,12 @@
1
1
  ## Changelog
2
2
 
3
+ ### 0.8.0
4
+
5
+ * Improved metadata when using `FmRest::Spyke::Model`. Metadata now uses
6
+ Struct/OpenStruct, so properties are accessible through `.property`, as well
7
+ as `[:property]`
8
+ * Implemented `.find_in_batches` and `.find_each` for `FmRest::Spyke::Model`
9
+
3
10
  ### 0.7.1
4
11
 
5
12
  * Made sure `Model.find_one` and `Model.find_some` work without needing to call
data/README.md CHANGED
@@ -195,7 +195,7 @@ FmRest.token_store = FmRest::TokenStore::Redis.new(redis: Redis.new, prefix: "my
195
195
  FmRest.token_store = FmRest::TokenStore::Redis.new(prefix: "my-fancy-prefix:", host: "10.0.1.1", port: 6380, db: 15)
196
196
  ```
197
197
 
198
- **NOTE:** redis-rb is not included as a gem dependency of fmrest-ruby, so you'll
198
+ NOTE: redis-rb is not included as a gem dependency of fmrest-ruby, so you'll
199
199
  have to add it to your Gemfile.
200
200
 
201
201
  ### Moneta
@@ -232,7 +232,7 @@ FmRest.token_store = FmRest::TokenStore::Moneta.new(
232
232
  )
233
233
  ```
234
234
 
235
- **NOTE:** the moneta gem is not included as a dependency of fmrest-ruby, so
235
+ NOTE: the moneta gem is not included as a dependency of fmrest-ruby, so
236
236
  you'll have to add it to your Gemfile.
237
237
 
238
238
 
@@ -555,7 +555,7 @@ Honeybee.limit(10)
555
555
  ```
556
556
 
557
557
  NOTE: You can also set a default limit value for a model class, see
558
- [Other notes on querying](#other-notes-on-querying).
558
+ [other notes on querying](#other-notes-on-querying).
559
559
 
560
560
  You can also use `.limit` to set limits on portals:
561
561
 
@@ -727,15 +727,15 @@ the scope object:
727
727
  Honeybee.limit(10).sort(:name).find_some # => [<Honeybee...>, ...]
728
728
  ```
729
729
 
730
- If you want just a single result you can use `.find_one` instead (this will
730
+ If you want just a single result you can use `.first` instead (this will
731
731
  force `.limit(1)`):
732
732
 
733
733
  ```ruby
734
- Honeybee.query(name: "Hutch").find_one # => <Honeybee...>
734
+ Honeybee.query(name: "Hutch").first # => <Honeybee...>
735
735
  ```
736
736
 
737
737
  If you know the id of the record you should use `.find(id)` instead of
738
- `.query(id: id).find_one` (so that the sent request is
738
+ `.query(id: id).first` (so that the sent request is
739
739
  `GET ../:layout/records/:id` instead of `POST ../:layout/_find`).
740
740
 
741
741
  ```ruby
@@ -746,6 +746,52 @@ Note also that if you use `.find(id)` your `.query()` parameters (as well as
746
746
  limit, offset and sort parameters) will be discarded as they're not supported
747
747
  by the single record endpoint.
748
748
 
749
+
750
+ ### Finding records in batches
751
+
752
+ Sometimes you want to iterate over a very large number of records to do some
753
+ processing, but requesting them all at once would result in one huge request to
754
+ the Data API, and loading too many records in memory all at once.
755
+
756
+ To mitigate this problem you can use `.find_in_batches` and `.find_each`. If
757
+ you've used ActiveRecord you're probably familiar with how they operate:
758
+
759
+ ```ruby
760
+ # Find records in batches of 100 each
761
+ Honeybee.query(hive: "Queensville").find_in_batches(batch_size: 100) do |batch|
762
+ dispatch_bees(batch)
763
+ end
764
+
765
+ # Iterate over all records using batches
766
+ Honeybee.query(hive: "Queensville").find_each(batch_size: 100) do |bee|
767
+ bee.dispatch
768
+ end
769
+ ```
770
+
771
+ `.find_in_batches` yields collections of records (batches), while `.find_each`
772
+ yields individual records, but using batches behind the scenes.
773
+
774
+ Both methods accept a block-less form in which case they return an
775
+ `Enumerator`:
776
+
777
+ ```ruby
778
+ batch_enum = Honeybee.find_in_batches
779
+
780
+ batch = batch_enum.next # => Spyke::Collection
781
+
782
+ batch_enum.each do |batch|
783
+ process_batch(batch)
784
+ end
785
+
786
+ record_enum = Honeybee.find_each
787
+
788
+ record_enum.next # => Honeybee
789
+ ```
790
+
791
+ NOTE: By its nature, batch processing is subject to race conditions if other
792
+ processes are modifying the database.
793
+
794
+
749
795
  ### Container fields
750
796
 
751
797
  You can define container fields on your model class with `container`:
@@ -783,6 +829,7 @@ bee.photo.upload(filename_or_io) # Upload a file to the container
783
829
  * `:content_type` - The MIME content type to use (defaults to
784
830
  `application/octet-stream`)
785
831
 
832
+
786
833
  ### Script execution
787
834
 
788
835
  The Data API allows running scripts as part of many types of requests.
@@ -870,7 +917,7 @@ separately, under their matching key.
870
917
  ```ruby
871
918
  bee.save(script: { presort: "My Presort Script", after: "My Script" })
872
919
 
873
- Honeybee.last_request_metadata[:script]
920
+ Honeybee.last_request_metadata.script
874
921
  # => { after: { result: "oh hi", error: "0" }, presort: { result: "lo", error: "0" } }
875
922
  ```
876
923
 
@@ -884,7 +931,7 @@ is performed on that scope.
884
931
 
885
932
  ```ruby
886
933
  # Find one Honeybee record executing a presort and after script
887
- Honeybee.script(presort: ["My Presort Script", "parameter"], after: "My Script").find_one
934
+ Honeybee.script(presort: ["My Presort Script", "parameter"], after: "My Script").first
888
935
  ```
889
936
 
890
937
  The model class' `.last_request_metadata` will be set in case you need to get the result.
@@ -24,7 +24,8 @@ module FmRest
24
24
  # Methods delegated to FmRest::Spyke::Relation
25
25
  delegate :limit, :offset, :sort, :order, :query, :omit, :portal,
26
26
  :portals, :includes, :with_all_portals, :without_portals,
27
- :script, :find_one, :find_some, to: :all
27
+ :script, :find_one, :first, :any, :find_some,
28
+ :find_in_batches, :find_each, to: :all
28
29
 
29
30
  def all
30
31
  # Use FmRest's Relation instead of Spyke's vanilla one
@@ -190,6 +190,79 @@ module FmRest
190
190
  rescue ::Spyke::ConnectionError => error
191
191
  fallback_or_reraise(error, default: nil)
192
192
  end
193
+ alias_method :first, :find_one
194
+ alias_method :any, :find_one
195
+
196
+ # Yields each batch of records that was found by the find options.
197
+ #
198
+ # NOTE: By its nature, batch processing is subject to race conditions if
199
+ # other processes are modifying the database
200
+ #
201
+ # @param batch_size [Integer] Specifies the size of the batch.
202
+ # @return [Enumerator] if called without a block.
203
+ def find_in_batches(batch_size: 1000)
204
+ unless block_given?
205
+ return to_enum(:find_in_batches, batch_size: batch_size) do
206
+ total = limit(1).find_some.metadata.data_info.found_count
207
+ (total - 1).div(batch_size) + 1
208
+ end
209
+ end
210
+
211
+ offset = 1 # DAPI offset is 1-based
212
+
213
+ loop do
214
+ relation = offset(offset).limit(batch_size)
215
+
216
+ records = relation.find_some
217
+
218
+ yield records if records.length > 0
219
+
220
+ break if records.length < batch_size
221
+
222
+ # Save one iteration if the total is a multiple of batch_size
223
+ if found_count = records.metadata.data_info && records.metadata.data_info.found_count
224
+ break if found_count == (offset - 1) + batch_size
225
+ end
226
+
227
+ offset += batch_size
228
+ end
229
+ end
230
+
231
+ # Looping through a collection of records from the database (using the
232
+ # #all method, for example) is very inefficient since it will fetch and
233
+ # instantiate all the objects at once.
234
+ #
235
+ # In that case, batch processing methods allow you to work with the
236
+ # records in batches, thereby greatly reducing memory consumption and be
237
+ # lighter on the Data API server.
238
+ #
239
+ # The find_each method uses #find_in_batches with a batch size of 1000
240
+ # (or as specified by the :batch_size option).
241
+ #
242
+ # NOTE: By its nature, batch processing is subject to race conditions if
243
+ # other processes are modifying the database
244
+ #
245
+ # @param (see #find_in_batches)
246
+ # @example
247
+ # Person.find_each do |person|
248
+ # person.greet
249
+ # end
250
+ #
251
+ # Person.query(name: "==Mitch").find_each do |person|
252
+ # person.say_hi
253
+ # end
254
+ # @return (see #find_in_batches)
255
+ def find_each(batch_size: 1000)
256
+ unless block_given?
257
+ return to_enum(:find_each, batch_size: batch_size) do
258
+ limit(1).find_some.metadata.data_info.found_count
259
+ end
260
+ end
261
+
262
+ find_in_batches(batch_size: batch_size) do |records|
263
+ records.each { |r| yield r }
264
+ end
265
+ end
193
266
 
194
267
  protected
195
268
 
@@ -1,9 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "ostruct"
4
5
 
5
6
  module FmRest
6
7
  module Spyke
8
+ # Metadata class to be passed to Spyke::Collection#metadata
9
+ class Metadata < Struct.new(:messages, :script, :data_info)
10
+ alias_method :scripts, :script
11
+ end
12
+
13
+ class DataInfo < OpenStruct
14
+ def total_record_count; totalRecordCount; end
15
+ def found_count; foundCount; end
16
+ def returned_count; returnedCount; end
17
+ end
18
+
7
19
  # Response Faraday middleware for converting FM API's response JSON into
8
20
  # Spyke's expected format
9
21
  class SpykeFormatter < ::Faraday::Response::Middleware
@@ -77,36 +89,61 @@ module FmRest
77
89
 
78
90
  # @param json [Hash]
79
91
  # @param include_errors [Boolean]
80
- # @return [Hash] the skeleton structure for a Spyke-formatted response
92
+ # @return [FmRest::Spyke::Metadata] the skeleton structure for a
93
+ # Spyke-formatted response
81
94
  def build_base_hash(json, include_errors = false)
82
95
  {
83
- metadata: { messages: json[:messages] }.merge(script: prepare_script_results(json).presence),
84
- errors: include_errors ? prepare_errors(json) : {}
96
+ metadata: Metadata.new(
97
+ prepare_messages(json),
98
+ prepare_script_results(json),
99
+ prepare_data_info(json)
100
+ ).freeze,
101
+ errors: include_errors ? prepare_errors(json) : {}
85
102
  }
86
103
  end
87
104
 
88
105
  # @param json [Hash]
89
- # @return [Hash] the script(s) execution results for Spyke metadata format
106
+ # @return [Array<OpenStruct>] the skeleton structure for a
107
+ # Spyke-formatted response
108
+ def prepare_messages(json)
109
+ return [] unless json[:messages]
110
+ json[:messages].map { |m| OpenStruct.new(m).freeze }.freeze
111
+ end
112
+
113
+ # @param json [Hash]
114
+ # @return [OpenStruct] the script(s) execution results for Spyke metadata
115
+ # format
90
116
  def prepare_script_results(json)
91
117
  results = {}
92
118
 
93
119
  [:prerequest, :presort].each do |s|
94
120
  if json[:response][:"scriptError.#{s}"]
95
- results[s] = {
121
+ results[s] = OpenStruct.new(
96
122
  result: json[:response][:"scriptResult.#{s}"],
97
123
  error: json[:response][:"scriptError.#{s}"]
98
- }
124
+ ).freeze
99
125
  end
100
126
  end
101
127
 
102
128
  if json[:response][:scriptError]
103
- results[:after] = {
129
+ results[:after] = OpenStruct.new(
104
130
  result: json[:response][:scriptResult],
105
131
  error: json[:response][:scriptError]
106
- }
132
+ ).freeze
107
133
  end
108
134
 
109
- results
135
+ results.present? ? OpenStruct.new(results).freeze : nil
136
+ end
137
+
138
+ # @param json [Hash]
139
+ # @return [OpenStruct] the script(s) execution results for
140
+ # Spyke metadata format
141
+ def prepare_data_info(json)
142
+ data_info = json[:response] && json[:response][:dataInfo]
143
+
144
+ return nil unless data_info.present?
145
+
146
+ DataInfo.new(data_info).freeze
110
147
  end
111
148
 
112
149
  # @param json [Hash]
@@ -31,10 +31,6 @@ module FmRest
31
31
  conn.request :multipart
32
32
  conn.request :json
33
33
 
34
- if options[:log]
35
- conn.response :logger, nil, bodies: true, headers: true
36
- end
37
-
38
34
  # Allow overriding the default response middleware
39
35
  if block_given?
40
36
  yield conn, options
@@ -43,6 +39,10 @@ module FmRest
43
39
  conn.response :json
44
40
  end
45
41
 
42
+ if options[:log]
43
+ conn.response :logger, nil, bodies: true, headers: true
44
+ end
45
+
46
46
  conn.adapter Faraday.default_adapter
47
47
  end
48
48
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FmRest
4
- VERSION = "0.7.1"
4
+ VERSION = "0.8.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fmrest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Carbajal
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-13 00:00:00.000000000 Z
11
+ date: 2020-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday