fmrest 0.9.0 → 0.12.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.
@@ -6,6 +6,10 @@ require "fmrest/spyke/validation_error"
6
6
  module FmRest
7
7
  module Spyke
8
8
  module Model
9
+ # This module adds and extends various ORM features in Spyke models,
10
+ # including custom query methods, remote script execution and
11
+ # exception-raising persistence methods.
12
+ #
9
13
  module Orm
10
14
  extend ::ActiveSupport::Concern
11
15
 
@@ -21,22 +25,25 @@ module FmRest
21
25
  end
22
26
 
23
27
  class_methods do
24
- # Methods delegated to FmRest::Spyke::Relation
28
+ # Methods delegated to `FmRest::Spyke::Relation`
25
29
  delegate :limit, :offset, :sort, :order, :query, :omit, :portal,
26
30
  :portals, :includes, :with_all_portals, :without_portals,
27
31
  :script, :find_one, :first, :any, :find_some,
28
32
  :find_in_batches, :find_each, to: :all
29
33
 
34
+ # Spyke override -- Use FmRest's Relation instead of Spyke's vanilla
35
+ # one
36
+ #
30
37
  def all
31
- # Use FmRest's Relation instead of Spyke's vanilla one
32
38
  current_scope || Relation.new(self, uri: uri)
33
39
  end
34
40
 
35
- # Extended fetch to allow properly setting limit, offset and other
41
+ # Spyke override -- allows properly setting limit, offset and other
36
42
  # options, as well as using the appropriate HTTP method/URL depending
37
- # on whether there's a query present in the current scope, e.g.:
43
+ # on whether there's a query present in the current scope.
38
44
  #
39
- # Person.query(first_name: "Stefan").fetch # POST .../_find
45
+ # @example
46
+ # Person.query(first_name: "Stefan").fetch # -> POST .../_find
40
47
  #
41
48
  def fetch
42
49
  if current_scope.has_query?
@@ -62,12 +69,21 @@ module FmRest
62
69
  self.current_scope = previous
63
70
  end
64
71
 
65
- # API-error-raising version of #create
72
+ # Exception-raising version of `#create`
73
+ #
74
+ # @param attributes [Hash] the attributes to initialize the
75
+ # record with
66
76
  #
67
77
  def create!(attributes = {})
68
78
  new(attributes).tap(&:save!)
69
79
  end
70
80
 
81
+ # Requests execution of a FileMaker script.
82
+ #
83
+ # @param script_name [String] the name of the FileMaker script to
84
+ # execute
85
+ # @param param [String] an optional paramater for the script
86
+ #
71
87
  def execute_script(script_name, param: nil)
72
88
  params = {}
73
89
  params = {"script.param" => param} unless param.nil?
@@ -108,12 +124,20 @@ module FmRest
108
124
  end
109
125
  end
110
126
 
111
- # Overwrite Spyke's save to provide a number of features:
127
+ # Spyke override -- Adds a number of features to original `#save`:
112
128
  #
113
129
  # * Validations
114
130
  # * Data API scripts execution
115
131
  # * Refresh of dirty attributes
116
132
  #
133
+ # @option options [String] :script the name of a FileMaker script to execute
134
+ # upon saving
135
+ # @option options [Boolean] :raise_validation_errors whether to raise an
136
+ # exception if validations fail
137
+ #
138
+ # @return [true] if saved successfully
139
+ # @return [false] if validations or persistence failed
140
+ #
117
141
  def save(options = {})
118
142
  callback = persisted? ? :update : :create
119
143
 
@@ -123,11 +147,34 @@ module FmRest
123
147
  true
124
148
  end
125
149
 
150
+ # Exception-raising version of `#save`.
151
+ #
152
+ # @option (see #save)
153
+ #
154
+ # @return [true] if saved successfully
155
+ #
156
+ # @raise if validations or presistence failed
157
+ #
126
158
  def save!(options = {})
127
159
  save(options.merge(raise_validation_errors: true))
128
160
  end
129
161
 
130
- # Overwrite Spyke's destroy to provide Data API script execution
162
+ # Exception-raising version of `#update`.
163
+ #
164
+ # @param new_attributes [Hash] a hash of record attributes to update
165
+ # the record with
166
+ #
167
+ # @option (see #save)
168
+ #
169
+ def update!(new_attributes, options = {})
170
+ self.attributes = new_attributes
171
+ save!(options)
172
+ end
173
+
174
+ # Spyke override -- Adds support for Data API script execution.
175
+ #
176
+ # @option options [String] :script the name of a FileMaker script to execute
177
+ # upon deletion
131
178
  #
132
179
  def destroy(options = {})
133
180
  # For whatever reason the Data API wants the script params as query
@@ -142,22 +189,19 @@ module FmRest
142
189
  self.attributes = delete(uri.to_s + script_query_string)
143
190
  end
144
191
 
145
- # API-error-raising version of #update
192
+ # (see #destroy)
193
+ #
194
+ # @option (see #destroy)
146
195
  #
147
- def update!(new_attributes, options = {})
148
- self.attributes = new_attributes
149
- save!(options)
150
- end
151
-
152
196
  def reload(options = {})
153
197
  scope = self.class
154
198
  scope = scope.script(options[:script]) if options.has_key?(:script)
155
- reloaded = scope.find(id)
199
+ reloaded = scope.find(__record_id)
156
200
  self.attributes = reloaded.attributes
157
- self.mod_id = reloaded.mod_id
201
+ self.__mod_id = reloaded.mod_id
158
202
  end
159
203
 
160
- # ActiveModel 5+ implements this method, so we only needed if we're in
204
+ # ActiveModel 5+ implements this method, so we only need it if we're in
161
205
  # the older AM4
162
206
  if ActiveModel::VERSION::MAJOR == 4
163
207
  def validate!(context = nil)
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FmRest
4
+ module Spyke
5
+ module Model
6
+ # Modifies Spyke models to use `__record_id` instead of `id` as the
7
+ # "primary key" method, so that we can map a model class to a FM layout
8
+ # with a field named `id` without clobbering it.
9
+ #
10
+ # The `id` reader method still maps to the record ID for backwards
11
+ # compatibility and because Spyke hardcodes its use at various points
12
+ # through its codebase, but it can be safely overwritten (e.g. to map to
13
+ # a FM field).
14
+ #
15
+ # The recommended way to deal with a layout that maps an `id` attribute
16
+ # is to remap it in the model to something else, e.g. `unique_id`.
17
+ #
18
+ module RecordID
19
+ extend ::ActiveSupport::Concern
20
+
21
+ included do
22
+ # @return [Integer] the record's recordId
23
+ attr_accessor :__record_id
24
+ alias_method :record_id, :__record_id
25
+ alias_method :id, :__record_id
26
+
27
+ # @return [Integer] the record's modId
28
+ attr_accessor :__mod_id
29
+ alias_method :mod_id, :__mod_id
30
+
31
+ # Get rid of Spyke's id= setter method, as we'll be using __record_id=
32
+ # instead
33
+ undef_method :id=
34
+
35
+ # Tell Spyke that we want __record_id as the PK
36
+ self.primary_key = :__record_id
37
+ end
38
+
39
+ def __record_id?
40
+ __record_id.present?
41
+ end
42
+ alias_method :record_id?, :__record_id?
43
+ alias_method :persisted?, :__record_id?
44
+
45
+ # Spyke override -- Use `__record_id` instead of `id`
46
+ #
47
+ def hash
48
+ __record_id.hash
49
+ end
50
+
51
+ # Spyke override -- Renders class string with layout name and
52
+ # `record_id`.
53
+ #
54
+ # @return [String] A string representation of the class
55
+ #
56
+ def inspect
57
+ "#<#{self.class}(layout: #{self.class.layout}) record_id: #{__record_id.inspect} #{inspect_attributes}>"
58
+ end
59
+
60
+ # Spyke override -- Use `__record_id` instead of `id`
61
+ #
62
+ # @param id [Integer] The id of the record to destroy
63
+ #
64
+ def destroy(id = nil)
65
+ new(__record_id: id).destroy
66
+ end
67
+
68
+ private
69
+
70
+ # Spyke override (private)
71
+ #
72
+ def conflicting_ids?(attributes)
73
+ false
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -4,11 +4,11 @@ module FmRest
4
4
  module Spyke
5
5
  module Model
6
6
  module Serialization
7
- FM_DATE_FORMAT = "%m/%d/%Y".freeze
8
- FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S".freeze
7
+ FM_DATE_FORMAT = "%m/%d/%Y"
8
+ FM_DATETIME_FORMAT = "#{FM_DATE_FORMAT} %H:%M:%S"
9
9
 
10
- # Override Spyke's to_params to return FM Data API's expected JSON
11
- # format, and including only modified fields
10
+ # Spyke override -- Return FM Data API's expected JSON format,
11
+ # including only modified fields.
12
12
  #
13
13
  def to_params
14
14
  params = {
@@ -63,9 +63,9 @@ module FmRest
63
63
  def serialize_values!(params)
64
64
  params.transform_values! do |value|
65
65
  case value
66
- when DateTime, Time
67
- value.strftime(FM_DATETIME_FORMAT)
68
- when Date
66
+ when *datetime_classes
67
+ convert_datetime_timezone(value.to_datetime).strftime(FM_DATETIME_FORMAT)
68
+ when *date_classes
69
69
  value.strftime(FM_DATE_FORMAT)
70
70
  else
71
71
  value
@@ -74,6 +74,25 @@ module FmRest
74
74
 
75
75
  params
76
76
  end
77
+
78
+ def convert_datetime_timezone(dt)
79
+ case fmrest_config.timezone
80
+ when :utc, "utc"
81
+ dt.new_offset(0)
82
+ when :local, "local"
83
+ dt.new_offset(FmRest::V1.local_offset_for_datetime(dt))
84
+ when nil
85
+ dt
86
+ end
87
+ end
88
+
89
+ def datetime_classes
90
+ [DateTime, Time, defined?(FmRest::StringDateTime) && FmRest::StringDateTime].compact
91
+ end
92
+
93
+ def date_classes
94
+ [Date, defined?(FmRest::StringDate) && FmRest::StringDate].compact
95
+ end
77
96
  end
78
97
  end
79
98
  end
@@ -3,7 +3,7 @@
3
3
  module FmRest
4
4
  module Spyke
5
5
  module Model
6
- module Uri
6
+ module URI
7
7
  extend ::ActiveSupport::Concern
8
8
 
9
9
  class_methods do
@@ -14,13 +14,12 @@ module FmRest
14
14
  @layout ||= model_name.name
15
15
  end
16
16
 
17
- # Extend uri acccessor to default to FM Data schema
17
+ # Spyke override -- Extends `uri` to default to FM Data's URI schema
18
18
  #
19
19
  def uri(uri_template = nil)
20
20
  if @uri.nil? && uri_template.nil?
21
- return FmRest::V1.record_path(layout) + "(/:id)"
21
+ return FmRest::V1.record_path(layout) + "(/:#{primary_key})"
22
22
  end
23
-
24
23
  super
25
24
  end
26
25
  end
@@ -63,8 +63,8 @@ module FmRest
63
63
  response = json[:response]
64
64
 
65
65
  data = {}
66
- data[:mod_id] = response[:modId] if response[:modId]
67
- data[:id] = response[:recordId].to_i if response[:recordId]
66
+ data[:__mod_id] = response[:modId] if response[:modId]
67
+ data[:__record_id] = response[:recordId].to_i if response[:recordId]
68
68
 
69
69
  build_base_hash(json, true).merge!(data: data)
70
70
  end
@@ -188,7 +188,7 @@ module FmRest
188
188
  # @param json_data [Hash]
189
189
  # @return [Hash] the record data in Spyke format
190
190
  def prepare_record_data(json_data)
191
- out = { id: json_data[:recordId].to_i, mod_id: json_data[:modId] }
191
+ out = { __record_id: json_data[:recordId].to_i, __mod_id: json_data[:modId] }
192
192
  out.merge!(json_data[:fieldData])
193
193
  out.merge!(prepare_portal_data(json_data[:portalData])) if json_data[:portalData]
194
194
  out
@@ -213,8 +213,8 @@ module FmRest
213
213
 
214
214
  out[portal_name] =
215
215
  portal_records.map do |portal_fields|
216
- attributes = { id: portal_fields[:recordId].to_i }
217
- attributes[:mod_id] = portal_fields[:modId] if portal_fields[:modId]
216
+ attributes = { __record_id: portal_fields[:recordId].to_i }
217
+ attributes[:__mod_id] = portal_fields[:modId] if portal_fields[:modId]
218
218
 
219
219
  prefix = portal_options[:attribute_prefix] || portal_name
220
220
  prefix_matcher = /\A#{prefix}::/
@@ -79,17 +79,24 @@ module FmRest
79
79
  class InvalidDate < ArgumentError; end
80
80
 
81
81
  class << self
82
- alias_method :strptime, :new
82
+ def strptime(str, date_format, *_)
83
+ begin
84
+ date = self::DELEGATE_CLASS.strptime(str, date_format)
85
+ rescue ArgumentError
86
+ raise InvalidDate
87
+ end
88
+
89
+ new(str, date)
90
+ end
83
91
  end
84
92
 
85
- def initialize(str, date_format, **str_args)
93
+ def initialize(str, date, **str_args)
94
+ raise ArgumentError, "str must be of class String" unless str.is_a?(String)
95
+ raise ArgumentError, "date must be of class #{self.class::DELEGATE_CLASS.name}" unless date.is_a?(self.class::DELEGATE_CLASS)
96
+
86
97
  super(str, **str_args)
87
98
 
88
- begin
89
- @delegate = self.class::DELEGATE_CLASS.strptime(str, date_format)
90
- rescue ArgumentError
91
- raise InvalidDate
92
- end
99
+ @delegate = date
93
100
 
94
101
  freeze
95
102
  end
@@ -178,4 +185,36 @@ module FmRest
178
185
  @delegate
179
186
  end
180
187
  end
188
+
189
+ module StringDateAwareness
190
+ def _parse(v, *_)
191
+ if v.is_a?(StringDateTime)
192
+ return { year: v.year, mon: v.month, mday: v.mday, hour: v.hour, min: v.min, sec: v.sec, sec_fraction: v.sec_fraction, offset: v.offset }
193
+ end
194
+ if v.is_a?(StringDate)
195
+ return { year: v.year, mon: v.month, mday: v.mday }
196
+ end
197
+ super
198
+ end
199
+
200
+ def parse(v, *_)
201
+ if v.is_a?(StringDate)
202
+ return self == ::DateTime ? v.to_datetime : v.to_date
203
+ end
204
+ super
205
+ end
206
+
207
+ # Overriding case equality method so that it returns true for
208
+ # `FmRest::StringDate` instances
209
+ #
210
+ # Calls superclass method
211
+ #
212
+ def ===(other)
213
+ super || other.is_a?(StringDate)
214
+ end
215
+
216
+ def self.enable(classes: [Date, DateTime])
217
+ classes.each { |klass| klass.singleton_class.prepend(self) }
218
+ end
219
+ end
181
220
  end
@@ -2,5 +2,11 @@
2
2
 
3
3
  module FmRest
4
4
  module TokenStore
5
+ autoload :Base, "fmrest/token_store/base"
6
+ autoload :Memory, "fmrest/token_store/memory"
7
+ autoload :Null, "fmrest/token_store/null"
8
+ autoload :ActiveRecord, "fmrest/token_store/active_record"
9
+ autoload :Moneta, "fmrest/token_store/moneta"
10
+ autoload :Redis, "fmrest/token_store/redis"
5
11
  end
6
12
  end
@@ -10,15 +10,15 @@ module FmRest
10
10
  end
11
11
 
12
12
  def load(key)
13
- raise "Not implemented"
13
+ raise NotImplementedError
14
14
  end
15
15
 
16
16
  def store(key, value)
17
- raise "Not implemented"
17
+ raise NotImplementedError
18
18
  end
19
19
 
20
20
  def delete(key)
21
- raise "Not implemented"
21
+ raise NotImplementedError
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module FmRest
6
+ module TokenStore
7
+ module Null < Base
8
+ include Singleton
9
+
10
+ def delete(key)
11
+ end
12
+
13
+ def load(key)
14
+ end
15
+
16
+ def store(key, value)
17
+ end
18
+ end
19
+ end
20
+ end