squake 0.2.4 → 0.3.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: 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