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

[RFC] Parameterize context type of decorators #137

Merged
merged 10 commits into from
Nov 3, 2024
Merged

Conversation

jodersky
Copy link
Member

@jodersky jodersky commented Jul 23, 2024

This allows customizing the way by-type parameters can be read: instead of always reading data from a cask.Request, this allows a decorator to parameterize the context type and pass it in explicitly from the wrapFunction method.

Essentially, this makes the context parameter of ArgReaders (which are used to translate data from a HTTP request to a scala parameter) customizable for every decorator.

Motivation

The idea behind this proposal is to uniformize the way "named" and "by-type" parameters within the same endpoint are handled. By "named" parameters I mean parameters which are set from the Map[String, Input] in the delegate function, and by by-type parameters I mean parameters which are computed from an arity zero ArgReader, and thus use the cask.Request context to compute their value.

E.g.

@cask.get(/:foo/:bar)
def index(foo: String, bar: Int, cookie1: cask.Cookies, req: cask.Request)

In this case, foo and bar and "named" and cookie1 and req are "by-type".

Right now, the wrapFunction method handles how named parameters are set, and therefore can centrally do arbitrary pre- and post-processing. However, by-type parameters must always use a cask.Request to compute any values, which can be problematic if the computation is expensive or if any kind of state is maintained.

This pull request removes this asymmetry, by allowing the wrapFunction to compute a custom context which is computed once before being passed to all InputReaders

Implementation approach

  • add InputContext type parameter to decorators
  • add a parameter of type InputContext to delegate. type Delegate = (InputContext, Map[String, Input)) => ...
  • change Decorator.invoke() to carry a list of input contexts, one for each parameter list
  • also change the Entrypoint macros to use these input contexts rather than hardcoding cask.Request (currently I've only done the work to support Scala 3 for this)

This is a proof of concept PR. As of this writing, the code only works for Scala 3.

@@ -14,10 +15,10 @@ import cask.model.{Request, Response}
* to `wrapFunction`, which takes a `Map` representing any additional argument
* lists (if any).
*/
trait Decorator[OuterReturned, InnerReturned, Input] extends scala.annotation.Annotation {
trait Decorator[OuterReturned, InnerReturned, Input, InputContext] extends scala.annotation.Annotation {
Copy link
Member Author

Choose a reason for hiding this comment

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

this is the main change here, along with adding an extra parameter to delegate and changing invocation to accommodate these changes

@jodersky jodersky marked this pull request as ready for review July 24, 2024 15:57
Comment on lines +1 to +55
package app

case class Context(
session: Session
)

case class Session(data: collection.mutable.Map[String, String])

trait CustomParser[T] extends cask.router.ArgReader[Any, T, Context]
object CustomParser:
given CustomParser[Context] with
def arity = 0
def read(ctx: Context, label: String, input: Any): Context = ctx
given CustomParser[Session] with
def arity = 0
def read(ctx: Context, label: String, input: Any): Session = ctx.session
given literal[Literal]: CustomParser[Literal] with
def arity = 1
def read(ctx: Context, label: String, input: Any): Literal = input.asInstanceOf[Literal]

object DecoratedContext extends cask.MainRoutes{

class custom extends cask.router.Decorator[cask.Response.Raw, cask.Response.Raw, Any, Context]{

override type InputParser[T] = CustomParser[T]

def wrapFunction(req: cask.Request, delegate: Delegate) = {
// Create a custom context out of the request. Custom contexts are useful
// to group an expensive operation that may be used by multiple
// parameter readers or that carry state. This example focuses on carrying
// state.
val ctx = Context(Session(collection.mutable.Map.empty)) // this would typically be populated from a signed cookie

delegate(ctx, Map("user" -> 1337)).map{ response =>
val extraCookies = ctx.session.data.map(
(k, v) => cask.Cookie(k, v)
)

response.copy(
cookies = response.cookies ++ extraCookies
)
}

}
}

@custom()
@cask.get("/hello/:world")
def hello(world: String, req: cask.Request)(session: Session, user: Int) = {
session.data("hello") = "world"
world + user
}

initialize()
}
Copy link
Member Author

Choose a reason for hiding this comment

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

See this as a motivating example

@lihaoyi
Copy link
Member

lihaoyi commented Jul 26, 2024

I think this looks reasonable

@jodersky
Copy link
Member Author

jodersky commented Nov 2, 2024

@lihaoyi, I ported the changes to also work with scala 2, and included the example in the published ones. Let me know if you think this is now good to merge.

@lihaoyi
Copy link
Member

lihaoyi commented Nov 3, 2024

@jodersky looks good, you have commit access so go ahead and merge this and the other PRs when you're happy and tag a release

@jodersky jodersky merged commit d823a8a into master Nov 3, 2024
5 checks passed
@lolgab lolgab deleted the jo/inputcontext branch November 3, 2024 11:41
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.

2 participants