Json path

class val JsonPath
  """
  Compiled JSONPath query for extracting values from JSON documents.

  JSONPath queries navigate JSON structures using string path expressions
  (RFC 9535). Compile a path with `JsonPathParser.parse()`, then apply
  it to any number of documents with `query()`:

  ```pony
  match JsonPathParser.parse("$.store.book[*].author")
  | let path: JsonPath =>
    let authors = path.query(doc)
  | let err: JsonPathParseError =>
    env.err.print(err.string())
  end
  ```

  Evaluation follows RFC 9535 semantics: missing keys, out-of-bounds
  indices, and type mismatches produce empty results, never errors.
  Only malformed path strings produce errors (at parse time). Filter
  expressions support function extensions (`length`, `count`, `match`,
  `search`, `value`) per RFC 9535 Section 2.4.

  For simple single-value extraction, `query_one()` returns the first
  match or JsonNotFound.
  """

  let _segments: Array[_Segment] val

  new val _create(segments': Array[_Segment] val) =>
    _segments = segments'

  fun query(root: JsonValue): Array[JsonValue] val =>
    """
    Execute this query against a JSON document.

    Returns all matching values. Returns an empty array if no values
    match. Evaluation never errors.
    """
    _JsonPathEval(root, root, _segments)

  fun query_one(root: JsonValue): (JsonValue | JsonNotFound) =>
    """
    Execute this query and return the first matching value, or
    JsonNotFound if no values match.

    Convenience for paths known to select at most one value.
    """
    let results = query(root)
    if results.size() > 0 then
      try results(0)? else JsonNotFound end
    else
      JsonNotFound
    end


primitive JsonPathParser
  """
  Parser for JSONPath expressions.

  Provides two entry points:
  - `parse()` returns errors as data (consistent with `JsonParser.parse()`)
  - `compile()` raises on invalid input (convenience for known-valid paths)
  """

  fun parse(path: String): (JsonPath | JsonPathParseError) =>
    """
    Parse a JSONPath expression. Returns a compiled query on success
    or a structured error on failure.
    """
    let parser = _JsonPathParser(path)
    try
      let segments = parser.parse()?
      JsonPath._create(segments)
    else
      parser.error_result()
    end

  fun compile(path: String): JsonPath ? =>
    """
    Parse a JSONPath expression, raising on invalid input.

    Use this when the path string is known to be valid (e.g., a string
    literal). For user-provided paths, prefer `parse()` which returns
    errors as data.
    """
    match JsonPathParser.parse(path)
    | let jp: JsonPath => jp
    | let _: JsonPathParseError => error
    end


class val JsonPathParseError is Stringable
  """Structured parse error for JSONPath expressions."""

  let message: String
  let offset: USize

  new val create(message': String, offset': USize) =>
    message = message'
    offset = offset'

  fun string(): String iso^ =>
    let s = recover iso String(64) end
    s.append("JSONPath parse error at offset ")
    s.append(offset.string())
    s.append(": ")
    s.append(message)
    consume s