squake 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbe25ba449a2b7838a20ae80a5bcd9106bfe21a2f2626185a9eb901dc9e2f58a
4
- data.tar.gz: 18b8e92dabac3f0f0657a27b6adadd9616530421c640c4b6a584f1d454e16596
3
+ metadata.gz: f7838d5d10515e9d94811494249d36e789e441a04ab31f7e94c83c306a9fb2bf
4
+ data.tar.gz: 61d6f7415ec816567ebf260493d43f1a3515090b9062e1740b339f4eb40de405
5
5
  SHA512:
6
- metadata.gz: b302c402858193ee25d25ed3ed610362b1bb00ce477956c9c6f7e440b312e758b9eddd6861841f5fb8059eb0300728d85707b44964d2f10614a4f20429c3bf4c
7
- data.tar.gz: ee363ceeb88006801dfcc7a6be2535ac82357e001cd3e71c6b4d86e722f71719b84fcc536897cb1505374cd7a2226f17764ddc2e7bff678c0cef62ac234473ae
6
+ metadata.gz: 314d29865f690a9d4f7faf867269686dc5b184e9e36aa988d5ea19c60b75d608e09d9e90ccd086f733cfb01dd753c8c8b036a406cb48f1d3444d61f57b049fdc
7
+ data.tar.gz: 1234e584e92277e455c8d3094b3624e54e37b972af10c27c1f6836ce5bb4ca54f92f2b548fe95be5dca2531b6c9d7f0a87072aae1fb0afd4e5f06be163287394
data/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2023-06-21
11
+
12
+ ### Added
13
+
14
+ * added `Squake.configure`
15
+ * introduced global config allowig to omit the `client` from all service calls
16
+
17
+ ### Changed
18
+
19
+ * All public services now return a `Return` class that contains the actual data or an error object. This replaces the throwing of errors for control-flow.
20
+
21
+ ## [0.2.1 - 0.2.4] - 2023-06-21
22
+
23
+ ### Fixed
24
+
25
+ * Boilerplate bugs
26
+
10
27
  ## [0.2.0] - 2023-06-15
11
28
 
12
29
  ### Added
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Find the documentation here: [docs](https://docs.squake.earth/).
4
4
 
5
+ This gem follows SemVer, however only after a stable release 1.0.0 is made.
6
+
5
7
  ## Installation
6
8
 
7
9
  Via rubygems:
@@ -10,10 +12,10 @@ Via rubygems:
10
12
  gem 'squake'
11
13
  ```
12
14
 
13
- Via git:
15
+ Test the latest version via git:
14
16
 
15
17
  ```ruby
16
- gem 'squake', git: 'git@github.com:squake-earth/squake-ruby'
18
+ gem 'squake', git: 'git@github.com:squake-earth/squake-ruby', branch: :main
17
19
  ```
18
20
 
19
21
  ## Auth
@@ -30,7 +32,9 @@ You need a different API Key for production and sandbox.
30
32
 
31
33
  ## Usage
32
34
 
33
- Initialize the client:
35
+ ### Initialize the client
36
+
37
+ either explicitly anywhere in the app
34
38
 
35
39
  ```ruby
36
40
  config = Squake::Config.new(
@@ -46,9 +50,34 @@ config = Squake::Config.new(
46
50
  client = Squake::Client.new(config: config)
47
51
  ```
48
52
 
49
- Calculate emissions
53
+ or once globally in e.g. an initializer
54
+
55
+ ```ruby
56
+ Squake.configure do |config|
57
+ config.api_key = ENV.fetch('SQUAKE_API_KEY') # optional if ENV var `SQUAKE_API_KEY` is set
58
+ config.sandbox_mode = true # set to `false` for production
59
+ config.keep_alive_timeout = 30 # optional, default: 30
60
+ config.logger = Logger.new($stdout) # Set this to `Rails.logger` when using Rails
61
+ config.enforced_api_base = nil # for testing to e.g. point to a local server
62
+ end
63
+ ```
64
+
65
+ If you have the API Key in an ENV var named `SQUAKE_API_KEY`, you don't need any further config.
66
+
67
+ ### Fetch available products
68
+
69
+ ```ruby
70
+ context = Squake::Products.get
71
+ products = context.result # [Squake::Model::Product]
72
+ ```
73
+
74
+ ### Define items you want emissions to be computed
75
+
76
+ Find all available item types and methodologies in the [docs](https://docs-v2.squake.earth/group/endpoint-calculations).
50
77
 
51
78
  ```ruby
79
+ # NOTE: some items are also available as typed objects, found at `lib/squake/model/items/**/*.rb`
80
+ # where `.../items/private_jet/squake` is the item for type "private_jet" and methodology "squake".
52
81
  items = [
53
82
  {
54
83
  type: 'flight',
@@ -59,69 +88,81 @@ items = [
59
88
  number_of_travellers: 2,
60
89
  aircraft_type: '350',
61
90
  external_reference: 'booking-id',
62
- }
91
+ },
92
+ Squake::Model::Items::PrivateJet::Squake.new(
93
+ origin: 'SIN',
94
+ destination: 'HND',
95
+ external_reference: 'my-booking-id',
96
+ identifier: 'P180',
97
+ identifier_kind: 'ICAO',
98
+ ),
63
99
  ]
100
+ ```
101
+
102
+ ### Calculate emissions (without price quote)
64
103
 
65
- # returns Squake::Model::Carbon
66
- carbon = Squake::Calculation.create(
67
- client: client, # required
104
+ ```ruby
105
+ context = Squake::Calculation.create(
68
106
  items: items, # required
69
107
  carbon_unit: 'gram', # optional, default: 'gram', other options: 'kilogram', 'tonne'
70
108
  expand: [], # optional, default: [], allowed values: 'items' to enrich the response
109
+ client: client, # optional
71
110
  )
111
+
112
+ # alias: context.failed?
113
+ if context.success?
114
+ context.result # Squake::Model::Carbon
115
+ else
116
+ context.errors # [Squake::Errors::APIErrorResult]
117
+ end
72
118
  ```
73
119
 
74
- Calculate emissions and include a price quote:
120
+ ### Calculate emissions and include a price quote
75
121
 
76
122
  ```ruby
77
- # Find all available item types and methodologies here:
78
- # https://docs-v2.squake.earth/group/endpoint-calculations
79
- items = [
80
- {
81
- type: 'flight',
82
- methodology: 'ICAO',
83
- origin: 'BER',
84
- destination: 'SIN',
85
- booking_class: 'economy',
86
- number_of_travellers: 2,
87
- aircraft_type: '350',
88
- external_reference: 'booking-id',
89
- }
90
- ]
91
-
92
- # returns Squake::Model::Pricing
93
- pricing = Squake::CalculationWithPricing.quote(
94
- client: client, # required
123
+ context = Squake::CalculationWithPricing.quote(
95
124
  items: items, # required
96
125
  product: 'product-id', # required
97
126
  currency: 'EUR', # optional, default: 'EUR'
98
127
  carbon_unit: 'gram', # optional, default: 'gram', other options: 'kilogram', 'tonne'
99
128
  expand: [], # optional, default: [], allowed values: 'items', 'product', 'price' to enrich the response
129
+ client: client, # optional
100
130
  )
131
+
132
+ if context.success?
133
+ context.result # Squake::Model::Pricing
134
+ else
135
+ context.errors # [Squake::Errors::APIErrorResult]
136
+ end
101
137
  ```
102
138
 
103
- Place a purchase order; by default the library injects a `SecureRandom.uuid` as `external_reference` to ensure idempotency, i.e. you can safely retry the request if it fails.
139
+ ### Place a purchase order
140
+
141
+ by default the library injects a `SecureRandom.uuid` as `external_reference` to ensure idempotency, i.e. you can safely retry the request if it fails.
104
142
 
105
143
  ```ruby
106
144
  uuid = SecureRandom.uuid
107
145
 
108
146
  # returns Squake::Model::Purchase
109
- purchase = Squake::Purchase.create(
110
- client: client, # required
147
+ context = Squake::Purchase.create(
111
148
  pricing: pricing.id, # required, from above
112
149
  external_reference: uuid, # optional, default: SecureRandom.uuid, used for idempotency, if given, MUST be unique
150
+ client: client, # optional
113
151
  )
114
152
 
153
+ context.result # Squake::Model::Purchase
154
+ context.errors # [Squake::Errors::APIErrorResult]
155
+
115
156
  # retrieve the purchase later
116
157
  Squake::Purchase.retrieve(
117
- client: client, # required
118
158
  id: purchase.id, # required
159
+ client: client, # optional
119
160
  )
120
161
 
121
162
  # within 14 days, you can cancel the purchase worry-free
122
163
  Squake::Purchase.cancel(
123
- client: client, # required
124
164
  id: purchase.id, # required
165
+ client: client, # optional
125
166
  )
126
167
  ```
127
168
 
@@ -133,22 +174,10 @@ Please commit small and focused PRs with descriptive commit messages. If you are
133
174
 
134
175
  ## Publishing a new version
135
176
 
136
- Have a Rubygems API key in your `~/.gem/credentials` file.
137
-
138
- ```shell
139
- ---
140
- :rubygems_squake: rubygems_xxx
141
- ```
142
-
143
- Then run:
177
+ run
144
178
 
145
179
  ```shell
146
180
  bin/release
147
181
  ```
148
182
 
149
- which will guide you through the release process:
150
-
151
- * create a new git tag from current main
152
- * create a GitHub release from the tag
153
- * build the gem locally
154
- * publish the gem to RubyGems
183
+ which will guide you through the release process.
@@ -10,13 +10,13 @@ module Squake
10
10
 
11
11
  sig do
12
12
  params(
13
- client: Squake::Client,
14
13
  items: T::Array[T.any(Squake::Model::Items::BaseType, T::Hash[T.any(String, Symbol), T.untyped])],
15
14
  carbon_unit: String,
16
15
  expand: T::Array[String],
17
- ).returns(Squake::Model::Carbon)
16
+ client: Squake::Client,
17
+ ).returns(Squake::Return[Squake::Model::Carbon])
18
18
  end
19
- def self.create(client:, items:, carbon_unit: 'gram', expand: [])
19
+ def self.create(items:, carbon_unit: 'gram', expand: [], client: Squake::Client.new)
20
20
  # @TODO: add typed objects for all possible items. Until then, we allow either a Hash or a T::Struct
21
21
  items = items.map do |item|
22
22
  item.is_a?(T::Struct) ? item.serialize : item
@@ -26,16 +26,24 @@ module Squake
26
26
  path: ENDPOINT,
27
27
  method: :post,
28
28
  params: {
29
- items: items.map(&:serialize),
29
+ items: items,
30
30
  carbon_unit: carbon_unit,
31
31
  expand: expand,
32
32
  },
33
33
  )
34
- raise Squake::APIError.new(response: result) unless result.success?
35
34
 
36
- Squake::Model::Carbon.from_api_response(
37
- T.cast(result.body, T::Hash[Symbol, T.untyped]),
38
- )
35
+ if result.success?
36
+ carbon = Squake::Model::Carbon.from_api_response(
37
+ T.cast(result.body, T::Hash[Symbol, T.untyped]),
38
+ )
39
+ Return.new(result: carbon)
40
+ else
41
+ error = Squake::Errors::APIErrorResult.new(
42
+ code: :"api_error_#{result.code}",
43
+ detail: result.error_message,
44
+ )
45
+ Return.new(errors: [error])
46
+ end
39
47
  end
40
48
  end
41
49
  end
@@ -10,15 +10,15 @@ module Squake
10
10
 
11
11
  sig do
12
12
  params(
13
- client: Squake::Client,
14
13
  items: T::Array[T.any(Squake::Model::Items::BaseType, T::Hash[T.any(String, Symbol), T.untyped])],
15
14
  product: String,
16
15
  currency: String,
17
16
  carbon_unit: String,
18
17
  expand: T::Array[String],
19
- ).returns(Squake::Model::Pricing)
18
+ client: Squake::Client,
19
+ ).returns(Squake::Return[Squake::Model::Pricing])
20
20
  end
21
- def self.quote(client:, items:, product:, currency: 'EUR', carbon_unit: 'gram', expand: [])
21
+ def self.quote(items:, product:, currency: 'EUR', carbon_unit: 'gram', expand: [], client: Squake::Client.new)
22
22
  # @TODO: add typed objects for all possible items. Until then, we allow either a Hash or a T::Struct
23
23
  items = items.map do |item|
24
24
  item.is_a?(T::Struct) ? item.serialize : item
@@ -35,11 +35,19 @@ module Squake
35
35
  expand: expand,
36
36
  },
37
37
  )
38
- raise Squake::APIError.new(response: result) unless result.success?
39
38
 
40
- Squake::Model::Pricing.from_api_response(
41
- T.cast(result.body, T::Hash[Symbol, T.untyped]),
42
- )
39
+ if result.success?
40
+ pricing = Squake::Model::Pricing.from_api_response(
41
+ T.cast(result.body, T::Hash[Symbol, T.untyped]),
42
+ )
43
+ Return.new(result: pricing)
44
+ else
45
+ error = Squake::Errors::APIErrorResult.new(
46
+ code: :"api_error_#{result.code}",
47
+ detail: result.error_message,
48
+ )
49
+ Return.new(errors: [error])
50
+ end
43
51
  end
44
52
  end
45
53
  end
data/lib/squake/client.rb CHANGED
@@ -14,9 +14,9 @@ module Squake
14
14
  )
15
15
  end
16
16
 
17
- sig { params(config: Squake::Config).void }
18
- def initialize(config:)
19
- @config = config
17
+ sig { params(config: T.nilable(Squake::Config)).void }
18
+ def initialize(config: nil)
19
+ @config = T.let(config || T.must(Squake.configuration), Squake::Config)
20
20
  end
21
21
 
22
22
  sig do
@@ -53,7 +53,7 @@ module Squake
53
53
  end
54
54
  private def execute_request(method:, path:, headers: {}, params: nil, api_base: nil, api_key: nil)
55
55
  api_base ||= @config.api_base
56
- api_key ||= @config.api_key
56
+ api_key ||= T.must(@config.api_key)
57
57
 
58
58
  body_params = nil
59
59
  query_params = nil
data/lib/squake/config.rb CHANGED
@@ -10,11 +10,11 @@ module Squake
10
10
  DEFAULT_BASE_URL = T.let('https://api.squake.earth', String)
11
11
  SANDBOX_BASE_URL = T.let('https://api.sandbox.squake.earth', String)
12
12
 
13
- const :api_key, String
14
- const :keep_alive_timeout, Integer, default: 30
15
- const :logger, ::Logger, factory: -> { ::Logger.new($stdout) }
16
- const :sandbox_mode, T::Boolean, default: true
17
- const :enforced_api_base, T.nilable(String)
13
+ prop :api_key, T.nilable(String)
14
+ prop :keep_alive_timeout, Integer, default: 30
15
+ prop :logger, ::Logger, factory: -> { ::Logger.new($stdout) }
16
+ prop :sandbox_mode, T::Boolean, default: true
17
+ prop :enforced_api_base, T.nilable(String)
18
18
 
19
19
  sig { returns(String) }
20
20
  def api_base
@@ -0,0 +1,22 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'api_error_source'
5
+
6
+ module Squake
7
+ module Errors
8
+ #
9
+ # https://jsonapi.org/format/#errors
10
+ #
11
+ # > Error objects MUST be returned as an array keyed by errors in the top level of a JSON:API document.
12
+ #
13
+ class APIErrorResult < T::Struct
14
+ const :code, Symbol # An application-specific error code
15
+
16
+ # A human-readable explanation specific to this occurrence of the problem.
17
+ # Like title, this field's value can be localized.
18
+ const :detail, T.nilable(String)
19
+ const :source, T.nilable(APIErrorSource)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Squake
5
+ module Errors
6
+ class APIErrorSource < T::Struct
7
+ const :attribute, T.any(Symbol, String)
8
+ const :model, T.nilable(String), default: nil
9
+ const :id, T.nilable(String), default: nil # external reference if given
10
+ end
11
+ end
12
+ end
@@ -15,8 +15,8 @@ module Squake
15
15
  sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Carbon) }
16
16
  def self.from_api_response(response_body)
17
17
  Squake::Model::Carbon.new(
18
- quantity: BigDecimal(String(response_body.fetch(:quantity))),
19
- unit: CarbonUnit.deserialize(response_body.fetch(:unit)),
18
+ quantity: BigDecimal(String(response_body.fetch(:carbon_quantity))),
19
+ unit: CarbonUnit.deserialize(response_body.fetch(:carbon_unit)),
20
20
  items: response_body.fetch(:items, nil),
21
21
  )
22
22
  end
@@ -9,21 +9,19 @@ module Squake
9
9
  const :id, String
10
10
  const :product, String
11
11
  const :unit_amount, BigDecimal
12
- const :valid_from, T.nilable(Date)
12
+ const :valid_from, Date
13
13
  const :carbon_unit, String
14
14
  const :currency, String
15
15
 
16
16
  sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Price) }
17
17
  def self.from_api_response(response_body)
18
- product = response_body.fetch(:price, {})
19
-
20
18
  Squake::Model::Price.new(
21
- id: product.fetch(:id),
22
- product: product.fetch(:product),
23
- unit_amount: product.fetch(:unit_amount).to_d,
24
- valid_from: Date.parse(product.fetch(:valid_from, nil)),
25
- carbon_unit: product.fetch(:carbon_unit),
26
- currency: product.fetch(:currency),
19
+ id: response_body.fetch(:id),
20
+ product: response_body.fetch(:product),
21
+ unit_amount: BigDecimal(response_body.fetch(:unit_amount).to_s),
22
+ valid_from: Date.parse(response_body.fetch(:valid_from)),
23
+ carbon_unit: response_body.fetch(:carbon_unit),
24
+ currency: response_body.fetch(:currency),
27
25
  )
28
26
  end
29
27
  end
@@ -24,8 +24,8 @@ module Squake
24
24
  const :carbon_quantity, BigDecimal
25
25
  const :carbon_unit, String
26
26
  const :payment_link, T.nilable(String)
27
- const :price, Squake::Model::Price
28
- const :product, Squake::Model::Product
27
+ const :price, T.any(Squake::Model::Price, String)
28
+ const :product, T.any(Squake::Model::Product, String)
29
29
  const :valid_until, Date
30
30
  const :currency, String, default: 'EUR'
31
31
  const :total, Integer
@@ -33,10 +33,13 @@ module Squake
33
33
 
34
34
  sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Pricing) }
35
35
  def self.from_api_response(response_body)
36
- price = Squake::Model::Price.from_api_response(response_body)
37
- product = Squake::Model::Product.from_api_response(response_body)
36
+ price_or_id = T.let(response_body.fetch(:price), T.any(String, T::Hash[Symbol, T.untyped]))
37
+ price = price_or_id.is_a?(Hash) ? Squake::Model::Price.from_api_response(price_or_id) : price_or_id
38
38
 
39
- items = response_body.fetch(:items, [])
39
+ product_or_id = T.let(response_body.fetch(:product), T.any(String, T::Hash[Symbol, T.untyped]))
40
+ product = product_or_id.is_a?(Hash) ? Squake::Model::Product.from_api_response(product_or_id) : product_or_id
41
+
42
+ items = response_body.fetch(:items, []) || []
40
43
  items.map! do |item|
41
44
  item[:carbon_quantity] = item.fetch(:carbon_quantity).to_d
42
45
  item[:distance] = item.fetch(:distance).to_d
@@ -8,14 +8,14 @@ module Squake
8
8
 
9
9
  const :id, String
10
10
  const :title, String
11
+ const :certifications, T::Array[String], default: []
11
12
 
12
13
  sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Product) }
13
14
  def self.from_api_response(response_body)
14
- product = response_body.fetch(:product, {})
15
-
16
15
  Squake::Model::Product.new(
17
- id: product.fetch(:id),
18
- title: product.fetch(:title),
16
+ id: response_body.fetch(:id),
17
+ title: response_body.fetch(:title),
18
+ certifications: response_body.fetch(:certifications, []),
19
19
  )
20
20
  end
21
21
  end
@@ -11,12 +11,12 @@ module Squake
11
11
 
12
12
  sig do
13
13
  params(
14
- client: Squake::Client,
15
- locale: String,
16
14
  product_id: T.nilable(String),
17
- ).returns(T::Array[Squake::Model::Product])
15
+ locale: String,
16
+ client: Squake::Client,
17
+ ).returns(Squake::Return[T::Array[Squake::Model::Product]])
18
18
  end
19
- def self.get(client:, locale: DEFAULT_LOCALE, product_id: nil)
19
+ def self.get(product_id: nil, locale: DEFAULT_LOCALE, client: Squake::Client.new)
20
20
  path = product_id.nil? ? ENDPOINT : "#{ENDPOINT}/#{product_id}"
21
21
 
22
22
  result = client.call(
@@ -26,10 +26,18 @@ module Squake
26
26
  locale: locale,
27
27
  },
28
28
  )
29
- raise Squake::APIError.new(response: result) unless result.success?
30
29
 
31
- Array(result.body).map do |product_data|
32
- Squake::Model::Product.from_api_response({ product: product_data })
30
+ if result.success?
31
+ products = T.cast(Array(result.body), T::Array[T::Hash[Symbol, T.untyped]]).map do |product_data|
32
+ Squake::Model::Product.from_api_response(product_data)
33
+ end
34
+ Return.new(result: products)
35
+ else
36
+ error = Squake::Errors::APIErrorResult.new(
37
+ code: :"api_error_#{result.code}",
38
+ detail: result.error_message,
39
+ )
40
+ Return.new(errors: [error])
33
41
  end
34
42
  end
35
43
  end
@@ -11,16 +11,16 @@ module Squake
11
11
  # rubocop:disable Metrics/ParameterLists, Layout/LineLength
12
12
  sig do
13
13
  params(
14
- client: Squake::Client,
15
14
  pricing: String,
16
15
  confirmation_document: T.nilable(T::Hash[Symbol, T.untyped]),
17
16
  certificate_document: T.nilable(T::Hash[Symbol, T.untyped]),
18
17
  metadata: T.nilable(T::Hash[Symbol, T.untyped]),
19
18
  external_reference: String, # used for idempotency, if given, MUST be unique
20
19
  expand: T::Array[String],
21
- ).returns(Squake::Model::Purchase)
20
+ client: Squake::Client,
21
+ ).returns(Squake::Return[Squake::Model::Purchase])
22
22
  end
23
- def self.create(client:, pricing:, confirmation_document: nil, certificate_document: nil, metadata: nil, external_reference: SecureRandom.uuid, expand: [])
23
+ def self.create(pricing:, confirmation_document: nil, certificate_document: nil, metadata: nil, external_reference: SecureRandom.uuid, expand: [], client: Squake::Client.new)
24
24
  result = client.call(
25
25
  path: ENDPOINT,
26
26
  method: :post,
@@ -33,48 +33,73 @@ module Squake
33
33
  expand: expand,
34
34
  },
35
35
  )
36
- raise Squake::APIError.new(response: result) unless result.success?
37
36
 
38
- Squake::Model::Purchase.from_api_response(
39
- T.cast(result.body, T::Hash[Symbol, T.untyped]),
40
- )
37
+ if result.success?
38
+ purchase = Squake::Model::Purchase.from_api_response(
39
+ T.cast(result.body, T::Hash[Symbol, T.untyped]),
40
+ )
41
+ Return.new(result: purchase)
42
+ else
43
+ error = Squake::Errors::APIErrorResult.new(
44
+ code: :"api_error_#{result.code}",
45
+ detail: result.error_message,
46
+ )
47
+ Return.new(errors: [error])
48
+ end
41
49
  end
42
50
  # rubocop:enable Metrics/ParameterLists, Layout/LineLength
43
51
 
44
52
  sig do
45
53
  params(
46
- client: Squake::Client,
47
54
  id: String,
48
- ).returns(T.nilable(Squake::Model::Purchase))
55
+ client: Squake::Client,
56
+ ).returns(T.nilable(Squake::Return[Squake::Model::Purchase]))
49
57
  end
50
- def self.retrieve(client:, id:)
58
+ def self.retrieve(id:, client: Squake::Client.new)
51
59
  result = client.call(
52
60
  path: "#{ENDPOINT}/#{id}",
53
61
  )
54
62
  return nil if result.code == 404
55
- raise Squake::APIError.new(response: result) unless result.success?
56
63
 
57
- Squake::Model::Purchase.from_api_response(
58
- T.cast(result.body, T::Hash[Symbol, T.untyped]),
59
- )
64
+ if result.success?
65
+ purchase = Squake::Model::Purchase.from_api_response(
66
+ T.cast(result.body, T::Hash[Symbol, T.untyped]),
67
+ )
68
+ Return.new(result: purchase)
69
+ else
70
+ error = Squake::Errors::APIErrorResult.new(
71
+ code: :"api_error_#{result.code}",
72
+ detail: result.error_message,
73
+ )
74
+ Return.new(errors: [error])
75
+ end
60
76
  end
61
77
 
62
78
  sig do
63
79
  params(
64
- client: Squake::Client,
65
80
  id: String,
66
- ).returns(T.nilable(Squake::Model::Purchase))
81
+ client: Squake::Client,
82
+ ).returns(T.nilable(Squake::Return[Squake::Model::Purchase]))
67
83
  end
68
- def self.cancel(client:, id:)
84
+ def self.cancel(id:, client: Squake::Client.new)
69
85
  result = client.call(
70
86
  path: "#{ENDPOINT}/#{id}/cancel",
71
87
  method: :post,
72
88
  )
73
- raise Squake::APIError.new(response: result) unless result.success?
89
+ return nil if result.code == 404
74
90
 
75
- Squake::Model::Purchase.from_api_response(
76
- T.cast(result.body, T::Hash[Symbol, T.untyped]),
77
- )
91
+ if result.success?
92
+ purchase = Squake::Model::Purchase.from_api_response(
93
+ T.cast(result.body, T::Hash[Symbol, T.untyped]),
94
+ )
95
+ Return.new(result: purchase)
96
+ else
97
+ error = Squake::Errors::APIErrorResult.new(
98
+ code: :"api_error_#{result.code}",
99
+ detail: result.error_message,
100
+ )
101
+ Return.new(errors: [error])
102
+ end
78
103
  end
79
104
  end
80
105
  end
@@ -0,0 +1,64 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'errors/api_error_result'
5
+
6
+ module Squake
7
+ class Return
8
+ class Failure < StandardError; end
9
+
10
+ FAILURE_MSG_NO_ARGS = 'Must provide either a result or errors'
11
+ FAILURE_MSG_BOTH_ARGS = 'Cannot provide both a result and errors'
12
+ FAILURE_ERRORS_CALLED_WHEN_SUCCESS = 'Cannot call errors when result is present'
13
+
14
+ extend T::Sig
15
+ extend T::Generic
16
+
17
+ Error = type_member { { fixed: Errors::APIErrorResult } }
18
+ Result = type_member { { upper: Object } }
19
+
20
+ sig { params(result: T.nilable(Result), errors: T.nilable(T::Array[Error])).void }
21
+ def initialize(result: nil, errors: nil)
22
+ raise Failure, FAILURE_MSG_NO_ARGS if result.nil? && errors.nil?
23
+ raise Failure, FAILURE_MSG_BOTH_ARGS if present?(result) && present?(errors)
24
+
25
+ @result = result
26
+ @errors = errors
27
+ end
28
+
29
+ sig { returns(T::Boolean) }
30
+ def success?
31
+ present?(@result)
32
+ end
33
+
34
+ sig { returns(T::Boolean) }
35
+ def failed?
36
+ !success?
37
+ end
38
+
39
+ sig { returns(T::Array[Error]) }
40
+ def errors
41
+ raise Failure, FAILURE_ERRORS_CALLED_WHEN_SUCCESS if success?
42
+
43
+ T.must(@errors)
44
+ end
45
+
46
+ sig { returns(Result) }
47
+ def result
48
+ raise Failure, @errors&.map(&:serialize) if failed?
49
+
50
+ T.must(@result)
51
+ end
52
+
53
+ # courtesy to Rails, see: https://api.rubyonrails.org/v7.0.5/classes/Object.html
54
+ sig { params(object: T.untyped).returns(T::Boolean) }
55
+ private def blank?(object)
56
+ object.respond_to?(:empty?) ? !!object.empty? : !object
57
+ end
58
+
59
+ sig { params(object: T.untyped).returns(T::Boolean) }
60
+ private def present?(object)
61
+ !blank?(object)
62
+ end
63
+ end
64
+ end
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Squake
5
- VERSION = '0.2.4'
5
+ VERSION = '0.3.0'
6
6
  end
data/lib/squake.rb CHANGED
@@ -5,11 +5,36 @@ require 'sorbet-runtime'
5
5
  require 'oj'
6
6
  require 'net/http'
7
7
 
8
- Dir[File.join(__dir__, './**/*', '*.rb')].each { require(_1) }
9
-
10
- module Squake; end
11
-
12
8
  Oj.default_options = {
13
9
  mode: :compat, # required to dump hashes with symbol-keys
14
10
  symbol_keys: true,
15
11
  }
12
+
13
+ Dir[File.join(__dir__, './**/*', '*.rb')].each { require(_1) }
14
+
15
+ module Squake
16
+ class << self
17
+ extend T::Sig
18
+
19
+ sig { returns(T.nilable(Squake::Config)) }
20
+ attr_accessor :configuration
21
+
22
+ sig do
23
+ params(
24
+ _: T.proc.params(configuration: Squake::Config).void,
25
+ ).void
26
+ end
27
+ def configure(&_)
28
+ self.configuration ||= Squake::Config.new(
29
+ api_key: ENV.fetch('SQUAKE_API_KEY', nil),
30
+ keep_alive_timeout: ENV.fetch('SQUAKE_KEEP_ALIVE_TIMEOUT', 30).to_i,
31
+ sandbox_mode: ENV.fetch('SQUAKE_SANDBOX_MODE', 'true').casecmp?('true'),
32
+ enforced_api_base: ENV.fetch('SQUAKE_API_BASE', nil),
33
+ )
34
+
35
+ yield(T.must(configuration))
36
+ end
37
+ end
38
+ end
39
+
40
+ Squake.configure(&:itself) unless ENV['SQUAKE_API_KEY'].nil?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: squake
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - SQUAKE
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-15 00:00:00.000000000 Z
11
+ date: 2023-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-http
@@ -138,6 +138,8 @@ files:
138
138
  - lib/squake/calculation_with_pricing.rb
139
139
  - lib/squake/client.rb
140
140
  - lib/squake/config.rb
141
+ - lib/squake/errors/api_error_result.rb
142
+ - lib/squake/errors/api_error_source.rb
141
143
  - lib/squake/model/carbon.rb
142
144
  - lib/squake/model/carbon_unit.rb
143
145
  - lib/squake/model/items/base_type.rb
@@ -149,6 +151,7 @@ files:
149
151
  - lib/squake/products.rb
150
152
  - lib/squake/purchase.rb
151
153
  - lib/squake/response.rb
154
+ - lib/squake/return.rb
152
155
  - lib/squake/util.rb
153
156
  - lib/squake/version.rb
154
157
  - sorbet/rbi/dsl/active_support/callbacks.rbi