fmrest 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|