sofia 0.1.1 → 0.1.3
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 +16 -0
- data/CLAUDE.md +53 -0
- data/README.md +67 -1
- data/lib/sofia/adapter/net_http.rb +28 -14
- data/lib/sofia/adapter/soren.rb +101 -0
- data/lib/sofia/adapter.rb +1 -0
- data/lib/sofia/client.rb +9 -4
- data/lib/sofia/defaults/timeouts.rb +12 -0
- data/lib/sofia/defaults.rb +8 -0
- data/lib/sofia/options.rb +29 -0
- data/lib/sofia/request.rb +18 -14
- data/lib/sofia/response.rb +2 -2
- data/lib/sofia/types/client/adapter.rb +43 -0
- data/lib/sofia/types/client/base_url.rb +35 -0
- data/lib/sofia/types/client/body.rb +62 -0
- data/lib/sofia/types/client/headers.rb +58 -0
- data/lib/sofia/types/client/options.rb +33 -0
- data/lib/sofia/types/client/params.rb +57 -0
- data/lib/sofia/types/client/path.rb +30 -0
- data/lib/sofia/types/client.rb +16 -0
- data/lib/sofia/types/options/timeout/base.rb +34 -0
- data/lib/sofia/types/options/timeout/connection.rb +12 -0
- data/lib/sofia/types/options/timeout/read.rb +12 -0
- data/lib/sofia/types/options/timeout/write.rb +12 -0
- data/lib/sofia/types/options/timeout.rb +15 -0
- data/lib/sofia/types/options.rb +10 -0
- data/lib/sofia/types.rb +2 -6
- data/lib/sofia/version.rb +1 -1
- data/lib/sofia.rb +7 -5
- data/sorbet/rbi/gems/soren@0.1.2.rbi +934 -0
- metadata +36 -8
- data/lib/sofia/types/adapter.rb +0 -39
- data/lib/sofia/types/base_url.rb +0 -33
- data/lib/sofia/types/body.rb +0 -60
- data/lib/sofia/types/headers.rb +0 -56
- data/lib/sofia/types/params.rb +0 -56
- data/lib/sofia/types/path.rb +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b5f47033833ca64f26bc2c6c9e7807dd68c2eae26c5f9e425390f9bf08d7b3d
|
|
4
|
+
data.tar.gz: 77fb9dbbb9106d5d3c15363bed3b218b0c6796cf8434047775de552b01e96765
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b94051c45846711ad7939161b963928443fb1a877d80fe1b786eba4a888d78b7f48b00deb31edf9baaf8c0f46fd063cc91805ef6b92c863c204b3f48cce8e134
|
|
7
|
+
data.tar.gz: f73c2498619c4bd113c5aa79ef113b6e99152b8c0432f52ac469ea7b6201cfac42b671c03ee062329742f590721e50b28ed0bf4e39a984251435152fb0a102b0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.3] - 2026-04-17
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Soren gem updated to 0.1.3
|
|
8
|
+
|
|
9
|
+
## [0.1.2] - 2026-04-17
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Soren adapter (`adapter: :soren`) — a second pluggable HTTP backend backed by the [soren](https://github.com/KubaJadrzak/soren) gem.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- `Sofia::Response#headers` now returns `Hash[String, Array[String]]` instead of `Hash[String, String]`, correctly preserving multi-value headers such as `Set-Cookie`.
|
|
18
|
+
|
|
3
19
|
## [0.1.0] - 2025-09-25
|
|
4
20
|
|
|
5
21
|
- Initial release
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bin/setup # Install dependencies
|
|
9
|
+
rake test # Run all tests
|
|
10
|
+
rake rubocop # Lint with RuboCop
|
|
11
|
+
bundle exec srb tc # Sorbet type checking (run after every change)
|
|
12
|
+
bin/console # IRB prompt with gem loaded
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run a single test file:
|
|
16
|
+
```bash
|
|
17
|
+
bundle exec ruby -Ilib:test test/sofia/client_test.rb
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
Sofia is a Ruby gem providing a lightweight HTTP client abstraction with pluggable adapters (similar to Faraday). Ruby >= 3.1 required. Uses Sorbet for static type checking (`# typed: strict` throughout).
|
|
23
|
+
|
|
24
|
+
**Request lifecycle:**
|
|
25
|
+
1. `Sofia.new(base_url:, adapter:)` → `Sofia::Client`
|
|
26
|
+
2. User calls `.get`, `.post`, etc. with a block that configures a `Request`
|
|
27
|
+
3. Client delegates to the adapter's `#call(request:)` method
|
|
28
|
+
4. Adapter returns a `Sofia::Response`
|
|
29
|
+
|
|
30
|
+
**Key classes:**
|
|
31
|
+
- `Sofia::Client` — stores base URL + adapter; exposes HTTP verb methods
|
|
32
|
+
- `Sofia::Request` — mutable DSL object; builds full URL from base + path + params
|
|
33
|
+
- `Sofia::Response` — immutable result; `#body` parses JSON, `#success?` checks 2xx
|
|
34
|
+
- `Sofia::Adapter::Base` — abstract; subclasses implement `#call(request:) → Response`
|
|
35
|
+
- `Sofia::Adapter::NetHTTP` — only current adapter; uses stdlib `Net::HTTP` with SSL auto-detection
|
|
36
|
+
|
|
37
|
+
**Type wrappers** (`lib/sofia/types/`) validate and normalize all inputs at construction time (fail-fast). `BaseUrl` auto-prepends `https://`; `Path` ensures leading `/`; `Headers` defaults to JSON content/accept types; `Body` strips nil values recursively.
|
|
38
|
+
|
|
39
|
+
**Error hierarchy** all inherit from `Sofia::Error::Base < StandardError`: `ArgumentError`, `ConnectionFailed`, `SSLError`, `TimeoutError`, `ParserError`.
|
|
40
|
+
|
|
41
|
+
## Adding a New Adapter
|
|
42
|
+
|
|
43
|
+
1. Create `lib/sofia/adapter/your_adapter.rb` subclassing `Sofia::Adapter::Base`
|
|
44
|
+
2. Implement `sig { override.params(request: Sofia::Request).returns(Sofia::Response) }` on `#call`
|
|
45
|
+
3. Register the adapter symbol in `lib/sofia/types/adapter.rb`
|
|
46
|
+
|
|
47
|
+
## Testing Notes
|
|
48
|
+
|
|
49
|
+
Tests mix real HTTP calls (against httpbin.org) in `test/sofia/client_test.rb` with unit tests for type wrappers. FactoryBot factories live in `test/factories/`.
|
|
50
|
+
|
|
51
|
+
## RuboCop
|
|
52
|
+
|
|
53
|
+
Config at `.rubocop.yml` uses `EnabledByDefault: true` (strict). Key limits: method 40 lines, class 200 lines, cyclomatic complexity 12. Trailing commas required in multiline literals.
|
data/README.md
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
# Sofia
|
|
2
2
|
|
|
3
|
-
This is a personal project created for self-learning purposes. The goal is to create a
|
|
3
|
+
This is a personal project created for self-learning purposes. The goal is to create a simple HTTP client abstraction layer, similar to `Faraday`. At the current moment `Sofia` supports only `NetHTTP` as adapter, and only with `Content-Type: JSON` and default configuration. While basic, at the current moment `Sofia` is implemented into my other project [Shopik](https://github.com/KubaJadrzak/Shopik) and allows to perform HTTP requests correctly. I will add more functionality with time. The goal is to ultimately create my own HTTP client as well.
|
|
4
|
+
|
|
5
|
+
# How it works
|
|
6
|
+
|
|
7
|
+
In order to perform a request with `Sofia` you need to initialize an instance of `client` class by providing `base_url` and `adapter`. At the current moment the only supported adapter is `NetHTTP` and it will be used by default.
|
|
8
|
+
```rb
|
|
9
|
+
@client = Sofia.new(base_url: base_url, adapter: adapter)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
You can perform a request via method `send` on the instance of `client` class. You need to provide http request type (for now only `get`, `post`, `put`, `patch`, `delete` are supported) as method argument as well as block of code with configuration, for example:
|
|
13
|
+
```rb
|
|
14
|
+
response = @client.send(method) do |req|
|
|
15
|
+
req.path = path
|
|
16
|
+
req.headers['Accept'] = 'application/json'
|
|
17
|
+
req.headers['Authorization'] = "Basic #{encoded_credentials}"
|
|
18
|
+
req.body = body if body
|
|
19
|
+
end
|
|
20
|
+
```
|
|
21
|
+
It is a good practice to rescue errors which can be thrown by `Sofia`. The response codes `400-499` and `500-599` are not errors, instead you need to handle these on your own. This is an example of the entire flow that allows you to make a request based on my [Shopik](https://github.com/KubaJadrzak/Shopik) project where `Sofia` is implement as HTTP client abstraction layer:
|
|
22
|
+
```rb
|
|
23
|
+
class EspagoClient
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
base_url = ENV.fetch('ESPAGO_BASE_URL')
|
|
27
|
+
@user = Rails.application.credentials.dig(:espago, :app_id)
|
|
28
|
+
@password = Rails.application.credentials.dig(:espago, :password)
|
|
29
|
+
|
|
30
|
+
@client = Sofia.new(base_url: base_url)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def send(path, body: nil, method: :get)
|
|
34
|
+
response = @client.send(method) do |req|
|
|
35
|
+
req.path = path
|
|
36
|
+
req.headers['Accept'] = 'application/vnd.espago.v3+json'
|
|
37
|
+
req.headers['Authorization'] = "Basic #{encoded_credentials}"
|
|
38
|
+
req.body = body if body
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
::Response.new(
|
|
42
|
+
connected: true,
|
|
43
|
+
status: response.status,
|
|
44
|
+
body: response.body,
|
|
45
|
+
)
|
|
46
|
+
rescue Sofia::Error::TimeoutError
|
|
47
|
+
::Response.new(connected: false, body: { error: 'timeout' })
|
|
48
|
+
rescue Sofia::Error::ConnectionFailed
|
|
49
|
+
::Response.new(connected: false, body: { error: 'connection_failed' })
|
|
50
|
+
rescue Sofia::Error::SSLError
|
|
51
|
+
::Response.new(connected: false, body: { error: 'ssl_error' })
|
|
52
|
+
rescue Sofia::Error::ParserError
|
|
53
|
+
::Response.new(connected: false, body: { error: 'parsing_error' })
|
|
54
|
+
rescue URI::InvalidURIError, URI::BadURIError
|
|
55
|
+
::Response.new(connected: false, body: { error: 'invalid_uri' })
|
|
56
|
+
rescue StandardError
|
|
57
|
+
::Response.new(connected: false, body: { error: 'unexpected_error' })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
#: -> String
|
|
63
|
+
def encoded_credentials
|
|
64
|
+
Base64.strict_encode64("#{@user}:#{@password}")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
|
|
4
70
|
|
|
5
71
|
## Development
|
|
6
72
|
|
|
@@ -17,34 +17,48 @@ module Sofia
|
|
|
17
17
|
# @override
|
|
18
18
|
#: (Sofia::Request request) -> Sofia::Response
|
|
19
19
|
def call(request)
|
|
20
|
-
uri =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
uri = parse_uri(request.url)
|
|
21
|
+
http = configure_http(uri, request)
|
|
22
|
+
net_req = build_request(uri, request)
|
|
23
|
+
response = perform_request(http, net_req)
|
|
24
|
+
adapt_response(response, request)
|
|
25
|
+
end
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
private
|
|
26
28
|
|
|
29
|
+
#: (String url) -> URI::HTTP
|
|
30
|
+
def parse_uri(url)
|
|
31
|
+
uri = URI.parse(url)
|
|
27
32
|
raise Sofia::Error::ArgumentError, 'only HTTP(S) URLs are supported' unless uri.is_a?(URI::HTTP)
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
uri
|
|
35
|
+
end
|
|
30
36
|
|
|
37
|
+
#: (URI::HTTP uri, Sofia::Request request) -> Net::HTTP
|
|
38
|
+
def configure_http(uri, request)
|
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
40
|
+
http.use_ssl = uri.scheme == 'https'
|
|
41
|
+
http.read_timeout = request.options.read_timeout.to_f
|
|
42
|
+
http.write_timeout = request.options.write_timeout.to_f
|
|
43
|
+
http.open_timeout = request.options.connection_timeout.to_f
|
|
44
|
+
http
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#: (URI::HTTP uri, Sofia::Request request) -> Net::HTTPRequest
|
|
48
|
+
def build_request(uri, request)
|
|
49
|
+
klass = Net::HTTP.const_get(request.http_method.capitalize)
|
|
50
|
+
net_req = klass.new(uri.request_uri)
|
|
31
51
|
request.headers.each { |k, v| net_req[k] = v }
|
|
32
52
|
body_hash = request.body.to_h
|
|
33
53
|
net_req.body = JSON.dump(body_hash) unless body_hash.empty? || request.http_method == :get
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
response = perform_request(http, net_req)
|
|
37
|
-
|
|
38
|
-
adapt_response(response, request)
|
|
54
|
+
net_req
|
|
39
55
|
end
|
|
40
56
|
|
|
41
|
-
private
|
|
42
|
-
|
|
43
57
|
#: (Net::HTTPResponse response, Sofia::Request request) -> Sofia::Response
|
|
44
58
|
def adapt_response(response, request)
|
|
45
59
|
Sofia::Response.new(
|
|
46
60
|
status: response.code.to_i,
|
|
47
|
-
headers: response.
|
|
61
|
+
headers: response.to_hash,
|
|
48
62
|
raw_body: response.body,
|
|
49
63
|
request: request,
|
|
50
64
|
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'soren'
|
|
5
|
+
|
|
6
|
+
module Sofia
|
|
7
|
+
module Adapter
|
|
8
|
+
class Soren < Base
|
|
9
|
+
class << self
|
|
10
|
+
|
|
11
|
+
SOREN_CONNECTION_EXCEPTIONS = T.let(
|
|
12
|
+
[
|
|
13
|
+
::Soren::Error::ConnectionError,
|
|
14
|
+
::Soren::Error::ConnectionRefused,
|
|
15
|
+
::Soren::Error::DNSFailure,
|
|
16
|
+
].freeze,
|
|
17
|
+
T::Array[T.class_of(::Soren::Error::Base)],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
SOREN_TIMEOUT_EXCEPTIONS = T.let(
|
|
21
|
+
[
|
|
22
|
+
::Soren::Error::TimeoutError,
|
|
23
|
+
::Soren::Error::ReadTimeout,
|
|
24
|
+
].freeze,
|
|
25
|
+
T::Array[T.class_of(::Soren::Error::Base)],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# @override
|
|
29
|
+
#: (Sofia::Request request) -> Sofia::Response
|
|
30
|
+
def call(request)
|
|
31
|
+
uri = parse_uri(request.url)
|
|
32
|
+
connection = build_connection(uri, request)
|
|
33
|
+
soren_req = build_request(uri, request)
|
|
34
|
+
response = perform_request(connection, soren_req)
|
|
35
|
+
adapt_response(response, request)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
#: (String url) -> URI::HTTP
|
|
41
|
+
def parse_uri(url)
|
|
42
|
+
uri = URI.parse(url)
|
|
43
|
+
raise Sofia::Error::ArgumentError, 'only HTTP(S) URLs are supported' unless uri.is_a?(URI::HTTP)
|
|
44
|
+
|
|
45
|
+
uri
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#: (URI::HTTP uri, Sofia::Request request) -> ::Soren::Connection
|
|
49
|
+
def build_connection(uri, request)
|
|
50
|
+
::Soren::Connection.new(
|
|
51
|
+
host: uri.host,
|
|
52
|
+
port: uri.port,
|
|
53
|
+
scheme: uri.scheme,
|
|
54
|
+
options: {
|
|
55
|
+
connect_timeout: request.options.connection_timeout.to_f,
|
|
56
|
+
read_timeout: request.options.read_timeout.to_f,
|
|
57
|
+
write_timeout: request.options.write_timeout.to_f,
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#: (URI::HTTP uri, Sofia::Request request) -> ::Soren::Request
|
|
63
|
+
def build_request(uri, request)
|
|
64
|
+
body_hash = request.body.to_h
|
|
65
|
+
body = body_hash.empty? || request.http_method == :get ? nil : JSON.dump(body_hash)
|
|
66
|
+
|
|
67
|
+
::Soren::Request.new(
|
|
68
|
+
method: request.http_method.to_s,
|
|
69
|
+
target: uri.request_uri,
|
|
70
|
+
headers: request.headers.to_h,
|
|
71
|
+
body: body,
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
#: (::Soren::Connection connection, ::Soren::Request soren_req) -> ::Soren::Response
|
|
76
|
+
def perform_request(connection, soren_req)
|
|
77
|
+
connection.send(soren_req)
|
|
78
|
+
rescue ::Soren::Error::SSLError => e
|
|
79
|
+
raise Sofia::Error::SSLError, e
|
|
80
|
+
rescue *SOREN_CONNECTION_EXCEPTIONS => e
|
|
81
|
+
raise Sofia::Error::ConnectionFailed, e
|
|
82
|
+
rescue *SOREN_TIMEOUT_EXCEPTIONS => e
|
|
83
|
+
raise Sofia::Error::TimeoutError, e
|
|
84
|
+
rescue ::Soren::Error::ParseError => e
|
|
85
|
+
raise Sofia::Error::ParserError, e
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
#: (::Soren::Response response, Sofia::Request request) -> Sofia::Response
|
|
89
|
+
def adapt_response(response, request)
|
|
90
|
+
Sofia::Response.new(
|
|
91
|
+
status: response.code,
|
|
92
|
+
headers: response.headers,
|
|
93
|
+
raw_body: response.body,
|
|
94
|
+
request: request,
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
data/lib/sofia/adapter.rb
CHANGED
data/lib/sofia/client.rb
CHANGED
|
@@ -5,10 +5,11 @@ module Sofia
|
|
|
5
5
|
class Client
|
|
6
6
|
HTTP_METHODS = %w[get post put patch delete].freeze
|
|
7
7
|
|
|
8
|
-
#: (base_url: untyped, adapter: untyped) -> void
|
|
9
|
-
def initialize(base_url:, adapter:)
|
|
10
|
-
@base_url = Sofia::Types::BaseUrl.new(base_url) #: Sofia::Types::BaseUrl
|
|
11
|
-
@adapter
|
|
8
|
+
#: (base_url: untyped, adapter: untyped, ?options: untyped) -> void
|
|
9
|
+
def initialize(base_url:, adapter:, options: nil)
|
|
10
|
+
@base_url = Sofia::Types::Client::BaseUrl.new(base_url) #: Sofia::Types::Client::BaseUrl
|
|
11
|
+
@adapter = Sofia::Types::Client::Adapter.new(adapter) #: Sofia::Types::Client::Adapter
|
|
12
|
+
@options = Sofia::Types::Client::Options.new(options).value #: Sofia::Options
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
#: -> String
|
|
@@ -21,6 +22,9 @@ module Sofia
|
|
|
21
22
|
@adapter.to_sym
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
#: Sofia::Options
|
|
26
|
+
attr_reader :options
|
|
27
|
+
|
|
24
28
|
#: ?{ (Request req) -> untyped } -> Response
|
|
25
29
|
def get(&) = request(:get, &)
|
|
26
30
|
|
|
@@ -45,6 +49,7 @@ module Sofia
|
|
|
45
49
|
req = Request.new(
|
|
46
50
|
http_method: http_method,
|
|
47
51
|
base_url: @base_url,
|
|
52
|
+
options: @options,
|
|
48
53
|
)
|
|
49
54
|
|
|
50
55
|
block.call(req)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Sofia
|
|
5
|
+
class Options
|
|
6
|
+
|
|
7
|
+
#: Sofia::Types::Options::Timeout::Read
|
|
8
|
+
attr_reader :read_timeout
|
|
9
|
+
|
|
10
|
+
#: Sofia::Types::Options::Timeout::Write
|
|
11
|
+
attr_reader :write_timeout
|
|
12
|
+
|
|
13
|
+
#: Sofia::Types::Options::Timeout::Connection
|
|
14
|
+
attr_reader :connection_timeout
|
|
15
|
+
|
|
16
|
+
#: (?read_timeout: untyped, ?write_timeout: untyped, ?connection_timeout: untyped) -> void
|
|
17
|
+
def initialize(read_timeout: nil, write_timeout: nil, connection_timeout: nil)
|
|
18
|
+
@read_timeout = Sofia::Types::Options::Timeout::Read.new(
|
|
19
|
+
read_timeout || Sofia::Defaults::Timeouts::READ_TIMEOUT,
|
|
20
|
+
) #: Sofia::Types::Options::Timeout::Read
|
|
21
|
+
@write_timeout = Sofia::Types::Options::Timeout::Write.new(
|
|
22
|
+
write_timeout || Sofia::Defaults::Timeouts::WRITE_TIMEOUT,
|
|
23
|
+
) #: Sofia::Types::Options::Timeout::Write
|
|
24
|
+
@connection_timeout = Sofia::Types::Options::Timeout::Connection.new(
|
|
25
|
+
connection_timeout || Sofia::Defaults::Timeouts::CONNECTION_TIMEOUT,
|
|
26
|
+
) #: Sofia::Types::Options::Timeout::Connection
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/sofia/request.rb
CHANGED
|
@@ -7,43 +7,47 @@ module Sofia
|
|
|
7
7
|
#: Symbol
|
|
8
8
|
attr_reader :http_method
|
|
9
9
|
|
|
10
|
-
#: Sofia::Types::Headers
|
|
10
|
+
#: Sofia::Types::Client::Headers
|
|
11
11
|
attr_reader :headers
|
|
12
12
|
|
|
13
|
-
#: Sofia::Types::Params
|
|
13
|
+
#: Sofia::Types::Client::Params
|
|
14
14
|
attr_reader :params
|
|
15
15
|
|
|
16
|
-
#: Sofia::Types::Body
|
|
16
|
+
#: Sofia::Types::Client::Body
|
|
17
17
|
attr_reader :body
|
|
18
18
|
|
|
19
|
-
#:
|
|
20
|
-
|
|
19
|
+
#: Sofia::Options
|
|
20
|
+
attr_reader :options
|
|
21
|
+
|
|
22
|
+
#: (http_method: Symbol, base_url: Sofia::Types::Client::BaseUrl, ?options: Sofia::Options) -> void
|
|
23
|
+
def initialize(http_method:, base_url:, options: Sofia::Options.new)
|
|
21
24
|
@http_method = http_method
|
|
22
|
-
@base_url
|
|
23
|
-
@
|
|
24
|
-
@
|
|
25
|
-
@
|
|
26
|
-
@
|
|
25
|
+
@base_url = base_url
|
|
26
|
+
@options = options #: Sofia::Options
|
|
27
|
+
@path = Sofia::Types::Client::Path.new #: Sofia::Types::Client::Path
|
|
28
|
+
@params = Sofia::Types::Client::Params.new #: Sofia::Types::Client::Params
|
|
29
|
+
@headers = Sofia::Types::Client::Headers.new #: Sofia::Types::Client::Headers
|
|
30
|
+
@body = Sofia::Types::Client::Body.new #: Sofia::Types::Client::Body
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
#: (untyped path) -> void
|
|
30
34
|
def path=(path)
|
|
31
|
-
@path = Sofia::Types::Path.new(path)
|
|
35
|
+
@path = Sofia::Types::Client::Path.new(path)
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
#: (untyped params) -> void
|
|
35
39
|
def params=(params)
|
|
36
|
-
@params = Sofia::Types::Params.new(params)
|
|
40
|
+
@params = Sofia::Types::Client::Params.new(params)
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
#: (untyped headers) -> void
|
|
40
44
|
def headers=(headers)
|
|
41
|
-
@headers = Sofia::Types::Headers.new(headers)
|
|
45
|
+
@headers = Sofia::Types::Client::Headers.new(headers)
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
#: (untyped? body) -> void
|
|
45
49
|
def body=(body)
|
|
46
|
-
@body = Sofia::Types::Body.new(body)
|
|
50
|
+
@body = Sofia::Types::Client::Body.new(body)
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
#: -> String
|
data/lib/sofia/response.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Sofia
|
|
|
7
7
|
#: Integer
|
|
8
8
|
attr_reader :status
|
|
9
9
|
|
|
10
|
-
#: Hash[String, String]
|
|
10
|
+
#: Hash[String, Array[String]]
|
|
11
11
|
attr_reader :headers
|
|
12
12
|
|
|
13
13
|
#: String?
|
|
@@ -16,7 +16,7 @@ module Sofia
|
|
|
16
16
|
#: Sofia::Request
|
|
17
17
|
attr_reader :request
|
|
18
18
|
|
|
19
|
-
#: (status: Integer, headers: Hash[String, String], raw_body: String?, request: Sofia::Request) -> void
|
|
19
|
+
#: (status: Integer, headers: Hash[String, Array[String]], raw_body: String?, request: Sofia::Request) -> void
|
|
20
20
|
def initialize(status:, headers:, raw_body:, request:)
|
|
21
21
|
@status = status
|
|
22
22
|
@headers = headers
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Sofia
|
|
5
|
+
module Types
|
|
6
|
+
module Client
|
|
7
|
+
class Adapter
|
|
8
|
+
|
|
9
|
+
#: singleton(Sofia::Adapter::Base)
|
|
10
|
+
attr_reader :klass
|
|
11
|
+
|
|
12
|
+
#: (?untyped adapter) -> void
|
|
13
|
+
def initialize(adapter = :net_http)
|
|
14
|
+
name, klass = validate_and_set(adapter || :net_http)
|
|
15
|
+
@name = T.let(name, Symbol)
|
|
16
|
+
@klass = T.let(klass, T.class_of(Sofia::Adapter::Base))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
#: -> Symbol
|
|
20
|
+
def to_sym
|
|
21
|
+
@name
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
#: (untyped) -> [Symbol, singleton(Sofia::Adapter::Base)]
|
|
27
|
+
def validate_and_set(adapter)
|
|
28
|
+
case adapter&.to_sym
|
|
29
|
+
when :net_http
|
|
30
|
+
[:net_http, Sofia::Adapter::NetHTTP]
|
|
31
|
+
when :soren
|
|
32
|
+
[:soren, Sofia::Adapter::Soren]
|
|
33
|
+
else
|
|
34
|
+
Kernel.raise Sofia::Error::ArgumentError, "unknown adapter #{adapter}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
rescue NoMethodError
|
|
38
|
+
Kernel.raise Sofia::Error::ArgumentError, "unknown adapter #{adapter}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Sofia
|
|
5
|
+
module Types
|
|
6
|
+
module Client
|
|
7
|
+
class BaseUrl
|
|
8
|
+
|
|
9
|
+
#: (untyped base_url) -> void
|
|
10
|
+
def initialize(base_url)
|
|
11
|
+
@base_url = validate_and_normalize(base_url).freeze #: String
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
#: -> String
|
|
15
|
+
def to_s
|
|
16
|
+
@base_url
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
#: (untyped) -> String
|
|
22
|
+
def validate_and_normalize(base_url)
|
|
23
|
+
unless base_url.is_a?(String) && !base_url.strip.empty?
|
|
24
|
+
raise Sofia::Error::ArgumentError, 'base_url must be a non-empty String'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
normalized = base_url.chomp('/')
|
|
28
|
+
return normalized if normalized.start_with?('http://', 'https://')
|
|
29
|
+
|
|
30
|
+
"https://#{normalized}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Sofia
|
|
5
|
+
module Types
|
|
6
|
+
module Client
|
|
7
|
+
class Body
|
|
8
|
+
|
|
9
|
+
#: Hash[String, untyped]
|
|
10
|
+
attr_reader :body
|
|
11
|
+
|
|
12
|
+
#: (?untyped? body) -> void
|
|
13
|
+
def initialize(body = nil)
|
|
14
|
+
@body = validate_and_normalize(body) #: Hash[String, untyped]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: -> Hash[String, untyped]
|
|
18
|
+
def to_h
|
|
19
|
+
@body.dup
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
#: (untyped? body) -> Hash[String, untyped]
|
|
25
|
+
def validate_and_normalize(body)
|
|
26
|
+
return {} unless body
|
|
27
|
+
|
|
28
|
+
Kernel.raise Sofia::Error::ArgumentError, 'body must be a Hash' unless body.is_a?(Hash)
|
|
29
|
+
body.each_with_object({}) do |(key, value), acc|
|
|
30
|
+
next if value.nil?
|
|
31
|
+
|
|
32
|
+
acc[key.to_s] =
|
|
33
|
+
case value
|
|
34
|
+
when Hash
|
|
35
|
+
validate_and_normalize(value)
|
|
36
|
+
when Array
|
|
37
|
+
validate_and_normalize_array(value)
|
|
38
|
+
else
|
|
39
|
+
value
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#: (Array[untyped]) -> Array[untyped]
|
|
45
|
+
def validate_and_normalize_array(array)
|
|
46
|
+
array.each_with_object([]) do |value, acc|
|
|
47
|
+
next if value.nil?
|
|
48
|
+
|
|
49
|
+
acc << case value
|
|
50
|
+
when Hash
|
|
51
|
+
validate_and_normalize(value)
|
|
52
|
+
when Array
|
|
53
|
+
validate_and_normalize_array(value)
|
|
54
|
+
else
|
|
55
|
+
value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|