uri

package module
v0.0.0-...-3bc8878 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

README

go-ruby-uri/uri

uri — go-ruby-uri

License Go Coverage Arches

A pure-Go (no cgo) reimplementation of Ruby's uri standard library, matching the behavior of MRI (CRuby) 4.0.x byte-for-byte: URI parsing and assembly, the 9-element URI.split decomposition, the scheme registry with default ports, RFC 3986 reference resolution (merge / + / route_to), normalization, and the escape/unescape and encode_www_form / decode_www_form percent-encoders — with MRI's InvalidURIError / InvalidComponentError / BadURIError taxonomy.

It is the URI backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime. Unlike net/url, it reproduces Ruby's exact semantics (opaque vs. hierarchical paths, default-port elision in to_s, '+'-for-space www-form encoding, FTP path re-rooting and ;type=), so a parsed URI round-trips through String() exactly as MRI's to_s does.

Usage

import "github.com/go-ruby-uri/uri"

u, _ := uri.Parse("http://user@host:8080/a/b?q=1#frag")
u.Scheme   // "http"
u.Userinfo // "user"
u.Host     // "host"
u.Port     // 8080  (u.HasPort == true)
u.Path     // "/a/b"
u.Query    // "q=1"
u.Fragment // "frag"
u.String() // round-trips: "http://user@host:8080/a/b?q=1#frag"

// Default-port elision matches MRI's to_s.
uri.MustParse("https://h:443/x").String() // "https://h/x"

// RFC 3986 reference resolution.
b, _ := uri.Parse("http://h/a/b/c")
m, _ := b.Merge("../x")          // http://h/a/x
r, _ := b.RouteTo("http://h/a/y") // ../y  (the inverse of Merge)

// The 9-element decomposition (scheme, userinfo, host, port, registry,
// path, opaque, query, fragment), identical to URI.split.
parts, _ := uri.Split("http://a:b@h:9/p?q#f")
// ["http" "a:b" "h" "9" "" "/p" "" "q" "f"]

// Percent-encoding (component form: '+' for space, uppercase hex).
uri.Encode("a b&c=d")              // "a+b%26c%3Dd"
uri.Decode("a+b%26c")              // "a b&c"
uri.EncodeWWWForm([][2]string{{"a","1"},{"b","x y"}}) // "a=1&b=x+y"
pairs, _ := uri.DecodeWWWForm("a=1&b=x+y")            // [[a 1] [b x y]]

API

Go Ruby
Parse(s) URI.parse(s) / URI(s)
Join(base, rels...) URI.join(base, *rels)
Split(s) [9]string URI.split(s)
(*URI).String() URI#to_s
(*URI).Merge(rel) URI#merge / URI#+
(*URI).RouteTo(dst) URI#route_to
(*URI).Normalize() URI#normalize
(*URI).Absolute() / Relative() URI#absolute? / URI#relative?
(*URI).Hostname() URI#hostname
(*URI).DefaultPort() / EffectivePort() URI#default_port / URI#port
(*URI).SetScheme/SetHost/SetPort/SetUserinfo component setters (validated)
Encode(s) / Decode(s) URI.encode_www_form_component / decode_www_form_component
EncodeWWWForm / DecodeWWWForm URI.encode_www_form / decode_www_form
Escape / Unescape / EscapeDefault URI::DEFAULT_PARSER.escape / unescape
InvalidURIError / InvalidComponentError / BadURIError URI::InvalidURIError / InvalidComponentError / BadURIError

Conformance

The test suite pins a broad differential corpus against MRI 4.0.5: parse → components, to_s round-trip, merge/route_to, encode_www_form / decode_www_form, and the error taxonomy. Deterministic golden tables (captured from MRI) run on every lane; a live MRI oracle re-derives them from a local ruby when one is present (gated on RUBY_VERSION >= 4.0, skipped on Windows / where ruby is absent), so the ruby-free, qemu and Windows lanes still hold 100% coverage.

One documented divergence is implemented to match MRI: the FTP scheme strips the path's leading slash and parses a trailing ;type=X typecode (RFC 1738), re-rooting on to_s.

Testing

go test -race -coverprofile=cover.out ./...   # 100.0% of statements
go tool cover -func=cover.out | tail -1

Portability

Pure Go, CGO_ENABLED=0. Built and tested on the six supported 64-bit targets — amd64, arm64, riscv64, loong64, ppc64le, s390x — across Linux, macOS and Windows.

License

BSD-3-Clause. See LICENSE.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package uri is a pure-Go, CGO-free port of Ruby's "uri" standard library, matching the behavior of MRI (CRuby) 4.0.x byte-for-byte across parsing, component access, reference resolution, normalization and percent-encoding.

It mirrors the semantics implemented by rbgo's prelude URI module (the generic component model plus the scheme registry, RFC 3986 reference resolution, www-form encoding and the InvalidURIError/InvalidComponentError taxonomy) and is the library rbgo binds for its `require "uri"`.

Unlike net/url, this package reproduces Ruby's exact decomposition: the 9-element URI.split tuple, opaque vs. hierarchical paths, default-port elision in to_s, www-form '+'-for-space encoding and MRI's error messages.

Index

Constants

This section is empty.

Variables

View Source
var DefaultPorts = map[string]int{
	"http":   80,
	"https":  443,
	"ftp":    21,
	"ldap":   389,
	"ldaps":  636,
	"ws":     80,
	"wss":    443,
	"ssh":    22,
	"telnet": 23,
	"nntp":   119,
}

DefaultPorts maps a scheme to its well-known port, matching URI::Generic::DEFAULT_PORTS in MRI. A scheme absent from this map has no default port and its explicit port (if any) is always rendered.

Functions

func Decode

func Decode(s string) string

Decode reverses Encode / URI.decode_www_form_component: '+' becomes a space and "%XX" sequences are decoded. A stray '%' not followed by two hex digits is left verbatim (matching MRI's lenient component decoder).

func DecodeWWWForm

func DecodeWWWForm(s string) ([][2]string, error)

DecodeWWWForm parses an application/x-www-form-urlencoded body into key/value pairs, matching URI.decode_www_form. The separator is '&' (MRI 4.0's default); each side is component-decoded. An empty input yields no pairs. A pair with no '=' decodes to a value of "".

func Encode

func Encode(s string, unsafe ...string) string

Encode percent-encodes s the way URI.encode_www_form_component does: every byte outside the www-form unreserved set ([A-Za-z0-9*\-._]) is %-escaped, and a space becomes '+'. This is the per-component encoder used for query values.

When an optional unsafe pattern is supplied, the older URI.escape semantics are used instead via Escape; Encode itself ignores it (kept variadic to match the documented rbgo binding signature).

func EncodeWWWForm

func EncodeWWWForm(pairs [][2]string) string

EncodeWWWForm renders pairs as an application/x-www-form-urlencoded body, matching URI.encode_www_form: each key and value is component-encoded and the pairs are joined with '&'.

func Escape

func Escape(s string, unsafe string) string

Escape implements the legacy URI.escape / DEFAULT_PARSER.escape: it %-escapes every byte that is *not* allowed, where the allowed set is "unreserved or reserved" minus the characters matched by the unsafe pattern. With no unsafe argument MRI escapes everything outside the default safe set (alphanumerics, "-_.!~*'()" and the reserved/path delimiters); to keep behavior deterministic and dependency-free, an explicit unsafe string lists the characters to escape.

func EscapeDefault

func EscapeDefault(s string) string

EscapeDefault is DEFAULT_PARSER.escape with no second argument: it %-escapes every byte outside the default "safe" set, which is the unreserved set plus the reserved path/query delimiters MRI leaves intact ("/?:@!$&'()*+,;=~").

func Split

func Split(s string) ([9]string, error)

Split decomposes a URI string into the 9-element tuple MRI's URI.split returns: [scheme, userinfo, host, port, registry, path, opaque, query, fragment]. Absent components are the empty string, except that a missing port is "" and an absent query/fragment is also "" in the returned array (MRI uses nil); callers needing the absent/empty distinction should use Parse.

The element order and opaque-vs-path decision reproduce MRI exactly: a URI with a scheme whose remainder neither begins with "//" nor "/" is opaque.

func Unescape

func Unescape(s string) string

Unescape reverses Escape / EscapeDefault: it decodes "%XX" sequences but, unlike Decode, does not treat '+' as a space (URI.unescape leaves '+' alone).

Types

type BadURIError

type BadURIError struct {
	Message string
}

BadURIError corresponds to URI::BadURIError: an operation is not valid for the receiver URI (for example resolving a reference against a non-absolute base).

func (*BadURIError) Error

func (e *BadURIError) Error() string

type Error

type Error interface {
	error
	// contains filtered or unexported methods
}

Error is the common interface implemented by every error this package returns, corresponding to MRI's URI::Error module.

type InvalidComponentError

type InvalidComponentError struct {
	Component string // e.g. "scheme", "host", "port"
	Value     string
	// contains filtered or unexported fields
}

InvalidComponentError corresponds to URI::InvalidComponentError: a component value (scheme, host, port, ...) did not satisfy its grammar. The message matches MRI's "bad component(expected X component): Y".

func (*InvalidComponentError) Error

func (e *InvalidComponentError) Error() string

type InvalidURIError

type InvalidURIError struct {
	URI string
}

InvalidURIError corresponds to URI::InvalidURIError: the input could not be parsed as a URI. Its message reproduces MRI's wording byte-for-byte.

func (*InvalidURIError) Error

func (e *InvalidURIError) Error() string

type URI

type URI struct {
	Scheme   string
	Userinfo string
	Host     string
	Port     int
	HasPort  bool // distinguishes an absent port from port 0
	Path     string
	Query    string
	HasQuery bool // distinguishes an absent query ("a") from an empty one ("a?")
	Fragment string
	HasFrag  bool // distinguishes an absent fragment from an empty one ("a#")
	Opaque   string

	// FTP-specific (RFC 1738): IsFTP marks an ftp:// URI whose Path has had its
	// leading slash stripped, and Typecode holds an optional ";type=X" suffix.
	IsFTP    bool
	Typecode string
}

URI is the parsed, idiomatic-Go representation of a Ruby URI::Generic (or a scheme-specific subclass such as URI::HTTP). It is the value rbgo wraps in a URI::Generic object.

A URI is either hierarchical (it has a Path, optionally an authority) or opaque (Opaque is set and Path is empty), exactly as in MRI: a URI with a scheme whose remainder neither begins with "//" nor "/" is opaque (e.g. "mailto:foo@bar.com", "urn:isbn:1").

func Build

func Build(scheme, userinfo, host string, port int, hasPort bool, path, query string, hasQuery bool, fragment string, hasFrag bool) *URI

Build constructs a URI from explicit components, matching URI::Generic.build with a component slice in [scheme, userinfo, host, port, path, query, fragment] order. A blank component is treated as absent.

func Join

func Join(base string, rels ...string) (*URI, error)

Join resolves each successive reference against the running base, matching URI.join(base, rels...).

func MustParse

func MustParse(s string) *URI

MustParse is Parse without the error return; it panics on an invalid URI. It is a convenience for tests and constants.

func Parse

func Parse(s string) (*URI, error)

Parse decomposes a URI string into a *URI, matching URI.parse / URI(). A string that cannot be matched against the URI grammar yields an InvalidURIError, as does a port that is not all digits.

func (*URI) Absolute

func (u *URI) Absolute() bool

Absolute reports whether u has a scheme (URI#absolute?). Relative is its negation (URI#relative?).

func (*URI) DefaultPort

func (u *URI) DefaultPort() (int, bool)

DefaultPort returns the well-known port for u's scheme and whether one is defined, mirroring URI::Generic#default_port.

func (*URI) EffectivePort

func (u *URI) EffectivePort() (int, bool)

EffectivePort returns the port MRI's URI#port getter would report: the explicit port if set, otherwise the scheme's default port, and whether any port applies. rbgo uses this for the .port reader.

func (*URI) Hostname

func (u *URI) Hostname() string

Hostname returns u's host with surrounding brackets stripped from an IPv6 literal, matching URI::Generic#hostname.

func (*URI) Merge

func (u *URI) Merge(rel string) (*URI, error)

Merge resolves the relative reference rel against u using RFC 3986 reference resolution, matching URI::Generic#merge / the + operator. If rel is absolute (has a scheme) it is returned as-is.

func (*URI) Normalize

func (u *URI) Normalize() *URI

Normalize returns a copy of u with the scheme and host lowercased and an empty hierarchical path set to "/", matching URI::Generic#normalize. Opaque URIs are returned with only the scheme lowercased.

func (*URI) Relative

func (u *URI) Relative() bool

Relative reports whether u lacks a scheme (URI#relative?).

func (*URI) RouteTo

func (u *URI) RouteTo(dst string) (*URI, error)

RouteTo returns the relative reference that, resolved against u, yields dst: the inverse of Merge, matching URI::Generic#route_to. dst is given as a string and the result is the shortest relative URI MRI would produce.

func (*URI) SetHost

func (u *URI) SetHost(h string) error

SetHost validates and assigns the host. A host may not contain whitespace or the authority delimiters "/?#@"; the empty string clears it.

func (*URI) SetPort

func (u *URI) SetPort(p string) error

SetPort validates and assigns the port from its string form, matching MRI's acceptance of an integer or an all-digit string. A non-numeric value is an InvalidComponentError whose message quotes the value, as MRI does.

func (*URI) SetScheme

func (u *URI) SetScheme(s string) error

SetScheme validates and assigns the scheme. A scheme must be ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ); the empty string clears it.

func (*URI) SetUserinfo

func (u *URI) SetUserinfo(ui string) error

SetUserinfo assigns the userinfo. MRI accepts any userinfo without "/?#@", so the same delimiter check as host applies.

func (*URI) String

func (u *URI) String() string

String renders u back to its URI string, round-tripping a parsed URI exactly as MRI's URI#to_s does: the scheme default port is elided, an opaque URI renders "scheme:opaque", and empty-but-present query/fragment ("a?", "a#") are preserved.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL