feat: add forward-compatible enum and union runtime primitives#152
feat: add forward-compatible enum and union runtime primitives#152OmarAlJarrah wants to merge 1 commit into
Conversation
Closed enums and Kotlin sealed unions are a poor fit for evolving HTTP APIs: a new server-side variant crashes older clients on deserialization, and `sealed`/`permits` does not compile to Java-8 bytecode. Add two hand-writable serde primitives that a DTO author (or, later, a generator) can target while staying forward-compatible. OpenEnum is an open value type over a single wire string built around the two-enum pattern: a closed Known enum the build understands and a parallel Value enum with a lint-clean UNKNOWN sentinel. Deserialization never throws; value() is total, known() throws only when a caller demands a recognised variant, and the raw string is always retained so an unknown value round-trips back unchanged. OpenUnion is a private-constructor union with one nullable slot plus typed accessor per variant, visitor dispatch over an ordered arm list, and a retained raw node on every instance. accept() routes the active arm to its visitor case, falling back to Visitor.unknown (throws by default) for an unrecognised variant whose raw shape is still preserved. No sealed/permits, so it is Java-8 safe. Both live in sdk-core/serde with zero new dependencies.
|
Adds two forward-compatibility value types to the serde package: Issues
|
Closed enums and Kotlin sealed unions are a poor fit for evolving HTTP APIs: a new server-side variant crashes older clients on deserialization, and
sealed/permitsdoes not compile to Java-8 bytecode. This adds two hand-writable serde primitives that a DTO author can target today (and a generator could target later) while staying forward-compatible.OpenEnum<K, V>An open value type over a single wire string, built on the two-enum pattern:
Knownenum the build understands, and a parallelValueenum carrying a lint-cleanUNKNOWNsentinel (no underscore-prefixed marker).value()is total (returns theUNKNOWNsentinel for unrecognised input);knownOrNull()returns null;known()throws only when a call-site genuinely demands a recognised variant.rawValue, so an unknown value re-serializes unchanged.OpenUnion<VIS>A private-constructor union with forward-compat and Java-8 safety:
sealed/permits.accept(visitor)walks the subclass-supplied arm list in declaration order and routes the first populated arm to its visitor case, falling back toVisitor.unknown(rawValue)(throws by default, overridable) for an unrecognised variant.rawValue, format-agnostic) on every instance, including the unknown case, so an unrecognised shape survives a round-trip instead of being dropped.Both live in
sdk-core/serdewith zero new dependencies. Each ships with a hand-written concrete subclass in its test, matching how a generated type would extend the base.Closes #54
Closes #53
Tests
Both primitives have thorough unit tests including the required cases:
OpenEnumTest: unknown-value deserializes without throwing and round-trips its raw string;known()throws on unknown; totalvalue()classification; raw-value equality (including two unknowns comparing equal).OpenUnionTest: visitor dispatch per variant; unknown variant retains its raw node; defaultunknown()throws; a visitor overridingunknown()handles the forward-compat path.Gated build (scoped, run locally with
--no-daemon)