Skip to content

Commit 894f65c

Browse files
authored
Add RFC document for Options architecture refactoring plan (#1644)
Merging RFC document to serve as central reference for Options refactoring work
1 parent 397e3de commit 894f65c

File tree

1 file changed

+185
-0
lines changed

1 file changed

+185
-0
lines changed

docs/options-detach-plan.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# RFC/Tracking: Detach Options subclasses into explicit OOP (BaseOptions + OptionsLike), keep legacy Options
2+
3+
## Summary
4+
- Keep Faraday::Options as-is for backward compatibility.
5+
- Detach existing subclasses (ConnectionOptions, RequestOptions, SSLOptions, ProxyOptions, Env) into explicit OOP classes that do not inherit from Options.
6+
- Introduce:
7+
- Faraday::OptionsLike (marker module) to identify "options-like" objects.
8+
- Faraday::BaseOptions (abstract superclass) that centralizes .from/update/merge!/merge/deep_dup/to_hash/inspect and nested coercion via constants.
9+
- Drop legacy ergonomics for detached subclasses (Struct indexing, fetch, each_key, .options macro, .memoized macro). Preserve Env's hash-like []/[]= since middleware uses it.
10+
- Convert classes one-by-one: ProxyOptions → RequestOptions → SSLOptions → ConnectionOptions → Env.
11+
12+
## Main decisions
13+
- Inheritance + marker: BaseOptions reduces duplication and drift for correctness-sensitive logic (nested coercion/merge/deep-dup). OptionsLike provides a stable way to integrate with Utils.deep_merge! and any duck-typed interop.
14+
- Back-compat boundary: Faraday::Options remains unchanged. New classes' .from accept Hash and legacy Options (via to_hash). Deep-merge continues to work on both legacy and new options classes.
15+
- Ergonomics: For internal code we drop Struct-like features. Env keeps []/[]=.
16+
17+
## Short code examples
18+
19+
### OptionsLike and BaseOptions
20+
21+
```ruby
22+
module Faraday
23+
module OptionsLike; end
24+
25+
class BaseOptions
26+
include OptionsLike
27+
28+
MEMBERS = [].freeze # override in subclasses
29+
COERCIONS = {}.freeze # key => Class or Proc
30+
31+
def self.from(value)
32+
case value
33+
when nil
34+
new
35+
when self
36+
value
37+
when OptionsLike
38+
new.update(value.to_hash)
39+
when Hash
40+
new.update(value)
41+
else
42+
raise ArgumentError, "unsupported options: #{value.class}"
43+
end
44+
end
45+
46+
def initialize(**attrs)
47+
attrs.each { |k, v| public_send("#{k}=", coerce(k, v)) }
48+
end
49+
50+
def update(hash_like)
51+
hash_like.each { |k, v| public_send("#{k}=", coerce(k, v)) }
52+
self
53+
end
54+
55+
def merge!(other)
56+
other.each do |k, v|
57+
next if v.nil?
58+
cur = public_send(k)
59+
newv = coerce(k, v)
60+
if cur.is_a?(OptionsLike) && newv.is_a?(OptionsLike)
61+
cur.merge!(newv.to_hash)
62+
else
63+
public_send("#{k}=", newv)
64+
end
65+
end
66+
self
67+
end
68+
69+
def merge(other)
70+
self.class.from(to_hash).merge!(other)
71+
end
72+
73+
def deep_dup
74+
self.class.from(to_hash)
75+
end
76+
77+
def to_hash
78+
self.class::MEMBERS.each_with_object({}) do |k, h|
79+
v = public_send(k)
80+
next if v.nil?
81+
h[k] = v.is_a?(OptionsLike) ? v.to_hash : v
82+
end
83+
end
84+
85+
def inspect
86+
pairs = to_hash.map { |k, v| "#{k}=#{v.inspect}" }
87+
"#<#{self.class} #{pairs.join(', ')}>"
88+
end
89+
90+
private
91+
92+
def coerce(key, value)
93+
return value if value.nil?
94+
coercer = self.class::COERCIONS[key.to_sym]
95+
case coercer
96+
when Class then coercer.from(value)
97+
when Proc then coercer.call(value)
98+
else value
99+
end
100+
end
101+
end
102+
end
103+
```
104+
105+
### ProxyOptions (detached)
106+
107+
```ruby
108+
class Faraday::ProxyOptions < Faraday::BaseOptions
109+
MEMBERS = [:uri, :user, :password].freeze
110+
attr_accessor(*MEMBERS)
111+
112+
COERCIONS = {
113+
uri: ->(v) do
114+
case v
115+
when String
116+
v = "http://#{v}" unless v.include?('://')
117+
Faraday::Utils.URI(v)
118+
when URI
119+
v
120+
else
121+
v
122+
end
123+
end
124+
}.freeze
125+
126+
def user
127+
@user || (uri && Faraday::Utils.unescape(uri.user))
128+
end
129+
130+
def password
131+
@password || (uri && Faraday::Utils.unescape(uri.password))
132+
end
133+
end
134+
```
135+
136+
### Utils.deep_merge! change
137+
138+
```ruby
139+
# lib/faraday/utils.rb
140+
# Treat OptionsLike like Options when deep-merging nested structures
141+
if value.is_a?(Hash) && (target_value.is_a?(Hash) || target_value.is_a?(Faraday::OptionsLike))
142+
target[key] = deep_merge(target_value, value)
143+
else
144+
target[key] = value
145+
end
146+
```
147+
148+
## Incremental rollout
149+
1) Foundation: add OptionsLike + BaseOptions; update Utils.deep_merge!.
150+
2) Convert ProxyOptions (smallest surface, minimal coupling).
151+
3) Convert RequestOptions (ensure proxy coercion and deep_merge! semantics).
152+
4) Convert SSLOptions (larger surface; explicit lazy cert_store).
153+
5) Convert ConnectionOptions (nested request/ssl, builder_class default, new_builder).
154+
6) Convert Env last (preserve []/[]= and to_hash; confirm middleware compatibility).
155+
7) Tests/docs: expand tests for nested coercion, nil-preserving merge, deep_dup, to_hash; update docs.
156+
157+
## Tasks (use "Create issue from task list")
158+
- [ ] Foundation: Introduce OptionsLike and BaseOptions; update Utils.deep_merge!
159+
- Files: add lib/faraday/options_like.rb, lib/faraday/base_options.rb; modify lib/faraday/utils.rb
160+
- Tests: BaseOptions unit tests; deep_merge! tests for OptionsLike
161+
- Docs: brief section on OptionsLike/BaseOptions
162+
- Research: grep for is_a?(Options) checks and update where needed
163+
- [ ] Convert ProxyOptions to BaseOptions (preserve behavior; drop Struct ergonomics)
164+
- Files: lib/faraday/options/proxy_options.rb
165+
- Tests: string/URI coercion, empty string => nil (via RequestOptions#proxy=), user/password derivation
166+
- Research: find ProxyOptions usages and delegators (scheme/host/port/path)
167+
- [ ] Convert RequestOptions to BaseOptions (keep proxy coercion and stream_response?)
168+
- Files: lib/faraday/options/request_options.rb
169+
- Tests: nested proxy coercion, deep_merge!, to_hash
170+
- Research: usages of []/[]= on RequestOptions; replace with explicit accessors
171+
- [ ] Convert SSLOptions to BaseOptions (preserve lazy cert_store and semantics)
172+
- Files: lib/faraday/options/ssl_options.rb
173+
- Tests: coercion, deep_dup, to_hash, lazy cert_store
174+
- Research: adapter interactions with SSLOptions
175+
- [ ] Convert ConnectionOptions to BaseOptions (coerce request/ssl; default builder_class)
176+
- Files: lib/faraday/options/connection_options.rb, lib/faraday.rb
177+
- Tests: Faraday.new/default_connection_options, deep_merge!, builder behavior
178+
- Research: usages of members/values/[]; migrate to explicit access
179+
- [ ] Convert Env (last; preserve middleware hash-like API)
180+
- Files: lib/faraday/options/env.rb
181+
- Tests: env and middleware integration
182+
- Research: env[:key] usages; avoid breaking third-party middleware
183+
- [ ] Optional: Deprecate Options.memoized macro (new classes don't use it)
184+
- Files: lib/faraday/options.rb
185+
- Tests/Docs: deprecation notice and changelog entry

0 commit comments

Comments
 (0)