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

### Public API

- **`CvSkill` carries the level as the document words it.** A rated skill could say how
much — a number in `[0, 1]` a preset draws as dots or a meter — or it could say it in
words by not being a skill at all and living in a `RowsSection` instead. It could not
say both, which is why `CharcoalGold` sets its languages as rows: that design writes
"Native" and "B2 – Upper Intermediate" out, and a number could not carry them back. A
design that shows a rating *and* names it had nowhere to go. `CvSkill` now carries
`note` — a plain string, blank when absent, like the fields beside it — reachable
through `CvSkill.of(name, level, note)`. The two channels stay separate because a
number cannot carry a wording and a wording cannot be measured into a meter; a preset
draws whichever it has room for, or both. The two-argument constructor and both
existing factories are kept explicitly, so existing calls compile and link unchanged
and every skill built through them still carries no note.

- **The structured invoice model carries what a second sheet needs.** It landed with
one consumer, `ConsultingInvoice`, and a model shaped around one document is a model
nobody has tested. Fitting a second published invoice to it found six things it could
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.demcha.compose.document.templates.cv.data;

import org.junit.jupiter.api.Test;

import java.util.OptionalDouble;

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

/**
* Pins the written-out level on {@link CvSkill}: what it normalises, and that
* every way of building a skill that predates it still compiles and still
* produces a skill without one.
*/
class CvSkillNoteTest {

@Test
void aSkillCarriesTheLevelAsTheDocumentWordsIt() {
CvSkill skill = new CvSkill("Spanish", OptionalDouble.of(0.6),
"Professional Working");
assertThat(skill.note()).isEqualTo("Professional Working");
assertThat(skill.level()).hasValue(0.6);
}

@Test
void theFactoryTakesBothChannels() {
CvSkill skill = CvSkill.of("French", 0.4, "Basic");
assertThat(skill.note()).isEqualTo("Basic");
assertThat(skill.level()).hasValue(0.4);
}

@Test
void anAbsentNoteIsBlankRatherThanNull() {
assertThat(new CvSkill("Java", OptionalDouble.empty(), null).note()).isEmpty();
assertThat(new CvSkill("Java", OptionalDouble.empty(), " ").note()).isEmpty();
}

@Test
void aNoteIsTrimmed() {
assertThat(new CvSkill("Java", OptionalDouble.empty(), " Native ").note())
.isEqualTo("Native");
}

@Test
void theTwoArgumentConstructorStillBuildsASkillWithoutANote() {
CvSkill skill = new CvSkill("Java", OptionalDouble.of(0.9));
assertThat(skill.note()).isEmpty();
assertThat(skill.level()).hasValue(0.9);
}

@Test
void theNameOnlyAndLevelFactoriesStillBuildSkillsWithoutANote() {
assertThat(CvSkill.of("Java").note()).isEmpty();
assertThat(CvSkill.of("Java").level()).isEmpty();
assertThat(CvSkill.of("Java", 0.5).note()).isEmpty();
assertThat(CvSkill.of("Java", 0.5).level()).hasValue(0.5);
}

@Test
void aSkillGroupBuiltFromNamesCarriesNoNotes() {
SkillGroup group = SkillGroup.of("Languages", "Java", "Kotlin");
assertThat(group.entries()).allSatisfy(skill -> assertThat(skill.note()).isEmpty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@
* empty level, so name-only renderers are completely unaffected by the
* level channel.</p>
*
* <p>The note is the level written out — "Native", "B2 – Upper
* Intermediate", "3 years" — for the designs that show a rating and name it
* in the same line. It is a separate channel from {@link #level} because a
* number cannot carry a wording and a wording cannot be measured into a
* meter; a preset draws whichever of the two it has room for, or both.</p>
*
* @param name non-blank skill label
* @param level optional proficiency in {@code [0, 1]}; empty when
* unspecified
* @param note the level as the document words it; blank when unspecified
*/
public record CvSkill(String name, OptionalDouble level) {
public record CvSkill(String name, OptionalDouble level, String note) {

/**
* Validates that both fields are non-null, trims {@code name} and
Expand All @@ -29,6 +36,7 @@ public record CvSkill(String name, OptionalDouble level) {
public CvSkill {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(level, "level");
note = note == null ? "" : note.trim();
name = name.trim();
if (name.isBlank()) {
throw new IllegalArgumentException("name must not be blank");
Expand All @@ -42,14 +50,25 @@ public record CvSkill(String name, OptionalDouble level) {
}
}

/**
* Backward-compatible constructor for callers that predate the written-out
* level.
*
* @param name non-blank skill label
* @param level optional proficiency in {@code [0, 1]}
*/
public CvSkill(String name, OptionalDouble level) {
this(name, level, "");
}

/**
* Skill with no proficiency level.
*
* @param name non-blank skill label
* @return a {@code CvSkill} carrying an empty level
*/
public static CvSkill of(String name) {
return new CvSkill(name, OptionalDouble.empty());
return new CvSkill(name, OptionalDouble.empty(), "");
}

/**
Expand All @@ -61,6 +80,19 @@ public static CvSkill of(String name) {
*/
public static CvSkill of(String name, double level) {
return new CvSkill(name,
OptionalDouble.of(Math.max(0.0, Math.min(1.0, level))));
OptionalDouble.of(Math.max(0.0, Math.min(1.0, level))), "");
}

/**
* Skill with a proficiency level and the wording that names it.
*
* @param name non-blank skill label
* @param level proficiency, clamped into {@code [0, 1]}
* @param note the level as the document words it
* @return a {@code CvSkill} carrying both channels
*/
public static CvSkill of(String name, double level, String note) {
return new CvSkill(name,
OptionalDouble.of(Math.max(0.0, Math.min(1.0, level))), note);
}
}
Loading