dev.constructive.eo.jsoniter
Byte-level JSON optics over jsoniter-scala: JsoniterPrism and JsoniterTraversal focus a JSONPath '''directly in the encoded byte buffer''' — scanning for the span, decoding only the focused slice, and splicing writes back — so reads and writes never materialise the full document AST.
Attributes
Members list
Grouped members
Scanner
Hand-rolled JSON byte scanner. Resolves a PathStep list against an Array[Byte] JSON document and returns the byte span [start, end) of the resolved value, or -1 for the start if the path doesn't match.
Hand-rolled JSON byte scanner. Resolves a PathStep list against an Array[Byte] JSON document and returns the byte span [start, end) of the resolved value, or -1 for the start if the path doesn't match.
Why hand-rolled rather than reusing jsoniter-scala-core's JsonReader: the scanner only needs to skip-with-backtracking, not decode. Embedding JsonReader would force us to thread its mark / rollback state through every call site, and its API doesn't expose byte offsets directly. 100-ish LoC of dispatch is cheaper than the impedance mismatch.
The scanner is permissive about whitespace (skips it everywhere) and strict about structure (unbalanced quotes / brackets short-circuit to "miss"). It does NOT validate the document; an invalid JSON whose path step happens to resolve will return a span — decoding is the layer that surfaces validity failures.
Attributes
- Source
- JsonPathScanner.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
JsonPathScanner.type
Parser
Parser for the JSONPath subset we accept in JsoniterPrism / JsoniterTraversal. Grammar:
Parser for the JSONPath subset we accept in JsoniterPrism / JsoniterTraversal. Grammar:
path := '$' (step)*
step := '.' ident | '[' int ']' | '[*]'
ident := [A-Za-z_][A-Za-z_0-9]*
int := [0-9]+
[*] is the wildcard step — only meaningful inside a multi-focus JsoniterTraversal; JsoniterPrism rejects paths containing it at construction. Filters / recursive descent are still out of scope.
Attributes
- Source
- PathParser.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
PathParser.type
Optics
Read-write optic over a JSON byte buffer. Resolves a JSONPath subset against Array[Byte], decodes the focused slice via JsonValueCodec[A] on read, encodes-and-splices on write. No runtime AST.
Read-write optic over a JSON byte buffer. Resolves a JSONPath subset against Array[Byte], decodes the focused slice via JsonValueCodec[A] on read, encodes-and-splices on write. No runtime AST.
Carrier: dev.constructive.eo.data.Affine. Shape Optic[Array[Byte], Array[Byte], A, A, Affine]:
type X = (Array[Byte], (Array[Byte], Int, Int))Fst[X] = Array[Byte]— original source bytes (Miss carries this for pass-through)Snd[X] = (Array[Byte], Int, Int)— bytes + the focused span (Hit carries this; phase-2 splice writes use the span to memcpy the new encoding back in)
to: Array[Byte] => Affine[X, A]runs the path scanner; on hit, decodes the slice viareadFromSubArrayand packsHit(snd = (bytes, start, end), b = decoded). On miss (path doesn't resolve, decode throws), packsMiss(fst = bytes)for pass-through.from: Affine[X, A] => Array[Byte](phase 2) — Hit encodesh.bviawriteToArrayand splices into the source bytes at the recorded[start, end)span. Threearraycopys into a fresh buffer; cost is O(src.length). Miss returns the original bytes unchanged.
Composability: the standard cats-eo extension methods on Optic[..., Affine] light up automatically — .foldMap, .modify, .replace, .andThen, etc. No new Composer / AssociativeFunctor needed; reuses the existing Affine machinery.
'''Laws & preconditions''' (normative):
- The Optional laws hold '''up to canonical re-encoding of the focused slice''':
modify(identity)re-encodes the focus through the codec, normalising number forms (1e0→1.0), escapes, and key order INSIDE the span. Bytes outside the span (whitespace, sibling formatting) are never touched. Byte-for-byte identity holds only for focus slices already in the codec's canonical form. - '''Writes require a decodable current focus''': the Affine
todecodes eagerly, so.replaceonto a span whose current value doesn't decode asAis a Miss pass-through — template placeholders must be VALID encodings of the focus type.
Attributes
- Source
- JsoniterPrism.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
JsoniterPrism.type
Read-WRITE Traversal over JSON byte buffers. Resolves a wildcard-bearing JSONPath against Array[Byte], decodes each matched span via JsonValueCodec[A] on read, and encodes-and- splices every focus back in one pass on write. No runtime AST.
Read-WRITE Traversal over JSON byte buffers. Resolves a wildcard-bearing JSONPath against Array[Byte], decodes each matched span via JsonValueCodec[A] on read, and encodes-and- splices every focus back in one pass on write. No runtime AST.
Carrier: MultiFocus[PSVec] — the same carrier Traversal.each uses, peer to all the classical Traversal idioms. The choice over MultiFocus[List] matters for downstream composition: JsoniterTraversal[A].andThen(Traversal.each[List, A]) and similar same-carrier chains work via the existing mfAssocPSVec AssociativeFunctor instance, with zero-copy per-element reassembly. A List-carrier traversal would force a Composer hop or a manual .modify rebuild.
type X = (Array[Byte], List[JsonPathScanner.Span])— original bytes + each KEPT focus's span, 1:1 aligned with the foci (spans whose decode throws are dropped TOGETHER with their focus, so the write side can trust the pairing).to: Array[Byte] => (X, PSVec[A])— runs JsonPathScanner.findAll, decodes every span via the codec into a singleArray[AnyRef]allocation, wraps it as a PSVec. Spans whose decode throws are silently dropped (matches the read semantic "fold the focuses that exist") — and stay byte-untouched on write.from: ((X, PSVec[A])) => Array[Byte]— encodes each focus viawriteToArrayand splices all spans back in a single pass (segments between spans arearraycopyd verbatim). A span↔foci length mismatch (an aggregatefromafter a shape-changing carrier op) passes the original bytes through unchanged, as does an empty focus set.
Composability: standard cats-eo extensions on Optic[..., MultiFocus[PSVec]] light up automatically via mfFold[PSVec], mfFunctor[PSVec], mfAssocPSVec, etc. — .foldMap, .modify, .headOption, .length, .exists, .modifyA, same-carrier .andThen all work without anything new shipping in this module. Mirrors dev.constructive.eo.avro.AvroTraversal's byte-carried write semantics. JsoniterPrism's '''Laws & preconditions''' apply verbatim: laws hold up to canonical re-encoding of the focused slices, and writes need decodable current focuses.
Attributes
- Source
- JsoniterTraversal.scala
- Supertypes
-
class Objecttrait Matchableclass Any
- Self type
-
JsoniterTraversal.type
AST
One step of a JSON path expression:
One step of a JSON path expression:
.field— descend into an object property by exact name (powers JsoniterPrism).[i]— descend into an array element by zero-based index (powers JsoniterPrism).[*]— fan out across every element of the current array (powers JsoniterTraversal).
Filters / recursive descent are out of scope.
Attributes
- Source
- PathStep.scala
- Supertypes
-
trait Enumtrait Serializabletrait Producttrait Equalsclass Objecttrait Matchableclass AnyShow all