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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.yardopts +1 -0
  4. data/README.md +101 -7
  5. data/fmrest.gemspec +3 -0
  6. data/lib/fmrest.rb +2 -0
  7. data/lib/fmrest/errors.rb +27 -0
  8. data/lib/fmrest/spyke.rb +9 -0
  9. data/lib/fmrest/spyke/base.rb +2 -0
  10. data/lib/fmrest/spyke/container_field.rb +59 -0
  11. data/lib/fmrest/spyke/json_parser.rb +83 -24
  12. data/lib/fmrest/spyke/model.rb +7 -0
  13. data/lib/fmrest/spyke/model/associations.rb +2 -0
  14. data/lib/fmrest/spyke/model/attributes.rb +14 -55
  15. data/lib/fmrest/spyke/model/connection.rb +2 -0
  16. data/lib/fmrest/spyke/model/container_fields.rb +25 -0
  17. data/lib/fmrest/spyke/model/orm.rb +72 -5
  18. data/lib/fmrest/spyke/model/serialization.rb +80 -0
  19. data/lib/fmrest/spyke/model/uri.rb +2 -0
  20. data/lib/fmrest/spyke/portal.rb +2 -0
  21. data/lib/fmrest/spyke/relation.rb +30 -14
  22. data/lib/fmrest/token_store.rb +6 -0
  23. data/lib/fmrest/token_store/active_record.rb +74 -0
  24. data/lib/fmrest/token_store/base.rb +25 -0
  25. data/lib/fmrest/token_store/memory.rb +26 -0
  26. data/lib/fmrest/token_store/redis.rb +45 -0
  27. data/lib/fmrest/v1.rb +10 -49
  28. data/lib/fmrest/v1/connection.rb +57 -0
  29. data/lib/fmrest/v1/container_fields.rb +73 -0
  30. data/lib/fmrest/v1/paths.rb +36 -0
  31. data/lib/fmrest/v1/raise_errors.rb +55 -0
  32. data/lib/fmrest/v1/token_session.rb +32 -12
  33. data/lib/fmrest/v1/token_store/active_record.rb +6 -66
  34. data/lib/fmrest/v1/token_store/memory.rb +6 -19
  35. data/lib/fmrest/v1/utils.rb +94 -0
  36. data/lib/fmrest/version.rb +3 -1
  37. metadata +60 -5
  38. data/lib/fmrest/v1/token_store.rb +0 -6
  39. data/lib/fmrest/v1/token_store/base.rb +0 -14
@@ -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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "fmrest/spyke/portal"
2
4
 
3
5
  module FmRest
@@ -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 save(*_args)
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
- # Override to_params to return FM Data API's expected JSON format, and
116
- # including only modified fields
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FmRest
2
4
  module Spyke
3
5
  module Model
@@ -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
- # Ensure save returns true/false, following ActiveRecord's convention
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
- if options[:validate] == false || valid?
73
- super().present? # Failed save returns empty hash
74
- else
75
- false
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FmRest
2
4
  module Spyke
3
5
  module Model
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FmRest
2
4
  module Spyke
3
5
  # Extend Spyke's HasMany association with custom options
@@ -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 these separate from regular params because FM Data API
7
- # uses either "limit" or "_limit" (or "_offset", etc.) as param keys
8
- # depending on the type of request, so we can't set the params until the
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 ! or __desc[end] to
45
+ # a known mapped attribute, optionally suffixed with `!` or `__desc` to
37
46
  # signify it should use descending order.
38
47
  #
39
- # E.g.
40
- #
41
- # Person.sort(:first_name, :age!)
42
- # Person.sort(:first_name, :age__desc)
43
- # Person.sort(:first_name, :age__descend)
44
- # Person.sort({ fieldName: "FirstName" }, { fieldName: "Age", sortOrder: "descend" })
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 :order :sort
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 :includes :portal
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