json_parser.pony

primitive JSONParser
  """
  High-level JSON parser. Returns a structured result rather than raising.

  ```pony
  match JSONParser.parse(source)
  | let json: JSONValue => // use json
  | let err: JSONParseError => env.err.print(err.string())
  end
  ```

  Built on top of `JSONTokenParser`: the token parser handles all parsing
  logic, and a `JSONReassembler` assembles the tokens into the result.
  `parse()` applies no resource limits — it is for a whole document already
  in memory, which is trusted to be as large as it is. For input of unknown
  origin, or that arrives in pieces, use `JSONTokenParser` directly with a
  `JSONParseLimits`.
  """

  fun parse(source: String): (JSONValue | JSONParseError) =>
    """
    Parse a complete JSON document from a string.
    """
    // A whole in-memory document is trusted, so parse it with no limits.
    let notify = _FirstValue
    let parser = JSONTokenParser(notify, JSONParseLimits.unlimited())
    try
      parser.feed(source)?
      parser.finish()?
    else
      if notify.trailing then
        return JSONParseError(
          "Trailing content after JSON value",
          parser.token_end(),
          parser.line())
      end
      return parser.parse_error()
    end

    let vs = notify.take_values()
    if vs.size() == 0 then
      if parser.incomplete() then
        JSONParseError(
          "Unexpected end of input",
          parser.token_end(),
          parser.line())
      else
        JSONParseError("Empty input", 0, 1)
      end
    else
      try vs(0)? else _Unreachable(); JSONParseError("Empty input", 0, 1) end
    end

class ref _FirstValue is JSONTokenNotify
  """
  `JSONParser`'s notifier. It assembles exactly the first top-level value;
  the moment a second top-level value begins it aborts the parse, so
  trailing content is detected without being built — matching the batch
  parser's contract that a document is a single value and any following
  content is an error.
  """
  embed _reassembler: JSONReassembler
  var _depth: USize = 0
  var _done: Bool = false
  var trailing: Bool = false
    """Set when a second top-level value began, aborting the parse."""

  new ref create() =>
    _reassembler = JSONReassembler

  fun ref apply(parser: JSONTokenParser, token: JSONToken) =>
    if _done then
      // A second top-level value is starting; stop before building any of it.
      trailing = true
      parser.abort()
      return
    end
    _reassembler.add(token)
    match token
    | JSONTokenObjectStart | JSONTokenArrayStart => _depth = _depth + 1
    | JSONTokenObjectEnd | JSONTokenArrayEnd =>
      _depth = _depth - 1
      if _depth == 0 then _done = true end
    | let k: JSONTokenKey => None // a key is always inside an object
    else
      // a scalar value; at the top level it completes the document
      if _depth == 0 then _done = true end
    end

  fun ref take_values(): Array[JSONValue] iso^ =>
    _reassembler.take_values()