kdiff 0.7.0
Two things a stranger meets before the first line of Kotlin: the coordinates, and how much annotating
a domain model costs. This release settles both — kdiff resolves from Maven Central with no repository
block and no token, with sources and KDoc reaching the IDE, and the value types a domain model is full
of now compare by equality with nothing declared on them.
It also writes down what 1.0.0 will promise, in docs/api-stability.md,
before the tag rather than after it.
Added
-
The types a domain model is full of are values now, with no annotation.
BigDecimal,
BigInteger,UUID,Currency,Locale,URI, everyjava.timevalue type (Instant,
LocalDate,LocalTime,LocalDateTime,OffsetDateTime,OffsetTime,ZonedDateTime,
Duration,Period,Year,YearMonth,MonthDay,ZoneId,ZoneOffset),kotlin.time.Instant
andkotlin.uuid.Uuidare compared by equality wherever they appear — as a property, as a list
element, as a map value. The criterion is written down so the list can be read rather than
remembered: immutable, equal by value, from the JDK or the Kotlin standard library.java.util.Date
is deliberately not on it.Each of these previously cost an
objectimplementingDifferplus a@DiffWithon every property
holding one, to express the one comparison kdiff already knew how to do. If you wrote such a differ,
you may delete it and the annotation; nothing forces you to.One caveat in writing:
BigDecimal'sequalsis scale-sensitive, so10and10.00report a
change. A numeric comparison is still a@DiffWithdiffer's business. -
An inline
value classis a value. A class declaredvalue classis compared by equality
wherever it appears, with no annotation on it or on the property.kotlin.time.Durationis one, so
it needs no place on the list above. -
@DiffAsValuedeclares that a type, or one property, compares as a single value. On a class,
every property, list element and map value of that type is compared by equality, in every
@Diffableclass that reaches it — the per-type declaration the FAQ used to say did not exist. On a
property, that property alone is compared as one value whatever its type:@DiffAsValue val billing: Addressreports one change atbilling, and@DiffAsValue val tags: List<String>one change at
tags. It is the annotation counterpart of thediffer { }builder'sfield, so the annotated and
hand-written routes can once again describe the same model.A
@DiffAsValuethat could change nothing is a compile error — on an enum, avalue class, or a
property whose type is already a value — and so is one that contradicts another declaration: beside
@Diffableon a class, or beside@DiffWithor@DiffIgnoreon a property. -
The "cannot compare" diagnostic now names three ways out instead of two:
@Diffableon the type,
@DiffAsValueon the property, or@DiffWith. -
DifferandPatcherare functional interfaces. A one-off differ can be written where it is
used —Differ<Money> { before, after -> … }— and so can a patcher. Source- and binary-compatible;
the recorded API does not move. The descent-bound caveat anobject : Differ<T>already carried
applies to the lambda form too, and is now stated for both. -
Segment.Key.MAP_ENTRYis the declared name a map entry's key segment carries, replacing the
"key"literal in the runtime. Its value is unchanged, so every renderedamounts[key=eur]and
every assertion on one reads as before — but a hand-written differ or patcher building a map path can
now name the constant, and a reader of that path can find wherekeycomes from. -
docs/api-stability.mdstates what1.0.0will promise: the recorded
public API of the three published modules, the two closed vocabularies and the three declared
exception types, which types are data classes and by what criterion, what generated code guarantees
and what in a generated file is not API, what is explicitly not API at all, and the JVM platform. It
also records the questions answered "no" — a typedDiff<T>,TypeChangedcarrying classes rather
than names, a common exception supertype — each with its reason.
Changed
-
BREAKING — kdiff is published to Maven Central, and the group id changes. A consumer needs
mavenCentral(), which every Gradle build already declares, and nothing else: no repository block, no
personal access token, no401. GitHub Packages is no longer published to.The group id is now
io.github.rcapraro— a namespace Central can verify against the maintainer's
GitHub account, whichio.github.kdiffis not. Package names do not change, so no import moves:// before — GitHub Packages, with a repository block and a token implementation("io.github.kdiff:kdiff-annotations:0.6.0") implementation("io.github.kdiff:kdiff-runtime:0.6.0") ksp("io.github.kdiff:kdiff-processor:0.6.0") // after — Maven Central, nothing else to declare implementation("io.github.rcapraro:kdiff-annotations:0.7.0") implementation("io.github.rcapraro:kdiff-runtime:0.7.0") ksp("io.github.rcapraro:kdiff-processor:0.7.0")
Migration: change the group id on the three coordinates and delete the
maven.pkg.github.com/rcapraro/kdiffrepository block, along with thegpr.user/gpr.key
properties it read. Imports stayio.github.kdiff.*. -
Every published module now ships a sources jar and a documentation jar, both signed, as Central
requires. Go to declaration onDiffer,Changeor any runtime helper lands on the Kotlin source
with its KDoc instead of decompiled bytecode — the KDoc inkdiff-runtimecarries the why of every
helper, and until now it was only readable by cloning the repository. -
BREAKING —
Patched<T>is removed; the patch helpers returnPatchResult<T>.patchValue,
patchNested,patchNestedNullable,patchNullable,patchKeyedList,patchPositionalList,
patchSet,patchMap,unpatchableandnotConstructorPropertyall return the type a
Patcher.applyreturns, so a helper's result carriesisCleanandgetOrThrow()too. The two types
had identical shape, andpatchNestedexisted partly to convert one into the other.Migration: rename
PatchedtoPatchResult; the members are the same. A hand-written patcher that
only reads.valueand.failures— which is every example in the docs — compiles unchanged, and so
does generated code, which reads the same two members. -
BREAKING —
Diffis no longer a data class. It keeps its constructor, itschanges, its
equality over those changes and every member it had; it losescopyandcomponent1, which would
have fixedDiff's shape forever. ItstoString()is now the textrender()produces, so a diff
printed in a log line or an assertion failure reads as a diff — over several lines when it holds
several changes.Migration: build a new
Diff(changes)instead ofdiff.copy(changes = …), and readdiff.changes
instead of destructuring. A caller that wants a diff on one line haschanges.toString()orsize.
Fixed
-
BREAKING — a comparison annotation on a property a
@Diffablesealed parent declares is now
honoured by a subclass that overrides it. Kotlin puts none of an overridden declaration's annotations
on theoverride, so@DiffAsValue,@DiffIgnoreand@DiffWithon a sealed parent's property held
across a subclass swap — where the parent's own properties are compared — and were silently ignored
for two instances of one subclass, which is the common case. If your model looks like this, its diffs
change:@Diffable sealed interface Shipment { @DiffAsValue val origin: Address } @Diffable data class Parcel(override val origin: Address, val tracking: String) : Shipment
Two
Parcels differing insideoriginnow report one change atorigin; they reported changes at
origin.cityand the like before.@DiffIgnorethere now excludes the property where it did not.Migration: nothing to edit — the new behaviour is what the annotated source always asked for. But a
test or a consumer asserting on the old paths sees them move, so check any assertion naming a path
beneath such a property. To keep the old behaviour, remove the annotation from the parent; to have it
apply to one subclass only, annotate that subclass's override, which wins over the parent's. -
A generated differ is now regenerated when a
value classorenum classit compared as a value is
edited. Only@DiffAsValuerecorded the declaring file before, so an incremental build could keep a
differ that a clean build refuses: redeclaring@JvmInline value class Sku(val code: String)as an
ordinary type left the generatedcompareValue("sku", …)in place and compiling, while a clean build
failed with kdiff cannot compare sku of type demo.Sku. Builds now agree; the cost is that editing
such a type regenerates the differs that read it.
Nothing else that compiled before means anything different, and the generated code for an existing
class is unchanged unless it is a subclass overriding an annotated property. kdiff-annotations gains
one annotation; kdiff-runtime's recorded API moves exactly where the two breaks above say it does,
and kdiff-processor adds no public declaration.
Coordinates
implementation("io.github.rcapraro:kdiff-annotations:0.7.0")
implementation("io.github.rcapraro:kdiff-runtime:0.7.0")
ksp("io.github.rcapraro:kdiff-processor:0.7.0")About this release run
This was the first release published to Maven Central, and it needed one manual step. The tagged run
failed at the upload with Invalid token — the Central Portal user token was rejected — and published
nothing. The token secrets were replaced and the same tag re-run through workflow_dispatch, which
validated and released deployment ec2c3f07-a016-43c8-a70f-88a49465d72e. No version was published
twice, and the tag and its commit are unchanged.