-
Notifications
You must be signed in to change notification settings - Fork 0
feat: define XML preset schema (XSD v1.0) with formal validator and schema versioning #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,9 +5,42 @@ tab structure used by the application (`project_info`, `configuration`, `impleme | |
| `user_experience`, and `development_workflow`). Each section contains simple elements with | ||
| string, integer, or boolean values. | ||
|
|
||
| ### Schema Versioning | ||
|
|
||
| The `<preset>` element carries a `schema_version` attribute. This allows the application to | ||
| detect and handle format changes across releases. The current schema version is **1.0**. | ||
|
|
||
| When loading a preset file the application reads this attribute and defaults to `"1.0"` if | ||
| the attribute is absent (backwards-compatible with older preset files). | ||
|
|
||
| ### Formal XSD Schema | ||
|
|
||
| A machine-readable W3C XML Schema Definition is bundled at: | ||
|
|
||
| ```text | ||
| src/resources/presets/preset_schema.xsd | ||
| ``` | ||
|
|
||
| The XSD can be used by XML-aware IDEs (VS Code, IntelliJ, etc.) and external validators to | ||
| check preset files before loading them into the application. Python code can validate files | ||
| programmatically using `PresetXSDValidator` from `core.preset_validator`: | ||
|
|
||
| ```python | ||
| from core.preset_validator import PresetXSDValidator | ||
|
|
||
| validator = PresetXSDValidator() | ||
| ok, errors = validator.validate_file("path/to/my_preset.xml") | ||
| if not ok: | ||
| print("\n".join(errors)) | ||
| ``` | ||
|
|
||
| `validate_string(xml_content: str)` is also available for validating in-memory XML. | ||
|
|
||
| ### Full Preset Example | ||
|
|
||
| ```xml | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <preset name="PresetName"> | ||
| <preset name="PresetName" schema_version="1.0"> | ||
| <meta> | ||
| <description>Optional human-friendly description</description> | ||
| </meta> | ||
|
|
@@ -29,6 +62,16 @@ string, integer, or boolean values. | |
| <au>true</au> | ||
| <auv3>false</auv3> | ||
| <clap>true</clap> | ||
| <!-- Audio Unit component descriptor --> | ||
| <au_component_type>aufx</au_component_type> | ||
| <au_component_subtype>plug</au_component_subtype> | ||
| <au_component_manufacturer>DkFX</au_component_manufacturer> | ||
| <au_version>1.0.0</au_version> | ||
| <!-- CLAP identifiers (comma-separated lists) --> | ||
| <clap_extensions>note-ports,state</clap_extensions> | ||
| <clap_features>audio-effect</clap_features> | ||
| <!-- AUv3 target platform --> | ||
| <auv3_platform>iOS</auv3_platform> | ||
| <gui_width>1100</gui_width> | ||
| <gui_height>700</gui_height> | ||
| <resizable>true</resizable> | ||
|
|
@@ -66,15 +109,114 @@ string, integer, or boolean values. | |
| </preset> | ||
| ``` | ||
|
|
||
| ### Schema Field Reference | ||
|
|
||
| #### `<preset>` attributes | ||
|
|
||
| | Attribute | Required | Description | | ||
| |------------------|----------|------------------------------------------------------| | ||
| | `name` | No | Human-readable preset display name. | | ||
| | `schema_version` | No | Schema format version; defaults to `"1.0"`. | | ||
|
|
||
| #### `<meta>` section (optional) | ||
|
|
||
| | Element | Type | Description | | ||
| |---------------|--------|-----------------------------------------| | ||
| | `description` | string | Free-text description of the preset. | | ||
|
|
||
| #### `<project_info>` section | ||
|
|
||
| | Element | Type | Required* | Description | | ||
| |----------------------|--------|-----------|---------------------------------------------------| | ||
| | `template_name` | string | | Display name of the JUCE project template. | | ||
| | `template_url` | string | | Git URL of the project template repository. | | ||
| | `project_name` | string | yes | CamelCase project identifier (no spaces). | | ||
| | `product_name` | string | yes | Human-readable plugin display name. | | ||
| | `version` | string | | Semantic version string, e.g. `1.0.0`. | | ||
| | `company_name` | string | yes | Developer or studio name. | | ||
| | `bundle_id` | string | yes | Reverse-domain ID, e.g. `com.company.plugin`. | | ||
| | `manufacturer_code` | string | yes | 4-character JUCE manufacturer code. | | ||
| | `plugin_code` | string | | 4-character JUCE plugin code (auto-generated if empty). | | ||
| | `output_directory` | string | yes | Filesystem path for the generated project. | | ||
|
|
||
| #### `<configuration>` section | ||
|
|
||
| | Element | Type | Default | Description | | ||
| |----------------------------|---------|-------------------|----------------------------------------------------| | ||
| | `standalone` | boolean | `false` | Build a standalone application target. | | ||
| | `vst3` | boolean | `true` | Build a VST3 target. | | ||
| | `au` | boolean | `true` | Build an Audio Unit (AU) target. | | ||
| | `auv3` | boolean | `false` | Build an AUv3 target. | | ||
| | `clap` | boolean | `true` | Build a CLAP target. | | ||
| | `au_component_type` | string | `aufx` | Four-char AU component type code. | | ||
| | `au_component_subtype` | string | `plug` | Four-char AU component subtype code. | | ||
| | `au_component_manufacturer`| string | `Ddsp` | Four-char AU manufacturer code. | | ||
| | `au_version` | string | `1.0.0` | AU bundle version string. | | ||
| | `clap_extensions` | string | `note-ports,state`| Comma-separated CLAP extension identifiers. | | ||
| | `clap_features` | string | `audio-effect` | Comma-separated CLAP feature identifiers. | | ||
| | `auv3_platform` | string | `iOS` | Target platform for AUv3 (`iOS` or `macOS`). | | ||
| | `gui_width` | integer | `800` | Default plugin window width in pixels (> 0). | | ||
| | `gui_height` | integer | `600` | Default plugin window height in pixels (> 0). | | ||
| | `resizable` | boolean | `false` | Allow the plugin window to be resized. | | ||
| | `background_image` | string | *(empty)* | Path to an optional background image asset. | | ||
| | `code_signing` | boolean | `false` | Enable macOS/iOS code signing. | | ||
| | `installer` | boolean | `false` | Generate an installer package. | | ||
| | `default_bypass` | boolean | `false` | Start with the plugin bypassed. | | ||
| | `input_gain` | boolean | `false` | Include an input gain stage. | | ||
| | `output_gain` | boolean | `false` | Include an output gain stage. | | ||
|
|
||
| #### `<implementations>` section | ||
|
|
||
| | Element | Type | Default | Description | | ||
| |------------------------|---------|---------|---------------------------------------------------------| | ||
| | `moonbase_licensing` | boolean | `false` | Integrate Moonbase licensing module. | | ||
| | `melatonin_inspector` | boolean | `false` | Include Melatonin component inspector. | | ||
| | `custom_gui_framework` | boolean | `false` | Use a custom GUI framework instead of JUCE defaults. | | ||
| | `logging_framework` | boolean | `false` | Add structured logging support. | | ||
| | `clap_builds` | boolean | `false` | Configure CLAP-specific build scripts. | | ||
| | `preset_management` | boolean | `false` | Enable built-in preset management. | | ||
| | `preset_format` | string | *(empty)*| Preset serialisation format: `XML`, `Binary`, or empty. | | ||
| | `ab_comparison` | boolean | `false` | Include A/B comparison functionality. | | ||
| | `state_management` | boolean | `false` | Enable explicit plugin state management. | | ||
| | `gpu_audio` | boolean | `false` | Enable GPU Audio processing integration. | | ||
|
|
||
| #### `<user_experience>` section | ||
|
|
||
| | Element | Type | Default | Description | | ||
| |--------------------|---------|---------|--------------------------------------------| | ||
| | `wizard` | boolean | `false` | Show a setup wizard on first launch. | | ||
| | `preview` | boolean | `false` | Enable in-app audio preview functionality. | | ||
| | `preset_management`| boolean | `false` | Show preset management UI controls. | | ||
|
|
||
| #### `<development_workflow>` section | ||
|
|
||
| | Element | Type | Default | Description | | ||
| |--------------------|---------|---------|-------------------------------------------------| | ||
| | `vcs` | boolean | `false` | Initialise a Git repository. | | ||
| | `testing` | boolean | `false` | Add a unit-test scaffold. | | ||
| | `code_quality` | boolean | `false` | Configure linting and formatting tools. | | ||
| | `validation_tools` | boolean | `false` | Include static analysis and sanitiser builds. | | ||
| | `scaffolding` | boolean | `false` | Generate project scaffolding scripts. | | ||
|
|
||
| *Required fields are enforced at the Python semantic-validation level (not by XSD structure | ||
| alone), because the XSD marks all elements as optional to allow partial presets to be loaded | ||
| with defaults filled in by `ConfigManager`. | ||
|
|
||
| ### Validation Rules | ||
|
|
||
| - All five sections must be present. | ||
| - All five sections (`project_info`, `configuration`, `implementations`, `user_experience`, | ||
| `development_workflow`) must be present. | ||
| - Required fields (project names, company identifiers, and `output_directory`) must not be | ||
| empty. | ||
| empty strings. | ||
| - Types: | ||
| - Booleans: `true`/`false` (case-insensitive; `1/0/yes/on` also accepted) | ||
| - Integers: whole numbers (GUI sizes) | ||
| - Strings: everything else | ||
| - **Booleans**: `true`/`false` (case-insensitive; `1`/`0`/`yes`/`no`/`on`/`off` also | ||
| accepted when loading). | ||
| - **Integers**: whole positive numbers (GUI dimensions). | ||
| - **Strings**: everything else. | ||
| - Two-layer validation is applied on every load and save: | ||
| 1. **XSD structural validation** via `PresetXSDValidator` (element names, nesting, types). | ||
| 2. **Semantic validation** via `ConfigManager.validate_config` (required fields, value | ||
| constraints). | ||
|
Comment on lines
+216
to
+219
|
||
|
|
||
| ### Bundled Example Presets | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -190,9 +190,15 @@ class ConfigManager: | |||||
| }, | ||||||
| } | ||||||
|
|
||||||
| SCHEMA_VERSION: ClassVar[str] = "1.0" | ||||||
|
|
||||||
| META_FIELDS: ClassVar[dict[str, dict[str, Any]]] = { | ||||||
| "name": {"type": str, "default": ""}, | ||||||
| "description": {"type": str, "default": ""}, | ||||||
| # schema_version is stored as an attribute on the root <preset> element, | ||||||
| # not as a child of <meta>. It is included here so that _apply_defaults | ||||||
| # preserves it in the meta dict when round-tripping config through save/load. | ||||||
| "schema_version": {"type": str, "default": "1.0"}, | ||||||
|
||||||
| "schema_version": {"type": str, "default": "1.0"}, | |
| "schema_version": {"type": str, "default": SCHEMA_VERSION}, |
Copilot
AI
Apr 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_load_structured_preset() now reads schema_version but never checks whether the value is supported/recognized. Since versioning is being introduced specifically to detect breaking format changes, consider rejecting (or explicitly warning/migrating) presets with an unknown schema_version rather than silently proceeding (which could load an incompatible preset incorrectly).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| """XSD-based validation utilities for preset XML files. | ||
|
|
||
| This module provides :class:`PresetXSDValidator`, which validates preset XML | ||
| files and strings against the formal ``preset_schema.xsd`` W3C schema bundled | ||
| with the application. It requires the ``lxml`` library. | ||
|
|
||
| Typical usage:: | ||
|
|
||
| from core.preset_validator import PresetXSDValidator | ||
|
|
||
| validator = PresetXSDValidator() | ||
|
|
||
| ok, errors = validator.validate_file("path/to/preset.xml") | ||
| if not ok: | ||
| print("\\n".join(errors)) | ||
|
|
||
| ok, errors = validator.validate_string(xml_content) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| from typing import ClassVar | ||
|
|
||
| from lxml import etree | ||
|
|
||
|
|
||
| class PresetXSDValidator: | ||
| """Validates preset XML files against the bundled XSD schema. | ||
|
|
||
| The compiled :class:`lxml.etree.XMLSchema` object is cached on the class so | ||
| that the schema file is parsed only once per interpreter session. | ||
|
|
||
| Attributes: | ||
| SCHEMA_VERSION: The preset schema version this validator understands. | ||
| XSD_PATH: Absolute path to the bundled ``preset_schema.xsd`` file. | ||
| """ | ||
|
|
||
| SCHEMA_VERSION: ClassVar[str] = "1.0" | ||
| XSD_PATH: ClassVar[Path] = ( | ||
| Path(__file__).resolve().parents[1] / "resources" / "presets" / "preset_schema.xsd" | ||
| ) | ||
|
Comment on lines
+39
to
+42
|
||
|
|
||
| _schema: ClassVar[etree.XMLSchema | None] = None | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Construction / schema loading | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| @classmethod | ||
| def _get_schema(cls) -> etree.XMLSchema: | ||
| """Return the compiled XMLSchema, loading it on first access.""" | ||
| if cls._schema is None: | ||
| schema_doc = etree.parse(str(cls.XSD_PATH)) | ||
| cls._schema = etree.XMLSchema(schema_doc) | ||
| return cls._schema | ||
|
Comment on lines
+50
to
+56
|
||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Public API | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def validate_file(self, file_path: Path | str) -> tuple[bool, list[str]]: | ||
| """Validate a preset XML file against the XSD schema. | ||
|
|
||
| Args: | ||
| file_path: Path to the XML preset file to validate. | ||
|
|
||
| Returns: | ||
| A ``(is_valid, errors)`` tuple. *errors* is an empty list when | ||
| *is_valid* is ``True``. | ||
| """ | ||
| try: | ||
| doc = etree.parse(str(file_path)) | ||
| except etree.XMLSyntaxError as exc: | ||
| return False, [f"XML syntax error: {exc}"] | ||
| except OSError as exc: | ||
| return False, [f"Cannot read file: {exc}"] | ||
| return self._validate_doc(doc) | ||
|
|
||
| def validate_string(self, xml_content: str) -> tuple[bool, list[str]]: | ||
| """Validate an XML string against the XSD schema. | ||
|
|
||
| Args: | ||
| xml_content: XML text to validate. | ||
|
|
||
| Returns: | ||
| A ``(is_valid, errors)`` tuple. *errors* is an empty list when | ||
| *is_valid* is ``True``. | ||
| """ | ||
| try: | ||
| doc = etree.fromstring(xml_content.encode()) | ||
| except etree.XMLSyntaxError as exc: | ||
| return False, [f"XML syntax error: {exc}"] | ||
| return self._validate_doc(etree.ElementTree(doc)) | ||
|
|
||
| # ------------------------------------------------------------------ | ||
| # Internal helpers | ||
| # ------------------------------------------------------------------ | ||
|
|
||
| def _validate_doc(self, doc: etree._ElementTree) -> tuple[bool, list[str]]: # type: ignore[name-defined] | ||
| """Run the schema against a parsed lxml document tree.""" | ||
| schema = self._get_schema() | ||
| is_valid = schema.validate(doc) | ||
| errors = [str(err) for err in schema.error_log] | ||
| return is_valid, errors | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The boolean rules here say “case-insensitive”, but the XSD’s
BooleanFieldultimately relies onxs:boolean+ lowercase enumerations (yes/no/on/off), which are case-sensitive. If XSD validation is intended to run (as described above), either update the docs to clarify that XSD validation requires lowercase boolean tokens, or expand the schema to accept the uppercase/mixed-case variants you want to support.