fulfil_api 0.7.2 → 0.8.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.
@@ -48,8 +48,8 @@ module FulfilApi
48
48
  #
49
49
  # @yield [resource] Yields each resource object to the given block.
50
50
  # @return [Enumerator, self] Returns an Enumerator if no block is given; otherwise, returns self.
51
- def each(&block)
52
- all.each(&block)
51
+ def each(&)
52
+ all.each(&)
53
53
  end
54
54
 
55
55
  # Resets any of the previously provided query conditions.
@@ -76,23 +76,23 @@ module FulfilApi
76
76
  # $ build_attribute("warehouse.id", 10)
77
77
  # => { "warehouse" => { "id" => 10 } }
78
78
  #
79
- # @param attribute_names [String, Symbol] The expanded list of attribute names
79
+ # @example attribute where the relation and its field share a name
80
+ # $ build_attribute("tracking_number.tracking_number", "T04DDA5014242536")
81
+ # => { "tracking_number" => { "tracking_number" => "T04DDA5014242536" } }
82
+ #
83
+ # @param name [String, Symbol] The attribute name
80
84
  # @param value [Any] The attribute value
81
85
  # @return [Hash] The newly build attribute
82
- def build_attribute(name, value) # rubocop:disable Metrics/MethodLength
83
- attribute_names = name.to_s.split(".")
84
- attribute = {}
85
- attribute_level = attribute
86
+ def build_attribute(name, value)
87
+ *relation_names, attribute_name = name.to_s.split(".")
88
+ return {} if attribute_name.nil?
86
89
 
87
- attribute_names.each do |attribute_name|
88
- if attribute_name == attribute_names.last
89
- attribute_level[attribute_name] = type_cast_attribute_value(value)
90
- else
91
- attribute_level[attribute_name] ||= {}
92
- attribute_level = attribute_level[attribute_name]
93
- end
90
+ attribute = {}
91
+ attribute_level = relation_names.reduce(attribute) do |level, relation_name|
92
+ level[relation_name] ||= {}
94
93
  end
95
94
 
95
+ attribute_level[attribute_name] = type_cast_attribute_value(value)
96
96
  attribute
97
97
  end
98
98
 
@@ -128,6 +128,9 @@ module FulfilApi
128
128
  configure_persistent_connection(http)
129
129
  end
130
130
 
131
+ # Retries and other safeguards wrap every request, so they're added first
132
+ FulfilApi::Middleware.apply(connection, configuration)
133
+
131
134
  # Configuration of the request middleware
132
135
  connection.request :json
133
136
 
@@ -151,11 +154,9 @@ module FulfilApi
151
154
  # Tunes the underlying Net::HTTP::Persistent connection.
152
155
  #
153
156
  # The `net_http_persistent` adapter forces `max_retries` to 0 on every
154
- # request, which disables Ruby's built-in retry for idempotent requests.
155
- # Restoring it lets a stale keep-alive socket — one the server has already
156
- # closed — be retried transparently on a fresh socket instead of surfacing
157
- # as a read timeout. The config block runs after the adapter zeroes the
158
- # value, so this takes effect.
157
+ # request. Retries are handled by {FulfilApi::Middleware::Retry} instead, so
158
+ # `max_retries` is only restored when it's configured explicitly. The config
159
+ # block runs after the adapter zeroes the value, so this takes effect.
159
160
  #
160
161
  # @param http [Net::HTTP::Persistent] The live persistent connection.
161
162
  # @return [void]
@@ -170,7 +171,10 @@ module FulfilApi
170
171
 
171
172
  # @return [Array] The cache key identifying a unique connection.
172
173
  def connection_cache_key
173
- [merchant_id, api_version, configuration.request_options, configuration.connection_options]
174
+ [
175
+ merchant_id, api_version, configuration.request_options, configuration.connection_options,
176
+ configuration.retry_options, configuration.circuit_breaker
177
+ ]
174
178
  end
175
179
 
176
180
  # @param relative_path [String] The relative path to the API endpoint.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FulfilApi
4
- VERSION = "0.7.2"
4
+ VERSION = "0.8.0"
5
5
  end
data/lib/fulfil_api.rb CHANGED
@@ -3,18 +3,29 @@
3
3
  require "zeitwerk"
4
4
 
5
5
  loader = Zeitwerk::Loader.for_gem
6
+ loader.inflector.inflect("oauth" => "OAuth")
7
+ loader.ignore("#{__dir__}/fulfil_api/engine.rb")
6
8
  loader.ignore("#{__dir__}/fulfil_api/test_helper.rb")
9
+ loader.ignore("#{__dir__}/generators")
7
10
  loader.setup
8
11
 
9
12
  require "active_support"
13
+ require "active_support/cache"
10
14
  require "active_support/core_ext/enumerable"
11
15
  require "active_support/core_ext/hash/deep_merge"
12
16
  require "active_support/core_ext/hash/indifferent_access"
13
17
  require "active_support/core_ext/module/delegation"
14
18
  require "active_support/core_ext/object/blank"
15
19
  require "active_support/notifications"
20
+ require "faraday"
21
+ require "faraday/retry"
16
22
 
17
23
  module FulfilApi
18
24
  end
19
25
 
20
26
  loader.eager_load
27
+
28
+ # The Rails-specific parts of the gem — the engine, its models, its controllers,
29
+ # and its generators — are only loaded when the gem is used from within a
30
+ # Rails application.
31
+ require "fulfil_api/engine" if defined?(Rails::Engine)
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module FulfilApi
7
+ module Generators
8
+ # Sets a Rails application up for Fulfil's OAuth flow: it writes an
9
+ # initializer, adds the migration for {FulfilApi::Installation}, and mounts
10
+ # {FulfilApi::Engine}.
11
+ #
12
+ # @example
13
+ # bin/rails generate fulfil_api:install
14
+ class InstallGenerator < Rails::Generators::Base
15
+ include ActiveRecord::Generators::Migration
16
+
17
+ source_root File.expand_path("templates", __dir__)
18
+
19
+ desc "Installs the Fulfil OAuth initializer, migration, and engine route."
20
+
21
+ # @return [void]
22
+ def copy_initializer
23
+ template "fulfil_api.rb", "config/initializers/fulfil_api.rb"
24
+ end
25
+
26
+ # @return [void]
27
+ def create_migration_file
28
+ migration_template "create_fulfil_api_installations.rb",
29
+ "db/migrate/create_fulfil_api_installations.rb"
30
+ end
31
+
32
+ # @return [void]
33
+ def mount_engine
34
+ route %(mount FulfilApi::Engine => "#{FulfilApi::OAuth::Configuration::DEFAULT_MOUNT_PATH}")
35
+ end
36
+
37
+ # @return [void]
38
+ def show_readme
39
+ say(remaining_steps)
40
+ end
41
+
42
+ private
43
+
44
+ # @return [String] The Active Record migration version to generate against.
45
+ def migration_version
46
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
47
+ end
48
+
49
+ # Nothing changes in the application until these are done, so an existing
50
+ # application can install the gem without switching to OAuth yet.
51
+ #
52
+ # @return [String]
53
+ def remaining_steps
54
+ <<~MESSAGE
55
+
56
+ Fulfil's OAuth flow is installed. To finish setting it up:
57
+
58
+ 1. Run `bin/rails db:migrate` to create the fulfil_api_installations table.
59
+ 2. Run `bin/rails db:encryption:init` and store the keys in your credentials,
60
+ unless Active Record encryption is already configured. The access tokens
61
+ are encrypted at rest.
62
+ 3. Create an app on https://auth.fulfil.io/user/clients and whitelist
63
+ #{FulfilApi::OAuth::Configuration::DEFAULT_MOUNT_PATH}/callback as a redirection URL.
64
+ 4. Fill in FULFIL_OAUTH_CLIENT_ID and FULFIL_OAUTH_CLIENT_SECRET.
65
+ 5. Set config.oauth.parent_controller to the controller that authenticates
66
+ a request in this application. The OAuth flow refuses to run without it.
67
+
68
+ MESSAGE
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateFulfilApiInstallations < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :fulfil_api_installations do |t|
6
+ # An installation without an owner is the application-wide installation.
7
+ t.references :owner, polymorphic: true, null: true, index: false
8
+
9
+ t.string :merchant_id, null: false
10
+
11
+ # Fulfil grants the permanent token only in `offline_access` mode, and the
12
+ # short-lived one is the only token in `user_session` mode. Neither is
13
+ # required on its own; FulfilApi::Installation requires one of the two.
14
+ t.text :access_token
15
+ t.text :offline_access_token
16
+
17
+ # The gem fills the token type in rather than relying on Fulfil to send it.
18
+ t.string :token_type, null: false, default: "Bearer"
19
+
20
+ # Nullable because Active Record serialization writes an empty list as
21
+ # NULL and reads it back as one, so the column is never meaningfully
22
+ # blank even though it can be NULL.
23
+ t.text :scopes
24
+
25
+ # Describes the short-lived token only. A permanent token has no expiry:
26
+ # it is revoked by uninstalling the app, not by time.
27
+ t.datetime :expires_at
28
+
29
+ # Who walked through the authorization flow. Recorded for support, nothing
30
+ # in the gem depends on it, and Fulfil is free to leave parts of it out.
31
+ t.integer :associated_user_id
32
+ t.string :associated_user_email
33
+ t.string :associated_user_name
34
+
35
+ t.timestamps
36
+ end
37
+
38
+ # An owner is connected to a single Fulfil workspace. Databases treat NULLs
39
+ # as distinct, so this index does not constrain the application-wide
40
+ # installation; FulfilApi::Installation validates that case.
41
+ add_index :fulfil_api_installations, %i[owner_type owner_id], unique: true
42
+ end
43
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ FulfilApi.configure do |config|
4
+ # The subdomain of the Fulfil workspace this application talks to. Leave it
5
+ # blank when every tenant brings its own workspace, and override
6
+ # `fulfil_merchant_id` in ApplicationController instead.
7
+ config.merchant_id = ENV.fetch("FULFIL_MERCHANT_ID", nil)
8
+
9
+ # The credentials of the app registered on https://auth.fulfil.io/user/clients.
10
+ config.oauth.client_id = ENV.fetch("FULFIL_OAUTH_CLIENT_ID", nil)
11
+ config.oauth.client_secret = ENV.fetch("FULFIL_OAUTH_CLIENT_SECRET", nil)
12
+
13
+ # The scopes to request. See https://developers.fulfil.io for the full list.
14
+ config.oauth.scopes = []
15
+
16
+ # `:offline_access` grants a permanent token that keeps working when no user
17
+ # is around, which is what background jobs need. `:user_session` grants a
18
+ # token that expires with the user's Fulfil session.
19
+ config.oauth.access_type = :offline_access
20
+
21
+ # Where the user ends up after installing the app, unless they were sent to
22
+ # the flow from somewhere else. Accepts a path or a callable taking the
23
+ # FulfilApi::Installation that was created.
24
+ config.oauth.after_install_path = "/"
25
+
26
+ # REQUIRED before the OAuth flow can be used. The engine's controllers inherit
27
+ # from this one, so it has to be a controller that authenticates the request
28
+ # and — in an application serving many merchants — establishes the current
29
+ # tenant. There is deliberately no default: an unauthenticated flow lets a
30
+ # stranger record their own Fulfil workspace over yours, and a flow outside
31
+ # the application's tenancy records installations against the wrong owner.
32
+ #
33
+ # config.oauth.parent_controller = "AuthenticatedController"
34
+
35
+ # Fulfil requires the redirect URL to be whitelisted on the app. It defaults
36
+ # to the engine's callback URL, which is derived from the incoming request.
37
+ # config.oauth.redirect_uri = "https://example.com/fulfil/callback"
38
+ end
metadata CHANGED
@@ -1,29 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fulfil_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Vermaas
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-08-18 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - ">"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '7'
18
+ version: '8.0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - ">"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '7'
25
+ version: '8.0'
27
26
  - !ruby/object:Gem::Dependency
28
27
  name: faraday
29
28
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +51,20 @@ dependencies:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
53
  version: '2.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: faraday-retry
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.2'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.2'
55
68
  - !ruby/object:Gem::Dependency
56
69
  name: zeitwerk
57
70
  requirement: !ruby/object:Gem::Requirement
@@ -80,14 +93,29 @@ files:
80
93
  - LICENSE.txt
81
94
  - README.md
82
95
  - Rakefile
96
+ - app/controllers/concerns/fulfil_api/authenticated.rb
97
+ - app/controllers/concerns/fulfil_api/installation_scoped.rb
98
+ - app/controllers/fulfil_api/application_controller.rb
99
+ - app/controllers/fulfil_api/installations_controller.rb
100
+ - app/models/concerns/fulfil_api/installable.rb
101
+ - app/models/fulfil_api/installation.rb
102
+ - config/routes.rb
83
103
  - lib/fulfil_api.rb
84
104
  - lib/fulfil_api/access_token.rb
105
+ - lib/fulfil_api/circuit_open.rb
85
106
  - lib/fulfil_api/client.rb
86
107
  - lib/fulfil_api/configuration.rb
87
108
  - lib/fulfil_api/customer_shipment.rb
109
+ - lib/fulfil_api/engine.rb
88
110
  - lib/fulfil_api/error.rb
89
111
  - lib/fulfil_api/error/notifiable.rb
90
112
  - lib/fulfil_api/http_error.rb
113
+ - lib/fulfil_api/middleware.rb
114
+ - lib/fulfil_api/middleware/circuit_breaker.rb
115
+ - lib/fulfil_api/oauth.rb
116
+ - lib/fulfil_api/oauth/authorization.rb
117
+ - lib/fulfil_api/oauth/configuration.rb
118
+ - lib/fulfil_api/oauth/token.rb
91
119
  - lib/fulfil_api/relation.rb
92
120
  - lib/fulfil_api/relation/batchable.rb
93
121
  - lib/fulfil_api/relation/countable.rb
@@ -105,6 +133,9 @@ files:
105
133
  - lib/fulfil_api/test_helper.rb
106
134
  - lib/fulfil_api/tpl_client.rb
107
135
  - lib/fulfil_api/version.rb
136
+ - lib/generators/fulfil_api/install/install_generator.rb
137
+ - lib/generators/fulfil_api/install/templates/create_fulfil_api_installations.rb
138
+ - lib/generators/fulfil_api/install/templates/fulfil_api.rb
108
139
  - sig/fulfil_api.rbs
109
140
  homepage: https://www.github.com/codeturebv/fulfil_api
110
141
  licenses:
@@ -114,7 +145,13 @@ metadata:
114
145
  source_code_uri: https://www.github.com/codeturebv/fulfil_api
115
146
  changelog_uri: https://www.github.com/codeturebv/fulfil_api/blob/main/CHANGELOG.md
116
147
  rubygems_mfa_required: 'true'
117
- post_install_message:
148
+ post_install_message: |+
149
+ Using fulfil_api in a Rails application? Run the install generator to set up
150
+ Fulfil's OAuth flow, then run the migration it adds:
151
+
152
+ bin/rails generate fulfil_api:install
153
+ bin/rails db:migrate
154
+
118
155
  rdoc_options: []
119
156
  require_paths:
120
157
  - lib
@@ -122,15 +159,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
159
  requirements:
123
160
  - - ">="
124
161
  - !ruby/object:Gem::Version
125
- version: 3.0.0
162
+ version: 3.2.0
126
163
  required_rubygems_version: !ruby/object:Gem::Requirement
127
164
  requirements:
128
165
  - - ">="
129
166
  - !ruby/object:Gem::Version
130
167
  version: '0'
131
168
  requirements: []
132
- rubygems_version: 3.5.11
133
- signing_key:
169
+ rubygems_version: 4.0.20
134
170
  specification_version: 4
135
171
  summary: A HTTP client to interact the Fulfil.io API
136
172
  test_files: []
173
+ ...