fulfil_api 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 124631d877876585ac53ed309947e4f11f20fc74a53736f8d2d642d2c169d0fc
4
+ data.tar.gz: fe5ee384f9fd99b5c7870128bda748236470bee352b352b789fb2d6a871d8b67
5
+ SHA512:
6
+ metadata.gz: 53ebe91f5b165c7e0d67bb16afbbaa0622ed2e2dd52bd3837952f903be9543f8170c2eb7190d219d26991955bbfde5196232ec78e46d22e7578c11b2c0f9e582
7
+ data.tar.gz: 52223100fa5671720036c9ee9b8cf9d5b7fc1ed62604528a08e8c94391aad7749796740db80700297cef935380bb6011382165f43d4303a76672b286ab0c60b7
data/.rubocop.yml ADDED
@@ -0,0 +1,35 @@
1
+ require:
2
+ - rubocop-minitest
3
+ - rubocop-performance
4
+ - rubocop-rake
5
+
6
+ AllCops:
7
+ Exclude:
8
+ - bin/*
9
+ - vendor/**/*
10
+ NewCops: enable
11
+ TargetRubyVersion: 3.0
12
+
13
+ Layout/LineLength:
14
+ Exclude:
15
+ - fulfil_api.gemspec
16
+
17
+ Metrics/ClassLength:
18
+ Exclude:
19
+ - test/**/*_test.rb
20
+
21
+ Metrics/MethodLength:
22
+ Exclude:
23
+ - test/**/*_test.rb
24
+
25
+ Minitest/MultipleAssertions:
26
+ Max: 5
27
+
28
+ Style/Documentation:
29
+ Enabled: false
30
+
31
+ Style/StringLiterals:
32
+ EnforcedStyle: double_quotes
33
+
34
+ Style/StringLiteralsInInterpolation:
35
+ EnforcedStyle: double_quotes
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.4
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-08-10
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at stefan@codeture.nl. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Stefan Vermaas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # The `fulfil_api` Ruby gem
2
+
3
+ The `fulfil_api` is a simple, powerful HTTP client written in Ruby to interact with Fulfil's API. It takes learnings from many years of working with Fulfil's APIs and turns it into an easy to use HTTP client.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ ```shell
10
+ $ bundle add fulfil_api
11
+ ```
12
+
13
+ If bundler is not being used to manage dependencies, install the gem by executing:
14
+
15
+ ```shell
16
+ $ gem install fulfil_api
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ### Configuration
22
+
23
+ There are two ways of configuring the HTTP client:
24
+
25
+ 1. Staticly through an initializer file (typically used in a Rails application)
26
+ 2. Dynamically through calling the `FulfilApi.with_config` method.
27
+
28
+ The configuration of the FulfilApi client is thread-safe and therefore you can even combine both the static and dynamic configuration of Fulfil in case you need to.
29
+
30
+ #### Using a Static Configuration
31
+
32
+ ```ruby
33
+ # config/initializers/fulfil_api.rb
34
+
35
+ FulfilApi.configure do |config|
36
+ config.access_token = FulfilApi::AccessToken.new(ENV["FULFIL_API_KEY"])
37
+ config.merchant_id = "the-id-of-the-merchant"
38
+ end
39
+ ```
40
+
41
+
42
+ #### Using a Dynamic Configuration
43
+
44
+ ```ruby
45
+ FulfilApi.with_config(
46
+ access_token: FulfilApi::AccessToken.new(ENV["FULFIL_API_KEY"]),
47
+ merchant_id: "the-id-of-the-merchant"
48
+ ) do
49
+ # Query the Fulfil API
50
+ end
51
+ ```
52
+
53
+ #### Available Configuration Options
54
+
55
+ The following configuration options are (currently) available throught both configuration methods:
56
+
57
+ - `access_token` (`FulfilApi::AccessToken`): The `access_token` is required to authenticate with Fulfil's API endpoints. Fulfil supports two types of access tokens: "OAuth" and "Personal" access tokens. The gem supports both tokens and defaults to the personal access token.
58
+
59
+ > **NOTE:** To use an OAuth access token, use `FulfilApi::AccessToken.new(oauth_token, type: :oauth)`. Typically, you would use the OAuth access token only when using the [dynamic configuration](#using-a-dynamic-configuration) mode of the gem.
60
+
61
+ - `merchant_id` (`String`): The `merchant_id` is the subdomain that the Fulfil instance is hosted on. This configuration option is required to be able to query Fulfil's API endpoints.
62
+
63
+ ### Querying the Fulfil API
64
+
65
+ > **NOTE:** Currently, the gem is under heavy development. The querying interface of the gem is really basic at the moment. In the future, we will closer match the querying interface of `ActiveRecord`.
66
+
67
+ The gem uses an `ActiveRecord` like query interface to query the Fulfil API.
68
+
69
+ ```ruby
70
+ # Find one specific resource
71
+ sales_order = FulfilApi::Resource.set(name: "sale.sale").find_by(["id", "=", 100])
72
+ p sales_order["id"] # => 100
73
+
74
+ # Find a list of resources
75
+ sales_orders = FulfilApi::Resource.set(name: "sale.sale").where(["channel", "=", 4])
76
+ p sales_orders.size # => 500 (standard number of resources returned by Fulfil)
77
+ p sales_orders.first["id"] # => 10 (an example of an ID returned by Fulfil)
78
+
79
+ # Find a limited list of resources
80
+ sales_orders = FulfilApi::Resource.set(name: "sale.sale").where(["channel", "=", 4]).limit(50)
81
+ p sales_orders.size # => 50
82
+
83
+ # Include more resource details than the ID only
84
+ sales_orders = FulfilApi::Resource.set(name: "sale.sale").select("reference").where(["channel", "=", 4])
85
+ p sales_orders.first["reference"] # => SO1234
86
+
87
+ # Fetch nested data from a relation
88
+ line_items = FulfilApi::Resource.set(name: "sale.line").select("sale.reference")
89
+ p line_items.first["sale"]["reference"] # => SO1234
90
+
91
+ # Query nested data from a relation
92
+ line_items = FulfilApi::Resource.set(name: "sale.line").where(["sale.reference", "=", "SO1234"])
93
+ p line_items.first["id"] # => 10
94
+ ```
95
+
96
+ > **NOTE:** It's important to note that the results from the Fulfil API are cached. This prevents you from accidentally overasking the Fulfil API. To reload the resources from the Fulfil API after you've already fetchted them, use the `.reload` on the returned relation (e.g. `line_items.reload`).
97
+
98
+ ### Interacting with the `FulfilApi::Resource`
99
+
100
+ Any data returned through the `FulfilApi` gem returns a list or a single `FulfilApi::Resource`. The data of the API resource is accessible through a `Hash`-like method.
101
+
102
+ ```ruby
103
+ sales_order = FulfilApi::Resource.set(name: "sale.sale").find_by(["id", "=", 100])
104
+ p sales_order["id"] # => 100
105
+ ```
106
+
107
+ When you're requesting relational data for an API resource, you can access it in a similar manner.
108
+
109
+ ```ruby
110
+ sales_order = FulfilApi::Resource.set(name: "sale.sale").select("channel.name").find_by(["id", "=", 100])
111
+ p sales_order["channel"]["name"] # => Shopify
112
+ ```
113
+
114
+ > **NOTE:** Fulfil is not able to return nested data from `Array`-like API resources. If you want to find all line items of a sales order, it's typically better to query the line item resource directly.
115
+
116
+ ```ruby
117
+ # You can't do this
118
+ FulfilApi::Resource.set(name: "sale.sale").select("lines.reference").find_by(["id", "=", 100])
119
+
120
+ # You can do this (BUT it's not recommended)
121
+ sales_order = FulfilApi::Resource.set(name: "sale.sale").select("lines").find_by(["id", "=", 100])
122
+ line_items = FulfilApi::Resource.set(name: "sale.line").where(["id", "in", sales_order["lines"]])
123
+
124
+ # You can do this (recommended)
125
+ line_items = FulfilApi::Resource.set(name: "sale.line").find_by(["sale.id", "=", 100])
126
+ ```
127
+
128
+ ## Development
129
+
130
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
131
+
132
+ To install this gem onto your local machine, run `bin/rake install`.
133
+
134
+ ## Releasing
135
+
136
+ To release a new version, run the `bin/release` script. This will update the version number in `version.rb`, create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
137
+
138
+ ## Contributing
139
+
140
+ Bug reports and pull requests are welcome on GitHub at https://github.com/codeturebv/fulfil_api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/codeturebv/fulfil_api/blob/main/CODE_OF_CONDUCT.md).
141
+
142
+ ## License
143
+
144
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
145
+
146
+ ## Code of Conduct
147
+
148
+ Everyone interacting in the Fulfil project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/codeturebv/fulfil_api/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ end
10
+
11
+ task default: %i[test]
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ # The {Fulfil::AccessToken} provides information about the type of access token
5
+ # that is provided to access the Fulfil API.
6
+ class AccessToken
7
+ attr_reader :value, :type
8
+
9
+ class TypeInvalid < Error; end
10
+
11
+ # @param value [String] The raw access token contents
12
+ # @param type [Symbol, String] The access token type (personal or oauth)
13
+ def initialize(value, type: :personal)
14
+ @type = type.to_sym
15
+ @value = value
16
+ end
17
+
18
+ # Builds the HTTP headers for the access token based on the {#type}.
19
+ #
20
+ # @return [Hash]
21
+ def to_http_header
22
+ case type
23
+ when :personal
24
+ { "X-API-KEY" => value }
25
+ when :oauth
26
+ { "Authorization" => "Bearer #{value}" }
27
+ else
28
+ raise TypeInvalid, "#{type} is not a valid access token type. Use :personal or :oauth instead."
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "faraday/net_http_persistent"
5
+
6
+ module FulfilApi
7
+ class Client
8
+ # @param configuration [FulfilApi::Configuration]
9
+ def initialize(configuration)
10
+ @configuration = configuration
11
+ end
12
+
13
+ # Performs an HTTP DELETE request to a Fulfil API endpoint.
14
+ #
15
+ # @param relative_path [String] The relative path to the API resource.
16
+ # @return [Array, Hash, String] The parsed response body.
17
+ def delete(relative_path)
18
+ request(:delete, relative_path)
19
+ end
20
+
21
+ # Performs an HTTP GET request to a Fulfil API endpoint.
22
+ #
23
+ # @param relative_path [String] The relative path to the API resource.
24
+ # @param url_parameters [Hash, nil] The optional URL parameters for the API endpoint.
25
+ # @return [Array, Hash, String] The parsed response body.
26
+ def get(relative_path, url_parameters: nil)
27
+ request(:get, relative_path, url_parameters)
28
+ end
29
+
30
+ # Performs an HTTP POST request to a Fulfil API endpoint.
31
+ #
32
+ # @param relative_path [String] The relative path to the API resource.
33
+ # @param body [Array, Hash, nil] The request body for the POST HTTP request.
34
+ # @return [Array, Hash, String] The parsed response body.
35
+ def post(relative_path, body: {})
36
+ request(:post, relative_path, body)
37
+ end
38
+
39
+ # Performs an HTTP PUT request to a Fulfil API endpoint.
40
+ #
41
+ # @param relative_path [String] The relative path to the API resource.
42
+ # @param body [Array, Hash, nil] The optional request body for the PUT HTTP request.
43
+ # @return [Array, Hash, String] The parsed response body.
44
+ def put(relative_path, body: nil)
45
+ return request(:put, relative_path) if body.nil?
46
+
47
+ request(:put, relative_path, body)
48
+ end
49
+
50
+ private
51
+
52
+ attr_reader :configuration
53
+
54
+ # @return [String] The absolute URL to the API base URL.
55
+ def api_endpoint
56
+ @api_endpoint ||= "https://#{configuration.merchant_id}.fulfil.io"
57
+ end
58
+
59
+ # @return [Faraday::Connection]
60
+ def connection
61
+ # TODO: Allow passing configuration options for the request
62
+ @connection ||= Faraday.new(headers: request_headers, url: api_endpoint) do |connection|
63
+ connection.adapter :net_http_persistent # TODO: Allow passing configuration options
64
+
65
+ # Configuration of the request middleware
66
+ connection.request :json
67
+
68
+ # Configuration of the response middleware
69
+ connection.response :json
70
+ connection.response :raise_error
71
+ end
72
+ end
73
+
74
+ # @param relative_path [String] The relative path to the API endpoint.
75
+ # @return [String] The absolute path for the request to the API endpoint.
76
+ def expand_relative_path(relative_path)
77
+ path = relative_path.start_with?("/") ? relative_path[1..] : relative_path
78
+ "/api/#{configuration.api_version}/#{path}"
79
+ end
80
+
81
+ # @param exception [Faraday::Error] Any error raised by Faraday during the execution
82
+ # of the HTTP request to the API endpoint.
83
+ def handle_request_error(exception)
84
+ raise FulfilApi::Error.new(
85
+ exception.message,
86
+ details: {
87
+ response_body: exception.response_body,
88
+ response_headers: exception.response_headers,
89
+ response_status: exception.response_status
90
+ }
91
+ )
92
+ end
93
+
94
+ # @param method [Symbol, String] The HTTP verb for the HTTP request.
95
+ # @param relative_path [String] The relative path to the API endpoint.
96
+ # @return [Array, Hash, String] The parsed response body.
97
+ def request(method, relative_path, *args, **kwargs)
98
+ connection.send(method.to_sym, expand_relative_path(relative_path), *args, **kwargs).body
99
+ rescue Faraday::Error => e
100
+ handle_request_error(e)
101
+ end
102
+
103
+ # @return [Hash] The HTTP headers for any HTTP request to Fulfil.
104
+ def request_headers
105
+ default_headers = { "Content-Type" => "application/json" }
106
+ return default_headers if configuration.access_token.nil?
107
+
108
+ default_headers.merge(**configuration.access_token.to_http_header)
109
+ end
110
+ end
111
+
112
+ # Builds an HTTP client to interact with an API endpoint of Fulfil.
113
+ #
114
+ # @example with a custom configuration
115
+ #
116
+ # To use a different configuration, wrap the call to the {.client} method into
117
+ # an {.with_config} block.
118
+ #
119
+ # FulfilApi.with_config(...) do
120
+ # FulfilApi.client.get(...)
121
+ # end
122
+ #
123
+ # @return [FulfilApi::Client]
124
+ def self.client
125
+ Client.new(FulfilApi.configuration)
126
+ end
127
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ # Configuration model for the Fulfil gem.
5
+ #
6
+ # This model holds configuration settings and provides thread-safe access
7
+ # to these settings.
8
+ class Configuration
9
+ attr_accessor :access_token, :api_version, :merchant_id
10
+
11
+ # Initializes the configuration with optional settings.
12
+ #
13
+ # @param options [Hash, nil] An optional list of configuration options.
14
+ # Each key in the hash should correspond to a configuration attribute.
15
+ def initialize(options = {})
16
+ @mutex = Mutex.new
17
+
18
+ # Assigns the optional configuration options
19
+ options.each_pair do |key, value|
20
+ send(:"#{key}=", value) if respond_to?(:"#{key}=")
21
+ end
22
+
23
+ # Sets the default options if not provided
24
+ set_default_options
25
+ end
26
+
27
+ # Provides thread-safe access to missing methods, allowing dynamic handling of configuration options.
28
+ #
29
+ # @param method [Symbol] The method name.
30
+ # @param args [Array] The arguments passed to the method.
31
+ # @param block [Proc] An optional block passed to the method.
32
+ # @return [void]
33
+ def method_missing(method, *args, &block)
34
+ @mutex.synchronize { super }
35
+ end
36
+
37
+ # Ensures that the object responds correctly to methods handled by method_missing.
38
+ #
39
+ # @param method [Symbol] The method name.
40
+ # @param include_private [Boolean] Whether to include private methods.
41
+ # @return [Boolean] Whether the object responds to the method.
42
+ def respond_to_missing?(method, include_private = false)
43
+ @mutex.synchronize { super }
44
+ end
45
+
46
+ private
47
+
48
+ # Sets the default options for the gem configuration.
49
+ #
50
+ # This method is called during initialization to ensure all configuration
51
+ # options have sensible defaults if not explicitly set.
52
+ #
53
+ # @return [void]
54
+ def set_default_options
55
+ self.api_version = "v2" if api_version.nil?
56
+ end
57
+ end
58
+
59
+ @configuration = Configuration.new
60
+ @configuration_mutex = Mutex.new
61
+
62
+ # Provides thread-safe access to the gem's configuration.
63
+ #
64
+ # @return [Fulfil::Configuration] The current configuration object.
65
+ def self.configuration
66
+ @configuration_mutex.synchronize { @configuration }
67
+ end
68
+
69
+ # Allows the configuration of the gem in a thread-safe manner.
70
+ #
71
+ # @yieldparam [Fulfil::Configuration] config The current configuration object.
72
+ # @return [void]
73
+ def self.configure
74
+ @configuration_mutex.synchronize { yield(@configuration) }
75
+ end
76
+
77
+ # Overwrites the configuration with the newly provided configuration options.
78
+ #
79
+ # @param options [Hash, Fulfil::Configuration] A list of configuration options for the gem.
80
+ # @return [Fulfil::Configuration] The updated configuration object.
81
+ def self.configuration=(options_or_configuration)
82
+ @configuration_mutex.synchronize do
83
+ if options_or_configuration.is_a?(Hash)
84
+ options_or_configuration.each_pair do |key, value|
85
+ @configuration.send(:"#{key}=", value) if @configuration.respond_to?(:"#{key}=")
86
+ end
87
+ elsif options_or_configuration.is_a?(Configuration)
88
+ @configuration = options_or_configuration
89
+ end
90
+ end
91
+ end
92
+
93
+ # Temporarily applies the provided configuration options within a block,
94
+ # and then reverts to the original configuration after the block executes.
95
+ #
96
+ # @param temporary_options [Hash] A hash of temporary configuration options.
97
+ # @yield Executes the block with the temporary configuration.
98
+ # @return [void]
99
+ def self.with_config(temporary_options)
100
+ original_configuration = configuration.dup
101
+ self.configuration = temporary_options
102
+
103
+ yield
104
+ ensure
105
+ # Revert to the original configuration
106
+ self.configuration = original_configuration
107
+ end
108
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ # The {FulfilApi::Error} is the base class for all FulfilApi errors, also used
5
+ # for generic or unexpected errors.
6
+ class Error < StandardError
7
+ attr_reader :details
8
+
9
+ # @param message [String] The displayable error message for the receiver.
10
+ # @param details [Hash] Any additional details exposed by the issuer of the exception.
11
+ def initialize(message, details: nil)
12
+ @details = details
13
+ super(message)
14
+ end
15
+
16
+ # @return [String]
17
+ def message
18
+ "[FulfilApi::Error] #{super}"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ class Resource
5
+ # The {FulfilApi::Resource::AttributeAssignable} module provides a set of helper
6
+ # methods to assign and cast attributes (including their values) to a {FulfilApi::Resource}.
7
+ module AttributeAssignable
8
+ # Assigns and casts a set of attributes for the {FulfilApi::Resource}
9
+ #
10
+ # @param attributes [Hash] The assignable attributes
11
+ # @return [Hash] The resource attributes
12
+ def assign_attributes(attributes)
13
+ attributes.each_pair do |key, value|
14
+ assign_attribute(key, value)
15
+ end
16
+
17
+ @attributes
18
+ end
19
+
20
+ # Assigns and casts a single attribute for the {FulfilApi::Resource}.
21
+ #
22
+ # @param name [String, Symbol] The attribute name
23
+ # @param value [Any] The attribute value
24
+ # @return [Hash] The resource attributes
25
+ def assign_attribute(name, value)
26
+ attribute = build_attribute(name, value)
27
+ attribute.deep_stringify_keys!
28
+
29
+ @attributes = @attributes.deep_merge(attribute)
30
+ end
31
+
32
+ private
33
+
34
+ # Builds the attribute value and clears the path to the attribute when the
35
+ # attribute name doesn't exist yet on the {FulfilApi::Resource}.
36
+ #
37
+ # @example attribute with a single attribute name
38
+ # $ build_attribute("warehouse", "Main Warehouse")
39
+ # => { "warehouse" => "Main Warehouse" }
40
+ #
41
+ # @example attribute with multiple/nested attribute names
42
+ # $ build_attribute("warehouse.id", 10)
43
+ # => { "warehouse" => { "id" => 10 } }
44
+ #
45
+ # @param attribute_names [String, Symbol] The expanded list of attribute names
46
+ # @param value [Any] The attribute value
47
+ # @return [Hash] The newly build attribute
48
+ def build_attribute(name, value) # rubocop:disable Metrics/MethodLength
49
+ attribute_names = name.to_s.split(".")
50
+ attribute = {}
51
+ attribute_level = attribute
52
+
53
+ attribute_names.each do |attribute_name|
54
+ if attribute_name == attribute_names.last
55
+ attribute_level[attribute_name] = type_cast_attribute_value(value)
56
+ else
57
+ attribute_level[attribute_name] ||= {}
58
+ attribute_level = attribute_level[attribute_name]
59
+ end
60
+ end
61
+
62
+ attribute
63
+ end
64
+
65
+ # @param value [Any] The raw attribute value before type casting
66
+ # @return [Any] The type casted attribute value
67
+ def type_cast_attribute_value(value)
68
+ case value
69
+ when Array
70
+ value.map { type_cast_attribute_value(_1) }
71
+ when Hash
72
+ AttributeType.cast(value)
73
+ else
74
+ value
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "bigdecimal"
5
+
6
+ module FulfilApi
7
+ class Resource
8
+ # The {FulfilApi::Resource::AttributeType} enables parsing any attribute value
9
+ # returned by the Fulfil API. To preserve type information Fulfil extends the JSON format.
10
+ # When the response value is extended, it is considered castable.
11
+ #
12
+ # @example an extended attribute value (date)
13
+ # $ AttributeType.cast({ "__class__" => "date", "iso_string" => "2024-12-12" })
14
+ # => #<Date: 2024-08-30 />
15
+ #
16
+ # For all possible special
17
+ class AttributeType
18
+ # Casts any attribute value to its final form.
19
+ #
20
+ # @param value [Any] The attribute value to cast
21
+ # @return [Any] The casted attribute value
22
+ def self.cast(value)
23
+ new(value).cast_value
24
+ end
25
+
26
+ # @param value [Any]
27
+ def initialize(value)
28
+ @type = extended?(value) ? value.fetch("__class__") : nil
29
+ @value = value
30
+ end
31
+
32
+ # Casts the attribute value to an useable format for a Ruby application.
33
+ #
34
+ # @return [Any]
35
+ def cast_value
36
+ case @type
37
+ when "bytes" then Base64.decode64(value_before_cast)
38
+ when "date" then Date.parse(value_before_cast)
39
+ when "datetime" then DateTime.parse(value_before_cast)
40
+ when "decimal" then BigDecimal(value_before_cast)
41
+ when "time" then Time.parse(value_before_cast)
42
+ when "timedelta" then value_before_cast
43
+ else
44
+ @value
45
+ end
46
+ end
47
+
48
+ # Retrieves the raw attribute value.
49
+ #
50
+ # @return [Any]
51
+ def value_before_cast
52
+ case @type
53
+ when "bytes" then @value["base64"]
54
+ when "date", "datetime", "time", "timedelta" then @value["iso_string"]
55
+ when "decimal" then @value["decimal"]
56
+ else
57
+ @value
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # The {#extended?} checks if the provided value is considered an extended
64
+ # attribute value.
65
+ #
66
+ # @param value [Any] The attribute value returned by Fulfil's API endpoint.
67
+ # @return [true, false]
68
+ def extended?(value)
69
+ value.is_a?(Hash) && value.key?("__class__")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ class Resource
5
+ class Relation
6
+ # The {FulfilApi::Resource::Relation::Loadable} extends the relation by
7
+ # adding methods to load, reload and identify loaded resources from Fulfil's
8
+ # API endpoints.
9
+ #
10
+ # By default, all HTTP requests to Fulfil are delayed until they're directly
11
+ # or indirectly requested by the user of the gem. This way, we ensure that
12
+ # we only request data when we need to.
13
+ module Loadable
14
+ # Loads resources from Fulfil's API based on the current filters, fields, and limits
15
+ # if they haven't been loaded yet.
16
+ #
17
+ # Requires that {#name} is set; raises an exception if it's not.
18
+ #
19
+ # @return [true, false] True if the resources were loaded successfully.
20
+ def load
21
+ return true if loaded?
22
+
23
+ if name.nil?
24
+ raise FulfilApi::Resource::Relation::ModelNameMissing, "The model name is missing. Use #set to define it."
25
+ end
26
+
27
+ response = FulfilApi.client.put(
28
+ "/model/#{name}/search_read",
29
+ body: { filters: conditions, fields: fields, limit: request_limit }.compact_blank
30
+ )
31
+
32
+ @resources = response.map { |resource| @resource_klass.new(resource) }
33
+ @loaded = true
34
+ end
35
+
36
+ # Checks whether the resources have been loaded to avoid repeated API calls when
37
+ # using enumerable methods.
38
+ #
39
+ # @return [true, false] True if the resources are already loaded.
40
+ def loaded?
41
+ @loaded
42
+ end
43
+
44
+ # Reloads the resources from Fulfil's API by resetting the {@loaded} flag.
45
+ #
46
+ # @return [true, false] True if the resources were successfully reloaded.
47
+ def reload
48
+ @loaded = false
49
+ load
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ class Resource
5
+ class Relation
6
+ # The {FulfilApi::Resource::Relation::Naming} extends the relation by
7
+ # adding methods to it that allow us to identify the type of resource that
8
+ # is being requested.
9
+ module Naming
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ # Custom error class for missing model name. The model name is required to be
14
+ # able to build the API endpoint to perform the search/read HTTP request.
15
+ class ModelNameMissing < Error; end # rubocop:disable Lint/ConstantDefinitionInBlock
16
+ end
17
+
18
+ # Sets the name of the resource model to be queried.
19
+ #
20
+ # @todo In the future, derive the {#name} from the @resource_klass automatically.
21
+ #
22
+ # @param name [String] The name of the resource model in Fulfil.
23
+ # @return [FulfilApi::Resource::Relation] A new {Relation} instance with the model name set.
24
+ def set(name:)
25
+ clone.tap do |relation|
26
+ relation.name = name
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ class Resource
5
+ class Relation
6
+ # The {FulfilApi::Resource::Relation::QueryMethods} extends the relation by
7
+ # adding query methods to it.
8
+ module QueryMethods
9
+ # Finds the first resource that matches the given conditions.
10
+ #
11
+ # It constructs a query using the `where` method, limits the result to one record,
12
+ # and then returns the first result.
13
+ #
14
+ # @note Unlike the other methods in this module, `#find_by` will immediately trigger an
15
+ # HTTP request to retrieve the resource, rather than allowing for lazy evaluation.
16
+ #
17
+ # @param conditions [Array<String, String, String>] The filter conditions as required by Fulfil.
18
+ # @return [FulfilApi::Resource, nil] The first resource that matches the conditions,
19
+ # or nil if no match is found.
20
+ def find_by(conditions)
21
+ where(conditions).limit(1).first
22
+ end
23
+
24
+ # Limits the number of resources returned by Fulfil's API. This is useful when only
25
+ # a specific number of resources are needed.
26
+ #
27
+ # @note If not specified, Fulfil's API defaults to returning up to 500 resources per call.
28
+ #
29
+ # @param value [Integer] The maximum number of resources to return.
30
+ # @return [FulfilApi::Resource::Relation] A new {Relation} instance with the limit applied.
31
+ def limit(value)
32
+ clone.tap do |relation|
33
+ relation.request_limit = value
34
+ end
35
+ end
36
+
37
+ # Specifies the fields to include in the response from Fulfil's API. By default, only
38
+ # the ID is returned.
39
+ #
40
+ # Supports dot notation for nested data fields, though not all nested data may be available
41
+ # depending on the API's limitations.
42
+ #
43
+ # @example Requesting nested data fields
44
+ # FulfilApi::Resource.set(name: "sale.line").select("sale.reference").find_by(["id", "=", 10])
45
+ #
46
+ # @example Requesting additional fields
47
+ # FulfilApi::Resource.set(name: "sale.sale").select(:reference).find_by(["id", "=", 10])
48
+ #
49
+ # @param fields [Array<Symbol, String>] The fields to include in the response.
50
+ # @return [FulfilApi::Resource::Relation] A new {Relation} instance with the selected fields.
51
+ def select(*fields)
52
+ clone.tap do |relation|
53
+ relation.fields.concat(fields.map(&:to_s))
54
+ relation.fields.uniq!
55
+ end
56
+ end
57
+
58
+ # Adds filter conditions for querying Fulfil's API. Conditions should be formatted
59
+ # as arrays according to the Fulfil API documentation.
60
+ #
61
+ # @example Simple querying with conditions
62
+ # FulfilApi::Resource.set(name: "sale.line").where(["sale.reference", "=", "ORDER-123"])
63
+ #
64
+ # @todo Enhance the {#where} method to allow more natural and flexible queries.
65
+ #
66
+ # @param conditions [Array<String, String, String>] The filter conditions as required by Fulfil.
67
+ # @return [FulfilApi::Resource::Relation] A new {Relation} instance with the conditions applied.
68
+ def where(conditions)
69
+ clone.tap do |relation|
70
+ relation.conditions << conditions.flatten
71
+ relation.conditions.uniq!
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ class Resource
5
+ # The {FulfilApi::Resource::Relation} class provides an abstraction for chaining multiple API operations.
6
+ #
7
+ # It allows handling a set of API resources in a uniform way, similar to
8
+ # ActiveRecord's query interface, enabling the user to build complex queries
9
+ # in a clean and reusable manner.
10
+ class Relation
11
+ include Enumerable
12
+ include Loadable
13
+ include Naming
14
+ include QueryMethods
15
+
16
+ attr_accessor :conditions, :fields, :name, :request_limit
17
+
18
+ delegate_missing_to :all
19
+
20
+ # @param resource_klass [FulfilApi::Resource] The resource data model class.
21
+ def initialize(resource_klass)
22
+ @resource_klass = resource_klass
23
+
24
+ @loaded = false
25
+ @resources = []
26
+
27
+ reset
28
+ end
29
+
30
+ # Loads and returns all resources from Fulfil's API. This method functions as a proxy,
31
+ # deferring the loading of resources until they are required, thus avoiding unnecessary
32
+ # HTTP requests.
33
+ #
34
+ # @return [Array<FulfilApi::Resource>] An array of loaded resource objects.
35
+ def all
36
+ load
37
+ @resources
38
+ end
39
+
40
+ # The {#each} method allows iteration over the resources. If no block is given,
41
+ # it returns an Enumerator, enabling lazy evaluation and allowing for chaining
42
+ # without immediately triggering an API request.
43
+ #
44
+ # @yield [resource] Yields each resource object to the given block.
45
+ # @return [Enumerator, self] Returns an Enumerator if no block is given; otherwise, returns self.
46
+ def each(&block)
47
+ all.each(&block)
48
+ end
49
+
50
+ # Resets any of the previously provided query conditions.
51
+ #
52
+ # @return [FulfilApi::Resource::Relation] The relation with cleared query conditions.
53
+ def reset
54
+ @conditions = []
55
+ @fields = %w[id]
56
+ @limit = nil
57
+
58
+ self
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ # The {FulfilApi::Resource} represents a single resource returned by the API
5
+ # endpoints of Fulfil.
6
+ class Resource
7
+ include AttributeAssignable
8
+
9
+ def initialize(attributes = {})
10
+ @attributes = {}.with_indifferent_access
11
+ assign_attributes(attributes)
12
+ end
13
+
14
+ class << self
15
+ delegate_missing_to :relation
16
+
17
+ # Builds a new {Fulfil::Resource::Relation} based on the current class to
18
+ # enable us to chain requests to Fulfil without querying their API endpoints
19
+ # multiple times in a row.
20
+ #
21
+ # @note it makes use of the {.delegate_missing_to} method from {ActiveSupport}
22
+ # to ensure that all unknown class methods for the {FulfilApi::Resource} are
23
+ # forwarded to the {FulfilApi::Resource.relation}.
24
+ #
25
+ # @example forwarding of the .where class method
26
+ # FulfilApi::Resource.set(name: "sale.sale").find_by(["id", "=", 100])
27
+ #
28
+ # @return [FulfilApi::Resource::Relation]
29
+ def relation
30
+ Relation.new(self)
31
+ end
32
+ end
33
+
34
+ # Looks up the value for the given attribute name.
35
+ #
36
+ # @param attribute_name [String, Symbol] The name of the attribute
37
+ # @return [Any, nil]
38
+ def [](attribute_name)
39
+ @attributes[attribute_name]
40
+ end
41
+
42
+ # Returns all currently assigned attributes for a {FulfilApi::Resource}.
43
+ #
44
+ # @return [Hash]
45
+ def to_h
46
+ @attributes
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webmock"
4
+
5
+ module FulfilApi
6
+ # The TestHelper module provides utility methods for stubbing HTTP requests
7
+ # to the Fulfil API in test environments. It uses WebMock to intercept and
8
+ # simulate API requests, allowing developers to test how their code interacts
9
+ # with the Fulfil API without making real HTTP requests.
10
+ #
11
+ # This module is designed to be included in test cases where you need to
12
+ # simulate API interactions. It offers a flexible interface to stub requests
13
+ # for various models and resources, making it easier to write comprehensive
14
+ # and isolated tests.
15
+ #
16
+ # @example Including the TestHelper in your test case
17
+ # class MyTest < Minitest::Test
18
+ # include FulfilApi::TestHelper
19
+ #
20
+ # def test_api_call
21
+ # stub_fulfil_request(:get, response: { name: "Product A" }, model: "product.product", id: "123")
22
+ # # Your test code here
23
+ # end
24
+ # end
25
+ module TestHelper
26
+ # Stubs an HTTP request to the Fulfil API based on the provided parameters.
27
+ #
28
+ # @param [String, Symbol] method The HTTP method to be stubbed (e.g., :get, :post).
29
+ # @param [Hash] response The response body to return as a JSON object (default is {}).
30
+ # @param [Integer] status The HTTP status code to return (default is 200).
31
+ # @param [Hash] options Additional options, such as the model and ID for the request URL.
32
+ # @option options [String] :model The API model (e.g., 'product.product', 'sale.sale').
33
+ # @option options [String] :id The ID of the resource within the model (optional).
34
+ #
35
+ # @return [WebMock::RequestStub] The WebMock request stub object.
36
+ #
37
+ # @example Stub a GET request for a product model
38
+ # stub_fulfil_request(:get, response: { name: "Product A" }, model: "product.product", id: "123")
39
+ def stub_fulfil_request(method, response: {}, status: 200, **options)
40
+ stubbed_request_for(method, **options)
41
+ .and_return(status: status, body: response.to_json, headers: { "Content-Type": "application/json" })
42
+ end
43
+
44
+ private
45
+
46
+ # Builds the WebMock request stub for the Fulfil API based on the provided method and options.
47
+ #
48
+ # @param [String, Symbol] method The HTTP method to be stubbed (e.g., :get, :post).
49
+ # @param [Hash] options Additional options, such as the model and ID for the request URL.
50
+ # @option options [String] :model The API model (e.g., 'product.product', 'sale.sale').
51
+ # @option options [String] :id The ID of the resource within the model (optional).
52
+ #
53
+ # @return [WebMock::RequestStub] The WebMock request stub object.
54
+ #
55
+ # @example Stub a POST request for creating a new order
56
+ # stubbed_request_for(:post, model: "sale.sale")
57
+ #
58
+ # @example Stub a GET request for a specific product
59
+ # stubbed_request_for(:get, model: "product.product", id: "123")
60
+ def stubbed_request_for(method, **options)
61
+ case options.transform_keys(&:to_sym)
62
+ in { model:, id: }
63
+ stub_request(method.to_sym, %r{fulfil.io/api/v\d+/(?:model/)?#{model}/#{id}(.*)}i)
64
+ in { model: }
65
+ stub_request(method.to_sym, %r{fulfil.io/api/v\d+/(?:model/)?#{model}(.*)}i)
66
+ else
67
+ stub_request(method.to_sym, %r{fulfil.io/api/v\d+}i)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ VERSION = "0.0.1"
5
+ end
data/lib/fulfil_api.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+
5
+ loader = Zeitwerk::Loader.for_gem
6
+ loader.setup
7
+
8
+ require "active_support"
9
+ require "active_support/core_ext/enumerable"
10
+ require "active_support/core_ext/hash/deep_merge"
11
+ require "active_support/core_ext/hash/indifferent_access"
12
+ require "active_support/core_ext/module/delegation"
13
+ require "active_support/core_ext/object/blank"
14
+
15
+ module FulfilApi
16
+ end
17
+
18
+ loader.eager_load
@@ -0,0 +1,4 @@
1
+ module FulfilApi
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fulfil_api
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Stefan Vermaas
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-09-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.10'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.10'
41
+ - !ruby/object:Gem::Dependency
42
+ name: faraday-net_http_persistent
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.6'
69
+ description: A Ruby HTTP client to interact with the API endpoints of Fulfil.io
70
+ email:
71
+ - stefan@codeture.nl
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".rubocop.yml"
77
+ - ".ruby-version"
78
+ - CHANGELOG.md
79
+ - CODE_OF_CONDUCT.md
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - lib/fulfil_api.rb
84
+ - lib/fulfil_api/access_token.rb
85
+ - lib/fulfil_api/client.rb
86
+ - lib/fulfil_api/configuration.rb
87
+ - lib/fulfil_api/error.rb
88
+ - lib/fulfil_api/resource.rb
89
+ - lib/fulfil_api/resource/attribute_assignable.rb
90
+ - lib/fulfil_api/resource/attribute_type.rb
91
+ - lib/fulfil_api/resource/relation.rb
92
+ - lib/fulfil_api/resource/relation/loadable.rb
93
+ - lib/fulfil_api/resource/relation/naming.rb
94
+ - lib/fulfil_api/resource/relation/query_methods.rb
95
+ - lib/fulfil_api/test_helper.rb
96
+ - lib/fulfil_api/version.rb
97
+ - sig/fulfil_api.rbs
98
+ homepage: https://www.github.com/codeturebv/fulfil_api
99
+ licenses:
100
+ - MIT
101
+ metadata:
102
+ homepage_uri: https://www.github.com/codeturebv/fulfil_api
103
+ source_code_uri: https://www.github.com/codeturebv/fulfil_api
104
+ changelog_uri: https://www.github.com/codeturebv/fulfil_api/blob/main/CHANGELOG.md
105
+ rubygems_mfa_required: 'true'
106
+ post_install_message:
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: 3.0.0
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 3.5.18
122
+ signing_key:
123
+ specification_version: 4
124
+ summary: A HTTP client to interact the Fulfil.io API
125
+ test_files: []