fmrest 0.1.0 → 0.2.0

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