Metadata-Version: 2.4
Name: jolt-python
Version: 0.1.3
Summary: Python re-implementation of Bazaarvoice Jolt (JSON-to-JSON transformation)
License: Apache-2.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: LICENSE-jolt-upstream
License-File: NOTICE
Dynamic: license-file

# joltpy

A Python re-implementation of [Bazaarvoice Jolt](https://github.com/bazaarvoice/jolt)
(JSON-to-JSON transformation), hand-ported from the Java source and validated
against Jolt's own upstream test fixtures.

No dependency on the Java library - this is a standalone, original implementation.

## Status

| Operation | Ported | Fixture coverage |
|---|---|---|
| `remove` (Removr) | Yes | 11/11 upstream fixtures pass |
| `default` (Defaultr) | Yes | 13/13 upstream fixtures pass |
| `sort` (Sortr) | Yes | 2/2 upstream fixtures pass |
| `shift` (Shiftr) | Yes | 62/62 upstream fixtures pass (literal/`*`/`&`/`@`/`$`/`#`/`[]`/transpose, all of it) |
| `modify-overwrite-beta` / `-default-beta` / `-define-beta` (Modifier) | Yes | 87/87 upstream fixtures pass (full stock function registry: Strings/Math/Objects/Lists, `^`/`@` context+self lookups, custom function registration) |
| Chainr (operation runner) | Yes for all of the above | Partial `(from, to)` execution API and Java-class-loading ("operation": "some.java.ClassName") not ported - N/A for a Python target |

**Ballina's real `job_jolt_spec.json` and `prefilledData_jolt_spec.json` now
run end-to-end through `Chainr`, unmodified, producing output that matches
byte-for-byte what was hand-verified earlier against the real payload.**
This was the whole point of the port - it's done.

Total: **177/177 tests passing.**

## Project layout

```
joltpy/
  chainr.py              # Chainr: runs a list of {"operation", "spec"} steps in order
  sortr.py               # Sortr
  removr/__init__.py     # Removr + RemovrLeafSpec/RemovrCompositeSpec
  defaultr/__init__.py   # Defaultr + Key/MapKey/ArrayKey
  traversr.py            # generic Map/List tree walker+writer (auto-creates containers, expands arrays)
  shiftr/__init__.py     # Shiftr + ShiftrLeafSpec/ShiftrCompositeSpec/ShiftrWriter/ShiftrTraversr + ExecutionStrategy
  modifier/
    __init__.py            # OpMode/DataType + ModifierSpec/Leaf/Composite + Overwritr/Definr/Defaultr + Modifier's ExecutionStrategy
    functions.py            # Strings/Math/Objects/Lists stock function registry + dispatch helpers
  common/
    pathelement.py         # Literal/Star*/Amp/At/Dollar/Hash/Array/Transpose path elements (LHS match + RHS evaluate)
    reference.py            # &/$/# sugar-syntax parsing (&, &1, &(1,2), ...)
    spec_string.py           # dot-notation RHS parsing, LHS key -> PathElement factory, function-arg parsing
    path_eval.py              # PathEvaluatingTraversal base + TransposeReader + prepend_root() (shared root-wrapping trick)
    tree.py                    # WalkedPath/PathStep/MatchedElement - the parallel-tree-walk bookkeeping
    optional.py                 # ABSENT sentinel (Java's 3-state Optional: absent vs present-null vs present-value)
tests/
  fixtures/              # verbatim copies of jolt-core/src/test/resources/json/{shiftr,modifier,chainr,removr,defaultr,sortr,cardinality}
  _fixture_utils.py       # loads Jolt's {input, spec, expected} JSON5-ish fixtures (strips // comments)
  test_removr.py, test_defaultr.py, test_sortr.py, test_chainr.py, test_shiftr.py, test_modifier.py
```

`tests/fixtures/cardinality` is copied in, unused - Cardinality (`cardinality`
Chainr operation) was never needed by any real spec seen so far, so it's
deliberately not ported.

## Installation

```bash
pip install jolt-python
```

## Usage

The package installs as `jolt-python` but imports as `joltpy`. The main entry
point is `Chainr`, which runs an ordered list of `{"operation", "spec"}` steps
against an input document:

```python
from joltpy import Chainr

chainr_spec = [
    {
        "operation": "shift",
        "spec": {
            "name": "customer.name",
            "email": "customer.email",
        },
    }
]

chainr = Chainr.from_spec(chainr_spec)
result = chainr.transform({"name": "Ada", "email": "ada@example.com"})
# {"customer": {"name": "Ada", "email": "ada@example.com"}}
```

Errors raise `joltpy.exceptions.JoltpyException` (or its subclasses,
`SpecException` for a malformed spec and `TransformException` for a runtime
failure) instead of failing silently:

```python
from joltpy import Chainr
from joltpy.exceptions import JoltpyException

try:
    result = chainr.transform(input_doc)
except JoltpyException:
    logger.exception("Jolt transform failed")
    raise
```

A `Chainr` built from a spec is immutable and safe to reuse across many
`transform()` calls - build it once (e.g. at app startup, from a spec file
bundled with your service) rather than per-request:

```python
import json
from pathlib import Path
from joltpy import Chainr

with open(Path("resources/jolt_spec.json"), encoding="utf-8") as f:
    chainr_spec = json.load(f)

chainr = Chainr.from_spec(chainr_spec)  # build once, reuse
```

## Running tests

```bash
python -m venv venv
venv\Scripts\activate      # Windows
pip install -e . pytest
pytest -q
```

## License

Apache 2.0 (see `LICENSE`). `tests/fixtures/` is copied verbatim from
bazaarvoice/jolt for compatibility testing - see `NOTICE`.

## Notable correctness traps hit while porting

- `TraversalBuilder.build()` silently **prepends `"root."` to every RHS
  output path** (Shiftr) and every `@`/`^` argument path (Modifier), and
  `transform()` unwraps it via `output.get("root")` / `{"root": context}` at
  the end. Missing this makes every single shift/modify produce nothing -
  it's what makes a blank RHS ("") work as a pure identity write, and what
  makes `^context.path` lookups work at all.
- `Traversr.get()` must preserve "not found" as distinct from "found, value
  is null" (a 3-state Optional, ported here as an `ABSENT` sentinel) -
  collapsing it to `None` breaks `TransposePathElement`'s "bail out cleanly
  if the lookup target doesn't exist" behavior.
- `LiteralPathElement` must implement `evaluate()` (return its own raw key)
  since RHS dot-paths are made of literal segments too, not just `&`/`$`.
- Modifier's `ModifierCompositeSpec.applyElement` must unwrap `ABSENT` to
  `None` at entry (same reason as above) - otherwise a missing top-level key
  in the input is never created via `DataType.create()`, and the whole
  branch silently no-ops instead of building the missing container.
- Modifier's `ALL_LITERALS` execution strategy (unlike Shiftr's
  `AVAILABLE_LITERALS`) must call `.apply()` for **every** literal child even
  when the key/index doesn't exist in the input yet - that's what lets it
  create map keys / expand arrays beyond current bounds. Skipping absent
  keys (like Shiftr does) breaks all of Modifier's "fill in defaults" use
  cases.

## Extensibility

`Overwritr`/`Defaultr`/`Definr` all accept an optional second constructor
arg, a `functions_map` dict (`name -> callable(*args) -> ABSENT | value`),
to register custom functions on top of (or instead of) `STOCK_FUNCTIONS` -
mirrors Java's `Modifier.Overwritr(spec, functionsMap)` constructor.
