linzer 0.7.9.beta1 → 0.7.9.beta3

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.
data/flake.nix CHANGED
@@ -1,11 +1,23 @@
1
1
  {
2
2
  inputs = {
3
3
  nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
4
- # XXX: should work, not tested yet on MacOS
5
- # systems.url = "github:nix-systems/default";
6
- systems.url = "github:nix-systems/default-linux";
7
- ruby-nix.url = "github:inscapist/ruby-nix";
8
- bundix.url = "github:inscapist/bundix";
4
+
5
+ systems = {
6
+ url = "github:nix-systems/default-linux";
7
+ # XXX: should work, not tested yet on MacOS
8
+ # url = "github:nix-systems/default";
9
+ inputs.nixpkgs.follows = "nixpkgs";
10
+ };
11
+
12
+ ruby-nix = {
13
+ url = "github:inscapist/ruby-nix";
14
+ inputs.nixpkgs.follows = "nixpkgs";
15
+ };
16
+
17
+ bundix = {
18
+ url = "github:inscapist/bundix";
19
+ inputs.nixpkgs.follows = "nixpkgs";
20
+ };
9
21
  };
10
22
 
11
23
  outputs = {
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Faraday
4
+ module HttpSignature
5
+ # Raised when HTTP response signature verification fails in strict mode.
6
+ #
7
+ # Inherits from {Faraday::Error} so that standard Faraday error handling
8
+ # (e.g. +rescue Faraday::Error+) catches verification failures.
9
+ # The original {Linzer::VerifyError} is preserved as {#wrapped_exception}
10
+ # and the {Faraday::Response} is available via {#response}.
11
+ #
12
+ # @example Catching a verification failure
13
+ # begin
14
+ # response = conn.get("/")
15
+ # rescue Faraday::HttpSignature::VerifyError => e
16
+ # e.message # => "Failed to verify message: Invalid signature."
17
+ # e.response # => the Faraday::Response object
18
+ # e.wrapped_exception # => the original Linzer::VerifyError
19
+ # end
20
+ #
21
+ # @see Middleware
22
+ class VerifyError < Faraday::Error; end
23
+
24
+ # Raised when HTTP request signature creation fails.
25
+ #
26
+ # Inherits from {Faraday::Error} so that standard Faraday error handling
27
+ # (e.g. +rescue Faraday::Error+) catches verification failures.
28
+ # The original {Linzer::Error} is preserved as {#wrapped_exception}.
29
+ #
30
+ # @example Catching a signing failure
31
+ # begin
32
+ # response = conn.post("/")
33
+ # rescue Faraday::HttpSignature::SigningError => e
34
+ # e.message # => "Failed to sign message: Missing component."
35
+ # e.wrapped_exception # => the original Linzer::Error
36
+ # end
37
+ #
38
+ # @see Middleware
39
+ class SigningError < Faraday::Error; end
40
+
41
+ # Faraday middleware for HTTP message signing and verification (RFC 9421).
42
+ #
43
+ # When registered via +request+, signs outgoing requests (default).
44
+ # When registered via +response+, verifies incoming response signatures.
45
+ # When registered via +use+, signs requests by default; pass
46
+ # +verify_response: true+ to also verify responses.
47
+ #
48
+ # == Verification result metadata
49
+ #
50
+ # After response verification, the middleware stores results in
51
+ # +env[:http_signature_verified]+ (+true+ or +false+) and
52
+ # +env[:http_signature]+ (the {Linzer::Signature} on success).
53
+ # These are accessible via +response.env[:http_signature_verified]+.
54
+ #
55
+ # @example Sign requests
56
+ # conn = Faraday.new(url: "https://example.com") do |f|
57
+ # f.request :http_signature, key: my_key, components: %w[@method @path]
58
+ # end
59
+ #
60
+ # @example Verify responses
61
+ # conn = Faraday.new(url: "https://example.com") do |f|
62
+ # f.response :http_signature, verify_key: server_pubkey
63
+ # end
64
+ #
65
+ # @example Lenient verification (no exception on failure)
66
+ # conn = Faraday.new(url: "https://example.com") do |f|
67
+ # f.response :http_signature, verify_key: server_pubkey, strict: false
68
+ # end
69
+ # response = conn.get("/")
70
+ # response.env[:http_signature_verified] # => true or false
71
+ #
72
+ # @see https://datatracker.ietf.org/doc/html/rfc9421 RFC 9421
73
+ class Middleware < Faraday::Middleware
74
+ # Default options for the base middleware class (used by +use+
75
+ # and +request+ registrations). Signs requests, does not verify
76
+ # responses, strict mode enabled.
77
+ DEFAULT_OPTIONS = {
78
+ sign_request: true,
79
+ verify_response: false,
80
+ strict: true
81
+ }.freeze
82
+
83
+ # Configuration options for the HTTP signature middleware.
84
+ #
85
+ # @!attribute [rw] key
86
+ # @return [Linzer::Key, nil] generic key used for signing or
87
+ # verification when only one mode is active
88
+ # @!attribute [rw] sign_request
89
+ # @return [Boolean] whether to sign outgoing requests
90
+ # (defaults to +true+)
91
+ # @!attribute [rw] sign_key
92
+ # @return [Linzer::Key, nil] explicit key for signing; required
93
+ # when both signing and verification are enabled
94
+ # @!attribute [rw] components
95
+ # @return [Array<String>] HTTP message components to include in
96
+ # the signature (e.g. +["@method", "@path", "content-type"]+)
97
+ # @!attribute [rw] verify_response
98
+ # @return [Boolean] whether to verify incoming response signatures
99
+ # (defaults to +false+)
100
+ # @!attribute [rw] verify_key
101
+ # @return [Linzer::Key, nil] explicit key for verification; required
102
+ # when both signing and verification are enabled
103
+ # @!attribute [rw] params
104
+ # @return [Hash] additional signature parameters
105
+ # (e.g. +{ tag: "my_tag" }+)
106
+ # @!attribute [rw] strict
107
+ # @return [Boolean] when +true+ (default), raises
108
+ # {VerifyError} on verification failure; when +false+,
109
+ # sets +env[:http_signature_verified]+ to +false+ and continues
110
+ class Options < Faraday::Options.new(:key, :sign_request, :sign_key, :components, :verify_response, :verify_key, :params, :strict)
111
+ # Returns the generic key.
112
+ # @return [Linzer::Key, nil]
113
+ def key
114
+ self[:key]
115
+ end
116
+
117
+ # Whether outgoing requests should be signed.
118
+ # Defaults to +true+ (returns +true+ when unset).
119
+ # @return [Boolean]
120
+ def sign_request?
121
+ self[:sign_request] != false
122
+ end
123
+
124
+ # Whether incoming responses should be verified.
125
+ # Defaults to +false+ (returns +false+ when unset).
126
+ # @return [Boolean]
127
+ def verify_response?
128
+ self[:verify_response]
129
+ end
130
+
131
+ # Whether verification failures should raise an exception.
132
+ # Defaults to +true+ (returns +true+ when unset).
133
+ # @return [Boolean]
134
+ def strict?
135
+ self[:strict] != false
136
+ end
137
+
138
+ # Returns the list of HTTP message components to sign.
139
+ # @return [Array<String>]
140
+ def components
141
+ Array(self[:components])
142
+ end
143
+
144
+ # Returns additional signature parameters.
145
+ # @return [Hash]
146
+ def params
147
+ Hash(self[:params])
148
+ end
149
+ end
150
+
151
+ # Creates a new middleware instance.
152
+ #
153
+ # Merges class-level {DEFAULT_OPTIONS} with the user-provided options
154
+ # so that subclasses ({Request}, {Response}) can override defaults.
155
+ #
156
+ # @param app [#call] the next middleware or adapter in the stack
157
+ # @param options [Hash, nil] middleware options
158
+ # @option options [Linzer::Key] :key generic key for signing or verification
159
+ # @option options [Linzer::Key] :sign_key explicit signing key
160
+ # @option options [Linzer::Key] :verify_key explicit verification key
161
+ # @option options [Array<String>] :components components to sign
162
+ # @option options [Hash] :params additional signature parameters
163
+ # @option options [Boolean] :sign_request (+true+) whether to sign requests
164
+ # @option options [Boolean] :verify_response (+false+) whether to verify responses
165
+ # @option options [Boolean] :strict (+true+) raise on verification failure
166
+ def initialize(app, options = nil)
167
+ super(app)
168
+ defaults = self.class::DEFAULT_OPTIONS
169
+ merged = defaults.merge(Hash(options))
170
+ @options = Options.from(merged)
171
+ end
172
+
173
+ # Signs the outgoing request when {Options#sign_request?} is +true+.
174
+ #
175
+ # Resolves the signing key, builds a {Linzer::Message} from the
176
+ # Faraday environment, generates a signature over the configured
177
+ # components, and merges the +signature+ and +signature-input+
178
+ # headers into the request.
179
+ #
180
+ # @param env [Faraday::Env] the middleware environment
181
+ # @return [Faraday::Env, nil] the modified env, or +nil+ if signing
182
+ # is disabled
183
+ # @raise [Linzer::Error] if no valid signing key is available
184
+ def on_request(env)
185
+ return unless options.sign_request?
186
+
187
+ key = resolve_signing_key
188
+ request = Linzer::Faraday::Utils.create_request(env)
189
+ message = Linzer::Message.new(request)
190
+
191
+ signature = Linzer.sign(key, message, options.components, options.params)
192
+ env.request_headers.merge!(signature.to_h)
193
+ env
194
+ rescue Linzer::Error => e
195
+ raise SigningError, e if options.strict?
196
+ end
197
+
198
+ # Verifies the response signature when {Options#verify_response?} is +true+.
199
+ #
200
+ # On success, sets +env[:http_signature_verified]+ to +true+ and
201
+ # +env[:http_signature]+ to the verified {Linzer::Signature}.
202
+ #
203
+ # On failure in strict mode (default), raises {VerifyError}.
204
+ # In lenient mode (+strict: false+), sets
205
+ # +env[:http_signature_verified]+ to +false+ and allows the response
206
+ # to continue through the middleware stack.
207
+ #
208
+ # @param env [Faraday::Env] the middleware environment
209
+ # @return [Faraday::Env, nil] the modified env, or +nil+ if verifying
210
+ # is disabled
211
+ # @raise [VerifyError] if verification fails and +strict+ is +true+
212
+ # @raise [Linzer::Error] if no valid verification key is available
213
+ def on_complete(env)
214
+ env[:http_signature_verified] = false
215
+ return unless options.verify_response?
216
+
217
+ key = resolve_verify_key
218
+ response = ::Faraday::Response.new(env)
219
+ message = Linzer::Message.new(response)
220
+ signature = Linzer::Signature.build(response.headers)
221
+
222
+ Linzer.verify(key, message, signature)
223
+ env[:http_signature_verified] = true
224
+ env[:http_signature] = signature
225
+ env
226
+ rescue Linzer::Error => e
227
+ raise VerifyError.new(e, response: response) if options.strict?
228
+ end
229
+
230
+ private
231
+
232
+ # Resolves the key to use for signing requests.
233
+ #
234
+ # Prefers {Options#sign_key}. Falls back to the generic {Options#key}
235
+ # when only one mode (sign or verify) is active. When both modes are
236
+ # active, the generic key is ambiguous and +sign_key+ must be set
237
+ # explicitly.
238
+ #
239
+ # @return [Linzer::Key] the resolved signing key
240
+ # @raise [Linzer::Error] if no key is available or the key is invalid
241
+ def resolve_signing_key
242
+ key = options.sign_key
243
+ key ||= options.key unless options.sign_request? && options.verify_response?
244
+ raise Linzer::Error, "No signing key provided!" if !key
245
+ raise Linzer::Error, "Invalid key!" if !key.is_a?(Linzer::Key)
246
+
247
+ key
248
+ end
249
+
250
+ # Resolves the key to use for verifying response signatures.
251
+ #
252
+ # Prefers {Options#verify_key}. Falls back to the generic {Options#key}
253
+ # when only one mode (sign or verify) is active. When both modes are
254
+ # active, the generic key is ambiguous and +verify_key+ must be set
255
+ # explicitly.
256
+ #
257
+ # @return [Linzer::Key] the resolved verification key
258
+ # @raise [Linzer::Error] if no key is available or the key is invalid
259
+ def resolve_verify_key
260
+ key = options.verify_key
261
+ key ||= options.key unless options.sign_request? && options.verify_response?
262
+ raise Linzer::Error, "No verification key provided!" if !key
263
+ raise Linzer::Error, "Invalid key!" if !key.is_a?(Linzer::Key)
264
+
265
+ key
266
+ end
267
+
268
+ # Subclass registered under {Faraday::Request}.
269
+ #
270
+ # Inherits the base {DEFAULT_OPTIONS} which sign requests by default
271
+ # and do not verify responses.
272
+ #
273
+ # @example
274
+ # f.request :http_signature, key: my_key, components: %w[@method @path]
275
+ class Request < self
276
+ end
277
+
278
+ # Subclass registered under {Faraday::Response}.
279
+ #
280
+ # Overrides {DEFAULT_OPTIONS} to verify responses by default and
281
+ # not sign requests.
282
+ #
283
+ # @example
284
+ # f.response :http_signature, verify_key: server_pubkey
285
+ class Response < self
286
+ # Default options for the response subclass. Verifies responses
287
+ # and does not sign requests.
288
+ DEFAULT_OPTIONS = {
289
+ sign_request: false,
290
+ verify_response: true,
291
+ strict: true
292
+ }.freeze
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require_relative "http_signature/middleware"
5
+
6
+ module Faraday
7
+ # Faraday middleware for signing and verifying HTTP messages
8
+ # as defined in RFC 9421.
9
+ #
10
+ # Three registration points are provided so the middleware can be added
11
+ # via +request+, +response+ or +use+, each with appropriate defaults:
12
+ #
13
+ # @example Sign outgoing requests
14
+ # conn = Faraday.new(url: "https://example.com") do |f|
15
+ # f.request :http_signature, key: my_key, components: %w[@method @path]
16
+ # end
17
+ #
18
+ # @example Verify incoming responses
19
+ # conn = Faraday.new(url: "https://example.com") do |f|
20
+ # f.response :http_signature, verify_key: server_pubkey
21
+ # end
22
+ #
23
+ # @example Sign requests and verify responses
24
+ # conn = Faraday.new(url: "https://example.com") do |f|
25
+ # f.use :http_signature, sign_key: my_key, verify_key: server_pubkey,
26
+ # verify_response: true, components: %w[@method @path]
27
+ # end
28
+ #
29
+ # @see Faraday::HttpSignature::Middleware
30
+ # @see https://datatracker.ietf.org/doc/html/rfc9421 RFC 9421 - HTTP Message Signatures
31
+ module HttpSignature
32
+ Faraday::Request.register_middleware(http_signature: Faraday::HttpSignature::Middleware::Request)
33
+ Faraday::Response.register_middleware(http_signature: Faraday::HttpSignature::Middleware::Response)
34
+ Faraday::Middleware.register_middleware(http_signature: Faraday::HttpSignature::Middleware)
35
+ end
36
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ # Faraday integration for Linzer.
5
+ #
6
+ # @see file:lib/linzer/faraday.rb
7
+ module Faraday
8
+ # Utility methods for converting Faraday middleware objects into
9
+ # types compatible with Linzer adapters.
10
+ module Utils
11
+ # Creates a {::Faraday::Request} from a middleware environment.
12
+ #
13
+ # Builds a minimal request suitable for use with
14
+ # {Linzer::Message::Adapter::Faraday::Request}, preserving the
15
+ # original HTTP method, URL, and headers from the environment.
16
+ #
17
+ # @param env [::Faraday::Env] the middleware environment
18
+ # @return [::Faraday::Request] a new request object
19
+ def self.create_request(env)
20
+ ::Faraday::Request.create(env.method) do |req|
21
+ req.params = ::Faraday::Utils::ParamsHash.new
22
+ req.headers = ::Faraday::Utils::Headers.new(env.request_headers.dup)
23
+ req.options = ::Faraday::ConnectionOptions.from(nil).request
24
+ req.url env.url
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Faraday integration for Linzer.
4
+ #
5
+ # Require this file to automatically register Faraday message adapters
6
+ # and the HTTP signature (RFC 9421) middleware.
7
+ #
8
+ # This sets up:
9
+ # - {Linzer::Message::Adapter::Faraday::Request} for {::Faraday::Request}
10
+ # - {Linzer::Message::Adapter::Faraday::Response} for {::Faraday::Response}
11
+ # - {Faraday::HttpSignature::Middleware} registered as +:http_signature+
12
+ # on +Faraday::Request+, +Faraday::Response+ and +Faraday::Middleware+
13
+ #
14
+ # @example
15
+ # require "linzer/faraday"
16
+ #
17
+ # conn = Faraday.new(url: "https://example.com") do |f|
18
+ # f.request :http_signature, key: my_key, components: %w[@method @path]
19
+ # end
20
+
21
+ require "faraday"
22
+ require "linzer"
23
+ require "faraday/http_signature"
24
+ require "linzer/message/adapter/faraday/request"
25
+ require "linzer/message/adapter/faraday/response"
26
+ require "linzer/faraday/utils"
27
+
28
+ Linzer::Message.register_adapter(Faraday::Request, Linzer::Message::Adapter::Faraday::Request)
29
+ Linzer::Message.register_adapter(Faraday::Response, Linzer::Message::Adapter::Faraday::Response)
data/lib/linzer/http.rb CHANGED
@@ -78,6 +78,14 @@ module Linzer
78
78
  private
79
79
 
80
80
  # Executes a signed HTTP request.
81
+ #
82
+ # Validates inputs, builds and signs the request, then sends it.
83
+ #
84
+ # @param verb [Symbol] the HTTP method (e.g. +:get+, +:post+)
85
+ # @param uri [String] the request URI
86
+ # @param options [Hash] request options
87
+ # @return [Net::HTTPResponse] the response
88
+ # @raise [Linzer::Error] if the verb or key is invalid
81
89
  def request(verb, uri, options = {})
82
90
  validate_verb(verb)
83
91
 
@@ -100,10 +108,16 @@ module Linzer
100
108
  do_request(http, uri, verb, options[:data], signature, headers)
101
109
  end
102
110
 
111
+ # Returns the default covered components for signing.
112
+ # @return [Array<String>]
103
113
  def default_components
104
114
  Linzer::Options::DEFAULT[:covered_components]
105
115
  end
106
116
 
117
+ # Validates that the HTTP verb is recognized.
118
+ #
119
+ # @param verb [Symbol] the HTTP method
120
+ # @raise [Linzer::Error] if the verb is unknown
107
121
  def validate_verb(verb)
108
122
  method_name = verb.to_s.upcase
109
123
  if !known_http_methods.include?(method_name)
@@ -111,17 +125,32 @@ module Linzer
111
125
  end
112
126
  end
113
127
 
128
+ # Validates that the signing key is present and usable.
129
+ #
130
+ # @param key [Linzer::Key] the signing key
131
+ # @return [Linzer::Key] the validated key
132
+ # @raise [Linzer::Error] if the key is nil or does not respond to +#sign+
114
133
  def validate_key(key)
115
134
  raise Linzer::Error, "Key can not be nil!" if !key
116
135
  raise Linzer::Error, "Key object is invalid!" if !key.respond_to?(:sign)
117
136
  key
118
137
  end
119
138
 
139
+ # Builds request headers, adding a default User-Agent if not present.
140
+ #
141
+ # @param headers [Hash] user-provided headers
142
+ # @return [Hash] headers with User-Agent ensured
120
143
  def build_headers(headers)
121
144
  return headers if headers.transform_keys(&:downcase).key?("user-agent")
122
145
  headers.merge({"user-agent" => "Linzer/#{Linzer::VERSION}"})
123
146
  end
124
147
 
148
+ # Builds a Net::HTTP request object for the given method and URI.
149
+ #
150
+ # @param method [Symbol] the HTTP method
151
+ # @param uri [String] the request URI
152
+ # @param headers [Hash] request headers to set
153
+ # @return [Net::HTTPRequest] the constructed request
125
154
  def build_request(method, uri, headers)
126
155
  request_class = Net::HTTP.const_get(method.to_s.capitalize)
127
156
  request = request_class.new(URI(uri))
@@ -129,7 +158,10 @@ module Linzer
129
158
  request
130
159
  end
131
160
 
132
- # Determines if the HTTP method typically has a request body.
161
+ # Checks if the HTTP method typically carries a request body.
162
+ #
163
+ # @param verb [Symbol] the HTTP method
164
+ # @return [Boolean] +true+ for POST, PUT, PATCH, and WebDAV write methods
133
165
  def with_body?(verb)
134
166
  # common HTTP
135
167
  return false if %i[get head options trace delete].include?(verb)
@@ -142,6 +174,16 @@ module Linzer
142
174
  true
143
175
  end
144
176
 
177
+ # Sends the HTTP request with the signature headers attached.
178
+ #
179
+ # @param http [Net::HTTP] the HTTP connection
180
+ # @param uri [String] the request URI
181
+ # @param verb [Symbol] the HTTP method
182
+ # @param data [String, nil] the request body
183
+ # @param signature [Linzer::Signature] the generated signature
184
+ # @param headers [Hash] request headers
185
+ # @return [Net::HTTPResponse] the response
186
+ # @raise [Linzer::Error] if a body is required but not provided
145
187
  def do_request(http, uri, verb, data, signature, headers)
146
188
  if with_body?(verb)
147
189
  if !data
@@ -96,7 +96,10 @@ module Linzer
96
96
  private
97
97
 
98
98
  # Parses a field name string into a FieldId.
99
- # @return [FieldId, nil] The parsed identifier, or nil if invalid
99
+ #
100
+ # @param field_name [String] the component identifier string
101
+ # @return [FieldId, nil] the parsed identifier, or +nil+ if invalid
102
+ # @raise [Error] if +@status+ is used in a request message
100
103
  def parse_field_name(field_name)
101
104
  field_id = FieldId.new(field_name: field_name)
102
105
  component = field_id.item
@@ -117,8 +120,12 @@ module Linzer
117
120
  raise Linzer::Error, msg unless message.request?
118
121
  end
119
122
 
120
- # Validates component identifier parameters.
121
- # @return [Object, nil] The validated name, or nil if invalid
123
+ # Validates component identifier parameters against RFC 9421 rules.
124
+ #
125
+ # @param name [Starry::Item] the parsed component identifier
126
+ # @param method [Symbol] +:derived+ or +:field+
127
+ # @return [Starry::Item, nil] the validated name, or +nil+ if
128
+ # the parameter combination is invalid
122
129
  def validate_parameters(name, method)
123
130
  has_unknown = name.parameters.any? { |p, _| !KNOWN_PARAMETERS.include?(p) }
124
131
  return nil if has_unknown
@@ -149,6 +156,13 @@ module Linzer
149
156
  private_constant :KNOWN_PARAMETERS
150
157
 
151
158
  # Retrieves a component value with parameter processing.
159
+ #
160
+ # Handles +;req+, +;sf+, +;key+, and +;bs+ parameters by
161
+ # delegating to the corresponding helper methods.
162
+ #
163
+ # @param name [Starry::Item] the parsed component identifier
164
+ # @param method [Symbol] +:derived+ or +:field+
165
+ # @return [String, Integer, nil] the component value
152
166
  def retrieve(name, method)
153
167
  if !name.parameters.empty?
154
168
  valid_params = validate_parameters(name, method)
@@ -176,6 +190,10 @@ module Linzer
176
190
  end
177
191
 
178
192
  # Processes a structured field value with optional key extraction.
193
+ #
194
+ # @param value [String] the raw header value to parse as a dictionary
195
+ # @param key [String, nil] if present, extracts a single dictionary member
196
+ # @return [String] the serialized structured field value
179
197
  # @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.1
180
198
  def sf(value, key = nil)
181
199
  dict = Starry.parse_dictionary(value)
@@ -188,19 +206,31 @@ module Linzer
188
206
  end
189
207
  end
190
208
 
191
- # Binary-wraps a field value.
209
+ # Binary-wraps a field value as a byte sequence.
210
+ #
211
+ # @param value [String] the header value to wrap
212
+ # @return [String] the serialized byte sequence
192
213
  # @see https://www.rfc-editor.org/rfc/rfc9421.html#section-2.1.3
193
214
  def bs(value)
194
215
  Starry.serialize(value.encode(Encoding::ASCII_8BIT))
195
216
  end
196
217
 
197
218
  # Retrieves a trailer field value.
219
+ #
198
220
  # @abstract Subclasses should implement if trailer support is needed.
221
+ # @param trailer [Object] the trailer field identifier
222
+ # @return [String, nil] the trailer value
223
+ # @raise [Error] always, since no built-in adapters support trailers
199
224
  def tr(trailer)
200
225
  raise Error, "Sub-classes are required to implement this method!"
201
226
  end
202
227
 
203
- # Retrieves a field from the attached request.
228
+ # Retrieves a field from the attached request (for +;req+ parameter).
229
+ #
230
+ # @param field [Starry::Item] the component identifier
231
+ # @param method [Symbol] +:derived+ or +:field+
232
+ # @return [String, nil] the value from the attached request, or
233
+ # +nil+ if no request is attached
204
234
  def req(field, method)
205
235
  attached_request? ? @attached_request[String(field)] : nil
206
236
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Linzer
4
+ class Message
5
+ module Adapter
6
+ module Faraday
7
+ # Adapter for {::Faraday::Request} objects from the faraday gem.
8
+ #
9
+ # Extends the generic request adapter with faraday-specific
10
+ # derived component retrieval, field lookup, and URI handling.
11
+ #
12
+ # @note Not loaded automatically to avoid making faraday a hard
13
+ # dependency. Require +"linzer/faraday"+ to register this adapter.
14
+ #
15
+ # @see Generic::Request
16
+ # @see https://github.com/lostisland/faraday faraday gem
17
+ class Request < Generic::Request
18
+ private
19
+
20
+ # Resolves a derived component value from the request.
21
+ #
22
+ # @param name [Starry::Item] the parsed component identifier
23
+ # @return [String, nil] the derived value, or +nil+ if unknown
24
+ def derived(name)
25
+ url = @operation.path
26
+ case name.value
27
+ when "@method" then @operation.http_method.to_s.upcase
28
+ when "@target-uri" then uri.to_s
29
+ when "@authority" then url.authority.downcase
30
+ when "@scheme" then url.scheme.downcase
31
+ when "@request-target" then uri.request_uri
32
+ when "@path" then url.path
33
+ when "@query" then "?%s" % String(uri_query)
34
+ when "@query-param" then query_param(uri_query, name)
35
+ end
36
+ end
37
+
38
+ # Builds the full URI including query parameters.
39
+ #
40
+ # @return [URI] the complete request URI with encoded query string
41
+ def uri
42
+ uri = @operation.path.dup
43
+ uri.query = URI.encode_www_form(@operation.params)
44
+ uri
45
+ end
46
+
47
+ # Returns the raw query string from the request URI.
48
+ #
49
+ # Prefers the raw query string from the URI when available,
50
+ # as Faraday normalises percent-encoding when parsing params
51
+ # (e.g. +%2D+ becomes +-+), which would break signature
52
+ # verification.
53
+ #
54
+ # @return [String, nil] the raw query string
55
+ def uri_query
56
+ url = @operation.path
57
+ url.query || uri.query
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end