composable-client 0.0.13 → 0.0.14
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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +141 -22
- data/lib/composable/client/base.rb +15 -11
- data/lib/composable/client/gem_version.rb +1 -1
- data/lib/composable/client/http_verb_methods.rb +13 -2
- data/lib/composable/client.rb +1 -0
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8df11c5d1490ba7df19f3133dfe586f175803ab74ff0af9609a68423c72911f1
|
|
4
|
+
data.tar.gz: 14cbb35ef55afd129435e3607f8f4d04605398702966924fdda20b0cf1ed2afd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2ee5510644d5c9fca86cd8e1c0e654f196129c3719f3ccfdc88de22455772670fca9d96a7c90f8341f4c939bae00a92bd50d3be1593d63190dd8fefda3280f4
|
|
7
|
+
data.tar.gz: b5e0effbf438e1cdc2f05b7fe6b8882c3557b9ef49e7291dd6519188660dfda8701ca7bdf6c16171a552d9c754a369742486720452ff28ae3ed6bc6a76507689
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
- Reject infinite and NaN timeout values before starting a request.
|
|
4
|
+
- Require HTTPS by default; sanitize URL failures and reject embedded credentials.
|
|
5
|
+
- Set positive timeouts, certificate/hostname verification, and explicit proxy/retry policy.
|
|
6
|
+
- Preserve inherited command results and require a request implementation.
|
|
7
|
+
- Document the API and add complete coverage with local HTTP/TLS tests.
|
|
8
|
+
|
|
3
9
|
## [0.1.0] - 2023-03-06
|
|
4
10
|
|
|
5
11
|
- Initial release
|
data/README.md
CHANGED
|
@@ -1,43 +1,162 @@
|
|
|
1
|
-
#
|
|
1
|
+
# composable-client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
HTTP clients with typed inputs, validation, lifecycle callbacks and command
|
|
4
|
+
results. Requires Ruby 3.2+ and ActiveModel/ActiveSupport 7.2+.
|
|
5
|
+
Install `gem "composable-client"`; require `composable/client`.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
## Implement a request
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
```ruby
|
|
10
|
+
require "composable/client"
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
class CatalogClient < Composable::Client::Base
|
|
13
|
+
attribute :endpoint, type: :string, default: "https://api.example.test"
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
before_request do
|
|
16
|
+
headers["Authorization"] = "Bearer #{ENV.fetch('CATALOG_API_TOKEN')}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def request
|
|
22
|
+
get("items")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Configure CATALOG_API_TOKEN before calling the client.
|
|
27
|
+
# operation = CatalogClient.call
|
|
28
|
+
# response = operation.result
|
|
13
29
|
```
|
|
14
30
|
|
|
15
|
-
|
|
31
|
+
Implement `request`; omitting it raises `NotImplementedError`. The private helpers
|
|
32
|
+
`get`, `post`, `put`, `patch`, and `delete` return a `Net::HTTPResponse`. They append
|
|
33
|
+
`/path` to `endpoint`; use an endpoint without a trailing slash and paths without
|
|
34
|
+
a leading slash. The response becomes `.result`.
|
|
16
35
|
|
|
17
|
-
|
|
36
|
+
## Inputs and lifecycle
|
|
18
37
|
|
|
19
|
-
|
|
38
|
+
| Attribute | Default | Meaning |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| `endpoint` | Required when requesting | HTTP(S) base URL without embedded credentials |
|
|
41
|
+
| `payload` | `{}` | JSON body, sent when present |
|
|
42
|
+
| `headers` | `{}` | Request headers |
|
|
43
|
+
| `basic_auth` | `{}` | String keys `"user"` and `"password"` |
|
|
44
|
+
| `allow_http` | `false` | Explicitly allow unencrypted HTTP |
|
|
45
|
+
| `open_timeout` | `10` | Positive, finite connection timeout in seconds |
|
|
46
|
+
| `read_timeout` | `30` | Positive, finite read timeout in seconds |
|
|
47
|
+
| `write_timeout` | `30` | Positive, finite write timeout in seconds |
|
|
20
48
|
|
|
21
|
-
|
|
49
|
+
Validation precedes `before_request`, `around_request`, and `after_request`.
|
|
50
|
+
Callbacks can update headers/payload. An aborted callback or a `false` request
|
|
51
|
+
result records failure. `.call` returns the command, and `.call!` raises on errors.
|
|
52
|
+
Timeout, TLS and transport errors propagate unless an application `rescue_from`
|
|
53
|
+
handler translates them into errors. A received HTTP 4xx/5xx response is still a
|
|
54
|
+
completed request; interpret status codes in your client/application.
|
|
22
55
|
|
|
23
|
-
##
|
|
56
|
+
## Transport security
|
|
24
57
|
|
|
25
|
-
|
|
58
|
+
HTTPS validates the peer certificate through Ruby/OpenSSL. Plain HTTP needs
|
|
59
|
+
`allow_http: true`, intended for explicitly controlled endpoints such as a local
|
|
60
|
+
test server. URLs with credentials, unsupported schemes, missing hosts or invalid
|
|
61
|
+
syntax are rejected without including the URL in the error. Automatic retries,
|
|
62
|
+
redirect following and environment-provided HTTP proxies are disabled.
|
|
26
63
|
|
|
27
|
-
|
|
64
|
+
Use endpoints defined by trusted application configuration. This gem is not an
|
|
65
|
+
SSRF firewall: it does not restrict DNS resolution, private addresses or paths
|
|
66
|
+
supplied by users. Enforce allowed destinations and network egress when accepting
|
|
67
|
+
user-configurable integrations. Never log auth headers, secrets or entire payloads.
|
|
28
68
|
|
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
30
69
|
|
|
31
|
-
|
|
70
|
+
## Send JSON and interpret HTTP status
|
|
32
71
|
|
|
33
|
-
|
|
72
|
+
This client creates an item and treats non-success HTTP responses as command
|
|
73
|
+
failures. The endpoint is synthetic: replace it with trusted configuration before
|
|
74
|
+
making a real request.
|
|
34
75
|
|
|
35
|
-
|
|
76
|
+
```ruby
|
|
77
|
+
class CreateCatalogItem < Composable::Client::Base
|
|
78
|
+
attribute :endpoint, type: :string, default: "https://api.example.test"
|
|
79
|
+
attribute :name, type: :string
|
|
80
|
+
validates :name, presence: true
|
|
81
|
+
|
|
82
|
+
before_request do
|
|
83
|
+
self.payload = { name: name }
|
|
84
|
+
headers["Accept"] = "application/json"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def request
|
|
90
|
+
response = post("items")
|
|
91
|
+
errors.add(:base, "Catalog rejected the request") unless response.is_a?(Net::HTTPSuccess)
|
|
92
|
+
response
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
operation = CreateCatalogItem.call(name: "Example item", read_timeout: 5)
|
|
97
|
+
if operation.success?
|
|
98
|
+
item = JSON.parse(operation.result.body)
|
|
99
|
+
else
|
|
100
|
+
messages = operation.errors.full_messages
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
An HTTP error still leaves the response in `result` in this example. Checking
|
|
105
|
+
`success?` before parsing also avoids treating an error page as successful JSON.
|
|
106
|
+
Successful responses can still contain malformed JSON; handle that separately
|
|
107
|
+
according to the remote API's contract.
|
|
108
|
+
|
|
109
|
+
## Handle transport failures
|
|
36
110
|
|
|
37
|
-
|
|
111
|
+
```ruby
|
|
112
|
+
class ResilientCatalogClient < CatalogClient
|
|
113
|
+
rescue_from Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout do
|
|
114
|
+
errors.add(:base, "Catalog request timed out")
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# With the endpoint and token configured:
|
|
119
|
+
# operation = ResilientCatalogClient.call(open_timeout: 2, read_timeout: 5)
|
|
120
|
+
# operation.failure? # => true if a handled timeout occurred
|
|
121
|
+
```
|
|
38
122
|
|
|
39
|
-
|
|
123
|
+
A handled transport error may have no response, so inspect status before reading
|
|
124
|
+
`result`. `.call!` propagates transport exceptions. No retry is performed; decide
|
|
125
|
+
at the application level whether the operation is safe to retry.
|
|
126
|
+
|
|
127
|
+
## Supply basic authentication or use a local server
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
# Supply credentials from your application's secret configuration.
|
|
131
|
+
operation = CreateCatalogItem.call(
|
|
132
|
+
name: "Example item",
|
|
133
|
+
basic_auth: {
|
|
134
|
+
"user" => ENV.fetch("CATALOG_USERNAME"),
|
|
135
|
+
"password" => ENV.fetch("CATALOG_PASSWORD")
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The earlier `CatalogClient` adds a Bearer header in its callback; use
|
|
141
|
+
`CreateCatalogItem` above for Basic authentication. For local development, supply
|
|
142
|
+
an explicitly allowed HTTP endpoint to a client such as:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class LocalCatalogClient < Composable::Client::Base
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def request
|
|
149
|
+
get("items")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Requires a server running on this address.
|
|
154
|
+
# LocalCatalogClient.call(endpoint: "http://127.0.0.1:3001", allow_http: true)
|
|
155
|
+
```
|
|
40
156
|
|
|
41
|
-
|
|
157
|
+
Headers are applied after `basic_auth`; an explicit `Authorization` header takes
|
|
158
|
+
precedence. Implement `request`, not `call`, in client subclasses. Endpoint
|
|
159
|
+
defaults configured in an application base client are inherited by its children,
|
|
160
|
+
and payload/header defaults are independent for each instance.
|
|
42
161
|
|
|
43
|
-
|
|
162
|
+
See [testing](../docs/testing.md) for real local HTTP/TLS tests and coverage.
|
|
@@ -7,28 +7,32 @@ module Composable
|
|
|
7
7
|
include Callbacks
|
|
8
8
|
include HTTPVerbMethods
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
prepend Core::Command
|
|
11
|
+
|
|
12
|
+
attribute :endpoint, type: :string, default: -> { raise "endpoint is required" }
|
|
13
|
+
attribute :payload, default: {}
|
|
14
|
+
attribute :headers, default: {}
|
|
15
|
+
attribute :basic_auth, default: {}
|
|
16
|
+
attribute :allow_http, type: :boolean, default: false
|
|
17
|
+
attribute :open_timeout, type: :float, default: 10
|
|
18
|
+
attribute :read_timeout, :write_timeout, type: :float, default: 30
|
|
19
|
+
validates :open_timeout, :read_timeout, :write_timeout,
|
|
20
|
+
numericality: { greater_than: 0, less_than: Float::INFINITY }
|
|
20
21
|
|
|
21
22
|
def call
|
|
22
23
|
return unless valid?
|
|
23
24
|
|
|
24
|
-
run_callbacks :request do
|
|
25
|
+
completed = run_callbacks :request do
|
|
25
26
|
@result = request
|
|
26
27
|
end
|
|
28
|
+
errors.add(:base, :invalid) if completed == false
|
|
29
|
+
@result
|
|
27
30
|
end
|
|
28
31
|
|
|
29
32
|
private
|
|
30
33
|
|
|
31
34
|
def request
|
|
35
|
+
raise Core::Command::NotImplementedError, "implement #request in your client"
|
|
32
36
|
end
|
|
33
37
|
end
|
|
34
38
|
end
|
|
@@ -7,8 +7,13 @@ module Composable
|
|
|
7
7
|
|
|
8
8
|
def parse_uri(uri)
|
|
9
9
|
URI.parse(uri).tap do |uri|
|
|
10
|
-
|
|
10
|
+
unless uri.host && %w[http https].include?(uri.scheme) && uri.userinfo.nil?
|
|
11
|
+
raise ArgumentError, "endpoint must be an HTTP(S) URL without credentials"
|
|
12
|
+
end
|
|
13
|
+
raise ArgumentError, "HTTP requires allow_http: true" if uri.scheme == "http" && !allow_http
|
|
11
14
|
end
|
|
15
|
+
rescue URI::InvalidURIError
|
|
16
|
+
raise ArgumentError, "endpoint is not a valid URL", cause: nil
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def get(path)
|
|
@@ -33,8 +38,14 @@ module Composable
|
|
|
33
38
|
|
|
34
39
|
def make_request(method, path)
|
|
35
40
|
uri = parse_uri("#{endpoint}/#{path}")
|
|
36
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port, nil)
|
|
37
42
|
http.use_ssl = uri.scheme == "https"
|
|
43
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
44
|
+
http.verify_hostname = true
|
|
45
|
+
http.open_timeout = open_timeout
|
|
46
|
+
http.read_timeout = read_timeout
|
|
47
|
+
http.write_timeout = write_timeout
|
|
48
|
+
http.max_retries = 0
|
|
38
49
|
|
|
39
50
|
request = Net::HTTP.const_get(method.to_s.capitalize).new(uri).tap do |r|
|
|
40
51
|
r.body = payload.to_json if payload.present?
|
data/lib/composable/client.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: composable-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jairo Vazquez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-10-07 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: composable-core
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.14
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.14
|
|
26
26
|
description: Composable Client provides CURD operations for a REST API
|
|
27
27
|
email:
|
|
28
28
|
- jairovm20@gmail.com
|
|
@@ -53,7 +53,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
53
53
|
requirements:
|
|
54
54
|
- - ">="
|
|
55
55
|
- !ruby/object:Gem::Version
|
|
56
|
-
version: 2.
|
|
56
|
+
version: 3.2.0
|
|
57
57
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - ">="
|