Skip to content

[Repo Assist] feat: add uri_string module bindings and tests#86

Draft
github-actions[bot] wants to merge 1 commit intomainfrom
repo-assist/bindings-uri-string-2026-04-c132d7ba8abba994
Draft

[Repo Assist] feat: add uri_string module bindings and tests#86
github-actions[bot] wants to merge 1 commit intomainfrom
repo-assist/bindings-uri-string-2026-04-c132d7ba8abba994

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

🤖 This is an automated contribution from Repo Assist, an AI assistant.

What

Adds Fable.Beam.UriString — F# bindings for Erlang's uri_string stdlib module (OTP 21+). This was on the Repo Assist bindings backlog.

Changes

src/otp/UriString.fs (new)

Parsing — opaque UriMap type + accessors:

Binding Description
parse(uri) Parse a URI string → Result<UriMap, string>
scheme(m) Scheme component → string option (e.g. "https")
userinfo(m) Userinfo component → string option (e.g. "user:pass")
host(m) Host component → string option (e.g. "example.com")
port(m) Port component → int option
path(m) Path component → string option
query(m) Query string component → string option (without ?)
fragment(m) Fragment component → string option (without #)

Normalization and resolution:

Binding Description
normalize(uri) RFC 3986 normalization (lowercase scheme/host, remove default ports, resolve dot-segments) → Result<string, string>
resolve(ref, base) Resolve a URI reference against a base URI → Result<string, string>

Query string handling:

Binding Description
dissectQuery(qs) Parse query string → (string * string) list
composeQuery(pairs) Build query string from pairs → string

Percent encoding / decoding:

Binding Description
percentDecode(uri) Decode %XX sequences → Result<string, string>
quote(data) Percent-encode preserving unreserved chars → string
quoteWith(data, safe) Like quote but with caller-specified safe chars → string
unquote(data) Decode percent-encoded string (never errors) → string

Design notes

  • UriMap is [<Erase>] type UriMap = UriMap of obj — zero runtime cost; it's the raw Erlang map from uri_string:parse/1
  • Accessors use maps:get(key, Map, undefined) so missing components correctly become None
  • Error-returning functions are wrapped in Emit IIFEs (same pattern as File.fs and Re.fs) to handle Erlang's {error, atom(), term()} and convert to Result<_, string>
  • quote/2 SafeChars accepts an F# string (binary) which is valid in OTP 27+

test/TestUriString.fs (new) — 20 tests

  • Parse full URI with all 7 components (scheme, userinfo, host, port, path, query, fragment)
  • Parse minimal URI (just scheme + host)
  • Parse relative and path-only URIs — verify absent components are None
  • Normalize: scheme/host lowercasing, default port removal (HTTP:80, HTTPS:443), dot-segment resolution
  • Resolve: absolute path reference, relative path reference, full URI reference
  • dissectQuery / composeQuery: key-value parsing, empty cases, roundtrip
  • percentDecode: success, passthrough, malformed encoding returns Error
  • quote, quoteWith with safe chars, unquote

Other

  • src/Fable.Beam.fsproj — added UriString.fs after Re.fs
  • test/Fable.Beam.Test.fsproj — added TestUriString.fs after TestRe.fs
  • README.md — added Fable.Beam.UriString row to the OTP modules table

Trade-offs

  • dissectQuery return type (string * string) list cannot represent query keys without values (where Erlang returns true as the value). Documented in the binding's doc comment. This edge case is rare in practice.
  • quote/1, quote/2, unquote/1 require OTP 24+. Documented; the CI uses OTP 27 so tests will pass.

Validation

F# compilation verified with dotnet build test/. Full BEAM validation (F# → Erlang → rebar3 compile → BEAM test runner) by CI.

Generated by Repo Assist · ● 2.8M ·

Adds `Fable.Beam.UriString` — F# bindings for Erlang's `uri_string` stdlib
module (available since OTP 21; quote/unquote require OTP 24+).

### Bindings (src/otp/UriString.fs)

**Parsing**:
- `parse` — parses a URI string into an opaque `UriMap` (returns Result)
- Accessors: `scheme`, `userinfo`, `host`, `port`, `path`, `query`, `fragment`
  (each returns an option since URI components are optional)

**Normalization / resolution**:
- `normalize` — RFC 3986 normalization: lowercases scheme/host, removes
  default ports, resolves dot-segments, normalizes percent-encoding
- `resolve` — resolves a URI reference against a base URI (RFC 3986)

**Query string**:
- `dissectQuery` — parses a query string into `(string * string) list`
- `composeQuery` — builds a query string from `(string * string) list`

**Percent encoding**:
- `percentDecode` — decodes %XX sequences (returns Result; errors on invalid)
- `quote` — percent-encodes data preserving only unreserved characters
- `quoteWith` — like quote but with caller-specified safe characters
- `unquote` — decodes percent-encoded data without erroring

### Tests (test/TestUriString.fs) — 20 tests

- parse full URI with all 7 components
- parse minimal URI (no port/query/fragment)
- parse relative URI, path-only URI
- normalize: scheme/host lowercasing, default port removal, dot-segment resolution
- resolve: absolute ref, relative ref, full URI reference
- dissectQuery / composeQuery: key-value parsing, empty cases, roundtrip
- percentDecode: success, passthrough, malformed encoding error
- quote, quoteWith with safe chars, unquote

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants