fulfil_api 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2f9290d460010359822ca7683de870fea5a0e694fb49c3d4b7eb752377a47dc
4
- data.tar.gz: 11b8066943e9e9559e37fd5aa33dfff5d36aa8ad0af4110e2e5e34afc2c2da1a
3
+ metadata.gz: 2a26b32a0feab8bd7e697b27db519ac3945fefcd00159aa714742c40ac5abffe
4
+ data.tar.gz: e8e5c494d57360d1cacbbea096c6c4cac3d70d504d04f4478a5f5508db658a63
5
5
  SHA512:
6
- metadata.gz: ec1dea5391c0951596e70baab45118e143f43e0e98f254a773114a62db8635fdfb545ff8fe7bf5ffe37041f57aa14b69f54c72e93b60c5afae1bcdc1d92a3147
7
- data.tar.gz: 035f253caee934a63c349e1bb206d100347cc7537836e9a04f34e7baa15a6bd1c03fe5dfccb3b7218df02ae0ff133d075c216e77e689fbdfb2b0915a2b3ff638
6
+ metadata.gz: 1d4b7f5ca1a4bc4e8449cc3b235fd9f3b467e7977d76feaa52fa11dac18a6a34ba1ab6b4a8280183e798e45f4ddfda00e0e2d27534ca467110a3cdec203adc49
7
+ data.tar.gz: 63ce6d7793de383098af74f7ca3c81ef8730351d7483390f8256f2dcd5cecc5cb3385ff46e9046fe7ed51f62e66f183b3b4abbf7b11f92a8322abe1ef44eeec9
data/README.md CHANGED
@@ -68,28 +68,28 @@ The gem uses an `ActiveRecord` like query interface to query the Fulfil API.
68
68
 
69
69
  ```ruby
70
70
  # Find one specific resource
71
- sales_order = FulfilApi::Resource.set(name: "sale.sale").find_by(["id", "=", 100])
71
+ sales_order = FulfilApi::Resource.set(model_name: "sale.sale").find_by(["id", "=", 100])
72
72
  p sales_order["id"] # => 100
73
73
 
74
74
  # Find a list of resources
75
- sales_orders = FulfilApi::Resource.set(name: "sale.sale").where(["channel", "=", 4])
75
+ sales_orders = FulfilApi::Resource.set(model_name: "sale.sale").where(["channel", "=", 4])
76
76
  p sales_orders.size # => 500 (standard number of resources returned by Fulfil)
77
77
  p sales_orders.first["id"] # => 10 (an example of an ID returned by Fulfil)
78
78
 
79
79
  # Find a limited list of resources
80
- sales_orders = FulfilApi::Resource.set(name: "sale.sale").where(["channel", "=", 4]).limit(50)
80
+ sales_orders = FulfilApi::Resource.set(model_name: "sale.sale").where(["channel", "=", 4]).limit(50)
81
81
  p sales_orders.size # => 50
82
82
 
83
83
  # Include more resource details than the ID only
84
- sales_orders = FulfilApi::Resource.set(name: "sale.sale").select("reference").where(["channel", "=", 4])
84
+ sales_orders = FulfilApi::Resource.set(model_name: "sale.sale").select("reference").where(["channel", "=", 4])
85
85
  p sales_orders.first["reference"] # => SO1234
86
86
 
87
87
  # Fetch nested data from a relation
88
- line_items = FulfilApi::Resource.set(name: "sale.line").select("sale.reference")
88
+ line_items = FulfilApi::Resource.set(model_name: "sale.line").select("sale.reference")
89
89
  p line_items.first["sale"]["reference"] # => SO1234
90
90
 
91
91
  # Query nested data from a relation
92
- line_items = FulfilApi::Resource.set(name: "sale.line").where(["sale.reference", "=", "SO1234"])
92
+ line_items = FulfilApi::Resource.set(model_name: "sale.line").where(["sale.reference", "=", "SO1234"])
93
93
  p line_items.first["id"] # => 10
94
94
  ```
95
95
 
@@ -100,14 +100,14 @@ p line_items.first["id"] # => 10
100
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
101
 
102
102
  ```ruby
103
- sales_order = FulfilApi::Resource.set(name: "sale.sale").find_by(["id", "=", 100])
103
+ sales_order = FulfilApi::Resource.set(model_name: "sale.sale").find_by(["id", "=", 100])
104
104
  p sales_order["id"] # => 100
105
105
  ```
106
106
 
107
107
  When you're requesting relational data for an API resource, you can access it in a similar manner.
108
108
 
109
109
  ```ruby
110
- sales_order = FulfilApi::Resource.set(name: "sale.sale").select("channel.name").find_by(["id", "=", 100])
110
+ sales_order = FulfilApi::Resource.set(model_name: "sale.sale").select("channel.name").find_by(["id", "=", 100])
111
111
  p sales_order["channel"]["name"] # => Shopify
112
112
  ```
113
113
 
@@ -115,14 +115,14 @@ p sales_order["channel"]["name"] # => Shopify
115
115
 
116
116
  ```ruby
117
117
  # You can't do this
118
- FulfilApi::Resource.set(name: "sale.sale").select("lines.reference").find_by(["id", "=", 100])
118
+ FulfilApi::Resource.set(model_name: "sale.sale").select("lines.reference").find_by(["id", "=", 100])
119
119
 
120
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"]])
121
+ sales_order = FulfilApi::Resource.set(model_name: "sale.sale").select("lines").find_by(["id", "=", 100])
122
+ line_items = FulfilApi::Resource.set(model_name: "sale.line").where(["id", "in", sales_order["lines"]])
123
123
 
124
124
  # You can do this (recommended)
125
- line_items = FulfilApi::Resource.set(name: "sale.line").find_by(["sale.id", "=", 100])
125
+ line_items = FulfilApi::Resource.set(model_name: "sale.line").find_by(["sale.id", "=", 100])
126
126
  ```
127
127
 
128
128
  ## Development
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ # The {FulfilApi::CustomerShipment} represents a single StockShipmentOut resource returned
5
+ # by the API endpoints of Fulfil.
6
+ class CustomerShipment < Resource
7
+ MODEL_NAME = "stock.shipment.out"
8
+
9
+ class << self
10
+ # Sets the fulfillment status of the customer shipment on hold
11
+ #
12
+ # @param id_or_ids [String, Integer, Array[String], Array[Integer]] The ID(s) of the customer shipment(s) to hold.
13
+ # @param note [String] A note to define the reason for holding. (Optional)
14
+ # @param hold_reason [String] An hold reason ID. (Optional)
15
+ # @return [Boolean] Returns true if hold successfully.
16
+ # @raise [FulfilApi::Error] If an error occurs during holding the customer shipment.
17
+ #
18
+ # @example Hold a customer shipment
19
+ # FulfilApi::CustomerShipment.hold(123, note: "Double booking", hold_reason: hold_reason_id)
20
+ #
21
+ # @example Hold multipe customer shipments
22
+ # FulfilApi::CustomerShipment.hold([123, 456], note: "Double booking", hold_reason: hold_reason_id)
23
+ def hold!(id_or_ids, note: nil, hold_reason: nil)
24
+ FulfilApi.client.put("/model/#{MODEL_NAME}/hold",
25
+ body: [[*id_or_ids].flatten, { note: note, hold_reason: hold_reason }.compact_blank])
26
+
27
+ true
28
+ end
29
+
30
+ # Unholds the fulfillment status of the customer shipment
31
+ #
32
+ # @param id_or_ids [String, Integer, Array[String], Array[Integer]]
33
+ # The ID(s) of the customer shipment(s) to unhold.
34
+ # @param note [String] A note to define the reason for unholding.
35
+ # @return [Boolean] Returns true if hold successfully.
36
+ # @raise [FulfilApi::Error] If an error occurs during holding the customer shipment.
37
+ #
38
+ # @example Unhold a customer shipment
39
+ # FulfilApi::CustomerShipment.unhold(123, note: "All clear")
40
+ #
41
+ # @example Unhold a customer shipment
42
+ # FulfilApi::CustomerShipment.unhold([123, 456], note: "All clear")
43
+ def unhold!(id_or_ids, note: nil)
44
+ FulfilApi.client.put("/model/#{MODEL_NAME}/unhold", body: [[*id_or_ids].flatten, { note: note }.compact_blank])
45
+
46
+ true
47
+ end
48
+ end
49
+
50
+ # Sets the current customer shipment on hold, rescuing any errors that occur and handling them based on error type.
51
+ #
52
+ # @param note [String] A note to define the reason for holding. (Optional)
53
+ # @param hold_reason [String] An hold reason ID. (Optional)
54
+ # @return [Boolean] Returns true if hold successfully, otherwise false.
55
+ #
56
+ # @example Holds a customer_shipment
57
+ # customer_shipment.hold(note: "Double booking", hold_reason: hold_reason_id)
58
+ def hold(note: nil, hold_reason: nil)
59
+ self.class.hold!(id, note: note, hold_reason: hold_reason)
60
+ rescue FulfilApi::Error => e
61
+ handle_error(e)
62
+ false
63
+ end
64
+
65
+ # Unholds the current customer shipment, rescuing any errors that occur and handling them based on error type.
66
+ #
67
+ # @param note [String] A note to define the reason for unholding.
68
+ # @return [Boolean] Returns true if unhold successfully, otherwise false.
69
+ #
70
+ # @example Unholds a customer_shipment
71
+ # customer_shipment.unhold(note: "Double booking")
72
+ def unhold(note: nil)
73
+ self.class.unhold!(id, note: note)
74
+ rescue FulfilApi::Error => e
75
+ handle_error(e)
76
+ false
77
+ end
78
+
79
+ private
80
+
81
+ def handle_error(err)
82
+ errors.add(code: err.details[:response_status], type: :system, message: err.details[:response_body])
83
+ end
84
+ end
85
+ end
@@ -25,7 +25,7 @@ module FulfilApi
25
25
 
26
26
  # @param value [Any]
27
27
  def initialize(value)
28
- @type = extended?(value) ? value.fetch("__class__") : nil
28
+ @type = extended?(value) ? value.fetch("__class__").downcase : nil
29
29
  @value = value
30
30
  end
31
31
 
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ class Resource
5
+ # The Errors class provides a structure to track and manage errors related to a API resource.
6
+ class Errors
7
+ include Enumerable
8
+
9
+ delegate_missing_to :@errors
10
+
11
+ # @param resource_klass [FulfilApi::Resource] The resource class that this Errors instance is associated with.
12
+ def initialize(resource_klass)
13
+ @errors = []
14
+ @resource_klass = resource_klass
15
+ end
16
+
17
+ # Adds a new error to the collection, unless the same error already exists.
18
+ #
19
+ # @param code [String, Symbol] The error code.
20
+ # @param message [String] A description of the error.
21
+ # @param type [String, Symbol] The type of the error (e.g. user, authorization).
22
+ # @return [Array<Hash>] The updated list of errors.
23
+ #
24
+ # @example Adding an error
25
+ # errors.add(code: "invalid_field", message: "Field is required", type: "validation")
26
+ def add(code:, message:, type:)
27
+ @errors << { code: code.to_s, type: type.to_sym, message: message } unless added?(code: code, type: type)
28
+ @errors
29
+ end
30
+
31
+ # Checks if an error with the specified code and type has already been added.
32
+ #
33
+ # @param code [String, Symbol] The error code to check.
34
+ # @param type [String, Symbol] The error type to check.
35
+ # @return [Boolean] True if the error has already been added, false otherwise.
36
+ #
37
+ # @example Checking if an error exists
38
+ # errors.added?(code: "invalid_field", type: "validation")
39
+ def added?(code:, type:)
40
+ @errors.any? do |error|
41
+ error[:code] == code.to_s && error[:type] == type.to_sym
42
+ end
43
+ end
44
+
45
+ # Clears all errors from the collection.
46
+ #
47
+ # @return [Array] The cleared list of errors
48
+ #
49
+ # @example Clearing all errors
50
+ # errors.clear
51
+ def clear
52
+ @errors = []
53
+ @errors
54
+ end
55
+
56
+ # Returns an array of the full error messages (just the message field).
57
+ #
58
+ # @return [Array<String>] The list of error messages.
59
+ #
60
+ # @example Retrieving full error messages
61
+ # errors.full_messages
62
+ def full_messages
63
+ @errors.pluck(:message)
64
+ end
65
+
66
+ # Returns the collection of error messages as an array of hashes.
67
+ #
68
+ # @return [Array<Hash>] The array of error hashes.
69
+ #
70
+ # @example Retrieving all error messages
71
+ # errors.messages
72
+ def messages
73
+ @errors
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FulfilApi
4
+ class Resource
5
+ # The Persistable module provides methods for saving and updating resources
6
+ # in the Fulfil API. It defines both instance and class methods for persisting
7
+ # changes to resources.
8
+ #
9
+ # This module handles common actions like saving and updating a resource,
10
+ # including error handling for different types of API errors.
11
+ module Persistable
12
+ extend ActiveSupport::Concern
13
+
14
+ class_methods do
15
+ # Updates a resource by its ID and model name.
16
+ #
17
+ # @param id [String, Integer] The ID of the resource to update.
18
+ # @param model_name [String] The name of the model to which the resource belongs.
19
+ # @param attributes [Hash] The attributes to update on the resource.
20
+ # @return [FulfilApi::Resource] The updated resource.
21
+ #
22
+ # @example Updating a resource
23
+ # FulfilApi::Resource.update(id: 123, model_name: "sale.sale", reference: "MK123")
24
+ def update(id:, model_name:, **attributes)
25
+ resource = new(id: id, model_name: model_name)
26
+ resource.update(attributes)
27
+ end
28
+
29
+ # Updates a resource by its ID and model name, raising an error if the update fails.
30
+ #
31
+ # @param id [String, Integer] The ID of the resource to update.
32
+ # @param model_name [String] The name of the model to which the resource belongs.
33
+ # @param attributes [Hash] The attributes to update on the resource.
34
+ # @return [FulfilApi::Resource] The updated resource.
35
+ # @raise [FulfilApi::Error] If the update fails.
36
+ #
37
+ # @example Updating a resource with error raising
38
+ # FulfilApi::Resource.update!(id: 123, model_name: "sale.sale", reference: "MK123")
39
+ def update!(id:, model_name:, **attributes)
40
+ resource = new(id: id, model_name: model_name)
41
+ resource.update!(attributes)
42
+ end
43
+ end
44
+
45
+ # Saves the current resource, rescuing any errors that occur and handling them based on error type.
46
+ #
47
+ # @return [FulfilApi::Resource, nil] Returns the resource if saved successfully, otherwise nil.
48
+ # @raise [FulfilApi::Error] If an error occurs during saving.
49
+ #
50
+ # @example Saving a resource
51
+ # resource.save
52
+ def save
53
+ save!
54
+ rescue FulfilApi::Error => e
55
+ case (error = JSON.parse(e.details[:response_body]).deep_symbolize_keys!)
56
+ in { type: "UserError" }
57
+ errors.add(code: error[:code], type: :user, message: error[:message])
58
+ in { code: Integer, name: String, description: String }
59
+ errors.add(code: error[:code], type: :authorization, message: error[:description])
60
+ end
61
+
62
+ self
63
+ end
64
+
65
+ # Saves the current resource, raising an error if it cannot be saved.
66
+ #
67
+ # @return [FulfilApi::Resource] The saved resource.
68
+ # @raise [FulfilApi::Error] If an error occurs during saving.
69
+ #
70
+ # @example Saving a resource with error raising
71
+ # resource.save!
72
+ def save!
73
+ errors.clear
74
+
75
+ if id.present?
76
+ FulfilApi.client.put("/model/#{model_name}/#{id}", body: to_h)
77
+ else # rubocop:disable Style/EmptyElse
78
+ # TODO: Implement the {#create} and {#create!} methods to save a new resource
79
+ end
80
+
81
+ self
82
+ end
83
+
84
+ # Updates the resource with the given attributes and saves it.
85
+ #
86
+ # @param attributes [Hash] The attributes to assign to the resource.
87
+ # @return [FulfilApi::Resource] The updated resource.
88
+ #
89
+ # @example Updating a resource
90
+ # resource.update(reference: "MK123")
91
+ def update(attributes)
92
+ assign_attributes(attributes)
93
+ save
94
+ end
95
+
96
+ # Updates the resource with the given attributes and saves it, raising an error if saving fails.
97
+ #
98
+ # @param attributes [Hash] The attributes to assign to the resource.
99
+ # @return [FulfilApi::Resource] The updated resource.
100
+ # @raise [FulfilApi::Error] If an error occurs during the update.
101
+ #
102
+ # @example Updating a resource with error raising
103
+ # resource.update!(reference: "MK123")
104
+ def update!(attributes)
105
+ assign_attributes(attributes)
106
+ save!
107
+ end
108
+ end
109
+ end
110
+ end
@@ -14,22 +14,25 @@ module FulfilApi
14
14
  # Loads resources from Fulfil's API based on the current filters, fields, and limits
15
15
  # if they haven't been loaded yet.
16
16
  #
17
- # Requires that {#name} is set; raises an exception if it's not.
17
+ # Requires that {#model_name} is set; raises an exception if it's not.
18
18
  #
19
19
  # @return [true, false] True if the resources were loaded successfully.
20
- def load
20
+ def load # rubocop:disable Metrics/MethodLength
21
21
  return true if loaded?
22
22
 
23
- if name.nil?
23
+ if model_name.nil?
24
24
  raise FulfilApi::Resource::Relation::ModelNameMissing, "The model name is missing. Use #set to define it."
25
25
  end
26
26
 
27
27
  response = FulfilApi.client.put(
28
- "/model/#{name}/search_read",
28
+ "/model/#{model_name}/search_read",
29
29
  body: { filters: conditions, fields: fields, limit: request_limit }.compact_blank
30
30
  )
31
31
 
32
- @resources = response.map { |resource| @resource_klass.new(resource) }
32
+ @resources = response.map do |attributes|
33
+ @resource_klass.new(attributes.merge(model_name: model_name))
34
+ end
35
+
33
36
  @loaded = true
34
37
  end
35
38
 
@@ -19,11 +19,11 @@ module FulfilApi
19
19
  #
20
20
  # @todo In the future, derive the {#name} from the @resource_klass automatically.
21
21
  #
22
- # @param name [String] The name of the resource model in Fulfil.
22
+ # @param model_name [String] The name of the resource model in Fulfil.
23
23
  # @return [FulfilApi::Resource::Relation] A new {Relation} instance with the model name set.
24
- def set(name:)
24
+ def set(model_name:)
25
25
  clone.tap do |relation|
26
- relation.name = name
26
+ relation.model_name = model_name
27
27
  end
28
28
  end
29
29
  end
@@ -41,10 +41,10 @@ module FulfilApi
41
41
  # depending on the API's limitations.
42
42
  #
43
43
  # @example Requesting nested data fields
44
- # FulfilApi::Resource.set(name: "sale.line").select("sale.reference").find_by(["id", "=", 10])
44
+ # FulfilApi::Resource.set(model_name: "sale.line").select("sale.reference").find_by(["id", "=", 10])
45
45
  #
46
46
  # @example Requesting additional fields
47
- # FulfilApi::Resource.set(name: "sale.sale").select(:reference).find_by(["id", "=", 10])
47
+ # FulfilApi::Resource.set(model_name: "sale.sale").select(:reference).find_by(["id", "=", 10])
48
48
  #
49
49
  # @param fields [Array<Symbol, String>] The fields to include in the response.
50
50
  # @return [FulfilApi::Resource::Relation] A new {Relation} instance with the selected fields.
@@ -59,7 +59,7 @@ module FulfilApi
59
59
  # as arrays according to the Fulfil API documentation.
60
60
  #
61
61
  # @example Simple querying with conditions
62
- # FulfilApi::Resource.set(name: "sale.line").where(["sale.reference", "=", "ORDER-123"])
62
+ # FulfilApi::Resource.set(model_name: "sale.line").where(["sale.reference", "=", "ORDER-123"])
63
63
  #
64
64
  # @todo Enhance the {#where} method to allow more natural and flexible queries.
65
65
  #
@@ -13,7 +13,7 @@ module FulfilApi
13
13
  include Naming
14
14
  include QueryMethods
15
15
 
16
- attr_accessor :conditions, :fields, :name, :request_limit
16
+ attr_accessor :conditions, :fields, :model_name, :request_limit
17
17
 
18
18
  delegate_missing_to :all
19
19
 
@@ -5,9 +5,17 @@ module FulfilApi
5
5
  # endpoints of Fulfil.
6
6
  class Resource
7
7
  include AttributeAssignable
8
+ include Persistable
9
+
10
+ class ModelNameMissing < Error; end
8
11
 
9
12
  def initialize(attributes = {})
13
+ attributes.deep_stringify_keys!
14
+
10
15
  @attributes = {}.with_indifferent_access
16
+ @model_name = attributes.delete("model_name").presence ||
17
+ raise(ModelNameMissing, "The model name is missing. Use the :model_name attribute to define it.")
18
+
11
19
  assign_attributes(attributes)
12
20
  end
13
21
 
@@ -23,7 +31,7 @@ module FulfilApi
23
31
  # forwarded to the {FulfilApi::Resource.relation}.
24
32
  #
25
33
  # @example forwarding of the .where class method
26
- # FulfilApi::Resource.set(name: "sale.sale").find_by(["id", "=", 100])
34
+ # FulfilApi::Resource.set(model_name: "sale.sale").find_by(["id", "=", 100])
27
35
  #
28
36
  # @return [FulfilApi::Resource::Relation]
29
37
  def relation
@@ -39,11 +47,30 @@ module FulfilApi
39
47
  @attributes[attribute_name]
40
48
  end
41
49
 
50
+ # Builds a structure for keeping track of any errors when trying to use the
51
+ # persistance methods for the API resource.
52
+ #
53
+ # @return [FulfilApi::Resource::Errors]
54
+ def errors
55
+ @errors ||= Errors.new(self)
56
+ end
57
+
58
+ # The {#id} is a shorthand to easily grab the ID of an API resource.
59
+ #
60
+ # @return [Integer, nil]
61
+ def id
62
+ @attributes["id"]
63
+ end
64
+
42
65
  # Returns all currently assigned attributes for a {FulfilApi::Resource}.
43
66
  #
44
67
  # @return [Hash]
45
68
  def to_h
46
69
  @attributes
47
70
  end
71
+
72
+ private
73
+
74
+ attr_reader :model_name
48
75
  end
49
76
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FulfilApi
4
- VERSION = "0.0.3"
4
+ VERSION = "0.1.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fulfil_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Vermaas
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-06 00:00:00.000000000 Z
11
+ date: 2024-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -84,10 +84,13 @@ files:
84
84
  - lib/fulfil_api/access_token.rb
85
85
  - lib/fulfil_api/client.rb
86
86
  - lib/fulfil_api/configuration.rb
87
+ - lib/fulfil_api/customer_shipment.rb
87
88
  - lib/fulfil_api/error.rb
88
89
  - lib/fulfil_api/resource.rb
89
90
  - lib/fulfil_api/resource/attribute_assignable.rb
90
91
  - lib/fulfil_api/resource/attribute_type.rb
92
+ - lib/fulfil_api/resource/errors.rb
93
+ - lib/fulfil_api/resource/persistable.rb
91
94
  - lib/fulfil_api/resource/relation.rb
92
95
  - lib/fulfil_api/resource/relation/loadable.rb
93
96
  - lib/fulfil_api/resource/relation/naming.rb