# Url — a small Base-only URL parser and builder for Bend 2.0.2. # # Url v1 models four textual fields: scheme, host, path, and query. Parsing is # deliberately narrower than RFC 3986: only absolute http:// and https:// URLs # are accepted; the authority must contain a non-empty host and no userinfo. # Paths and queries are retained as text, without percent decoding. import Base type Url is Data: Url{scheme: String, host: String, path: String, query: String} # Construct a URL value without validation. Use Url.parse for the v1 grammar. def Url.make(scheme: String, host: String, path: String, query: String) -> Url: Url{scheme, host, path, query} # Parse a slash-delimited path tail. def Url.parse.path( scheme: String, host: String, query: String, +tail: List<&2, String> ) -> Maybe<&2, Url>: match tail: case Nil{}: Some{Url{scheme, host, "", query}} case +first <> +rest: Some{Url{ scheme, host, String.append("/", String.join(first <> rest, "/")), query }} # Reject empty hosts and the userinfo marker before building a URL. def Url.parse.authority.host( scheme: String, query: String, host: String, +tail: List<&2, String> ) -> Maybe<&2, Url>: match host: case SNil{}: None{} case SCon{+h, +t}: Bool.pick( Maybe<&2, Url>, String.contains(SCon{h, t}, "@"), None{}, Url.parse.path(scheme, SCon{h, t}, query, tail) ) # Parse the authority and path after query splitting. The first slash-delimited # component is the host; the remaining components are rejoined verbatim. def Url.parse.authority( scheme: String, query: String, +parts: List<&2, String> ) -> Maybe<&2, Url>: match parts: case Nil{}: None{} case +host <> +tail: Url.parse.authority.host(scheme, query, host, tail) # A query is optional but, when present, must occur exactly once. Fragment # syntax is outside this subset and is rejected by parse.scheme. def Url.parse.query( scheme: String, +parts: List<&2, String> ) -> Maybe<&2, Url>: match parts: case Nil{}: None{} case +before <> Nil{}: Url.parse.authority(scheme, "", String.split(before, '/')) case +before <> +query <> Nil{}: Url.parse.authority(scheme, query, String.split(before, '/')) case _ <> _ <> _: None{} def Url.parse.after_scheme( scheme: String, +rest: String ) -> Maybe<&2, Url>: Bool.pick( Maybe<&2, Url>, String.contains(rest, "#"), None{}, Url.parse.query(scheme, String.split(rest, '?')) ) # Match the fixed lowercase "http://" or "https://" prefix without # duplicating the input string in a Boolean branch. def Url.parse.https.t8(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 47), Url.parse.after_scheme("https", t), None{}) def Url.parse.https.t7(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 47), Url.parse.https.t8(t), None{}) def Url.parse.https.t6(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 58), Url.parse.https.t7(t), None{}) def Url.parse.https.t5(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 115), Url.parse.https.t6(t), None{}) def Url.parse.http.t6(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 47), Url.parse.after_scheme("http", t), None{}) def Url.parse.http.t5(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 47), Url.parse.http.t6(t), None{}) def Url.parse.http.t4(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 58), Url.parse.http.t5(t), Url.parse.https.t6(t)) def Url.parse.http.t3(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 112), Url.parse.http.t4(t), None{}) def Url.parse.http.t2(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 116), Url.parse.http.t3(t), None{}) def Url.parse.http.t(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 116), Url.parse.http.t2(t), None{}) # Parse one complete absolute HTTP(S) URL. Scheme matching is lowercase and # exact. No whitespace, fragment, percent decoding, or DNS validation is # imposed beyond the no-userinfo host rule. def Url.parse(s: String) -> Maybe<&2, Url>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&2, Url>, U32.is_eq(x, 104), Url.parse.http.t(t), None{}) # Render the four fields in the v1 wire form. An empty query omits '?'. def Url.to_string.query(+head: String, query: String) -> String: match query: case SNil{}: head case SCon{h, t}: String.append(head, String.append("?", SCon{h, t})) def Url.to_string(u: Url) -> String: match u: case Url{scheme, host, path, query}: +head = String.append( String.append(String.append(scheme, "://"), host), path) Url.to_string.query(head, query) def Url.build(u: Url) -> String: Url.to_string(u) def Url.show(u: Url) -> String: Url.to_string(u) # Join one non-empty relative path segment to an existing URL path. A segment # beginning with '/' is not stripped; callers should pass "users", not "/users". def Url.join_path.finish( +acc: String, +segment: String, last_slash: Bool ) -> String: match last_slash: case True{}: String.append(String.reverse(acc), segment) case False{}: String.append(String.reverse(acc), String.append("/", segment)) def Url.join_path.scan( path: String, +segment: String, +acc: String, last_slash: Bool ) -> String: match path: case SNil{}: Url.join_path.finish(acc, segment, last_slash) case SCon{+h, +t}: Bool.pick( String, Char.is_eq(h, '/'), Url.join_path.scan(t, segment, SCon{h, acc}, True{}), Url.join_path.scan(t, segment, SCon{h, acc}, False{}) ) def Url.join_path.path(path: String, +segment: String) -> String: Url.join_path.scan(path, segment, SNil{}, False{}) def Url.join_path.segment( scheme: String, host: String, path: String, query: String, +segment: String ) -> Url: match segment: case SNil{}: Url{scheme, host, path, query} case SCon{+h, +t}: Url{ scheme, host, Url.join_path.path(path, SCon{h, t}), query } def Url.join_path( u: Url, +segment: String ) -> Url: match u: case Url{scheme, host, path, query}: Url.join_path.segment(scheme, host, path, query, segment)