Class: Dommy::Rack::Navigation
- Inherits:
-
Object
- Object
- Dommy::Rack::Navigation
- Defined in:
- lib/dommy/rack/navigation.rb
Overview
URL resolution, redirect following, same-origin enforcement, and browser-tab-style history. Reads policy from the frozen Config and drives the Session only through its request seam (raw_request / apply_navigation_response / current_url).
Constant Summary collapse
- KEEP_METHOD_STATUSES =
[307, 308].freeze
Instance Method Summary collapse
- #check_same_origin!(url) ⇒ Object
-
#fetch(url, method: "GET", params: nil, body: nil, headers: {}, redirect: :follow) ⇒ Object
Fetch-style request: resolves and enforces origin, runs the redirect loop per mode, and returns the Response without touching session state.
-
#initialize(session, config) ⇒ Navigation
constructor
A new instance of Navigation.
-
#navigate(method:, url:, params: nil, body: nil, headers: {}) ⇒ Object
Perform a navigation, following redirects per session policy, then apply the final response to the session (updating document + history).
-
#resolve_url(url_or_path, base_url) ⇒ Object
Resolve a possibly-relative URL against a base (current URL or host).
-
#revisit(url) ⇒ Object
Re-navigate to an already-resolved URL without pushing a new history entry (used by Session#back / #forward).
-
#run(method:, url:, params: nil, body: nil, headers: {}, follow: true) ⇒ Object
Run the request/redirect loop.
Constructor Details
#initialize(session, config) ⇒ Navigation
Returns a new instance of Navigation.
14 15 16 17 |
# File 'lib/dommy/rack/navigation.rb', line 14 def initialize(session, config) @session = session @config = config end |
Instance Method Details
#check_same_origin!(url) ⇒ Object
27 28 29 30 31 32 |
# File 'lib/dommy/rack/navigation.rb', line 27 def check_same_origin!(url) return unless @config.enforce_same_origin return if same_origin?(url, @config.default_host) raise CrossOriginError, "cross-origin request to #{url} is not allowed" end |
#fetch(url, method: "GET", params: nil, body: nil, headers: {}, redirect: :follow) ⇒ Object
Fetch-style request: resolves and enforces origin, runs the redirect loop per mode, and returns the Response without touching session state.
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/dommy/rack/navigation.rb', line 48 def fetch(url, method: "GET", params: nil, body: nil, headers: {}, redirect: :follow) verb = method.to_s.upcase target = resolve_url(url, @session.current_url) check_same_origin!(target) args = {method: verb, url: target, params: params, body: body, headers: headers} case redirect when :follow run(**args, follow: true).first when :manual run(**args, follow: false).first when :error response = run(**args, follow: false).first raise Error, "redirect encountered with redirect: :error" if response.redirect? response else raise ArgumentError, "unsupported redirect mode: #{redirect.inspect}" end end |
#navigate(method:, url:, params: nil, body: nil, headers: {}) ⇒ Object
Perform a navigation, following redirects per session policy, then apply the final response to the session (updating document + history).
36 37 38 39 40 41 42 43 44 |
# File 'lib/dommy/rack/navigation.rb', line 36 def navigate(method:, url:, params: nil, body: nil, headers: {}) verb = method.to_s.upcase target = resolve_url(url, @session.current_url) check_same_origin!(target) response, final_url = run(method: verb, url: target, params: params, body: body, headers: headers) @session.(response, final_url) (response) || response end |
#resolve_url(url_or_path, base_url) ⇒ Object
Resolve a possibly-relative URL against a base (current URL or host).
20 21 22 23 24 25 |
# File 'lib/dommy/rack/navigation.rb', line 20 def resolve_url(url_or_path, base_url) base = base_url || @config.default_host URI.join(base, url_or_path.to_s).to_s rescue URI::InvalidURIError url_or_path.to_s end |
#revisit(url) ⇒ Object
Re-navigate to an already-resolved URL without pushing a new history entry (used by Session#back / #forward).
71 72 73 74 75 |
# File 'lib/dommy/rack/navigation.rb', line 71 def revisit(url) response, final_url = run(method: "GET", url: url) @session.(response, final_url, push_history: false) response end |
#run(method:, url:, params: nil, body: nil, headers: {}, follow: true) ⇒ Object
Run the request/redirect loop. Returns [response, final_url]. Public so Session#fetch can reuse it without applying navigation state.
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/dommy/rack/navigation.rb', line 79 def run(method:, url:, params: nil, body: nil, headers: {}, follow: true) verb = method target = url # Carry the fragment across redirects: a redirect Location without its # own fragment preserves the previous one (browser behavior). fragment = uri_fragment(target) redirect_count = 0 chain = [] loop do response = @session.raw_request( verb, target, params: params, body: body, headers: headers ) unless follow && redirect_to_follow?(response) response.redirects = chain return [response, with_fragment(target, fragment)] end chain << {status: response.status, url: target, location: response.location_header} redirect_count += 1 if redirect_count > @config.max_redirects raise TooManyRedirectsError, "exceeded #{@config.max_redirects} redirects" end target = resolve_url(response.location_header, target) check_same_origin!(target) location_fragment = uri_fragment(target) fragment = location_fragment unless location_fragment.nil? || location_fragment.empty? unless KEEP_METHOD_STATUSES.include?(response.status) params = nil body = nil end verb = redirect_method(response.status, verb) end end |