Metadata-Version: 2.4
Name: jsonshift
Version: 0.8.1
Summary: A zero-dependency, deterministic JSON payload mapper that transforms source data into target structures using dotted paths and array indices.
Author-email: pml-guardian <pml@guardian-asset.com>
License: MIT
Project-URL: Homepage, https://github.com/pml-guardian/jsonshift
Project-URL: Documentation, https://github.com/pml-guardian/jsonshift#readme
Project-URL: Issues, https://github.com/pml-guardian/jsonshift/issues
Keywords: json,mapping,transform,payload,data,mapper,integration
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pytest>=8.4.2
Dynamic: license-file

# ✨ jsonshift

A lightweight Python package to **convert one JSON payload into another** using a declarative mapping spec defined in JSON.

**Engine rules (designed for system integrations):**

* If the **source path does not exist** → raises **`MappingMissingError`**.
* If the **source value is `null` / `None`** → the destination receives **`None`** (not replaced).
* `defaults` only fills values when the **destination field is absent** (does not overwrite `None` or existing values).
* Supports **dotted and indexed paths**, e.g. `a.b[0].c`.
* Extended mapper (`ArrayMapper`) supports **wildcard list expansion** using `[*]`.
* NEW: Support for **optional mappings** using `optional: true`.

---

## 🧩 Installation

```bash
pip install jsonshift
# or for development:
pip install -e .
```

---

## 🚀 Basic usage

```python
from jsonshift import Mapper

payload = {
    "customer_name": "John Doe",
    "cpf": None,
    "amount": 1000.0,
    "installments": 12
}

spec = {
  "map": {
    "customer.name": "customer_name",
    "customer.cpf": "cpf",          # None is preserved
    "contract.amount": "amount",
    "contract.installments": "installments"
  },
  "defaults": {
    "contract.type": "CCB",
    "contract.origin": "ORQ"
  }
}

out = Mapper().transform(spec, payload)
print(out)
# {
#   "customer": {"name": "John Doe", "cpf": null},
#   "contract": {"amount": 1000.0, "installments": 12, "type": "CCB", "origin": "ORQ"}
# }
```

---

## 🧠 ArrayMapper — mapping lists with `[*]`

The `ArrayMapper` extends `Mapper` and adds support for **wildcard list mappings**.
You can transform lists of objects using the same declarative spec syntax.

```python
from jsonshift.array_mapper import ArrayMapper

payload = {
    "products": [
        {"id": "P-001", "name": "Notebook", "price": 4500.0, "stock": 12},
        {"id": "P-002", "name": "Mouse Gamer", "price": 250.0, "stock": 100}
    ]
}

spec = {
    "map": {
        "new_products[*].code": "products[*].id",
        "new_products[*].title": "products[*].name",
        "new_products[*].price_brl": "products[*].price",
        "new_products[*].available": "products[*].stock"
    },
    "defaults": {
        "new_products[*].currency": "BRL"
    }
}

out = ArrayMapper().transform(spec, payload)
print(out)
# {
#   "new_products": [
#     {"code": "P-001", "title": "Notebook", "price_brl": 4500.0, "available": 12, "currency": "BRL"},
#     {"code": "P-002", "title": "Mouse Gamer", "price_brl": 250.0, "available": 100, "currency": "BRL"}
#   ]
# }
```

🧩 The same number of elements is preserved — each input list item becomes one transformed output item.
Defaults with wildcards (`new_products[*].currency`) apply individually to every object in the list.

---

## 🆕 Optional fields (`optional: true`)

You can mark a mapping as **optional**, meaning:

* If the source path exists → it is mapped normally
* If the source path does NOT exist → it is silently ignored
* No `MappingMissingError` is raised

This is useful when transforming payloads with optional fields.

### Example (simple object)

```python
from jsonshift import Mapper

payload = {
    "name": "John Doe"
}

spec = {
    "map": {
        "user.full_name": "name",
        "user.nickname": {
            "path": "nickname",
            "optional": True
        }
    }
}

out = Mapper().transform(spec, payload)
print(out)
# {
#   "user": {
#     "full_name": "John Doe"
#   }
# }
```

### Example (inside arrays)

```python
from jsonshift import ArrayMapper

payload = {
    "users": [
        {"id": 1},
        {"id": 2, "phone": "9999"}
    ]
}

spec = {
    "map": {
        "items[*].phone": {
            "path": "users[*].phone",
            "optional": True
        }
    }
}

out = ArrayMapper().transform(spec, payload)
print(out)
# {
#   "items": [
#       {},
#       {"phone": "9999"}
#   ]
# }
```

Optional mappings never overwrite defaults, and defaults only apply when the destination field does not exist.

---

## 🖥️ Command-line interface (CLI)

```bash
# Transform using JSON files
jsonshift --spec spec.json --input payload.json

# or via stdin
cat payload.json | jsonshift --spec spec.json
```

---

## ▶️ Example run (from the repository)

This repository includes ready-to-use files under the [`examples/`](./examples) folder.

```bash
# From the project root
jsonshift --spec examples/spec.json --input examples/payload.json
```

**Expected output:**

```json
{
  "customer": {
    "name": "John Doe",
    "cpf": "12345678901"
  },
  "contract": {
    "amount": 1500.0,
    "installments": 6,
    "type": "CCB",
    "origin": "ORQ"
  }
}
```

---

## 📘 Spec format

```json
{
  "map": {
    "destination.path": "source.path",
    "destination[*].field": "source[*].field"
  },
  "defaults": {
    "destination.path": "<fixed_value>",
    "destination[*].field": "<fixed_value>"
  }
}
```

---

## ⚠️ Error handling

* **`MappingMissingError`** — source path not found (unless `optional: true`).
* **`InvalidDestinationPath`** — invalid destination (e.g., destination with index).
* **`TypeError`** — source list expected but got non-list in wildcard paths.

---

## 🧪 Testing

Run all unit tests:

```bash
pytest -v
```

Includes tests for:

* Core `Mapper`
* `ArrayMapper` wildcard behavior
* Optional mappings (`optional: true`)

---

## 📄 License

MIT © 2025 Pedro Marques
