squake 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +100 -0
- data/lib/squake/api_error.rb +17 -0
- data/lib/squake/calculation_with_pricing.rb +40 -0
- data/lib/squake/client.rb +132 -0
- data/lib/squake/config.rb +24 -0
- data/lib/squake/model/items/base_type.rb +13 -0
- data/lib/squake/model/items/private_jet/squake.rb +21 -0
- data/lib/squake/model/price.rb +31 -0
- data/lib/squake/model/pricing.rb +61 -0
- data/lib/squake/model/product.rb +23 -0
- data/lib/squake/model/purchase.rb +39 -0
- data/lib/squake/products.rb +36 -0
- data/lib/squake/purchase.rb +80 -0
- data/lib/squake/response.rb +45 -0
- data/lib/squake/util.rb +29 -0
- data/lib/squake/version.rb +6 -0
- data/lib/squake.rb +7 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7860b4b9cfbf3110355582ab95ce9faee0055fc6ea852b00093703fbcf8f7c8d
|
4
|
+
data.tar.gz: 6f05154a58469f7f52f9730bf808a3005961b7ba29e7f6ce8fe47e47b27cc651
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d2065bbbe6bbbffa5067acf0810dbfe2c3ef32b50b5bce14b949f170a138f29ec16d2897093d9305af615e768ebcaa6da24873660572c24141e09f11b896cf08
|
7
|
+
data.tar.gz: 53f6a57201c32df605f6d30932baa4408bc3eafd7c02fb6f2dff6f55d0313c1e0b0f501980aa93e17bcea5ecd3bc11491f81639d138aa4c60005e62237689e92
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# SQUAKE
|
2
|
+
|
3
|
+
Find the documentation here: [docs](https://docs.squake.earth/).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Via git:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Gemfile
|
11
|
+
|
12
|
+
gem 'squake', git: 'git@github.com:squake-earth/squake-ruby'
|
13
|
+
```
|
14
|
+
|
15
|
+
Via rubygems:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
# Gemfile
|
19
|
+
|
20
|
+
gem 'squake', '~> 0.1.0'
|
21
|
+
```
|
22
|
+
|
23
|
+
## Auth
|
24
|
+
|
25
|
+
An API Key can be generated on the [dashboard](https://dashboard.squake.earth/.)
|
26
|
+
You need a different API Key for production and sandbox.
|
27
|
+
|
28
|
+
## Functionality
|
29
|
+
|
30
|
+
* compute emission values
|
31
|
+
* request a price quote (with a product id)
|
32
|
+
* place a purchase order (with a price quote id)
|
33
|
+
* get a list of all available products (includes price information)
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Calculate emissions and include a price quote:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
config = Squake::Config.new(
|
41
|
+
api_key: 'Your API Key', # required
|
42
|
+
sandbox_mode: true, # optional, default: true
|
43
|
+
keep_alive_timeout: 30, # optional, default: 30
|
44
|
+
logger: Logger.new($stdout), # optional, default: Logger.new($stdout)
|
45
|
+
enforced_api_base: nil, # optional, default: nil. If given, overrides the API base URL.
|
46
|
+
)
|
47
|
+
|
48
|
+
# the SQUAKE client emits canonical log lines, e.g.
|
49
|
+
# Request started http_method=get http_path=/api/v2/calculations
|
50
|
+
client = Squake::Client.new(config: config)
|
51
|
+
|
52
|
+
# Find all available item types and methodologies here:
|
53
|
+
# https://docs-v2.squake.earth/group/endpoint-calculations
|
54
|
+
items = [
|
55
|
+
{
|
56
|
+
type: 'flight',
|
57
|
+
methodology: 'ICAO',
|
58
|
+
origin: 'BER',
|
59
|
+
destination: 'SIN',
|
60
|
+
booking_class: 'economy',
|
61
|
+
number_of_travellers: 2,
|
62
|
+
aircraft_type: '350',
|
63
|
+
external_reference: 'booking-id',
|
64
|
+
}
|
65
|
+
]
|
66
|
+
|
67
|
+
# returns Squake::Model::Pricing
|
68
|
+
pricing = Squake::CalculationWithPricing.quote(
|
69
|
+
client: client, # required
|
70
|
+
items: items, # required
|
71
|
+
product: 'product-id', # required
|
72
|
+
currency: 'EUR', # optional, default: 'EUR'
|
73
|
+
carbon_unit: 'gram', # optional, default: 'gram', other options: 'kilogram', 'tonne'
|
74
|
+
)
|
75
|
+
```
|
76
|
+
|
77
|
+
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.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
uuid = SecureRandom.uuid
|
81
|
+
|
82
|
+
# returns Squake::Model::Purchase
|
83
|
+
purchase = Squake::Purchase.create(
|
84
|
+
client: client, # required
|
85
|
+
pricing: pricing.id, # required, from above
|
86
|
+
external_reference: uuid, # optional, default: SecureRandom.uuid, used for idempotency, if given, MUST be unique
|
87
|
+
)
|
88
|
+
|
89
|
+
# retrieve the purchase later
|
90
|
+
Squake::Purchase.retrieve(
|
91
|
+
client: client, # required
|
92
|
+
id: purchase.id, # required
|
93
|
+
)
|
94
|
+
|
95
|
+
# within 14 days, you can cancel the purchase worry-free
|
96
|
+
Squake::Purchase.cancel(
|
97
|
+
client: client, # required
|
98
|
+
id: purchase.id, # required
|
99
|
+
)
|
100
|
+
```
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
class APIError < StandardError
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { returns(Squake::Response) }
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
sig { params(response: Squake::Response).void }
|
12
|
+
def initialize(response:)
|
13
|
+
@response = response
|
14
|
+
super(T.must(response.error_message))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# https://docs-v2.squake.earth/group/endpoint-combined-calculation-pricing
|
5
|
+
module Squake
|
6
|
+
class CalculationWithPricing
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
ENDPOINT = T.let('/v2/calculations-with-pricing', String)
|
10
|
+
|
11
|
+
sig do
|
12
|
+
params(
|
13
|
+
client: Squake::Client,
|
14
|
+
items: T::Array[Squake::Model::Items::BaseType],
|
15
|
+
product: String,
|
16
|
+
currency: String,
|
17
|
+
carbon_unit: String,
|
18
|
+
expand: T::Array[String],
|
19
|
+
).returns(Squake::Model::Pricing)
|
20
|
+
end
|
21
|
+
def self.quote(client:, items:, product:, currency: 'EUR', carbon_unit: 'gram', expand: [])
|
22
|
+
result = client.call(
|
23
|
+
path: ENDPOINT,
|
24
|
+
method: :post,
|
25
|
+
params: {
|
26
|
+
items: items.map(&:serialize),
|
27
|
+
product: product,
|
28
|
+
currency: currency,
|
29
|
+
carbon_unit: carbon_unit,
|
30
|
+
expand: expand,
|
31
|
+
},
|
32
|
+
)
|
33
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
34
|
+
|
35
|
+
Squake::Model::Pricing.from_api_response(
|
36
|
+
T.cast(result.body, T::Hash[Symbol, T.untyped]),
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
module Squake
|
7
|
+
class Client
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
JsonResponseBody = T.type_alias do
|
11
|
+
T.any(
|
12
|
+
T::Array[T::Hash[Symbol, T.untyped]],
|
13
|
+
T::Hash[Symbol, T.untyped],
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(config: Squake::Config).void }
|
18
|
+
def initialize(config:)
|
19
|
+
@config = config
|
20
|
+
end
|
21
|
+
|
22
|
+
sig do
|
23
|
+
params(
|
24
|
+
path: String,
|
25
|
+
headers: T::Hash[String, String],
|
26
|
+
method: Symbol,
|
27
|
+
params: T.nilable(T::Hash[Symbol, String]),
|
28
|
+
api_base: T.nilable(String),
|
29
|
+
api_key: T.nilable(String),
|
30
|
+
).returns(Squake::Response)
|
31
|
+
end
|
32
|
+
def call(path:, headers: {}, method: :get, params: nil, api_base: nil, api_key: nil)
|
33
|
+
execute_request(
|
34
|
+
method: method,
|
35
|
+
path: path,
|
36
|
+
headers: headers,
|
37
|
+
params: params,
|
38
|
+
api_base: api_base,
|
39
|
+
api_key: api_key,
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
44
|
+
sig do
|
45
|
+
params(
|
46
|
+
method: Symbol,
|
47
|
+
path: String,
|
48
|
+
headers: T::Hash[String, String],
|
49
|
+
params: T.nilable(T::Hash[Symbol, String]),
|
50
|
+
api_base: T.nilable(String),
|
51
|
+
api_key: T.nilable(String),
|
52
|
+
).returns(Squake::Response)
|
53
|
+
end
|
54
|
+
private def execute_request(method:, path:, headers: {}, params: nil, api_base: nil, api_key: nil)
|
55
|
+
api_base ||= @config.api_base
|
56
|
+
api_key ||= @config.api_key
|
57
|
+
|
58
|
+
body_params = nil
|
59
|
+
query_params = nil
|
60
|
+
case method
|
61
|
+
when :get, :head, :delete
|
62
|
+
query_params = params
|
63
|
+
when :post
|
64
|
+
body_params = params
|
65
|
+
else
|
66
|
+
raise "Unrecognized HTTP method: #{method}. Expected one of: get, head, delete, post"
|
67
|
+
end
|
68
|
+
|
69
|
+
headers = request_headers(api_key).update(headers)
|
70
|
+
query = query_params ? Util.encode_parameters(query_params) : nil
|
71
|
+
body = body_params ? ::Oj.dump(body_params) : nil
|
72
|
+
sanitized_path = path[0] == '/' ? path : "/#{path}"
|
73
|
+
uri = URI.parse(api_base + sanitized_path)
|
74
|
+
|
75
|
+
connection = connection(uri)
|
76
|
+
|
77
|
+
method_name = method.to_s.upcase
|
78
|
+
has_response_body = method_name != 'HEAD'
|
79
|
+
request = Net::HTTPGenericRequest.new(
|
80
|
+
method_name,
|
81
|
+
(body_params ? true : false),
|
82
|
+
has_response_body,
|
83
|
+
(query ? [uri.path, query].join('?') : uri.path),
|
84
|
+
headers,
|
85
|
+
)
|
86
|
+
|
87
|
+
# https://stripe.com/blog/canonical-log-lines
|
88
|
+
canonical_request_line = <<~TXT
|
89
|
+
Request started
|
90
|
+
http_method=#{request.method}
|
91
|
+
http_path=#{request.path}
|
92
|
+
http_headers=#{request.to_hash}
|
93
|
+
TXT
|
94
|
+
canonical_request_line.gsub!(api_key, 'API_KEY_REDACTED')
|
95
|
+
@config.logger.info(canonical_request_line)
|
96
|
+
|
97
|
+
result = connection.request(request, body)
|
98
|
+
|
99
|
+
Squake::Response.new(
|
100
|
+
original_request: request,
|
101
|
+
code: Integer(result.code),
|
102
|
+
body: try_parse_json(result.body),
|
103
|
+
headers: result.to_hash,
|
104
|
+
)
|
105
|
+
end
|
106
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
107
|
+
|
108
|
+
sig { params(api_key: String).returns(T::Hash[String, String]) }
|
109
|
+
private def request_headers(api_key)
|
110
|
+
{
|
111
|
+
'Authorization' => "Bearer #{api_key}",
|
112
|
+
'Content-Type' => 'application/json',
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
sig { params(result_body: T.untyped).returns(JsonResponseBody) }
|
117
|
+
private def try_parse_json(result_body)
|
118
|
+
::Oj.load(result_body, symbol_keys: true)
|
119
|
+
rescue ::Oj::ParseError, TypeError, JSON::ParserError, EncodingError => e
|
120
|
+
# in case of an error, Squake's response body is HTML not JSON
|
121
|
+
{ 'error' => { 'message' => e.message, 'body' => result_body } }
|
122
|
+
end
|
123
|
+
|
124
|
+
sig { params(uri: URI::Generic).returns(Net::HTTP) }
|
125
|
+
private def connection(uri)
|
126
|
+
connection = Net::HTTP.new(uri.host, uri.port)
|
127
|
+
connection.keep_alive_timeout = @config.keep_alive_timeout
|
128
|
+
connection.use_ssl = uri.scheme == 'https'
|
129
|
+
connection
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
class Config < T::Struct
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
DEFAULT_BASE_URL = T.let('https://api.squake.earth', String)
|
9
|
+
SANDBOX_BASE_URL = T.let('https://api.sandbox.squake.earth', String)
|
10
|
+
|
11
|
+
const :api_key, String
|
12
|
+
const :keep_alive_timeout, Integer, default: 30
|
13
|
+
const :logger, Logger, factory: -> { Logger.new($stdout) }
|
14
|
+
const :sandbox_mode, T::Boolean, default: true
|
15
|
+
const :enforced_api_base, T.nilable(String)
|
16
|
+
|
17
|
+
sig { returns(String) }
|
18
|
+
def api_base
|
19
|
+
return T.must(enforced_api_base) unless enforced_api_base.nil?
|
20
|
+
|
21
|
+
sandbox_mode ? SANDBOX_BASE_URL : DEFAULT_BASE_URL
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Squake
|
4
|
+
module Model
|
5
|
+
module Items
|
6
|
+
module PrivateJet
|
7
|
+
class Squake < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
const :type, String, default: 'private_jet'
|
11
|
+
const :methodology, String, default: 'SQUAKE'
|
12
|
+
const :origin, String
|
13
|
+
const :destination, String
|
14
|
+
const :identifier, String
|
15
|
+
const :identifier_kind, String, default: 'ICAO'
|
16
|
+
const :external_reference, T.nilable(String)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
module Model
|
6
|
+
class Price < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const :id, String
|
10
|
+
const :product, String
|
11
|
+
const :unit_amount, BigDecimal
|
12
|
+
const :valid_from, T.nilable(Date)
|
13
|
+
const :carbon_unit, String
|
14
|
+
const :currency, String
|
15
|
+
|
16
|
+
sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Price) }
|
17
|
+
def self.from_api_response(response_body)
|
18
|
+
product = response_body.fetch(:price, {})
|
19
|
+
|
20
|
+
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),
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative 'price'
|
5
|
+
require_relative 'product'
|
6
|
+
|
7
|
+
module Squake
|
8
|
+
module Model
|
9
|
+
class Pricing < T::Struct
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
class Item < T::Struct
|
13
|
+
extend T::Sig
|
14
|
+
const :carbon_quantity, BigDecimal
|
15
|
+
const :carbon_unit, String
|
16
|
+
const :distance, BigDecimal
|
17
|
+
const :distance_unit, String
|
18
|
+
const :external_reference, T.nilable(String)
|
19
|
+
const :type, String
|
20
|
+
const :methodology, String
|
21
|
+
end
|
22
|
+
|
23
|
+
const :id, String
|
24
|
+
const :carbon_quantity, BigDecimal
|
25
|
+
const :carbon_unit, String
|
26
|
+
const :payment_link, T.nilable(String)
|
27
|
+
const :price, Squake::Model::Price
|
28
|
+
const :product, Squake::Model::Product
|
29
|
+
const :valid_until, Date
|
30
|
+
const :currency, String, default: 'EUR'
|
31
|
+
const :total, Integer
|
32
|
+
const :items, T::Array[Pricing::Item] # when constructing this, construct nested items first explicitly
|
33
|
+
|
34
|
+
sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Pricing) }
|
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)
|
38
|
+
|
39
|
+
items = response_body.fetch(:items, [])
|
40
|
+
items.map! do |item|
|
41
|
+
item[:carbon_quantity] = item.fetch(:carbon_quantity).to_d
|
42
|
+
item[:distance] = item.fetch(:distance).to_d
|
43
|
+
Item.new(item)
|
44
|
+
end
|
45
|
+
|
46
|
+
Squake::Model::Pricing.new(
|
47
|
+
id: response_body.fetch(:id),
|
48
|
+
items: items,
|
49
|
+
carbon_quantity: response_body.fetch(:carbon_quantity).to_d,
|
50
|
+
carbon_unit: response_body.fetch(:carbon_unit),
|
51
|
+
payment_link: response_body.fetch(:payment_link, nil),
|
52
|
+
price: price,
|
53
|
+
product: product,
|
54
|
+
valid_until: Date.parse(response_body.fetch(:valid_until, nil)),
|
55
|
+
currency: response_body.fetch(:currency),
|
56
|
+
total: response_body.fetch(:total),
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
module Model
|
6
|
+
class Product < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const :id, String
|
10
|
+
const :title, String
|
11
|
+
|
12
|
+
sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Product) }
|
13
|
+
def self.from_api_response(response_body)
|
14
|
+
product = response_body.fetch(:product, {})
|
15
|
+
|
16
|
+
Squake::Model::Product.new(
|
17
|
+
id: product.fetch(:id),
|
18
|
+
title: product.fetch(:title),
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
module Model
|
6
|
+
class Purchase < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const :id, String
|
10
|
+
const :files, T::Array[String] # TODO: optional object, typed?
|
11
|
+
const :payment_method, String
|
12
|
+
const :state, String
|
13
|
+
const :metadata, T::Hash[Symbol, T.untyped], default: {}
|
14
|
+
const :checkout_page, T.nilable(T::Hash[Symbol, T.untyped])
|
15
|
+
const :carbon_quantity, BigDecimal
|
16
|
+
const :carbon_unit, String
|
17
|
+
const :total, Integer
|
18
|
+
const :currency, String
|
19
|
+
const :external_reference, T.nilable(String)
|
20
|
+
|
21
|
+
sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Purchase) }
|
22
|
+
def self.from_api_response(response_body)
|
23
|
+
Squake::Model::Purchase.new(
|
24
|
+
id: response_body.fetch(:id),
|
25
|
+
files: response_body.fetch(:files),
|
26
|
+
payment_method: response_body.fetch(:payment_method),
|
27
|
+
state: response_body.fetch(:state),
|
28
|
+
metadata: response_body.fetch(:metadata),
|
29
|
+
checkout_page: response_body.fetch(:checkout_page, nil),
|
30
|
+
carbon_quantity: BigDecimal(response_body.fetch(:carbon_quantity).to_s),
|
31
|
+
carbon_unit: response_body.fetch(:carbon_unit),
|
32
|
+
total: response_body.fetch(:total),
|
33
|
+
currency: response_body.fetch(:currency),
|
34
|
+
external_reference: response_body.fetch(:external_reference, nil),
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# https://docs-v2.squake.earth/operation/operation-get-products
|
5
|
+
module Squake
|
6
|
+
class Products
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
ENDPOINT = T.let('/v2/products', String)
|
10
|
+
DEFAULT_LOCALE = T.let('en', String)
|
11
|
+
|
12
|
+
sig do
|
13
|
+
params(
|
14
|
+
client: Squake::Client,
|
15
|
+
locale: String,
|
16
|
+
product_id: T.nilable(String),
|
17
|
+
).returns(T::Array[Squake::Model::Product])
|
18
|
+
end
|
19
|
+
def self.get(client:, locale: DEFAULT_LOCALE, product_id: nil)
|
20
|
+
path = product_id.nil? ? ENDPOINT : "#{ENDPOINT}/#{product_id}"
|
21
|
+
|
22
|
+
result = client.call(
|
23
|
+
path: path,
|
24
|
+
method: :get,
|
25
|
+
params: {
|
26
|
+
locale: locale,
|
27
|
+
},
|
28
|
+
)
|
29
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
30
|
+
|
31
|
+
Array(result.body).map do |product_data|
|
32
|
+
Squake::Model::Product.from_api_response({ product: product_data })
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# https://docs-v2.squake.earth/operation/operation-post-purchases
|
5
|
+
module Squake
|
6
|
+
class Purchase
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
ENDPOINT = T.let('/v2/purchases', String)
|
10
|
+
|
11
|
+
# rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
12
|
+
sig do
|
13
|
+
params(
|
14
|
+
client: Squake::Client,
|
15
|
+
pricing: String,
|
16
|
+
confirmation_document: T.nilable(T::Hash[Symbol, T.untyped]),
|
17
|
+
certificate_document: T.nilable(T::Hash[Symbol, T.untyped]),
|
18
|
+
metadata: T.nilable(T::Hash[Symbol, T.untyped]),
|
19
|
+
external_reference: String, # used for idempotency, if given, MUST be unique
|
20
|
+
expand: T::Array[String],
|
21
|
+
).returns(Squake::Model::Purchase)
|
22
|
+
end
|
23
|
+
def self.create(client:, pricing:, confirmation_document: nil, certificate_document: nil, metadata: nil, external_reference: SecureRandom.uuid, expand: [])
|
24
|
+
result = client.call(
|
25
|
+
path: ENDPOINT,
|
26
|
+
method: :post,
|
27
|
+
params: {
|
28
|
+
pricing: pricing,
|
29
|
+
confirmation_document: confirmation_document,
|
30
|
+
certificate_document: certificate_document,
|
31
|
+
metadata: metadata,
|
32
|
+
external_reference: external_reference,
|
33
|
+
expand: expand,
|
34
|
+
},
|
35
|
+
)
|
36
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
37
|
+
|
38
|
+
Squake::Model::Purchase.from_api_response(
|
39
|
+
T.cast(result.body, T::Hash[Symbol, T.untyped]),
|
40
|
+
)
|
41
|
+
end
|
42
|
+
# rubocop:enable Metrics/ParameterLists, Layout/LineLength
|
43
|
+
|
44
|
+
sig do
|
45
|
+
params(
|
46
|
+
client: Squake::Client,
|
47
|
+
id: String,
|
48
|
+
).returns(T.nilable(Squake::Model::Purchase))
|
49
|
+
end
|
50
|
+
def self.retrieve(client:, id:)
|
51
|
+
result = client.call(
|
52
|
+
path: "#{ENDPOINT}/#{id}",
|
53
|
+
)
|
54
|
+
return nil if result.code == 404
|
55
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
56
|
+
|
57
|
+
Squake::Model::Purchase.from_api_response(
|
58
|
+
T.cast(result.body, T::Hash[Symbol, T.untyped]),
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig do
|
63
|
+
params(
|
64
|
+
client: Squake::Client,
|
65
|
+
id: String,
|
66
|
+
).returns(T.nilable(Squake::Model::Purchase))
|
67
|
+
end
|
68
|
+
def self.cancel(client:, id:)
|
69
|
+
result = client.call(
|
70
|
+
path: "#{ENDPOINT}/#{id}/cancel",
|
71
|
+
method: :post,
|
72
|
+
)
|
73
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
74
|
+
|
75
|
+
Squake::Model::Purchase.from_api_response(
|
76
|
+
T.cast(result.body, T::Hash[Symbol, T.untyped]),
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
module Squake
|
7
|
+
class Response < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
const :original_request, Net::HTTPGenericRequest
|
11
|
+
const :code, Integer
|
12
|
+
const :body, Squake::Client::JsonResponseBody
|
13
|
+
const :headers, T::Hash[String, T.untyped]
|
14
|
+
|
15
|
+
sig { returns(T::Boolean) }
|
16
|
+
def success?
|
17
|
+
code >= 200 && code < 300
|
18
|
+
end
|
19
|
+
|
20
|
+
sig { returns(T::Boolean) }
|
21
|
+
def failure?
|
22
|
+
!success?
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(T.nilable(Squake::APIError)) }
|
26
|
+
def error
|
27
|
+
return if success?
|
28
|
+
|
29
|
+
Squake::APIError.new(response: self)
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { returns(T.nilable(String)) }
|
33
|
+
def error_message
|
34
|
+
return if success?
|
35
|
+
|
36
|
+
<<~TXT
|
37
|
+
Failed Request
|
38
|
+
http_code=#{code}
|
39
|
+
body=#{::Oj.dump(body)}
|
40
|
+
original_request_http_method=#{original_request.method}
|
41
|
+
original_request_http_path=#{original_request.path}
|
42
|
+
TXT
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/squake/util.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
class Util
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
# Encodes a hash of parameters in a way that's suitable for use as query
|
9
|
+
# parameters in a URI or as form parameters in a request body. This mainly
|
10
|
+
# involves escaping special characters from parameter keys and values (e.g.
|
11
|
+
# `&`).
|
12
|
+
sig { params(params: T::Hash[Symbol, String]).returns(String) }
|
13
|
+
def self.encode_parameters(params)
|
14
|
+
params.map { |k, v| "#{url_encode(k.to_s)}=#{url_encode(v)}" }.join('&')
|
15
|
+
end
|
16
|
+
|
17
|
+
# Encodes a string in a way that makes it suitable for use in a set of
|
18
|
+
# query parameters in a URI or in a set of form parameters in a request
|
19
|
+
# body.
|
20
|
+
sig { params(key: String).returns(String) }
|
21
|
+
def self.url_encode(key)
|
22
|
+
CGI.escape(key.to_s).
|
23
|
+
# Don't use strict form encoding by changing the square bracket control
|
24
|
+
# characters back to their literals. This is fine by the server, and
|
25
|
+
# makes these parameter strings easier to read.
|
26
|
+
gsub('%5B', '[').gsub('%5D', ']')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/squake.rb
ADDED
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: squake
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- 'Team SQUAKE :rocket:'
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-06-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: oj
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sorbet
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sorbet-runtime
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop-dbl
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: spoom
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: tapioca
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: This gem provides an interface for the SQUAKE API to calculate and compensate
|
98
|
+
carbon emissions.
|
99
|
+
email: oss@squake.earth
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- README.md
|
105
|
+
- lib/squake.rb
|
106
|
+
- lib/squake/api_error.rb
|
107
|
+
- lib/squake/calculation_with_pricing.rb
|
108
|
+
- lib/squake/client.rb
|
109
|
+
- lib/squake/config.rb
|
110
|
+
- lib/squake/model/items/base_type.rb
|
111
|
+
- lib/squake/model/items/private_jet/squake.rb
|
112
|
+
- lib/squake/model/price.rb
|
113
|
+
- lib/squake/model/pricing.rb
|
114
|
+
- lib/squake/model/product.rb
|
115
|
+
- lib/squake/model/purchase.rb
|
116
|
+
- lib/squake/products.rb
|
117
|
+
- lib/squake/purchase.rb
|
118
|
+
- lib/squake/response.rb
|
119
|
+
- lib/squake/util.rb
|
120
|
+
- lib/squake/version.rb
|
121
|
+
homepage: https://github.com/squake-earth/squake-ruby
|
122
|
+
licenses:
|
123
|
+
- MIT
|
124
|
+
metadata:
|
125
|
+
rubygems_mfa_required: 'true'
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '2.7'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubygems_version: 3.4.13
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: The industry solution for sustainable travel and logistics.
|
145
|
+
test_files: []
|