fmrest 0.9.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,7 +42,7 @@ module FmRest
42
42
  )
43
43
 
44
44
  # Update mod id on record
45
- @base.mod_id = response.body[:data][:mod_id]
45
+ @base.__mod_id = response.body[:data][:__mod_id]
46
46
 
47
47
  true
48
48
  end
@@ -52,7 +52,7 @@ module FmRest
52
52
  # @param repetition [Integer]
53
53
  # @return [String] the path for uploading a file to the container
54
54
  def upload_path(repetition)
55
- FmRest::V1.container_field_path(@base.class.layout, @base.id, name, repetition)
55
+ FmRest::V1.container_field_path(@base.class.layout, @base.__record_id, name, repetition)
56
56
  end
57
57
  end
58
58
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "fmrest/spyke/model/connection"
4
4
  require "fmrest/spyke/model/uri"
5
+ require "fmrest/spyke/model/record_id"
5
6
  require "fmrest/spyke/model/attributes"
6
7
  require "fmrest/spyke/model/serialization"
7
8
  require "fmrest/spyke/model/associations"
@@ -17,7 +18,8 @@ module FmRest
17
18
  extend ::ActiveSupport::Concern
18
19
 
19
20
  include Connection
20
- include Uri
21
+ include URI
22
+ include RecordID
21
23
  include Attributes
22
24
  include Serialization
23
25
  include Associations
@@ -26,11 +28,6 @@ module FmRest
26
28
  include GlobalFields
27
29
  include Http
28
30
  include Auth
29
-
30
- included do
31
- # @return [Integer] the record's modId
32
- attr_accessor :mod_id
33
- end
34
31
  end
35
32
  end
36
33
  end
@@ -5,6 +5,8 @@ require "fmrest/spyke/portal"
5
5
  module FmRest
6
6
  module Spyke
7
7
  module Model
8
+ # This module adds portal support to Spyke models.
9
+ #
8
10
  module Associations
9
11
  extend ::ActiveSupport::Concern
10
12
 
@@ -22,17 +24,18 @@ module FmRest
22
24
  end
23
25
 
24
26
  class_methods do
25
- # Based on +has_many+, but creates a special Portal association
27
+ # Based on `has_many`, but creates a special Portal association
26
28
  # instead.
27
29
  #
28
- # Custom options:
30
+ # @option :portal_key [String] The key used for the portal in the FM
31
+ # Data JSON portalData
32
+ # @option :attribute_prefix [String] The prefix used for portal
33
+ # attributes in the FM Data JSON
29
34
  #
30
- # * <tt>:portal_key</tt> - The key used for the portal in the FM Data JSON portalData.
31
- # * <tt>:attribute_prefix</tt> - The prefix used for portal attributes in the FM Data JSON.
32
- #
33
- # Example:
34
- #
35
- # has_portal :jobs, portal_key: "JobsTable", attribute_prefix: "Job"
35
+ # @example
36
+ # class Person < FmRest::Spyke::Base
37
+ # has_portal :jobs, portal_key: "JobsTable", attribute_prefix: "Job"
38
+ # end
36
39
  #
37
40
  def has_portal(name, options = {})
38
41
  create_association(name, Portal, options)
@@ -47,9 +50,8 @@ module FmRest
47
50
  end
48
51
  end
49
52
 
50
- # Override Spyke's association reader to keep a cache of loaded
51
- # portals. Spyke's default behavior is to reload the association
52
- # each time.
53
+ # Spyke override -- Keep a cache of loaded portals. Spyke's default
54
+ # behavior is to reload the association each time.
53
55
  #
54
56
  def association(name)
55
57
  @loaded_portals ||= {}
@@ -64,6 +66,8 @@ module FmRest
64
66
  end
65
67
  end
66
68
 
69
+ # Spyke override -- Add portals awareness
70
+ #
67
71
  def reload(*_)
68
72
  super.tap { @loaded_portals = nil }
69
73
  end
@@ -5,6 +5,10 @@ require "fmrest/spyke/model/orm"
5
5
  module FmRest
6
6
  module Spyke
7
7
  module Model
8
+ # Extends Spyke models with support for mapped attributes,
9
+ # `ActiveModel::Dirty` and forbidden attributes (e.g. Rails'
10
+ # `params.permit`).
11
+ #
8
12
  module Attributes
9
13
  extend ::ActiveSupport::Concern
10
14
 
@@ -20,9 +24,6 @@ module FmRest
20
24
  # by Spyke
21
25
  self.attribute_method_matchers.shift
22
26
 
23
- # ActiveModel::Dirty methods for id
24
- define_attribute_method(:id)
25
-
26
27
  # Keep track of attribute mappings so we can get the FM field names
27
28
  # for changed attributes
28
29
  class_attribute :mapped_attributes, instance_writer: false, instance_predicate: false
@@ -36,10 +37,12 @@ module FmRest
36
37
  end
37
38
 
38
39
  class_methods do
40
+ # Spyke override
41
+ #
39
42
  # Similar to Spyke::Base.attributes, but allows defining attribute
40
43
  # methods that map to FM attributes with different names.
41
44
  #
42
- # Example:
45
+ # @example
43
46
  #
44
47
  # class Person < Spyke::Base
45
48
  # include FmRest::Spyke::Model
@@ -61,9 +64,10 @@ module FmRest
61
64
 
62
65
  private
63
66
 
64
- # Override Spyke::Base.new_or_return (private), called whenever
65
- # loading records from the HTTP API, so we can reset dirty info on
66
- # freshly loaded records
67
+ # Spyke override (private)
68
+ #
69
+ # Called whenever loading records from the HTTP API, so we can reset
70
+ # dirty info on freshly loaded records
67
71
  #
68
72
  # See: https://github.com/balvig/spyke/blob/master/lib/spyke/http.rb
69
73
  #
@@ -88,8 +92,6 @@ module FmRest
88
92
  end
89
93
 
90
94
  def _fmrest_define_attribute(from, to)
91
- raise ArgumentError, "attribute name `id' is reserved for the recordId" if from.to_s == "id"
92
-
93
95
  # We use a setter here instead of injecting the hash key/value pair
94
96
  # directly with #[]= so that we don't change the mapped_attributes
95
97
  # hash on the parent class. The resulting hash is frozen for the
@@ -112,35 +114,25 @@ module FmRest
112
114
  end
113
115
  end
114
116
 
115
- def id=(value)
116
- id_will_change! unless value == id
117
- super
118
- end
119
-
117
+ # Spyke override -- Adds AM::Dirty support
118
+ #
120
119
  def reload(*args)
121
120
  super.tap { |r| clear_changes_information }
122
121
  end
123
122
 
123
+ # Spyke override -- Adds AM::Dirty support
124
+ #
124
125
  def save(*args)
125
126
  super.tap { |r| changes_applied_after_save if r }
126
127
  end
127
128
 
128
- # ActiveModel::Dirty since version 5.2 assumes that if there's an
129
- # @attributes instance variable set we must be using ActiveRecord, so
130
- # we override the instance variable name used by Spyke to avoid issues.
131
- #
132
- # TODO: Submit a pull request to Spyke so this isn't needed
133
- #
134
- def attributes
135
- @_spyke_attributes
136
- end
137
-
138
- # In addition to the comments above on `attributes`, this also adds
139
- # support for forbidden attributes
129
+ # Spyke override -- Adds support for forbidden attributes (i.e. Rails'
130
+ # `params.permit`, etc.)
140
131
  #
141
132
  def attributes=(new_attributes)
142
- @_spyke_attributes ||= ::Spyke::Attributes.new(scope.params)
143
- use_setters(sanitize_for_mass_assignment(new_attributes)) if new_attributes && !new_attributes.empty?
133
+ @spyke_attributes ||= ::Spyke::Attributes.new(scope.params)
134
+ return unless new_attributes && !new_attributes.empty?
135
+ use_setters(sanitize_for_mass_assignment(new_attributes))
144
136
  end
145
137
 
146
138
  private
@@ -153,7 +145,7 @@ module FmRest
153
145
  mapped_attributes.values_at(*changed)
154
146
  end
155
147
 
156
- # Use known mapped_attributes for inspect
148
+ # Spyke override (private) -- Use known mapped_attributes for inspect
157
149
  #
158
150
  def inspect_attributes
159
151
  mapped_attributes.except(primary_key).map do |k, v|
@@ -28,6 +28,14 @@ module FmRest
28
28
  rescue FmRest::V1::TokenSession::NoSessionTokenSet
29
29
  false
30
30
  end
31
+
32
+ def request_auth_token
33
+ FmRest::V1.request_auth_token(FmRest::V1.auth_connection(fmrest_config))
34
+ end
35
+
36
+ def request_auth_token!
37
+ FmRest::V1.request_auth_token!(FmRest::V1.auth_connection(fmrest_config))
38
+ end
31
39
  end
32
40
  end
33
41
  end
@@ -3,33 +3,112 @@
3
3
  module FmRest
4
4
  module Spyke
5
5
  module Model
6
+ # This module provides methods for configuring the Farday connection for
7
+ # the model, as well as setting up the connection itself.
8
+ #
6
9
  module Connection
7
- extend ::ActiveSupport::Concern
10
+ extend ActiveSupport::Concern
8
11
 
9
12
  included do
10
- class_attribute :fmrest_config, instance_accessor: false, instance_predicate: false
11
-
12
13
  class_attribute :faraday_block, instance_accessor: false, instance_predicate: false
13
14
  class << self; private :faraday_block, :faraday_block=; end
14
15
 
15
- # FM Data API expects PATCH for updates (Spyke's default was PUT)
16
+ # FM Data API expects PATCH for updates (Spyke uses PUT by default)
16
17
  self.callback_methods = { create: :post, update: :patch }.freeze
17
18
  end
18
19
 
19
20
  class_methods do
21
+ def fmrest_config
22
+ if fmrest_config_overlay
23
+ return FmRest.default_connection_settings.merge(fmrest_config_overlay, skip_validation: true)
24
+ end
25
+
26
+ FmRest.default_connection_settings
27
+ end
28
+
29
+ # Sets the FileMaker connection settings for the model.
30
+ #
31
+ # Behaves similar to ActiveSupport's `class_attribute`, so it can be
32
+ # inherited and safely overwritten in subclasses.
33
+ #
34
+ # @param settings [Hash] The settings hash
35
+ #
36
+ def fmrest_config=(settings)
37
+ settings = ConnectionSettings.new(settings, skip_validation: true)
38
+
39
+ singleton_class.redefine_method(:fmrest_config) do
40
+ overlay = fmrest_config_overlay
41
+ return settings.merge(overlay, skip_validation: true) if overlay
42
+ settings
43
+ end
44
+ end
45
+
46
+ # Allows overriding some connection settings in a thread-local
47
+ # manner. Useful in the use case where you want to connect to the
48
+ # same database using different accounts (e.g. credentials provided
49
+ # by users in a web app context).
50
+ #
51
+ # @param (see #fmrest_config=)
52
+ #
53
+ def fmrest_config_overlay=(settings)
54
+ Thread.current[fmrest_config_overlay_key] = settings
55
+ end
56
+
57
+ # @return [FmRest::ConnectionSettings] the connection settings
58
+ # overlay if any is in use
59
+ #
60
+ def fmrest_config_overlay
61
+ Thread.current[fmrest_config_overlay_key] || begin
62
+ superclass.fmrest_config_overlay
63
+ rescue NoMethodError
64
+ nil
65
+ end
66
+ end
67
+
68
+ # Clears the connection settings overlay.
69
+ #
70
+ def clear_fmrest_config_overlay
71
+ Thread.current[fmrest_config_overlay_key] = nil
72
+ end
73
+
74
+ # Runs a block of code in the context of the given connection
75
+ # settings without affecting the connection settings outside said
76
+ # block.
77
+ #
78
+ # @param (see #fmrest_config=)
79
+ #
80
+ # @example
81
+ # Honeybee.with_overlay(username: "...", password: "...") do
82
+ # Honeybee.query(...)
83
+ # end
84
+ #
85
+ def with_overlay(settings, &block)
86
+ Fiber.new do
87
+ begin
88
+ self.fmrest_config_overlay = settings
89
+ yield
90
+ ensure
91
+ self.clear_fmrest_config_overlay
92
+ end
93
+ end.resume
94
+ end
95
+
96
+ # Spyke override -- Defaults to `fmrest_connection`
97
+ #
20
98
  def connection
21
99
  super || fmrest_connection
22
100
  end
23
101
 
24
102
  # Sets a block for injecting custom middleware into the Faraday
25
- # connection. Example usage:
103
+ # connection.
26
104
  #
27
- # class MyModel < FmRest::Spyke::Base
28
- # faraday do |conn|
29
- # # Set up a custom logger for the model
30
- # conn.response :logger, MyApp.logger, bodies: true
31
- # end
105
+ # @example
106
+ # class MyModel < FmRest::Spyke::Base
107
+ # faraday do |conn|
108
+ # # Set up a custom logger for the model
109
+ # conn.response :logger, MyApp.logger, bodies: true
32
110
  # end
111
+ # end
33
112
  #
34
113
  def faraday(&block)
35
114
  self.faraday_block = block
@@ -38,26 +117,45 @@ module FmRest
38
117
  private
39
118
 
40
119
  def fmrest_connection
41
- @fmrest_connection ||=
42
- begin
43
- config = fmrest_config || FmRest.default_connection_settings
120
+ memoize = false
121
+
122
+ # Don't memoize the connection if there's an overlay, since
123
+ # overlays are thread-local and so should be the connection
124
+ unless fmrest_config_overlay
125
+ return @fmrest_connection if @fmrest_connection
126
+ memoize = true
127
+ end
128
+
129
+ config = ConnectionSettings.wrap(fmrest_config)
44
130
 
45
- FmRest::V1.build_connection(config) do |conn|
46
- faraday_block.call(conn) if faraday_block
131
+ connection =
132
+ FmRest::V1.build_connection(config) do |conn|
133
+ faraday_block.call(conn) if faraday_block
47
134
 
48
- # Pass the class to SpykeFormatter's initializer so it can have
49
- # access to extra context defined in the model, e.g. a portal
50
- # where name of the portal and the attributes prefix don't match
51
- # and need to be specified as options to `portal`
52
- conn.use FmRest::Spyke::SpykeFormatter, self
135
+ # Pass the class to SpykeFormatter's initializer so it can have
136
+ # access to extra context defined in the model, e.g. a portal
137
+ # where name of the portal and the attributes prefix don't match
138
+ # and need to be specified as options to `portal`
139
+ conn.use FmRest::Spyke::SpykeFormatter, self
53
140
 
54
- conn.use FmRest::V1::TypeCoercer, config
141
+ conn.use FmRest::V1::TypeCoercer, config
55
142
 
56
- # FmRest::Spyke::JsonParse expects symbol keys
57
- conn.response :json, parser_options: { symbolize_names: true }
58
- end
143
+ # FmRest::Spyke::JsonParse expects symbol keys
144
+ conn.response :json, parser_options: { symbolize_names: true }
59
145
  end
146
+
147
+ @fmrest_connection = connection if memoize
148
+
149
+ connection
60
150
  end
151
+
152
+ def fmrest_config_overlay_key
153
+ :"#{object_id}.fmrest_config_overlay"
154
+ end
155
+ end
156
+
157
+ def fmrest_config
158
+ self.class.fmrest_config
61
159
  end
62
160
  end
63
161
  end
@@ -5,10 +5,25 @@ require "fmrest/spyke/container_field"
5
5
  module FmRest
6
6
  module Spyke
7
7
  module Model
8
+ # This module adds support for container fields.
9
+ #
8
10
  module ContainerFields
9
11
  extend ::ActiveSupport::Concern
10
12
 
11
13
  class_methods do
14
+ # Defines a container field on the model.
15
+ #
16
+ # @param name [Symbol] the name of the container field
17
+ #
18
+ # @option options [String] :field_name (nil) the name of the container
19
+ # field in the FileMaker layout (only needed if it doesn't match
20
+ # the name given)
21
+ #
22
+ # @example
23
+ # class Honeybee < FmRest::Spyke::Base
24
+ # container :photo, field_name: "Beehive Photo ID"
25
+ # end
26
+ #
12
27
  def container(name, options = {})
13
28
  field_name = options[:field_name] || name
14
29
 
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fmrest/spyke/relation"
4
-
5
3
  module FmRest
6
4
  module Spyke
7
5
  module Model
@@ -15,6 +13,8 @@ module FmRest
15
13
  # execution results after a save, etc.
16
14
 
17
15
 
16
+ # Spyke override -- Keeps metadata in thread-local class variable.
17
+ #
18
18
  def request(*args)
19
19
  super.tap do |r|
20
20
  Thread.current[last_request_metadata_key] = r.metadata
@@ -32,6 +32,46 @@ module FmRest
32
32
  end
33
33
  end
34
34
  end
35
+
36
+ # Spyke override -- Uses `__record_id` for building the record URI.
37
+ #
38
+ def uri
39
+ ::Spyke::Path.new(@uri_template, fmrest_uri_attributes) if @uri_template
40
+ end
41
+
42
+ private
43
+
44
+ # Spyke override (private) -- Use `__record_id` instead of `id`
45
+ #
46
+ def resolve_path_from_action(action)
47
+ case action
48
+ when Symbol then uri.join(action)
49
+ when String then ::Spyke::Path.new(action, fmrest_uri_attributes)
50
+ else uri
51
+ end
52
+ end
53
+
54
+ def fmrest_uri_attributes
55
+ if persisted?
56
+ { __record_id: __record_id }
57
+ else
58
+ # NOTE: it seems silly to be calling attributes.slice(:__record_id)
59
+ # when the record is supposed to not have a record_id set (since
60
+ # persisted? is false here), but it makes sense in the context of how
61
+ # Spyke works:
62
+ #
63
+ # When calling Model.find(id), Spyke will internally create a scope
64
+ # with .where(primary_key => id) and call .find_one on it. Then,
65
+ # somewhere down the line Spyke creates a new empty instance of the
66
+ # current model class to get its .uri property (the one we're
67
+ # partially building through this method and which contains these URI
68
+ # attributes). When initializing a record Spyke first forcefully
69
+ # assigns the .where()-set attributes from the current scope onto
70
+ # that instance's attributes hash, which then leads us right here,
71
+ # where we might have __record_id assigned as a scope attribute:
72
+ attributes.slice(:__record_id)
73
+ end
74
+ end
35
75
  end
36
76
  end
37
77
  end