A backend server for OneLiteFeather's Vulpes project, providing a REST API and database access.
- REST API for managing custom attributes, fonts, items, and notifications
- OpenAPI documentation
- Automatic Dart Dio client generation
This project automatically generates a Dart Dio client from the OpenAPI specification during the build process. The client is then pushed to a separate Git repository with the project version as a tag.
- The OpenAPI specification is generated during the build process using Micronaut's OpenAPI support.
- The OpenAPI Generator Gradle plugin is used to generate a Dart Dio client from the specification.
- The generated client is pushed to the vulpes-client repository with the project version as a tag.
The OpenAPI Generator is configured in the build.gradle.kts file:
openApiGenerate {
generatorName.set("dart-dio")
inputSpec.set("$buildDir/tmp/kapt3/classes/main/META-INF/swagger/vulpes-backend-1.0.yml")
outputDir.set("$buildDir/generated/dart-client")
apiPackage.set("net.onelitefeather.vulpes.client.api")
invokerPackage.set("net.onelitefeather.vulpes.client.invoker")
modelPackage.set("net.onelitefeather.vulpes.client.model")
configOptions.set(mapOf(
"pubName" to "vulpes_client",
"pubVersion" to (project.version as String),
"pubDescription" to "Vulpes API Client",
"pubAuthor" to "OneLiteFeather",
"pubAuthorEmail" to "p.glanz@madfix.me",
"pubHomepage" to "https://github.com/OneLiteFeatherNET/vulpes-client",
"pubRepository" to "https://github.com/OneLiteFeatherNET/vulpes-client",
"dateLibrary" to "core",
"enumUnknownDefaultCase" to "true"
))
}The GitHub Actions workflow is configured to run the client generation and repository pushing during the release process. The workflow uses a custom secret called CLIENT_REPO_TOKEN for authenticating with GitHub when pushing to the client repository.
To set up the CLIENT_REPO_TOKEN:
- Create a personal access token with the
reposcope. - Add the token as a secret in the repository settings with the name
CLIENT_REPO_TOKEN.
- Java 21
- Gradle
- Node.js (for semantic-release)
./gradlew build./gradlew run./gradlew testThe seed environment fills an empty database with realistic test data on startup, so every screen of the UI can be tested without creating data by hand. Start the local database (docker compose -f docker/compose.yml up -d) and run:
MICRONAUT_ENVIRONMENTS=local,seed ./gradlew runSeeding only happens when the database contains no projects; otherwise it is skipped and the log says so. To throw away all data and seed again:
MICRONAUT_ENVIRONMENTS=local,seed VULPES_SEED_RESET=true ./gradlew runWarning: a reset deletes all Vulpes data in the database, including projects you created yourself.
The data is the same on every run (VULPES_SEED_RANDOM_SEED changes the generated filler). It contains:
| Project | Purpose |
|---|---|
eldoria_rpg — Eldoria RPG |
Fantasy RPG setup: enchanted weapons with coloured lore, icon fonts, quest sounds, custom attributes, notifications and a custom "Shadow Realm" dimension. Every entity type has children. |
skyblock_lab — Skyblock Lab |
Smaller labor project. Shares one key per entity type with Eldoria (starter_sword, hud_icons, ui_click, lobby, bonus_health, welcome) to show project scoping. |
edge_cases — Edge Cases |
More than one page per list, records without children, every enum value, validation min/max values, 255-character, Unicode and §-formatted texts, long lore/char lists for reordering, unsafe enchantments. |
empty_project — Empty Project |
No entities at all, for empty states. |
The seeder lives in src/dev and is only on the classpath of ./gradlew run; it is not part of the built jars or the Docker image.
Every endpoint answers a failure with a single body shape, RFC 9457 Problem Details, served as application/problem+json:
{
"type": "https://vulpes.onelitefeather.net/errors/resource-not-found",
"title": "Resource not found",
"status": 404,
"detail": "Attribute not found.",
"instance": "/project/6f1c.../attribute/update",
"code": "RESOURCE_NOT_FOUND",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": []
}This holds for framework errors too — unbindable path variables, malformed JSON, 405, 415 — because the shape is produced by an ErrorResponseProcessor, which is the hook every built-in Micronaut handler routes its body through.
- Branch and localize on
code, never ondetail. The codes are theErrorCodeenum and reach the generated Dart client as an enum;detailis English prose and may be reworded at any time. - On
VALIDATION_FAILED,errorslists the rejected fields as{field, code, message}, wherefieldis the request property path (displayName), so a form can mark the matching input. - Show
traceIdin support dialogs. It is the OpenTelemetry trace id when tracing is enabled, and always identifies the matching server log line.
- Raise failures with
ApiException; the status, title and problem type come from theErrorCodeyou pass. - The message you pass is the response body. Author it at the throw site from data the caller already sent us. Never forward a message from JDBC, Hibernate or any other lower layer — those carry table names, column names and SQL fragments (CWE-209). Details for 5xx are a fixed constant for the same reason.
- When the honest reason differs from what the caller may learn — a cross-project access, for instance — pass it as
internalDetail. It is logged and never serialized.
This project is licensed under the AGPL-3.0 License - see the LICENSE file for details.