roas-overlay 0.2.1

Rust implementation of the OpenAPI Overlay Specification v1.0 / v1.1 — parse, validate, and apply
Documentation
# roas-overlay

Rust implementation of the [OpenAPI Overlay Specification](https://spec.openapis.org/overlay/v1.0.0.html): parse, validate, and apply Overlay documents to OpenAPI specs.

[![crates.io](https://img.shields.io/crates/v/roas-overlay.svg)](https://crates.io/crates/roas-overlay)
[![docs.rs](https://docs.rs/roas-overlay/badge.svg)](https://docs.rs/roas-overlay)

An *Overlay* is a sidecar document whose ordered list of *actions* — each a [RFC 9535 JSONPath](https://www.rfc-editor.org/rfc/rfc9535) `target` plus an `update`, `remove`, or v1.1 `copy` instruction — transforms a target OpenAPI document. Common uses: layering environment-specific changes over a base API, adding vendor extensions without forking, removing internal endpoints from a public bundle.

This crate is a sibling of [`roas`](https://crates.io/crates/roas) (the typed parser / validator / merger for OpenAPI 2.0–3.2). It operates on `serde_json::Value` so a single implementation works across every OpenAPI version, and so overlays that produce intermediate states the typed model would reject (drop a required field then add it back) still apply cleanly.

## Versions

| Overlay version | Feature flag     | Status         | Adds over the previous version                     |
|-----------------|------------------|----------------|----------------------------------------------------|
| 1.0             | `v1_0` (default) | ✅ implemented  ||
| 1.1             | `v1_1`           | ✅ implemented  | `copy` action, `info.description`                  |

`v1_0` and `v1_1` are independent — enable whichever you need. With both enabled, an `impl From<v1_0::Overlay> for v1_1::Overlay` is available for upconverting an existing v1.0 document.

## Quick start

```rust
use enumset::EnumSet;
use roas_overlay::apply::Apply;
use roas_overlay::v1_0::Overlay;
use roas_overlay::validation::Validate;

let overlay: Overlay = serde_json::from_str(r#"{
    "overlay": "1.0.0",
    "info": { "title": "Example", "version": "1.0.0" },
    "actions": [
        { "target": "$.info", "update": { "description": "Patched." } },
        { "target": "$.paths['/internal/metrics']", "remove": true }
    ]
}"#).unwrap();

overlay.validate(EnumSet::empty()).expect("overlay is well-formed");

let mut target: serde_json::Value = serde_json::from_str(r#"{
    "openapi": "3.1.0",
    "info": { "title": "API", "version": "1.0.0" },
    "paths": { "/internal/metrics": { "get": {} } }
}"#).unwrap();

let report = overlay.apply(&mut target, EnumSet::empty()).unwrap();
assert_eq!(report.actions.len(), 2);
assert_eq!(target["info"]["description"], "Patched.");
assert!(target["paths"].as_object().unwrap().is_empty());
```

YAML overlays work the same way — parse with `serde_yaml_ng` (or any other YAML crate) into `Overlay`.

## Apply algorithm

For each action in declaration order, against the *current* working copy of the target:

1. Compile the `target` JSONPath. Syntax errors abort the merge with `InvalidJsonPath`.
2. Resolve matching nodes via [`serde_json_path`]https://crates.io/crates/serde_json_path.
3. Zero matches → silent no-op (or `ZeroMatch` error under `ApplyOptions::ErrorOnZeroMatch`).
4. Targets must be objects or arrays for *every* action (spec §4.4); primitives or `null` raise `PrimitiveActionTarget`.
5. `remove: true` → drop each matched node from its container. Sibling array indices are preserved by processing matches in reverse.
6. **v1.1 only:** if `copy` is set, the action's source JSONPath is evaluated against the *current* doc; it must match exactly one node (`CopySourceNotFound` / `CopySourceMultiple`). The source value is then used as the merge value, exactly like `update`. Setting both `update` and `copy` raises `ConflictingMergeSources`.
7. Otherwise, the merge value (`update`, or the `copy` source) is merged into each matched node by kind:
   - **Object target** → recursive merge per [§4.4.3.1]https://spec.openapis.org/overlay/v1.0.0.html#merging-rules: shared object keys recurse; primitives replace; nested arrays concatenate.
   - **Array target** → depends on the version:
     - **v1.0** — the merge value is appended as a single new element (spec §3.3: *"an entry to append to the array"*).
     - **v1.1** — an *array* merge value is concatenated element-wise; an *object or primitive* merge value is appended as a single element (spec §3.3).

On any error the target document is left **untouched** — `Overlay::apply` operates on a clone and commits only on success.

## Options

`ApplyOptions` (EnumSet):

- `ErrorOnZeroMatch` — fail when an action's `target` selects zero nodes (default: silent no-op).
- `ErrorOnMixedKindMatch` — fail when an `update` selects nodes of mixed kind (some objects, some arrays). The v1.1 spec calls this out normatively; this option lets v1.0 callers opt in.

`ValidationOptions` (EnumSet):

- `IgnoreEmptyInfoTitle`, `IgnoreEmptyInfoVersion` — allow `info.title` / `info.version` to be empty.

Behind the `clap` feature, both enums implement `clap::ValueEnum` so downstream CLIs (such as `roas-cli`) can surface them directly.

## Validation

`Validate::validate` returns every diagnostic it finds rather than failing on the first one. Diagnostics carry a JSONPath-flavor `path` (e.g. `#.actions[3].target`).

## License

Licensed under either of [Apache License, Version 2.0](../../LICENSE-APACHE) or [MIT license](../../LICENSE-MIT) at your option.