Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Spike of API changes, #50 #62

Merged
merged 3 commits into from
Jul 1, 2021
Merged

Conversation

patriknw
Copy link
Contributor

@patriknw patriknw commented Jun 23, 2021

API proposal that is introducing the Effect return type and changes to state management.

This is a "compile only" spike until we agree on the API.

Probably easies to look at the changes of the examples:

  • valueentity-counter ... CounterImpl.java
  • java-eventsourced-shopping-cart ... ShoppingCartImpl.java

Refs #50

.addAllItems(cart.values().stream().map(this::convert).collect(Collectors.toList()))
.build();
protected ShoppingCartDomain.Cart emptyState() {
return ShoppingCartDomain.Cart.getDefaultInstance();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something we need to decide. Shall we have an emptyState or use Optional in all currentState parameters? For ValueEntity it's Optional.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how explicit this is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have emptyState we have to include make that accessible from the SDK also, not only in generated code like this. We could have an interface or abstract class that the impl has to implement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@octonato not that I would like to bring back memories to our old discussion about emptyState 😄 , but what do you think we should do here? emptyState or Optional parameters everywhere? Same in ValueEntity or is the difference motivated?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bring back those memories! :-)

My whole idea in the past is that I wanted to avoid exposing users to Option because I believe there is a clear point in time where the journal is empty and therefore we start with None. So, first event moves from None to Some. All subsequent events can only work on a Some.

From that observation, we could say there are two possible event handlers (and command handlers): Event => State and (State, Event) => State. And we can then hide the Option from the user.

Anyway, that just to remind my opinion about it since you triggered my memories. :-)

Back to the present...

I think we don't want to introduce two kinds of command and event handlers because that would require the user to annotate the methods in the proto file. Event handlers would even be more challenging because there are not even defined in the proto file.

So, that to say, my opinion is that we should not bother users with an Optional type if most of the time it will be a Some. Only the first command handler and event handler will see it as whatever the user defines as empty state.

And most probably, java developers will use a null.

For the ValueEntity, I think we can apply the same reasoning. The emtpyState will only be used when there is no row (including after deleting a row and starting over again).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Then I introduce emptyState in ValueEntity also to make it consistent.

null or the proto empty instance can be used as starting point.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added emptyState for ValueEntity also: bd4aff8

.build();
}
cart.put(item.getProductId(), item);
protected ShoppingCartDomain.Cart itemAdded(ShoppingCartDomain.ItemAdded itemAdded, ShoppingCartDomain.Cart currentState) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of the parameters is something we need to decide. Now I use event/command as the first parameter, but it might read better with state first. We have the state first in Akka.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it reads better with state is first. In my brain I read it as: given this State, what should happen if we apply this Command.

Which also reads as, given this State what is the command handler (Cmd => Effect) we have for it.

Same reasoning applies event handlers.

So, yeah. State first all the way down.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State first would probably read better. JS SDK has command, state, context. Probably so that handlers can just have the command if current state is not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed the order, state first: 9017436

ShoppingCartApi.RemoveLineItem command,
ShoppingCartDomain.Cart currentState,
EventSourcedEntityEffect.Builder<Empty, ShoppingCartDomain.Cart> effectBuilder,
CommandContext ctx);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CommandContext will rarely be needed so can we remove that parameter somehow?

Combine with with the effectBuilder?

Use a ThreadLocal accessor instead of a method parameter? CommandContext.currentContext().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will be left in the command context?

Otherwise, we could have a CallContext<State> that we use to get command context and build the reply.

(CallContext sucks, I just need to pick a name.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combining into one context / effect builder would be good. And a simpler name too. Does look too heavy currently.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have included the effect builder in the CommandContext: fc680a3

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼 looks better behind the CommandContext.

Small thing, but I wonder if it would be cleaner as just effects rather than effectBuilder:

return context.effects()
        .emitEvent(event)
        .thenReply(newState -> Empty.getDefaultInstance());

but maybe it's clearer to say that it's a builder explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the suggestion of context.effects().

On that topic. Effect is typically well know terminology for those that know some functional programming, but is it known for the target Java developers? Do we have any alternative naming suggestion?

Action? but that would be confusing since we already have that for Action components.

Just looking at the english dictionary for Effect, it might be just fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Effect is fine, and could be useful having it aligned with Akka.

Agree that Action would be confusing. Alternative could be something like Operation.

@SuppressWarnings("unused")
private final String entityId;

private final Map<String, ShoppingCartApi.LineItem> cart = new LinkedHashMap<>();

public ShoppingCartImpl(@EntityId String entityId) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move to a factory function in the registration of the components instead of register them by class. There can be other shared things that should be possible to "inject" into the component when creating it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, one thing that we might one to inject are clients to other services (local component or any other service)

public static class Builder<T, S> {
private Builder() {}

public Builder2<T, S> emitEvent(Object event) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object here is not great, but there is no interface that all events would implement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A marker interface here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's typically the generated proto messages that are the events

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, thanks.

* @param payload The payload of the reply.
* @return A message reply.
*/
public MessageReply<T> message(T payload) { // FIXME rename to reply? Note thenReply
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this should be renamed to reply(T message)

Copy link
Member

@octonato octonato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick pass. I will checkout and look more in detail later.

.build();
}
cart.put(item.getProductId(), item);
protected ShoppingCartDomain.Cart itemAdded(ShoppingCartDomain.ItemAdded itemAdded, ShoppingCartDomain.Cart currentState) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think it reads better with state is first. In my brain I read it as: given this State, what should happen if we apply this Command.

Which also reads as, given this State what is the command handler (Cmd => Effect) we have for it.

Same reasoning applies event handlers.

So, yeah. State first all the way down.

Comment on lines 77 to 78
EventSourcedEntityEffect.Builder<Empty, ShoppingCartDomain.Cart> effectBuilder,
CommandContext ctx) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventSourcedEntityEffect.Builder + CommandContext is looking heavy.

Not fully clear why we need it. I have to checkout the code and feel it under my fingers.

In any case, worth experiment more to reduce it.

ShoppingCartApi.RemoveLineItem command,
ShoppingCartDomain.Cart currentState,
EventSourcedEntityEffect.Builder<Empty, ShoppingCartDomain.Cart> effectBuilder,
CommandContext ctx);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will be left in the command context?

Otherwise, we could have a CallContext<State> that we use to get command context and build the reply.

(CallContext sucks, I just need to pick a name.)

.addAllItems(cart.values().stream().map(this::convert).collect(Collectors.toList()))
.build();
protected ShoppingCartDomain.Cart emptyState() {
return ShoppingCartDomain.Cart.getDefaultInstance();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have emptyState we have to include make that accessible from the SDK also, not only in generated code like this. We could have an interface or abstract class that the impl has to implement.

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@EventSourcedEntity(entityType = "eventsourced-shopping-cart")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where should the entityType be defined? I think it belongs to the registration, similar to how the viewId is defined in the registration of views.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the registration sounds good. There may be an option for that already? (I remember this being discussed before)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are touching all this, it might be good to rename this to entityTypeHint. Maybe on a follow-up PR.

* @param description The description of the failure.
* @return A failure reply.
*/
public FailureReply<T> failure(String description) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we rename this to error? https://www.reactivemanifesto.org/glossary#Failure

public ErrorReply<T> error(String description)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

error it is: b5cea76

@raboof raboof mentioned this pull request Jun 30, 2021
20 tasks
Copy link
Member

@octonato octonato left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good @patriknw

I drop some new ideas about the effect builders and commands.

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

@EventSourcedEntity(entityType = "eventsourced-shopping-cart")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are touching all this, it might be good to rename this to entityTypeHint. Maybe on a follow-up PR.

@SuppressWarnings("unused")
private final String entityId;

private final Map<String, ShoppingCartApi.LineItem> cart = new LinkedHashMap<>();

public ShoppingCartImpl(@EntityId String entityId) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely, one thing that we might one to inject are clients to other services (local component or any other service)


return context.effectBuilder()
.emitEvent(event)
.thenReply(newState -> Empty.getDefaultInstance());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking much better.

I wonder what we could do to remove the context at all.

For instance, in Akka Typed, the entity needs to implement a given type. Could we do the same here?

abstract class ShoppingCartInterface2 implements EventSourcedEntity<Cart>
class ShoppingCartImpl extends ShoppingCartInterface2

EventSourcedEntity would have the effect builders already typed on Cart and will also have the commandHandler and eventHandler entry points to be implemented.

ShoppingCartInterface2 would implement commandHandler and eventHandler and do the dispatch to the methods the user needs to implement. And we make handler final in the interface. The user only touch the method to dispatch to.

For the return type, I don't think the builder needs to have a type argument, because the compiler will force the user to finish it with the right type, ie: .thenReply(newState -> "I'm done"); won't compile.

We are left with the data that is provide by the context, command name, command id and seqNum (for event sourcing).

Would that be a good idea to wrap the command instead?

class ShoppingCartImpl extends implements EventSourcedEntity<Cart> {
  
  public Effect<Empty> addItem(
    ShoppingCartDomain.Cart currentState,
    Command<ShoppingCartApi.AddLineItem> input) {
	  
    // use wrap to access command and other call related info
    input.command() // returns ShoppingCartApi.AddLineItem
    input.commandName()
    input.commandId()
	    
    return effectBuilder() // already typed on Cart because of interface
             .emitEvent(event)
             // compile enforces return type
             .thenReply(newState -> Empty.getDefaultInstance()); 

	  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need the implements EventSourcedEntity<Cart> anyway, for other reasons. I think I mentioned that in relation to the emptyState.

I like the idea of having the effectBuilder() (or effects() as was suggested elsewhere) in that base interface. I have to try if that really works with the reply type as you say.

I'm skeptical to the Command wrapping. One less parameter, but it becomes more indirection for the most common usage, which is accessing the command message. The context parameter can often be ignored.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having the interface typed looks good, and for empty / initial state, and with the effects builder in the base rather than context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm skeptical to the Command wrapping. One less parameter, but it becomes more indirection for the most common usage, which is accessing the command message. The context parameter can often be ignored.

Yeah, you may be right. I just dislike having params that are not used. Specially if they are often ignored. They are always available and often ignored. Just noise.

I wish we didn't have that at all. Do we really need a commandName if we have already the command?

And what is a commandId exactly?
If that's something generated by the framework, why do I need such a thing in my domain? And if it's something that I need in my domain than that should be a field in my command.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we will be able to fully get rid of the context. There are several metadata related things that should be accessible, such as

CommandContext.commandName
CommandContext.commandId
MetadataContext.metadata

EntityContext.entityId
Context.serviceCallFactory

EventContext.sequenceNumber


ActionContext.eventSubject

UpdateHandlerContext.eventSubject
UpdateHandlerContext.commandName
ViewContext.viewId

Let me try the reply type first...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried with the base class and the effects builder from there. It works great. The part I was worried about was that type inference wouldn't work with the lambda in .thenReply(newState -> Empty.getDefaultInstance()). No problem, and the compiler error message is even good:

ShoppingCartImpl.java:[66,23] incompatible types: inference variable T has incompatible bounds
[ERROR]     equality constraints: com.google.protobuf.Empty
[ERROR]     lower bounds: java.lang.String

I tried with both Java 8 and 11.

Copy link
Member

@octonato octonato Jul 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's go by pieces.

I believe that some of then can be a constructor parameter or deemed obsolete (or not crucial). We will see..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to the base class approach: 23c7fb9
Looking good.

I'll give the context some more thought. We could always give access to it via the base class instead of a parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the context parameter, and it's accessed via the base class instead. Some runtime caveats but that is worth it. 05239d2

return Empty.getDefaultInstance();
return context.effectBuilder()
.updateState(newState) // <5>
.thenReply(Empty.getDefaultInstance()); // FIXME add convenience shortcut for reply Empty?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I was having the same thought.

We should consider is if it realistic to think that one day we will support other formats than protobuf. If not (most likely not), it's fine to bound ourselves to Empty.

An alternative is to have our own type (eg: Done) to replace Empty. And then promote the idea that if you just want to return an ack, you can use Done and the builder has a convenience for it.

Anyway, not a big deal if we are sure about having a long term relationship with proto's Empty

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already support json. I agree that this is just a convenience that can be added if needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than tying this to protobuf Empty specifically, it could be nicer if it returned a default instance of whatever the gRPC service method's return type is. I think that's supported in the JS SDK. Not sure how easy with the new approach, but possible if it has a handle on the method descriptor?

Copy link
Member

@octonato octonato Jul 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be interesting, but I don't know if I understand what you mean with a handle on the method descriptor.

If a method returns Foo, we need to return an Effect<Foo>. If the builder is not already typed on Foo we will need something like: replyWith(Foo.class) and later behind the scenes build a Foo.getDefaultInstance() on behalf of the user.

Between replyWith(Foo.class) and thenReply(Foo.getDefaultInstance()), we save some characters but we increase API surface for something that is not so hard for the users to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would work with return effects().replyWithEmpty(). The Foo can be inferred from the return type of the method. We would create an EmptyReply<Foo>, which we will translate to the Foo.getDefaultInstance().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and Foo.getDefaultInstance() would be constructed via the proto method from the gRPC service method that Peter mentioned.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was thinking of just replyWithEmpty() or replyWithDefault() or similar and the actual default instance created using the gRPC method descriptor.

import com.akkaserverless.javasdk.impl.reply.EffectImpl;

/** A side effect. */
public interface SideEffect {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Good to call it by its name!

}

@Override
public void handleSnapshot(ShoppingCartDomain.Cart cart) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we can consider adding would be conversion methods to/from storage representation, so the business logic in command and event handlers can use a more convenient state representation than the proto objects (or json). That class would be somewhat difficult to define in the proto codegen options though, unless it's defined by it's fully qualified java class name.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, working with protobuf objects directly is more awkward. With going to and from builders, changing anything deeper. Would be interesting to see what could be improved, whether that's conversions, or wrappers that make it easier to mutate, in the way lenses are used for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

created issue #102 for this idea

*
* <p><code>null</code> is an allowed value.
*/
protected abstract S emptyState();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should pass the entityId to the emptyState?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entityId will be passed to the factory lambda (constructor) via the CreationContext, so it's possible to keep that in an instance variable and accessed here

BREAKING CHANGE: many things renamed and replaced

* rename Effect to SideEffect
* add Effect and ValueEntityEffect
* add state mgmt in ValueEntityEffect
* builder for ValueEntityEffect
* deprecate context.fail
* add EventSourcedEffect and managed state
* remove CommandNotImplementedException
* change order parameters, currentState first
* move low level api to separate package
  * to get it out of the way, not intended for end-users
  * might be removed?
  * also moved the registration methods for low level api in AkkaServerless
    to separate .lowLevel() class
* rename message to reply
* add base class and effects via the base class
* remove context parameter, accessed via base class instead
* emptyState instead of Optional for ValueEntity also
* rename failure to error
* exclude samples from build
@patriknw patriknw changed the base branch from main to sdk-codegen-dev July 1, 2021 14:41
@patriknw patriknw marked this pull request as ready for review July 1, 2021 14:42
@patriknw
Copy link
Contributor Author

patriknw commented Jul 1, 2021

We are starting a feature branch sdk-codegen-dev and this PR is now squashed and targeting that.

@patriknw
Copy link
Contributor Author

patriknw commented Jul 1, 2021

Merging into feature branch

@patriknw patriknw merged commit 5b4fe30 into sdk-codegen-dev Jul 1, 2021
@patriknw patriknw deleted the wip-50-effect-patriknw branch July 1, 2021 16:00
johanandren pushed a commit that referenced this pull request Jul 6, 2021
* feat!: Spike of new Java SDK API

BREAKING CHANGE: many things renamed and replaced

* rename Effect to SideEffect
* add Effect and ValueEntityEffect
* add state mgmt in ValueEntityEffect
* builder for ValueEntityEffect
* deprecate context.fail
* add EventSourcedEffect and managed state
* remove CommandNotImplementedException
* change order parameters, currentState first
* move low level api to separate package
  * to get it out of the way, not intended for end-users
  * might be removed?
  * also moved the registration methods for low level api in AkkaServerless
    to separate .lowLevel() class
* rename message to reply
* add base class and effects via the base class
* remove context parameter, accessed via base class instead
* emptyState instead of Optional for ValueEntity also
* rename failure to error
* exclude samples from build
* exclude mvn test and integration tests
franciscolopezsancho added a commit to franciscolopezsancho/kalix-jvm-sdk that referenced this pull request Jul 22, 2021
* feat!: Spike of new Java SDK API

BREAKING CHANGE: many things renamed and replaced

* rename Effect to SideEffect
* add Effect and ValueEntityEffect
* add state mgmt in ValueEntityEffect
* builder for ValueEntityEffect
* deprecate context.fail
* add EventSourcedEffect and managed state
* remove CommandNotImplementedException
* change order parameters, currentState first
* move low level api to separate package
  * to get it out of the way, not intended for end-users
  * might be removed?
  * also moved the registration methods for low level api in AkkaServerless
    to separate .lowLevel() class
* rename message to reply
* add base class and effects via the base class
* remove context parameter, accessed via base class instead
* emptyState instead of Optional for ValueEntity also
* rename failure to error
* exclude samples from build
* exclude mvn test and integration tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants