Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Example - API-Only TranscriberJ Description Bundles Build
on:
push:
paths:
- 'examples/api-only-transcriberj/description-bundles/**/*.java'
- 'examples/api-only-transcriberj/description-bundles/**/*.yaml'
- 'examples/api-only-transcriberj/description-bundles/**/*.gradle'
- 'examples/api-only-transcriberj/description-bundles/**/*.properties'
- 'examples/api-only-transcriberj/description-bundles/**/libs.versions.toml'
- 'examples/api-only-transcriberj/description-bundles/package.json'
- 'examples/api-only-transcriberj/description-bundles/package-lock.json'
- '.github/workflows/example-api-only-transcriberj-description-bundles-build.yml'

permissions:
contents: read

jobs:
Build-Example:
name: Build Example
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
persist-credentials: false

- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin'
java-version: 21

# The example builds its contract with the API-Only Publisher, which is a Node.js tool.
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 24

- name: Setup Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0

- name: Build example
run: |
cd examples/api-only-transcriberj/description-bundles
chmod +x ./gradlew
./gradlew test --no-daemon
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
build/
.gradle/
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
extends:
- recommended
84 changes: 84 additions & 0 deletions examples/api-only-transcriberj/description-bundles/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
= Example: descriptions from a ResourceBundle, in English and Dutch
:toc: left
:toc-title: Contents
:icons: font
:source-highlighter: rouge

A small, complete project that documents one contract in two languages from one generated class tree.

The API-Only TranscriberJ generates the contract's classes; this project supplies the descriptions its documentation shows, in a `ResourceBundle`, and leaves the rest to the contract.
The guide that walks through it step by step is https://github.com/Arc-E-Tect/SoftwareEngineeringDoneRight-API/blob/main/docs/guides/description-bundles/tutorial.adoc[the description bundles tutorial ↗]; the full reference is https://github.com/Arc-E-Tect/SoftwareEngineeringDoneRight-API/blob/main/api-only-transcriberj/README.adoc#description-bundles[the TranscriberJ's README ↗].

== What is in it

[cols="2,3", options="header"]
|===
|File |What it is

|`src/main/api/openapi/`
|The `orders` contract: `OrderV1`, with a nested `customer.address` and an array of `lines`, and `ProblemV1`, an error with an array of `errors`.
Every field is described in the contract's own words.

|`src/test/resources/docs/Descriptions.properties`
|The project's own English text -- for *some* fields only, so the rest visibly fall back to the contract.

|`src/test/resources/docs/Descriptions_nl.properties`
|The Dutch text, also deliberately incomplete, so a missing translation visibly falls back too.

|`build.gradle`
|The Publisher builds the contract into `build/`, the Subscriber fetches it, and the TranscriberJ generates from it, with `descriptionBundle = 'docs.Descriptions'`.

|`src/test/java/com/example/orders/DescriptionsTest.java`
|Renders the documentation in both languages and checks the text, the fallbacks, and the keys the bundles miss or no longer need.
|===

== Run it

You need Java 21 and Node.js (`^22.14.0`, `^24.10.0` or `>=26.0.0`).

[source,console]
----
./gradlew test
----

The build installs the Publisher, builds and publishes the contract, generates the classes, and runs the tests.

The tests run in English unless the build asks for another language, which `build.gradle` passes to the test JVM:

[source,console]
----
./gradlew test -PdocsLocale=nl
----

== What to look at

`build/generated/sources/transcriberj/orders/com/example/contract/ContractDescriptions.java`::
Every key a bundle for this contract may carry, in `KEYS`.
`OrderV1.customer.address.street` is a nested field; `OrderV1.lines[].sku` and `ProblemV1.errors[].message` are fields of an array's items.

`build/generated/sources/transcriberj/orders/com/example/contract/OrderV1.java`::
Every description is a call to `ContractDescriptions.of(key, text)`, where the text is what the contract says -- the fallback.

`build/reports/tests/test/index.html`::
The tests, one per behaviour: the bundle winning where it has a key, the contract where it has none, the same tree in Dutch, the language a run asks for, REST Docs rendering each language, and the keys the bundles miss or no longer need.

`build/reports/transcriberj/orders.txt`::
The generation report, which names the bundle and how many keys it may carry.

== Try it

* Add `OrderV1.customer.address.city=De plaats.` to `Descriptions_nl.properties` and run the tests again.
Two fail, and both for the right reason: `theSameTreeDocumentsTheContractInDutch` expected the city to fall back to the contract's English, and `whatTheBundlesDoNotCoverCanBeListed` expected it to be untranslated.
* Rename `OrderV1.yaml` to `PurchaseOrderV1.yaml`, update the `$ref` in `paths/Order.yaml`, and raise `version` and `apiContractVersion` to `2.0.0`.
The test no longer compiles: `OrderV1` does not exist any more, which is the first thing you see.
Remove every test that names it, as a class or in a key, and `everyKeyInTheBundlesNamesSomethingTheContractDescribes` lists the `OrderV1` keys the bundles still carry, which nothing uses now:
+
[source,text]
----
[keys the contract no longer describes, per file]
Expecting empty but was: {"docs/Descriptions.properties"=["OrderV1", "OrderV1.customer.address.street", "OrderV1.lines[].sku"], "docs/Descriptions_nl.properties"=["OrderV1",
"OrderV1.customer.address.street",
"OrderV1.customer.name",
"OrderV1.id",
"OrderV1.lines[].quantity"]}
----
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# apionly.lock -- the published contracts this project builds against.
# Generated by the Arc-E-Tect API-Only Subscriber. Commit it; do not edit it.
# Run `verifyApiSpec` to check that what is on disk still matches.
format 1

target orders
version 1.0.0
channel file
sha256 caecfedabb33e61725faf0129b8cd29594efcd40d0c01f590d05f1d1ad88f357 openapi.yaml
30 changes: 30 additions & 0 deletions examples/api-only-transcriberj/description-bundles/apionly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
schemaVersion: 1

sources:
root: src/main/api
openapi: openapi

defaults:
openapi:
lint: .redocly.yaml
outputName: openapi.yaml

build:
staging: build/api-only/staging
dist: build/api-only/dist

reports:
lint: build/api-only/reports/lint

toolchain:
redocly: "@redocly/cli@2.52.0"

channels:
file:
directory: build/api-only/publish
clean: true

targets:
orders:
openapi:
bundle: bundles/orders.yaml
87 changes: 87 additions & 0 deletions examples/api-only-transcriberj/description-bundles/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
plugins {
id 'java'
alias(libs.plugins.api.only.transcriberj)
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

repositories {
mavenCentral()
}

dependencies {
transcriberjEmitters libs.api.only.transcriberj.restdocs

testImplementation platform(libs.junit.bom)
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation libs.assertj.core
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
useJUnitPlatform()
// The language this run documents in, English unless asked: ./gradlew test -PdocsLocale=nl
systemProperty 'apionly.descriptions.locale', providers.gradleProperty('docsLocale').getOrElse('en')
}

// ---------------------------------------------------------------------------
// The contract. The Publisher builds it into build/, and the Subscriber, which
// the TranscriberJ applies, fetches it from there.
// ---------------------------------------------------------------------------

def npm = System.getProperty('os.name').toLowerCase().contains('windows') ? 'npm.cmd' : 'npm'

def installApiOnlyPublisher = tasks.register('installApiOnlyPublisher', Exec) {
group = 'api-only'
description = 'Installs the API-Only Publisher at the version package-lock.json pins.'
inputs.files('package.json', 'package-lock.json').withPropertyName('manifests')
outputs.file('node_modules/.package-lock.json').withPropertyName('installation')
commandLine npm, 'ci', '--no-audit', '--no-fund'
}

def buildApiContract = tasks.register('buildApiContract', Exec) {
group = 'api-only'
description = 'Bundles, stamps and lints the orders contract.'
dependsOn installApiOnlyPublisher
inputs.dir('src/main/api').withPropertyName('specification')
inputs.files('apionly.yaml', 'package-lock.json').withPropertyName('configuration')
outputs.dir('build/api-only/dist/orders').withPropertyName('bundle')
commandLine npm, 'run', '--silent', 'apionly', '--', 'build', '--target', 'orders'
}

def publishApiContract = tasks.register('publishApiContract', Exec) {
group = 'api-only'
description = 'Publishes the orders contract to the file channel in build/api-only/publish.'
dependsOn buildApiContract
inputs.dir('build/api-only/dist/orders').withPropertyName('bundle')
outputs.dir('build/api-only/publish/orders').withPropertyName('archive')
commandLine npm, 'run', '--silent', 'apionly', '--', 'publish', '--target', 'orders', '--channel', 'file'
}

apiOnlySubscriber {
channel {
type = 'file'
directory = file('build/api-only/publish').path
}
subscribe('orders')
}

tasks.named('fetchApiSpecOrders') { dependsOn publishApiContract }

// ---------------------------------------------------------------------------
// The generated classes, with descriptions this project owns.
// ---------------------------------------------------------------------------

apiOnlyTranscriberJ {
subscription('orders') {
basePackage = 'com.example.contract'
// The contract's own descriptions are the fallback for any key the bundle lacks.
generateDocs = true
// src/test/resources/docs/Descriptions.properties, and one file per language.
descriptionBundle = 'docs.Descriptions'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# The version of the orders contract this project implements.
apiContractVersion=1.0.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[versions]
api-only-transcriberj = "0.3.2"
api-only-transcriberj-restdocs = "0.1.1"
junit = "6.1.3"
assertj = "3.27.7"

[libraries]
api-only-transcriberj-restdocs = { module = "com.arc-e-tect:api-only-transcriberj-restdocs", version.ref = "api-only-transcriberj-restdocs" }
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" }

[plugins]
api-only-transcriberj = { id = "com.arc-e-tect.api-only-transcriberj", version.ref = "api-only-transcriberj" }
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading
Loading