-
-
Notifications
You must be signed in to change notification settings - Fork 94
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
PUT
semantics for patching
#123
Comments
I think we could consider some modifier like: a.using[PatchA].allowNoneOverride.patch(patchA) I think we didn't have requests for patching modifiers, but since we have one (and might have in the future) it might make sense to introduce a syntax for passing these modifiers. @krzemin what do you think? |
@luksow isn't it a default behavior as for now? > case class A(int: Int, stringOpt: Option[String], doesntChange: Boolean)
defined class A
> case class PatchA(int: Int, stringOpt: Option[String])
defined class PatchA
> val x = A(0, Some("abc"), false)
x: A = A(0, Some("abc"), false)
> val y = x.patchWith(PatchA(10, None))
y: A = A(10, None, false)
> val z = y.patchWith(PatchA(20, Some("xyz")))
z: A = A(20, Some("xyz"), false) We also support optional patches which allows you to optionally deliver a value in a patch. This is extremely useful when delivering JSON for HTTP PATCH request that may contains optional fields. > case class OptPatchInt(int: Option[Int])
defined class OptPatchInt
> val u = y.patchWith(OptPatchInt(Some(30)))
u: A = A(30, None, false)
> val v = y.patchWith(OptPatchInt(None))
v: A = A(10, None, false) |
Oh, ok, than I'm a victim of confusion... and also should check my codebase for bugs 😆 I expected import java.util.UUID
import io.scalaland.chimney.dsl._
case class User(id: UUID, emailAddress: String, companyId: Option[UUID])
case class UserPatch(emailAddress: Option[String], companyId: Option[UUID])
case class UserPut(emailAddress: String, companyId: Option[UUID])
val testUser = User(UUID.fromString("5b2d5ece-e0c8-41d8-af58-07b11873eb6e"), "[email protected]", Some(UUID.fromString("5b6b0d88-9e6c-42e7-9fe2-203954fe5eaf")))
// PATCHing - only emailAddress changes
testUser.patchWith(UserPatch(Some("[email protected]"), None))
// res0: User = User(5b2d5ece-e0c8-41d8-af58-07b11873eb6e,[email protected],None)
// WRONG! I expected to (partial) patch only email, thus leaving companyId = 5b6b0d88-9e6c-42e7-9fe2-203954fe5eaf
// expected: User(5b2d5ece-e0c8-41d8-af58-07b11873eb6e,[email protected],5b6b0d88-9e6c-42e7-9fe2-203954fe5eaf)
// it'd be nice to hint expected behaviour here
// PUTing - both fields are replaced
testUser.patchWith(UserPut("[email protected]", None))
// res1: User = User(5b2d5ece-e0c8-41d8-af58-07b11873eb6e,[email protected],None)
// YEY! So I'd love to have methods:
Hope it's clear now. |
Oh, I see now. Let's summarize current semantics that consists of 2 rules:
Btw, results of your examples follows it, so it's not a bug. What might be problematic is the fact, that in case of It would be more convenient to have some dedicated support for cases where we have
I don't think this should be a default mode for all cases. Now there are few questions:
|
I believe that the default should be the current behavior. I expect, that some other people would come up with some modifiers case I cannot see today (so, 2 cases would not be enough). I think that leaving current behavior as a default and allowing modifiers (like with transformer) could be a good solution. @luksow ? |
Current behavior should not be changed for the sake of compatibility. Having said that, since I was confused once, I'll be probably confused again, so I'd like to have some kind of "modifiers" that are very explicit and tells me if I'm PATCHing or PUTing. I guess the most flexible API is one that allows you to define strategies. A strategy would be defining functions: // (entity value, patch value) => patched value
(A, A) => A
(A, Option[A]) => A
(Option[A], A) => Option[A]
(Option[A], Option[A]) => Option[A] With some default strategies defined in the lib (current, patch, put, ...). However, since I'm not doing anything crazy with chimney, I don't know how it plays with current customization options. |
Tried to realize how many possible semantics/strategies of handling optional fields we may have and my reasoning is as follows:
Let's draw a table of all reasonable semantics for all possible cases.
I'm pretty sure that cases 1-5 are obvious and the only reasonable choices (pls correct mi if I missed something). The only real choice we have in case 6., where current semantics is to take a value from patch - so the semantics is the same as in case 2, without distinguishing However we can drive this semantics with DSL modifiers introduced in #130. So little example how it could work: case class User(name: Option[String], age: Option[Int])
case class UserPatch(name: Option[String], age: Option[Int])
val user = User(Some("John"), Some(30))
val userPatch = UserPatch(None, None)
user.patchUsing(userPatch) // current behavior: User(None, None)
user
.using(userPatch)
.ignoreNoneInPatch
.patch // new behavior: User(Some("John"), Some(30)) Also realized that this proposal is about the same thing as #69. |
@krzemin I like your table, it clarifies this problem. TBH, I don't like your proposal, as it seems to be verbose while
Thanks for taking care of it! |
There are cases where you don't have For the sake of making decision explicitly visible, theoretically we could add For both transformers and patchers (since #130) DSLs we have set of boolean flags that drive semantics of generated code. Current convention is that these flags have their default values and users can change them using DSL. But for cases when they don't care - it's not required. Proposed |
That's true. However, I'd prefer to explicitly say what to do in case I update case classes later and don't really come back to chimney part 😉
Exactly my point. No-op is fine for me. Maybe there's another way of being explicit? Anyway, I can live with that, so that's not a big issue once we have |
One can argue you can do it anyway - for option cleaning behavior you call
I'm pretty sure that better API that exposes boolean flags in more visible way can be proposed, but I would like to treat all the flags (including transformers' ones) in the same way. So it's rather a proposal for another ticket. If you really require no-op combinator, you can simply define extension method on |
Resolved in #131 . |
Hi,
First of all, thanks for all the great work!
Recently I came across use case that I believe is not covered by chimney.
I have
case class A(int: Int, stringOpt: Option[String], doesntChange: Boolean)
and then I'd like to do a
PUT
-like update with a case classcase class PatchA(int: Int, stringOpt: Option[String])
which is a subset of
A
.Using
patchWith
would result in checking ifstringOpt
is defined inPatchA
and appling change only if it'sSome
. However, I'd expect a direct assignment, so ifstringOpt
inPatchA
is None,stringOpt
in patched version ofA
should beNone
as well.Use case: it's very useful if you're working on some REST service and you support both PATCH (partial update) and PUT.
The text was updated successfully, but these errors were encountered: