metadata-gen 0.0.7

A powerful Rust library for extracting, validating, and processing metadata in YAML, TOML, and JSON formats from any content or data file.
Documentation
# `metadata-gen` architecture

How the crate is put together, for contributors. The user-facing story
is in the [README](../README.md); this page is about the shape of the
code and the decisions behind it.

## Layout

```text
metadata-gen/
├── src/
│   ├── lib.rs        # public surface, type aliases, extract_and_prepare_metadata
│   ├── metadata.rs   # front-matter detection, parsing, flattening, processing
│   ├── metatags.rs   # <meta> generation (groups) and extraction (quick-xml)
│   ├── utils.rs      # HTML escape/unescape, async file helper
│   └── error.rs      # MetadataError and ContextError
├── tests/            # one integration suite per module + an end-to-end pass
├── examples/         # one runnable example per module
├── benches/          # Criterion harness
├── fuzz/             # libFuzzer targets, seed corpus, regression inputs
├── docs/adr/         # architecture decision records
└── supply-chain/     # cargo-vet state
```

Four modules, one direction of dependency: `lib.rs` → `metadata.rs` →
`error.rs`, and `lib.rs` → `metatags.rs` → `utils.rs`. Nothing in
`metadata.rs` knows about HTML; nothing in `metatags.rs` knows about
front matter.

## End-to-end pipeline

`extract_and_prepare_metadata(content)` is the whole crate in one call:

1. **Detect and parse** (`metadata::extract_metadata`). The content is
   tried against three front-matter shapes in order: a `---` YAML fence,
   a `+++` TOML fence, then a JSON object at the top of the document.
   The first shape that *matches* wins; a shape that matches but fails
   to parse is reported as that parser's error rather than falling
   through, so a broken YAML block never silently becomes "no front
   matter".
2. **Flatten** into `Metadata`, a `HashMap<String, String>`. Nested
   tables become dotted keys (`author.name`); sequences render as
   `[a, b]`; every other scalar takes its `Display` form. The three
   flatteners (`flatten_yaml_recursive`, `flatten_toml`, `flatten_json`)
   are deliberately parallel so a change to one is visibly missing from
   the others.
3. **Process** (`metadata::process_metadata`): dates are normalised to
   `YYYY-MM-DD` (accepting ISO and `DD/MM/YYYY`), required fields
   (`title`, `date`) are checked, and a `slug` is derived from the title
   when absent.
4. **Derive keywords** (`lib::extract_keywords`) from the `keywords`
   field, split on commas.
5. **Generate tags** (`metatags::generate_metatags`): primary,
   Open Graph, Twitter, Apple and Microsoft groups, each a string of
   `<meta>` elements with attribute values passed through
   `utils::escape_html`.

The reverse direction, `metatags::extract_meta_tags`, is a single
streaming pass with `quick-xml` over an HTML document. Both `<meta …>`
and `<meta … />` shapes are handled; attribute names are compared
case-insensitively; entity references in values are decoded. Malformed
markup ends the scan and returns what was found so far, because the
common input is a whole HTML page that a strict XML reader will not
accept end to end.

## Errors

`MetadataError` is one enum with a variant per failure class and
`#[from]` conversions for the three parsers, I/O and UTF-8.
`MetadataError::context(msg)` rewrites every variant to carry a prefix;
parser errors become `custom` errors of the same parser type so the
variant is preserved. `Other` wraps a boxed error in `ContextError`,
which keeps the original reachable through `Error::source`.

## Tests

- `src/**` `#[cfg(test)]` modules pin behaviour next to the code,
  including every `context` arm and every flattener leaf kind.
- `tests/test_*.rs` are the per-module integration suites;
  `tests/test_integration.rs` drives the whole pipeline.
- Doc tests on every public item run under `cargo test`.
- `fuzz/` holds three libFuzzer targets: `fuzz_extract_metadata`,
  `fuzz_extract_meta_tags` and `fuzz_html_escape` (escape/unescape
  round trip). The seed corpus and every fixed-bug reproducer replay on
  each push.

## Coverage

The gate is 98% lines, measured with `cargo llvm-cov --all-features`.
Test-only code counts: the uncovered lines that remain are the
`_ => panic!` arms inside tests, unreachable by construction. See
[`DEVELOPMENT.md`](../DEVELOPMENT.md) for the command.

## Where to read next

- [`docs/adr/`]adr/README.md for the decisions that shape the above.
- [`DEVELOPMENT.md`]../DEVELOPMENT.md for reproducing every CI gate.