fmrest 0.9.0 → 0.12.0

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