padla icon indicating copy to clipboard operation
padla copied to clipboard

Add `@EnumHelpers`

Open JarvisCraft opened this issue 4 years ago • 1 comments

Description

Implement @EnumHelpers annotation and the corresponsing annotation processor to generate boilerplate enum methods.

Problem

While Java's enum is a powerful mechanism for describing varios constructions, it has a number of negative aspects which make it not that helpful.

The biggest problem is static E @NotNull [] values() method which is required to always return a fresh copy of the array due to array mutability although it's usually needed for iteration.

Another serious design problem (especially considering padla's philosophy) is inability to do try-match operation, i.e. to try to find an enum constant by a string name alternatively fallin back to null or default value etc.

This PR addresses this by provifing a @EnumHelpers annotation causing the generation of ES utility class for enum E.

Goals

  • [x] Add @EnumHelper allowing generation of:
    • [x] @NotNull Stream<@NotNull E> stream()
    • [x] @NotNull @Unmodifiable List<@NotNull E> values()
    • [ ] @Nullable E match(final @NotNull String name) and similar ones
  • [x] implement annotation-processor to generate the helper class

Example

Given

@EnumHelpers
public enum Foo {
    A, B, C
}

the following should be generated:

@Generated
public final class Foos {

    private static final @NotNull Foo @NotNull [] VALUES_ARRAY
            = Foo.values(); // may be used for faster iterations
    private static final @NotNull List<@NotNull Foo> VALUE_LIST
            = Collections.unmodifiableList(Arrays.asList(VALUES_ARRAY));

    private Foos() {
        throw new AssertionError("Foos is an utility class and thus should never be instantiated");
    }

    public static @NotNull Stream<@NotNull Foo> stream() {
        return Arrays.stream(VALUES_ARRAY);
    }

    public static @NotNull @Unmodifiable List<@NotNull Foo> values() {
        return VALUES_LIST;
    }

    public static @Nullable Foo match(final @NotNull String name) {
        if (name == null) throw new NullPointerException("name is null");

        switch (name) {
            case "A": return A;
            case "B": return B;
            case "C": return C;
            default: return null;
        }
    }

    public static Foo match(final @NotNull String name, final Foo defaultValue) {
        if (name == null) throw new NullPointerException("name is null");

        switch (name) {
            case "A": return A;
            case "B": return B;
            case "C": return C;
            default: return defaultValue;
        }
    }

    // note: possibly worth specialized lambda
    public static @Nullable Foo match(final @NotNull String name, final @NotNull Supplier<Foo> defaultValueSupplier) {
        if (name == null) throw new NullPointerException("name is null");
        if (defaultValueSupplier== null) throw new NullPointerException("defaultValueSupplieris null");

        switch (foo) {
            case "A": return A;
            case "B": return B;
            case "C": return C;
            default: return defaultValueSupplier.get();
        }
    }

    // ...
}

Notes

Generated class should net rely on Lombok as it may be not used by annotation users.

Subjects to discuss:

  • Documenting annotation should be generated if possible(?) but it is not clear whether some eaxct one (org.jetbrains) should be used or custom (via an annotation parameter).

JarvisCraft avatar Aug 30 '21 20:08 JarvisCraft

Suggestions:

  • use following naming: Foos.stream(), Foos.list(), Foos.array()
  • use List.of() instead of Collections.unmodifiableList(Arrays.asList()) for Java 9+
  • make Foos.match generation optional
  • add additional annotation to generate match-like methods for finding by field:
@RequiredArgsConstructor
@Getter
enum Foo {
  FOO_BAR("foo-bar"),
  BAR_BAZ("bar-baz");

  @EnumHelpers.Finder
  private final String value;
}


class Foos {

  public static Foo fromValue(String value) {
    // some optimal finding logic
    // for example: 
    //   1) use HashMap for big enums and array-foreach for small enums
    //   2) switch-case if enum field is compile-time constant
  }

}
  • add additional annotation to generate enum name constants and field constants:
@RequiredArgsConstructor
@Getter
@EnumHelpers.NameConstants
enum Foo {
  FOO_BAR("foo-bar"),
  BAR_BAZ("bar-baz");

  @EnumHelpers.FieldConstants
  private final String value;
}

class Foos {
  public static final String FOO_BAR_NAME = "FOO_BAR";
  public static final String BAR_BAZ_NAME = "BAR_BAZ";

  public static final String FOO_BAR_VALUE = "foo-bar";
  public static final String BAR_BAZ_VALUE = "bar-baz";
}

slutmaker avatar Oct 14 '21 21:10 slutmaker