diff --git a/.agents/skills/writer/SKILL.md b/.agents/skills/writer/SKILL.md index 5c720265b3..30cb51e38b 100644 --- a/.agents/skills/writer/SKILL.md +++ b/.agents/skills/writer/SKILL.md @@ -3,7 +3,7 @@ name: writer description: > Write, edit, and restructure user-facing and developer-facing documentation. Use when asked to create/update docs such as `README.md`, `docs/**`, and - other Markdown documentation; + other Markdown documentation, including keeping docs navigation data in sync; when drafting tutorials, guides, troubleshooting pages, or migration notes; and when improving inline API documentation (KDoc) and examples. --- @@ -24,10 +24,36 @@ description: > - `docs/`: longer-form docs (follow existing conventions in that tree). - Source KDoc: API usage, examples, and semantics that belong with the code. +## Keep docs navigation in sync + +- When adding, removing, moving, or renaming a page under + `docs/content/docs/
/`, keep the current version's matching + `sidenav.yml` in sync. +- Use `docs/data/versions.yml` to identify the current documentation version for + that section. The current version is the entry with `is_main: true`; its + `version_id` maps to `docs/data/docs/
//sidenav.yml`. +- Do not update historical version entries or their navigation files unless the + user explicitly asks to edit that historical version. +- Map page files to `file_path` values relative to the current version's + `content_path`, without `.md`; `_index.md` maps to its directory path, such as + `01-getting-started/_index.md` -> `01-getting-started`. +- Keep each `page` label aligned with the page frontmatter `title` unless the + existing navigation intentionally uses a shorter reader-facing label. +- Preserve the existing ordering, nesting, keys, comments, and YAML quoting + style. Remove nav entries for deleted pages and update `file_path` values for + moved pages. +- If a docs content change should not appear in navigation, say so explicitly in + the final response. + ## Follow local documentation conventions - Follow `.agents/documentation-guidelines.md` and `.agents/documentation-tasks.md`. - Use fenced code blocks for commands and examples; format file/dir names as code. +- In Markdown files, prefer footnote-style reference links for external `https://` + targets instead of inline links. Write readable body text like + `[label][short-id]`, then place the URL definition near the end of the file, + such as `[short-id]: https://example.com/long/path`. Keep reference IDs short + and descriptive. Inline links are still fine for local relative paths. - Avoid widows, runts, orphans, and rivers by reflowing paragraphs when needed. ## Make docs actionable @@ -48,4 +74,3 @@ description: > - For code changes, follow `.agents/running-builds.md`. - For documentation-only changes in Kotlin/Java sources, prefer `./gradlew dokka`. - diff --git a/.github/workflows/build-on-ubuntu.yml b/.github/workflows/build-on-ubuntu.yml index f8c24933f9..b571ce82f4 100644 --- a/.github/workflows/build-on-ubuntu.yml +++ b/.github/workflows/build-on-ubuntu.yml @@ -1,10 +1,9 @@ -name: Build under Ubuntu +name: Build on Ubuntu on: push jobs: build: - name: Build under Ubuntu runs-on: ubuntu-latest steps: diff --git a/.github/workflows/build-on-windows.yml b/.github/workflows/build-on-windows.yml index 4e6b57f1fd..37eba1be7c 100644 --- a/.github/workflows/build-on-windows.yml +++ b/.github/workflows/build-on-windows.yml @@ -1,10 +1,9 @@ -name: Build under Windows +name: Build on Windows on: pull_request jobs: build: - name: Build under Windows runs-on: windows-latest steps: diff --git a/.github/workflows/ensure-reports-updated.yml b/.github/workflows/ensure-reports-updated.yml index fdd8b8e673..93531059e9 100644 --- a/.github/workflows/ensure-reports-updated.yml +++ b/.github/workflows/ensure-reports-updated.yml @@ -8,8 +8,7 @@ on: - '**' jobs: - build: - name: Ensure license reports updated + check: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 858cebbcc4..a7cde85e58 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -9,7 +9,6 @@ on: jobs: validation: - name: Gradle Wrapper Validation runs-on: ubuntu-latest steps: - name: Checkout latest code diff --git a/.github/workflows/increment-guard.yml b/.github/workflows/increment-guard.yml index 1993841a65..dd7df71378 100644 --- a/.github/workflows/increment-guard.yml +++ b/.github/workflows/increment-guard.yml @@ -9,8 +9,7 @@ on: - '**' jobs: - build: - name: Check version increment + check: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml index fe8ad849d0..f706171007 100644 --- a/.github/workflows/remove-obsolete-artifacts-from-packages.yaml +++ b/.github/workflows/remove-obsolete-artifacts-from-packages.yaml @@ -34,7 +34,7 @@ env: jobs: retrieve-package-names: - name: Retrieve the package names published from this repository + name: Retrieve package names runs-on: ubuntu-latest outputs: package-names: ${{ steps.request-package-names.outputs.package-names }} @@ -54,7 +54,7 @@ jobs: echo "package-names=$(<./package-names.json)" >> $GITHUB_OUTPUT delete-obsolete-artifacts: - name: Remove obsolete artifacts published from this repository to GitHub Packages + name: Delete obsolete artifacts needs: retrieve-package-names runs-on: ubuntu-latest strategy: diff --git a/.gitignore b/.gitignore index a3e0ec13bb..5f85d295e8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ # Internal tool directories. .fleet/ +.junie/memory/ # Kotlin temp directories. **/.kotlin/ diff --git a/.gitmodules b/.gitmodules index 97a9d16eb5..c9606ebb47 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "docs/_examples"] path = docs/_examples url = https://github.com/spine-examples/hello-validation +[submodule "docs/_time"] + path = docs/_time + url = https://github.com/SpineEventEngine/time diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 459e28ecae..5160f499e6 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -10,7 +10,7 @@ Also follow the Junie-specific rules described below. ## Junie Assistance Tips -When working with Junie AI on the Spine Tool-Base project: +When working with Junie AI on the Spine family of projects: 1. **Project Navigation**: Use `search_project` to find relevant files and code segments. 2. **Code Understanding**: Request file structure with `get_file_structure` before editing. diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6496c646c3..4184e9bf2d 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -137,8 +137,6 @@ val koverVersion = "0.9.1" /** * The version of the Shadow Plugin. * - * `7.1.2` is the last version compatible with Gradle 7.x. Newer versions require Gradle v8.x. - * * @see Shadow Plugin releases */ val shadowVersion = "9.4.1" diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/build/Dokka.kt b/buildSrc/src/main/kotlin/io/spine/dependency/build/Dokka.kt index 858731b3fc..6b4822dcd0 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/build/Dokka.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/build/Dokka.kt @@ -26,7 +26,6 @@ package io.spine.dependency.build -import io.spine.dependency.build.Dokka.GradlePlugin.id import io.spine.dependency.local.Spine // https://github.com/Kotlin/dokka diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt index dfdaf95357..2333e4ce7e 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/CoreJvmCompiler.kt @@ -46,7 +46,7 @@ object CoreJvmCompiler { /** * The version used to in the build classpath. */ - const val dogfoodingVersion = "2.0.0-SNAPSHOT.062" + const val dogfoodingVersion = "2.0.0-SNAPSHOT.063" /** * The version to be used for integration tests. diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt index e78af421da..7be4e8473f 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Time.kt @@ -40,7 +40,7 @@ import io.spine.dependency.Dependency ) object Time : Dependency() { override val group = Spine.group - override val version = "2.0.0-SNAPSHOT.235" + override val version = "2.0.0-SNAPSHOT.237" private const val infix = "spine-time" fun lib(version: String): String = "$group:$infix:$version" @@ -56,6 +56,12 @@ object Time : Dependency() { fun testLib(version: String): String = "${Spine.toolsGroup}:time-testlib:$version" val testLib get() = testLib(version) + fun validation(version: String): String = "${Spine.toolsGroup}:time-validation:$version" + val validation get() = validation(version) + + fun gradlePlugin(version: String): String = "${Spine.toolsGroup}:time-gradle-plugin:$version" + val gradlePlugin get() = gradlePlugin(version) + override val modules: List get() = listOf( lib, diff --git a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt index a5ef403185..121d7aac24 100644 --- a/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt +++ b/buildSrc/src/main/kotlin/io/spine/dependency/local/Validation.kt @@ -36,7 +36,7 @@ object Validation { /** * The version of the Validation library artifacts. */ - const val version = "2.0.0-SNAPSHOT.413" + const val version = "2.0.0-SNAPSHOT.414" /** * The last version of Validation compatible with ProtoData. diff --git a/config b/config index 8d540c50a6..f24236f0a8 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 8d540c50a6ecb118c8fd1080b45054c0a3909ea9 +Subproject commit f24236f0a897e9d6a101e60d1dd936317c7a3c06 diff --git a/dependencies.md b/dependencies.md index 497e7fddf8..5821b16736 100644 --- a/dependencies.md +++ b/dependencies.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine.tools:validation-context:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-context:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -1090,14 +1090,14 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:19 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:37 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-context-tests:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-context-tests:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -1791,28 +1791,28 @@ This report was generated on **Wed Apr 29 15:56:19 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:37 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-docs:2.0.0-SNAPSHOT.412` +# Dependencies of `io.spine.tools:validation-docs:2.0.0-SNAPSHOT.415` ## Runtime ## Compile, tests, and tooling The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri Apr 24 19:11:43 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:34 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-gradle-plugin:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-gradle-plugin:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -2864,14 +2864,14 @@ This report was generated on **Fri Apr 24 19:11:43 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:19 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:37 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-java:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-java:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -3961,14 +3961,14 @@ This report was generated on **Wed Apr 29 15:56:19 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:19 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:37 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-java-bundle:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-java-bundle:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : org.jetbrains. **Name** : annotations. **Version** : 13.0. @@ -4015,14 +4015,14 @@ This report was generated on **Wed Apr 29 15:56:19 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:16 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:35 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-validation-jvm-runtime:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine:spine-validation-jvm-runtime:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -4822,14 +4822,14 @@ This report was generated on **Wed Apr 29 15:56:16 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:19 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:37 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-consumer:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-consumer:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -5511,14 +5511,14 @@ This report was generated on **Wed Apr 29 15:56:19 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:36 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-consumer-dependency:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-consumer-dependency:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -5976,14 +5976,14 @@ This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:36 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-extensions:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-extensions:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -6602,14 +6602,14 @@ This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:36 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-runtime:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-runtime:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7170,14 +7170,14 @@ This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:36 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-validating:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-validating:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -7781,14 +7781,14 @@ This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:37 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-validator:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-validator:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.fasterxml.jackson. **Name** : jackson-bom. **Version** : 2.20.0. @@ -8526,14 +8526,14 @@ This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:36 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-validator-dependency:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-validator-dependency:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -8766,14 +8766,14 @@ This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:36 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:validation-vanilla:2.0.0-SNAPSHOT.414` +# Dependencies of `io.spine.tools:validation-vanilla:2.0.0-SNAPSHOT.415` ## Runtime 1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2. @@ -9116,6 +9116,6 @@ This report was generated on **Wed Apr 29 15:56:18 WEST 2026** using The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Wed Apr 29 15:56:17 WEST 2026** using +This report was generated on **Thu Apr 30 22:06:36 WEST 2026** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/docs/_examples b/docs/_examples index b75f86539e..bc2c4a547f 160000 --- a/docs/_examples +++ b/docs/_examples @@ -1 +1 @@ -Subproject commit b75f86539e9f6a8d4512ccc14e4be28bc5b97492 +Subproject commit bc2c4a547fa600e01a1f67c0529c18c9d81939f4 diff --git a/docs/_preview/package-lock.json b/docs/_preview/package-lock.json index 248e35a432..9a5ffb8720 100644 --- a/docs/_preview/package-lock.json +++ b/docs/_preview/package-lock.json @@ -11,7 +11,7 @@ "devDependencies": { "@fullhuman/postcss-purgecss": "^7.0.2", "autoprefixer": "^10.4.22", - "postcss": "^8.5.6", + "postcss": "^8.5.10", "postcss-cli": "^11.0.1", "postcss-discard-comments": "^7.0.5" } @@ -785,9 +785,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "dev": true, "funding": [ { @@ -803,6 +803,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", diff --git a/docs/_preview/package.json b/docs/_preview/package.json index 143fef495c..92ad933edf 100644 --- a/docs/_preview/package.json +++ b/docs/_preview/package.json @@ -11,7 +11,7 @@ "devDependencies": { "@fullhuman/postcss-purgecss": "^7.0.2", "autoprefixer": "^10.4.22", - "postcss": "^8.5.6", + "postcss": "^8.5.10", "postcss-cli": "^11.0.1", "postcss-discard-comments": "^7.0.5" } diff --git a/docs/_time b/docs/_time new file mode 160000 index 0000000000..ff52ea3322 --- /dev/null +++ b/docs/_time @@ -0,0 +1 @@ +Subproject commit ff52ea332267a8f912c3d19c94b0bd5a37d00667 diff --git a/docs/content/docs/validation/01-getting-started/adding-to-build.md b/docs/content/docs/validation/01-getting-started/adding-to-build.md index 2d65b5d8f0..1a461a8636 100644 --- a/docs/content/docs/validation/01-getting-started/adding-to-build.md +++ b/docs/content/docs/validation/01-getting-started/adding-to-build.md @@ -90,7 +90,7 @@ Add the Validation plugin to the build. ```kotlin plugins { module - id("io.spine.validation") version "2.0.0-SNAPSHOT.413" + id("io.spine.validation") version "2.0.0-SNAPSHOT.415" } ``` @@ -120,7 +120,7 @@ adding Validation directly. CoreJvm brings in the Validation Gradle plugin for y ```kotlin plugins { module - id("io.spine.core-jvm") version "2.0.0-SNAPSHOT.062" + id("io.spine.core-jvm") version "2.0.0-SNAPSHOT.063" } ``` diff --git a/docs/content/docs/validation/01-getting-started/generated-code.md b/docs/content/docs/validation/01-getting-started/generated-code.md index ce767b0786..40ba676c1f 100644 --- a/docs/content/docs/validation/01-getting-started/generated-code.md +++ b/docs/content/docs/validation/01-getting-started/generated-code.md @@ -110,4 +110,4 @@ such as API requests or deserialized data, for example, when building an anticor - Learn the core concepts: [Concepts](../02-concepts/). - If you need organization-specific rules: - [Custom validation](../08-custom-validation/). + [Custom validation](../05-custom-validation/). diff --git a/docs/content/docs/validation/02-concepts/_index.md b/docs/content/docs/validation/02-concepts/_index.md index 09d8628ea5..30b48e0da1 100644 --- a/docs/content/docs/validation/02-concepts/_index.md +++ b/docs/content/docs/validation/02-concepts/_index.md @@ -55,7 +55,7 @@ The generated API exposes two main ways to validate data: Generated messages implement `ValidatableMessage` and provide `validate()`, which returns `Optional`. -See [Using the generated code](../01-getting-started/generated-code.md) for end-to-end examples. +See “[Using the generated code](../01-getting-started/generated-code.md)” for end-to-end examples. ## What a violation contains @@ -68,7 +68,7 @@ Each `ConstraintViolation` points to the invalid value and explains what went wr - `field_value` — the invalid value packed as `google.protobuf.Any`. When you need a human-readable message, format the `TemplateString` from the violation. -See [Working with error messages](error-messages.md) for message formatting, +See “[Working with error messages](error-messages.md)” for message formatting, placeholders, and customization. @@ -91,7 +91,7 @@ which message was validated at the top level. If built-in options are not enough, you can add organization-specific options and generate code for them. -See [Custom validation](../08-custom-validation/) for the workflow and a reference example. +See “[Custom validation](../05-custom-validation/)” for the workflow and a reference example. ## What’s next @@ -107,4 +107,4 @@ See [Custom validation](../08-custom-validation/) for the workflow and a referen - [Built-in options](../03-built-in-options/) - [Using validators](../04-validators/) - Add custom validation options: - [Custom validation](../08-custom-validation/). + [Custom validation](../05-custom-validation/). diff --git a/docs/content/docs/validation/02-concepts/error-messages.md b/docs/content/docs/validation/02-concepts/error-messages.md index 4a78f893b1..b573bb98d3 100644 --- a/docs/content/docs/validation/02-concepts/error-messages.md +++ b/docs/content/docs/validation/02-concepts/error-messages.md @@ -139,4 +139,4 @@ Recommended actions: - Learn how to validate messages with custom code: [Using validators](../04-validators/). - If built-in options are not enough, define your own constraints and messages: - [Custom validation](../08-custom-validation/). + [Custom validation](../05-custom-validation/). diff --git a/docs/content/docs/validation/02-concepts/options-overview.md b/docs/content/docs/validation/02-concepts/options-overview.md index a304cca991..e9e3eb661c 100644 --- a/docs/content/docs/validation/02-concepts/options-overview.md +++ b/docs/content/docs/validation/02-concepts/options-overview.md @@ -60,7 +60,7 @@ Generated messages and builders provide a small validation-focused API surface: - `buildPartial()` builds without validation. - `validate()` checks an existing instance and returns `Optional`. -See [Using the generated code](../01-getting-started/generated-code.md) for Java and Kotlin examples. +See “[Using the generated code](../01-getting-started/generated-code.md)” for Java and Kotlin examples. ## What does not happen @@ -111,4 +111,4 @@ message Temperature { ## What’s next - [Built-in options](../03-built-in-options/) -- [Custom validation](../08-custom-validation/) +- [Custom validation](../05-custom-validation/) diff --git a/docs/content/docs/validation/03-built-in-options/_index.md b/docs/content/docs/validation/03-built-in-options/_index.md index 04356e24de..45d4096550 100644 --- a/docs/content/docs/validation/03-built-in-options/_index.md +++ b/docs/content/docs/validation/03-built-in-options/_index.md @@ -47,4 +47,4 @@ on GitHub: [spine/options.proto](https://github.com/SpineEventEngine/base-librar - [Message-level options](message-level-options.md) - [Options for `repeated` and `map` fields](repeated-and-map-fields.md) - [Using validators](../04-validators/) -- [Custom validation](../08-custom-validation/) +- [Custom validation](../05-custom-validation/) diff --git a/docs/content/docs/validation/04-validators/_index.md b/docs/content/docs/validation/04-validators/_index.md index e7bb17d412..e6969bc49a 100644 --- a/docs/content/docs/validation/04-validators/_index.md +++ b/docs/content/docs/validation/04-validators/_index.md @@ -26,7 +26,7 @@ Validators are implemented via `io.spine.validation.MessageValidator` and exe Prefer `.proto` options when you can: 1. Use [built-in options](../03-built-in-options/). -2. If built-ins are not enough, implement [custom validation options](../08-custom-validation/). +2. If built-ins are not enough, implement [custom validation options](../05-custom-validation/). Use `MessageValidator` when: @@ -107,7 +107,7 @@ their violations together. ## What’s next - [Implement a validator](implement-a-validator.md) - [Using `ValidatorRegistry`](validator-registry.md) -- [Custom validation](../08-custom-validation/) +- [Custom validation](../05-custom-validation/) - [Architecture](../09-developers-guide/architecture.md) [auto-service]: https://github.com/google/auto/tree/main/service diff --git a/docs/content/docs/validation/04-validators/implement-a-validator.md b/docs/content/docs/validation/04-validators/implement-a-validator.md index 0ae1f65e3b..a2f33f235c 100644 --- a/docs/content/docs/validation/04-validators/implement-a-validator.md +++ b/docs/content/docs/validation/04-validators/implement-a-validator.md @@ -136,7 +136,7 @@ to the nested field in error (for example, to `starts_at.seconds`). ## What’s next - [Using `ValidatorRegistry`](validator-registry.md) -- [Custom validation](../08-custom-validation/) +- [Custom validation](../05-custom-validation/) - [Architecture](../09-developers-guide/architecture.md) diff --git a/docs/content/docs/validation/05-custom-validation/_index.md b/docs/content/docs/validation/05-custom-validation/_index.md new file mode 100644 index 0000000000..5056de4d6c --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/_index.md @@ -0,0 +1,63 @@ +--- +title: Overview +description: Extending the library with custom options and code generation. +headline: Documentation +--- + +# Overview + +Users can extend the Validation library by providing custom Protobuf options and code generation logic. + +Follow these steps to create a custom option: + +1. [Declare the option](declare-the-option.md) as a Protobuf extension in your `.proto` files. +2. [Register the option](register-the-option.md) via `io.spine.option.OptionsProvider` + and wire up the entities via `io.spine.tools.validation.java.ValidationOption`. +3. [Declare the event and view state](declare-event-and-view.md) — the Protobuf types that + track the option's discovery during compilation. +4. [Implement the `Reaction`](implement-the-reaction.md) — discovers and validates the option. +5. [Implement the `View`](implement-the-view.md) — accumulates valid option applications. +6. [Implement the `Generator`](implement-the-generator.md) — generates Java code for the option. + +Below is a workflow diagram for a typical option: + +![Typical custom option](typical_custom_option.jpg) + +Note that a custom option can provide several reactions and views, but only one generator. +This allows building more complex models using more entities and events. + +## Components + +### `Reaction` + +Subscribes to `*OptionDiscovered` events and filters them by option name. +See “[Implement the `Reaction`](implement-the-reaction.md)” for details. + +### `View` + +Accumulates events emitted by the `Reaction` so the `Generator` can query them. +See “[Implement the `View`](implement-the-view.md)” for details. + +### `Generator` + +Produces Java code for every application of the option within a message type. +See “[Implement the `Generator`](implement-the-generator.md)” for details. + +## Running example + +Throughout this section, the `(when)` option from the +[Spine Time](https://github.com/SpineEventEngine/time) library serves as the running example. +Spine Time is a library of Protobuf-based date and time types for business models. It defines +its own Protobuf message types — such as `LocalDate`, `LocalTime`, and `ZonedDateTime` — and +provides converters to and from the standard Java Time API. Among other features, it ships a +`(when)` validation option that constrains a time-typed field to hold either a past or a +future value. + +## What’s next + +- [Declare the option in Protobuf](declare-the-option.md) +- [Register the option](register-the-option.md) +- [Declare the event and view state](declare-event-and-view.md) +- [Implement the `Reaction`](implement-the-reaction.md) +- [Implement the `View`](implement-the-view.md) +- [Implement the `Generator`](implement-the-generator.md). diff --git a/docs/content/docs/validation/05-custom-validation/declare-event-and-view.md b/docs/content/docs/validation/05-custom-validation/declare-event-and-view.md new file mode 100644 index 0000000000..4e18aac2d7 --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/declare-event-and-view.md @@ -0,0 +1,64 @@ +--- +title: Declare the event and view state +description: How to define the domain event and view state Protobuf types for a custom option. +headline: Documentation +--- + +# Declare the event and view state + +After registering the option, declare the Protobuf types that track its discovery during +compilation: a domain event emitted by the `Reaction` when a valid option application is found, +and a view state that persists the event data for the `Generator` to query. + +## Declare the event + +The `Reaction` emits a domain event carrying all data that the `View` and `Generator` need. The event +travels through the compilation bounded context, so it must be a proper Protobuf message. + +```protobuf +message WhenFieldDiscovered { + + compiler.FieldRef id = 1; + + compiler.Field subject = 2; + + string error_message = 3; + + Time bound = 4; + + spine.tools.time.validation.TimeFieldType type = 5; +} +``` + +The `id` field must be the **first** field and must be the same type that the `View` uses as its +entity identity (`compiler.FieldRef` in this case). The framework uses the identity field to +route the event to the correct `View` instance. + +## Declare the view state + +The view state is the persistent accumulator queried by the `Generator`. It mirrors the event +fields and is marked as a Spine projection: + +```protobuf +message WhenField { + option (entity).kind = PROJECTION; + + compiler.FieldRef id = 1; + + compiler.Field subject = 2; + + string error_message = 3; + + Time bound = 4; + + spine.tools.time.validation.TimeFieldType type = 5; +} +``` + +The `id` field type must match the event `id` type exactly. Without this match, the framework +cannot route `WhenFieldDiscovered` events to the correct `WhenField` view instance. + +## What's next + +- [Implement the `Reaction`](implement-the-reaction.md) +- [Back to Custom Validation](../) diff --git a/docs/content/docs/validation/05-custom-validation/declare-the-option.md b/docs/content/docs/validation/05-custom-validation/declare-the-option.md new file mode 100644 index 0000000000..c88879e593 --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/declare-the-option.md @@ -0,0 +1,58 @@ +--- +title: Declare the option in Protobuf +description: How to define the option message in Protobuf. +headline: Documentation +--- + +# Declare the option in Protobuf + +An option is declared as a Protobuf `extend` block targeting one of the standard +descriptor option types. + +## Declare the option message + +An option is declared as a Protobuf `extend` block targeting one of the standard +descriptor option types — `google.protobuf.FieldOptions`, `MessageOptions`, and so on. + +```protobuf +extend google.protobuf.FieldOptions { + TimeOption when = 73819; +} + +message TimeOption { + + option (default_message) = "The field `${parent.type}.${field.path}`" + " of the type `${field.type}` must be in the `${when.in}`." + " The encountered value: `${field.value}`."; + + Time in = 1; + + // field 2 is reserved (deprecated msg_format). + + // A user-defined error message. + string error_msg = 3; +} + +enum Time { + TIME_UNDEFINED = 0; + PAST = 1; + FUTURE = 2; +} +``` + +The `(default_message)` option on `TimeOption` sets the error template used when the caller +does not supply a custom `error_msg`. Field number `73819` is the globally registered extension +number for this option; every extension must have a unique number in the allowed range. + +{{% note-block class="note" %}} +### Packaging trade-off + +If you omit the `package` declaration from the `.proto` file that defines the extension, callers +can write `[(when).in = FUTURE]` instead of `[(spine.time.when).in = FUTURE]`. This is a +deliberate trade-off: shorter option syntax at the cost of no package-level namespacing. +{{% /note-block %}} + +## What's next + +- [Register the option](register-the-option.md) +- [Back to Custom Validation](../) diff --git a/docs/content/docs/validation/05-custom-validation/implement-the-generator.md b/docs/content/docs/validation/05-custom-validation/implement-the-generator.md new file mode 100644 index 0000000000..4659800e1e --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/implement-the-generator.md @@ -0,0 +1,79 @@ +--- +title: 'Implement the `Generator`' +description: How to implement an OptionGenerator that produces Java validation code. +headline: Documentation +--- + +# Implement the `Generator` + +The `Generator` produces Java code for every application of the option within a compiled message +type. The framework calls `codeFor` once per message type and inlines each returned +`SingleOptionCode` into the generated `validate()` method. + +## Class declaration + +```kotlin +internal class WhenGenerator : OptionGeneratorWithConverter() +``` + +Use `OptionGeneratorWithConverter` when your generated code needs to convert a Protobuf field +value to a Java expression — for example, to format a field value as a JSON string in an error +message. This base class injects a `JavaValueConverter` that handles the conversion. Use the +plain `OptionGenerator` base class when no value conversion is needed. + +## Querying the `View` + +```kotlin +private val allWhenFields by lazy { + querying.select().all() +} +``` + +`querying` is injected by the framework and provides read access to all accumulated `View` +instances. The `by lazy` delegate is required because `querying` is not available until the +framework initialises the generator; accessing it during construction causes an error. The +query result is cached after the first call. + +## `codeFor` override + +```kotlin +override fun codeFor(type: TypeName): List = + allWhenFields + .filter { it.id.type == type } + .map { GenerateWhen(it, converter).code() } +``` + +The framework calls `codeFor` once for each message type it processes. Filter the view list +by `id.type == type` to select only the fields that belong to the current message. Each +filtered view is passed to a helper that composes the actual `CodeBlock`. + +Each `SingleOptionCode` wraps a `CodeBlock` that is inlined directly into the generated +`validate()` method, so the code must be a valid Java statement or block. + +For complete context, see [`WhenGenerator.kt`][when-generator-kt] in the Spine Time repository. + +## Generated code paths + +The `GenerateWhen.code()` method chooses the Java code shape for a single application of +the `(when)` option: + +- For a single message field, it generates one validation block for the field value. +- For a repeated message field, it generates a `for` loop and validates each element inside + that loop. +- For a map field, it generates a `for` loop over the map's `.values()` and validates each + value inside that loop. + +All three branches delegate to the same `validateTime(...)` helper, so the time comparison, +violation construction, and placeholder handling stay in one place. The difference is only +where the checked value comes from: the field getter for a single message, the loop variable +for each repeated element, or the map value variable for each map entry. + +See the full source around [`GenerateWhen.code()`][when-generator-kt] for the exact generated +Java shape. + +## What's next + +- [Pass the option to the Compiler](pass-to-compiler.md) +- [Back to Custom Validation](../) + +[when-generator-kt]: https://github.com/SpineEventEngine/time/blob/5268c2386f2dd4f400a7fb6885474c2945475b3a/validation/src/main/kotlin/io/spine/tools/time/validation/java/WhenGenerator.kt diff --git a/docs/content/docs/validation/05-custom-validation/implement-the-reaction.md b/docs/content/docs/validation/05-custom-validation/implement-the-reaction.md new file mode 100644 index 0000000000..fd61606a8e --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/implement-the-reaction.md @@ -0,0 +1,122 @@ +--- +title: 'Implement the `Reaction`' +description: 'How to implement a `Reaction` that discovers and validates a custom option.' +headline: Documentation +--- + +# Implement the `Reaction` + +The `Reaction` is the entry point for option handling. It subscribes to +`FieldOptionDiscovered` events (or one of the other `*OptionDiscovered` variants), +filters them by option name, validates the option application, and emits a domain event +when the option is applied correctly. + +## Class declaration + +```kotlin +internal class WhenReaction : Reaction() +``` + +The type parameter is the event the `Reaction` listens to. For a field-level option, use +`FieldOptionDiscovered`. Other variants include `FileOptionDiscovered`, +`MessageOptionDiscovered`, and `OneofOptionDiscovered`. + +## `Reaction` method + +```kotlin +@React +override fun whenever( + @External @Where(field = OPTION_NAME, equals = WhenOption.NAME) + event: FieldOptionDiscovered +): EitherOf2 { + val field = event.subject + val file = event.file + + val timeType = checkFieldType(field, typeSystem, file) + + val option = event.option.value.unpack() + val timeBound = option.`in` + if (timeBound == Time.TIME_UNDEFINED) { + return ignore() + } + + val message = option.errorMsg.ifEmpty { option.descriptorForType.defaultMessage } + message.checkPlaceholders(SUPPORTED_PLACEHOLDERS, field, file, WhenOption.NAME) + + return whenFieldDiscovered { + id = field.ref + subject = field + errorMessage = message + bound = timeBound + type = timeType + }.asA() +} +``` + +### Annotations + +- `@React` marks the method as the reaction handler; only one such method is allowed per class. +- `@External` tells the framework that `FieldOptionDiscovered` originates from the compiler's + bounded context, not the current one. +- `@Where(field = OPTION_NAME, equals = WhenOption.NAME)` narrows the subscription so that + `whenever` receives only events where the option name equals `"when"`. `OPTION_NAME` is a + constant from `io.spine.tools.validation` that names the filter field. Without this filter, + the `Reaction` would be called for every field option discovered during compilation. + +### Return type + +`EitherOf2` expresses that the method either emits a domain +event or signals that no reaction should take place. + +## Three possible outcomes + +### 1. Unsupported field type + +The main `whenever` snippet calls `checkFieldType(field, typeSystem, file)`, which is a +private helper that wraps `Compilation.check`: + +```kotlin +private fun checkFieldType(field: Field, typeSystem: TypeSystem, file: File): TimeFieldType { + val timeType = typeSystem.determineTimeType(field.type) + Compilation.check(timeType != TFT_UNKNOWN, file, field.span) { + "The field type `${field.type.name}` of `${field.qualifiedName}` " + + "is not supported by the `(${WhenOption.NAME})` option." + } + return timeType +} +``` + +`Compilation.check` throws a compilation exception when the condition is `false`, causing the +build to fail with the supplied message pointing to the source file and span. Extracting the +check into a helper keeps the main reaction method readable and allows the helper to also return +the resolved `TimeFieldType` for later use. + +### 2. Disabled option + +Short-circuit with `return ignore()` (which returns `NoReaction`) when the option value equals +the sentinel `TIME_UNDEFINED`. This represents a correctly formed but effectively disabled +option — for example, `[(when).in = TIME_UNDEFINED]`. No domain event is emitted and no code is +generated for that field. + +### 3. Valid, enabled option + +Validate the error message template with `checkPlaceholders`, then build and emit the domain +event using the Kotlin DSL: + +```kotlin +return whenFieldDiscovered { + id = field.ref + subject = field + errorMessage = message + bound = timeBound + type = timeType +}.asA() +``` + +`checkPlaceholders` reports a compilation error if the template contains placeholder names that +the option does not support. `.asA()` wraps the event in the `EitherOf2` left slot. + +## What's next + +- [Implement the `View`](implement-the-view.md) +- [Back to Custom Validation](../) diff --git a/docs/content/docs/validation/05-custom-validation/implement-the-view.md b/docs/content/docs/validation/05-custom-validation/implement-the-view.md new file mode 100644 index 0000000000..92d5e2df98 --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/implement-the-view.md @@ -0,0 +1,49 @@ +--- +title: 'Implement the `View`' +description: 'How to implement a `View` that accumulates option data for code generation.' +headline: Documentation +--- + +# Implement the `View` + +The `View` is a Spine projection that persists the data emitted by the `Reaction`. The `Generator` +queries the `View` to obtain the information it needs to produce Java code. + +## Class declaration + +```kotlin +internal class WhenFieldView : View() +``` + +The three type parameters are: + +| Parameter | Type | Description | +| --- | --- | --- | +| ID | `FieldRef` | Entity identity; must match the `id` field type in the Protobuf view state. | +| State | `WhenField` | The generated Protobuf message that holds the accumulated data. | +| Builder | `WhenField.Builder` | The corresponding Protobuf builder, used by the `alter { }` DSL. | + +## Subscriber method + +```kotlin +@Subscribe +fun on(e: WhenFieldDiscovered) = alter { + subject = e.subject + errorMessage = e.errorMessage + bound = e.bound + type = e.type +} +``` + +The `@Subscribe` annotation registers `on` as the event handler. The `alter { }` DSL block +provides access to the state builder; any assignments inside the block are applied atomically +when the block exits. One `@Subscribe` method is required for each event type the `View` handles. + +`View`s only accumulate data. Validation and business logic belong in the `Reaction`; by the time +an event reaches the `View`, the `Reaction` has already confirmed that the option application is +correct. + +## What's next + +- [Implement the `Generator`](implement-the-generator.md) +- [Back to Custom Validation](../) diff --git a/docs/content/docs/validation/05-custom-validation/pass-to-compiler.md b/docs/content/docs/validation/05-custom-validation/pass-to-compiler.md new file mode 100644 index 0000000000..d915f5e63d --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/pass-to-compiler.md @@ -0,0 +1,77 @@ +--- +title: 'Pass the option to the Compiler' +description: How to make a ValidationOption implementation available to the Spine Compiler. +headline: Documentation +--- + +# Pass the option to the Compiler + +`JavaValidationPlugin` — the Spine Compiler plugin that generates Java validation code — +discovers `ValidationOption` implementations via the Java `ServiceLoader` mechanism. To be +found, a `ValidationOption` implementation must be present on the Compiler's user classpath. + +The `io.spine.validation` Gradle plugin places `JavaValidationPlugin` itself on that classpath +automatically. Adding your custom `ValidationOption` requires one additional step. + +There are two approaches: + +1. **Direct dependency** — add the implementation module to the `spineCompiler` dependency + configuration. Use this for a single project or a single module in a multi-module build. + +2. **Gradle plugin** — wrap the classpath configuration in a Gradle plugin. Use this when + the custom option is packaged as a library or shared across multiple modules. + +## Direct dependency + +The `io.spine.validation` plugin registers a `spineCompiler` dependency configuration that +populates the Compiler's user classpath. Add your implementation as a `spineCompiler` dependency: + +```kotlin +dependencies { + spineCompiler("com.example:my-validation-option:1.0.0") +} +``` + +When the Compiler runs, all `spineCompiler` artifacts are placed on the user classpath and +`ServiceLoader` can discover your `ValidationOption` implementation. + +## Gradle plugin + +When the custom option is distributed as a library or reused across projects, a dedicated +Gradle plugin is the preferred approach. The plugin registers the validation module on the +Compiler's user classpath whenever `io.spine.validation` is applied to a project. + +The Spine Time library uses this pattern. Its Gradle plugin reacts to the presence of +`io.spine.validation` and calls `addUserClasspathDependency`: + +```kotlin +import io.spine.tools.compiler.gradle.api.addUserClasspathDependency + +private fun Project.passValidationToCompiler() { + pluginManager.withPlugin(TimeValidation.validationPluginId) { + addUserClasspathDependency(TimeValidation.artifact) + } +} +``` + +`pluginManager.withPlugin(...)` fires only when the `io.spine.validation` plugin is applied to +the target project. Projects that do not use Spine Validation are unaffected. + +`addUserClasspathDependency` from `io.spine.tools.compiler.gradle.api` adds a `MavenArtifact` +to the Compiler's user classpath. The artifact version is resolved from metadata embedded in +the plugin at build time. + +For users of the `io.spine.time` Gradle plugin, no extra configuration is needed: applying the +plugin to a project that has `io.spine.validation` automatically places the `time-validation` +module on the Compiler's classpath. + +See the full source in +[`TimeGradlePlugin.kt`](https://github.com/SpineEventEngine/time/blob/master/gradle-plugin/src/main/kotlin/io/spine/tools/time/gradle/TimeGradlePlugin.kt) +and the artifact definition in +[`TimeValidation.kt`](https://github.com/SpineEventEngine/time/blob/master/gradle-plugin/src/main/kotlin/io/spine/tools/time/gradle/TimeValidation.kt) +in the Spine Time repository. + +## What's next + +- [Summary](summary.md) +- [Architecture](../09-developers-guide/architecture.md) diff --git a/docs/content/docs/validation/05-custom-validation/register-the-option.md b/docs/content/docs/validation/05-custom-validation/register-the-option.md new file mode 100644 index 0000000000..af90d917a4 --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/register-the-option.md @@ -0,0 +1,100 @@ +--- +title: Register the option +description: How to register the Protobuf extension and wire up the ValidationOption. +headline: Documentation +--- + +# Register the option + +Two registrations are required before the build plugin can use a custom option: + +1. Register the Protobuf extension so the descriptor machinery can resolve it at runtime. +2. Wire up the `Reaction`, `View`, and `Generator` via `ValidationOption`. + +## Register the proto extension + +Create a class that implements `OptionsProvider` and annotate it with +`@AutoService(OptionsProvider.class)`: + +```java +import com.google.auto.service.AutoService; +import com.google.protobuf.ExtensionRegistry; +import io.spine.option.OptionsProvider; + +@AutoService(OptionsProvider.class) +public class TimeOptionsProvider implements OptionsProvider { + + @Override + public void registerIn(ExtensionRegistry registry) { + TimeOptionsProto.registerAllExtensions(registry); + } +} +``` + +Call `registerAllExtensions` on the generated outer class of the `.proto` file that contains +the `extend` block. The `java_outer_classname` option in the proto file controls this class name +(for example, `option java_outer_classname = "TimeOptionsProto"`). Without this registration, +the Protobuf runtime cannot deserialize the extension field and the option will be silently +ignored. + +## Wire up ValidationOption + +Create a class that implements `ValidationOption` and annotate it with +`@AutoService(ValidationOption::class)`: + +```kotlin +import com.google.auto.service.AutoService +import io.spine.tools.validation.java.ValidationOption +import io.spine.tools.validation.java.generate.OptionGenerator + +@AutoService(ValidationOption::class) +public class WhenOption : ValidationOption { + + public companion object { + public const val NAME: String = "when" + } + + override val reactions: Set> = setOf(WhenReaction()) + + override val view: Set>> = setOf(WhenFieldView::class.java) + + override val generator: OptionGenerator = WhenGenerator() +} +``` + +Key points: + +- `NAME` is a `const val` in the companion object. Its value must exactly match the field name + used in the `extend` block (for example, `when`). The `Reaction` uses this constant in its + `@Where` filter to subscribe only to events for this option. +- `reactions` can contain multiple `Reaction` instances; `view` can list multiple `View` classes. +- `generator` accepts exactly **one** `OptionGenerator`. Only one generator per + `ValidationOption` is allowed. + +Both `OptionsProvider` and `ValidationOption` are discovered via Java `ServiceLoader`, so the +`@AutoService` annotation must be present and the annotation processor must run during +compilation. + +For Kotlin implementations, configure AutoService's KSP processor in the module that contains the +`@AutoService` classes: + +```kotlin +plugins { + id("com.google.devtools.ksp") +} + +dependencies { + compileOnly("com.google.auto.service:auto-service-annotations:1.1.1") + ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0") +} +``` + +If the provider is written in Java, use AutoService's `annotationProcessor` dependency instead of +KSP. See the [AutoService documentation][auto-service] for Gradle examples. + +## What's next + +- [Declare the event and view state](declare-event-and-view.md) +- [Back to Custom Validation](../) + +[auto-service]: https://github.com/google/auto/tree/main/service diff --git a/docs/content/docs/validation/05-custom-validation/summary.md b/docs/content/docs/validation/05-custom-validation/summary.md new file mode 100644 index 0000000000..0f0670e133 --- /dev/null +++ b/docs/content/docs/validation/05-custom-validation/summary.md @@ -0,0 +1,46 @@ +--- +title: 'Summary' +description: A recap of the steps for implementing and integrating a custom validation option. +headline: Documentation +--- + +# Summary + +This section walked through the complete workflow for adding a custom validation option to the +Spine Validation library, from declaring the option in Protobuf to making it available to the +Spine Compiler. + +## What you covered + +- **[Declare the option in Protobuf](declare-the-option.md)** — define a Protobuf `extend` + block targeting a standard descriptor option type, with a unique field number and an error + message template that supports named placeholders. + +- **[Register the option](register-the-option.md)** — wire an `OptionsProvider` to register the + Protobuf extension at runtime, and a `ValidationOption` to bind the option name to its + `Reaction`, `View`, and `Generator`. Both are discovered via Java `ServiceLoader`. + +- **[Declare the event and view state](declare-event-and-view.md)** — define a domain event + emitted when the option is encountered, and a projection state message the `Generator` + queries to accumulate option data. + +- **[Implement the `Reaction`](implement-the-reaction.md)** — subscribe to `FieldOptionDiscovered` + events, filter by option name, validate the field type and option value, and emit the domain + event — or signal no reaction when the option is disabled. + +- **[Implement the `View`](implement-the-view.md)** — build a projection that accumulates event + data, making the full set of option applications queryable by the `Generator`. + +- **[Implement the `Generator`](implement-the-generator.md)** — query the `View` and produce + Java validation code inlined into the generated `validate()` method, handling both single + and repeated field cardinalities. + +- **[Pass the option to the Compiler](pass-to-compiler.md)** — place the `ValidationOption` + implementation on the Compiler's user classpath via a direct `spineCompiler` dependency or + a distributable Gradle plugin. + +## What's next + +- Explore library internals: [Architecture](../09-developers-guide/architecture.md) +- See the modules that back this pipeline: + [Key modules](../09-developers-guide/key-modules.md) diff --git a/docs/content/docs/validation/08-custom-validation/typical_custom_option.jpg b/docs/content/docs/validation/05-custom-validation/typical_custom_option.jpg similarity index 100% rename from docs/content/docs/validation/08-custom-validation/typical_custom_option.jpg rename to docs/content/docs/validation/05-custom-validation/typical_custom_option.jpg diff --git a/docs/content/docs/validation/08-custom-validation/_index.md b/docs/content/docs/validation/08-custom-validation/_index.md deleted file mode 100644 index d207d6a7b6..0000000000 --- a/docs/content/docs/validation/08-custom-validation/_index.md +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: Custom Validation -description: Extending the library with custom options and code generation. -headline: Documentation ---- - -# Custom validation - -Users can extend the library by providing custom Protobuf options and code generation logic. - -Follow these steps to create a custom option: - -1. Declare a Protobuf [extension](https://protobuf.dev/programming-guides/proto3/#customoptions) - in your `.proto` file. -2. Register it via `io.spine.option.OptionsProvider`. -3. Implement the following entities: - - Reaction (`MyOptionReaction`) – discovers and validates the option. - - View (`MyOptionView`) – accumulates valid option applications. - - Generator (`MyOptionGenerator`) – generates Java code for the option. -4. Register them via `io.spine.tools.validation.java.ValidationOption`. - -Below is a workflow diagram for a typical option: - -![Typical custom option](typical_custom_option.jpg) - -## What’s next - -- [Using validators](../04-validators/) -- Learn where this plugs in: [Architecture](../09-developers-guide/architecture.md). - -Take a look at the `:tests:extensions` module that contains a full example of -implementation of the custom `(currency)` option. - -Note that a custom option can provide several policies and views, but only one generator. -This allows building more complex models, using more entities and events. - -Let's take a closer look at each entity. - -### Reaction - -Usually, this is an entry point to the option handling. - -The reaction subscribes to one of `*OptionDiscovered` events: - -- `FileOptionDiscovered`. -- `MessageOptionDiscovered`. -- `FieldOptionDiscovered`. -- `OneofOptionDiscovered`. - -It filters incoming events, taking only those who contain the option of the interest. The reaction -may validate the option application, query `TypeSystem`, extract and transform data arrived with -the option, if any. Once ready, it emits an event signaling that the discovered option is valid -and ready for the code generation. - -The reaction may report a compilation warning or an error, failing the whole compilation if it -finds an illegal application of the option. - -For example: - -1. An unsupported field type. -2. Illegal option content (invalid regex, parameter, signature). - -The reaction may just ignore the discovered option and emit `NoReaction`. A typical example -of this is a boolean option, such as `(required)`, which does nothing when it is set to `false`. - -The desired behavior depends on the option itself. - -### View - -Views accumulate events from policies, serving as data providers for the validation model -used by code generators. Views are typically simple and only accumulate data; for more complex -logic, use policies. - -Usually, one view represents a single application of an option. - -### Generator - -The generator is an entity that provides an actual implementation of the option behavior. -The generator produces Java code for every application of that option within the message type. - -It has access to the `Querying` interface and can query views to find those belonging -to the processed message type. diff --git a/docs/content/docs/validation/09-developers-guide/architecture.md b/docs/content/docs/validation/09-developers-guide/architecture.md index 1ba2c01559..0a4240b081 100644 --- a/docs/content/docs/validation/09-developers-guide/architecture.md +++ b/docs/content/docs/validation/09-developers-guide/architecture.md @@ -24,4 +24,4 @@ The workflow is the following: ## What’s next - See the project layout: [Key modules](key-modules.md). -- If you need organization-specific rules: [Custom validation](../08-custom-validation/). +- If you need organization-specific rules: [Custom validation](../05-custom-validation/). diff --git a/docs/content/docs/validation/09-developers-guide/key-modules.md b/docs/content/docs/validation/09-developers-guide/key-modules.md index 6b13444c51..38fd4a20a9 100644 --- a/docs/content/docs/validation/09-developers-guide/key-modules.md +++ b/docs/content/docs/validation/09-developers-guide/key-modules.md @@ -38,4 +38,4 @@ project paths (e.g. `:java`, `:tests:vanilla`). ## What’s next -- Build custom validation rules: [Custom validation](../08-custom-validation/). +- Build custom validation rules: [Custom validation](../05-custom-validation/). diff --git a/docs/content/docs/validation/_index.md b/docs/content/docs/validation/_index.md index befa11289e..6082e60873 100644 --- a/docs/content/docs/validation/_index.md +++ b/docs/content/docs/validation/_index.md @@ -19,4 +19,4 @@ options, and runs those checks automatically when you build messages. - [Built-in options](03-built-in-options/) - [Using validators](04-validators/) - How it works: [Architecture](09-developers-guide/architecture.md) -- Extension points: [Custom validation](08-custom-validation/) +- Extension points: [Custom validation](05-custom-validation/) diff --git a/docs/data/docs/validation/2-0-0-snapshot/sidenav.yml b/docs/data/docs/validation/2-0-0-snapshot/sidenav.yml index ac1887e642..afdc7f1821 100644 --- a/docs/data/docs/validation/2-0-0-snapshot/sidenav.yml +++ b/docs/data/docs/validation/2-0-0-snapshot/sidenav.yml @@ -58,7 +58,26 @@ - page: Using `ValidatorRegistry` file_path: 04-validators/validator-registry - page: Custom validation - file_path: 08-custom-validation + key: 05-custom-validation + children: + - page: Overview + file_path: 05-custom-validation + - page: Declare the option in Protobuf + file_path: 05-custom-validation/declare-the-option + - page: Register the option + file_path: 05-custom-validation/register-the-option + - page: Declare the event and view state + file_path: 05-custom-validation/declare-event-and-view + - page: 'Implement the `Reaction`' + file_path: 05-custom-validation/implement-the-reaction + - page: 'Implement the `View`' + file_path: 05-custom-validation/implement-the-view + - page: 'Implement the `Generator`' + file_path: 05-custom-validation/implement-the-generator + - page: Pass the option to the Compiler + file_path: 05-custom-validation/pass-to-compiler + - page: Summary + file_path: 05-custom-validation/summary - page: Developer’s guide key: 09-developers-guide children: diff --git a/pom.xml b/pom.xml index 0e1aa88d5c..07964fc1f4 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject. --> io.spine.tools validation -2.0.0-SNAPSHOT.414 +2.0.0-SNAPSHOT.415 2015 @@ -56,13 +56,13 @@ all modules and does not describe the project structure per-subproject. io.spine spine-time - 2.0.0-SNAPSHOT.235 + 2.0.0-SNAPSHOT.237 compile io.spine spine-validation-jvm-runtime - 2.0.0-SNAPSHOT.411 + 2.0.0-SNAPSHOT.414 compile @@ -269,7 +269,7 @@ all modules and does not describe the project structure per-subproject. io.spine.tools core-jvm-plugins - 2.0.0-SNAPSHOT.062 + 2.0.0-SNAPSHOT.063 io.spine.tools @@ -281,10 +281,15 @@ all modules and does not describe the project structure per-subproject. spine-dokka-extensions 2.0.0-SNAPSHOT.7 + + io.spine.tools + time-validation + 2.0.0-SNAPSHOT.236 + io.spine.tools validation-java-bundle - 2.0.0-SNAPSHOT.411 + 2.0.0-SNAPSHOT.414 net.sourceforge.pmd diff --git a/version.gradle.kts b/version.gradle.kts index f7cddf7dd1..2e5fcb8bd6 100644 --- a/version.gradle.kts +++ b/version.gradle.kts @@ -27,4 +27,4 @@ /** * The version of the Validation SDK to publish. */ -val validationVersion by extra("2.0.0-SNAPSHOT.414") +val validationVersion by extra("2.0.0-SNAPSHOT.415")