fmrest-spyke 0.13.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.
@@ -0,0 +1,274 @@
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[:__record_id] = response[:recordId] if response[:recordId]
68
+ data[:__new_portal_record_info] = response[:newPortalRecordInfo] if response[:newPortalRecordInfo]
69
+
70
+ build_base_hash(json, true).merge!(data: data)
71
+ end
72
+
73
+ # (see #prepare_save_response)
74
+ def prepare_single_record(json)
75
+ data =
76
+ json[:response][:data] &&
77
+ prepare_record_data(json[:response][:data].first)
78
+
79
+ build_base_hash(json).merge!(data: data)
80
+ end
81
+
82
+ # (see #prepare_save_response)
83
+ def prepare_collection(json)
84
+ data =
85
+ json[:response][:data] &&
86
+ json[:response][:data].map { |record_data| prepare_record_data(record_data) }
87
+
88
+ build_base_hash(json).merge!(data: data)
89
+ end
90
+
91
+ # @param json [Hash]
92
+ # @param include_errors [Boolean]
93
+ # @return [FmRest::Spyke::Metadata] the skeleton structure for a
94
+ # Spyke-formatted response
95
+ def build_base_hash(json, include_errors = false)
96
+ {
97
+ metadata: Metadata.new(
98
+ prepare_messages(json),
99
+ prepare_script_results(json),
100
+ prepare_data_info(json)
101
+ ).freeze,
102
+ errors: include_errors ? prepare_errors(json) : {}
103
+ }
104
+ end
105
+
106
+ # @param json [Hash]
107
+ # @return [Array<OpenStruct>] the skeleton structure for a
108
+ # Spyke-formatted response
109
+ def prepare_messages(json)
110
+ return [] unless json[:messages]
111
+ json[:messages].map { |m| OpenStruct.new(m).freeze }.freeze
112
+ end
113
+
114
+ # @param json [Hash]
115
+ # @return [OpenStruct] the script(s) execution results for Spyke metadata
116
+ # format
117
+ def prepare_script_results(json)
118
+ results = {}
119
+
120
+ [:prerequest, :presort].each do |s|
121
+ if json[:response][:"scriptError.#{s}"]
122
+ results[s] = OpenStruct.new(
123
+ result: json[:response][:"scriptResult.#{s}"],
124
+ error: json[:response][:"scriptError.#{s}"]
125
+ ).freeze
126
+ end
127
+ end
128
+
129
+ if json[:response][:scriptError]
130
+ results[:after] = OpenStruct.new(
131
+ result: json[:response][:scriptResult],
132
+ error: json[:response][:scriptError]
133
+ ).freeze
134
+ end
135
+
136
+ results.present? ? OpenStruct.new(results).freeze : nil
137
+ end
138
+
139
+ # @param json [Hash]
140
+ # @return [OpenStruct] the script(s) execution results for
141
+ # Spyke metadata format
142
+ def prepare_data_info(json)
143
+ data_info = json[:response] && json[:response][:dataInfo]
144
+
145
+ return nil unless data_info.present?
146
+
147
+ DataInfo.new(data_info).freeze
148
+ end
149
+
150
+ # @param json [Hash]
151
+ # @return [Hash] the errors hash in Spyke format
152
+ def prepare_errors(json)
153
+ # Code 0 means "No Error"
154
+ # https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
155
+ return {} if json[:messages][0][:code].to_i == 0
156
+
157
+ json[:messages].each_with_object(base: []) do |message, hash|
158
+ # Only include validation errors
159
+ next unless VALIDATION_ERROR_RANGE.include?(message[:code].to_i)
160
+
161
+ hash[:base] << "#{message[:message]} (#{message[:code]})"
162
+ end
163
+ end
164
+
165
+ # `json_data` is expected in this format:
166
+ #
167
+ # {
168
+ # "fieldData": {
169
+ # "fieldName1" : "fieldValue1",
170
+ # "fieldName2" : "fieldValue2",
171
+ # ...
172
+ # },
173
+ # "portalData": {
174
+ # "portal1" : [
175
+ # { <portalRecord1> },
176
+ # { <portalRecord2> },
177
+ # ...
178
+ # ],
179
+ # "portal2" : [
180
+ # { <portalRecord1> },
181
+ # { <portalRecord2> },
182
+ # ...
183
+ # ]
184
+ # },
185
+ # "modId": <Id_for_last_modification>,
186
+ # "recordId": <Unique_internal_ID_for_this_record>
187
+ # }
188
+ #
189
+ # @param json_data [Hash]
190
+ # @return [Hash] the record data in Spyke format
191
+ def prepare_record_data(json_data)
192
+ out = { __record_id: json_data[:recordId], __mod_id: json_data[:modId] }
193
+ out.merge!(json_data[:fieldData])
194
+ out.merge!(prepare_portal_data(json_data[:portalData])) if json_data[:portalData]
195
+ out
196
+ end
197
+
198
+ # Extracts `recordId` and strips the `"PortalName::"` field prefix for each
199
+ # portal
200
+ #
201
+ # Sample `json_portal_data`:
202
+ #
203
+ # "portalData": {
204
+ # "Orders":[
205
+ # { "Orders::DeliveryDate": "3/7/2017", "recordId": "23" }
206
+ # ]
207
+ # }
208
+ #
209
+ # @param json_portal_data [Hash]
210
+ # @return [Hash] the portal data in Spyke format
211
+ def prepare_portal_data(json_portal_data)
212
+ json_portal_data.each_with_object({}) do |(portal_name, portal_records), out|
213
+ portal_options = @model.portal_options[portal_name.to_s] || {}
214
+
215
+ out[portal_name] =
216
+ portal_records.map do |portal_fields|
217
+ attributes = { __record_id: portal_fields[:recordId] }
218
+ attributes[:__mod_id] = portal_fields[:modId] if portal_fields[:modId]
219
+
220
+ prefix = portal_options[:attribute_prefix] || portal_name
221
+ prefix_matcher = /\A#{prefix}::/
222
+
223
+ portal_fields.each do |k, v|
224
+ next if :recordId == k || :modId == k
225
+ attributes[k.to_s.gsub(prefix_matcher, "").to_sym] = v
226
+ end
227
+
228
+ attributes
229
+ end
230
+ end
231
+ end
232
+
233
+ # @param env [Faraday::Env]
234
+ # @return [Boolean]
235
+ def single_record_request?(env)
236
+ env.method == :get && env.url.path.match(SINGLE_RECORD_RE)
237
+ end
238
+
239
+ # (see #single_record_request?)
240
+ def multiple_records_request?(env)
241
+ env.method == :get && env.url.path.match(MULTIPLE_RECORDS_RE)
242
+ end
243
+
244
+ # (see #single_record_request?)
245
+ def find_request?(env)
246
+ env.method == :post && env.url.path.match(FIND_RECORDS_RE)
247
+ end
248
+
249
+ # (see #single_record_request?)
250
+ def update_request?(env)
251
+ env.method == :patch && env.url.path.match(SINGLE_RECORD_RE)
252
+ end
253
+
254
+ # (see #single_record_request?)
255
+ def create_request?(env)
256
+ env.method == :post && env.url.path.match(MULTIPLE_RECORDS_RE)
257
+ end
258
+
259
+ # (see #single_record_request?)
260
+ def container_upload_request?(env)
261
+ env.method == :post && env.url.path.match(CONTAINER_RE)
262
+ end
263
+
264
+ # (see #single_record_request?)
265
+ def delete_request?(env)
266
+ env.method == :delete && env.url.path.match(SINGLE_RECORD_RE)
267
+ end
268
+
269
+ def execute_script_request?(env)
270
+ env.method == :get && env.url.path.match(SCRIPT_REQUEST_RE)
271
+ end
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,25 @@
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
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fmrest-spyke
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.13.0
5
+ platform: ruby
6
+ authors:
7
+ - Pedro Carbajal
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-02-11 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fmrest-core
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.13.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.13.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: spyke
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 5.3.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 5.3.3
41
+ description: fmrest-spyke is an ActiveRecord-like ORM client library for the FileMaker
42
+ Data API built on top of fmrest-core and Spyke (https://github.com/balvig/spyke).
43
+ email:
44
+ - pedro_c@beezwax.net
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".yardopts"
50
+ - CHANGELOG.md
51
+ - LICENSE.txt
52
+ - README.md
53
+ - lib/fmrest-spyke.rb
54
+ - lib/fmrest/spyke.rb
55
+ - lib/fmrest/spyke/base.rb
56
+ - lib/fmrest/spyke/container_field.rb
57
+ - lib/fmrest/spyke/model.rb
58
+ - lib/fmrest/spyke/model/associations.rb
59
+ - lib/fmrest/spyke/model/attributes.rb
60
+ - lib/fmrest/spyke/model/auth.rb
61
+ - lib/fmrest/spyke/model/connection.rb
62
+ - lib/fmrest/spyke/model/container_fields.rb
63
+ - lib/fmrest/spyke/model/global_fields.rb
64
+ - lib/fmrest/spyke/model/http.rb
65
+ - lib/fmrest/spyke/model/orm.rb
66
+ - lib/fmrest/spyke/model/record_id.rb
67
+ - lib/fmrest/spyke/model/serialization.rb
68
+ - lib/fmrest/spyke/model/uri.rb
69
+ - lib/fmrest/spyke/portal.rb
70
+ - lib/fmrest/spyke/relation.rb
71
+ - lib/fmrest/spyke/spyke_formatter.rb
72
+ - lib/fmrest/spyke/validation_error.rb
73
+ homepage: https://github.com/beezwax/fmrest-ruby
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.2.3
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: FileMaker Data API ORM client library
96
+ test_files: []