├── .scalafmt.conf ├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── ci ├── test.sh ├── test.yml └── pipeline.yml ├── serverAkkaHttp └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ ├── package.scala │ │ ├── FailSupport.scala │ │ ├── RPCController.scala │ │ ├── RPCServer.scala │ │ ├── RouterDerivation.scala │ │ ├── AutowireErrorsSupport.scala │ │ └── RPCRouter.scala │ └── test │ └── scala │ ├── TestController.scala │ └── AppSpecs.scala ├── clientAkkaHttp └── src │ ├── main │ └── scala │ │ ├── package.scala │ │ ├── ClientDerivation.scala │ │ ├── FailSupport.scala │ │ ├── RPCClient.scala │ │ └── RequestBuilder.scala │ └── test │ └── scala │ ├── RPCTestClient.scala │ └── AppSpecs.scala ├── .gitignore ├── core └── src │ └── main │ └── scala │ ├── annotations.scala │ ├── BooleanDecoder.scala │ ├── models.scala │ ├── TypePathMacro.scala │ ├── PathMacro.scala │ ├── OptionDecoder.scala │ └── MetaDataMacro.scala ├── README.md ├── docs └── src │ └── main │ └── tut │ ├── getting-started.md │ ├── authorization.md │ ├── index.md │ └── step-by-step-example.md ├── LICENSE └── examples └── src └── main └── scala ├── Doghouse.scala └── Users.scala /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.0.0-RC4" 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.8.2-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | apk update && apk add --no-cache git 6 | sbt test 7 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | wiro { 2 | routes-prefix = ${?WIRO_ROUTES_PREFIX} 3 | } -------------------------------------------------------------------------------- /clientAkkaHttp/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | package wiro.client 2 | 3 | import io.circe.Json 4 | 5 | package object akkaHttp { 6 | trait WiroDecoder[A] { 7 | def decode(j: Json): A 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .metals 6 | .bloop 7 | .cache 8 | .history 9 | .lib/ 10 | dist/* 11 | target/ 12 | lib_managed/ 13 | src_managed/ 14 | project/boot/ 15 | project/plugins/project/ 16 | 17 | # Scala-IDE specific 18 | .scala_dependencies 19 | .worksheet 20 | -------------------------------------------------------------------------------- /ci/test.yml: -------------------------------------------------------------------------------- 1 | platform: linux 2 | 3 | image_resource: 4 | type: docker-image 5 | source: 6 | repository: buildo/scala-sbt-alpine 7 | tag: 8u201_2.12.8_1.2.8 8 | 9 | inputs: 10 | - name: wiro 11 | 12 | caches: 13 | - path: .sbt 14 | - path: .ivy2 15 | 16 | run: 17 | path: ci/test.sh 18 | dir: wiro 19 | -------------------------------------------------------------------------------- /core/src/main/scala/annotations.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import scala.annotation.StaticAnnotation 4 | 5 | package object annotation { 6 | class command(name: Option[String] = None) extends StaticAnnotation 7 | class query(name: Option[String] = None) extends StaticAnnotation 8 | class path(name: String) extends StaticAnnotation 9 | } 10 | -------------------------------------------------------------------------------- /core/src/main/scala/BooleanDecoder.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import io.circe.Decoder 4 | import scala.util.Try 5 | 6 | trait CustomBooleanDecoder { 7 | private[this] def asBoolean(s: String) = Try(s.toBoolean) 8 | implicit final val decodeBoolean: Decoder[Boolean] = 9 | Decoder.decodeBoolean or Decoder.decodeString.emapTry(asBoolean).withErrorMessage("Boolean") 10 | } 11 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | package wiro.server 2 | 3 | import akka.http.scaladsl.model.HttpResponse 4 | import io.circe.Json 5 | 6 | package object akkaHttp { 7 | final case class ReferenceConfig(routesPrefix: Option[String]) 8 | 9 | trait ToHttpResponse[A] { 10 | def response(a: A): HttpResponse 11 | } 12 | 13 | trait WiroEncoder[A] { 14 | def encode(a: A): Json 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/main/scala/models.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import io.circe.Json 4 | 5 | case class Config( 6 | host: String, 7 | port: Int 8 | ) 9 | 10 | case class WiroRequest( 11 | args: String 12 | ) 13 | 14 | case class RpcRequest( 15 | path: Seq[String], 16 | args: Map[String, Json] 17 | ) 18 | 19 | case class Auth( 20 | token: String 21 | ) 22 | 23 | case class OperationParameters( 24 | parameters: Map[String, String] 25 | ) 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > Moved to https://github.com/buildo/retro 2 | 3 | # Wiro 4 | 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/53a17fb6396c4a0daa835c407ca22866)](https://www.codacy.com/app/claudio_4/wiro?utm_source=github.com&utm_medium=referral&utm_content=buildo/wiro&utm_campaign=badger) 6 | 7 | Wiro is a lightweight Scala library for writing HTTP routes. 8 | 9 | ## Documentation 10 | 11 | Please see the [guide](https://buildo.github.io/wiro/) for more information. 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.2") 2 | 3 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") 4 | 5 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "0.9.3") 6 | 7 | addSbtPlugin("com.47deg" % "sbt-microsites" % "0.7.15") 8 | 9 | addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.5.6") 10 | 11 | resolvers += "jgit-repo" at "http://download.eclipse.org/jgit/maven" 12 | 13 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.6.2") 14 | 15 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.7") 16 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/main/scala/FailSupport.scala: -------------------------------------------------------------------------------- 1 | package wiro.server.akkaHttp 2 | 3 | import io.circe._ 4 | import io.circe.syntax._ 5 | 6 | object FailSupport { 7 | case class FailException[T: ToHttpResponse](hasResponse: T) extends Throwable { 8 | def response = implicitly[ToHttpResponse[T]].response(hasResponse) 9 | } 10 | 11 | implicit def wiroCanFailEncoder[T: ToHttpResponse, A: Encoder] = new WiroEncoder[Either[T, A]] { 12 | def encode(d: Either[T, A]): Json = d.fold(error => throw FailException(error), _.asJson) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/main/scala/RPCController.scala: -------------------------------------------------------------------------------- 1 | package wiro.server.akkaHttp 2 | 3 | import io.circe._ 4 | import wiro.{ CustomOptionDecoder, CustomBooleanDecoder } 5 | 6 | import cats.syntax.either._ 7 | 8 | trait RPCServer extends autowire.Server[Json, Decoder, WiroEncoder] with CustomOptionDecoder with CustomBooleanDecoder { 9 | def write[Result: WiroEncoder](r: Result): Json = 10 | implicitly[WiroEncoder[Result]].encode(r) 11 | 12 | def read[Result: Decoder](p: Json): Result = 13 | p.as[Result].toTry.get 14 | 15 | def routes: Router 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/main/tut/getting-started.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | Wiro is published on [Bintray](https://bintray.com/buildo/maven/wiro-http-server) and cross-built for Scala 2.11, and Scala 2.12. 4 | 5 | How to add dependency: 6 | 7 | ```scala 8 | libraryDependencies ++= Seq( 9 | "io.buildo" %% "wiro-http-server" % "X.X.X", 10 | "org.slf4j" % "slf4j-nop" % "1.6.4" 11 | ) 12 | ``` 13 | 14 | 15 | Wiro uses scala-logging, so you need to include an SLF4J backend. We include slf4j-nop to disable logging, but you can replace this with the logging framework you prefer (log4j2, logback). 16 | -------------------------------------------------------------------------------- /core/src/main/scala/TypePathMacro.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import scala.language.experimental.macros 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | import wiro.annotation.path 7 | 8 | trait TypePathMacro { 9 | def typePath[A]: Seq[String] = macro TypePathMacro.typePathImpl[A] 10 | } 11 | 12 | object TypePathMacro extends TypePathMacro { 13 | def typePathImpl[A: c.WeakTypeTag](c: Context): c.Tree = { 14 | import c.universe._ 15 | 16 | val tpe = weakTypeOf[A].typeSymbol 17 | val path = tpe.fullName.toString.split('.').toSeq 18 | 19 | q"Seq(..$path)" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/main/scala/PathMacro.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import scala.reflect.macros.blackbox.Context 4 | import scala.language.experimental.macros 5 | 6 | import wiro.annotation.path 7 | 8 | trait PathMacro { 9 | def derivePath[A]: String = macro PathMacro.derivePathImpl[A] 10 | } 11 | 12 | object PathMacro extends PathMacro { 13 | def derivePathImpl[A: c.WeakTypeTag](c: Context): c.Tree = { 14 | import c.universe._ 15 | 16 | val tpe = weakTypeOf[A].typeSymbol 17 | 18 | tpe.annotations.collectFirst { 19 | case pathAnnotation if pathAnnotation.tree.tpe <:< weakTypeOf[path] => 20 | pathAnnotation.tree.children.tail.head 21 | }.getOrElse { 22 | c.abort(c.enclosingPosition, s"\n\nMissing annotation @path() on $tpe\n") 23 | } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /clientAkkaHttp/src/test/scala/RPCTestClient.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import akka.http.scaladsl.model.HttpRequest 4 | import akka.http.scaladsl.server._ 5 | import akka.http.scaladsl.server.Directives._ 6 | import akka.http.scaladsl.testkit._ 7 | 8 | import cats.implicits._ 9 | import client.akkaHttp.{ RPCClient, RPCClientContext } 10 | 11 | import scala.concurrent.Future 12 | 13 | trait RPCRouteTest extends RouteTest { this: TestFrameworkInterface => 14 | val prefix = Some("test") 15 | class RPCClientTest(ctx: RPCClientContext[_], route: Route) 16 | extends RPCClient(config = Config("localhost", 80), prefix = prefix, ctx = ctx){ 17 | override private[wiro] def doHttpRequest(request: HttpRequest) = 18 | (request ~> pathPrefix(prefix.get)(route)).response.pure[Future] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /clientAkkaHttp/src/main/scala/ClientDerivation.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | package client.akkaHttp 3 | 4 | import scala.language.experimental.macros 5 | import scala.reflect.macros.blackbox.Context 6 | 7 | trait ClientDerivationModule extends TypePathMacro { 8 | def deriveClientContext[A]: RPCClientContext[A] = macro ClientDerivationMacro.deriveClientContextImpl[A] 9 | } 10 | 11 | object ClientDerivationMacro extends ClientDerivationModule { 12 | def deriveClientContextImpl[A: c.WeakTypeTag](c: Context): c.Tree = { 13 | import c.universe._ 14 | val tpe = weakTypeOf[A] 15 | 16 | q""" 17 | new _root_.wiro.client.akkaHttp.RPCClientContext[$tpe] { 18 | override val methodsMetaData = deriveMetaData[$tpe] 19 | override val tp = typePath[$tpe] 20 | override val path = derivePath[$tpe] 21 | } 22 | """ 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/scala/OptionDecoder.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import io.circe.{ Decoder, FailedCursor, DecodingFailure, HCursor } 4 | 5 | //This code is modified from circe (https://github.com/circe/circe). 6 | //Circe is licensed under http://www.apache.org/licenses/LICENSE-2.0 7 | //with the following notice https://github.com/circe/circe/blob/master/NOTICE. 8 | trait CustomOptionDecoder { 9 | implicit final def decodeOption[A](implicit d: Decoder[A]): Decoder[Option[A]] = Decoder.withReattempt { 10 | case c: HCursor => 11 | if (c.value.isNull) rightNone else d(c) match { 12 | case Right(a) => Right(Some(a)) 13 | //removed part of the code here 14 | case Left(df) => Left(df) 15 | } 16 | case c: FailedCursor => 17 | if (!c.incorrectFocus) rightNone else Left(DecodingFailure("[A]Option[A]", c.history)) 18 | } 19 | 20 | private[this] final val rightNone: Either[DecodingFailure, Option[Nothing]] = Right(None) 21 | } 22 | -------------------------------------------------------------------------------- /clientAkkaHttp/src/test/scala/AppSpecs.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import akka.http.scaladsl.testkit.ScalatestRouteTest 4 | import autowire._ 5 | 6 | import io.circe._ 7 | import io.circe.generic.auto._ 8 | 9 | import org.scalatest.concurrent.ScalaFutures 10 | import org.scalatest.{ Matchers, WordSpec } 11 | 12 | import wiro.client.akkaHttp._ 13 | import wiro.client.akkaHttp.FailSupport._ 14 | import wiro.TestController.{ UserController, User, userRouter } 15 | 16 | class WiroSpec extends WordSpec with Matchers with RPCRouteTest with ScalatestRouteTest with ClientDerivationModule with ScalaFutures { 17 | private[this] val rpcClient = new RPCClientTest( 18 | deriveClientContext[UserController], userRouter.buildRoute 19 | ).apply[UserController] 20 | 21 | "A GET request" when { 22 | "it's right" should { 23 | "return 200 and content" in { 24 | whenReady(rpcClient.read(1).call()) { 25 | user => user shouldBe Right(User(1, "read")) 26 | } 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /clientAkkaHttp/src/main/scala/FailSupport.scala: -------------------------------------------------------------------------------- 1 | package wiro.client.akkaHttp 2 | 3 | import cats.data.{ NonEmptyList, ValidatedNel } 4 | import cats.syntax.either._ 5 | 6 | import io.circe.{ Decoder, DecodingFailure, Json } 7 | 8 | object FailSupport { 9 | implicit def wiroCanFailDecoder[T: Decoder, A: Decoder] = new WiroDecoder[Either[T, A]] { 10 | private[this] def decodeLeft(j: Json): Decoder.Result[Left[T, A]] = j.as[T].map(Left.apply) 11 | private[this] def decodeRight(j: Json): Decoder.Result[Right[T, A]] = j.as[A].map(Right.apply) 12 | 13 | private[this] def decodeEither(j: Json): ValidatedNel[DecodingFailure, Either[T, A]] = 14 | decodeRight(j).toValidatedNel findValid decodeLeft(j).toValidatedNel 15 | private[this] def errorMessage(errorList: NonEmptyList[DecodingFailure]) = 16 | errorList.map(_.getMessage).toList.mkString 17 | 18 | def decode(j: Json): Either[T, A] = decodeEither(j).fold( 19 | error => throw new Exception(errorMessage(error)), identity 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 buildo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/src/main/tut/authorization.md: -------------------------------------------------------------------------------- 1 | ## Authorization 2 | 3 | How do I authorize my routes? just add a `Auth` argument to your methods. 4 | 5 | 6 | Wiro takes care of extracting the token from "Authorization" http header in the form `Token token=${YOUR_TOKEN}`. The token will automagically passed as `token` argument. 7 | 8 | For example, 9 | ```tut:silent 10 | import akka.http.scaladsl.model.{ HttpResponse, StatusCodes } 11 | import scala.concurrent.Future 12 | import wiro.Auth 13 | import wiro.annotation._ 14 | import wiro.server.akkaHttp._ 15 | 16 | case class Unauthorized(msg: String) 17 | implicit def unauthorizedToResponse = new ToHttpResponse[Unauthorized] { 18 | def response(error: Unauthorized) = HttpResponse( 19 | status = StatusCodes.Unauthorized, 20 | entity = "canna cross it" 21 | ) 22 | } 23 | 24 | @path("users") 25 | trait UsersApi { 26 | 27 | @query 28 | def getUser( 29 | token: Auth, 30 | id: Int 31 | ): Future[Either[Unauthorized, String]] 32 | 33 | @command 34 | def insertString( 35 | token: Auth, 36 | id: Int, 37 | name: String 38 | ): Future[Either[Unauthorized, String]] 39 | } 40 | ``` 41 | -------------------------------------------------------------------------------- /ci/pipeline.yml: -------------------------------------------------------------------------------- 1 | resource_types: 2 | - name: pull-request 3 | type: docker-image 4 | source: 5 | repository: teliaoss/github-pr-resource 6 | 7 | resources: 8 | - name: master 9 | type: git 10 | icon: github-circle 11 | source: 12 | uri: git@github.com:buildo/wiro 13 | branch: master 14 | private_key: ((private-key)) 15 | 16 | - name: pr 17 | type: pull-request 18 | source: 19 | repository: buildo/wiro 20 | access_token: ((github-token)) 21 | 22 | jobs: 23 | - name: pr-test 24 | plan: 25 | - get: wiro 26 | resource: pr 27 | trigger: true 28 | version: every 29 | - put: pr 30 | params: 31 | path: wiro 32 | status: pending 33 | context: concourse 34 | - do: 35 | - task: test 36 | file: wiro/ci/test.yml 37 | attempts: 2 38 | on_success: 39 | put: pr 40 | params: 41 | path: wiro 42 | status: success 43 | context: concourse 44 | on_failure: 45 | put: pr 46 | params: 47 | path: wiro 48 | status: failure 49 | context: concourse 50 | 51 | - name: test 52 | plan: 53 | - get: wiro 54 | resource: master 55 | trigger: true 56 | - do: 57 | - task: test 58 | file: wiro/ci/test.yml 59 | attempts: 2 60 | -------------------------------------------------------------------------------- /docs/src/main/tut/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: "Home" 4 | section: "home" 5 | --- 6 | 7 | [![Download](https://api.bintray.com/packages/buildo/maven/wiro-http-server/images/download.svg)](https://bintray.com/buildo/maven/wiro-http-server/_latestVersion) 8 | [![Build Status](https://drone.our.buildo.io/api/badges/buildo/wiro/status.svg)](https://drone.our.buildo.io/buildo/wiro) 9 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/53a17fb6396c4a0daa835c407ca22866)](https://www.codacy.com/app/claudio_4/wiro?utm_source=github.com&utm_medium=referral&utm_content=buildo/wiro&utm_campaign=badger) 10 | 11 | 12 | 13 | {% include_relative getting-started.md %} 14 | 15 | ## Why Wiro? 16 | 17 | Wiro is a lightweight Scala library to automatically generate http routes from scala traits. 18 | At buildo, we don't like writing routes. Writing routes is an error-prone and frustrating procedure. Futhermore, in most of our use-cases, it can be completely automatized. 19 | 20 | ## Features 21 | 22 | Here is the list of the most relevant features of Wiro: 23 | 24 | - Automatic generation of http routes from decorated Scala traits 25 | - Automatic generation of an http client that matches the generated routes 26 | - Extensible error module (based on type classes) 27 | - Custom routes definition (in case you want to use http methods or write paths that Wiro doesn't provide) 28 | - Support for HTTP authorization header 29 | 30 | {% include_relative step-by-step-example.md %} 31 | 32 | {% include_relative authorization.md %} 33 | -------------------------------------------------------------------------------- /core/src/main/scala/MetaDataMacro.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import scala.language.experimental.macros 4 | import scala.reflect.macros.blackbox.Context 5 | 6 | import wiro.annotation._ 7 | 8 | sealed trait OperationType { 9 | def name: Option[String] 10 | } 11 | 12 | object OperationType { 13 | case class Command(name: Option[String]) extends OperationType 14 | case class Query(name: Option[String]) extends OperationType 15 | } 16 | 17 | case class MethodMetaData( 18 | operationType: OperationType 19 | ) 20 | 21 | trait MetaDataMacro { 22 | def deriveMetaData[A]: Map[String, MethodMetaData] = 23 | macro MetaDataMacro.deriveMetaDataImpl[A] 24 | } 25 | 26 | object MetaDataMacro extends MetaDataMacro { 27 | def deriveMetaDataImpl[A: c.WeakTypeTag](c: Context): c.Tree = { 28 | import c.universe._ 29 | 30 | val decls = weakTypeOf[A].decls.collect { 31 | case m: MethodSymbol if !m.isParamWithDefault => 32 | val methodName = m.fullName 33 | val operationType = m.annotations.collectFirst { 34 | case opAnnotation if opAnnotation.tree.tpe <:< weakTypeOf[command] => 35 | val name = opAnnotation.tree.children.tail.head 36 | q"_root_.wiro.OperationType.Command($name)" 37 | case opAnnotation if opAnnotation.tree.tpe <:< weakTypeOf[query] => 38 | val name = opAnnotation.tree.children.tail.head 39 | q"_root_.wiro.OperationType.Query($name)" 40 | } 41 | 42 | q"($methodName -> $operationType.map(_root_.wiro.MethodMetaData(_)))" 43 | } 44 | 45 | q"_root_.scala.collection.immutable.Map(..$decls) collect { case (k, Some(v)) => (k -> v) }" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /clientAkkaHttp/src/main/scala/RPCClient.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | package client.akkaHttp 3 | 4 | import akka.actor.ActorSystem 5 | 6 | import akka.http.scaladsl.Http 7 | import akka.http.scaladsl.model.{ HttpRequest, HttpResponse } 8 | import akka.http.scaladsl.unmarshalling.Unmarshal 9 | 10 | import akka.stream.ActorMaterializer 11 | 12 | import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ 13 | 14 | import io.circe._ 15 | import io.circe.syntax._ 16 | 17 | import scala.concurrent.{ ExecutionContext, Future } 18 | 19 | trait RPCClientContext[T] extends MetaDataMacro with PathMacro { 20 | def methodsMetaData: Map[String, MethodMetaData] 21 | def tp: Seq[String] 22 | def path: String = tp.last 23 | } 24 | 25 | class RPCClient( 26 | config: Config, 27 | prefix: Option[String] = None, 28 | scheme: String = "http", 29 | ctx: RPCClientContext[_] 30 | )(implicit 31 | system: ActorSystem, 32 | materializer: ActorMaterializer, 33 | executionContext: ExecutionContext 34 | ) extends autowire.Client[Json, WiroDecoder, Encoder] { 35 | private[wiro] val requestBuilder = new RequestBuilder(config, prefix, scheme, ctx) 36 | 37 | def write[Result: Encoder](r: Result): Json = r.asJson 38 | 39 | def read[Result: WiroDecoder](p: Json): Result = 40 | implicitly[WiroDecoder[Result]].decode(p) 41 | 42 | private[wiro] def buildRequest(autowireRequest: Request): HttpRequest = 43 | requestBuilder.build(autowireRequest.path, autowireRequest.args) 44 | 45 | private[wiro] def unmarshalResponse(response: HttpResponse): Future[Json] = 46 | Unmarshal(response.entity).to[Json] 47 | 48 | private[wiro] def doHttpRequest(request: HttpRequest): Future[HttpResponse] = 49 | Http().singleRequest(request) 50 | 51 | override def doCall(autowireRequest: Request): Future[Json] = { 52 | val httpRequest = buildRequest(autowireRequest) 53 | doHttpRequest(httpRequest).flatMap(unmarshalResponse) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/main/scala/RPCServer.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | package server.akkaHttp 3 | 4 | import akka.actor.{Actor, ActorSystem, Props, Status} 5 | import akka.http.scaladsl.Http 6 | import akka.http.scaladsl.server.Directives._ 7 | import akka.http.scaladsl.server.Route 8 | import akka.pattern.pipe 9 | import akka.stream.ActorMaterializer 10 | 11 | import com.typesafe.scalalogging.LazyLogging 12 | 13 | import wiro.server.akkaHttp.{Router => WiroRouter} 14 | 15 | import pureconfig.generic.auto._ 16 | import pureconfig.loadConfigOrThrow 17 | 18 | class HttpRPCServer( 19 | config: Config, 20 | routers: List[WiroRouter], 21 | customRoute: Route = reject 22 | )( 23 | implicit 24 | system: ActorSystem, 25 | materializer: ActorMaterializer 26 | ) { 27 | private[this] val referenceConfig = loadConfigOrThrow[ReferenceConfig]("wiro") 28 | private[this] val foldedRoutes = routers 29 | .map(_.buildRoute) 30 | .foldLeft(customRoute)(_ ~ _) 31 | 32 | val route = referenceConfig.routesPrefix match { 33 | case Some(prefix) => pathPrefix(prefix) { foldedRoutes } 34 | case None => foldedRoutes 35 | } 36 | 37 | system.actorOf(Props(new HttpRPCServerActor(config, route)), "wiro-server") 38 | } 39 | 40 | class HttpRPCServerActor( 41 | config: Config, 42 | route: Route 43 | )( 44 | implicit 45 | system: ActorSystem, 46 | materializer: ActorMaterializer 47 | ) extends Actor 48 | with LazyLogging { 49 | import system.dispatcher 50 | 51 | override def receive = { 52 | case binding: Http.ServerBinding => 53 | logger.info(s"Binding on {}", binding.localAddress) 54 | case Status.Failure(cause) => 55 | logger.error(s"Unable to bind to ${config.host}:${config.port}", cause) 56 | } 57 | 58 | val bindingFuture = Http() 59 | .bindAndHandle(route, config.host, config.port) 60 | .pipeTo(self) 61 | 62 | scala.sys.addShutdownHook({ 63 | logger.info("Wiro unbinding on exit") 64 | bindingFuture 65 | .flatMap(_.unbind()) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/main/scala/RouterDerivation.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | package server.akkaHttp 3 | 4 | import scala.language.experimental.macros 5 | import scala.reflect.macros.blackbox.Context 6 | import scala.reflect.macros.Universe 7 | 8 | import wiro.annotation.path 9 | 10 | import io.circe.Printer 11 | 12 | trait RouterDerivationModule extends PathMacro with MetaDataMacro with TypePathMacro { 13 | def deriveRouter[A](a: A): Router = macro RouterDerivationMacro.deriveRouterImpl[A] 14 | def deriveRouter[A](a: A, printer: Printer): Router = macro RouterDerivationMacro.deriveRouterImplPrinter[A] 15 | } 16 | 17 | object RouterDerivationMacro extends RouterDerivationModule { 18 | //val is required to make universe public 19 | object MacroHelper { 20 | //check only annotations of path type 21 | //Since universe is public Tree type can be returned 22 | def derivePath(c: Context)(tpe: c.Type): c.Tree = { 23 | import c.universe._ 24 | tpe.typeSymbol.annotations 25 | .collectFirst { 26 | case pathAnnotation if pathAnnotation.tree.tpe <:< weakTypeOf[path] => 27 | q"override val path = derivePath[$tpe]" 28 | } 29 | .getOrElse(EmptyTree) 30 | } 31 | } 32 | 33 | def deriveRouterImpl[A: c.WeakTypeTag](c: Context)(a: c.Tree): c.Tree = { 34 | import c.universe._ 35 | val tpe = weakTypeOf[A] 36 | val derivePath = MacroHelper.derivePath(c)(tpe) 37 | 38 | q""" 39 | new _root_.wiro.server.akkaHttp.Router { 40 | override val routes = route[$tpe]($a) 41 | override val methodsMetaData = deriveMetaData[$tpe] 42 | override val tp = typePath[$tpe] 43 | $derivePath 44 | } 45 | """ 46 | } 47 | 48 | def deriveRouterImplPrinter[A: c.WeakTypeTag]( 49 | c: Context)(a: c.Tree, printer: c.Tree): c.Tree = { 50 | import c.universe._ 51 | val tpe = weakTypeOf[A] 52 | val derivePath = MacroHelper.derivePath(c)(tpe) 53 | 54 | q""" 55 | new _root_.wiro.server.akkaHttp.Router { 56 | override val routes = route[$tpe]($a) 57 | override val methodsMetaData = deriveMetaData[$tpe] 58 | override val tp = typePath[$tpe] 59 | override implicit val printer: _root_.io.circe.Printer = $printer 60 | $derivePath 61 | } 62 | """ 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/main/scala/AutowireErrorsSupport.scala: -------------------------------------------------------------------------------- 1 | package wiro.server.akkaHttp 2 | 3 | import akka.http.scaladsl.server._ 4 | import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected 5 | import akka.http.scaladsl.server.Directives._ 6 | import akka.http.scaladsl.model.headers.HttpChallenges 7 | import akka.http.scaladsl.model._ 8 | 9 | import com.typesafe.scalalogging.LazyLogging 10 | 11 | import io.circe.{ DecodingFailure, CursorOp } 12 | 13 | object AutowireErrorSupport extends LazyLogging { 14 | def handleUnwrapErrors( 15 | throwable: Throwable 16 | ) = throwable match { 17 | case autowire.Error.InvalidInput(xs) => 18 | handleAutowireInputErrors(xs) 19 | case e: autowire.Error.InvalidInput => 20 | logger.info(s"received input is not valid, cannot process entity", e) 21 | complete(HttpResponse( 22 | status = StatusCodes.UnprocessableEntity, 23 | entity = "Unprocessable entity" 24 | )) 25 | case e: scala.MatchError => 26 | logger.info(s"match error found, route does not exist, returning 404", e) 27 | complete(HttpResponse( 28 | status = StatusCodes.NotFound, 29 | entity = "Operation not found" 30 | )) 31 | case e: Exception => 32 | logger.error(s"unexpected error, returning 500", e) 33 | complete(HttpResponse( 34 | status = StatusCodes.InternalServerError, 35 | entity = "Internal Error" 36 | )) 37 | } 38 | 39 | private[this] def handleMissingParamErrors( 40 | param: String 41 | ): StandardRoute = param match { 42 | case "token" => 43 | logger.info("couldn't find token parameter, rejecting request as unauthorized") 44 | complete(HttpResponse( 45 | status = StatusCodes.Unauthorized, 46 | entity = s"Unauthorized" 47 | )) 48 | case "actionQuery" => 49 | logger.info("required query parameter is missing, method is not allowed") 50 | complete(HttpResponse( 51 | status = StatusCodes.MethodNotAllowed, 52 | entity = s"Method not allowed" 53 | )) 54 | case "actionCommand" => 55 | logger.info("required command parameter is missing, method is not allowed") 56 | complete(HttpResponse( 57 | status = StatusCodes.MethodNotAllowed, 58 | entity = s"Method not allowed" 59 | )) 60 | case _ => 61 | logger.info(s"missing parameter $param from input") 62 | complete(HttpResponse( 63 | status = StatusCodes.UnprocessableEntity, 64 | entity = s"Missing parameter '$param' from input" 65 | )) 66 | } 67 | 68 | private[this] def handleAutowireInputErrors( 69 | xs: autowire.Error.Param 70 | ): StandardRoute = xs match { 71 | case autowire.Error.Param.Missing(param) => 72 | handleMissingParamErrors(param) 73 | case error@autowire.Error.Param.Invalid(param, e) => { 74 | logger.info(s"the received parameter '$param' is invalid", e) 75 | e match { 76 | case DecodingFailure(tpe, history) => 77 | val path = CursorOp.opsToPath(history) 78 | complete(HttpResponse( 79 | status = StatusCodes.UnprocessableEntity, 80 | entity = s"Failed decoding of '${path}' of type '${tpe}'" 81 | )) 82 | case _ => 83 | complete(HttpResponse( 84 | status = StatusCodes.UnprocessableEntity, 85 | entity = s"Missing parameter '$param' from input" 86 | )) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/src/main/scala/Doghouse.scala: -------------------------------------------------------------------------------- 1 | package wiro.apps 2 | 3 | import akka.actor.ActorSystem 4 | import akka.stream.ActorMaterializer 5 | import akka.http.scaladsl.model.{ HttpResponse, StatusCodes, ContentType, HttpEntity, MediaTypes } 6 | 7 | import io.circe.Json 8 | import io.circe.generic.auto._ 9 | 10 | import scala.concurrent.ExecutionContext.Implicits.global 11 | import scala.concurrent.Future 12 | 13 | import wiro.{ Config, Auth, OperationParameters } 14 | import wiro.server.akkaHttp._ 15 | import wiro.server.akkaHttp.{ FailSupport => ServerFailSupport } 16 | import wiro.client.akkaHttp._ 17 | import wiro.client.akkaHttp.{ FailSupport => ClientFailSupport } 18 | 19 | object controllers { 20 | import models._ 21 | import wiro.annotation._ 22 | import ServerFailSupport._ 23 | 24 | case class Nope(msg: String) 25 | case class Wa(lol: String, bah: Int, dah: Int) 26 | 27 | @path("woff") 28 | trait DoghouseApi { 29 | @query(name = Some("puppy")) 30 | def getPuppy( 31 | token: Auth, 32 | wa: Int, 33 | parameters: OperationParameters 34 | ): Future[Either[Nope, Dog]] 35 | 36 | @query(name = Some("pallino")) 37 | def getPallino( 38 | something: String 39 | ): Future[Either[Nope, Dog]] 40 | } 41 | 42 | class DoghouseApiImpl() extends DoghouseApi { 43 | override def getPallino( 44 | something: String 45 | ): Future[Either[Nope, Dog]] = Future { 46 | Right(Dog("pallino")) 47 | } 48 | 49 | override def getPuppy( 50 | token: Auth, 51 | wa: Int, 52 | parameters: OperationParameters 53 | ): Future[Either[Nope, Dog]] = Future { 54 | if (token == Auth("tokenone")) Right(Dog("pallino")) 55 | else Left(Nope("nope")) 56 | } 57 | } 58 | } 59 | 60 | object errors { 61 | import ServerFailSupport._ 62 | import controllers.Nope 63 | 64 | import io.circe.syntax._ 65 | implicit def nopeToResponse = new ToHttpResponse[Nope] { 66 | def response(error: Nope) = HttpResponse( 67 | status = StatusCodes.UnprocessableEntity, 68 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), error.asJson.noSpaces) 69 | ) 70 | } 71 | } 72 | 73 | object Client extends App with ClientDerivationModule { 74 | import controllers._ 75 | import autowire._ 76 | import ClientFailSupport._ 77 | 78 | val config = Config("localhost", 8080) 79 | 80 | implicit val system = ActorSystem() 81 | implicit val materializer = ActorMaterializer() 82 | 83 | val doghouseClient = deriveClientContext[DoghouseApi] 84 | val rpcClient = new RPCClient(config, ctx = doghouseClient) 85 | 86 | val res = rpcClient[DoghouseApi].getPuppy(Auth("tokenone"), 1, OperationParameters(parameters = Map())).call() 87 | 88 | res map (println(_)) recover { case e: Exception => e.printStackTrace } 89 | } 90 | 91 | object Server extends App with RouterDerivationModule { 92 | import controllers._ 93 | import models._ 94 | import errors._ 95 | import ServerFailSupport._ 96 | 97 | val doghouseRouter = deriveRouter[DoghouseApi](new DoghouseApiImpl) 98 | 99 | implicit val system = ActorSystem() 100 | implicit val materializer = ActorMaterializer() 101 | 102 | val rpcServer = new HttpRPCServer( 103 | config = Config("localhost", 8080), 104 | routers = List(doghouseRouter) 105 | ) 106 | } 107 | 108 | object models { 109 | case class Dog(name: String) 110 | case class Kitten(name: String) 111 | } 112 | -------------------------------------------------------------------------------- /examples/src/main/scala/Users.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.model.{ HttpResponse, StatusCodes, ContentType, HttpEntity, MediaTypes } 3 | import akka.stream.ActorMaterializer 4 | 5 | import io.circe.generic.auto._ 6 | 7 | import scala.concurrent.Future 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | import wiro.Config 11 | import wiro.server.akkaHttp._ 12 | import wiro.server.akkaHttp.{ FailSupport => ServerFailSupport } 13 | import wiro.client.akkaHttp._ 14 | import wiro.client.akkaHttp.{ FailSupport => ClientFailSupport } 15 | 16 | // Models definition 17 | object models { 18 | case class User(name: String) 19 | } 20 | 21 | object controllers { 22 | import models._ 23 | import wiro.annotation._ 24 | 25 | // Error messages 26 | case class Error(msg: String) 27 | case class UserNotFoundError(msg: String) 28 | 29 | // API interface 30 | @path("users") 31 | trait UsersApi { 32 | 33 | @query(name = Some("getUser")) 34 | def getUser( 35 | id: Int 36 | ): Future[Either[UserNotFoundError, User]] 37 | 38 | @command(name = Some("insertUser")) 39 | def insertUser( 40 | id: Int, 41 | name: String 42 | ): Future[Either[Error, User]] 43 | } 44 | 45 | val users = collection.mutable.Map.empty[Int, User] 46 | 47 | // API implementation 48 | class UsersApiImpl() extends UsersApi { 49 | override def getUser( 50 | id: Int 51 | ): Future[Either[UserNotFoundError, User]] = { 52 | users.get(id) match { 53 | case Some(user) => Future(Right(user)) 54 | case None => Future(Left(UserNotFoundError("User not found"))) 55 | } 56 | } 57 | 58 | override def insertUser( 59 | id: Int, 60 | name: String 61 | ): Future[Either[Error, User]] = { 62 | val newUser = User(name) 63 | users(id) = newUser 64 | Future(Right(newUser)) 65 | } 66 | } 67 | } 68 | 69 | 70 | object errors { 71 | import controllers.UserNotFoundError 72 | 73 | import io.circe.syntax._ 74 | implicit def notFoundToResponse = new ToHttpResponse[UserNotFoundError] { 75 | def response(error: UserNotFoundError) = HttpResponse( 76 | status = StatusCodes.NotFound, 77 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), error.asJson.noSpaces) 78 | ) 79 | } 80 | 81 | import controllers.Error 82 | implicit def errorToResponse = new ToHttpResponse[Error] { 83 | def response(error: Error) = HttpResponse( 84 | status = StatusCodes.InternalServerError, 85 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), error.asJson.noSpaces) 86 | ) 87 | } 88 | } 89 | 90 | object UsersServer extends App with RouterDerivationModule { 91 | import controllers._ 92 | import models._ 93 | import errors._ 94 | import ServerFailSupport._ 95 | 96 | val usersRouter = deriveRouter[UsersApi](new UsersApiImpl) 97 | 98 | implicit val system = ActorSystem() 99 | implicit val materializer = ActorMaterializer() 100 | 101 | val rpcServer = new HttpRPCServer( 102 | config = Config("localhost", 8080), 103 | routers = List(usersRouter) 104 | ) 105 | } 106 | 107 | object UsersClient extends App with ClientDerivationModule { 108 | import controllers._ 109 | import autowire._ 110 | import ClientFailSupport._ 111 | 112 | val config = Config("localhost", 8080) 113 | 114 | implicit val system = ActorSystem() 115 | implicit val materializer = ActorMaterializer() 116 | 117 | val rpcClient = new RPCClient(config, ctx = deriveClientContext[UsersApi]) 118 | 119 | rpcClient[UsersApi].insertUser(0, "Pippo").call() map (println(_)) 120 | } 121 | -------------------------------------------------------------------------------- /clientAkkaHttp/src/main/scala/RequestBuilder.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | package client.akkaHttp 3 | 4 | import akka.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpMethods, HttpRequest, Uri } 5 | import akka.http.scaladsl.model.headers.RawHeader 6 | import akka.http.scaladsl.model.Uri.{ Host, Authority, Path, Query } 7 | 8 | import io.circe._ 9 | import io.circe.generic.auto._ 10 | import io.circe.syntax._ 11 | 12 | import cats.syntax.either._ 13 | 14 | class RequestBuilder( 15 | config: Config, 16 | prefix: Option[String], 17 | scheme: String, 18 | ctx: RPCClientContext[_] 19 | ) { 20 | def build(path : Seq[String], args: Map[String, Json]): HttpRequest = { 21 | val completePath = path.mkString(".") 22 | //we're trying to match here the paths generated by two different macros 23 | //if it fails at runtime it means something is wrong in the implementation 24 | val methodMetaData = ctx.methodsMetaData 25 | .getOrElse(completePath, throw new Exception(s"Couldn't find metadata about method $completePath")) 26 | val operationName = methodMetaData.operationType.name 27 | .getOrElse(path.lastOption.getOrElse(throw new Exception("Couldn't find appropriate method path"))) 28 | 29 | val (tokenArgs, nonTokenArgs) = splitTokenArgs(args) 30 | val (headersArgs, remainingArgs) = splitHeaderArgs(nonTokenArgs) 31 | val tokenHeader = handleAuth(tokenArgs.values.toList) 32 | val headers = handleHeaders(headersArgs.values.toList) ++ tokenHeader 33 | val uri = buildUri(operationName) 34 | val httpRequest = methodMetaData.operationType match { 35 | case _: OperationType.Command => commandHttpRequest(remainingArgs, uri) 36 | case _: OperationType.Query => queryHttpRequest(remainingArgs, uri) 37 | } 38 | 39 | httpRequest.withHeaders(headers) 40 | } 41 | 42 | private[this] def buildUri(operationName: String) = { 43 | val path = prefix match { 44 | case None => Path / ctx.path / operationName 45 | case Some(prefix) => Path / prefix /ctx.path / operationName 46 | } 47 | 48 | Uri( 49 | scheme = scheme, 50 | path = path, 51 | authority = Authority(host = Host(config.host), port = config.port) 52 | ) 53 | } 54 | 55 | private[this] def splitTokenArgs(args: Map[String, Json]): (Map[String, Json], Map[String, Json]) = 56 | args.partition { case (_, value) => value.as[wiro.Auth].isRight } 57 | 58 | private[this] def splitHeaderArgs(args: Map[String, Json]): (Map[String, Json], Map[String, Json]) = 59 | args.partition { case (_, value) => value.as[wiro.OperationParameters].isRight } 60 | 61 | private[this] def handleAuth(tokenCandidates: List[Json]): List[RawHeader] = 62 | if (tokenCandidates.length > 1) 63 | throw new Exception("Only one parameter of wiro.Auth type should be provided") 64 | else tokenCandidates.map(_.as[wiro.Auth]).collect { 65 | case Right(wiro.Auth(token)) => RawHeader("Authorization", s"Token token=$token") 66 | } 67 | 68 | private[this] val stringPairToHeader: PartialFunction[(String, String), RawHeader] = { 69 | case (headerName: String, headerValue: String) => RawHeader(headerName, headerValue) 70 | } 71 | 72 | private[this] def handleHeaders(headersCandidates: List[Json]): List[RawHeader] = { 73 | val headers: Option[List[RawHeader]] = for { 74 | parameters: Decoder.Result[OperationParameters] <- headersCandidates.headOption.map(_.as[wiro.OperationParameters]) 75 | headers: List[RawHeader] <- parameters.toOption.map(_.parameters.map(stringPairToHeader).toList) 76 | } yield headers 77 | 78 | headers.getOrElse(Nil) 79 | } 80 | 81 | private[this] def commandHttpRequest(nonTokenArgs: Map[String, Json], uri: Uri) = HttpRequest( 82 | method = HttpMethods.POST, uri = uri, entity = HttpEntity( 83 | contentType = ContentTypes.`application/json`, 84 | string = nonTokenArgs.asJson.noSpaces 85 | ) 86 | ) 87 | 88 | private[this] def queryHttpRequest(nonTokenArgs: Map[String, Json], uri: Uri) = HttpRequest( 89 | method = HttpMethods.GET, uri = uri.withQuery(Query(nonTokenArgs.mapValues(_.noSpaces))) 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/test/scala/TestController.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | 3 | import akka.http.scaladsl.model.{ HttpResponse, StatusCodes } 4 | 5 | import io.circe.generic.auto._ 6 | 7 | import scala.concurrent.{ ExecutionContext, Future } 8 | 9 | import wiro.annotation._ 10 | import wiro.server.akkaHttp._ 11 | import wiro.server.akkaHttp.FailSupport._ 12 | 13 | object TestController extends RouterDerivationModule { 14 | case class UserNotFound(userId: Int) 15 | case class Conflict(userId: Int) 16 | case object GenericError 17 | type GenericError = GenericError.type 18 | case class User(id: Int, username: String) 19 | case class Ok(msg: String) 20 | case class Unauthorized(msg: String) 21 | 22 | implicit def genericErrorToResponse = new ToHttpResponse[GenericError] { 23 | def response(error: GenericError) = HttpResponse( 24 | status = StatusCodes.InternalServerError, 25 | entity = "Very Bad" 26 | ) 27 | } 28 | 29 | implicit def unauthorizedToResponse = new ToHttpResponse[Unauthorized] { 30 | def response(error: Unauthorized) = HttpResponse( 31 | status = StatusCodes.Unauthorized, 32 | entity = "Very Bad" 33 | ) 34 | } 35 | 36 | implicit def conflictToResponse = new ToHttpResponse[Conflict] { 37 | def response(error: Conflict) = HttpResponse( 38 | status = StatusCodes.Conflict, 39 | entity = s"User already exists: ${error.userId}" 40 | ) 41 | } 42 | 43 | implicit def notFoundToResponse = new ToHttpResponse[UserNotFound] { 44 | def response(error: UserNotFound) = HttpResponse( 45 | status = StatusCodes.NotFound, 46 | entity = s"User not found: ${error.userId}" 47 | ) 48 | } 49 | 50 | //controllers interface and implementation 51 | @path("user") 52 | trait UserController { 53 | @query 54 | def nobodyCannaCrossIt(token: Auth): Future[Either[Unauthorized, Ok]] 55 | 56 | @query 57 | def inLoveWithMyHeaders(parameters: OperationParameters): Future[Either[GenericError, OperationParameters]] 58 | 59 | @command 60 | def update(id: Int, user: User): Future[Either[UserNotFound, Ok]] 61 | 62 | @command 63 | def updateCommand(id: Int, user: User): Future[Either[UserNotFound, Ok]] 64 | 65 | @query 66 | def read(id: Int): Future[Either[UserNotFound, User]] 67 | 68 | @query 69 | def readString(id: String): Future[Either[UserNotFound, User]] 70 | 71 | @query 72 | def readQuery(id: Int): Future[Either[UserNotFound, User]] 73 | 74 | @command(name = Some("insert")) 75 | def insertUser(id: Int, user: User): Future[Either[Conflict, Ok]] 76 | 77 | @query(name = Some("number")) 78 | def usersNumber(): Future[Either[GenericError, Int]] 79 | } 80 | 81 | private[this] class UserControllerImpl(implicit 82 | ec: ExecutionContext 83 | ) extends UserController { 84 | def nobodyCannaCrossIt(token: Auth): Future[Either[Unauthorized, Ok]] = Future { 85 | if (token == Auth("bus")) Right(Ok("di bus can swim")) 86 | else Left(Unauthorized("yuh cannot cross it")) 87 | } 88 | 89 | def update(id: Int, user: User): Future[Either[UserNotFound, Ok]] = Future { 90 | if (id == 1) Right(Ok("update")) 91 | else Left(UserNotFound(id)) 92 | } 93 | 94 | def updateCommand(id: Int, user: User): Future[Either[UserNotFound, Ok]] = Future { 95 | if (id == 1) Right(Ok("updateCommand")) 96 | else Left(UserNotFound(id)) 97 | } 98 | 99 | def read(id: Int): Future[Either[UserNotFound, User]] = Future { 100 | if (id == 1) Right(User(id, "read")) 101 | else Left(UserNotFound(id)) 102 | } 103 | 104 | def readString(id: String): Future[Either[UserNotFound, User]] = 105 | Future(Right(User(1, "read"))) 106 | 107 | def readQuery(id: Int): Future[Either[UserNotFound, User]] = Future { 108 | if (id == 1) Right(User(id, "readQuery")) 109 | else Left(UserNotFound(id)) 110 | } 111 | 112 | def insertUser(id: Int, user: User): Future[Either[Conflict, Ok]] = Future { 113 | Right(Ok("inserted!")) 114 | } 115 | 116 | def usersNumber(): Future[Either[GenericError, Int]] = Future { 117 | Right(1) 118 | } 119 | 120 | def inLoveWithMyHeaders(parameters: OperationParameters): Future[Either[GenericError, OperationParameters]] = Future { 121 | Right(parameters) 122 | } 123 | } 124 | 125 | import scala.concurrent.ExecutionContext.Implicits.global 126 | private[this] val userController = new UserControllerImpl 127 | def userRouter = deriveRouter[UserController](userController) 128 | } 129 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/main/scala/RPCRouter.scala: -------------------------------------------------------------------------------- 1 | package wiro 2 | package server.akkaHttp 3 | 4 | import AutowireErrorSupport._ 5 | 6 | import akka.http.scaladsl.server.Directives._ 7 | import akka.http.scaladsl.server.{ Directive0, Directive1, ExceptionHandler, Route } 8 | import akka.http.scaladsl.model.HttpEntity 9 | 10 | import cats.syntax.either._ 11 | 12 | import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ 13 | 14 | import FailSupport._ 15 | 16 | import io.circe.{ Json, JsonObject, Printer, ParsingFailure } 17 | import io.circe.parser._ 18 | 19 | import com.typesafe.scalalogging.LazyLogging 20 | 21 | trait Router extends RPCServer with PathMacro with MetaDataMacro with LazyLogging { 22 | def tp: Seq[String] 23 | def methodsMetaData: Map[String, MethodMetaData] 24 | def routes: autowire.Core.Router[Json] 25 | def path: String = tp.last 26 | implicit def printer: Printer = Printer.noSpaces.copy(dropNullValues = true) 27 | 28 | def buildRoute: Route = handleExceptions(exceptionHandler) { 29 | pathPrefix(path) { 30 | methodsMetaData.map { 31 | case (k, v @ MethodMetaData(OperationType.Command(_))) => command(k, v) 32 | case (k, v @ MethodMetaData(OperationType.Query(_))) => query(k, v) 33 | }.reduce(_ ~ _) 34 | } 35 | } 36 | 37 | def exceptionHandler = ExceptionHandler { 38 | case e: FailException[_] => 39 | //logging left 40 | e.response.entity match { 41 | case HttpEntity.Strict(_, data) => logger.error(s"${e.response.status.value} ${data.utf8String}") 42 | case complexEntity => logger.error(s"${e.response.status} ${e.response.entity}") 43 | } 44 | complete(e.response) 45 | } 46 | 47 | private[this] val requestToken: Directive1[Option[String]] = { 48 | val TokenPattern = "Token token=(.+)".r 49 | optionalHeaderValueByName("Authorization").map { 50 | case Some(TokenPattern(token)) => Some(token) 51 | case _ => None 52 | } 53 | } 54 | 55 | private[this] def operationPath(operationFullName: String): Array[String] = 56 | operationFullName.split('.') 57 | 58 | private[this] def operationName(operationFullName: String, methodMetaData: MethodMetaData): String = 59 | methodMetaData.operationType.name.getOrElse(operationPath(operationFullName).last) 60 | 61 | private[this] def autowireRequest(operationFullName: String, args: Map[String, Json]): autowire.Core.Request[Json] = 62 | autowire.Core.Request(path = operationPath(operationFullName), args = args) 63 | 64 | private[this] def autowireRequestRoute(operationFullName: String, args: Map[String, Json]): Route = 65 | Either.catchNonFatal(routes(autowireRequest(operationFullName, args))) 66 | .fold(handleUnwrapErrors, result => complete(result)) 67 | 68 | private[this] def autowireRequestRouteWithToken(operationFullName: String, args: Map[String, Json], headers: Map[String, Json]): Route = 69 | requestToken(token => autowireRequestRoute(operationFullName, args ++ token.map(tokenAsArg) ++ Some(headersAsArg(headers)))) 70 | 71 | private[this] def routePathPrefix(operationFullName: String, methodMetaData: MethodMetaData): Directive0 = 72 | pathPrefix(operationName(operationFullName, methodMetaData)) 73 | 74 | private[this] def loggingClientIP: Directive0 = extractClientIP.map { ip => logger.debug(s"client ip: ${ip}") } 75 | 76 | //Generates GET requests 77 | private[this] def query(operationFullName: String, methodMetaData: MethodMetaData): Route = 78 | loggingClientIP { 79 | (routePathPrefix(operationFullName, methodMetaData) & pathEnd & get & parameterMap) { params => 80 | headersDirective { headers => 81 | val args = params.mapValues(parseJsonObjectOrString) 82 | autowireRequestRouteWithToken(operationFullName, args, headers) 83 | } 84 | } 85 | } 86 | 87 | private[this] def headersDirective: Directive1[Map[String, Json]] = 88 | extract(_.request.headers).map(_.map(header => header.name -> Json.fromString(header.value)).toMap) 89 | 90 | //Generates POST requests 91 | private[this] def command(operationFullName: String, methodMetaData: MethodMetaData): Route = 92 | loggingClientIP { 93 | (routePathPrefix(operationFullName, methodMetaData) & pathEnd & post & entity(as[JsonObject])) { request => 94 | headersDirective { headers => autowireRequestRouteWithToken(operationFullName, request.toMap, headers) } 95 | } 96 | } 97 | 98 | private[this] def parseJsonObject(s: String): Either[ParsingFailure, Json] = { 99 | val failure = ParsingFailure("The parsed Json is not an object", new Exception()) 100 | parse(s).ensure(failure)(_.isObject) 101 | } 102 | 103 | private[this] def parseJsonObjectOrString(s: String): Json = 104 | parseJsonObject(s).getOrElse(Json.fromString(s)) 105 | 106 | private[this] def tokenAsArg(token: String): (String, Json) = 107 | "token" -> Json.obj("token" -> Json.fromString(token)) 108 | 109 | private[this] def headersAsArg(headersMap: Map[String, Json]): (String, Json) = 110 | "parameters" -> Json.obj("parameters" -> Json.fromFields(headersMap)) 111 | } 112 | -------------------------------------------------------------------------------- /docs/src/main/tut/step-by-step-example.md: -------------------------------------------------------------------------------- 1 | ## Step by Step Example 2 | 3 | In this example we will build an api to store and retrieve users. 4 | 5 | ### 1 - Model and Controller Interface 6 | 7 | First, let's start by defining the interface of the API controller. We want the API to support the two following 8 | operations: 9 | 10 | 1. The client should be able to insert a new user by specifying `id` and `name` 11 | 2. The client should be able to retrieve a user by `id` 12 | 13 | The following snippet defines a model for `User` and an interface that follows this specification. 14 | 15 | ```tut:silent 16 | import scala.concurrent.Future 17 | import wiro.annotation._ 18 | 19 | // Models definition 20 | case class User(name: String) 21 | 22 | // Error messages 23 | case class Error(msg: String) 24 | case class UserNotFoundError(msg: String) 25 | 26 | @path("users") 27 | trait UsersApi { 28 | 29 | @query 30 | def getUser( 31 | id: Int 32 | ): Future[Either[UserNotFoundError, User]] 33 | 34 | @command 35 | def insertUser( 36 | id: Int, 37 | name: String 38 | ): Future[Either[Error, User]] 39 | } 40 | ``` 41 | * Use the `@query` and `@command` annotations to handle `GET` and `POST` requests respectively. 42 | * Return types must be `Future` of `Either` like above 43 | 44 | ### 2 - Controller implementation 45 | 46 | Now that we have the API interface, we need to implement it. Let's add the following implementation 47 | inside the `controllers` object: 48 | 49 | ```tut:silent 50 | val users = collection.mutable.Map.empty[Int, User] // Users DB 51 | 52 | // API implementation 53 | class UsersApiImpl(implicit ec: ExecutionContext) extends UsersApi { 54 | 55 | override def getUser( 56 | id: Int 57 | ): Future[Either[UserNotFoundError, User]] = { 58 | users.get(id) match { 59 | case Some(user) => Future(Right(user)) 60 | case None => Future(Left(UserNotFoundError("User not found"))) 61 | } 62 | } 63 | 64 | override def insertUser( 65 | id: Int, 66 | name: String 67 | ): Future[Either[Error, User]] = { 68 | val newUser = User(name) 69 | users(id) = newUser 70 | Future(Right(newUser)) 71 | } 72 | } 73 | 74 | ``` 75 | ### 3 - Serialization and deserialization 76 | 77 | Requests and responses composed only by standard types can be serialized and deserialized automatically by [circe](https://github.com/circe/circe), thanks to the following import: 78 | 79 | ```tut:silent 80 | import io.circe.generic.auto._ 81 | ``` 82 | 83 | We need, however, to specify how we want the errors that we defined to be converted into HTTP responses. To do it, it is sufficient to define the corresponding implicits, like we do in the following snippet: 84 | 85 | ```tut:silent 86 | import wiro.server.akkaHttp.ToHttpResponse 87 | import wiro.server.akkaHttp.FailSupport._ 88 | 89 | import akka.http.scaladsl.model.{ HttpResponse, StatusCodes, ContentType, HttpEntity} 90 | import akka.http.scaladsl.model.MediaTypes 91 | 92 | import io.circe.syntax._ 93 | implicit def notFoundToResponse = new ToHttpResponse[UserNotFoundError] { 94 | def response(error: UserNotFoundError) = HttpResponse( 95 | status = StatusCodes.NotFound, 96 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), error.asJson.noSpaces) 97 | ) 98 | } 99 | 100 | implicit def errorToResponse = new ToHttpResponse[Error] { 101 | def response(error: Error) = HttpResponse( 102 | status = StatusCodes.InternalServerError, 103 | entity = HttpEntity(ContentType(MediaTypes.`application/json`), error.asJson.noSpaces) 104 | ) 105 | } 106 | ``` 107 | 108 | ### 4 - Router creation 109 | 110 | Now we have everything we need to instance and start the router: 111 | 112 | ```tut:silent 113 | import akka.actor.ActorSystem 114 | import akka.stream.ActorMaterializer 115 | 116 | import wiro.Config 117 | import wiro.server.akkaHttp._ 118 | 119 | object UserServer extends App with RouterDerivationModule { 120 | implicit val system = ActorSystem() 121 | implicit val materializer = ActorMaterializer() 122 | implicit val ec = system.dispatcher 123 | 124 | val usersRouter = deriveRouter[UsersApi](new UsersApiImpl) 125 | 126 | val rpcServer = new HttpRPCServer( 127 | config = Config("localhost", 8080), 128 | routers = List(usersRouter) 129 | ) 130 | } 131 | ``` 132 | 133 | ### 5 - Requests examples 134 | 135 | Inserting a user: 136 | 137 | ```bash 138 | curl -XPOST 'http://localhost:8080/users/insertUser' \ 139 | -d '{"id":0, "name":"Pippo"}' \ 140 | -H "Content-Type: application/json" 141 | ``` 142 | 143 | `>> {"name":"Pippo"}` 144 | 145 | Getting a user: 146 | ```bash 147 | curl -XGET 'http://localhost:8080/users/getUser?id=0' 148 | ``` 149 | 150 | `>> {"name":"Pippo"}` 151 | 152 | ### 6 - Wiro client 153 | 154 | With wiro you can also create clients and perform requests: 155 | 156 | ```tut:silent 157 | import wiro.client.akkaHttp._ 158 | import autowire._ 159 | 160 | object UserClient extends App with ClientDerivationModule { 161 | import FailSupport._ 162 | 163 | val config = Config("localhost", 8080) 164 | 165 | implicit val system = ActorSystem() 166 | implicit val materializer = ActorMaterializer() 167 | implicit val ec = system.dispatcher 168 | 169 | val rpcClient = new RPCClient(config, deriveClientContext[UsersApi]) 170 | rpcClient[UsersApi].insertUser(0, "Pippo").call() map (println(_)) 171 | } 172 | ``` 173 | 174 | ### 7 - Testing 175 | 176 | To write tests for the router you can use the Akka [Route Testkit](http://doc.akka.io/docs/akka-http/current/scala/http/routing-dsl/testkit.html). The router to be tested can be extracted from the object that we defined above, as follows: 177 | 178 | ```tut:silent 179 | import org.scalatest.{ Matchers, FlatSpec } 180 | import akka.http.scaladsl.testkit.ScalatestRouteTest 181 | import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ 182 | 183 | class UserSpec extends FlatSpec with Matchers with ScalatestRouteTest with RouterDerivationModule { 184 | val route = deriveRouter[UsersApi](new UsersApiImpl).buildRoute 185 | 186 | it should "get a user" in { 187 | Get("/users/getUser?id=0") ~> route ~> check { 188 | responseAs[User].name shouldBe "Pippo" 189 | } 190 | } 191 | } 192 | ``` 193 | -------------------------------------------------------------------------------- /serverAkkaHttp/src/test/scala/AppSpecs.scala: -------------------------------------------------------------------------------- 1 | 2 | package wiro 3 | 4 | import akka.http.scaladsl.model._ 5 | import akka.http.scaladsl.model.headers.HttpChallenge 6 | import akka.http.scaladsl.model.HttpMethods._ 7 | import akka.http.scaladsl.model.StatusCodes._ 8 | import akka.http.scaladsl.server.{ AuthenticationFailedRejection, MethodRejection } 9 | import akka.http.scaladsl.server.Directives._ 10 | import akka.http.scaladsl.testkit.ScalatestRouteTest 11 | 12 | import akka.util.ByteString 13 | 14 | import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport._ 15 | 16 | import io.circe.Json 17 | import io.circe.generic.auto._ 18 | 19 | import org.scalatest.{ Matchers, WordSpec } 20 | import org.scalatest.time.{ Millis, Seconds, Span } 21 | import org.scalatest.concurrent.ScalaFutures 22 | 23 | import wiro.TestController._ 24 | 25 | class WiroSpec extends WordSpec with Matchers with ScalatestRouteTest with ScalaFutures { 26 | 27 | implicit val defaultPatience = 28 | PatienceConfig(timeout = Span(2, Seconds), interval = Span(50, Millis)) 29 | 30 | private[this] def jsonEntity(data: ByteString) = HttpEntity( 31 | contentType = MediaTypes.`application/json`, 32 | data = data 33 | ) 34 | 35 | "A POST request" when { 36 | "it's right" should { 37 | "return 200 and content" in { 38 | val data = ByteString( 39 | s""" 40 | |{ 41 | | "id": 1, 42 | | "user": { 43 | | "id": 1, 44 | | "username": "foo" 45 | | } 46 | |} 47 | """.stripMargin) 48 | 49 | Post("/user/update", jsonEntity(data)) ~> userRouter.buildRoute ~> check { 50 | status should be (OK) 51 | responseAs[Ok] should be (Ok("update")) 52 | } 53 | } 54 | } 55 | 56 | "points to route that includes the name of another route" should { 57 | "invoke the correct path" in { 58 | val data = ByteString( 59 | s""" 60 | |{ 61 | | "id": 1, 62 | | "user": { 63 | | "id": 1, 64 | | "username": "foo" 65 | | } 66 | |} 67 | """.stripMargin) 68 | 69 | Post("/user/updateCommand", jsonEntity(data)) ~> userRouter.buildRoute ~> check { 70 | status should be (OK) 71 | responseAs[Ok] should be (Ok("updateCommand")) 72 | } 73 | } 74 | } 75 | 76 | "it's left" should { 77 | "return provided error" in { 78 | val data = ByteString( 79 | s""" 80 | |{ 81 | | "id": 2, 82 | | "user": { 83 | | "id": 2, 84 | | "username": "foo" 85 | | } 86 | |} 87 | """.stripMargin) 88 | 89 | Post("/user/update", jsonEntity(data)) ~> userRouter.buildRoute ~> check { 90 | status should be (NotFound) 91 | } 92 | } 93 | } 94 | 95 | "operation is overridden" should { 96 | "overridden route should be used" in { 97 | val data = ByteString( 98 | s""" 99 | |{ 100 | | "id": 2, 101 | | "user": { 102 | | "id": 2, 103 | | "username": "foo" 104 | | } 105 | |} 106 | """.stripMargin) 107 | 108 | Post("/user/insert", jsonEntity(data)) ~> userRouter.buildRoute ~> check { 109 | status should be (OK) 110 | } 111 | } 112 | } 113 | 114 | "has unsuitable body" should { 115 | "return 422" in { 116 | val data = ByteString( 117 | s""" 118 | |{ 119 | | "id": "foo", 120 | | "user": { 121 | | "username": "foo", 122 | | "id": 1 123 | | } 124 | |} 125 | """.stripMargin) 126 | 127 | Post("/user/update", jsonEntity(data)) ~> userRouter.buildRoute ~> check { 128 | status should be (UnprocessableEntity) 129 | } 130 | } 131 | } 132 | 133 | "has unsuitable body in nested resource" should { 134 | "return 422" in { 135 | val data = ByteString( 136 | s""" 137 | |{ 138 | | "id": 1, 139 | | "user": { 140 | | "username": "foo" 141 | | } 142 | |} 143 | """.stripMargin) 144 | 145 | Post("/user/update", jsonEntity(data)) ~> userRouter.buildRoute ~> check { 146 | status should be (UnprocessableEntity) 147 | } 148 | } 149 | } 150 | 151 | "HTTP method is wrong" should { 152 | "return method is missing when GET" in { 153 | Get("/user/update?id=1") ~> userRouter.buildRoute ~> check { 154 | rejections shouldEqual List(MethodRejection(POST)) 155 | } 156 | } 157 | 158 | "return method is missing when DELETE" in { 159 | Delete("/user/update?id=1") ~> userRouter.buildRoute ~> check { 160 | rejections shouldEqual List(MethodRejection(POST)) 161 | } 162 | } 163 | 164 | "return method is missing when PUT" in { 165 | Put("/user/update?id=1") ~> userRouter.buildRoute ~> check { 166 | rejections shouldEqual List(MethodRejection(POST)) 167 | } 168 | } 169 | } 170 | 171 | "operation doesn't exist" should { 172 | "return 405" in { 173 | val data = ByteString( 174 | s""" 175 | |{ 176 | | "id": 1, 177 | | "user": { 178 | | "username": "foo" 179 | | } 180 | |} 181 | """.stripMargin) 182 | 183 | Post("/user/updat", jsonEntity(data)) ~> userRouter.buildRoute ~> check { 184 | rejections shouldEqual Nil 185 | } 186 | } 187 | } 188 | } 189 | 190 | "A GET request" when { 191 | "it's right" should { 192 | "return 200 and content" in { 193 | Get("/user/read?id=1") ~> userRouter.buildRoute ~> check { 194 | status should be (OK) 195 | responseAs[User] should be (User(1, "read")) 196 | } 197 | } 198 | } 199 | 200 | "contains query params with integer as only argument" should { 201 | "return 200 and content" in { 202 | Get("/user/readString?id=1") ~> userRouter.buildRoute ~> check { 203 | status should be (OK) 204 | responseAs[User] should be (User(1, "read")) 205 | } 206 | } 207 | } 208 | 209 | "it's authenticated" should { 210 | "block user without proper token" in { 211 | Get("/user/nobodyCannaCrossIt") ~> userRouter.buildRoute ~> check { 212 | status should be (StatusCodes.Unauthorized) 213 | } 214 | } 215 | 216 | "not block user having proper token" in { 217 | Get("/user/nobodyCannaCrossIt") ~> addHeader("Authorization", "Token token=bus") ~> userRouter.buildRoute ~> check { 218 | status should be (OK) 219 | responseAs[Ok] should be (Ok("di bus can swim")) 220 | } 221 | } 222 | } 223 | 224 | "has headers" should { 225 | "allow the user to read them" in { 226 | val headerName = "header" 227 | val headerContent = "content" 228 | Get("/user/inLoveWithMyHeaders") ~> addHeader(headerName, headerContent) ~> userRouter.buildRoute ~> check { 229 | status should be (OK) 230 | responseAs[OperationParameters].parameters should contain (headerName -> headerContent) 231 | } 232 | } 233 | } 234 | 235 | "points to route that includes the name of another route" should { 236 | "invoke the correct path" in { 237 | Get("/user/readQuery?id=1") ~> userRouter.buildRoute ~> check { 238 | status should be (OK) 239 | responseAs[User] should be (User(1, "readQuery")) 240 | } 241 | } 242 | } 243 | 244 | "it's left" should { 245 | "return provided error" in { 246 | Get("/user/read?id=2") ~> userRouter.buildRoute ~> check { 247 | status should be (NotFound) 248 | } 249 | } 250 | } 251 | 252 | "operation is overridden" should { 253 | "use overridden route should be used" in { 254 | Get("/user/number") ~> userRouter.buildRoute ~> check { 255 | status should be (OK) 256 | } 257 | } 258 | } 259 | 260 | "HTTP method is wrong" should { 261 | "return method is missing when POST" in { 262 | val data = ByteString( 263 | s""" 264 | |{ 265 | | "id": 1 266 | |} 267 | """.stripMargin) 268 | 269 | Post("/user/read", jsonEntity(data)) ~> userRouter.buildRoute ~> check { 270 | rejections shouldEqual List(MethodRejection(GET)) 271 | } 272 | } 273 | 274 | "return method is missing when DELETE" in { 275 | Delete("/user/read") ~> userRouter.buildRoute ~> check { 276 | rejections shouldEqual List(MethodRejection(GET)) 277 | } 278 | } 279 | 280 | "return method is missing when PUT" in { 281 | Put("/user/read") ~> userRouter.buildRoute ~> check { 282 | rejections shouldEqual List(MethodRejection(GET)) 283 | } 284 | } 285 | } 286 | 287 | "operation doesn't exist" should { 288 | "return be rejected" in { 289 | Get("/user/rea") ~> userRouter.buildRoute ~> check { 290 | rejections shouldEqual Nil 291 | } 292 | } 293 | } 294 | } 295 | } 296 | --------------------------------------------------------------------------------