-
Notifications
You must be signed in to change notification settings - Fork 412
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
Add Flash, Flash.Message and Flash.Backend #2481
Conversation
6d4a851
to
04661b7
Compare
Codecov ReportAttention:
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## main #2481 +/- ##
==========================================
+ Coverage 64.75% 65.14% +0.39%
==========================================
Files 135 137 +2
Lines 7135 7291 +156
Branches 1200 1285 +85
==========================================
+ Hits 4620 4750 +130
- Misses 2515 2541 +26
☔ View full report in Codecov by Sentry. |
34e1365
to
65d0f40
Compare
65d0f40
to
cc778b1
Compare
9b7fcff
to
ea70b8a
Compare
@TomTriple To overcome the cookie content size limitation we could split the content of the flash into several cookies. WDYT? |
def flatMap[B](f: A => Flash[B]): Flash[B] = Flash.FlatMap(self, f) | ||
|
||
def map[B](f: A => B): Flash[B] = self.flatMap(a => Flash.succeed(f(a))) | ||
|
||
def orElse[B >: A](that: => Flash[B]): Flash[B] = Flash.OrElse(self, that) | ||
|
||
/** | ||
* Operator alias for `orElse`. | ||
*/ | ||
def <>[B >: A](that: => Flash[B]): Flash[B] = self.orElse(that) | ||
|
||
def zip[B](that: => Flash[B]): Flash[(A, B)] = self.zipWith(that)((a, b) => a -> b) | ||
|
||
/** | ||
* Operator alias for `zip`. | ||
*/ | ||
def <*>[B](that: => Flash[B]): Flash[(A, B)] = self.zip(that) | ||
|
||
def zipWith[B, C](that: => Flash[B])(f: (A, B) => C): Flash[C] = | ||
self.flatMap(a => that.map(b => f(a, b))) | ||
|
||
def optional: Flash[Option[A]] = self.map(Option(_)) <> Flash.succeed(None) | ||
|
||
def foldHtml[A1 >: A, B](f: Html => B, g: Html => B)(h: (B, B) => B)(implicit | ||
ev: A1 =:= Flash.Message[Html, Html], | ||
): Flash[B] = | ||
self.map(a => a.asInstanceOf[A1].fold(f, g)(h)) | ||
|
||
def toHtml[A1 >: A](implicit ev: A1 =:= String): Flash[Html] = | ||
self.map(Html.fromString(_)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we need these methods to be final
? Is there a need for them to be overridable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't hurt to make them final!
} | ||
} | ||
|
||
val layer: ULayer[Backend] = ZLayer(Ref.make(Map.empty[UUID, Map[String, String]]).map(Impl.apply)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not see the point of providing a flash backend based on a Map as most probably users' apps will be horizontally scaled. For dev purposes? 🤔
What if we rename this layer to inMemory
then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you're right - when you're scaling the app that does not work anymore. In this case one needs to use an external data service. I think it should be possible to add another Backend
with the given interface.
Yes, calling it inMemory
is a good idea!
Flash.COOKIE_NAME, | ||
URLEncoder.encode( | ||
JsonCodec.jsonEncoder(Schema[Map[String, String]]).encodeJson(run(setter, Map.empty)).toString, | ||
java.nio.charset.Charset.defaultCharset.toString.toLowerCase, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not UTF8? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can not hurt to make that explicit!
), | ||
) | ||
|
||
def run[A](setter: Setter[A], map: Map[String, String]): Map[String, String] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could inline this run
function into the previous one, no?
def run[A](setter: Setter[A]): Cookie.Response = {
def loop(setter: Setter[A], map: mutable.Map[String, String]): mutable.Map[String, String] =
setter match {
case SetValue(schema, key, a) =>
map += (key -> JsonCodec.jsonEncoder(schema).encodeJson(a).toString)
case Concat(left, right) =>
loop(right, loop(left, map))
case Empty => map
}
Cookie.Response(
Flash.COOKIE_NAME,
URLEncoder.encode(
JsonCodec.jsonEncoder(Schema[Map[String, String]]).encodeJson(loop(setter, mutable.Map.empty).toMap).toString,
java.nio.charset.Charset.defaultCharset.toString.toLowerCase,
)
)
}
Or you want to expose both function maybe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Internally there both run
operations are used
/** | ||
* Gets an `A` from the backend-based flash-scope and provides a fallback. | ||
*/ | ||
def flashOrElse[A](request: Request, flash: Flash[A])(orElse: => A): UIO[A] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
* Optionally adds flash values to the backend-based flash-scope and returns | ||
* a workflow with an updated `Response`. | ||
*/ | ||
def addFlash[A](response: Response, setterOpt: Option[Flash.Setter[A]]): UIO[Response] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
/** | ||
* Combines setting this flash value with another setter `that`. | ||
*/ | ||
def ++[B](that: => Setter[B]): Setter[(A, B)] = Setter.Concat(self, that) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
map.keys.map(a => Flash.get(a)(Schema[A])).reduce(_ <> _) | ||
} | ||
|
||
private def loop[A](flash: Flash[A], map: Map[String, String]): Either[Throwable, A] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why using Throwable
here in the Left part and not just String? 🤔
Using String would simplify the code a bit, no?
🟢 Also, as this function is only used in the run
function below (I'm maybe wrong here), I'd put it as a inner function of the run
function:
private[http] def run[A](flash: Flash[A], sourceMap: Map[String, String]): Either[Throwable, A] = {
def loop[A](flash: Flash[A], map: Map[String, String]): Either[Throwable, A] = ...
loop(flash, sourceMap)
}
.flatMap { cookie => | ||
try Right(URLDecoder.decode(cookie.content, java.nio.charset.Charset.defaultCharset.toString.toLowerCase)) | ||
catch { | ||
case e: Exception => Left(e) | ||
} | ||
} | ||
.flatMap { cookieContent => | ||
JsonCodec.jsonDecoder(Schema.map[String, String]).decodeJson(cookieContent).left.map(e => new Throwable(e)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟢 For performances, I'd inline these 2 .flatMap
:
.flatMap { cookie =>
try {
val content = URLDecoder.decode(cookie.content, java.nio.charset.Charset.defaultCharset.toString.toLowerCase))
JsonCodec.jsonDecoder(Schema.map[String, String]).decodeJson(content).left.map(e => new Throwable(e))
} catch {
case e: Exception => Left(e)
}
}
I think that's a interesting proposal - even though the limit then is at about 300 cookies (which... should be enough for most / all? things regarding payload data in the flash). I like your idea but right now I´m not sure about any other side effects that come up with it. |
@TomTriple We could keep this "split into several cookies" idea for another PR, I just wanted to mention it 🙂 |
Co-authored-by: Jules Ivanic <jules.ivanic@gmail.com>
Co-authored-by: Jules Ivanic <jules.ivanic@gmail.com>
Co-authored-by: Jules Ivanic <jules.ivanic@gmail.com>
Yes of course - we will see what other reviewers think about it. I'll send another PR with your changes from the review. Thanks a lot for reviewing 👍 |
.flatMap { cookie => | ||
try { | ||
val content = | ||
URLDecoder.decode(cookie.content, java.nio.charset.Charset.forName("UTF-8").toString.toLowerCase) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't you just use StandardCharsets.UTF_8
? 🤔 Same for the .encode
usage
URLDecoder.decode(cookie.content, java.nio.charset.Charset.forName("UTF-8").toString.toLowerCase) | |
URLDecoder.decode(cookie.content, StandardCharsets.UTF_8) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, somehow I did not know about StandardCharsets
03ef4d5
to
00873c6
Compare
00873c6
to
47e8268
Compare
Same idea as in #2466. Summary:
Flash
. For that there exists variousFlash#get
as well asFlash#set
operators.Flash.Message
that adds an explicit type for typical use cases of flash messages representing anotice
oralert
(or both!). Inspired by https://guides.rubyonrails.org/action_controller_overview.html#the-flash.Response#addFlash
/Request#flash
.Flash.Backend
) flash-scope that has no limitation in size as the payload is not transported in a cookie but kept in an internal structure. Still, it uses a cookie to transport an identifier for the lookup into the internal structure. Using this flash-scope involves effects as we need to access an internalRef
for the payload.