fulfil_api 0.7.2 → 0.8.1

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.
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ module OAuth
5
+ # Holds the credentials and options of the OAuth app that was registered in
6
+ # Fulfil's authentication dashboard, plus the Rails-specific settings used
7
+ # by {FulfilApi::Engine}.
8
+ #
9
+ # @see https://auth.fulfil.io/user/clients
10
+ class Configuration
11
+ ACCESS_TYPES = %i[offline_access user_session].freeze
12
+
13
+ # Fulfil's own default is `user_session`, but the gem defaults to
14
+ # `offline_access`. An app that talks to Fulfil from background jobs
15
+ # needs a token that outlives the user's web session, and personal
16
+ # access tokens are no longer a viable alternative now that they expire.
17
+ DEFAULT_ACCESS_TYPE = :offline_access
18
+ DEFAULT_AFTER_INSTALL_PATH = "/"
19
+ DEFAULT_MOUNT_PATH = "/fulfil"
20
+ DEFAULT_SCOPES = [].freeze
21
+
22
+ # What the engine's controllers inherit from until {#parent_controller} is
23
+ # set. It carries no authentication, which is why the flow refuses to run
24
+ # before an application has named a controller of its own.
25
+ UNCONFIGURED_PARENT_CONTROLLER = "ActionController::Base"
26
+
27
+ attr_accessor :after_install_path, :client_id, :client_secret, :parent_controller, :redirect_uri
28
+ attr_reader :access_type, :scopes
29
+
30
+ # @param options [Hash, nil] An optional list of configuration options.
31
+ def initialize(options = {})
32
+ (options || {}).each_pair do |key, value|
33
+ public_send(:"#{key}=", value) if respond_to?(:"#{key}=")
34
+ end
35
+
36
+ set_default_options
37
+ end
38
+
39
+ # @param value [Symbol, String] Either `:offline_access` or `:user_session`.
40
+ # @raise [ArgumentError] When the access type is not supported by Fulfil.
41
+ # @return [void]
42
+ def access_type=(value)
43
+ @access_type = value.to_sym
44
+
45
+ return if ACCESS_TYPES.include?(@access_type)
46
+
47
+ raise ArgumentError, "#{value} is not a valid access type. Use :offline_access or :user_session instead."
48
+ end
49
+
50
+ # @return [true, false] Whether an access token can be requested at all.
51
+ def configured?
52
+ client_id.present? && client_secret.present?
53
+ end
54
+
55
+ # @return [true, false] Whether a permanent, user-independent token is requested.
56
+ def offline_access?
57
+ access_type == :offline_access
58
+ end
59
+
60
+ # The class the engine's controllers inherit from.
61
+ #
62
+ # @return [Class]
63
+ def parent_controller_class
64
+ (parent_controller.presence || UNCONFIGURED_PARENT_CONTROLLER).constantize
65
+ end
66
+
67
+ # Whether the application named the controller the OAuth flow runs under.
68
+ #
69
+ # There is no sensible default: the flow has to run behind whatever the
70
+ # application uses to authenticate a request, and guessing wrong leaves
71
+ # an endpoint that lets a stranger install their own Fulfil workspace
72
+ # over the application's.
73
+ #
74
+ # @return [true, false]
75
+ def parent_controller_configured?
76
+ parent_controller.present?
77
+ end
78
+
79
+ # @param value [Array<String>, String, nil] A list of scopes, or a string
80
+ # of scopes separated by commas or spaces.
81
+ # @return [void]
82
+ def scopes=(value)
83
+ @scopes = Array(value).flat_map { |scope| scope.to_s.split(Token::SCOPE_SEPARATOR) }.reject(&:empty?).uniq
84
+ end
85
+
86
+ private
87
+
88
+ # @return [void]
89
+ def set_default_options
90
+ self.access_type = DEFAULT_ACCESS_TYPE if access_type.nil?
91
+ self.after_install_path = DEFAULT_AFTER_INSTALL_PATH if after_install_path.nil?
92
+ self.scopes = DEFAULT_SCOPES if scopes.nil?
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ module OAuth
5
+ # Wraps the payload returned by Fulfil's `POST /oauth/token` endpoint.
6
+ #
7
+ # @example An `offline_access` payload
8
+ # {
9
+ # "access_token" => "user-xxxxx-xxxxxxx",
10
+ # "associated_user" => { "email" => "first.last@example.com", "id" => 1, "name" => "First Last" },
11
+ # "expires_in" => 3600,
12
+ # "offline_access_token" => "bot-xxxx",
13
+ # "scope" => ["user_session,party.party"],
14
+ # "token_type" => "Bearer"
15
+ # }
16
+ class Token
17
+ DEFAULT_TOKEN_TYPE = "Bearer"
18
+ SCOPE_SEPARATOR = /[\s,]+/
19
+
20
+ attr_reader :access_token, :associated_user, :expires_in, :offline_access_token, :scopes, :token_type
21
+
22
+ # @param payload [Hash] The parsed response body of the token endpoint.
23
+ def initialize(payload)
24
+ payload = (payload || {}).with_indifferent_access
25
+
26
+ @access_token = payload[:access_token].presence
27
+ @associated_user = (payload[:associated_user] || {}).with_indifferent_access
28
+ @expires_in = payload[:expires_in]&.to_i
29
+ @offline_access_token = payload[:offline_access_token].presence
30
+ @scopes = extract_scopes(payload[:scope])
31
+ @token_type = payload[:token_type].presence || DEFAULT_TOKEN_TYPE
32
+ end
33
+
34
+ # The moment the short-lived {#access_token} stops working. The
35
+ # {#offline_access_token} is not covered by this: it stays valid until
36
+ # the app is uninstalled from the workspace.
37
+ #
38
+ # @return [Time, nil]
39
+ def expires_at
40
+ return if expires_in.nil?
41
+
42
+ Time.now.utc + expires_in
43
+ end
44
+
45
+ # @return [true, false] Whether a permanent token was granted.
46
+ def offline?
47
+ offline_access_token.present?
48
+ end
49
+
50
+ # Prefers the permanent token over the short-lived one, so a token used
51
+ # outside of the request it was granted in keeps working.
52
+ #
53
+ # @return [FulfilApi::AccessToken]
54
+ def to_access_token
55
+ FulfilApi::AccessToken.new(value, type: :oauth)
56
+ end
57
+
58
+ # @return [String, nil] The token to authenticate API requests with.
59
+ def value
60
+ offline_access_token || access_token
61
+ end
62
+
63
+ private
64
+
65
+ # Fulfil returns the granted scopes as an array holding a single string
66
+ # rather than as a list of scopes. Its docs show that string comma
67
+ # separated (`["user_session,party.party"]`), but a real workspace
68
+ # separates them with spaces (`["sale.order stock.move.view"]`), so both
69
+ # are flattened into a plain list.
70
+ #
71
+ # @param scope [Array<String>, String, nil] The granted scopes.
72
+ # @return [Array<String>]
73
+ def extract_scopes(scope)
74
+ Array(scope).flat_map { |value| value.to_s.split(SCOPE_SEPARATOR) }.reject(&:empty?).uniq
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ # The {FulfilApi::OAuth} namespace contains everything needed to obtain an
5
+ # access token through Fulfil's OAuth 2.0 authorization flow.
6
+ #
7
+ # Fulfil's flow has two access modes. The `user_session` mode returns a
8
+ # short-lived token tied to the user's web session, while the
9
+ # `offline_access` mode additionally returns an `offline_access_token` that
10
+ # remains valid until the app is uninstalled. There is no refresh token in
11
+ # either mode: an expired `user_session` token can only be replaced by
12
+ # sending the user through the flow again.
13
+ #
14
+ # @see https://docs.fulfil.io/developers/rest-api/authentication/oauth
15
+ module OAuth
16
+ class Error < FulfilApi::Error; end
17
+
18
+ # Raised when the flow is started without a client ID or secret.
19
+ class ConfigurationError < Error; end
20
+
21
+ # Raised when the merchant's workspace ID is missing or malformed. The
22
+ # workspace ID ends up in the host of the URL the user is redirected to,
23
+ # so an unchecked value would allow redirecting to an arbitrary host.
24
+ class MerchantIdInvalid < Error; end
25
+
26
+ # Raised when the user cancelled the installation, or when Fulfil reported
27
+ # an error on the callback.
28
+ class AuthorizationDenied < Error; end
29
+
30
+ # Raised when the `state` on the callback does not match the one generated
31
+ # when the flow was started, which means the callback cannot be trusted.
32
+ class StateMismatch < Error; end
33
+
34
+ # Raised when exchanging the authorization code for an access token failed.
35
+ class TokenExchangeFailed < Error; end
36
+ end
37
+ end
@@ -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.1"
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.1
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
+ ...