# OGC API - EDR Part 3 Service Profile Generator

Authoritative tooling for creating OGC API - Environmental Data Retrieval (EDR) Part 3 Service Profiles, built on Pydantic and [edr-pydantic](https://github.com/KNMI/edr-pydantic).

## Overview

Profile structure is defined as Pydantic models (`src/models.py`). Instantiating a `ServiceProfile` validates the entire profile — enums enforce normative OGC values, cross-model validators catch referential errors — before any files are written.

Collections use `edr-pydantic`'s authoritative `Collection` model directly, meaning a profile config is simultaneously a valid EDR collection descriptor and a Part 3 profile definition.

## Installation

```bash
pip install ogc-edr-profile
```

## Usage

### Generate profile artifacts

```bash
ogc-edr-profile generate --config examples/water_gauge.yaml --output ./my_profile
```

### Validate a config without generating output

```bash
ogc-edr-profile validate --config examples/water_gauge.yaml
```

### Export the JSON Schema

```bash
ogc-edr-profile schema --output profile.schema.json
```

The schema can be used for editor autocompletion — point your YAML extension at `profile.schema.json` to get inline validation and suggestions while authoring configs.

### Output

```
my_profile/
├── openapi.yaml
├── asyncapi.yaml                        # if pubsub is configured
├── profile_config.json                  # round-trip model export
├── requirements/
│   ├── requirements_class_core.adoc
│   └── core/REQ_<id>.adoc
└── abstract_tests/
    ├── ATS_class_core.adoc
    └── core/ATS_<id>.adoc
```

## Getting Started

The fastest way to create a profile is to copy the example and modify it:

```bash
cp examples/water_gauge.yaml my_profile.yaml
```

Then edit `my_profile.yaml` — at minimum change `name`, `title`, and the collection `id`. Validate as you go:

```bash
ogc-edr-profile validate --config my_profile.yaml
```

Once valid, generate:

```bash
ogc-edr-profile generate --config my_profile.yaml --output ./my_profile
```

If you prefer to build from scratch, the minimal valid config looks like this:

```yaml
name: my_profile
title: My EDR Profile

collections:
  - id: my_collection
    links:
      - href: https://example.com/collections/my_collection
        rel: self
        type: application/json
    extent:
      spatial:
        bbox:
          - [-180, -90, 180, 90]
        crs: "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
    parameter_names: {}
```

From there, add `data_queries` to declare which query types your collection supports, `output_formats` to declare response formats, `requirements` to define normative statements, and `abstract_tests` to define conformance tests. Each section is described in detail below.

---

## Authoring a Profile Config

A profile config is a YAML or JSON file that defines a `ServiceProfile`. The full machine-readable schema is in [`profile.schema.json`](profile.schema.json). Below is a field-by-field reference.

### Top-level fields

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Lowercase identifier using only `a-z`, `0-9`, `_`. Used in OGC URIs, filenames, and OpenAPI `operationId`s. e.g. `water_gauge` |
| `title` | `string` | yes | Human-readable profile title. e.g. `Water Gauge Observations Profile` |
| `version` | `string` | no | Profile version. Defaults to `1.0` |
| `collections` | `list` | yes | One or more EDR collections (see below) |
| `requirements` | `list` | no | Normative requirements (see below) |
| `abstract_tests` | `list` | no | Conformance tests — each must reference a valid requirement `id` (see below) |
| `pubsub` | `object` | no | OGC API - EDR Part 2 PubSub configuration (see below) |

---

### `collections[]`

Each collection uses the [edr-pydantic](https://github.com/KNMI/edr-pydantic) `Collection` schema — the same model an EDR server returns at `/collections/{id}`. Key fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Collection identifier. e.g. `water_gauge` |
| `title` | `string` | no | Human-readable collection name |
| `description` | `string` | no | Collection description |
| `links` | `list` | yes | At minimum a `self` link |
| `extent` | `object` | yes | Spatial (and optionally temporal/vertical) extent |
| `extent.spatial.bbox` | `list` | yes | Bounding box as `[[minLon, minLat, maxLon, maxLat]]` |
| `extent.spatial.crs` | `string` | yes | CRS URI, typically `http://www.opengis.net/def/crs/OGC/1.3/CRS84` |
| `data_queries` | `object` | no | Which EDR query types this collection supports (see below) |
| `output_formats` | `list` | no | Supported output format labels e.g. `GeoJSON`, `CoverageJSON`, `CSV` |
| `parameter_names` | `object` | no | Map of parameter id → `Parameter` object describing observed properties |

#### `data_queries`

Each key is an EDR query type. Set it to enable that query type for the collection. Supported keys:

`items` · `position` · `area` · `radius` · `cube` · `trajectory` · `corridor` · `locations` · `instances`

Each value is an `EDRQuery` object with a `link` field:

```yaml
data_queries:
  position:
    link:
      href: https://example.com/collections/water_gauge/position
      rel: data
      variables:
        query_type: position
        output_formats:
          - CoverageJSON
  items:
    link:
      href: https://example.com/collections/water_gauge/items
      rel: data
      variables:
        query_type: items
        output_formats:
          - GeoJSON
          - CSV
```

#### `parameter_names`

Describes the observed properties the collection exposes. Each entry is a `Parameter` object:

```yaml
parameter_names:
  gauge_height:
    type: Parameter
    observedProperty:
      label: Gauge Height
    unit:
      label: feet
      symbol: ft
  streamflow:
    type: Parameter
    observedProperty:
      label: Streamflow
    unit:
      label: cubic feet per second
      symbol: cfs
```

---

### `requirements[]`

Normative requirements that implementations of this profile must satisfy. Each requirement becomes a `REQ_<id>.adoc` file and an entry in the requirements class.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Lowercase, hyphen-separated identifier. e.g. `data-query-items-water-gauge`. Must match pattern `^[a-z0-9][a-z0-9\-]*$` |
| `statement` | `string` | yes | One-sentence normative statement of the requirement |
| `parts` | `list[string]` | yes | One or more SHALL/MUST clauses that make up the requirement body |

```yaml
requirements:
  - id: data-query-items-water-gauge
    statement: The water_gauge collection SHALL support the Items query type.
    parts:
      - The service SHALL provide a /collections/water_gauge/items endpoint.
      - The Items query SHALL return a GeoJSON FeatureCollection.
```

---

### `abstract_tests[]`

Conformance tests corresponding to requirements. Each test becomes an `ATS_<id>.adoc` file and an entry in the conformance class. **Every `requirement_id` must match an existing requirement `id`** — the model validator will reject the profile if not.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Must equal `requirement_id` |
| `requirement_id` | `string` | yes | The `id` of the requirement this test validates |
| `steps` | `list[string]` | yes | Ordered test steps |

```yaml
abstract_tests:
  - id: data-query-items-water-gauge
    requirement_id: data-query-items-water-gauge
    steps:
      - Send GET request to /collections/water_gauge/items.
      - Verify the response is a valid GeoJSON FeatureCollection.
```

---

### `pubsub`

Optional. Configures OGC API - EDR Part 2 (PubSub) output. When present, an `asyncapi.yaml` is generated.

| Field | Type | Default | Description |
|---|---|---|---|
| `broker_host` | `string` | `localhost` | Message broker hostname |
| `broker_port` | `integer` | `5672` | Broker port (1–65535) |
| `protocol` | `string` | `amqp` | One of `amqp`, `mqtt`, `kafka` |
| `filters` | `list` | `[]` | Subscription filters (see below) |

#### `pubsub.filters[]`

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Filter parameter name |
| `description` | `string` | yes | What the filter does |
| `type` | `string` | no | One of `string`, `number`, `array`, `boolean`. Defaults to `string` |

```yaml
pubsub:
  broker_host: localhost
  broker_port: 5672
  protocol: amqp
  filters:
    - name: state
      description: Filter by US state abbreviation.
      type: string
    - name: threshold_ft
      description: Only notify when gauge height exceeds this value.
      type: number
```

---

See [`examples/water_gauge.yaml`](examples/water_gauge.yaml) for a complete working config.

## Programmatic Use

```python
from ogc_edr_profile.models import ServiceProfile
from ogc_edr_profile.generate import generate
from pathlib import Path

profile = ServiceProfile.model_validate(config_dict)
generate(profile, Path("./output"))
```

## Repository Structure

```
├── src/
│   ├── models.py         # Authoritative Pydantic schema
│   ├── generate.py       # Serialization: validated model → OpenAPI, AsyncAPI, AsciiDoc
│   └── cli.py            # CLI entry point
├── examples/
│   └── water_gauge.yaml  # Complete example profile config
├── profile.schema.json   # Machine-readable JSON Schema for profile configs
└── pyproject.toml
```

## Standards

- OGC API - EDR Part 1: Core
- OGC API - EDR Part 2: PubSub
- OGC API - EDR Part 3: Service Profiles (draft)
- OpenAPI 3.0 / AsyncAPI 3.0
- Metanorma/AsciiDoc documentation format

## License

Apache License 2.0 — See [LICENSE](LICENSE) for details.

## Contact

- **Author**: Shane Mill (NOAA/NWS/MDL)
- **Email**: shane.mill@noaa.gov
- **Issues**: https://github.com/ShaneMill1/OGC-Service-Profile-Creation/issues
