fmrest 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.yardopts +1 -0
- data/README.md +101 -7
- data/fmrest.gemspec +3 -0
- data/lib/fmrest.rb +2 -0
- data/lib/fmrest/errors.rb +27 -0
- data/lib/fmrest/spyke.rb +9 -0
- data/lib/fmrest/spyke/base.rb +2 -0
- data/lib/fmrest/spyke/container_field.rb +59 -0
- data/lib/fmrest/spyke/json_parser.rb +83 -24
- data/lib/fmrest/spyke/model.rb +7 -0
- data/lib/fmrest/spyke/model/associations.rb +2 -0
- data/lib/fmrest/spyke/model/attributes.rb +14 -55
- data/lib/fmrest/spyke/model/connection.rb +2 -0
- data/lib/fmrest/spyke/model/container_fields.rb +25 -0
- data/lib/fmrest/spyke/model/orm.rb +72 -5
- data/lib/fmrest/spyke/model/serialization.rb +80 -0
- data/lib/fmrest/spyke/model/uri.rb +2 -0
- data/lib/fmrest/spyke/portal.rb +2 -0
- data/lib/fmrest/spyke/relation.rb +30 -14
- data/lib/fmrest/token_store.rb +6 -0
- data/lib/fmrest/token_store/active_record.rb +74 -0
- data/lib/fmrest/token_store/base.rb +25 -0
- data/lib/fmrest/token_store/memory.rb +26 -0
- data/lib/fmrest/token_store/redis.rb +45 -0
- data/lib/fmrest/v1.rb +10 -49
- data/lib/fmrest/v1/connection.rb +57 -0
- data/lib/fmrest/v1/container_fields.rb +73 -0
- data/lib/fmrest/v1/paths.rb +36 -0
- data/lib/fmrest/v1/raise_errors.rb +55 -0
- data/lib/fmrest/v1/token_session.rb +32 -12
- data/lib/fmrest/v1/token_store/active_record.rb +6 -66
- data/lib/fmrest/v1/token_store/memory.rb +6 -19
- data/lib/fmrest/v1/utils.rb +94 -0
- data/lib/fmrest/version.rb +3 -1
- metadata +60 -5
- data/lib/fmrest/v1/token_store.rb +0 -6
- data/lib/fmrest/v1/token_store/base.rb +0 -14
data/lib/fmrest/spyke/model.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "fmrest/spyke/model/connection"
|
2
4
|
require "fmrest/spyke/model/uri"
|
3
5
|
require "fmrest/spyke/model/attributes"
|
6
|
+
require "fmrest/spyke/model/serialization"
|
4
7
|
require "fmrest/spyke/model/associations"
|
5
8
|
require "fmrest/spyke/model/orm"
|
9
|
+
require "fmrest/spyke/model/container_fields"
|
6
10
|
|
7
11
|
module FmRest
|
8
12
|
module Spyke
|
@@ -12,10 +16,13 @@ module FmRest
|
|
12
16
|
include Connection
|
13
17
|
include Uri
|
14
18
|
include Attributes
|
19
|
+
include Serialization
|
15
20
|
include Associations
|
16
21
|
include Orm
|
22
|
+
include ContainerFields
|
17
23
|
|
18
24
|
included do
|
25
|
+
# @return [Integer] the record's modId
|
19
26
|
attr_accessor :mod_id
|
20
27
|
end
|
21
28
|
end
|
@@ -1,16 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/spyke/model/orm"
|
4
|
+
|
1
5
|
module FmRest
|
2
6
|
module Spyke
|
3
7
|
module Model
|
4
8
|
module Attributes
|
5
9
|
extend ::ActiveSupport::Concern
|
6
10
|
|
11
|
+
include Orm # Needed to extend custom save and reload
|
12
|
+
|
7
13
|
include ::ActiveModel::Dirty
|
8
14
|
include ::ActiveModel::ForbiddenAttributesProtection
|
9
15
|
|
10
16
|
included do
|
11
|
-
# Keep mod_id as a separate, custom accessor
|
12
|
-
attr_accessor :mod_id
|
13
|
-
|
14
17
|
# Prevent the creation of plain (no prefix/suffix) attribute methods
|
15
18
|
# when calling ActiveModels' define_attribute_method, otherwise it
|
16
19
|
# will define an `attribute` method which overrides the one provided
|
@@ -100,29 +103,12 @@ module FmRest
|
|
100
103
|
super
|
101
104
|
end
|
102
105
|
|
103
|
-
def
|
104
|
-
super.tap do |r|
|
105
|
-
next unless r.present?
|
106
|
-
changes_applied
|
107
|
-
portals.each(&:parent_changes_applied)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def reload
|
106
|
+
def reload(*args)
|
112
107
|
super.tap { |r| clear_changes_information }
|
113
108
|
end
|
114
109
|
|
115
|
-
|
116
|
-
|
117
|
-
#
|
118
|
-
def to_params
|
119
|
-
params = { fieldData: changed_params_not_embedded_in_url }
|
120
|
-
params[:modId] = mod_id if mod_id
|
121
|
-
|
122
|
-
portal_data = serialize_portals
|
123
|
-
params[:portalData] = portal_data unless portal_data.empty?
|
124
|
-
|
125
|
-
params
|
110
|
+
def save(*args)
|
111
|
+
super.tap { |r| changes_applied_after_save if r }
|
126
112
|
end
|
127
113
|
|
128
114
|
# ActiveModel::Dirty since version 5.2 assumes that if there's an
|
@@ -143,44 +129,12 @@ module FmRest
|
|
143
129
|
use_setters(sanitize_for_mass_assignment(new_attributes)) if new_attributes && !new_attributes.empty?
|
144
130
|
end
|
145
131
|
|
146
|
-
protected
|
147
|
-
|
148
|
-
def serialize_for_portal(portal)
|
149
|
-
params =
|
150
|
-
changed_params.except(:id).transform_keys do |key|
|
151
|
-
"#{portal.attribute_prefix}::#{key}"
|
152
|
-
end
|
153
|
-
|
154
|
-
params[:recordId] = id if id
|
155
|
-
params[:modId] = mod_id if mod_id
|
156
|
-
|
157
|
-
params
|
158
|
-
end
|
159
|
-
|
160
132
|
private
|
161
133
|
|
162
|
-
def serialize_portals
|
163
|
-
portal_data = {}
|
164
|
-
|
165
|
-
portals.each do |portal|
|
166
|
-
portal.each do |portal_record|
|
167
|
-
next unless portal_record.changed?
|
168
|
-
portal_params = portal_data[portal.portal_key] ||= []
|
169
|
-
portal_params << portal_record.serialize_for_portal(portal)
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
portal_data
|
174
|
-
end
|
175
|
-
|
176
134
|
def changed_params
|
177
135
|
attributes.to_params.slice(*mapped_changed)
|
178
136
|
end
|
179
137
|
|
180
|
-
def changed_params_not_embedded_in_url
|
181
|
-
params_not_embedded_in_url.slice(*mapped_changed)
|
182
|
-
end
|
183
|
-
|
184
138
|
def mapped_changed
|
185
139
|
mapped_attributes.values_at(*changed)
|
186
140
|
end
|
@@ -192,6 +146,11 @@ module FmRest
|
|
192
146
|
"#{k}: #{attribute(v).inspect}"
|
193
147
|
end.join(', ')
|
194
148
|
end
|
149
|
+
|
150
|
+
def changes_applied_after_save
|
151
|
+
changes_applied
|
152
|
+
portals.each(&:parent_changes_applied)
|
153
|
+
end
|
195
154
|
end
|
196
155
|
end
|
197
156
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/spyke/container_field"
|
4
|
+
|
5
|
+
module FmRest
|
6
|
+
module Spyke
|
7
|
+
module Model
|
8
|
+
module ContainerFields
|
9
|
+
extend ::ActiveSupport::Concern
|
10
|
+
|
11
|
+
class_methods do
|
12
|
+
def container(name, options = {})
|
13
|
+
field_name = options[:field_name] || name
|
14
|
+
|
15
|
+
define_method(name) do
|
16
|
+
@container_fields ||= {}
|
17
|
+
@container_fields[name.to_sym] ||= ContainerField.new(self, field_name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "fmrest/spyke/relation"
|
2
4
|
|
3
5
|
module FmRest
|
@@ -14,6 +16,7 @@ module FmRest
|
|
14
16
|
end
|
15
17
|
|
16
18
|
class_methods do
|
19
|
+
# Methods delegated to FmRest::Spyke::Relation
|
17
20
|
delegate :limit, :offset, :sort, :query, :portal, to: :all
|
18
21
|
|
19
22
|
def all
|
@@ -42,6 +45,12 @@ module FmRest
|
|
42
45
|
self.current_scope = previous
|
43
46
|
end
|
44
47
|
|
48
|
+
# API-error-raising version of #create
|
49
|
+
#
|
50
|
+
def create!(attributes = {})
|
51
|
+
new(attributes).tap(&:save!)
|
52
|
+
end
|
53
|
+
|
45
54
|
private
|
46
55
|
|
47
56
|
def extend_scope_with_fm_params(scope, prefixed: false)
|
@@ -66,13 +75,71 @@ module FmRest
|
|
66
75
|
end
|
67
76
|
end
|
68
77
|
|
69
|
-
#
|
78
|
+
# Completely override Spyke's save to provide a number of features:
|
79
|
+
#
|
80
|
+
# * Validations
|
81
|
+
# * Data API scripts execution
|
82
|
+
# * Refresh of dirty attributes
|
70
83
|
#
|
71
84
|
def save(options = {})
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
85
|
+
callback = persisted? ? :update : :create
|
86
|
+
|
87
|
+
return false unless perform_save_validations(callback, options)
|
88
|
+
return false unless perform_save_persistence(callback, options)
|
89
|
+
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
def save!(options = {})
|
94
|
+
save(options.merge(raise_validation_errors: true))
|
95
|
+
end
|
96
|
+
|
97
|
+
# API-error-raising version of #update
|
98
|
+
#
|
99
|
+
def update!(new_attributes)
|
100
|
+
self.attributes = new_attributes
|
101
|
+
save!
|
102
|
+
end
|
103
|
+
|
104
|
+
def reload
|
105
|
+
reloaded = self.class.find(id)
|
106
|
+
self.attributes = reloaded.attributes
|
107
|
+
self.mod_id = reloaded.mod_id
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def perform_save_validations(context, options)
|
113
|
+
return true if options[:validate] == false
|
114
|
+
options[:raise_validation_errors] ? validate!(context) : validate(context)
|
115
|
+
end
|
116
|
+
|
117
|
+
def perform_save_persistence(callback, options)
|
118
|
+
run_callbacks :save do
|
119
|
+
run_callbacks(callback) do
|
120
|
+
|
121
|
+
begin
|
122
|
+
send self.class.method_for(callback), build_params_for_save(options)
|
123
|
+
|
124
|
+
rescue APIError::ValidationError => e
|
125
|
+
if options[:raise_validation_errors]
|
126
|
+
raise e
|
127
|
+
else
|
128
|
+
return false
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
true
|
136
|
+
end
|
137
|
+
|
138
|
+
def build_params_for_save(options)
|
139
|
+
to_params.tap do |params|
|
140
|
+
if options.has_key?(:script)
|
141
|
+
params.merge!(FmRest::V1.convert_script_params(options[:script]))
|
142
|
+
end
|
76
143
|
end
|
77
144
|
end
|
78
145
|
end
|
@@ -0,0 +1,80 @@
|
|
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".freeze
|
8
|
+
FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S".freeze
|
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
|
67
|
+
value.strftime(FM_DATETIME_FORMAT)
|
68
|
+
when Date
|
69
|
+
value.strftime(FM_DATE_FORMAT)
|
70
|
+
else
|
71
|
+
value
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
params
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/fmrest/spyke/portal.rb
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FmRest
|
2
4
|
module Spyke
|
3
5
|
class Relation < ::Spyke::Relation
|
4
6
|
SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
|
5
7
|
|
6
|
-
# We need to keep
|
7
|
-
#
|
8
|
-
#
|
9
|
-
# last moment
|
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
|
+
|
10
14
|
attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
|
11
15
|
:portal_params
|
12
16
|
|
@@ -23,32 +27,40 @@ module FmRest
|
|
23
27
|
@portal_params = []
|
24
28
|
end
|
25
29
|
|
30
|
+
# @param value [Integer] the limit value
|
31
|
+
# @return [FmRest::Spyke::Relation] a new relation with the limit applied
|
26
32
|
def limit(value)
|
27
33
|
with_clone { |r| r.limit_value = value }
|
28
34
|
end
|
29
35
|
|
36
|
+
# @param value [Integer] the offset value
|
37
|
+
# @return [FmRest::Spyke::Relation] a new relation with the offset
|
38
|
+
# applied
|
30
39
|
def offset(value)
|
31
40
|
with_clone { |r| r.offset_value = value }
|
32
41
|
end
|
33
42
|
|
34
43
|
# Allows sort params given in either hash format (using FM Data API's
|
35
44
|
# format), or as a symbol, in which case the of the attribute must match
|
36
|
-
# a known mapped attribute, optionally suffixed with
|
45
|
+
# a known mapped attribute, optionally suffixed with `!` or `__desc` to
|
37
46
|
# signify it should use descending order.
|
38
47
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
48
|
+
# @param args [Array<Symbol, Hash>] the names of attributes to sort by with
|
49
|
+
# optional `!` or `__desc` suffix, or a hash of options as expected by
|
50
|
+
# the FM Data API
|
51
|
+
# @example
|
52
|
+
# Person.sort(:first_name, :age!)
|
53
|
+
# Person.sort(:first_name, :age__desc)
|
54
|
+
# Person.sort(:first_name, :age__descend)
|
55
|
+
# Person.sort({ fieldName: "FirstName" }, { fieldName: "Age", sortOrder: "descend" })
|
56
|
+
# @return [FmRest::Spyke::Relation] a new relation with the sort options
|
57
|
+
# applied
|
46
58
|
def sort(*args)
|
47
59
|
with_clone do |r|
|
48
60
|
r.sort_params = args.flatten.map { |s| normalize_sort_param(s) }
|
49
61
|
end
|
50
62
|
end
|
51
|
-
alias
|
63
|
+
alias order sort
|
52
64
|
|
53
65
|
def portal(*args)
|
54
66
|
with_clone do |r|
|
@@ -56,7 +68,7 @@ module FmRest
|
|
56
68
|
r.portal_params.uniq!
|
57
69
|
end
|
58
70
|
end
|
59
|
-
alias
|
71
|
+
alias includes portal
|
60
72
|
|
61
73
|
def query(*params)
|
62
74
|
with_clone do |r|
|
@@ -68,10 +80,14 @@ module FmRest
|
|
68
80
|
query(params.merge(omit: true))
|
69
81
|
end
|
70
82
|
|
83
|
+
# @return [Boolean] whether a query was set on this relation
|
71
84
|
def has_query?
|
72
85
|
query_params.present?
|
73
86
|
end
|
74
87
|
|
88
|
+
# Finds a single instance of the model by forcing limit = 1
|
89
|
+
#
|
90
|
+
# @return [FmRest::Spyke::Base]
|
75
91
|
def find_one
|
76
92
|
return super if params[klass.primary_key].present?
|
77
93
|
@find_one ||= klass.new_collection_from_result(limit(1).fetch).first
|