Skip to content
Open
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
41 changes: 28 additions & 13 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,13 @@ follow semantic versioning; release dates are ISO 8601.
is now headed by its own name, which is both what the contract asks and easier to
read; a section that was never grouped carries a blank category and gets no label.

`kit()` returns the canonical kit for all three, and says why: a kit draws one body
style and these presets have two — the sidebar's and the main column's — with nothing
in `CvRenderKit` to say which column it is drawing into. Nothing in these templates
routes a body through `SectionDispatcher`, so the kit is what a caller outside them
would draw with, and canonical is the honest answer there.
Their `CvConstructor` methods forward to the canonical `ModuleRenderer` for the
same reason: a kit (and now a constructor method) draws one body style, and these
presets have two — the sidebar's and the main column's. Claimed modules still
lower through `SectionRouter` onto the slot's own renderer, which is why they
already come out in the preset's style. The constructor methods are the contract
a leftover or unclaimed module will use, and the canonical look is the honest
answer for a caller that is not sitting in a slot.

`ModularCvTemplateFidelityTest` grew two corrections in the process. Its item
assertions read raw composed text while its heading assertion dropped spacing, so a
Expand All @@ -321,14 +323,27 @@ follow semantic versioning; release dates are ISO 8601.
rendering style rather than a promise: it asserts every skill and the group's name
reach the page, and leaves the join to the preset drawing bars, chips, or a list.

- **A preset can draw runtime modules in its own style.** `CvRenderKit` is the three
shapes a section body reduces to — a paragraph, a label/value row, a timeline entry —
and a template hands back the kit it draws them with. The lowering from `CvItem`
stays shared, because deciding what a linked title looks like or which fields a kind
reads belongs to the model and must not be re-decided per preset; only the drawing is
the preset's. `BlueBanner`, `ClassicSerif`, and `EditorialBlue` now render modules
with their own entry and project shapes rather than the canonical ones — the
limitation the entry above left open.
- **The constructor contract is the module shapes, not CV meanings.** A template
that can be handed a runtime CV implements `CvConstructor`: one method per
`CvKind` (`paragraph`, `bullets`, `bulletsStacked`, `inlineList`, `entries`,
`entriesDated`) and no defaults. JSON (or any mapper) picks the kind; the
template draws the kind. It does not know whether the section is Experience or
a heading nobody anticipated — that knowledge is not in the contract. Adding a
kind is adding a method, and every `ModularCvTemplate` fails to compile until
it implements it. `CvConstructorKindGateTest` holds the bijection and that
every modular template declares the methods rather than inheriting a default.

The shared `ModuleRenderer` still owns which fields a kind reads, so a
template that wants the canonical look of a kind forwards to it. A template
that already had its own entry or project drawing implements the kind
methods onto that drawing (`BlueBanner`, `EditorialBlue`). `CvRenderKit`
remains the optional primitive hook underneath a kind method, not the
template contract: `ModularCvTemplate` no longer has `kit()`.

- **A preset can draw runtime modules in its own style.** `BlueBanner` and
`EditorialBlue` implement the kind methods through their own entry and
project shapes rather than the canonical ones. `ClassicSerif` is not on
`ModularCvTemplate` yet and still restyles through a private `CvRenderKit`.

### Fixed

Expand Down
22 changes: 14 additions & 8 deletions docs/templates/v2-layered/using-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,20 @@ the section: Sidebar Portrait's language list, which picks the language rows out
of an "Additional Information" section, and Mint Editorial's skill-bar block,
whose group index above it already carries the author's heading.

A template also says *how* it draws through `CvRenderKit`. The shared
lowering turns a module into paragraphs, rows, and entries; the kit draws
them, so a preset with its own entry style renders your runtime module in
that style rather than the canonical one. Presets whose bodies already use
the shared components return `CvRenderKit.defaults()` — and so do the three
column-flow presets, which never route a body through the kit at all: each
lowers a module to the shape its slot draws and lets that slot's own renderer
draw it, which is why a runtime module already comes out in their style.
A template also says *how* it draws through `CvConstructor`: one method per
`CvKind` (`paragraph`, `bullets`, `bulletsStacked`, `inlineList`, `entries`,
`entriesDated`), no defaults. JSON picks the kind; the template implements
the kind. It does not know whether the section is Experience or a heading
the author invented. Adding a kind is adding a method, and every modular
template has to draw it.

The shared `ModuleRenderer` is what a template forwards to when it wants
the canonical look of that kind. A preset with its own entry style
implements the kind methods itself (or forwards through a `CvRenderKit` of
primitives) so a runtime module takes that style rather than the canonical
one. The three column-flow presets still lower a claimed module to the
shape their slot already draws; their constructor methods are the same
contract, ready for a leftover or unclaimed module that has no slot.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.demcha.compose.document.templates.cv.api;

import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.core.theme.BrandTheme;
import com.demcha.compose.document.templates.cv.CvComposedText;
import com.demcha.compose.document.templates.cv.data.CvDocument;
import com.demcha.compose.document.templates.cv.data.CvIdentity;
Expand Down Expand Up @@ -164,10 +166,19 @@ void aSidebarSectionIsNotRenderedAndTheContractSaysSo(ModularCvTemplate template

@ParameterizedTest(name = "{0}")
@MethodSource("modularTemplates")
void everyModularTemplateDeclaresAKit(ModularCvTemplate template) {
assertThat(template.kit())
.as("%s must hand back a kit — the drawing half of the promise", template.id())
.isNotNull();
void everyModularTemplateImplementsEveryKind(ModularCvTemplate template)
throws NoSuchMethodException {
Class<?> type = template.getClass();
for (CvKind kind : CvKind.values()) {
assertThat(type.getMethod(
CvConstructor.methodName(kind),
SectionBuilder.class,
ModuleSection.class,
BrandTheme.class)
.getDeclaringClass())
.as("%s must implement %s", template.id(), kind)
.isNotEqualTo(CvConstructor.class);
}
}

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

import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.templates.core.theme.BrandTheme;
import com.demcha.compose.document.templates.cv.data.CvKind;
import com.demcha.compose.document.templates.cv.data.ModuleSection;

import java.util.Objects;

/**
* The constructor contract: one method per module <em>shape</em>, not per
* CV meaning.
*
* <p>A template that implements this does not know whether a section is
* Experience, Projects, or a heading the author invented. It knows how
* to draw prose, a bullet list, an inline list, and a timeline — with
* and without dates. JSON (or any runtime mapper) picks the kind; the
* template implements the kind.</p>
*
* <p>There are no defaults on the kind methods. Adding a {@link CvKind}
* constant adds a method here, and every {@link ModularCvTemplate}
* fails to compile until it draws the new shape. That is the point: a
* constructor surface that grows in one place and pulls every template
* with it, rather than a shared renderer that can absorb a new kind
* without the templates noticing.</p>
*
* <p>{@link #render(SectionBuilder, ModuleSection, BrandTheme)} is the
* dispatcher, and it is a default because it is not a shape. Its
* {@code switch} is exhaustive over {@link CvKind}, so a new constant
* without a method is a compile error here too.</p>
*
* @since 2.3.0
*/
public interface CvConstructor {

/**
* The method on this interface that draws {@code kind}.
*
* <p>Kept next to the methods themselves so a test can prove the
* bijection without copying the names.</p>
*
* @param kind a module shape
* @return the method name, such as {@code "entriesDated"}
*/
static String methodName(CvKind kind) {
Objects.requireNonNull(kind, "kind");
return switch (kind) {
case PARAGRAPH -> "paragraph";
case BULLETS -> "bullets";
case BULLETS_STACKED -> "bulletsStacked";
case INLINE_LIST -> "inlineList";
case ENTRIES -> "entries";
case ENTRIES_DATED -> "entriesDated";
};
}

/**
* Dispatches {@code module} to the kind method the author picked.
*
* @param host host section receiving the body
* @param module the module; its {@link ModuleSection#kind() kind} selects
* the method
* @param theme the active theme
*/
default void render(SectionBuilder host, ModuleSection module, BrandTheme theme) {
Objects.requireNonNull(host, "host");
Objects.requireNonNull(module, "module");
Objects.requireNonNull(theme, "theme");
switch (module.kind()) {
case PARAGRAPH -> paragraph(host, module, theme);
case BULLETS -> bullets(host, module, theme);
case BULLETS_STACKED -> bulletsStacked(host, module, theme);
case INLINE_LIST -> inlineList(host, module, theme);
case ENTRIES -> entries(host, module, theme);
case ENTRIES_DATED -> entriesDated(host, module, theme);
}
}

/**
* Prose under the section heading. Reads each item's body only.
*
* @param host host section receiving the body
* @param module a {@link CvKind#PARAGRAPH} module
* @param theme the active theme
*/
void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme);

/**
* A bullet per item, description on the same line.
*
* @param host host section receiving the body
* @param module a {@link CvKind#BULLETS} module
* @param theme the active theme
*/
void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme);

/**
* A bullet per item, description stacked underneath.
*
* @param host host section receiving the body
* @param module a {@link CvKind#BULLETS_STACKED} module
* @param theme the active theme
*/
void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme);

/**
* One line per item, the description collapsed after a bold label.
*
* @param host host section receiving the body
* @param module a {@link CvKind#INLINE_LIST} module
* @param theme the active theme
*/
void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme);

/**
* Timeline entries without the date column.
*
* @param host host section receiving the body
* @param module a {@link CvKind#ENTRIES} module
* @param theme the active theme
*/
void entries(SectionBuilder host, ModuleSection module, BrandTheme theme);

/**
* Timeline entries with the date column.
*
* @param host host section receiving the body
* @param module a {@link CvKind#ENTRIES_DATED} module
* @param theme the active theme
*/
void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme);
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package com.demcha.compose.document.templates.cv.api;

import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.cv.components.CvRenderKit;
import com.demcha.compose.document.templates.cv.data.CvDocument;
import com.demcha.compose.document.templates.cv.data.CvKind;
import com.demcha.compose.document.templates.cv.data.ModuleSection;
import com.demcha.compose.document.templates.cv.data.Slot;

/**
* A CV template that renders every section placed in {@link Slot#MAIN} —
* every {@link CvKind}, under whatever heading the author wrote.
* A CV template that implements every constructor shape and renders every
* section placed in {@link Slot#MAIN} — every {@link CvKind}, under
* whatever heading the author wrote.
*
* <p><strong>The promise is exactly that, and the slot is part of it.</strong>
* <p>The constructor half is {@link CvConstructor}: one method per module
* shape, no defaults. A JSON mapper (or any runtime assembler) picks a
* kind; this template draws that kind. It does not know whether the
* section is Experience or a heading nobody anticipated — that knowledge
* is not in the contract.</p>
*
* <p><strong>The placement promise is exactly {@link Slot#MAIN}, and the
* slot is part of it.</strong>
* Every shipped preset reads {@code sectionsIn(Slot.MAIN)} and no other
* slot — including the ones that compose a sidebar of their own, which fill
* it from the identity and from the main-slot sections their routing sends
Expand Down Expand Up @@ -40,22 +46,10 @@
* last because a preset with an editorial vocabulary of its own is the one
* likely to rename what the author wrote. Each item must reach the page, so
* the interface cannot be worn by a template that would drop or retitle
* one.</p>
*
* <p>{@link #kit()} is how the promise stays compatible with a preset's own
* look: the shared lowering turns a {@link ModuleSection} into paragraphs,
* rows, and entries, and the kit draws them the way this template draws
* everything else.</p>
* one. {@code CvConstructorKindGateTest} holds the other half: every kind
* has a non-default method, and every modular template declares it.</p>
*
* @since 2.3.0
*/
public interface ModularCvTemplate extends DocumentTemplate<CvDocument> {

/**
* How this template draws the shapes a module lowers to.
*
* @return this template's kit; {@link CvRenderKit#defaults()} for a
* template whose modules look like the canonical components
*/
CvRenderKit kit();
public interface ModularCvTemplate extends DocumentTemplate<CvDocument>, CvConstructor {
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,21 @@
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.templates.core.theme.BrandTheme;
import com.demcha.compose.document.templates.cv.data.CvEntry;
import com.demcha.compose.document.templates.cv.data.CvItem;
import com.demcha.compose.document.templates.cv.data.CvKind;
import com.demcha.compose.document.templates.cv.data.CvRow;
import com.demcha.compose.document.templates.cv.data.RowStyle;

/**
* How one template draws the three shapes a CV section body reduces to:
* Primitive drawing a {@link ModuleRenderer} kind method can restyle:
* a paragraph of prose, a label/value row, a timeline entry.
*
* <p>A preset that wants runtime {@code ModuleSection}s to look like the
* rest of its own document implements this and hands it back through
* {@link com.demcha.compose.document.templates.cv.api.ModularCvTemplate};
* {@link #defaults()} draws them the canonical way, and every method has a
* default, so a preset overrides only the shapes it actually styles
* differently.</p>
*
* <p><strong>Why the primitives and not the kinds.</strong> The obvious
* alternative is a function per {@link CvKind}. It puts the wrong work on
* the preset: turning a {@link CvItem} into an entry or a row means
* deciding what a linked title looks like, how a subtitle and a location
* join, which fields the kind ignores, what an empty description does to a
* trailing colon — rules that belong to the model and must not be
* re-decided sixteen times. {@link ModuleRenderer} keeps that lowering and
* asks the kit only to draw what came out of it, which is exactly the part
* a preset has an opinion about. It is also the shape the presets already
* have: their private renderers take a {@code CvEntry} or a {@code CvRow}
* today.</p>
* <p>This is not the template contract. A modular template implements
* {@link com.demcha.compose.document.templates.cv.api.CvConstructor} —
* one method per {@link CvKind}. The kit is the optional hook underneath
* a kind method that wants its own entry or row look without re-deciding
* which fields the kind reads. {@link #defaults()} is the canonical
* drawing; every method has a default, so a preset overrides only the
* primitives it actually styles differently.</p>
*
* <p>Implementations draw into the host and return; they do not set the
* host's spacing or padding, which the caller has already settled, and
Expand Down
Loading
Loading