diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..75b2dd8 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,15 @@ +RELEASE_TYPE: patch + +This patch adds `Generators.optionalInts`, `Generators.optionalLongs` and `Generators.optionalDoubles`, +which generate the primitive optionals `OptionalInt`, `OptionalLong` and `OptionalDouble` the same way +`Generators.optional` generates `Optional` ([#27](https://github.com/hegeldev/hegel-java/issues/27)). +Each one generates either an empty optional or a value from the generator you pass in, and shrinks +toward empty. + +```java +Generator max = Generators.optionalLongs(Generators.longs().min(0).max(100)); +``` + +`Generators.forType` and `Generators.records` now also derive generators for these types, so a record +like `record Stats(long count, OptionalLong max, OptionalDouble mean)` no longer fails with +`No default generator for java.util.OptionalLong`. diff --git a/shared/src/main/java/dev/hegel/Generators.java b/shared/src/main/java/dev/hegel/Generators.java index 11ebdb8..876dcdf 100644 --- a/shared/src/main/java/dev/hegel/Generators.java +++ b/shared/src/main/java/dev/hegel/Generators.java @@ -38,6 +38,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.UUID; import java.util.function.Function; @@ -198,6 +201,36 @@ public static Generator> optional(Generator generator) { return oneOf(just(Optional.empty()), generator.map(Optional::of)); } + /** + * Generates an {@link OptionalInt} that is empty or wraps a value from {@code generator}. + * + * @param generator the value generator + * @return an optional-int generator + */ + public static Generator optionalInts(Generator generator) { + return oneOf(just(OptionalInt.empty()), generator.map(OptionalInt::of)); + } + + /** + * Generates an {@link OptionalLong} that is empty or wraps a value from {@code generator}. + * + * @param generator the value generator + * @return an optional-long generator + */ + public static Generator optionalLongs(Generator generator) { + return oneOf(just(OptionalLong.empty()), generator.map(OptionalLong::of)); + } + + /** + * Generates an {@link OptionalDouble} that is empty or wraps a value from {@code generator}. + * + * @param generator the value generator + * @return an optional-double generator + */ + public static Generator optionalDoubles(Generator generator) { + return oneOf(just(OptionalDouble.empty()), generator.map(OptionalDouble::of)); + } + // --- collections --- /** @@ -465,9 +498,9 @@ public static Deferred deferred() { * Derive a generator for {@code type} by reflection. * *

Supports scalar types ({@code int}, {@code long}, {@code boolean}, {@code float}, {@code - * double}, {@code String}, {@code byte[]}, {@link UUID}, and their wrappers), enums, records - * (recursively), and {@code List}, {@code Set}, {@code Optional} and {@code Map} of supported - * element types. + * double}, {@code String}, {@code byte[]}, {@link UUID}, and their wrappers), {@link + * OptionalInt}, {@link OptionalLong}, {@link OptionalDouble}, enums, records (recursively), and + * {@code List}, {@code Set}, {@code Optional} and {@code Map} of supported element types. * * @param type the type to derive a generator for * @param the type diff --git a/shared/src/main/java/dev/hegel/generators/Derive.java b/shared/src/main/java/dev/hegel/generators/Derive.java index c4efcb0..6d73ae4 100644 --- a/shared/src/main/java/dev/hegel/generators/Derive.java +++ b/shared/src/main/java/dev/hegel/generators/Derive.java @@ -9,9 +9,10 @@ /** * Reflection-based generator derivation. Maps a Java type to a default generator: scalar wrappers - * to their primitive generators, enums to a choice over their constants, records to a generator - * over their components, and {@code List}/{@code Set}/{@code Optional}/{@code Map} to the matching - * collection generators (using the declared element types). + * to their primitive generators, {@code OptionalInt}/{@code OptionalLong}/{@code OptionalDouble} to + * optionals over them, enums to a choice over their constants, records to a generator over their + * components, and {@code List}/{@code Set}/{@code Optional}/{@code Map} to the matching collection + * generators (using the declared element types). */ public final class Derive { private Derive() {} @@ -64,6 +65,15 @@ private static Generator scalar(Class cls) { if (cls == double.class || cls == Double.class) { return Generators.doubles(); } + if (cls == java.util.OptionalInt.class) { + return Generators.optionalInts(Generators.integers()); + } + if (cls == java.util.OptionalLong.class) { + return Generators.optionalLongs(Generators.longs()); + } + if (cls == java.util.OptionalDouble.class) { + return Generators.optionalDoubles(Generators.doubles()); + } if (cls == String.class) { return Generators.text(); } diff --git a/shared/src/test/java/dev/hegel/ConformanceTest.java b/shared/src/test/java/dev/hegel/ConformanceTest.java index 97ab607..a093ea1 100644 --- a/shared/src/test/java/dev/hegel/ConformanceTest.java +++ b/shared/src/test/java/dev/hegel/ConformanceTest.java @@ -21,6 +21,9 @@ import static dev.hegel.Generators.maps; import static dev.hegel.Generators.oneOf; import static dev.hegel.Generators.optional; +import static dev.hegel.Generators.optionalDoubles; +import static dev.hegel.Generators.optionalInts; +import static dev.hegel.Generators.optionalLongs; import static dev.hegel.Generators.sampledFrom; import static dev.hegel.Generators.sets; import static dev.hegel.Generators.text; @@ -43,6 +46,9 @@ import java.time.ZoneOffset; import java.util.List; import java.util.Objects; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.regex.Pattern; import org.junit.jupiter.api.Test; @@ -170,6 +176,42 @@ void optionalBasicAndNonBasic() { assertAllExamples(optional(integers().min(0).max(9).filter(x -> x > 4)), o -> o.isEmpty() || o.get() > 4); } + // Each primitive optional stays within its inner generator's bounds, shrinks to empty (the + // simplest value), and when present shrinks to the inner generator's simplest value. + + @Test + void optionalIntsBoundsAndShrinking() { + assertAllExamples( + optionalInts(integers().min(0).max(3)), o -> o.isEmpty() || (o.getAsInt() >= 0 && o.getAsInt() <= 3)); + assertTrue(minimal(optionalInts(integers()), o -> true).isEmpty()); + assertEquals( + 5, + minimal(optionalInts(integers().min(5)), OptionalInt::isPresent).getAsInt()); + } + + @Test + void optionalLongsBoundsAndShrinking() { + assertAllExamples( + optionalLongs(longs().min(10L).max(20L)), + o -> o.isEmpty() || (o.getAsLong() >= 10L && o.getAsLong() <= 20L)); + assertTrue(minimal(optionalLongs(longs()), o -> true).isEmpty()); + assertEquals( + 5L, + minimal(optionalLongs(longs().min(5L)), OptionalLong::isPresent).getAsLong()); + } + + @Test + void optionalDoublesBoundsAndShrinking() { + assertAllExamples( + optionalDoubles(doubles().min(0.0).max(1.0)), + o -> o.isEmpty() || (o.getAsDouble() >= 0.0 && o.getAsDouble() <= 1.0)); + assertTrue(minimal(optionalDoubles(doubles()), o -> true).isEmpty()); + assertEquals( + 5.0, + minimal(optionalDoubles(doubles().min(5.0)), OptionalDouble::isPresent) + .getAsDouble()); + } + @Test void combinators() { assertAllExamples(integers().min(0).max(50).map(x -> x * 2), x -> x % 2 == 0); diff --git a/shared/src/test/java/dev/hegel/DerivationTest.java b/shared/src/test/java/dev/hegel/DerivationTest.java index cf1e3ca..f63edf2 100644 --- a/shared/src/test/java/dev/hegel/DerivationTest.java +++ b/shared/src/test/java/dev/hegel/DerivationTest.java @@ -13,6 +13,9 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.Set; import java.util.UUID; import org.junit.jupiter.api.Test; @@ -98,6 +101,13 @@ void derivesWrapperScalarsAndBytes() { w -> w.l() != null && w.b() != null && w.d() != null && w.data() != null && w.i() != null); } + record Stats(long count, OptionalInt min, OptionalLong max, OptionalDouble mean) {} + + @Test + void derivesPrimitiveOptionals() { + assertAllExamples(forType(Stats.class), s -> s.min() != null && s.max() != null && s.mean() != null); + } + @Test void derivesUuidScalars() { assertAllExamples(forType(UUID.class), Objects::nonNull); diff --git a/shared/src/test/java/dev/hegel/GeneratorSmokeTest.java b/shared/src/test/java/dev/hegel/GeneratorSmokeTest.java index 49bcc5a..233d631 100644 --- a/shared/src/test/java/dev/hegel/GeneratorSmokeTest.java +++ b/shared/src/test/java/dev/hegel/GeneratorSmokeTest.java @@ -12,9 +12,13 @@ import static dev.hegel.Generators.fromRegex; import static dev.hegel.Generators.integers; import static dev.hegel.Generators.ipAddresses; +import static dev.hegel.Generators.longs; import static dev.hegel.Generators.maps; import static dev.hegel.Generators.oneOf; import static dev.hegel.Generators.optional; +import static dev.hegel.Generators.optionalDoubles; +import static dev.hegel.Generators.optionalInts; +import static dev.hegel.Generators.optionalLongs; import static dev.hegel.Generators.sampledFrom; import static dev.hegel.Generators.sets; import static dev.hegel.Generators.text; @@ -26,6 +30,9 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; import java.util.Set; class GeneratorSmokeTest { @@ -78,6 +85,12 @@ void tuplesAndOneOfAndOptional(TestCase tc) { assertTrue(iv <= 1 || iv >= 100); Optional o = tc.draw(optional(integers().min(0).max(3))); o.ifPresent(x -> assertTrue(x >= 0 && x <= 3)); + OptionalInt oi = tc.draw(optionalInts(integers().min(0).max(3))); + oi.ifPresent(x -> assertTrue(x >= 0 && x <= 3)); + OptionalLong ol = tc.draw(optionalLongs(longs().min(0L).max(3L))); + ol.ifPresent(x -> assertTrue(x >= 0L && x <= 3L)); + OptionalDouble od = tc.draw(optionalDoubles(doubles().min(0.0).max(1.0))); + od.ifPresent(x -> assertTrue(x >= 0.0 && x <= 1.0)); } @HegelTest