fmrest-spyke 0.13.0

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