fmrest 0.7.1 → 0.8.0

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