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.
@@ -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