Giter Club home page Giter Club logo

cask's People

Contributors

anatoliykmetyuk avatar ckipp01 avatar danielmiddleton avatar emilbahnsen avatar he-pin avatar jodersky avatar lihaoyi avatar lihaoyi-databricks avatar lolgab avatar manuelweiss avatar megri avatar nicolaenmv avatar nicolasstucki avatar rom9 avatar syspulse avatar vincenzobaz 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  avatar

cask's Issues

Suppress match may not be exhaustive compiler warning

I am on Scala 2.13 and I like to enable compiler warnings on non-exhaustive pattern matching but when I enable it, the following piece of code:

@cask.get(s"/app/:app")
  def routeApp(app: String) =
    s"Hello $app!"

@cask.staticFiles("/js/")
  def scalaJs() =
    "web/target/scala-2.13/web-fastopt/"

spits these warnings:

[info] compiling 1 Scala source to target/scala-2.13/classes ...
[error] Router.scala:25:7: match may not be exhaustive.
[error]   def routeApi(req: cask.Request) =
[error]       ^
[error] Router.scala:16:7: match may not be exhaustive.
[error] It would fail on the following input: (x: Seq[?] forSome x not in Nil)
[error]   def scalaJs() =
[error]       ^

I cannot myself add @unchecked anywhere because the code is being generated by macro expansion. So perhaps in the macro expansion itself, can we add the @unchecked annotation?

[request] Some mechanism to add CORS-rules to an endpoint/a router/everything.

Here's what I've found so far:

  • Adding a custom decorator won't work for OPTIONS-requests as the routes are tried before the decorators come into play.
  • Overriding the Main#defaultHandler seems to be the way forward but this feels very "low-level" and introduces some concern-splitting

I'm willing to do some legwork here but I'd like to know if you have any ideas on how this can best be solved.

It is not possible to handle unknown query parameters

Sometimes it would be handy to allow any unknown query parameters.

Motivation 1:

One of my endpoints is used for GitHub OAuth loopback. While the behavior in case of success is well documented as receiving code and state parameters, in case of error the documentation is lacking. While I have experimentally determined the parameters in such case to be error, error_description and error_uri, this is undocumented and perhaps could change anytime, causing my error handling not working as expected. Moreover. I am not really interested in some of them, but I cannot prevent them being included. At the moment I have to add them to the endpoint just to ignore them.

Motivation 2:

I am processing query parameters using request.queryParams in my endpoint, yet I have to list all possible parameter names in my endpoint signature. This contrast with subPath option available for paths. I can easily create an endpoint serving any subpath, but not any query parameters.

Type safe decorators?

Hi there - thanks for this!

I notice that the decorators populate downstream with string based keys:

  class withExtra extends cask.RawDecorator {
    def wrapFunction(ctx: cask.Request, delegate: Delegate) = {
      delegate(Map("extra" -> 31337))
    }
  }

  @withExtra()
  @cask.get("/hello/:world")
  def hello(world: String)(extra: Int) = {
    world + extra
  }

Is there a way/a plan to make these type safe (enums for example)? My concern is that renaming the parameter name won't pick up the string key in the decorator's output, but this won't be picked up until runtime.

Thanks!

`./mill -i` build repl, type mismatch error

Hello,
tried to get the cask build repl running by issuing ./mill -i on linux ubuntu 19.04 bash, scalac vesion 2.11.12.
Any ideas what's wrong? (It's a plain git clone from this repo here).
Thx.

$ ./mill -i
Loading...
Compiling /home/hape/cask/build.sc
build.sc:133: object ci is not a member of package ammonite.$file
def publishVersion = T.input($file.ci.version.publishVersion)
                                   ^
build.sc:145: type mismatch;
 found   : Any
 required: ujson.Value
          "name" -> releaseTag
                    ^
build.sc:153: object example is not a member of package ammonite.$file
    $file.example.compress.build.millSourcePath,
          ^
build.sc:154: object example is not a member of package ammonite.$file
    $file.example.compress2.build.millSourcePath,
          ^
build.sc:155: object example is not a member of package ammonite.$file
    $file.example.compress3.build.millSourcePath,
          ^
build.sc:156: object example is not a member of package ammonite.$file
    $file.example.cookies.build.millSourcePath,
          ^
build.sc:157: object example is not a member of package ammonite.$file
    $file.example.decorated.build.millSourcePath,
          ^
build.sc:158: object example is not a member of package ammonite.$file
    $file.example.decorated2.build.millSourcePath,
          ^
build.sc:159: object example is not a member of package ammonite.$file
    $file.example.endpoints.build.millSourcePath,
          ^
build.sc:160: object example is not a member of package ammonite.$file
    $file.example.formJsonPost.build.millSourcePath,
          ^
build.sc:161: object example is not a member of package ammonite.$file
    $file.example.httpMethods.build.millSourcePath,
          ^
build.sc:162: object example is not a member of package ammonite.$file
    $file.example.minimalApplication.build.millSourcePath,
          ^
build.sc:163: object example is not a member of package ammonite.$file
    $file.example.minimalApplication2.build.millSourcePath,                                      
          ^                                                                                      
build.sc:164: object example is not a member of package ammonite.$file
    $file.example.redirectAbort.build.millSourcePath,                                            
          ^                                                                                      
build.sc:165: object example is not a member of package ammonite.$file
    $file.example.scalatags.build.millSourcePath,                                                
          ^                                                                                      
build.sc:166: object example is not a member of package ammonite.$file
    $file.example.staticFiles.build.millSourcePath,                                              
          ^                                                                                      
build.sc:167: object example is not a member of package ammonite.$file
    $file.example.staticFiles2.build.millSourcePath,                                             
          ^                                                                                      
build.sc:168: object example is not a member of package ammonite.$file
    $file.example.todo.build.millSourcePath,                                                     
          ^                                                                                      
build.sc:169: object example is not a member of package ammonite.$file
    $file.example.todoApi.build.millSourcePath,                                                  
          ^                                                                                      
build.sc:170: object example is not a member of package ammonite.$file
    $file.example.todoDb.build.millSourcePath,                                                   
          ^                                                                                      
build.sc:171: object example is not a member of package ammonite.$file
    $file.example.twirl.build.millSourcePath,                                                    
          ^                                                                                      
build.sc:172: object example is not a member of package ammonite.$file
    $file.example.variableRoutes.build.millSourcePath,                                           
          ^                                                                                      
build.sc:173: object example is not a member of package ammonite.$file
    $file.example.websockets.build.millSourcePath,                                               
          ^                                                                                      
build.sc:174: object example is not a member of package ammonite.$file
    $file.example.websockets2.build.millSourcePath,                                              
          ^                                                                                      
build.sc:175: object example is not a member of package ammonite.$file
    $file.example.websockets3.build.millSourcePath,                                              
          ^                                                                                      
build.sc:176: object example is not a member of package ammonite.$file
    $file.example.websockets4.build.millSourcePath,                                              
          ^                                                                                      
build.sc:134: object ci is not a member of package ammonite.$file
def gitHead = T.input($file.ci.version.gitHead)                                                  
                            ^                                                                    
build.sc:130: object example is not a member of package ammonite.$file
  object websockets4 extends $file.example.websockets4.build.AppModule with LocalModule          
                                   ^                                                             
build.sc:129: object example is not a member of package ammonite.$file
  object websockets3 extends $file.example.websockets3.build.AppModule with LocalModule          
                                   ^                                                             
build.sc:128: object example is not a member of package ammonite.$file
  object websockets2 extends $file.example.websockets2.build.AppModule with LocalModule          
                                   ^                                                             
build.sc:127: object example is not a member of package ammonite.$file
  object websockets extends $file.example.websockets.build.AppModule with LocalModule            
                                  ^                                                              
build.sc:126: object example is not a member of package ammonite.$file
  object variableRoutes extends $file.example.variableRoutes.build.AppModule with LocalModule    
                                      ^                                                          
build.sc:125: object example is not a member of package ammonite.$file
  object twirl extends $file.example.twirl.build.AppModule with LocalModule                      
                             ^                                                                   
build.sc:124: object example is not a member of package ammonite.$file
  object todoDb extends $file.example.todoDb.build.AppModule with LocalModule                    
                              ^                                                                  
build.sc:123: object example is not a member of package ammonite.$file
  object todoApi extends $file.example.todoApi.build.AppModule with LocalModule                  
                               ^                                                                 
build.sc:122: object example is not a member of package ammonite.$file
  object todo extends $file.example.todo.build.AppModule with LocalModule                        
                            ^                                                                    
build.sc:121: object example is not a member of package ammonite.$file
  object staticFiles2 extends $file.example.staticFiles2.build.AppModule with LocalModule        
                                    ^                                                            
build.sc:120: object example is not a member of package ammonite.$file
  object staticFiles extends $file.example.staticFiles.build.AppModule with LocalModule          
                                   ^                                                             
build.sc:119: object example is not a member of package ammonite.$file
  object scalatags extends $file.example.scalatags.build.AppModule with LocalModule              
                                 ^                                                               
build.sc:118: object example is not a member of package ammonite.$file
  object redirectAbort extends $file.example.redirectAbort.build.AppModule with LocalModule      
                                     ^
build.sc:117: object example is not a member of package ammonite.$file
  object minimalApplication2 extends $file.example.minimalApplication2.build.AppModule with LocalModule
                                           ^
build.sc:116: object example is not a member of package ammonite.$file
  object minimalApplication extends $file.example.minimalApplication.build.AppModule with LocalModule
                                          ^
build.sc:115: object example is not a member of package ammonite.$file
  object httpMethods extends $file.example.httpMethods.build.AppModule with LocalModule
                                   ^
build.sc:114: object example is not a member of package ammonite.$file
  object formJsonPost extends $file.example.formJsonPost.build.AppModule with LocalModule
                                    ^
build.sc:113: object example is not a member of package ammonite.$file
  object endpoints extends $file.example.endpoints.build.AppModule with LocalModule
                                 ^
build.sc:112: object example is not a member of package ammonite.$file
  object decorated2 extends $file.example.decorated2.build.AppModule with LocalModule
                                  ^
build.sc:111: object example is not a member of package ammonite.$file
  object decorated extends $file.example.decorated.build.AppModule with LocalModule
                                 ^
build.sc:110: object example is not a member of package ammonite.$file
  object cookies extends $file.example.cookies.build.AppModule with LocalModule
                               ^
build.sc:109: object example is not a member of package ammonite.$file
  object compress3 extends $file.example.compress3.build.AppModule with LocalModule
                                 ^
build.sc:108: object example is not a member of package ammonite.$file
  object compress2 extends $file.example.compress2.build.AppModule with LocalModule
                                 ^
build.sc:107: object example is not a member of package ammonite.$file
  object compress extends $file.example.compress.build.AppModule with LocalModule
                                ^
build.sc:32: `T.command` definitions must have 1 parameter list
  def publishVersion = build.publishVersion()._2
      ^
Compilation Failed

Add flag to ignore unused endpoint parameters

Currently, you cannot write an endpoint that takes a dynamic set of query parameters by manipulating req.queryParams, as unknown query parameters are rejected during input validation. While that's a reasonable default, we should provide a flag to disable that validation so a developer can work with req.queryParams fully dynamically if that's what they want

Path with different query parameters should be considered unique

The overloading based on query parameters is inconsistent. When checking routes for overlaps, the parameters are ignored, yet when serving the requests, the overloads are selected based on them.

Following routes are rejected as invalid.

  @cask.get("/hello")
  def hello() = {
    "Hello World!"
  }

  @cask.get("/hello")
  def helloName(name: String) = {
    s"Hello ${name.reverse}"
  }

The error is:

More than one endpoint has the same path: get /hello, get /hello

However when I remove the helloName parameter, requests with the query parameter name are not served.

I suggest query parameters are considered similar to path segments, as following similar routes are valid:

  @cask.get("/hello")
  def hello() = {
    "Hello World!"
  }

  @cask.get("/hello/:name")
  def helloName(name: String) = {
    s"Hello ${name.reverse}"
  }

Unable to (or do not know how) pass query parameters

I am not able to process even simple requests with query parameters.

Assume request http://localhost:8080/?param=hi

Following does not work:

object MinimalApplication extends cask.MainRoutes {
  @cask.get("/")
  def hello(request: cask.Request) = {
    s"Hello"
  }
  initialize()
}

The error is:

Unknown argument: "param"

Arguments provided did not match expected signature:

hello
request cask.Request

Not knowing how to fix that, I have tried:

object MinimalApplication extends cask.MainRoutes {
  @cask.get("/")
  def hello(request: cask.Request)(param: String) = {
    s"Hello"
  }
  initialize()
}

Now I get an error:

Unknown argument: "param"

Arguments provided did not match expected signature:

hello
param String

I did not find any documentation about query params. I do not see any annotation like @cask.Cookie, maybe there is a way, but not documented?

Optional query parameters do not work as expected

Routes with optional query parameters return a 400 response when the query parameter is not provided. Below is a minimal repro example:

package app

import scala.annotation.nowarn

@nowarn
object MinimalApplication extends cask.MainRoutes {
  @cask.get("/")
  def hello(param: Option[String]) = {
    "Hello World!"
  }

  initialize()
}

I ran this application and then ran the following in my terminal:

$ curl http://localhost:8080?param=asdf
Hello World!
$ curl http://localhost:8080
Missing argument: (param: Option[String])

Arguments provided did not match expected signature:

hello
  param  Option[String]

The expected behavior is that both curl requests succeed.

Twirl template gets wrapped by additional HTML

Hi, thank you for the great project. I ran into an issue, which prevents me from using twirl. Scalatags works without issues.

package app
object Twirl extends cask.MainRoutes{
  @cask.get("/")
  def hello() = {
    "<!doctype html>" + html.hello("Hello World")
  }

  initialize()
}

This example from the docs does not work. It gets wrapped by additional HTML instead:

<html data-lt-installed="true"><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">&lt;!doctype html&gt;&lt;html&gt;
&lt;body&gt;
&lt;h1&gt;Hello World&lt;/h1&gt;
&lt;p&gt;I am cow&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</pre></body></html>

Still maintained?

Hi all - is this project still maintained? I notice commits have slowed down and issues aren't being addressed.

Not trying to ruffle feathers(!) - I'm just trying to get the lay of the land.

Support for URL rewriting?

Hi,

I'm trying to use case for a very simple website: a few web pages wrapping a simple REST API. My routes look like this:

  @cask.getJson("/api/")
  def api(exprType: ExprType, expr: String) = ???

  @cask.staticFiles("/www/")
  def sourcesStaticFileRoute() = STATIC_PATH

  @cask.get("/")
  def indexRedirect() = cask.Redirect("/www/index.html")

There are two problems with this approach:

  1. Users see the redirect to /www/index.html.
  2. Static files are served under /www instead of /

In Apache or nginx I would use a URL rewrite instead to rewrite / to /www/ transparently except when the path starts with /api. I have not found a way to do this in Cask. Hence, I have three questions:

  1. Is there a way to do a recursive resolution on the server side for a single file, so that I could, in the body of the / route, call the staticFiles route to get the contents of /www/index.html?
  2. More generally, is there a way to do URL rewriting for a whole URL prefix, so that I could redirect all / queries to /www, except /api?
  3. Is there a way to have a staticFiles route that overlaps with other routes, to be used as a fallback when other routes don't match?

The main blocker for me is the index.html rewrite; I can live with the non-overlapping routes.

Thanks a lot!

Feature Requests

Are these improvements being considered for implementation?

  1. Concurrency

Use Java Virtual Threads (Loom) (500USD Bounty)

Spawn a virtual thread for each incoming request, which would enable asynchronous behaviour and increase performance of the library. The new Java virtual threads are lightweight and can replace the traditional Java threads. Given that Cask does not have any strong async/concurrency model of its own, leveraging the new JVM feature would be really nice.

Haoyi: To incentivize contribution, I'm putting a 500USD bounty on resolving this ticket. This is payable via bank transfer, and at my discretion in case of ambiguity. The acceptance criteria is a PR implementing support for virtual threads, tests exercising them, and performance benchmarks (ad-hoc is OK) demonstrating a performance improvement (e.g. in high concurrency use cases) vs normal threads in Java 21+

Serving css / js appears to have incorrect / no mime types?

To be clear - I'm unclear whether this is actually expected behaviour or not.

in "dev"

@cask.staticResource("assets")
def s() = "assets"

Works fine, where my html file has something like

<link rel = "stylesheet" href = "assets/css/todo.css"/>

However, once I "publish" (for me that looks like going through sbt stage, it's hosted somewhere I don't understand on AWS), and the environment changes to goes through https, some authentication wrapper etc...

the styles are not applied. I can see them being correctly loaded in the browser... but ... no style.

This appears to fix my problem... i.e. manually butcher some contents types into the resource routes


  class myStaticResources(val path: String,
                        resourceRoot: ClassLoader = classOf[myStaticResources].getClassLoader,
                        headers: Seq[(String, String)] = Nil)
    extends HttpEndpoint[String, Seq[String]]{
    val methods = Seq("get")    
    type InputParser[T] = QueryParamReader[T]
    override def subpath = true
    
    def wrapFunction(ctx: Request, delegate: Delegate) = {
      delegate(Map
      ()).map(t => {                
        println(ctx.remainingPathSegments.headOption)
        val headersOut : Seq[(String, String)] = ctx.remainingPathSegments.headOption match {
          case Some(s) if s.takeRight(2) == "js" => Seq(("Content-Type", "text/javascript"))
          case Some(s) if s.takeRight(3) == "css" => Seq(("Content-Type", "text/css"))
          case Some(s) if s.takeRight(4) == "html" => Seq(("Content-Type", "text/html"))
          case _ => Nil
        }       
        cask.model.StaticResource(StaticUtil.makePath(t, ctx), resourceRoot, headersOut)
      }
      )
    }

    def wrapPathSegment(s: String): Seq[String] = Seq(s)
  }

@ myStaticResources("assets")
def s() = "assets"

But I have to say the behaviour was non-obvious to me... I didn't spot it in the docs - is there an existing decorator for this? It feels like something which should be "standard", but then perhaps there is complexity here I don0t understand. The underlying cause is the CSS being served as "text/plain" rather than "text/css"

Pong message actually sends a Ping frame

There seems to be a typo in WebSocketEndpoint.scala. Sending a Pong message to a WsChannelActor, translates to a WebSockets.sendPingBlocking(...) call on the underlying library. I think it should call sendPongBlocking instead.

case Ws.Pong(value) => WebSockets.sendPingBlocking(ByteBuffer.wrap(value), channel)

This makes it impossible to implement a standard ping/pong keepalive.

Steps to reproduce

  • Implement a simple route which responds to ping messages with a pong, as per the websocket RFC:
      cask.WsActor {
        case cask.Ws.Ping(data) => channel.send(cask.Ws.Pong(data))
      }
  • Verify the behavior with a tool like websocat:
$ websocat -vv --ping-interval 3 ws://127.0.0.1:8080/connect
...
[INFO  websocat::ws_client_peer] Connected to ws
[DEBUG websocat::ws_client_peer] Starting pinger
[INFO  websocat::ws_peer] Sending WebSocket ping
[DEBUG websocat::ws_peer] incoming ping
[INFO  websocat::ws_peer] Sending WebSocket ping
[DEBUG websocat::ws_peer] incoming ping
[INFO  websocat::ws_peer] Sending WebSocket ping
[DEBUG websocat::ws_peer] incoming ping
[INFO  websocat::ws_peer] Sending WebSocket ping
[DEBUG websocat::ws_peer] incoming ping
^C

The above looks wrong. The endpoint should respond to a ping with a pong, like this:

...
[INFO  websocat::ws_client_peer] Connected to ws
[DEBUG websocat::ws_client_peer] Starting pinger
[INFO  websocat::ws_peer] Sending WebSocket ping
[INFO  websocat::ws_peer] Received a pong from websocket
[INFO  websocat::ws_peer] Sending WebSocket ping
[INFO  websocat::ws_peer] Received a pong from websocket
[INFO  websocat::ws_peer] Sending WebSocket ping
[INFO  websocat::ws_peer] Received a pong from websocket
[INFO  websocat::ws_peer] Sending WebSocket ping
[INFO  websocat::ws_peer] Received a pong from websocket
^C

PR incoming ;-)

Need observability hook for "failed" routes.

I've been trying to set up distributed tracing for Cask, and found a lack of hooks I need to set things up.

A few requests (or help in docs):

  • Decorators that are able to know the http status code of the result
  • A mechanism to register a decorator for all failed routes (for metrics + traces)
  • Some (efficient) way to annotate the size of request/response bytes

If you're curious what we have, here's a simple library I threw together to give Cask the "best possible" OpenTelemetry HTTP experience: https://github.com/GoogleCloudPlatform/scala-o11y-cui-showcase/tree/main/utils/src/main/scala/com/google/example/o11y/cask

The meat of the implementation is all within the @traced decorator

SBT version conflict error when cask and os-lib are used together

SBT version is 1.8.0.

Here is build.sbt:

lazy val root = project
  .in(file("."))
  .settings(
    scalaVersion := "3.2.1",
    libraryDependencies ++= Seq(
      "com.lihaoyi"   %% "cask"     % "0.8.3",
      "com.lihaoyi"   %% "os-lib"   % "0.9.0",
    )
  )

The error:

[error] (update) found version conflict(s) in library dependencies; some are suspected to be binary incompatible:
[error]
[error]         * com.lihaoyi:geny_3:1.0.0 (early-semver) is selected over {0.6.10, 0.7.1}
[error]             +- com.lihaoyi:os-lib_3:0.9.0                         (depends on 1.0.0)
[error]             +- com.lihaoyi:upickle-core_3:1.6.0                   (depends on 0.7.1)
[error]             +- com.lihaoyi:cask-util_3:0.8.3                      (depends on 0.6.10)
[error]
[error]
[error] this can be overridden using libraryDependencySchemes or evictionErrorLevel

Cask returns 405 for an undefined route that uses a method not used by other routes.

As tested with Scala 3 and cask 0.7.11

Server definition

class ServerRoutes(using cask.Logger) extends cask.Routes:
    @cask.get("/foo")
    def foo() = "get foo!"

    @cask.post("/bar")
    def bar() = "post bar!"

    initialize()


object server extends cask.Main:
    val allRoutes = Seq(ServerRoutes())

Example queries with responses

> curl -D - 'localhost:8080/foo' 
HTTP/1.1 200 OK
Connection: keep-alive
Content-Type: text/plain; charset=utf-8
Content-Length: 3
Date: Sun, 16 May 2021 19:57:13 GMT

get foo!
> curl -D - 'localhost:8080/bar'
HTTP/1.1 404 Not Found
Connection: keep-alive
Content-Type: text/plain; charset=utf-8
Content-Length: 20
Date: Sun, 16 May 2021 19:57:16 GMT

Error 404: Not Found

> curl -D - -XPUT 'localhost:8080/baz'
HTTP/1.1 405 Method Not Allowed
Connection: keep-alive
Content-Type: text/plain; charset=utf-8
Content-Length: 29
Date: Sun, 16 May 2021 19:57:24 GMT

Error 405: Method Not Allowed

Expected outcomes

Request -> response 1 is expected
Request -> response 2 should be 405 but is 404
Request -> response 3 should be 404 but is 405

[Question] Async support

First of all, thanks @lihaoyi for your work on this library. There have been discussions in my team whether we should drop Akka HTTP in favor of something more simple, and Cask looks very promising in this regard.

Before jumping into prototyping I have a question: The docs mention that cask intentionally does not support async as means to keep the library simple. I'm wondering, what does this mean exactly?

  • Does this mean that request processing is blocking, i.e. it can only handle one request at a time?
  • How should you deal with async route logic, like accessing a database using a library that returns Futures?

Collected route tries are non-exhaustive, leading to `java.util.NoSuchElementException` on missing route method

Since #27 I believe, Main.DefaultHandler throws a NoSuchElementException for any incoming request which uses a method that's not used in any route (this also oddly enough results in a 200 response).

The is the line that triggers the error: https://github.com/lihaoyi/cask/blob/4617b7e1f70eaa902b1aa492413a3a8ddc1491dd/cask/src/cask/main/Main.scala#L89

I think this could be resolved by adding a .withDefaultValue(DispatchTrie.empty) to the map in Main.prepareRouteTries, provided a static empty instance of DispatchTrie is possible to create. Another solution is to change L89 to a routeTries.get(effectiveMethod), but I believe that would add more overhead to each call and also make it easier for users to fall into the trap of missing this peculiarity if overriding the defaultHandler method on Main.

edit: A use-site workaround for now is to override the default routeTries with

  override def routeTries: Map[String,DispatchTrie[(Routes, EndpointMetadata[_])]] = 
    super.routeTries.withDefaultValue(DispatchTrie.construct(-1, Seq.empty))

Ammonite cannot download cask

If i run the following file with ammonite (amm -w sdk_ui_server.sc):

interp.load.ivy("com.lihaoyi" %% "cask" % "0.5.0")

@

object StaticFiles extends cask.MainRoutes{

  @cask.staticFiles("/identity-ui.js")
  def staticFileRoutes() = "/Users/guillaumebersac/projects/identity-web-ui-sdk/umd/identity-ui.js"

  initialize()
}

I get the following error:

ompiling /Users/guillaumebersac/projects/sdk_ui_server.sc
java.lang.Exception: Failed to resolve ivy dependencies:Error downloading com.lihaoyi:cask_2.12:0.5.0
  not found: /Users/guillaumebersac/.ivy2/local/com.lihaoyi/cask_2.12/0.5.0/ivys/ivy.xml
  not found: https://repo1.maven.org/maven2/com/lihaoyi/cask_2.12/0.5.0/cask_2.12-0.5.0.pom
  ammonite.interp.Interpreter$DefaultLoadJar.ivy(Interpreter.scala:663)
  ammonite.$file.sdk_ui_server$.<init>(sdk_ui_server.sc:1)
  ammonite.$file.sdk_ui_server$.<clinit>(sdk_ui_server.sc)

Note: if I replace the first line by interp.load.ivy("com.typesafe.akka" %% "akka-stream-kafka" % "0.22"), it works as expected, so it looks like a cask related issue.

uPickle 0.7.1 compatibility

Cask version 0.1.9 throws runtime exceptions when used with uPickle 0.7.1. Exception is:

ClassNotFoundException: uJson.Visitor

HTTPS?

It would be nice if cask had some built-in way of encrypting communications via TLS / HTTPS.

Modules were resolved with conflicting cross-version suffixes

add "com.lihaoyi" %% "cask" % "0.7.11" to libraryDependencies, sbt 1.5.5 failed when resolving:

error] Modules were resolved with conflicting cross-version suffixes in ProjectRef(uri("file:/D:/project/scala/exceltransformerdotty/"), "exceltransformerdotty"):
[error]    com.lihaoyi:sourcecode _3, _2.13
[error] stack trace is suppressed; run 'last update' for the full output
[error] stack trace is suppressed; run 'last ssExtractDependencies' for the full output
[error] (update) Conflicting cross-version suffixes in: com.lihaoyi:sourcecode
[error] (ssExtractDependencies) Conflicting cross-version suffixes in: com.lihaoyi:sourcecode

this problem does not occur for

  "com.lihaoyi" %% "pprint" % "0.6.6" ,
  "com.lihaoyi" %% "requests" % "0.6.9",

Strange overlap report after adding another route

Following code prints error when run:

object MinimalApplication extends cask.MainRoutes {

  @cask.get("/root/x")
  def hello() = {
    "Hello World!"
  }

  @cask.get("/:path")
  def doThing(path: String) = {
    path.reverse
  }

  initialize()
}

The error is:

Routes overlap with wildcards: get /root/x, get /:path

When I replace the "/root/x" route with "/", the error disappears (this is how default MinimalApplication looks like).

cask_3 depends on castor_2.13

Using cask 0.7.11 in a Scala 3 projects results in the following compilation error:

[error] Modules were resolved with conflicting cross-version suffixes in ProjectRef(uri("file:/home/vincenzo/Desktop/scala3-full-stack-example/"), "webserver"):
[error]    com.lihaoyi:sourcecode _3, _2.13

The dependency tree shows that cask_3:0.7.11 depends on castor_2.13:0.1.7 which itself depends on sourcecode_2.13:0.2.1:

[info]   +-com.lihaoyi:cask_3:0.7.11
[info]   | +-com.lihaoyi:cask-util_3:0.7.11
[info]   | | +-com.lihaoyi:castor_2.13:0.1.7
[info]   | | | +-com.lihaoyi:sourcecode_2.13:0.2.1

This could be fixed by upgrading cask-util to depend on castor 1.8 which was published for Scala 3

build.sc of complete TodoMVC App is broken

Hello,

just stumbled over that the build of the full Todo-Example in the docs is not working. Found the error: just a comma was missing in the build.sc file.

Unfortunately I could not find where to change this and create a PR.

todo-0.5.6/build.sc

import mill._, scalalib._


object app extends ScalaModule{
  def scalaVersion = "2.13.1"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::cask:0.5.6" // <- here is a comma missing
    ivy"org.xerial:sqlite-jdbc:3.18.0",
    ivy"io.getquill::quill-jdbc:3.4.10",
    ivy"com.lihaoyi::scalatags:0.8.4",
  )

  object test extends Tests{
    def testFrameworks = Seq("utest.runner.Framework")

    def ivyDeps = Agg(
      ivy"com.lihaoyi::utest::0.7.3",
      ivy"com.lihaoyi::requests::0.5.0",
    )
  }
}

Thanks.

support non-English content

  @cask.get("/")
  def getNonEnglishContent1() = "조선글"

  @cask.get("/test1")
  def getNonEnglishContent2() = "سىناك"

  @cask.get("/test2")
  def getNonEnglishContent3() = "שלום"

Browser content of non-English character changes to a garbled message.

Failing to run helloworld app minimalApplication-0.1.9 on macOS 10.13.6, scala 2.12.2

macOS 10.13.6, unzipped minimalApplication-0.1.9 and cd-ed into folder, then,

$ ./cask -w app.runBackground
Error: Could not find or load main class Documents.Development.Scala.cask.minimalApplication-0.1.9.out.mill-cask

out/ folder contains mill-cask file and that's it, while

$ scala -version
Scala code runner version 2.12.2 -- Copyright 2002-2017, LAMP/EPFL and Lightbend, Inc.

thoughts?

Upgrade header check should be case-insensitive

I've noticed that websocket endpoints always reject connections when the Upgrade request header value is not exactly "websocket", all lowercase.
The WebSocket RFC specifies that the match should be case-insensitive:

https://www.rfc-editor.org/rfc/rfc6455#section-4.2.1

   3.   An |Upgrade| header field containing the value "websocket",
        treated as an ASCII case-insensitive value.

So, i believe that the check in cask.main.Main.DefaultHandler is too strict

val (effectiveMethod, runner) = if (exchange.getRequestHeaders.getFirst("Upgrade") == "websocket") {

This problem can be encountered in practice when the requests are going through Apache mod_proxy: even though the client specified the header as "Upgrade: websocket", for some reason mod_proxy decides that it would be cool to rewrite the header as WebSocket in the downstream request, causing it to fail. Specifically, i noticed this behavior in Apache 2.4.6.

Steps to reproduce:

  • run the "Websockets" example from the repo
  • try this curl request:
$ curl -v \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: atR+IRHDBUO+zMfMLfMKJA==" \
  -H "Connection: keep-alive, Upgrade" \
  -H "Upgrade: WebSocket" \
  http://localhost:8080/connect/haoyi

(notice the capitalized "WebSocket" in the "Upgrade" request header)

> GET /connect/haoyi HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.85.0
> Accept: */*
> Sec-WebSocket-Version: 13
> Sec-WebSocket-Key: atR+IRHDBUO+zMfMLfMKJA==
> Connection: keep-alive, Upgrade
> Upgrade: WebSocket
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 405 Method Not Allowed
< Connection: keep-alive
< Content-Type: text/plain; charset=utf-8
< Content-Length: 29
< Date: Sun, 12 Mar 2023 20:44:09 GMT
< 
  • request has failed with status 405. Should have been status 101 "Switching Protocols"

Pull request follows shortly ;-)

Document optional query parameters

This took me a while to find:

  @cask.get("/get")
  def hello(param: String = "Default") = {
    s"Hello $param"
  }

or:

  @cask.get("/get")
  def hello(param: Option[String] = None) = {
    s"Hello $param"
  }

This way the query parameter is optional. It may seem intuitive, but I have tried several different ways before trying this (e.g. just making param: Option[String]. or providing overloads of def hello).

Documentation not updated when new versions are published

Latest version is 0.5.6, but the documentation references 0.5.2:

Cask is just a Scala library, and you can use Cask in any existing Scala project via the following coordinates:

// Mill
ivy"com.lihaoyi::cask:0.5.2"

// SBT
"com.lihaoyi" %% "cask" % "0.5.2"

It would be interesting to mention that Scala 2.13 is required.

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.