├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── actions.yml ├── .gitignore ├── .mill-version ├── LICENSE ├── build.mill ├── cask ├── src-2 │ └── cask │ │ ├── main │ │ └── Routes.scala │ │ └── router │ │ ├── Macros.scala │ │ └── RoutesEndpointMetadata.scala ├── src-3 │ └── cask │ │ ├── main │ │ └── Routes.scala │ │ └── router │ │ ├── Macros.scala │ │ └── RoutesEndpointMetadata.scala ├── src │ └── cask │ │ ├── decorators │ │ └── compress.scala │ │ ├── endpoints │ │ ├── FormEndpoint.scala │ │ ├── JsonEndpoint.scala │ │ ├── ParamReader.scala │ │ ├── StaticEndpoints.scala │ │ ├── WebEndpoints.scala │ │ └── WebSocketEndpoint.scala │ │ ├── internal │ │ ├── Conversion.scala │ │ ├── DispatchTrie.scala │ │ ├── ThreadBlockingHandler.scala │ │ └── Util.scala │ │ ├── main │ │ ├── ErrorMsgs.scala │ │ └── Main.scala │ │ ├── model │ │ ├── Params.scala │ │ ├── Response.scala │ │ └── Status.scala │ │ ├── package.scala │ │ └── router │ │ ├── Decorators.scala │ │ ├── EndpointMetadata.scala │ │ ├── EntryPoint.scala │ │ ├── Misc.scala │ │ ├── Result.scala │ │ └── Runtime.scala ├── test │ ├── src-3 │ │ └── test │ │ │ └── cask │ │ │ └── FailureTests3.scala │ └── src │ │ └── test │ │ └── cask │ │ ├── DispatchTrieTests.scala │ │ ├── FailureTests.scala │ │ └── UtilTests.scala └── util │ ├── src-js │ └── cask │ │ └── util │ │ ├── Scheduler.scala │ │ └── WebsocketClientImpl.scala │ ├── src-jvm │ └── cask │ │ └── util │ │ ├── Scheduler.scala │ │ └── WebsocketClientImpl.scala │ └── src │ └── cask │ └── util │ ├── Logger.scala │ ├── WebsocketBase.scala │ ├── Ws.scala │ └── WsClient.scala ├── ci ├── package.mill └── upload.mill ├── docs ├── amm ├── build.sc ├── favicon.png ├── logo-white.svg ├── pageStyles.sc ├── pages.sc └── pages │ ├── 1 - Cask - a Scala HTTP micro-framework.md │ ├── 2 - Main Customization.md │ └── 3 - About Cask.md ├── example ├── compress │ ├── app │ │ ├── src │ │ │ └── Compress.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── compress2 │ ├── app │ │ ├── src │ │ │ └── Compress2.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── compress3 │ ├── app │ │ ├── src │ │ │ └── Compress3.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── cookies │ ├── app │ │ ├── src │ │ │ └── Cookies.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── decorated │ ├── app │ │ ├── src │ │ │ └── Decorated.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── decorated2 │ ├── app │ │ ├── src │ │ │ └── Decorated2.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── decoratedContext │ ├── app │ │ ├── src │ │ │ └── DecoratedContext.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── endpoints │ ├── app │ │ ├── src │ │ │ └── Endpoints.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── formJsonPost │ ├── app │ │ ├── src │ │ │ └── FormJsonPost.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── httpMethods │ ├── app │ │ ├── src │ │ │ └── HttpMethods.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── minimalApplication │ ├── app │ │ ├── src │ │ │ └── MinimalApplication.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── minimalApplication2 │ ├── app │ │ ├── src │ │ │ └── MinimalApplication2.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── minimalApplicationWithLoom │ ├── app │ │ ├── src │ │ │ └── MinimalApplicationWithLoom.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── multipartFormSubmission │ ├── app │ │ ├── resources │ │ │ └── example.txt │ │ ├── src │ │ │ └── MultipartFormSubmission.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── package.mill ├── queryParams │ ├── app │ │ ├── src │ │ │ └── QueryParams.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── redirectAbort │ ├── app │ │ ├── src │ │ │ └── RedirectAbort.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── scalatags │ ├── app │ │ ├── src │ │ │ └── Scalatags.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── staticFiles │ ├── app │ │ ├── resources │ │ │ └── cask │ │ │ │ └── example.txt │ │ ├── src │ │ │ └── StaticFiles.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── staticFiles2 │ ├── app │ │ ├── resources │ │ │ └── cask │ │ │ │ └── example.txt │ │ ├── src │ │ │ └── StaticFiles2.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── staticFilesWithLoom │ ├── app │ │ ├── resources │ │ │ └── cask │ │ │ │ └── example.txt │ │ ├── src │ │ │ └── StaticFilesWithLoom.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── todo │ ├── app │ │ ├── resources │ │ │ └── todo │ │ │ │ ├── app.js │ │ │ │ └── index.css │ │ ├── src │ │ │ └── TodoServer.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── todoApi │ ├── app │ │ ├── src │ │ │ └── TodoMvcApi.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── todoDb │ ├── app │ │ ├── src │ │ │ └── TodoMvcDb.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── todoDbWithLoom │ ├── app │ │ ├── src │ │ │ └── TodoMvcDbWithLoom.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── twirl │ ├── app │ │ ├── src │ │ │ └── Twirl.scala │ │ ├── test │ │ │ └── src │ │ │ │ └── ExampleTests.scala │ │ └── views │ │ │ └── hello.scala.html │ └── package.mill ├── variableRoutes │ ├── app │ │ ├── src │ │ │ └── VariableRoutes.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── websockets │ ├── app │ │ ├── src │ │ │ └── Websockets.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── websockets2 │ ├── app │ │ ├── src │ │ │ └── Websockets2.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill ├── websockets3 │ ├── app │ │ ├── src │ │ │ └── Websockets3.scala │ │ └── test │ │ │ └── src │ │ │ └── ExampleTests.scala │ └── package.mill └── websockets4 │ ├── app │ ├── src │ │ └── Websockets4.scala │ └── test │ │ └── src │ │ └── ExampleTests.scala │ └── package.mill ├── mill └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: lihaoyi 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | java: [ '11', '17', '21' ] 19 | name: Tests local for Java ${{ matrix.Java }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Setup java 23 | uses: actions/setup-java@v3 24 | with: 25 | distribution: 'temurin' 26 | java-version: ${{ matrix.java }} 27 | - name: Run tests 28 | run: | 29 | set -eux 30 | if [ "${{ matrix.java }}" == "21" ]; then 31 | JAVA_OPTS='--add-opens java.base/java.lang=ALL-UNNAMED -Dcask.virtual-threads.enabled=true' ./mill -ikj1 --disable-ticker __.testLocal 32 | else 33 | ./mill -ikj1 --disable-ticker __.testLocal 34 | fi 35 | 36 | test-examples: 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | java: [ '11', '17', '21' ] 41 | name: Tests examples for Java ${{ matrix.Java }} 42 | steps: 43 | - uses: actions/checkout@v3 44 | - name: Setup java 45 | uses: actions/setup-java@v3 46 | with: 47 | distribution: 'temurin' 48 | java-version: ${{ matrix.java }} 49 | - name: Run tests 50 | run: | 51 | set -eux 52 | if [ "${{ matrix.java }}" == "21" ]; then 53 | ./mill __.publishLocal 54 | JAVA_OPTS='--add-opens java.base/java.lang=ALL-UNNAMED -Dcask.virtual-threads.enabled=true' ./mill -ikj1 --disable-ticker testExamples 55 | else 56 | ./mill __.publishLocal 57 | ./mill -ikj1 --disable-ticker testExamples 58 | fi 59 | 60 | publish-sonatype: 61 | if: github.repository == 'com-lihaoyi/cask' && contains(github.ref, 'refs/tags/') 62 | needs: test 63 | runs-on: ubuntu-latest 64 | env: 65 | MILL_SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 66 | MILL_SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 67 | MILL_PGP_SECRET_BASE64: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY }} 68 | MILL_PGP_PASSPHRASE: ${{ secrets.SONATYPE_PGP_PRIVATE_KEY_PASSWORD }} 69 | LANG: "en_US.UTF-8" 70 | LC_MESSAGES: "en_US.UTF-8" 71 | LC_ALL: "en_US.UTF-8" 72 | 73 | steps: 74 | - uses: actions/checkout@v3 75 | - uses: actions/setup-java@v3 76 | with: 77 | distribution: 'temurin' 78 | java-version: 11 79 | - name: Publish to Maven Central 80 | run: ./mill -i mill.scalalib.PublishModule/ 81 | 82 | uploadExamples: 83 | if: github.repository == 'com-lihaoyi/cask' && contains(github.ref, 'refs/tags/') 84 | runs-on: ubuntu-latest 85 | env: 86 | AMMONITE_BOT_AUTH_TOKEN: ${{ secrets.AMMONITE_BOT_AUTH_TOKEN }} 87 | steps: 88 | - uses: actions/checkout@v2 89 | with: 90 | fetch-depth: 0 91 | - uses: actions/setup-java@v3 92 | with: 93 | distribution: 'temurin' 94 | java-version: '11' 95 | - name: Upload Example Zips 96 | run: ./mill uploadToGithub 97 | 98 | 99 | generate_docs: 100 | if: github.repository == 'com-lihaoyi/cask' 101 | runs-on: ubuntu-20.04 102 | env: 103 | LANG: "en_US.UTF-8" 104 | LC_MESSAGES: "en_US.UTF-8" 105 | LC_ALL: "en_US.UTF-8" 106 | steps: 107 | - uses: actions/checkout@v3 108 | with: 109 | fetch-depth: 0 110 | - uses: actions/setup-java@v3 111 | with: 112 | distribution: 'temurin' 113 | java-version: 11 114 | - name: Generate Website 115 | run: | 116 | cd docs 117 | ./amm build.sc 118 | - name: Deploy Website 119 | uses: peaceiris/actions-gh-pages@v3 120 | with: 121 | github_token: ${{ secrets.GITHUB_TOKEN }} 122 | publish_dir: docs/target 123 | publish_branch: gh-pages 124 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bloop/ 2 | .metals/ 3 | .vscode/ 4 | target/ 5 | *.iml 6 | .idea 7 | .settings 8 | .classpath 9 | .project 10 | .cache 11 | .sbtserver 12 | project/.sbtserver 13 | tags 14 | nohup.out 15 | out 16 | .bsp/ 17 | -------------------------------------------------------------------------------- /.mill-version: -------------------------------------------------------------------------------- 1 | 0.12.0 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2017 Li Haoyi (haoyi.sg@gmail.com) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /cask/src-2/cask/main/Routes.scala: -------------------------------------------------------------------------------- 1 | package cask.main 2 | 3 | import cask.router.RoutesEndpointsMetadata 4 | 5 | import language.experimental.macros 6 | 7 | trait Routes{ 8 | 9 | def decorators = Seq.empty[cask.router.Decorator[_, _, _, _]] 10 | private[this] var metadata0: RoutesEndpointsMetadata[this.type] = null 11 | def caskMetadata = 12 | if (metadata0 != null) metadata0 13 | else throw new Exception("Routes not yet initialized") 14 | 15 | protected[this] def initialize()(implicit routes: RoutesEndpointsMetadata[this.type]): Unit = { 16 | metadata0 = routes 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /cask/src-2/cask/router/RoutesEndpointMetadata.scala: -------------------------------------------------------------------------------- 1 | package cask.router 2 | 3 | import cask.router.EntryPoint 4 | 5 | import language.experimental.macros 6 | import scala.reflect.macros.blackbox 7 | 8 | case class RoutesEndpointsMetadata[T](value: EndpointMetadata[T]*) 9 | object RoutesEndpointsMetadata{ 10 | 11 | implicit def initialize[T]: RoutesEndpointsMetadata[T] = macro initializeImpl[T] 12 | implicit def initializeImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[RoutesEndpointsMetadata[T]] = { 13 | import c.universe._ 14 | val router = new cask.router.Macros[c.type](c) 15 | 16 | val routeParts = for{ 17 | m <- c.weakTypeOf[T].members 18 | annotations = m.annotations.filter(_.tree.tpe <:< c.weakTypeOf[Decorator[_, _, _, _]]) 19 | if annotations.nonEmpty 20 | } yield { 21 | if(!(annotations.last.tree.tpe <:< weakTypeOf[Endpoint[_, _, _, _]])) c.abort( 22 | annotations.head.tree.pos, 23 | s"Last annotation applied to a function must be an instance of Endpoint, " + 24 | s"not ${annotations.last.tree.tpe}" 25 | ) 26 | val allEndpoints = annotations.filter(_.tree.tpe <:< weakTypeOf[Endpoint[_, _, _, _]]) 27 | if(allEndpoints.length > 1) c.abort( 28 | annotations.last.tree.pos, 29 | s"You can only apply one Endpoint annotation to a function, not " + 30 | s"${allEndpoints.length} in ${allEndpoints.map(_.tree.tpe).mkString(", ")}" 31 | ) 32 | 33 | val annotObjects = 34 | for(annot <- annotations) 35 | yield q"new ${annot.tree.tpe}(..${annot.tree.children.tail})" 36 | 37 | val annotObjectSyms = 38 | for(_ <- annotations.indices) 39 | yield c.universe.TermName(c.freshName("annotObject")) 40 | 41 | val annotPositions = 42 | for(a <- annotations) 43 | yield a.tree.find(_.pos != NoPosition) match{ 44 | case None => m.pos 45 | case Some(t) => t.pos 46 | } 47 | 48 | val route = router.extractMethod( 49 | m.asInstanceOf[MethodSymbol], 50 | weakTypeOf[T], 51 | q"${annotObjectSyms.last}.convertToResultType", 52 | annotObjectSyms.reverse.map(annotObjectSym => q"$annotObjectSym.getParamParser"), 53 | annotObjectSyms.reverse.map(annotObjectSym => tq"$annotObjectSym.InputTypeAlias") 54 | ) 55 | 56 | val declarations = 57 | for((sym, obj) <- annotObjectSyms.zip(annotObjects)) 58 | yield q"val $sym = $obj" 59 | 60 | val seqify = TermName("seqify" + annotObjectSyms.length) 61 | 62 | val seqifyCall = annotObjectSyms 63 | .zip(annotPositions) 64 | .reverse 65 | .foldLeft[Tree](q"cask.router.EndpointMetadata.$seqify"){ 66 | case (lhs, (rhs, pos)) => q"$lhs(${c.internal.setPos(q"$rhs", pos)})" 67 | } 68 | 69 | q"""{ 70 | ..$declarations 71 | cask.router.EndpointMetadata( 72 | $seqifyCall.reverse.dropRight(1), 73 | ${annotObjectSyms.last}, 74 | $route 75 | ) 76 | }""" 77 | } 78 | 79 | c.Expr[RoutesEndpointsMetadata[T]](q"""cask.router.RoutesEndpointsMetadata(..$routeParts)""") 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /cask/src-3/cask/main/Routes.scala: -------------------------------------------------------------------------------- 1 | package cask.main 2 | 3 | import cask.router.RoutesEndpointsMetadata 4 | 5 | import language.experimental.macros 6 | 7 | trait Routes{ 8 | 9 | def decorators = Seq.empty[cask.router.Decorator[_, _, _, _]] 10 | private[this] var metadata0: RoutesEndpointsMetadata[this.type] = null 11 | def caskMetadata = 12 | if (metadata0 != null) metadata0 13 | else throw new Exception("Routes not yet initialized") 14 | 15 | protected[this] inline def initialize(): Unit = ${ 16 | RoutesEndpointsMetadata.setRoutesImpl[this.type]( 17 | '{(x: RoutesEndpointsMetadata[this.type]) => metadata0 = x} 18 | ) 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /cask/src-3/cask/router/RoutesEndpointMetadata.scala: -------------------------------------------------------------------------------- 1 | package cask.router 2 | 3 | import cask.router.EntryPoint 4 | 5 | case class RoutesEndpointsMetadata[T](value: Seq[EndpointMetadata[T]]) 6 | object RoutesEndpointsMetadata{ 7 | import scala.quoted._ 8 | 9 | inline given initialize[T]: RoutesEndpointsMetadata[T] = ${initializeImpl} 10 | 11 | def setRoutesImpl[T: Type](setter: Expr[RoutesEndpointsMetadata[T] => Unit])(using Quotes): Expr[Unit] = { 12 | '{ 13 | $setter(${initializeImpl[T]}) 14 | () 15 | } 16 | } 17 | 18 | def initializeImpl[T: Type](using q: Quotes): Expr[RoutesEndpointsMetadata[T]] = { 19 | import quotes.reflect._ 20 | 21 | val routeParts: List[Expr[EndpointMetadata[T]]] = for { 22 | m <- TypeRepr.of[T].typeSymbol.memberMethods 23 | annotations = m.annotations.filter(_.tpe <:< TypeRepr.of[Decorator[_, _, _, _]]) 24 | if (annotations.nonEmpty) 25 | } yield { 26 | 27 | if(!(annotations.head.tpe <:< TypeRepr.of[Endpoint[_, _, _, _]])) { 28 | report.error(s"Last annotation applied to a function must be an instance of Endpoint, " + 29 | s"not ${annotations.head.tpe.show}", 30 | annotations.head.pos 31 | ) 32 | return '{???} // in this case, we can't continue expansion of this macro 33 | } 34 | val allEndpoints = annotations.filter(_.tpe <:< TypeRepr.of[Endpoint[_, _, _, _]]) 35 | if(allEndpoints.length > 1) { 36 | report.error( 37 | s"You can only apply one Endpoint annotation to a function, not " + 38 | s"${allEndpoints.length} in ${allEndpoints.map(_.tpe.show).mkString(", ")}", 39 | annotations.last.pos, 40 | ) 41 | return '{???} 42 | } 43 | 44 | val decorators = annotations.map(_.asExprOf[Decorator[_, _, _, _]]) 45 | 46 | if (!Macros.checkDecorators(decorators)) 47 | return '{???} // there was a type mismatch in the decorator chain 48 | 49 | val endpointExpr = decorators.head.asExprOf[Endpoint[_, _, _, _]] 50 | val entrypointExpr = Macros.extractMethod[T](m, decorators, endpointExpr) 51 | 52 | '{ 53 | val entrypoint: EntryPoint[T, Any] = ${entrypointExpr} 54 | 55 | EndpointMetadata[T]( 56 | // the Scala 2 version and non-macro code expects decorators to be reversed 57 | ${Expr.ofList(decorators.drop(1).reverse)}, 58 | ${endpointExpr}, 59 | entrypoint 60 | ) 61 | } 62 | 63 | } 64 | 65 | '{ 66 | RoutesEndpointsMetadata[T]( 67 | ${Expr.ofList(routeParts)} 68 | ) 69 | } 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /cask/src/cask/decorators/compress.scala: -------------------------------------------------------------------------------- 1 | package cask.decorators 2 | import java.io.{ByteArrayOutputStream, OutputStream} 3 | import java.util.zip.{DeflaterOutputStream, GZIPOutputStream} 4 | 5 | import cask.model.{Request, Response} 6 | 7 | import collection.JavaConverters._ 8 | class compress extends cask.RawDecorator{ 9 | def wrapFunction(ctx: Request, delegate: Delegate) = { 10 | val acceptEncodings = Option(ctx.exchange.getRequestHeaders.get("Accept-Encoding")) 11 | .toSeq 12 | .flatMap(_.asScala) 13 | .flatMap(_.split(", ")) 14 | val finalResult = delegate(ctx, Map()).transform{ case v: cask.Response.Raw => 15 | val (newData, newHeaders) = if (acceptEncodings.exists(_.toLowerCase == "gzip")) { 16 | new Response.Data { 17 | def write(out: OutputStream): Unit = { 18 | val wrap = new GZIPOutputStream(out) 19 | v.data.write(wrap) 20 | wrap.flush() 21 | wrap.close() 22 | } 23 | // Since we don't know the length of the gzipped data ahead of time, 24 | // we drop the content length header. 25 | override def headers = v.data.headers.filter(_._1 != "Content-Length") 26 | } -> Seq("Content-Encoding" -> "gzip") 27 | }else if (acceptEncodings.exists(_.toLowerCase == "deflate")){ 28 | new Response.Data { 29 | def write(out: OutputStream): Unit = { 30 | val wrap = new DeflaterOutputStream(out) 31 | v.data.write(wrap) 32 | wrap.flush() 33 | } 34 | // Since we don't know the length of the compressed data ahead of 35 | // time, we drop the content length header. 36 | override def headers = v.data.headers.filter(_._1 != "Content-Length") 37 | } -> Seq("Content-Encoding" -> "deflate") 38 | }else v.data -> Nil 39 | Response( 40 | newData, 41 | v.statusCode, 42 | v.headers ++ newHeaders, 43 | v.cookies 44 | ) 45 | } 46 | finalResult 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cask/src/cask/endpoints/FormEndpoint.scala: -------------------------------------------------------------------------------- 1 | package cask.endpoints 2 | 3 | import cask.internal.Util 4 | import cask.router.HttpEndpoint 5 | import cask.model._ 6 | import cask.router.{ArgReader, Result} 7 | import io.undertow.server.handlers.form.FormParserFactory 8 | 9 | import collection.JavaConverters._ 10 | 11 | sealed trait FormReader[T] extends ArgReader[Seq[FormEntry], T, Request] 12 | object FormReader{ 13 | implicit def paramFormReader[T: QueryParamReader]: FormReader[T] = new FormReader[T]{ 14 | def arity = implicitly[QueryParamReader[T]].arity 15 | 16 | override def unknownQueryParams: Boolean = implicitly[QueryParamReader[T]].unknownQueryParams 17 | 18 | override def remainingPathSegments: Boolean = implicitly[QueryParamReader[T]].remainingPathSegments 19 | def read(ctx: Request, label: String, input: Seq[FormEntry]) = { 20 | implicitly[QueryParamReader[T]].read(ctx, label, if (input == null) null else input.map(_.valueOrFileName)) 21 | } 22 | } 23 | 24 | implicit def formEntryReader: FormReader[FormEntry] = new FormReader[FormEntry]{ 25 | def arity = 1 26 | def read(ctx: Request, label: String, input: Seq[FormEntry]) = input.head 27 | } 28 | implicit def formEntriesReader: FormReader[Seq[FormEntry]] = new FormReader[Seq[FormEntry]]{ 29 | def arity = 1 30 | def read(ctx: Request, label: String, input: Seq[FormEntry]) = input 31 | } 32 | 33 | implicit def formValueReader: FormReader[FormValue] = new FormReader[FormValue]{ 34 | def arity = 1 35 | def read(ctx: Request, label: String, input: Seq[FormEntry]) = input.head.asInstanceOf[FormValue] 36 | } 37 | implicit def formValuesReader: FormReader[Seq[FormValue]] = new FormReader[Seq[FormValue]]{ 38 | def arity = 1 39 | def read(ctx: Request, label: String, input: Seq[FormEntry]) = input.map(_.asInstanceOf[FormValue]) 40 | } 41 | implicit def formFileReader: FormReader[FormFile] = new FormReader[FormFile]{ 42 | def arity = 1 43 | def read(ctx: Request, label: String, input: Seq[FormEntry]) = input.head.asInstanceOf[FormFile] 44 | } 45 | implicit def formFilesReader: FormReader[Seq[FormFile]] = new FormReader[Seq[FormFile]]{ 46 | def arity = 1 47 | def read(ctx: Request, label: String, input: Seq[FormEntry]) = input.map(_.asInstanceOf[FormFile]) 48 | } 49 | } 50 | class postForm(val path: String, override val subpath: Boolean = false) 51 | extends HttpEndpoint[Response.Raw, Seq[FormEntry]] { 52 | 53 | val methods = Seq("post") 54 | type InputParser[T] = FormReader[T] 55 | def wrapFunction(ctx: Request, 56 | delegate: Delegate): Result[Response.Raw] = { 57 | try { 58 | val formData = FormParserFactory 59 | .builder().withDefaultCharset("utf-8").build() 60 | .createParser(ctx.exchange) 61 | .parseBlocking() 62 | delegate( 63 | ctx, 64 | formData 65 | .iterator() 66 | .asScala 67 | .map(k => (k, formData.get(k).asScala.map(FormEntry.fromUndertow).toSeq)) 68 | .toMap 69 | ) 70 | } catch{case e: Exception => 71 | Result.Success(cask.model.Response( 72 | "Unable to parse form data: " + e + "\n" + Util.stackTraceString(e), 73 | statusCode = 400 74 | )) 75 | } 76 | } 77 | 78 | def wrapPathSegment(s: String): Seq[FormEntry] = Seq(FormValue(s, new io.undertow.util.HeaderMap)) 79 | } 80 | 81 | -------------------------------------------------------------------------------- /cask/src/cask/endpoints/JsonEndpoint.scala: -------------------------------------------------------------------------------- 1 | package cask.endpoints 2 | 3 | import java.io.{ByteArrayOutputStream, InputStream, OutputStream, OutputStreamWriter} 4 | 5 | import cask.internal.Util 6 | import cask.router.HttpEndpoint 7 | import cask.model.Response.DataCompanion 8 | import cask.model.{Request, Response} 9 | import cask.router.{ArgReader, Result} 10 | 11 | import collection.JavaConverters._ 12 | 13 | sealed trait JsReader[T] extends ArgReader[ujson.Value, T, cask.model.Request] 14 | object JsReader{ 15 | implicit def defaultJsReader[T: upickle.default.Reader]: JsReader[T] = new JsReader[T]{ 16 | def arity = 1 17 | 18 | def read(ctx: cask.model.Request, label: String, input: ujson.Value): T = { 19 | val reader = implicitly[upickle.default.Reader[T]] 20 | upickle.default.read[T](input)(reader) 21 | } 22 | } 23 | 24 | implicit def paramReader[T: ParamReader]: JsReader[T] = new JsReader[T] { 25 | override def arity = 0 26 | 27 | override def unknownQueryParams: Boolean = implicitly[ParamReader[T]].unknownQueryParams 28 | override def remainingPathSegments: Boolean = implicitly[ParamReader[T]].remainingPathSegments 29 | override def read(ctx: cask.model.Request, label: String, v: ujson.Value) = { 30 | implicitly[ParamReader[T]].read(ctx, label, Nil) 31 | } 32 | } 33 | } 34 | trait JsonData extends Response.Data 35 | object JsonData extends DataCompanion[JsonData]{ 36 | implicit class JsonDataImpl[T: upickle.default.Writer](t: T) extends JsonData{ 37 | def headers = Seq("Content-Type" -> "application/json") 38 | def write(out: OutputStream) = { 39 | upickle.default.stream(t).writeBytesTo(out) 40 | out.flush() 41 | } 42 | } 43 | } 44 | 45 | class postJsonCached(path: String, subpath: Boolean = false) extends postJsonBase(path, subpath, true) 46 | class postJson(path: String, subpath: Boolean = false) extends postJsonBase(path, subpath, false) 47 | abstract class postJsonBase(val path: String, override val subpath: Boolean = false, cacheBody: Boolean = false) 48 | extends HttpEndpoint[Response[JsonData], ujson.Value]{ 49 | val methods = Seq("post") 50 | type InputParser[T] = JsReader[T] 51 | 52 | def wrapFunction(ctx: Request, delegate: Delegate): Result[Response.Raw] = { 53 | val obj = for{ 54 | json <- 55 | try Right(ujson.read(if (cacheBody) ctx.bytes else ctx.exchange.getInputStream)) 56 | catch{case e: Throwable => Left(cask.model.Response( 57 | "Input text is invalid JSON: " + e + "\n" + Util.stackTraceString(e), 58 | statusCode = 400 59 | ))} 60 | obj <- 61 | try Right(json.obj) 62 | catch {case e: Throwable => Left(cask.model.Response( 63 | "Input JSON must be a dictionary", 64 | statusCode = 400 65 | ))} 66 | } yield obj.toMap 67 | obj match{ 68 | case Left(r) => Result.Success(r.map(Response.Data.WritableData(_))) 69 | case Right(params) => delegate(ctx, params) 70 | } 71 | } 72 | def wrapPathSegment(s: String): ujson.Value = ujson.Str(s) 73 | } 74 | 75 | class getJson(val path: String, override val subpath: Boolean = false) 76 | extends HttpEndpoint[Response[JsonData], Seq[String]]{ 77 | val methods = Seq("get") 78 | type InputParser[T] = QueryParamReader[T] 79 | 80 | def wrapFunction(ctx: Request, delegate: Delegate): Result[Response.Raw] = { 81 | delegate(ctx, WebEndpoint.buildMapFromQueryParams(ctx)) 82 | } 83 | def wrapPathSegment(s: String) = Seq(s) 84 | } 85 | -------------------------------------------------------------------------------- /cask/src/cask/endpoints/ParamReader.scala: -------------------------------------------------------------------------------- 1 | package cask.endpoints 2 | 3 | import cask.router.ArgReader 4 | import cask.model.{Cookie, Request} 5 | import io.undertow.server.HttpServerExchange 6 | import io.undertow.server.handlers.form.{FormData, FormParserFactory} 7 | 8 | abstract class ParamReader[T] extends ArgReader[Unit, T, cask.model.Request]{ 9 | def arity: Int 10 | def read(ctx: cask.model.Request, label: String, v: Unit): T 11 | } 12 | object ParamReader{ 13 | class NilParam[T](f: (Request, String) => T) extends ParamReader[T]{ 14 | def arity = 0 15 | def read(ctx: cask.model.Request, label: String, v: Unit): T = f(ctx, label) 16 | } 17 | implicit object HttpExchangeParam extends NilParam[HttpServerExchange]((ctx, label) => ctx.exchange) 18 | 19 | implicit object FormDataParam extends NilParam[FormData]((ctx, label) => 20 | FormParserFactory.builder().build().createParser(ctx.exchange).parseBlocking() 21 | ) 22 | 23 | implicit object RequestParam extends NilParam[Request]((ctx, label) => ctx) 24 | 25 | implicit object CookieParam extends NilParam[Cookie]((ctx, label) => 26 | Cookie.fromUndertow(ctx.exchange.getRequestCookies().get(label)) 27 | ) 28 | 29 | implicit object QueryParams extends ParamReader[cask.model.QueryParams] { 30 | def arity: Int = 0 31 | 32 | override def unknownQueryParams = true 33 | 34 | def read(ctx: cask.model.Request, label: String, v: Unit) = { 35 | cask.model.QueryParams(ctx.queryParams) 36 | } 37 | } 38 | 39 | implicit object RemainingPathSegments extends ParamReader[cask.model.RemainingPathSegments] { 40 | def arity: Int = 0 41 | 42 | override def remainingPathSegments = true 43 | 44 | def read(ctx: cask.model.Request, label: String, v: Unit) = { 45 | cask.model.RemainingPathSegments(ctx.remainingPathSegments) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /cask/src/cask/endpoints/StaticEndpoints.scala: -------------------------------------------------------------------------------- 1 | package cask.endpoints 2 | 3 | import cask.router.{HttpEndpoint, Result} 4 | import cask.model.Request 5 | object StaticUtil{ 6 | def makePathAndContentType(t: String, ctx: Request) = { 7 | val leadingSlash = if (t.startsWith("/")) "/" else "" 8 | val path = leadingSlash + (cask.internal.Util.splitPath(t) ++ ctx.remainingPathSegments.flatMap(cask.internal.Util.splitPath)) 9 | .filter(s => s != "." && s != "..") 10 | .mkString("/") 11 | val contentType = java.nio.file.Files.probeContentType(java.nio.file.Paths.get(path)) 12 | 13 | (path, Option(contentType)) 14 | } 15 | } 16 | 17 | class staticFiles(val path: String, headers: Seq[(String, String)] = Nil) extends HttpEndpoint[String, Seq[String]]{ 18 | val methods = Seq("get") 19 | type InputParser[T] = QueryParamReader[T] 20 | override def subpath = true 21 | def wrapFunction(ctx: Request, delegate: Delegate) = { 22 | delegate(ctx, Map()).map{t => 23 | val (path, contentTypeOpt) = StaticUtil.makePathAndContentType(t, ctx) 24 | cask.model.StaticFile(path, headers ++ contentTypeOpt.map("Content-Type" -> _)) 25 | } 26 | } 27 | 28 | def wrapPathSegment(s: String): Seq[String] = Seq(s) 29 | } 30 | 31 | class staticResources(val path: String, 32 | resourceRoot: ClassLoader = classOf[staticResources].getClassLoader, 33 | headers: Seq[(String, String)] = Nil) 34 | extends HttpEndpoint[String, Seq[String]]{ 35 | val methods = Seq("get") 36 | type InputParser[T] = QueryParamReader[T] 37 | override def subpath = true 38 | def wrapFunction(ctx: Request, delegate: Delegate) = { 39 | delegate(ctx, Map()).map { t => 40 | val (path, contentTypeOpt) = StaticUtil.makePathAndContentType(t, ctx) 41 | cask.model.StaticResource(path, resourceRoot, headers ++ contentTypeOpt.map("Content-Type" -> _)) 42 | } 43 | } 44 | 45 | 46 | def wrapPathSegment(s: String): Seq[String] = Seq(s) 47 | } 48 | -------------------------------------------------------------------------------- /cask/src/cask/endpoints/WebEndpoints.scala: -------------------------------------------------------------------------------- 1 | package cask.endpoints 2 | 3 | import cask.router.HttpEndpoint 4 | import cask.model.{Request, Response} 5 | import cask.router.{ArgReader, Result} 6 | 7 | import collection.JavaConverters._ 8 | 9 | 10 | trait WebEndpoint extends HttpEndpoint[Response.Raw, Seq[String]]{ 11 | type InputParser[T] = QueryParamReader[T] 12 | def wrapFunction(ctx: Request, 13 | delegate: Delegate): Result[Response.Raw] = { 14 | delegate(ctx, WebEndpoint.buildMapFromQueryParams(ctx)) 15 | } 16 | def wrapPathSegment(s: String) = Seq(s) 17 | } 18 | object WebEndpoint{ 19 | def buildMapFromQueryParams(ctx: Request) = { 20 | val b = Map.newBuilder[String, Seq[String]] 21 | val queryParams = ctx.exchange.getQueryParameters 22 | for(k <- queryParams.keySet().iterator().asScala){ 23 | val deque = queryParams.get(k) 24 | val arr = new Array[String](deque.size) 25 | deque.toArray(arr) 26 | b += (k -> (arr: Seq[String])) 27 | } 28 | b.result() 29 | } 30 | } 31 | class get(val path: String, override val subpath: Boolean = false) extends WebEndpoint{ 32 | val methods = Seq("get") 33 | } 34 | class post(val path: String, override val subpath: Boolean = false) extends WebEndpoint{ 35 | val methods = Seq("post") 36 | } 37 | class put(val path: String, override val subpath: Boolean = false) extends WebEndpoint{ 38 | val methods = Seq("put") 39 | } 40 | class patch(val path: String, override val subpath: Boolean = false) extends WebEndpoint{ 41 | val methods = Seq("patch") 42 | } 43 | class delete(val path: String, override val subpath: Boolean = false) extends WebEndpoint{ 44 | val methods = Seq("delete") 45 | } 46 | class route(val path: String, val methods: Seq[String], override val subpath: Boolean = false) extends WebEndpoint 47 | 48 | class options(val path: String, override val subpath: Boolean = false) extends WebEndpoint{ 49 | val methods = Seq("options") 50 | } 51 | 52 | abstract class QueryParamReader[T] 53 | extends ArgReader[Seq[String], T, cask.model.Request]{ 54 | def arity: Int 55 | def read(ctx: cask.model.Request, label: String, v: Seq[String]): T 56 | } 57 | object QueryParamReader{ 58 | 59 | 60 | class SimpleParam[T](f: String => T) extends QueryParamReader[T]{ 61 | def arity = 1 62 | def read(ctx: cask.model.Request, label: String, v: Seq[String]): T = f(v.head) 63 | } 64 | 65 | implicit object StringParam extends SimpleParam[String](x => x) 66 | implicit object BooleanParam extends SimpleParam[Boolean](_.toBoolean) 67 | implicit object ByteParam extends SimpleParam[Byte](_.toByte) 68 | implicit object ShortParam extends SimpleParam[Short](_.toShort) 69 | implicit object IntParam extends SimpleParam[Int](_.toInt) 70 | implicit object LongParam extends SimpleParam[Long](_.toLong) 71 | implicit object DoubleParam extends SimpleParam[Double](_.toDouble) 72 | implicit object FloatParam extends SimpleParam[Float](_.toFloat) 73 | implicit def SeqParam[T: QueryParamReader]: QueryParamReader[Seq[T]] = new QueryParamReader[Seq[T]]{ 74 | def arity = 1 75 | def read(ctx: cask.model.Request, label: String, v: Seq[String]): Seq[T] = { 76 | v.map(x => implicitly[QueryParamReader[T]].read(ctx, label, Seq(x))) 77 | } 78 | } 79 | implicit def OptionParam[T: QueryParamReader]: QueryParamReader[Option[T]] = new QueryParamReader[Option[T]]{ 80 | def arity = 1 81 | def read(ctx: cask.model.Request, label: String, v: Seq[String]): Option[T] = { 82 | v.headOption.map(x => implicitly[QueryParamReader[T]].read(ctx, label, Seq(x))) 83 | } 84 | } 85 | implicit def paramReader[T: ParamReader]: QueryParamReader[T] = new QueryParamReader[T] { 86 | override def arity = 0 87 | 88 | override def unknownQueryParams: Boolean = implicitly[ParamReader[T]].unknownQueryParams 89 | override def remainingPathSegments: Boolean = implicitly[ParamReader[T]].remainingPathSegments 90 | override def read(ctx: cask.model.Request, label: String, v: Seq[String]) = { 91 | implicitly[ParamReader[T]].read(ctx, label, v) 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /cask/src/cask/endpoints/WebSocketEndpoint.scala: -------------------------------------------------------------------------------- 1 | package cask.endpoints 2 | 3 | import java.nio.ByteBuffer 4 | 5 | import cask.model.Request 6 | import cask.router.Result 7 | import cask.util.{Logger, Ws} 8 | import io.undertow.websockets.WebSocketConnectionCallback 9 | import io.undertow.websockets.core.{AbstractReceiveListener, BufferedBinaryMessage, BufferedTextMessage, CloseMessage, WebSocketChannel, WebSockets} 10 | import io.undertow.websockets.spi.WebSocketHttpExchange 11 | 12 | import scala.concurrent.ExecutionContext 13 | 14 | sealed trait WebsocketResult 15 | object WebsocketResult{ 16 | implicit class Response[T](value0: cask.model.Response[T]) 17 | (implicit f: T => cask.model.Response.Data) extends WebsocketResult{ 18 | def value = value0.map(f) 19 | } 20 | implicit class Listener(val value: WebSocketConnectionCallback) extends WebsocketResult 21 | } 22 | 23 | class websocket(val path: String, override val subpath: Boolean = false) 24 | extends cask.router.Endpoint[WebsocketResult, WebsocketResult, Seq[String], Request]{ 25 | val methods = Seq("websocket") 26 | type InputParser[T] = QueryParamReader[T] 27 | type OuterReturned = Result[WebsocketResult] 28 | def wrapFunction(ctx: Request, delegate: Delegate) = { 29 | delegate(ctx, WebEndpoint.buildMapFromQueryParams(ctx)) 30 | } 31 | 32 | def wrapPathSegment(s: String): Seq[String] = Seq(s) 33 | } 34 | 35 | case class WsHandler(f: WsChannelActor => castor.Actor[Ws.Event]) 36 | (implicit ac: castor.Context, log: Logger) 37 | extends WebsocketResult with WebSocketConnectionCallback { 38 | def onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel): Unit = { 39 | channel.suspendReceives() 40 | val actor = f(new WsChannelActor(channel)) 41 | // Somehow browsers closing tabs and Java processes being killed appear 42 | // as different events here; the former goes to AbstractReceiveListener#onClose, 43 | // while the latter to ChannelListener#handleEvent. Make sure we handle both cases. 44 | channel.addCloseTask(channel => actor.send(Ws.ChannelClosed())) 45 | channel.getReceiveSetter.set( 46 | new AbstractReceiveListener() { 47 | override def onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) = { 48 | actor.send(Ws.Text(message.getData)) 49 | } 50 | 51 | override def onFullBinaryMessage(channel: WebSocketChannel, message: BufferedBinaryMessage): Unit = { 52 | actor.send(Ws.Binary( 53 | WebSockets.mergeBuffers(message.getData.getResource:_*).array() 54 | )) 55 | } 56 | 57 | override def onFullPingMessage(channel: WebSocketChannel, message: BufferedBinaryMessage): Unit = { 58 | actor.send(Ws.Ping( 59 | WebSockets.mergeBuffers(message.getData.getResource:_*).array() 60 | )) 61 | } 62 | override def onFullPongMessage(channel: WebSocketChannel, message: BufferedBinaryMessage): Unit = { 63 | actor.send(Ws.Pong( 64 | WebSockets.mergeBuffers(message.getData.getResource:_*).array() 65 | )) 66 | } 67 | 68 | override def onCloseMessage(cm: CloseMessage, channel: WebSocketChannel) = { 69 | actor.send(Ws.Close(cm.getCode, cm.getReason)) 70 | } 71 | } 72 | ) 73 | channel.resumeReceives() 74 | } 75 | } 76 | 77 | class WsChannelActor(channel: WebSocketChannel) 78 | (implicit ac: castor.Context, log: Logger) 79 | extends castor.SimpleActor[Ws.Event]{ 80 | def run(item: Ws.Event): Unit = item match{ 81 | case Ws.Text(value) => WebSockets.sendTextBlocking(value, channel) 82 | case Ws.Binary(value) => WebSockets.sendBinaryBlocking(ByteBuffer.wrap(value), channel) 83 | case Ws.Ping(value) => WebSockets.sendPingBlocking(ByteBuffer.wrap(value), channel) 84 | case Ws.Pong(value) => WebSockets.sendPongBlocking(ByteBuffer.wrap(value), channel) 85 | case Ws.Close(code, reason) => WebSockets.sendCloseBlocking(code, reason, channel) 86 | } 87 | } 88 | 89 | case class WsActor(handle: PartialFunction[Ws.Event, Unit]) 90 | (implicit ac: castor.Context, log: Logger) 91 | extends castor.SimpleActor[Ws.Event]{ 92 | def run(item: Ws.Event): Unit = { 93 | handle.lift(item) 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /cask/src/cask/internal/Conversion.scala: -------------------------------------------------------------------------------- 1 | package cask.internal 2 | 3 | import scala.annotation.implicitNotFound 4 | 5 | @implicitNotFound("Cannot return ${T} as a ${V}") 6 | class Conversion[T, V](val f: T => V) 7 | object Conversion{ 8 | implicit def create[T, V](implicit f: T => V): Conversion[T, V] = new Conversion(f) 9 | } 10 | -------------------------------------------------------------------------------- /cask/src/cask/internal/DispatchTrie.scala: -------------------------------------------------------------------------------- 1 | package cask.internal 2 | import collection.mutable 3 | object DispatchTrie{ 4 | def construct[T, V](index: Int, 5 | inputs: collection.Seq[(collection.IndexedSeq[String], T, Boolean)]) 6 | (validationGroups: T => Seq[V]): DispatchTrie[T] = { 7 | val continuations = mutable.Map.empty[String, mutable.Buffer[(collection.IndexedSeq[String], T, Boolean)]] 8 | 9 | val terminals = mutable.Buffer.empty[(collection.IndexedSeq[String], T, Boolean)] 10 | 11 | for((path, endPoint, allowSubpath) <- inputs) { 12 | if (path.length < index) () // do nothing 13 | else if (path.length == index) { 14 | terminals.append((path, endPoint, allowSubpath)) 15 | } else if (path.length > index){ 16 | val buf = continuations.getOrElseUpdate(path(index), mutable.Buffer.empty) 17 | buf.append((path, endPoint, allowSubpath)) 18 | } 19 | } 20 | 21 | for(group <- inputs.flatMap(t => validationGroups(t._2)).distinct) { 22 | val groupTerminals = terminals.flatMap{case (path, v, allowSubpath) => 23 | validationGroups(v) 24 | .filter(_ == group) 25 | .map{group => (path, v, allowSubpath, group)} 26 | } 27 | 28 | val groupContinuations = continuations 29 | .map { case (k, vs) => 30 | k -> vs.flatMap { case (path, v, allowSubpath) => 31 | validationGroups(v) 32 | .filter(_ == group) 33 | .map { group => (path, v, allowSubpath, group) } 34 | } 35 | } 36 | .filter(_._2.nonEmpty) 37 | 38 | validateGroup(groupTerminals, groupContinuations) 39 | } 40 | 41 | val dynamicChildren = continuations.filter(_._1.startsWith(":")) 42 | .flatMap(_._2).toIndexedSeq 43 | 44 | DispatchTrie[T]( 45 | current = terminals.headOption 46 | .map{ case (path, value, capturesSubpath) => 47 | val argNames = path.filter(_.startsWith(":")).map(_.drop(1)).toVector 48 | (value, capturesSubpath, argNames) 49 | }, 50 | staticChildren = continuations 51 | .filter(!_._1.startsWith(":")) 52 | .map{ case (k, vs) => (k, construct(index + 1, vs)(validationGroups))} 53 | .toMap, 54 | dynamicChildren = if (dynamicChildren.isEmpty) None else Some(construct(index + 1, dynamicChildren)(validationGroups)) 55 | ) 56 | } 57 | 58 | def validateGroup[T, V](terminals: collection.Seq[(collection.Seq[String], T, Boolean, V)], 59 | continuations: mutable.Map[String, mutable.Buffer[(collection.IndexedSeq[String], T, Boolean, V)]]) = { 60 | 61 | def renderTerminals = terminals 62 | .map{case (path, v, allowSubpath, group) => s"$group${renderPath(path)}"} 63 | .mkString(", ") 64 | 65 | def renderContinuations = continuations.toSeq 66 | .flatMap(_._2) 67 | .map{case (path, v, allowSubpath, group) => s"$group${renderPath(path)}"} 68 | .mkString(", ") 69 | 70 | if (terminals.length > 1) { 71 | throw new Exception( 72 | s"More than one endpoint has the same path: $renderTerminals" 73 | ) 74 | } 75 | 76 | if (terminals.headOption.exists(_._3) && continuations.size == 1) { 77 | throw new Exception( 78 | s"Routes overlap with subpath capture: $renderTerminals, $renderContinuations" 79 | ) 80 | } 81 | } 82 | 83 | def renderPath(p: collection.Seq[String]) = " /" + p.mkString("/") 84 | } 85 | 86 | /** 87 | * A simple Trie that can be compiled from a list of endpoints, to allow 88 | * endpoint lookup in O(length-of-request-path) time. Lookup returns the 89 | * [[T]] this trie contains, as well as a map of bound wildcards (path 90 | * segments starting with `:`) and any remaining un-used path segments 91 | * (only when `current._2 == true`, indicating this route allows trailing 92 | * segments) 93 | * current = (value, captures subpaths, argument names) 94 | */ 95 | case class DispatchTrie[T]( 96 | current: Option[(T, Boolean, Vector[String])], 97 | staticChildren: Map[String, DispatchTrie[T]], 98 | dynamicChildren: Option[DispatchTrie[T]] 99 | ) { 100 | 101 | final def lookup(remainingInput: List[String], 102 | bindings: Vector[String]) 103 | : Option[(T, Map[String, String], Seq[String])] = { 104 | remainingInput match { 105 | case Nil => 106 | current.map(x => (x._1, x._3.zip(bindings).toMap, Nil)) 107 | case head :: rest if current.exists(_._2) => 108 | current.map(x => (x._1, x._3.zip(bindings).toMap, head :: rest)) 109 | case head :: rest => 110 | staticChildren.get(head) match { 111 | case Some(continuation) => continuation.lookup(rest, bindings) 112 | case None => 113 | dynamicChildren match { 114 | case Some(continuation) => continuation.lookup(rest, bindings :+ head) 115 | case None => None 116 | } 117 | } 118 | } 119 | } 120 | 121 | def map[V](f: T => V): DispatchTrie[V] = DispatchTrie( 122 | current.map{case (t, v, a) => (f(t), v, a)}, 123 | staticChildren.map { case (k, v) => (k, v.map(f))}, 124 | dynamicChildren.map { case v => v.map(f)}, 125 | ) 126 | } 127 | -------------------------------------------------------------------------------- /cask/src/cask/internal/ThreadBlockingHandler.scala: -------------------------------------------------------------------------------- 1 | package cask.internal 2 | 3 | import io.undertow.server.{HttpHandler, HttpServerExchange} 4 | 5 | import java.util.concurrent.Executor 6 | 7 | /** 8 | * A handler that dispatches the request to the given handler using the given executor. 9 | * */ 10 | final class ThreadBlockingHandler(executor: Executor, handler: HttpHandler) extends HttpHandler { 11 | require(executor ne null, "Executor should not be null") 12 | require(handler ne null, "Handler should not be null") 13 | 14 | def handleRequest(exchange: HttpServerExchange): Unit = { 15 | exchange.startBlocking() 16 | exchange.dispatch(executor, handler) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cask/src/cask/main/ErrorMsgs.scala: -------------------------------------------------------------------------------- 1 | package cask.main 2 | 3 | 4 | import cask.internal.Util 5 | import cask.internal.Util.literalize 6 | import cask.router.{ArgSig, EntryPoint, Result} 7 | 8 | object ErrorMsgs { 9 | def getLeftColWidth(items: Seq[ArgSig[_, _, _,_]]) = { 10 | items.map(_.name.length + 2) match{ 11 | case Nil => 0 12 | case x => x.max 13 | } 14 | } 15 | 16 | def renderArg[T](base: T, 17 | arg: ArgSig[_, T, _, _], 18 | leftOffset: Int, 19 | wrappedWidth: Int): (String, String) = { 20 | val suffix = arg.default match{ 21 | case Some(f) => " (default " + f(base) + ")" 22 | case None => "" 23 | } 24 | val docSuffix = arg.doc match{ 25 | case Some(d) => ": " + d 26 | case None => "" 27 | } 28 | val wrapped = Util.softWrap( 29 | arg.typeString + suffix + docSuffix, 30 | leftOffset, 31 | wrappedWidth - leftOffset 32 | ) 33 | (arg.name, wrapped) 34 | } 35 | 36 | def formatMainMethodSignature[T](base: T, 37 | main: EntryPoint[T, _], 38 | leftIndent: Int, 39 | leftColWidth: Int) = { 40 | // +2 for space on right of left col 41 | val args = main.argSignatures.last.map(as => renderArg(base, as, leftColWidth + leftIndent + 2 + 2, 80)) 42 | 43 | val leftIndentStr = " " * leftIndent 44 | val argStrings = 45 | for((lhs, rhs) <- args) 46 | yield { 47 | val lhsPadded = lhs.padTo(leftColWidth, ' ') 48 | val rhsPadded = rhs.linesIterator.mkString("\n") 49 | s"$leftIndentStr $lhsPadded $rhsPadded" 50 | } 51 | val mainDocSuffix = main.doc match{ 52 | case Some(d) => "\n" + leftIndentStr + Util.softWrap(d, leftIndent, 80) 53 | case None => "" 54 | } 55 | 56 | s"""$leftIndentStr${main.name}$mainDocSuffix 57 | |${argStrings.map(_ + "\n").mkString}""".stripMargin 58 | } 59 | 60 | def formatInvokeError[T](base: T, route: EntryPoint[T, _], x: Result.Error): String = { 61 | def expectedMsg = formatMainMethodSignature(base: T, route, 0, 0) 62 | 63 | x match{ 64 | case Result.Error.Exception(x) => Util.stackTraceString(x) 65 | case Result.Error.MismatchedArguments(missing, unknown) => 66 | val missingStr = 67 | if (missing.isEmpty) "" 68 | else { 69 | val chunks = 70 | for (x <- missing) 71 | yield x.name + ": " + x.typeString 72 | 73 | val argumentsStr = Util.pluralize("argument", chunks.length) 74 | s"Missing $argumentsStr: (${chunks.mkString(", ")})\n" 75 | } 76 | 77 | 78 | val unknownStr = 79 | if (unknown.isEmpty) "" 80 | else { 81 | val argumentsStr = Util.pluralize("argument", unknown.length) 82 | s"Unknown $argumentsStr: " + unknown.map(literalize(_)).mkString(" ") + "\n" 83 | } 84 | 85 | 86 | s"""$missingStr$unknownStr 87 | |Arguments provided did not match expected signature: 88 | | 89 | |$expectedMsg 90 | |""".stripMargin 91 | 92 | case Result.Error.InvalidArguments(x) => 93 | val argumentsStr = Util.pluralize("argument", x.length) 94 | val thingies = x.map{ 95 | case Result.ParamError.Invalid(p, v, ex) => 96 | val literalV = literalize(v) 97 | val trace = Util.stackTraceString(ex) 98 | s"${p.name}: ${p.typeString} = $literalV failed to parse with $ex\n$trace" 99 | case Result.ParamError.DefaultFailed(p, ex) => 100 | val trace = Util.stackTraceString(ex) 101 | s"${p.name}'s default value failed to evaluate with $ex\n$trace" 102 | } 103 | 104 | s"""The following $argumentsStr failed to parse: 105 | | 106 | |${thingies.mkString("\n")} 107 | | 108 | |expected signature: 109 | | 110 | |$expectedMsg 111 | |""".stripMargin 112 | 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /cask/src/cask/model/Params.scala: -------------------------------------------------------------------------------- 1 | package cask.model 2 | 3 | import java.io.{ByteArrayOutputStream, InputStream} 4 | import cask.internal.Util 5 | import io.undertow.server.HttpServerExchange 6 | import io.undertow.server.handlers.CookieImpl 7 | import io.undertow.util.HttpString 8 | import scala.util.Try 9 | import scala.collection.JavaConverters.collectionAsScalaIterableConverter 10 | 11 | case class QueryParams(value: Map[String, collection.Seq[String]]) 12 | case class RemainingPathSegments(value: Seq[String]) 13 | 14 | case class Request(exchange: HttpServerExchange, remainingPathSegments: Seq[String], boundPathSegments: Map[String, String]) 15 | extends geny.ByteData with geny.Readable { 16 | import collection.JavaConverters._ 17 | lazy val cookies: Map[String, Cookie] = { 18 | exchange.getRequestCookies.asScala.mapValues(Cookie.fromUndertow).toMap 19 | } 20 | lazy val data: InputStream = exchange.getInputStream 21 | 22 | /** 23 | * Read all the bytes of the incoming request *with* caching 24 | */ 25 | lazy val bytes = readAllBytes() 26 | 27 | /** 28 | * Read all the bytes of the incoming request *without* caching 29 | */ 30 | def readAllBytes() = { 31 | val baos = new ByteArrayOutputStream() 32 | Util.transferTo(data, baos) 33 | baos.toByteArray 34 | } 35 | lazy val queryParams: Map[String, collection.Seq[String]] = { 36 | exchange.getQueryParameters.asScala.mapValues(_.asScala.toArray.toSeq).toMap 37 | } 38 | lazy val headers: Map[String, collection.Seq[String]] = { 39 | exchange.getRequestHeaders.asScala 40 | .map{ header => header.getHeaderName.toString.toLowerCase -> header.asScala } 41 | .toMap 42 | } 43 | 44 | def readBytesThrough[T](f: InputStream => T) = f(data) 45 | } 46 | object Cookie{ 47 | 48 | def fromUndertow(from: io.undertow.server.handlers.Cookie): Cookie = { 49 | Cookie( 50 | from.getName, 51 | from.getValue, 52 | from.getComment, 53 | from.getDomain, 54 | if (from.getExpires == null) null else from.getExpires.toInstant, 55 | from.getMaxAge, 56 | from.getPath, 57 | from.getVersion, 58 | from.isDiscard, 59 | from.isHttpOnly, 60 | from.isSecure, 61 | from.getSameSiteMode 62 | ) 63 | } 64 | def toUndertow(from: Cookie): io.undertow.server.handlers.Cookie = { 65 | val out = new CookieImpl(from.name, from.value) 66 | out.setComment(from.comment) 67 | out.setDomain(from.domain) 68 | out.setExpires(if (from.expires == null) null else java.util.Date.from(from.expires)) 69 | out.setMaxAge(from.maxAge) 70 | out.setPath(from.path) 71 | out.setVersion(from.version) 72 | out.setDiscard(from.discard) 73 | out.setHttpOnly(from.httpOnly) 74 | out.setSecure(from.secure) 75 | out.setSameSiteMode(from.sameSite) 76 | } 77 | } 78 | case class Cookie(name: String, 79 | value: String, 80 | comment: String = null, 81 | domain: String = null, 82 | expires: java.time.Instant = null, 83 | maxAge: Integer = null, 84 | path: String = null, 85 | version: Int = 1, 86 | discard: Boolean = false, 87 | httpOnly: Boolean = false, 88 | secure: Boolean = false, 89 | sameSite: String = "Lax") { 90 | 91 | } 92 | 93 | 94 | sealed trait FormEntry{ 95 | def valueOrFileName: String 96 | def headers: io.undertow.util.HeaderMap 97 | def asFile: Option[FormFile] = this match{ 98 | case p: FormValue => None 99 | case p: FormFile => Some(p) 100 | } 101 | } 102 | object FormEntry{ 103 | def fromUndertow(from: io.undertow.server.handlers.form.FormData.FormValue): FormEntry = { 104 | val isOctetStream = Option(from.getHeaders) 105 | .flatMap(headers => Option(headers.get(HttpString.tryFromString("Content-Type")))) 106 | .exists(h => h.asScala.exists(v => v == "application/octet-stream")) 107 | // browsers will set empty file fields to content type: octet-stream 108 | if (isOctetStream || from.isFileItem) FormFile(from.getFileName, Try(from.getFileItem.getFile).toOption, from.getHeaders) 109 | else FormValue(from.getValue, from.getHeaders) 110 | } 111 | 112 | } 113 | case class FormValue(value: String, 114 | headers: io.undertow.util.HeaderMap) extends FormEntry{ 115 | def valueOrFileName = value 116 | } 117 | 118 | case class FormFile(fileName: String, 119 | filePath: Option[java.nio.file.Path], 120 | headers: io.undertow.util.HeaderMap) extends FormEntry{ 121 | def valueOrFileName = fileName 122 | } 123 | 124 | case class EmptyFormEntry() 125 | -------------------------------------------------------------------------------- /cask/src/cask/model/Response.scala: -------------------------------------------------------------------------------- 1 | package cask.model 2 | 3 | import java.io.{InputStream, OutputStream, OutputStreamWriter} 4 | 5 | import cask.internal.Util 6 | 7 | 8 | /** 9 | * The basic response returned by a HTTP endpoint. 10 | * 11 | * Note that [[data]] by default can take in a wide range of types: strings, 12 | * bytes, uPickle JSON-convertable types or arbitrary input streams. You can 13 | * also construct your own implementations of `Response.Data`. 14 | */ 15 | case class Response[+T]( 16 | data: T, 17 | statusCode: Int, 18 | headers: Seq[(String, String)], 19 | cookies: Seq[Cookie] 20 | ){ 21 | def map[V](f: T => V) = new Response(f(data), statusCode, headers, cookies) 22 | } 23 | 24 | object Response { 25 | type Raw = Response[Data] 26 | def apply[T](data: T, 27 | statusCode: Int = 200, 28 | headers: Seq[(String, String)] = Nil, 29 | cookies: Seq[Cookie] = Nil) = new Response(data, statusCode, headers, cookies) 30 | trait Data{ 31 | def write(out: OutputStream): Unit 32 | def headers: Seq[(String, String)] 33 | } 34 | trait DataCompanion[V]{ 35 | // Put the implicit constructors for Response[Data] into the `Data` companion 36 | // object and all subclasses of `Data`, because for some reason putting them in 37 | // the `Response` companion object doesn't work properly. For the same unknown 38 | // reasons, we cannot have `dataResponse` and `dataResponse2` take two type 39 | // params T and V, and instead have to embed the implicit target type as a 40 | // parameter of the enclosing trait 41 | 42 | implicit def dataResponse[T](t: T)(implicit c: T => V): Response[V] = { 43 | Response(c(t)) 44 | } 45 | 46 | implicit def dataResponse2[T](t: Response[T])(implicit c: T => V): Response[V] = { 47 | t.map(c(_)) 48 | } 49 | } 50 | object Data extends DataCompanion[Data]{ 51 | implicit class UnitData(s: Unit) extends Data{ 52 | def write(out: OutputStream) = () 53 | def headers = Nil 54 | } 55 | implicit class WritableData[T](s: T)(implicit f: T => geny.Writable) extends Data{ 56 | val writable = f(s) 57 | def write(out: OutputStream) = writable.writeBytesTo(out) 58 | 59 | def headers = 60 | writable.httpContentType.map("Content-Type" -> _).toSeq ++ 61 | writable.contentLength.map("Content-Length" -> _.toString) 62 | } 63 | implicit class NumericData[T: Numeric](s: T) extends Data{ 64 | def write(out: OutputStream) = out.write(s.toString.getBytes) 65 | def headers = Nil 66 | } 67 | implicit class BooleanData(s: Boolean) extends Data{ 68 | def write(out: OutputStream) = out.write(s.toString.getBytes) 69 | def headers = Nil 70 | } 71 | } 72 | } 73 | 74 | object Redirect{ 75 | def apply(url: String) = Response("", 301, Seq("Location" -> url), Nil) 76 | } 77 | object Abort{ 78 | def apply(code: Int) = Response("", code, Nil, Nil) 79 | } 80 | object StaticFile{ 81 | def apply(path: String, headers: Seq[(String, String)]) = { 82 | val relPath = java.nio.file.Paths.get(path) 83 | val (data0, statusCode0) = 84 | if (java.nio.file.Files.exists(relPath) && java.nio.file.Files.isRegularFile(relPath)){ 85 | (java.nio.file.Files.newInputStream(relPath): Response.Data, 200) 86 | }else{ 87 | ("": Response.Data, 404) 88 | } 89 | Response(data0, statusCode0, headers, Nil) 90 | } 91 | } 92 | object StaticResource{ 93 | def apply(path: String, resourceRoot: ClassLoader, headers: Seq[(String, String)]) = { 94 | val (data0, statusCode0) = resourceRoot.getResourceAsStream(path) match{ 95 | case null => ("": Response.Data, 404) 96 | case res => (res: Response.Data, 200) 97 | } 98 | Response(data0, statusCode0, headers, Nil) 99 | } 100 | } 101 | 102 | 103 | -------------------------------------------------------------------------------- /cask/src/cask/package.scala: -------------------------------------------------------------------------------- 1 | import cask.util.Logger 2 | 3 | package object cask { 4 | // model 5 | type Response[T] = model.Response[T] 6 | val Response = model.Response 7 | val Abort = model.Abort 8 | val Redirect = model.Redirect 9 | val StaticFile = model.StaticFile 10 | val StaticResource = model.StaticResource 11 | type FormEntry = model.FormEntry 12 | val FormEntry = model.FormEntry 13 | type FormValue = model.FormValue 14 | val FormValue = model.FormValue 15 | type FormFile = model.FormFile 16 | val FormFile = model.FormFile 17 | type Cookie = model.Cookie 18 | val Cookie = model.Cookie 19 | type Request = model.Request 20 | val Request = model.Request 21 | type QueryParams = model.QueryParams 22 | val QueryParams = model.QueryParams 23 | type RemainingPathSegments = model.RemainingPathSegments 24 | val RemainingPathSegments = model.RemainingPathSegments 25 | 26 | // endpoints 27 | type websocket = endpoints.websocket 28 | val WebsocketResult = endpoints.WebsocketResult 29 | type WebsocketResult = endpoints.WebsocketResult 30 | 31 | type get = endpoints.get 32 | type post = endpoints.post 33 | type put = endpoints.put 34 | type delete = endpoints.delete 35 | type patch = endpoints.patch 36 | type route = endpoints.route 37 | type staticFiles = endpoints.staticFiles 38 | type staticResources = endpoints.staticResources 39 | type postJson = endpoints.postJson 40 | type postJsonCached = endpoints.postJsonCached 41 | type getJson = endpoints.getJson 42 | type postForm = endpoints.postForm 43 | type options = endpoints.options 44 | 45 | // main 46 | type MainRoutes = main.MainRoutes 47 | type Routes = main.Routes 48 | 49 | type Main = main.Main 50 | type RawDecorator = router.RawDecorator 51 | type HttpEndpoint[InnerReturned, Input] = router.HttpEndpoint[InnerReturned, Input] 52 | 53 | type WsHandler = cask.endpoints.WsHandler 54 | val WsHandler = cask.endpoints.WsHandler 55 | type WsActor = cask.endpoints.WsActor 56 | val WsActor = cask.endpoints.WsActor 57 | type WsChannelActor = cask.endpoints.WsChannelActor 58 | type WsClient = cask.util.WsClient 59 | val WsClient = cask.util.WsClient 60 | val Ws = cask.util.Ws 61 | 62 | // util 63 | type Logger = util.Logger 64 | val Logger = util.Logger 65 | } 66 | -------------------------------------------------------------------------------- /cask/src/cask/router/Decorators.scala: -------------------------------------------------------------------------------- 1 | package cask.router 2 | 3 | import cask.internal.Conversion 4 | import cask.model.{Request, Response} 5 | 6 | /** 7 | * A [[Decorator]] allows you to annotate a function to wrap it, via 8 | * `wrapFunction`. You can use this to perform additional validation before or 9 | * after the function runs, provide an additional parameter list of params, 10 | * open/commit/rollback database transactions before/after the function runs, 11 | * or even retrying the wrapped function if it fails. 12 | * 13 | * Calls to the wrapped function are done on the `delegate` parameter passed 14 | * to `wrapFunction`, which takes a `Map` representing any additional argument 15 | * lists (if any). 16 | */ 17 | trait Decorator[OuterReturned, InnerReturned, Input, InputContext] extends scala.annotation.Annotation { 18 | final type InputTypeAlias = Input 19 | type InputParser[T] <: ArgReader[Input, T, InputContext] 20 | final type Delegate = (InputContext, Map[String, Input]) => Result[InnerReturned] 21 | def wrapFunction(ctx: Request, delegate: Delegate): Result[OuterReturned] 22 | def getParamParser[T](implicit p: InputParser[T]) = p 23 | } 24 | object Decorator{ 25 | /** 26 | * A stack of [[Decorator]]s is invoked recursively: each decorator's `wrapFunction` 27 | * is invoked around the invocation of all inner decorators, with the inner-most 28 | * decorator finally invoking the route's [[EntryPoint.invoke]] function. 29 | * 30 | * Each decorator (and the final `Endpoint`) contributes a dictionary of name-value 31 | * bindings, which are eventually all passed to [[EntryPoint.invoke]]. Each decorator's 32 | * dictionary corresponds to a different argument list on [[EntryPoint.invoke]]. The 33 | * bindings passed from the router are aggregated with those from the `EndPoint` and 34 | * used as the first argument list. 35 | */ 36 | def invoke[T](ctx: Request, 37 | endpoint: Endpoint[_, _, _, _], 38 | entryPoint: EntryPoint[T, _], 39 | routes: T, 40 | remainingDecorators: List[Decorator[_, _, _, _]], 41 | inputContexts: List[Any], 42 | bindings: List[Map[String, Any]]): Result[Any] = try { 43 | remainingDecorators match { 44 | case head :: rest => 45 | head.asInstanceOf[Decorator[Any, Any, Any, Any]].wrapFunction( 46 | ctx, 47 | (ictx, args) => invoke(ctx, endpoint, entryPoint, routes, rest, ictx :: inputContexts, args :: bindings) 48 | .asInstanceOf[Result[Nothing]] 49 | ) 50 | 51 | case Nil => 52 | endpoint.wrapFunction(ctx, { (ictx: Any, endpointBindings: Map[String, Any]) => 53 | 54 | val mergedEndpointBindings = endpointBindings ++ ctx.boundPathSegments.mapValues(endpoint.wrapPathSegment) 55 | val finalBindings = mergedEndpointBindings :: bindings 56 | 57 | entryPoint 58 | .asInstanceOf[EntryPoint[T, Any]] 59 | .invoke(routes, ictx :: inputContexts, finalBindings) 60 | .asInstanceOf[Result[Nothing]] 61 | }) 62 | } 63 | // Make sure we wrap any exceptions that bubble up from decorator 64 | // bodies, so outer decorators do not need to worry about their 65 | // delegate throwing on them 66 | }catch{case e: Throwable => Result.Error.Exception(e) } 67 | } 68 | 69 | /** 70 | * A [[RawDecorator]] is a decorator that operates on the raw request and 71 | * response stream, before and after the primary [[Endpoint]] does it's job. 72 | */ 73 | trait RawDecorator extends Decorator[Response.Raw, Response.Raw, Any, Request]{ 74 | type InputParser[T] = NoOpParser[Any, T, Request] 75 | } 76 | 77 | 78 | /** 79 | * An [[HttpEndpoint]] that may return something else than a HTTP response, e.g. 80 | * a websocket endpoint which may instead return a websocket event handler 81 | */ 82 | trait Endpoint[OuterReturned, InnerReturned, Input, InputContext] 83 | extends Decorator[OuterReturned, InnerReturned, Input, InputContext]{ 84 | 85 | /** 86 | * What is the path that this particular endpoint matches? 87 | */ 88 | val path: String 89 | /** 90 | * Which HTTP methods does this endpoint support? POST? GET? PUT? Or some 91 | * combination of those? 92 | */ 93 | val methods: Seq[String] 94 | 95 | /** 96 | * Whether or not this endpoint allows matching on sub-paths: does 97 | * `@endpoint("/foo")` capture the path "/foo/bar/baz"? Useful to e.g. have 98 | * an endpoint match URLs with paths in a filesystem (real or virtual) to 99 | * serve files 100 | */ 101 | def subpath: Boolean = false 102 | 103 | def convertToResultType[T](t: T) 104 | (implicit f: Conversion[T, InnerReturned]): InnerReturned = { 105 | f.f(t) 106 | } 107 | 108 | /** 109 | * [[HttpEndpoint]]s are unique among decorators in that they alone can bind 110 | * path segments to parameters, e.g. binding `/hello/:world` to `(world: Int)`. 111 | * In order to do so, we need to box up the path segment strings into an 112 | * [[Input]] so they can later be parsed by [[getParamParser]] into an 113 | * instance of the appropriate type. 114 | */ 115 | def wrapPathSegment(s: String): Input 116 | 117 | } 118 | 119 | /** 120 | * Annotates a Cask endpoint that returns a HTTP [[Response]]; similar to a 121 | * [[RawDecorator]] but with additional metadata and capabilities. 122 | */ 123 | trait HttpEndpoint[InnerReturned, Input] extends Endpoint[Response.Raw, InnerReturned, Input, Request] 124 | 125 | 126 | class NoOpParser[Input, T, InputContext] extends ArgReader[Input, T, InputContext] { 127 | def arity = 1 128 | 129 | def read(ctx: InputContext, label: String, input: Input) = input.asInstanceOf[T] 130 | } 131 | object NoOpParser{ 132 | implicit def instance[Input, T, InputContext]: NoOpParser[Input, T, InputContext] = new NoOpParser[Input, T, InputContext] 133 | implicit def instanceAny[T, InputContext]: NoOpParser[Any, T, InputContext] = new NoOpParser[Any, T, InputContext] 134 | implicit def instanceAnyRequest[T]: NoOpParser[Any, T, Request] = new NoOpParser[Any, T, Request] 135 | } 136 | -------------------------------------------------------------------------------- /cask/src/cask/router/EndpointMetadata.scala: -------------------------------------------------------------------------------- 1 | package cask.router 2 | 3 | case class EndpointMetadata[T](decorators: Seq[Decorator[_, _, _, _]], 4 | endpoint: Endpoint[_, _, _, _], 5 | entryPoint: EntryPoint[T, _]) 6 | object EndpointMetadata{ 7 | // `seqify` is used to statically check that the decorators applied to each 8 | // individual endpoint method line up, and each decorator's `OuterReturned` 9 | // correctly matches the enclosing decorator's `InnerReturned`. We don't bother 10 | // checking decorators defined as part of cask.Main or cask.Routes, since those 11 | // are both more dynamic (and hard to check) and also less often used and thus 12 | // less error prone 13 | def seqify1(d: Decorator[_, _, _, _]) = Seq(d) 14 | def seqify2[T1] 15 | (d1: Decorator[T1, _, _, _]) 16 | (d2: Decorator[_, T1, _, _]) = Seq(d1, d2) 17 | def seqify3[T1, T2] 18 | (d1: Decorator[T1, _, _, _]) 19 | (d2: Decorator[T2, T1, _, _]) 20 | (d3: Decorator[_, T2, _, _]) = Seq(d1, d2, d3) 21 | def seqify4[T1, T2, T3] 22 | (d1: Decorator[T1, _, _, _]) 23 | (d2: Decorator[T2, T1, _, _]) 24 | (d3: Decorator[T3, T2, _, _]) 25 | (d4: Decorator[_, T3, _, _]) = Seq(d1, d2, d3, d4) 26 | def seqify5[T1, T2, T3, T4] 27 | (d1: Decorator[T1, _, _, _]) 28 | (d2: Decorator[T2, T1, _, _]) 29 | (d3: Decorator[T3, T2, _, _]) 30 | (d4: Decorator[T4, T3, _, _]) 31 | (d5: Decorator[_, T4, _, _]) = Seq(d1, d2, d3, d4, d5) 32 | def seqify6[T1, T2, T3, T4, T5] 33 | (d1: Decorator[T1, _, _, _]) 34 | (d2: Decorator[T2, T1, _, _]) 35 | (d3: Decorator[T3, T2, _, _]) 36 | (d4: Decorator[T4, T3, _, _]) 37 | (d5: Decorator[T5, T4, _, _]) 38 | (d6: Decorator[_, T5, _, _]) = Seq(d1, d2, d3, d4) 39 | } 40 | -------------------------------------------------------------------------------- /cask/src/cask/router/EntryPoint.scala: -------------------------------------------------------------------------------- 1 | package cask.router 2 | 3 | 4 | import scala.collection.mutable 5 | 6 | 7 | /** 8 | * What is known about a single endpoint for our routes. It has a [[name]], 9 | * [[argSignatures]] for each argument, and a macro-generated [[invoke0]] 10 | * that performs all the necessary argument parsing and de-serialization. 11 | * 12 | * Realistically, you will probably spend most of your time calling [[invoke]] 13 | * instead, which provides a nicer API to call it that mimmicks the API of 14 | * calling a Scala method. 15 | */ 16 | case class EntryPoint[T, C](name: String, 17 | argSignatures: Seq[Seq[ArgSig[_, T, _, C]]], 18 | doc: Option[String], 19 | invoke0: (T, Seq[C], Seq[Map[String, Any]], Seq[Seq[ArgSig[Any, _, _, C]]]) => Result[Any]){ 20 | 21 | val firstArgs = argSignatures.head 22 | .map(x => x.name -> x) 23 | .toMap[String, ArgSig[_, T, _, C]] 24 | 25 | def invoke(target: T, 26 | ctxs: Seq[C], 27 | paramLists: Seq[Map[String, Any]]): Result[Any] = { 28 | 29 | val missing = mutable.Buffer.empty[ArgSig[_, T, _, C]] 30 | 31 | val unknown = paramLists.head.keys.filter(!firstArgs.contains(_)) 32 | 33 | for(k <- firstArgs.keys) { 34 | if (!paramLists.head.contains(k)) { 35 | val as = firstArgs(k) 36 | if (as.reads.arity > 0 && as.default.isEmpty) missing.append(as) 37 | } 38 | } 39 | 40 | if (missing.nonEmpty || (!argSignatures.exists(_.exists(_.reads.unknownQueryParams)) && unknown.nonEmpty)) { 41 | Result.Error.MismatchedArguments(missing.toSeq, unknown.toSeq) 42 | } else { 43 | try invoke0( 44 | target, 45 | ctxs, 46 | paramLists, 47 | argSignatures.asInstanceOf[Seq[Seq[ArgSig[Any, _, _, C]]]] 48 | ) 49 | catch{case e: Throwable => Result.Error.Exception(e)} 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /cask/src/cask/router/Misc.scala: -------------------------------------------------------------------------------- 1 | package cask.router 2 | 3 | import scala.annotation.StaticAnnotation 4 | 5 | 6 | class doc(s: String) extends StaticAnnotation 7 | 8 | /** 9 | * Models what is known by the router about a single argument: that it has 10 | * a [[name]], a human-readable [[typeString]] describing what the type is 11 | * (just for logging and reading, not a replacement for a `TypeTag`) and 12 | * possible a function that can compute its default value 13 | */ 14 | case class ArgSig[I, -T, +V, -C](name: String, 15 | typeString: String, 16 | doc: Option[String], 17 | default: Option[T => V]) 18 | (implicit val reads: ArgReader[I, V, C]) 19 | 20 | trait ArgReader[I, +T, -C]{ 21 | def arity: Int 22 | def unknownQueryParams: Boolean = false 23 | def remainingPathSegments: Boolean = false 24 | def read(ctx: C, label: String, input: I): T 25 | } 26 | -------------------------------------------------------------------------------- /cask/src/cask/router/Result.scala: -------------------------------------------------------------------------------- 1 | package cask.router 2 | 3 | 4 | 5 | 6 | /** 7 | * Represents what comes out of an attempt to invoke an [[EntryPoint]]. 8 | * Could succeed with a value, but could fail in many different ways. 9 | */ 10 | sealed trait Result[+T]{ 11 | def map[V](f: T => V): Result[V] 12 | def transform[V](f: PartialFunction[Any, V]): Result[V] 13 | } 14 | object Result{ 15 | 16 | /** 17 | * Invoking the [[EntryPoint]] was totally successful, and returned a 18 | * result 19 | */ 20 | case class Success[T](value: T) extends Result[T]{ 21 | def map[V](f: T => V) = Success(f(value)) 22 | def transform[V](f: PartialFunction[Any, V]) = f.lift(value) match { 23 | case None => Success(value).asInstanceOf[Result[V]] 24 | case Some(res) => Success(res) 25 | } 26 | } 27 | 28 | /** 29 | * Invoking the [[EntryPoint]] was not successful 30 | */ 31 | sealed trait Error extends Result[Nothing]{ 32 | def map[V](f: Nothing => V) = this 33 | def transform[V](f: PartialFunction[Any, V]) = this 34 | } 35 | 36 | 37 | object Error{ 38 | 39 | 40 | /** 41 | * Invoking the [[EntryPoint]] failed with an exception while executing 42 | * code within it. 43 | */ 44 | case class Exception(t: Throwable) extends Error 45 | 46 | /** 47 | * Invoking the [[EntryPoint]] failed because the arguments provided 48 | * did not line up with the arguments expected 49 | */ 50 | case class MismatchedArguments(missing: Seq[ArgSig[_, _, _, _]], 51 | unknown: Seq[String]) extends Error 52 | /** 53 | * Invoking the [[EntryPoint]] failed because there were problems 54 | * deserializing/parsing individual arguments 55 | */ 56 | case class InvalidArguments(values: Seq[ParamError]) extends Error 57 | } 58 | 59 | sealed trait ParamError 60 | object ParamError{ 61 | /** 62 | * Something went wrong trying to de-serialize the input parameter; 63 | * the thrown exception is stored in [[ex]] 64 | */ 65 | case class Invalid(arg: ArgSig[_, _, _, _], value: String, ex: Throwable) extends ParamError 66 | /** 67 | * Something went wrong trying to evaluate the default value 68 | * for this input parameter 69 | */ 70 | case class DefaultFailed(arg: ArgSig[_, _, _, _], ex: Throwable) extends ParamError 71 | } 72 | } -------------------------------------------------------------------------------- /cask/src/cask/router/Runtime.scala: -------------------------------------------------------------------------------- 1 | package cask.router 2 | 3 | object Runtime{ 4 | 5 | def tryEither[T](t: => T, error: Throwable => Result.ParamError) = { 6 | try Right(t) 7 | catch{ case e: Throwable => Left(error(e))} 8 | } 9 | 10 | 11 | def validate(args: Seq[Either[Seq[Result.ParamError], Any]]): Result[Seq[Any]] = { 12 | val lefts = args.collect{case Left(x) => x}.flatten 13 | 14 | if (lefts.nonEmpty) Result.Error.InvalidArguments(lefts) 15 | else { 16 | val rights = args.collect{case Right(x) => x} 17 | Result.Success(rights) 18 | } 19 | } 20 | 21 | def validateLists(argss: Seq[Seq[Either[Seq[Result.ParamError], Any]]]): Result[Seq[Seq[Any]]] = { 22 | val lefts: Seq[Result.ParamError] = argss.flatMap(_.collect{case Left(x) => x}.flatten) 23 | 24 | if (lefts.nonEmpty) Result.Error.InvalidArguments(lefts) 25 | else { 26 | val rights = argss.map(_.collect{case Right(x) => x}) 27 | Result.Success(rights) 28 | } 29 | } 30 | 31 | def makeReadCall[I, C](dict: Map[String, I], 32 | ctx: C, 33 | default: => Option[Any], 34 | arg: ArgSig[I, _, _, C]) = { 35 | arg.reads.arity match{ 36 | case 0 => 37 | tryEither( 38 | arg.reads.read(ctx, arg.name, null.asInstanceOf[I]), Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) 39 | case 1 => 40 | dict.get(arg.name) match{ 41 | case None => 42 | tryEither(default.get, Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) 43 | 44 | case Some(x) => 45 | tryEither(arg.reads.read(ctx, arg.name, x), Result.ParamError.Invalid(arg, x.toString, _)).left.map(Seq(_)) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cask/test/src-3/test/cask/FailureTests3.scala: -------------------------------------------------------------------------------- 1 | package test.cask 2 | 3 | import cask.model.Request 4 | import utest._ 5 | 6 | object FailureTests3 extends TestSuite { 7 | val tests = Tests{ 8 | "returnType" - { 9 | utest.compileError(""" 10 | object Routes extends cask.MainRoutes{ 11 | @cask.get("/foo") 12 | def hello(world: String) = (1, 1) 13 | initialize() 14 | } 15 | """).msg ==> 16 | "error in route definition `def hello` (at tasty-reflect:4:15): the method's return type scala.Tuple2[scala.Int, scala.Int] cannot be converted to the expected response type cask.model.Response[cask.model.Response.Data]" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cask/test/src/test/cask/DispatchTrieTests.scala: -------------------------------------------------------------------------------- 1 | package test.cask 2 | import cask.internal.DispatchTrie 3 | import utest._ 4 | 5 | object DispatchTrieTests extends TestSuite { 6 | val tests = Tests{ 7 | 8 | "hello" - { 9 | val x = DispatchTrie.construct(0, 10 | Seq((Vector("hello"), 1, false)) 11 | )(Seq(_)) 12 | 13 | assert( 14 | x.lookup(List("hello"), Vector()) == Some((1, Map(), Nil)), 15 | x.lookup(List("hello", "world"), Vector()) == None, 16 | x.lookup(List("world"), Vector()) == None 17 | ) 18 | } 19 | "nested" - { 20 | val x = DispatchTrie.construct(0, 21 | Seq( 22 | (Vector("hello", "world"), 1, false), 23 | (Vector("hello", "cow"), 2, false) 24 | ) 25 | )(Seq(_)) 26 | assert( 27 | x.lookup(List("hello", "world"), Vector()) == Some((1, Map(), Nil)), 28 | x.lookup(List("hello", "cow"), Vector()) == Some((2, Map(), Nil)), 29 | x.lookup(List("hello"), Vector()) == None, 30 | x.lookup(List("hello", "moo"), Vector()) == None, 31 | x.lookup(List("hello", "world", "moo"), Vector()) == None 32 | ) 33 | } 34 | "bindings" - { 35 | val x = DispatchTrie.construct(0, 36 | Seq((Vector(":hello", ":world"), 1, false)) 37 | )(Seq(_)) 38 | assert( 39 | x.lookup(List("hello", "world"), Vector()) == Some((1, Map("hello" -> "hello", "world" -> "world"), Nil)), 40 | x.lookup(List("world", "hello"), Vector()) == Some((1, Map("hello" -> "world", "world" -> "hello"), Nil)), 41 | 42 | x.lookup(List("hello", "world", "cow"), Vector()) == None, 43 | x.lookup(List("hello"), Vector()) == None 44 | ) 45 | } 46 | 47 | "path" - { 48 | val x = DispatchTrie.construct(0, 49 | Seq((Vector("hello"), 1, true)) 50 | )(Seq(_)) 51 | 52 | assert( 53 | x.lookup(List("hello", "world"), Vector()) == Some((1,Map(), Seq("world"))), 54 | x.lookup(List("hello", "world", "cow"), Vector()) == Some((1,Map(), Seq("world", "cow"))), 55 | x.lookup(List("hello"), Vector()) == Some((1,Map(), Seq())), 56 | x.lookup(List(), Vector()) == None 57 | ) 58 | } 59 | 60 | "wildcards" - { 61 | test - { 62 | DispatchTrie.construct(0, 63 | Seq( 64 | (Vector("hello", ":world"), 1, false), 65 | (Vector("hello", "world"), 1, false) 66 | ) 67 | )(Seq(_)) 68 | } 69 | test - { 70 | DispatchTrie.construct(0, 71 | Seq( 72 | (Vector("hello", ":world"), 1, false), 73 | (Vector("hello", "world", "omg"), 2, false) 74 | ) 75 | )(Seq(_)) 76 | } 77 | } 78 | "errors" - { 79 | test - { 80 | DispatchTrie.construct(0, 81 | Seq( 82 | (Vector("hello"), 1, true), 83 | (Vector("hello", "cow", "omg"), 2, false) 84 | ) 85 | )(Seq(_)) 86 | 87 | val ex = intercept[Exception]{ 88 | DispatchTrie.construct(0, 89 | Seq( 90 | (Vector("hello"), 1, true), 91 | (Vector("hello", "cow", "omg"), 1, false) 92 | ) 93 | )(Seq(_)) 94 | } 95 | 96 | assert( 97 | ex.getMessage == 98 | "Routes overlap with subpath capture: 1 /hello, 1 /hello/cow/omg" 99 | ) 100 | } 101 | test - { 102 | DispatchTrie.construct(0, 103 | Seq( 104 | (Vector("hello", ":world"), 1, false), 105 | (Vector("hello", ":cow"), 2, false) 106 | ) 107 | )(Seq(_)) 108 | 109 | val ex = intercept[Exception]{ 110 | DispatchTrie.construct(0, 111 | Seq( 112 | (Vector("hello", ":world"), 1, false), 113 | (Vector("hello", ":cow"), 1, false) 114 | ) 115 | )(Seq(_)) 116 | } 117 | 118 | assert( 119 | ex.getMessage == 120 | "More than one endpoint has the same path: 1 /hello/:world, 1 /hello/:cow" 121 | ) 122 | } 123 | test - { 124 | DispatchTrie.construct(0, 125 | Seq( 126 | (Vector("hello", "world"), 1, false), 127 | (Vector("hello", "world"), 2, false) 128 | ) 129 | )(Seq(_)) 130 | 131 | val ex = intercept[Exception]{ 132 | DispatchTrie.construct(0, 133 | Seq( 134 | (Vector("hello", "world"), 1, false), 135 | (Vector("hello", "world"), 1, false) 136 | ) 137 | )(Seq(_)) 138 | } 139 | assert( 140 | ex.getMessage == 141 | "More than one endpoint has the same path: 1 /hello/world, 1 /hello/world" 142 | ) 143 | } 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /cask/test/src/test/cask/FailureTests.scala: -------------------------------------------------------------------------------- 1 | package test.cask 2 | 3 | import cask.model.Request 4 | import utest._ 5 | 6 | object FailureTests extends TestSuite { 7 | class myDecorator extends cask.RawDecorator { 8 | def wrapFunction(ctx: Request, delegate: Delegate) = { 9 | delegate(ctx, Map("extra" -> 31337)) 10 | } 11 | } 12 | 13 | val tests = Tests{ 14 | "mismatchedDecorators" - { 15 | val m = utest.compileError(""" 16 | object Decorated extends cask.MainRoutes{ 17 | @myDecorator 18 | @cask.websocket("/hello/:world") 19 | def hello(world: String)(extra: Int) = ??? 20 | initialize() 21 | } 22 | """).msg 23 | assert(m.contains("required: cask.router.Decorator[_, cask.endpoints.WebsocketResult, _, _]")) 24 | } 25 | 26 | "noEndpoint" - { 27 | utest.compileError(""" 28 | object Decorated extends cask.MainRoutes{ 29 | @cask.get("/hello/:world") 30 | @myDecorator() 31 | def hello(world: String)(extra: Int)= world 32 | initialize() 33 | } 34 | """).msg ==> 35 | "Last annotation applied to a function must be an instance of Endpoint, not test.cask.FailureTests.myDecorator" 36 | } 37 | 38 | "tooManyEndpoint" - { 39 | utest.compileError(""" 40 | object Decorated extends cask.MainRoutes{ 41 | @cask.get("/hello/:world") 42 | @cask.get("/hello/:world") 43 | def hello(world: String)(extra: Int)= world 44 | initialize() 45 | } 46 | """).msg ==> 47 | "You can only apply one Endpoint annotation to a function, not 2 in cask.endpoints.get, cask.endpoints.get" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cask/test/src/test/cask/UtilTests.scala: -------------------------------------------------------------------------------- 1 | package test.cask 2 | 3 | import utest._ 4 | 5 | object UtilTests extends TestSuite { 6 | val tests = Tests{ 7 | "splitPath" - { 8 | cask.internal.Util.splitPath("") ==> Seq() 9 | cask.internal.Util.splitPath("/") ==> Seq() 10 | cask.internal.Util.splitPath("////") ==> Seq() 11 | 12 | cask.internal.Util.splitPath("abc") ==> Seq("abc") 13 | cask.internal.Util.splitPath("/abc/") ==> Seq("abc") 14 | cask.internal.Util.splitPath("//abc") ==> Seq("abc") 15 | cask.internal.Util.splitPath("abc//") ==> Seq("abc") 16 | 17 | cask.internal.Util.splitPath("abc//def") ==> Seq("abc", "def") 18 | cask.internal.Util.splitPath("//abc//def//") ==> Seq("abc", "def") 19 | } 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /cask/util/src-js/cask/util/Scheduler.scala: -------------------------------------------------------------------------------- 1 | package cask.util 2 | object Scheduler{ 3 | def schedule(millis: Long)(body: => Unit) = { 4 | scala.scalajs.js.timers.setTimeout(millis)(body) 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cask/util/src-js/cask/util/WebsocketClientImpl.scala: -------------------------------------------------------------------------------- 1 | package cask.util 2 | 3 | import org.scalajs.dom 4 | 5 | abstract class WebsocketClientImpl(url: String) extends WebsocketBase{ 6 | var websocket: dom.WebSocket = null 7 | var closed = false 8 | def connect(): Unit = { 9 | websocket = new dom.WebSocket(url) 10 | assert(closed == false) 11 | websocket.onopen = (e: dom.Event) => onOpen() 12 | websocket.onmessage = (e: dom.MessageEvent) => onMessage(e.data.asInstanceOf[String]) 13 | websocket.onclose = (e: dom.CloseEvent) => { 14 | closed = true 15 | onClose(e.code, e.reason) 16 | } 17 | websocket.onerror = (e: dom.Event) => onError(new Exception(e.toString)) 18 | } 19 | def onOpen(): Unit 20 | 21 | def send(value: String) = try { 22 | websocket.send(value) 23 | true 24 | } catch{case e: scala.scalajs.js.JavaScriptException => false} 25 | 26 | 27 | def send(value: Array[Byte]) = ??? 28 | def onError(ex: Exception): Unit 29 | def onMessage(value: String): Unit 30 | def onClose(code: Int, reason: String): Unit 31 | def close(): Unit = { 32 | if (!closed) websocket.close() 33 | } 34 | def isClosed() = closed 35 | } 36 | -------------------------------------------------------------------------------- /cask/util/src-jvm/cask/util/Scheduler.scala: -------------------------------------------------------------------------------- 1 | package cask.util 2 | 3 | import java.util.concurrent.{Executors, TimeUnit} 4 | 5 | object Scheduler{ 6 | val scheduler = Executors.newSingleThreadScheduledExecutor() 7 | def schedule(millis: Long)(body: => Unit) = { 8 | scheduler.schedule( 9 | new Runnable { 10 | def run(): Unit = body 11 | }, 12 | millis, 13 | TimeUnit.MILLISECONDS 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /cask/util/src-jvm/cask/util/WebsocketClientImpl.scala: -------------------------------------------------------------------------------- 1 | package cask.util 2 | import org.java_websocket.client.WebSocketClient 3 | import org.java_websocket.handshake.ServerHandshake 4 | 5 | abstract class WebsocketClientImpl(url: String) extends WebsocketBase{ 6 | var websocket: Client = null 7 | var closed = false 8 | def connect(): Unit = { 9 | assert(closed == false) 10 | websocket = new Client() 11 | websocket.connect() 12 | } 13 | def onOpen(): Unit 14 | def onMessage(message: String): Unit 15 | def send(message: String) = try{ 16 | websocket.send(message) 17 | true 18 | }catch{ 19 | case e: org.java_websocket.exceptions.WebsocketNotConnectedException => false 20 | } 21 | def send(message: Array[Byte]) = try{ 22 | websocket.send(message) 23 | true 24 | }catch{ 25 | case e: org.java_websocket.exceptions.WebsocketNotConnectedException => false 26 | } 27 | def onClose(code: Int, reason: String): Unit 28 | def onError(ex: Exception): Unit 29 | def close(): Unit = { 30 | if (!closed) websocket.close() 31 | } 32 | def isClosed() = websocket.isClosed() 33 | class Client() extends WebSocketClient(new java.net.URI(url)){ 34 | def onOpen(handshakedata: ServerHandshake) = { 35 | WebsocketClientImpl.this.onOpen() 36 | } 37 | def onMessage(message: String) = WebsocketClientImpl.this.onMessage(message) 38 | def onClose(code: Int, reason: String, remote: Boolean) = { 39 | closed = true 40 | WebsocketClientImpl.this.onClose(code, reason) 41 | } 42 | def onError(ex: Exception) = WebsocketClientImpl.this.onError(ex) 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /cask/util/src/cask/util/Logger.scala: -------------------------------------------------------------------------------- 1 | package cask.util 2 | 3 | import sourcecode.{File, Line, Text} 4 | 5 | trait Logger { 6 | def exception(t: Throwable): Unit 7 | 8 | def debug(t: sourcecode.Text[Any])(implicit f: sourcecode.File, line: sourcecode.Line): Unit 9 | } 10 | object Logger{ 11 | object Console { 12 | implicit object globalLogger extends Console() 13 | } 14 | class Console() extends Logger{ 15 | def exception(t: Throwable): Unit = t.printStackTrace() 16 | 17 | def debug(t: Text[Any])(implicit f: File, line: Line): Unit = { 18 | println(f.value.split('/').last + ":" + line + " " + t.source + " " + pprint.apply(t.value)) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cask/util/src/cask/util/WebsocketBase.scala: -------------------------------------------------------------------------------- 1 | package cask.util 2 | 3 | abstract class WebsocketBase{ 4 | def connect(): Unit 5 | def onOpen(): Unit 6 | def onMessage(message: String): Unit 7 | def onMessage(message: Array[Byte]): Unit 8 | def send(message: String): Boolean 9 | def send(message: Array[Byte]): Boolean 10 | def onClose(code: Int, reason: String): Unit 11 | def close(): Unit 12 | def isClosed(): Boolean 13 | def onError(ex: Exception): Unit 14 | } -------------------------------------------------------------------------------- /cask/util/src/cask/util/Ws.scala: -------------------------------------------------------------------------------- 1 | package cask.util 2 | 3 | object Ws{ 4 | trait Event 5 | case class Text(value: String) extends Event 6 | case class Binary(value: Array[Byte]) extends Event 7 | case class Ping(value: Array[Byte] = Array.empty[Byte]) extends Event 8 | case class Pong(value: Array[Byte] = Array.empty[Byte]) extends Event 9 | case class Close(code: Int = Close.NormalClosure, reason: String = "") extends Event 10 | case class Error(e: Throwable) extends Event 11 | case class ChannelClosed() extends Event 12 | object Close{ 13 | // Taken from io.undertow.websockets.core.CloseMessage.* 14 | // See also https://datatracker.ietf.org/doc/html/rfc6455#section-7.4 15 | val NormalClosure = 1000 16 | val GoingAway = 1001 17 | val ProtocolError = 1002 18 | val WrongCode = 1003 19 | val MsgContainsInvalidData = 1007 20 | val MsgViolatesPolicy = 1008 21 | val MsgTooBig = 1009 22 | val MissingExtensions = 1010 23 | val UnexpectedError = 1011 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cask/util/src/cask/util/WsClient.scala: -------------------------------------------------------------------------------- 1 | package cask.util 2 | 3 | import scala.concurrent.duration.Duration 4 | import scala.concurrent.{Await, ExecutionContext, Promise} 5 | 6 | class WsClient(impl: WebsocketBase) 7 | (implicit ac: castor.Context, log: Logger) 8 | extends castor.SimpleActor[Ws.Event]{ 9 | 10 | def run(item: Ws.Event): Unit = item match{ 11 | case Ws.Text(s) => impl.send(s) 12 | case Ws.Binary(s) => impl.send(s) 13 | case Ws.Close(_, _) => impl.close() 14 | case Ws.ChannelClosed() => impl.close() 15 | } 16 | } 17 | 18 | object WsClient{ 19 | def connect(url: String) 20 | (f: PartialFunction[cask.util.Ws.Event, Unit]) 21 | (implicit ac: castor.Context, log: Logger): WsClient = { 22 | Await.result(connectAsync(url)(f), Duration.Inf) 23 | } 24 | def connectAsync(url: String) 25 | (f: PartialFunction[cask.util.Ws.Event, Unit]) 26 | (implicit ac: castor.Context, log: Logger): scala.concurrent.Future[WsClient] = { 27 | object receiveActor extends castor.SimpleActor[Ws.Event] { 28 | def run(item: Ws.Event) = f.lift(item) 29 | } 30 | val p = Promise[WsClient] 31 | val impl = new WebsocketClientImpl(url) { 32 | def onOpen() = { 33 | if (!p.isCompleted) p.success(new WsClient(this)) 34 | } 35 | def onMessage(message: String) = { 36 | receiveActor.send(Ws.Text(message)) 37 | } 38 | def onMessage(message: Array[Byte]) = { 39 | receiveActor.send(Ws.Binary(message)) 40 | } 41 | def onClose(code: Int, reason: String) = { 42 | if (!p.isCompleted) p.failure(new Exception(s"WsClient failed: $code $reason")) 43 | else receiveActor.send(Ws.Close(code, reason)) 44 | } 45 | def onError(ex: Exception): Unit = { 46 | if (!p.isCompleted) p.failure(ex) 47 | else receiveActor.send(Ws.Error(ex)) 48 | } 49 | } 50 | 51 | impl.connect() 52 | p.future 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ci/package.mill: -------------------------------------------------------------------------------- 1 | package build.ci -------------------------------------------------------------------------------- /ci/upload.mill: -------------------------------------------------------------------------------- 1 | package build.ci 2 | 3 | import $ivy.`com.lihaoyi::os-lib:0.8.1` 4 | 5 | @mainargs.main 6 | def apply(uploadedFile: os.Path, 7 | tagName: String, 8 | uploadName: String, 9 | authKey: String): String = { 10 | val body = requests.get( 11 | s"https://api.github.com/repos/com-lihaoyi/cask/releases/tags/$tagName", 12 | headers = Seq("Authorization" -> s"token $authKey") 13 | ).text() 14 | 15 | val parsed = ujson.read(body) 16 | 17 | println(body) 18 | 19 | val snapshotReleaseId = parsed("id").num.toInt 20 | 21 | 22 | val uploadUrl = 23 | s"https://uploads.github.com/repos/com-lihaoyi/cask/releases/" + 24 | s"$snapshotReleaseId/assets?name=$uploadName" 25 | 26 | val res = requests.post( 27 | uploadUrl, 28 | data = os.read.stream(uploadedFile), 29 | headers = Seq( 30 | "Content-Type" -> "application/octet-stream", 31 | "Authorization" -> s"token $authKey" 32 | ), 33 | connectTimeout = 5000, readTimeout = 60000 34 | ) 35 | 36 | 37 | println(res.text()) 38 | val longUrl = ujson.read(res.text())("browser_download_url").str.toString 39 | 40 | longUrl 41 | } 42 | -------------------------------------------------------------------------------- /docs/amm: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This is a wrapper script, that automatically download ammonite from GitHub release pages 4 | # You can give the required ammonite version with AMM_VERSION env variable 5 | # If no version is given, it falls back to the value of DEFAULT_AMM_VERSION 6 | DEFAULT_AMM_VERSION=2.5.8 7 | SCALA_VERSION=2.13 8 | set -e 9 | 10 | if [ -z "$AMM_VERSION" ] ; then 11 | AMM_VERSION=$DEFAULT_AMM_VERSION 12 | fi 13 | 14 | AMM_DOWNLOAD_PATH="$HOME/.ammonite/download" 15 | AMM_EXEC_PATH="${AMM_DOWNLOAD_PATH}/${AMM_VERSION}_$SCALA_VERSION" 16 | 17 | if [ ! -x "$AMM_EXEC_PATH" ] ; then 18 | mkdir -p $AMM_DOWNLOAD_PATH 19 | DOWNLOAD_FILE=$AMM_EXEC_PATH-tmp-download 20 | AMM_DOWNLOAD_URL="https://github.com/lihaoyi/ammonite/releases/download/${AMM_VERSION%%-*}/$SCALA_VERSION-$AMM_VERSION" 21 | curl --fail -L -o "$DOWNLOAD_FILE" "$AMM_DOWNLOAD_URL" 22 | chmod +x "$DOWNLOAD_FILE" 23 | mv "$DOWNLOAD_FILE" "$AMM_EXEC_PATH" 24 | unset DOWNLOAD_FILE 25 | unset AMM_DOWNLOAD_URL 26 | fi 27 | 28 | unset AMM_DOWNLOAD_PATH 29 | unset AMM_VERSION 30 | 31 | exec $AMM_EXEC_PATH "$@" 32 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/com-lihaoyi/cask/0fc00e217bcfae22d4ffdae8644cad666fb3f826/docs/favicon.png -------------------------------------------------------------------------------- /docs/logo-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/pageStyles.sc: -------------------------------------------------------------------------------- 1 | import $ivy.`com.lihaoyi::scalatags:0.9.1` 2 | 3 | import scalatags.stylesheet._ 4 | import scalatags.Text.all._ 5 | 6 | 7 | val marginWidth = "25%" 8 | object WideStyles extends StyleSheet{ 9 | initStyleSheet() 10 | override def customSheetName = Some("WideStyles") 11 | def header = cls( 12 | position.fixed, 13 | top := 0, 14 | bottom := 0, 15 | width := marginWidth, 16 | justifyContent.center, 17 | display.flex, 18 | flexDirection.column 19 | ) 20 | def tableOfContentsItem = cls( 21 | // We have to use inline-block and verticalAlign.middle and width: 100% 22 | // here, instead of simply using display.block, because display.block items 23 | // with overflow.hidden seem to misbehave and render badly in different ways 24 | // between firefox (renders correctly), chrome (body of list item is offset 25 | // one row from the bullet) and safari (bullet is entirely missing) 26 | display.`inline-block`, 27 | width := "100%", 28 | verticalAlign.middle, 29 | overflow.hidden, 30 | textOverflow.ellipsis 31 | 32 | ) 33 | def tableOfContents = cls( 34 | display.flex, 35 | flexDirection.column, 36 | flexGrow := 1, 37 | flexShrink := 1, 38 | minHeight := 0, 39 | width := "100%" 40 | 41 | ) 42 | def content = cls( 43 | padding := "2em 3em 0", 44 | padding := 48, 45 | marginLeft := marginWidth, 46 | boxSizing.`border-box` 47 | ) 48 | def footer = cls( 49 | position.fixed, 50 | bottom := 0, 51 | height := 50, 52 | width := marginWidth 53 | ) 54 | def marginLeftZero = cls( 55 | marginLeft := 0 56 | ) 57 | } 58 | object NarrowStyles extends StyleSheet{ 59 | initStyleSheet() 60 | override def customSheetName = Some("NarrowStyles") 61 | def header = cls( 62 | marginBottom := 10 63 | ) 64 | def content = cls( 65 | padding := 16 66 | ) 67 | def headerContent = cls( 68 | flexDirection.row, 69 | width := "100%", 70 | display.flex, 71 | alignItems.center 72 | ) 73 | 74 | def flexFont = cls( 75 | fontSize := "4vw" 76 | ) 77 | def disappear = cls( 78 | display.none 79 | ) 80 | def floatLeft = cls( 81 | float.left, 82 | marginLeft := 30 83 | ) 84 | } 85 | object Styles extends CascadingStyleSheet{ 86 | initStyleSheet() 87 | override def customSheetName = Some("Styles") 88 | def hoverBox = cls( 89 | display.flex, 90 | flexDirection.row, 91 | alignItems.center, 92 | justifyContent.spaceBetween, 93 | &hover( 94 | hoverLink( 95 | opacity := 0.5 96 | ) 97 | ) 98 | ) 99 | def hoverLink = cls( 100 | opacity := 0.1, 101 | &hover( 102 | opacity := 1.0 103 | ) 104 | ) 105 | def headerStyle = cls( 106 | backgroundColor := "rgb(61, 79, 93)", 107 | display.flex, 108 | boxSizing.`border-box` 109 | ) 110 | def headerLinkBox = cls( 111 | flex := 1, 112 | display.flex, 113 | flexDirection.column, 114 | ) 115 | def headerLink = cls( 116 | flex := 1, 117 | display.flex, 118 | justifyContent.center, 119 | alignItems.center, 120 | padding := "10px 10px" 121 | ) 122 | def footerStyle = cls( 123 | display.flex, 124 | justifyContent.center, 125 | color := "rgb(158, 167, 174)" 126 | ) 127 | def subtleLink = cls( 128 | textDecoration.none 129 | ) 130 | } -------------------------------------------------------------------------------- /docs/pages.sc: -------------------------------------------------------------------------------- 1 | import $ivy.`com.lihaoyi::scalatags:0.9.1` 2 | import scalatags.Text.all._, scalatags.Text.tags2 3 | import java.time.LocalDate 4 | import $file.pageStyles, pageStyles._ 5 | 6 | case class PostInfo(name: String, 7 | headers: Seq[(String, Int)], 8 | rawHtmlContent: String) 9 | 10 | 11 | def sanitize(s: String): String = { 12 | s.split(" |-", -1).map(_.filter(_.isLetterOrDigit)).mkString("-").toLowerCase 13 | } 14 | def pageChrome(titleText: Option[String], 15 | homePage: Boolean, 16 | contents: Frag, 17 | contentHeaders: Seq[(String, Int)], 18 | pageHeaders: Seq[String]): String = { 19 | val pageTitle = titleText.getOrElse("Mill") 20 | val sheets = Seq( 21 | "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", 22 | "https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css", 23 | "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.1.0/styles/github-gist.min.css" 24 | ) 25 | 26 | 27 | html( 28 | head( 29 | meta(charset := "utf-8"), 30 | for(sheet <- sheets) 31 | yield link(href := sheet, rel := "stylesheet", `type` := "text/css" ), 32 | tags2.title(pageTitle), 33 | tags2.style(s"@media (min-width: 60em) {${WideStyles.styleSheetText}}"), 34 | tags2.style(s"@media (max-width: 60em) {${NarrowStyles.styleSheetText}}"), 35 | tags2.style(Styles.styleSheetText), 36 | script(src:="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.1.0/highlight.min.js"), 37 | script(src:="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.1.0/languages/scala.min.js"), 38 | script(raw("hljs.initHighlightingOnLoad();")), 39 | // This makes media queries work on iphone (???) 40 | // http://stackoverflow.com/questions/13002731/responsive-design-media-query-not-working-on-iphone 41 | meta(name:="viewport", content:="initial-scale = 1.0,maximum-scale = 1.0") 42 | ), 43 | body(margin := 0, backgroundColor := "#f8f8f8")( 44 | navBar(homePage, contentHeaders, pageHeaders), 45 | div( 46 | WideStyles.content, 47 | NarrowStyles.content, 48 | maxWidth := 900, 49 | titleText.map(h1(_)), 50 | contents 51 | ), 52 | if (contentHeaders.nonEmpty) frag() 53 | else div( 54 | WideStyles.footer, 55 | Styles.footerStyle, 56 | "Last published ", currentTimeText 57 | ) 58 | 59 | ) 60 | ).render 61 | } 62 | 63 | def navBar(homePage: Boolean, contentHeaders: Seq[(String, Int)], pageHeaders: Seq[String]) = { 64 | def navList(navLabel: String, frags: Frag, narrowHide: Boolean) = { 65 | div( 66 | WideStyles.tableOfContents, 67 | if(narrowHide) NarrowStyles.disappear else frag(), 68 | color := "#f8f8f8" 69 | )( 70 | div(paddingLeft := 40, NarrowStyles.disappear)( 71 | b(navLabel) 72 | ), 73 | div(overflowY.auto, flexShrink := 1, minHeight := 0)( 74 | ul( 75 | overflow.hidden, 76 | textAlign.start, 77 | marginTop := 10, 78 | whiteSpace.nowrap, 79 | textOverflow.ellipsis, 80 | marginRight := 10 81 | )( 82 | frags 83 | ) 84 | ) 85 | ) 86 | } 87 | val pageList = navList( 88 | "Pages", 89 | for((header, i) <- pageHeaders.zipWithIndex) yield li( 90 | WideStyles.marginLeftZero, 91 | NarrowStyles.floatLeft 92 | )( 93 | a( 94 | color := "#f8f8f8", 95 | WideStyles.tableOfContentsItem, 96 | href := { 97 | (homePage, i == 0) match { 98 | case (true, true) => s"index.html" 99 | case (true, false) => s"page/${sanitize(header)}.html" 100 | case (false, true) => s"../index.html" 101 | case (false, false) => s"${sanitize(header)}.html" 102 | } 103 | } 104 | )( 105 | header 106 | ) 107 | ), 108 | narrowHide = false 109 | ) 110 | 111 | val headerBox = div( 112 | NarrowStyles.headerContent, 113 | h1( 114 | textAlign.center, 115 | a( 116 | // img( 117 | // src := {homePage match{ 118 | // case false => s"../logo-white.svg" 119 | // case true => "logo-white.svg" 120 | // }}, 121 | // height := 30, 122 | // marginTop := -5 123 | // ), 124 | color := "#f8f8f8", 125 | " Cask", 126 | href := (if (homePage) "" else ".."), 127 | Styles.subtleLink, 128 | NarrowStyles.flexFont, 129 | fontWeight.bold 130 | ), 131 | padding := "30px 30px", 132 | margin := 0 133 | ), 134 | div( 135 | Styles.headerLinkBox, 136 | pageList 137 | ) 138 | ) 139 | 140 | 141 | val tableOfContents = navList( 142 | "Table of Contents", 143 | for { 144 | (header, indent) <- contentHeaders 145 | offset <- indent match{ 146 | case 2 => Some(0) 147 | case 3 => Some(20) 148 | case _ => None 149 | } 150 | } yield li(marginLeft := offset)( 151 | a( 152 | color := "#f8f8f8", 153 | WideStyles.tableOfContentsItem, 154 | href := s"#${sanitize(header)}" 155 | )( 156 | header 157 | ) 158 | ), 159 | narrowHide = true 160 | ) 161 | 162 | div( 163 | WideStyles.header, 164 | NarrowStyles.header, 165 | Styles.headerStyle, 166 | headerBox, 167 | hr(NarrowStyles.disappear, backgroundColor := "#f8f8f8", width := "80%"), 168 | tableOfContents 169 | ) 170 | } 171 | 172 | 173 | val currentTimeText = LocalDate.now.toString 174 | 175 | 176 | def renderAdjacentLink(next: Boolean, name: String, url: String) = { 177 | a(href := url)( 178 | if(next) frag(name, " ", i(cls:="fa fa-arrow-right" , aria.hidden:=true)) 179 | else frag(i(cls:="fa fa-arrow-left" , aria.hidden:=true), " ", name) 180 | ) 181 | } 182 | def postContent(homePage: Boolean, post: PostInfo, adjacentLinks: Frag, posts: Seq[String]) = pageChrome( 183 | Some(post.name), 184 | homePage, 185 | Seq[Frag]( 186 | div(adjacentLinks, marginBottom := 10), 187 | raw(post.rawHtmlContent), 188 | adjacentLinks 189 | ), 190 | post.headers, 191 | pageHeaders = posts 192 | ) -------------------------------------------------------------------------------- /docs/pages/2 - Main Customization.md: -------------------------------------------------------------------------------- 1 | Apart from the code used to configure and define your routes and endpoints, Cask 2 | also allows global configuration for things that apply to the entire web server. 3 | This can be done by overriding the following methods on `cask.Main` or 4 | `cask.MainRoutes`: 5 | 6 | ## def debugMode: Boolean = true 7 | 8 | Makes the Cask report verbose error messages and stack traces if an endpoint 9 | fails; useful for debugging, should be disabled for production. 10 | 11 | ## def main 12 | 13 | The cask program entrypoint. By default just spins up a webserver, but you can 14 | override it to do whatever you like before or after the webserver runs. 15 | 16 | ## def log 17 | 18 | A logger that gets passed around the application. Used for convenient debug 19 | logging, as well as logging exceptions either to the terminal or to a 20 | centralized exception handler. 21 | 22 | ## def defaultHandler 23 | 24 | Cask is built on top of the [Undertow](http://undertow.io/) web server. If you 25 | need some low-level functionality not exposed by the Cask API, you can override 26 | `defaultHandler` to make use of Undertow's own 27 | [handler API](http://undertow.io/undertow-docs/undertow-docs-2.0.0/index.html#built-in-handlers) 28 | for customizing your webserver. This allows for things that Cask itself doesn't 29 | internally support. 30 | 31 | ## def port: Int = 8080, def host: String = "localhost" 32 | 33 | The host & port to attach your webserver to. 34 | 35 | ## def handleNotFound 36 | 37 | The response to serve when the incoming request does not match any of the routes 38 | or endpoints; defaults to a typical 404 39 | 40 | ## def handleEndpointError 41 | 42 | The response to serve when the incoming request matches a route and endpoint, 43 | but then fails for other reasons. Defaults to 400 for mismatched or invalid 44 | endpoint arguments and 500 for exceptions in the endpoint body, and provides 45 | useful stack traces or metadata for debugging if `debugMode = true`. 46 | 47 | ## def mainDecorators 48 | 49 | Any `cask.Decorator`s that you want to apply to all routes and all endpoints in 50 | the entire web application. Useful for inserting application-wide 51 | instrumentation, logging, security-checks, and similar things. 52 | -------------------------------------------------------------------------------- /docs/pages/3 - About Cask.md: -------------------------------------------------------------------------------- 1 | 2 | ## Functions First 3 | 4 | Inspired by [Flask](http://flask.pocoo.org/), Cask allows you to define your web 5 | applications endpoints using simple function `def`s that you already know and 6 | love, annotated with the minimal additional metadata necessary to work as HTTP 7 | endpoints. 8 | 9 | It turns out that function `def`s already provide almost everything you need in 10 | a HTTP endpoint: 11 | 12 | - The parameters the endpoint takes 13 | - If any parameters are optional, and their default values 14 | - The ability to return a `Response` 15 | 16 | Cask extends these basics with annotations, providing: 17 | 18 | - What request path the endpoint is available at 19 | - Automated deserialization of endpoint parameters from the respective format 20 | (Form-encoded? Query-string? JSON?) 21 | - Wrapping the endpoint's function `def` with custom logic: logging, 22 | authentication, ... 23 | 24 | While these annotations add a bit of complexity, they allow Cask to avoid 25 | needing custom DSLs for defining your HTTP routes, custom action-types, and many 26 | other things which you may be used to working with HTTP in Scala. 27 | 28 | ## Extensible Annotations 29 | 30 | Unlike most other annotation-based frameworks in Scala or Java, Cask's 31 | annotations are not magic markers, but self-contained classes containing all the 32 | logic they need to function. This has several benefits: 33 | 34 | - You can jump to the definition of an annotation and see what it does 35 | 36 | - It trivial to implement your own annotations as 37 | [decorators](/cask#extending-endpoints-with-decorators) or 38 | [endpoints](/cask#custom-endpoints). 39 | 40 | - Stacking multiple annotations on a single function has a well-defined contract 41 | and semantics 42 | 43 | Overall, Cask annotations behave a lot more like Python decorators than 44 | "traditional" Java/Scala annotations: first-class, customizable, inspectable, 45 | and self-contained. This allows Cask to have the syntactic convenience of an 46 | annotation-based API, without the typical downsides of inflexibility and 47 | undiscoverability. 48 | 49 | ## Simple First 50 | 51 | Cask intentionally eskews many things that other, more enterprise-grade 52 | frameworks provide: 53 | 54 | - Async 55 | - Akka 56 | - Streaming Computations 57 | - Backpressure 58 | 59 | While these features all are valuable in specific cases, Cask aims for the 99% 60 | of code for which simple, boring code is perfectly fine. Cask's endpoints are 61 | synchronous by default, do not tie you to any underlying concurrency model, and 62 | should "just work" without any advanced knowledge apart from basic Scala and 63 | HTTP. Cask's [websockets](/cask#websockets) API is intentionally low-level, making it 64 | both simple to use and also simple to build on top of if you want to wrap it in 65 | your own concurrency-library-of-choice. 66 | 67 | ## Thin Wrapper 68 | 69 | Cask is implemented as a thin wrapper around the excellent Undertow HTTP server. 70 | If you need more advanced functionality, Cask lets you ask for the `exchange: 71 | HttpServerExchange` in your endpoint, override 72 | [defaultHandler](/cask#def-defaulthandler) and add your own Undertow handlers next to 73 | Cask's and avoid Cask's routing/endpoint system altogether, or override 74 | [main](/cask#def-main) if you want to change how the server is initialized. 75 | 76 | Rather than trying to provide APIs for all conceivable functionality, Cask 77 | simply provides what it does best - simple routing for simple endpoints - and 78 | leaves the door wide open in case you need to drop down to the lower level 79 | Undertow APIs. 80 | 81 | ## Community Libraries 82 | 83 | Cask aims to re-use much of the excellent code that is already written and being 84 | used out in the Scala community, rather than trying to re-invent the wheel. Cask 85 | uses the [Mill](https://github.com/lihaoyi/mill) build tool, comes bundled with 86 | the [uPickle](https://github.com/lihaoyi/upickle) JSON library, and makes it 87 | trivial to pull in libraries like 88 | [Scalatags](https://github.com/lihaoyi/scalatags) to render HTML or 89 | [ScalaSql](https://github.com/com-lihaoyi/scalasql/) for database access. 90 | 91 | Each of these are stable, well-known, well-documented libraries you may already 92 | be familiar with, and Cask simply provides the HTTP/routing layer with the hooks 93 | necessary to tie everything together (e.g. into a 94 | [TodoMVC](/cask#todomvc-full-stack-web) webapp) -------------------------------------------------------------------------------- /example/compress/app/src/Compress.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object Compress extends cask.MainRoutes{ 3 | 4 | @cask.decorators.compress 5 | @cask.get("/") 6 | def hello() = { 7 | "Hello World! Hello World! Hello World!" 8 | } 9 | 10 | initialize() 11 | } 12 | -------------------------------------------------------------------------------- /example/compress/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("Compress") - withServer(Compress){ host => 21 | val expected = "Hello World! Hello World! Hello World!" 22 | requests.get(s"$host").text() ==> expected 23 | assert( 24 | requests.get(s"$host", autoDecompress = false).text().length < expected.length 25 | ) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/compress/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.compress 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/compress2/app/src/Compress2.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | case class Compress2()(implicit cc: castor.Context, 4 | log: cask.Logger) extends cask.Routes{ 5 | override def decorators = Seq(new cask.decorators.compress()) 6 | 7 | @cask.get("/") 8 | def hello() = { 9 | "Hello World! Hello World! Hello World!" 10 | } 11 | 12 | initialize() 13 | } 14 | 15 | object Compress2Main extends cask.Main{ 16 | val allRoutes = Seq(Compress2()) 17 | } 18 | -------------------------------------------------------------------------------- /example/compress2/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("Compress2Main") - withServer(Compress2Main) { host => 21 | val expected = "Hello World! Hello World! Hello World!" 22 | requests.get(s"$host").text() ==> expected 23 | assert( 24 | requests.get(s"$host", autoDecompress = false).text().length < expected.length 25 | ) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/compress2/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.compress2 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/compress3/app/src/Compress3.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | case class Compress3()(implicit cc: castor.Context, 4 | log: cask.Logger) extends cask.Routes{ 5 | 6 | @cask.get("/") 7 | def hello() = { 8 | "Hello World! Hello World! Hello World!" 9 | } 10 | 11 | initialize() 12 | } 13 | 14 | object Compress3Main extends cask.Main{ 15 | override def mainDecorators = Seq(new cask.decorators.compress()) 16 | val allRoutes = Seq(Compress3()) 17 | } -------------------------------------------------------------------------------- /example/compress3/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("Compress3Main") - withServer(Compress3Main){ host => 21 | val expected = "Hello World! Hello World! Hello World!" 22 | requests.get(s"$host").text() ==> expected 23 | val compressed = requests.get(s"$host", autoDecompress = false).text() 24 | assert( 25 | compressed.length < expected.length 26 | ) 27 | 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/compress3/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.compress3 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/cookies/app/src/Cookies.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object Cookies extends cask.MainRoutes{ 3 | @cask.get("/read-cookie") 4 | def readCookies(username: cask.Cookie) = { 5 | username.value 6 | } 7 | 8 | @cask.get("/store-cookie") 9 | def storeCookies() = { 10 | cask.Response( 11 | "Cookies Set!", 12 | cookies = Seq(cask.Cookie("username", "the_username")) 13 | ) 14 | } 15 | 16 | @cask.get("/delete-cookie") 17 | def deleteCookie() = { 18 | cask.Response( 19 | "Cookies Deleted!", 20 | cookies = Seq(cask.Cookie("username", "", expires = java.time.Instant.EPOCH)) 21 | ) 22 | } 23 | 24 | initialize() 25 | } 26 | -------------------------------------------------------------------------------- /example/cookies/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("Cookies") - withServer(Cookies){ host => 21 | val sess = requests.Session() 22 | sess.get(s"$host/read-cookie", check = false).statusCode ==> 400 23 | sess.get(s"$host/store-cookie") 24 | sess.get(s"$host/read-cookie").text() ==> "the_username" 25 | sess.get(s"$host/read-cookie").statusCode ==> 200 26 | sess.get(s"$host/delete-cookie") 27 | sess.get(s"$host/read-cookie", check = false).statusCode ==> 400 28 | 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/cookies/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.cookies 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/decorated/app/src/Decorated.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object Decorated extends cask.MainRoutes { 3 | class User { 4 | override def toString = "[haoyi]" 5 | } 6 | class loggedIn extends cask.RawDecorator { 7 | def wrapFunction(ctx: cask.Request, delegate: Delegate) = { 8 | delegate(ctx, Map("user" -> new User())) 9 | } 10 | } 11 | class withExtra extends cask.RawDecorator { 12 | def wrapFunction(ctx: cask.Request, delegate: Delegate) = { 13 | delegate(ctx, Map("extra" -> 31337)) 14 | } 15 | } 16 | 17 | class withCustomHeader extends cask.RawDecorator { 18 | def wrapFunction(request: cask.Request, delegate: Delegate) = { 19 | request.headers.get("x-custom-header").map(_.head) match { 20 | case Some(header) => delegate(request, Map("customHeader" -> header)) 21 | case None => 22 | cask.router.Result.Success( 23 | cask.model.Response( 24 | s"Request is missing required header: 'X-CUSTOM-HEADER'", 25 | 400 26 | ) 27 | ) 28 | } 29 | } 30 | } 31 | 32 | @withExtra() 33 | @cask.get("/hello/:world") 34 | def hello(world: String)(extra: Int) = { 35 | world + extra 36 | } 37 | 38 | @loggedIn() 39 | @cask.get("/internal/:world") 40 | def internal(world: String)(user: User) = { 41 | world + user 42 | } 43 | 44 | @withCustomHeader() 45 | @cask.get("/echo") 46 | def echoHeader(request: cask.Request)(customHeader: String) = { 47 | customHeader 48 | } 49 | 50 | @withExtra() 51 | @loggedIn() 52 | @cask.get("/internal-extra/:world") 53 | def internalExtra(world: String)(user: User)(extra: Int) = { 54 | world + user + extra 55 | } 56 | 57 | @withExtra() 58 | @loggedIn() 59 | @cask.get("/ignore-extra/:world") 60 | def ignoreExtra(world: String)(user: User) = { 61 | world + user 62 | } 63 | 64 | @loggedIn() 65 | @cask.get("/hello-default") 66 | def defaults(world: String = "world")(user: User) = { 67 | world + user 68 | } 69 | initialize() 70 | } 71 | -------------------------------------------------------------------------------- /example/decorated/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite { 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("Decorated") - withServer(Decorated){ host => 21 | requests.get(s"$host/hello/woo").text() ==> "woo31337" 22 | requests.get(s"$host/internal/boo").text() ==> "boo[haoyi]" 23 | requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337" 24 | requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337" 25 | requests.get(s"$host/hello-default?world=worldz").text() ==> "worldz[haoyi]" 26 | requests.get(s"$host/hello-default").text() ==> "world[haoyi]" 27 | requests.get(s"$host/echo", headers = Map("X-CUSTOM-HEADER" -> "header")).text() ==> "header" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/decorated/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.decorated 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/decorated2/app/src/Decorated2.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object Decorated2 extends cask.MainRoutes{ 3 | class User{ 4 | override def toString = "[haoyi]" 5 | } 6 | class loggedIn extends cask.RawDecorator { 7 | def wrapFunction(ctx: cask.Request, delegate: Delegate) = { 8 | delegate(ctx, Map("user" -> new User())) 9 | } 10 | } 11 | class withExtra extends cask.RawDecorator { 12 | def wrapFunction(ctx: cask.Request, delegate: Delegate) = { 13 | delegate(ctx, Map("extra" -> 31337)) 14 | } 15 | } 16 | 17 | override def decorators = Seq(new withExtra()) 18 | 19 | @cask.get("/hello/:world") 20 | def hello(world: String)(extra: Int) = { 21 | world + extra 22 | } 23 | 24 | @loggedIn() 25 | @cask.get("/internal-extra/:world") 26 | def internalExtra(world: String)(user: User)(extra: Int) = { 27 | world + user + extra 28 | } 29 | 30 | @loggedIn() 31 | @cask.get("/ignore-extra/:world") 32 | def ignoreExtra(world: String)(user: User)(extra: Int) = { 33 | world + user 34 | } 35 | 36 | initialize() 37 | } 38 | -------------------------------------------------------------------------------- /example/decorated2/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("Decorated2") - withServer(Decorated2){ host => 21 | requests.get(s"$host/hello/woo").text() ==> "woo31337" 22 | requests.get(s"$host/internal-extra/goo").text() ==> "goo[haoyi]31337" 23 | requests.get(s"$host/ignore-extra/boo").text() ==> "boo[haoyi]" 24 | 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/decorated2/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.decorated2 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/decoratedContext/app/src/DecoratedContext.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | case class Context( 4 | session: Session 5 | ) 6 | 7 | case class Session(data: collection.mutable.Map[String, String]) 8 | 9 | trait CustomParser[T] extends cask.router.ArgReader[Any, T, Context] 10 | object CustomParser: 11 | given CustomParser[Context] with 12 | def arity = 0 13 | def read(ctx: Context, label: String, input: Any): Context = ctx 14 | given CustomParser[Session] with 15 | def arity = 0 16 | def read(ctx: Context, label: String, input: Any): Session = ctx.session 17 | given literal[Literal]: CustomParser[Literal] with 18 | def arity = 1 19 | def read(ctx: Context, label: String, input: Any): Literal = input.asInstanceOf[Literal] 20 | 21 | object DecoratedContext extends cask.MainRoutes{ 22 | 23 | class custom extends cask.router.Decorator[cask.Response.Raw, cask.Response.Raw, Any, Context]{ 24 | 25 | override type InputParser[T] = CustomParser[T] 26 | 27 | def wrapFunction(req: cask.Request, delegate: Delegate) = { 28 | // Create a custom context out of the request. Custom contexts are useful 29 | // to group an expensive operation that may be used by multiple 30 | // parameter readers or that carry state. This example focuses on carrying 31 | // state. 32 | val ctx = Context(Session(collection.mutable.Map.empty)) // this would typically be populated from a signed cookie 33 | 34 | delegate(ctx, Map("user" -> 1337)).map{ response => 35 | val extraCookies = ctx.session.data.map( 36 | (k, v) => cask.Cookie(k, v) 37 | ) 38 | 39 | response.copy( 40 | cookies = response.cookies ++ extraCookies 41 | ) 42 | } 43 | 44 | } 45 | } 46 | 47 | @custom() 48 | @cask.get("/hello/:world") 49 | def hello(world: String, req: cask.Request)(session: Session, user: Int) = { 50 | session.data("hello") = "world" 51 | world + user 52 | } 53 | 54 | initialize() 55 | } 56 | -------------------------------------------------------------------------------- /example/decoratedContext/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("DecoratedContext") - withServer(DecoratedContext){ host => 21 | val response = requests.get(s"$host/hello/woo") 22 | response.text() ==> "woo1337" 23 | response.cookies("hello").getValue ==> "world" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/decoratedContext/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.decoratedContext 2 | import mill._, scalalib._ 3 | 4 | object app extends ScalaModule{ 5 | 6 | def scalaVersion = build.scala3 7 | 8 | def moduleDeps = Seq(build.cask(build.scala3)) 9 | 10 | def ivyDeps = Agg[Dep]( 11 | ) 12 | object test extends ScalaTests with TestModule.Utest{ 13 | 14 | def ivyDeps = Agg( 15 | ivy"com.lihaoyi::utest::0.8.4", 16 | ivy"com.lihaoyi::requests::0.9.0", 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/endpoints/app/src/Endpoints.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | class custom(val path: String, val methods: Seq[String]) 4 | extends cask.HttpEndpoint[Int, Seq[String]]{ 5 | def wrapFunction(ctx: cask.Request, delegate: Delegate) = { 6 | delegate(ctx, Map()).map{num => 7 | cask.Response("Echo " + num, statusCode = num) 8 | } 9 | } 10 | 11 | def wrapPathSegment(s: String) = Seq(s) 12 | 13 | type InputParser[T] = cask.endpoints.QueryParamReader[T] 14 | } 15 | 16 | object Endpoints extends cask.MainRoutes{ 17 | 18 | 19 | @custom("/echo/:status", methods = Seq("get")) 20 | def echoStatus(status: String) = { 21 | status.toInt 22 | } 23 | 24 | initialize() 25 | } 26 | -------------------------------------------------------------------------------- /example/endpoints/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("Endpoints") - withServer(Endpoints){ host => 21 | requests.get(s"$host/echo/200").text() ==> "Echo 200" 22 | requests.get(s"$host/echo/200").statusCode ==> 200 23 | requests.get(s"$host/echo/400", check = false).text() ==> "Echo 400" 24 | requests.get(s"$host/echo/400", check = false).statusCode ==> 400 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/endpoints/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.endpoints 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/formJsonPost/app/src/FormJsonPost.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object FormJsonPost extends cask.MainRoutes{ 3 | @cask.postJson("/json") 4 | def jsonEndpoint(value1: ujson.Value, value2: Seq[Int]) = { 5 | "OK " + value1 + " " + value2 6 | } 7 | 8 | @cask.postJson("/json-obj") 9 | def jsonEndpointObj(value1: ujson.Value, value2: Seq[Int]) = { 10 | ujson.Obj( 11 | "value1" -> value1, 12 | "value2" -> value2 13 | ) 14 | } 15 | 16 | @cask.postForm("/form") 17 | def formEndpoint(value1: cask.FormValue, value2: Seq[Int]) = { 18 | "OK " + value1 + " " + value2 19 | } 20 | 21 | @cask.postForm("/form-obj") 22 | def formEndpointObj(value1: cask.FormValue, value2: Seq[Int]) = { 23 | ujson.Obj( 24 | "value1" -> value1.value, 25 | "value2" -> value2 26 | ) 27 | } 28 | 29 | @cask.postForm("/upload") 30 | def uploadFile(image: cask.FormFile) = { 31 | image.fileName 32 | } 33 | 34 | 35 | @cask.postJson("/json-extra") 36 | def jsonEndpointExtra(value1: ujson.Value, 37 | value2: Seq[Int], 38 | params: cask.QueryParams, 39 | segments: cask.RemainingPathSegments) = { 40 | "OK " + value1 + " " + value2 + " " + params.value + " " + segments.value 41 | } 42 | 43 | @cask.postJsonCached("/json-obj-cached") 44 | def jsonEndpointObjCached(value1: ujson.Value, value2: Seq[Int], request: cask.Request) = { 45 | ujson.Obj( 46 | "value1" -> value1, 47 | "value2" -> value2, 48 | // `postJsonCached` buffers up the body of the request in memory before parsing, 49 | // giving you access to the request body data if you want to use it yourself 50 | "body" -> request.text() 51 | ) 52 | } 53 | 54 | @cask.postForm("/form-extra") 55 | def formEndpointExtra(value1: cask.FormValue, 56 | value2: Seq[Int], 57 | params: cask.QueryParams, 58 | segments: cask.RemainingPathSegments) = { 59 | "OK " + value1 + " " + value2 + " " + params.value + " " + segments.value 60 | } 61 | 62 | initialize() 63 | } 64 | -------------------------------------------------------------------------------- /example/formJsonPost/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("FormJsonPost") - withServer(FormJsonPost){ host => 21 | val response1 = requests.post(s"$host/json", data = """{"value1": true, "value2": [3]}""") 22 | assert( 23 | ujson.read(response1.text()) == ujson.Str("OK true List(3)") || 24 | ujson.read(response1.text()) == ujson.Str("OK true Vector(3)") 25 | ) 26 | 27 | val response2 = requests.post( 28 | s"$host/json-obj", 29 | data = """{"value1": true, "value2": [3]}""" 30 | ) 31 | ujson.read(response2.text()) ==> ujson.Obj("value1" -> true, "value2" -> ujson.Arr(3)) 32 | 33 | 34 | val response2Cached = requests.post( 35 | s"$host/json-obj-cached", 36 | data = """{"value1": true, "value2": [3]}""" 37 | ) 38 | ujson.read(response2Cached.text()) ==> 39 | ujson.Obj( 40 | "value1" -> true, 41 | "value2" -> ujson.Arr(3), 42 | "body" -> """{"value1": true, "value2": [3]}""" 43 | ) 44 | 45 | 46 | val response3 = requests.post( 47 | s"$host/form", 48 | data = Seq("value1" -> "hello", "value2" -> "1", "value2" -> "2") 49 | ) 50 | assert( 51 | response3.text() == "OK FormValue(hello,null) List(1, 2)" || 52 | response3.text() == "OK FormValue(hello,null) Vector(1, 2)" 53 | ) 54 | 55 | val response4 = requests.post( 56 | s"$host/form-obj", 57 | data = Seq("value1" -> "hello", "value2" -> "1", "value2" -> "2") 58 | ) 59 | ujson.read(response4.text()) ==> ujson.Obj("value1" -> "hello", "value2" -> ujson.Arr(1, 2)) 60 | 61 | val response5 = requests.post( 62 | s"$host/upload", 63 | data = requests.MultiPart( 64 | requests.MultiItem("image", "...", "my-best-image.txt") 65 | ) 66 | ) 67 | response5.text() ==> "my-best-image.txt" 68 | 69 | 70 | val response6 = requests.post( 71 | s"$host/json-extra/omg/wtf/bbq?iam=cow&hearme=moo", 72 | data = """{"value1": true, "value2": [3]}""" 73 | ) 74 | 75 | val text6 = response6.text() 76 | assert( 77 | text6 == "\"OK true List(3) Map(hearme -> ArraySeq(moo), iam -> ArraySeq(cow)) List(omg, wtf, bbq)\"" || 78 | text6 == "\"OK true Vector(3) Map(hearme -> WrappedArray(moo), iam -> WrappedArray(cow)) List(omg, wtf, bbq)\"" 79 | ) 80 | 81 | val response7 = requests.post( 82 | s"$host/form-extra/omg/wtf/bbq?iam=cow&hearme=moo", 83 | data = Seq("value1" -> "hello", "value2" -> "1", "value2" -> "2") 84 | ) 85 | 86 | val text7 = response7.text() 87 | assert( 88 | text7 == "OK FormValue(hello,null) List(1, 2) Map(hearme -> ArraySeq(moo), iam -> ArraySeq(cow)) List(omg, wtf, bbq)" || 89 | text7 == "OK FormValue(hello,null) List(1, 2) Map(hearme -> WrappedArray(moo), iam -> WrappedArray(cow)) List(omg, wtf, bbq)" 90 | ) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /example/formJsonPost/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.formJsonPost 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0" 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/httpMethods/app/src/HttpMethods.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object HttpMethods extends cask.MainRoutes{ 3 | @cask.route("/login", methods = Seq("get", "post")) 4 | def login(request: cask.Request) = { 5 | if (request.exchange.getRequestMethod.equalToString("post")) "do_the_login" 6 | else "show_the_login_form" 7 | } 8 | 9 | @cask.route("/session", methods = Seq("delete")) 10 | def session(request: cask.Request) = { 11 | "delete_the_session" 12 | } 13 | 14 | @cask.route("/session", methods = Seq("secretmethod")) 15 | def admin(request: cask.Request) = { 16 | "security_by_obscurity" 17 | } 18 | 19 | @cask.route("/api", methods = Seq("options")) 20 | def cors(request: cask.Request) = { 21 | "allow_cors" 22 | } 23 | 24 | 25 | initialize() 26 | } 27 | -------------------------------------------------------------------------------- /example/httpMethods/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("HttpMethods") - withServer(HttpMethods){ host => 21 | requests.post(s"$host/login").text() ==> "do_the_login" 22 | requests.get(s"$host/login").text() ==> "show_the_login_form" 23 | requests.put(s"$host/login", check = false).statusCode ==> 405 24 | requests.delete(s"$host/session").text() ==> "delete_the_session" 25 | requests.get.copy(verb="secretmethod")(s"$host/session").text() ==> "security_by_obscurity" 26 | requests.options(s"$host/api").text() ==> "allow_cors" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/httpMethods/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.httpMethods 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | def forkArgs = Seq("--add-opens=java.base/java.net=ALL-UNNAMED") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/minimalApplication/app/src/MinimalApplication.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object MinimalApplication extends cask.MainRoutes{ 3 | @cask.get("/") 4 | def hello() = { 5 | "Hello World!" 6 | } 7 | 8 | @cask.post("/do-thing") 9 | def doThing(request: cask.Request) = { 10 | request.text().reverse 11 | } 12 | 13 | initialize() 14 | } 15 | -------------------------------------------------------------------------------- /example/minimalApplication/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests { 20 | test("MinimalApplication") - withServer(MinimalApplication) { host => 21 | val success = requests.get(host) 22 | 23 | success.text() ==> "Hello World!" 24 | success.statusCode ==> 200 25 | 26 | requests.get(s"$host/doesnt-exist", check = false).statusCode ==> 404 27 | 28 | requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh" 29 | 30 | requests.delete(s"$host/do-thing", check = false).statusCode ==> 405 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/minimalApplication/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.minimalApplication 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/minimalApplication2/app/src/MinimalApplication2.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | case class MinimalRoutes()(implicit cc: castor.Context, 4 | log: cask.Logger) extends cask.Routes{ 5 | @cask.get("/") 6 | def hello() = { 7 | "Hello World!" 8 | } 9 | 10 | @cask.post("/do-thing") 11 | def doThing(request: cask.Request) = { 12 | request.text().reverse 13 | } 14 | 15 | initialize() 16 | } 17 | object MinimalRoutesMain extends cask.Main{ 18 | val allRoutes = Seq(MinimalRoutes()) 19 | } -------------------------------------------------------------------------------- /example/minimalApplication2/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("MinimalApplication2") - withServer(MinimalRoutesMain){ host => 21 | val success = requests.get(host) 22 | 23 | success.text() ==> "Hello World!" 24 | success.statusCode ==> 200 25 | 26 | requests.get(s"$host/doesnt-exist", check = false).statusCode ==> 404 27 | 28 | requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh" 29 | 30 | requests.delete(s"$host/do-thing", check = false).statusCode ==> 405 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/minimalApplication2/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.minimalApplication2 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/minimalApplicationWithLoom/app/src/MinimalApplicationWithLoom.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import cask.main.Main 4 | 5 | import java.lang.management.{ManagementFactory, RuntimeMXBean} 6 | import java.util.concurrent.{ExecutorService, Executors} 7 | 8 | // run benchmark with : ./mill benchmark.runBenchmark 9 | object MinimalApplicationWithLoom extends cask.MainRoutes { 10 | // Print Java version 11 | private val javaVersion: String = System.getProperty("java.version") 12 | println("Java Version: " + javaVersion) 13 | 14 | // Print JVM arguments// Print JVM arguments 15 | private val runtimeMxBean: RuntimeMXBean = ManagementFactory.getRuntimeMXBean 16 | private val jvmArguments = runtimeMxBean.getInputArguments 17 | println("JVM Arguments:") 18 | 19 | jvmArguments.forEach((arg: String) => println(arg)) 20 | 21 | println(Main.VIRTUAL_THREAD_ENABLED + " :" + System.getProperty(Main.VIRTUAL_THREAD_ENABLED)) 22 | 23 | //Use the same underlying executor for both virtual and non-virtual threads 24 | private val executor = Executors.newFixedThreadPool(4) 25 | 26 | //TO USE LOOM: 27 | //1. JDK 21 or later is needed. 28 | //2. add VM option: --add-opens java.base/java.lang=ALL-UNNAMED 29 | //3. set system property: cask.virtual-threads.enabled=true 30 | //4. NOTE: `java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor` is using the shared 31 | // ForkJoinPool in VirtualThread. If you want to use a separate ForkJoinPool, you can create 32 | // a new ForkJoinPool instance and pass it to `createVirtualThreadExecutor` method. 33 | 34 | override protected def handlerExecutor(): Option[ExecutorService] = { 35 | super.handlerExecutor().orElse(Some(executor)) 36 | } 37 | 38 | /** 39 | * With curl: curl -X GET http://localhost:8080/ 40 | * you wil see something like: 41 | * Hello World! from thread:VirtualThread[#63,cask-handler-executor-virtual-thread-10]/runnable@ForkJoinPool-1-worker-1% 42 | * */ 43 | @cask.get("/") 44 | def hello() = { 45 | Thread.sleep(100) // simulate some blocking work 46 | "Hello World!" 47 | } 48 | 49 | @cask.post("/do-thing") 50 | def doThing(request: cask.Request) = { 51 | request.text().reverse 52 | } 53 | 54 | initialize() 55 | } 56 | -------------------------------------------------------------------------------- /example/minimalApplicationWithLoom/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | import org.xnio.Options 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setSocketOption(Options.REUSE_ADDRESSES, java.lang.Boolean.TRUE) 11 | .setHandler(example.defaultHandler) 12 | .build 13 | server.start() 14 | val res = 15 | try f("http://localhost:8081") 16 | finally server.stop() 17 | res 18 | } 19 | 20 | val tests = Tests { 21 | test("MinimalApplicationWithLoom") - withServer(MinimalApplicationWithLoom) { host => 22 | val success = requests.get(host) 23 | 24 | success.text() ==> "Hello World!" 25 | success.statusCode ==> 200 26 | 27 | requests.get(s"$host/doesnt-exist", check = false).statusCode ==> 404 28 | 29 | requests.post(s"$host/do-thing", data = "hello").text() ==> "olleh" 30 | 31 | requests.delete(s"$host/do-thing", check = false).statusCode ==> 405 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/minimalApplicationWithLoom/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.minimalApplicationWithLoom 2 | 3 | import mill._, scalalib._ 4 | import mill.define.ModuleRef 5 | 6 | object app extends Cross[AppModule](build.scalaVersions) 7 | trait AppModule extends CrossScalaModule{ 8 | 9 | private def parseJvmArgs(argsStr: String) = { 10 | argsStr.split(" ").filter(_.nonEmpty).toSeq 11 | } 12 | 13 | def forkArgs = Task.Input { 14 | //TODO not sure why the env passing is not working 15 | val envVirtualThread: String = T.env.getOrElse("CASK_VIRTUAL_THREAD", "false") 16 | println("envVirtualThread: " + envVirtualThread) 17 | 18 | val systemProps = Seq(s"-Dcask.virtual-threads.enabled=$envVirtualThread") 19 | 20 | val baseArgs = Seq( 21 | "--add-opens", "java.base/java.lang=ALL-UNNAMED" 22 | ) 23 | 24 | val seq = baseArgs ++ systemProps 25 | println("final forkArgs: " + seq) 26 | seq 27 | } 28 | 29 | def zincWorker = ModuleRef(ZincWorkerJava11Latest) 30 | 31 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 32 | 33 | def ivyDeps = Agg[Dep]( 34 | ) 35 | 36 | object test extends ScalaTests with TestModule.Utest { 37 | def ivyDeps = Agg( 38 | ivy"com.lihaoyi::utest::0.8.4", 39 | ivy"com.lihaoyi::requests::0.9.0", 40 | ) 41 | } 42 | } 43 | 44 | object ZincWorkerJava11Latest extends ZincWorkerModule with CoursierModule { 45 | def jvmId = "temurin:23.0.1" 46 | def jvmIndexVersion = "latest.release" 47 | } 48 | -------------------------------------------------------------------------------- /example/multipartFormSubmission/app/resources/example.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/com-lihaoyi/cask/0fc00e217bcfae22d4ffdae8644cad666fb3f826/example/multipartFormSubmission/app/resources/example.txt -------------------------------------------------------------------------------- /example/multipartFormSubmission/app/src/MultipartFormSubmission.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | object MultipartFormSubmission extends cask.MainRoutes { 4 | 5 | @cask.get("/") 6 | def index() = 7 | cask.model.Response( 8 | """ 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 | 19 | """, 200, Seq(("Content-Type", "text/html"))) 20 | 21 | @cask.postForm("/post") 22 | def post(somefile: cask.FormFile) = 23 | s"filename: ${somefile.fileName}" 24 | 25 | initialize() 26 | } 27 | -------------------------------------------------------------------------------- /example/multipartFormSubmission/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests { 20 | test("MultipartFormSubmission") - withServer(MultipartFormSubmission) { host => 21 | val withFile = requests.post(s"$host/post", data = requests.MultiPart( 22 | requests.MultiItem("somefile", Array[Byte](1,2,3,4,5) , "example.txt"), 23 | )) 24 | withFile.text() ==> s"filename: example.txt" 25 | withFile.statusCode ==> 200 26 | 27 | val withoutFile = requests.post(s"$host/post", data = requests.MultiPart( 28 | requests.MultiItem("somefile", Array[Byte]()), 29 | )) 30 | withoutFile.text() ==> s"filename: null" 31 | withoutFile.statusCode ==> 200 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/multipartFormSubmission/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.multipartFormSubmission 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/package.mill: -------------------------------------------------------------------------------- 1 | package build.example -------------------------------------------------------------------------------- /example/queryParams/app/src/QueryParams.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object QueryParams extends cask.MainRoutes{ 3 | 4 | @cask.get("/article/:articleId") // Mandatory query param, e.g. HOST/article/foo?param=bar 5 | def getArticle(articleId: Int, param: String) = { 6 | s"Article $articleId $param" 7 | } 8 | 9 | @cask.get("/article2/:articleId") // Optional query param 10 | def getArticleOptional(articleId: Int, param: Option[String] = None) = { 11 | s"Article $articleId $param" 12 | } 13 | 14 | @cask.get("/article3/:articleId") // Optional query param with default 15 | def getArticleDefault(articleId: Int, param: String = "DEFAULT VALUE") = { 16 | s"Article $articleId $param" 17 | } 18 | 19 | @cask.get("/article4/:articleId") // 1-or-more param, e.g. HOST/article/foo?param=bar¶m=qux 20 | def getArticleSeq(articleId: Int, param: Seq[String]) = { 21 | s"Article $articleId $param" 22 | } 23 | 24 | @cask.get("/article5/:articleId") // 0-or-more query param 25 | def getArticleOptionalSeq(articleId: Int, param: Seq[String] = Nil) = { 26 | s"Article $articleId $param" 27 | } 28 | 29 | @cask.get("/user2/:userName") // allow unknown params, e.g. HOST/article/foo?foo=bar&qux=baz 30 | def getUserProfileAllowUnknown(userName: String, params: cask.QueryParams) = { 31 | s"User $userName " + params.value 32 | } 33 | 34 | @cask.get("/statics/foo") 35 | def getStatic() = { 36 | "static route takes precedence" 37 | } 38 | 39 | @cask.get("/statics/:foo") 40 | def getDynamics(foo: String) = { 41 | s"dynamic route $foo" 42 | } 43 | 44 | @cask.get("/statics/bar") 45 | def getStatic2() = { 46 | "another static route" 47 | } 48 | 49 | initialize() 50 | } 51 | -------------------------------------------------------------------------------- /example/queryParams/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("QueryParams") - withServer(QueryParams){ host => 21 | val noIndexPage = requests.get(host, check = false) 22 | noIndexPage.statusCode ==> 404 23 | 24 | requests.get(s"$host/article/123?param=xyz").text() ==> "Article 123 xyz" 25 | 26 | requests.get(s"$host/article/123?param=1+%2B+1+%3D+2%25%3F%26%2F").text() ==> 27 | "Article 123 1 + 1 = 2%?&/" 28 | 29 | requests.get(s"$host/article/123", check = false).text() ==> 30 | """Missing argument: (param: String) 31 | | 32 | |Arguments provided did not match expected signature: 33 | | 34 | |getArticle 35 | | articleId Int 36 | | param String 37 | | 38 | |""".stripMargin 39 | 40 | assert( 41 | requests.get(s"$host/article2/123?param=xyz").text() == 42 | "Article 123 Some(xyz)" 43 | ) 44 | 45 | assert( 46 | requests.get(s"$host/article2/123").text() == 47 | "Article 123 None" 48 | ) 49 | 50 | assert( 51 | requests.get(s"$host/article3/123?param=xyz").text() == 52 | "Article 123 xyz" 53 | ) 54 | 55 | assert( 56 | requests.get(s"$host/article3/123").text() == 57 | "Article 123 DEFAULT VALUE" 58 | ) 59 | 60 | 61 | val res1 = requests.get(s"$host/article4/123?param=xyz¶m=abc").text() 62 | assert( 63 | res1 == "Article 123 ArraySeq(xyz, abc)" || 64 | res1 == "Article 123 ArrayBuffer(xyz, abc)" 65 | ) 66 | 67 | requests.get(s"$host/article4/123", check = false).text() ==> 68 | """Missing argument: (param: Seq[String]) 69 | | 70 | |Arguments provided did not match expected signature: 71 | | 72 | |getArticleSeq 73 | | articleId Int 74 | | param Seq[String] 75 | | 76 | |""".stripMargin 77 | 78 | val res2 = requests.get(s"$host/article5/123?param=xyz¶m=abc").text() 79 | assert( 80 | res2 == "Article 123 ArraySeq(xyz, abc)" || 81 | res2 == "Article 123 ArrayBuffer(xyz, abc)" 82 | ) 83 | assert( 84 | requests.get(s"$host/article5/123").text() == "Article 123 List()" 85 | ) 86 | 87 | val res3 = requests.get(s"$host/user2/lihaoyi?unknown1=123&unknown2=abc", check = false).text() 88 | assert( 89 | res3 == "User lihaoyi Map(unknown1 -> ArrayBuffer(123), unknown2 -> ArrayBuffer(abc))" || 90 | res3 == "User lihaoyi Map(unknown1 -> WrappedArray(123), unknown2 -> WrappedArray(abc))" || 91 | res3 == "User lihaoyi Map(unknown1 -> ArraySeq(123), unknown2 -> ArraySeq(abc))" 92 | ) 93 | 94 | assert( 95 | requests.get(s"$host/statics/foo").text() == "static route takes precedence" 96 | ) 97 | assert( 98 | requests.get(s"$host/statics/hello").text() == "dynamic route hello" 99 | ) 100 | assert( 101 | requests.get(s"$host/statics/bar").text() == "another static route" 102 | ) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /example/queryParams/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.queryParams 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/redirectAbort/app/src/RedirectAbort.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object RedirectAbort extends cask.MainRoutes{ 3 | @cask.get("/") 4 | def index() = { 5 | cask.Redirect("/login") 6 | } 7 | 8 | @cask.get("/login") 9 | def login() = { 10 | cask.Abort(401) 11 | } 12 | 13 | initialize() 14 | } 15 | -------------------------------------------------------------------------------- /example/redirectAbort/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | 21 | test("RedirectAbort") - withServer(RedirectAbort){ host => 22 | val resp = requests.get(s"$host/", check = false) 23 | resp.statusCode ==> 401 24 | resp.history.get.statusCode ==> 301 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /example/redirectAbort/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.redirectAbort 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/scalatags/app/src/Scalatags.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import scalatags.Text.all._ 3 | object Scalatags extends cask.MainRoutes{ 4 | @cask.get("/") 5 | def hello() = { 6 | doctype("html")( 7 | html( 8 | body( 9 | h1("Hello World"), 10 | p("I am cow") 11 | ) 12 | ) 13 | ) 14 | } 15 | 16 | initialize() 17 | } 18 | -------------------------------------------------------------------------------- /example/scalatags/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests { 20 | test("Scalatags") - withServer(Scalatags) { host => 21 | val body = requests.get(host).text() 22 | 23 | assert( 24 | body.contains("

Hello World

"), 25 | body.contains("

I am cow

"), 26 | ) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/scalatags/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.scalatags 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ivy"com.lihaoyi::scalatags:0.12.0" 11 | ) 12 | 13 | object test extends ScalaTests with TestModule.Utest{ 14 | 15 | def ivyDeps = Agg( 16 | ivy"com.lihaoyi::utest::0.8.4", 17 | ivy"com.lihaoyi::requests::0.9.0", 18 | ) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/staticFiles/app/resources/cask/example.txt: -------------------------------------------------------------------------------- 1 | the quick brown fox jumps over the lazy dog -------------------------------------------------------------------------------- /example/staticFiles/app/src/StaticFiles.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object StaticFiles extends cask.MainRoutes{ 3 | @cask.get("/") 4 | def index() = { 5 | "Hello!" 6 | } 7 | 8 | @cask.staticFiles("/static/file") 9 | def staticFileRoutes() = "resources/cask" 10 | 11 | @cask.staticResources("/static/resource") 12 | def staticResourceRoutes() = "cask" 13 | 14 | @cask.staticResources("/static/resource2") 15 | def staticResourceRoutes2() = "." 16 | 17 | initialize() 18 | } 19 | -------------------------------------------------------------------------------- /example/staticFiles/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | 21 | test("StaticFiles") - withServer(StaticFiles){ host => 22 | requests.get(s"$host/static/file/example.txt").text() ==> 23 | "the quick brown fox jumps over the lazy dog" 24 | 25 | requests.get(s"$host/static/resource/example.txt").text() ==> 26 | "the quick brown fox jumps over the lazy dog" 27 | 28 | requests.get(s"$host/static/resource2/cask/example.txt").text() ==> 29 | "the quick brown fox jumps over the lazy dog" 30 | 31 | requests.get(s"$host/static/file/../../../build.sc", check = false).statusCode ==> 404 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/staticFiles/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.staticFiles 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ app => 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def forkWorkingDir = app.millSourcePath 10 | def ivyDeps = Agg[Dep]( 11 | ) 12 | object test extends ScalaTests with TestModule.Utest{ 13 | 14 | def ivyDeps = Agg( 15 | ivy"com.lihaoyi::utest::0.8.4", 16 | ivy"com.lihaoyi::requests::0.9.0", 17 | ) 18 | 19 | def forkWorkingDir = app.millSourcePath 20 | 21 | def testSandboxWorkingDir = false 22 | 23 | // redirect this to the forked `test` to make sure static file serving works 24 | def testLocal(args: String*) = T.command{ 25 | this.test(args:_*) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/staticFiles2/app/resources/cask/example.txt: -------------------------------------------------------------------------------- 1 | the quick brown fox jumps over the lazy dog -------------------------------------------------------------------------------- /example/staticFiles2/app/src/StaticFiles2.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object StaticFiles2 extends cask.MainRoutes{ 3 | @cask.get("/") 4 | def index() = { 5 | "Hello!" 6 | } 7 | 8 | @cask.staticFiles("/static/file", headers = Seq("Cache-Control" -> "max-age=31536000")) 9 | def staticFileRoutes() = "resources/cask" 10 | 11 | @cask.decorators.compress 12 | @cask.staticResources("/static/resource") 13 | def staticResourceRoutes() = "cask" 14 | 15 | @cask.staticResources("/static/resource2") 16 | def staticResourceRoutes2() = "." 17 | 18 | initialize() 19 | } 20 | -------------------------------------------------------------------------------- /example/staticFiles2/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | 21 | test("StaticFiles") - withServer(StaticFiles2){ host => 22 | requests.get(s"$host/static/file/example.txt").text() ==> 23 | "the quick brown fox jumps over the lazy dog" 24 | 25 | requests.get(s"$host/static/resource/example.txt").text() ==> 26 | "the quick brown fox jumps over the lazy dog" 27 | 28 | requests.get(s"$host/static/resource2/cask/example.txt").text() ==> 29 | "the quick brown fox jumps over the lazy dog" 30 | 31 | requests.get(s"$host/static/file/../../../build.sc", check = false).statusCode ==> 404 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/staticFiles2/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.staticFiles2 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ app => 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def forkWorkingDir = app.millSourcePath 10 | def ivyDeps = Agg[Dep]( 11 | ) 12 | object test extends ScalaTests with TestModule.Utest{ 13 | 14 | def ivyDeps = Agg( 15 | ivy"com.lihaoyi::utest::0.8.4", 16 | ivy"com.lihaoyi::requests::0.9.0", 17 | ) 18 | 19 | def forkWorkingDir = app.millSourcePath 20 | 21 | def testSandboxWorkingDir = false 22 | 23 | // redirect this to the forked `test` to make sure static file serving works 24 | def testLocal(args: String*) = T.command{ 25 | this.test(args:_*) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /example/staticFilesWithLoom/app/resources/cask/example.txt: -------------------------------------------------------------------------------- 1 | the quick brown fox jumps over the lazy dog -------------------------------------------------------------------------------- /example/staticFilesWithLoom/app/src/StaticFilesWithLoom.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import cask.internal.Util 4 | 5 | import java.util.concurrent.{ExecutorService, Executors} 6 | 7 | object StaticFilesWithLoom extends cask.MainRoutes{ 8 | private val executor = Executors.newFixedThreadPool(4) 9 | 10 | override protected def handlerExecutor(): Option[ExecutorService] = { 11 | super.handlerExecutor().orElse(Some(executor)) 12 | } 13 | 14 | @cask.get("/") 15 | def index() = { 16 | "Hello!" 17 | } 18 | 19 | @cask.staticFiles("/static/file") 20 | def staticFileRoutes() = "resources/cask" 21 | 22 | @cask.staticResources("/static/resource") 23 | def staticResourceRoutes() = "cask" 24 | 25 | @cask.staticResources("/static/resource2") 26 | def staticResourceRoutes2() = "." 27 | 28 | initialize() 29 | } 30 | -------------------------------------------------------------------------------- /example/staticFilesWithLoom/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | 21 | test("StaticFiles") - withServer(StaticFilesWithLoom){ host => 22 | requests.get(s"$host/static/file/example.txt").text() ==> 23 | "the quick brown fox jumps over the lazy dog" 24 | 25 | requests.get(s"$host/static/resource/example.txt").text() ==> 26 | "the quick brown fox jumps over the lazy dog" 27 | 28 | requests.get(s"$host/static/resource2/cask/example.txt").text() ==> 29 | "the quick brown fox jumps over the lazy dog" 30 | 31 | requests.get(s"$host/static/file/../../../build.sc", check = false).statusCode ==> 404 32 | } 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/staticFilesWithLoom/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.staticFilesWithLoom 2 | import mill._, scalalib._ 3 | import mill.define.ModuleRef 4 | 5 | object app extends Cross[AppModule](build.scalaVersions) 6 | trait AppModule extends CrossScalaModule{ app => 7 | 8 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 9 | 10 | def forkWorkingDir = app.millSourcePath 11 | def ivyDeps = Agg[Dep]( 12 | ) 13 | 14 | private def parseJvmArgs(argsStr: String) = { 15 | argsStr.split(" ").filter(_.nonEmpty).toSeq 16 | } 17 | 18 | def forkArgs = Task.Input { 19 | //TODO not sure why the env passing is not working 20 | val envVirtualThread: String = T.env.getOrElse("CASK_VIRTUAL_THREAD", "false") 21 | println("envVirtualThread: " + envVirtualThread) 22 | 23 | val systemProps = Seq(s"-Dcask.virtual-threads.enabled=$envVirtualThread") 24 | 25 | val baseArgs = Seq( 26 | "--add-opens", "java.base/java.lang=ALL-UNNAMED" 27 | ) 28 | 29 | val seq = baseArgs ++ systemProps 30 | println("final forkArgs: " + seq) 31 | seq 32 | } 33 | 34 | def zincWorker = ModuleRef(ZincWorkerJava11Latest) 35 | 36 | object test extends ScalaTests with TestModule.Utest{ 37 | 38 | def ivyDeps = Agg( 39 | ivy"com.lihaoyi::utest::0.8.4", 40 | ivy"com.lihaoyi::requests::0.9.0", 41 | ) 42 | 43 | def forkWorkingDir = app.millSourcePath 44 | 45 | def testSandboxWorkingDir = false 46 | 47 | // redirect this to the forked `test` to make sure static file serving works 48 | def testLocal(args: String*) = T.command{ 49 | this.test(args:_*) 50 | } 51 | } 52 | } 53 | 54 | 55 | object ZincWorkerJava11Latest extends ZincWorkerModule with CoursierModule { 56 | def jvmId = "temurin:23.0.1" 57 | def jvmIndexVersion = "latest.release" 58 | } -------------------------------------------------------------------------------- /example/todo/app/resources/todo/app.js: -------------------------------------------------------------------------------- 1 | var state = "all"; 2 | 3 | var todoApp = document.getElementsByClassName("todoapp")[0]; 4 | function postFetchUpdate(url){ 5 | fetch(url, { 6 | method: "POST", 7 | }) 8 | .then(function(response){ return response.text()}) 9 | .then(function (text) { 10 | todoApp.innerHTML = text; 11 | initListeners() 12 | }) 13 | } 14 | 15 | function bindEvent(cls, url, endState){ 16 | 17 | document.getElementsByClassName(cls)[0].addEventListener( 18 | "mousedown", 19 | function(evt){ 20 | postFetchUpdate(url) 21 | if (endState) state = endState 22 | } 23 | ); 24 | } 25 | 26 | function bindIndexedEvent(cls, func){ 27 | Array.from(document.getElementsByClassName(cls)).forEach( function(elem) { 28 | elem.addEventListener( 29 | "mousedown", 30 | function(evt){ 31 | postFetchUpdate(func(elem.getAttribute("data-todo-index"))) 32 | } 33 | ) 34 | }); 35 | } 36 | 37 | function initListeners(){ 38 | bindIndexedEvent( 39 | "destroy", 40 | function(index){return "/delete/" + state + "/" + index} 41 | ); 42 | bindIndexedEvent( 43 | "toggle", 44 | function(index){return "/toggle/" + state + "/" + index} 45 | ); 46 | bindEvent("toggle-all", "/toggle-all/" + state); 47 | bindEvent("todo-all", "/list/all", "all"); 48 | bindEvent("todo-active", "/list/active", "active"); 49 | bindEvent("todo-completed", "/list/completed", "completed"); 50 | bindEvent("clear-completed", "/clear-completed/" + state); 51 | var newTodoInput = document.getElementsByClassName("new-todo")[0]; 52 | newTodoInput.addEventListener( 53 | "keydown", 54 | function(evt){ 55 | if (evt.keyCode === 13) { 56 | fetch("/add/" + state, { 57 | method: "POST", 58 | body: newTodoInput.value 59 | }) 60 | .then(function(response){ return response.text()}) 61 | .then(function (text) { 62 | newTodoInput.value = ""; 63 | todoApp.innerHTML = text; 64 | initListeners() 65 | }) 66 | } 67 | } 68 | ); 69 | } 70 | initListeners() -------------------------------------------------------------------------------- /example/todo/app/src/TodoServer.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import scalasql.DbApi.Txn 3 | import scalasql.Sc 4 | import scalasql.SqliteDialect._ 5 | import scalatags.Text.all._ 6 | import scalatags.Text.tags2 7 | 8 | object TodoServer extends cask.MainRoutes{ 9 | val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite") 10 | 11 | val sqliteDataSource = new org.sqlite.SQLiteDataSource() 12 | sqliteDataSource.setUrl(s"jdbc:sqlite:$tmpDb/file.db") 13 | lazy val sqliteClient = new scalasql.DbClient.DataSource( 14 | sqliteDataSource, 15 | config = new scalasql.Config {} 16 | ) 17 | 18 | class transactional extends cask.RawDecorator{ 19 | def wrapFunction(pctx: cask.Request, delegate: Delegate) = { 20 | sqliteClient.transaction { txn => 21 | val res = delegate(pctx, Map("txn" -> txn)) 22 | if (res.isInstanceOf[cask.router.Result.Error]) txn.rollback() 23 | res 24 | } 25 | } 26 | } 27 | 28 | case class Todo[T[_]](id: T[Int], checked: T[Boolean], text: T[String]) 29 | object Todo extends scalasql.Table[Todo] 30 | 31 | sqliteClient.getAutoCommitClientConnection.updateRaw( 32 | """CREATE TABLE todo ( 33 | | id INTEGER PRIMARY KEY AUTOINCREMENT, 34 | | checked BOOLEAN, 35 | | text TEXT 36 | |); 37 | | 38 | |INSERT INTO todo (checked, text) VALUES 39 | |(1, 'Get started with Cask'), 40 | |(0, 'Profit!'); 41 | |""".stripMargin 42 | ) 43 | 44 | @transactional 45 | @cask.post("/list/:state") 46 | def list(state: String)(txn: Txn) = renderBody(state)(txn).render 47 | 48 | @transactional 49 | @cask.post("/add/:state") 50 | def add(state: String, request: cask.Request)(implicit txn: Txn) = { 51 | val body = request.text() 52 | txn.run(Todo.insert.columns(_.checked := false, _.text := body)) 53 | renderBody(state).render 54 | } 55 | 56 | @transactional 57 | @cask.post("/delete/:state/:index") 58 | def delete(state: String, index: Int)(implicit txn: Txn) = { 59 | txn.run(Todo.delete(_.id === index)) 60 | renderBody(state).render 61 | } 62 | 63 | @transactional 64 | @cask.post("/toggle/:state/:index") 65 | def toggle(state: String, index: Int)(implicit txn: Txn) = { 66 | txn.run(Todo.update(_.id === index).set(p => p.checked := !p.checked)) 67 | renderBody(state).render 68 | } 69 | 70 | @transactional 71 | @cask.post("/clear-completed/:state") 72 | def clearCompleted(state: String)(implicit txn: Txn) = { 73 | txn.run(Todo.delete(_.checked)) 74 | renderBody(state).render 75 | } 76 | 77 | @transactional 78 | @cask.post("/toggle-all/:state") 79 | def toggleAll(state: String)(implicit txn: Txn) = { 80 | val next = txn.run(Todo.select.filter(_.checked).size) != 0 81 | txn.run(Todo.update(_ => true).set(_.checked := !next)) 82 | renderBody(state).render 83 | } 84 | 85 | def renderBody(state: String)(implicit txn: Txn) = { 86 | val filteredTodos = state match{ 87 | case "all" => txn.run(Todo.select).sortBy(-_.id) 88 | case "active" => txn.run(Todo.select.filter(!_.checked)).sortBy(-_.id) 89 | case "completed" => txn.run(Todo.select.filter(_.checked)).sortBy(-_.id) 90 | } 91 | frag( 92 | header(cls := "header", 93 | h1("todos"), 94 | input(cls := "new-todo", placeholder := "What needs to be done?", autofocus := "") 95 | ), 96 | tags2.section(cls := "main", 97 | input( 98 | id := "toggle-all", 99 | cls := "toggle-all", 100 | `type` := "checkbox", 101 | if (txn.run(Todo.select.filter(_.checked).size !== 0)) checked else () 102 | ), 103 | label(`for` := "toggle-all","Mark all as complete"), 104 | ul(cls := "todo-list", 105 | for(todo <- filteredTodos) yield li( 106 | if (todo.checked) cls := "completed" else (), 107 | div(cls := "view", 108 | input( 109 | cls := "toggle", 110 | `type` := "checkbox", 111 | if (todo.checked) checked else (), 112 | data("todo-index") := todo.id 113 | ), 114 | label(todo.text), 115 | button(cls := "destroy", data("todo-index") := todo.id) 116 | ), 117 | input(cls := "edit", value := todo.text) 118 | ) 119 | ) 120 | ), 121 | footer(cls := "footer", 122 | span(cls := "todo-count", 123 | strong(txn.run(Todo.select.filter(!_.checked).size).toInt), 124 | " items left" 125 | ), 126 | ul(cls := "filters", 127 | li(cls := "todo-all", 128 | a(if (state == "all") cls := "selected" else (), "All") 129 | ), 130 | li(cls := "todo-active", 131 | a(if (state == "active") cls := "selected" else (), "Active") 132 | ), 133 | li(cls := "todo-completed", 134 | a(if (state == "completed") cls := "selected" else (), "Completed") 135 | ) 136 | ), 137 | button(cls := "clear-completed","Clear completed") 138 | ) 139 | ) 140 | } 141 | 142 | @transactional 143 | @cask.get("/") 144 | def index()(implicit txn: Txn) = { 145 | doctype("html")( 146 | html(lang := "en", 147 | head( 148 | meta(charset := "utf-8"), 149 | meta(name := "viewport", content := "width=device-width, initial-scale=1"), 150 | tags2.title("Template • TodoMVC"), 151 | link(rel := "stylesheet", href := "/static/index.css") 152 | ), 153 | body( 154 | tags2.section(cls := "todoapp", renderBody("all")), 155 | footer(cls := "info", 156 | p("Double-click to edit a todo"), 157 | p("Created by ", 158 | a(href := "http://todomvc.com","Li Haoyi") 159 | ), 160 | p("Part of ", 161 | a(href := "http://todomvc.com","TodoMVC") 162 | ) 163 | ), 164 | script(src := "/static/app.js") 165 | ) 166 | ) 167 | ) 168 | } 169 | 170 | @cask.staticResources("/static") 171 | def static() = "todo" 172 | 173 | initialize() 174 | } 175 | -------------------------------------------------------------------------------- /example/todo/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import utest._ 3 | object ExampleTests extends TestSuite{ 4 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 5 | val server = io.undertow.Undertow.builder 6 | .addHttpListener(8081, "localhost") 7 | .setHandler(example.defaultHandler) 8 | .build 9 | server.start() 10 | val res = 11 | try f("http://localhost:8081") 12 | finally server.stop() 13 | res 14 | } 15 | val tests = Tests{ 16 | test("TodoServer") - withServer(TodoServer){ host => 17 | val page = requests.get(host).text() 18 | assert(page.contains("What needs to be done?")) 19 | 20 | val cssResponse = requests.get(host + "/static/index.css") 21 | assert(cssResponse.text().contains("font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;")) 22 | assert(cssResponse.headers("content-type") == List("text/css")) 23 | 24 | val jsResponse = requests.get(host + "/static/app.js") 25 | assert(jsResponse.text().contains("initListeners()")) 26 | assert(jsResponse.headers("content-type") == List("text/javascript")) 27 | } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /example/todo/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.todo 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scala213) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ivy"org.xerial:sqlite-jdbc:3.42.0.0", 11 | ivy"com.lihaoyi::scalasql:0.1.0", 12 | ivy"com.lihaoyi::scalatags:0.12.0", 13 | ivy"org.slf4j:slf4j-simple:1.7.30", 14 | ) 15 | 16 | object test extends ScalaTests with TestModule.Utest{ 17 | 18 | def ivyDeps = Agg( 19 | ivy"com.lihaoyi::utest::0.8.4", 20 | ivy"com.lihaoyi::requests::0.9.0", 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/todoApi/app/src/TodoMvcApi.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object TodoMvcApi extends cask.MainRoutes{ 3 | case class Todo(checked: Boolean, text: String) 4 | object Todo{ 5 | implicit def todoRW: upickle.default.ReadWriter[Todo] = upickle.default.macroRW[Todo] 6 | } 7 | var todos = Seq( 8 | Todo(true, "Get started with Cask"), 9 | Todo(false, "Profit!") 10 | ) 11 | 12 | @cask.get("/list/:state") 13 | def list(state: String) = { 14 | val filteredTodos = state match{ 15 | case "all" => todos 16 | case "active" => todos.filter(!_.checked) 17 | case "completed" => todos.filter(_.checked) 18 | } 19 | upickle.default.write(filteredTodos) 20 | } 21 | 22 | @cask.post("/add") 23 | def add(request: cask.Request) = { 24 | todos = Seq(Todo(false, request.text())) ++ todos 25 | } 26 | 27 | @cask.post("/toggle/:index") 28 | def toggle(index: Int) = { 29 | todos = todos.updated(index, todos(index).copy(checked = !todos(index).checked)) 30 | } 31 | 32 | @cask.post("/delete/:index") 33 | def delete(index: Int) = { 34 | todos = todos.patch(index, Nil, 1) 35 | } 36 | 37 | initialize() 38 | } 39 | -------------------------------------------------------------------------------- /example/todoApi/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("TodoMvcApi") - withServer(TodoMvcApi){ host => 21 | requests.get(s"$host/list/all").text() ==> 22 | """[{"checked":true,"text":"Get started with Cask"},{"checked":false,"text":"Profit!"}]""" 23 | requests.get(s"$host/list/active").text() ==> 24 | """[{"checked":false,"text":"Profit!"}]""" 25 | requests.get(s"$host/list/completed").text() ==> 26 | """[{"checked":true,"text":"Get started with Cask"}]""" 27 | 28 | requests.post(s"$host/toggle/1") 29 | 30 | requests.get(s"$host/list/all").text() ==> 31 | """[{"checked":true,"text":"Get started with Cask"},{"checked":true,"text":"Profit!"}]""" 32 | 33 | requests.get(s"$host/list/active").text() ==> 34 | """[]""" 35 | 36 | requests.post(s"$host/add", data = "new Task") 37 | 38 | requests.get(s"$host/list/active").text() ==> 39 | """[{"checked":false,"text":"new Task"}]""" 40 | 41 | requests.post(s"$host/delete/0") 42 | 43 | requests.get(s"$host/list/active").text() ==> 44 | """[]""" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/todoApi/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.todoApi 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/todoDb/app/src/TodoMvcDb.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import scalasql.DbApi.Txn 3 | import scalasql.Sc 4 | import scalasql.SqliteDialect._ 5 | 6 | object TodoMvcDb extends cask.MainRoutes{ 7 | val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite") 8 | val sqliteDataSource = new org.sqlite.SQLiteDataSource() 9 | sqliteDataSource.setUrl(s"jdbc:sqlite:$tmpDb/file.db") 10 | lazy val sqliteClient = new scalasql.DbClient.DataSource( 11 | sqliteDataSource, 12 | config = new scalasql.Config {} 13 | ) 14 | 15 | class transactional extends cask.RawDecorator{ 16 | def wrapFunction(pctx: cask.Request, delegate: Delegate) = { 17 | sqliteClient.transaction { txn => 18 | val res = delegate(pctx, Map("txn" -> txn)) 19 | if (res.isInstanceOf[cask.router.Result.Error]) txn.rollback() 20 | res 21 | } 22 | } 23 | } 24 | 25 | case class Todo[T[_]](id: T[Int], checked: T[Boolean], text: T[String]) 26 | object Todo extends scalasql.Table[Todo]{ 27 | implicit def todoRW = upickle.default.macroRW[Todo[Sc]] 28 | } 29 | 30 | sqliteClient.getAutoCommitClientConnection.updateRaw( 31 | """CREATE TABLE todo ( 32 | | id INTEGER PRIMARY KEY AUTOINCREMENT, 33 | | checked BOOLEAN, 34 | | text TEXT 35 | |); 36 | | 37 | |INSERT INTO todo (checked, text) VALUES 38 | |(1, 'Get started with Cask'), 39 | |(0, 'Profit!'); 40 | |""".stripMargin 41 | ) 42 | 43 | @transactional 44 | @cask.get("/list/:state") 45 | def list(state: String)(txn: Txn) = { 46 | val filteredTodos = state match{ 47 | case "all" => txn.run(Todo.select) 48 | case "active" => txn.run(Todo.select.filter(!_.checked)) 49 | case "completed" => txn.run(Todo.select.filter(_.checked)) 50 | } 51 | upickle.default.write(filteredTodos) 52 | } 53 | 54 | @transactional 55 | @cask.post("/add") 56 | def add(request: cask.Request)(txn: Txn) = { 57 | val body = request.text() 58 | txn.run( 59 | Todo 60 | .insert 61 | .columns(_.checked := false, _.text := body) 62 | .returning(_.id) 63 | .single 64 | ) 65 | 66 | if (body == "FORCE FAILURE") throw new Exception("FORCE FAILURE BODY") 67 | } 68 | 69 | @transactional 70 | @cask.post("/toggle/:index") 71 | def toggle(index: Int)(txn: Txn) = { 72 | txn.run(Todo.update(_.id === index).set(p => p.checked := !p.checked)) 73 | } 74 | 75 | @transactional 76 | @cask.post("/delete/:index") 77 | def delete(index: Int)(txn: Txn) = { 78 | txn.run(Todo.delete(_.id === index)) 79 | } 80 | 81 | initialize() 82 | } 83 | -------------------------------------------------------------------------------- /example/todoDb/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("TodoMvcDb") - withServer(TodoMvcDb){ host => 21 | requests.get(s"$host/list/all").text() ==> 22 | """[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":false,"text":"Profit!"}]""" 23 | requests.get(s"$host/list/active").text() ==> 24 | """[{"id":2,"checked":false,"text":"Profit!"}]""" 25 | requests.get(s"$host/list/completed").text() ==> 26 | """[{"id":1,"checked":true,"text":"Get started with Cask"}]""" 27 | 28 | requests.post(s"$host/toggle/2") 29 | 30 | requests.get(s"$host/list/all").text() ==> 31 | """[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":true,"text":"Profit!"}]""" 32 | 33 | requests.get(s"$host/list/active").text() ==> 34 | """[]""" 35 | 36 | requests.post(s"$host/add", data = "new Task") 37 | 38 | // Make sure endpoint failures do not commit their transaction 39 | requests.post(s"$host/add", data = "FORCE FAILURE", check = false).statusCode ==> 500 40 | 41 | requests.get(s"$host/list/active").text() ==> 42 | """[{"id":3,"checked":false,"text":"new Task"}]""" 43 | 44 | requests.post(s"$host/delete/3") 45 | 46 | requests.get(s"$host/list/active").text() ==> 47 | """[]""" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example/todoDb/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.todoDb 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scala213) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ivy"org.xerial:sqlite-jdbc:3.42.0.0", 11 | ivy"com.lihaoyi::scalasql:0.1.0", 12 | ) 13 | 14 | object test extends ScalaTests with TestModule.Utest{ 15 | 16 | def ivyDeps = Agg( 17 | ivy"com.lihaoyi::utest::0.8.4", 18 | ivy"com.lihaoyi::requests::0.9.0", 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/todoDbWithLoom/app/src/TodoMvcDbWithLoom.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import scalasql.DbApi.Txn 3 | import scalasql.Sc 4 | import scalasql.SqliteDialect._ 5 | 6 | import java.util.concurrent.{ExecutorService, Executors} 7 | 8 | object TodoMvcDbWithLoom extends cask.MainRoutes { 9 | val tmpDb = java.nio.file.Files.createTempDirectory("todo-cask-sqlite") 10 | val sqliteDataSource = new org.sqlite.SQLiteDataSource() 11 | sqliteDataSource.setUrl(s"jdbc:sqlite:$tmpDb/file.db") 12 | lazy val sqliteClient = new scalasql.DbClient.DataSource( 13 | sqliteDataSource, 14 | config = new scalasql.Config {} 15 | ) 16 | 17 | private val executor = Executors.newFixedThreadPool(4) 18 | override protected def handlerExecutor(): Option[ExecutorService] = { 19 | super.handlerExecutor().orElse(Some(executor)) 20 | } 21 | 22 | class transactional extends cask.RawDecorator{ 23 | def wrapFunction(pctx: cask.Request, delegate: Delegate) = { 24 | sqliteClient.transaction { txn => 25 | val res = delegate(pctx, Map("txn" -> txn)) 26 | if (res.isInstanceOf[cask.router.Result.Error]) txn.rollback() 27 | res 28 | } 29 | } 30 | } 31 | 32 | case class Todo[T[_]](id: T[Int], checked: T[Boolean], text: T[String]) 33 | object Todo extends scalasql.Table[Todo]{ 34 | implicit def todoRW = upickle.default.macroRW[Todo[Sc]] 35 | } 36 | 37 | sqliteClient.getAutoCommitClientConnection.updateRaw( 38 | """CREATE TABLE todo ( 39 | | id INTEGER PRIMARY KEY AUTOINCREMENT, 40 | | checked BOOLEAN, 41 | | text TEXT 42 | |); 43 | | 44 | |INSERT INTO todo (checked, text) VALUES 45 | |(1, 'Get started with Cask'), 46 | |(0, 'Profit!'); 47 | |""".stripMargin 48 | ) 49 | 50 | @transactional 51 | @cask.get("/list/:state") 52 | def list(state: String)(txn: Txn) = { 53 | val filteredTodos = state match{ 54 | case "all" => txn.run(Todo.select) 55 | case "active" => txn.run(Todo.select.filter(!_.checked)) 56 | case "completed" => txn.run(Todo.select.filter(_.checked)) 57 | } 58 | upickle.default.write(filteredTodos) 59 | } 60 | 61 | @transactional 62 | @cask.post("/add") 63 | def add(request: cask.Request)(txn: Txn) = { 64 | val body = request.text() 65 | txn.run( 66 | Todo 67 | .insert 68 | .columns(_.checked := false, _.text := body) 69 | .returning(_.id) 70 | .single 71 | ) 72 | 73 | if (body == "FORCE FAILURE") throw new Exception("FORCE FAILURE BODY") 74 | } 75 | 76 | @transactional 77 | @cask.post("/toggle/:index") 78 | def toggle(index: Int)(txn: Txn) = { 79 | txn.run(Todo.update(_.id === index).set(p => p.checked := !p.checked)) 80 | } 81 | 82 | @transactional 83 | @cask.post("/delete/:index") 84 | def delete(index: Int)(txn: Txn) = { 85 | txn.run(Todo.delete(_.id === index)) 86 | } 87 | 88 | initialize() 89 | } 90 | -------------------------------------------------------------------------------- /example/todoDbWithLoom/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("TodoMvcDb") - withServer(TodoMvcDbWithLoom){ host => 21 | requests.get(s"$host/list/all").text() ==> 22 | """[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":false,"text":"Profit!"}]""" 23 | requests.get(s"$host/list/active").text() ==> 24 | """[{"id":2,"checked":false,"text":"Profit!"}]""" 25 | requests.get(s"$host/list/completed").text() ==> 26 | """[{"id":1,"checked":true,"text":"Get started with Cask"}]""" 27 | 28 | requests.post(s"$host/toggle/2") 29 | 30 | requests.get(s"$host/list/all").text() ==> 31 | """[{"id":1,"checked":true,"text":"Get started with Cask"},{"id":2,"checked":true,"text":"Profit!"}]""" 32 | 33 | requests.get(s"$host/list/active").text() ==> 34 | """[]""" 35 | 36 | requests.post(s"$host/add", data = "new Task") 37 | 38 | // Make sure endpoint failures do not commit their transaction 39 | requests.post(s"$host/add", data = "FORCE FAILURE", check = false).statusCode ==> 500 40 | 41 | requests.get(s"$host/list/active").text() ==> 42 | """[{"id":3,"checked":false,"text":"new Task"}]""" 43 | 44 | requests.post(s"$host/delete/3") 45 | 46 | requests.get(s"$host/list/active").text() ==> 47 | """[]""" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /example/todoDbWithLoom/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.todoDbWithLoom 2 | import mill._, scalalib._ 3 | import mill.define.ModuleRef 4 | 5 | object app extends Cross[AppModule](build.scala213) 6 | trait AppModule extends CrossScalaModule{ 7 | 8 | private def parseJvmArgs(argsStr: String) = { 9 | argsStr.split(" ").filter(_.nonEmpty).toSeq 10 | } 11 | 12 | def forkArgs = Task.Input { 13 | //TODO not sure why the env passing is not working 14 | val envVirtualThread: String = T.env.getOrElse("CASK_VIRTUAL_THREAD", "false") 15 | println("envVirtualThread: " + envVirtualThread) 16 | 17 | val systemProps = Seq(s"-Dcask.virtual-threads.enabled=$envVirtualThread") 18 | 19 | val baseArgs = Seq( 20 | "--add-opens", "java.base/java.lang=ALL-UNNAMED" 21 | ) 22 | 23 | val seq = baseArgs ++ systemProps 24 | println("final forkArgs: " + seq) 25 | seq 26 | } 27 | 28 | def zincWorker = ModuleRef(ZincWorkerJava11Latest) 29 | 30 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 31 | 32 | def ivyDeps = Agg[Dep]( 33 | ivy"org.xerial:sqlite-jdbc:3.42.0.0", 34 | ivy"com.lihaoyi::scalasql:0.1.0", 35 | ) 36 | 37 | object test extends ScalaTests with TestModule.Utest { 38 | def ivyDeps = Agg( 39 | ivy"com.lihaoyi::utest::0.8.4", 40 | ivy"com.lihaoyi::requests::0.9.0", 41 | ) 42 | } 43 | } 44 | 45 | object ZincWorkerJava11Latest extends ZincWorkerModule with CoursierModule { 46 | def jvmId = "temurin:23.0.1" 47 | def jvmIndexVersion = "latest.release" 48 | } -------------------------------------------------------------------------------- /example/twirl/app/src/Twirl.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object Twirl extends cask.MainRoutes{ 3 | @cask.get("/") 4 | def hello() = { 5 | "" + html.hello("Hello World") 6 | } 7 | 8 | initialize() 9 | } 10 | -------------------------------------------------------------------------------- /example/twirl/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests { 20 | test("Twirl") - withServer(Twirl) { host => 21 | val body = requests.get(host).text() 22 | 23 | assert( 24 | body.contains("

Hello World

"), 25 | body.contains("

I am cow

"), 26 | ) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/twirl/app/views/hello.scala.html: -------------------------------------------------------------------------------- 1 | @(titleTxt: String) 2 | 3 | 4 |

@titleTxt

5 |

I am cow

6 | 7 | -------------------------------------------------------------------------------- /example/twirl/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.twirl 2 | import mill._, scalalib._ 3 | import $ivy.`com.lihaoyi::mill-contrib-twirllib:` 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule with mill.twirllib.TwirlModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def twirlScalaVersion = "2.13.15" 10 | def twirlVersion = "1.6.8" 11 | 12 | def generatedSources = T{ Seq(compileTwirl().classes) } 13 | def ivyDeps = Agg[Dep]( 14 | ivy"com.lihaoyi::scalatags:0.9.1".withDottyCompat(scalaVersion()), 15 | ivy"com.typesafe.play::twirl-api:${twirlVersion()}".withDottyCompat(scalaVersion()), 16 | ) 17 | 18 | object test extends ScalaTests with TestModule.Utest{ 19 | 20 | def ivyDeps = Agg( 21 | ivy"com.lihaoyi::utest::0.8.4", 22 | ivy"com.lihaoyi::requests::0.9.0", 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/variableRoutes/app/src/VariableRoutes.scala: -------------------------------------------------------------------------------- 1 | package app 2 | object VariableRoutes extends cask.MainRoutes{ 3 | @cask.get("/user/:userName") // variable path segment, e.g. HOST/user/lihaoyi 4 | def getUserProfile(userName: String) = { 5 | s"User $userName" 6 | } 7 | 8 | @cask.get("/path") // GET allowing arbitrary sub-paths, e.g. HOST/path/foo/bar/baz 9 | def getSubpath(segments: cask.RemainingPathSegments) = { 10 | s"Subpath ${segments.value}" 11 | } 12 | 13 | @cask.post("/path") // POST allowing arbitrary sub-paths, e.g. HOST/path/foo/bar/baz 14 | def postArticleSubpath(segments: cask.RemainingPathSegments) = { 15 | s"POST Subpath ${segments.value}" 16 | } 17 | 18 | initialize() 19 | } 20 | -------------------------------------------------------------------------------- /example/variableRoutes/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | import io.undertow.Undertow 3 | 4 | import utest._ 5 | 6 | object ExampleTests extends TestSuite{ 7 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 8 | val server = Undertow.builder 9 | .addHttpListener(8081, "localhost") 10 | .setHandler(example.defaultHandler) 11 | .build 12 | server.start() 13 | val res = 14 | try f("http://localhost:8081") 15 | finally server.stop() 16 | res 17 | } 18 | 19 | val tests = Tests{ 20 | test("VariableRoutes") - withServer(VariableRoutes){ host => 21 | val noIndexPage = requests.get(host, check = false) 22 | noIndexPage.statusCode ==> 404 23 | 24 | requests.get(s"$host/user/lihaoyi").text() ==> "User lihaoyi" 25 | requests.get(s"$host/user/li+haoyi").text() ==> "User li haoyi" 26 | requests.get(s"$host/user/1+%2B+1+%3D+2%25%3F%26%2F").text() ==> "User 1 + 1 = 2%?&/" 27 | 28 | requests.get(s"$host/user", check = false).statusCode ==> 404 29 | 30 | requests.get(s"$host/path/one/two/three").text() ==> 31 | "Subpath List(one, two, three)" 32 | 33 | requests.post(s"$host/path/one/two/three").text() ==> 34 | "POST Subpath List(one, two, three)" 35 | 36 | requests.get(s"$host/user/lihaoyi?unknown1=123&unknown2=abc", check = false).text() ==> 37 | """Unknown arguments: "unknown1" "unknown2" 38 | | 39 | |Arguments provided did not match expected signature: 40 | | 41 | |getUserProfile 42 | | userName String 43 | | 44 | |""".stripMargin 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/variableRoutes/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.variableRoutes 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/websockets/app/src/Websockets.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | object Websockets extends cask.MainRoutes{ 4 | @cask.websocket("/connect/:userName") 5 | def showUserProfile(userName: String): cask.WebsocketResult = { 6 | if (userName != "haoyi") cask.Response("", statusCode = 403) 7 | else cask.WsHandler { channel => 8 | cask.WsActor { 9 | case cask.Ws.Text("") => channel.send(cask.Ws.Close()) 10 | case cask.Ws.Text(data) => 11 | channel.send(cask.Ws.Text(userName + " " + data)) 12 | } 13 | } 14 | } 15 | 16 | initialize() 17 | } 18 | -------------------------------------------------------------------------------- /example/websockets/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | 2 | package app 3 | 4 | import java.util.concurrent.atomic.AtomicInteger 5 | import castor.Context.Simple.global 6 | import org.asynchttpclient.ws.{WebSocket, WebSocketListener, WebSocketUpgradeHandler} 7 | import utest._ 8 | import cask.Logger.Console.globalLogger 9 | 10 | object ExampleTests extends TestSuite{ 11 | 12 | 13 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 14 | val server = io.undertow.Undertow.builder 15 | .addHttpListener(8081, "localhost") 16 | .setHandler(example.defaultHandler) 17 | .build 18 | server.start() 19 | val res = 20 | try f("http://localhost:8081") 21 | finally server.stop() 22 | res 23 | } 24 | val tests = Tests{ 25 | test("Websockets") - withServer(Websockets){ host => 26 | @volatile var out = List.empty[String] 27 | // 4. open websocket 28 | 29 | val ws = cask.WsClient.connect("ws://localhost:8081/connect/haoyi"){ 30 | case cask.Ws.Text(s) => out = s :: out 31 | } 32 | 33 | try { 34 | // 5. send messages 35 | ws.send(cask.Ws.Text("hello")) 36 | ws.send(cask.Ws.Text("world")) 37 | ws.send(cask.Ws.Text("")) 38 | Thread.sleep(100) 39 | out ==> List("haoyi world", "haoyi hello") 40 | 41 | val ex = intercept[Exception] { 42 | cask.WsClient.connect("ws://localhost:8081/connect/nobody") { 43 | case _ => /*do nothing*/ 44 | } 45 | } 46 | assert(ex.getMessage.contains("403")) 47 | 48 | }finally ws.send(cask.Ws.Close()) 49 | } 50 | 51 | test("Websockets2000") - withServer(Websockets){ host => 52 | @volatile var out = List.empty[String] 53 | val closed = new AtomicInteger(0) 54 | val client = org.asynchttpclient.Dsl.asyncHttpClient(); 55 | val ws = Seq.fill(2000)(client.prepareGet("ws://localhost:8081/connect/haoyi") 56 | .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener( 57 | new WebSocketListener() { 58 | 59 | override def onTextFrame(payload: String, finalFragment: Boolean, rsv: Int) = { 60 | ExampleTests.synchronized { 61 | out = payload :: out 62 | } 63 | } 64 | 65 | def onOpen(websocket: WebSocket) = () 66 | 67 | def onClose(websocket: WebSocket, code: Int, reason: String) = { 68 | closed.incrementAndGet() 69 | } 70 | 71 | def onError(t: Throwable) = () 72 | }).build() 73 | ).get()) 74 | 75 | try{ 76 | // 5. send messages 77 | ws.foreach(_.sendTextFrame("hello")) 78 | 79 | Thread.sleep(1500) 80 | out.length ==> 2000 81 | 82 | ws.foreach(_.sendTextFrame("world")) 83 | 84 | Thread.sleep(1500) 85 | out.length ==> 4000 86 | closed.get() ==> 0 87 | 88 | ws.foreach(_.sendTextFrame("")) 89 | 90 | Thread.sleep(1500) 91 | closed.get() ==> 2000 92 | 93 | }finally{ 94 | client.close() 95 | } 96 | } 97 | 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /example/websockets/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.websockets 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ivy"org.asynchttpclient:async-http-client:2.12.3" 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/websockets2/app/src/Websockets2.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import io.undertow.websockets.WebSocketConnectionCallback 4 | import io.undertow.websockets.core.{AbstractReceiveListener, BufferedTextMessage, WebSocketChannel, WebSockets} 5 | import io.undertow.websockets.spi.WebSocketHttpExchange 6 | 7 | object Websockets2 extends cask.MainRoutes{ 8 | @cask.websocket("/connect/:userName") 9 | def showUserProfile(userName: String): cask.WebsocketResult = { 10 | if (userName != "haoyi") cask.Response("", statusCode = 403) 11 | else new WebSocketConnectionCallback() { 12 | override def onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel): Unit = { 13 | channel.getReceiveSetter.set( 14 | new AbstractReceiveListener() { 15 | override def onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) = { 16 | message.getData match{ 17 | case "" => channel.close() 18 | case data => WebSockets.sendTextBlocking(userName + " " + data, channel) 19 | } 20 | } 21 | } 22 | ) 23 | channel.resumeReceives() 24 | } 25 | } 26 | } 27 | 28 | initialize() 29 | } 30 | -------------------------------------------------------------------------------- /example/websockets2/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | import castor.Context.Simple.global 5 | import org.asynchttpclient.ws.{WebSocket, WebSocketListener, WebSocketUpgradeHandler} 6 | import utest._ 7 | 8 | import cask.Logger.Console.globalLogger 9 | object ExampleTests extends TestSuite{ 10 | 11 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 12 | val server = io.undertow.Undertow.builder 13 | .addHttpListener(8081, "localhost") 14 | .setHandler(example.defaultHandler) 15 | .build 16 | server.start() 17 | val res = 18 | try f("http://localhost:8081") 19 | finally server.stop() 20 | res 21 | } 22 | 23 | val tests = Tests{ 24 | test("Websockets") - withServer(Websockets2){ host => 25 | @volatile var out = List.empty[String] 26 | // 4. open websocket 27 | val ws = cask.WsClient.connect("ws://localhost:8081/connect/haoyi"){ 28 | case cask.Ws.Text(s) => out = s :: out 29 | } 30 | 31 | try { 32 | // 5. send messages 33 | ws.send(cask.Ws.Text("hello")) 34 | ws.send(cask.Ws.Text("world")) 35 | ws.send(cask.Ws.Text("")) 36 | Thread.sleep(100) 37 | out ==> List("haoyi world", "haoyi hello") 38 | 39 | val ex = intercept[Exception]( 40 | cask.WsClient.connect("ws://localhost:8081/connect/nobody") { 41 | case _ => /*do nothing*/ 42 | } 43 | ) 44 | assert(ex.getMessage.contains("403")) 45 | }finally ws.send(cask.Ws.Close()) 46 | } 47 | 48 | test("Websockets2000") - withServer(Websockets2){ host => 49 | @volatile var out = List.empty[String] 50 | val closed = new AtomicInteger(0) 51 | val client = org.asynchttpclient.Dsl.asyncHttpClient(); 52 | val ws = Seq.fill(2000)(client.prepareGet("ws://localhost:8081/connect/haoyi") 53 | .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener( 54 | new WebSocketListener() { 55 | 56 | override def onTextFrame(payload: String, finalFragment: Boolean, rsv: Int) = { 57 | ExampleTests.synchronized { 58 | out = payload :: out 59 | } 60 | } 61 | 62 | def onOpen(websocket: WebSocket) = () 63 | 64 | def onClose(websocket: WebSocket, code: Int, reason: String) = { 65 | closed.incrementAndGet() 66 | } 67 | 68 | def onError(t: Throwable) = () 69 | }).build() 70 | ).get()) 71 | 72 | try{ 73 | // 5. send messages 74 | ws.foreach(_.sendTextFrame("hello")) 75 | 76 | Thread.sleep(1500) 77 | out.length ==> 2000 78 | 79 | ws.foreach(_.sendTextFrame("world")) 80 | 81 | Thread.sleep(1500) 82 | out.length ==> 4000 83 | closed.get() ==> 0 84 | 85 | ws.foreach(_.sendTextFrame("")) 86 | 87 | Thread.sleep(1500) 88 | closed.get() ==> 2000 89 | 90 | }finally{ 91 | client.close() 92 | } 93 | } 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /example/websockets2/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.websockets2 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ivy"org.asynchttpclient:async-http-client:2.12.3" 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/websockets3/app/src/Websockets3.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | case class Websockets3()(implicit cc: castor.Context, 4 | log: cask.Logger) extends cask.Routes{ 5 | @cask.websocket("/connect/:userName") 6 | def showUserProfile(userName: String): cask.WebsocketResult = { 7 | if (userName != "haoyi") cask.Response("", statusCode = 403) 8 | else cask.WsHandler { channel => 9 | cask.WsActor { 10 | case cask.Ws.Text("") => channel.send(cask.Ws.Close()) 11 | case cask.Ws.Text(data) => 12 | channel.send(cask.Ws.Text(userName + " " + data)) 13 | } 14 | } 15 | } 16 | 17 | initialize() 18 | } 19 | 20 | object Websockets3Main extends cask.Main{ 21 | val allRoutes = Seq(Websockets3()) 22 | } 23 | -------------------------------------------------------------------------------- /example/websockets3/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import utest._ 4 | import cask.Logger.Console.globalLogger 5 | import castor.Context.Simple.global 6 | 7 | object ExampleTests extends TestSuite{ 8 | 9 | 10 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 11 | val server = io.undertow.Undertow.builder 12 | .addHttpListener(8081, "localhost") 13 | .setHandler(example.defaultHandler) 14 | .build 15 | server.start() 16 | val res = 17 | try f("http://localhost:8081") 18 | finally server.stop() 19 | res 20 | } 21 | 22 | val tests = Tests{ 23 | test("Websockets") - withServer(Websockets3Main){ host => 24 | @volatile var out = List.empty[String] 25 | // 4. open websocket 26 | val ws = cask.WsClient.connect("ws://localhost:8081/connect/haoyi"){ 27 | case cask.Ws.Text(s) => out = s :: out 28 | } 29 | 30 | try { 31 | // 5. send messages 32 | ws.send(cask.Ws.Text("hello")) 33 | ws.send(cask.Ws.Text("world")) 34 | ws.send(cask.Ws.Text("")) 35 | Thread.sleep(100) 36 | out ==> List("haoyi world", "haoyi hello") 37 | 38 | val ex = intercept[Exception]( 39 | cask.WsClient.connect("ws://localhost:8081/connect/nobody") { 40 | case _ => /*do nothing*/ 41 | } 42 | ) 43 | assert(ex.getMessage.contains("403")) 44 | }finally ws.send(cask.Ws.Close()) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /example/websockets3/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.websockets3 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ivy"org.asynchttpclient:async-http-client:2.12.3" 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/websockets4/app/src/Websockets4.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | case class Websockets4()(implicit cc: castor.Context, 4 | log: cask.Logger) extends cask.Routes{ 5 | // make sure compress decorator passes non-requests through correctly 6 | override def decorators = Seq(new cask.decorators.compress()) 7 | @cask.websocket("/connect/:userName") 8 | def showUserProfile(userName: String): cask.WebsocketResult = { 9 | if (userName != "haoyi") cask.Response("", statusCode = 403) 10 | else cask.WsHandler { channel => 11 | cask.WsActor { 12 | case cask.Ws.Text("") => channel.send(cask.Ws.Close()) 13 | case cask.Ws.Text(data) => 14 | channel.send(cask.Ws.Text(userName + " " + data)) 15 | } 16 | } 17 | } 18 | 19 | initialize() 20 | } 21 | 22 | object Websockets4Main extends cask.Main{ 23 | val allRoutes = Seq(Websockets4()) 24 | } 25 | -------------------------------------------------------------------------------- /example/websockets4/app/test/src/ExampleTests.scala: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import utest._ 4 | import cask.Logger.Console.globalLogger 5 | import castor.Context.Simple.global 6 | object ExampleTests extends TestSuite{ 7 | 8 | 9 | def withServer[T](example: cask.main.Main)(f: String => T): T = { 10 | val server = io.undertow.Undertow.builder 11 | .addHttpListener(8081, "localhost") 12 | .setHandler(example.defaultHandler) 13 | .build 14 | server.start() 15 | val res = 16 | try f("http://localhost:8081") 17 | finally server.stop() 18 | res 19 | } 20 | 21 | val tests = Tests{ 22 | test("Websockets") - withServer(Websockets4Main){ host => 23 | @volatile var out = List.empty[String] 24 | // 4. open websocket 25 | val ws = cask.WsClient.connect("ws://localhost:8081/connect/haoyi"){ 26 | case cask.Ws.Text(s) => out = s :: out 27 | } 28 | 29 | try { 30 | // 5. send messages 31 | ws.send(cask.Ws.Text("hello")) 32 | ws.send(cask.Ws.Text("world")) 33 | ws.send(cask.Ws.Text("")) 34 | Thread.sleep(100) 35 | out ==> List("haoyi world", "haoyi hello") 36 | 37 | val ex = intercept[Exception]( 38 | cask.WsClient.connect("ws://localhost:8081/connect/nobody") { 39 | case _ => /*do nothing*/ 40 | } 41 | ) 42 | assert(ex.getMessage.contains("403")) 43 | }finally ws.send(cask.Ws.Close()) 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/websockets4/package.mill: -------------------------------------------------------------------------------- 1 | package build.example.websockets4 2 | import mill._, scalalib._ 3 | 4 | object app extends Cross[AppModule](build.scalaVersions) 5 | trait AppModule extends CrossScalaModule{ 6 | 7 | def moduleDeps = Seq(build.cask(crossScalaVersion)) 8 | 9 | def ivyDeps = Agg[Dep]( 10 | ) 11 | object test extends ScalaTests with TestModule.Utest{ 12 | 13 | def ivyDeps = Agg( 14 | ivy"com.lihaoyi::utest::0.8.4", 15 | ivy"com.lihaoyi::requests::0.9.0", 16 | ivy"org.asynchttpclient:async-http-client:2.12.3" 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mill: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # This is a wrapper script, that automatically download mill from GitHub release pages 4 | # You can give the required mill version with MILL_VERSION env variable 5 | # If no version is given, it falls back to the value of DEFAULT_MILL_VERSION 6 | 7 | set -e 8 | 9 | if [ -z "${DEFAULT_MILL_VERSION}" ] ; then 10 | DEFAULT_MILL_VERSION=0.11.12 11 | fi 12 | 13 | if [ -z "$MILL_VERSION" ] ; then 14 | if [ -f ".mill-version" ] ; then 15 | MILL_VERSION="$(head -n 1 .mill-version 2> /dev/null)" 16 | elif [ -f ".config/mill-version" ] ; then 17 | MILL_VERSION="$(head -n 1 .config/mill-version 2> /dev/null)" 18 | elif [ -f "mill" ] && [ "$0" != "mill" ] ; then 19 | MILL_VERSION=$(grep -F "DEFAULT_MILL_VERSION=" "mill" | head -n 1 | cut -d= -f2) 20 | else 21 | MILL_VERSION=$DEFAULT_MILL_VERSION 22 | fi 23 | fi 24 | 25 | if [ "x${XDG_CACHE_HOME}" != "x" ] ; then 26 | MILL_DOWNLOAD_PATH="${XDG_CACHE_HOME}/mill/download" 27 | else 28 | MILL_DOWNLOAD_PATH="${HOME}/.cache/mill/download" 29 | fi 30 | MILL_EXEC_PATH="${MILL_DOWNLOAD_PATH}/${MILL_VERSION}" 31 | 32 | version_remainder="$MILL_VERSION" 33 | MILL_MAJOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" 34 | MILL_MINOR_VERSION="${version_remainder%%.*}"; version_remainder="${version_remainder#*.}" 35 | 36 | if [ ! -s "$MILL_EXEC_PATH" ] ; then 37 | mkdir -p "$MILL_DOWNLOAD_PATH" 38 | if [ "$MILL_MAJOR_VERSION" -gt 0 ] || [ "$MILL_MINOR_VERSION" -ge 5 ] ; then 39 | ASSEMBLY="-assembly" 40 | fi 41 | DOWNLOAD_FILE=$MILL_EXEC_PATH-tmp-download 42 | MILL_VERSION_TAG=$(echo $MILL_VERSION | sed -E 's/([^-]+)(-M[0-9]+)?(-.*)?/\1\2/') 43 | MILL_DOWNLOAD_URL="https://repo1.maven.org/maven2/com/lihaoyi/mill-dist/$MILL_VERSION/mill-dist-$MILL_VERSION.jar" 44 | curl --fail -L -o "$DOWNLOAD_FILE" "$MILL_DOWNLOAD_URL" 45 | chmod +x "$DOWNLOAD_FILE" 46 | mv "$DOWNLOAD_FILE" "$MILL_EXEC_PATH" 47 | unset DOWNLOAD_FILE 48 | unset MILL_DOWNLOAD_URL 49 | fi 50 | 51 | if [ -z "$MILL_MAIN_CLI" ] ; then 52 | MILL_MAIN_CLI="${0}" 53 | fi 54 | 55 | MILL_FIRST_ARG="" 56 | 57 | # first arg is a long flag for "--interactive" or starts with "-i" 58 | if [ "$1" = "--bsp" ] || [ "${1#"-i"}" != "$1" ] || [ "$1" = "--interactive" ] || [ "$1" = "--no-server" ] || [ "$1" = "--repl" ] || [ "$1" = "--help" ] ; then 59 | # Need to preserve the first position of those listed options 60 | MILL_FIRST_ARG=$1 61 | shift 62 | fi 63 | 64 | unset MILL_DOWNLOAD_PATH 65 | unset MILL_VERSION 66 | 67 | exec $MILL_EXEC_PATH $MILL_FIRST_ARG -D "mill.main.cli=${MILL_MAIN_CLI}" "$@" 68 | --------------------------------------------------------------------------------