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,273 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "ostruct"
5
-
6
- module FmRest
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
-
19
- # Response Faraday middleware for converting FM API's response JSON into
20
- # Spyke's expected format
21
- class SpykeFormatter < ::Faraday::Response::Middleware
22
- SINGLE_RECORD_RE = %r(/records/\d+\z).freeze
23
- MULTIPLE_RECORDS_RE = %r(/records\z).freeze
24
- CONTAINER_RE = %r(/records/\d+/containers/[^/]+/\d+\z).freeze
25
- FIND_RECORDS_RE = %r(/_find\b).freeze
26
- SCRIPT_REQUEST_RE = %r(/script/[^/]+\z).freeze
27
-
28
- VALIDATION_ERROR_RANGE = 500..599
29
-
30
- # @param app [#call]
31
- # @param model [Class<FmRest::Spyke::Base>]
32
- def initialize(app, model)
33
- super(app)
34
- @model = model
35
- end
36
-
37
- # @param env [Faraday::Env]
38
- def on_complete(env)
39
- return unless env.body.is_a?(Hash)
40
-
41
- json = env.body
42
-
43
- case
44
- when single_record_request?(env)
45
- env.body = prepare_single_record(json)
46
- when multiple_records_request?(env), find_request?(env)
47
- env.body = prepare_collection(json)
48
- when create_request?(env), update_request?(env), delete_request?(env), container_upload_request?(env)
49
- env.body = prepare_save_response(json)
50
- when execute_script_request?(env)
51
- env.body = build_base_hash(json)
52
- else
53
- # Attempt to parse unknown requests too
54
- env.body = build_base_hash(json)
55
- end
56
- end
57
-
58
- private
59
-
60
- # @param json [Hash]
61
- # @return [Hash] the response in Spyke format
62
- def prepare_save_response(json)
63
- response = json[:response]
64
-
65
- data = {}
66
- data[:mod_id] = response[:modId] if response[:modId]
67
- data[:id] = response[:recordId].to_i if response[:recordId]
68
-
69
- build_base_hash(json, true).merge!(data: data)
70
- end
71
-
72
- # (see #prepare_save_response)
73
- def prepare_single_record(json)
74
- data =
75
- json[:response][:data] &&
76
- prepare_record_data(json[:response][:data].first)
77
-
78
- build_base_hash(json).merge!(data: data)
79
- end
80
-
81
- # (see #prepare_save_response)
82
- def prepare_collection(json)
83
- data =
84
- json[:response][:data] &&
85
- json[:response][:data].map { |record_data| prepare_record_data(record_data) }
86
-
87
- build_base_hash(json).merge!(data: data)
88
- end
89
-
90
- # @param json [Hash]
91
- # @param include_errors [Boolean]
92
- # @return [FmRest::Spyke::Metadata] the skeleton structure for a
93
- # Spyke-formatted response
94
- def build_base_hash(json, include_errors = false)
95
- {
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) : {}
102
- }
103
- end
104
-
105
- # @param json [Hash]
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
116
- def prepare_script_results(json)
117
- results = {}
118
-
119
- [:prerequest, :presort].each do |s|
120
- if json[:response][:"scriptError.#{s}"]
121
- results[s] = OpenStruct.new(
122
- result: json[:response][:"scriptResult.#{s}"],
123
- error: json[:response][:"scriptError.#{s}"]
124
- ).freeze
125
- end
126
- end
127
-
128
- if json[:response][:scriptError]
129
- results[:after] = OpenStruct.new(
130
- result: json[:response][:scriptResult],
131
- error: json[:response][:scriptError]
132
- ).freeze
133
- end
134
-
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
147
- end
148
-
149
- # @param json [Hash]
150
- # @return [Hash] the errors hash in Spyke format
151
- def prepare_errors(json)
152
- # Code 0 means "No Error"
153
- # https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
154
- return {} if json[:messages][0][:code].to_i == 0
155
-
156
- json[:messages].each_with_object(base: []) do |message, hash|
157
- # Only include validation errors
158
- next unless VALIDATION_ERROR_RANGE.include?(message[:code].to_i)
159
-
160
- hash[:base] << "#{message[:message]} (#{message[:code]})"
161
- end
162
- end
163
-
164
- # `json_data` is expected in this format:
165
- #
166
- # {
167
- # "fieldData": {
168
- # "fieldName1" : "fieldValue1",
169
- # "fieldName2" : "fieldValue2",
170
- # ...
171
- # },
172
- # "portalData": {
173
- # "portal1" : [
174
- # { <portalRecord1> },
175
- # { <portalRecord2> },
176
- # ...
177
- # ],
178
- # "portal2" : [
179
- # { <portalRecord1> },
180
- # { <portalRecord2> },
181
- # ...
182
- # ]
183
- # },
184
- # "modId": <Id_for_last_modification>,
185
- # "recordId": <Unique_internal_ID_for_this_record>
186
- # }
187
- #
188
- # @param json_data [Hash]
189
- # @return [Hash] the record data in Spyke format
190
- def prepare_record_data(json_data)
191
- out = { id: json_data[:recordId].to_i, mod_id: json_data[:modId] }
192
- out.merge!(json_data[:fieldData])
193
- out.merge!(prepare_portal_data(json_data[:portalData])) if json_data[:portalData]
194
- out
195
- end
196
-
197
- # Extracts `recordId` and strips the `"PortalName::"` field prefix for each
198
- # portal
199
- #
200
- # Sample `json_portal_data`:
201
- #
202
- # "portalData": {
203
- # "Orders":[
204
- # { "Orders::DeliveryDate": "3/7/2017", "recordId": "23" }
205
- # ]
206
- # }
207
- #
208
- # @param json_portal_data [Hash]
209
- # @return [Hash] the portal data in Spyke format
210
- def prepare_portal_data(json_portal_data)
211
- json_portal_data.each_with_object({}) do |(portal_name, portal_records), out|
212
- portal_options = @model.portal_options[portal_name.to_s] || {}
213
-
214
- out[portal_name] =
215
- portal_records.map do |portal_fields|
216
- attributes = { id: portal_fields[:recordId].to_i }
217
- attributes[:mod_id] = portal_fields[:modId] if portal_fields[:modId]
218
-
219
- prefix = portal_options[:attribute_prefix] || portal_name
220
- prefix_matcher = /\A#{prefix}::/
221
-
222
- portal_fields.each do |k, v|
223
- next if :recordId == k || :modId == k
224
- attributes[k.to_s.gsub(prefix_matcher, "").to_sym] = v
225
- end
226
-
227
- attributes
228
- end
229
- end
230
- end
231
-
232
- # @param env [Faraday::Env]
233
- # @return [Boolean]
234
- def single_record_request?(env)
235
- env.method == :get && env.url.path.match(SINGLE_RECORD_RE)
236
- end
237
-
238
- # (see #single_record_request?)
239
- def multiple_records_request?(env)
240
- env.method == :get && env.url.path.match(MULTIPLE_RECORDS_RE)
241
- end
242
-
243
- # (see #single_record_request?)
244
- def find_request?(env)
245
- env.method == :post && env.url.path.match(FIND_RECORDS_RE)
246
- end
247
-
248
- # (see #single_record_request?)
249
- def update_request?(env)
250
- env.method == :patch && env.url.path.match(SINGLE_RECORD_RE)
251
- end
252
-
253
- # (see #single_record_request?)
254
- def create_request?(env)
255
- env.method == :post && env.url.path.match(MULTIPLE_RECORDS_RE)
256
- end
257
-
258
- # (see #single_record_request?)
259
- def container_upload_request?(env)
260
- env.method == :post && env.url.path.match(CONTAINER_RE)
261
- end
262
-
263
- # (see #single_record_request?)
264
- def delete_request?(env)
265
- env.method == :delete && env.url.path.match(SINGLE_RECORD_RE)
266
- end
267
-
268
- def execute_script_request?(env)
269
- env.method == :get && env.url.path.match(SCRIPT_REQUEST_RE)
270
- end
271
- end
272
- end
273
- end
@@ -1,25 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module Spyke
5
- # ActiveModel 4 doesn't include a ValidationError class, which we want to
6
- # raise when model.validate! fails.
7
- #
8
- # In order to break the least amount of code that uses AM5+, while still
9
- # supporting AM4 we use this proxy class that inherits from
10
- # AM::ValidationError if it's there, or reimplements it otherwise
11
- if defined?(::ActiveModel::ValidationError)
12
- class ValidationError < ::ActiveModel::ValidationError; end
13
- else
14
- class ValidationError < StandardError
15
- attr_reader :model
16
-
17
- def initialize(model)
18
- @model = model
19
- errors = @model.errors.full_messages.join(", ")
20
- super("Invalid model: #{errors}")
21
- end
22
- end
23
- end
24
- end
25
- end
@@ -1,220 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "date"
4
-
5
- module FmRest
6
- # Gotchas:
7
- #
8
- # 1.
9
- #
10
- # Date === <StringDate instance> # => false
11
- #
12
- # The above can affect case conditions, as trying to match a StringDate
13
- # with:
14
- #
15
- # case obj
16
- # when Date
17
- # ...
18
- #
19
- # ...will not work.
20
- #
21
- # Instead one must specify the FmRest::StringDate class:
22
- #
23
- # case obj
24
- # when Date, FmRest::StringDate
25
- # ...
26
- #
27
- # 2.
28
- #
29
- # StringDate#eql? only matches other strings, not dates.
30
- #
31
- # This could affect hash indexing when a StringDate is used as a key.
32
- #
33
- # TODO: Verify the above
34
- #
35
- # 3.
36
- #
37
- # StringDate#succ and StringDate#next return a String, despite Date#succ
38
- # and Date#next also existing.
39
- #
40
- # Workaround: Use StringDate#next_day or strdate + 1
41
- #
42
- # 4.
43
- #
44
- # StringDate#to_s returns the original string, not the Date string
45
- # representation.
46
- #
47
- # Workaround: Use strdate.to_date.to_s
48
- #
49
- # 5.
50
- #
51
- # StringDate#hash returns the hash for the string (important when using
52
- # a StringDate as a hash key)
53
- #
54
- # 6.
55
- #
56
- # StringDate#as_json returns the string
57
- #
58
- # Workaround: Use strdate.to_date.as_json
59
- #
60
- # 7.
61
- #
62
- # Equality with Date is not reciprocal:
63
- #
64
- # str_date == date #=> true
65
- # date == str_date #=> false
66
- #
67
- # NOTE: Potential workaround: Inherit StringDate from Date instead of String
68
- #
69
- # 8.
70
- #
71
- # Calling string transforming methods (e.g. .upcase) returns a StringDate
72
- # instead of a String.
73
- #
74
- # NOTE: Potential workaround: Inherit StringDate from Date instead of String
75
- #
76
- class StringDate < String
77
- DELEGATE_CLASS = ::Date
78
-
79
- class InvalidDate < ArgumentError; end
80
-
81
- class << self
82
- def strptime(str, date_format, *_)
83
- begin
84
- date = self::DELEGATE_CLASS.strptime(str, date_format)
85
- rescue ArgumentError
86
- raise InvalidDate
87
- end
88
-
89
- new(str, date)
90
- end
91
- end
92
-
93
- def initialize(str, date, **str_args)
94
- raise ArgumentError, "str must be of class String" unless str.is_a?(String)
95
- raise ArgumentError, "date must be of class #{self.class::DELEGATE_CLASS.name}" unless date.is_a?(self.class::DELEGATE_CLASS)
96
-
97
- super(str, **str_args)
98
-
99
- @delegate = date
100
-
101
- freeze
102
- end
103
-
104
- def is_a?(klass)
105
- klass == ::Date || super
106
- end
107
- alias_method :kind_of?, :is_a?
108
-
109
- def to_date
110
- @delegate
111
- end
112
-
113
- def to_datetime
114
- @delegate.to_datetime
115
- end
116
-
117
- def to_time
118
- @delegate.to_time
119
- end
120
-
121
- # ActiveSupport method
122
- def in_time_zone(*_)
123
- @delegate.in_time_zone(*_)
124
- end
125
-
126
- def inspect
127
- "#<#{self.class.name} #{@delegate.inspect} - #{super}>"
128
- end
129
-
130
- def <=>(oth)
131
- return @delegate <=> oth if oth.is_a?(::Date) || oth.is_a?(Numeric)
132
- super
133
- end
134
-
135
- def +(val)
136
- return @delegate + val if val.kind_of?(Numeric)
137
- super
138
- end
139
-
140
- def <<(val)
141
- return @delegate << val if val.kind_of?(Numeric)
142
- super
143
- end
144
-
145
- def ==(oth)
146
- return @delegate == oth if oth.kind_of?(::Date) || oth.kind_of?(Numeric)
147
- super
148
- end
149
- alias_method :===, :==
150
-
151
- def upto(oth, &blk)
152
- return @delegate.upto(oth, &blk) if oth.kind_of?(::Date) || oth.kind_of?(Numeric)
153
- super
154
- end
155
-
156
- def between?(a, b)
157
- return @delegate.between?(a, b) if [a, b].any? {|o| o.is_a?(::Date) || o.is_a?(Numeric) }
158
- super
159
- end
160
-
161
- private
162
-
163
- def respond_to_missing?(name, include_private = false)
164
- @delegate.respond_to?(name, include_private)
165
- end
166
-
167
- def method_missing(method, *args, &block)
168
- @delegate.send(method, *args, &block)
169
- end
170
- end
171
-
172
- class StringDateTime < StringDate
173
- DELEGATE_CLASS = ::DateTime
174
-
175
- def is_a?(klass)
176
- klass == ::DateTime || super
177
- end
178
- alias_method :kind_of?, :is_a?
179
-
180
- def to_date
181
- @delegate.to_date
182
- end
183
-
184
- def to_datetime
185
- @delegate
186
- end
187
- end
188
-
189
- module StringDateAwareness
190
- def _parse(v, *_)
191
- if v.is_a?(StringDateTime)
192
- return { year: v.year, mon: v.month, mday: v.mday, hour: v.hour, min: v.min, sec: v.sec, sec_fraction: v.sec_fraction, offset: v.offset }
193
- end
194
- if v.is_a?(StringDate)
195
- return { year: v.year, mon: v.month, mday: v.mday }
196
- end
197
- super
198
- end
199
-
200
- def parse(v, *_)
201
- if v.is_a?(StringDate)
202
- return self == ::DateTime ? v.to_datetime : v.to_date
203
- end
204
- super
205
- end
206
-
207
- # Overriding case equality method so that it returns true for
208
- # `FmRest::StringDate` instances
209
- #
210
- # Calls superclass method
211
- #
212
- def ===(other)
213
- super || other.is_a?(StringDate)
214
- end
215
-
216
- def self.enable(classes: [Date, DateTime])
217
- classes.each { |klass| klass.singleton_class.prepend(self) }
218
- end
219
- end
220
- end