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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,22 @@ follow semantic versioning; release dates are ISO 8601.

### Documentation

- **The last three documents that put `MissingBackendException` at the render call.**
An earlier pass moved the module READMEs and the exception's own Javadoc onto
`create()` and stopped at the repository root, so the troubleshooting entry, the 2.0
migration guide and ADR 0016 kept telling a reader to look at `buildPdf()` — and the
migration guide links straight into the troubleshooting entry, so the two reinforced
each other. All three now name the call that actually fails, and the troubleshooting
entry adds the one case that really does surface at the output call: `buildPptx()`
when the PPTX backend is missing but the PDF one is not. The contract test carried
the same confusion: it wrapped `create()`, `pageFlow(...)` and `toPdfBytes()` in one
assertion, so only the first line ever ran while its name promised the third. It is
split, and a second case pins the other side of the boundary — configuring a document
needs no backend, opening the session does. The output-call half is covered where it
is actually reachable: a test in `render-pdf`, whose classpath has one backend and not
the other, calls `buildPptx(...)` through the public API rather than the resolver
underneath it. The old heading keeps working as an anchor, so links already published
against it still land on the entry.
- **The examples stop describing the releases they were written for.** Eight committed
previews read as documents about 1.x: three framed a current feature as "v1.6 Phase
A/B/C" — a plan for a release that shipped — one told the reader to tag v1.9.0 to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,59 @@
import com.demcha.compose.document.exceptions.MissingBackendException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* The lean {@code graph-compose-core} carries no render backend. Any operation that
* needs one — measuring text to lay out, rendering, or rasterizing a document — must
* fail with a {@link MissingBackendException} that names the artifact to add
* ({@code graph-compose-render-pdf}). This is the defining contract of the lean core.
* The lean {@code graph-compose-core} carries no render backend, and says so at the
* first operation that needs one with a {@link MissingBackendException} naming the
* artifact to add ({@code graph-compose-render-pdf}). This is the defining contract
* of the lean core.
*
* <p>That first operation is {@code create()}, not a render call: opening a session
* resolves the font-metrics provider, because layout measures text before anything is
* drawn. The render-time site exists too, but it is unreachable from here — no session
* opens on this classpath — so the format-specific cases below assert on the resolver
* directly. The public path to it, {@code buildPptx()} without {@code render-pptx},
* needs a classpath where one backend is present and another is not, and is covered by
* {@code MissingPptxBackendContractTest} in {@code render-pdf}.</p>
*
* <p>The test lives in core's own <em>backend-free</em> test scope on purpose: a
* backend on the classpath (as in the qa module) would resolve the provider and hide
* a regression here.</p>
*/
class MissingBackendContractTest {

/**
* The throw is at {@code create()}, not at the render call.
*
* <p>This used to wrap {@code create()}, {@code pageFlow(...)} and
* {@code toPdfBytes()} in one assertion. It passed, but only the first line ever
* ran — and its name said "rendering", which is where three published documents
* then placed the failure. Asserting on {@code create()} alone is what pins the
* documented contract: opening a session resolves the font-metrics provider,
* because layout measures text before anything is drawn.</p>
*/
@Test
void renderingWithoutABackendThrowsMissingBackendExceptionNamingRenderPdf() {
assertThatThrownBy(() -> {
try (DocumentSession session = GraphCompose.document()
.pageSize(200, 200)
.create()) {
session.pageFlow(page -> page.module("m", module -> module.paragraph("hi")));
session.toPdfBytes();
}
})
void openingASessionWithoutABackendThrowsNamingRenderPdf() {
assertThatThrownBy(() -> GraphCompose.document().pageSize(200, 200).create())
.isInstanceOf(MissingBackendException.class)
.hasMessageContaining("graph-compose-render-pdf");
}

/**
* The other side of the same boundary: configuring a document is backend-free, so
* a builder that resolved the provider eagerly — or one that deferred it past
* {@code create()} to the render call — would fail here.
*/
@Test
void configuringADocumentDoesNotNeedABackendUntilTheSessionOpens() {
GraphCompose.DocumentBuilder builder = GraphCompose.document().pageSize(200, 200);

assertThat(builder).isNotNull();
assertThatThrownBy(builder::create).isInstanceOf(MissingBackendException.class);
}

@Test
void missingKnownFormatNamesTheArtifactToAdd() {
assertThatThrownBy(() -> BackendProviders.fixedLayout("pptx"))
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ back here.
- **[architecture/lifecycle.md](architecture/lifecycle.md)** — the document lifecycle from `GraphCompose.document(...)` through `buildPdf()`.
- **[architecture/pagination-ordering.md](architecture/pagination-ordering.md)** — how nodes are paginated and ordered.
- **[architecture/package-map.md](architecture/package-map.md)** — what's in which package.
- **[architecture/canonical-legacy-parity.md](architecture/canonical-legacy-parity.md)** — canonical (v1.5+) vs legacy compatibility matrix.
- **[architecture/canonical-legacy-parity.md](architecture/canonical-legacy-parity.md)** — per-feature authoring coverage of the canonical API, refreshed for the 2.1 line. The recipes, the capabilities catalogue and the troubleshooting guide all link into it.

### Operations
- **[operations/production-rendering.md](operations/production-rendering.md)** — server-side rendering, streaming, thread safety.
Expand Down
4 changes: 2 additions & 2 deletions docs/adr/0016-multi-module-packaging.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ render backends discovered at runtime through a `ServiceLoader` SPI.
the canonical DSL / nodes / style / layout, chart / svg / markdown / barcode, and
the `FixedLayoutBackendProvider` / `FontMetricsProvider` SPI seams. Depends only on
`slf4j-api` + `flexmark`. Rendering nothing until a backend is on the classpath —
it throws `MissingBackendException` (naming the artifact to add) if asked to build a
PDF without one.
`create()` throws `MissingBackendException` (naming the artifact to add) without one,
since opening a session resolves the font-metrics provider that ships with the backend.
- **`graph-compose-render-pdf`** — the entire PDFBox backend (`document.backend.fixed.pdf.**`
and the `engine.render.pdf.**` tree), PDFBox, and zxing. Registers the PDF
`FixedLayoutBackendProvider` / `FontMetricsProvider` via `META-INF/services`.
Expand Down
7 changes: 4 additions & 3 deletions docs/migration/v2.0.0-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ source break in the split — PDF compatibility was kept as the priority.
## `MissingBackendException` — the lean-core signal

`graph-compose-core` on its own renders nothing until a render backend is on the classpath.
Asking it to build a PDF throws `MissingBackendException`, whose message names the artifact
to add:
**Opening a session** — `create()` — throws `MissingBackendException`, because measuring text
is the first thing layout needs and the metrics provider ships with the backend. You do not
get as far as asking for a PDF. The message names the artifact to add:

```
No fixed-layout render backend on the classpath: add the
Expand All @@ -82,7 +83,7 @@ implementation) to render, rasterize, or measure a document.

Depend on `graph-compose` (or `graph-compose-bundle`) instead of `graph-compose-core` and
the PDF backend is already present. See the
[troubleshooting entry](../troubleshooting.md#missingbackendexception-when-rendering).
[troubleshooting entry](../troubleshooting.md#missingbackendexception-when-opening-a-session).

## Removed deprecated APIs

Expand Down
7 changes: 4 additions & 3 deletions docs/recipes/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ which uses the v1.5 `ShapeContainerNode` work as a worked example.
| You want to... | Touch | Read |
| --- | --- | --- |
| Add a new semantic node | `DocumentNode` record + `NodeDefinition` + render handler | [Extension guide § 1](../contributing/extension-guide.md#1-add-a-semantic-node) |
| Add a fluent setter | One `*Builder` only | [Extension guide § 2](../contributing/extension-guide.md#2-add-a-fluent-setter-to-a-builder) |
| Add a render backend | Implement `FixedLayoutBackend` or `SemanticBackend` | [Extension guide § 3](../contributing/extension-guide.md#3-add-a-render-backend) |
| Pin layout in a snapshot test | Use `LayoutSnapshotAssertions.assertMatches` | [Extension guide § 4](../contributing/extension-guide.md#4-validate-a-custom-nodes-layout-via-snapshots) |
| Add a fluent setter | One `*Builder` only | [Extension guide § 2](../contributing/extension-guide.md#2-add-a-fluent-setter-to-an-existing-builder) |
| Add a render handler for an existing backend | One `*FragmentRenderHandler` | [Extension guide § 3](../contributing/extension-guide.md#3-add-a-render-handler-for-an-existing-backend) |
| Add a render backend | Implement `FixedLayoutBackend` or `SemanticBackend` | [Extension guide § 4](../contributing/extension-guide.md#4-add-a-new-backend) |
| Pin layout in a snapshot test | Use `LayoutSnapshotAssertions.assertMatches` | [Extension guide § 5](../contributing/extension-guide.md#5-layout-snapshot-tests-for-your-own-nodes) |

## 1. Add a semantic node — five-step skeleton

Expand Down
19 changes: 14 additions & 5 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,22 @@ fall back to inline content with a one-time capability warning.
Use DOCX only for paragraph / list / table / image / section content.
Per-feature mapping: [canonical ↔ legacy parity matrix](architecture/canonical-legacy-parity.md).

## `MissingBackendException` when rendering
<a id="missingbackendexception-when-rendering"></a>

**Cause.** You depend on `graph-compose-core` (the lean 2.0 engine) but no render
## `MissingBackendException` when opening a session

**Cause.** You depend on `graph-compose-core` (the lean engine artifact) but no render
backend is on the classpath. The core carries the `DocumentSession` authoring API and a
`ServiceLoader` seam, but the actual renderer ships separately — so `document.buildPdf()`
/ `toPdfBytes()` / `toImages()` throws `MissingBackendException` until a backend is
discoverable.
`ServiceLoader` seam; the renderer ships separately.

The throw comes **earlier than the name suggests**: `create()` resolves the font-metrics
provider immediately, because laying text out needs measurement before anything is drawn.
So **opening the session** fails — you never reach `buildPdf()` / `toPdfBytes()` /
`toImages()`, and the stack trace points at your `create()` call, not at a render call.

The one case that does surface at the output call is asking for a format whose backend is
missing while another is present: `buildPptx()` without `graph-compose-render-pptx` on a
classpath that has the PDF backend. The message names that artifact instead.

**Fix.** Add the PDF backend, or depend on `graph-compose` (which already bundles it):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.demcha.compose.document.api;

import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.exceptions.MissingBackendException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Path;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* The other half of the {@code MissingBackendException} contract: the one case that
* really does surface at the output call.
*
* <p>On a lean core the session never opens — {@code create()} resolves the
* font-metrics provider first — so no test on a backend-free classpath can reach a
* render call. This module is where the case becomes reachable: the PDF backend is
* present, so measurement resolves and the session opens, while {@code render-pptx}
* is absent, so asking for a deck fails at {@code buildPptx(...)} and names the
* artifact to add.</p>
*
* <p>The engine-side test asserts on {@code BackendProviders.fixedLayout("pptx")}
* directly. That pins the resolver, not the path a caller takes to it: a convenience
* method that stopped routing through the resolver, or started resolving eagerly,
* would leave that test green. This one goes through the public API.</p>
*
* <p>Note the ordering the assertion depends on: a document with no roots fails
* {@code ensureRenderable()} with an {@code IllegalStateException} <em>before</em>
* any backend is looked up, so the page content below is not decoration — without it
* this test would pass for the wrong reason.</p>
*/
class MissingPptxBackendContractTest {

@Test
void buildingADeckWithoutThePptxBackendThrowsNamingRenderPptx(@TempDir Path directory) throws Exception {
Path deck = directory.resolve("deck.pptx");

try (DocumentSession session = GraphCompose.document()
.pageSize(200, 200)
.create()) {
session.pageFlow(page -> page.module("m", module -> module.paragraph("hi")));

assertThatThrownBy(() -> session.buildPptx(deck))
.isInstanceOf(MissingBackendException.class)
.hasMessageContaining("graph-compose-render-pptx");
}
}
}
Loading