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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0dd40f2e54bb8bd4ae807d120d9cf791d5a943302783fd25d946daa6e109fef3
4
- data.tar.gz: '091f77972ba7ad1331ee7f24f620e276801434ee4c58990903fc2d999d33c111'
3
+ metadata.gz: 8df11c5d1490ba7df19f3133dfe586f175803ab74ff0af9609a68423c72911f1
4
+ data.tar.gz: 14cbb35ef55afd129435e3607f8f4d04605398702966924fdda20b0cf1ed2afd
5
5
  SHA512:
6
- metadata.gz: 63a5545a8ede6f7ed5ffd8c027b5c0c93ad59925e409387b8a900375d3ffe9b8e7aacfa51b9d6d9dec98574e786601de8e43ceba61c1f12d6cf5e64916bcd477
7
- data.tar.gz: 8cdfd01d74f200368c4d76db990271e6c5bcb26079cef9e296a2230c9ba0b7ed758e2bf051429453f4e10ab45e9c1314bb2aed963f1ee5a04ab9628bd38d652c
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
- # Composable::Client
1
+ # composable-client
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/composable/client`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- TODO: Delete this and the text above, and describe your gem
7
+ ## Implement a request
6
8
 
7
- ## Installation
9
+ ```ruby
10
+ require "composable/client"
8
11
 
9
- Add this line to your application's Gemfile:
12
+ class CatalogClient < Composable::Client::Base
13
+ attribute :endpoint, type: :string, default: "https://api.example.test"
10
14
 
11
- ```ruby
12
- gem 'composable-client'
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
- And then execute:
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
- $ bundle install
36
+ ## Inputs and lifecycle
18
37
 
19
- Or install it yourself as:
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
- $ gem install composable-client
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
- ## Usage
56
+ ## Transport security
24
57
 
25
- TODO: Write usage instructions here
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
- ## Development
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
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
70
+ ## Send JSON and interpret HTTP status
32
71
 
33
- ## Contributing
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
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/composable-client. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/composable-client/blob/master/CODE_OF_CONDUCT.md).
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
- ## License
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
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
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
- ## Code of Conduct
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
- Everyone interacting in the Composable::Client project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/composable-client/blob/master/CODE_OF_CONDUCT.md).
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
- class << self
11
- def inherited(subclass)
12
- super
13
- subclass.prepend(Core::Command)
14
- subclass.attribute :endpoint, type: :string, default: -> { raise "endpoint is required" }
15
- subclass.attribute :payload, default: {}
16
- subclass.attribute :headers, default: {}
17
- subclass.attribute :basic_auth, default: {}
18
- end
19
- end
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
@@ -10,7 +10,7 @@ module Composable
10
10
  module VERSION
11
11
  MAJOR = 0
12
12
  MINOR = 0
13
- TINY = 13
13
+ TINY = 14
14
14
  PRE = nil
15
15
 
16
16
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -7,8 +7,13 @@ module Composable
7
7
 
8
8
  def parse_uri(uri)
9
9
  URI.parse(uri).tap do |uri|
10
- raise "Invalid URL: #{uri}" unless uri.host
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?
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
3
4
  require "uri"
4
5
  require "net/http"
5
6
  require "composable/core"
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.13
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: 2025-08-09 00:00:00.000000000 Z
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.13
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.13
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.7.0
56
+ version: 3.2.0
57
57
  required_rubygems_version: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="