fmrest 0.11.0 → 0.14.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +32 -0
  4. data/README.md +228 -844
  5. metadata +71 -101
  6. data/.github/workflows/ci.yml +0 -33
  7. data/.gitignore +0 -26
  8. data/.rspec +0 -3
  9. data/.travis.yml +0 -5
  10. data/Gemfile +0 -3
  11. data/Rakefile +0 -6
  12. data/fmrest.gemspec +0 -38
  13. data/lib/fmrest.rb +0 -34
  14. data/lib/fmrest/connection_settings.rb +0 -124
  15. data/lib/fmrest/errors.rb +0 -30
  16. data/lib/fmrest/spyke.rb +0 -21
  17. data/lib/fmrest/spyke/base.rb +0 -23
  18. data/lib/fmrest/spyke/container_field.rb +0 -59
  19. data/lib/fmrest/spyke/model.rb +0 -36
  20. data/lib/fmrest/spyke/model/associations.rb +0 -82
  21. data/lib/fmrest/spyke/model/attributes.rb +0 -171
  22. data/lib/fmrest/spyke/model/auth.rb +0 -43
  23. data/lib/fmrest/spyke/model/connection.rb +0 -135
  24. data/lib/fmrest/spyke/model/container_fields.rb +0 -25
  25. data/lib/fmrest/spyke/model/global_fields.rb +0 -40
  26. data/lib/fmrest/spyke/model/http.rb +0 -37
  27. data/lib/fmrest/spyke/model/orm.rb +0 -212
  28. data/lib/fmrest/spyke/model/serialization.rb +0 -91
  29. data/lib/fmrest/spyke/model/uri.rb +0 -30
  30. data/lib/fmrest/spyke/portal.rb +0 -55
  31. data/lib/fmrest/spyke/relation.rb +0 -359
  32. data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
  33. data/lib/fmrest/spyke/validation_error.rb +0 -25
  34. data/lib/fmrest/string_date.rb +0 -220
  35. data/lib/fmrest/token_store.rb +0 -12
  36. data/lib/fmrest/token_store/active_record.rb +0 -74
  37. data/lib/fmrest/token_store/base.rb +0 -25
  38. data/lib/fmrest/token_store/memory.rb +0 -26
  39. data/lib/fmrest/token_store/moneta.rb +0 -41
  40. data/lib/fmrest/token_store/redis.rb +0 -45
  41. data/lib/fmrest/v1.rb +0 -23
  42. data/lib/fmrest/v1/auth.rb +0 -30
  43. data/lib/fmrest/v1/connection.rb +0 -115
  44. data/lib/fmrest/v1/container_fields.rb +0 -114
  45. data/lib/fmrest/v1/dates.rb +0 -81
  46. data/lib/fmrest/v1/paths.rb +0 -47
  47. data/lib/fmrest/v1/raise_errors.rb +0 -59
  48. data/lib/fmrest/v1/token_session.rb +0 -134
  49. data/lib/fmrest/v1/token_store/active_record.rb +0 -13
  50. data/lib/fmrest/v1/token_store/memory.rb +0 -13
  51. data/lib/fmrest/v1/type_coercer.rb +0 -192
  52. data/lib/fmrest/v1/utils.rb +0 -94
  53. data/lib/fmrest/version.rb +0 -5
@@ -1,91 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module Spyke
5
- module Model
6
- module Serialization
7
- FM_DATE_FORMAT = "%m/%d/%Y"
8
- FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
9
-
10
- # Override Spyke's to_params to return FM Data API's expected JSON
11
- # format, and including only modified fields
12
- #
13
- def to_params
14
- params = {
15
- fieldData: serialize_values!(changed_params_not_embedded_in_url)
16
- }
17
-
18
- params[:modId] = mod_id if mod_id
19
-
20
- portal_data = serialize_portals
21
- params[:portalData] = portal_data unless portal_data.empty?
22
-
23
- params
24
- end
25
-
26
- protected
27
-
28
- def serialize_for_portal(portal)
29
- params =
30
- changed_params.except(:id).transform_keys do |key|
31
- "#{portal.attribute_prefix}::#{key}"
32
- end
33
-
34
- params[:recordId] = id if id
35
- params[:modId] = mod_id if mod_id
36
-
37
- serialize_values!(params)
38
- end
39
-
40
- private
41
-
42
- def serialize_portals
43
- portal_data = {}
44
-
45
- portals.each do |portal|
46
- portal.each do |portal_record|
47
- next unless portal_record.changed?
48
- portal_params = portal_data[portal.portal_key] ||= []
49
- portal_params << portal_record.serialize_for_portal(portal)
50
- end
51
- end
52
-
53
- portal_data
54
- end
55
-
56
- def changed_params_not_embedded_in_url
57
- params_not_embedded_in_url.slice(*mapped_changed)
58
- end
59
-
60
- # Modifies the given hash in-place encoding non-string values (e.g.
61
- # dates) to their string representation when appropriate.
62
- #
63
- def serialize_values!(params)
64
- params.transform_values! do |value|
65
- case value
66
- when DateTime, Time, FmRest::StringDateTime
67
- convert_datetime_timezone(value.to_datetime).strftime(FM_DATETIME_FORMAT)
68
- when Date, FmRest::StringDate
69
- value.strftime(FM_DATE_FORMAT)
70
- else
71
- value
72
- end
73
- end
74
-
75
- params
76
- end
77
-
78
- def convert_datetime_timezone(dt)
79
- case fmrest_config.timezone
80
- when :utc, "utc"
81
- dt.new_offset(0)
82
- when :local, "local"
83
- dt.new_offset(FmRest::V1.local_offset_for_datetime(dt))
84
- when nil
85
- dt
86
- end
87
- end
88
- end
89
- end
90
- end
91
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module Spyke
5
- module Model
6
- module Uri
7
- extend ::ActiveSupport::Concern
8
-
9
- class_methods do
10
- # Accessor for FM layout (helps with building the URI)
11
- #
12
- def layout(layout = nil)
13
- @layout = layout if layout
14
- @layout ||= model_name.name
15
- end
16
-
17
- # Extend uri acccessor to default to FM Data schema
18
- #
19
- def uri(uri_template = nil)
20
- if @uri.nil? && uri_template.nil?
21
- return FmRest::V1.record_path(layout) + "(/:id)"
22
- end
23
-
24
- super
25
- end
26
- end
27
- end
28
- end
29
- end
30
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module Spyke
5
- # Extend Spyke's HasMany association with custom options
6
- #
7
- class Portal < ::Spyke::Associations::HasMany
8
- def initialize(*args)
9
- super
10
-
11
- # Portals are always embedded, so no special URI
12
- @options[:uri] = ""
13
- end
14
-
15
- def portal_key
16
- return @options[:portal_key] if @options[:portal_key]
17
- name
18
- end
19
-
20
- def attribute_prefix
21
- @options[:attribute_prefix] || portal_key
22
- end
23
-
24
- def parent_changes_applied
25
- each do |record|
26
- record.changes_applied
27
- # Saving portal data doesn't provide new modIds for the
28
- # portal records, so we clear them instead. We can still save
29
- # portal data without a mod_id (it's optional in FM Data API)
30
- record.mod_id = nil
31
- end
32
- end
33
-
34
- private
35
-
36
- # Spyke::Associations::HasMany#initialize calls primary_key to build the
37
- # default URI, which causes a NameError, so this is here just to prevent
38
- # that. We don't care what it returns as we override the URI with nil
39
- # anyway
40
- def primary_key; end
41
-
42
- # Make sure the association doesn't try to fetch records through URI
43
- def uri; nil; end
44
-
45
- def embedded_data
46
- parent.attributes[portal_key]
47
- end
48
-
49
- def add_to_parent(record)
50
- find_some << record
51
- record
52
- end
53
- end
54
- end
55
- end
@@ -1,359 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module Spyke
5
- class Relation < ::Spyke::Relation
6
- SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
7
-
8
- # NOTE: We need to keep limit, offset, sort, query and portal accessors
9
- # separate from regular params because FM Data API uses either "limit" or
10
- # "_limit" (or "_offset", etc.) as param keys depending on the type of
11
- # request, so we can't set the params until the last moment
12
-
13
-
14
- attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
15
- :included_portals, :portal_params, :script_params
16
-
17
- def initialize(*_args)
18
- super
19
-
20
- @limit_value = klass.default_limit
21
-
22
- if klass.default_sort.present?
23
- @sort_params = Array.wrap(klass.default_sort).map { |s| normalize_sort_param(s) }
24
- end
25
-
26
- @query_params = []
27
-
28
- @included_portals = nil
29
- @portal_params = {}
30
- @script_params = {}
31
- end
32
-
33
- # @param options [String, Array, Hash, nil, false] sets script params to
34
- # execute in the next get or find request
35
- #
36
- # @example
37
- # # Find records and run the script named "My script"
38
- # Person.script("My script").find_some
39
- #
40
- # # Find records and run the script named "My script" with param "the param"
41
- # Person.script(["My script", "the param"]).find_some
42
- #
43
- # # Find records and run a prerequest, presort and after (normal) script
44
- # Person.script(after: "Script", prerequest: "Prereq script", presort: "Presort script").find_some
45
- #
46
- # # Same as above, but passing parameters too
47
- # Person.script(
48
- # after: ["After script", "the param"],
49
- # prerequest: ["Prereq script", "the param"],
50
- # presort: o ["Presort script", "the param"]
51
- # ).find_some
52
- #
53
- # Person.script(nil).find_some # Disable script execution
54
- # Person.script(false).find_some # Disable script execution
55
- #
56
- # @return [FmRest::Spyke::Relation] a new relation with the script
57
- # options applied
58
- def script(options)
59
- with_clone do |r|
60
- if options.eql?(false) || options.eql?(nil)
61
- r.script_params = {}
62
- else
63
- r.script_params = script_params.merge(FmRest::V1.convert_script_params(options))
64
- end
65
- end
66
- end
67
-
68
- # @param value_or_hash [Integer, Hash] the limit value for this layout,
69
- # or a hash with limits for the layout's portals
70
- # @example
71
- # Person.limit(10) # Set layout limit
72
- # Person.limit(children: 10) # Set portal limit
73
- # @return [FmRest::Spyke::Relation] a new relation with the limits
74
- # applied
75
- def limit(value_or_hash)
76
- with_clone do |r|
77
- if value_or_hash.respond_to?(:each)
78
- r.set_portal_params(value_or_hash, :limit)
79
- else
80
- r.limit_value = value_or_hash
81
- end
82
- end
83
- end
84
-
85
- # @param value_or_hash [Integer, Hash] the offset value for this layout,
86
- # or a hash with offsets for the layout's portals
87
- # @example
88
- # Person.offset(10) # Set layout offset
89
- # Person.offset(children: 10) # Set portal offset
90
- # @return [FmRest::Spyke::Relation] a new relation with the offsets
91
- # applied
92
- def offset(value_or_hash)
93
- with_clone do |r|
94
- if value_or_hash.respond_to?(:each)
95
- r.set_portal_params(value_or_hash, :offset)
96
- else
97
- r.offset_value = value_or_hash
98
- end
99
- end
100
- end
101
-
102
- # Allows sort params given in either hash format (using FM Data API's
103
- # format), or as a symbol, in which case the of the attribute must match
104
- # a known mapped attribute, optionally suffixed with `!` or `__desc` to
105
- # signify it should use descending order.
106
- #
107
- # @param args [Array<Symbol, Hash>] the names of attributes to sort by with
108
- # optional `!` or `__desc` suffix, or a hash of options as expected by
109
- # the FM Data API
110
- # @example
111
- # Person.sort(:first_name, :age!)
112
- # Person.sort(:first_name, :age__desc)
113
- # Person.sort(:first_name, :age__descend)
114
- # Person.sort({ fieldName: "FirstName" }, { fieldName: "Age", sortOrder: "descend" })
115
- # @return [FmRest::Spyke::Relation] a new relation with the sort options
116
- # applied
117
- def sort(*args)
118
- with_clone do |r|
119
- r.sort_params = args.flatten.map { |s| normalize_sort_param(s) }
120
- end
121
- end
122
- alias order sort
123
-
124
- # Sets the portals to include with each record in the response.
125
- #
126
- # @param args [Array<Symbol, String>, true, false] the names of portals to
127
- # include, or `false` to request no portals
128
- # @example
129
- # Person.portal(:relatives, :pets)
130
- # Person.portal(false) # Disables portals
131
- # Person.portal(true) # Enables portals (includes all)
132
- # @return [FmRest::Spyke::Relation] a new relation with the portal
133
- # options applied
134
- def portal(*args)
135
- raise ArgumentError, "Call `portal' with at least one argument" if args.empty?
136
-
137
- with_clone do |r|
138
- if args.length == 1 && args.first.eql?(true) || args.first.eql?(false)
139
- r.included_portals = args.first ? nil : []
140
- else
141
- r.included_portals ||= []
142
- r.included_portals += args.flatten.map { |p| normalize_portal_param(p) }
143
- r.included_portals.uniq!
144
- end
145
- end
146
- end
147
- alias includes portal
148
- alias portals portal
149
-
150
- # Same as calling `portal(true)`
151
- #
152
- # @return (see #portal)
153
- def with_all_portals
154
- portal(true)
155
- end
156
-
157
- # Same as calling `portal(false)`
158
- #
159
- # @return (see #portal)
160
- def without_portals
161
- portal(false)
162
- end
163
-
164
- def query(*params)
165
- with_clone do |r|
166
- r.query_params += params.flatten.map { |p| normalize_query_params(p) }
167
- end
168
- end
169
-
170
- def omit(params)
171
- query(params.merge(omit: true))
172
- end
173
-
174
- # @return [Boolean] whether a query was set on this relation
175
- def has_query?
176
- query_params.present?
177
- end
178
-
179
- # Finds a single instance of the model by forcing limit = 1, or simply
180
- # fetching the record by id if the primary key was set
181
- #
182
- # @return [FmRest::Spyke::Base]
183
- def find_one
184
- @find_one ||=
185
- if primary_key_set?
186
- without_collection_params { super }
187
- else
188
- klass.new_collection_from_result(limit(1).fetch).first
189
- end
190
- rescue ::Spyke::ConnectionError => error
191
- fallback_or_reraise(error, default: nil)
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
266
-
267
- protected
268
-
269
- def set_portal_params(params_hash, param)
270
- # Copy portal_params so we're not modifying the same hash as the parent
271
- # scope
272
- self.portal_params = portal_params.dup
273
-
274
- params_hash.each do |portal_name, value|
275
- # TODO: Use a hash like { portal_name: { param: value } } instead so
276
- # we can intelligently avoid including portal params for excluded
277
- # portals
278
- key = "#{param}.#{normalize_portal_param(portal_name)}"
279
-
280
- # Delete key if value is falsy
281
- if !value && portal_params.has_key?(key)
282
- portal_params.delete(key)
283
- else
284
- self.portal_params[key] = value
285
- end
286
- end
287
- end
288
-
289
- private
290
-
291
- def normalize_sort_param(param)
292
- if param.kind_of?(Symbol) || param.kind_of?(String)
293
- _, attr, descend = param.to_s.match(SORT_PARAM_MATCHER).to_a
294
-
295
- unless field_name = klass.mapped_attributes[attr]
296
- raise ArgumentError, "Unknown attribute `#{attr}' given to sort as #{param.inspect}. If you want to use a custom sort pass a hash in the Data API format"
297
- end
298
-
299
- hash = { fieldName: field_name }
300
- hash[:sortOrder] = "descend" if descend
301
- return hash
302
- end
303
-
304
- # TODO: Sanitize sort hash param for FM Data API conformity?
305
- param
306
- end
307
-
308
- def normalize_portal_param(param)
309
- if param.kind_of?(Symbol)
310
- portal_key, = klass.portal_options.find { |_, opts| opts[:name].to_s == param.to_s }
311
-
312
- unless portal_key
313
- raise ArgumentError, "Unknown portal #{param.inspect}. If you want to include a portal not defined in the model pass it as a string instead"
314
- end
315
-
316
- return portal_key
317
- end
318
-
319
- param
320
- end
321
-
322
- def normalize_query_params(params)
323
- params.each_with_object({}) do |(k, v), normalized|
324
- if k == :omit || k == "omit"
325
- # FM Data API wants omit values as strings, e.g. "true" or "false"
326
- # rather than true/false
327
- normalized["omit"] = v.to_s
328
- next
329
- end
330
-
331
- # TODO: Raise ArgumentError if an attribute given as symbol isn't defiend
332
- if k.kind_of?(Symbol) && klass.mapped_attributes.has_key?(k)
333
- normalized[klass.mapped_attributes[k].to_s] = v
334
- else
335
- normalized[k.to_s] = v
336
- end
337
- end
338
- end
339
-
340
- def primary_key_set?
341
- params[klass.primary_key].present?
342
- end
343
-
344
- def without_collection_params
345
- orig_values = limit_value, offset_value, sort_params, query_params
346
- self.limit_value = self.offset_value = self.sort_params = self.query_params = nil
347
- yield
348
- ensure
349
- self.limit_value, self.offset_value, self.sort_params, self.query_params = orig_values
350
- end
351
-
352
- def with_clone
353
- clone.tap do |relation|
354
- yield relation
355
- end
356
- end
357
- end
358
- end
359
- end