json_lens.pony

class val JSONLens
  """
  Composable, reusable JSON path for reading and modifying nested values.

  Define a lens by chaining key/index steps, then apply it to any document:

  ```pony
  let host_lens = JSONLens("config")("database")("host")

  // Read
  match host_lens.get(doc)
  | let host: String => env.out.print(host)
  | JSONNotFound => env.out.print("no host configured")
  end

  // Modify (returns new document with the change applied)
  match host_lens.set(doc, "newhost.example.com")
  | let updated: JSONValue => // updated doc
  | JSONNotFound => // path didn't exist
  end
  ```
  """

  let _traversal: _JSONTraversal

  new val create() =>
    """
    Create an identity lens (focuses on the root value).
    """
    _traversal = _NoTraversal

  new val _trav(trav': _JSONTraversal) =>
    _traversal = trav'

  fun apply(key_or_index: (String | USize)): JSONLens =>
    """
    Compose a navigation step onto this lens.
    """
    let step: _JSONTraversal =
      match \exhaustive\ key_or_index
      | let k: String => _TravObjKey(k)
      | let i: USize => _TravArrayIndex(i)
      end
    JSONLens._trav(_traversal.compose(step))

  fun get(input: JSONValue): (JSONValue | JSONNotFound) =>
    """
    Apply this lens to read a value.
    """
    _traversal(input)

  fun set(input: JSONValue, value: JSONValue): (JSONValue | JSONNotFound) =>
    """
    Apply this lens to update a value, returning a new root.
    Returns JSONNotFound if the path doesn't exist.
    """
    _traversal.update(input, value)

  fun remove(input: JSONValue): (JSONValue | JSONNotFound) =>
    """
    Apply this lens to remove a value, returning a new root.
    Returns JSONNotFound if the path doesn't exist.
    """
    _traversal.update(input, _Delete)

  fun compose(other: JSONLens): JSONLens =>
    """
    Sequential composition: navigate this lens, then the other.
    """
    JSONLens._trav(_traversal.compose(other._traversal))

  fun or_else(alt: JSONLens): JSONLens =>
    """
    Choice: try this lens, fall back to alt if JSONNotFound.
    """
    JSONLens._trav(_traversal.or_else(alt._traversal))