iyi

Standard library, 0.18.0

std/uri

URI references as RFC 3986 spells them: scheme, userinfo, host, port, path, query and fragment, read by URI.parse (URI.parse? answers nil where parse panics with the reason), written back by to_s, resolved against a base by resolve (section 5.2, dot segments included), normalized by normalize (lower-case scheme and host, the host's escapes with upper-case hex digits, default port dropped, dot segments removed). Params holds an ordered multi-valued query, read by Params.parse and written by to_s the application/x-www-form-urlencoded way.

import std/uri to use it. Source: src/std/uri.iyi at v0.18.0.

Percent-encoding: encode, encode_path, encode_path_segment and encode_www_form each keep a documented set of bytes and encode every other one, % included, so each is injective and decode undoes it. decode? answers nil and decode panics for a % without two hex digits after it. Bytes above ASCII pass through the parser as they are and are encoded one by one by the encoders.

What the parser refuses is listed at parse?. What it does not check: percent escapes (decode does), and the shape of a bracketed host.

A host is kept as the reference wrote it; URI.ascii_host is the form DNS looks up and an HTTP Host header carries, an internationalized name in its xn-- labels by std/idna.

Paramsstruct

An ordered multi-value collection representing URI query parameters or application/x-www-form-urlencoded form data.

std/uriiyi doc src/std/uri.iyi
pub struct Params
  # Returns the first value for *key*, or raises if not found.
  def [](key : String) : String
  # Sets the value for *key*, replacing all previous occurrences.
  def []=(key : String, value : String) : Nil
  # Returns the first value for *key*, or nil if not found.
  def []?(key : String) : String | ::Nil
  # Appends a key-value pair, preserving duplicate keys.
  def add(key : String, value : String) : Nil
  # Deletes all entries for *key*, returning the removed values or nil.
  def delete(key : String) : Array(String) | ::Nil
  # Iterates through each key-value pair.
  def each(& : (String, String -> Nil)) : Nil
  def empty? : Bool
  # Returns all values associated with *key*.
  def fetch_all(key : String) : Array(String)
  def has_key?(key : String) : Bool
  def initialize(keys : Array(String), values : Array(String))
  def initialize
  # Parses a query string into Params, decoding each name and value the
  # way the WHATWG URL standard reads `application/x-www-form-urlencoded`:
  # `+` is a space, `%XX` is the byte, and a `%` that does not start two
  # hex digits is itself. Pairs are separated by `&` and also by `;`, as
  # the other library separates them, where the standard and Python
  # 3.12's `parse_qsl` split at `&` alone: `a=1;b=2` is two pairs here and
  # one there.
  # Never a panic - the query is whatever a client sent, and `?q=100%`
  # is what browsers send for "100%". `URI.decode` stays strict: it is
  # asked of text a program made.
  def self.parse(query : String) : Params
  def size : Int32
  # Encodes parameters to application/x-www-form-urlencoded string.
  def to_s : String
end

URIclass

std/uriiyi doc src/std/uri.iyi
pub class URI
  # The userinfo is compared as written, as the path is: `u%3Btype%3Da` and
  # `u;type=a` are two URIs (RFC 3986 section 2.2).
  def ==(other : URI) : Bool
  def absolute? : Bool
  # *host* as DNS looks it up and an HTTP `Host` header carries it: a
  # registered name's escapes decoded, since RFC 3986 section 3.2.2 has
  # them stand for UTF-8, and the name carried by `IDNA.to_ascii`, so
  # `Bücher.example` and `b%C3%BCcher.example` are both
  # `xn--bcher-kva.example`. An IP literal in brackets and an empty host
  # (`file:///x`) are answered as they are. What `IDNA.to_ascii` refuses
  # is its `IdnaError`, and so is a malformed escape. `hostname` is not
  # this: it answers what the reference wrote.
  def self.ascii_host(host : String) : String | IdnaError
  # `userinfo@host:port`, the userinfo and the host written as stored.
  def authority : String | ::Nil
  def self.decode(str : String, plus_to_space : Bool = false) : String
  # Decodes `%XX` escapes (and `+` to a space when *plus_to_space*).
  # `decode?` answers nil and `decode` panics for a `%` not followed by
  # two hex digits; a multi-byte UTF-8 character encoded byte by byte
  # comes back as the character.
  def self.decode?(str : String, plus_to_space : Bool = false) : String | ::Nil
  # `+` a space and `%XX` a byte, any other `%` kept as it is: the form
  # reading `Params.parse` makes.
  protected def self.decode_form_lenient(str : String) : String
  def self.decode_www_form(str : String, plus_to_space : Bool = true) : String
  def self.decode_www_form?(str : String, plus_to_space : Bool = true) : String | ::Nil
  def self.default_port(scheme : String) : Int32 | ::Nil
  def dup : URI
  # Percent-encodes every byte that is neither unreserved nor reserved,
  # `%` among them: `"%20"` becomes `"%2520"`, and a space `"%20"` (or
  # `"+"` when *space_to_plus*, which then writes a `+` as `"%2B"`). Bytes
  # above ASCII are encoded one by one, so UTF-8 text round-trips through
  # `decode`.
  def self.encode(str : String, space_to_plus : Bool = false) : String
  # Encodes a path, keeping `/` between its segments.
  def self.encode_path(str : String) : String
  # Encodes one path segment: only unreserved bytes pass.
  def self.encode_path_segment(str : String) : String
  # Encodes a user or a password for the authority: unreserved bytes and
  # the sub-delims, which RFC 3986 section 3.2.1 lets userinfo hold, pass;
  # `:`, which separates the two, and `@`, which ends them, do not.
  def self.encode_userinfo(str : String) : String
  # Encodes a form field the application/x-www-form-urlencoded way: a
  # space is `+` unless *space_to_plus* is false, and only unreserved
  # bytes pass.
  def self.encode_www_form(str : String, space_to_plus : Bool = true) : String
  def fragment : String | ::Nil
  def fragment=(value : String | ::Nil) : String | ::Nil
  # Gen-delims: ":" / "/" / "?" / "#" / "[" / "]" / "@"
  def self.gen_delim?(byte : UInt8) : Bool
  # Hashing follows `==` (below), the eight parts mixed as a tuple's members
  # are. Without it every URI hashed to its type, so `http://a/` and
  # `https://b:8/x?y#z` hashed alike and a table of 16,000 URI keys took
  # 11.3 s to fill where the same texts as strings took 4 ms.
  def hash : Int32
  def host : String | ::Nil
  # Stored as given: an IPv6 address is written with its brackets,
  # `"[::1]"`, the way it appears in a URI, and `hostname` takes them off.
  def host=(h : String | ::Nil) : Nil
  # The host without the brackets an IPv6 address carries.
  def hostname : String | ::Nil
  def initialize(scheme : String | ::Nil = nil, host : String | ::Nil = nil, port : Int32 | ::Nil = nil, path : String = "", query : String | ::Nil = nil, user : String | ::Nil = nil, password : String | ::Nil = nil, fragment : String | ::Nil = nil)
  def normalize : URI
  def normalize_in_place : URI
  # The host in lower case and its escapes' hex digits upper-case, the
  # two rules RFC 3986 section 6.2.2.1 gives: `downcase` alone wrote
  # `%C3%A9` as `%c3%a9`, the one spelling of the escape the section
  # normalizes away from.
  protected def self.normalized_host(host : String) : String
  def opaque? : Bool
  def self.parse(raw_url : String) : URI
  # Reads *raw_url* as a URI reference. `parse?` answers nil and `parse`
  # panics with the reason for: a space or control character anywhere
  # (`space at byte 11 in "http://exa mple.com"`), a `:` before the first
  # `/`, `?` or `#` whose prefix is not a scheme (`invalid scheme "ht!tp"
  # in ...`), a `[` in the host with no `]`, a host with a byte a host
  # cannot hold, and a port that is not decimal digits or is above 65535.
  # Percent escapes are not checked here; `decode` does that.
  def self.parse?(raw_url : String) : URI | ::Nil
  def password : String | ::Nil
  def password=(password : String | ::Nil) : Nil
  def path : String
  def path=(value : String) : String
  def port : Int32 | ::Nil
  def port=(value : Int32 | ::Nil) : Int32 | ::Nil
  def query : String | ::Nil
  def query=(value : String | ::Nil) : String | ::Nil
  def query_params : Params
  def query_params=(params : Params) : Nil
  def relative? : Bool
  # Linear in the path: the input buffer of section 5.2.4 is a cursor into
  # *path*, and each step moves it past what it took. Each step sliced the
  # rest of the path into a new string, so removing the dot segments took
  # time in the square of the path's length: 132 KB of `/a/./b/../c` took
  # 12.8 s in a release build, where splitting it at its slashes took 3 ms.
  def self.remove_dot_segments(path : String) : String
  # The dot segments of a relative reference's path taken out as resolving
  # it against any base would take them: a `..` that climbs past the
  # reference's own segments stays, since only the base knows what it
  # climbs out of, and a path that ends in a dot segment names a
  # directory, so `a/..` is `./`.
  protected def self.remove_relative_dot_segments(path : String) : String
  def self.reserved?(byte : UInt8) : Bool
  # Resolves *ref* against this URI as Base URI according to RFC 3986 section 5.2.
  def resolve(ref : URI | String) : URI
  def scheme : String | ::Nil
  def scheme=(value : String | ::Nil) : String | ::Nil
  # Sub-delims: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
  def self.sub_delim?(byte : UInt8) : Bool
  def to_s : String
  # Unreserved characters: ALPHA / DIGIT / "-" / "." / "_" / "~"
  def self.unreserved?(byte : UInt8) : Bool
  def self.unwrap_ipv6(h : String) : String
  # The user and the password are kept as the reference wrote them, escapes
  # and all, and written back so: decoded by `parse` and re-encoded by
  # `authority`, `u%3Btype%3Da@h` came back `u;type=a@h`, which RFC 3986
  # section 2.2 counts a different URI, and `é` came back `%C3%A9`. The
  # getters decode; the setters and the constructor encode what they are
  # given with `encode_userinfo`.
  def user : String | ::Nil
  def user=(user : String | ::Nil) : Nil
  def userinfo : String | ::Nil
  def userinfo=(info : String | ::Nil) : Nil
  # The user and the password as written, taken as they are.
  protected def written_userinfo(user : String | ::Nil, password : String | ::Nil) : Nil
end