oapi-profile-builder 2.0.2__tar.gz → 2.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (19) hide show
  1. {oapi_profile_builder-2.0.2/src/oapi_profile_builder.egg-info → oapi_profile_builder-2.0.3}/PKG-INFO +163 -6
  2. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/README.md +162 -5
  3. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/pyproject.toml +1 -1
  4. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/generate.py +102 -3
  5. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/models.py +115 -0
  6. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3/src/oapi_profile_builder.egg-info}/PKG-INFO +163 -6
  7. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/LICENSE +0 -0
  8. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/setup.cfg +0 -0
  9. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/__init__.py +0 -0
  10. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/cite.py +0 -0
  11. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/cite_features.py +0 -0
  12. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/cli.py +0 -0
  13. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/compile.py +0 -0
  14. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/server_validation.py +0 -0
  15. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/SOURCES.txt +0 -0
  16. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
  17. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
  18. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/requires.txt +0 -0
  19. {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oapi-profile-builder
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: Authoritative tooling for creating OGC API Service Profiles (EDR, Features)
5
5
  Author-email: Shane Mill <shane.mill@noaa.gov>
6
6
  License: Apache License
@@ -63,6 +63,9 @@ pip install oapi-profile-builder
63
63
 
64
64
  ## Workflow
65
65
 
66
+ <img width="1001" height="721" alt="OGC API Service Profile Builder - Pydantic Validation Architecture drawio" src="https://github.com/user-attachments/assets/092c3dfc-549e-41b0-8a92-af0b89689950" />
67
+
68
+
66
69
  ### 1. Author a Profile Config
67
70
 
68
71
  A profile config is a YAML or JSON file. Start with the minimal example:
@@ -293,6 +296,133 @@ The skipped tests are optional features not implemented by the server.
293
296
 
294
297
  ---
295
298
 
299
+ ## Profile Configuration Guide
300
+
301
+ This section explains what is and isn't allowed when creating a profile, and how the tool validates your configuration.
302
+
303
+ ### What Gets Validated
304
+
305
+ When you run `generate` or `validate`, the tool instantiates a `ServiceProfile` Pydantic model that enforces all of the following rules before any files are written. If any rule is violated, you get a clear error message pointing to the offending field.
306
+
307
+ #### Profile-Level Fields
308
+
309
+ | Field | Rules |
310
+ |---|---|
311
+ | `name` | Must match `^[a-z0-9_]+$` — lowercase letters, digits, and underscores only. Used in OGC URIs. |
312
+ | `title` | Any non-empty string. |
313
+ | `version` | Any string. Defaults to `"1.0"`. |
314
+ | `collections` | At least one collection is required. No duplicate `id` values. |
315
+
316
+ #### Collection IDs
317
+
318
+ By default, collection IDs can be any string accepted by edr-pydantic. To enforce a naming convention across all collections, use `collection_id_pattern`:
319
+
320
+ ```yaml
321
+ # Only allow lowercase snake_case collection IDs
322
+ collection_id_pattern: "^[a-z][a-z0-9_]*$"
323
+ ```
324
+
325
+ The pattern is matched using Python's `re.fullmatch()`, so it must match the **entire** ID string.
326
+
327
+ #### CRS, TRS, and VRS Constraints
328
+
329
+ Each collection declares a CRS in `extent.spatial.crs`, and optionally a TRS in `extent.temporal.trs` and VRS in `extent.vertical.vrs`. The profile can constrain these values in two ways:
330
+
331
+ **Enumerated list** — only the exact values listed are accepted:
332
+
333
+ ```yaml
334
+ extent_requirements:
335
+ minimum_bbox: [-180, -90, 180, 90]
336
+ allowed_crs:
337
+ - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
338
+ - "http://www.opengis.net/def/crs/EPSG/0/4326"
339
+ ```
340
+
341
+ **Regex pattern** — any value matching the pattern is accepted:
342
+
343
+ ```yaml
344
+ extent_requirements:
345
+ minimum_bbox: [-180, -90, 180, 90]
346
+ # Accept any OGC or EPSG CRS
347
+ crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
348
+ ```
349
+
350
+ If both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both**. At least one of `allowed_crs` or `crs_pattern` is required when `extent_requirements` is present.
351
+
352
+ The same enum/regex approach works for TRS (`allowed_trs` / `trs_pattern`) and VRS (`allowed_vrs` / `vrs_pattern`).
353
+
354
+ #### Parameter Name Constraints
355
+
356
+ By default, parameter names (the keys in `parameter_names`) can be any string. To enforce a naming convention, use `parameter_name_pattern`:
357
+
358
+ ```yaml
359
+ # CF-style lowercase parameter names
360
+ parameter_name_pattern: "^[a-z][a-z0-9_]*$"
361
+ ```
362
+
363
+ ```yaml
364
+ # Allow uppercase abbreviations like WMO codes
365
+ parameter_name_pattern: "^[A-Za-z][A-Za-z0-9_]*$"
366
+ ```
367
+
368
+ Every key in every collection's `parameter_names` must match this pattern. The pattern uses `re.fullmatch()`.
369
+
370
+ Additionally, per OGC API - EDR Part 3, every parameter must specify both `unit` and `observedProperty`. The tool enforces this automatically.
371
+
372
+ #### Requirement and Test IDs
373
+
374
+ | Field | Rules |
375
+ |---|---|
376
+ | Requirement `id` | Must match `^[a-z0-9][a-z0-9\-]*$` — lowercase, digits, hyphens. Cannot end with a hyphen. |
377
+ | AbstractTest `id` | Must exactly equal its `requirement_id`. |
378
+ | AbstractTest `requirement_id` | Must reference an existing requirement `id`. |
379
+
380
+ #### What Happens When Validation Fails
381
+
382
+ The tool prints a Pydantic validation error with the field path and a human-readable message. For example:
383
+
384
+ ```
385
+ Value error, Collection 'my_data' CRS 'urn:ogc:def:crs:EPSG::4326'
386
+ does not match crs_pattern '^http://www\.opengis\.net/def/crs/(OGC|EPSG)/.*$'
387
+ ```
388
+
389
+ ```
390
+ Value error, Parameter name 'WIND_SPEED' in collection 'weather'
391
+ does not match parameter_name_pattern '^[a-z][a-z0-9_]*$'
392
+ ```
393
+
394
+ ```
395
+ Value error, Collection id 'My-Collection' does not match
396
+ collection_id_pattern '^[a-z][a-z0-9_]*$'
397
+ ```
398
+
399
+ ### How Patterns Flow Into the Generated OpenAPI
400
+
401
+ When you specify `crs_pattern`, `allowed_crs`, or `parameter_name_pattern`, those constraints are embedded in the generated `openapi.yaml` so that runtime validation tools can enforce them:
402
+
403
+ - `crs_pattern` → `pattern` on the CRS string schema in collection responses
404
+ - `allowed_crs` → `enum` on the CRS string schema
405
+ - `trs_pattern` / `allowed_trs` → `pattern` / `enum` on the TRS field in extent.temporal
406
+ - `vrs_pattern` / `allowed_vrs` → `pattern` / `enum` on the VRS field in extent.vertical
407
+ - `parameter_name_pattern` → `propertyNames.pattern` on the `parameter_names` object schema
408
+
409
+ This means schemathesis, CITE tests, and client SDKs can validate server responses against these constraints without needing access to the original profile YAML.
410
+
411
+ ### Quick Reference: Regex Examples
412
+
413
+ | Use Case | Pattern |
414
+ |---|---|
415
+ | Only OGC CRS84 | `^http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84$` |
416
+ | Any OGC or EPSG CRS | `^http://www\\.opengis\\.net/def/crs/(OGC\|EPSG)/.*$` |
417
+ | Any valid CRS URI | `^http://www\\.opengis\\.net/def/crs/.*$` |
418
+ | ISO-8601 TRS family | `^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$` |
419
+ | Lowercase snake_case names | `^[a-z][a-z0-9_]*$` |
420
+ | CF standard name style | `^[a-z][a-z0-9_]*(_[a-z0-9]+)*$` |
421
+ | WMO-style alphanumeric | `^[A-Za-z][A-Za-z0-9_]*$` |
422
+ | Lowercase with hyphens | `^[a-z][a-z0-9\\-]*$` |
423
+
424
+ ---
425
+
296
426
  ## Config Reference
297
427
 
298
428
  ### Top-level fields
@@ -313,7 +443,8 @@ The skipped tests are optional features not implemented by the server.
313
443
  | `required_conformance_classes` | `list[string]` | no | Conformance classes that implementations must declare. Defaults to EDR Core |
314
444
  | `extent_requirements` | `object` | no | Profile-level extent restrictions (see below) |
315
445
  | `output_formats` | `list` | no | Profile-level output format definitions with schema references (see below) |
316
- | `collection_id_pattern` | `string` | no | Regex pattern for valid collection IDs |
446
+ | `collection_id_pattern` | `string` | no | Regex pattern that all collection IDs must match (validated at build time) |
447
+ | `parameter_name_pattern` | `string` | no | Regex pattern that all `parameter_names` keys must match (validated at build time) |
317
448
 
318
449
  ---
319
450
 
@@ -376,7 +507,7 @@ parameter_names:
376
507
 
377
508
  ### `extent_requirements`
378
509
 
379
- Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
510
+ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent. These constraints are **enforced at profile build time** — if any collection's CRS, TRS, or VRS value violates the rules here, the profile will be rejected with a clear error message. The constraints are also **embedded in the generated OpenAPI** so that downstream tools (schemathesis, CITE tests, client SDKs) can enforce them at runtime.
380
511
 
381
512
  | Field | Type | Required | Description |
382
513
  |---|---|---|---|
@@ -390,6 +521,8 @@ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
390
521
 
391
522
  **Note:** Either `allowed_crs` or `crs_pattern` must be specified.
392
523
 
524
+ **Enum approach** — lock down to specific values:
525
+
393
526
  ```yaml
394
527
  extent_requirements:
395
528
  minimum_bbox: [-180, -90, 180, 90]
@@ -400,6 +533,19 @@ extent_requirements:
400
533
  - "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
401
534
  ```
402
535
 
536
+ **Regex approach** — allow any CRS from a family:
537
+
538
+ ```yaml
539
+ extent_requirements:
540
+ minimum_bbox: [-180, -90, 180, 90]
541
+ # Accept any OGC or EPSG CRS
542
+ crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
543
+ # Accept any ISO-8601 TRS
544
+ trs_pattern: "^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$"
545
+ ```
546
+
547
+ Both approaches can coexist — if both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both** constraints.
548
+
403
549
  ---
404
550
 
405
551
  ### `output_formats[]`
@@ -561,12 +707,23 @@ This tool implements the requirements of OGC API - EDR Part 3: Service Profiles
561
707
  4. **Extent Requirements** (REQ_extent)
562
708
  - Profile-level `extent_requirements` specify minimum bounds
563
709
  - CRS/TRS/VRS restrictions via enumerated lists or regex patterns
710
+ - **Enforced at build time**: collection CRS/TRS/VRS values are validated against `allowed_*` lists and `*_pattern` regexes
711
+ - **Propagated to OpenAPI**: constraints appear as `enum` or `pattern` in the generated collection response schemas
712
+
713
+ 5. **Parameter Names** (REQ_parameter-names)
714
+ - Validates that all parameters specify `unit` and `observedProperty`
715
+ - Optional `parameter_name_pattern` enforces naming conventions across all collections
716
+ - Pattern constraints are embedded in the generated OpenAPI as `propertyNames.pattern`
717
+
718
+ 6. **Collection ID Pattern**
719
+ - Optional `collection_id_pattern` enforces naming conventions for collection IDs
720
+ - Validated at build time via `re.fullmatch()`
564
721
 
565
- 5. **Output Formats** (REQ_output-format)
722
+ 7. **Output Formats** (REQ_output-format)
566
723
  - Profile-level `output_formats` with schema references
567
724
  - Links to JSON Schema, XML Schema, or format specifications
568
725
 
569
- 6. **Pub/Sub** (REQ_pubsub)
726
+ 8. **Pub/Sub** (REQ_pubsub)
570
727
  - Automatically adds Part 2 conformance requirement when `pubsub` is present
571
728
  - AsyncAPI document specifies channels and payloads
572
729
 
@@ -619,7 +776,7 @@ generate(profile, Path("./output"))
619
776
 
620
777
  ## License
621
778
 
622
- MIT — See [LICENSE](LICENSE) for details.
779
+ Apache — See [LICENSE](LICENSE) for details.
623
780
 
624
781
  ## Contact
625
782
 
@@ -18,6 +18,9 @@ pip install oapi-profile-builder
18
18
 
19
19
  ## Workflow
20
20
 
21
+ <img width="1001" height="721" alt="OGC API Service Profile Builder - Pydantic Validation Architecture drawio" src="https://github.com/user-attachments/assets/092c3dfc-549e-41b0-8a92-af0b89689950" />
22
+
23
+
21
24
  ### 1. Author a Profile Config
22
25
 
23
26
  A profile config is a YAML or JSON file. Start with the minimal example:
@@ -248,6 +251,133 @@ The skipped tests are optional features not implemented by the server.
248
251
 
249
252
  ---
250
253
 
254
+ ## Profile Configuration Guide
255
+
256
+ This section explains what is and isn't allowed when creating a profile, and how the tool validates your configuration.
257
+
258
+ ### What Gets Validated
259
+
260
+ When you run `generate` or `validate`, the tool instantiates a `ServiceProfile` Pydantic model that enforces all of the following rules before any files are written. If any rule is violated, you get a clear error message pointing to the offending field.
261
+
262
+ #### Profile-Level Fields
263
+
264
+ | Field | Rules |
265
+ |---|---|
266
+ | `name` | Must match `^[a-z0-9_]+$` — lowercase letters, digits, and underscores only. Used in OGC URIs. |
267
+ | `title` | Any non-empty string. |
268
+ | `version` | Any string. Defaults to `"1.0"`. |
269
+ | `collections` | At least one collection is required. No duplicate `id` values. |
270
+
271
+ #### Collection IDs
272
+
273
+ By default, collection IDs can be any string accepted by edr-pydantic. To enforce a naming convention across all collections, use `collection_id_pattern`:
274
+
275
+ ```yaml
276
+ # Only allow lowercase snake_case collection IDs
277
+ collection_id_pattern: "^[a-z][a-z0-9_]*$"
278
+ ```
279
+
280
+ The pattern is matched using Python's `re.fullmatch()`, so it must match the **entire** ID string.
281
+
282
+ #### CRS, TRS, and VRS Constraints
283
+
284
+ Each collection declares a CRS in `extent.spatial.crs`, and optionally a TRS in `extent.temporal.trs` and VRS in `extent.vertical.vrs`. The profile can constrain these values in two ways:
285
+
286
+ **Enumerated list** — only the exact values listed are accepted:
287
+
288
+ ```yaml
289
+ extent_requirements:
290
+ minimum_bbox: [-180, -90, 180, 90]
291
+ allowed_crs:
292
+ - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
293
+ - "http://www.opengis.net/def/crs/EPSG/0/4326"
294
+ ```
295
+
296
+ **Regex pattern** — any value matching the pattern is accepted:
297
+
298
+ ```yaml
299
+ extent_requirements:
300
+ minimum_bbox: [-180, -90, 180, 90]
301
+ # Accept any OGC or EPSG CRS
302
+ crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
303
+ ```
304
+
305
+ If both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both**. At least one of `allowed_crs` or `crs_pattern` is required when `extent_requirements` is present.
306
+
307
+ The same enum/regex approach works for TRS (`allowed_trs` / `trs_pattern`) and VRS (`allowed_vrs` / `vrs_pattern`).
308
+
309
+ #### Parameter Name Constraints
310
+
311
+ By default, parameter names (the keys in `parameter_names`) can be any string. To enforce a naming convention, use `parameter_name_pattern`:
312
+
313
+ ```yaml
314
+ # CF-style lowercase parameter names
315
+ parameter_name_pattern: "^[a-z][a-z0-9_]*$"
316
+ ```
317
+
318
+ ```yaml
319
+ # Allow uppercase abbreviations like WMO codes
320
+ parameter_name_pattern: "^[A-Za-z][A-Za-z0-9_]*$"
321
+ ```
322
+
323
+ Every key in every collection's `parameter_names` must match this pattern. The pattern uses `re.fullmatch()`.
324
+
325
+ Additionally, per OGC API - EDR Part 3, every parameter must specify both `unit` and `observedProperty`. The tool enforces this automatically.
326
+
327
+ #### Requirement and Test IDs
328
+
329
+ | Field | Rules |
330
+ |---|---|
331
+ | Requirement `id` | Must match `^[a-z0-9][a-z0-9\-]*$` — lowercase, digits, hyphens. Cannot end with a hyphen. |
332
+ | AbstractTest `id` | Must exactly equal its `requirement_id`. |
333
+ | AbstractTest `requirement_id` | Must reference an existing requirement `id`. |
334
+
335
+ #### What Happens When Validation Fails
336
+
337
+ The tool prints a Pydantic validation error with the field path and a human-readable message. For example:
338
+
339
+ ```
340
+ Value error, Collection 'my_data' CRS 'urn:ogc:def:crs:EPSG::4326'
341
+ does not match crs_pattern '^http://www\.opengis\.net/def/crs/(OGC|EPSG)/.*$'
342
+ ```
343
+
344
+ ```
345
+ Value error, Parameter name 'WIND_SPEED' in collection 'weather'
346
+ does not match parameter_name_pattern '^[a-z][a-z0-9_]*$'
347
+ ```
348
+
349
+ ```
350
+ Value error, Collection id 'My-Collection' does not match
351
+ collection_id_pattern '^[a-z][a-z0-9_]*$'
352
+ ```
353
+
354
+ ### How Patterns Flow Into the Generated OpenAPI
355
+
356
+ When you specify `crs_pattern`, `allowed_crs`, or `parameter_name_pattern`, those constraints are embedded in the generated `openapi.yaml` so that runtime validation tools can enforce them:
357
+
358
+ - `crs_pattern` → `pattern` on the CRS string schema in collection responses
359
+ - `allowed_crs` → `enum` on the CRS string schema
360
+ - `trs_pattern` / `allowed_trs` → `pattern` / `enum` on the TRS field in extent.temporal
361
+ - `vrs_pattern` / `allowed_vrs` → `pattern` / `enum` on the VRS field in extent.vertical
362
+ - `parameter_name_pattern` → `propertyNames.pattern` on the `parameter_names` object schema
363
+
364
+ This means schemathesis, CITE tests, and client SDKs can validate server responses against these constraints without needing access to the original profile YAML.
365
+
366
+ ### Quick Reference: Regex Examples
367
+
368
+ | Use Case | Pattern |
369
+ |---|---|
370
+ | Only OGC CRS84 | `^http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84$` |
371
+ | Any OGC or EPSG CRS | `^http://www\\.opengis\\.net/def/crs/(OGC\|EPSG)/.*$` |
372
+ | Any valid CRS URI | `^http://www\\.opengis\\.net/def/crs/.*$` |
373
+ | ISO-8601 TRS family | `^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$` |
374
+ | Lowercase snake_case names | `^[a-z][a-z0-9_]*$` |
375
+ | CF standard name style | `^[a-z][a-z0-9_]*(_[a-z0-9]+)*$` |
376
+ | WMO-style alphanumeric | `^[A-Za-z][A-Za-z0-9_]*$` |
377
+ | Lowercase with hyphens | `^[a-z][a-z0-9\\-]*$` |
378
+
379
+ ---
380
+
251
381
  ## Config Reference
252
382
 
253
383
  ### Top-level fields
@@ -268,7 +398,8 @@ The skipped tests are optional features not implemented by the server.
268
398
  | `required_conformance_classes` | `list[string]` | no | Conformance classes that implementations must declare. Defaults to EDR Core |
269
399
  | `extent_requirements` | `object` | no | Profile-level extent restrictions (see below) |
270
400
  | `output_formats` | `list` | no | Profile-level output format definitions with schema references (see below) |
271
- | `collection_id_pattern` | `string` | no | Regex pattern for valid collection IDs |
401
+ | `collection_id_pattern` | `string` | no | Regex pattern that all collection IDs must match (validated at build time) |
402
+ | `parameter_name_pattern` | `string` | no | Regex pattern that all `parameter_names` keys must match (validated at build time) |
272
403
 
273
404
  ---
274
405
 
@@ -331,7 +462,7 @@ parameter_names:
331
462
 
332
463
  ### `extent_requirements`
333
464
 
334
- Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
465
+ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent. These constraints are **enforced at profile build time** — if any collection's CRS, TRS, or VRS value violates the rules here, the profile will be rejected with a clear error message. The constraints are also **embedded in the generated OpenAPI** so that downstream tools (schemathesis, CITE tests, client SDKs) can enforce them at runtime.
335
466
 
336
467
  | Field | Type | Required | Description |
337
468
  |---|---|---|---|
@@ -345,6 +476,8 @@ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
345
476
 
346
477
  **Note:** Either `allowed_crs` or `crs_pattern` must be specified.
347
478
 
479
+ **Enum approach** — lock down to specific values:
480
+
348
481
  ```yaml
349
482
  extent_requirements:
350
483
  minimum_bbox: [-180, -90, 180, 90]
@@ -355,6 +488,19 @@ extent_requirements:
355
488
  - "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
356
489
  ```
357
490
 
491
+ **Regex approach** — allow any CRS from a family:
492
+
493
+ ```yaml
494
+ extent_requirements:
495
+ minimum_bbox: [-180, -90, 180, 90]
496
+ # Accept any OGC or EPSG CRS
497
+ crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
498
+ # Accept any ISO-8601 TRS
499
+ trs_pattern: "^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$"
500
+ ```
501
+
502
+ Both approaches can coexist — if both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both** constraints.
503
+
358
504
  ---
359
505
 
360
506
  ### `output_formats[]`
@@ -516,12 +662,23 @@ This tool implements the requirements of OGC API - EDR Part 3: Service Profiles
516
662
  4. **Extent Requirements** (REQ_extent)
517
663
  - Profile-level `extent_requirements` specify minimum bounds
518
664
  - CRS/TRS/VRS restrictions via enumerated lists or regex patterns
665
+ - **Enforced at build time**: collection CRS/TRS/VRS values are validated against `allowed_*` lists and `*_pattern` regexes
666
+ - **Propagated to OpenAPI**: constraints appear as `enum` or `pattern` in the generated collection response schemas
667
+
668
+ 5. **Parameter Names** (REQ_parameter-names)
669
+ - Validates that all parameters specify `unit` and `observedProperty`
670
+ - Optional `parameter_name_pattern` enforces naming conventions across all collections
671
+ - Pattern constraints are embedded in the generated OpenAPI as `propertyNames.pattern`
672
+
673
+ 6. **Collection ID Pattern**
674
+ - Optional `collection_id_pattern` enforces naming conventions for collection IDs
675
+ - Validated at build time via `re.fullmatch()`
519
676
 
520
- 5. **Output Formats** (REQ_output-format)
677
+ 7. **Output Formats** (REQ_output-format)
521
678
  - Profile-level `output_formats` with schema references
522
679
  - Links to JSON Schema, XML Schema, or format specifications
523
680
 
524
- 6. **Pub/Sub** (REQ_pubsub)
681
+ 8. **Pub/Sub** (REQ_pubsub)
525
682
  - Automatically adds Part 2 conformance requirement when `pubsub` is present
526
683
  - AsyncAPI document specifies channels and payloads
527
684
 
@@ -574,7 +731,7 @@ generate(profile, Path("./output"))
574
731
 
575
732
  ## License
576
733
 
577
- MIT — See [LICENSE](LICENSE) for details.
734
+ Apache — See [LICENSE](LICENSE) for details.
578
735
 
579
736
  ## Contact
580
737
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "oapi-profile-builder"
7
- version = "2.0.2"
7
+ version = "2.0.3"
8
8
  description = "Authoritative tooling for creating OGC API Service Profiles (EDR, Features)"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -176,12 +176,111 @@ _QUERY_PARAMS: dict[str, list[dict]] = {
176
176
  }
177
177
 
178
178
 
179
- def _collection_paths(coll: Collection, examples: dict | None = None) -> dict:
179
+ def _collection_response_schema(coll: Collection,
180
+ profile: "ServiceProfile | None") -> dict:
181
+ """Build a 200 response schema for a single collection endpoint.
182
+
183
+ When the profile specifies extent_requirements (allowed_crs / crs_pattern,
184
+ allowed_trs / trs_pattern, allowed_vrs / vrs_pattern) or a
185
+ parameter_name_pattern, those constraints are embedded in the response
186
+ schema so that downstream tools (schemathesis, CITE, client SDKs) can
187
+ enforce them at runtime.
188
+ """
189
+ # Start with the base collection schema
190
+ crs_schema: dict = {"type": "string"}
191
+ param_name_schema: dict = {"type": "string"}
192
+
193
+ if profile and profile.extent_requirements:
194
+ er = profile.extent_requirements
195
+ if er.allowed_crs:
196
+ crs_schema["enum"] = er.allowed_crs
197
+ elif er.crs_pattern:
198
+ crs_schema["pattern"] = er.crs_pattern
199
+
200
+ if profile and profile.parameter_name_pattern:
201
+ param_name_schema["pattern"] = profile.parameter_name_pattern
202
+
203
+ schema: dict = {
204
+ "type": "object",
205
+ "required": ["id"],
206
+ "properties": {
207
+ "id": {"type": "string"},
208
+ "title": {"type": "string"},
209
+ "description": {"type": "string"},
210
+ "links": _LINKS_ARRAY,
211
+ "crs": {
212
+ "type": "array",
213
+ "items": crs_schema,
214
+ },
215
+ "parameter_names": {
216
+ "type": "object",
217
+ "additionalProperties": {"type": "object"},
218
+ },
219
+ },
220
+ }
221
+
222
+ # If we have a parameter_name_pattern, constrain the property names
223
+ if profile and profile.parameter_name_pattern:
224
+ schema["properties"]["parameter_names"]["propertyNames"] = param_name_schema
225
+
226
+ # Embed TRS constraint in extent.temporal if present
227
+ if profile and profile.extent_requirements:
228
+ er = profile.extent_requirements
229
+ trs_schema: dict = {"type": "string"}
230
+ if er.allowed_trs:
231
+ trs_schema["enum"] = er.allowed_trs
232
+ elif er.trs_pattern:
233
+ trs_schema["pattern"] = er.trs_pattern
234
+
235
+ vrs_schema: dict = {"type": "string"}
236
+ if er.allowed_vrs:
237
+ vrs_schema["enum"] = er.allowed_vrs
238
+ elif er.vrs_pattern:
239
+ vrs_schema["pattern"] = er.vrs_pattern
240
+
241
+ schema["properties"]["extent"] = {
242
+ "type": "object",
243
+ "properties": {
244
+ "spatial": {
245
+ "type": "object",
246
+ "properties": {
247
+ "bbox": {"type": "array"},
248
+ "crs": crs_schema,
249
+ },
250
+ },
251
+ "temporal": {
252
+ "type": "object",
253
+ "properties": {
254
+ "trs": trs_schema,
255
+ },
256
+ },
257
+ "vertical": {
258
+ "type": "object",
259
+ "properties": {
260
+ "vrs": vrs_schema,
261
+ },
262
+ },
263
+ },
264
+ }
265
+
266
+ return {
267
+ "description": "Collection metadata",
268
+ "content": {"application/json": {"schema": schema}},
269
+ }
270
+
271
+
272
+ def _collection_paths(coll: Collection, examples: dict | None = None,
273
+ profile: ServiceProfile | None = None) -> dict:
180
274
  paths: dict = {}
181
275
  base = f"/collections/{coll.id}"
182
276
  tag = coll.id
183
277
  desc = getattr(coll, "description", None) or coll.id
184
278
 
279
+ # Build a collection-specific response schema that includes CRS and
280
+ # parameter-name constraints from the profile's extent_requirements
281
+ # and parameter_name_pattern.
282
+ coll_schema = _collection_response_schema(coll, profile)
283
+
185
284
  paths[base] = {"get": {
186
285
  "summary": f"Get {coll.title or coll.id} metadata",
187
286
  "description": desc,
@@ -189,7 +288,7 @@ def _collection_paths(coll: Collection, examples: dict | None = None) -> dict:
189
288
  "tags": [tag],
190
289
  "parameters": [_F, _LANG],
191
290
  "responses": {
192
- "200": _R200_COLLECTION,
291
+ "200": coll_schema,
193
292
  "400": _ERR_400, "404": _ERR_404, "500": _ERR_500,
194
293
  },
195
294
  }}
@@ -477,7 +576,7 @@ def _processes_paths(profile: ServiceProfile) -> dict:
477
576
  def build_openapi(profile: ServiceProfile) -> dict:
478
577
  paths: dict = _core_paths(profile)
479
578
  for coll in profile.collections:
480
- paths.update(_collection_paths(coll, profile.collection_examples.get(coll.id)))
579
+ paths.update(_collection_paths(coll, profile.collection_examples.get(coll.id), profile))
481
580
  paths.update(_processes_paths(profile))
482
581
 
483
582
  tags = [{"name": "server", "description": profile.title}]
@@ -38,6 +38,7 @@ so that EDR data model types are authoritative and shared with the broader ecosy
38
38
 
39
39
  from __future__ import annotations
40
40
 
41
+ import re
41
42
  from enum import Enum
42
43
  from typing import Annotated, Literal
43
44
 
@@ -201,6 +202,10 @@ class ServiceProfile(BaseModel):
201
202
  default=None,
202
203
  description="Regex pattern for valid collection IDs"
203
204
  )
205
+ parameter_name_pattern: str | None = Field(
206
+ default=None,
207
+ description="Regex pattern that all parameter_names keys must match"
208
+ )
204
209
 
205
210
  # OGC identifiers derived from name — not user-supplied
206
211
  @property
@@ -270,3 +275,113 @@ class ServiceProfile(BaseModel):
270
275
  )
271
276
  )
272
277
  return self
278
+
279
+ @model_validator(mode="after")
280
+ def validate_collection_id_pattern(self) -> ServiceProfile:
281
+ """Validate collection IDs against collection_id_pattern if specified."""
282
+ if not self.collection_id_pattern:
283
+ return self
284
+ try:
285
+ pat = re.compile(self.collection_id_pattern)
286
+ except re.error as exc:
287
+ raise ValueError(f"Invalid collection_id_pattern regex: {exc}") from exc
288
+ for coll in self.collections:
289
+ if not pat.fullmatch(coll.id):
290
+ raise ValueError(
291
+ f"Collection id '{coll.id}' does not match "
292
+ f"collection_id_pattern '{self.collection_id_pattern}'"
293
+ )
294
+ return self
295
+
296
+ @model_validator(mode="after")
297
+ def validate_collection_extent_patterns(self) -> ServiceProfile:
298
+ """Validate collection CRS/TRS/VRS values against extent_requirements patterns and enums."""
299
+ if not self.extent_requirements:
300
+ return self
301
+ er = self.extent_requirements
302
+
303
+ # Compile patterns once, validating regex syntax
304
+ crs_pat = _compile_optional_pattern("crs_pattern", er.crs_pattern)
305
+ trs_pat = _compile_optional_pattern("trs_pattern", er.trs_pattern)
306
+ vrs_pat = _compile_optional_pattern("vrs_pattern", er.vrs_pattern)
307
+
308
+ for coll in self.collections:
309
+ # --- CRS ---
310
+ crs = coll.extent.spatial.crs if coll.extent and coll.extent.spatial else None
311
+ if crs:
312
+ if er.allowed_crs and crs not in er.allowed_crs:
313
+ raise ValueError(
314
+ f"Collection '{coll.id}' CRS '{crs}' is not in allowed_crs "
315
+ f"{er.allowed_crs}"
316
+ )
317
+ if crs_pat and not crs_pat.fullmatch(crs):
318
+ raise ValueError(
319
+ f"Collection '{coll.id}' CRS '{crs}' does not match "
320
+ f"crs_pattern '{er.crs_pattern}'"
321
+ )
322
+
323
+ # --- TRS ---
324
+ trs = (
325
+ coll.extent.temporal.trs
326
+ if coll.extent and coll.extent.temporal else None
327
+ )
328
+ if trs:
329
+ if er.allowed_trs and trs not in er.allowed_trs:
330
+ raise ValueError(
331
+ f"Collection '{coll.id}' TRS '{trs}' is not in allowed_trs "
332
+ f"{er.allowed_trs}"
333
+ )
334
+ if trs_pat and not trs_pat.fullmatch(trs):
335
+ raise ValueError(
336
+ f"Collection '{coll.id}' TRS '{trs}' does not match "
337
+ f"trs_pattern '{er.trs_pattern}'"
338
+ )
339
+
340
+ # --- VRS ---
341
+ vrs = (
342
+ coll.extent.vertical.vrs
343
+ if coll.extent and coll.extent.vertical else None
344
+ )
345
+ if vrs:
346
+ if er.allowed_vrs and vrs not in er.allowed_vrs:
347
+ raise ValueError(
348
+ f"Collection '{coll.id}' VRS '{vrs}' is not in allowed_vrs "
349
+ f"{er.allowed_vrs}"
350
+ )
351
+ if vrs_pat and not vrs_pat.fullmatch(vrs):
352
+ raise ValueError(
353
+ f"Collection '{coll.id}' VRS '{vrs}' does not match "
354
+ f"vrs_pattern '{er.vrs_pattern}'"
355
+ )
356
+ return self
357
+
358
+ @model_validator(mode="after")
359
+ def validate_parameter_name_patterns(self) -> ServiceProfile:
360
+ """Validate parameter_names keys against parameter_name_pattern if specified."""
361
+ if not self.parameter_name_pattern:
362
+ return self
363
+ try:
364
+ pat = re.compile(self.parameter_name_pattern)
365
+ except re.error as exc:
366
+ raise ValueError(f"Invalid parameter_name_pattern regex: {exc}") from exc
367
+ for coll in self.collections:
368
+ if not coll.parameter_names:
369
+ continue
370
+ for name in coll.parameter_names.root:
371
+ if not pat.fullmatch(name):
372
+ raise ValueError(
373
+ f"Parameter name '{name}' in collection '{coll.id}' "
374
+ f"does not match parameter_name_pattern "
375
+ f"'{self.parameter_name_pattern}'"
376
+ )
377
+ return self
378
+
379
+
380
+ def _compile_optional_pattern(label: str, pattern: str | None) -> re.Pattern | None:
381
+ """Compile a regex pattern string, raising ValueError on invalid syntax."""
382
+ if pattern is None:
383
+ return None
384
+ try:
385
+ return re.compile(pattern)
386
+ except re.error as exc:
387
+ raise ValueError(f"Invalid {label} regex '{pattern}': {exc}") from exc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oapi-profile-builder
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: Authoritative tooling for creating OGC API Service Profiles (EDR, Features)
5
5
  Author-email: Shane Mill <shane.mill@noaa.gov>
6
6
  License: Apache License
@@ -63,6 +63,9 @@ pip install oapi-profile-builder
63
63
 
64
64
  ## Workflow
65
65
 
66
+ <img width="1001" height="721" alt="OGC API Service Profile Builder - Pydantic Validation Architecture drawio" src="https://github.com/user-attachments/assets/092c3dfc-549e-41b0-8a92-af0b89689950" />
67
+
68
+
66
69
  ### 1. Author a Profile Config
67
70
 
68
71
  A profile config is a YAML or JSON file. Start with the minimal example:
@@ -293,6 +296,133 @@ The skipped tests are optional features not implemented by the server.
293
296
 
294
297
  ---
295
298
 
299
+ ## Profile Configuration Guide
300
+
301
+ This section explains what is and isn't allowed when creating a profile, and how the tool validates your configuration.
302
+
303
+ ### What Gets Validated
304
+
305
+ When you run `generate` or `validate`, the tool instantiates a `ServiceProfile` Pydantic model that enforces all of the following rules before any files are written. If any rule is violated, you get a clear error message pointing to the offending field.
306
+
307
+ #### Profile-Level Fields
308
+
309
+ | Field | Rules |
310
+ |---|---|
311
+ | `name` | Must match `^[a-z0-9_]+$` — lowercase letters, digits, and underscores only. Used in OGC URIs. |
312
+ | `title` | Any non-empty string. |
313
+ | `version` | Any string. Defaults to `"1.0"`. |
314
+ | `collections` | At least one collection is required. No duplicate `id` values. |
315
+
316
+ #### Collection IDs
317
+
318
+ By default, collection IDs can be any string accepted by edr-pydantic. To enforce a naming convention across all collections, use `collection_id_pattern`:
319
+
320
+ ```yaml
321
+ # Only allow lowercase snake_case collection IDs
322
+ collection_id_pattern: "^[a-z][a-z0-9_]*$"
323
+ ```
324
+
325
+ The pattern is matched using Python's `re.fullmatch()`, so it must match the **entire** ID string.
326
+
327
+ #### CRS, TRS, and VRS Constraints
328
+
329
+ Each collection declares a CRS in `extent.spatial.crs`, and optionally a TRS in `extent.temporal.trs` and VRS in `extent.vertical.vrs`. The profile can constrain these values in two ways:
330
+
331
+ **Enumerated list** — only the exact values listed are accepted:
332
+
333
+ ```yaml
334
+ extent_requirements:
335
+ minimum_bbox: [-180, -90, 180, 90]
336
+ allowed_crs:
337
+ - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
338
+ - "http://www.opengis.net/def/crs/EPSG/0/4326"
339
+ ```
340
+
341
+ **Regex pattern** — any value matching the pattern is accepted:
342
+
343
+ ```yaml
344
+ extent_requirements:
345
+ minimum_bbox: [-180, -90, 180, 90]
346
+ # Accept any OGC or EPSG CRS
347
+ crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
348
+ ```
349
+
350
+ If both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both**. At least one of `allowed_crs` or `crs_pattern` is required when `extent_requirements` is present.
351
+
352
+ The same enum/regex approach works for TRS (`allowed_trs` / `trs_pattern`) and VRS (`allowed_vrs` / `vrs_pattern`).
353
+
354
+ #### Parameter Name Constraints
355
+
356
+ By default, parameter names (the keys in `parameter_names`) can be any string. To enforce a naming convention, use `parameter_name_pattern`:
357
+
358
+ ```yaml
359
+ # CF-style lowercase parameter names
360
+ parameter_name_pattern: "^[a-z][a-z0-9_]*$"
361
+ ```
362
+
363
+ ```yaml
364
+ # Allow uppercase abbreviations like WMO codes
365
+ parameter_name_pattern: "^[A-Za-z][A-Za-z0-9_]*$"
366
+ ```
367
+
368
+ Every key in every collection's `parameter_names` must match this pattern. The pattern uses `re.fullmatch()`.
369
+
370
+ Additionally, per OGC API - EDR Part 3, every parameter must specify both `unit` and `observedProperty`. The tool enforces this automatically.
371
+
372
+ #### Requirement and Test IDs
373
+
374
+ | Field | Rules |
375
+ |---|---|
376
+ | Requirement `id` | Must match `^[a-z0-9][a-z0-9\-]*$` — lowercase, digits, hyphens. Cannot end with a hyphen. |
377
+ | AbstractTest `id` | Must exactly equal its `requirement_id`. |
378
+ | AbstractTest `requirement_id` | Must reference an existing requirement `id`. |
379
+
380
+ #### What Happens When Validation Fails
381
+
382
+ The tool prints a Pydantic validation error with the field path and a human-readable message. For example:
383
+
384
+ ```
385
+ Value error, Collection 'my_data' CRS 'urn:ogc:def:crs:EPSG::4326'
386
+ does not match crs_pattern '^http://www\.opengis\.net/def/crs/(OGC|EPSG)/.*$'
387
+ ```
388
+
389
+ ```
390
+ Value error, Parameter name 'WIND_SPEED' in collection 'weather'
391
+ does not match parameter_name_pattern '^[a-z][a-z0-9_]*$'
392
+ ```
393
+
394
+ ```
395
+ Value error, Collection id 'My-Collection' does not match
396
+ collection_id_pattern '^[a-z][a-z0-9_]*$'
397
+ ```
398
+
399
+ ### How Patterns Flow Into the Generated OpenAPI
400
+
401
+ When you specify `crs_pattern`, `allowed_crs`, or `parameter_name_pattern`, those constraints are embedded in the generated `openapi.yaml` so that runtime validation tools can enforce them:
402
+
403
+ - `crs_pattern` → `pattern` on the CRS string schema in collection responses
404
+ - `allowed_crs` → `enum` on the CRS string schema
405
+ - `trs_pattern` / `allowed_trs` → `pattern` / `enum` on the TRS field in extent.temporal
406
+ - `vrs_pattern` / `allowed_vrs` → `pattern` / `enum` on the VRS field in extent.vertical
407
+ - `parameter_name_pattern` → `propertyNames.pattern` on the `parameter_names` object schema
408
+
409
+ This means schemathesis, CITE tests, and client SDKs can validate server responses against these constraints without needing access to the original profile YAML.
410
+
411
+ ### Quick Reference: Regex Examples
412
+
413
+ | Use Case | Pattern |
414
+ |---|---|
415
+ | Only OGC CRS84 | `^http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84$` |
416
+ | Any OGC or EPSG CRS | `^http://www\\.opengis\\.net/def/crs/(OGC\|EPSG)/.*$` |
417
+ | Any valid CRS URI | `^http://www\\.opengis\\.net/def/crs/.*$` |
418
+ | ISO-8601 TRS family | `^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$` |
419
+ | Lowercase snake_case names | `^[a-z][a-z0-9_]*$` |
420
+ | CF standard name style | `^[a-z][a-z0-9_]*(_[a-z0-9]+)*$` |
421
+ | WMO-style alphanumeric | `^[A-Za-z][A-Za-z0-9_]*$` |
422
+ | Lowercase with hyphens | `^[a-z][a-z0-9\\-]*$` |
423
+
424
+ ---
425
+
296
426
  ## Config Reference
297
427
 
298
428
  ### Top-level fields
@@ -313,7 +443,8 @@ The skipped tests are optional features not implemented by the server.
313
443
  | `required_conformance_classes` | `list[string]` | no | Conformance classes that implementations must declare. Defaults to EDR Core |
314
444
  | `extent_requirements` | `object` | no | Profile-level extent restrictions (see below) |
315
445
  | `output_formats` | `list` | no | Profile-level output format definitions with schema references (see below) |
316
- | `collection_id_pattern` | `string` | no | Regex pattern for valid collection IDs |
446
+ | `collection_id_pattern` | `string` | no | Regex pattern that all collection IDs must match (validated at build time) |
447
+ | `parameter_name_pattern` | `string` | no | Regex pattern that all `parameter_names` keys must match (validated at build time) |
317
448
 
318
449
  ---
319
450
 
@@ -376,7 +507,7 @@ parameter_names:
376
507
 
377
508
  ### `extent_requirements`
378
509
 
379
- Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
510
+ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent. These constraints are **enforced at profile build time** — if any collection's CRS, TRS, or VRS value violates the rules here, the profile will be rejected with a clear error message. The constraints are also **embedded in the generated OpenAPI** so that downstream tools (schemathesis, CITE tests, client SDKs) can enforce them at runtime.
380
511
 
381
512
  | Field | Type | Required | Description |
382
513
  |---|---|---|---|
@@ -390,6 +521,8 @@ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
390
521
 
391
522
  **Note:** Either `allowed_crs` or `crs_pattern` must be specified.
392
523
 
524
+ **Enum approach** — lock down to specific values:
525
+
393
526
  ```yaml
394
527
  extent_requirements:
395
528
  minimum_bbox: [-180, -90, 180, 90]
@@ -400,6 +533,19 @@ extent_requirements:
400
533
  - "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
401
534
  ```
402
535
 
536
+ **Regex approach** — allow any CRS from a family:
537
+
538
+ ```yaml
539
+ extent_requirements:
540
+ minimum_bbox: [-180, -90, 180, 90]
541
+ # Accept any OGC or EPSG CRS
542
+ crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
543
+ # Accept any ISO-8601 TRS
544
+ trs_pattern: "^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$"
545
+ ```
546
+
547
+ Both approaches can coexist — if both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both** constraints.
548
+
403
549
  ---
404
550
 
405
551
  ### `output_formats[]`
@@ -561,12 +707,23 @@ This tool implements the requirements of OGC API - EDR Part 3: Service Profiles
561
707
  4. **Extent Requirements** (REQ_extent)
562
708
  - Profile-level `extent_requirements` specify minimum bounds
563
709
  - CRS/TRS/VRS restrictions via enumerated lists or regex patterns
710
+ - **Enforced at build time**: collection CRS/TRS/VRS values are validated against `allowed_*` lists and `*_pattern` regexes
711
+ - **Propagated to OpenAPI**: constraints appear as `enum` or `pattern` in the generated collection response schemas
712
+
713
+ 5. **Parameter Names** (REQ_parameter-names)
714
+ - Validates that all parameters specify `unit` and `observedProperty`
715
+ - Optional `parameter_name_pattern` enforces naming conventions across all collections
716
+ - Pattern constraints are embedded in the generated OpenAPI as `propertyNames.pattern`
717
+
718
+ 6. **Collection ID Pattern**
719
+ - Optional `collection_id_pattern` enforces naming conventions for collection IDs
720
+ - Validated at build time via `re.fullmatch()`
564
721
 
565
- 5. **Output Formats** (REQ_output-format)
722
+ 7. **Output Formats** (REQ_output-format)
566
723
  - Profile-level `output_formats` with schema references
567
724
  - Links to JSON Schema, XML Schema, or format specifications
568
725
 
569
- 6. **Pub/Sub** (REQ_pubsub)
726
+ 8. **Pub/Sub** (REQ_pubsub)
570
727
  - Automatically adds Part 2 conformance requirement when `pubsub` is present
571
728
  - AsyncAPI document specifies channels and payloads
572
729
 
@@ -619,7 +776,7 @@ generate(profile, Path("./output"))
619
776
 
620
777
  ## License
621
778
 
622
- MIT — See [LICENSE](LICENSE) for details.
779
+ Apache — See [LICENSE](LICENSE) for details.
623
780
 
624
781
  ## Contact
625
782