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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ keeps going where MapStruct structurally stops — deep navigation, effectful up
merge, JPA-cycle and Hibernate-`LAZY` handling, all from one `Telescope<S, A>` type. Same speed, compile safety
MapStruct can't give you, and a strictly larger surface. [See it row by row →](#how-it-compares-to-mapstruct)

[![JVM 17+](https://img.shields.io/badge/JVM-17%2B-brightgreen.svg?&logo=openjdk)](https://openjdk.org/projects/jdk/17/)
[![JVM 21+](https://img.shields.io/badge/JVM-21%2B-brightgreen.svg?&logo=openjdk)](https://openjdk.org/projects/jdk/21/)
[![Build](https://github.com/eschizoid/telescope/actions/workflows/ci.yaml/badge.svg)](https://github.com/eschizoid/telescope/actions/workflows/ci.yaml)
[![Codecov](https://codecov.io/gh/eschizoid/telescope/graph/badge.svg?token=a235ea8b-e6dc-45c6-8fea-e5050940c5d4)](https://codecov.io/gh/eschizoid/telescope)
[![Maven Central](https://img.shields.io/maven-central/v/io.github.eschizoid/telescope-core.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/io.github.eschizoid/telescope-core)
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ dependencies {
}

tasks.withType<JavaCompile>().configureEach {
options.release = 17
options.release = 21
options.encoding = "UTF-8"
modularity.inferModulePath = true
}
Expand Down
2 changes: 1 addition & 1 deletion codegen/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ java {
}

tasks.withType<JavaCompile>().configureEach {
options.release = 17
options.release = 21
options.encoding = "UTF-8"
options.compilerArgs.addAll(listOf("-Xlint:all,-processing", "-parameters"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -924,7 +924,7 @@ private void emitBeanConstantsMap(final PrintWriter out, final List<Prop> props)
if (props.isEmpty()) {
out.println(" return Map.of();");
} else if (props.size() == 1) {
final var onlyName = props.get(0).name();
final var onlyName = props.getFirst().name();
out.println(" return Map.of(\"" + onlyName + "\", " + onlyName + ");");
} else {
out.println(" return Map.ofEntries(");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* setters — see {@link AbstractTelescopeProcessor#emitBeanNavigator}.
*/
@SupportedAnnotationTypes("io.github.eschizoid.telescope.annotations.BeanFocus")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public final class BeanFocusProcessor extends AbstractTelescopeProcessor {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
@SupportedAnnotationTypes(
{ "io.github.eschizoid.telescope.annotations.Bridge", "io.github.eschizoid.telescope.annotations.Bridges" }
)
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public final class BridgeProcessor extends AbstractTelescopeProcessor {

/**
Expand Down Expand Up @@ -1788,9 +1788,9 @@ private void generate(

// Sealed-source bridge: dispatch on the permits clause and delegate each case to the per-case
// bridge that the user already declared with @Bridge on the subtype. The emitted forward/backward
// are pattern-match switches over the sealed permits; no field walking, no rebuild — just one
// dispatch arm per case. Requires every permit case to be @Bridge-annotated and its target to be
// a permit of the sealed target.
// dispatch through the Match.of(...).when(...).exhaustive() fluent over the sealed permits; no
// field walking, no rebuild — just one dispatch arm per case. Requires every permit case to be
// @Bridge-annotated and its target to be a permit of the sealed target.
private void generateSealed(
final TypeElement source,
final TypeElement target,
Expand Down Expand Up @@ -1850,7 +1850,7 @@ record CaseEntry(TypeElement sourceCase, TypeElement targetCase, String bridgeFq
);
} else if (caseBridges.size() == 1) {
// Single @Bridge whose target isn't in the sealed-target permits — name it explicitly.
final var only = rawTargetValueFromMirror(caseBridges.get(0));
final var only = rawTargetValueFromMirror(caseBridges.getFirst());
if (!(only instanceof DeclaredType decl)) continue;
final var onlyEl = (TypeElement) decl.asElement();
error(
Expand Down Expand Up @@ -2170,13 +2170,13 @@ private ContainerShape containerShapeOf(final TypeMirror type) {
if (!(type instanceof DeclaredType dt)) return null;
final var args = dt.getTypeArguments();
if (assignableToRaw(type, "java.util.Optional")) {
return args.size() == 1 ? new ContainerShape(FieldPlan.Kind.OPTIONAL, args.get(0), null) : null;
return args.size() == 1 ? new ContainerShape(FieldPlan.Kind.OPTIONAL, args.getFirst(), null) : null;
}
if (args.size() == 1 && assignableToRaw(type, "java.util.List")) {
return new ContainerShape(FieldPlan.Kind.LIST, args.get(0), null);
return new ContainerShape(FieldPlan.Kind.LIST, args.getFirst(), null);
}
if (args.size() == 1 && assignableToRaw(type, "java.util.Set")) {
return new ContainerShape(FieldPlan.Kind.SET, args.get(0), null);
return new ContainerShape(FieldPlan.Kind.SET, args.getFirst(), null);
}
if (args.size() == 2 && assignableToRaw(type, "java.util.Map")) {
return new ContainerShape(FieldPlan.Kind.MAP_VALUES, args.get(1), args.get(0));
Expand All @@ -2194,11 +2194,11 @@ private ContainerShape rawContainerShapeOf(final TypeMirror type) {
if (!(type instanceof DeclaredType dt) || !dt.getTypeArguments().isEmpty()) return null;
if (assignableToRaw(type, "java.util.List")) {
final var args = containerViewArgs(type, "java.util.List");
return args.size() == 1 ? new ContainerShape(FieldPlan.Kind.LIST, args.get(0), null) : null;
return args.size() == 1 ? new ContainerShape(FieldPlan.Kind.LIST, args.getFirst(), null) : null;
}
if (assignableToRaw(type, "java.util.Set")) {
final var args = containerViewArgs(type, "java.util.Set");
return args.size() == 1 ? new ContainerShape(FieldPlan.Kind.SET, args.get(0), null) : null;
return args.size() == 1 ? new ContainerShape(FieldPlan.Kind.SET, args.getFirst(), null) : null;
}
if (assignableToRaw(type, "java.util.Map")) {
final var args = containerViewArgs(type, "java.util.Map");
Expand Down Expand Up @@ -2930,7 +2930,7 @@ private String rawAllocExpr(final TypeMirror container, final FieldPlan.Kind kin
return "new " + implFqn + "<" + args.get(0) + ", " + args.get(1) + ">()";
}
final var args = containerViewArgs(container, kind == FieldPlan.Kind.SET ? "java.util.Set" : "java.util.List");
return "new " + implFqn + "<" + args.get(0) + ">()";
return "new " + implFqn + "<" + args.getFirst() + ">()";
}

private void emitListHelper(
Expand All @@ -2941,8 +2941,8 @@ private void emitListHelper(
final String subBridge,
final String direction
) {
final var srcElement = ((DeclaredType) srcContainer).getTypeArguments().get(0);
final var tgtElement = ((DeclaredType) tgtContainer).getTypeArguments().get(0);
final var srcElement = ((DeclaredType) srcContainer).getTypeArguments().getFirst();
final var tgtElement = ((DeclaredType) tgtContainer).getTypeArguments().getFirst();
final var returnRaw = simpleName(containerRawFqn(tgtContainer));
final var paramRaw = simpleName(containerRawFqn(srcContainer));
final var implFqn = concreteImplFqn(tgtContainer, FieldPlan.Kind.LIST);
Expand Down Expand Up @@ -2977,8 +2977,8 @@ private void emitSetHelper(
final String subBridge,
final String direction
) {
final var srcElement = ((DeclaredType) srcContainer).getTypeArguments().get(0);
final var tgtElement = ((DeclaredType) tgtContainer).getTypeArguments().get(0);
final var srcElement = ((DeclaredType) srcContainer).getTypeArguments().getFirst();
final var tgtElement = ((DeclaredType) tgtContainer).getTypeArguments().getFirst();
final var returnRaw = simpleName(containerRawFqn(tgtContainer));
final var paramRaw = simpleName(containerRawFqn(srcContainer));
final var implFqn = concreteImplFqn(tgtContainer, FieldPlan.Kind.SET);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
* </ul>
*/
@SupportedAnnotationTypes("io.github.eschizoid.telescope.annotations.Focus")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public final class FocusProcessor extends AbstractTelescopeProcessor {

/**
Expand Down Expand Up @@ -231,7 +231,7 @@ private void emitRecordConstantsMap(final PrintWriter out, final List<? extends
if (components.isEmpty()) {
out.println(" return Map.of();");
} else if (components.size() == 1) {
final var onlyName = components.get(0).getSimpleName();
final var onlyName = components.getFirst().getSimpleName();
out.println(" return Map.of(\"" + onlyName + "\", " + onlyName + ");");
} else {
out.println(" return Map.ofEntries(");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* generated code is GraalVM native-image clean.
*/
@SupportedAnnotationTypes("io.github.eschizoid.telescope.annotations.FromMap")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public final class FromMapProcessor extends AbstractTelescopeProcessor {

private static final String ANNOTATION = "io.github.eschizoid.telescope.annotations.FromMap";
Expand Down Expand Up @@ -375,7 +375,7 @@ private Coercion.RenderedType renderType(final TypeMirror type) {
private TypeMirror singleArgOf(final DeclaredType type, final String rawFqn) {
if (!isErasure(type, rawFqn)) return null;
final var args = type.getTypeArguments();
return args.size() == 1 ? args.get(0) : null;
return args.size() == 1 ? args.getFirst() : null;
}

/** Whether {@code type}'s erasure is exactly the raw type named {@code rawFqn}. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ private void verifyWriteHint(
final Set<String> hintTargets
) {
if (!"writeBean".contentEquals(rowExec.getSimpleName())) return;
final var target = row.getArguments().isEmpty() ? null : classLiteral(row.getArguments().get(0));
final var target = row.getArguments().isEmpty() ? null : classLiteral(row.getArguments().getFirst());
final var targetEl = target == null ? null : props.elementOf(target);
if (targetEl == null) {
note(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1440,7 +1440,9 @@ public record Src(String id) {}
class SealedRoots {

@Test
@DisplayName("sealed interface ↔ sealed interface emits a switch over permits that delegates to per-case bridges")
@DisplayName(
"sealed interface ↔ sealed interface emits an exhaustive Match over permits that delegates to per-case bridges"
)
void sealedToSealed() {
final var compilation = compile(
source(
Expand Down
2 changes: 1 addition & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ java {
}

tasks.withType<JavaCompile>().configureEach {
options.release = 17
options.release = 21
options.encoding = "UTF-8"
options.compilerArgs.addAll(listOf("-Xlint:all,-processing", "-Werror", "-parameters"))
}
Expand Down
101 changes: 52 additions & 49 deletions core/src/main/java/io/github/eschizoid/telescope/DeepMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,11 @@ private static NullHint.NullStrategy extractNullStrategy(final List<NullHint> hi
private static WriteHint.WriteStrategy extractDefaultStrategy(final List<WriteHint<?>> hints) {
WriteHint.WriteStrategy defaultStrategy = null;
for (final var hint : hints) {
if (!(hint instanceof WriteHint.DefaultWriteHint defaultHint)) continue;
if (!(hint instanceof WriteHint.DefaultWriteHint(final WriteHint.WriteStrategy strat))) continue;
Comment thread
eschizoid marked this conversation as resolved.
if (defaultStrategy != null) throw new IllegalArgumentException(
"Duplicate writeBeans(...) default — at most one default write strategy per Telescope.map(...) call."
);
defaultStrategy = defaultHint.strategy();
defaultStrategy = strat;
}
return defaultStrategy;
}
Expand Down Expand Up @@ -1161,53 +1161,56 @@ private static <S, T> T overrideTargetField(
// Inline the contributed leaf-level Iso for each row variant. Reading the public components
// directly keeps Iso (internal) out of the mapping types' public signatures — so the mapping
// types stay portable across packages without needing @SuppressWarnings("exports").
// SameTypedTo carries no conversion; populateIso resolves it through autoIso (identity /
// primitive-wrapper / container lifting / incompatible-shape rejection), so it must not reach
// here. The guard surfaces a routing regression with a clear class name, like the rows below.
if (row instanceof SameTypedTo<?, ?, ?>) throw new IllegalStateException(
"SameTypedTo row should be resolved via autoIso, not fieldIsoOf"
);
if (row instanceof TypedTransformTo<?, ?, ?, ?> r) return Iso.of((Function) r.forward(), (Function) r.backward());
if (row instanceof ForwardOnlyTransformTo<?, ?, ?, ?> r) {
// DEAD-BRANCH-DEFENSIVE: this throwingBackward lambda is unreachable via the public API.
// Both factory entries Telescope.map(...) and Telescope.mapper(...) call
// rejectForwardOnlyRows
// up front; Telescope.mapperForward(...) accepts the row but never invokes the backward leg.
// The guard remains so a future cross-package construction path that bypasses
// rejectForwardOnlyRows produces a precise field-naming error rather than silent corruption.
// NOT a coverage target.
final String fieldName = r.sourceField();
final Function<Object, Object> throwingBackward = y -> {
throw new UnsupportedOperationException(
"Mapping.toOneWay is forward-only — backward direction is undefined for field '" +
fieldName +
"'. Use Telescope.mapperForward(...) for a forward-only mapper, or Mapping.to(src, " +
"tgt, forward, backward) for an explicit bidirectional row."
);
};
return Iso.of((Function) r.forward(), throwingBackward);
}
if (row instanceof Via<?, ?> r) return liftViaIfNeeded(r, srcType, tgtType);
// DEAD-BRANCH-DEFENSIVE block (rows below): every permit of the sealed Mapping hierarchy that
// is NOT a per-field leaf Iso is filtered out by populateIso BEFORE this method is called. The
// checks remain solely as a compile-time exhaustiveness backstop — if a future permit is added
// to Mapping and populateIso forgets to short-circuit, the corresponding throw here surfaces
// the routing bug with a clear class name. NOT coverage targets — these can only fire when a
// routing change in populateIso introduces a regression, at which point the test failure is in
// populateIso, not here.
if (row instanceof Drop<?, ?, ?>) throw new IllegalStateException("Drop row should not reach fieldIsoOf");
if (row instanceof TelescopeTo<?, ?, ?>) throw new IllegalStateException(
"TelescopeTo row should not reach fieldIsoOf"
);
if (row instanceof FromTelescopeTo<?, ?, ?>) throw new IllegalStateException(
"FromTelescopeTo row should not reach fieldIsoOf"
);
if (row instanceof TelescopeToTelescope<?, ?, ?>) throw new IllegalStateException(
"TelescopeToTelescope row should not reach fieldIsoOf"
);
if (row instanceof Constant<?, ?, ?>) throw new IllegalStateException("Constant row should not reach fieldIsoOf");
if (row instanceof Compute<?, ?, ?>) throw new IllegalStateException("Compute row should not reach fieldIsoOf");
throw new IllegalStateException("unreachable: Mapping is sealed");
return switch (row) {
// SameTypedTo carries no conversion; populateIso resolves it through autoIso (identity /
// primitive-wrapper / container lifting / incompatible-shape rejection), so it must not reach
// here. The guard surfaces a routing regression with a clear class name, like the cases
// below.
case SameTypedTo<?, ?, ?> __ -> throw new IllegalStateException(
"SameTypedTo row should be resolved via autoIso, not fieldIsoOf"
);
case TypedTransformTo<?, ?, ?, ?> r -> Iso.of((Function) r.forward(), (Function) r.backward());
case ForwardOnlyTransformTo<?, ?, ?, ?> r -> {
// DEAD-BRANCH-DEFENSIVE: this throwingBackward lambda is unreachable via the public API.
// Both factory entries Telescope.map(...) and Telescope.mapper(...) call
// rejectForwardOnlyRows
// up front; Telescope.mapperForward(...) accepts the row but never invokes the backward
// leg. The guard remains so a future cross-package construction path that bypasses
// rejectForwardOnlyRows produces a precise field-naming error rather than silent
// corruption. NOT a coverage target.
final String fieldName = r.sourceField();
final Function<Object, Object> throwingBackward = y -> {
throw new UnsupportedOperationException(
"Mapping.toOneWay is forward-only — backward direction is undefined for field '" +
fieldName +
"'. Use Telescope.mapperForward(...) for a forward-only mapper, or Mapping.to(src, " +
"tgt, forward, backward) for an explicit bidirectional row."
);
};
yield Iso.of((Function) r.forward(), throwingBackward);
}
case Via<?, ?> r -> liftViaIfNeeded(r, srcType, tgtType);
// DEAD-BRANCH-DEFENSIVE block (cases below): every permit of the sealed Mapping hierarchy
// that is NOT a per-field leaf Iso is filtered out by populateIso BEFORE this method is
// called. The cases exist only because the sealed switch must stay exhaustive — if a future
// permit is added to Mapping and populateIso forgets to short-circuit, the compiler forces a
// case here whose throw surfaces the routing bug with a clear class name. NOT coverage
// targets — these can only fire when a routing change in populateIso introduces a
// regression, at which point the test failure is in populateIso, not here.
case Drop<?, ?, ?> __ -> throw new IllegalStateException("Drop row should not reach fieldIsoOf");
case TelescopeTo<?, ?, ?> __ -> throw new IllegalStateException("TelescopeTo row should not reach fieldIsoOf");
case FromTelescopeTo<?, ?, ?> __ -> throw new IllegalStateException(
"FromTelescopeTo row should not reach fieldIsoOf"
);
case TelescopeToTelescope<?, ?, ?> __ -> throw new IllegalStateException(
"TelescopeToTelescope row should not reach fieldIsoOf"
);
case Constant<?, ?, ?> __ -> throw new IllegalStateException("Constant row should not reach fieldIsoOf");
case Compute<?, ?, ?> __ -> throw new IllegalStateException("Compute row should not reach fieldIsoOf");
case Conditional<?, ?> __ -> throw new IllegalStateException(
"Conditional row should be unwrapped by populateIso before fieldIsoOf"
);
};
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1980,12 +1980,9 @@ public Optional<S> updateOptional(final S source, final Function<? super A, ? ex
* literal upper bound on in-flight operations.
*
* <pre>{@code
* final var pool = Executors.newFixedThreadPool(10);
* try {
* try (final var pool = Executors.newFixedThreadPool(10)) {
* final CompletableFuture<Batch> done = path.updateAsync(batch, this::fetch, pool);
* done.join();
* } finally {
* pool.shutdown();
* }
* }</pre>
*/
Expand Down Expand Up @@ -2307,7 +2304,7 @@ public Telescope<S, X> present() {
// fn.backward in one virtual hop. The Iso is still the underlying optic — composition (.then,
// .field, .each, .as, etc.) returns a regular Telescope and keeps lattice semantics intact; the
// fast path applies only when a terminal is invoked directly on the bridge constant.
static final class BridgeTelescope<S, T> extends Telescope<S, T> {
private static final class BridgeTelescope<S, T> extends Telescope<S, T> {

private final BridgeFn<S, T> fn;

Expand Down
Loading
Loading