URL Inspector — Ruby source
Break any URL into its components - protocol, host, port, path, query params, hash, and credentials. Detects default ports and security at a glance, with a decode toggle for query values. Runs entirely in your browser.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# url-inspector — break any URL into components (protocol, credentials, host, port, path, query params, fragment), detecting default ports and security at a glance. Language: Ruby (3.1+, stdlib 'uri' only). Port of src/lib/url-inspector.ts — same logic as this dir's javascript.js; URI.parse is RFC 3986-leaning, so the WHATWG touches (default-port stripping, '/'-for-empty-path) are re-imposed by hand.
require 'uri'
module UrlInspector
# Well-known default ports per scheme, keyed WHATWG-style with the ':'.
DEFAULT_PORTS = { 'http:' => '80', 'https:' => '443', 'ftp:' => '21', 'ws:' => '80', 'wss:' => '443' }.freeze
# Schemes that yield a non-opaque origin (others serialise origin as "null").
ORIGIN_SCHEMES = %w[http https ws wss ftp].freeze
INVALID = 'Invalid URL - could not be parsed (include the scheme, e.g. https://)'
SCHEME_RE = %r{\A([a-zA-Z][a-zA-Z0-9+.\-]*):\/\/(.*)\z}m
module_function
# Percent-decode a query value; decode_www_form_component also maps '+' to a
# space. Malformed escapes raise - fall back to the original, like the TS.
def decode_param(v)
URI.decode_www_form_component(v)
rescue ArgumentError
v
end
# Decode a raw query string into ordered pairs, preserving duplicates.
def parse_query(raw)
raw.to_s.split('&', -1).reject(&:empty?).map do |pair|
k, _, val = pair.partition('=')
{ key: decode_param(k), value: decode_param(val) }
end
end
# Read an explicitly-written port straight from the raw input: Ruby's URI
# fills .port with the scheme default even when nothing was written, so the
# authority segment is re-parsed by hand (userinfo + IPv6 literals aware).
def raw_port(trimmed)
m = trimmed.match(SCHEME_RE) or return nil
rest = m[2]
authority = rest[0, rest.index(/[\/?#]/) || rest.length]
# Drop userinfo: everything up to the LAST '@' belongs to credentials.
at = authority.rindex('@')
hostport = at ? authority[(at + 1)..] || '' : authority
candidate = nil
if hostport.start_with?('[')
close = hostport.index(']') or return nil # unterminated IPv6 literal
candidate = hostport[(close + 2)..] if hostport[(close + 1)..].start_with?(':')
elsif (colon = hostport.index(':'))
candidate = hostport[(colon + 1)..]
end
return nil if candidate.nil?
candidate.match?(/\A\d+\z/) ? candidate : nil
end
# Parse and decompose a URL into a structured report; never raises.
def inspect_url(raw)
trimmed = raw.to_s.strip
return { valid: false, warnings: ['URL is empty'] } if trimmed.empty?
begin
uri = URI.parse(trimmed)
rescue URI::InvalidURIError
return { valid: false, warnings: [INVALID] }
end
# WHATWG validity: an absolute scheme AND an authority are both required.
return { valid: false, warnings: [INVALID] } if uri.scheme.nil? || uri.host.nil? || uri.host.empty?
scheme = uri.scheme.downcase
proto = "#{scheme}:"
warnings = []
# Credentials - uri.user / uri.password are nil when absent.
username = uri.user
password = uri.password
warnings << 'URL contains a username credential' if username && !username.empty?
warnings << 'URL contains a password credential' if password && !password.empty?
# Hostname keeps IPv6 brackets (Ruby's URI preserves them, like WHATWG).
hostname = uri.host.downcase
# Explicit port from the raw input; flag it when it equals the scheme default.
port = raw_port(trimmed)
default_port = port && DEFAULT_PORTS[proto] == port
warnings << "Port #{port} is the default for #{proto}" if default_port
# host drops a scheme-default port (WHATWG serialisation).
host = default_port ? hostname : [hostname, port].compact.join(':')
# Query params, decoded in insertion order with duplicates preserved.
params = parse_query(uri.query)
pathname = uri.path.empty? ? '/' : uri.path # WHATWG: empty path -> '/'
if pathname == '/' && uri.query.to_s.empty? && params.empty?
warnings << 'URL points to the site root (no path or query)'
end
origin = ORIGIN_SCHEMES.include?(scheme) ? "#{scheme}://#{host}" : nil
{
valid: true,
protocol: proto,
username: username && !username.empty? ? username : nil,
password: password && !password.empty? ? password : nil,
host: host,
hostname: hostname,
port: port,
pathname: pathname,
search: uri.query.to_s.empty? ? nil : "?#{uri.query}",
hash: uri.fragment.to_s.empty? ? nil : "##{uri.fragment}",
search_params: params,
origin: origin,
is_secure: %w[https wss].include?(scheme),
default_port: default_port,
warnings: warnings
}
end
end
if $PROGRAM_NAME == __FILE__
r = UrlInspector.inspect_url('https://user:pass@example.com:8443/docs/api?q=hello+world&tags=a&tags=b&path=%2Fhome#section')
puts "protocol #{r[:protocol]} secure=#{r[:is_secure]}"
puts "creds #{r[:username]}:#{r[:password]}"
puts "host #{r[:host]} (port #{r[:port]}, default=#{r[:default_port]})"
puts "path #{r[:pathname]} search #{r[:search]} hash #{r[:hash]}"
puts "params #{r[:search_params].map { |p| "#{p[:key]}=#{p[:value]}" }.join(', ')}"
puts "origin #{r[:origin]}"
puts "warnings #{r[:warnings].empty? ? '(none)' : r[:warnings].join(' | ')}"
d = UrlInspector.inspect_url('http://example.com:80/')
puts "\nhttp://example.com:80/ -> #{d[:warnings].join(' | ')}"
e = UrlInspector.inspect_url('not a url')
puts "'not a url' -> valid=#{e[:valid]} (#{e[:warnings].first})"
end
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →