path_segments.pony

primitive PathSegments
  """
  Split a URI path on `/` delimiters, then percent-decode each segment.

  A leading `/` produces an empty first segment (distinguishes absolute
  from relative paths). Splitting happens before decoding, so a
  percent-encoded `/` (`%2F`) within a segment is preserved as a literal
  slash in the decoded segment, not treated as a delimiter.
  """
  fun apply(path: String val)
    : (Array[String val] val | InvalidPercentEncoding val)
  =>
    """
    Split `path` on `/` and percent-decode each segment.
    """
    // Empty path produces a single empty segment
    if path.size() == 0 then
      return recover val Array[String val](1) .> push("") end
    end

    // Count segments for pre-allocation
    var count: USize = 1
    for c in path.values() do
      if c == '/' then count = count + 1 end
    end

    let segments = Array[String val](count)
    var start: USize = 0
    var i: USize = 0

    while i < path.size() do
      try
        if path(i)? == '/' then
          let raw: String val = path.substring(start.isize(), i.isize())
          match \exhaustive\ PercentDecode(raw)
          | let decoded: String val => segments.push(decoded)
          | let err: InvalidPercentEncoding val => return err
          end
          start = i + 1
        end
      else
        _Unreachable()
      end
      i = i + 1
    end

    // Final segment after last '/' (or entire path if no '/')
    let raw: String val = path.substring(start.isize(), path.size().isize())
    match \exhaustive\ PercentDecode(raw)
    | let decoded: String val => segments.push(decoded)
    | let err: InvalidPercentEncoding val => return err
    end

    let result: Array[String val] iso =
      recover iso
        Array[String val](segments.size())
      end
    for seg in segments.values() do
      result.push(seg)
    end
    consume result