resolve_uri.pony

primitive ResolveURI
  """
  Resolve a URI reference against a base URI per RFC 3986 section 5.

  Given an absolute base URI and any URI-reference, produces the target URI
  that the reference identifies relative to the base. This is the operation
  browsers perform when resolving `href` attributes against the current
  document URL.

  The base must be an absolute URI (must have a scheme). Returns
  `BaseURINotAbsolute` if the base lacks a scheme.
  """
  fun apply(base: URI val, reference: URI val)
    : (URI val | ResolveURIError val)
  =>
    """
    Resolve `reference` against `base` using the algorithm from RFC 3986
    section 5.2.2. The base must have a scheme; the reference may be any
    URI-reference (absolute or relative).
    """
    let base_scheme =
      match \exhaustive\ base.scheme
      | let s: String => s
      | None => return BaseURINotAbsolute
      end

    match reference.scheme
    | let r_scheme: String =>
      // Reference has scheme — use it entirely (with dot-segment removal)
      URI(
        r_scheme,
        reference.authority,
        RemoveDotSegments(reference.path),
        reference.query,
        reference.fragment)
    else
      match reference.authority
      | let r_auth: URIAuthority =>
        // Reference has authority — inherit base scheme only
        URI(
          base_scheme,
          r_auth,
          RemoveDotSegments(reference.path),
          reference.query,
          reference.fragment)
      else
        if reference.path == "" then
          // Empty path — inherit base path; inherit base query only when
          // the reference has no query at all
          let q: (String | None) =
            match reference.query
            | let rq: String => rq
            else
              base.query
            end
          URI(
            base_scheme,
            base.authority,
            base.path,
            q,
            reference.fragment)
        else
          // Non-empty relative path
          let resolved_path =
            try
              if reference.path(0)? == '/' then
                // Absolute path — use directly with dot-segment removal
                RemoveDotSegments(reference.path)
              else
                // Relative path — merge with base then remove dot segments
                RemoveDotSegments(_merge(base, reference.path))
              end
            else
              _Unreachable()
              ""
            end
          URI(
            base_scheme,
            base.authority,
            resolved_path,
            reference.query,
            reference.fragment)
        end
      end
    end

  fun _merge(base: URI val, ref_path: String val): String val =>
    """
    RFC 3986 section 5.2.3: merge a relative-path reference with a base URI.
    """
    match base.authority
    | let _: URIAuthority =>
      if base.path == "" then
        // Base has authority and empty path — prepend "/"
        return "/" + ref_path
      end
    end
    // Take base path up to and including last "/", append reference path
    try
      var i = base.path.size()
      while i > 0 do
        i = i - 1
        if base.path(i)? == '/' then
          return base.path.substring(0, (i + 1).isize()) + ref_path
        end
      end
    else
      _Unreachable()
    end
    // No "/" in base path — reference path stands alone
    ref_path

primitive BaseURINotAbsolute is Stringable
  """
  The base URI passed to `ResolveURI` does not have a scheme.

  RFC 3986 section 5.2.2 requires the base URI to be an absolute URI.
  A relative reference cannot serve as a resolution base.
  """
  fun string(): String iso^ => "BaseURINotAbsolute".clone()

type ResolveURIError is BaseURINotAbsolute