Skip to content

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 →