Git Product home page Git Product logo

record-builder's Introduction

Maven Build - Java 17 Maven Central

RecordBuilder

What is RecordBuilder

Java 16 introduces Records. While this version of records is fantastic, it's currently missing some important features normally found in data classes: a builder and "with"ers. This project is an annotation processor that creates:

  • a companion builder class for Java records
  • an interface that adds "with" copy methods
  • an annotation that generates a Java record from an Interface template

Details:

RecordBuilder Example

@RecordBuilder
public record NameAndAge(String name, int age){}

This will generate a builder class that can be used ala:

// build from components
NameAndAge n1 = NameAndAgeBuilder.builder().name(aName).age(anAge).build();

// generate a copy with a changed value
NameAndAge n2 = NameAndAgeBuilder.builder(n1).age(newAge).build(); // name is the same as the name in n1

// pass to other methods to set components
var builder = new NameAndAgeBuilder();
setName(builder);
setAge(builder);
NameAndAge n3 = builder.build();

// use the generated static constructor/builder
import static NameAndAgeBuilder.NameAndAge;
...
var n4 = NameAndAge("hey", 42);

Wither Example

@RecordBuilder
public record NameAndAge(String name, int age) implements NameAndAgeBuilder.With {}

In addition to creating a builder, your record is enhanced by "wither" methods ala:

NameAndAge r1 = new NameAndAge("foo", 123);
NameAndAge r2 = r1.withName("bar");
NameAndAge r3 = r2.withAge(456);

// access the builder as well
NameAndAge r4 = r3.with().age(101).name("baz").build();

// alternate method of accessing the builder (note: no need to call "build()")
NameAndAge r5 = r4.with(b -> b.age(200).name("whatever"));

// perform some logic in addition to changing values
NameAndAge r5 = r4.with(b -> {
   if (b.age() > 13) {
       b.name("Teen " + b.name());
   } else {
       b.name("whatever"));
   }
});

// or, if you cannot add the "With" interface to your record...
NameAndAge r6 = NameAndAgeBuilder.from(r5).with(b -> b.age(200).name("whatever"));
NameAndAge r7 = NameAndAgeBuilder.from(r5).withName("boop");

Hat tip to Benji Weber for the Withers idea.

Builder Class Definition

(Note: you can see a builder class built using @RecordBuilderFull here: FullRecordBuilder.java)

The full builder class is defined as:

public class NameAndAgeBuilder {
    private String name;

    private int age;

    private NameAndAgeBuilder() {
    }

    private NameAndAgeBuilder(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * Static constructor/builder. Can be used instead of new NameAndAge(...)
     */
    public static NameAndAge NameAndAge(String name, int age) {
        return new NameAndAge(name, age);
    }

    /**
     * Return a new builder with all fields set to default Java values
     */
    public static NameAndAgeBuilder builder() {
        return new NameAndAgeBuilder();
    }

    /**
     * Return a new builder with all fields set to the values taken from the given record instance
     */
    public static NameAndAgeBuilder builder(NameAndAge from) {
        return new NameAndAgeBuilder(from.name(), from.age());
    }

    /**
     * Return a "with"er for an existing record instance
     */
    public static NameAndAgeBuilder.With from(NameAndAge from) {
        return new _FromWith(from);
    }

    /**
     * Return a stream of the record components as map entries keyed with the component name and the value as the component value
     */
    public static Stream<Map.Entry<String, Object>> stream(NameAndAge record) {
        return Stream.of(new AbstractMap.SimpleImmutableEntry<>("name", record.name()),
                 new AbstractMap.SimpleImmutableEntry<>("age", record.age()));
    }

    /**
     * Return a new record instance with all fields set to the current values in this builder
     */
    public NameAndAge build() {
        return new NameAndAge(name, age);
    }

    @Override
    public String toString() {
        return "NameAndAgeBuilder[name=" + name + ", age=" + age + "]";
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    @Override
    public boolean equals(Object o) {
        return (this == o) || ((o instanceof NameAndAgeBuilder r)
                && Objects.equals(name, r.name)
                && (age == r.age));
    }

    /**
     * Set a new value for the {@code name} record component in the builder
     */
    public NameAndAgeBuilder name(String name) {
        this.name = name;
        return this;
    }

    /**
     * Return the current value for the {@code name} record component in the builder
     */
    public String name() {
        return name;
    }

    /**
     * Set a new value for the {@code age} record component in the builder
     */
    public NameAndAgeBuilder age(int age) {
        this.age = age;
        return this;
    }

    /**
     * Return the current value for the {@code age} record component in the builder
     */
    public int age() {
        return age;
    }

    /**
     * Add withers to {@code NameAndAge}
     */
    public interface With {
        /**
         * Return the current value for the {@code name} record component in the builder
         */
        String name();

        /**
         * Return the current value for the {@code age} record component in the builder
         */
        int age();

        /**
         * Return a new record builder using the current values
         */
        default NameAndAgeBuilder with() {
            return new NameAndAgeBuilder(name(), age());
        }

        /**
         * Return a new record built from the builder passed to the given consumer
         */
        default NameAndAge with(Consumer<NameAndAgeBuilder> consumer) {
            NameAndAgeBuilder builder = with();
            consumer.accept(builder);
            return builder.build();
        }

        /**
         * Return a new instance of {@code NameAndAge} with a new value for {@code name}
         */
        default NameAndAge withName(String name) {
            return new NameAndAge(name, age());
        }

        /**
         * Return a new instance of {@code NameAndAge} with a new value for {@code age}
         */
        default NameAndAge withAge(int age) {
            return new NameAndAge(name(), age);
        }
    }

    private static final class _FromWith implements NameAndAgeBuilder.With {
        private final NameAndAge from;

        private _FromWith(NameAndAge from) {
            this.from = from;
        }

        @Override
        public String name() {
            return from.name();
        }

        @Override
        public int age() {
            return from.age();
        }
    }
}

RecordInterface Example

@RecordInterface
public interface NameAndAge {
    String name(); 
    int age();
}

This will generate a record ala:

@RecordBuilder
public record NameAndAgeRecord(String name, int age) implements 
    NameAndAge, NameAndAgeRecordBuilder.With {}

Note that the generated record is annotated with @RecordBuilder so a record builder is generated for the new record as well.

Notes:

  • Non static methods in the interface...
    • ...cannot have arguments
    • ...must return a value
    • ...cannot have type parameters
  • Methods with default implementations are used in the generation unless they are annotated with @IgnoreDefaultMethod
  • If you do not want a record builder generated, annotate your interface as @RecordInterface(addRecordBuilder = false)
  • If your interface is a JavaBean (e.g. getThing(), isThing()) the "get" and "is" prefixes are stripped and forwarding methods are added.

Generation Via Includes

An alternate method of generation is to use the Include variants of the annotations. These variants act on lists of specified classes. This allows the source classes to be pristine or even come from libraries where you are not able to annotate the source.

E.g.

import some.library.code.ImportedRecord
import some.library.code.ImportedInterface

@RecordBuilder.Include({
    ImportedRecord.class    // generates a record builder for ImportedRecord  
})
@RecordInterface.Include({
    ImportedInterface.class // generates a record interface for ImportedInterface 
})
public void Placeholder {
}

@RecordBuilder.Include also supports a packages attribute that includes all records in the listed packages.

The target package for generation is the same as the package that contains the "Include" annotation. Use packagePattern to change this (see Javadoc for details).

Usage

Maven

Add a dependency that contains the discoverable annotation processor:

<dependency>
    <groupId>io.soabase.record-builder</groupId>
    <artifactId>record-builder-processor</artifactId>
    <version>${record.builder.version}</version>
    <scope>provided</scope>
</dependency>

Gradle

Add the following to your build.gradle file:

dependencies {
    annotationProcessor 'io.soabase.record-builder:record-builder-processor:$version-goes-here'
    compileOnly 'io.soabase.record-builder:record-builder-core:$version-goes-here'
}

IDE

Depending on your IDE you are likely to need to enable Annotation Processing in your IDE settings.

Customizing

RecordBuilder can be customized to your needs and you can even create your own custom RecordBuilder annotations. See Customizing RecordBuilder for details.

record-builder's People

Contributors

codefish1 avatar dependabot[bot] avatar epm-dev-priporov avatar freelon avatar hofiisek avatar lpandzic avatar madisparn avatar mads-b avatar marcphilipp avatar mbarbero avatar mensinda avatar mgorniew avatar pawellabaj avatar randgalt avatar sebhoss avatar sipkab avatar stbischof avatar thihup avatar tisonkun avatar varunu28 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

record-builder's Issues

Default values in builder

Currently the fields of a generated builder are initialized with Java default values. Thus if one had a field of type Optional<SomeFancyClass> its initial value would be null whereas Optional.empty() may be a preferable initial value.

Suggestion: protected constructor on builder

Hi, I appreciate all you work on this project. Thank you.

What do you think about generating abstract builder classes (similar to AutoValue). This would allow for greater flexibility to add convenience methods and implement other interfaces? Like so,

@RecordBuilder
public record Example(String name) {

    public static CatalogBuilder newBuilder() {
        return new Builder();
    }
    
    public static final class Builder extends ExampleBuilder implement OtherStuff {
    }
}

Does not handle overriden accessors

Following class will fail if used with @RecordBuilder:

@RecordBuilder
public record ExceptionDetails(
    String internalMessage, String endUserMessage, HttpStatus httpStatus,
    ErrorType errorType, List<JsonProblem> jsonProblems, Throwable cause
) {
    @Override
    public List<JsonProblem> jsonProblems() {
        if (jsonProblems == null) {
            return List.of();
        }
        return jsonProblems;
    }
}

It fails with:

ExceptionDetailsBuilder.java:[195,4] error: method does not override or implement a method from a supertype

I looked at the options but I couldn't find anything that would help with that.

This is from version 24

Support @NonNull at package-info.java level

Discussed in #89

Originally posted by cykl March 3, 2022
A common way to manage nullability annotations is to put a default @nonnull annotation at method / parameter / field level using package-info.java and then only add @nullable where relevant. Rational being that it is far more common to want something @nonnull than @nullable.

The idiomatic way to do it is to define an annotation like that

@Target( { ElementType.PACKAGE, ElementType.TYPE } )
@Retention( RetentionPolicy.RUNTIME )
@Documented
@Nonnull
@TypeQualifierDefault( ElementType.FIELD )
public @interface NonNullFields {
}

And then apply it to the package-info.java:

@NonNullApi
@NonNullFields
package my.package;

It tried to define a such annotation for record components and apply it. But record-builder seems to not care.

@Target( { ElementType.PACKAGE, ElementType.TYPE } )
@Retention( RetentionPolicy.RUNTIME )
@Documented
@Nonnull
@TypeQualifierDefault( { ElementType.RECORD_COMPONENT } )
public @interface NonNullRecordComponents {
} 

Using getterPrefix and/or booleanPrefix generates incompatible Wither

E.g.

@RecordBuilder
@RecordBuilder.Options(
    setterPrefix = "set", getterPrefix = "get", booleanPrefix = "is", beanClassName = "Bean")
public record CustomMethodNames<K, V>(
    Map<K, V> kvMap,
    int theValue,
    List<Integer> theList,
    boolean theBoolean) implements Bean, CustomMethodNamesBuilder.With {
}

Generates With interface with incorrect method names. They shouldn't have the prefix.

Null checks fail to render for records having a canonical constructor

Thanks for the great tool. While testing some options, I figured something out, what probably is a bug. Using the option "interpretNotNulls" is working as expected, as long as one does not specify the canonical record constructor, even though it may not contain any code. So the following code will not contain any null checks for record component "a", but it will, when a remove the empty canonical constructor.

@RecordBuilder
@RecordBuilder.Options(interpretNotNulls = true)
public record MyDto(@NonNull String a, String b, String c, int x, List<String> strings)
		implements MyDtoBuilder.With {

	public MyDto {}
}

I am using version 35.

generate "add" method for collections

given this:

record Rule(String name, Filter filter, List<Action>) {}

I would love I could do:

RuleBuilder.name("arule").filter(f().from("[email protected]")).action(markAsSeen()).action(fileTo("folder")).build()

Basically, in addition to generating action(List<Action>) add a action(Action) that builds up the list.

Generate methods in builder to add a single value to collections

First of all thanks for making this library. Awesome project.

I'm in the middle of upgrading a project that heavily uses Lombok. We use @Value + @Builder a lot so the combination of Java records and @RecordBuilder is a really nice upgrade path.

Lombok provides a way to generate "singular" builder methods using @Singular annotation on a field. It would be really nice if the builder generated with @RecordBuilder would include these by default.

Immutables also generate builder methods to add single items to collections.

An example:

public record Foo(List<Item> items) {}

The generated builder would include

// this exists today
public FooBuilder items(List<Item> items) {
    this.items = items;
    return this;
}

public FooBuilder item(Item item) {
     items.add(item);
     return this;
}

// or
public FooBuilder addItem(Item item) {
     items.add(item);
     return this;
}

Cannot find symbol in code generated with addSingleItemCollectionBuilders

Code:

@RecordBuilder
@RecordBuilder.Options(addSingleItemCollectionBuilders = true)
record Person(String name, int age, Map<String, Object> other) implements PersonBuilder.With {}

POM:

    <dependencies>
        <dependency>
            <groupId>io.soabase.record-builder</groupId>
            <artifactId>record-builder-processor</artifactId>
            <version>35</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

Compiler error:

\target\generated-sources\annotations\org\example\PersonBuilder.java:141:22
java: cannot find symbol
  symbol:   method __map(java.util.Map<capture#1 of ? extends java.lang.String,capture#2 of ?>)
  location: class org.example.PersonBuilder

Generated snippet:

    /**
     * Re-create the internally allocated {@code Map<String, Object>} for {@code other} by copying the argument
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public PersonBuilder other(Map<? extends String, ?> other) {
        this.other = __map(other);
        return this;
    }

There is no __map defined anywhere
Lists fail the same way with missing __list.

Withers ConstraintDeclarationException

When I want to validate a record as a field of another record which implements With interface, the javax.validation.ConstraintDeclarationException is thrown.

Repro:

@RecordBuilder
public record Request(

  @NotNull
  @Valid
  Part part)

  implements RequestBuilder.With {

  public record Part(@NotBlank String name) {}

}

Error message:

HV000131: A method return value must not be marked for cascaded validation more than once in a class hierarchy, but the following two methods are marked as such: Request#part(), With#part().

It works without With.

I tried setting up a meta data file, but couldn't get it running with Maven. Maybe you could add an example for how it should be used? (Though probably not as part of this merge request.)

I tried setting up a meta data file, but couldn't get it running with Maven. Maybe you could add an example for how it should be used? (Though probably not as part of this merge request.)

Also, more of a general idea: It would be very useful if these things could also be declared per record class, so probably as part of the @RecordBuilder annotation or via a specific annotation for configuration. (Also, not something for this merge request.)

Originally posted by @blalasaadri in #33 (comment)

Support all varieties of Null handling

Yes that will probably be good for all. However I can't keep working on #106 or the other nullable annotation bugs till some decisions are made. Which is why I have been so back and forth on this as there are some serious previous design decisions that are impacting me.

So I'm fine with holding off indefinitely till some decisions are made (like lets copy what immutables does).

And I'm sorry for coming off rough and flooding the inbox but I have already sunken some time on this (a lot more than just changing some call from of to ofNullable) as I sort of felt obligated given previous discussions on reddit and whatnot.

I'll stop work and commenting till the dust clears.

Originally posted by @agentgt in #107 (comment)

Adding -jdk15 to version

Based on the idea using it as dependencies etc. the idea add something to the version to mark it for particular JDK usage is a bad practice. For such purpose a classifier exists... I would suggest to change that.

Propagate TYPE_USE annotations

As discussed on reddit the library does not propagate TYPE_USE annotations.

TYPE_USE annotations are far different than most other annotations in that they are extending the type.

Let us assume @Nullable is a TYPE_USE like it is for JSpecify.

@RecordBuilder
public record MyRecord(@Nullable String name, String required) {}

The record builder should copy the type java.lang. @Nullable String for wherever name is used. Yes that format of putting the annotation at the end of the package plus . is correct and that is why basically every library I have seen mess this up. Furthermore even the JDK mess this up but is now fixed cause ideally you could just toString on the TypeMirror.

Anyway it looks like your using Java Poet which notoriously ignores those annotations through its more or less broken TypeName. Someone posted a workaround here: square/javapoet#685 which you could use.

Generate builders for all classes in package

Discussed in #66

Originally posted by nikolavojicic August 30, 2021

@RecordBuilder.Include({
    ImportedRecord1.class,
    ImportedRecord2.class,
})
public class Config {}

Is it possible to pass package path(s) somehow, instead of classes, so that builders for all classes in those packages are generated?

Auxilliary values

Records by default utilize all values in computation of equals and hashcode

Sometimes that's not what you want.

It's trivial to add a custom implementation of equals and hashcode to the records, but we could also introduce an annotation that could be assigned to record fields to result in custom equals and hashcode appearing in the Bean interface

What do you think? It's not a must-have, but rather useful

1.16 doesn't seem to be compatible with Java 15

I just tried updating from 1.14 to 1.16 and got the following error:

> io/soabase/recordbuilder/processor/RecordBuilderProcessor has been compiled by a more recent version of the Java Runtime (class file version 60.0), this version of the Java Runtime only recognizes class file versions up to 59.0

I assume this is because 1.16 currently only works on the Java 16 preview?

- annotationProcessor 'io.soabase.record-builder:record-builder-processor:1.14.ea'
+ annotationProcessor 'io.soabase.record-builder:record-builder-processor:1.16'

null values and addConcreteSettersForOptional

I find that addConcreteSettersForOptional uses Optional#of instead of Optional#ofNullable quite surprising especially since Intellij IDEA by default doesn't warn against passing nulls to javax.validation.constraints.NotNull annotated parameters.
What is the reasoning behind this decision?

Support NotNull, NoNull, etc.

Now that we have easier to use options for RecordBuilder, add an option that recognizes annotations that are named "notnull", "nonull", etc. and validate that null is not used in the builder. Make this support opt-in - i.e. the option should be false by default.

See discussion here: #32

Example of JSR303 validation for a MinMax record

Hi,

With the JSR303 addition that you have now built in, could ou please provide an example of a MinMax(T min, T max) class that
is possible to validate that min and max is notNull and also that min should be less or equal to max param?

Is this case possible in the todays master?

/Dan

Allow nullable collections with useImmutableCollections=true

Hi,
somewhat related to #122, I'd like to propose allowing collections to become nullable, because right now useImmutableCollections = true does two things

  • it adds the conversion to immutable collections (awesome!)
  • but also enforces that the output record can never have the collection values nullable

I'm proposing to change the behaviour based on interpretNotNulls value.

  • useImmutableCollections = true && interpretNotNulls = false
    • mapping will be return (o != null) ? Map.copyOf(o) : null;
  • useImmutableCollections = true && interpretNotNulls = true
    • and field is determined to be nullable
      • mapping will be return (o != null) ? Map.copyOf(o) : null;
    • and field is determined to be notnull
      • mapping will be return (o != null) ? Map.copyOf(o) : Map.of();
  • useImmutableCollections = true
    • current behaviour

This should also (consistently) affect the default value for collections mentioned in #122

Defaults

I'd like a way to supply default values, so the record is in the proper state if a field is not set using the builder.

Suggestions:
@default(myvalueHere)

A magic static method that gets called prior to building with the builder as a parameter? This way we could do validation prior to building as well.

Heavy copying of collections

If using the useImmutableCollections and addSingleItemCollectionBuilders options the generated builder does copy collections on every change on the record. Example:

@RecordBuilderFull
public record Example(String name, List<Foo> foo) implements ExampleBuilder.With {}
...
Example e1 = ExampleBuilder.builder().name("a").foo( *some large collection*).build();
Example e2 = e1.with().name("b").build();
Example e3 = e2.with().name("c").build();

Whenever I do something like example.with().name("...").build() internally with() creates a new ExampleBuilder which instantiates a new ArrayList with all items from my old example object. Then calling build() creates a new immutable list of the array list. If I have a large collection and change other fields of my record frequently a lot (2 per record with-...-build-cycle) of list-copying is done in the background, although the list is never changed. So in the above example my large collection would already been copied six times without need.

I would prefer if the list was only copied into a new ArrayList if it actually changes. I think this can be done rather easily by using for instance a flag for every collection to indicate if the collection was modified. If a change is to be made and the flag is false, copy the current collection into a new ArrayList and then set the flag. After that, just add the collection item(s).

I'd be glad to create a PR if you're interested. Thanks for the great library so far :)

Add emptyDefaultForCollections options

I think many people would like to have emptyDefaultForCollections, where instead of nulls builder would set following instances in the record built:

  • List -> List.of()
  • Set -> Set.of()
  • Map -> Map.of()
  • Collection -> Set.of() or List.of()

Suppress warnings for unchecked exception on _downcast

We've quite a strict policy on warnings in our codebase so _downcast() methods are raised as issues when the code is generated. I'd created a quick update locally to add an annotation so consciously suppress the warning in that case, would you be open to accepting a PR for it?

Builder defaults for collections are inconsistent with output record

Hi,
awesome library! Love it! I just have a small suggestion...

If I have a record with any collection

@MyRecordBuilder // with useImmutableCollections = true, interpretNotNulls = true
public record Costs(
    @Nullable BigDecimal shipping,
    Map<String, Object> extraData
) { }
  • the default values when the builder initializes for all fields is null
  • calling CostsBuilder.builder().build().extraData() builds instance of Costs that has extraData initialized with empty Map.of() and does not return null
    • which means CostsBuilder.builder().build().extraData().get("value") will not throw NPE
  • calling CostsBuilder.builder().extraData() returns the default null
    • which means CostsBuilder.builder().extraData().get("value") will throw NPE

I'm proposing that since you're adding special handling of collections and always initializing them, they should also be initialized to empty collections when the empty builder is created, so that the second case will also not throw an NPE

Checker framework's NonNull annotation is ignored

Hi, first of all: thanks a lot for this project.

I wanted to use the option interpretNotNulls with the default regex pattern "(?i)((notnull)|(nonnull)|(nonull))" and the Checker framework's NonNull annotation but the generated code does not contain null checks.

Maybe it's related to the target ElementType.TYPE_USE - I read about this in issue #106 but that's the first time I stumbled across that target type, too.

When the following example's handwritten annotation would be used, the generated builder would contain null-checks.

Simple example (using version 33 of record-builder and version 3.22.0 of Checker framework):

package com.example;

import io.soabase.recordbuilder.core.RecordBuilder;
import org.checkerframework.checker.nullness.qual.NonNull; // <-- @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})

//@interface NonNull {} // <-- this would work

@RecordBuilder
@RecordBuilder.Options(interpretNotNulls = true)
public record RecordWithNonNullAnnotation(@NonNull Object foo) {

}

Generated builder:
relevant builder snippet:

    /**
     * Return a new record instance with all fields set to the current values in this builder
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public RecordWithNonNullAnnotation build() {
        return new RecordWithNonNullAnnotation(foo);
    }

complete builder:

// Auto generated by io.soabase.recordbuilder.core.RecordBuilder: https://github.com/Randgalt/record-builder
package com.example;

import java.util.AbstractMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.annotation.processing.Generated;

@Generated("io.soabase.recordbuilder.core.RecordBuilder")
public class RecordWithNonNullAnnotationBuilder {
    private Object foo;

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private RecordWithNonNullAnnotationBuilder() {
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private RecordWithNonNullAnnotationBuilder(Object foo) {
        this.foo = foo;
    }

    /**
     * Static constructor/builder. Can be used instead of new RecordWithNonNullAnnotation(...)
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static RecordWithNonNullAnnotation RecordWithNonNullAnnotation(Object foo) {
        return new RecordWithNonNullAnnotation(foo);
    }

    /**
     * Return a new builder with all fields set to default Java values
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static RecordWithNonNullAnnotationBuilder builder() {
        return new RecordWithNonNullAnnotationBuilder();
    }

    /**
     * Return a new builder with all fields set to the values taken from the given record instance
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static RecordWithNonNullAnnotationBuilder builder(RecordWithNonNullAnnotation from) {
        return new RecordWithNonNullAnnotationBuilder(from.foo());
    }

    /**
     * Return a "with"er for an existing record instance
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static RecordWithNonNullAnnotationBuilder.With from(RecordWithNonNullAnnotation from) {
        return new _FromWith(from);
    }

    /**
     * Return a stream of the record components as map entries keyed with the component name and the value as the component value
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static Stream<Map.Entry<String, Object>> stream(RecordWithNonNullAnnotation record) {
        return Stream.of(new AbstractMap.SimpleImmutableEntry<>("foo", record.foo()));
    }

    /**
     * Return a new record instance with all fields set to the current values in this builder
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public RecordWithNonNullAnnotation build() {
        return new RecordWithNonNullAnnotation(foo);
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    @Override
    public String toString() {
        return "RecordWithNonNullAnnotationBuilder[foo=" + foo + "]";
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    @Override
    public int hashCode() {
        return Objects.hash(foo);
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    @Override
    public boolean equals(Object o) {
        return (this == o) || ((o instanceof RecordWithNonNullAnnotationBuilder r)
                && Objects.equals(foo, r.foo));
    }

    /**
     * Set a new value for the {@code foo} record component in the builder
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public RecordWithNonNullAnnotationBuilder foo(Object foo) {
        this.foo = foo;
        return this;
    }

    /**
     * Return the current value for the {@code foo} record component in the builder
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public Object foo() {
        return foo;
    }

    /**
     * Add withers to {@code RecordWithNonNullAnnotation}
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public interface With {
        /**
         * Return the current value for the {@code foo} record component in the builder
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        Object foo();

        /**
         * Return a new record builder using the current values
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default RecordWithNonNullAnnotationBuilder with() {
            return new RecordWithNonNullAnnotationBuilder(foo());
        }

        /**
         * Return a new record built from the builder passed to the given consumer
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default RecordWithNonNullAnnotation with(
                Consumer<RecordWithNonNullAnnotationBuilder> consumer) {
            RecordWithNonNullAnnotationBuilder builder = with();
            consumer.accept(builder);
            return builder.build();
        }

        /**
         * Return a new instance of {@code RecordWithNonNullAnnotation} with a new value for {@code foo}
         */
        @Generated("io.soabase.recordbuilder.core.RecordBuilder")
        default RecordWithNonNullAnnotation withFoo(Object foo) {
            return new RecordWithNonNullAnnotation(foo);
        }
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private static final class _FromWith implements RecordWithNonNullAnnotationBuilder.With {
        private final RecordWithNonNullAnnotation from;

        private _FromWith(RecordWithNonNullAnnotation from) {
            this.from = from;
        }

        @Override
        public Object foo() {
            return from.foo();
        }
    }
}

useImmutableCollections=true combined with @Nonnull on a record component result in NPE

Discussed in #90

When useImmutableCollections is set to true, builder populates the record with empty collections rather than a null field. Generated code looks like:

   @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public Removeme.R build() {
        return new Removeme.R(__list(l));
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private static <T> List<T> __list(List<T> o) {
        return (o != null) ? List.copyOf(o) : List.of();
    }

So far, so good. Now, let's add @nonnull annotation on the record component. Building the record will result in a NullPointerException because builder check for nullity before __list() is invoked.

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public Removeme.R build() {
        Objects.requireNonNull(l, "l is required");
        return new Removeme.R(__list(l));
    }

Declaring a collection to not be null is likely the most common case (who want to deal with both nullity and emptiness?).

I'm not familiar with record-builder codebase but will submit a tentative PR soon.

Generic records produce code with `rawtypes` and `unchecked` warnings

At work, we are making great use of your library. Thanks!

That said, we introduced our first record using generics the other day, and found the generated Builder class to suffer from a few javac warnings, namely unchecked and rawtypes. We have a very low tolerance for compiler warnings, so this blocks us from using @RecordBuilder for these types.

We use JDK 17

Minimal example:

@RecordBuilder
public record Foo<T>(List<T> items) {}

Problematic generated code snippets:

// FooBuilder

@Generated("io.soabase.recordbuilder.core.RecordBuilder")
@Override
public boolean equals(Object o) {
    return (this == o) || ((o instanceof FooBuilder r)    // <- javac flags [rawtypes] warning here
            && Objects.equals(items, r.items));
}

// ...

@Generated("io.soabase.recordbuilder.core.RecordBuilder")
default FooBuilder<T> with() {
    return new FooBuilder(items());  // <- javac flags [rawtypes] and [unchecked] warnings here
}

It would be ideal if equals() could appease the compiler with (o instanceof FooBuilder<?> r) and with() could similarly with new FooBuilder<>(items()).

Barring that, perhaps a @SuppressWarnings({"unchecked","rawtypes"}) on these methods or on the class.

aa027af causes compile errors

Hello, The aforementioned commit is preventing me from upgrading from v33 to v34. The issue:

I have a quite simple record which should create a builder:

public record CombinedFields(
    @JsonValue String combinedField,
    List<CombinedFieldsLine> lines) implements CombinedFieldsBuilder.Bean {}

But now, the generated builder contains a setter with an undefined method (shim?)

    /**
     * Re-create the internally allocated {@code List<CombinedFields.CombinedFieldsLine>} for {@code lines} by copying the argument
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public CombinedFieldsBuilder setLines(
            Collection<? extends CombinedFields.CombinedFieldsLine> lines) {
        this.lines = __list(lines);
        return this;
    }

It's this __list method that does not exist. Is this a bug or something I need to change in my configuration?

Options to create unmodifiableLXYZ

Hi again:-)

Just thinking if it could be a good idea to have an new RecordBuilder.Options
like "useUnmodifiableCollections" or similar?

And then for List/Map/Set use Collections.unmodifiableXYZ in the builder..?

What is your thoughts around this suggestion?

Have a nice weekend :-)

/Dan

Method names should start with a lower case letter

As my SAST vulnerability report tool always complain that "Method names should start with a lower case letter". Is there any reason to use static constructor method name with capital letter.

@RecordBuilder
public record MyRecord(String name){}

The generated code:

     /**
     * Static constructor/builder. Can be used instead of new MyRecord(...)
     */
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    public static MyRecord MyRecord(String name) {...}

Identifier: Find Security Bugs-NM_METHOD_NAMING_CONVENTION

May be it would be great if we can configure the method name conventions.

Expected generated code:

/**
  * Static constructor/builder. Can be used instead of new MyRecord(...)
  */
 @Generated("io.soabase.recordbuilder.core.RecordBuilder")
 public static MyRecord myRecord(String name) {...}

New release

Hello, last release was in April and a lot of commits are now in master.
When is there going to be a new release?

Simplify updating immutable members when referencing the previous value

Hey there! I've been using your library a bit and think it's a nice improvement for records. When working with nested immutable types I've found some thing a bit cumbersome. Imagine that you have the following records:

@RecordBuilder
public record Context(int id, Counter count) implements ContextBuilder.With {}

@RecordBuilder
public record Counter(int count) implements CounterBuilder.With {}

If we want to increment the count in a context we need to do this:

var ctx = new Context(1, new Counter(0));

// I know withCounter can be used as well
var newCtx = ctx.with().counter(ctx.counter().with().counter(ctx.counter().count() + 1).build());

We need to repeat the ctx.counter() bit three times unless we want to bring in a temporary variable. It would be nice if RecordBuilder provided something that simplified mutation of members that are record or other immutable types like queues.
Ideally something that allows us to add arbitrary helper methods to the builder would be nice, but maybe that's difficult to do?

Another idea would be to generate variants of the setters in the builder that takes a Function<T, T>. That allows the user to mutate the inner types. The example above would become something like:

var ctx = new Context(1, new Counter(0));
var newCtx = ctx.with().counter(ctr -> ctr.with().count(c -> c + 1).build());

The drawback is of course that it could conflict with records that have members with a type of Function.

Sealed wither interface?

Its still down the line (Java 17, most likely), but it would be nice to have the RecordName.With generated interface be sealed so that only the record can implement it

Support records with 0 fields

This is an edge case I came across while playing around.
If you have a record with no fields the generated Builder source will have a duplicate 0 argument constructor.

Example:

@RecordBuilder
public record MyRecord() implements MyRecordBuilder.With{}

Generates:

@Generated("io.soabase.recordbuilder.core.RecordBuilder")
public class MyRecordBuilder {
    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private MyRecordBuilder() {
    }

    @Generated("io.soabase.recordbuilder.core.RecordBuilder")
    private MyRecordBuilder() {
    }

   ...Rest of the class

This results in a compilation error

Release new version

Hello, I see last release was 16 days ago and I'd really like to use the features that are present in master but haven't been released yet. When is a new release going to happen?

Question abour visibility of the builder

Hi Jordan

Just a quick question: Is there any reason why the builder always is public?

Im thinking if the record one has specified is package-private, would'nt be nice
if the builder was package-private as well?

Thanks and have a nice weekend,

/Dan

Add a simple way to remove items from a collection.

With the latest changes there is no easy and clean way to remove or replace items from a collection. Before one would always get a mutable collection when using the get methods, this has now changed. Maybe add remove and replace methods to the generated builders.

Validations for the withers

Hi Jordan

I'm playing around with new validation opportunities that you just have implemented. Very nice :-)

Howevere, It seems to be a possible to sneak thru the validation when using the withers methods.

For example when letting
RequiredRecord implement RequiredRecordBuilder.With
and
RequiredRecord2 implement RequiredRecord2Builder.With

I expect these two unit test to pass:

    void testNotNullsWithNewProperty() {
        var valid = RequiredRecordBuilder.builder().hey("hey").i(1).build();
        Assertions.assertThrows(NullPointerException.class, () -> valid.withHey(null));
    }

    @Test
    void testValidationWithNewProperty() {
        var valid = RequiredRecord2Builder.builder().hey("hey").i(1).build();
        Assertions.assertThrows(ValidationException.class, () -> valid.withHey(null));
    }

For the @RecordBuilder.Options(interpretNotNulls = true)
I guess it just as easy to add Objects.reuireNonNull in appropriate with-methods

but how to handle @RecordBuilder.Options(useValidationApi = true) ?

We dont want to perform a full validation of the entiere object via the builders static "new" method
but instead only validate the actual property.

Please let me know your thoughts around this and I'll be happy to start with an initial PR.

Regards
Dan

Maybe use Map::entry instead of AbstractMap.SimpleEntry?

There is a chance that using the Map::entry, it will be a primitive class and maybe will have better performace. Is there a reason to choose the AbstractMap.SimpleEntry over Map.entry or AbstractMap.SimpleImmutableEntry?

(By the way, this project is superb)

Add possibility to add custom annotations to Builder

In particular edu.umd.cs.findbugs.annotations.SuppressFBWarnings, otherweise SpotBugs detects violations which normally would be ignored in generated code.

It's not possible to make SpotBugs process @Generated annotation since SpotBugs process byte-code and @Generated has source retention type.

P.S. Nice library and nice work! I use Immutables a lot so it's really nice to have similar funtionality for java records.

RecordInterface does not support RecordBuilder.Options

Discussed in #63

Originally posted by marcusti August 26, 2021
Hey, thanks for creating this project! Looks like a fine replacement for Immutables.

I was wondering, is there a way to configure the RecordBuilder when using RecordInterface? I would like to pass options like useImmutableCollections = true.

Cannot invoke "javax.lang.model.element.ExecutableElement.getAnnotationMirrors()" because the return value of "javax.lang.model.element.RecordComponentElement.getAccessor()" is null

We use record-builder on a project and we often get compilation errors with the following message:
Cannot invoke "javax.lang.model.element.ExecutableElement.getAnnotationMirrors()" because the return value of "javax.lang.model.element.RecordComponentElement.getAccessor()" is null

The errors happen either in Intellij IDEA or via the CLI with Maven. Running a re-build in Intellij or running the command again fixes the issue (usually a mvn verify).

I suspect it could come from the two tools using a different JDK or compilation options and each generating slightly different bytecode. However I've made sure that the same JDK (17.0.6) is used in both cases.

I've managed to capture a stack trace below. I'm not confident about being able to provide a reproducer project but if the error message is not helpful in understanding the problem I can give it a shot.

Caused by: java.lang.NullPointerException: Cannot invoke "javax.lang.model.element.ExecutableElement.getAnnotationMirrors()" because the return value of "javax.lang.model.element.RecordComponentElement.getAccessor()" is null
    at io.soabase.recordbuilder.processor.InternalRecordBuilderProcessor.lambda$buildRecordComponents$2 (InternalRecordBuilderProcessor.java:143)
    at java.util.stream.ReferencePipeline$3$1.accept (ReferencePipeline.java:197)
    at java.util.Iterator.forEachRemaining (Iterator.java:133)
    at java.util.Spliterators$IteratorSpliterator.forEachRemaining (Spliterators.java:1845)
    at java.util.stream.AbstractPipeline.copyInto (AbstractPipeline.java:509)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto (AbstractPipeline.java:499)
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential (ReduceOps.java:921)
    at java.util.stream.AbstractPipeline.evaluate (AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.collect (ReferencePipeline.java:682)
    at io.soabase.recordbuilder.processor.InternalRecordBuilderProcessor.buildRecordComponents (InternalRecordBuilderProcessor.java:143)
    at io.soabase.recordbuilder.processor.InternalRecordBuilderProcessor.<init> (InternalRecordBuilderProcessor.java:66)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.processRecordBuilder (RecordBuilderProcessor.java:168)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.process (RecordBuilderProcessor.java:76)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.lambda$process$0 (RecordBuilderProcessor.java:54)
    at java.lang.Iterable.forEach (Iterable.java:75)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.lambda$process$1 (RecordBuilderProcessor.java:54)
    at java.lang.Iterable.forEach (Iterable.java:75)
    at io.soabase.recordbuilder.processor.RecordBuilderProcessor.process (RecordBuilderProcessor.java:54)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor (JavacProcessingEnvironment.java:1023)
    ....

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.