Metadata-Version: 2.4
Name: linkml-xsd
Version: 0.3.2
Summary: Convert LinkML schemas to XSD 1.0/1.1 and Schematron.
Author-email: Christian Gutknecht <christian.gutknecht@glue.ch>
License: MIT
Project-URL: Homepage, https://gitlab.com/glueswe/public/linkml
Project-URL: Repository, https://gitlab.com/glueswe/public/linkml
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: click>=8.0
Requires-Dist: lxml>=4.9
Requires-Dist: pyyaml>=6.0
Requires-Dist: linkml-runtime>=1.8
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: hypothesis>=6.0; extra == "dev"
Requires-Dist: xmlschema>=2.0; extra == "dev"
Requires-Dist: ruff>=0.4.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: types-pyyaml; extra == "dev"
Requires-Dist: lxml-stubs; extra == "dev"
Requires-Dist: pre-commit>=3.0; extra == "dev"
Dynamic: license-file

# linkml-to-xsd

[![pipeline](https://gitlab.com/glueswe/public/linkml/badges/main/pipeline.svg)](https://gitlab.com/glueswe/public/linkml/-/pipelines)
[![coverage](https://gitlab.com/glueswe/public/linkml/badges/main/coverage.svg)](https://gitlab.com/glueswe/public/linkml/-/pipelines)
[![release](https://gitlab.com/glueswe/public/linkml/-/badges/release.svg)](https://gitlab.com/glueswe/public/linkml/-/releases)
[![python](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue)](pyproject.toml)
[![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)

> **Status 0.x** — the mapping is exercised by 200+ tests and at parity on a
> real production schema, but the Python API may still change before 1.0.

A Python library and CLI that converts [LinkML](https://linkml.io) schemas into
[XML Schema (XSD)](https://www.w3.org/XML/Schema) — and, for constraints XSD
cannot express, [Schematron](https://schematron.com). Built on
[`linkml-runtime`](https://github.com/linkml/linkml-runtime)'s `SchemaView`, so
`imports`, `mixins`, `slot_usage` and inlining are resolved by the reference
implementation rather than re-guessed.

## Features

- **XSD 1.0** output: classes → `xs:complexType` + global element; `is_a` →
  `xs:extension` (UPA-safe); mixins flattened; enums, user types with facets
  (`pattern`, `minimum_value`, `maximum_value`); cardinality.
- **References vs. inlining** follow LinkML semantics: a non-inlined class-range
  slot with an identifier becomes a *reference*; `inlined`/`inlined_as_list`
  produce nested elements.
- **Identity constraints**: `identifier` → `xs:key`, `unique_keys` → `xs:unique`,
  references → `xs:keyref` (real referential integrity).
- **Value constraints**: `@default` from literal `ifabsent`, `@fixed` from
  `equals_string`/`equals_number`.
- **Documentation & metadata**: `description` → `xs:documentation`,
  `class_uri`/`slot_uri`/`enum_uri`/`meaning` → `xs:appinfo`.
- **XSD 1.1** (opt-in): class `rules` also emitted as `xs:assert`.
- **Schematron**: class `rules` (precondition ⇒ postcondition) and boolean
  combinators (`any_of`/`all_of`/`exactly_one_of`/`none_of`) as XPath assertions.
- **Profiles**: the eCH-0296 / Akoma Ntoso annotation vocabulary
  (`xml_element`, `xml_attribute`, `xml_namespace`, `xsd_mixed`,
  `xsd_content_model`) via `--profile akn` — proven at parity on the real Fedlex
  SR-101.
- **Instance data → XML**: LinkML YAML/JSON data (the input of
  `linkml-convert`) serialised as XML that validates against the generated XSD
  (`linkml2xsd instance`).
- Example-XML generation and XSD validation helpers.

## Installation

From a checkout:

```bash
pip install -e ".[dev]"
```

Released versions are published to PyPI and this project's GitLab package
registry on every `vX.Y.Z` tag:

```bash
# PyPI (recommended)
pip install linkml-xsd

# GitLab package registry
pip install linkml-xsd \
  --index-url https://gitlab.com/api/v4/projects/83894258/packages/pypi/simple
```

Requires Python ≥ 3.10 (CI tests 3.10 and 3.14). Versions are derived
from git tags (`setuptools_scm`) — no manual version bumps.

**No Python at hand** (Java/XSLT toolchains)? Use the Docker image published
on every release, replacing `vX.Y.Z` with the desired version:

```bash
docker run --rm -v "$PWD":/work \
  registry.gitlab.com/glueswe/public/linkml:vX.Y.Z /work/schema.yaml -o /work/schema.xsd
```

For reproducible releases, the image uses a pinned Python base and the exact
runtime dependency closure from `requirements-docker.txt`. Library installations
continue to use the compatible dependency ranges in `pyproject.toml`.

## Usage

### CLI

Two entry points: **`gen-xsd`** (the [linkml generator
convention](https://linkml.io/linkml/generators/), like `gen-json-schema`; see
[`docs/generators/xsd.md`](docs/generators/xsd.md)) and the richer `linkml2xsd`
group (`convert` / `instance` / `example` / `validate`).

```bash
# linkml-style generator CLI
gen-xsd schema.yaml -o schema.xsd
gen-xsd schema.yaml --xsd-version 1.1 --schematron schema.sch

# Standard LinkML → XSD (targetNamespace derived from the schema id/default_prefix)
linkml2xsd convert schema.yaml -o schema.xsd

# eCH-0296 / Akoma Ntoso profile + a companion Schematron for rules
linkml2xsd convert schema.yaml -o schema.xsd --profile akn --schematron schema.sch

# LinkML instance data (YAML/JSON) → XML; root = the tree_root class (or -C)
linkml2xsd instance data.yaml -s schema.yaml -o data.xml
linkml2xsd validate data.xml --xsd schema.xsd

# Synthetic example XML (placeholder values) from the schema alone
linkml2xsd example schema.yaml -o example.xml
```

### Python API

```python
from linkml_to_xsd import XSDGenerator, SchematronGenerator, ConversionOptions

options = ConversionOptions(profile="akn", xsd_version="1.1", emit_appinfo=True)
xsd = XSDGenerator("schema.yaml", options=options).generate()

sch_gen = SchematronGenerator("schema.yaml", options=options)
sch = sch_gen.generate() if sch_gen.has_rules() else None
```

```python
from linkml_to_xsd import InstanceSerializer
from linkml_to_xsd.instance import load_data

xml = InstanceSerializer("schema.yaml", options=options).serialize(load_data("data.yaml"))
```

Pass the **same** structural options (`profile`, `inlining`, `target_namespace`,
…) to both, so the XML matches the XSD.

`XSDGenerator` accepts a file path, inline YAML, a dict, a `SchemaDefinition` or a
`SchemaFrontend`.

## Options (`ConversionOptions`)

Every genuine mapping choice is a validated field with a default:

| Option | Default | Effect |
|--------|---------|--------|
| `xsd_version` | `"1.0"` | `"1.1"` also emits `xs:assert` for rules |
| `emit_schematron` | `False` | (API flag; the CLI uses `--schematron`) |
| `inlining` | `"linkml"` | or `always_inline` / `always_reference` |
| `reference_style` | `"identifier"` | or `uri` (`key`/`idref` planned) |
| `emit_value_constraints` | `True` | facets / `@fixed` |
| `emit_identity_constraints` | `True` | `xs:key`/`unique`/`keyref` |
| `emit_documentation` / `emit_appinfo` | `True` / `False` | annotations |
| `multilingual_documentation` | `False` | split `[en] … [de] …`-tagged descriptions into per-language `xs:documentation` |
| `target_namespace` | `None` | override the derived namespace |
| `profile` | `None` | e.g. `"akn"` |

## Architecture

```
src/linkml_to_xsd/
  config.py        # ConversionOptions — one toggle per mapping decision
  frontend/        # SchemaFrontend over linkml-runtime SchemaView
  emit/            # XSDGenerator (+ naming, typemap)
  schematron/      # SchematronGenerator
  rules.py         # shared rule → XPath compiler (Schematron + xs:assert)
  profile/         # base (no-op) + akn (eCH-0296 / Akoma Ntoso)
  instance.py      # InstanceSerializer — LinkML data → XML (decisions from XSDGenerator)
  cli.py  xml_generator.py
```

See [`docs/adr/`](docs/adr/) for architectural decisions (ADR 003 records the
move to `SchemaView`) and [`docs/LINKML_XSD_COMPARISON.md`](docs/LINKML_XSD_COMPARISON.md)
for the full LinkML ↔ XSD element mapping and current status.

## Testing

Multi-tier and documented in [`tests/README.md`](tests/README.md);
[`docs/TEST_COVERAGE.md`](docs/TEST_COVERAGE.md) is the enforced feature × engine
matrix.

| Tier | Checks |
|------|--------|
| **Compliance** (`tests/compliance/*`) | one feature per case → XSD compiles, matches its golden, accepts/rejects the right instances; `data/*.yaml` serialise to exactly the reviewed `valid/*.xml` |
| **Coverage** | every case is feature-tagged; baseline features stay covered |
| **Corpus** | real `kitchen_sink.yaml` + `meta.yaml` convert and compile |
| **Property** (Hypothesis) | random supported schemas always compile |
| **XSTS reference** | inventories XSD constructs outside the converter's current mapping |

```bash
pytest
ruff check src tests && ruff format --check src tests
mypy src/linkml_to_xsd/

python tools/fetch_corpora.py [--xsts]   # opt-in corpora (git-ignored)
python tools/gen_golden.py               # refresh golden XSDs after a change
python tools/gen_golden.py --data CASE   # also data/*.yaml → valid/*.xml (review!)
python tools/coverage_matrix.py          # regenerate docs/TEST_COVERAGE.md
```

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) — most improvements are data, not code:
a new feature mapping is one compliance case + one option + one emitter change.

## License

MIT
