json_reassembler.pony

use pc = "collections/persistent"

class ref _ObjectInProgress
  var map: pc.Map[String, JSONValue]
  var pending_key: (String | None)

  new ref create() =>
    map = pc.Map[String, JSONValue]
    pending_key = None

class ref _ArrayInProgress
  var vec: pc.Vec[JSONValue]

  new ref create() =>
    vec = pc.Vec[JSONValue]

class ref JSONReassembler is JSONTokenNotify
  """
  Assembles a run of JSON tokens into `JSONValue`s.

  This is the opt-in path from a token stream back to a materialized value. Plug
  it into a `JSONTokenParser` — it is a `JSONTokenNotify`, so tokens flow
  into it directly — or hand it tokens yourself with `add()`. Either way,
  `take_values()` returns the top-level values that have completed, the same
  `JSONValue` the batch `JSONParser` would build:

  ```pony
  let re = JSONReassembler
  let parser = JSONTokenParser(re)
  parser.feed(chunk)?
  for value in re.take_values().values() do
    // value : JSONValue — use with JSONNav, JSONLens, JSONPath, JSONPrinter
  end
  ```

  Memory is the caller's choice: building a value costs memory proportional
  to that value, so hand the reassembler only the tokens you actually want
  materialized and drop the rest. There is no built-in size cap on what it
  builds, so do not blind-drain an untrusted stream into it.

  `take_values()` returns only top-level values that have completed, and
  drains them — it is not a plain accessor. If a run ends part-way through
  a value, that value stays buffered — `mid_value()` reports it — and the
  tokens that finish it can arrive later. `add()` expects tokens in the
  order `JSONTokenParser` emits them; a run that is not a valid emission
  sequence produces an unspecified result.
  """
  embed _stack: Array[(_ObjectInProgress | _ArrayInProgress)]
  var _completed: Array[JSONValue] iso

  new ref create() =>
    _stack = Array[(_ObjectInProgress | _ArrayInProgress)]
    _completed = recover iso Array[JSONValue] end

  fun ref add(token: JSONToken) =>
    """
    Fold one token into the value under construction. Tokens must arrive in the
    order `JSONTokenParser` emits them; a run that is not a valid emission
    sequence produces an unspecified result (but never corrupts a well-formed
    run that follows a `take_values()`).
    """
    match \exhaustive\ token
    | JSONTokenObjectStart => _stack.push(_ObjectInProgress)
    | JSONTokenArrayStart => _stack.push(_ArrayInProgress)
    | let k: JSONTokenKey =>
      try
        match _stack(_stack.size() - 1)?
        | let obj: _ObjectInProgress => obj.pending_key = k.value
        end
      end
    | let s: JSONTokenString => _add_value(s.value)
    | let n: JSONTokenNumber => _add_value(n.value)
    | JSONTokenTrue => _add_value(true)
    | JSONTokenFalse => _add_value(false)
    | JSONTokenNull => _add_value(None)
    | JSONTokenObjectEnd =>
      // Check the top's type before popping, so a mismatched closer on an
      // invalid run leaves the stack intact rather than dropping the container
      // it popped.
      try
        match _stack(_stack.size() - 1)?
        | let obj: _ObjectInProgress =>
          _stack.pop()?
          _add_value(JSONObject(obj.map))
        end
      end
    | JSONTokenArrayEnd =>
      try
        match _stack(_stack.size() - 1)?
        | let arr: _ArrayInProgress =>
          _stack.pop()?
          _add_value(JSONArray(arr.vec))
        end
      end
    end

  fun ref apply(parser: JSONTokenParser, token: JSONToken) =>
    """
    `JSONTokenNotify` entry point — folds the token in, ignoring `parser`.
    """
    add(token)

  fun ref take_values(): Array[JSONValue] iso^ =>
    """
    Take the top-level values completed since the last call, and reset — a
    second call returns an empty array unless more values have completed. A
    value whose root container has not yet closed is not included; check
    `mid_value()` for it.
    """
    _completed = recover iso Array[JSONValue] end

  fun mid_value(): Bool =>
    """
    True while tokens have been folded for a value that has not closed yet.
    This reports only the reassembler's own buffered partial; for byte-level
    truncation of a stream, use `JSONTokenParser.incomplete()`.
    """
    _stack.size() > 0

  fun ref _add_value(v: JSONValue) =>
    if _stack.size() == 0 then
      _completed.push(v)
    else
      try
        match \exhaustive\ _stack(_stack.size() - 1)?
        | let obj: _ObjectInProgress =>
          match obj.pending_key
          | let key: String =>
            obj.map = obj.map(key) = v
            obj.pending_key = None
          end
        | let arr: _ArrayInProgress =>
          arr.vec = arr.vec.push(v)
        end
      end
    end