fmrest 0.12.0 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -0
  3. data/CHANGELOG.md +31 -0
  4. data/README.md +252 -851
  5. metadata +71 -108
  6. data/.gitignore +0 -26
  7. data/.rspec +0 -3
  8. data/.travis.yml +0 -5
  9. data/Gemfile +0 -3
  10. data/Rakefile +0 -6
  11. data/UPGRADING +0 -15
  12. data/fmrest.gemspec +0 -49
  13. data/lib/fmrest.rb +0 -36
  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 -9
  18. data/lib/fmrest/spyke/container_field.rb +0 -59
  19. data/lib/fmrest/spyke/model.rb +0 -33
  20. data/lib/fmrest/spyke/model/associations.rb +0 -86
  21. data/lib/fmrest/spyke/model/attributes.rb +0 -163
  22. data/lib/fmrest/spyke/model/auth.rb +0 -43
  23. data/lib/fmrest/spyke/model/connection.rb +0 -163
  24. data/lib/fmrest/spyke/model/container_fields.rb +0 -40
  25. data/lib/fmrest/spyke/model/global_fields.rb +0 -40
  26. data/lib/fmrest/spyke/model/http.rb +0 -77
  27. data/lib/fmrest/spyke/model/orm.rb +0 -256
  28. data/lib/fmrest/spyke/model/record_id.rb +0 -78
  29. data/lib/fmrest/spyke/model/serialization.rb +0 -99
  30. data/lib/fmrest/spyke/model/uri.rb +0 -29
  31. data/lib/fmrest/spyke/portal.rb +0 -55
  32. data/lib/fmrest/spyke/relation.rb +0 -359
  33. data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
  34. data/lib/fmrest/spyke/validation_error.rb +0 -25
  35. data/lib/fmrest/string_date.rb +0 -220
  36. data/lib/fmrest/token_store.rb +0 -12
  37. data/lib/fmrest/token_store/active_record.rb +0 -74
  38. data/lib/fmrest/token_store/base.rb +0 -25
  39. data/lib/fmrest/token_store/memory.rb +0 -26
  40. data/lib/fmrest/token_store/moneta.rb +0 -41
  41. data/lib/fmrest/token_store/null.rb +0 -20
  42. data/lib/fmrest/token_store/redis.rb +0 -45
  43. data/lib/fmrest/v1.rb +0 -23
  44. data/lib/fmrest/v1/auth.rb +0 -30
  45. data/lib/fmrest/v1/connection.rb +0 -115
  46. data/lib/fmrest/v1/container_fields.rb +0 -114
  47. data/lib/fmrest/v1/dates.rb +0 -81
  48. data/lib/fmrest/v1/paths.rb +0 -47
  49. data/lib/fmrest/v1/raise_errors.rb +0 -57
  50. data/lib/fmrest/v1/token_session.rb +0 -133
  51. data/lib/fmrest/v1/token_store/active_record.rb +0 -13
  52. data/lib/fmrest/v1/token_store/memory.rb +0 -13
  53. data/lib/fmrest/v1/type_coercer.rb +0 -192
  54. data/lib/fmrest/v1/utils.rb +0 -94
  55. data/lib/fmrest/version.rb +0 -5
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module Spyke
5
- module Model
6
- # Modifies Spyke models to use `__record_id` instead of `id` as the
7
- # "primary key" method, so that we can map a model class to a FM layout
8
- # with a field named `id` without clobbering it.
9
- #
10
- # The `id` reader method still maps to the record ID for backwards
11
- # compatibility and because Spyke hardcodes its use at various points
12
- # through its codebase, but it can be safely overwritten (e.g. to map to
13
- # a FM field).
14
- #
15
- # The recommended way to deal with a layout that maps an `id` attribute
16
- # is to remap it in the model to something else, e.g. `unique_id`.
17
- #
18
- module RecordID
19
- extend ::ActiveSupport::Concern
20
-
21
- included do
22
- # @return [Integer] the record's recordId
23
- attr_accessor :__record_id
24
- alias_method :record_id, :__record_id
25
- alias_method :id, :__record_id
26
-
27
- # @return [Integer] the record's modId
28
- attr_accessor :__mod_id
29
- alias_method :mod_id, :__mod_id
30
-
31
- # Get rid of Spyke's id= setter method, as we'll be using __record_id=
32
- # instead
33
- undef_method :id=
34
-
35
- # Tell Spyke that we want __record_id as the PK
36
- self.primary_key = :__record_id
37
- end
38
-
39
- def __record_id?
40
- __record_id.present?
41
- end
42
- alias_method :record_id?, :__record_id?
43
- alias_method :persisted?, :__record_id?
44
-
45
- # Spyke override -- Use `__record_id` instead of `id`
46
- #
47
- def hash
48
- __record_id.hash
49
- end
50
-
51
- # Spyke override -- Renders class string with layout name and
52
- # `record_id`.
53
- #
54
- # @return [String] A string representation of the class
55
- #
56
- def inspect
57
- "#<#{self.class}(layout: #{self.class.layout}) record_id: #{__record_id.inspect} #{inspect_attributes}>"
58
- end
59
-
60
- # Spyke override -- Use `__record_id` instead of `id`
61
- #
62
- # @param id [Integer] The id of the record to destroy
63
- #
64
- def destroy(id = nil)
65
- new(__record_id: id).destroy
66
- end
67
-
68
- private
69
-
70
- # Spyke override (private)
71
- #
72
- def conflicting_ids?(attributes)
73
- false
74
- end
75
- end
76
- end
77
- end
78
- end
@@ -1,99 +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
- # Spyke override -- Return FM Data API's expected JSON format,
11
- # 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_classes
67
- convert_datetime_timezone(value.to_datetime).strftime(FM_DATETIME_FORMAT)
68
- when *date_classes
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
-
89
- def datetime_classes
90
- [DateTime, Time, defined?(FmRest::StringDateTime) && FmRest::StringDateTime].compact
91
- end
92
-
93
- def date_classes
94
- [Date, defined?(FmRest::StringDate) && FmRest::StringDate].compact
95
- end
96
- end
97
- end
98
- end
99
- end
@@ -1,29 +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
- # Spyke override -- Extends `uri` to default to FM Data's URI schema
18
- #
19
- def uri(uri_template = nil)
20
- if @uri.nil? && uri_template.nil?
21
- return FmRest::V1.record_path(layout) + "(/:#{primary_key})"
22
- end
23
- super
24
- end
25
- end
26
- end
27
- end
28
- end
29
- 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