taler 0.3.0 → 0.5.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: dc1ef9028362bec363c1c645759524173618697fc1a0ce52cfb87539c5ecc2b1
4
- data.tar.gz: 50654905eb23b1f07571cbe96bb30d223320ea65f82779e2a3a7990bf5af4bfe
3
+ metadata.gz: 60ec832858a497c44d8211a3ac887052991b9a03ad309ffb3ed693205038c3c7
4
+ data.tar.gz: 247a12ee6e2ffbcc52e6887ddd7b205829de374cde7f37917c6d775db54affd9
5
5
  SHA512:
6
- metadata.gz: c4ada42f23bfc80478b5bff58d03b166c221bfc7d054153f8f079a6b4869eb319a0d6051826a05df53b6053dcf0e3b08cc133524a9a2e5085ece452ad1fdfa8b
7
- data.tar.gz: '0029a33dfd7313028d146748039acd2d38683cb5b25b6ec8916e3097e79c129a9281381423bac354c888c74336a63a2b33ced5697fee1c83bd383da301bdc510'
6
+ metadata.gz: 2988d2ccf7fa226cd0c781175f7bdd964b37af55070a489d4380d341154e200e5326e24617a03907e74193225560cf2dfd24a7e6ba71373536240ee2e0f4d8ec
7
+ data.tar.gz: 86fdfeb371bb5cede0492b31c5d7e459c27280cc84a448671ebcdf2b7fe7ae472ed8b7effeabd9e0c13391e636dfded5776b557ae1d2617e42739e82fc278225
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.5.0] - 2026-10-07
4
+
5
+ - Raise `Taler::RequestError` when the backend responds with anything but
6
+ 200 OK. The error carries the HTTP `status` and the response `body`, and
7
+ its message includes the backend's hint. Before, an error response was
8
+ parsed as a success and callers got a confusing `KeyError` or
9
+ `JSON::ParserError` instead.
10
+ - Raise `Taler::Error` on any other error during the HTTP request. This
11
+ could be a connection error, a network failure or something completely
12
+ unknown.
13
+
14
+ ## [0.4.0] - 2026-08-25
15
+
16
+ - Restore deprecated password auth as access tokens need 2FA in production.
17
+ Instead of obtaining a token on-the-fly, the user would provide a token
18
+ to the app (and this gem) instead of the password. We can add support for
19
+ that another time.
20
+
3
21
  ## [0.3.0] - 2026-02-24
4
22
 
5
23
  - Update docs and rbs file.
data/README.md CHANGED
@@ -16,7 +16,7 @@ bundle add taler
16
16
  require "taler"
17
17
 
18
18
  backend_url = "https://backend.demo.taler.net/instances/sandbox"
19
- backend_password = "sandbox"
19
+ password = "sandbox"
20
20
 
21
21
  order = Taler::Order.new(backend_url:, password:)
22
22
  order.create(
@@ -39,6 +39,25 @@ else
39
39
  end
40
40
  ```
41
41
 
42
+ ### Errors
43
+
44
+ Every request raises `Taler::RequestError` when the backend responds with
45
+ anything but 200 OK. The message includes the backend's explanation, and
46
+ the error carries the HTTP status and the response body:
47
+
48
+ ```rb
49
+ begin
50
+ order.create(amount: "KUDOS:4", summary: "Order total")
51
+ rescue Taler::RequestError => e
52
+ puts e.message # The Taler backend responded with 401: The merchant refused the request due to lack of authorization.
53
+ puts e.status # 401
54
+ puts e.body # {"code" => 2015, "hint" => "...", "detail" => "..."}
55
+ rescue Taler::Error => e
56
+ puts e.message # E.g. could not reach destination
57
+ puts e.cause # A StandardError that was raised during the request, e.g. Errno::ETIMEDOUT
58
+ end
59
+ ```
60
+
42
61
  Read more in the official documentation:
43
62
 
44
63
  - https://rubydoc.info/gems/taler
data/lib/taler/client.rb CHANGED
@@ -18,6 +18,7 @@ module Taler
18
18
  # Obtain an access token to authenticate all other API calls.
19
19
  #
20
20
  # @return [String]
21
+ # @raise [RequestError] If the backend rejects the request or asks for two-factor authentication.
21
22
  def request_token
22
23
  url = "#{@backend_url}/private/token"
23
24
  payload = {scope: "write"}
@@ -33,6 +34,7 @@ module Taler
33
34
  # payment.
34
35
  # @return [Hash] The resonse from the backend, usually just containing an
35
36
  # order id, e.g. `{order_id: "xxxxx"}`
37
+ # @raise [RequestError] If the backend rejects the request.
36
38
  def create_order(amount:, summary:, fulfillment_url: nil, fulfillment_message: nil)
37
39
  url = "#{@backend_url}/private/orders"
38
40
  order = {
@@ -53,6 +55,7 @@ module Taler
53
55
 
54
56
  # @param order_id [String]
55
57
  # @return [Hash] The order status returned by the backend.
58
+ # @raise [RequestError] If the backend rejects the request.
56
59
  def fetch_order(order_id)
57
60
  url = "#{@backend_url}/private/orders/#{order_id}"
58
61
  request(url)
@@ -63,6 +66,7 @@ module Taler
63
66
  # @param reason [String] Why are you refunding?
64
67
  #
65
68
  # @return [Hash] Response from the merchant backend.
69
+ # @raise [RequestError] If the backend rejects the request.
66
70
  def refund_order(order_id, refund:, reason:)
67
71
  url = "#{@backend_url}/private/orders/#{order_id}/refund"
68
72
  payload = {refund:, reason:}
@@ -76,16 +80,12 @@ module Taler
76
80
  "secret-token:#{@password}"
77
81
  end
78
82
 
79
- # @return [String]
80
- def access_token
81
- @access_token ||= request_token
82
- end
83
-
84
83
  # @param url [String]
85
84
  # @param token [String]
86
85
  # @param payload [Hash]
87
- # @return [String]
88
- def request(url, token: access_token, payload: nil)
86
+ # @return [Hash] The parsed JSON response.
87
+ # @raise [RequestError] If the backend responds with anything but 200 OK.
88
+ def request(url, token: auth_token, payload: nil)
89
89
  uri = URI(url)
90
90
  headers = {
91
91
  "Authorization" => "Bearer #{token}",
@@ -93,16 +93,30 @@ module Taler
93
93
  "User-Agent" => "Taler Ruby"
94
94
  }
95
95
 
96
+ response = http_request(uri, headers, payload)
97
+
98
+ # The merchant API answers 200 to every request this gem makes.
99
+ # Anything else explains a failure, including 202 which asks for
100
+ # two-factor authentication on the token endpoint.
101
+ # See: https://docs.taler.net/core/api-merchant.html
102
+ raise RequestError.new(response) unless response.is_a?(Net::HTTPOK)
103
+
104
+ JSON.parse(response.body)
105
+ end
106
+
107
+ # @param uri [URI]
108
+ # @param headers [Hash]
109
+ # @param payload [Hash]
110
+ # @return [Net::HTTPResponse]
111
+ def http_request(uri, headers, payload)
96
112
  if payload.nil?
97
- body = Net::HTTP.get(uri, headers)
113
+ Net::HTTP.get_response(uri, headers)
98
114
  else
99
115
  headers["Content-Type"] = "application/json"
100
- data = JSON.dump(payload)
101
- response = Net::HTTP.post(uri, data, headers)
102
- body = response.body
116
+ Net::HTTP.post(uri, JSON.dump(payload), headers)
103
117
  end
104
-
105
- JSON.parse(body)
118
+ rescue => e
119
+ raise Error, e.message
106
120
  end
107
121
  end
108
122
  end
data/lib/taler/order.rb CHANGED
@@ -27,6 +27,7 @@ module Taler
27
27
  # @param fulfillment_message [String] Text to display to the user after
28
28
  # payment.
29
29
  # @return [String] The id of the new order.
30
+ # @raise [RequestError] If the backend rejects the request.
30
31
  def create(amount:, summary:, fulfillment_url: nil, fulfillment_message: nil)
31
32
  response = @client.create_order(
32
33
  amount:, summary:, fulfillment_url:, fulfillment_message:
@@ -57,6 +58,7 @@ module Taler
57
58
  # Any simple type supported by JSON.
58
59
  # @example
59
60
  # fetch("order_status") #=> "unpaid"
61
+ # @raise [RequestError] If the backend rejects the request.
60
62
  def fetch(key)
61
63
  reload unless @status
62
64
  @status.fetch(key)
@@ -68,6 +70,7 @@ module Taler
68
70
  # user interaction, you want to call this.
69
71
  #
70
72
  # @return [Hash] The order status returned by the backend.
73
+ # @raise [RequestError] If the backend rejects the request.
71
74
  def reload
72
75
  @status = @client.fetch_order(@id)
73
76
  end
@@ -78,6 +81,7 @@ module Taler
78
81
  # @param reason [String] Why are you refunding?
79
82
  #
80
83
  # @return [Hash] Response from the merchant backend.
84
+ # @raise [RequestError] If the backend rejects the request.
81
85
  def refund(refund:, reason:)
82
86
  @client.refund_order(@id, refund:, reason:)
83
87
  end
data/lib/taler/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Taler
4
4
  # @return [String]
5
- VERSION = "0.3.0"
5
+ VERSION = "0.5.0"
6
6
  end
data/lib/taler.rb CHANGED
@@ -8,4 +8,42 @@ require_relative "taler/version"
8
8
  module Taler
9
9
  # Base error class for errors raised in this module
10
10
  class Error < StandardError; end
11
+
12
+ # Raised when the merchant backend answers with anything but 200 OK.
13
+ #
14
+ # The backend explains errors with a JSON body like
15
+ # `{"code": 2015, "hint": "...", "detail": "..."}`. The hint is used
16
+ # as the error message. A body that is not JSON, like an HTML page
17
+ # from a proxy, is kept as text.
18
+ class RequestError < Error
19
+ # @return [Integer] The HTTP status code of the response.
20
+ attr_reader :status
21
+
22
+ # @return [Hash, String] The parsed JSON body or the raw body.
23
+ attr_reader :body
24
+
25
+ # @param response [Net::HTTPResponse]
26
+ def initialize(response)
27
+ @status = response.code.to_i
28
+ @body = error_body(response.body)
29
+ super("The Taler backend responded with #{status}: #{summary}")
30
+ end
31
+
32
+ private
33
+
34
+ # @return [String]
35
+ def summary
36
+ return body.fetch("hint") if body.is_a?(Hash) && body.key?("hint")
37
+
38
+ body.to_s.strip
39
+ end
40
+
41
+ # @param body [String]
42
+ # @return [Hash, String] The parsed JSON body, or the raw body if it isn't JSON.
43
+ def error_body(body)
44
+ JSON.parse(body)
45
+ rescue JSON::ParserError
46
+ body
47
+ end
48
+ end
11
49
  end
data/sig/taler.rbs CHANGED
@@ -6,6 +6,31 @@ module Taler
6
6
  class Error < StandardError
7
7
  end
8
8
 
9
+ # Raised when the merchant backend answers with anything but 200 OK.
10
+ #
11
+ # The backend explains errors with a JSON body like
12
+ # `{"code": 2015, "hint": "...", "detail": "..."}`. The hint is used
13
+ # as the error message. A body that is not JSON, like an HTML page
14
+ # from a proxy, is kept as text.
15
+ class RequestError < Taler::Error
16
+ # sord warn - Net::HTTPResponse wasn't able to be resolved to a constant in this project
17
+ # _@param_ `response`
18
+ def initialize: (Net::HTTPResponse response) -> void
19
+
20
+ def summary: () -> String
21
+
22
+ # _@param_ `body`
23
+ #
24
+ # _@return_ — The parsed JSON body, or the raw body if it isn't JSON.
25
+ def error_body: (String body) -> (::Hash[untyped, untyped] | String)
26
+
27
+ # _@return_ — The HTTP status code of the response.
28
+ attr_reader status: Integer
29
+
30
+ # _@return_ — The parsed JSON body or the raw body.
31
+ attr_reader body: (::Hash[untyped, untyped] | String)
32
+ end
33
+
9
34
  # Order representation with convenient access to the merchant API.
10
35
  class Order
11
36
  # Connect an Order to the Taler merchant backend.
@@ -136,13 +161,22 @@ module Taler
136
161
 
137
162
  def auth_token: () -> String
138
163
 
139
- def access_token: () -> String
140
-
141
164
  # _@param_ `url`
142
165
  #
143
166
  # _@param_ `token`
144
167
  #
145
168
  # _@param_ `payload`
146
- def request: (String url, ?token: String, ?payload: ::Hash[untyped, untyped]?) -> String
169
+ #
170
+ # _@return_ — The parsed JSON response.
171
+ def request: (String url, ?token: String, ?payload: ::Hash[untyped, untyped]?) -> ::Hash[untyped, untyped]
172
+
173
+ # sord warn - URI wasn't able to be resolved to a constant in this project
174
+ # sord warn - Net::HTTPResponse wasn't able to be resolved to a constant in this project
175
+ # _@param_ `uri`
176
+ #
177
+ # _@param_ `headers`
178
+ #
179
+ # _@param_ `payload`
180
+ def http_request: (URI uri, ::Hash[untyped, untyped] headers, ::Hash[untyped, untyped] payload) -> Net::HTTPResponse
147
181
  end
148
182
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maikel Linke