From 8d5cbc2610ccf712f678e2938cc12f3ce1ef3cf3 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 27 Jun 2018 14:11:38 +0200 Subject: [PATCH 01/91] @composite parameters support in RPC metadata --- .../avsystem/commons/rpc/rpcAnnotations.scala | 2 + .../com/avsystem/commons/rest/RawRest.scala | 110 +++++++++++++ .../com/avsystem/commons/rest/rest.scala | 66 ++++++++ .../com/avsystem/commons/rpc/NewRawRpc.scala | 52 +++--- .../commons/macros/MacroCommons.scala | 11 +- .../commons/macros/rpc/RpcMacros.scala | 7 +- .../commons/macros/rpc/RpcMetadatas.scala | 150 +++++++++++++----- 7 files changed, 328 insertions(+), 70 deletions(-) create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala index c18905af6..de69e5939 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala @@ -149,6 +149,8 @@ final class optional extends RpcArity */ final class multi extends RpcArity +final class composite extends RawParamAnnotation + /** * Base trait for [[verbatim]] and [[encoded]]. These annotations can be applied either on a raw method or * raw parameter in order to specify how matching real method results or matching real parameter values are encoded diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala new file mode 100644 index 000000000..bdf8ed8d1 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -0,0 +1,110 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.rpc.{RawRpcCompanion, encoded, methodTag, multi, paramTag, tagged} + +import scala.collection.immutable.ListMap + +@methodTag[RestMethodTag, Sub] +@paramTag[RestParamTag, BodyParam] +trait RawRest { + @multi + @tagged[Sub] + @paramTag[RestParamTag, Path] + def sub(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue] + ): RawRest + + @multi + @tagged[GET] + def get(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] + ): Future[RestResponse] + + @multi + @tagged[GET] + def getSingle(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @encoded @tagged[Body] body: BodyValue + ): Future[RestResponse] + + @multi + @tagged[POST] + def post(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] + ): Future[RestResponse] + + @multi + @tagged[POST] + def postSingle(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @encoded @tagged[Body] body: BodyValue + ): Future[RestResponse] + + @multi + @tagged[PATCH] + def patch(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] + ): Future[RestResponse] + + @multi + @tagged[PATCH] + def patchSingle(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @encoded @tagged[Body] body: BodyValue + ): Future[RestResponse] + + @multi + @tagged[PUT] + def put(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] + ): Future[RestResponse] + + @multi + @tagged[PUT] + def putSingle(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @encoded @tagged[Body] body: BodyValue + ): Future[RestResponse] + + @multi + @tagged[DELETE] + def delete(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] + ): Future[RestResponse] + + @multi + @tagged[DELETE] + def deleteSingle(name: String)( + @multi @tagged[Path] pathParams: List[PathValue], + @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], + @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], + @encoded @tagged[Body] body: BodyValue + ): Future[RestResponse] +} +object RawRest extends RawRpcCompanion[RawRest] diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala new file mode 100644 index 000000000..1ed3e1f8d --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -0,0 +1,66 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.rpc.RpcTag + +import scala.collection.immutable.ListMap + +sealed trait RestMethodTag extends RpcTag +final class GET extends RestMethodTag +final class POST extends RestMethodTag +final class PATCH extends RestMethodTag +final class PUT extends RestMethodTag +final class DELETE extends RestMethodTag +sealed trait Sub extends RestMethodTag + +sealed trait RestParamTag extends RpcTag +final class Header extends RestParamTag +final class Path extends RestParamTag +final class QueryParam extends RestParamTag +final class BodyParam extends RestParamTag +final class Body extends RestParamTag + +sealed trait RestValue extends Any { + def value: String +} +case class PathValue(value: String) extends AnyVal with RestValue +case class HeaderValue(value: String) extends AnyVal with RestValue +case class QueryParamValue(value: String) extends AnyVal with RestValue +case class BodyValue(value: String) extends AnyVal with RestValue +object RestValue { + // AsReal, AsRaw, itp +} + +sealed abstract class RestMethod { + def name: String + def singleBody: Boolean +} +object RestMethod { + case class Get(name: String, singleBody: Boolean) extends RestMethod + case class Put(name: String, singleBody: Boolean) extends RestMethod + case class Post(name: String, singleBody: Boolean) extends RestMethod + case class Patch(name: String, singleBody: Boolean) extends RestMethod + case class Delete(name: String, singleBody: Boolean) extends RestMethod +} + +case class RestHeaders( + path: List[PathValue], + headers: List[(String, HeaderValue)], + query: List[(String, QueryParamValue)] +) { + def append(name: String, + pathParams: List[PathValue], + headerParams: ListMap[String, HeaderValue], + queryParams: ListMap[String, QueryParamValue] + ): RestHeaders = + RestHeaders(path ++ (PathValue(name) :: pathParams), headers ++ headerParams, query ++ queryParams) +} +case class RestRequest(method: RestMethod, headers: RestHeaders, body: BodyValue) +case class RestResponse(code: Int, body: BodyValue) + +trait AbstractRawRest extends RawRest { + def headers: RestHeaders + def withHeaders(headers: RestHeaders): AbstractRawRest + + def handle(request: RestRequest): Future[RestResponse] +} diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala index 73dfd6bee..0f55e8db2 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala @@ -70,11 +70,15 @@ object Utils { import com.avsystem.commons.rpc.Utils._ +case class DoSomethings( + doSomething: DoSomethingSignature, + @optional doSomethingElse: Opt[DoSomethingSignature], +) + @methodTag[RestMethod, RestMethod] @paramTag[DummyParamTag, untagged] case class NewRpcMetadata[T: TypeName]( - doSomething: DoSomethingSignature, - @optional doSomethingElse: Opt[DoSomethingSignature], + @composite doSomethings: DoSomethings, @multi @verbatim procedures: Map[String, FireMetadata], @multi functions: Map[String, CallMetadata[_]], @multi getters: Map[String, GetterMetadata[_]], @@ -84,7 +88,7 @@ case class NewRpcMetadata[T: TypeName]( def repr(open: List[NewRpcMetadata[_]]): String = if (open.contains(this)) "\n" else { val membersStr = - s"DO SOMETHING ELSE: ${doSomethingElse.nonEmpty}\n" + + s"DO SOMETHING ELSE: ${doSomethings.doSomethingElse.nonEmpty}\n" + procedures.iterator.map({ case (n, v) => s"$n -> ${v.repr}" }).mkString("PROCEDURES:\n", "\n", "") + "\n" + functions.iterator.map({ case (n, v) => s"$n -> ${v.repr}" }).mkString("FUNCTIONS:\n", "\n", "") + "\n" + posters.iterator.map({ case (n, v) => s"$n -> ${v.repr}" }).mkString("POSTERS:\n", "\n", "") + "\n" + @@ -101,18 +105,15 @@ case class DoSomethingSignature(arg: ArgMetadata) extends TypedMetadata[String] case class ArgMetadata() extends TypedMetadata[Double] trait MethodMetadata[T] { - @reifyName def name: String - @reifyName(rpcName = true) def rpcName: String + @composite def nameInfo: NameInfo def typeName: TypeName[T] - def basicRepr: String = { - val rpcNameStr = if (rpcName != name) s"<$rpcName>" else "" - s"def $name$rpcNameStr: ${typeName.name}" - } + def basicRepr: String = + s"def ${nameInfo.repr}: ${typeName.name}" } case class FireMetadata( - name: String, rpcName: String, + nameInfo: NameInfo, @optional @auxiliary ajdi: Opt[ParameterMetadata[Int]], @multi args: Map[String, ParameterMetadata[_]] ) extends TypedMetadata[Unit] with MethodMetadata[Unit] { @@ -124,7 +125,7 @@ case class FireMetadata( } case class CallMetadata[T]( - name: String, rpcName: String, + nameInfo: NameInfo, @tagged[renamed] @multi renamed: Map[String, ParameterMetadata[_]], @multi args: Map[String, ParameterMetadata[_]] )(implicit val typeName: TypeName[T]) @@ -135,20 +136,26 @@ case class CallMetadata[T]( args.iterator.map({ case (n, pm) => s"$n -> ${pm.repr}" }).mkString("ARGS:\n", "\n", "").indent(" ") } -case class GetterMetadata[T]( - name: String, rpcName: String, +case class GetterParams( @encoded head: ParameterMetadata[_], @multi tail: List[ParameterMetadata[_]], +) { + def repr: String = (head :: tail).map(_.repr).mkString("ARGS:\n", "\n", "") +} + +case class GetterMetadata[T]( + nameInfo: NameInfo, + @composite params: GetterParams, @infer @checked resultMetadata: NewRpcMetadata.Lazy[T] )(implicit val typeName: TypeName[T]) extends TypedMetadata[T] with MethodMetadata[T] { def repr(open: List[NewRpcMetadata[_]]): String = s"$basicRepr\n" + - (head :: tail).map(_.repr).mkString("ARGS:\n", "\n", "").indent(" ") + "\n" + + params.repr.indent(" ") + "\n" + s"RESULT: ${resultMetadata.value.repr(open)}".indent(" ") } case class PostMetadata[T: TypeName]( - name: String, rpcName: String, + nameInfo: NameInfo, @reifyAnnot post: POST, @tagged[header] @multi @verbatim headers: Vector[ParameterMetadata[String]], @multi body: MLinkedHashMap[String, ParameterMetadata[_]] @@ -161,7 +168,7 @@ case class PostMetadata[T: TypeName]( } case class PrefixMetadata[T]( - name: String, rpcName: String, + nameInfo: NameInfo, @infer @checked resultMetadata: NewRpcMetadata.Lazy[T], @infer typeName: TypeName[T] ) extends TypedMetadata[T] with MethodMetadata[T] { @@ -171,8 +178,7 @@ case class PrefixMetadata[T]( } case class ParameterMetadata[T: TypeName]( - @reifyName name: String, - @reifyName(rpcName = true) rpcName: String, + @composite nameInfo: NameInfo, @reifyPosition pos: ParamPosition, @reifyFlags flags: ParamFlags, @reifyAnnot @multi metas: List[suchMeta], @@ -180,9 +186,15 @@ case class ParameterMetadata[T: TypeName]( ) extends TypedMetadata[T] { def repr: String = { val flagsStr = if (flags != ParamFlags.Empty) s"[$flags]" else "" - val rpcNameStr = if (rpcName != name) s"<$rpcName>" else "" val posStr = s"${pos.index}:${pos.indexOfList}:${pos.indexInList}:${pos.indexInRaw}" val metasStr = if (metas.nonEmpty) metas.mkString(s",metas=", ",", "") else "" - s"$flagsStr$name$rpcNameStr@$posStr: ${TypeName.get[T]} suchMeta=$suchMeta$metasStr" + s"$flagsStr${nameInfo.repr}@$posStr: ${TypeName.get[T]} suchMeta=$suchMeta$metasStr" } } + +case class NameInfo( + @reifyName name: String, + @reifyName(rpcName = true) rpcName: String, +) { + def repr: String = name + (if (rpcName != name) s"<$rpcName>" else "") +} diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala index 35c1fec6e..c7d60acbe 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala @@ -186,7 +186,7 @@ trait MacroCommons { bundle => // Replace references to companion object being constructed with casted reference to lambda parameter // of function wrapped by `MacroGenerated` class. This is all to workaround overzealous Scala validation of - // self-reference being passed to super constructor parameter. + // self-reference being passed to super constructor parameter (https://github.com/scala/bug/issues/7666) def replaceCompanion(typedTree: Tree): Tree = { val symToCheck = typedTree match { case This(_) => typedTree.symbol.asClass.module @@ -687,6 +687,11 @@ trait MacroCommons { bundle => } } + /** + * @param apply case class constructor or companion object's apply method + * @param unapply companion object'a unapply method or [[NoSymbol]] for case class with more than 22 fields + * @param params parameters with trees evaluating to default values (or [[EmptyTree]]s) + */ case class ApplyUnapply(apply: Symbol, unapply: Symbol, params: List[(TermSymbol, Tree)]) def applyUnapplyFor(tpe: Type): Option[ApplyUnapply] = @@ -704,8 +709,8 @@ trait MacroCommons { bundle => } else EmptyTree - def paramsWithDefaults(methodSig: Type) = - methodSig.paramLists.head.zipWithIndex.map({ case (p, i) => (p.asTerm, defaultValueFor(p, i)) }) + def paramsWithDefaults(methodSig: Type): List[(TermSymbol, Tree)] = + methodSig.paramLists.head.zipWithIndex.map { case (p, i) => (p.asTerm, defaultValueFor(p, i)) } val applyUnapplyPairs = for { apply <- alternatives(typedCompanion.tpe.member(TermName("apply"))) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index c357e8614..eabe95a45 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -27,6 +27,7 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo val SingleArityAT: Type = getType(tq"$RpcPackage.single") val OptionalArityAT: Type = getType(tq"$RpcPackage.optional") val MultiArityAT: Type = getType(tq"$RpcPackage.multi") + val CompositeAnnotAT: Type = getType(tq"$RpcPackage.composite") val RpcEncodingAT: Type = getType(tq"$RpcPackage.RpcEncoding") val VerbatimAT: Type = getType(tq"$RpcPackage.verbatim") val AuxiliaryAT: Type = getType(tq"$RpcPackage.auxiliary") @@ -135,7 +136,7 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) case t => t } - val constructor = new RpcMetadataConstructor(metadataTpe) + val constructor = new RpcMetadataConstructor(metadataTpe, None) // separate object for cached implicits so that lazy vals are members instead of local variables val depsObj = c.freshName(TermName("deps")) val selfName = c.freshName(TermName("self")) @@ -146,7 +147,7 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) val lazyMetadataTpe = getType(tq"$comp.Lazy[${realRpc.tpe}]") val lazySelfName = c.freshName(TermName("lazySelf")) registerImplicit(lazyMetadataTpe, lazySelfName) - val tree = constructor.materializeFor(realRpc) + val tree = constructor.materializeFor(realRpc, constructor.methodMappings(realRpc)) q""" object $depsObj { @@ -159,7 +160,7 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) """ case None => - val tree = constructor.materializeFor(realRpc) + val tree = constructor.materializeFor(realRpc, constructor.methodMappings(realRpc)) q""" object $depsObj { ..$cachedImplicitDeclarations diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index fd959640f..a5a8cff91 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -35,6 +35,28 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def description = s"$shortDescription $nameStr of ${owner.description}" } + sealed abstract class CompositeMetadataParam[Real <: RealRpcSymbol]( + owner: MetadataConstructor[Real], symbol: Symbol) extends MetadataParam[Real](owner, symbol) { + val constructor: MetadataConstructor[Real] + + override def description: String = s"${super.description} at ${owner.description}" + } + + class RpcCompositeParam(override val owner: RpcMetadataConstructor, symbol: Symbol) + extends CompositeMetadataParam[RealRpcTrait](owner, symbol) { + val constructor: RpcMetadataConstructor = new RpcMetadataConstructor(actualType, Some(this)) + } + + class MethodCompositeParam(override val owner: MethodMetadataConstructor, symbol: Symbol) + extends CompositeMetadataParam[RealMethod](owner, symbol) { + val constructor: MethodMetadataConstructor = new MethodMetadataConstructor(actualType, Right(this)) + } + + class ParamCompositeParam(override val owner: ParamMetadataConstructor, symbol: Symbol) + extends CompositeMetadataParam[RealParam](owner, symbol) { + val constructor: ParamMetadataConstructor = new ParamMetadataConstructor(actualType, Right(this), owner.indexInRaw) + } + class MethodMetadataParam(owner: RpcMetadataConstructor, symbol: Symbol) extends MetadataParam[RealRpcTrait](owner, symbol) with RawRpcSymbol with ArityParam { @@ -59,15 +81,17 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def mappingFor(realMethod: RealMethod): Res[MethodMetadataMapping] = for { mdType <- actualMetadataType(arity.collectedType, realMethod, verbatimResult) - tree <- new MethodMetadataConstructor(mdType, this).tryMaterializeFor(realMethod) + constructor = new MethodMetadataConstructor(mdType, Left(this)) + paramMappings <- constructor.paramMappings(realMethod) + tree <- constructor.tryMaterializeFor(realMethod, paramMappings) } yield MethodMetadataMapping(realMethod, this, tree) } class ParamMetadataParam(owner: MethodMetadataConstructor, symbol: Symbol) extends MetadataParam[RealMethod](owner, symbol) with RawParamLike { - def baseTag: Type = owner.ownerParam.baseParamTag - def defaultTag: Type = owner.ownerParam.defaultParamTag + def baseTag: Type = owner.containingMethodParam.baseParamTag + def defaultTag: Type = owner.containingMethodParam.defaultParamTag def cannotMapClue = s"cannot map it to $shortDescription $nameStr of ${owner.ownerType}" @@ -78,7 +102,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => private def metadataTree(realParam: RealParam, indexInRaw: Int): Res[Tree] = { val result = for { mdType <- actualMetadataType(arity.collectedType, realParam, verbatim) - tree <- new ParamMetadataConstructor(mdType, this, indexInRaw).tryMaterializeFor(realParam) + tree <- new ParamMetadataConstructor(mdType, Left(this), indexInRaw).tryMaterializeFor(realParam) } yield tree result.mapFailure(msg => s"${realParam.problemStr}: $cannotMapClue: $msg") } @@ -123,19 +147,17 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case t => reportProblem(s"metadata param strategy $t is not allowed here") } + def createCompositeParam(paramSym: Symbol): CompositeMetadataParam[Real] def createDefaultParam(paramSym: Symbol): MetadataParam[Real] lazy val paramLists: List[List[MetadataParam[Real]]] = symbol.typeSignatureIn(ownerType).paramLists.map(_.map { ps => - findAnnotation(ps, MetadataParamStrategyType).map(createDirectParam(ps, _)) + if (findAnnotation(ps, CompositeAnnotAT).nonEmpty) + createCompositeParam(ps) + else findAnnotation(ps, MetadataParamStrategyType).map(createDirectParam(ps, _)) .getOrElse(if (ps.isImplicit) new ImplicitParam(this, ps) else createDefaultParam(ps)) }) - lazy val plainParams: List[DirectMetadataParam[Real]] = - paramLists.flatten.collect { - case dmp: DirectMetadataParam[Real] => dmp - } - def constructorCall(argDecls: List[Tree]): Tree = q""" ..$argDecls @@ -145,8 +167,11 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case class MethodMetadataMapping(realMethod: RealMethod, mdParam: MethodMetadataParam, tree: Tree) - class RpcMetadataConstructor(val ownerType: Type) - extends MetadataConstructor[RealRpcTrait](primaryConstructor(ownerType, None)) with RawRpcSymbol { + class RpcMetadataConstructor(val ownerType: Type, val atParam: Option[RpcCompositeParam]) + extends MetadataConstructor[RealRpcTrait](primaryConstructor(ownerType, atParam)) with RawRpcSymbol { + + override def description: String = + s"${super.description}${atParam.fold("")(p => s" at ${p.description}")}" def baseTag: Type = typeOf[Nothing] def defaultTag: Type = typeOf[Nothing] @@ -161,17 +186,22 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => .map(_.tpe.baseType(ParamTagAT.typeSymbol).typeArgs) .getOrElse(List(NothingTpe, NothingTpe)) - lazy val methodMdParams: List[MethodMetadataParam] = - paramLists.flatten.collect({ case mmp: MethodMetadataParam => mmp }) + lazy val methodMdParams: List[MethodMetadataParam] = paramLists.flatten.flatMap { + case mmp: MethodMetadataParam => List(mmp) + case rcp: RpcCompositeParam => rcp.constructor.methodMdParams + case _ => Nil + } - def createDefaultParam(paramSym: Symbol): MetadataParam[RealRpcTrait] = + def createDefaultParam(paramSym: Symbol): MethodMetadataParam = new MethodMetadataParam(this, paramSym) - def materializeFor(rpc: RealRpcTrait): Tree = { - val allMappings = collectMethodMappings(methodMdParams, "metadata parameters", rpc.realMethods)(_.mappingFor(_)) + def createCompositeParam(paramSym: Symbol): RpcCompositeParam = + new RpcCompositeParam(this, paramSym) - val mappingsByParam = allMappings.groupBy(_.mdParam) - mappingsByParam.foreach { case (mmp, mappings) => + def methodMappings(rpc: RealRpcTrait): Map[MethodMetadataParam, List[MethodMetadataMapping]] = { + val allMappings = collectMethodMappings(methodMdParams, "metadata parameters", rpc.realMethods)(_.mappingFor(_)) + val result = allMappings.groupBy(_.mdParam) + result.foreach { case (mmp, mappings) => mappings.groupBy(_.realMethod.rpcName).foreach { case (rpcName, MethodMetadataMapping(realMethod, _, _) :: tail) if tail.nonEmpty => realMethod.reportProblem(s"multiple RPCs named $rpcName map to metadata parameter ${mmp.nameStr}. " + @@ -179,11 +209,17 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case _ => } } + result + } + def materializeFor(rpc: RealRpcTrait, methodMappings: Map[MethodMetadataParam, List[MethodMetadataMapping]]): Tree = { val argDecls = paramLists.flatten.map { - case dmp: DirectMetadataParam[RealRpcTrait] => dmp.localValueDecl(dmp.materializeFor(rpc)) + case rcp: RpcCompositeParam => + rcp.localValueDecl(rcp.constructor.materializeFor(rpc, methodMappings)) + case dmp: DirectMetadataParam[RealRpcTrait] => + dmp.localValueDecl(dmp.materializeFor(rpc)) case mmp: MethodMetadataParam => mmp.localValueDecl { - val mappings = mappingsByParam.getOrElse(mmp, Nil) + val mappings = methodMappings.getOrElse(mmp, Nil) mmp.arity match { case RpcParamArity.Single(_) => mappings match { case Nil => abort(s"no real method found that would match ${mmp.description}") @@ -204,33 +240,54 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => } } - class MethodMetadataConstructor(val ownerType: Type, val ownerParam: MethodMetadataParam) - extends MetadataConstructor[RealMethod](primaryConstructor(ownerType, Some(ownerParam))) { + class MethodMetadataConstructor( + val ownerType: Type, + val atParam: Either[MethodMetadataParam, MethodCompositeParam] + ) extends MetadataConstructor[RealMethod]( + primaryConstructor(ownerType, Some(atParam.fold[RpcSymbol](identity, identity)))) { - lazy val paramMdParams: List[ParamMetadataParam] = - paramLists.flatten.collect({ case pmp: ParamMetadataParam => pmp }) + override def description: String = + s"${super.description} at ${atParam.fold(_.description, _.description)}" + + val containingMethodParam: MethodMetadataParam = + atParam.fold(identity, _.owner.containingMethodParam) - def createDefaultParam(paramSym: Symbol): MetadataParam[RealMethod] = + lazy val paramMdParams: List[ParamMetadataParam] = paramLists.flatten.flatMap { + case pmp: ParamMetadataParam => List(pmp) + case mcp: MethodCompositeParam => mcp.constructor.paramMdParams + case _ => Nil + } + + def createDefaultParam(paramSym: Symbol): ParamMetadataParam = new ParamMetadataParam(this, paramSym) - def tryMaterializeFor(realMethod: RealMethod): Res[Tree] = - for { - paramMappings <- collectParamMappings(paramMdParams, "metadata parameter", realMethod)( - (param, parser) => param.metadataFor(parser).map(t => (param, t))).map(_.toMap) - argDecls <- Res.traverse(paramLists.flatten) { - case dmp: DirectMetadataParam[RealMethod] => - dmp.tryMaterializeFor(realMethod).map(dmp.localValueDecl) - case pmp: ParamMetadataParam => - Ok(pmp.localValueDecl(paramMappings(pmp))) - } - } yield constructorCall(argDecls) + def createCompositeParam(paramSym: Symbol): MethodCompositeParam = + new MethodCompositeParam(this, paramSym) + + def paramMappings(realMethod: RealMethod): Res[Map[ParamMetadataParam, Tree]] = + collectParamMappings(paramMdParams, "metadata parameter", realMethod)( + (param, parser) => param.metadataFor(parser).map(t => (param, t))).map(_.toMap) + + def tryMaterializeFor(realMethod: RealMethod, paramMappings: Map[ParamMetadataParam, Tree]): Res[Tree] = + Res.traverse(paramLists.flatten) { + case cmp: MethodCompositeParam => + cmp.constructor.tryMaterializeFor(realMethod, paramMappings).map(cmp.localValueDecl) + case dmp: DirectMetadataParam[RealMethod] => + dmp.tryMaterializeFor(realMethod).map(dmp.localValueDecl) + case pmp: ParamMetadataParam => + Ok(pmp.localValueDecl(paramMappings(pmp))) + }.map(constructorCall) } - class ParamMetadataConstructor(val ownerType: Type, val ownerParam: ParamMetadataParam, val indexInRaw: Int) - extends MetadataConstructor[RealParam](primaryConstructor(ownerType, Some(ownerParam))) { + class ParamMetadataConstructor( + val ownerType: Type, + val atParam: Either[ParamMetadataParam, ParamCompositeParam], + val indexInRaw: Int + ) extends MetadataConstructor[RealParam]( + primaryConstructor(ownerType, Some(atParam.fold[RpcSymbol](identity, identity)))) { override def description: String = - s"${super.description} at ${ownerParam.description}" + s"${super.description} at ${atParam.fold(_.description, _.description)}" override def createDirectParam(paramSym: Symbol, annot: Annot): DirectMetadataParam[RealParam] = annot.tpe match { @@ -239,14 +296,19 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case _ => super.createDirectParam(paramSym, annot) } - def createDefaultParam(paramSym: Symbol): MetadataParam[RealParam] = + def createDefaultParam(paramSym: Symbol): UnknownParam[RealParam] = new UnknownParam(this, paramSym) - def materializeFor(param: RealParam): Tree = - constructorCall(plainParams.map(p => p.localValueDecl(p.materializeFor(param)))) + def createCompositeParam(paramSym: Symbol): ParamCompositeParam = + new ParamCompositeParam(this, paramSym) def tryMaterializeFor(param: RealParam): Res[Tree] = - Res.traverse(plainParams)(p => p.tryMaterializeFor(param).map(p.localValueDecl)).map(constructorCall) + Res.traverse(paramLists.flatten) { + case pcp: ParamCompositeParam => + pcp.constructor.tryMaterializeFor(param).map(pcp.localValueDecl) + case dmp: DirectMetadataParam[RealParam] => + dmp.tryMaterializeFor(param).map(dmp.localValueDecl) + }.map(constructorCall) } sealed abstract class DirectMetadataParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) From 77af6c5fb7bb24b700208626ba8709793a17d015 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 27 Jun 2018 17:14:40 +0200 Subject: [PATCH 02/91] @composite method parameters support in raw RPC traits --- .../avsystem/commons/rpc/rpcAnnotations.scala | 6 ++ .../com/avsystem/commons/rpc/NewRawRpc.scala | 10 ++- .../commons/rpc/NewRpcMetadataTest.scala | 2 +- .../commons/macros/rpc/RpcMappings.scala | 68 +++++++++++-------- .../commons/macros/rpc/RpcMetadatas.scala | 6 +- .../commons/macros/rpc/RpcSymbols.scala | 60 +++++++++++++--- 6 files changed, 111 insertions(+), 41 deletions(-) diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala index de69e5939..5667fa222 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala @@ -149,6 +149,12 @@ final class optional extends RpcArity */ final class multi extends RpcArity +/** + * Can be applied on raw method parameters or metadata parameters. When a parameter is annotated as `@composite`, + * the macro engine expects its type to be a class with public primary constructor. Then, it recursively inspects its + * constructor parameters and treats them as if they were direct parameters. This effectively groups multiple + * raw parameters or multiple metadata parameters into a single class. + */ final class composite extends RawParamAnnotation /** diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala index 0f55e8db2..8c44f37a2 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala @@ -29,6 +29,11 @@ case class POST() extends RestMethod case class GET() extends RestMethod case class PUT() extends RestMethod +case class HeadTail( + @encoded head: String, + @multi tail: List[String] +) + @methodTag[RestMethod, RestMethod] @paramTag[DummyParamTag, untagged] trait NewRawRpc { @@ -44,8 +49,7 @@ trait NewRawRpc { @tagged[renamed] @multi renamedArgs: => Map[String, String], @multi args: Map[String, String]): Future[String] - @multi def get(name: String)( - @encoded head: String, @multi tail: List[String]): NewRawRpc + @multi def get(name: String)(@composite ht: HeadTail): NewRawRpc @multi @tagged[POST] def post(name: String)( @@ -57,7 +61,7 @@ trait NewRawRpc { object NewRawRpc extends RawRpcCompanion[NewRawRpc] { override val implicits: this.type = this - implicit def AsRawRealFromGenCodec[T: GenCodec]: AsRawReal[String, T] = ??? + implicit def asRawRealFromGenCodec[T: GenCodec]: AsRawReal[String, T] = ??? implicit def futureAsRawRealFromGenCodec[T: GenCodec]: AsRawReal[Future[String], Future[T]] = ??? } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala index adf1e55ae..3e72ae90f 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala @@ -23,7 +23,7 @@ trait TestApi extends SomeBase { def postit(arg: String, bar: String, int: Int, @suchMeta(3, "c") foo: String): String } object TestApi { - implicit val AsRawReal: NewRawRpc.AsRawRealRpc[TestApi] = NewRawRpc.materializeAsRawReal[TestApi] + implicit val asRawReal: NewRawRpc.AsRawRealRpc[TestApi] = NewRawRpc.materializeAsRawReal[TestApi] implicit val metadata: NewRpcMetadata[TestApi] = NewRpcMetadata.materializeForRpc[TestApi] } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index 16bf49516..4433f85df 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -46,12 +46,12 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => result } - def collectParamMappings[R <: RawParamLike, M](raws: List[R], rawShortDesc: String, realMethod: RealMethod) + def collectParamMappings[R <: RealParamTarget, M](raws: List[R], rawShortDesc: String, realMethod: RealMethod) (createMapping: (R, ParamsParser) => Res[M]): Res[List[M]] = { val parser = new ParamsParser(realMethod) Res.traverse(raws)(createMapping(_, parser)).flatMap { result => - if (parser.remaining.isEmpty) Ok(result.reverse) + if (parser.remaining.isEmpty) Ok(result) else { val unmatched = parser.remaining.iterator.map(_.nameStr).mkString(",") Fail(s"no $rawShortDesc(s) were found that would match real parameter(s) $unmatched") @@ -68,7 +68,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def remaining: Seq[RealParam] = realParams.asScala - def extractSingle[B](raw: RawParamLike, matcher: RealParam => Res[B]): Res[B] = { + def extractSingle[B](raw: RealParamTarget, matcher: RealParam => Res[B]): Res[B] = { val it = realParams.listIterator() def loop(): Res[B] = if (it.hasNext) { @@ -79,11 +79,11 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } matcher(real) } else loop() - } else Fail(s"${raw.shortDescription} ${raw.nameStr} was not matched by real parameter") + } else Fail(s"${raw.shortDescription} ${raw.pathStr} was not matched by real parameter") loop() } - def extractOptional[B](raw: RawParamLike, matcher: RealParam => Res[B]): Option[B] = { + def extractOptional[B](raw: RealParamTarget, matcher: RealParam => Res[B]): Option[B] = { val it = realParams.listIterator() def loop(): Option[B] = if (it.hasNext) { @@ -99,7 +99,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => loop() } - def extractMulti[B](raw: RawParamLike, matcher: (RealParam, Int) => Res[B], named: Boolean): Res[List[B]] = { + def extractMulti[B](raw: RealParamTarget, matcher: (RealParam, Int) => Res[B], named: Boolean): Res[List[B]] = { val seenRpcNames = new mutable.HashSet[String] val it = realParams.listIterator() def loop(result: ListBuffer[B]): Res[List[B]] = @@ -132,39 +132,39 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def localValueDecl(body: Tree): Tree = realParam.localValueDecl(body) } object EncodedRealParam { - def create(rawParam: RawParam, realParam: RealParam): Res[EncodedRealParam] = + def create(rawParam: RawValueParam, realParam: RealParam): Res[EncodedRealParam] = RpcEncoding.forParam(rawParam, realParam).map(EncodedRealParam(realParam, _)) } sealed trait ParamMapping { - def rawParam: RawParam + def rawParam: RawValueParam def rawValueTree: Tree def realDecls: List[Tree] } object ParamMapping { - case class Single(rawParam: RawParam, realParam: EncodedRealParam) extends ParamMapping { + case class Single(rawParam: RawValueParam, realParam: EncodedRealParam) extends ParamMapping { def rawValueTree: Tree = realParam.rawValueTree def realDecls: List[Tree] = - List(realParam.localValueDecl(realParam.encoding.applyAsReal(rawParam.safeName))) + List(realParam.localValueDecl(realParam.encoding.applyAsReal(rawParam.safePath))) } - case class Optional(rawParam: RawParam, wrapped: Option[EncodedRealParam]) extends ParamMapping { + case class Optional(rawParam: RawValueParam, wrapped: Option[EncodedRealParam]) extends ParamMapping { def rawValueTree: Tree = rawParam.mkOptional(wrapped.map(_.rawValueTree)) def realDecls: List[Tree] = wrapped.toList.map { erp => val defaultValueTree = erp.realParam.defaultValueTree erp.realParam.localValueDecl(erp.encoding.foldWithAsReal( - rawParam.optionLike, rawParam.safeName, defaultValueTree)) + rawParam.optionLike, rawParam.safePath, defaultValueTree)) } } abstract class ListedMulti extends ParamMapping { protected def reals: List[EncodedRealParam] def rawValueTree: Tree = rawParam.mkMulti(reals.map(_.rawValueTree)) } - case class IterableMulti(rawParam: RawParam, reals: List[EncodedRealParam]) extends ListedMulti { + case class IterableMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ListedMulti { def realDecls: List[Tree] = { val itName = c.freshName(TermName("it")) - val itDecl = q"val $itName = ${rawParam.safeName}.iterator" + val itDecl = q"val $itName = ${rawParam.safePath}.iterator" itDecl :: reals.map { erp => val rp = erp.realParam if (rp.symbol.asTerm.isByNameParam) { @@ -176,26 +176,26 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } } } - case class IndexedMulti(rawParam: RawParam, reals: List[EncodedRealParam]) extends ListedMulti { + case class IndexedMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ListedMulti { def realDecls: List[Tree] = { reals.zipWithIndex.map { case (erp, idx) => val rp = erp.realParam erp.realParam.localValueDecl( q""" - ${erp.encoding.andThenAsReal(rawParam.safeName)} + ${erp.encoding.andThenAsReal(rawParam.safePath)} .applyOrElse($idx, (_: $IntCls) => ${rp.defaultValueTree}) """) } } } - case class NamedMulti(rawParam: RawParam, reals: List[EncodedRealParam]) extends ParamMapping { + case class NamedMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ParamMapping { def rawValueTree: Tree = rawParam.mkMulti(reals.map(erp => q"(${erp.realParam.rpcName}, ${erp.rawValueTree})")) def realDecls: List[Tree] = reals.map { erp => erp.realParam.localValueDecl( q""" - ${erp.encoding.andThenAsReal(rawParam.safeName)} + ${erp.encoding.andThenAsReal(rawParam.safePath)} .applyOrElse(${erp.realParam.rpcName}, (_: $StringCls) => ${erp.realParam.defaultValueTree}) """) } @@ -208,12 +208,12 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def applyAsRaw[T: Liftable](arg: T): Tree = q"$asRaw.asRaw($arg)" def applyAsReal[T: Liftable](arg: T): Tree = q"$asReal.asReal($arg)" - def foldWithAsReal(optionLike: TermName, opt: TermName, default: Tree): Tree = + def foldWithAsReal[T: Liftable](optionLike: TermName, opt: T, default: Tree): Tree = q"$optionLike.fold($opt, $default)($asReal.asReal(_))" - def andThenAsReal(func: TermName): Tree = q"$func.andThen($asReal.asReal(_))" + def andThenAsReal[T: Liftable](func: T): Tree = q"$func.andThen($asReal.asReal(_))" } object RpcEncoding { - def forParam(rawParam: RawParam, realParam: RealParam): Res[RpcEncoding] = { + def forParam(rawParam: RawValueParam, realParam: RealParam): Res[RpcEncoding] = { val encArgType = rawParam.arity.collectedType if (rawParam.verbatim) { if (realParam.actualType =:= encArgType) @@ -231,9 +231,9 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def asReal = q"$AsRealObj.identity[$tpe]" override def applyAsRaw[T: Liftable](arg: T): Tree = q"$arg" override def applyAsReal[T: Liftable](arg: T): Tree = q"$arg" - override def foldWithAsReal(optionLike: TermName, opt: TermName, default: Tree): Tree = + override def foldWithAsReal[T: Liftable](optionLike: TermName, opt: T, default: Tree): Tree = q"$optionLike.getOrElse($opt, $default)" - override def andThenAsReal(func: TermName): Tree = q"$func" + override def andThenAsReal[T: Liftable](func: T): Tree = q"$func" } case class RealRawEncoding(realType: Type, rawType: Type, clueWithPos: Option[(String, Position)]) extends RpcEncoding { @@ -253,7 +253,19 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } case class MethodMapping(realMethod: RealMethod, rawMethod: RawMethod, - paramMappings: List[ParamMapping], resultEncoding: RpcEncoding) { + paramMappingList: List[ParamMapping], resultEncoding: RpcEncoding) { + + val paramMappings: Map[RawValueParam, ParamMapping] = + paramMappingList.iterator.map(m => (m.rawParam, m)).toMap + + private def rawValueTree(rawParam: RawParam): Tree = rawParam match { + case rvp: RawValueParam => paramMappings(rvp).rawValueTree + case crp: CompositeRawParam => + q""" + ..${crp.paramLists.flatten.map(p => p.localValueDecl(rawValueTree(p)))} + new ${crp.actualType}(...${crp.paramLists.map(_.map(_.safeName))}) + """ + } def realImpl: Tree = { val rpcNameParamDecl: Option[Tree] = rawMethod.arity match { @@ -266,7 +278,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => q""" def ${realMethod.name}(...${realMethod.paramDecls}): ${realMethod.resultType} = { ..${rpcNameParamDecl.toList} - ..${paramMappings.map(pm => pm.rawParam.localValueDecl(pm.rawValueTree))} + ..${rawMethod.rawParams.getOrElse(Nil).map(rp => rp.localValueDecl(rawValueTree(rp)))} ${resultEncoding.applyAsReal(q"${rawMethod.owner.safeName}.${rawMethod.name}(...${rawMethod.argLists})")} } """ @@ -274,7 +286,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def rawCaseImpl: Tree = q""" - ..${paramMappings.filterNot(_.rawParam.auxiliary).flatMap(_.realDecls)} + ..${paramMappings.values.filterNot(_.rawParam.auxiliary).flatMap(_.realDecls)} ${resultEncoding.applyAsRaw(q"${realMethod.owner.safeName}.${realMethod.name}(...${realMethod.argLists})")} """ } @@ -290,7 +302,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } registerCompanionImplicits(raw.tpe) - private def extractMapping(rawParam: RawParam, parser: ParamsParser): Res[ParamMapping] = { + private def extractMapping(rawParam: RawValueParam, parser: ParamsParser): Res[ParamMapping] = { def createErp(realParam: RealParam, index: Int): Res[EncodedRealParam] = EncodedRealParam.create(rawParam, realParam) rawParam.arity match { case _: RpcParamArity.Single => @@ -324,7 +336,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => for { resultConv <- resultEncoding - paramMappings <- collectParamMappings(rawMethod.rawParams.getOrElse(Nil), "raw parameter", realMethod)(extractMapping) + paramMappings <- collectParamMappings(rawMethod.allValueParams, "raw parameter", realMethod)(extractMapping) } yield MethodMapping(realMethod, rawMethod, paramMappings, resultConv) } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index a5a8cff91..90ff5fcf0 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -50,6 +50,8 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => class MethodCompositeParam(override val owner: MethodMetadataConstructor, symbol: Symbol) extends CompositeMetadataParam[RealMethod](owner, symbol) { val constructor: MethodMetadataConstructor = new MethodMetadataConstructor(actualType, Right(this)) + + def pathStr: String = owner.atParam.fold(_ => nameStr, cp => s"${cp.pathStr}.$nameStr") } class ParamCompositeParam(override val owner: ParamMetadataConstructor, symbol: Symbol) @@ -88,7 +90,9 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => } class ParamMetadataParam(owner: MethodMetadataConstructor, symbol: Symbol) - extends MetadataParam[RealMethod](owner, symbol) with RawParamLike { + extends MetadataParam[RealMethod](owner, symbol) with RealParamTarget { + + def pathStr: String = owner.atParam.fold(_ => nameStr, cp => s"${cp.pathStr}.$nameStr") def baseTag: Type = owner.containingMethodParam.baseParamTag def defaultTag: Type = owner.containingMethodParam.defaultParamTag diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index 5066928b1..d2f3f8adb 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -241,11 +241,13 @@ trait RpcSymbols { this: RpcMacroCommons => } } - trait RawParamLike extends ArityParam with RawRpcSymbol { + trait RealParamTarget extends ArityParam with RawRpcSymbol { def allowMulti: Boolean = true def allowNamedMulti: Boolean = true def allowListedMulti: Boolean = true + def pathStr: String + val verbatim: Boolean = annot(RpcEncodingAT).map(_.tpe <:< VerbatimAT).getOrElse(arity.verbatimByDefault) @@ -255,13 +257,50 @@ trait RpcSymbols { this: RpcMacroCommons => def cannotMapClue: String } - case class RawParam(owner: RawMethod, symbol: Symbol) extends RawParamLike { - def baseTag: Type = owner.baseParamTag - def defaultTag: Type = owner.defaultParamTag + object RawParam { + def apply(owner: Either[RawMethod, CompositeRawParam], symbol: Symbol): RawParam = + if (findAnnotation(symbol, CompositeAnnotAT).nonEmpty) + CompositeRawParam(owner, symbol) + else RawValueParam(owner, symbol) + } + + sealed trait RawParam extends RpcParam { + val owner: Either[RawMethod, CompositeRawParam] + val containingRawMethod: RawMethod = owner.fold(identity, _.containingRawMethod) + + def safePath: Tree = owner.fold(_ => q"$safeName", _.safeSelect(this)) + def pathStr: String = owner.fold(_ => nameStr, cp => s"${cp.pathStr}.$nameStr") def shortDescription = "raw parameter" - def description = s"$shortDescription $nameStr of ${owner.description}" - def cannotMapClue = s"cannot map it to $shortDescription $nameStr of ${owner.nameStr}" + def description = s"$shortDescription $nameStr of ${owner.fold(_.description, _.description)}" + } + + case class CompositeRawParam(owner: Either[RawMethod, CompositeRawParam], symbol: Symbol) extends RawParam { + val constructorSig: Type = primaryConstructorOf(actualType, problemStr).typeSignatureIn(actualType) + + val paramLists: List[List[RawParam]] = + constructorSig.paramLists.map(_.map(RawParam(Right(this), _))) + + def allValueParams: List[RawValueParam] = paramLists.flatten.flatMap { + case rvp: RawValueParam => List(rvp) + case crp: CompositeRawParam => crp.allValueParams + } + + def safeSelect(subParam: RawParam): Tree = { + if (!alternatives(actualType.member(subParam.name)).exists(s => s.isMethod && s.asTerm.isParamAccessor)) { + subParam.reportProblem(s"it is not a public member and cannot be accessed, turn it into a val") + } + q"$safePath.${subParam.name}" + } + } + + case class RawValueParam(owner: Either[RawMethod, CompositeRawParam], symbol: Symbol) + extends RawParam with RealParamTarget { + + def baseTag: Type = containingRawMethod.baseParamTag + def defaultTag: Type = containingRawMethod.defaultParamTag + + def cannotMapClue = s"cannot map it to $shortDescription $pathStr of ${containingRawMethod.nameStr}" } case class RealParam(owner: RealMethod, symbol: Symbol, index: Int, indexOfList: Int, indexInList: Int) @@ -318,9 +357,9 @@ trait RpcSymbols { this: RpcMacroCommons => val rawParams: Option[List[RawParam]] = arity match { case RpcMethodArity.Single | RpcMethodArity.Optional => - sig.paramLists.headOption.map(_.map(RawParam(this, _))) + sig.paramLists.headOption.map(_.map(RawParam(Left(this), _))) case RpcMethodArity.Multi(_) => - sig.paramLists.tail.headOption.map(_.map(RawParam(this, _))) + sig.paramLists.tail.headOption.map(_.map(RawParam(Left(this), _))) } val paramLists: List[List[RpcParam]] = arity match { @@ -330,6 +369,11 @@ trait RpcSymbols { this: RpcMacroCommons => List(rpcNameParam) :: rawParams.toList } + val allValueParams: List[RawValueParam] = rawParams.map(_.flatMap { + case rvp: RawValueParam => List(rvp) + case crp: CompositeRawParam => crp.allValueParams + }).getOrElse(Nil) + def rawImpl(caseDefs: List[(String, Tree)]): Tree = { val body = arity match { case RpcMethodArity.Single => caseDefs match { From bd09b68d2035b3176cb0d1ced6cae74499df40f4 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 27 Jun 2018 17:26:20 +0200 Subject: [PATCH 03/91] -trailing commas --- .../src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala index 8c44f37a2..8be763b0c 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala @@ -76,7 +76,7 @@ import com.avsystem.commons.rpc.Utils._ case class DoSomethings( doSomething: DoSomethingSignature, - @optional doSomethingElse: Opt[DoSomethingSignature], + @optional doSomethingElse: Opt[DoSomethingSignature] ) @methodTag[RestMethod, RestMethod] @@ -198,7 +198,7 @@ case class ParameterMetadata[T: TypeName]( case class NameInfo( @reifyName name: String, - @reifyName(rpcName = true) rpcName: String, + @reifyName(rpcName = true) rpcName: String ) { def repr: String = name + (if (rpcName != name) s"<$rpcName>" else "") } From d234e9852ab1a071b050965a45eae74cfa316e59 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 28 Jun 2018 09:55:27 +0200 Subject: [PATCH 04/91] more -trailing commas --- .../src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala index 8be763b0c..6d6562c72 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala @@ -142,7 +142,7 @@ case class CallMetadata[T]( case class GetterParams( @encoded head: ParameterMetadata[_], - @multi tail: List[ParameterMetadata[_]], + @multi tail: List[ParameterMetadata[_]] ) { def repr: String = (head :: tail).map(_.repr).mkString("ARGS:\n", "\n", "") } From 7f7cf744c1f899c4c6c1bf7aea195a8ab52b0b32 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 28 Jun 2018 12:23:57 +0200 Subject: [PATCH 05/91] RPC name raw parameter is denoted by @methodName and may appear anywhere --- .../commons/rpc/akka/MonixRPCFramework.scala | 2 +- .../commons/rpc/akka/RemoteMessage.scala | 19 ++-- .../rpc/akka/client/ClientRawRPC.scala | 18 +-- .../RemoteMessageSerializer.scala | 2 - .../commons/rpc/akka/server/ServerActor.scala | 25 +++-- .../avsystem/commons/rpc/rpcAnnotations.scala | 27 +++-- .../avsystem/commons/rpc/RPCFramework.scala | 10 +- .../commons/rpc/StandardRPCFramework.scala | 13 +-- .../com/avsystem/commons/rpc/NewRawRpc.scala | 15 ++- .../com/avsystem/commons/rpc/RPCTest.scala | 105 +++++++++--------- .../com/avsystem/commons/rpc/TestRPC.scala | 13 ++- .../commons/jetty/rpc/JettyRPCFramework.scala | 25 ++--- .../commons/macros/rpc/RpcMacros.scala | 3 +- .../commons/macros/rpc/RpcMappings.scala | 14 +-- .../commons/macros/rpc/RpcMetadatas.scala | 2 +- .../commons/macros/rpc/RpcSymbols.scala | 79 ++++++------- 16 files changed, 186 insertions(+), 186 deletions(-) diff --git a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/MonixRPCFramework.scala b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/MonixRPCFramework.scala index 2a6089957..bb402e0c4 100644 --- a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/MonixRPCFramework.scala +++ b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/MonixRPCFramework.scala @@ -8,7 +8,7 @@ trait MonixRPCFramework extends RPCFramework { override type RawRPC <: MonixRawRPC trait MonixRawRPC { this: RawRPC => - @multi def observe(rpcName: String)(@multi args: List[RawValue]): Observable[RawValue] + @multi def observe(@composite invocation: RawInvocation): Observable[RawValue] } implicit def readerBasedObservableAsReal[T: Reader]: AsReal[Observable[RawValue], Observable[T]] = diff --git a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/RemoteMessage.scala b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/RemoteMessage.scala index e55224c51..589b56ee0 100644 --- a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/RemoteMessage.scala +++ b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/RemoteMessage.scala @@ -2,7 +2,7 @@ package com.avsystem.commons package rpc.akka import akka.util.ByteString -import com.avsystem.commons.rpc.akka.AkkaRPCFramework.RawValue +import com.avsystem.commons.rpc.akka.AkkaRPCFramework._ import com.avsystem.commons.serialization.GenCodec /** @@ -11,8 +11,8 @@ import com.avsystem.commons.serialization.GenCodec private sealed trait RemoteMessage extends Serializable private object RemoteMessage { - implicit val byteStringCodec = GenCodec.create[ByteString](input => ByteString(input.readBinary()), (output, byteString) => output.writeBinary(byteString.toArray)) - implicit val rawInvocationCodec = GenCodec.materialize[RawInvocation] + implicit val byteStringCodec: GenCodec[ByteString] = + GenCodec.create[ByteString](input => ByteString(input.readBinary()), (output, byteString) => output.writeBinary(byteString.toArray)) implicit val procedureInvocationMessageCodec: GenCodec[ProcedureInvocationMessage] = GenCodec.materialize[ProcedureInvocationMessage] implicit val functionInvocationMessageCodec: GenCodec[FunctionInvocationMessage] = GenCodec.materialize[FunctionInvocationMessage] @@ -28,16 +28,13 @@ private object RemoteMessage { implicit val heatBeatCodec: GenCodec[MonixProtocol.Heartbeat.type] = GenCodec.materialize[MonixProtocol.Heartbeat.type] } -private final case class RawInvocation(rpcName: String, args: List[RawValue]) extends RemoteMessage - private sealed trait InvocationMessage extends RemoteMessage { def getterChain: Seq[RawInvocation] - def name: String - def args: List[RawValue] + def invocation: RawInvocation } -private final case class ProcedureInvocationMessage(name: String, args: List[RawValue], getterChain: Seq[RawInvocation]) extends InvocationMessage -private final case class FunctionInvocationMessage(name: String, args: List[RawValue], getterChain: Seq[RawInvocation]) extends InvocationMessage -private final case class ObservableInvocationMessage(name: String, args: List[RawValue], getterChain: Seq[RawInvocation]) extends InvocationMessage +private final case class ProcedureInvocationMessage(invocation: RawInvocation, getterChain: Seq[RawInvocation]) extends InvocationMessage +private final case class FunctionInvocationMessage(invocation: RawInvocation, getterChain: Seq[RawInvocation]) extends InvocationMessage +private final case class ObservableInvocationMessage(invocation: RawInvocation, getterChain: Seq[RawInvocation]) extends InvocationMessage private sealed trait InvocationResult extends RemoteMessage private final case class InvocationSuccess(value: RawValue) extends InvocationResult @@ -52,4 +49,4 @@ private object MonixProtocol { case object StreamCompleted extends RemoteMessage case object Heartbeat extends RemoteMessage -} \ No newline at end of file +} diff --git a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/client/ClientRawRPC.scala b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/client/ClientRawRPC.scala index ea4078827..2d760978e 100644 --- a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/client/ClientRawRPC.scala +++ b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/client/ClientRawRPC.scala @@ -4,7 +4,7 @@ package rpc.akka.client import akka.actor.ActorSystem import akka.pattern.ask import akka.util.Timeout -import com.avsystem.commons.rpc.akka.AkkaRPCFramework.{RawRPC, RawValue} +import com.avsystem.commons.rpc.akka.AkkaRPCFramework._ import com.avsystem.commons.rpc.akka._ import monix.execution.Cancelable import monix.reactive.{Observable, OverflowStrategy} @@ -14,12 +14,12 @@ import monix.reactive.{Observable, OverflowStrategy} */ private[akka] final class ClientRawRPC(config: AkkaRPCClientConfig, getterChain: Seq[RawInvocation] = Nil)(implicit system: ActorSystem) extends AkkaRPCFramework.RawRPC { - override def fire(rpcName: String)(args: List[RawValue]): Unit = { - system.actorSelection(config.serverPath) ! ProcedureInvocationMessage(rpcName, args, getterChain) + override def fire(invocation: RawInvocation): Unit = { + system.actorSelection(config.serverPath) ! ProcedureInvocationMessage(invocation, getterChain) } - override def call(rpcName: String)(args: List[RawValue]): Future[RawValue] = { + override def call(invocation: RawInvocation): Future[RawValue] = { implicit val timeout: Timeout = Timeout(config.functionCallTimeout) - val future = system.actorSelection(config.serverPath) ? FunctionInvocationMessage(rpcName, args, getterChain) + val future = system.actorSelection(config.serverPath) ? FunctionInvocationMessage(invocation, getterChain) import com.avsystem.commons.concurrent.RunNowEC.Implicits.executionContext @@ -29,13 +29,13 @@ private[akka] final class ClientRawRPC(config: AkkaRPCClientConfig, getterChain: case value => Future.failed(new IllegalStateException(s"Illegal message type. Should be InvocationResult, but received value was: $value")) } } - override def get(rpcName: String)(args: List[RawValue]): RawRPC = - new ClientRawRPC(config, getterChain :+ RawInvocation(rpcName, args)) + override def get(invocation: RawInvocation): RawRPC = + new ClientRawRPC(config, getterChain :+ invocation) - override def observe(rpcName: String)(args: List[RawValue]): Observable[RawValue] = { + override def observe(invocation: RawInvocation): Observable[RawValue] = { Observable.create(OverflowStrategy.Unbounded) { s => val actor = system.actorOf(MonixClientActor.props(s, config)) - actor ! ObservableInvocationMessage(rpcName, args, getterChain) + actor ! ObservableInvocationMessage(invocation, getterChain) Cancelable.empty // TODO implement proper canceling } diff --git a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/serialization/RemoteMessageSerializer.scala b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/serialization/RemoteMessageSerializer.scala index 2bcf10449..e09fbee2c 100644 --- a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/serialization/RemoteMessageSerializer.scala +++ b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/serialization/RemoteMessageSerializer.scala @@ -26,7 +26,6 @@ final class RemoteMessageSerializer extends Serializer { case c if c == classOf[ProcedureInvocationMessage] => GenCodec.read[ProcedureInvocationMessage](input) case c if c == classOf[FunctionInvocationMessage] => GenCodec.read[FunctionInvocationMessage](input) case c if c == classOf[ObservableInvocationMessage] => GenCodec.read[ObservableInvocationMessage](input) - case c if c == classOf[RawInvocation] => GenCodec.read[RawInvocation](input) case c if c == classOf[InvocationSuccess] => GenCodec.read[InvocationSuccess](input) case c if c == classOf[InvocationFailure] => GenCodec.read[InvocationFailure](input) case c if c == MonixProtocol.Continue.getClass => GenCodec.read[MonixProtocol.Continue.type](input) @@ -46,7 +45,6 @@ final class RemoteMessageSerializer extends Serializer { case m: ProcedureInvocationMessage => GenCodec.write(output, m) case m: FunctionInvocationMessage => GenCodec.write(output, m) case m: ObservableInvocationMessage => GenCodec.write(output, m) - case m: RawInvocation => GenCodec.write(output, m) case m: InvocationSuccess => GenCodec.write(output, m) case m: InvocationFailure => GenCodec.write(output, m) case MonixProtocol.Continue => GenCodec.write(output, MonixProtocol.Continue) diff --git a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/server/ServerActor.scala b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/server/ServerActor.scala index cc1116bb7..03edee5ad 100644 --- a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/server/ServerActor.scala +++ b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/server/ServerActor.scala @@ -5,6 +5,7 @@ import akka.actor.{Actor, ActorLogging, Props} import akka.pattern.{AskTimeoutException, ask} import akka.util.Timeout import com.avsystem.commons.concurrent.RunNowEC +import com.avsystem.commons.rpc.akka.AkkaRPCFramework._ import com.avsystem.commons.rpc.akka._ import monix.execution.{Ack, Scheduler} import monix.reactive.Observable @@ -12,20 +13,20 @@ import monix.reactive.Observable /** * @author Wojciech Milewski */ -private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPCServerConfig) extends Actor with ActorLogging { +private final class ServerActor(rawRPC: RawRPC, config: AkkaRPCServerConfig) extends Actor with ActorLogging { override def receive: Receive = { - case msg@ProcedureInvocationMessage(name, argLists, getterChain) => - resolveRpc(msg).fire(name)(argLists) - case msg@FunctionInvocationMessage(name, argLists, getterChain) => + case ProcedureInvocationMessage(invocation, getterChain) => + resolveRpc(getterChain).fire(invocation) + case FunctionInvocationMessage(invocation, getterChain) => val s = sender() - resolveRpc(msg).call(name)(argLists).onCompleteNow { + resolveRpc(getterChain).call(invocation).onCompleteNow { case Success(value) => s ! InvocationSuccess(value) case Failure(e) => - logError(e, name) + logError(e, invocation.rpcName) s ! InvocationFailure(e.getClass.getCanonicalName, e.getMessage) } - case msg@ObservableInvocationMessage(name, argLists, getterChain) => + case ObservableInvocationMessage(invocation, getterChain) => implicit val scheduler: Scheduler = Scheduler(RunNowEC) implicit val timeout: Timeout = Timeout(config.observableAckTimeout) val s = sender() @@ -36,7 +37,7 @@ private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPC Ack.Continue } - resolveRpc(msg).observe(name)(argLists).subscribe( + resolveRpc(getterChain).observe(invocation).subscribe( value => { val result = s ? InvocationSuccess(value) result.mapTo[MonixProtocol.RemoteAck].map { @@ -53,7 +54,7 @@ private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPC }, e => { heartbeat.cancel() - logError(e, name) + logError(e, invocation.rpcName) s ! InvocationFailure(e.getClass.getCanonicalName, e.getMessage) }, () => { @@ -63,8 +64,8 @@ private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPC ) } - private def resolveRpc(msg: InvocationMessage) = - rawRPC.resolveGetterChain(msg.getterChain.map(r => AkkaRPCFramework.RawInvocation(r.rpcName, r.args)).toList) + private def resolveRpc(getterChain: Seq[RawInvocation]): RawRPC = + rawRPC.resolveGetterChain(getterChain) private def logError(e: Throwable, methodName: String): Unit = { log.error(e, @@ -76,5 +77,5 @@ private final class ServerActor(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPC } private[akka] object ServerActor { - def props(rawRPC: AkkaRPCFramework.RawRPC, config: AkkaRPCServerConfig): Props = Props(new ServerActor(rawRPC, config)) + def props(rawRPC: RawRPC, config: AkkaRPCServerConfig): Props = Props(new ServerActor(rawRPC, config)) } diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala index 5667fa222..1556ced5d 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala @@ -31,6 +31,25 @@ class rpcName(val name: String) extends RpcAnnotation */ trait RpcTag extends RpcAnnotation +/** + * May be applied on raw method parameter of type `String` to indicate that macro generated implementation of + * `AsReal` should pass real method's RPC name as this parameter and that macro generated implementation of + * `AsRaw` should expect real method's RPC name to be passed there. + * + * Macro generation of `AsRaw` implementations require that raw methods annotated as [[multi]] must take at least + * one raw parameter annotated as [[methodName]] (it may also be aggregated into some [[composite]] parameter). + * This is necessary to properly identify which real method should be called. + */ +final class methodName extends RawParamAnnotation + +/** + * Can be applied on raw method parameters or metadata parameters. When a parameter is annotated as `@composite`, + * the macro engine expects its type to be a class with public primary constructor. Then, it recursively inspects its + * constructor parameters and treats them as if they were direct parameters. This effectively groups multiple + * raw parameters or multiple metadata parameters into a single class. + */ +final class composite extends RawParamAnnotation + /** * Base trait for RPC arity annotations, [[single]], [[optional]] and [[multi]]. * Arity annotations may be used in multiple contexts: @@ -149,14 +168,6 @@ final class optional extends RpcArity */ final class multi extends RpcArity -/** - * Can be applied on raw method parameters or metadata parameters. When a parameter is annotated as `@composite`, - * the macro engine expects its type to be a class with public primary constructor. Then, it recursively inspects its - * constructor parameters and treats them as if they were direct parameters. This effectively groups multiple - * raw parameters or multiple metadata parameters into a single class. - */ -final class composite extends RawParamAnnotation - /** * Base trait for [[verbatim]] and [[encoded]]. These annotations can be applied either on a raw method or * raw parameter in order to specify how matching real method results or matching real parameter values are encoded diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RPCFramework.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RPCFramework.scala index 2f8692c06..5d34d2100 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RPCFramework.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RPCFramework.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package rpc +import com.avsystem.commons.serialization.GenCodec + import scala.language.higherKinds trait RPCFramework { @@ -8,6 +10,11 @@ trait RPCFramework { type Reader[T] type Writer[T] + case class RawInvocation(@methodName rpcName: String, @multi args: List[RawValue]) + object RawInvocation { + implicit def codec(implicit rawValueCodec: GenCodec[RawValue]): GenCodec[RawInvocation] = GenCodec.materialize + } + type RawRPC val RawRPC: BaseRawRpcCompanion @@ -61,7 +68,8 @@ trait RPCFramework { trait Signature { @reifyName def name: String @multi def paramMetadata: List[ParamMetadata[_]] - @reifyAnnot @multi def annotations: List[MetadataAnnotation] + @reifyAnnot + @multi def annotations: List[MetadataAnnotation] } case class ParamMetadata[T]( diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/StandardRPCFramework.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/StandardRPCFramework.scala index c59746b92..c1e0bc321 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/StandardRPCFramework.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/StandardRPCFramework.scala @@ -9,7 +9,8 @@ trait ProcedureRPCFramework extends RPCFramework { type RawRPC <: ProcedureRawRPC trait ProcedureRawRPC { this: RawRPC => - @multi @verbatim def fire(rpcName: String)(@multi args: List[RawValue]): Unit + @multi + @verbatim def fire(@composite invocation: RawInvocation): Unit } case class ProcedureSignature( @@ -27,7 +28,7 @@ trait FunctionRPCFramework extends RPCFramework { type RawRPC <: FunctionRawRPC trait FunctionRawRPC { this: RawRPC => - @multi def call(rpcName: String)(@multi args: List[RawValue]): Future[RawValue] + @multi def call(@composite invocation: RawInvocation): Future[RawValue] } case class FunctionSignature[T]( @@ -49,13 +50,11 @@ trait FunctionRPCFramework extends RPCFramework { trait GetterRPCFramework extends RPCFramework { type RawRPC <: GetterRawRPC - case class RawInvocation(rpcName: String, args: List[RawValue]) - trait GetterRawRPC { this: RawRPC => - @multi def get(rpcName: String)(@multi args: List[RawValue]): RawRPC + @multi def get(@composite invocation: RawInvocation): RawRPC - def resolveGetterChain(getters: List[RawInvocation]): RawRPC = - getters.foldRight(this)((inv, rpc) => rpc.get(inv.rpcName)(inv.args)) + def resolveGetterChain(getters: Seq[RawInvocation]): RawRPC = + getters.foldRight(this)((inv, rpc) => rpc.get(inv)) } case class GetterSignature[T]( diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala index 6d6562c72..a5d86d946 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala @@ -29,7 +29,8 @@ case class POST() extends RestMethod case class GET() extends RestMethod case class PUT() extends RestMethod -case class HeadTail( +case class GetterInvocation( + @methodName name: String, @encoded head: String, @multi tail: List[String] ) @@ -41,22 +42,24 @@ trait NewRawRpc { @optional def doSomethingElse(arg: Double): String @multi - @verbatim def fire(name: String)( + @verbatim def fire( + @methodName name: String, @optional @auxiliary ajdi: Opt[Int], @multi args: Map[String, String]): Unit - @multi def call(name: String)( + @multi def call( + @methodName name: String, @tagged[renamed] @multi renamedArgs: => Map[String, String], @multi args: Map[String, String]): Future[String] - @multi def get(name: String)(@composite ht: HeadTail): NewRawRpc + @multi def get(@composite invocation: GetterInvocation): NewRawRpc @multi - @tagged[POST] def post(name: String)( + @tagged[POST] def post(@methodName name: String, @tagged[header] @multi @verbatim headers: Vector[String], @multi body: MLinkedHashMap[String, String]): String - @multi def prefix(name: String): NewRawRpc + @multi def prefix(@methodName name: String): NewRawRpc } object NewRawRpc extends RawRpcCompanion[NewRawRpc] { override val implicits: this.type = this diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/RPCTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/RPCTest.scala index e0bec61e8..21456b680 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/RPCTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/RPCTest.scala @@ -19,60 +19,61 @@ class RPCTest extends WordSpec with Matchers with BeforeAndAfterAll { "rpc caller" should { "should properly deserialize RPC calls" in { - val invocations = new ArrayBuffer[(String, List[Any])] - val rawRpc = AsRawRPC[TestRPC].asRaw(TestRPC.rpcImpl((name, args, _) => { - invocations += ((name, args)) - name + val invocations = new ArrayBuffer[RawInvocation] + val rawRpc = AsRawRPC[TestRPC].asRaw(TestRPC.rpcImpl((inv, _) => { + invocations += inv + inv.rpcName })) - rawRpc.fire("handleMore")(Nil) - rawRpc.fire("doStuff")(List(42, "omgsrsly", Some(true))) - assert("doStuffResult" === get(rawRpc.call("doStuffBoolean")(List(true)))) - rawRpc.fire("doStuffInt")(List(5)) - rawRpc.fire("doStuffInt")(Nil) - rawRpc.fire("handleMore")(Nil) - rawRpc.fire("handle")(Nil) - rawRpc.fire("takeCC")(Nil) - rawRpc.fire("srslyDude")(Nil) - rawRpc.get("innerRpc")(List("innerName")).fire("proc")(Nil) - assert("innerRpc.funcResult" === get(rawRpc.get("innerRpc")(List("innerName")).call("func")(List(42)))) + rawRpc.fire(RawInvocation("handleMore", Nil)) + rawRpc.fire(RawInvocation("doStuff", List(42, "omgsrsly", Some(true)))) + assert("doStuffResult" === get(rawRpc.call(RawInvocation("doStuffBoolean", List(true))))) + rawRpc.fire(RawInvocation("doStuffInt", List(5))) + rawRpc.fire(RawInvocation("doStuffInt", Nil)) + rawRpc.fire(RawInvocation("handleMore", Nil)) + rawRpc.fire(RawInvocation("handle", Nil)) + rawRpc.fire(RawInvocation("takeCC", Nil)) + rawRpc.fire(RawInvocation("srslyDude", Nil)) + rawRpc.get(RawInvocation("innerRpc", List("innerName"))).fire(RawInvocation("proc", Nil)) + assert("innerRpc.funcResult" === get(rawRpc.get(RawInvocation("innerRpc", List("innerName"))) + .call(RawInvocation("func", List(42))))) assert(invocations.toList === List( - ("handleMore", Nil), - ("doStuff", List(42, "omgsrsly", Some(true))), - ("doStuffBoolean", List(true)), - ("doStuffInt", List(5)), - ("doStuffInt", List(42)), - ("handleMore", Nil), - ("handle", Nil), - ("takeCC", List(Record(-1, "_"))), - ("srslyDude", Nil), - ("innerRpc", List("innerName")), - ("innerRpc.proc", Nil), - ("innerRpc", List("innerName")), - ("innerRpc.func", List(42)) + RawInvocation("handleMore", Nil), + RawInvocation("doStuff", List(42, "omgsrsly", Some(true))), + RawInvocation("doStuffBoolean", List(true)), + RawInvocation("doStuffInt", List(5)), + RawInvocation("doStuffInt", List(42)), + RawInvocation("handleMore", Nil), + RawInvocation("handle", Nil), + RawInvocation("takeCC", List(Record(-1, "_"))), + RawInvocation("srslyDude", Nil), + RawInvocation("innerRpc", List("innerName")), + RawInvocation("innerRpc.proc", Nil), + RawInvocation("innerRpc", List("innerName")), + RawInvocation("innerRpc.func", List(42)) )) } "fail on bad input" in { - val rawRpc = AsRawRPC[TestRPC].asRaw(TestRPC.rpcImpl((_, _, _) => ())) - intercept[Exception](rawRpc.fire("whatever")(Nil)) + val rawRpc = AsRawRPC[TestRPC].asRaw(TestRPC.rpcImpl((_, _) => ())) + intercept[Exception](rawRpc.fire(RawInvocation("whatever", Nil))) } "real rpc should properly serialize calls to raw rpc" in { - val invocations = new ArrayBuffer[(String, List[Any])] + val invocations = new ArrayBuffer[RawInvocation] object rawRpc extends RawRPC with RunNowFutureCallbacks { - def fire(rpcName: String)(args: List[Any]): Unit = - invocations += ((rpcName, args)) + def fire(inv: RawInvocation): Unit = + invocations += inv - def call(rpcName: String)(args: List[Any]): Future[Any] = { - invocations += ((rpcName, args)) - Future.successful(rpcName + "Result") + def call(inv: RawInvocation): Future[Any] = { + invocations += inv + Future.successful(inv.rpcName + "Result") } - def get(rpcName: String)(args: List[Any]): RawRPC = { - invocations += ((rpcName, args)) + def get(inv: RawInvocation): RawRPC = { + invocations += inv this } } @@ -90,20 +91,20 @@ class RPCTest extends WordSpec with Matchers with BeforeAndAfterAll { realRpc.innerRpc("innerName").moreInner("moreInner").moreInner("evenMoreInner").func(42) assert(invocations.toList === List( - ("handleMore", Nil), - ("doStuff", List(42, "omgsrsly", Some(true))), - ("doStuffBoolean", List(true)), - ("doStuffInt", List(5)), - ("handleMore", Nil), - ("handle", Nil), - - ("innerRpc", List("innerName")), - ("proc", Nil), - - ("innerRpc", List("innerName")), - ("moreInner", List("moreInner")), - ("moreInner", List("evenMoreInner")), - ("func", List(42)) + RawInvocation("handleMore", Nil), + RawInvocation("doStuff", List(42, "omgsrsly", Some(true))), + RawInvocation("doStuffBoolean", List(true)), + RawInvocation("doStuffInt", List(5)), + RawInvocation("handleMore", Nil), + RawInvocation("handle", Nil), + + RawInvocation("innerRpc", List("innerName")), + RawInvocation("proc", Nil), + + RawInvocation("innerRpc", List("innerName")), + RawInvocation("moreInner", List("moreInner")), + RawInvocation("moreInner", List("evenMoreInner")), + RawInvocation("func", List(42)) )) } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/TestRPC.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/TestRPC.scala index 93ca2cb94..fde27c916 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/TestRPC.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/TestRPC.scala @@ -1,6 +1,7 @@ package com.avsystem.commons package rpc +import com.avsystem.commons.rpc.DummyRPC._ import com.avsystem.commons.serialization.{HasGenCodec, whenAbsent} import com.github.ghik.silencer.silent @@ -16,7 +17,7 @@ trait InnerRPC { def indirectRecursion(): TestRPC } -object InnerRPC extends DummyRPC.RPCCompanion[InnerRPC] +object InnerRPC extends RPCCompanion[InnerRPC] trait TestRPC { def defaultNum: Int = 42 @@ -42,18 +43,18 @@ trait TestRPC { } @silent -object TestRPC extends DummyRPC.RPCCompanion[TestRPC] { - def rpcImpl(onInvocation: (String, List[Any], Option[Any]) => Any) = new TestRPC { outer => +object TestRPC extends RPCCompanion[TestRPC] { + def rpcImpl(onInvocation: (RawInvocation, Option[Any]) => Any): TestRPC = new TestRPC { outer => private def onProcedure(methodName: String, args: List[Any]): Unit = - onInvocation(methodName, args, None) + onInvocation(RawInvocation(methodName, args), None) private def onCall[T](methodName: String, args: List[Any], result: T): Future[T] = { - onInvocation(methodName, args, Some(result)) + onInvocation(RawInvocation(methodName, args), Some(result)) Future.successful(result) } private def onGet[T](methodName: String, args: List[Any], result: T): T = { - onInvocation(methodName, args, None) + onInvocation(RawInvocation(methodName, args), None) result } diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRPCFramework.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRPCFramework.scala index ae766b723..3dbcaba89 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRPCFramework.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRPCFramework.scala @@ -37,22 +37,19 @@ object JettyRPCFramework extends StandardRPCFramework with LazyLogging { override def read[T: Reader](raw: RawValue): T = JsonStringInput.read[T](raw.s) override def write[T: Writer](value: T): RawValue = new RawValue(JsonStringOutput.write[T](value)) - case class Invocation(rpcName: String, args: List[RawValue]) - object Invocation extends HasGenCodec[Invocation] - - case class Call(chain: List[Invocation], leaf: Invocation) + case class Call(chain: List[RawInvocation], leaf: RawInvocation) object Call extends HasGenCodec[Call] class RPCClient(httpClient: HttpClient, uri: String)(implicit ec: ExecutionContext) { - private class RawRPCImpl(chain: List[Invocation]) extends RawRPC { - override def fire(rpcName: String)(args: List[RawValue]): Unit = - put(Call(chain, Invocation(rpcName, args))) + private class RawRPCImpl(chain: List[RawInvocation]) extends RawRPC { + override def fire(invocation: RawInvocation): Unit = + put(Call(chain, invocation)) - override def call(rpcName: String)(args: List[RawValue]): Future[RawValue] = - post(Call(chain, Invocation(rpcName, args))) + override def call(invocation: RawInvocation): Future[RawValue] = + post(Call(chain, invocation)) - override def get(rpcName: String)(args: List[RawValue]): RawRPC = - new RawRPCImpl(chain :+ Invocation(rpcName, args)) + override def get(invocation: RawInvocation): RawRPC = + new RawRPCImpl(chain :+ invocation) } val rawRPC: RawRPC = new RawRPCImpl(List.empty) @@ -123,11 +120,11 @@ object JettyRPCFramework extends StandardRPCFramework with LazyLogging { } } - type InvokeFunction[T] = RawRPC => String => List[RawValue] => T + type InvokeFunction[T] = RawRPC => RawInvocation => T def invoke[T](call: Call)(f: InvokeFunction[T]): T = { - val rpc = call.chain.foldLeft(rootRpc)((rpc, inv) => rpc.get(inv.rpcName)(inv.args)) - f(rpc)(call.leaf.rpcName)(call.leaf.args) + val rpc = call.chain.foldLeft(rootRpc)((rpc, inv) => rpc.get(inv)) + f(rpc)(call.leaf) } def handlePost(call: Call): Future[RawValue] = diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index eabe95a45..959da1100 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -23,11 +23,12 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo val RpcNameAT: Type = getType(tq"$RpcPackage.rpcName") val RpcNameNameSym: Symbol = RpcNameAT.member(TermName("name")) val WhenAbsentAT: Type = getType(tq"$CommonsPkg.serialization.whenAbsent[_]") + val MethodNameAT: Type = getType(tq"$RpcPackage.methodName") + val CompositeAT: Type = getType(tq"$RpcPackage.composite") val RpcArityAT: Type = getType(tq"$RpcPackage.RpcArity") val SingleArityAT: Type = getType(tq"$RpcPackage.single") val OptionalArityAT: Type = getType(tq"$RpcPackage.optional") val MultiArityAT: Type = getType(tq"$RpcPackage.multi") - val CompositeAnnotAT: Type = getType(tq"$RpcPackage.composite") val RpcEncodingAT: Type = getType(tq"$RpcPackage.RpcEncoding") val VerbatimAT: Type = getType(tq"$RpcPackage.verbatim") val AuxiliaryAT: Type = getType(tq"$RpcPackage.auxiliary") diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index 4433f85df..d93a77211 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -259,6 +259,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => paramMappingList.iterator.map(m => (m.rawParam, m)).toMap private def rawValueTree(rawParam: RawParam): Tree = rawParam match { + case _: MethodNameParam => q"${realMethod.rpcName}" case rvp: RawValueParam => paramMappings(rvp).rawValueTree case crp: CompositeRawParam => q""" @@ -267,22 +268,13 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => """ } - def realImpl: Tree = { - val rpcNameParamDecl: Option[Tree] = rawMethod.arity match { - case RpcMethodArity.Multi(rpcNameParam) => - Some(q"val ${rpcNameParam.safeName} = ${realMethod.rpcName}") - case RpcMethodArity.Single | RpcMethodArity.Optional => - None - } - + def realImpl: Tree = q""" def ${realMethod.name}(...${realMethod.paramDecls}): ${realMethod.resultType} = { - ..${rpcNameParamDecl.toList} - ..${rawMethod.rawParams.getOrElse(Nil).map(rp => rp.localValueDecl(rawValueTree(rp)))} + ..${rawMethod.rawParams.map(rp => rp.localValueDecl(rawValueTree(rp)))} ${resultEncoding.applyAsReal(q"${rawMethod.owner.safeName}.${rawMethod.name}(...${rawMethod.argLists})")} } """ - } def rawCaseImpl: Tree = q""" diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index 90ff5fcf0..8a6cf9001 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -156,7 +156,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => lazy val paramLists: List[List[MetadataParam[Real]]] = symbol.typeSignatureIn(ownerType).paramLists.map(_.map { ps => - if (findAnnotation(ps, CompositeAnnotAT).nonEmpty) + if (findAnnotation(ps, CompositeAT).nonEmpty) createCompositeParam(ps) else findAnnotation(ps, MetadataParamStrategyType).map(createDirectParam(ps, _)) .getOrElse(if (ps.isImplicit) new ImplicitParam(this, ps) else createDefaultParam(ps)) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index d2f3f8adb..1affb7dee 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -53,29 +53,15 @@ trait RpcSymbols { this: RpcMacroCommons => object RpcMethodArity { def fromAnnotation(method: RawMethod): RpcMethodArity = { val at = method.annot(RpcArityAT).fold(SingleArityAT)(_.tpe) - if (at <:< SingleArityAT || at <:< OptionalArityAT) { - method.sig.paramLists match { - case Nil | List(_) => - case _ => method.reportProblem(s"non-multi raw method can only have zero or one parameter list") - } - if (at <:< OptionalArityAT) Optional else Single - } - else if (at <:< MultiArityAT) method.sig.paramLists match { - case List(rpcNameParam) :: (Nil | List(_)) => - if (!(actualParamType(rpcNameParam) <:< typeOf[String])) { - method.reportProblem("RPC name parameter of multi raw method must be of type String", rpcNameParam.pos) - } - Multi(RpcNameParam(method, rpcNameParam)) - case _ => - method.reportProblem(s"multi raw method must take at most two parameter lists where the first one " + - "must contain RPC name parameter typed as String") - } + if (at <:< SingleArityAT) Single + else if (at <:< OptionalArityAT) Optional + else if (at <:< MultiArityAT) Multi else method.reportProblem(s"unrecognized RPC arity annotation: $at") } case object Single extends RpcMethodArity(true) with RpcArity.Single case object Optional extends RpcMethodArity(true) with RpcArity.Optional - case class Multi(rpcNameParam: RpcNameParam) extends RpcMethodArity(false) with RpcArity.Multi + case object Multi extends RpcMethodArity(false) with RpcArity.Multi } abstract class RpcSymbol { @@ -191,11 +177,6 @@ trait RpcSymbols { this: RpcMacroCommons => if (isRepeated(symbol)) q"$safeName: _*" else q"$safeName" } - case class RpcNameParam(owner: RawMethod, symbol: Symbol) extends RpcParam { - def shortDescription = "RPC name parameter" - def description = s"$shortDescription $nameStr of ${owner.description}" - } - trait AritySymbol extends RpcSymbol { val arity: RpcArity @@ -259,7 +240,9 @@ trait RpcSymbols { this: RpcMacroCommons => object RawParam { def apply(owner: Either[RawMethod, CompositeRawParam], symbol: Symbol): RawParam = - if (findAnnotation(symbol, CompositeAnnotAT).nonEmpty) + if (findAnnotation(symbol, MethodNameAT).nonEmpty) + MethodNameParam(owner, symbol) + else if (findAnnotation(symbol, CompositeAT).nonEmpty) CompositeRawParam(owner, symbol) else RawValueParam(owner, symbol) } @@ -275,15 +258,21 @@ trait RpcSymbols { this: RpcMacroCommons => def description = s"$shortDescription $nameStr of ${owner.fold(_.description, _.description)}" } + case class MethodNameParam(owner: Either[RawMethod, CompositeRawParam], symbol: Symbol) extends RawParam { + if (!(actualType =:= typeOf[String])) { + reportProblem("@methodName parameter must be of type String") + } + } + case class CompositeRawParam(owner: Either[RawMethod, CompositeRawParam], symbol: Symbol) extends RawParam { val constructorSig: Type = primaryConstructorOf(actualType, problemStr).typeSignatureIn(actualType) val paramLists: List[List[RawParam]] = constructorSig.paramLists.map(_.map(RawParam(Right(this), _))) - def allValueParams: List[RawValueParam] = paramLists.flatten.flatMap { - case rvp: RawValueParam => List(rvp) - case crp: CompositeRawParam => crp.allValueParams + def allLeafParams: Iterator[RawParam] = paramLists.iterator.flatten.flatMap { + case crp: CompositeRawParam => crp.allLeafParams + case other => Iterator(other) } def safeSelect(subParam: RawParam): Tree = { @@ -355,24 +344,25 @@ trait RpcSymbols { this: RpcMacroCommons => .map(_.tpe.baseType(ParamTagAT.typeSymbol).typeArgs) .getOrElse(List(owner.baseParamTag, owner.defaultParamTag)) - val rawParams: Option[List[RawParam]] = arity match { - case RpcMethodArity.Single | RpcMethodArity.Optional => - sig.paramLists.headOption.map(_.map(RawParam(Left(this), _))) - case RpcMethodArity.Multi(_) => - sig.paramLists.tail.headOption.map(_.map(RawParam(Left(this), _))) + val rawParams: List[RawParam] = sig.paramLists match { + case Nil | List(_) => sig.paramLists.flatten.map(RawParam(Left(this), _)) + case _ => reportProblem(s"raw methods cannot take multiple parameter lists") } - val paramLists: List[List[RpcParam]] = arity match { - case RpcMethodArity.Single | RpcMethodArity.Optional => - rawParams.toList - case RpcMethodArity.Multi(rpcNameParam) => - List(rpcNameParam) :: rawParams.toList + val paramLists: List[List[RawParam]] = + if (sig.paramLists.isEmpty) Nil else List(rawParams) + + def allLeafParams: Iterator[RawParam] = rawParams.iterator.flatMap { + case crp: CompositeRawParam => crp.allLeafParams + case other => Iterator(other) } - val allValueParams: List[RawValueParam] = rawParams.map(_.flatMap { - case rvp: RawValueParam => List(rvp) - case crp: CompositeRawParam => crp.allValueParams - }).getOrElse(Nil) + val allValueParams: List[RawValueParam] = + allLeafParams.collect({ case rvp: RawValueParam => rvp }).toList + + lazy val methodNameParam: MethodNameParam = + allLeafParams.collectFirst({ case mnp: MethodNameParam => mnp }) + .getOrElse(reportProblem("no @methodName parameter found on @multi raw method")) def rawImpl(caseDefs: List[(String, Tree)]): Tree = { val body = arity match { @@ -386,11 +376,12 @@ trait RpcSymbols { this: RpcMacroCommons => case List((_, single)) => single case _ => abort(s"multiple real methods match $description") } - case RpcMethodArity.Multi(rpcNameParam) => + case RpcMethodArity.Multi => + val methodNameName = c.freshName(TermName("methodName")) q""" - ${rpcNameParam.safeName} match { + ${methodNameParam.safePath} match { case ..${caseDefs.map({ case (rpcName, tree) => cq"$rpcName => $tree" })} - case _ => $RpcPackage.RpcUtils.unknownRpc(${rpcNameParam.safeName}, $nameStr) + case $methodNameName => $RpcPackage.RpcUtils.unknownRpc($methodNameName, $nameStr) } """ } From 862ec7bd722e316238c184e3c780d5bc51c6ffa7 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 2 Jul 2018 12:29:50 +0200 Subject: [PATCH 06/91] rpcNamePrefix annotation and RawRest refactor --- .../avsystem/commons/rpc/rpcAnnotations.scala | 13 ++ .../com/avsystem/commons/rest/RawRest.scala | 110 --------------- .../com/avsystem/commons/rest/rest.scala | 130 +++++++++++++----- .../com/avsystem/commons/rpc/NewRawRpc.scala | 4 +- .../commons/rpc/NewRpcMetadataTest.scala | 2 +- .../com/avsystem/commons/macros/rpc/Res.scala | 4 + .../commons/macros/rpc/RpcMacros.scala | 4 +- .../commons/macros/rpc/RpcSymbols.scala | 7 +- 8 files changed, 128 insertions(+), 146 deletions(-) delete mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala index 1556ced5d..376974489 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala @@ -23,6 +23,19 @@ sealed trait RawParamAnnotation extends RawRpcAnnotation */ class rpcName(val name: String) extends RpcAnnotation +/** + * You can use this annotation on real RPC methods to instruct macro engine to prepend method name (or [[rpcName]] if + * specified) with given prefix. This annotation is mostly useful when aggregated by another annotation e.g. + * + * {{{ + * sealed trait RestMethod extends RpcTag + * final class GET extends RestMethod with AnnotationAggregate { + * @rpcNamePrefix("GET_") type Implied + * } + * }}} + */ +class rpcNamePrefix(val prefix: String) extends RpcAnnotation + /** * Base trait for RPC tag annotations. Tagging gives more direct control over how real methods * and their parameters are matched against raw methods and their parameters. diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala deleted file mode 100644 index bdf8ed8d1..000000000 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ /dev/null @@ -1,110 +0,0 @@ -package com.avsystem.commons -package rest - -import com.avsystem.commons.rpc.{RawRpcCompanion, encoded, methodTag, multi, paramTag, tagged} - -import scala.collection.immutable.ListMap - -@methodTag[RestMethodTag, Sub] -@paramTag[RestParamTag, BodyParam] -trait RawRest { - @multi - @tagged[Sub] - @paramTag[RestParamTag, Path] - def sub(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue] - ): RawRest - - @multi - @tagged[GET] - def get(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] - ): Future[RestResponse] - - @multi - @tagged[GET] - def getSingle(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @encoded @tagged[Body] body: BodyValue - ): Future[RestResponse] - - @multi - @tagged[POST] - def post(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] - ): Future[RestResponse] - - @multi - @tagged[POST] - def postSingle(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @encoded @tagged[Body] body: BodyValue - ): Future[RestResponse] - - @multi - @tagged[PATCH] - def patch(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] - ): Future[RestResponse] - - @multi - @tagged[PATCH] - def patchSingle(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @encoded @tagged[Body] body: BodyValue - ): Future[RestResponse] - - @multi - @tagged[PUT] - def put(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] - ): Future[RestResponse] - - @multi - @tagged[PUT] - def putSingle(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @encoded @tagged[Body] body: BodyValue - ): Future[RestResponse] - - @multi - @tagged[DELETE] - def delete(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @multi @tagged[BodyParam] bodyParams: ListMap[String, BodyValue] - ): Future[RestResponse] - - @multi - @tagged[DELETE] - def deleteSingle(name: String)( - @multi @tagged[Path] pathParams: List[PathValue], - @multi @tagged[Header] headerParams: ListMap[String, HeaderValue], - @multi @tagged[QueryParam] urlParams: ListMap[String, PathValue], - @encoded @tagged[Body] body: BodyValue - ): Future[RestResponse] -} -object RawRest extends RawRpcCompanion[RawRest] diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 1ed3e1f8d..01e8f70b0 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -1,22 +1,36 @@ package com.avsystem.commons package rest -import com.avsystem.commons.rpc.RpcTag +import com.avsystem.commons.annotation.AnnotationAggregate +import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx} +import com.avsystem.commons.rpc._ +import com.avsystem.commons.serialization.json.JsonStringOutput import scala.collection.immutable.ListMap sealed trait RestMethodTag extends RpcTag -final class GET extends RestMethodTag -final class POST extends RestMethodTag -final class PATCH extends RestMethodTag -final class PUT extends RestMethodTag -final class DELETE extends RestMethodTag -sealed trait Sub extends RestMethodTag +sealed trait HttpMethodTag extends RestMethodTag with AnnotationAggregate +final class GET extends HttpMethodTag { + @rpcNamePrefix("GET_") type Implied +} +final class POST extends HttpMethodTag { + @rpcNamePrefix("POST_") type Implied +} +final class PATCH extends HttpMethodTag { + @rpcNamePrefix("PATCH_") type Implied +} +final class PUT extends HttpMethodTag { + @rpcNamePrefix("PUT_") type Implied +} +final class DELETE extends HttpMethodTag { + @rpcNamePrefix("DELETE_") type Implied +} +sealed trait Prefix extends RestMethodTag sealed trait RestParamTag extends RpcTag final class Header extends RestParamTag final class Path extends RestParamTag -final class QueryParam extends RestParamTag +final class Query extends RestParamTag final class BodyParam extends RestParamTag final class Body extends RestParamTag @@ -27,40 +41,94 @@ case class PathValue(value: String) extends AnyVal with RestValue case class HeaderValue(value: String) extends AnyVal with RestValue case class QueryParamValue(value: String) extends AnyVal with RestValue case class BodyValue(value: String) extends AnyVal with RestValue +object BodyValue { + def combineIntoObject(fields: BIterable[(String, BodyValue)]): BodyValue = { + val sb = new JStringBuilder + val oo = new JsonStringOutput(sb).writeObject() + fields.foreach { + case (key, BodyValue(json)) => + oo.writeField(key).writeRawJson(json) + } + oo.finish() + BodyValue(sb.toString) + } +} object RestValue { // AsReal, AsRaw, itp } -sealed abstract class RestMethod { - def name: String - def singleBody: Boolean +final class HttpRestMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum { + def toRpcName(pathName: PathValue): String = + s"${name}_${pathName.value}" } -object RestMethod { - case class Get(name: String, singleBody: Boolean) extends RestMethod - case class Put(name: String, singleBody: Boolean) extends RestMethod - case class Post(name: String, singleBody: Boolean) extends RestMethod - case class Patch(name: String, singleBody: Boolean) extends RestMethod - case class Delete(name: String, singleBody: Boolean) extends RestMethod +object HttpRestMethod extends AbstractValueEnumCompanion[HttpRestMethod] { + final val GET, PUT, POST, PATCH, DELETE: Value = new HttpRestMethod + + def parseRpcName(rpcName: String): (HttpRestMethod, PathValue) = rpcName.split("_", 2) match { + case Array(httpMethod, pathName) => + val httpRestMethod = byName.getOrElse(httpMethod, + throw new IllegalArgumentException(s"Unknown REST HTTP method: $httpMethod")) + val pathValue = PathValue(pathName) + (httpRestMethod, pathValue) + case _ => + throw new IllegalArgumentException(s"Bad RPC name for REST method: $rpcName") + } } case class RestHeaders( - path: List[PathValue], - headers: List[(String, HeaderValue)], - query: List[(String, QueryParamValue)] + @multi @tagged[Path] path: List[PathValue], + @multi @tagged[Header] headers: ListMap[String, HeaderValue], + @multi @tagged[Query] query: ListMap[String, PathValue] ) { - def append(name: String, - pathParams: List[PathValue], - headerParams: ListMap[String, HeaderValue], - queryParams: ListMap[String, QueryParamValue] - ): RestHeaders = - RestHeaders(path ++ (PathValue(name) :: pathParams), headers ++ headerParams, query ++ queryParams) + def append(pathName: PathValue, otherHeaders: RestHeaders): RestHeaders = RestHeaders( + path ++ (pathName :: otherHeaders.path), + headers ++ otherHeaders.headers, + query ++ otherHeaders.query + ) +} +object RestHeaders { + final val Empty = RestHeaders(Nil, ListMap.empty, ListMap.empty) } -case class RestRequest(method: RestMethod, headers: RestHeaders, body: BodyValue) + +case class RestRequest(method: HttpRestMethod, headers: RestHeaders, body: BodyValue) case class RestResponse(code: Int, body: BodyValue) -trait AbstractRawRest extends RawRest { - def headers: RestHeaders - def withHeaders(headers: RestHeaders): AbstractRawRest +@methodTag[RestMethodTag, Prefix] +@paramTag[RestParamTag, BodyParam] +trait RawRest { + @multi + @tagged[Prefix] + @paramTag[RestParamTag, Path] + def prefix(@methodName name: String, @composite headers: RestHeaders): RawRest + + @multi + @tagged[HttpMethodTag] + def handle(@methodName name: String, @composite headers: RestHeaders, + @multi @tagged[BodyParam] body: IListMap[String, BodyValue]): Future[RestResponse] + + @multi + @tagged[HttpMethodTag] + def handleSingle(@methodName name: String, @composite headers: RestHeaders, + @encoded @tagged[Body] body: BodyValue): Future[RestResponse] +} +object RawRest extends RawRpcCompanion[RawRest] { + private final class DefaultRawRest( + prefixHeaders: RestHeaders, handleRequest: RestRequest => Future[RestResponse]) extends RawRest { + + def prefix(name: String, headers: RestHeaders): RawRest = + new DefaultRawRest(prefixHeaders.append(PathValue(name), headers), handleRequest) + + def handle(name: String, headers: RestHeaders, body: IListMap[String, BodyValue]): Future[RestResponse] = { + val (method, pathName) = HttpRestMethod.parseRpcName(name) + handleRequest(RestRequest(method, prefixHeaders.append(pathName, headers), BodyValue.combineIntoObject(body))) + } + + def handleSingle(name: String, headers: RestHeaders, body: BodyValue): Future[RestResponse] = { + val (method, pathName) = HttpRestMethod.parseRpcName(name) + handleRequest(RestRequest(method, prefixHeaders.append(pathName, headers), body)) + } + } - def handle(request: RestRequest): Future[RestResponse] + def apply(handleRequest: RestRequest => Future[RestResponse]): RawRest = + new DefaultRawRest(RestHeaders.Empty, handleRequest) } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala index a5d86d946..a51b28c6c 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala @@ -25,7 +25,9 @@ case class suchMeta(intMeta: Int, strMeta: String) extends StaticAnnotation sealed trait untagged extends DummyParamTag sealed trait RestMethod extends RpcTag -case class POST() extends RestMethod +case class POST() extends RestMethod with AnnotationAggregate { + @rpcNamePrefix("POST_") type Implied +} case class GET() extends RestMethod case class PUT() extends RestMethod diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala index 3e72ae90f..84ab17e8a 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala @@ -58,7 +58,7 @@ class NewRpcMetadataTest extends FunSuite { | dubl -> dubl@1:0:1:1: double suchMeta=false | czy -> czy@2:1:0:2: boolean suchMeta=false | POSTERS: - | postit -> POST() def postit: String + | POST_postit -> POST() def postit: String | HEADERS: | bar@1:0:1:0: String suchMeta=false | foo@3:0:3:1: String suchMeta=true,metas=suchMeta(3,c),suchMeta(2,b) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/Res.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/Res.scala index cda5c997b..74505d3f7 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/Res.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/Res.scala @@ -5,6 +5,10 @@ import scala.collection.generic.CanBuildFrom import scala.collection.mutable sealed trait Res[+A] { + def isOk: Boolean = this match { + case Ok(_) => true + case _: Fail => false + } def map[B](fun: A => B): Res[B] = this match { case Ok(value) => Ok(fun(value)) case f: Fail => f diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index 959da1100..ab0a10e86 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -21,7 +21,9 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo val ParamPositionObj = q"$RpcPackage.ParamPosition" val RpcNameAT: Type = getType(tq"$RpcPackage.rpcName") - val RpcNameNameSym: Symbol = RpcNameAT.member(TermName("name")) + val RpcNameArg: Symbol = RpcNameAT.member(TermName("name")) + val RpcNamePrefixAT: Type = getType(tq"$RpcPackage.rpcNamePrefix") + val RpcNamePrefixArg: Symbol = RpcNamePrefixAT.member(TermName("prefix")) val WhenAbsentAT: Type = getType(tq"$CommonsPkg.serialization.whenAbsent[_]") val MethodNameAT: Type = getType(tq"$RpcPackage.methodName") val CompositeAT: Type = getType(tq"$RpcPackage.composite") diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index 1affb7dee..fa30f11d7 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -127,8 +127,11 @@ trait RpcSymbols { this: RpcMacroCommons => def tag(baseTag: Type, defaultTag: Type): Type = annot(baseTag).fold(defaultTag)(_.tpe) - lazy val rpcName: String = - annot(RpcNameAT).fold(nameStr)(_.findArg[String](RpcNameNameSym)) + lazy val rpcName: String = { + val prefixes = allAnnotations(symbol, RpcNamePrefixAT).map(_.findArg[String](RpcNamePrefixArg)) + val rpcName = annot(RpcNameAT).fold(nameStr)(_.findArg[String](RpcNameArg)) + prefixes.mkString("", "", rpcName) + } } abstract class RpcTrait(val symbol: Symbol) extends RpcSymbol { From b6463623fa56966b70806a50ed53fb56ee8a95d3 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 2 Jul 2018 18:33:00 +0200 Subject: [PATCH 07/91] real method global uniqueness constraint comes back --- .../commons/rpc/NewRpcMetadataTest.scala | 8 +++---- .../commons/macros/rpc/RpcMappings.scala | 13 +---------- .../commons/macros/rpc/RpcMetadatas.scala | 15 ++----------- .../commons/macros/rpc/RpcSymbols.scala | 22 ++++++++++++++++--- 4 files changed, 26 insertions(+), 32 deletions(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala index 84ab17e8a..422b52d4b 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala @@ -17,8 +17,8 @@ trait TestApi extends SomeBase { def defaultValueMethod(int: Int = 0, @whenAbsent(difolt) bul: Boolean): Future[Unit] def flames(arg: String, otherArg: => Int, varargsy: Double*): Unit def overload(int: Int): Unit - def overload(lel: String): TestApi - def overload: TestApi + @rpcName("ovgetter") def overload(lel: String): TestApi + @rpcName("ovprefix") def overload: TestApi def getit(stuff: String, @suchMeta(1, "a") otherStuff: List[Int]): TestApi def postit(arg: String, bar: String, int: Int, @suchMeta(3, "c") foo: String): String } @@ -72,12 +72,12 @@ class NewRpcMetadataTest extends FunSuite { | otherStuff@1:0:1:0: List suchMeta=true,metas=suchMeta(1,a) | RESULT: | - | overload -> def overload: TestApi + | ovgetter -> def overload: TestApi | ARGS: | lel@0:0:0:0: String suchMeta=false | RESULT: | PREFIXERS: - | overload -> def overload: TestApi + | ovprefix -> def overload: TestApi | RESULT: |""".stripMargin ) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index d93a77211..938169a98 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -100,7 +100,6 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } def extractMulti[B](raw: RealParamTarget, matcher: (RealParam, Int) => Res[B], named: Boolean): Res[List[B]] = { - val seenRpcNames = new mutable.HashSet[String] val it = realParams.listIterator() def loop(result: ListBuffer[B]): Res[List[B]] = if (it.hasNext) { @@ -112,10 +111,6 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => matcher(real, result.size) match { case Ok(b) => result += b - if (named && !seenRpcNames.add(real.rpcName)) { - realMethod.reportProblem(s"multiple parameters matched to ${raw.shortDescription} ${raw.nameStr} " + - s"have the same @rpcName: ${real.rpcName}") - } loop(result) case fail: Fail => fail @@ -345,14 +340,8 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def asRawImpl: Tree = { val caseImpls = raw.rawMethods.iterator.map(rm => (rm, new mutable.LinkedHashMap[String, Tree])).toMap methodMappings.foreach { mapping => - val prevCaseDef = caseImpls(mapping.rawMethod).put(mapping.realMethod.rpcName, mapping.rawCaseImpl) - if (prevCaseDef.nonEmpty) { - mapping.realMethod.reportProblem( - s"multiple RPCs named ${mapping.realMethod.rpcName} map to raw method ${mapping.rawMethod.nameStr}. " + - "If you want to overload RPCs, disambiguate them with @rpcName annotation") - } + caseImpls(mapping.rawMethod).put(mapping.realMethod.rpcName, mapping.rawCaseImpl) } - val rawMethodImpls = raw.rawMethods.map(m => m.rawImpl(caseImpls(m).toList)) q""" diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index 8a6cf9001..63584c5b1 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -202,19 +202,8 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def createCompositeParam(paramSym: Symbol): RpcCompositeParam = new RpcCompositeParam(this, paramSym) - def methodMappings(rpc: RealRpcTrait): Map[MethodMetadataParam, List[MethodMetadataMapping]] = { - val allMappings = collectMethodMappings(methodMdParams, "metadata parameters", rpc.realMethods)(_.mappingFor(_)) - val result = allMappings.groupBy(_.mdParam) - result.foreach { case (mmp, mappings) => - mappings.groupBy(_.realMethod.rpcName).foreach { - case (rpcName, MethodMetadataMapping(realMethod, _, _) :: tail) if tail.nonEmpty => - realMethod.reportProblem(s"multiple RPCs named $rpcName map to metadata parameter ${mmp.nameStr}. " + - s"If you want to overload RPCs, disambiguate them with @rpcName annotation") - case _ => - } - } - result - } + def methodMappings(rpc: RealRpcTrait): Map[MethodMetadataParam, List[MethodMetadataMapping]] = + collectMethodMappings(methodMdParams, "metadata parameters", rpc.realMethods)(_.mappingFor(_)).groupBy(_.mdParam) def materializeFor(rpc: RealRpcTrait, methodMappings: Map[MethodMetadataParam, List[MethodMetadataMapping]]): Tree = { val argDecls = paramLists.flatten.map { diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index fa30f11d7..13b07d73d 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -414,7 +414,15 @@ trait RpcSymbols { this: RpcMacroCommons => } } - val realParams: List[RealParam] = paramLists.flatten + val realParams: List[RealParam] = { + val result = paramLists.flatten + result.groupBy(_.rpcName).foreach { + case (_, head :: tail) if tail.nonEmpty => + head.reportProblem(s"it has the same RPC name as ${tail.size} other parameters") + case _ => + } + result + } } case class RawRpcTrait(tpe: Type) extends RpcTrait(tpe.typeSymbol) with RawRpcSymbol { @@ -444,7 +452,15 @@ trait RpcSymbols { this: RpcMacroCommons => def shortDescription = "real RPC" def description = s"$shortDescription $tpe" - lazy val realMethods: List[RealMethod] = - tpe.members.iterator.filter(m => m.isTerm && m.isAbstract).map(RealMethod(this, _)).toList + lazy val realMethods: List[RealMethod] = { + val result = tpe.members.iterator.filter(m => m.isTerm && m.isAbstract).map(RealMethod(this, _)).toList + result.groupBy(_.rpcName).foreach { + case (_, head :: tail) if tail.nonEmpty => + head.reportProblem(s"it has the same RPC name as ${tail.size} other methods - " + + s"if you want to overload RPC methods, disambiguate them with @rpcName") + case _ => + } + result + } } } From 091cfa1d9f5a1913068285c71e67d992c4e8f689 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 3 Jul 2018 14:01:16 +0200 Subject: [PATCH 08/91] some generated code size optimizations for RPC macros --- .../com/avsystem/commons/rpc/RpcUtils.scala | 12 +++++++++ .../commons/macros/rpc/RpcMacros.scala | 1 + .../commons/macros/rpc/RpcMappings.scala | 2 +- .../commons/macros/rpc/RpcMetadatas.scala | 2 +- .../commons/macros/rpc/RpcSymbols.scala | 25 ++++++++++--------- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcUtils.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcUtils.scala index 481734f6d..37ba1e753 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcUtils.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcUtils.scala @@ -3,10 +3,22 @@ package rpc import com.avsystem.commons.macros.misc.MiscMacros +import scala.collection.generic.CanBuildFrom +import scala.collection.mutable + /** * @author ghik */ object RpcUtils { + def createEmpty[Coll](cbf: CanBuildFrom[Nothing, Nothing, Coll]): Coll = + createBuilder[Nothing, Coll](cbf, 0).result() + + def createBuilder[Elem, Coll](cbf: CanBuildFrom[Nothing, Elem, Coll], size: Int): mutable.Builder[Elem, Coll] = { + val b = cbf() + b.sizeHint(size) + b + } + def missingArg(rpcName: String, argName: String): Nothing = throw new IllegalArgumentException(s"Can't interpret raw RPC call $rpcName: argument $argName is missing") diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index ab0a10e86..ce02002a0 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -10,6 +10,7 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo import c.universe._ val RpcPackage = q"$CommonsPkg.rpc" + val RpcUtils = q"$RpcPackage.RpcUtils" val AsRealCls = tq"$RpcPackage.AsReal" val AsRealObj = q"$RpcPackage.AsReal" val AsRawCls = tq"$RpcPackage.AsRaw" diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index 938169a98..2882f32de 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -157,7 +157,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def rawValueTree: Tree = rawParam.mkMulti(reals.map(_.rawValueTree)) } case class IterableMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ListedMulti { - def realDecls: List[Tree] = { + def realDecls: List[Tree] = if(reals.isEmpty) Nil else { val itName = c.freshName(TermName("it")) val itDecl = q"val $itName = ${rawParam.safePath}.iterator" itDecl :: reals.map { erp => diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index 63584c5b1..890b4142b 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -349,7 +349,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case RpcParamArity.Single(annotTpe) => rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree)).getOrElse { val msg = s"${rpcSym.problemStr}: cannot materialize value for $description: no annotation of type $annotTpe found" - q"$RpcPackage.RpcUtils.compilationError(${StringLiteral(msg, rpcSym.pos)})" + q"$RpcUtils.compilationError(${StringLiteral(msg, rpcSym.pos)})" } case RpcParamArity.Optional(annotTpe) => mkOptional(rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree))) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index 13b07d73d..8d8c60919 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -214,15 +214,16 @@ trait RpcSymbols { this: RpcMacroCommons => def mkOptional[T: Liftable](opt: Option[T]): Tree = opt.map(t => q"$optionLike.some($t)").getOrElse(q"$optionLike.none") - def mkMulti[T: Liftable](elements: List[T]): Tree = { - val builderName = c.freshName(TermName("builder")) - q""" - val $builderName = $canBuildFrom() - $builderName.sizeHint(${elements.size}) - ..${elements.map(t => q"$builderName += $t")} - $builderName.result() - """ - } + def mkMulti[T: Liftable](elements: List[T]): Tree = + if (elements.isEmpty) q"$RpcUtils.createEmpty($canBuildFrom)" + else { + val builderName = c.freshName(TermName("builder")) + q""" + val $builderName = $RpcUtils.createBuilder($canBuildFrom, ${elements.size}) + ..${elements.map(t => q"$builderName += $t")} + $builderName.result() + """ + } } trait RealParamTarget extends ArityParam with RawRpcSymbol { @@ -326,7 +327,7 @@ trait RpcSymbols { this: RpcMacroCommons => val prevListParamss = List(prevListParams).filter(_.nonEmpty) q"${owner.owner.safeName}.${TermName(s"${owner.encodedNameStr}$$default$$${index + 1}")}(...$prevListParamss)" } - else q"$RpcPackage.RpcUtils.missingArg(${owner.rpcName}, $rpcName)" + else q"$RpcUtils.missingArg(${owner.rpcName}, $rpcName)" } case class RawMethod(owner: RawRpcTrait, symbol: Symbol) extends RpcMethod with RawRpcSymbol with AritySymbol { @@ -375,7 +376,7 @@ trait RpcSymbols { this: RpcMacroCommons => case _ => abort(s"multiple real methods match $description") } case RpcMethodArity.Optional => caseDefs match { - case Nil => q"$RpcPackage.RpcUtils.missingOptionalRpc($nameStr)" + case Nil => q"$RpcUtils.missingOptionalRpc($nameStr)" case List((_, single)) => single case _ => abort(s"multiple real methods match $description") } @@ -384,7 +385,7 @@ trait RpcSymbols { this: RpcMacroCommons => q""" ${methodNameParam.safePath} match { case ..${caseDefs.map({ case (rpcName, tree) => cq"$rpcName => $tree" })} - case $methodNameName => $RpcPackage.RpcUtils.unknownRpc($methodNameName, $nameStr) + case $methodNameName => $RpcUtils.unknownRpc($methodNameName, $nameStr) } """ } From 7ce263470e6e00469cf721785ffe940409ac9eef Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 3 Jul 2018 14:11:11 +0200 Subject: [PATCH 09/91] WIP prototype of REST core framework --- .../com/avsystem/commons/rest/rest.scala | 117 ++++++++++++++++-- .../avsystem/commons/rest/RestTestApi.scala | 18 +++ 2 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 01e8f70b0..d79e87e0b 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -4,7 +4,8 @@ package rest import com.avsystem.commons.annotation.AnnotationAggregate import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx} import com.avsystem.commons.rpc._ -import com.avsystem.commons.serialization.json.JsonStringOutput +import com.avsystem.commons.serialization.json.{JsonReader, JsonStringInput, JsonStringOutput} +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} import scala.collection.immutable.ListMap @@ -28,18 +29,19 @@ final class DELETE extends HttpMethodTag { sealed trait Prefix extends RestMethodTag sealed trait RestParamTag extends RpcTag -final class Header extends RestParamTag final class Path extends RestParamTag +final class Header extends RestParamTag final class Query extends RestParamTag -final class BodyParam extends RestParamTag -final class Body extends RestParamTag +sealed trait BodyTag extends RestParamTag +final class BodyParam extends BodyTag +final class Body extends BodyTag sealed trait RestValue extends Any { def value: String } case class PathValue(value: String) extends AnyVal with RestValue case class HeaderValue(value: String) extends AnyVal with RestValue -case class QueryParamValue(value: String) extends AnyVal with RestValue +case class QueryValue(value: String) extends AnyVal with RestValue case class BodyValue(value: String) extends AnyVal with RestValue object BodyValue { def combineIntoObject(fields: BIterable[(String, BodyValue)]): BodyValue = { @@ -52,9 +54,25 @@ object BodyValue { oo.finish() BodyValue(sb.toString) } + def uncombineFromObject(body: BodyValue): ListMap[String, BodyValue] = { + val oi = new JsonStringInput(new JsonReader(body.value)).readObject() + val builder = ListMap.newBuilder[String, BodyValue] + while (oi.hasNext) { + val fi = oi.nextField() + builder += ((fi.fieldName, BodyValue(fi.readRawJson()))) + } + builder.result() + } } object RestValue { - // AsReal, AsRaw, itp + implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = + AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = + AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[QueryValue, T] = + AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def bodyValueDefaultAsRealRaw[T: GenCodec]: AsRawReal[BodyValue, T] = + AsRawReal.create(v => BodyValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) } final class HttpRestMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum { @@ -78,20 +96,45 @@ object HttpRestMethod extends AbstractValueEnumCompanion[HttpRestMethod] { case class RestHeaders( @multi @tagged[Path] path: List[PathValue], @multi @tagged[Header] headers: ListMap[String, HeaderValue], - @multi @tagged[Query] query: ListMap[String, PathValue] + @multi @tagged[Query] query: ListMap[String, QueryValue] ) { def append(pathName: PathValue, otherHeaders: RestHeaders): RestHeaders = RestHeaders( path ++ (pathName :: otherHeaders.path), headers ++ otherHeaders.headers, query ++ otherHeaders.query ) + + def extractPathName: (PathValue, RestHeaders) = path match { + case pathName :: pathTail => + (pathName, copy(path = pathTail)) + case Nil => + throw new IllegalArgumentException("empty path") + } + + def extractPrefix(metadata: RestHeadersMetadata): (RestHeaders, RestHeaders) = { + val (prefixPath, tailPath) = path.splitAt(metadata.pathSize) + (copy(path = prefixPath), copy(path = tailPath)) + } } object RestHeaders { final val Empty = RestHeaders(Nil, ListMap.empty, ListMap.empty) } +class HttpErrorException(code: Int, payload: String) + extends RuntimeException(s"$code: $payload") + case class RestRequest(method: HttpRestMethod, headers: RestHeaders, body: BodyValue) case class RestResponse(code: Int, body: BodyValue) +object RestResponse { + implicit def genCodecBasedFutureAsRawReal[T: GenCodec]: AsRawReal[Future[RestResponse], Future[T]] = + AsRawReal.create( + _.mapNow(v => RestResponse(200, BodyValue(JsonStringOutput.write[T](v)))), + _.mapNow { + case RestResponse(200, BodyValue(json)) => JsonStringInput.read[T](json) + case RestResponse(code, BodyValue(payload)) => throw new HttpErrorException(code, payload) + } + ) +} @methodTag[RestMethodTag, Prefix] @paramTag[RestParamTag, BodyParam] @@ -110,7 +153,32 @@ trait RawRest { @tagged[HttpMethodTag] def handleSingle(@methodName name: String, @composite headers: RestHeaders, @encoded @tagged[Body] body: BodyValue): Future[RestResponse] + + def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = request => { + val (pathName, headers) = request.headers.extractPathName + + def forPrefix = + metadata.prefixMethods.get(pathName.value).map { prefixMeta => + val (prefixHeaders, restOfHeaders) = headers.extractPrefix(prefixMeta.headersMetadata) + prefix(pathName.value, prefixHeaders) + .asHandleRequest(prefixMeta.result.value)(request.copy(headers = restOfHeaders)) + } + + def forHttpMethod = { + val rpcName = request.method.toRpcName(pathName) + metadata.httpMethods.get(rpcName).map { httpMeta => + if (httpMeta.singleBody) handleSingle(rpcName, headers, request.body) + else handle(rpcName, headers, BodyValue.uncombineFromObject(request.body)) + } + } + + def notFound = + Future.successful(RestResponse(404, BodyValue(s"path ${pathName.value} not found"))) + + forPrefix orElse forHttpMethod getOrElse notFound + } } + object RawRest extends RawRpcCompanion[RawRest] { private final class DefaultRawRest( prefixHeaders: RestHeaders, handleRequest: RestRequest => Future[RestResponse]) extends RawRest { @@ -132,3 +200,38 @@ object RawRest extends RawRpcCompanion[RawRest] { def apply(handleRequest: RestRequest => Future[RestResponse]): RawRest = new DefaultRawRest(RestHeaders.Empty, handleRequest) } + +@methodTag[RestMethodTag, Prefix] +@paramTag[RestParamTag, BodyParam] +case class RestMetadata[T]( + @multi @tagged[Prefix] prefixMethods: Map[String, PrefixMetadata[_]], + @multi @tagged[HttpMethodTag] httpMethods: Map[String, HttpMethodMetadata[_]] +) +object RestMetadata extends RpcMetadataCompanion[RestMetadata] + +@paramTag[RestParamTag, Path] +case class PrefixMetadata[T]( + @composite headersMetadata: RestHeadersMetadata, + @checked @infer result: RestMetadata.Lazy[T] +) extends TypedMetadata[T] + +case class HttpMethodMetadata[T]( + @reifyName(rpcName = true) rpcName: String, + @composite headersParams: RestHeadersMetadata, + @multi @tagged[BodyTag] bodyParams: Map[String, ParamMetadata[_]] +) extends TypedMetadata[Future[T]] { + val (httpMethod, pathName) = HttpRestMethod.parseRpcName(rpcName) + val singleBody: Boolean = bodyParams.values.exists(_.singleBody) +} + +case class RestHeadersMetadata( + @multi @tagged[Path] path: List[ParamMetadata[_]], + @multi @tagged[Header] headers: Map[String, ParamMetadata[_]], + @multi @tagged[Query] query: Map[String, ParamMetadata[_]] +) { + val pathSize: Int = path.size +} + +case class ParamMetadata[T]( + @hasAnnot[Body] singleBody: Boolean +) extends TypedMetadata[T] diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala new file mode 100644 index 000000000..4f3f66c4e --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala @@ -0,0 +1,18 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.serialization.HasGenCodec + +case class User(id: String, name: String) +object User extends HasGenCodec[User] + +trait RestTestApi { + def subApi(id: Int, @Query query: String): RestTestApi + + @GET def user(userId: String): Future[User] + @POST def user(@Body user: User): Future[Unit] +} +object RestTestApi { + implicit val asRawReal: RawRest.AsRawRealRpc[RestTestApi] = RawRest.materializeAsRawReal[RestTestApi] + implicit val metadata: RestMetadata[RestTestApi] = RestMetadata.materializeForRpc[RestTestApi] +} From 7bb620fff55ce291c35bb80b0e2d4effd7696c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kuleta?= Date: Tue, 3 Jul 2018 17:23:56 +0200 Subject: [PATCH 10/91] commons-core: fixed support for empty request body commons-jetty: implemented handler for new rest --- build.sbt | 1 + .../com/avsystem/commons/rest/rest.scala | 34 ++++++---- .../commons/jetty/rpc/JettyRestHandler.scala | 64 +++++++++++++++++++ .../jetty/rpc/JettyRestHandlerMain.scala | 28 ++++++++ 4 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala diff --git a/build.sbt b/build.sbt index 35c78f95f..a449cce8f 100644 --- a/build.sbt +++ b/build.sbt @@ -236,6 +236,7 @@ lazy val `commons-jetty` = project "org.eclipse.jetty" % "jetty-client" % jettyVersion, "org.eclipse.jetty" % "jetty-server" % jettyVersion, "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, + "org.slf4j" % "slf4j-simple" % "1.7.25" % Test, ), ) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index d79e87e0b..db27ed5e9 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -45,23 +45,31 @@ case class QueryValue(value: String) extends AnyVal with RestValue case class BodyValue(value: String) extends AnyVal with RestValue object BodyValue { def combineIntoObject(fields: BIterable[(String, BodyValue)]): BodyValue = { - val sb = new JStringBuilder - val oo = new JsonStringOutput(sb).writeObject() - fields.foreach { - case (key, BodyValue(json)) => - oo.writeField(key).writeRawJson(json) + if (fields.isEmpty) { + BodyValue("") + } else { + val sb = new JStringBuilder + val oo = new JsonStringOutput(sb).writeObject() + fields.foreach { + case (key, BodyValue(json)) => + oo.writeField(key).writeRawJson(json) + } + oo.finish() + BodyValue(sb.toString) } - oo.finish() - BodyValue(sb.toString) } def uncombineFromObject(body: BodyValue): ListMap[String, BodyValue] = { - val oi = new JsonStringInput(new JsonReader(body.value)).readObject() - val builder = ListMap.newBuilder[String, BodyValue] - while (oi.hasNext) { - val fi = oi.nextField() - builder += ((fi.fieldName, BodyValue(fi.readRawJson()))) + if (body.value.isEmpty) { + ListMap.empty + } else { + val oi = new JsonStringInput(new JsonReader(body.value)).readObject() + val builder = ListMap.newBuilder[String, BodyValue] + while (oi.hasNext) { + val fi = oi.nextField() + builder += ((fi.fieldName, BodyValue(fi.readRawJson()))) + } + builder.result() } - builder.result() } } object RestValue { diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala new file mode 100644 index 000000000..495dc2a58 --- /dev/null +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala @@ -0,0 +1,64 @@ +package com.avsystem.commons +package jetty.rpc + +import java.util.regex.Pattern + +import com.avsystem.commons.rest.{BodyValue, HeaderValue, HttpRestMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import org.eclipse.jetty.http.{HttpHeader, HttpStatus, MimeTypes} +import org.eclipse.jetty.server.Request +import org.eclipse.jetty.server.handler.AbstractHandler + +class JettyRestHandler(handleRequest: RestRequest => Future[RestResponse]) extends AbstractHandler { + + import JettyRestHandler._ + + override def handle(target: String, baseRequest: Request, request: HttpServletRequest, response: HttpServletResponse): Unit = { + val method = HttpRestMethod.byName(baseRequest.getMethod) + + val path = separatorPattern + .splitAsStream(baseRequest.getPathInfo) + .asScala + .skip(1) + .map(PathValue) + .to[List] + + val headersBuilder = IListMap.newBuilder[String, HeaderValue] + baseRequest.getHeaderNames.asScala.foreach { headerName => + headersBuilder += headerName -> HeaderValue(baseRequest.getHeader(headerName)) + } + val headers = headersBuilder.result() + + val queryBuilder = IListMap.newBuilder[String, QueryValue] + baseRequest.getParameterNames.asScala.foreach { parameterName => + queryBuilder += parameterName -> QueryValue(baseRequest.getParameter(parameterName)) + } + val query = queryBuilder.result() + + val bodyReader = baseRequest.getReader + val bodyBuilder = new JStringBuilder + Iterator.continually(bodyReader.read()) + .takeWhile(_ != -1) + .foreach(bodyBuilder.append) + val body = BodyValue(bodyBuilder.toString) + + val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) + + baseRequest.setHandled(true) + val asyncContext = request.startAsync() + handleRequest(restRequest).andThenNow { + case Success(restResponse) => + response.setStatus(restResponse.code) + response.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.APPLICATION_JSON_UTF_8.asString()) + response.getWriter.write(restResponse.body.value) + case Failure(e) => + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) + response.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()) + response.getWriter.write(e.getMessage) + }.andThenNow { case _ => asyncContext.complete() } + } +} + +object JettyRestHandler { + val separatorPattern: Pattern = Pattern.compile("/") +} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala new file mode 100644 index 000000000..0acb14732 --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala @@ -0,0 +1,28 @@ +package com.avsystem.commons +package jetty.rpc + +import com.avsystem.commons.rest.{GET, Query, RawRest, RestMetadata} +import org.eclipse.jetty.server.Server + +object JettyRestHandlerMain { + trait SomeApi { + @GET + def hello(@Query who: String): Future[String] + } + object SomeApi { + implicit val asRawReal: RawRest.AsRawRealRpc[SomeApi] = RawRest.materializeAsRawReal + implicit val metadata: RestMetadata[SomeApi] = RestMetadata.materializeForRpc + } + + def main(args: Array[String]): Unit = { + val someApiImpl = new SomeApi { + override def hello(who: String): Future[String] = Future.successful(s"Hello, $who!") + } + + val handler = new JettyRestHandler(SomeApi.asRawReal.asRaw(someApiImpl).asHandleRequest(SomeApi.metadata)) + val server = new Server(9090) + server.setHandler(handler) + server.start() + server.join() + } +} From d055e1bd3f68925bd43d9cd745e15aee84b55280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kuleta?= Date: Tue, 3 Jul 2018 17:34:43 +0200 Subject: [PATCH 11/91] commons-jetty: fixed body reading --- .../com/avsystem/commons/jetty/rpc/JettyRestHandler.scala | 2 +- .../avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala index 495dc2a58..06d0b3fe4 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala @@ -39,7 +39,7 @@ class JettyRestHandler(handleRequest: RestRequest => Future[RestResponse]) exten val bodyBuilder = new JStringBuilder Iterator.continually(bodyReader.read()) .takeWhile(_ != -1) - .foreach(bodyBuilder.append) + .foreach(bodyBuilder.appendCodePoint) val body = BodyValue(bodyBuilder.toString) val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala index 0acb14732..2927dc405 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala @@ -1,13 +1,18 @@ package com.avsystem.commons package jetty.rpc -import com.avsystem.commons.rest.{GET, Query, RawRest, RestMetadata} +import com.avsystem.commons.rest.{GET, POST, Query, RawRest, RestMetadata} +import com.avsystem.commons.rpc.rpcName import org.eclipse.jetty.server.Server object JettyRestHandlerMain { trait SomeApi { @GET def hello(@Query who: String): Future[String] + + @POST + @rpcName("hello") + def helloThere(who: String): Future[String] } object SomeApi { implicit val asRawReal: RawRest.AsRawRealRpc[SomeApi] = RawRest.materializeAsRawReal @@ -17,6 +22,7 @@ object JettyRestHandlerMain { def main(args: Array[String]): Unit = { val someApiImpl = new SomeApi { override def hello(who: String): Future[String] = Future.successful(s"Hello, $who!") + override def helloThere(who: String): Future[String] = hello(who) } val handler = new JettyRestHandler(SomeApi.asRawReal.asRaw(someApiImpl).asHandleRequest(SomeApi.metadata)) From d7a4e8048535efe5f2273f70ab9431444410164b Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 3 Jul 2018 18:27:27 +0200 Subject: [PATCH 12/91] convenience companions for REST API traits --- .../com/avsystem/commons/rest/rest.scala | 32 +++++++++++++++ .../avsystem/commons/rest/RestTestApi.scala | 5 +-- .../jetty/rpc/JettyRestHandlerMain.scala | 9 ++--- .../commons/macros/MacroCommons.scala | 4 +- .../commons/macros/rest/RestMacros.scala | 39 +++++++++++++++++++ 5 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 commons-macros/src/main/scala/com/avsystem/commons/macros/rest/RestMacros.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index db27ed5e9..edb479ffd 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -207,6 +207,38 @@ object RawRest extends RawRpcCompanion[RawRest] { def apply(handleRequest: RestRequest => Future[RestResponse]): RawRest = new DefaultRawRest(RestHeaders.Empty, handleRequest) + + trait ClientMacroInstances[Real] { + def asReal: AsRealRpc[Real] + } + + trait ServerMacroInstances[Real] { + def metadata: RestMetadata[Real] + def asRaw: AsRawRpc[Real] + } + + trait FullMacroInstances[Real] { + def metadata: RestMetadata[Real] + def asRawReal: AsRawRealRpc[Real] + } + + implicit def clientInstances[Real]: ClientMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] + implicit def serverInstances[Real]: ServerMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] + implicit def fullInstances[Real]: FullMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] +} + +abstract class RestClientApiCompanion[Real](implicit instances: RawRest.ClientMacroInstances[Real]) { + implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal +} + +abstract class RestServerApiCompanion[Real](implicit instances: RawRest.ServerMacroInstances[Real]) { + implicit def restMetadata: RestMetadata[Real] = instances.metadata + implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw +} + +abstract class RestApiCompanion[Real](implicit instances: RawRest.FullMacroInstances[Real]) { + implicit def restMetadata: RestMetadata[Real] = instances.metadata + implicit def restAsRealRaw: RawRest.AsRawRealRpc[Real] = instances.asRawReal } @methodTag[RestMethodTag, Prefix] diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala index 4f3f66c4e..0f8608121 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala @@ -12,7 +12,4 @@ trait RestTestApi { @GET def user(userId: String): Future[User] @POST def user(@Body user: User): Future[Unit] } -object RestTestApi { - implicit val asRawReal: RawRest.AsRawRealRpc[RestTestApi] = RawRest.materializeAsRawReal[RestTestApi] - implicit val metadata: RestMetadata[RestTestApi] = RestMetadata.materializeForRpc[RestTestApi] -} +object RestTestApi extends RestApiCompanion[RestTestApi] diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala index 2927dc405..e43dc1ce0 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala @@ -1,7 +1,7 @@ package com.avsystem.commons package jetty.rpc -import com.avsystem.commons.rest.{GET, POST, Query, RawRest, RestMetadata} +import com.avsystem.commons.rest.{GET, POST, Query, RestApiCompanion} import com.avsystem.commons.rpc.rpcName import org.eclipse.jetty.server.Server @@ -14,10 +14,7 @@ object JettyRestHandlerMain { @rpcName("hello") def helloThere(who: String): Future[String] } - object SomeApi { - implicit val asRawReal: RawRest.AsRawRealRpc[SomeApi] = RawRest.materializeAsRawReal - implicit val metadata: RestMetadata[SomeApi] = RestMetadata.materializeForRpc - } + object SomeApi extends RestApiCompanion[SomeApi] def main(args: Array[String]): Unit = { val someApiImpl = new SomeApi { @@ -25,7 +22,7 @@ object JettyRestHandlerMain { override def helloThere(who: String): Future[String] = hello(who) } - val handler = new JettyRestHandler(SomeApi.asRawReal.asRaw(someApiImpl).asHandleRequest(SomeApi.metadata)) + val handler = new JettyRestHandler(SomeApi.restAsRealRaw.asRaw(someApiImpl).asHandleRequest(SomeApi.restMetadata)) val server = new Server(9090) server.setHandler(handler) server.start() diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala index c7d60acbe..b452cba2c 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala @@ -689,8 +689,8 @@ trait MacroCommons { bundle => /** * @param apply case class constructor or companion object's apply method - * @param unapply companion object'a unapply method or [[NoSymbol]] for case class with more than 22 fields - * @param params parameters with trees evaluating to default values (or [[EmptyTree]]s) + * @param unapply companion object'a unapply method or `NoSymbol` for case class with more than 22 fields + * @param params parameters with trees evaluating to default values (or `EmptyTree`s) */ case class ApplyUnapply(apply: Symbol, unapply: Symbol, params: List[(TermSymbol, Tree)]) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rest/RestMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rest/RestMacros.scala new file mode 100644 index 000000000..991166aaf --- /dev/null +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rest/RestMacros.scala @@ -0,0 +1,39 @@ +package com.avsystem.commons +package macros.rest + +import com.avsystem.commons.macros.AbstractMacroCommons + +import scala.reflect.macros.blackbox + +class RestMacros(val ctx: blackbox.Context) extends AbstractMacroCommons(ctx) { + + import c.universe._ + + val RestPkg: Tree = q"$CommonsPkg.rest" + val RawRestObj: Tree = q"$RestPkg.RawRest" + val RestMetadataObj: Tree = q"$RestPkg.RestMetadata" + val RestMetadataCls: Tree = tq"$RestPkg.RestMetadata" + + def instances[Real: WeakTypeTag]: Tree = { + val realTpe = weakTypeOf[Real] + val instancesTpe = c.macroApplication.tpe + + val asRawTpe: Type = getType(tq"$RawRestObj.AsRawRpc[$realTpe]") + val asRealTpe: Type = getType(tq"$RawRestObj.AsRealRpc[$realTpe]") + val asRawRealTpe: Type = getType(tq"$RawRestObj.AsRawRealRpc[$realTpe]") + val metadataTpe: Type = getType(tq"$RestMetadataCls[$realTpe]") + + val memberDefs = instancesTpe.members.iterator.filter(m => m.isAbstract && m.isMethod).map { m => + val resultTpe = m.typeSignatureIn(instancesTpe).finalResultType + val body = + if (resultTpe <:< asRawRealTpe) q"$RawRestObj.materializeAsRawReal[$realTpe]" + else if (resultTpe <:< asRawTpe) q"$RawRestObj.materializeAsRaw[$realTpe]" + else if (resultTpe <:< asRealTpe) q"$RawRestObj.materializeAsReal[$realTpe]" + else if (resultTpe <:< metadataTpe) q"$RestMetadataObj.materializeForRpc[$realTpe]" + else abort(s"Unexpected result type: $resultTpe") + q"lazy val ${m.name.toTermName} = $body" + }.toList + + q"new $instancesTpe { ..$memberDefs }" + } +} From 84d57a522ecdfa9b268bcb4d34e3aef363521122 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 3 Jul 2018 18:51:16 +0200 Subject: [PATCH 13/91] added simple test for RawRest --- .../avsystem/commons/rest/RawRestTest.scala | 46 +++++++++++++++++++ .../avsystem/commons/rest/RestTestApi.scala | 15 ------ version.sbt | 2 +- 3 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala delete mode 100644 commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala new file mode 100644 index 000000000..e1604adce --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -0,0 +1,46 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.serialization.HasGenCodec +import org.scalactic.source.Position +import org.scalatest.FunSuite +import org.scalatest.concurrent.ScalaFutures + +case class User(id: String, name: String) +object User extends HasGenCodec[User] + +trait RestTestApi { + def subApi(id: Int, @Query query: String): RestTestApi + + @GET def user(userId: String): Future[User] + @POST def user(@Body user: User): Future[Unit] +} +object RestTestApi extends RestApiCompanion[RestTestApi] + +class RawRestTest extends FunSuite with ScalaFutures { + test("round trip test") { + class RestTestApiImpl(id: Int, query: String) extends RestTestApi { + def subApi(newId: Int, newQuery: String): RestTestApi = new RestTestApiImpl(newId, query + newQuery) + def user(userId: String): Future[User] = Future.successful(User(userId, s"$userId-$id-$query")) + def user(user: User): Future[Unit] = Future.unit + } + + val callRecord = new MListBuffer[(RestRequest, RestResponse)] + + val real = new RestTestApiImpl(0, "") + val serverHandle: RestRequest => Future[RestResponse] = request => { + RestTestApi.restAsRealRaw.asRaw(real).asHandleRequest(RestTestApi.restMetadata)(request).andThenNow { + case Success(response) => callRecord += ((request, response)) + } + } + + val realProxy: RestTestApi = + RestTestApi.restAsRealRaw.asReal(RawRest(serverHandle)) + + def assertSame[T](call: RestTestApi => Future[T])(implicit pos: Position): Unit = + assert(call(realProxy).futureValue == call(real).futureValue) + + assertSame(_.user("ID")) + assertSame(_.subApi(1, "query").user("ID")) + } +} diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala deleted file mode 100644 index 0f8608121..000000000 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.avsystem.commons -package rest - -import com.avsystem.commons.serialization.HasGenCodec - -case class User(id: String, name: String) -object User extends HasGenCodec[User] - -trait RestTestApi { - def subApi(id: Int, @Query query: String): RestTestApi - - @GET def user(userId: String): Future[User] - @POST def user(@Body user: User): Future[Unit] -} -object RestTestApi extends RestApiCompanion[RestTestApi] diff --git a/version.sbt b/version.sbt index 69ba3ed6c..3c89c5095 100644 --- a/version.sbt +++ b/version.sbt @@ -1 +1 @@ -version in ThisBuild := "1.28.1" +version in ThisBuild := "1.29.0-SNAPSHOT" From 269079e97232c171106609767c9eeae2fb6f1883 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 11:13:47 +0200 Subject: [PATCH 14/91] added Future.unit compat extension for Scala 2.11 --- .../com/avsystem/commons/CompatSharedExtensions.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/commons-core/src/main/scala-2.11/com/avsystem/commons/CompatSharedExtensions.scala b/commons-core/src/main/scala-2.11/com/avsystem/commons/CompatSharedExtensions.scala index 08b924bae..6bfe5b344 100644 --- a/commons-core/src/main/scala-2.11/com/avsystem/commons/CompatSharedExtensions.scala +++ b/commons-core/src/main/scala-2.11/com/avsystem/commons/CompatSharedExtensions.scala @@ -1,10 +1,13 @@ package com.avsystem.commons -import com.avsystem.commons.CompatSharedExtensions.FutureCompatOps import com.avsystem.commons.concurrent.RunNowEC trait CompatSharedExtensions { + import CompatSharedExtensions._ + implicit def futureCompatOps[A](fut: Future[A]): FutureCompatOps[A] = new FutureCompatOps(fut) + + implicit def futureCompanionCompatOps(fut: Future.type): FutureCompanionCompatOps.type = FutureCompanionCompatOps } object CompatSharedExtensions { @@ -28,4 +31,8 @@ object CompatSharedExtensions { def zipWith[U, R](that: Future[U])(f: (A, U) => R)(implicit executor: ExecutionContext): Future[R] = fut.flatMap(r1 => that.map(r2 => f(r1, r2)))(RunNowEC) } + + object FutureCompanionCompatOps { + final val unit: Future[Unit] = Future.successful(()) + } } From 8810e197b94a5bd3db33a9d9f055268153d1427d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kuleta?= Date: Wed, 4 Jul 2018 11:41:22 +0200 Subject: [PATCH 15/91] commons-jetty: refactored JettyRestHandler - now available as RestServlet and RestHandler commons-jetty: added unit tests for RestHandler --- build.sbt | 2 + .../commons/jetty/rpc/RestHandler.scala | 14 +++++ ...ttyRestHandler.scala => RestServlet.scala} | 38 +++++++------ .../jetty/rpc/JettyRestHandlerMain.scala | 31 ---------- .../commons/jetty/rpc/RestHandlerMain.scala | 14 +++++ .../commons/jetty/rpc/RestServletTest.scala | 56 +++++++++++++++++++ .../avsystem/commons/jetty/rpc/SomeApi.scala | 32 +++++++++++ .../commons/jetty/rpc/UsesHttpClient.scala | 19 +++++++ .../commons/jetty/rpc/UsesHttpServer.scala | 23 ++++++++ 9 files changed, 180 insertions(+), 49 deletions(-) create mode 100644 commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala rename commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/{JettyRestHandler.scala => RestServlet.scala} (62%) delete mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpClient.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala diff --git a/build.sbt b/build.sbt index a449cce8f..227ad7ebe 100644 --- a/build.sbt +++ b/build.sbt @@ -236,6 +236,8 @@ lazy val `commons-jetty` = project "org.eclipse.jetty" % "jetty-client" % jettyVersion, "org.eclipse.jetty" % "jetty-server" % jettyVersion, "com.typesafe.scala-logging" %% "scala-logging" % scalaLoggingVersion, + + "org.eclipse.jetty" % "jetty-servlet" % jettyVersion % Test, "org.slf4j" % "slf4j-simple" % "1.7.25" % Test, ), ) diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala new file mode 100644 index 000000000..5a024802b --- /dev/null +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala @@ -0,0 +1,14 @@ +package com.avsystem.commons +package jetty.rpc + +import com.avsystem.commons.rest.{RestRequest, RestResponse} +import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import org.eclipse.jetty.server.Request +import org.eclipse.jetty.server.handler.AbstractHandler + +class RestHandler(handleRequest: RestRequest => Future[RestResponse]) extends AbstractHandler { + override def handle(target: String, baseRequest: Request, request: HttpServletRequest, response: HttpServletResponse): Unit = { + baseRequest.setHandled(true) + RestServlet.handle(handleRequest, request, response) + } +} diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala similarity index 62% rename from commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala rename to commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index 06d0b3fe4..fc7020e44 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/JettyRestHandler.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -4,38 +4,45 @@ package jetty.rpc import java.util.regex.Pattern import com.avsystem.commons.rest.{BodyValue, HeaderValue, HttpRestMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} -import javax.servlet.http.{HttpServletRequest, HttpServletResponse} +import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.http.{HttpHeader, HttpStatus, MimeTypes} -import org.eclipse.jetty.server.Request -import org.eclipse.jetty.server.handler.AbstractHandler -class JettyRestHandler(handleRequest: RestRequest => Future[RestResponse]) extends AbstractHandler { +class RestServlet(handleRequest: RestRequest => Future[RestResponse]) extends HttpServlet { + override def service(req: HttpServletRequest, resp: HttpServletResponse): Unit = { + RestServlet.handle(handleRequest, req, resp) + } +} - import JettyRestHandler._ +object RestServlet { + val separatorPattern: Pattern = Pattern.compile("/") - override def handle(target: String, baseRequest: Request, request: HttpServletRequest, response: HttpServletResponse): Unit = { - val method = HttpRestMethod.byName(baseRequest.getMethod) + def handle( + handleRequest: RestRequest => Future[RestResponse], + request: HttpServletRequest, + response: HttpServletResponse + ): Unit = { + val method = HttpRestMethod.byName(request.getMethod) val path = separatorPattern - .splitAsStream(baseRequest.getPathInfo) + .splitAsStream(request.getPathInfo) .asScala .skip(1) .map(PathValue) .to[List] val headersBuilder = IListMap.newBuilder[String, HeaderValue] - baseRequest.getHeaderNames.asScala.foreach { headerName => - headersBuilder += headerName -> HeaderValue(baseRequest.getHeader(headerName)) + request.getHeaderNames.asScala.foreach { headerName => + headersBuilder += headerName -> HeaderValue(request.getHeader(headerName)) } val headers = headersBuilder.result() val queryBuilder = IListMap.newBuilder[String, QueryValue] - baseRequest.getParameterNames.asScala.foreach { parameterName => - queryBuilder += parameterName -> QueryValue(baseRequest.getParameter(parameterName)) + request.getParameterNames.asScala.foreach { parameterName => + queryBuilder += parameterName -> QueryValue(request.getParameter(parameterName)) } val query = queryBuilder.result() - val bodyReader = baseRequest.getReader + val bodyReader = request.getReader val bodyBuilder = new JStringBuilder Iterator.continually(bodyReader.read()) .takeWhile(_ != -1) @@ -44,7 +51,6 @@ class JettyRestHandler(handleRequest: RestRequest => Future[RestResponse]) exten val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) - baseRequest.setHandled(true) val asyncContext = request.startAsync() handleRequest(restRequest).andThenNow { case Success(restResponse) => @@ -58,7 +64,3 @@ class JettyRestHandler(handleRequest: RestRequest => Future[RestResponse]) exten }.andThenNow { case _ => asyncContext.complete() } } } - -object JettyRestHandler { - val separatorPattern: Pattern = Pattern.compile("/") -} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala deleted file mode 100644 index e43dc1ce0..000000000 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/JettyRestHandlerMain.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.avsystem.commons -package jetty.rpc - -import com.avsystem.commons.rest.{GET, POST, Query, RestApiCompanion} -import com.avsystem.commons.rpc.rpcName -import org.eclipse.jetty.server.Server - -object JettyRestHandlerMain { - trait SomeApi { - @GET - def hello(@Query who: String): Future[String] - - @POST - @rpcName("hello") - def helloThere(who: String): Future[String] - } - object SomeApi extends RestApiCompanion[SomeApi] - - def main(args: Array[String]): Unit = { - val someApiImpl = new SomeApi { - override def hello(who: String): Future[String] = Future.successful(s"Hello, $who!") - override def helloThere(who: String): Future[String] = hello(who) - } - - val handler = new JettyRestHandler(SomeApi.restAsRealRaw.asRaw(someApiImpl).asHandleRequest(SomeApi.restMetadata)) - val server = new Server(9090) - server.setHandler(handler) - server.start() - server.join() - } -} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala new file mode 100644 index 000000000..77b30423f --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala @@ -0,0 +1,14 @@ +package com.avsystem.commons +package jetty.rpc + +import org.eclipse.jetty.server.Server + +object RestHandlerMain { + def main(args: Array[String]): Unit = { + val handler = new RestHandler(SomeApi.asHandleRequest(SomeApi.impl)) + val server = new Server(9090) + server.setHandler(handler) + server.start() + server.join() + } +} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala new file mode 100644 index 000000000..d372cdcd2 --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala @@ -0,0 +1,56 @@ +package com.avsystem.commons +package jetty.rpc + +import org.eclipse.jetty.client.util.StringContentProvider +import org.eclipse.jetty.http.{HttpMethod, HttpStatus} +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.{ServletHandler, ServletHolder} +import org.scalatest.FunSuite + +class RestServletTest extends FunSuite with UsesHttpServer with UsesHttpClient { + val baseUrl = s"http://localhost:$port/api" + + override protected def setupServer(server: Server): Unit = { + val servlet = new RestServlet(SomeApi.asHandleRequest(SomeApi.impl)) + val holder = new ServletHolder(servlet) + val handler = new ServletHandler + handler.addServletWithMapping(holder, "/api/*") + server.setHandler(handler) + } + + test("GET method") { + val response = client.newRequest(s"$baseUrl/hello") + .method(HttpMethod.GET) + .param("who", "World") + .send() + + assert(response.getContentAsString === """"Hello, World!"""") + } + + test("POST method") { + val response = client.newRequest(s"$baseUrl/hello") + .method(HttpMethod.POST) + .content(new StringContentProvider("""{"who":"World"}""")) + .send() + + assert(response.getContentAsString === """"Hello, World!"""") + } + + test("error handling") { + val response = client.newRequest(s"$baseUrl/hello") + .method(HttpMethod.GET) + .param("who", SomeApi.poison) + .send() + + assert(response.getStatus === HttpStatus.INTERNAL_SERVER_ERROR_500) + assert(response.getContentAsString === SomeApi.poison) + } + + test("invalid path") { + val response = client.newRequest(s"$baseUrl/invalidPath") + .method(HttpMethod.GET) + .send() + + assert(response.getStatus === HttpStatus.NOT_FOUND_404) + } +} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala new file mode 100644 index 000000000..71987918b --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala @@ -0,0 +1,32 @@ +package com.avsystem.commons +package jetty.rpc + +import com.avsystem.commons.rest.{GET, POST, Query, RestApiCompanion, RestRequest, RestResponse} +import com.avsystem.commons.rpc.rpcName + +trait SomeApi { + @GET + def hello(@Query who: String): Future[String] + + @POST + @rpcName("hello") + def helloThere(who: String): Future[String] +} + +object SomeApi extends RestApiCompanion[SomeApi] { + def asHandleRequest(real: SomeApi): RestRequest => Future[RestResponse] = { + restAsRealRaw.asRaw(real).asHandleRequest(restMetadata) + } + + def format(who: String) = s"Hello, $who!" + val poison: String = "poison" + + val impl: SomeApi = new SomeApi { + override def hello(who: String): Future[String] = { + if (who == poison) Future.failed(new IllegalArgumentException(poison)) + else Future.successful(format(who)) + } + + override def helloThere(who: String): Future[String] = hello(who) + } +} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpClient.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpClient.scala new file mode 100644 index 000000000..621ce02a4 --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpClient.scala @@ -0,0 +1,19 @@ +package com.avsystem.commons +package jetty.rpc + +import org.eclipse.jetty.client.HttpClient +import org.scalatest.{BeforeAndAfterAll, Suite} + +trait UsesHttpClient extends BeforeAndAfterAll { this: Suite => + val client: HttpClient = new HttpClient() + + override protected def beforeAll(): Unit = { + super.beforeAll() + client.start() + } + + override protected def afterAll(): Unit = { + client.stop() + super.afterAll() + } +} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala new file mode 100644 index 000000000..979673d18 --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala @@ -0,0 +1,23 @@ +package com.avsystem.commons +package jetty.rpc + +import org.eclipse.jetty.server.Server +import org.scalatest.{BeforeAndAfterAll, Suite} + +trait UsesHttpServer extends BeforeAndAfterAll { this: Suite => + val port: Int = 9090 + val server: Server = new Server(port) + + protected def setupServer(server: Server): Unit + + override protected def beforeAll(): Unit = { + super.beforeAll() + setupServer(server) + server.start() + } + + override protected def afterAll(): Unit = { + server.stop() + super.afterAll() + } +} From 887329acd247fbf38cce236de227b8b96f5dd150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kuleta?= Date: Wed, 4 Jul 2018 12:16:21 +0200 Subject: [PATCH 16/91] commons-jetty: RestServlet will now handle exceptions thrown by real methods the same way as returning failed futures --- .../main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala | 2 +- .../src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index fc7020e44..88b348369 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -52,7 +52,7 @@ object RestServlet { val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) val asyncContext = request.startAsync() - handleRequest(restRequest).andThenNow { + handleRequest(restRequest).catchFailures.andThenNow { case Success(restResponse) => response.setStatus(restResponse.code) response.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.APPLICATION_JSON_UTF_8.asString()) diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala index 71987918b..c2cd3d9e3 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala @@ -23,7 +23,7 @@ object SomeApi extends RestApiCompanion[SomeApi] { val impl: SomeApi = new SomeApi { override def hello(who: String): Future[String] = { - if (who == poison) Future.failed(new IllegalArgumentException(poison)) + if (who == poison) throw new IllegalArgumentException(poison) else Future.successful(format(who)) } From 976c5da31c0cf7275b10799d2e2836a643d2e857 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 12:26:01 +0200 Subject: [PATCH 17/91] added various convenience methods for working with RPC typeclasses --- .../scala/com/avsystem/commons/CommonAliases.scala | 2 +- .../main/scala/com/avsystem/commons/rest/rest.scala | 12 +++++++++--- .../scala/com/avsystem/commons/rpc/AsRawReal.scala | 6 ++++++ .../com/avsystem/commons/rpc/RawRpcCompanion.scala | 3 +++ .../avsystem/commons/rpc/RpcMetadataCompanion.scala | 2 ++ 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/CommonAliases.scala b/commons-core/src/main/scala/com/avsystem/commons/CommonAliases.scala index 1f20bf7a3..6a3d78f82 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/CommonAliases.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/CommonAliases.scala @@ -19,7 +19,7 @@ trait CommonAliases { type ClassTag[T] = scala.reflect.ClassTag[T] final val ClassTag = scala.reflect.ClassTag - final def classTag[T: ClassTag] = scala.reflect.classTag[T] + final def classTag[T: ClassTag]: ClassTag[T] = scala.reflect.classTag[T] type Opt[+T] = misc.Opt[T] final val Opt = misc.Opt diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index edb479ffd..666269528 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -165,14 +165,14 @@ trait RawRest { def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = request => { val (pathName, headers) = request.headers.extractPathName - def forPrefix = + def forPrefix: Option[Future[RestResponse]] = metadata.prefixMethods.get(pathName.value).map { prefixMeta => val (prefixHeaders, restOfHeaders) = headers.extractPrefix(prefixMeta.headersMetadata) prefix(pathName.value, prefixHeaders) .asHandleRequest(prefixMeta.result.value)(request.copy(headers = restOfHeaders)) } - def forHttpMethod = { + def forHttpMethod: Option[Future[RestResponse]] = { val rpcName = request.method.toRpcName(pathName) metadata.httpMethods.get(rpcName).map { httpMeta => if (httpMeta.singleBody) handleSingle(rpcName, headers, request.body) @@ -180,7 +180,7 @@ trait RawRest { } } - def notFound = + def notFound: Future[RestResponse] = Future.successful(RestResponse(404, BodyValue(s"path ${pathName.value} not found"))) forPrefix orElse forHttpMethod getOrElse notFound @@ -188,6 +188,12 @@ trait RawRest { } object RawRest extends RawRpcCompanion[RawRest] { + def fromHandleRequest[Real: AsRealRpc](handleRequest: RestRequest => Future[RestResponse]): Real = + RawRest.asReal(RawRest(handleRequest)) + + def toHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): RestRequest => Future[RestResponse] = + RawRest.asRaw(real).asHandleRequest(RestMetadata[Real]) + private final class DefaultRawRest( prefixHeaders: RestHeaders, handleRequest: RestRequest => Future[RestResponse]) extends RawRest { diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala index 6ba1a2d41..9afafabd7 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala @@ -8,6 +8,8 @@ trait AsRaw[Raw, Real] { def asRaw(real: Real): Raw } object AsRaw { + def apply[Raw, Real](implicit asRaw: AsRaw[Raw, Real]): AsRaw[Raw, Real] = asRaw + def create[Raw, Real](asRawFun: Real => Raw): AsRaw[Raw, Real] = new AsRaw[Raw, Real] { def asRaw(real: Real): Raw = asRawFun(real) @@ -21,6 +23,8 @@ trait AsReal[Raw, Real] { def asReal(raw: Raw): Real } object AsReal { + def apply[Raw, Real](implicit asReal: AsReal[Raw, Real]): AsReal[Raw, Real] = asReal + def create[Raw, Real](asRealFun: Raw => Real): AsReal[Raw, Real] = new AsReal[Raw, Real] { def asReal(raw: Raw): Real = asRealFun(raw) @@ -32,6 +36,8 @@ object AsReal { @implicitNotFound("don't know how to encode and decode between ${Real} and ${Raw}, appropriate AsRawReal instance not found") trait AsRawReal[Raw, Real] extends AsReal[Raw, Real] with AsRaw[Raw, Real] object AsRawReal { + def apply[Raw, Real](implicit asRawReal: AsRawReal[Raw, Real]): AsRawReal[Raw, Real] = asRawReal + def create[Raw, Real](asRawFun: Real => Raw, asRealFun: Raw => Real): AsRawReal[Raw, Real] = new AsRawReal[Raw, Real] { def asRaw(real: Real): Raw = asRawFun(real) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RawRpcCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RawRpcCompanion.scala index 36ebcf8a0..9549f336b 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RawRpcCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RawRpcCompanion.scala @@ -11,6 +11,9 @@ trait RawRpcCompanion[Raw] extends RpcImplicitsProvider { type AsRealRpc[Real] = AsReal[Raw, Real] type AsRawRealRpc[Real] = AsRawReal[Raw, Real] + def asReal[Real](raw: Raw)(implicit asRealRpc: AsRealRpc[Real]): Real = asRealRpc.asReal(raw) + def asRaw[Real](real: Real)(implicit asRawRpc: AsRawRpc[Real]): Raw = asRawRpc.asRaw(real) + def materializeAsRaw[Real]: AsRawRpc[Real] = macro RpcMacros.rpcAsRaw[Raw, Real] def materializeAsReal[Real]: AsRealRpc[Real] = macro RpcMacros.rpcAsReal[Raw, Real] def materializeAsRawReal[Real]: AsRawRealRpc[Real] = macro RpcMacros.rpcAsRawReal[Raw, Real] diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala index 2aa9beb4a..e5b5b98cd 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala @@ -4,6 +4,8 @@ package rpc import com.avsystem.commons.macros.rpc.RpcMacros trait RpcMetadataCompanion[M[_]] extends RpcImplicitsProvider { + def apply[Real](implicit metadata: M[Real]): M[Real] = metadata + def materializeForRpc[Real]: M[Real] = macro RpcMacros.rpcMetadata[M[Real], Real] final class Lazy[Real](metadata: => M[Real]) { From c0ef6de3133727dbb17e636cdddbb2bb36eb02b5 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 13:32:32 +0200 Subject: [PATCH 18/91] replaced BodyValue with separate JsonValue and mime-type aware HttpBody --- .../com/avsystem/commons/rest/rest.scala | 107 ++++++++++-------- .../commons/jetty/rpc/RestServlet.scala | 6 +- 2 files changed, 63 insertions(+), 50 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 666269528..9968ae5e9 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -4,6 +4,7 @@ package rest import com.avsystem.commons.annotation.AnnotationAggregate import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx} import com.avsystem.commons.rpc._ +import com.avsystem.commons.serialization.GenCodec.ReadFailure import com.avsystem.commons.serialization.json.{JsonReader, JsonStringInput, JsonStringOutput} import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} @@ -33,7 +34,7 @@ final class Path extends RestParamTag final class Header extends RestParamTag final class Query extends RestParamTag sealed trait BodyTag extends RestParamTag -final class BodyParam extends BodyTag +final class JsonBodyParam extends BodyTag final class Body extends BodyTag sealed trait RestValue extends Any { @@ -42,45 +43,58 @@ sealed trait RestValue extends Any { case class PathValue(value: String) extends AnyVal with RestValue case class HeaderValue(value: String) extends AnyVal with RestValue case class QueryValue(value: String) extends AnyVal with RestValue -case class BodyValue(value: String) extends AnyVal with RestValue -object BodyValue { - def combineIntoObject(fields: BIterable[(String, BodyValue)]): BodyValue = { - if (fields.isEmpty) { - BodyValue("") - } else { +case class JsonValue(value: String) extends AnyVal with RestValue +object RestValue { + implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = + AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = + AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[QueryValue, T] = + AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def jsonValueDefaultAsRealRaw[T: GenCodec]: AsRawReal[JsonValue, T] = + AsRawReal.create(v => JsonValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) +} + +case class HttpBody(value: String, mimeType: String) { + def jsonValue: String = mimeType match { + case HttpBody.JsonType => value + case _ => throw new ReadFailure(s"Expected application/json type, got $mimeType") + } +} +object HttpBody { + final val PlainType = "text/plain" + final val JsonType = "application/json" + + def plain(value: String): HttpBody = HttpBody(value, PlainType) + def json(value: String): HttpBody = HttpBody(value, JsonType) + + final val Empty: HttpBody = HttpBody.plain("") + + def createJsonBody(fields: BIterable[(String, JsonValue)]): HttpBody = + if (fields.isEmpty) HttpBody.Empty else { val sb = new JStringBuilder val oo = new JsonStringOutput(sb).writeObject() fields.foreach { - case (key, BodyValue(json)) => + case (key, JsonValue(json)) => oo.writeField(key).writeRawJson(json) } oo.finish() - BodyValue(sb.toString) + HttpBody.json(sb.toString) } - } - def uncombineFromObject(body: BodyValue): ListMap[String, BodyValue] = { - if (body.value.isEmpty) { - ListMap.empty - } else { - val oi = new JsonStringInput(new JsonReader(body.value)).readObject() - val builder = ListMap.newBuilder[String, BodyValue] + + def parseJsonBody(body: HttpBody): ListMap[String, JsonValue] = + if (body.value.isEmpty) ListMap.empty else { + val oi = new JsonStringInput(new JsonReader(body.jsonValue)).readObject() + val builder = ListMap.newBuilder[String, JsonValue] while (oi.hasNext) { val fi = oi.nextField() - builder += ((fi.fieldName, BodyValue(fi.readRawJson()))) + builder += ((fi.fieldName, JsonValue(fi.readRawJson()))) } builder.result() } - } -} -object RestValue { - implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = - AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = - AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[QueryValue, T] = - AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def bodyValueDefaultAsRealRaw[T: GenCodec]: AsRawReal[BodyValue, T] = - AsRawReal.create(v => BodyValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) + + implicit def httpBodyJsonAsRawReal[T: GenCodec]: AsRawReal[HttpBody, T] = + AsRawReal.create(v => HttpBody.json(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.jsonValue)) } final class HttpRestMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum { @@ -131,21 +145,20 @@ object RestHeaders { class HttpErrorException(code: Int, payload: String) extends RuntimeException(s"$code: $payload") -case class RestRequest(method: HttpRestMethod, headers: RestHeaders, body: BodyValue) -case class RestResponse(code: Int, body: BodyValue) +case class RestRequest(method: HttpRestMethod, headers: RestHeaders, body: HttpBody) +case class RestResponse(code: Int, body: HttpBody) object RestResponse { - implicit def genCodecBasedFutureAsRawReal[T: GenCodec]: AsRawReal[Future[RestResponse], Future[T]] = - AsRawReal.create( - _.mapNow(v => RestResponse(200, BodyValue(JsonStringOutput.write[T](v)))), - _.mapNow { - case RestResponse(200, BodyValue(json)) => JsonStringInput.read[T](json) - case RestResponse(code, BodyValue(payload)) => throw new HttpErrorException(code, payload) - } - ) + implicit def defaultFutureAsRaw[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[Future[RestResponse], Future[T]] = + AsRaw.create(_.mapNow(v => RestResponse(200, bodyAsRaw.asRaw(v)))) + implicit def defaultFutureAsReal[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[Future[RestResponse], Future[T]] = + AsReal.create(_.mapNow { + case RestResponse(200, body) => bodyAsReal.asReal(body) + case RestResponse(code, body) => throw new HttpErrorException(code, body.value) + }) } @methodTag[RestMethodTag, Prefix] -@paramTag[RestParamTag, BodyParam] +@paramTag[RestParamTag, JsonBodyParam] trait RawRest { @multi @tagged[Prefix] @@ -155,12 +168,12 @@ trait RawRest { @multi @tagged[HttpMethodTag] def handle(@methodName name: String, @composite headers: RestHeaders, - @multi @tagged[BodyParam] body: IListMap[String, BodyValue]): Future[RestResponse] + @multi @tagged[JsonBodyParam] body: IListMap[String, JsonValue]): Future[RestResponse] @multi @tagged[HttpMethodTag] def handleSingle(@methodName name: String, @composite headers: RestHeaders, - @encoded @tagged[Body] body: BodyValue): Future[RestResponse] + @encoded @tagged[Body] body: HttpBody): Future[RestResponse] def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = request => { val (pathName, headers) = request.headers.extractPathName @@ -176,12 +189,12 @@ trait RawRest { val rpcName = request.method.toRpcName(pathName) metadata.httpMethods.get(rpcName).map { httpMeta => if (httpMeta.singleBody) handleSingle(rpcName, headers, request.body) - else handle(rpcName, headers, BodyValue.uncombineFromObject(request.body)) + else handle(rpcName, headers, HttpBody.parseJsonBody(request.body)) } } def notFound: Future[RestResponse] = - Future.successful(RestResponse(404, BodyValue(s"path ${pathName.value} not found"))) + Future.successful(RestResponse(404, HttpBody.plain(s"path ${pathName.value} not found"))) forPrefix orElse forHttpMethod getOrElse notFound } @@ -200,12 +213,12 @@ object RawRest extends RawRpcCompanion[RawRest] { def prefix(name: String, headers: RestHeaders): RawRest = new DefaultRawRest(prefixHeaders.append(PathValue(name), headers), handleRequest) - def handle(name: String, headers: RestHeaders, body: IListMap[String, BodyValue]): Future[RestResponse] = { + def handle(name: String, headers: RestHeaders, body: IListMap[String, JsonValue]): Future[RestResponse] = { val (method, pathName) = HttpRestMethod.parseRpcName(name) - handleRequest(RestRequest(method, prefixHeaders.append(pathName, headers), BodyValue.combineIntoObject(body))) + handleRequest(RestRequest(method, prefixHeaders.append(pathName, headers), HttpBody.createJsonBody(body))) } - def handleSingle(name: String, headers: RestHeaders, body: BodyValue): Future[RestResponse] = { + def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Future[RestResponse] = { val (method, pathName) = HttpRestMethod.parseRpcName(name) handleRequest(RestRequest(method, prefixHeaders.append(pathName, headers), body)) } @@ -248,7 +261,7 @@ abstract class RestApiCompanion[Real](implicit instances: RawRest.FullMacroInsta } @methodTag[RestMethodTag, Prefix] -@paramTag[RestParamTag, BodyParam] +@paramTag[RestParamTag, JsonBodyParam] case class RestMetadata[T]( @multi @tagged[Prefix] prefixMethods: Map[String, PrefixMetadata[_]], @multi @tagged[HttpMethodTag] httpMethods: Map[String, HttpMethodMetadata[_]] diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index 88b348369..2474f41d7 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -3,7 +3,7 @@ package jetty.rpc import java.util.regex.Pattern -import com.avsystem.commons.rest.{BodyValue, HeaderValue, HttpRestMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} +import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpRestMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.http.{HttpHeader, HttpStatus, MimeTypes} @@ -47,7 +47,7 @@ object RestServlet { Iterator.continually(bodyReader.read()) .takeWhile(_ != -1) .foreach(bodyBuilder.appendCodePoint) - val body = BodyValue(bodyBuilder.toString) + val body = HttpBody(bodyBuilder.toString, MimeTypes.getContentTypeWithoutCharset(request.getContentType)) val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) @@ -55,7 +55,7 @@ object RestServlet { handleRequest(restRequest).catchFailures.andThenNow { case Success(restResponse) => response.setStatus(restResponse.code) - response.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.APPLICATION_JSON_UTF_8.asString()) + response.addHeader(HttpHeader.CONTENT_TYPE.asString(), s"${restResponse.body.mimeType};charset=utf-8") response.getWriter.write(restResponse.body.value) case Failure(e) => response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) From 101eeef3c569d2dee8c270e40e517679f5392883 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 13:53:50 +0200 Subject: [PATCH 19/91] fixed empty content handling --- .../scala/com/avsystem/commons/jetty/rpc/RestServlet.scala | 5 ++++- .../com/avsystem/commons/jetty/rpc/RestServletTest.scala | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index 2474f41d7..d3551a480 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -47,7 +47,10 @@ object RestServlet { Iterator.continually(bodyReader.read()) .takeWhile(_ != -1) .foreach(bodyBuilder.appendCodePoint) - val body = HttpBody(bodyBuilder.toString, MimeTypes.getContentTypeWithoutCharset(request.getContentType)) + val bodyString = bodyBuilder.toString + val body = + if (bodyString.isEmpty && request.getContentType == null) HttpBody.Empty + else HttpBody(bodyString, MimeTypes.getContentTypeWithoutCharset(request.getContentType)) val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala index d372cdcd2..2a6a3c3d7 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package jetty.rpc +import java.nio.charset.StandardCharsets + import org.eclipse.jetty.client.util.StringContentProvider import org.eclipse.jetty.http.{HttpMethod, HttpStatus} import org.eclipse.jetty.server.Server @@ -30,7 +32,7 @@ class RestServletTest extends FunSuite with UsesHttpServer with UsesHttpClient { test("POST method") { val response = client.newRequest(s"$baseUrl/hello") .method(HttpMethod.POST) - .content(new StringContentProvider("""{"who":"World"}""")) + .content(new StringContentProvider("application/json", """{"who":"World"}""", StandardCharsets.UTF_8)) .send() assert(response.getContentAsString === """"Hello, World!"""") From 73dd4a6cfb2c5d32f6742911a0ae5ed1d80b9af1 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 14:11:18 +0200 Subject: [PATCH 20/91] cosmetic --- .../src/main/scala/com/avsystem/commons/rest/rest.scala | 2 +- .../scala/com/avsystem/commons/jetty/rpc/SomeApi.scala | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 9968ae5e9..e8ff5654f 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -204,7 +204,7 @@ object RawRest extends RawRpcCompanion[RawRest] { def fromHandleRequest[Real: AsRealRpc](handleRequest: RestRequest => Future[RestResponse]): Real = RawRest.asReal(RawRest(handleRequest)) - def toHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): RestRequest => Future[RestResponse] = + def asHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): RestRequest => Future[RestResponse] = RawRest.asRaw(real).asHandleRequest(RestMetadata[Real]) private final class DefaultRawRest( diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala index c2cd3d9e3..76fea910a 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala @@ -1,7 +1,7 @@ package com.avsystem.commons package jetty.rpc -import com.avsystem.commons.rest.{GET, POST, Query, RestApiCompanion, RestRequest, RestResponse} +import com.avsystem.commons.rest.{GET, POST, Query, RawRest, RestApiCompanion, RestRequest, RestResponse} import com.avsystem.commons.rpc.rpcName trait SomeApi { @@ -14,9 +14,8 @@ trait SomeApi { } object SomeApi extends RestApiCompanion[SomeApi] { - def asHandleRequest(real: SomeApi): RestRequest => Future[RestResponse] = { - restAsRealRaw.asRaw(real).asHandleRequest(restMetadata) - } + def asHandleRequest(real: SomeApi): RestRequest => Future[RestResponse] = + RawRest.asHandleRequest(real) def format(who: String) = s"Hello, $who!" val poison: String = "poison" From 7fc792b2291e7198fd70ba1d31a3cfa502233238 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 14:47:13 +0200 Subject: [PATCH 21/91] GET is now handled by separate raw method --- .../com/avsystem/commons/rest/rest.scala | 27 +++++++++++++------ .../avsystem/commons/rest/RawRestTest.scala | 4 +-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index e8ff5654f..1515eacc6 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -12,19 +12,20 @@ import scala.collection.immutable.ListMap sealed trait RestMethodTag extends RpcTag sealed trait HttpMethodTag extends RestMethodTag with AnnotationAggregate +sealed trait BodyMethodTag extends HttpMethodTag final class GET extends HttpMethodTag { @rpcNamePrefix("GET_") type Implied } -final class POST extends HttpMethodTag { +final class POST extends BodyMethodTag { @rpcNamePrefix("POST_") type Implied } -final class PATCH extends HttpMethodTag { +final class PATCH extends BodyMethodTag { @rpcNamePrefix("PATCH_") type Implied } -final class PUT extends HttpMethodTag { +final class PUT extends BodyMethodTag { @rpcNamePrefix("PUT_") type Implied } -final class DELETE extends HttpMethodTag { +final class DELETE extends BodyMethodTag { @rpcNamePrefix("DELETE_") type Implied } sealed trait Prefix extends RestMethodTag @@ -158,7 +159,7 @@ object RestResponse { } @methodTag[RestMethodTag, Prefix] -@paramTag[RestParamTag, JsonBodyParam] +@paramTag[RestParamTag, RestParamTag] trait RawRest { @multi @tagged[Prefix] @@ -166,12 +167,18 @@ trait RawRest { def prefix(@methodName name: String, @composite headers: RestHeaders): RawRest @multi - @tagged[HttpMethodTag] + @tagged[GET] + @paramTag[RestParamTag, Query] + def get(@methodName name: String, @composite headers: RestHeaders): Future[RestResponse] + + @multi + @tagged[BodyMethodTag] + @paramTag[RestParamTag, JsonBodyParam] def handle(@methodName name: String, @composite headers: RestHeaders, @multi @tagged[JsonBodyParam] body: IListMap[String, JsonValue]): Future[RestResponse] @multi - @tagged[HttpMethodTag] + @tagged[BodyMethodTag] def handleSingle(@methodName name: String, @composite headers: RestHeaders, @encoded @tagged[Body] body: HttpBody): Future[RestResponse] @@ -188,7 +195,8 @@ trait RawRest { def forHttpMethod: Option[Future[RestResponse]] = { val rpcName = request.method.toRpcName(pathName) metadata.httpMethods.get(rpcName).map { httpMeta => - if (httpMeta.singleBody) handleSingle(rpcName, headers, request.body) + if (request.method == HttpRestMethod.GET) get(rpcName, headers) + else if (httpMeta.singleBody) handleSingle(rpcName, headers, request.body) else handle(rpcName, headers, HttpBody.parseJsonBody(request.body)) } } @@ -213,6 +221,9 @@ object RawRest extends RawRpcCompanion[RawRest] { def prefix(name: String, headers: RestHeaders): RawRest = new DefaultRawRest(prefixHeaders.append(PathValue(name), headers), handleRequest) + def get(name: String, headers: RestHeaders): Future[RestResponse] = + handleSingle(name, headers, HttpBody.Empty) + def handle(name: String, headers: RestHeaders, body: IListMap[String, JsonValue]): Future[RestResponse] = { val (method, pathName) = HttpRestMethod.parseRpcName(name) handleRequest(RestRequest(method, prefixHeaders.append(pathName, headers), HttpBody.createJsonBody(body))) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index e1604adce..f16e72188 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -27,9 +27,9 @@ class RawRestTest extends FunSuite with ScalaFutures { val callRecord = new MListBuffer[(RestRequest, RestResponse)] - val real = new RestTestApiImpl(0, "") + val real: RestTestApi = new RestTestApiImpl(0, "") val serverHandle: RestRequest => Future[RestResponse] = request => { - RestTestApi.restAsRealRaw.asRaw(real).asHandleRequest(RestTestApi.restMetadata)(request).andThenNow { + RawRest.asHandleRequest(real).apply(request).andThenNow { case Success(response) => callRecord += ((request, response)) } } From 6529e73d94a0a62a663dbae717a221cfbcfd5fea Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 14:56:07 +0200 Subject: [PATCH 22/91] minor --- .../test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala index 76fea910a..bff151141 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala @@ -1,12 +1,12 @@ package com.avsystem.commons package jetty.rpc -import com.avsystem.commons.rest.{GET, POST, Query, RawRest, RestApiCompanion, RestRequest, RestResponse} +import com.avsystem.commons.rest.{GET, POST, RawRest, RestApiCompanion, RestRequest, RestResponse} import com.avsystem.commons.rpc.rpcName trait SomeApi { @GET - def hello(@Query who: String): Future[String] + def hello(who: String): Future[String] @POST @rpcName("hello") From 2d7b102046c18b66262acab67d2eb3d68f2a8c89 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 15:00:12 +0200 Subject: [PATCH 23/91] empty body encoding for Unit --- .../src/main/scala/com/avsystem/commons/rest/rest.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 1515eacc6..e486605cb 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -94,6 +94,8 @@ object HttpBody { builder.result() } + implicit val emptyBodyForUnit: AsRawReal[HttpBody, Unit] = + AsRawReal.create(_ => HttpBody.Empty, _ => ()) implicit def httpBodyJsonAsRawReal[T: GenCodec]: AsRawReal[HttpBody, T] = AsRawReal.create(v => HttpBody.json(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.jsonValue)) } From 201e50abe7f006c1f893675a75baf6bc228174a5 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 15:20:06 +0200 Subject: [PATCH 24/91] AsReal/Raw for HttpBody is by default based on AsReal/Raw for JsonValue --- .../scala/com/avsystem/commons/rest/rest.scala | 16 +++++++++------- .../com/avsystem/commons/rest/RawRestTest.scala | 3 +-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index e486605cb..20e6a126e 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -57,8 +57,8 @@ object RestValue { } case class HttpBody(value: String, mimeType: String) { - def jsonValue: String = mimeType match { - case HttpBody.JsonType => value + def jsonValue: JsonValue = mimeType match { + case HttpBody.JsonType => JsonValue(value) case _ => throw new ReadFailure(s"Expected application/json type, got $mimeType") } } @@ -67,7 +67,7 @@ object HttpBody { final val JsonType = "application/json" def plain(value: String): HttpBody = HttpBody(value, PlainType) - def json(value: String): HttpBody = HttpBody(value, JsonType) + def json(json: JsonValue): HttpBody = HttpBody(json.value, JsonType) final val Empty: HttpBody = HttpBody.plain("") @@ -80,12 +80,12 @@ object HttpBody { oo.writeField(key).writeRawJson(json) } oo.finish() - HttpBody.json(sb.toString) + HttpBody.json(JsonValue(sb.toString)) } def parseJsonBody(body: HttpBody): ListMap[String, JsonValue] = if (body.value.isEmpty) ListMap.empty else { - val oi = new JsonStringInput(new JsonReader(body.jsonValue)).readObject() + val oi = new JsonStringInput(new JsonReader(body.jsonValue.value)).readObject() val builder = ListMap.newBuilder[String, JsonValue] while (oi.hasNext) { val fi = oi.nextField() @@ -96,8 +96,10 @@ object HttpBody { implicit val emptyBodyForUnit: AsRawReal[HttpBody, Unit] = AsRawReal.create(_ => HttpBody.Empty, _ => ()) - implicit def httpBodyJsonAsRawReal[T: GenCodec]: AsRawReal[HttpBody, T] = - AsRawReal.create(v => HttpBody.json(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.jsonValue)) + implicit def httpBodyJsonAsRaw[T](implicit jsonAsRaw: AsRaw[JsonValue, T]): AsRaw[HttpBody, T] = + AsRaw.create(v => HttpBody.json(jsonAsRaw.asRaw(v))) + implicit def httpBodyJsonAsReal[T](implicit jsonAsReal: AsReal[JsonValue, T]): AsReal[HttpBody, T] = + AsReal.create(v => jsonAsReal.asReal(v.jsonValue)) } final class HttpRestMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum { diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index f16e72188..ffa9abbd0 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -34,8 +34,7 @@ class RawRestTest extends FunSuite with ScalaFutures { } } - val realProxy: RestTestApi = - RestTestApi.restAsRealRaw.asReal(RawRest(serverHandle)) + val realProxy: RestTestApi = RawRest.fromHandleRequest[RestTestApi](serverHandle) def assertSame[T](call: RestTestApi => Future[T])(implicit pos: Position): Unit = assert(call(realProxy).futureValue == call(real).futureValue) From e0d5e1005f9db74d47b32ac1c8e34f472bbc8ac8 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 4 Jul 2018 20:23:55 +0200 Subject: [PATCH 25/91] arbitrary multi-level paths independent from rpcName for REST methods --- .../com/avsystem/commons/rest/rest.scala | 223 +++++++++++------- .../avsystem/commons/rest/RawRestTest.scala | 19 +- .../commons/jetty/rpc/RestServlet.scala | 4 +- .../avsystem/commons/jetty/rpc/SomeApi.scala | 4 +- .../commons/macros/rpc/RpcMacros.scala | 2 +- .../commons/macros/rpc/RpcMetadatas.scala | 33 +-- 6 files changed, 167 insertions(+), 118 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 20e6a126e..55ca18f37 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -10,25 +10,28 @@ import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} import scala.collection.immutable.ListMap -sealed trait RestMethodTag extends RpcTag -sealed trait HttpMethodTag extends RestMethodTag with AnnotationAggregate -sealed trait BodyMethodTag extends HttpMethodTag -final class GET extends HttpMethodTag { +sealed trait RestMethodTag extends RpcTag { + def path: OptArg[String] +} +sealed abstract class HttpMethodTag(val method: HttpMethod) extends RestMethodTag with AnnotationAggregate +sealed abstract class BodyMethodTag(method: HttpMethod) extends HttpMethodTag(method) + +final class GET(val path: OptArg[String] = OptArg.Empty) extends HttpMethodTag(HttpMethod.GET) { @rpcNamePrefix("GET_") type Implied } -final class POST extends BodyMethodTag { +final class POST(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.POST) { @rpcNamePrefix("POST_") type Implied } -final class PATCH extends BodyMethodTag { +final class PATCH(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.PATCH) { @rpcNamePrefix("PATCH_") type Implied } -final class PUT extends BodyMethodTag { +final class PUT(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.PUT) { @rpcNamePrefix("PUT_") type Implied } -final class DELETE extends BodyMethodTag { +final class DELETE(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.DELETE) { @rpcNamePrefix("DELETE_") type Implied } -sealed trait Prefix extends RestMethodTag +final class Prefix(val path: OptArg[String] = OptArg.Empty) extends RestMethodTag sealed trait RestParamTag extends RpcTag final class Path extends RestParamTag @@ -102,22 +105,9 @@ object HttpBody { AsReal.create(v => jsonAsReal.asReal(v.jsonValue)) } -final class HttpRestMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum { - def toRpcName(pathName: PathValue): String = - s"${name}_${pathName.value}" -} -object HttpRestMethod extends AbstractValueEnumCompanion[HttpRestMethod] { - final val GET, PUT, POST, PATCH, DELETE: Value = new HttpRestMethod - - def parseRpcName(rpcName: String): (HttpRestMethod, PathValue) = rpcName.split("_", 2) match { - case Array(httpMethod, pathName) => - val httpRestMethod = byName.getOrElse(httpMethod, - throw new IllegalArgumentException(s"Unknown REST HTTP method: $httpMethod")) - val pathValue = PathValue(pathName) - (httpRestMethod, pathValue) - case _ => - throw new IllegalArgumentException(s"Bad RPC name for REST method: $rpcName") - } +final class HttpMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum +object HttpMethod extends AbstractValueEnumCompanion[HttpMethod] { + final val GET, PUT, POST, PATCH, DELETE: Value = new HttpMethod } case class RestHeaders( @@ -125,23 +115,11 @@ case class RestHeaders( @multi @tagged[Header] headers: ListMap[String, HeaderValue], @multi @tagged[Query] query: ListMap[String, QueryValue] ) { - def append(pathName: PathValue, otherHeaders: RestHeaders): RestHeaders = RestHeaders( - path ++ (pathName :: otherHeaders.path), + def append(methodPath: List[PathValue], otherHeaders: RestHeaders): RestHeaders = RestHeaders( + path ::: methodPath ::: otherHeaders.path, headers ++ otherHeaders.headers, query ++ otherHeaders.query ) - - def extractPathName: (PathValue, RestHeaders) = path match { - case pathName :: pathTail => - (pathName, copy(path = pathTail)) - case Nil => - throw new IllegalArgumentException("empty path") - } - - def extractPrefix(metadata: RestHeadersMetadata): (RestHeaders, RestHeaders) = { - val (prefixPath, tailPath) = path.splitAt(metadata.pathSize) - (copy(path = prefixPath), copy(path = tailPath)) - } } object RestHeaders { final val Empty = RestHeaders(Nil, ListMap.empty, ListMap.empty) @@ -150,7 +128,7 @@ object RestHeaders { class HttpErrorException(code: Int, payload: String) extends RuntimeException(s"$code: $payload") -case class RestRequest(method: HttpRestMethod, headers: RestHeaders, body: HttpBody) +case class RestRequest(method: HttpMethod, headers: RestHeaders, body: HttpBody) case class RestResponse(code: Int, body: HttpBody) object RestResponse { implicit def defaultFutureAsRaw[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[Future[RestResponse], Future[T]] = @@ -186,63 +164,71 @@ trait RawRest { def handleSingle(@methodName name: String, @composite headers: RestHeaders, @encoded @tagged[Body] body: HttpBody): Future[RestResponse] - def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = request => { - val (pathName, headers) = request.headers.extractPathName - - def forPrefix: Option[Future[RestResponse]] = - metadata.prefixMethods.get(pathName.value).map { prefixMeta => - val (prefixHeaders, restOfHeaders) = headers.extractPrefix(prefixMeta.headersMetadata) - prefix(pathName.value, prefixHeaders) - .asHandleRequest(prefixMeta.result.value)(request.copy(headers = restOfHeaders)) - } - - def forHttpMethod: Option[Future[RestResponse]] = { - val rpcName = request.method.toRpcName(pathName) - metadata.httpMethods.get(rpcName).map { httpMeta => - if (request.method == HttpRestMethod.GET) get(rpcName, headers) - else if (httpMeta.singleBody) handleSingle(rpcName, headers, request.body) - else handle(rpcName, headers, HttpBody.parseJsonBody(request.body)) - } + def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = { + case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { + case List(ResolvedPath(prefixes, RpcWithPath(finalRpcName, finalPathParams), singleBody)) => + val finalRawRest = prefixes.foldLeft(this) { + case (rawRest, RpcWithPath(rpcName, pathParams)) => + rawRest.prefix(rpcName, headers.copy(path = pathParams)) + } + val finalHeaders = headers.copy(path = finalPathParams) + + if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) + else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) + else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) + + case Nil => + val pathStr = headers.path.iterator.map(_.value).mkString("/") + Future.successful(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) + + case multiple => + val pathStr = headers.path.iterator.map(_.value).mkString("/") + val callsRepr = multiple.iterator.map(p => s" ${p.rpcChainRepr}").mkString("\n", "\n", "") + throw new IllegalArgumentException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") } - - def notFound: Future[RestResponse] = - Future.successful(RestResponse(404, HttpBody.plain(s"path ${pathName.value} not found"))) - - forPrefix orElse forHttpMethod getOrElse notFound } } object RawRest extends RawRpcCompanion[RawRest] { - def fromHandleRequest[Real: AsRealRpc](handleRequest: RestRequest => Future[RestResponse]): Real = - RawRest.asReal(RawRest(handleRequest)) + def fromHandleRequest[Real: AsRealRpc : RestMetadata](handleRequest: RestRequest => Future[RestResponse]): Real = + RawRest.asReal(new DefaultRawRest(RestMetadata[Real], RestHeaders.Empty, handleRequest)) def asHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): RestRequest => Future[RestResponse] = RawRest.asRaw(real).asHandleRequest(RestMetadata[Real]) private final class DefaultRawRest( - prefixHeaders: RestHeaders, handleRequest: RestRequest => Future[RestResponse]) extends RawRest { - - def prefix(name: String, headers: RestHeaders): RawRest = - new DefaultRawRest(prefixHeaders.append(PathValue(name), headers), handleRequest) + metadata: RestMetadata[_], + prefixHeaders: RestHeaders, + handleRequest: RestRequest => Future[RestResponse] + ) extends RawRest { + + def prefix(name: String, headers: RestHeaders): RawRest = { + val prefixMeta = metadata.prefixMethods.getOrElse(name, + throw new IllegalArgumentException(s"no such prefix method: $name")) + val newHeaders = prefixHeaders.append(prefixMeta.methodPath, headers) + new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest) + } def get(name: String, headers: RestHeaders): Future[RestResponse] = handleSingle(name, headers, HttpBody.Empty) def handle(name: String, headers: RestHeaders, body: IListMap[String, JsonValue]): Future[RestResponse] = { - val (method, pathName) = HttpRestMethod.parseRpcName(name) - handleRequest(RestRequest(method, prefixHeaders.append(pathName, headers), HttpBody.createJsonBody(body))) + val methodMeta = metadata.httpMethods.getOrElse(name, + throw new IllegalArgumentException(s"no such HTTP method: $name")) + val newHeaders = prefixHeaders.append(methodMeta.methodPath, headers) + handleRequest(RestRequest(methodMeta.method, newHeaders, HttpBody.createJsonBody(body))) } def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Future[RestResponse] = { - val (method, pathName) = HttpRestMethod.parseRpcName(name) - handleRequest(RestRequest(method, prefixHeaders.append(pathName, headers), body)) + val methodMeta = metadata.httpMethods.getOrElse(name, + throw new IllegalArgumentException(s"no such HTTP method: $name")) + val newHeaders = prefixHeaders.append(methodMeta.methodPath, headers) + handleRequest(RestRequest(methodMeta.method, newHeaders, body)) } } - def apply(handleRequest: RestRequest => Future[RestResponse]): RawRest = - new DefaultRawRest(RestHeaders.Empty, handleRequest) - trait ClientMacroInstances[Real] { + def metadata: RestMetadata[Real] def asReal: AsRealRpc[Real] } @@ -251,10 +237,7 @@ object RawRest extends RawRpcCompanion[RawRest] { def asRaw: AsRawRpc[Real] } - trait FullMacroInstances[Real] { - def metadata: RestMetadata[Real] - def asRawReal: AsRawRealRpc[Real] - } + trait FullMacroInstances[Real] extends ClientMacroInstances[Real] with ServerMacroInstances[Real] implicit def clientInstances[Real]: ClientMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] implicit def serverInstances[Real]: ServerMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] @@ -262,6 +245,7 @@ object RawRest extends RawRpcCompanion[RawRest] { } abstract class RestClientApiCompanion[Real](implicit instances: RawRest.ClientMacroInstances[Real]) { + implicit def restMetadata: RestMetadata[Real] = instances.metadata implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal } @@ -272,7 +256,17 @@ abstract class RestServerApiCompanion[Real](implicit instances: RawRest.ServerMa abstract class RestApiCompanion[Real](implicit instances: RawRest.FullMacroInstances[Real]) { implicit def restMetadata: RestMetadata[Real] = instances.metadata - implicit def restAsRealRaw: RawRest.AsRawRealRpc[Real] = instances.asRawReal + implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal + implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw +} + +case class RpcWithPath(rpcName: String, pathParams: List[PathValue]) +case class ResolvedPath(prefixes: List[RpcWithPath], finalCall: RpcWithPath, singleBody: Boolean) { + def prepend(rpcName: String, pathParams: List[PathValue]): ResolvedPath = + copy(prefixes = RpcWithPath(rpcName, pathParams) :: prefixes) + + def rpcChainRepr: String = + prefixes.iterator.map(_.rpcName).mkString("", "->", s"->${finalCall.rpcName}") } @methodTag[RestMethodTag, Prefix] @@ -280,22 +274,62 @@ abstract class RestApiCompanion[Real](implicit instances: RawRest.FullMacroInsta case class RestMetadata[T]( @multi @tagged[Prefix] prefixMethods: Map[String, PrefixMetadata[_]], @multi @tagged[HttpMethodTag] httpMethods: Map[String, HttpMethodMetadata[_]] -) +) { + def resolvePath(method: HttpMethod, path: List[PathValue]): Iterator[ResolvedPath] = { + val asFinalCall = for { + (rpcName, m) <- httpMethods.iterator if m.method == method + (pathParams, Nil) <- m.extractPathParams(path) + } yield ResolvedPath(Nil, RpcWithPath(rpcName, pathParams), m.singleBody) + + val usingPrefix = for { + (rpcName, prefix) <- prefixMethods.iterator + (pathParams, pathTail) <- prefix.extractPathParams(path).iterator + suffixPath <- prefix.result.value.resolvePath(method, pathTail) + } yield suffixPath.prepend(rpcName, pathParams) + + asFinalCall ++ usingPrefix + } +} object RestMetadata extends RpcMetadataCompanion[RestMetadata] +sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { + def name: String + def tagPath: Opt[String] + def headersMetadata: RestHeadersMetadata + + val methodPath: List[PathValue] = + tagPath.fold(List(PathValue(name)))(_.split("/").iterator.filter(_.nonEmpty).map(PathValue).toList) + + def extractPathParams(path: List[PathValue]): Opt[(List[PathValue], List[PathValue])] = { + def suffix(prefix: List[PathValue], path: List[PathValue]): Opt[List[PathValue]] = (prefix, path) match { + case (Nil, result) => Opt(result) + case (prefixHead :: prefixTail, pathHead :: pathTail) if prefixHead == pathHead => + suffix(prefixTail, pathTail) + case _ => Opt.Empty + } + suffix(methodPath, path).flatMap(headersMetadata.extractPathParams) + } +} + @paramTag[RestParamTag, Path] case class PrefixMetadata[T]( + @reifyName name: String, + @optional @reifyAnnot methodTag: Opt[Prefix], @composite headersMetadata: RestHeadersMetadata, @checked @infer result: RestMetadata.Lazy[T] -) extends TypedMetadata[T] +) extends RestMethodMetadata[T] { + def tagPath: Opt[String] = methodTag.flatMap(_.path.toOpt) +} case class HttpMethodMetadata[T]( - @reifyName(rpcName = true) rpcName: String, - @composite headersParams: RestHeadersMetadata, + @reifyName name: String, + @reifyAnnot methodTag: HttpMethodTag, + @composite headersMetadata: RestHeadersMetadata, @multi @tagged[BodyTag] bodyParams: Map[String, ParamMetadata[_]] -) extends TypedMetadata[Future[T]] { - val (httpMethod, pathName) = HttpRestMethod.parseRpcName(rpcName) +) extends RestMethodMetadata[Future[T]] { + val method: HttpMethod = methodTag.method val singleBody: Boolean = bodyParams.values.exists(_.singleBody) + def tagPath: Opt[String] = methodTag.path.toOpt } case class RestHeadersMetadata( @@ -303,9 +337,18 @@ case class RestHeadersMetadata( @multi @tagged[Header] headers: Map[String, ParamMetadata[_]], @multi @tagged[Query] query: Map[String, ParamMetadata[_]] ) { - val pathSize: Int = path.size + val pathLength: Int = path.size + + def extractPathParams(path: List[PathValue]): Opt[(List[PathValue], List[PathValue])] = { + def loop(acc: List[PathValue], path: List[PathValue], index: Int): Opt[(List[PathValue], List[PathValue])] = + if (index == 0) Opt((acc.reverse, path)) + else path match { + case head :: tail => loop(head :: acc, tail, index - 1) + case Nil => Opt.Empty + } + loop(Nil, path, pathLength) + } } -case class ParamMetadata[T]( - @hasAnnot[Body] singleBody: Boolean -) extends TypedMetadata[T] +case class ParamMetadata[T](@hasAnnot[Body] singleBody: Boolean) + extends TypedMetadata[T] diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index ffa9abbd0..32e40b744 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -9,18 +9,23 @@ import org.scalatest.concurrent.ScalaFutures case class User(id: String, name: String) object User extends HasGenCodec[User] -trait RestTestApi { - def subApi(id: Int, @Query query: String): RestTestApi - +trait UserApi { @GET def user(userId: String): Future[User] - @POST def user(@Body user: User): Future[Unit] + @POST("user/save") def user(@Body user: User): Future[Unit] +} +object UserApi extends RestApiCompanion[UserApi] + +trait RestTestApi { + @Prefix("") def self: UserApi + def subApi(id: Int, @Query query: String): UserApi } object RestTestApi extends RestApiCompanion[RestTestApi] class RawRestTest extends FunSuite with ScalaFutures { test("round trip test") { - class RestTestApiImpl(id: Int, query: String) extends RestTestApi { - def subApi(newId: Int, newQuery: String): RestTestApi = new RestTestApiImpl(newId, query + newQuery) + class RestTestApiImpl(id: Int, query: String) extends RestTestApi with UserApi { + def self: UserApi = this + def subApi(newId: Int, newQuery: String): UserApi = new RestTestApiImpl(newId, query + newQuery) def user(userId: String): Future[User] = Future.successful(User(userId, s"$userId-$id-$query")) def user(user: User): Future[Unit] = Future.unit } @@ -39,7 +44,7 @@ class RawRestTest extends FunSuite with ScalaFutures { def assertSame[T](call: RestTestApi => Future[T])(implicit pos: Position): Unit = assert(call(realProxy).futureValue == call(real).futureValue) - assertSame(_.user("ID")) + assertSame(_.self.user("ID")) assertSame(_.subApi(1, "query").user("ID")) } } diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index d3551a480..f2a1d1e3b 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -3,7 +3,7 @@ package jetty.rpc import java.util.regex.Pattern -import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpRestMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} +import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.http.{HttpHeader, HttpStatus, MimeTypes} @@ -21,7 +21,7 @@ object RestServlet { request: HttpServletRequest, response: HttpServletResponse ): Unit = { - val method = HttpRestMethod.byName(request.getMethod) + val method = HttpMethod.byName(request.getMethod) val path = separatorPattern .splitAsStream(request.getPathInfo) diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala index bff151141..169622d98 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala @@ -2,14 +2,12 @@ package com.avsystem.commons package jetty.rpc import com.avsystem.commons.rest.{GET, POST, RawRest, RestApiCompanion, RestRequest, RestResponse} -import com.avsystem.commons.rpc.rpcName trait SomeApi { @GET def hello(who: String): Future[String] - @POST - @rpcName("hello") + @POST("hello") def helloThere(who: String): Future[String] } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index ce02002a0..d8f0a5e68 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -70,7 +70,7 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo } def containsInaccessibleThises(tree: Tree): Boolean = tree.exists { - case t@This(_) if !enclosingClasses.contains(t.symbol) => true + case t@This(_) if !t.symbol.isPackageClass && !enclosingClasses.contains(t.symbol) => true case _ => false } } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index 890b4142b..a323aacde 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -338,23 +338,26 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem(s"${arity.collectedType} is not a subtype of StaticAnnotation") } - def validated(annot: Annot): Annot = { - if (containsInaccessibleThises(annot.tree)) { - reportProblem(s"reified annotation must not contain this-references inaccessible outside RPC trait") + def materializeFor(rpcSym: Real): Tree = { + def validated(annot: Annot): Annot = { + if (containsInaccessibleThises(annot.tree)) { + echo(showCode(annot.tree)) + rpcSym.reportProblem(s"reified annotation contains this-references inaccessible outside RPC trait") + } + annot } - annot - } - def materializeFor(rpcSym: Real): Tree = arity match { - case RpcParamArity.Single(annotTpe) => - rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree)).getOrElse { - val msg = s"${rpcSym.problemStr}: cannot materialize value for $description: no annotation of type $annotTpe found" - q"$RpcUtils.compilationError(${StringLiteral(msg, rpcSym.pos)})" - } - case RpcParamArity.Optional(annotTpe) => - mkOptional(rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree))) - case RpcParamArity.Multi(annotTpe, _) => - mkMulti(allAnnotations(rpcSym.symbol, annotTpe).map(a => c.untypecheck(validated(a).tree))) + arity match { + case RpcParamArity.Single(annotTpe) => + rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree)).getOrElse { + val msg = s"${rpcSym.problemStr}: cannot materialize value for $description: no annotation of type $annotTpe found" + q"$RpcUtils.compilationError(${StringLiteral(msg, rpcSym.pos)})" + } + case RpcParamArity.Optional(annotTpe) => + mkOptional(rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree))) + case RpcParamArity.Multi(annotTpe, _) => + mkMulti(allAnnotations(rpcSym.symbol, annotTpe).map(a => c.untypecheck(validated(a).tree))) + } } def tryMaterializeFor(rpcSym: Real): Res[Tree] = From 0e2007f1dc82f5c491e38b333958df7cf426aad4 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 5 Jul 2018 13:22:30 +0200 Subject: [PATCH 26/91] rest scaladocs, introduced NamedParams instead of ListMap --- .../com/avsystem/commons/rest/rest.scala | 171 ++++++++++++++++-- .../avsystem/commons/rpc/NamedParams.scala | 55 ++++++ .../commons/jetty/rpc/RestServlet.scala | 5 +- 3 files changed, 217 insertions(+), 14 deletions(-) create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 55ca18f37..0acda73dc 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -8,46 +8,172 @@ import com.avsystem.commons.serialization.GenCodec.ReadFailure import com.avsystem.commons.serialization.json.{JsonReader, JsonStringInput, JsonStringOutput} import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} -import scala.collection.immutable.ListMap - +/** + * Base trait for tag annotations that determine how a REST method is translated into actual HTTP request. + * A REST method may be annotated with one of HTTP method tags ([[GET]], [[PUT]], [[POST]], [[PATCH]], [[DELETE]]) + * which means that this method represents actual HTTP call and is expected to return a `Future[Result]` where + * `Result` is encodable as [[RestResponse]]. + * + * If a REST method is not annotated with any of HTTP method tags, [[Prefix]] is assumed by default which means + * that this method only contributes to URL path, HTTP headers and query parameters but does not yet represent an + * actual HTTP request. Instead, it is expected to return some other REST API trait. + */ sealed trait RestMethodTag extends RpcTag { + /** + * HTTP URL path segment associated with REST method annotated with this tag. This path may be multipart + * (i.e. contain slashes). It may also be empty which means that this particular REST method does not contribute + * anything to URL path. If path is not specified explicitly, method name is used (the actual method name, not + * [[rpcName]]). + * + * @example + * {{{ + * trait SomeRestApi { + * @GET("users/find") + * def findUser(userId: String): Future[User] + * } + * object SomeRestApi extends RestApiCompanion[SomeRestApi] + * }}} + */ def path: OptArg[String] } + sealed abstract class HttpMethodTag(val method: HttpMethod) extends RestMethodTag with AnnotationAggregate + +/** + * Base trait for annotations representing HTTP methods which may define a HTTP body. This includes + * [[PUT]], [[POST]], [[PATCH]] and [[DELETE]]. Parameters of REST methods annotated with one of these tags are + * by default serialized into JSON (through encoding to [[JsonValue]]) and combined into JSON object that is sent + * as HTTP body. + * + * Parameters may also contribute to URL path, HTTP headers and query parameters if annotated as + * [[Path]], [[Header]] or [[Query]]. + * + * REST method may also take a single parameter representing the entire HTTP body. Such parameter must be annotated + * as [[Body]] and must be the only body parameter of that method. Value of this parameter will be encoded as + * [[HttpBody]] which doesn't necessarily have to be JSON (it may define its own MIME type). + * + * @example + * {{{ + * trait SomeRestApi { + * @POST("users/create") def createUser(@Body user: User): Future[Unit] + * @PATCH("users/update") def updateUser(id: String, name: String): Future[User] + * } + * object SomeRestApi extends RestApiCompanion[SomeRestApi] + * }}} + */ sealed abstract class BodyMethodTag(method: HttpMethod) extends HttpMethodTag(method) +/** + * REST method annotated as `@GET` will translate to HTTP GET request. By default, parameters of such method + * are translated into URL query parameters (encoded as [[QueryValue]]). Alternatively, each parameter + * may be annotated as [[Path]] or [[Header]] which means that it will be translated into HTTP header value + * + * @param path see [[RestMethodTag.path]] + */ final class GET(val path: OptArg[String] = OptArg.Empty) extends HttpMethodTag(HttpMethod.GET) { @rpcNamePrefix("GET_") type Implied } + +/** See [[BodyMethodTag]] */ final class POST(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.POST) { @rpcNamePrefix("POST_") type Implied } +/** See [[BodyMethodTag]] */ final class PATCH(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.PATCH) { @rpcNamePrefix("PATCH_") type Implied } +/** See [[BodyMethodTag]] */ final class PUT(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.PUT) { @rpcNamePrefix("PUT_") type Implied } +/** See [[BodyMethodTag]] */ final class DELETE(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.DELETE) { @rpcNamePrefix("DELETE_") type Implied } + +/** + * REST methods annotated as [[Prefix]] are expected to return another REST API trait as their result. + * They do not yet represent an actual HTTP request but contribute to URL path, HTTP headers and query parameters. + * + * By default, parameters of a prefix method are interpreted as URL path fragments. Their values are encoded as + * [[PathValue]] and appended to URL path. Alternatively, each parameter may also be explicitly annotated as + * [[Header]] or [[Query]]. + * + * NOTE: REST method is interpreted as prefix method by default which means that there is no need to apply [[Prefix]] + * annotation explicitly unless you want to specify a custom path. + * + * @param path see [[RestMethodTag.path]] + */ final class Prefix(val path: OptArg[String] = OptArg.Empty) extends RestMethodTag sealed trait RestParamTag extends RpcTag + +/** + * REST method parameters annotated as [[Path]] will be encoded as [[PathValue]] and appended to URL path, in the + * declaration order. Parameters of [[Prefix]] REST methods are interpreted as [[Path]] parameters by default. + */ final class Path extends RestParamTag -final class Header extends RestParamTag + +/** + * REST method parameters annotated as [[Header]] will be encoded as [[HeaderValue]] and added to HTTP headers. + * Header name must be explicitly given as argument of this annotation. + */ +final class Header(override val name: String) extends rpcName(name) with RestParamTag + +/** + * REST method parameters annotated as [[Query]] will be encoded as [[QueryValue]] and added to URL query + * parameters. Parameters of [[GET]] REST methods are interpreted as [[Query]] parameters by default. + */ final class Query extends RestParamTag sealed trait BodyTag extends RestParamTag + +/** + * REST method parameters annotated as [[JsonBodyParam]] will be encoded as [[JsonValue]] and combined into + * a JSON object that will be sent as HTTP body. Body parameters are allowed only in REST methods annotated as + * [[POST]], [[PATCH]], [[PUT]] or [[DELETE]]. Actually, parameters of these methods are interpreted as + * [[JsonBodyParam]] by default which means that this annotation rarely needs to be applied explicitly. + */ final class JsonBodyParam extends BodyTag + +/** + * REST methods that can send HTTP body ([[POST]], [[PATCH]], [[PUT]] and [[DELETE]]) may take a single + * parameter annotated as [[Body]] which will be encoded as [[HttpBody]] and sent as the body of HTTP request. + * Such a method may not define any other body parameters (although it may take additional [[Path]], [[Header]] + * or [[Query]] parameters). + * + * The single body parameter may have a completely custom encoding to [[HttpBody]] which may define its own MIME type + * and doesn't necessarily have to be JSON. + */ final class Body extends BodyTag sealed trait RestValue extends Any { def value: String } + +/** + * Value used as encoding of [[Path]] parameters. Types that have [[GenKeyCodec]] instance have automatic encoding + * to [[PathValue]]. + */ case class PathValue(value: String) extends AnyVal with RestValue + +/** + * Value used as encoding of [[Header]] parameters. Types that have [[GenKeyCodec]] instance have automatic encoding + * to [[HeaderValue]]. + */ case class HeaderValue(value: String) extends AnyVal with RestValue + +/** + * Value used as encoding of [[Query]] parameters. Types that have [[GenKeyCodec]] instance have automatic encoding + * to [[QueryValue]]. + */ case class QueryValue(value: String) extends AnyVal with RestValue + +/** + * Value used as encoding of [[JsonBodyParam]] parameters. Types that have [[GenCodec]] instance have automatic encoding + * to [[JsonValue]]. + */ case class JsonValue(value: String) extends AnyVal with RestValue + object RestValue { implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) @@ -59,6 +185,15 @@ object RestValue { AsRawReal.create(v => JsonValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) } +/** + * Value used to represent HTTP body. Also used as direct encoding of [[Body]] parameters. Types that have + * encoding to [[JsonValue]] (e.g. types that have [[GenCodec]] instance) automatically have encoding to [[HttpBody]] + * which uses application/json MIME type. There is also a specialized encoding provided for `Unit` which maps it + * to empty HTTP body instead of JSON containing "null". + * + * @param value raw HTTP body content + * @param mimeType MIME type, i.e. HTTP `Content-Type` without charset specified + */ case class HttpBody(value: String, mimeType: String) { def jsonValue: JsonValue = mimeType match { case HttpBody.JsonType => JsonValue(value) @@ -74,7 +209,7 @@ object HttpBody { final val Empty: HttpBody = HttpBody.plain("") - def createJsonBody(fields: BIterable[(String, JsonValue)]): HttpBody = + def createJsonBody(fields: NamedParams[JsonValue]): HttpBody = if (fields.isEmpty) HttpBody.Empty else { val sb = new JStringBuilder val oo = new JsonStringOutput(sb).writeObject() @@ -86,10 +221,10 @@ object HttpBody { HttpBody.json(JsonValue(sb.toString)) } - def parseJsonBody(body: HttpBody): ListMap[String, JsonValue] = - if (body.value.isEmpty) ListMap.empty else { + def parseJsonBody(body: HttpBody): NamedParams[JsonValue] = + if (body.value.isEmpty) NamedParams.empty else { val oi = new JsonStringInput(new JsonReader(body.jsonValue.value)).readObject() - val builder = ListMap.newBuilder[String, JsonValue] + val builder = NamedParams.newBuilder[JsonValue] while (oi.hasNext) { val fi = oi.nextField() builder += ((fi.fieldName, JsonValue(fi.readRawJson()))) @@ -105,6 +240,9 @@ object HttpBody { AsReal.create(v => jsonAsReal.asReal(v.jsonValue)) } +/** + * Enum representing HTTP methods. + */ final class HttpMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum object HttpMethod extends AbstractValueEnumCompanion[HttpMethod] { final val GET, PUT, POST, PATCH, DELETE: Value = new HttpMethod @@ -112,8 +250,8 @@ object HttpMethod extends AbstractValueEnumCompanion[HttpMethod] { case class RestHeaders( @multi @tagged[Path] path: List[PathValue], - @multi @tagged[Header] headers: ListMap[String, HeaderValue], - @multi @tagged[Query] query: ListMap[String, QueryValue] + @multi @tagged[Header] headers: NamedParams[HeaderValue], + @multi @tagged[Query] query: NamedParams[QueryValue] ) { def append(methodPath: List[PathValue], otherHeaders: RestHeaders): RestHeaders = RestHeaders( path ::: methodPath ::: otherHeaders.path, @@ -122,7 +260,7 @@ case class RestHeaders( ) } object RestHeaders { - final val Empty = RestHeaders(Nil, ListMap.empty, ListMap.empty) + final val Empty = RestHeaders(Nil, NamedParams.empty, NamedParams.empty) } class HttpErrorException(code: Int, payload: String) @@ -157,7 +295,7 @@ trait RawRest { @tagged[BodyMethodTag] @paramTag[RestParamTag, JsonBodyParam] def handle(@methodName name: String, @composite headers: RestHeaders, - @multi @tagged[JsonBodyParam] body: IListMap[String, JsonValue]): Future[RestResponse] + @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): Future[RestResponse] @multi @tagged[BodyMethodTag] @@ -212,7 +350,7 @@ object RawRest extends RawRpcCompanion[RawRest] { def get(name: String, headers: RestHeaders): Future[RestResponse] = handleSingle(name, headers, HttpBody.Empty) - def handle(name: String, headers: RestHeaders, body: IListMap[String, JsonValue]): Future[RestResponse] = { + def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Future[RestResponse] = { val methodMeta = metadata.httpMethods.getOrElse(name, throw new IllegalArgumentException(s"no such HTTP method: $name")) val newHeaders = prefixHeaders.append(methodMeta.methodPath, headers) @@ -244,16 +382,25 @@ object RawRest extends RawRpcCompanion[RawRest] { implicit def fullInstances[Real]: FullMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] } +/** + * Base class for companions of REST API traits used only for REST clients to external services. + */ abstract class RestClientApiCompanion[Real](implicit instances: RawRest.ClientMacroInstances[Real]) { implicit def restMetadata: RestMetadata[Real] = instances.metadata implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal } +/** + * Base class for companions of REST API traits used only for REST servers exposed to external world. + */ abstract class RestServerApiCompanion[Real](implicit instances: RawRest.ServerMacroInstances[Real]) { implicit def restMetadata: RestMetadata[Real] = instances.metadata implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw } +/** + * Base class for companions of REST API traits used for both REST clients and servers. + */ abstract class RestApiCompanion[Real](implicit instances: RawRest.FullMacroInstances[Real]) { implicit def restMetadata: RestMetadata[Real] = instances.metadata implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala new file mode 100644 index 000000000..79f8f261d --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala @@ -0,0 +1,55 @@ +package com.avsystem.commons +package rpc + +import com.avsystem.commons.rpc.NamedParams.ConcatIterable +import com.avsystem.commons.serialization.GenCodec + +import scala.collection.generic.CanBuildFrom +import scala.collection.mutable + +/** + * Simple immutable structure to collect named RPC parameters while retaining their order and + * providing fast, hashed lookup by parameter name when necessary. + * Intended to be used for [[multi]] raw parameters. + */ +final class NamedParams[+V](private val wrapped: IIterable[(String, V)]) + extends IIterable[(String, V)] with PartialFunction[String, V] { + + private[this] lazy val hashMap = new MHashMap[String, V].setup(_ ++= wrapped) + + def iterator: Iterator[(String, V)] = + hashMap.iterator + def isDefinedAt(key: String): Boolean = + hashMap.isDefinedAt(key) + override def applyOrElse[A1 <: String, B1 >: V](key: A1, default: A1 => B1): B1 = + hashMap.applyOrElse(key, default) + override def apply(key: String): V = + hashMap.apply(key) + + def ++[V0 >: V](other: NamedParams[V0]): NamedParams[V0] = + if (wrapped.isEmpty) other + else if (other.wrapped.isEmpty) this + else new NamedParams(ConcatIterable(wrapped, other.wrapped)) +} +object NamedParams { + def empty[V]: NamedParams[V] = new NamedParams(Nil) + def newBuilder[V]: mutable.Builder[(String, V), NamedParams[V]] = + new MListBuffer[(String, V)].mapResult(new NamedParams(_)) + + private case class ConcatIterable[+V](first: IIterable[V], second: IIterable[V]) extends IIterable[V] { + def iterator: Iterator[V] = first.iterator ++ second.iterator + } + + private val reusableCBF = new CanBuildFrom[Nothing, (String, Any), NamedParams[Any]] { + def apply(from: Nothing): mutable.Builder[(String, Any), NamedParams[Any]] = newBuilder[Any] + def apply(): mutable.Builder[(String, Any), NamedParams[Any]] = newBuilder[Any] + } + + implicit def canBuildFrom[V]: CanBuildFrom[Nothing, (String, V), NamedParams[V]] = + reusableCBF.asInstanceOf[CanBuildFrom[Nothing, (String, V), NamedParams[V]]] + + implicit def genCodec[V: GenCodec]: GenCodec[NamedParams[V]] = GenCodec.createNullableObject( + oi => new NamedParams(oi.iterator(GenCodec.read[V]).toList), + (oo, np) => np.foreach({ case (k, v) => GenCodec.write[V](oo.writeField(k), v) }) + ) +} diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index f2a1d1e3b..cbb7f4fbf 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -4,6 +4,7 @@ package jetty.rpc import java.util.regex.Pattern import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} +import com.avsystem.commons.rpc.NamedParams import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.http.{HttpHeader, HttpStatus, MimeTypes} @@ -30,13 +31,13 @@ object RestServlet { .map(PathValue) .to[List] - val headersBuilder = IListMap.newBuilder[String, HeaderValue] + val headersBuilder = NamedParams.newBuilder[HeaderValue] request.getHeaderNames.asScala.foreach { headerName => headersBuilder += headerName -> HeaderValue(request.getHeader(headerName)) } val headers = headersBuilder.result() - val queryBuilder = IListMap.newBuilder[String, QueryValue] + val queryBuilder = NamedParams.newBuilder[QueryValue] request.getParameterNames.asScala.foreach { parameterName => queryBuilder += parameterName -> QueryValue(request.getParameter(parameterName)) } From 2321ce164cf6c91769153f3c618c04e437bdc4cd Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 5 Jul 2018 13:57:30 +0200 Subject: [PATCH 27/91] fixed scaladocs and error messages --- .../main/scala/com/avsystem/commons/rest/rest.scala | 10 +++++----- .../avsystem/commons/macros/rpc/RpcMetadatas.scala | 11 +---------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 0acda73dc..e0f8c54fe 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -23,7 +23,7 @@ sealed trait RestMethodTag extends RpcTag { * HTTP URL path segment associated with REST method annotated with this tag. This path may be multipart * (i.e. contain slashes). It may also be empty which means that this particular REST method does not contribute * anything to URL path. If path is not specified explicitly, method name is used (the actual method name, not - * [[rpcName]]). + * `rpcName`). * * @example * {{{ @@ -151,25 +151,25 @@ sealed trait RestValue extends Any { } /** - * Value used as encoding of [[Path]] parameters. Types that have [[GenKeyCodec]] instance have automatic encoding + * Value used as encoding of [[Path]] parameters. Types that have `GenKeyCodec` instance have automatic encoding * to [[PathValue]]. */ case class PathValue(value: String) extends AnyVal with RestValue /** - * Value used as encoding of [[Header]] parameters. Types that have [[GenKeyCodec]] instance have automatic encoding + * Value used as encoding of [[Header]] parameters. Types that have `GenKeyCodec` instance have automatic encoding * to [[HeaderValue]]. */ case class HeaderValue(value: String) extends AnyVal with RestValue /** - * Value used as encoding of [[Query]] parameters. Types that have [[GenKeyCodec]] instance have automatic encoding + * Value used as encoding of [[Query]] parameters. Types that have `GenKeyCodec` instance have automatic encoding * to [[QueryValue]]. */ case class QueryValue(value: String) extends AnyVal with RestValue /** - * Value used as encoding of [[JsonBodyParam]] parameters. Types that have [[GenCodec]] instance have automatic encoding + * Value used as encoding of [[JsonBodyParam]] parameters. Types that have `GenCodec` instance have automatic encoding * to [[JsonValue]]. */ case class JsonValue(value: String) extends AnyVal with RestValue diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index a323aacde..b77ec0786 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -174,9 +174,6 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => class RpcMetadataConstructor(val ownerType: Type, val atParam: Option[RpcCompositeParam]) extends MetadataConstructor[RealRpcTrait](primaryConstructor(ownerType, atParam)) with RawRpcSymbol { - override def description: String = - s"${super.description}${atParam.fold("")(p => s" at ${p.description}")}" - def baseTag: Type = typeOf[Nothing] def defaultTag: Type = typeOf[Nothing] @@ -239,9 +236,6 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => ) extends MetadataConstructor[RealMethod]( primaryConstructor(ownerType, Some(atParam.fold[RpcSymbol](identity, identity)))) { - override def description: String = - s"${super.description} at ${atParam.fold(_.description, _.description)}" - val containingMethodParam: MethodMetadataParam = atParam.fold(identity, _.owner.containingMethodParam) @@ -279,9 +273,6 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => ) extends MetadataConstructor[RealParam]( primaryConstructor(ownerType, Some(atParam.fold[RpcSymbol](identity, identity)))) { - override def description: String = - s"${super.description} at ${atParam.fold(_.description, _.description)}" - override def createDirectParam(paramSym: Symbol, annot: Annot): DirectMetadataParam[RealParam] = annot.tpe match { case t if t <:< ReifyPositionAT => new ReifiedPositionParam(this, paramSym) @@ -322,7 +313,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def tryMaterializeFor(rpcSym: Real): Res[Tree] = if (checked) tryInferCachedImplicit(actualType).map(n => Ok(q"$n")) - .getOrElse(Fail(s"no implicit value $actualType for parameter $description could be found")) + .getOrElse(Fail(s"no implicit value $actualType for $description could be found")) else Ok(materializeFor(rpcSym)) } From 4a6de7d1bf3a8ff634365b56472d38c41bde701d Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 12:05:15 +0200 Subject: [PATCH 28/91] added path suffixes to Path rest params --- .../rpc/akka/AkkaRPCFrameworkTest.scala | 7 +- .../commons/rpc/akka/RPCFrameworkTest.scala | 5 +- .../avsystem/commons/SharedExtensions.scala | 3 + .../com/avsystem/commons/rest/rest.scala | 96 +++++++++++-------- .../avsystem/commons/rpc/NamedParams.scala | 2 +- .../avsystem/commons/rest/RawRestTest.scala | 90 +++++++++++++---- .../commons/jetty/rpc/RestServlet.scala | 4 +- 7 files changed, 137 insertions(+), 70 deletions(-) diff --git a/commons-akka/src/test/scala/com/avsystem/commons/rpc/akka/AkkaRPCFrameworkTest.scala b/commons-akka/src/test/scala/com/avsystem/commons/rpc/akka/AkkaRPCFrameworkTest.scala index a58a43377..b818d8bd7 100644 --- a/commons-akka/src/test/scala/com/avsystem/commons/rpc/akka/AkkaRPCFrameworkTest.scala +++ b/commons-akka/src/test/scala/com/avsystem/commons/rpc/akka/AkkaRPCFrameworkTest.scala @@ -14,10 +14,9 @@ import scala.concurrent.duration._ * @author Wojciech Milewski */ abstract class AkkaRPCFrameworkTest( - serverSystem: ActorSystem, - clientSystem: ActorSystem, - serverSystemPath: Option[String] = None) - extends FlatSpec with RPCFrameworkTest with ProcedureRPCTest with FunctionRPCTest with GetterRPCTest with ObservableRPCTest with BeforeAndAfterAll { + serverSystem: ActorSystem, clientSystem: ActorSystem, serverSystemPath: Option[String] = None) + extends FlatSpec with RPCFrameworkTest with ProcedureRPCTest with FunctionRPCTest with GetterRPCTest with ObservableRPCTest + with BeforeAndAfterAll { /** * Servers as identifier supplier for each test case to allow tests parallelization. diff --git a/commons-akka/src/test/scala/com/avsystem/commons/rpc/akka/RPCFrameworkTest.scala b/commons-akka/src/test/scala/com/avsystem/commons/rpc/akka/RPCFrameworkTest.scala index f37bb41fc..8be0b5988 100644 --- a/commons-akka/src/test/scala/com/avsystem/commons/rpc/akka/RPCFrameworkTest.scala +++ b/commons-akka/src/test/scala/com/avsystem/commons/rpc/akka/RPCFrameworkTest.scala @@ -21,9 +21,10 @@ trait RPCFrameworkTest extends FlatSpecLike with Matchers with MockitoSugar with import RPCFrameworkTest._ - val callTimeout = 200.millis + val callTimeout: FiniteDuration = 200.millis - override implicit val patienceConfig: PatienceConfig = PatienceConfig(timeout = Span.convertDurationToSpan(3.seconds)) + override implicit val patienceConfig: PatienceConfig = + PatienceConfig(timeout = Span.convertDurationToSpan(10.seconds)) /** * Run tests with connection between client and server. diff --git a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index e1b56555f..94751774e 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -452,6 +452,9 @@ object SharedExtensions extends SharedExtensions { def minOpt[B >: A : Ordering]: Opt[B] = if (coll.isEmpty) Opt.Empty else coll.min[B].opt def minOptBy[B: Ordering](f: A => B): Opt[A] = if (coll.isEmpty) Opt.Empty else coll.minBy(f).opt + + def mkStringOrEmpty(start: String, sep: String, end: String): String = + if (coll.nonEmpty) coll.mkString(start, sep, end) else "" } class SetOps[A](private val set: BSet[A]) extends AnyVal { diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index e0f8c54fe..342fdc6cb 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -112,7 +112,7 @@ sealed trait RestParamTag extends RpcTag * REST method parameters annotated as [[Path]] will be encoded as [[PathValue]] and appended to URL path, in the * declaration order. Parameters of [[Prefix]] REST methods are interpreted as [[Path]] parameters by default. */ -final class Path extends RestParamTag +final class Path(val pathSuffix: String = "") extends RestParamTag /** * REST method parameters annotated as [[Header]] will be encoded as [[HeaderValue]] and added to HTTP headers. @@ -155,6 +155,10 @@ sealed trait RestValue extends Any { * to [[PathValue]]. */ case class PathValue(value: String) extends AnyVal with RestValue +object PathValue { + def split(path: String): List[PathValue] = + path.split("/").iterator.filter(_.nonEmpty).map(PathValue(_)).toList +} /** * Value used as encoding of [[Header]] parameters. Types that have `GenKeyCodec` instance have automatic encoding @@ -187,16 +191,16 @@ object RestValue { /** * Value used to represent HTTP body. Also used as direct encoding of [[Body]] parameters. Types that have - * encoding to [[JsonValue]] (e.g. types that have [[GenCodec]] instance) automatically have encoding to [[HttpBody]] + * encoding to [[JsonValue]] (e.g. types that have `GenCodec` instance) automatically have encoding to [[HttpBody]] * which uses application/json MIME type. There is also a specialized encoding provided for `Unit` which maps it * to empty HTTP body instead of JSON containing "null". * - * @param value raw HTTP body content + * @param content raw HTTP body content * @param mimeType MIME type, i.e. HTTP `Content-Type` without charset specified */ -case class HttpBody(value: String, mimeType: String) { +case class HttpBody(content: String, mimeType: String) { def jsonValue: JsonValue = mimeType match { - case HttpBody.JsonType => JsonValue(value) + case HttpBody.JsonType => JsonValue(content) case _ => throw new ReadFailure(s"Expected application/json type, got $mimeType") } } @@ -222,7 +226,7 @@ object HttpBody { } def parseJsonBody(body: HttpBody): NamedParams[JsonValue] = - if (body.value.isEmpty) NamedParams.empty else { + if (body.content.isEmpty) NamedParams.empty else { val oi = new JsonStringInput(new JsonReader(body.jsonValue.value)).readObject() val builder = NamedParams.newBuilder[JsonValue] while (oi.hasNext) { @@ -253,8 +257,8 @@ case class RestHeaders( @multi @tagged[Header] headers: NamedParams[HeaderValue], @multi @tagged[Query] query: NamedParams[QueryValue] ) { - def append(methodPath: List[PathValue], otherHeaders: RestHeaders): RestHeaders = RestHeaders( - path ::: methodPath ::: otherHeaders.path, + def append(method: RestMethodMetadata[_], otherHeaders: RestHeaders): RestHeaders = RestHeaders( + path ::: method.applyPathParams(otherHeaders.path), headers ++ otherHeaders.headers, query ++ otherHeaders.query ) @@ -274,7 +278,7 @@ object RestResponse { implicit def defaultFutureAsReal[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[Future[RestResponse], Future[T]] = AsReal.create(_.mapNow { case RestResponse(200, body) => bodyAsReal.asReal(body) - case RestResponse(code, body) => throw new HttpErrorException(code, body.value) + case RestResponse(code, body) => throw new HttpErrorException(code, body.content) }) } @@ -343,7 +347,7 @@ object RawRest extends RawRpcCompanion[RawRest] { def prefix(name: String, headers: RestHeaders): RawRest = { val prefixMeta = metadata.prefixMethods.getOrElse(name, throw new IllegalArgumentException(s"no such prefix method: $name")) - val newHeaders = prefixHeaders.append(prefixMeta.methodPath, headers) + val newHeaders = prefixHeaders.append(prefixMeta, headers) new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest) } @@ -353,14 +357,14 @@ object RawRest extends RawRpcCompanion[RawRest] { def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Future[RestResponse] = { val methodMeta = metadata.httpMethods.getOrElse(name, throw new IllegalArgumentException(s"no such HTTP method: $name")) - val newHeaders = prefixHeaders.append(methodMeta.methodPath, headers) + val newHeaders = prefixHeaders.append(methodMeta, headers) handleRequest(RestRequest(methodMeta.method, newHeaders, HttpBody.createJsonBody(body))) } def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Future[RestResponse] = { val methodMeta = metadata.httpMethods.getOrElse(name, throw new IllegalArgumentException(s"no such HTTP method: $name")) - val newHeaders = prefixHeaders.append(methodMeta.methodPath, headers) + val newHeaders = prefixHeaders.append(methodMeta, headers) handleRequest(RestRequest(methodMeta.method, newHeaders, body)) } } @@ -440,21 +444,35 @@ case class RestMetadata[T]( object RestMetadata extends RpcMetadataCompanion[RestMetadata] sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { - def name: String - def tagPath: Opt[String] + def methodPath: List[PathValue] def headersMetadata: RestHeadersMetadata - val methodPath: List[PathValue] = - tagPath.fold(List(PathValue(name)))(_.split("/").iterator.filter(_.nonEmpty).map(PathValue).toList) + private val pathPattern: List[Opt[PathValue]] = + methodPath.map(Opt(_)) ++ headersMetadata.path.flatMap(pp => Opt.Empty :: pp.pathSuffix.map(Opt(_))) + + def applyPathParams(params: List[PathValue]): List[PathValue] = { + def loop(params: List[PathValue], pattern: List[Opt[PathValue]]): List[PathValue] = + (params, pattern) match { + case (Nil, Nil) => Nil + case (_, Opt(patternHead) :: patternTail) => patternHead :: loop(params, patternTail) + case (param :: paramsTail, Opt.Empty :: patternTail) => param :: loop(paramsTail, patternTail) + case _ => throw new IllegalArgumentException( + s"got ${params.size} path params, expected ${headersMetadata.path.size}") + } + loop(params, pathPattern) + } def extractPathParams(path: List[PathValue]): Opt[(List[PathValue], List[PathValue])] = { - def suffix(prefix: List[PathValue], path: List[PathValue]): Opt[List[PathValue]] = (prefix, path) match { - case (Nil, result) => Opt(result) - case (prefixHead :: prefixTail, pathHead :: pathTail) if prefixHead == pathHead => - suffix(prefixTail, pathTail) - case _ => Opt.Empty - } - suffix(methodPath, path).flatMap(headersMetadata.extractPathParams) + def loop(path: List[PathValue], pattern: List[Opt[PathValue]]): Opt[(List[PathValue], List[PathValue])] = + (path, pattern) match { + case (pathTail, Nil) => Opt((Nil, pathTail)) + case (param :: pathTail, Opt.Empty :: patternTail) => + loop(pathTail, patternTail).map { case (params, tail) => (param :: params, tail) } + case (pathHead :: pathTail, Opt(patternHead) :: patternTail) if pathHead == patternHead => + loop(pathTail, patternTail) + case _ => Opt.Empty + } + loop(path, pathPattern) } } @@ -465,37 +483,31 @@ case class PrefixMetadata[T]( @composite headersMetadata: RestHeadersMetadata, @checked @infer result: RestMetadata.Lazy[T] ) extends RestMethodMetadata[T] { - def tagPath: Opt[String] = methodTag.flatMap(_.path.toOpt) + def methodPath: List[PathValue] = + PathValue.split(methodTag.flatMap(_.path.toOpt).getOrElse(name)) } case class HttpMethodMetadata[T]( @reifyName name: String, @reifyAnnot methodTag: HttpMethodTag, @composite headersMetadata: RestHeadersMetadata, - @multi @tagged[BodyTag] bodyParams: Map[String, ParamMetadata[_]] + @multi @tagged[BodyTag] bodyParams: Map[String, BodyParamMetadata[_]] ) extends RestMethodMetadata[Future[T]] { val method: HttpMethod = methodTag.method val singleBody: Boolean = bodyParams.values.exists(_.singleBody) - def tagPath: Opt[String] = methodTag.path.toOpt + def methodPath: List[PathValue] = PathValue.split(methodTag.path.getOrElse(name)) } case class RestHeadersMetadata( - @multi @tagged[Path] path: List[ParamMetadata[_]], - @multi @tagged[Header] headers: Map[String, ParamMetadata[_]], - @multi @tagged[Query] query: Map[String, ParamMetadata[_]] -) { - val pathLength: Int = path.size + @multi @tagged[Path] path: List[PathParamMetadata[_]], + @multi @tagged[Header] headers: Map[String, HeaderParamMetadata[_]], + @multi @tagged[Query] query: Map[String, QueryParamMetadata[_]] +) - def extractPathParams(path: List[PathValue]): Opt[(List[PathValue], List[PathValue])] = { - def loop(acc: List[PathValue], path: List[PathValue], index: Int): Opt[(List[PathValue], List[PathValue])] = - if (index == 0) Opt((acc.reverse, path)) - else path match { - case head :: tail => loop(head :: acc, tail, index - 1) - case Nil => Opt.Empty - } - loop(Nil, path, pathLength) - } +case class PathParamMetadata[T](@optional @reifyAnnot pathAnnot: Opt[Path]) extends TypedMetadata[T] { + val pathSuffix: List[PathValue] = PathValue.split(pathAnnot.fold("")(_.pathSuffix)) } -case class ParamMetadata[T](@hasAnnot[Body] singleBody: Boolean) - extends TypedMetadata[T] +case class HeaderParamMetadata[T]() extends TypedMetadata[T] +case class QueryParamMetadata[T]() extends TypedMetadata[T] +case class BodyParamMetadata[T](@hasAnnot[Body] singleBody: Boolean) extends TypedMetadata[T] diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala index 79f8f261d..61d491d9f 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala @@ -15,7 +15,7 @@ import scala.collection.mutable final class NamedParams[+V](private val wrapped: IIterable[(String, V)]) extends IIterable[(String, V)] with PartialFunction[String, V] { - private[this] lazy val hashMap = new MHashMap[String, V].setup(_ ++= wrapped) + private[this] lazy val hashMap = new MLinkedHashMap[String, V].setup(_ ++= wrapped) def iterator: Iterator[(String, V)] = hashMap.iterator diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index 32e40b744..a5f0ff14f 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -11,7 +11,12 @@ object User extends HasGenCodec[User] trait UserApi { @GET def user(userId: String): Future[User] - @POST("user/save") def user(@Body user: User): Future[Unit] + + @POST("user/save") def user( + @Path("moar/path") paf: String, + @Header("X-Awesome") awesome: Boolean, + @Body user: User + ): Future[Unit] } object UserApi extends RestApiCompanion[UserApi] @@ -22,29 +27,76 @@ trait RestTestApi { object RestTestApi extends RestApiCompanion[RestTestApi] class RawRestTest extends FunSuite with ScalaFutures { - test("round trip test") { - class RestTestApiImpl(id: Int, query: String) extends RestTestApi with UserApi { - def self: UserApi = this - def subApi(newId: Int, newQuery: String): UserApi = new RestTestApiImpl(newId, query + newQuery) - def user(userId: String): Future[User] = Future.successful(User(userId, s"$userId-$id-$query")) - def user(user: User): Future[Unit] = Future.unit - } + def repr(req: RestRequest): String = { + val pathRepr = req.headers.path.map(_.value).mkString("/") + val queryRepr = req.headers.query.iterator + .map({ case (k, v) => s"$k=${v.value}" }).mkStringOrEmpty("?", "&", "") + val hasHeaders = req.headers.headers.nonEmpty + val headersRepr = req.headers.headers.iterator + .map({ case (n, v) => s"$n: ${v.value}" }).mkStringOrEmpty("\n", "\n", "\n") + + val contentRepr = + if (req.body.content.isEmpty) "" + else s"${if (hasHeaders) "" else " "}${req.body.mimeType}\n${req.body.content}" + s"-> ${req.method} $pathRepr$queryRepr$headersRepr$contentRepr" + } + + def repr(resp: RestResponse): String = { + val contentRepr = + if (resp.body.content.isEmpty) "" + else s" ${resp.body.mimeType}\n${resp.body.content}" + s"<- ${resp.code}$contentRepr" + } + + class RestTestApiImpl(id: Int, query: String) extends RestTestApi with UserApi { + def self: UserApi = this + def subApi(newId: Int, newQuery: String): UserApi = new RestTestApiImpl(newId, query + newQuery) + def user(userId: String): Future[User] = Future.successful(User(userId, s"$userId-$id-$query")) + def user(paf: String, awesome: Boolean, user: User): Future[Unit] = Future.unit + } - val callRecord = new MListBuffer[(RestRequest, RestResponse)] + var trafficLog: String = _ - val real: RestTestApi = new RestTestApiImpl(0, "") - val serverHandle: RestRequest => Future[RestResponse] = request => { - RawRest.asHandleRequest(real).apply(request).andThenNow { - case Success(response) => callRecord += ((request, response)) - } + val real: RestTestApi = new RestTestApiImpl(0, "") + val serverHandle: RestRequest => Future[RestResponse] = request => { + RawRest.asHandleRequest(real).apply(request).andThenNow { + case Success(response) => + trafficLog = s"${repr(request)}\n${repr(response)}\n" } + } - val realProxy: RestTestApi = RawRest.fromHandleRequest[RestTestApi](serverHandle) + val realProxy: RestTestApi = RawRest.fromHandleRequest[RestTestApi](serverHandle) + + def testRestCall[T](call: RestTestApi => Future[T], expectedTraffic: String)(implicit pos: Position): Unit = { + assert(call(realProxy).futureValue == call(real).futureValue) + assert(trafficLog == expectedTraffic) + } - def assertSame[T](call: RestTestApi => Future[T])(implicit pos: Position): Unit = - assert(call(realProxy).futureValue == call(real).futureValue) + test("simple GET") { + testRestCall(_.self.user("ID"), + """-> GET user?userId=ID + |<- 200 application/json + |{"id":"ID","name":"ID-0-"} + |""".stripMargin + ) + } + + test("simple POST with path, header and query") { + testRestCall(_.self.user("paf", awesome = true, user = User("ID", "Fred")), + """-> POST user/save/paf/moar/path + |X-Awesome: true + |application/json + |{"id":"ID","name":"Fred"} + |<- 200 + |""".stripMargin) + } - assertSame(_.self.user("ID")) - assertSame(_.subApi(1, "query").user("ID")) + test("simple GET after prefix call") { + testRestCall(_.subApi(1, "query").user("ID"), + """-> GET subApi/1/user?query=query&userId=ID + |<- 200 application/json + |{"id":"ID","name":"ID-1-query"} + |""".stripMargin + ) } } diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index cbb7f4fbf..f96b0a665 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -28,7 +28,7 @@ object RestServlet { .splitAsStream(request.getPathInfo) .asScala .skip(1) - .map(PathValue) + .map(PathValue(_)) .to[List] val headersBuilder = NamedParams.newBuilder[HeaderValue] @@ -60,7 +60,7 @@ object RestServlet { case Success(restResponse) => response.setStatus(restResponse.code) response.addHeader(HttpHeader.CONTENT_TYPE.asString(), s"${restResponse.body.mimeType};charset=utf-8") - response.getWriter.write(restResponse.body.value) + response.getWriter.write(restResponse.body.content) case Failure(e) => response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) response.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()) From 2625e733a635f2eac142cf90c4bef293c7775d21 Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 13:38:57 +0200 Subject: [PATCH 29/91] @defaultsToName meta annotation for annotation parameters --- .../commons/annotation/defaultsToName.scala | 22 +++++++++++++ .../com/avsystem/commons/rest/rest.scala | 31 ++++++++++--------- .../commons/macros/MacroCommons.scala | 29 ++++++++++++++--- 3 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 commons-annotations/src/main/scala/com/avsystem/commons/annotation/defaultsToName.scala diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/annotation/defaultsToName.scala b/commons-annotations/src/main/scala/com/avsystem/commons/annotation/defaultsToName.scala new file mode 100644 index 000000000..62ccf4bf2 --- /dev/null +++ b/commons-annotations/src/main/scala/com/avsystem/commons/annotation/defaultsToName.scala @@ -0,0 +1,22 @@ +package com.avsystem.commons +package annotation + +import scala.annotation.StaticAnnotation + +/** + * Meta annotation that may be used on `String` constructor parameter of an annotation. This constructor parameter + * must take a default `null` value. [[defaultsToName]] makes annotation processing macro engines insert the name + * of annotated symbol instead of `null`. + * + * @example + * + * {{{ + * class SomeMethodAnnotation(@defaultsToName val name: String = null) + * + * @SomeMethodAnnotation def someMethod: String + * }}} + * + * Now, when some macro engine has to inspect `SomeMethodAnnotation` of `someMethod`, it will automatically insert + * the string "someMethod" as the argument of `SomeMethodAnnotation`. + */ +class defaultsToName extends StaticAnnotation diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 342fdc6cb..368932026 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -1,7 +1,7 @@ package com.avsystem.commons package rest -import com.avsystem.commons.annotation.AnnotationAggregate +import com.avsystem.commons.annotation.{AnnotationAggregate, defaultsToName} import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx} import com.avsystem.commons.rpc._ import com.avsystem.commons.serialization.GenCodec.ReadFailure @@ -34,7 +34,7 @@ sealed trait RestMethodTag extends RpcTag { * object SomeRestApi extends RestApiCompanion[SomeRestApi] * }}} */ - def path: OptArg[String] + @defaultsToName def path: String } sealed abstract class HttpMethodTag(val method: HttpMethod) extends RestMethodTag with AnnotationAggregate @@ -70,24 +70,24 @@ sealed abstract class BodyMethodTag(method: HttpMethod) extends HttpMethodTag(me * * @param path see [[RestMethodTag.path]] */ -final class GET(val path: OptArg[String] = OptArg.Empty) extends HttpMethodTag(HttpMethod.GET) { +final class GET(val path: String = null) extends HttpMethodTag(HttpMethod.GET) { @rpcNamePrefix("GET_") type Implied } /** See [[BodyMethodTag]] */ -final class POST(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.POST) { +final class POST(val path: String = null) extends BodyMethodTag(HttpMethod.POST) { @rpcNamePrefix("POST_") type Implied } /** See [[BodyMethodTag]] */ -final class PATCH(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.PATCH) { +final class PATCH(val path: String = null) extends BodyMethodTag(HttpMethod.PATCH) { @rpcNamePrefix("PATCH_") type Implied } /** See [[BodyMethodTag]] */ -final class PUT(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.PUT) { +final class PUT(val path: String = null) extends BodyMethodTag(HttpMethod.PUT) { @rpcNamePrefix("PUT_") type Implied } /** See [[BodyMethodTag]] */ -final class DELETE(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTag(HttpMethod.DELETE) { +final class DELETE(val path: String = null) extends BodyMethodTag(HttpMethod.DELETE) { @rpcNamePrefix("DELETE_") type Implied } @@ -104,7 +104,7 @@ final class DELETE(val path: OptArg[String] = OptArg.Empty) extends BodyMethodTa * * @param path see [[RestMethodTag.path]] */ -final class Prefix(val path: OptArg[String] = OptArg.Empty) extends RestMethodTag +final class Prefix(val path: String = null) extends RestMethodTag sealed trait RestParamTag extends RpcTag @@ -118,13 +118,16 @@ final class Path(val pathSuffix: String = "") extends RestParamTag * REST method parameters annotated as [[Header]] will be encoded as [[HeaderValue]] and added to HTTP headers. * Header name must be explicitly given as argument of this annotation. */ -final class Header(override val name: String) extends rpcName(name) with RestParamTag +final class Header(override val name: String) + extends rpcName(name) with RestParamTag /** * REST method parameters annotated as [[Query]] will be encoded as [[QueryValue]] and added to URL query * parameters. Parameters of [[GET]] REST methods are interpreted as [[Query]] parameters by default. */ -final class Query extends RestParamTag +final class Query(@defaultsToName override val name: String = null) + extends rpcName(name) with RestParamTag + sealed trait BodyTag extends RestParamTag /** @@ -133,7 +136,8 @@ sealed trait BodyTag extends RestParamTag * [[POST]], [[PATCH]], [[PUT]] or [[DELETE]]. Actually, parameters of these methods are interpreted as * [[JsonBodyParam]] by default which means that this annotation rarely needs to be applied explicitly. */ -final class JsonBodyParam extends BodyTag +final class JsonBodyParam(@defaultsToName override val name: String = null) + extends rpcName(name) with BodyTag /** * REST methods that can send HTTP body ([[POST]], [[PATCH]], [[PUT]] and [[DELETE]]) may take a single @@ -484,18 +488,17 @@ case class PrefixMetadata[T]( @checked @infer result: RestMetadata.Lazy[T] ) extends RestMethodMetadata[T] { def methodPath: List[PathValue] = - PathValue.split(methodTag.flatMap(_.path.toOpt).getOrElse(name)) + PathValue.split(methodTag.map(_.path).getOrElse(name)) } case class HttpMethodMetadata[T]( - @reifyName name: String, @reifyAnnot methodTag: HttpMethodTag, @composite headersMetadata: RestHeadersMetadata, @multi @tagged[BodyTag] bodyParams: Map[String, BodyParamMetadata[_]] ) extends RestMethodMetadata[Future[T]] { val method: HttpMethod = methodTag.method val singleBody: Boolean = bodyParams.values.exists(_.singleBody) - def methodPath: List[PathValue] = PathValue.split(methodTag.path.getOrElse(name)) + def methodPath: List[PathValue] = PathValue.split(methodTag.path) } case class RestHeadersMetadata( diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala index b452cba2c..d045d0f28 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala @@ -41,6 +41,7 @@ trait MacroCommons { bundle => final val OptionClass = definitions.OptionClass final val ImplicitsObj = q"$CommonsPkg.misc.Implicits" final val AnnotationAggregateType = getType(tq"$CommonsPkg.annotation.AnnotationAggregate") + final val DefaultsToNameAT = getType(tq"$CommonsPkg.annotation.defaultsToName") final lazy val isScalaJs = definitions.ScalaPackageClass.toType.member(TermName("scalajs")) != NoSymbol @@ -68,12 +69,30 @@ trait MacroCommons { bundle => error(msg) } - case class Annot(tree: Tree)(val directSource: Symbol, val aggregate: Option[Annot]) { + class Annot(annotTree: Tree, val directSource: Symbol, val aggregate: Option[Annot]) { def aggregationChain: List[Annot] = aggregate.fold(List.empty[Annot])(a => a :: a.aggregationChain) def source: Symbol = aggregate.fold(directSource)(_.source) - def tpe: Type = tree.tpe + + lazy val tree: Tree = annotTree match { + case Apply(constr@Select(New(tpt), termNames.CONSTRUCTOR), args) => + val clsTpe = tpt.tpe + val params = primaryConstructorOf(clsTpe).typeSignature.paramLists.head + + val newArgs = (args zip params) map { + case (arg, param) if param.asTerm.isParamWithDefault && arg.symbol != null && + arg.symbol.isSynthetic && arg.symbol.name.decodedName.toString.contains("$default$") && + findAnnotation(param, DefaultsToNameAT).nonEmpty => + q"${source.name.decodedName.toString}" + case (arg, _) => arg + } + + treeCopy.Apply(annotTree, constr, newArgs) + case _ => annotTree + } + + def tpe: Type = annotTree.tpe def findArg[T: ClassTag](valSym: Symbol, defaultValue: Option[T] = None): T = tree match { case Apply(Select(New(tpt), termNames.CONSTRUCTOR), args) => @@ -103,7 +122,7 @@ trait MacroCommons { bundle => if (tpe <:< AnnotationAggregateType) { val argsInliner = new AnnotationArgInliner(tree) val impliedMember = tpe.member(TypeName("Implied")) - impliedMember.annotations.map(a => Annot(argsInliner.transform(a.tree))(impliedMember, Some(this))) + impliedMember.annotations.map(a => new Annot(argsInliner.transform(a.tree), impliedMember, Some(this))) } else Nil } @@ -137,7 +156,7 @@ trait MacroCommons { bundle => def allAnnotations(s: Symbol, tpeFilter: Type = typeOf[Any], withInherited: Boolean = true): List[Annot] = maybeWithSuperSymbols(s, withInherited) - .flatMap(ss => ss.annotations.map(a => Annot(a.tree)(ss, None))) + .flatMap(ss => ss.annotations.map(a => new Annot(a.tree, ss, None))) .flatMap(_.withAllAggregated).filter(_.tpe <:< tpeFilter).toList def findAnnotation(s: Symbol, tpe: Type, withInherited: Boolean = true): Option[Annot] = @@ -159,7 +178,7 @@ trait MacroCommons { bundle => fromHead orElse find(tail) case Nil => None } - find(ss.annotations.map(a => Annot(a.tree)(ss, None))) + find(ss.annotations.map(a => new Annot(a.tree, ss, None))) }.collectFirst { case Some(annot) => annot } From 89208f64c1097d1f08c4c71fa0ec9b3038bebc7a Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 14:07:47 +0200 Subject: [PATCH 30/91] test improvement --- .../test/scala/com/avsystem/commons/rest/RawRestTest.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index a5f0ff14f..c0362c211 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -15,6 +15,7 @@ trait UserApi { @POST("user/save") def user( @Path("moar/path") paf: String, @Header("X-Awesome") awesome: Boolean, + @Query("f") foo: Int, @Body user: User ): Future[Unit] } @@ -52,7 +53,7 @@ class RawRestTest extends FunSuite with ScalaFutures { def self: UserApi = this def subApi(newId: Int, newQuery: String): UserApi = new RestTestApiImpl(newId, query + newQuery) def user(userId: String): Future[User] = Future.successful(User(userId, s"$userId-$id-$query")) - def user(paf: String, awesome: Boolean, user: User): Future[Unit] = Future.unit + def user(paf: String, awesome: Boolean, f: Int, user: User): Future[Unit] = Future.unit } var trafficLog: String = _ @@ -82,8 +83,8 @@ class RawRestTest extends FunSuite with ScalaFutures { } test("simple POST with path, header and query") { - testRestCall(_.self.user("paf", awesome = true, user = User("ID", "Fred")), - """-> POST user/save/paf/moar/path + testRestCall(_.self.user("paf", awesome = true, 42, User("ID", "Fred")), + """-> POST user/save/paf/moar/path?f=42 |X-Awesome: true |application/json |{"id":"ID","name":"Fred"} From 5517aa7f4fa174776cc94c2007e99d42c7a96949 Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 16:26:37 +0200 Subject: [PATCH 31/91] added REST server API validation to detect ambiguous paths --- .../com/avsystem/commons/rest/rest.scala | 125 +++++++++++++++--- .../avsystem/commons/rest/RawRestTest.scala | 8 +- .../commons/rest/RestPathValidationTest.scala | 38 ++++++ 3 files changed, 145 insertions(+), 26 deletions(-) create mode 100644 commons-core/src/test/scala/com/avsystem/commons/rest/RestPathValidationTest.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala index 368932026..f701e3dcf 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala @@ -311,26 +311,29 @@ trait RawRest { @encoded @tagged[Body] body: HttpBody): Future[RestResponse] def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = { - case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { - case List(ResolvedPath(prefixes, RpcWithPath(finalRpcName, finalPathParams), singleBody)) => - val finalRawRest = prefixes.foldLeft(this) { - case (rawRest, RpcWithPath(rpcName, pathParams)) => - rawRest.prefix(rpcName, headers.copy(path = pathParams)) - } - val finalHeaders = headers.copy(path = finalPathParams) - - if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) - else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) - else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) - - case Nil => - val pathStr = headers.path.iterator.map(_.value).mkString("/") - Future.successful(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) - - case multiple => - val pathStr = headers.path.iterator.map(_.value).mkString("/") - val callsRepr = multiple.iterator.map(p => s" ${p.rpcChainRepr}").mkString("\n", "\n", "") - throw new IllegalArgumentException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") + metadata.ensureUnambiguousPaths() + locally[RestRequest => Future[RestResponse]] { + case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { + case List(ResolvedPath(prefixes, RpcWithPath(finalRpcName, finalPathParams), singleBody)) => + val finalRawRest = prefixes.foldLeft(this) { + case (rawRest, RpcWithPath(rpcName, pathParams)) => + rawRest.prefix(rpcName, headers.copy(path = pathParams)) + } + val finalHeaders = headers.copy(path = finalPathParams) + + if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) + else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) + else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) + + case Nil => + val pathStr = headers.path.iterator.map(_.value).mkString("/") + Future.successful(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) + + case multiple => + val pathStr = headers.path.iterator.map(_.value).mkString("/") + val callsRepr = multiple.iterator.map(p => s" ${p.rpcChainRepr}").mkString("\n", "\n", "") + throw new IllegalArgumentException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") + } } } } @@ -430,6 +433,20 @@ case class RestMetadata[T]( @multi @tagged[Prefix] prefixMethods: Map[String, PrefixMetadata[_]], @multi @tagged[HttpMethodTag] httpMethods: Map[String, HttpMethodMetadata[_]] ) { + def ensureUnambiguousPaths(): Unit = { + val trie = new RestMetadata.Trie + trie.fillWith(this) + trie.mergeWildcardToNamed() + val ambiguities = new MListBuffer[(String, List[String])] + trie.collectAmbiguousCalls(ambiguities) + if (ambiguities.nonEmpty) { + val problems = ambiguities.map { case (path, chains) => + s"$path may result from multiple calls:\n ${chains.mkString("\n ")}" + } + throw new IllegalArgumentException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") + } + } + def resolvePath(method: HttpMethod, path: List[PathValue]): Iterator[ResolvedPath] = { val asFinalCall = for { (rpcName, m) <- httpMethods.iterator if m.method == method @@ -445,13 +462,77 @@ case class RestMetadata[T]( asFinalCall ++ usingPrefix } } -object RestMetadata extends RpcMetadataCompanion[RestMetadata] +object RestMetadata extends RpcMetadataCompanion[RestMetadata] { + private class Trie { + val rpcChains: Map[HttpMethod, MBuffer[String]] = + HttpMethod.values.mkMap(identity, _ => new MArrayBuffer[String]) + + val byName: MMap[String, Trie] = new MHashMap + var wildcard: Opt[Trie] = Opt.Empty + + def forPattern(pattern: List[Opt[PathValue]]): Trie = pattern match { + case Nil => this + case Opt(PathValue(pathName)) :: tail => + byName.getOrElseUpdate(pathName, new Trie).forPattern(tail) + case Opt.Empty :: tail => + wildcard.getOrElse(new Trie().setup(t => wildcard = Opt(t))).forPattern(tail) + } + + def fillWith(metadata: RestMetadata[_], prefixStack: List[(String, PrefixMetadata[_])] = Nil): Unit = { + def prefixChain: String = + prefixStack.reverseIterator.map({ case (k, _) => k }).mkStringOrEmpty("", "->", "->") + + metadata.prefixMethods.foreach { case entry@(rpcName, pm) => + if (prefixStack.contains(entry)) { + throw new IllegalArgumentException( + s"call chain $prefixChain$rpcName is recursive, recursively defined server APIs are forbidden") + } + forPattern(pm.pathPattern).fillWith(pm.result.value, entry :: prefixStack) + } + metadata.httpMethods.foreach { case (rpcName, hm) => + forPattern(hm.pathPattern).rpcChains(hm.method) += s"$prefixChain${rpcName.stripPrefix(s"${hm.method}_")}" + } + } + + private def merge(other: Trie): Unit = { + HttpMethod.values.foreach { meth => + rpcChains(meth) ++= other.rpcChains(meth) + } + for (w <- wildcard; ow <- other.wildcard) w.merge(ow) + wildcard = wildcard orElse other.wildcard + other.byName.foreach { case (name, trie) => + byName.getOrElseUpdate(name, new Trie).merge(trie) + } + } + + def mergeWildcardToNamed(): Unit = wildcard.foreach { wc => + wc.mergeWildcardToNamed() + byName.values.foreach { trie => + trie.merge(wc) + trie.mergeWildcardToNamed() + } + } + + def collectAmbiguousCalls(ambiguities: MBuffer[(String, List[String])], pathPrefix: List[String] = Nil): Unit = { + rpcChains.foreach { case (method, chains) => + if (chains.size > 1) { + val path = pathPrefix.reverse.mkString(s"$method /", "/", "") + ambiguities += ((path, chains.toList)) + } + } + wildcard.foreach(_.collectAmbiguousCalls(ambiguities, "*" :: pathPrefix)) + byName.foreach { case (name, trie) => + trie.collectAmbiguousCalls(ambiguities, name :: pathPrefix) + } + } + } +} sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { def methodPath: List[PathValue] def headersMetadata: RestHeadersMetadata - private val pathPattern: List[Opt[PathValue]] = + val pathPattern: List[Opt[PathValue]] = methodPath.map(Opt(_)) ++ headersMetadata.path.flatMap(pp => Opt.Empty :: pp.pathSuffix.map(Opt(_))) def applyPathParams(params: List[PathValue]): List[PathValue] = { diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index c0362c211..bb106d9a9 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -29,7 +29,7 @@ object RestTestApi extends RestApiCompanion[RestTestApi] class RawRestTest extends FunSuite with ScalaFutures { def repr(req: RestRequest): String = { - val pathRepr = req.headers.path.map(_.value).mkString("/") + val pathRepr = req.headers.path.map(_.value).mkString("/", "/", "") val queryRepr = req.headers.query.iterator .map({ case (k, v) => s"$k=${v.value}" }).mkStringOrEmpty("?", "&", "") val hasHeaders = req.headers.headers.nonEmpty @@ -75,7 +75,7 @@ class RawRestTest extends FunSuite with ScalaFutures { test("simple GET") { testRestCall(_.self.user("ID"), - """-> GET user?userId=ID + """-> GET /user?userId=ID |<- 200 application/json |{"id":"ID","name":"ID-0-"} |""".stripMargin @@ -84,7 +84,7 @@ class RawRestTest extends FunSuite with ScalaFutures { test("simple POST with path, header and query") { testRestCall(_.self.user("paf", awesome = true, 42, User("ID", "Fred")), - """-> POST user/save/paf/moar/path?f=42 + """-> POST /user/save/paf/moar/path?f=42 |X-Awesome: true |application/json |{"id":"ID","name":"Fred"} @@ -94,7 +94,7 @@ class RawRestTest extends FunSuite with ScalaFutures { test("simple GET after prefix call") { testRestCall(_.subApi(1, "query").user("ID"), - """-> GET subApi/1/user?query=query&userId=ID + """-> GET /subApi/1/user?query=query&userId=ID |<- 200 application/json |{"id":"ID","name":"ID-1-query"} |""".stripMargin diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RestPathValidationTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RestPathValidationTest.scala new file mode 100644 index 000000000..591ca065e --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RestPathValidationTest.scala @@ -0,0 +1,38 @@ +package com.avsystem.commons +package rest + +import org.scalatest.FunSuite + +class RestPathValidationTest extends FunSuite { + trait Api2 { + def self: Api2 + } + object Api2 { + implicit val metadata: RestMetadata[Api2] = RestMetadata.materializeForRpc[Api2] + } + + test("recursive API") { + val failure = intercept[IllegalArgumentException](Api2.metadata.ensureUnambiguousPaths()) + assert(failure.getMessage == "call chain self->self is recursive, recursively defined server APIs are forbidden") + } + + trait Api1 { + @GET("p") def g1: Future[String] + @GET("p") def g2: Future[String] + @GET("") def g3(@Path("p") arg: String): Future[String] + + @POST("p") def p1: Future[String] + } + + test("simple ambiguous paths") { + val failure = intercept[IllegalArgumentException] { + RestMetadata.materializeForRpc[Api1].ensureUnambiguousPaths() + } + assert(failure.getMessage == + """REST API has ambiguous paths: + |GET /p may result from multiple calls: + | g2 + | g1""".stripMargin + ) + } +} From 697d1d6b6711b5a215205c3b060393bc2a65e2f7 Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 16:45:49 +0200 Subject: [PATCH 32/91] refactored rest.scala to multiple files --- .../com/avsystem/commons/rest/RawRest.scala | 120 ++++ .../commons/rest/RestApiCompanion.scala | 27 + .../avsystem/commons/rest/RestMetadata.scala | 173 +++++ .../avsystem/commons/rest/annotations.scala | 147 +++++ .../com/avsystem/commons/rest/data.scala | 144 +++++ .../com/avsystem/commons/rest/rest.scala | 597 ------------------ 6 files changed, 611 insertions(+), 597 deletions(-) create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/data.scala delete mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala new file mode 100644 index 000000000..08df91f01 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -0,0 +1,120 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.rpc._ + +case class RpcWithPath(rpcName: String, pathParams: List[PathValue]) +case class ResolvedPath(prefixes: List[RpcWithPath], finalCall: RpcWithPath, singleBody: Boolean) { + def prepend(rpcName: String, pathParams: List[PathValue]): ResolvedPath = + copy(prefixes = RpcWithPath(rpcName, pathParams) :: prefixes) + + def rpcChainRepr: String = + prefixes.iterator.map(_.rpcName).mkString("", "->", s"->${finalCall.rpcName}") +} + +@methodTag[RestMethodTag, Prefix] +@paramTag[RestParamTag, RestParamTag] +trait RawRest { + @multi + @tagged[Prefix] + @paramTag[RestParamTag, Path] + def prefix(@methodName name: String, @composite headers: RestHeaders): RawRest + + @multi + @tagged[GET] + @paramTag[RestParamTag, Query] + def get(@methodName name: String, @composite headers: RestHeaders): Future[RestResponse] + + @multi + @tagged[BodyMethodTag] + @paramTag[RestParamTag, JsonBodyParam] + def handle(@methodName name: String, @composite headers: RestHeaders, + @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): Future[RestResponse] + + @multi + @tagged[BodyMethodTag] + def handleSingle(@methodName name: String, @composite headers: RestHeaders, + @encoded @tagged[Body] body: HttpBody): Future[RestResponse] + + def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = { + metadata.ensureUnambiguousPaths() + locally[RestRequest => Future[RestResponse]] { + case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { + case List(ResolvedPath(prefixes, RpcWithPath(finalRpcName, finalPathParams), singleBody)) => + val finalRawRest = prefixes.foldLeft(this) { + case (rawRest, RpcWithPath(rpcName, pathParams)) => + rawRest.prefix(rpcName, headers.copy(path = pathParams)) + } + val finalHeaders = headers.copy(path = finalPathParams) + + if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) + else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) + else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) + + case Nil => + val pathStr = headers.path.iterator.map(_.value).mkString("/") + Future.successful(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) + + case multiple => + val pathStr = headers.path.iterator.map(_.value).mkString("/") + val callsRepr = multiple.iterator.map(p => s" ${p.rpcChainRepr}").mkString("\n", "\n", "") + throw new IllegalArgumentException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") + } + } + } +} + +object RawRest extends RawRpcCompanion[RawRest] { + def fromHandleRequest[Real: AsRealRpc : RestMetadata](handleRequest: RestRequest => Future[RestResponse]): Real = + RawRest.asReal(new DefaultRawRest(RestMetadata[Real], RestHeaders.Empty, handleRequest)) + + def asHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): RestRequest => Future[RestResponse] = + RawRest.asRaw(real).asHandleRequest(RestMetadata[Real]) + + private final class DefaultRawRest( + metadata: RestMetadata[_], + prefixHeaders: RestHeaders, + handleRequest: RestRequest => Future[RestResponse] + ) extends RawRest { + + def prefix(name: String, headers: RestHeaders): RawRest = { + val prefixMeta = metadata.prefixMethods.getOrElse(name, + throw new IllegalArgumentException(s"no such prefix method: $name")) + val newHeaders = prefixHeaders.append(prefixMeta, headers) + new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest) + } + + def get(name: String, headers: RestHeaders): Future[RestResponse] = + handleSingle(name, headers, HttpBody.Empty) + + def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Future[RestResponse] = { + val methodMeta = metadata.httpMethods.getOrElse(name, + throw new IllegalArgumentException(s"no such HTTP method: $name")) + val newHeaders = prefixHeaders.append(methodMeta, headers) + handleRequest(RestRequest(methodMeta.method, newHeaders, HttpBody.createJsonBody(body))) + } + + def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Future[RestResponse] = { + val methodMeta = metadata.httpMethods.getOrElse(name, + throw new IllegalArgumentException(s"no such HTTP method: $name")) + val newHeaders = prefixHeaders.append(methodMeta, headers) + handleRequest(RestRequest(methodMeta.method, newHeaders, body)) + } + } + + trait ClientMacroInstances[Real] { + def metadata: RestMetadata[Real] + def asReal: AsRealRpc[Real] + } + + trait ServerMacroInstances[Real] { + def metadata: RestMetadata[Real] + def asRaw: AsRawRpc[Real] + } + + trait FullMacroInstances[Real] extends ClientMacroInstances[Real] with ServerMacroInstances[Real] + + implicit def clientInstances[Real]: ClientMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] + implicit def serverInstances[Real]: ServerMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] + implicit def fullInstances[Real]: FullMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala new file mode 100644 index 000000000..6b6e026bc --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala @@ -0,0 +1,27 @@ +package com.avsystem.commons +package rest + +/** + * Base class for companions of REST API traits used only for REST clients to external services. + */ +abstract class RestClientApiCompanion[Real](implicit instances: RawRest.ClientMacroInstances[Real]) { + implicit def restMetadata: RestMetadata[Real] = instances.metadata + implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal +} + +/** + * Base class for companions of REST API traits used only for REST servers exposed to external world. + */ +abstract class RestServerApiCompanion[Real](implicit instances: RawRest.ServerMacroInstances[Real]) { + implicit def restMetadata: RestMetadata[Real] = instances.metadata + implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw +} + +/** + * Base class for companions of REST API traits used for both REST clients and servers. + */ +abstract class RestApiCompanion[Real](implicit instances: RawRest.FullMacroInstances[Real]) { + implicit def restMetadata: RestMetadata[Real] = instances.metadata + implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal + implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala new file mode 100644 index 000000000..cb7d7203b --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -0,0 +1,173 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.rpc._ + +@methodTag[RestMethodTag, Prefix] +@paramTag[RestParamTag, JsonBodyParam] +case class RestMetadata[T]( + @multi @tagged[Prefix] prefixMethods: Map[String, PrefixMetadata[_]], + @multi @tagged[HttpMethodTag] httpMethods: Map[String, HttpMethodMetadata[_]] +) { + def ensureUnambiguousPaths(): Unit = { + val trie = new RestMetadata.Trie + trie.fillWith(this) + trie.mergeWildcardToNamed() + val ambiguities = new MListBuffer[(String, List[String])] + trie.collectAmbiguousCalls(ambiguities) + if (ambiguities.nonEmpty) { + val problems = ambiguities.map { case (path, chains) => + s"$path may result from multiple calls:\n ${chains.mkString("\n ")}" + } + throw new IllegalArgumentException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") + } + } + + def resolvePath(method: HttpMethod, path: List[PathValue]): Iterator[ResolvedPath] = { + val asFinalCall = for { + (rpcName, m) <- httpMethods.iterator if m.method == method + (pathParams, Nil) <- m.extractPathParams(path) + } yield ResolvedPath(Nil, RpcWithPath(rpcName, pathParams), m.singleBody) + + val usingPrefix = for { + (rpcName, prefix) <- prefixMethods.iterator + (pathParams, pathTail) <- prefix.extractPathParams(path).iterator + suffixPath <- prefix.result.value.resolvePath(method, pathTail) + } yield suffixPath.prepend(rpcName, pathParams) + + asFinalCall ++ usingPrefix + } +} +object RestMetadata extends RpcMetadataCompanion[RestMetadata] { + private class Trie { + val rpcChains: Map[HttpMethod, MBuffer[String]] = + HttpMethod.values.mkMap(identity, _ => new MArrayBuffer[String]) + + val byName: MMap[String, Trie] = new MHashMap + var wildcard: Opt[Trie] = Opt.Empty + + def forPattern(pattern: List[Opt[PathValue]]): Trie = pattern match { + case Nil => this + case Opt(PathValue(pathName)) :: tail => + byName.getOrElseUpdate(pathName, new Trie).forPattern(tail) + case Opt.Empty :: tail => + wildcard.getOrElse(new Trie().setup(t => wildcard = Opt(t))).forPattern(tail) + } + + def fillWith(metadata: RestMetadata[_], prefixStack: List[(String, PrefixMetadata[_])] = Nil): Unit = { + def prefixChain: String = + prefixStack.reverseIterator.map({ case (k, _) => k }).mkStringOrEmpty("", "->", "->") + + metadata.prefixMethods.foreach { case entry@(rpcName, pm) => + if (prefixStack.contains(entry)) { + throw new IllegalArgumentException( + s"call chain $prefixChain$rpcName is recursive, recursively defined server APIs are forbidden") + } + forPattern(pm.pathPattern).fillWith(pm.result.value, entry :: prefixStack) + } + metadata.httpMethods.foreach { case (rpcName, hm) => + forPattern(hm.pathPattern).rpcChains(hm.method) += s"$prefixChain${rpcName.stripPrefix(s"${hm.method}_")}" + } + } + + private def merge(other: Trie): Unit = { + HttpMethod.values.foreach { meth => + rpcChains(meth) ++= other.rpcChains(meth) + } + for (w <- wildcard; ow <- other.wildcard) w.merge(ow) + wildcard = wildcard orElse other.wildcard + other.byName.foreach { case (name, trie) => + byName.getOrElseUpdate(name, new Trie).merge(trie) + } + } + + def mergeWildcardToNamed(): Unit = wildcard.foreach { wc => + wc.mergeWildcardToNamed() + byName.values.foreach { trie => + trie.merge(wc) + trie.mergeWildcardToNamed() + } + } + + def collectAmbiguousCalls(ambiguities: MBuffer[(String, List[String])], pathPrefix: List[String] = Nil): Unit = { + rpcChains.foreach { case (method, chains) => + if (chains.size > 1) { + val path = pathPrefix.reverse.mkString(s"$method /", "/", "") + ambiguities += ((path, chains.toList)) + } + } + wildcard.foreach(_.collectAmbiguousCalls(ambiguities, "*" :: pathPrefix)) + byName.foreach { case (name, trie) => + trie.collectAmbiguousCalls(ambiguities, name :: pathPrefix) + } + } + } +} + +sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { + def methodPath: List[PathValue] + def headersMetadata: RestHeadersMetadata + + val pathPattern: List[Opt[PathValue]] = + methodPath.map(Opt(_)) ++ headersMetadata.path.flatMap(pp => Opt.Empty :: pp.pathSuffix.map(Opt(_))) + + def applyPathParams(params: List[PathValue]): List[PathValue] = { + def loop(params: List[PathValue], pattern: List[Opt[PathValue]]): List[PathValue] = + (params, pattern) match { + case (Nil, Nil) => Nil + case (_, Opt(patternHead) :: patternTail) => patternHead :: loop(params, patternTail) + case (param :: paramsTail, Opt.Empty :: patternTail) => param :: loop(paramsTail, patternTail) + case _ => throw new IllegalArgumentException( + s"got ${params.size} path params, expected ${headersMetadata.path.size}") + } + loop(params, pathPattern) + } + + def extractPathParams(path: List[PathValue]): Opt[(List[PathValue], List[PathValue])] = { + def loop(path: List[PathValue], pattern: List[Opt[PathValue]]): Opt[(List[PathValue], List[PathValue])] = + (path, pattern) match { + case (pathTail, Nil) => Opt((Nil, pathTail)) + case (param :: pathTail, Opt.Empty :: patternTail) => + loop(pathTail, patternTail).map { case (params, tail) => (param :: params, tail) } + case (pathHead :: pathTail, Opt(patternHead) :: patternTail) if pathHead == patternHead => + loop(pathTail, patternTail) + case _ => Opt.Empty + } + loop(path, pathPattern) + } +} + +@paramTag[RestParamTag, Path] +case class PrefixMetadata[T]( + @reifyName name: String, + @optional @reifyAnnot methodTag: Opt[Prefix], + @composite headersMetadata: RestHeadersMetadata, + @checked @infer result: RestMetadata.Lazy[T] +) extends RestMethodMetadata[T] { + def methodPath: List[PathValue] = + PathValue.split(methodTag.map(_.path).getOrElse(name)) +} + +case class HttpMethodMetadata[T]( + @reifyAnnot methodTag: HttpMethodTag, + @composite headersMetadata: RestHeadersMetadata, + @multi @tagged[BodyTag] bodyParams: Map[String, BodyParamMetadata[_]] +) extends RestMethodMetadata[Future[T]] { + val method: HttpMethod = methodTag.method + val singleBody: Boolean = bodyParams.values.exists(_.singleBody) + def methodPath: List[PathValue] = PathValue.split(methodTag.path) +} + +case class RestHeadersMetadata( + @multi @tagged[Path] path: List[PathParamMetadata[_]], + @multi @tagged[Header] headers: Map[String, HeaderParamMetadata[_]], + @multi @tagged[Query] query: Map[String, QueryParamMetadata[_]] +) + +case class PathParamMetadata[T](@optional @reifyAnnot pathAnnot: Opt[Path]) extends TypedMetadata[T] { + val pathSuffix: List[PathValue] = PathValue.split(pathAnnot.fold("")(_.pathSuffix)) +} + +case class HeaderParamMetadata[T]() extends TypedMetadata[T] +case class QueryParamMetadata[T]() extends TypedMetadata[T] +case class BodyParamMetadata[T](@hasAnnot[Body] singleBody: Boolean) extends TypedMetadata[T] diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala new file mode 100644 index 000000000..f1991a08f --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala @@ -0,0 +1,147 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.annotation.{AnnotationAggregate, defaultsToName} +import com.avsystem.commons.rpc._ + +/** + * Base trait for tag annotations that determine how a REST method is translated into actual HTTP request. + * A REST method may be annotated with one of HTTP method tags ([[GET]], [[PUT]], [[POST]], [[PATCH]], [[DELETE]]) + * which means that this method represents actual HTTP call and is expected to return a `Future[Result]` where + * `Result` is encodable as [[RestResponse]]. + * + * If a REST method is not annotated with any of HTTP method tags, [[Prefix]] is assumed by default which means + * that this method only contributes to URL path, HTTP headers and query parameters but does not yet represent an + * actual HTTP request. Instead, it is expected to return some other REST API trait. + */ +sealed trait RestMethodTag extends RpcTag { + /** + * HTTP URL path segment associated with REST method annotated with this tag. This path may be multipart + * (i.e. contain slashes). It may also be empty which means that this particular REST method does not contribute + * anything to URL path. If path is not specified explicitly, method name is used (the actual method name, not + * `rpcName`). + * + * @example + * {{{ + * trait SomeRestApi { + * @GET("users/find") + * def findUser(userId: String): Future[User] + * } + * object SomeRestApi extends RestApiCompanion[SomeRestApi] + * }}} + */ + @defaultsToName def path: String +} + +sealed abstract class HttpMethodTag(val method: HttpMethod) extends RestMethodTag with AnnotationAggregate + +/** + * Base trait for annotations representing HTTP methods which may define a HTTP body. This includes + * [[PUT]], [[POST]], [[PATCH]] and [[DELETE]]. Parameters of REST methods annotated with one of these tags are + * by default serialized into JSON (through encoding to [[JsonValue]]) and combined into JSON object that is sent + * as HTTP body. + * + * Parameters may also contribute to URL path, HTTP headers and query parameters if annotated as + * [[Path]], [[Header]] or [[Query]]. + * + * REST method may also take a single parameter representing the entire HTTP body. Such parameter must be annotated + * as [[Body]] and must be the only body parameter of that method. Value of this parameter will be encoded as + * [[HttpBody]] which doesn't necessarily have to be JSON (it may define its own MIME type). + * + * @example + * {{{ + * trait SomeRestApi { + * @POST("users/create") def createUser(@Body user: User): Future[Unit] + * @PATCH("users/update") def updateUser(id: String, name: String): Future[User] + * } + * object SomeRestApi extends RestApiCompanion[SomeRestApi] + * }}} + */ +sealed abstract class BodyMethodTag(method: HttpMethod) extends HttpMethodTag(method) + +/** + * REST method annotated as `@GET` will translate to HTTP GET request. By default, parameters of such method + * are translated into URL query parameters (encoded as [[QueryValue]]). Alternatively, each parameter + * may be annotated as [[Path]] or [[Header]] which means that it will be translated into HTTP header value + * + * @param path see [[RestMethodTag.path]] + */ +final class GET(val path: String = null) extends HttpMethodTag(HttpMethod.GET) { + @rpcNamePrefix("GET_") type Implied +} + +/** See [[BodyMethodTag]] */ +final class POST(val path: String = null) extends BodyMethodTag(HttpMethod.POST) { + @rpcNamePrefix("POST_") type Implied +} +/** See [[BodyMethodTag]] */ +final class PATCH(val path: String = null) extends BodyMethodTag(HttpMethod.PATCH) { + @rpcNamePrefix("PATCH_") type Implied +} +/** See [[BodyMethodTag]] */ +final class PUT(val path: String = null) extends BodyMethodTag(HttpMethod.PUT) { + @rpcNamePrefix("PUT_") type Implied +} +/** See [[BodyMethodTag]] */ +final class DELETE(val path: String = null) extends BodyMethodTag(HttpMethod.DELETE) { + @rpcNamePrefix("DELETE_") type Implied +} + +/** + * REST methods annotated as [[Prefix]] are expected to return another REST API trait as their result. + * They do not yet represent an actual HTTP request but contribute to URL path, HTTP headers and query parameters. + * + * By default, parameters of a prefix method are interpreted as URL path fragments. Their values are encoded as + * [[PathValue]] and appended to URL path. Alternatively, each parameter may also be explicitly annotated as + * [[Header]] or [[Query]]. + * + * NOTE: REST method is interpreted as prefix method by default which means that there is no need to apply [[Prefix]] + * annotation explicitly unless you want to specify a custom path. + * + * @param path see [[RestMethodTag.path]] + */ +final class Prefix(val path: String = null) extends RestMethodTag + +sealed trait RestParamTag extends RpcTag + +/** + * REST method parameters annotated as [[Path]] will be encoded as [[PathValue]] and appended to URL path, in the + * declaration order. Parameters of [[Prefix]] REST methods are interpreted as [[Path]] parameters by default. + */ +final class Path(val pathSuffix: String = "") extends RestParamTag + +/** + * REST method parameters annotated as [[Header]] will be encoded as [[HeaderValue]] and added to HTTP headers. + * Header name must be explicitly given as argument of this annotation. + */ +final class Header(override val name: String) + extends rpcName(name) with RestParamTag + +/** + * REST method parameters annotated as [[Query]] will be encoded as [[QueryValue]] and added to URL query + * parameters. Parameters of [[GET]] REST methods are interpreted as [[Query]] parameters by default. + */ +final class Query(@defaultsToName override val name: String = null) + extends rpcName(name) with RestParamTag + +sealed trait BodyTag extends RestParamTag + +/** + * REST method parameters annotated as [[JsonBodyParam]] will be encoded as [[JsonValue]] and combined into + * a JSON object that will be sent as HTTP body. Body parameters are allowed only in REST methods annotated as + * [[POST]], [[PATCH]], [[PUT]] or [[DELETE]]. Actually, parameters of these methods are interpreted as + * [[JsonBodyParam]] by default which means that this annotation rarely needs to be applied explicitly. + */ +final class JsonBodyParam(@defaultsToName override val name: String = null) + extends rpcName(name) with BodyTag + +/** + * REST methods that can send HTTP body ([[POST]], [[PATCH]], [[PUT]] and [[DELETE]]) may take a single + * parameter annotated as [[Body]] which will be encoded as [[HttpBody]] and sent as the body of HTTP request. + * Such a method may not define any other body parameters (although it may take additional [[Path]], [[Header]] + * or [[Query]] parameters). + * + * The single body parameter may have a completely custom encoding to [[HttpBody]] which may define its own MIME type + * and doesn't necessarily have to be JSON. + */ +final class Body extends BodyTag diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala new file mode 100644 index 000000000..d04e88316 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala @@ -0,0 +1,144 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx} +import com.avsystem.commons.rpc._ +import com.avsystem.commons.serialization.GenCodec.ReadFailure +import com.avsystem.commons.serialization.json.{JsonReader, JsonStringInput, JsonStringOutput} +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} + +sealed trait RestValue extends Any { + def value: String +} + +/** + * Value used as encoding of [[Path]] parameters. Types that have `GenKeyCodec` instance have automatic encoding + * to [[PathValue]]. + */ +case class PathValue(value: String) extends AnyVal with RestValue +object PathValue { + def split(path: String): List[PathValue] = + path.split("/").iterator.filter(_.nonEmpty).map(PathValue(_)).toList +} + +/** + * Value used as encoding of [[Header]] parameters. Types that have `GenKeyCodec` instance have automatic encoding + * to [[HeaderValue]]. + */ +case class HeaderValue(value: String) extends AnyVal with RestValue + +/** + * Value used as encoding of [[Query]] parameters. Types that have `GenKeyCodec` instance have automatic encoding + * to [[QueryValue]]. + */ +case class QueryValue(value: String) extends AnyVal with RestValue + +/** + * Value used as encoding of [[JsonBodyParam]] parameters. Types that have `GenCodec` instance have automatic encoding + * to [[JsonValue]]. + */ +case class JsonValue(value: String) extends AnyVal with RestValue + +object RestValue { + implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = + AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = + AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[QueryValue, T] = + AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def jsonValueDefaultAsRealRaw[T: GenCodec]: AsRawReal[JsonValue, T] = + AsRawReal.create(v => JsonValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) +} + +/** + * Value used to represent HTTP body. Also used as direct encoding of [[Body]] parameters. Types that have + * encoding to [[JsonValue]] (e.g. types that have `GenCodec` instance) automatically have encoding to [[HttpBody]] + * which uses application/json MIME type. There is also a specialized encoding provided for `Unit` which maps it + * to empty HTTP body instead of JSON containing "null". + * + * @param content raw HTTP body content + * @param mimeType MIME type, i.e. HTTP `Content-Type` without charset specified + */ +case class HttpBody(content: String, mimeType: String) { + def jsonValue: JsonValue = mimeType match { + case HttpBody.JsonType => JsonValue(content) + case _ => throw new ReadFailure(s"Expected application/json type, got $mimeType") + } +} +object HttpBody { + final val PlainType = "text/plain" + final val JsonType = "application/json" + + def plain(value: String): HttpBody = HttpBody(value, PlainType) + def json(json: JsonValue): HttpBody = HttpBody(json.value, JsonType) + + final val Empty: HttpBody = HttpBody.plain("") + + def createJsonBody(fields: NamedParams[JsonValue]): HttpBody = + if (fields.isEmpty) HttpBody.Empty else { + val sb = new JStringBuilder + val oo = new JsonStringOutput(sb).writeObject() + fields.foreach { + case (key, JsonValue(json)) => + oo.writeField(key).writeRawJson(json) + } + oo.finish() + HttpBody.json(JsonValue(sb.toString)) + } + + def parseJsonBody(body: HttpBody): NamedParams[JsonValue] = + if (body.content.isEmpty) NamedParams.empty else { + val oi = new JsonStringInput(new JsonReader(body.jsonValue.value)).readObject() + val builder = NamedParams.newBuilder[JsonValue] + while (oi.hasNext) { + val fi = oi.nextField() + builder += ((fi.fieldName, JsonValue(fi.readRawJson()))) + } + builder.result() + } + + implicit val emptyBodyForUnit: AsRawReal[HttpBody, Unit] = + AsRawReal.create(_ => HttpBody.Empty, _ => ()) + implicit def httpBodyJsonAsRaw[T](implicit jsonAsRaw: AsRaw[JsonValue, T]): AsRaw[HttpBody, T] = + AsRaw.create(v => HttpBody.json(jsonAsRaw.asRaw(v))) + implicit def httpBodyJsonAsReal[T](implicit jsonAsReal: AsReal[JsonValue, T]): AsReal[HttpBody, T] = + AsReal.create(v => jsonAsReal.asReal(v.jsonValue)) +} + +/** + * Enum representing HTTP methods. + */ +final class HttpMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum +object HttpMethod extends AbstractValueEnumCompanion[HttpMethod] { + final val GET, PUT, POST, PATCH, DELETE: Value = new HttpMethod +} + +case class RestHeaders( + @multi @tagged[Path] path: List[PathValue], + @multi @tagged[Header] headers: NamedParams[HeaderValue], + @multi @tagged[Query] query: NamedParams[QueryValue] +) { + def append(method: RestMethodMetadata[_], otherHeaders: RestHeaders): RestHeaders = RestHeaders( + path ::: method.applyPathParams(otherHeaders.path), + headers ++ otherHeaders.headers, + query ++ otherHeaders.query + ) +} +object RestHeaders { + final val Empty = RestHeaders(Nil, NamedParams.empty, NamedParams.empty) +} + +class HttpErrorException(code: Int, payload: String) + extends RuntimeException(s"$code: $payload") + +case class RestRequest(method: HttpMethod, headers: RestHeaders, body: HttpBody) +case class RestResponse(code: Int, body: HttpBody) +object RestResponse { + implicit def defaultFutureAsRaw[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[Future[RestResponse], Future[T]] = + AsRaw.create(_.mapNow(v => RestResponse(200, bodyAsRaw.asRaw(v)))) + implicit def defaultFutureAsReal[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[Future[RestResponse], Future[T]] = + AsReal.create(_.mapNow { + case RestResponse(200, body) => bodyAsReal.asReal(body) + case RestResponse(code, body) => throw new HttpErrorException(code, body.content) + }) +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala deleted file mode 100644 index f701e3dcf..000000000 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/rest.scala +++ /dev/null @@ -1,597 +0,0 @@ -package com.avsystem.commons -package rest - -import com.avsystem.commons.annotation.{AnnotationAggregate, defaultsToName} -import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx} -import com.avsystem.commons.rpc._ -import com.avsystem.commons.serialization.GenCodec.ReadFailure -import com.avsystem.commons.serialization.json.{JsonReader, JsonStringInput, JsonStringOutput} -import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} - -/** - * Base trait for tag annotations that determine how a REST method is translated into actual HTTP request. - * A REST method may be annotated with one of HTTP method tags ([[GET]], [[PUT]], [[POST]], [[PATCH]], [[DELETE]]) - * which means that this method represents actual HTTP call and is expected to return a `Future[Result]` where - * `Result` is encodable as [[RestResponse]]. - * - * If a REST method is not annotated with any of HTTP method tags, [[Prefix]] is assumed by default which means - * that this method only contributes to URL path, HTTP headers and query parameters but does not yet represent an - * actual HTTP request. Instead, it is expected to return some other REST API trait. - */ -sealed trait RestMethodTag extends RpcTag { - /** - * HTTP URL path segment associated with REST method annotated with this tag. This path may be multipart - * (i.e. contain slashes). It may also be empty which means that this particular REST method does not contribute - * anything to URL path. If path is not specified explicitly, method name is used (the actual method name, not - * `rpcName`). - * - * @example - * {{{ - * trait SomeRestApi { - * @GET("users/find") - * def findUser(userId: String): Future[User] - * } - * object SomeRestApi extends RestApiCompanion[SomeRestApi] - * }}} - */ - @defaultsToName def path: String -} - -sealed abstract class HttpMethodTag(val method: HttpMethod) extends RestMethodTag with AnnotationAggregate - -/** - * Base trait for annotations representing HTTP methods which may define a HTTP body. This includes - * [[PUT]], [[POST]], [[PATCH]] and [[DELETE]]. Parameters of REST methods annotated with one of these tags are - * by default serialized into JSON (through encoding to [[JsonValue]]) and combined into JSON object that is sent - * as HTTP body. - * - * Parameters may also contribute to URL path, HTTP headers and query parameters if annotated as - * [[Path]], [[Header]] or [[Query]]. - * - * REST method may also take a single parameter representing the entire HTTP body. Such parameter must be annotated - * as [[Body]] and must be the only body parameter of that method. Value of this parameter will be encoded as - * [[HttpBody]] which doesn't necessarily have to be JSON (it may define its own MIME type). - * - * @example - * {{{ - * trait SomeRestApi { - * @POST("users/create") def createUser(@Body user: User): Future[Unit] - * @PATCH("users/update") def updateUser(id: String, name: String): Future[User] - * } - * object SomeRestApi extends RestApiCompanion[SomeRestApi] - * }}} - */ -sealed abstract class BodyMethodTag(method: HttpMethod) extends HttpMethodTag(method) - -/** - * REST method annotated as `@GET` will translate to HTTP GET request. By default, parameters of such method - * are translated into URL query parameters (encoded as [[QueryValue]]). Alternatively, each parameter - * may be annotated as [[Path]] or [[Header]] which means that it will be translated into HTTP header value - * - * @param path see [[RestMethodTag.path]] - */ -final class GET(val path: String = null) extends HttpMethodTag(HttpMethod.GET) { - @rpcNamePrefix("GET_") type Implied -} - -/** See [[BodyMethodTag]] */ -final class POST(val path: String = null) extends BodyMethodTag(HttpMethod.POST) { - @rpcNamePrefix("POST_") type Implied -} -/** See [[BodyMethodTag]] */ -final class PATCH(val path: String = null) extends BodyMethodTag(HttpMethod.PATCH) { - @rpcNamePrefix("PATCH_") type Implied -} -/** See [[BodyMethodTag]] */ -final class PUT(val path: String = null) extends BodyMethodTag(HttpMethod.PUT) { - @rpcNamePrefix("PUT_") type Implied -} -/** See [[BodyMethodTag]] */ -final class DELETE(val path: String = null) extends BodyMethodTag(HttpMethod.DELETE) { - @rpcNamePrefix("DELETE_") type Implied -} - -/** - * REST methods annotated as [[Prefix]] are expected to return another REST API trait as their result. - * They do not yet represent an actual HTTP request but contribute to URL path, HTTP headers and query parameters. - * - * By default, parameters of a prefix method are interpreted as URL path fragments. Their values are encoded as - * [[PathValue]] and appended to URL path. Alternatively, each parameter may also be explicitly annotated as - * [[Header]] or [[Query]]. - * - * NOTE: REST method is interpreted as prefix method by default which means that there is no need to apply [[Prefix]] - * annotation explicitly unless you want to specify a custom path. - * - * @param path see [[RestMethodTag.path]] - */ -final class Prefix(val path: String = null) extends RestMethodTag - -sealed trait RestParamTag extends RpcTag - -/** - * REST method parameters annotated as [[Path]] will be encoded as [[PathValue]] and appended to URL path, in the - * declaration order. Parameters of [[Prefix]] REST methods are interpreted as [[Path]] parameters by default. - */ -final class Path(val pathSuffix: String = "") extends RestParamTag - -/** - * REST method parameters annotated as [[Header]] will be encoded as [[HeaderValue]] and added to HTTP headers. - * Header name must be explicitly given as argument of this annotation. - */ -final class Header(override val name: String) - extends rpcName(name) with RestParamTag - -/** - * REST method parameters annotated as [[Query]] will be encoded as [[QueryValue]] and added to URL query - * parameters. Parameters of [[GET]] REST methods are interpreted as [[Query]] parameters by default. - */ -final class Query(@defaultsToName override val name: String = null) - extends rpcName(name) with RestParamTag - -sealed trait BodyTag extends RestParamTag - -/** - * REST method parameters annotated as [[JsonBodyParam]] will be encoded as [[JsonValue]] and combined into - * a JSON object that will be sent as HTTP body. Body parameters are allowed only in REST methods annotated as - * [[POST]], [[PATCH]], [[PUT]] or [[DELETE]]. Actually, parameters of these methods are interpreted as - * [[JsonBodyParam]] by default which means that this annotation rarely needs to be applied explicitly. - */ -final class JsonBodyParam(@defaultsToName override val name: String = null) - extends rpcName(name) with BodyTag - -/** - * REST methods that can send HTTP body ([[POST]], [[PATCH]], [[PUT]] and [[DELETE]]) may take a single - * parameter annotated as [[Body]] which will be encoded as [[HttpBody]] and sent as the body of HTTP request. - * Such a method may not define any other body parameters (although it may take additional [[Path]], [[Header]] - * or [[Query]] parameters). - * - * The single body parameter may have a completely custom encoding to [[HttpBody]] which may define its own MIME type - * and doesn't necessarily have to be JSON. - */ -final class Body extends BodyTag - -sealed trait RestValue extends Any { - def value: String -} - -/** - * Value used as encoding of [[Path]] parameters. Types that have `GenKeyCodec` instance have automatic encoding - * to [[PathValue]]. - */ -case class PathValue(value: String) extends AnyVal with RestValue -object PathValue { - def split(path: String): List[PathValue] = - path.split("/").iterator.filter(_.nonEmpty).map(PathValue(_)).toList -} - -/** - * Value used as encoding of [[Header]] parameters. Types that have `GenKeyCodec` instance have automatic encoding - * to [[HeaderValue]]. - */ -case class HeaderValue(value: String) extends AnyVal with RestValue - -/** - * Value used as encoding of [[Query]] parameters. Types that have `GenKeyCodec` instance have automatic encoding - * to [[QueryValue]]. - */ -case class QueryValue(value: String) extends AnyVal with RestValue - -/** - * Value used as encoding of [[JsonBodyParam]] parameters. Types that have `GenCodec` instance have automatic encoding - * to [[JsonValue]]. - */ -case class JsonValue(value: String) extends AnyVal with RestValue - -object RestValue { - implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = - AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = - AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[QueryValue, T] = - AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def jsonValueDefaultAsRealRaw[T: GenCodec]: AsRawReal[JsonValue, T] = - AsRawReal.create(v => JsonValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) -} - -/** - * Value used to represent HTTP body. Also used as direct encoding of [[Body]] parameters. Types that have - * encoding to [[JsonValue]] (e.g. types that have `GenCodec` instance) automatically have encoding to [[HttpBody]] - * which uses application/json MIME type. There is also a specialized encoding provided for `Unit` which maps it - * to empty HTTP body instead of JSON containing "null". - * - * @param content raw HTTP body content - * @param mimeType MIME type, i.e. HTTP `Content-Type` without charset specified - */ -case class HttpBody(content: String, mimeType: String) { - def jsonValue: JsonValue = mimeType match { - case HttpBody.JsonType => JsonValue(content) - case _ => throw new ReadFailure(s"Expected application/json type, got $mimeType") - } -} -object HttpBody { - final val PlainType = "text/plain" - final val JsonType = "application/json" - - def plain(value: String): HttpBody = HttpBody(value, PlainType) - def json(json: JsonValue): HttpBody = HttpBody(json.value, JsonType) - - final val Empty: HttpBody = HttpBody.plain("") - - def createJsonBody(fields: NamedParams[JsonValue]): HttpBody = - if (fields.isEmpty) HttpBody.Empty else { - val sb = new JStringBuilder - val oo = new JsonStringOutput(sb).writeObject() - fields.foreach { - case (key, JsonValue(json)) => - oo.writeField(key).writeRawJson(json) - } - oo.finish() - HttpBody.json(JsonValue(sb.toString)) - } - - def parseJsonBody(body: HttpBody): NamedParams[JsonValue] = - if (body.content.isEmpty) NamedParams.empty else { - val oi = new JsonStringInput(new JsonReader(body.jsonValue.value)).readObject() - val builder = NamedParams.newBuilder[JsonValue] - while (oi.hasNext) { - val fi = oi.nextField() - builder += ((fi.fieldName, JsonValue(fi.readRawJson()))) - } - builder.result() - } - - implicit val emptyBodyForUnit: AsRawReal[HttpBody, Unit] = - AsRawReal.create(_ => HttpBody.Empty, _ => ()) - implicit def httpBodyJsonAsRaw[T](implicit jsonAsRaw: AsRaw[JsonValue, T]): AsRaw[HttpBody, T] = - AsRaw.create(v => HttpBody.json(jsonAsRaw.asRaw(v))) - implicit def httpBodyJsonAsReal[T](implicit jsonAsReal: AsReal[JsonValue, T]): AsReal[HttpBody, T] = - AsReal.create(v => jsonAsReal.asReal(v.jsonValue)) -} - -/** - * Enum representing HTTP methods. - */ -final class HttpMethod(implicit enumCtx: EnumCtx) extends AbstractValueEnum -object HttpMethod extends AbstractValueEnumCompanion[HttpMethod] { - final val GET, PUT, POST, PATCH, DELETE: Value = new HttpMethod -} - -case class RestHeaders( - @multi @tagged[Path] path: List[PathValue], - @multi @tagged[Header] headers: NamedParams[HeaderValue], - @multi @tagged[Query] query: NamedParams[QueryValue] -) { - def append(method: RestMethodMetadata[_], otherHeaders: RestHeaders): RestHeaders = RestHeaders( - path ::: method.applyPathParams(otherHeaders.path), - headers ++ otherHeaders.headers, - query ++ otherHeaders.query - ) -} -object RestHeaders { - final val Empty = RestHeaders(Nil, NamedParams.empty, NamedParams.empty) -} - -class HttpErrorException(code: Int, payload: String) - extends RuntimeException(s"$code: $payload") - -case class RestRequest(method: HttpMethod, headers: RestHeaders, body: HttpBody) -case class RestResponse(code: Int, body: HttpBody) -object RestResponse { - implicit def defaultFutureAsRaw[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[Future[RestResponse], Future[T]] = - AsRaw.create(_.mapNow(v => RestResponse(200, bodyAsRaw.asRaw(v)))) - implicit def defaultFutureAsReal[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[Future[RestResponse], Future[T]] = - AsReal.create(_.mapNow { - case RestResponse(200, body) => bodyAsReal.asReal(body) - case RestResponse(code, body) => throw new HttpErrorException(code, body.content) - }) -} - -@methodTag[RestMethodTag, Prefix] -@paramTag[RestParamTag, RestParamTag] -trait RawRest { - @multi - @tagged[Prefix] - @paramTag[RestParamTag, Path] - def prefix(@methodName name: String, @composite headers: RestHeaders): RawRest - - @multi - @tagged[GET] - @paramTag[RestParamTag, Query] - def get(@methodName name: String, @composite headers: RestHeaders): Future[RestResponse] - - @multi - @tagged[BodyMethodTag] - @paramTag[RestParamTag, JsonBodyParam] - def handle(@methodName name: String, @composite headers: RestHeaders, - @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): Future[RestResponse] - - @multi - @tagged[BodyMethodTag] - def handleSingle(@methodName name: String, @composite headers: RestHeaders, - @encoded @tagged[Body] body: HttpBody): Future[RestResponse] - - def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = { - metadata.ensureUnambiguousPaths() - locally[RestRequest => Future[RestResponse]] { - case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { - case List(ResolvedPath(prefixes, RpcWithPath(finalRpcName, finalPathParams), singleBody)) => - val finalRawRest = prefixes.foldLeft(this) { - case (rawRest, RpcWithPath(rpcName, pathParams)) => - rawRest.prefix(rpcName, headers.copy(path = pathParams)) - } - val finalHeaders = headers.copy(path = finalPathParams) - - if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) - else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) - else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) - - case Nil => - val pathStr = headers.path.iterator.map(_.value).mkString("/") - Future.successful(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) - - case multiple => - val pathStr = headers.path.iterator.map(_.value).mkString("/") - val callsRepr = multiple.iterator.map(p => s" ${p.rpcChainRepr}").mkString("\n", "\n", "") - throw new IllegalArgumentException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") - } - } - } -} - -object RawRest extends RawRpcCompanion[RawRest] { - def fromHandleRequest[Real: AsRealRpc : RestMetadata](handleRequest: RestRequest => Future[RestResponse]): Real = - RawRest.asReal(new DefaultRawRest(RestMetadata[Real], RestHeaders.Empty, handleRequest)) - - def asHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): RestRequest => Future[RestResponse] = - RawRest.asRaw(real).asHandleRequest(RestMetadata[Real]) - - private final class DefaultRawRest( - metadata: RestMetadata[_], - prefixHeaders: RestHeaders, - handleRequest: RestRequest => Future[RestResponse] - ) extends RawRest { - - def prefix(name: String, headers: RestHeaders): RawRest = { - val prefixMeta = metadata.prefixMethods.getOrElse(name, - throw new IllegalArgumentException(s"no such prefix method: $name")) - val newHeaders = prefixHeaders.append(prefixMeta, headers) - new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest) - } - - def get(name: String, headers: RestHeaders): Future[RestResponse] = - handleSingle(name, headers, HttpBody.Empty) - - def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Future[RestResponse] = { - val methodMeta = metadata.httpMethods.getOrElse(name, - throw new IllegalArgumentException(s"no such HTTP method: $name")) - val newHeaders = prefixHeaders.append(methodMeta, headers) - handleRequest(RestRequest(methodMeta.method, newHeaders, HttpBody.createJsonBody(body))) - } - - def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Future[RestResponse] = { - val methodMeta = metadata.httpMethods.getOrElse(name, - throw new IllegalArgumentException(s"no such HTTP method: $name")) - val newHeaders = prefixHeaders.append(methodMeta, headers) - handleRequest(RestRequest(methodMeta.method, newHeaders, body)) - } - } - - trait ClientMacroInstances[Real] { - def metadata: RestMetadata[Real] - def asReal: AsRealRpc[Real] - } - - trait ServerMacroInstances[Real] { - def metadata: RestMetadata[Real] - def asRaw: AsRawRpc[Real] - } - - trait FullMacroInstances[Real] extends ClientMacroInstances[Real] with ServerMacroInstances[Real] - - implicit def clientInstances[Real]: ClientMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] - implicit def serverInstances[Real]: ServerMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] - implicit def fullInstances[Real]: FullMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] -} - -/** - * Base class for companions of REST API traits used only for REST clients to external services. - */ -abstract class RestClientApiCompanion[Real](implicit instances: RawRest.ClientMacroInstances[Real]) { - implicit def restMetadata: RestMetadata[Real] = instances.metadata - implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal -} - -/** - * Base class for companions of REST API traits used only for REST servers exposed to external world. - */ -abstract class RestServerApiCompanion[Real](implicit instances: RawRest.ServerMacroInstances[Real]) { - implicit def restMetadata: RestMetadata[Real] = instances.metadata - implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw -} - -/** - * Base class for companions of REST API traits used for both REST clients and servers. - */ -abstract class RestApiCompanion[Real](implicit instances: RawRest.FullMacroInstances[Real]) { - implicit def restMetadata: RestMetadata[Real] = instances.metadata - implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal - implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw -} - -case class RpcWithPath(rpcName: String, pathParams: List[PathValue]) -case class ResolvedPath(prefixes: List[RpcWithPath], finalCall: RpcWithPath, singleBody: Boolean) { - def prepend(rpcName: String, pathParams: List[PathValue]): ResolvedPath = - copy(prefixes = RpcWithPath(rpcName, pathParams) :: prefixes) - - def rpcChainRepr: String = - prefixes.iterator.map(_.rpcName).mkString("", "->", s"->${finalCall.rpcName}") -} - -@methodTag[RestMethodTag, Prefix] -@paramTag[RestParamTag, JsonBodyParam] -case class RestMetadata[T]( - @multi @tagged[Prefix] prefixMethods: Map[String, PrefixMetadata[_]], - @multi @tagged[HttpMethodTag] httpMethods: Map[String, HttpMethodMetadata[_]] -) { - def ensureUnambiguousPaths(): Unit = { - val trie = new RestMetadata.Trie - trie.fillWith(this) - trie.mergeWildcardToNamed() - val ambiguities = new MListBuffer[(String, List[String])] - trie.collectAmbiguousCalls(ambiguities) - if (ambiguities.nonEmpty) { - val problems = ambiguities.map { case (path, chains) => - s"$path may result from multiple calls:\n ${chains.mkString("\n ")}" - } - throw new IllegalArgumentException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") - } - } - - def resolvePath(method: HttpMethod, path: List[PathValue]): Iterator[ResolvedPath] = { - val asFinalCall = for { - (rpcName, m) <- httpMethods.iterator if m.method == method - (pathParams, Nil) <- m.extractPathParams(path) - } yield ResolvedPath(Nil, RpcWithPath(rpcName, pathParams), m.singleBody) - - val usingPrefix = for { - (rpcName, prefix) <- prefixMethods.iterator - (pathParams, pathTail) <- prefix.extractPathParams(path).iterator - suffixPath <- prefix.result.value.resolvePath(method, pathTail) - } yield suffixPath.prepend(rpcName, pathParams) - - asFinalCall ++ usingPrefix - } -} -object RestMetadata extends RpcMetadataCompanion[RestMetadata] { - private class Trie { - val rpcChains: Map[HttpMethod, MBuffer[String]] = - HttpMethod.values.mkMap(identity, _ => new MArrayBuffer[String]) - - val byName: MMap[String, Trie] = new MHashMap - var wildcard: Opt[Trie] = Opt.Empty - - def forPattern(pattern: List[Opt[PathValue]]): Trie = pattern match { - case Nil => this - case Opt(PathValue(pathName)) :: tail => - byName.getOrElseUpdate(pathName, new Trie).forPattern(tail) - case Opt.Empty :: tail => - wildcard.getOrElse(new Trie().setup(t => wildcard = Opt(t))).forPattern(tail) - } - - def fillWith(metadata: RestMetadata[_], prefixStack: List[(String, PrefixMetadata[_])] = Nil): Unit = { - def prefixChain: String = - prefixStack.reverseIterator.map({ case (k, _) => k }).mkStringOrEmpty("", "->", "->") - - metadata.prefixMethods.foreach { case entry@(rpcName, pm) => - if (prefixStack.contains(entry)) { - throw new IllegalArgumentException( - s"call chain $prefixChain$rpcName is recursive, recursively defined server APIs are forbidden") - } - forPattern(pm.pathPattern).fillWith(pm.result.value, entry :: prefixStack) - } - metadata.httpMethods.foreach { case (rpcName, hm) => - forPattern(hm.pathPattern).rpcChains(hm.method) += s"$prefixChain${rpcName.stripPrefix(s"${hm.method}_")}" - } - } - - private def merge(other: Trie): Unit = { - HttpMethod.values.foreach { meth => - rpcChains(meth) ++= other.rpcChains(meth) - } - for (w <- wildcard; ow <- other.wildcard) w.merge(ow) - wildcard = wildcard orElse other.wildcard - other.byName.foreach { case (name, trie) => - byName.getOrElseUpdate(name, new Trie).merge(trie) - } - } - - def mergeWildcardToNamed(): Unit = wildcard.foreach { wc => - wc.mergeWildcardToNamed() - byName.values.foreach { trie => - trie.merge(wc) - trie.mergeWildcardToNamed() - } - } - - def collectAmbiguousCalls(ambiguities: MBuffer[(String, List[String])], pathPrefix: List[String] = Nil): Unit = { - rpcChains.foreach { case (method, chains) => - if (chains.size > 1) { - val path = pathPrefix.reverse.mkString(s"$method /", "/", "") - ambiguities += ((path, chains.toList)) - } - } - wildcard.foreach(_.collectAmbiguousCalls(ambiguities, "*" :: pathPrefix)) - byName.foreach { case (name, trie) => - trie.collectAmbiguousCalls(ambiguities, name :: pathPrefix) - } - } - } -} - -sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { - def methodPath: List[PathValue] - def headersMetadata: RestHeadersMetadata - - val pathPattern: List[Opt[PathValue]] = - methodPath.map(Opt(_)) ++ headersMetadata.path.flatMap(pp => Opt.Empty :: pp.pathSuffix.map(Opt(_))) - - def applyPathParams(params: List[PathValue]): List[PathValue] = { - def loop(params: List[PathValue], pattern: List[Opt[PathValue]]): List[PathValue] = - (params, pattern) match { - case (Nil, Nil) => Nil - case (_, Opt(patternHead) :: patternTail) => patternHead :: loop(params, patternTail) - case (param :: paramsTail, Opt.Empty :: patternTail) => param :: loop(paramsTail, patternTail) - case _ => throw new IllegalArgumentException( - s"got ${params.size} path params, expected ${headersMetadata.path.size}") - } - loop(params, pathPattern) - } - - def extractPathParams(path: List[PathValue]): Opt[(List[PathValue], List[PathValue])] = { - def loop(path: List[PathValue], pattern: List[Opt[PathValue]]): Opt[(List[PathValue], List[PathValue])] = - (path, pattern) match { - case (pathTail, Nil) => Opt((Nil, pathTail)) - case (param :: pathTail, Opt.Empty :: patternTail) => - loop(pathTail, patternTail).map { case (params, tail) => (param :: params, tail) } - case (pathHead :: pathTail, Opt(patternHead) :: patternTail) if pathHead == patternHead => - loop(pathTail, patternTail) - case _ => Opt.Empty - } - loop(path, pathPattern) - } -} - -@paramTag[RestParamTag, Path] -case class PrefixMetadata[T]( - @reifyName name: String, - @optional @reifyAnnot methodTag: Opt[Prefix], - @composite headersMetadata: RestHeadersMetadata, - @checked @infer result: RestMetadata.Lazy[T] -) extends RestMethodMetadata[T] { - def methodPath: List[PathValue] = - PathValue.split(methodTag.map(_.path).getOrElse(name)) -} - -case class HttpMethodMetadata[T]( - @reifyAnnot methodTag: HttpMethodTag, - @composite headersMetadata: RestHeadersMetadata, - @multi @tagged[BodyTag] bodyParams: Map[String, BodyParamMetadata[_]] -) extends RestMethodMetadata[Future[T]] { - val method: HttpMethod = methodTag.method - val singleBody: Boolean = bodyParams.values.exists(_.singleBody) - def methodPath: List[PathValue] = PathValue.split(methodTag.path) -} - -case class RestHeadersMetadata( - @multi @tagged[Path] path: List[PathParamMetadata[_]], - @multi @tagged[Header] headers: Map[String, HeaderParamMetadata[_]], - @multi @tagged[Query] query: Map[String, QueryParamMetadata[_]] -) - -case class PathParamMetadata[T](@optional @reifyAnnot pathAnnot: Opt[Path]) extends TypedMetadata[T] { - val pathSuffix: List[PathValue] = PathValue.split(pathAnnot.fold("")(_.pathSuffix)) -} - -case class HeaderParamMetadata[T]() extends TypedMetadata[T] -case class QueryParamMetadata[T]() extends TypedMetadata[T] -case class BodyParamMetadata[T](@hasAnnot[Body] singleBody: Boolean) extends TypedMetadata[T] From c6b65475f9cfdb2c4082c796273acc6953e5101a Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 16:59:05 +0200 Subject: [PATCH 33/91] path pattern now holds param metadata --- .../avsystem/commons/rest/RestMetadata.scala | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index cb7d7203b..a78abe932 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -46,11 +46,11 @@ object RestMetadata extends RpcMetadataCompanion[RestMetadata] { val byName: MMap[String, Trie] = new MHashMap var wildcard: Opt[Trie] = Opt.Empty - def forPattern(pattern: List[Opt[PathValue]]): Trie = pattern match { + def forPattern(pattern: List[PathPatternElement]): Trie = pattern match { case Nil => this - case Opt(PathValue(pathName)) :: tail => + case PathName(PathValue(pathName)) :: tail => byName.getOrElseUpdate(pathName, new Trie).forPattern(tail) - case Opt.Empty :: tail => + case PathParam(_) :: tail => wildcard.getOrElse(new Trie().setup(t => wildcard = Opt(t))).forPattern(tail) } @@ -104,19 +104,23 @@ object RestMetadata extends RpcMetadataCompanion[RestMetadata] { } } +sealed trait PathPatternElement +case class PathName(value: PathValue) extends PathPatternElement +case class PathParam(parameter: PathParamMetadata[_]) extends PathPatternElement + sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { def methodPath: List[PathValue] def headersMetadata: RestHeadersMetadata - val pathPattern: List[Opt[PathValue]] = - methodPath.map(Opt(_)) ++ headersMetadata.path.flatMap(pp => Opt.Empty :: pp.pathSuffix.map(Opt(_))) + val pathPattern: List[PathPatternElement] = + methodPath.map(PathName) ++ headersMetadata.path.flatMap(pp => PathParam(pp) :: pp.pathSuffix.map(PathName)) def applyPathParams(params: List[PathValue]): List[PathValue] = { - def loop(params: List[PathValue], pattern: List[Opt[PathValue]]): List[PathValue] = + def loop(params: List[PathValue], pattern: List[PathPatternElement]): List[PathValue] = (params, pattern) match { case (Nil, Nil) => Nil - case (_, Opt(patternHead) :: patternTail) => patternHead :: loop(params, patternTail) - case (param :: paramsTail, Opt.Empty :: patternTail) => param :: loop(paramsTail, patternTail) + case (_, PathName(patternHead) :: patternTail) => patternHead :: loop(params, patternTail) + case (param :: paramsTail, PathParam(_) :: patternTail) => param :: loop(paramsTail, patternTail) case _ => throw new IllegalArgumentException( s"got ${params.size} path params, expected ${headersMetadata.path.size}") } @@ -124,12 +128,12 @@ sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { } def extractPathParams(path: List[PathValue]): Opt[(List[PathValue], List[PathValue])] = { - def loop(path: List[PathValue], pattern: List[Opt[PathValue]]): Opt[(List[PathValue], List[PathValue])] = + def loop(path: List[PathValue], pattern: List[PathPatternElement]): Opt[(List[PathValue], List[PathValue])] = (path, pattern) match { case (pathTail, Nil) => Opt((Nil, pathTail)) - case (param :: pathTail, Opt.Empty :: patternTail) => + case (param :: pathTail, PathParam(_) :: patternTail) => loop(pathTail, patternTail).map { case (params, tail) => (param :: params, tail) } - case (pathHead :: pathTail, Opt(patternHead) :: patternTail) if pathHead == patternHead => + case (pathHead :: pathTail, PathName(patternHead) :: patternTail) if pathHead == patternHead => loop(pathTail, patternTail) case _ => Opt.Empty } From 205edf99ecb0d152c98a88ebe7e961dcabc0b11c Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 17:01:40 +0200 Subject: [PATCH 34/91] PathParamMetadata has rpcName --- .../main/scala/com/avsystem/commons/rest/RestMetadata.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index a78abe932..850a8ba9e 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -168,7 +168,10 @@ case class RestHeadersMetadata( @multi @tagged[Query] query: Map[String, QueryParamMetadata[_]] ) -case class PathParamMetadata[T](@optional @reifyAnnot pathAnnot: Opt[Path]) extends TypedMetadata[T] { +case class PathParamMetadata[T]( + @reifyName(rpcName = true) rpcName: String, + @optional @reifyAnnot pathAnnot: Opt[Path] +) extends TypedMetadata[T] { val pathSuffix: List[PathValue] = PathValue.split(pathAnnot.fold("")(_.pathSuffix)) } From 27d64b09713f9aaa50931d4bc81134f8360b0476 Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 18:09:44 +0200 Subject: [PATCH 35/91] transientDefault support for optional and named RPC parameters --- .../commons/rpc/NewRpcMetadataTest.scala | 6 ++-- .../commons/macros/rpc/RpcMacros.scala | 1 + .../commons/macros/rpc/RpcMappings.scala | 36 ++++++++++++++----- .../commons/macros/rpc/RpcSymbols.scala | 25 +++++++++---- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala index 422b52d4b..71babb289 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala @@ -1,7 +1,7 @@ package com.avsystem.commons package rpc -import com.avsystem.commons.serialization.whenAbsent +import com.avsystem.commons.serialization.{transientDefault, whenAbsent} import org.scalatest.FunSuite trait SomeBase { @@ -14,9 +14,9 @@ trait TestApi extends SomeBase { def doSomething(double: Double): String def doSomethingElse(double: Double): String def varargsMethod(krap: String, dubl: Double)(czy: Boolean, @renamed(42, "nejm") ints: Int*): Future[Unit] - def defaultValueMethod(int: Int = 0, @whenAbsent(difolt) bul: Boolean): Future[Unit] + def defaultValueMethod(@transientDefault int: Int = 0, @whenAbsent(difolt) bul: Boolean): Future[Unit] def flames(arg: String, otherArg: => Int, varargsy: Double*): Unit - def overload(int: Int): Unit + def overload(@transientDefault int: Int = 42): Unit @rpcName("ovgetter") def overload(lel: String): TestApi @rpcName("ovprefix") def overload: TestApi def getit(stuff: String, @suchMeta(1, "a") otherStuff: List[Int]): TestApi diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index d8f0a5e68..2316f7a4d 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -26,6 +26,7 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo val RpcNamePrefixAT: Type = getType(tq"$RpcPackage.rpcNamePrefix") val RpcNamePrefixArg: Symbol = RpcNamePrefixAT.member(TermName("prefix")) val WhenAbsentAT: Type = getType(tq"$CommonsPkg.serialization.whenAbsent[_]") + val TransientDefaultAT: Type = getType(tq"$CommonsPkg.serialization.transientDefault") val MethodNameAT: Type = getType(tq"$RpcPackage.methodName") val CompositeAT: Type = getType(tq"$RpcPackage.composite") val RpcArityAT: Type = getType(tq"$RpcPackage.RpcArity") diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index 2882f32de..85663b033 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -144,10 +144,17 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => List(realParam.localValueDecl(realParam.encoding.applyAsReal(rawParam.safePath))) } case class Optional(rawParam: RawValueParam, wrapped: Option[EncodedRealParam]) extends ParamMapping { - def rawValueTree: Tree = - rawParam.mkOptional(wrapped.map(_.rawValueTree)) + def rawValueTree: Tree = { + val noneRes: Tree = q"${rawParam.optionLike}.none" + wrapped.fold(noneRes) { erp => + val baseRes = q"${rawParam.optionLike}.some(${erp.rawValueTree})" + if (erp.realParam.transientDefault) + q"if(${erp.realParam.safeName} != ${erp.realParam.transientValueTree}) $baseRes else $noneRes" + else baseRes + } + } def realDecls: List[Tree] = wrapped.toList.map { erp => - val defaultValueTree = erp.realParam.defaultValueTree + val defaultValueTree = erp.realParam.fallbackValueTree erp.realParam.localValueDecl(erp.encoding.foldWithAsReal( rawParam.optionLike, rawParam.safePath, defaultValueTree)) } @@ -157,7 +164,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def rawValueTree: Tree = rawParam.mkMulti(reals.map(_.rawValueTree)) } case class IterableMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ListedMulti { - def realDecls: List[Tree] = if(reals.isEmpty) Nil else { + def realDecls: List[Tree] = if (reals.isEmpty) Nil else { val itName = c.freshName(TermName("it")) val itDecl = q"val $itName = ${rawParam.safePath}.iterator" itDecl :: reals.map { erp => @@ -167,7 +174,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => s"${rawParam.cannotMapClue}: by-name real parameters cannot be extracted from @multi raw parameters") } erp.localValueDecl( - q"if($itName.hasNext) ${erp.encoding.applyAsReal(q"$itName.next()")} else ${rp.defaultValueTree}") + q"if($itName.hasNext) ${erp.encoding.applyAsReal(q"$itName.next()")} else ${rp.fallbackValueTree}") } } } @@ -178,20 +185,33 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => erp.realParam.localValueDecl( q""" ${erp.encoding.andThenAsReal(rawParam.safePath)} - .applyOrElse($idx, (_: $IntCls) => ${rp.defaultValueTree}) + .applyOrElse($idx, (_: $IntCls) => ${rp.fallbackValueTree}) """) } } } case class NamedMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ParamMapping { def rawValueTree: Tree = - rawParam.mkMulti(reals.map(erp => q"(${erp.realParam.rpcName}, ${erp.rawValueTree})")) + if (reals.isEmpty) q"$RpcUtils.createEmpty(${rawParam.canBuildFrom})" else { + val builderName = c.freshName(TermName("builder")) + val addStatements = reals.map { erp => + val baseStat = q"$builderName += ((${erp.realParam.rpcName}, ${erp.rawValueTree}))" + if (erp.realParam.transientDefault) + q"if(${erp.realParam.safeName} != ${erp.realParam.transientValueTree}) $baseStat" + else baseStat + } + q""" + val $builderName = $RpcUtils.createBuilder(${rawParam.canBuildFrom}, ${reals.size}) + ..$addStatements + $builderName.result() + """ + } def realDecls: List[Tree] = reals.map { erp => erp.realParam.localValueDecl( q""" ${erp.encoding.andThenAsReal(rawParam.safePath)} - .applyOrElse(${erp.realParam.rpcName}, (_: $StringCls) => ${erp.realParam.defaultValueTree}) + .applyOrElse(${erp.realParam.rpcName}, (_: $StringCls) => ${erp.realParam.fallbackValueTree}) """) } } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index 8d8c60919..8e95f0384 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -320,14 +320,27 @@ trait RpcSymbols { this: RpcMacroCommons => transformer.transform(annotatedDefault) } - def defaultValueTree: Tree = + val hasDefaultValue: Boolean = + whenAbsent != EmptyTree || symbol.asTerm.isParamWithDefault + + val transientDefault: Boolean = + hasDefaultValue && annot(TransientDefaultAT).nonEmpty + + def fallbackValueTree: Tree = if (whenAbsent != EmptyTree) c.untypecheck(whenAbsent) - else if (symbol.asTerm.isParamWithDefault) { - val prevListParams = owner.realParams.take(index - indexInList).map(rp => q"${rp.safeName}") - val prevListParamss = List(prevListParams).filter(_.nonEmpty) - q"${owner.owner.safeName}.${TermName(s"${owner.encodedNameStr}$$default$$${index + 1}")}(...$prevListParamss)" - } + else if (symbol.asTerm.isParamWithDefault) defaultValue(false) else q"$RpcUtils.missingArg(${owner.rpcName}, $rpcName)" + + def transientValueTree: Tree = + if (symbol.asTerm.isParamWithDefault) defaultValue(true) + else c.untypecheck(whenAbsent) + + private def defaultValue(useThis: Boolean): Tree = { + val prevListParams = owner.realParams.take(index - indexInList).map(rp => q"${rp.safeName}") + val prevListParamss = List(prevListParams).filter(_.nonEmpty) + val realInst = if (useThis) q"this" else q"${owner.owner.safeName}" + q"$realInst.${TermName(s"${owner.encodedNameStr}$$default$$${index + 1}")}(...$prevListParamss)" + } } case class RawMethod(owner: RawRpcTrait, symbol: Symbol) extends RpcMethod with RawRpcSymbol with AritySymbol { From 227e6d461e36012cb91ab01b5468bccfd4d51978 Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 18:13:04 +0200 Subject: [PATCH 36/91] definalized annotations --- .../avsystem/commons/rest/annotations.scala | 20 +++++++++---------- .../commons/rpc/NewRpcMetadataTest.scala | 6 ++++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala index f1991a08f..47fa6b37c 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala @@ -66,24 +66,24 @@ sealed abstract class BodyMethodTag(method: HttpMethod) extends HttpMethodTag(me * * @param path see [[RestMethodTag.path]] */ -final class GET(val path: String = null) extends HttpMethodTag(HttpMethod.GET) { +class GET(val path: String = null) extends HttpMethodTag(HttpMethod.GET) { @rpcNamePrefix("GET_") type Implied } /** See [[BodyMethodTag]] */ -final class POST(val path: String = null) extends BodyMethodTag(HttpMethod.POST) { +class POST(val path: String = null) extends BodyMethodTag(HttpMethod.POST) { @rpcNamePrefix("POST_") type Implied } /** See [[BodyMethodTag]] */ -final class PATCH(val path: String = null) extends BodyMethodTag(HttpMethod.PATCH) { +class PATCH(val path: String = null) extends BodyMethodTag(HttpMethod.PATCH) { @rpcNamePrefix("PATCH_") type Implied } /** See [[BodyMethodTag]] */ -final class PUT(val path: String = null) extends BodyMethodTag(HttpMethod.PUT) { +class PUT(val path: String = null) extends BodyMethodTag(HttpMethod.PUT) { @rpcNamePrefix("PUT_") type Implied } /** See [[BodyMethodTag]] */ -final class DELETE(val path: String = null) extends BodyMethodTag(HttpMethod.DELETE) { +class DELETE(val path: String = null) extends BodyMethodTag(HttpMethod.DELETE) { @rpcNamePrefix("DELETE_") type Implied } @@ -100,7 +100,7 @@ final class DELETE(val path: String = null) extends BodyMethodTag(HttpMethod.DEL * * @param path see [[RestMethodTag.path]] */ -final class Prefix(val path: String = null) extends RestMethodTag +class Prefix(val path: String = null) extends RestMethodTag sealed trait RestParamTag extends RpcTag @@ -108,20 +108,20 @@ sealed trait RestParamTag extends RpcTag * REST method parameters annotated as [[Path]] will be encoded as [[PathValue]] and appended to URL path, in the * declaration order. Parameters of [[Prefix]] REST methods are interpreted as [[Path]] parameters by default. */ -final class Path(val pathSuffix: String = "") extends RestParamTag +class Path(val pathSuffix: String = "") extends RestParamTag /** * REST method parameters annotated as [[Header]] will be encoded as [[HeaderValue]] and added to HTTP headers. * Header name must be explicitly given as argument of this annotation. */ -final class Header(override val name: String) +class Header(override val name: String) extends rpcName(name) with RestParamTag /** * REST method parameters annotated as [[Query]] will be encoded as [[QueryValue]] and added to URL query * parameters. Parameters of [[GET]] REST methods are interpreted as [[Query]] parameters by default. */ -final class Query(@defaultsToName override val name: String = null) +class Query(@defaultsToName override val name: String = null) extends rpcName(name) with RestParamTag sealed trait BodyTag extends RestParamTag @@ -132,7 +132,7 @@ sealed trait BodyTag extends RestParamTag * [[POST]], [[PATCH]], [[PUT]] or [[DELETE]]. Actually, parameters of these methods are interpreted as * [[JsonBodyParam]] by default which means that this annotation rarely needs to be applied explicitly. */ -final class JsonBodyParam(@defaultsToName override val name: String = null) +class JsonBodyParam(@defaultsToName override val name: String = null) extends rpcName(name) with BodyTag /** diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala index 71babb289..1c54af912 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala @@ -4,6 +4,8 @@ package rpc import com.avsystem.commons.serialization.{transientDefault, whenAbsent} import org.scalatest.FunSuite +class td extends transientDefault + trait SomeBase { def difolt: Boolean = true @@ -14,9 +16,9 @@ trait TestApi extends SomeBase { def doSomething(double: Double): String def doSomethingElse(double: Double): String def varargsMethod(krap: String, dubl: Double)(czy: Boolean, @renamed(42, "nejm") ints: Int*): Future[Unit] - def defaultValueMethod(@transientDefault int: Int = 0, @whenAbsent(difolt) bul: Boolean): Future[Unit] + def defaultValueMethod(@td int: Int = 0, @whenAbsent(difolt) bul: Boolean): Future[Unit] def flames(arg: String, otherArg: => Int, varargsy: Double*): Unit - def overload(@transientDefault int: Int = 42): Unit + def overload(@td int: Int = 42): Unit @rpcName("ovgetter") def overload(lel: String): TestApi @rpcName("ovprefix") def overload: TestApi def getit(stuff: String, @suchMeta(1, "a") otherStuff: List[Int]): TestApi From 222819aecf6f15b9fdcecd583abee68d0c152d80 Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 18:19:44 +0200 Subject: [PATCH 37/91] minor doc update --- .../com/avsystem/commons/serialization/transientDefault.scala | 2 ++ .../scala/com/avsystem/commons/serialization/whenAbsent.scala | 2 ++ 2 files changed, 4 insertions(+) diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/serialization/transientDefault.scala b/commons-annotations/src/main/scala/com/avsystem/commons/serialization/transientDefault.scala index e1b73231a..1933fd824 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/serialization/transientDefault.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/serialization/transientDefault.scala @@ -19,5 +19,7 @@ import scala.annotation.StaticAnnotation * `GenCodec.write(someOutput, Something("lol", 10))` would yield object `{"str": "lol", "int": 10}` but * `GenCodec.write(someOutput, Something("lol", 42))` would yield object `{"str": "lol"}` because the value of `int` * is the same as the default value. + * + * NOTE: [[transientDefault]] also works for method parameters in RPC framework. */ class transientDefault extends StaticAnnotation diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/serialization/whenAbsent.scala b/commons-annotations/src/main/scala/com/avsystem/commons/serialization/whenAbsent.scala index 0da4eb2e8..88e28c5f1 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/serialization/whenAbsent.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/serialization/whenAbsent.scala @@ -22,6 +22,8 @@ import scala.annotation.StaticAnnotation * case class HasNoDefault(@whenAbsent(throw new Exception) str: String = "default") * object HasDefault extends HasGenCodec[HasDefault] * }}} + * + * NOTE: [[whenAbsent]] also works for method parameters in RPC framework. */ class whenAbsent[+T](v: => T) extends StaticAnnotation { def value: T = v From 776fe01396026b25f76dc1daa72ccaa563c75b40 Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 18:32:18 +0200 Subject: [PATCH 38/91] test fix --- .../scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala index 1c54af912..8ba09b83f 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRpcMetadataTest.scala @@ -36,9 +36,9 @@ class NewRpcMetadataTest extends FunSuite { | DO SOMETHING ELSE: true | PROCEDURES: | overload -> def overload: void - | AJDI: int@0:0:0:0: int suchMeta=false + | AJDI: [hasDefaultValue]int@0:0:0:0: int suchMeta=false | ARGS: - | int -> int@0:0:0:0: int suchMeta=false + | int -> [hasDefaultValue]int@0:0:0:0: int suchMeta=false | flames -> def flames: void | NO AJDI | ARGS: From 0f6376cc910ba3ce283cb38fd3a813fe3243bebe Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 6 Jul 2018 19:35:09 +0200 Subject: [PATCH 39/91] RestClient based on Jetty HTTP client --- .../commons/jetty/rpc/RestClient.scala | 46 +++++++++++++++++++ .../commons/jetty/rpc/RestServlet.scala | 8 ++-- .../avsystem/commons/jetty/rpc/SomeApi.scala | 3 ++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala new file mode 100644 index 000000000..bf492b813 --- /dev/null +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala @@ -0,0 +1,46 @@ +package com.avsystem.commons +package jetty.rpc + +import java.net.URLEncoder +import java.nio.charset.StandardCharsets + +import com.avsystem.commons.rest.{HeaderValue, HttpBody, QueryValue, RestRequest, RestResponse} +import org.eclipse.jetty.client.HttpClient +import org.eclipse.jetty.client.api.Result +import org.eclipse.jetty.client.util.{BufferingResponseListener, StringContentProvider} +import org.eclipse.jetty.http.{HttpHeader, MimeTypes} + +object RestClient { + def asHandleRequest(client: HttpClient, baseUrl: String): RestRequest => Future[RestResponse] = request => { + val path = request.headers.path.iterator + .map(pv => URLEncoder.encode(pv.value, "utf-8")) + .mkString(baseUrl.ensureSuffix("/"), "/", "") + + val httpReq = client.newRequest(baseUrl).method(request.method.toString) + + httpReq.path(path) + request.headers.query.foreach { + case (name, QueryValue(value)) => httpReq.param(name, value) + } + request.headers.headers.foreach { + case (name, HeaderValue(value)) => httpReq.header(name, value) + } + httpReq.content(new StringContentProvider(request.body.mimeType, request.body.content, StandardCharsets.UTF_8)) + + val promise = Promise[RestResponse] + httpReq.send(new BufferingResponseListener() { + override def onComplete(result: Result): Unit = + if (result.isSucceeded) { + val httpResp = result.getResponse + val contentType = httpResp.getHeaders.get(HttpHeader.CONTENT_TYPE) + val body = HttpBody(getContentAsString(), MimeTypes.getContentTypeWithoutCharset(contentType)) + val response = RestResponse(httpResp.getStatus, body) + promise.success(response) + } else { + promise.failure(result.getFailure) + } + }) + + promise.future + } +} diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index f96b0a665..dd22fad24 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -6,7 +6,7 @@ import java.util.regex.Pattern import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} import com.avsystem.commons.rpc.NamedParams import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} -import org.eclipse.jetty.http.{HttpHeader, HttpStatus, MimeTypes} +import org.eclipse.jetty.http.{HttpStatus, MimeTypes} class RestServlet(handleRequest: RestRequest => Future[RestResponse]) extends HttpServlet { override def service(req: HttpServletRequest, resp: HttpServletResponse): Unit = { @@ -59,11 +59,13 @@ object RestServlet { handleRequest(restRequest).catchFailures.andThenNow { case Success(restResponse) => response.setStatus(restResponse.code) - response.addHeader(HttpHeader.CONTENT_TYPE.asString(), s"${restResponse.body.mimeType};charset=utf-8") + response.setContentLength(restResponse.body.content.length) + response.setContentType(s"${restResponse.body.mimeType};charset=utf-8") response.getWriter.write(restResponse.body.content) case Failure(e) => response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) - response.addHeader(HttpHeader.CONTENT_TYPE.asString(), MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()) + response.setContentLength(e.getMessage.length) + response.setContentType(MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()) response.getWriter.write(e.getMessage) }.andThenNow { case _ => asyncContext.complete() } } diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala index 169622d98..b4e9fd1fb 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala @@ -15,6 +15,9 @@ object SomeApi extends RestApiCompanion[SomeApi] { def asHandleRequest(real: SomeApi): RestRequest => Future[RestResponse] = RawRest.asHandleRequest(real) + def fromHandleRequest(handle: RestRequest => Future[RestResponse]): SomeApi = + RawRest.fromHandleRequest[SomeApi](handle) + def format(who: String) = s"Hello, $who!" val poison: String = "poison" From 02f315c24f88d7c318530a5cc4e62e4644d11c84 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 9 Jul 2018 13:32:55 +0200 Subject: [PATCH 40/91] more REST tests and corrections --- .../avsystem/commons/SharedExtensions.scala | 8 +- .../com/avsystem/commons/rest/RawRest.scala | 26 +++-- .../com/avsystem/commons/rest/data.scala | 14 ++- .../commons/rest/AbstractRestCallTest.scala | 104 ++++++++++++++++++ .../avsystem/commons/rest/RawRestTest.scala | 16 +-- .../commons/jetty/rpc/RestClient.scala | 4 +- .../commons/jetty/rpc/RestHandler.scala | 4 +- .../commons/jetty/rpc/RestServlet.scala | 15 +-- .../commons/jetty/rpc/HttpRestCallTest.scala | 24 ++++ .../commons/jetty/rpc/RestServletTest.scala | 4 +- .../avsystem/commons/jetty/rpc/SomeApi.scala | 6 +- .../commons/jetty/rpc/UsesHttpServer.scala | 1 + 12 files changed, 184 insertions(+), 42 deletions(-) create mode 100644 commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index 94751774e..95862b1e4 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -453,8 +453,14 @@ object SharedExtensions extends SharedExtensions { def minOptBy[B: Ordering](f: A => B): Opt[A] = if (coll.isEmpty) Opt.Empty else coll.minBy(f).opt + def mkStringOr(start: String, sep: String, end: String, default: String): String = + if (coll.nonEmpty) coll.mkString(start, sep, end) else default + + def mkStringOr(sep: String, default: String): String = + if (coll.nonEmpty) coll.mkString(sep) else default + def mkStringOrEmpty(start: String, sep: String, end: String): String = - if (coll.nonEmpty) coll.mkString(start, sep, end) else "" + mkStringOr(start, sep, end, "") } class SetOps[A](private val set: BSet[A]) extends AnyVal { diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 08df91f01..a3cac6ea2 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -36,9 +36,9 @@ trait RawRest { def handleSingle(@methodName name: String, @composite headers: RestHeaders, @encoded @tagged[Body] body: HttpBody): Future[RestResponse] - def asHandleRequest(metadata: RestMetadata[_]): RestRequest => Future[RestResponse] = { + def asHandleRequest(metadata: RestMetadata[_]): RawRest.HandleRequest = { metadata.ensureUnambiguousPaths() - locally[RestRequest => Future[RestResponse]] { + locally[RawRest.HandleRequest] { case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { case List(ResolvedPath(prefixes, RpcWithPath(finalRpcName, finalPathParams), singleBody)) => val finalRawRest = prefixes.foldLeft(this) { @@ -47,9 +47,12 @@ trait RawRest { } val finalHeaders = headers.copy(path = finalPathParams) - if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) - else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) - else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) + def result: Future[RestResponse] = + if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) + else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) + else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) + + result.catchFailures case Nil => val pathStr = headers.path.iterator.map(_.value).mkString("/") @@ -65,17 +68,16 @@ trait RawRest { } object RawRest extends RawRpcCompanion[RawRest] { - def fromHandleRequest[Real: AsRealRpc : RestMetadata](handleRequest: RestRequest => Future[RestResponse]): Real = + type HandleRequest = RestRequest => Future[RestResponse] + + def fromHandleRequest[Real: AsRealRpc : RestMetadata](handleRequest: HandleRequest): Real = RawRest.asReal(new DefaultRawRest(RestMetadata[Real], RestHeaders.Empty, handleRequest)) - def asHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): RestRequest => Future[RestResponse] = + def asHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): HandleRequest = RawRest.asRaw(real).asHandleRequest(RestMetadata[Real]) - private final class DefaultRawRest( - metadata: RestMetadata[_], - prefixHeaders: RestHeaders, - handleRequest: RestRequest => Future[RestResponse] - ) extends RawRest { + private final class DefaultRawRest(metadata: RestMetadata[_], prefixHeaders: RestHeaders, handleRequest: HandleRequest) + extends RawRest { def prefix(name: String, headers: RestHeaders): RawRest = { val prefixMeta = metadata.prefixMethods.getOrElse(name, diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala index d04e88316..562894dc7 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala @@ -7,6 +7,8 @@ import com.avsystem.commons.serialization.GenCodec.ReadFailure import com.avsystem.commons.serialization.json.{JsonReader, JsonStringInput, JsonStringOutput} import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} +import scala.util.control.NoStackTrace + sealed trait RestValue extends Any { def value: String } @@ -128,17 +130,21 @@ object RestHeaders { final val Empty = RestHeaders(Nil, NamedParams.empty, NamedParams.empty) } -class HttpErrorException(code: Int, payload: String) - extends RuntimeException(s"$code: $payload") +case class HttpErrorException(code: Int, payload: String) + extends RuntimeException(s"$code: $payload") with NoStackTrace case class RestRequest(method: HttpMethod, headers: RestHeaders, body: HttpBody) case class RestResponse(code: Int, body: HttpBody) object RestResponse { implicit def defaultFutureAsRaw[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[Future[RestResponse], Future[T]] = - AsRaw.create(_.mapNow(v => RestResponse(200, bodyAsRaw.asRaw(v)))) + AsRaw.create(_.transformNow { + case Success(v) => Success(RestResponse(200, bodyAsRaw.asRaw(v))) + case Failure(HttpErrorException(code, payload)) => Success(RestResponse(code, HttpBody.plain(payload))) + case Failure(cause) => Failure(cause) + }) implicit def defaultFutureAsReal[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[Future[RestResponse], Future[T]] = AsReal.create(_.mapNow { case RestResponse(200, body) => bodyAsReal.asReal(body) - case RestResponse(code, body) => throw new HttpErrorException(code, body.content) + case RestResponse(code, body) => throw HttpErrorException(code, body.content) }) } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala new file mode 100644 index 000000000..386235cb3 --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala @@ -0,0 +1,104 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.rest.RawRest.HandleRequest +import com.avsystem.commons.serialization.HasGenCodec +import org.scalactic.source.Position +import org.scalatest.FunSuite +import org.scalatest.concurrent.ScalaFutures + +case class RestEntity(id: String, name: String) +object RestEntity extends HasGenCodec[RestEntity] + +trait RestTestApi { + @GET def trivialGet: Future[Unit] + + @GET def failingGet: Future[Unit] + + @GET("a/b") def complexGet( + @Path("p1") p1: Int, @Path p2: String, + @Header("X-H1") h1: Int, @Header("X-H2") h2: String, + q1: Int, @Query("q=2") q2: String + ): Future[RestEntity] + + @POST def multiParamPost( + @Path("p1") p1: Int, @Path p2: String, + @Header("X-H1") h1: Int, @Header("X-H2") h2: String, + @Query q1: Int, @Query("q=2") q2: String, + b1: Int, @JsonBodyParam("b\"2") b2: String + ): Future[RestEntity] + + @PUT("") def singleBodyPut( + @Body entity: RestEntity + ): Future[String] + + def prefix( + p0: String, + @Header("X-H0") h0: String, + @Query q0: String, + ): RestTestSubApi +} +object RestTestApi extends RestApiCompanion[RestTestApi] { + val Impl: RestTestApi = new RestTestApi { + def trivialGet: Future[Unit] = Future.unit + def failingGet: Future[Unit] = Future.failed(HttpErrorException(503, "nie")) + def complexGet(p1: Int, p2: String, h1: Int, h2: String, q1: Int, q2: String): Future[RestEntity] = + Future.successful(RestEntity(s"$p1-$h1-$q1", s"$p2-$h2-$q2")) + def multiParamPost(p1: Int, p2: String, h1: Int, h2: String, q1: Int, q2: String, b1: Int, b2: String): Future[RestEntity] = + Future.successful(RestEntity(s"$p1-$h1-$q1-$b1", s"$p2-$h2-$q2-$b2")) + def singleBodyPut(entity: RestEntity): Future[String] = + Future.successful(entity.toString) + def prefix(p0: String, h0: String, q0: String): RestTestSubApi = + RestTestSubApi.impl(s"$p0-$h0-$q0") + } +} + +trait RestTestSubApi { + @GET def subget(@Path p1: Int, @Header("X-H1") h1: Int, q1: Int): Future[String] +} +object RestTestSubApi extends RestApiCompanion[RestTestSubApi] { + def impl(arg: String): RestTestSubApi = new RestTestSubApi { + def subget(p1: Int, h1: Int, q1: Int): Future[String] = Future.successful(s"$arg-$p1-$h1-$q1") + } +} + +abstract class AbstractRestCallTest extends FunSuite with ScalaFutures { + final val serverHandler: RawRest.HandleRequest = + RawRest.asHandleRequest[RestTestApi](RestTestApi.Impl) + + def clientHandler: RawRest.HandleRequest + + lazy val proxy: RestTestApi = + RawRest.fromHandleRequest[RestTestApi](clientHandler) + + def testCall[T](call: RestTestApi => Future[T])(implicit pos: Position): Unit = + assert(call(proxy).wrapToTry.futureValue == call(RestTestApi.Impl).wrapToTry.futureValue) + + test("trivial GET") { + testCall(_.trivialGet) + } + + test("failing GET") { + testCall(_.failingGet) + } + + test("complex GET") { + testCall(_.complexGet(0, "a/+&", 1, "b/+&", 2, "c/+&")) + } + + test("multi-param body POST") { + testCall(_.multiParamPost(0, "a/+&", 1, "b/+&", 2, "c/+&", 3, "l\"l")) + } + + test("single body PUT") { + testCall(_.singleBodyPut(RestEntity("id", "name"))) + } + + test("prefixed GET") { + testCall(_.prefix("p0", "h0", "q0").subget(0, 1, 2)) + } +} + +class DirectRestCallTest extends AbstractRestCallTest { + def clientHandler: HandleRequest = serverHandler +} diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index bb106d9a9..a86440c21 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -21,11 +21,11 @@ trait UserApi { } object UserApi extends RestApiCompanion[UserApi] -trait RestTestApi { +trait RootApi { @Prefix("") def self: UserApi def subApi(id: Int, @Query query: String): UserApi } -object RestTestApi extends RestApiCompanion[RestTestApi] +object RootApi extends RestApiCompanion[RootApi] class RawRestTest extends FunSuite with ScalaFutures { def repr(req: RestRequest): String = { @@ -49,26 +49,26 @@ class RawRestTest extends FunSuite with ScalaFutures { s"<- ${resp.code}$contentRepr" } - class RestTestApiImpl(id: Int, query: String) extends RestTestApi with UserApi { + class RootApiImpl(id: Int, query: String) extends RootApi with UserApi { def self: UserApi = this - def subApi(newId: Int, newQuery: String): UserApi = new RestTestApiImpl(newId, query + newQuery) + def subApi(newId: Int, newQuery: String): UserApi = new RootApiImpl(newId, query + newQuery) def user(userId: String): Future[User] = Future.successful(User(userId, s"$userId-$id-$query")) def user(paf: String, awesome: Boolean, f: Int, user: User): Future[Unit] = Future.unit } var trafficLog: String = _ - val real: RestTestApi = new RestTestApiImpl(0, "") - val serverHandle: RestRequest => Future[RestResponse] = request => { + val real: RootApi = new RootApiImpl(0, "") + val serverHandle: RawRest.HandleRequest = request => { RawRest.asHandleRequest(real).apply(request).andThenNow { case Success(response) => trafficLog = s"${repr(request)}\n${repr(response)}\n" } } - val realProxy: RestTestApi = RawRest.fromHandleRequest[RestTestApi](serverHandle) + val realProxy: RootApi = RawRest.fromHandleRequest[RootApi](serverHandle) - def testRestCall[T](call: RestTestApi => Future[T], expectedTraffic: String)(implicit pos: Position): Unit = { + def testRestCall[T](call: RootApi => Future[T], expectedTraffic: String)(implicit pos: Position): Unit = { assert(call(realProxy).futureValue == call(real).futureValue) assert(trafficLog == expectedTraffic) } diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala index bf492b813..6055de1a5 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala @@ -4,14 +4,14 @@ package jetty.rpc import java.net.URLEncoder import java.nio.charset.StandardCharsets -import com.avsystem.commons.rest.{HeaderValue, HttpBody, QueryValue, RestRequest, RestResponse} +import com.avsystem.commons.rest.{HeaderValue, HttpBody, QueryValue, RawRest, RestResponse} import org.eclipse.jetty.client.HttpClient import org.eclipse.jetty.client.api.Result import org.eclipse.jetty.client.util.{BufferingResponseListener, StringContentProvider} import org.eclipse.jetty.http.{HttpHeader, MimeTypes} object RestClient { - def asHandleRequest(client: HttpClient, baseUrl: String): RestRequest => Future[RestResponse] = request => { + def asHandleRequest(client: HttpClient, baseUrl: String): RawRest.HandleRequest = request => { val path = request.headers.path.iterator .map(pv => URLEncoder.encode(pv.value, "utf-8")) .mkString(baseUrl.ensureSuffix("/"), "/", "") diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala index 5a024802b..99c9da606 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala @@ -1,12 +1,12 @@ package com.avsystem.commons package jetty.rpc -import com.avsystem.commons.rest.{RestRequest, RestResponse} +import com.avsystem.commons.rest.RawRest import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.server.Request import org.eclipse.jetty.server.handler.AbstractHandler -class RestHandler(handleRequest: RestRequest => Future[RestResponse]) extends AbstractHandler { +class RestHandler(handleRequest: RawRest.HandleRequest) extends AbstractHandler { override def handle(target: String, baseRequest: Request, request: HttpServletRequest, response: HttpServletResponse): Unit = { baseRequest.setHandled(true) RestServlet.handle(handleRequest, request, response) diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index dd22fad24..4798564f0 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -1,14 +1,15 @@ package com.avsystem.commons package jetty.rpc +import java.net.URLDecoder import java.util.regex.Pattern -import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RestHeaders, RestRequest, RestResponse} +import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RawRest, RestHeaders, RestRequest} import com.avsystem.commons.rpc.NamedParams import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.http.{HttpStatus, MimeTypes} -class RestServlet(handleRequest: RestRequest => Future[RestResponse]) extends HttpServlet { +class RestServlet(handleRequest: RawRest.HandleRequest) extends HttpServlet { override def service(req: HttpServletRequest, resp: HttpServletResponse): Unit = { RestServlet.handle(handleRequest, req, resp) } @@ -18,17 +19,17 @@ object RestServlet { val separatorPattern: Pattern = Pattern.compile("/") def handle( - handleRequest: RestRequest => Future[RestResponse], + handleRequest: RawRest.HandleRequest, request: HttpServletRequest, response: HttpServletResponse ): Unit = { val method = HttpMethod.byName(request.getMethod) + // can't use request.getPathInfo because it decodes the URL before we can split it + val encodedPath = request.getRequestURI.stripPrefix(request.getServletPath).stripPrefix("/") val path = separatorPattern - .splitAsStream(request.getPathInfo) - .asScala - .skip(1) - .map(PathValue(_)) + .splitAsStream(encodedPath).asScala + .map(v => PathValue(URLDecoder.decode(v, "utf-8"))) .to[List] val headersBuilder = NamedParams.newBuilder[HeaderValue] diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala new file mode 100644 index 000000000..3e38243a2 --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala @@ -0,0 +1,24 @@ +package com.avsystem.commons +package jetty.rpc + +import com.avsystem.commons.rest.AbstractRestCallTest +import com.avsystem.commons.rest.RawRest.HandleRequest +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.servlet.{ServletHandler, ServletHolder} + +import scala.concurrent.duration._ + +class HttpRestCallTest extends AbstractRestCallTest with UsesHttpServer with UsesHttpClient { + override def patienceConfig: PatienceConfig = PatienceConfig(1.second) + + protected def setupServer(server: Server): Unit = { + val servlet = new RestServlet(serverHandler) + val holder = new ServletHolder(servlet) + val handler = new ServletHandler + handler.addServletWithMapping(holder, "/api/*") + server.setHandler(handler) + } + + def clientHandler: HandleRequest = + RestClient.asHandleRequest(client, s"$baseUrl/api") +} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala index 2a6a3c3d7..14567779a 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala @@ -10,13 +10,11 @@ import org.eclipse.jetty.servlet.{ServletHandler, ServletHolder} import org.scalatest.FunSuite class RestServletTest extends FunSuite with UsesHttpServer with UsesHttpClient { - val baseUrl = s"http://localhost:$port/api" - override protected def setupServer(server: Server): Unit = { val servlet = new RestServlet(SomeApi.asHandleRequest(SomeApi.impl)) val holder = new ServletHolder(servlet) val handler = new ServletHandler - handler.addServletWithMapping(holder, "/api/*") + handler.addServletWithMapping(holder, "/*") server.setHandler(handler) } diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala index b4e9fd1fb..753e042ff 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala @@ -1,7 +1,7 @@ package com.avsystem.commons package jetty.rpc -import com.avsystem.commons.rest.{GET, POST, RawRest, RestApiCompanion, RestRequest, RestResponse} +import com.avsystem.commons.rest.{GET, POST, RawRest, RestApiCompanion} trait SomeApi { @GET @@ -12,10 +12,10 @@ trait SomeApi { } object SomeApi extends RestApiCompanion[SomeApi] { - def asHandleRequest(real: SomeApi): RestRequest => Future[RestResponse] = + def asHandleRequest(real: SomeApi): RawRest.HandleRequest = RawRest.asHandleRequest(real) - def fromHandleRequest(handle: RestRequest => Future[RestResponse]): SomeApi = + def fromHandleRequest(handle: RawRest.HandleRequest): SomeApi = RawRest.fromHandleRequest[SomeApi](handle) def format(who: String) = s"Hello, $who!" diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala index 979673d18..0730e880a 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala @@ -7,6 +7,7 @@ import org.scalatest.{BeforeAndAfterAll, Suite} trait UsesHttpServer extends BeforeAndAfterAll { this: Suite => val port: Int = 9090 val server: Server = new Server(port) + val baseUrl = s"http://localhost:$port" protected def setupServer(server: Server): Unit From b2ff7982e0b0a1cd4521dce62e7f6f2f50ec1123 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 9 Jul 2018 14:31:24 +0200 Subject: [PATCH 41/91] trailing comma --- .../scala/com/avsystem/commons/rest/AbstractRestCallTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala index 386235cb3..4449573d9 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala @@ -35,7 +35,7 @@ trait RestTestApi { def prefix( p0: String, @Header("X-H0") h0: String, - @Query q0: String, + @Query q0: String ): RestTestSubApi } object RestTestApi extends RestApiCompanion[RestTestApi] { From 7d6cabe175b3af1cf1c0e38043b76ed83ef81125 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 9 Jul 2018 14:37:53 +0200 Subject: [PATCH 42/91] inter project dependencies include tests --- build.sbt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index 227ad7ebe..361c00c43 100644 --- a/build.sbt +++ b/build.sbt @@ -192,7 +192,7 @@ lazy val `commons-macros` = project.settings( ) lazy val `commons-core` = project - .dependsOn(`commons-macros`, `commons-annotations`) + .dependsOn(`commons-macros`, `commons-annotations` % CompileAndTest) .settings( jvmCommonSettings, sourceDirsSettings(_ / "jvm"), @@ -206,7 +206,7 @@ lazy val `commons-core` = project lazy val `commons-core-js` = project.in(`commons-core`.base / "js") .enablePlugins(ScalaJSPlugin) .configure(p => if (forIdeaImport) p.dependsOn(`commons-core`) else p) - .dependsOn(`commons-macros`, `commons-annotations-js`) + .dependsOn(`commons-macros`, `commons-annotations-js` % CompileAndTest) .settings( jsCommonSettings, name := (name in `commons-core`).value, @@ -229,7 +229,7 @@ lazy val `commons-analyzer` = project ) lazy val `commons-jetty` = project - .dependsOn(`commons-core`) + .dependsOn(`commons-core` % CompileAndTest) .settings( jvmCommonSettings, libraryDependencies ++= Seq( @@ -286,7 +286,7 @@ lazy val `commons-benchmark-js` = project.in(`commons-benchmark`.base / "js") ) lazy val `commons-mongo` = project - .dependsOn(`commons-core`) + .dependsOn(`commons-core` % CompileAndTest) .settings( jvmCommonSettings, libraryDependencies ++= Seq( @@ -300,7 +300,7 @@ lazy val `commons-mongo` = project ) lazy val `commons-kafka` = project - .dependsOn(`commons-core`) + .dependsOn(`commons-core` % CompileAndTest) .settings( jvmCommonSettings, libraryDependencies ++= Seq( @@ -309,7 +309,7 @@ lazy val `commons-kafka` = project ) lazy val `commons-redis` = project - .dependsOn(`commons-core`) + .dependsOn(`commons-core` % CompileAndTest) .settings( jvmCommonSettings, libraryDependencies ++= Seq( @@ -321,7 +321,7 @@ lazy val `commons-redis` = project ) lazy val `commons-spring` = project - .dependsOn(`commons-core`) + .dependsOn(`commons-core` % CompileAndTest) .settings( jvmCommonSettings, libraryDependencies ++= Seq( @@ -331,7 +331,7 @@ lazy val `commons-spring` = project ) lazy val `commons-akka` = project - .dependsOn(`commons-core`) + .dependsOn(`commons-core` % CompileAndTest) .settings( jvmCommonSettings, libraryDependencies ++= Seq( From e0a7fe08b53408f5def6b570154d389a5e5ada16 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 9 Jul 2018 16:02:19 +0200 Subject: [PATCH 43/91] fixed definition of RestMetadata to have correct default param tags --- .../com/avsystem/commons/rest/RestMetadata.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index 850a8ba9e..e3a1581a6 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -4,11 +4,17 @@ package rest import com.avsystem.commons.rpc._ @methodTag[RestMethodTag, Prefix] -@paramTag[RestParamTag, JsonBodyParam] case class RestMetadata[T]( - @multi @tagged[Prefix] prefixMethods: Map[String, PrefixMetadata[_]], - @multi @tagged[HttpMethodTag] httpMethods: Map[String, HttpMethodMetadata[_]] + @paramTag[RestParamTag, Path] @multi @tagged[Prefix] + prefixMethods: Map[String, PrefixMetadata[_]], + @paramTag[RestParamTag, Query] @multi @tagged[GET] + httpGetMethods: Map[String, HttpMethodMetadata[_]], + @paramTag[RestParamTag, JsonBodyParam] @multi @tagged[BodyMethodTag] + httpBodyMethods: Map[String, HttpMethodMetadata[_]] ) { + val httpMethods: Map[String, HttpMethodMetadata[_]] = + httpGetMethods ++ httpBodyMethods + def ensureUnambiguousPaths(): Unit = { val trie = new RestMetadata.Trie trie.fillWith(this) From f178a17ba5d6b9cfdc64cc38513abfd92fea1235 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 9 Jul 2018 16:34:39 +0200 Subject: [PATCH 44/91] better empty HttpBody support --- .../avsystem/commons/rest/RestMetadata.scala | 1 - .../com/avsystem/commons/rest/data.scala | 67 +++++++++++++------ .../avsystem/commons/rest/RawRestTest.scala | 19 +++--- .../commons/jetty/rpc/RestClient.scala | 10 ++- .../commons/jetty/rpc/RestServlet.scala | 26 +++---- .../commons/jetty/rpc/HttpRestCallTest.scala | 2 +- 6 files changed, 77 insertions(+), 48 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index e3a1581a6..5ce1895cc 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -147,7 +147,6 @@ sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { } } -@paramTag[RestParamTag, Path] case class PrefixMetadata[T]( @reifyName name: String, @optional @reifyAnnot methodTag: Opt[Prefix], diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala index 562894dc7..0a66d0492 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala @@ -57,25 +57,51 @@ object RestValue { * encoding to [[JsonValue]] (e.g. types that have `GenCodec` instance) automatically have encoding to [[HttpBody]] * which uses application/json MIME type. There is also a specialized encoding provided for `Unit` which maps it * to empty HTTP body instead of JSON containing "null". - * - * @param content raw HTTP body content - * @param mimeType MIME type, i.e. HTTP `Content-Type` without charset specified */ -case class HttpBody(content: String, mimeType: String) { - def jsonValue: JsonValue = mimeType match { - case HttpBody.JsonType => JsonValue(content) - case _ => throw new ReadFailure(s"Expected application/json type, got $mimeType") +sealed trait HttpBody { + def contentOpt: Opt[String] = this match { + case HttpBody(content, _) => Opt(content) + case HttpBody.Empty => Opt.Empty + } + + def forNonEmpty(consumer: (String, String) => Unit): Unit = this match { + case HttpBody(content, mimeType) => consumer(content, mimeType) + case HttpBody.Empty => + } + + def readContent(): String = this match { + case HttpBody(content, _) => content + case HttpBody.Empty => throw new ReadFailure("Expected non-empty body") + } + + def readJson(): JsonValue = this match { + case HttpBody(content, HttpBody.JsonType) => JsonValue(content) + case HttpBody(_, mimeType) => + throw new ReadFailure(s"Expected body with application/json type, got $mimeType") + case HttpBody.Empty => + throw new ReadFailure("Expected body with application/json type, got empty body") } } object HttpBody { + object Empty extends HttpBody + final case class NonEmpty(content: String, mimeType: String) extends HttpBody + + def empty: HttpBody = Empty + + def apply(content: String, mimeType: String): HttpBody = + NonEmpty(content, mimeType) + + def unapply(body: HttpBody): Opt[(String, String)] = body match { + case Empty => Opt.Empty + case NonEmpty(content, mimeType) => Opt((content, mimeType)) + } + final val PlainType = "text/plain" final val JsonType = "application/json" def plain(value: String): HttpBody = HttpBody(value, PlainType) def json(json: JsonValue): HttpBody = HttpBody(json.value, JsonType) - final val Empty: HttpBody = HttpBody.plain("") - def createJsonBody(fields: NamedParams[JsonValue]): HttpBody = if (fields.isEmpty) HttpBody.Empty else { val sb = new JStringBuilder @@ -88,23 +114,24 @@ object HttpBody { HttpBody.json(JsonValue(sb.toString)) } - def parseJsonBody(body: HttpBody): NamedParams[JsonValue] = - if (body.content.isEmpty) NamedParams.empty else { - val oi = new JsonStringInput(new JsonReader(body.jsonValue.value)).readObject() + def parseJsonBody(body: HttpBody): NamedParams[JsonValue] = body match { + case HttpBody.Empty => NamedParams.empty + case _ => + val oi = new JsonStringInput(new JsonReader(body.readJson().value)).readObject() val builder = NamedParams.newBuilder[JsonValue] while (oi.hasNext) { val fi = oi.nextField() builder += ((fi.fieldName, JsonValue(fi.readRawJson()))) } builder.result() - } + } implicit val emptyBodyForUnit: AsRawReal[HttpBody, Unit] = AsRawReal.create(_ => HttpBody.Empty, _ => ()) implicit def httpBodyJsonAsRaw[T](implicit jsonAsRaw: AsRaw[JsonValue, T]): AsRaw[HttpBody, T] = AsRaw.create(v => HttpBody.json(jsonAsRaw.asRaw(v))) implicit def httpBodyJsonAsReal[T](implicit jsonAsReal: AsReal[JsonValue, T]): AsReal[HttpBody, T] = - AsReal.create(v => jsonAsReal.asReal(v.jsonValue)) + AsReal.create(v => jsonAsReal.asReal(v.readJson())) } /** @@ -130,21 +157,23 @@ object RestHeaders { final val Empty = RestHeaders(Nil, NamedParams.empty, NamedParams.empty) } -case class HttpErrorException(code: Int, payload: String) - extends RuntimeException(s"$code: $payload") with NoStackTrace +case class HttpErrorException(code: Int, payload: OptArg[String] = OptArg.Empty) + extends RuntimeException(s"HTTP ERROR $code${payload.fold("")(p => s": $p")}") with NoStackTrace case class RestRequest(method: HttpMethod, headers: RestHeaders, body: HttpBody) case class RestResponse(code: Int, body: HttpBody) object RestResponse { implicit def defaultFutureAsRaw[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[Future[RestResponse], Future[T]] = AsRaw.create(_.transformNow { - case Success(v) => Success(RestResponse(200, bodyAsRaw.asRaw(v))) - case Failure(HttpErrorException(code, payload)) => Success(RestResponse(code, HttpBody.plain(payload))) + case Success(v) => + Success(RestResponse(200, bodyAsRaw.asRaw(v))) + case Failure(HttpErrorException(code, payload)) => + Success(RestResponse(code, payload.fold(HttpBody.empty)(HttpBody.plain))) case Failure(cause) => Failure(cause) }) implicit def defaultFutureAsReal[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[Future[RestResponse], Future[T]] = AsReal.create(_.mapNow { case RestResponse(200, body) => bodyAsReal.asReal(body) - case RestResponse(code, body) => throw HttpErrorException(code, body.content) + case RestResponse(code, body) => throw HttpErrorException(code, body.contentOpt.toOptArg) }) } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index a86440c21..251659c6b 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -28,6 +28,11 @@ trait RootApi { object RootApi extends RestApiCompanion[RootApi] class RawRestTest extends FunSuite with ScalaFutures { + def repr(body: HttpBody, inNewLine: Boolean = true): String = body match { + case HttpBody.Empty => "" + case HttpBody(content, mimeType) => s"${if (inNewLine) "" else " "}$mimeType\n$content" + } + def repr(req: RestRequest): String = { val pathRepr = req.headers.path.map(_.value).mkString("/", "/", "") val queryRepr = req.headers.query.iterator @@ -35,19 +40,11 @@ class RawRestTest extends FunSuite with ScalaFutures { val hasHeaders = req.headers.headers.nonEmpty val headersRepr = req.headers.headers.iterator .map({ case (n, v) => s"$n: ${v.value}" }).mkStringOrEmpty("\n", "\n", "\n") - - val contentRepr = - if (req.body.content.isEmpty) "" - else s"${if (hasHeaders) "" else " "}${req.body.mimeType}\n${req.body.content}" - s"-> ${req.method} $pathRepr$queryRepr$headersRepr$contentRepr" + s"-> ${req.method} $pathRepr$queryRepr$headersRepr${repr(req.body, hasHeaders)}".trim } - def repr(resp: RestResponse): String = { - val contentRepr = - if (resp.body.content.isEmpty) "" - else s" ${resp.body.mimeType}\n${resp.body.content}" - s"<- ${resp.code}$contentRepr" - } + def repr(resp: RestResponse): String = + s"<- ${resp.code} ${repr(resp.body)}".trim class RootApiImpl(id: Int, query: String) extends RootApi with UserApi { def self: UserApi = this diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala index 6055de1a5..7db1a3008 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala @@ -25,15 +25,19 @@ object RestClient { request.headers.headers.foreach { case (name, HeaderValue(value)) => httpReq.header(name, value) } - httpReq.content(new StringContentProvider(request.body.mimeType, request.body.content, StandardCharsets.UTF_8)) + + request.body.forNonEmpty { (content, mimeType) => + httpReq.content(new StringContentProvider(s"$mimeType;charset=utf-8", content, StandardCharsets.UTF_8)) + } val promise = Promise[RestResponse] httpReq.send(new BufferingResponseListener() { override def onComplete(result: Result): Unit = if (result.isSucceeded) { val httpResp = result.getResponse - val contentType = httpResp.getHeaders.get(HttpHeader.CONTENT_TYPE) - val body = HttpBody(getContentAsString(), MimeTypes.getContentTypeWithoutCharset(contentType)) + val body = httpResp.getHeaders.get(HttpHeader.CONTENT_TYPE).opt.fold(HttpBody.empty) { contentType => + HttpBody(getContentAsString(), MimeTypes.getContentTypeWithoutCharset(contentType)) + } val response = RestResponse(httpResp.getStatus, body) promise.success(response) } else { diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala index 4798564f0..68ce70c07 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala @@ -44,25 +44,25 @@ object RestServlet { } val query = queryBuilder.result() - val bodyReader = request.getReader - val bodyBuilder = new JStringBuilder - Iterator.continually(bodyReader.read()) - .takeWhile(_ != -1) - .foreach(bodyBuilder.appendCodePoint) - val bodyString = bodyBuilder.toString - val body = - if (bodyString.isEmpty && request.getContentType == null) HttpBody.Empty - else HttpBody(bodyString, MimeTypes.getContentTypeWithoutCharset(request.getContentType)) - + val body = request.getContentType.opt.fold(HttpBody.empty) { contentType => + val bodyReader = request.getReader + val bodyBuilder = new JStringBuilder + Iterator.continually(bodyReader.read()) + .takeWhile(_ != -1) + .foreach(bodyBuilder.appendCodePoint) + HttpBody(bodyBuilder.toString, MimeTypes.getContentTypeWithoutCharset(contentType)) + } val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) val asyncContext = request.startAsync() handleRequest(restRequest).catchFailures.andThenNow { case Success(restResponse) => response.setStatus(restResponse.code) - response.setContentLength(restResponse.body.content.length) - response.setContentType(s"${restResponse.body.mimeType};charset=utf-8") - response.getWriter.write(restResponse.body.content) + restResponse.body.forNonEmpty { (content, mimeType) => + response.setContentLength(content.length) + response.setContentType(s"$mimeType;charset=utf-8") + response.getWriter.write(content) + } case Failure(e) => response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) response.setContentLength(e.getMessage.length) diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala index 3e38243a2..c0b083bce 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala @@ -9,7 +9,7 @@ import org.eclipse.jetty.servlet.{ServletHandler, ServletHolder} import scala.concurrent.duration._ class HttpRestCallTest extends AbstractRestCallTest with UsesHttpServer with UsesHttpClient { - override def patienceConfig: PatienceConfig = PatienceConfig(1.second) + override def patienceConfig: PatienceConfig = PatienceConfig(10.seconds) protected def setupServer(server: Server): Unit = { val servlet = new RestServlet(serverHandler) From 07284f451db0eceb71bcf078907867edd8a4704d Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 9 Jul 2018 16:46:55 +0200 Subject: [PATCH 45/91] cosmetic --- .../com/avsystem/commons/rest/AbstractRestCallTest.scala | 8 ++++---- .../com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala index 4449573d9..56ed90084 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala @@ -63,13 +63,13 @@ object RestTestSubApi extends RestApiCompanion[RestTestSubApi] { } abstract class AbstractRestCallTest extends FunSuite with ScalaFutures { - final val serverHandler: RawRest.HandleRequest = + final val serverHandle: RawRest.HandleRequest = RawRest.asHandleRequest[RestTestApi](RestTestApi.Impl) - def clientHandler: RawRest.HandleRequest + def clientHandle: RawRest.HandleRequest lazy val proxy: RestTestApi = - RawRest.fromHandleRequest[RestTestApi](clientHandler) + RawRest.fromHandleRequest[RestTestApi](clientHandle) def testCall[T](call: RestTestApi => Future[T])(implicit pos: Position): Unit = assert(call(proxy).wrapToTry.futureValue == call(RestTestApi.Impl).wrapToTry.futureValue) @@ -100,5 +100,5 @@ abstract class AbstractRestCallTest extends FunSuite with ScalaFutures { } class DirectRestCallTest extends AbstractRestCallTest { - def clientHandler: HandleRequest = serverHandler + def clientHandle: HandleRequest = serverHandle } diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala index c0b083bce..b6d3df007 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala @@ -12,13 +12,13 @@ class HttpRestCallTest extends AbstractRestCallTest with UsesHttpServer with Use override def patienceConfig: PatienceConfig = PatienceConfig(10.seconds) protected def setupServer(server: Server): Unit = { - val servlet = new RestServlet(serverHandler) + val servlet = new RestServlet(serverHandle) val holder = new ServletHolder(servlet) val handler = new ServletHandler handler.addServletWithMapping(holder, "/api/*") server.setHandler(handler) } - def clientHandler: HandleRequest = + def clientHandle: HandleRequest = RestClient.asHandleRequest(client, s"$baseUrl/api") } From 421c335be7e51157c374fbcb5e6207f0a8a65e69 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 11 Jul 2018 13:08:27 +0200 Subject: [PATCH 46/91] moved jetty rest implementations to rest package --- .../com/avsystem/commons/jetty/{rpc => rest}/RestClient.scala | 2 +- .../com/avsystem/commons/jetty/{rpc => rest}/RestHandler.scala | 2 +- .../com/avsystem/commons/jetty/{rpc => rest}/RestServlet.scala | 2 +- .../scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala | 1 + .../scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala | 1 + .../scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala | 1 + 6 files changed, 6 insertions(+), 3 deletions(-) rename commons-jetty/src/main/scala/com/avsystem/commons/jetty/{rpc => rest}/RestClient.scala (98%) rename commons-jetty/src/main/scala/com/avsystem/commons/jetty/{rpc => rest}/RestHandler.scala (96%) rename commons-jetty/src/main/scala/com/avsystem/commons/jetty/{rpc => rest}/RestServlet.scala (99%) diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala similarity index 98% rename from commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala rename to commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala index 7db1a3008..02de89d1f 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestClient.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala @@ -1,5 +1,5 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest import java.net.URLEncoder import java.nio.charset.StandardCharsets diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestHandler.scala similarity index 96% rename from commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala rename to commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestHandler.scala index 99c9da606..a7add0ec9 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestHandler.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestHandler.scala @@ -1,5 +1,5 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest import com.avsystem.commons.rest.RawRest import javax.servlet.http.{HttpServletRequest, HttpServletResponse} diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala similarity index 99% rename from commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala rename to commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala index 68ce70c07..6961a92b4 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rpc/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala @@ -1,5 +1,5 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest import java.net.URLDecoder import java.util.regex.Pattern diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala index b6d3df007..4e23c1828 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala @@ -1,6 +1,7 @@ package com.avsystem.commons package jetty.rpc +import com.avsystem.commons.jetty.rest.{RestClient, RestServlet} import com.avsystem.commons.rest.AbstractRestCallTest import com.avsystem.commons.rest.RawRest.HandleRequest import org.eclipse.jetty.server.Server diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala index 77b30423f..b83283ef4 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala @@ -1,6 +1,7 @@ package com.avsystem.commons package jetty.rpc +import com.avsystem.commons.jetty.rest.RestHandler import org.eclipse.jetty.server.Server object RestHandlerMain { diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala index 14567779a..1fdfed709 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala @@ -3,6 +3,7 @@ package jetty.rpc import java.nio.charset.StandardCharsets +import com.avsystem.commons.jetty.rest.RestServlet import org.eclipse.jetty.client.util.StringContentProvider import org.eclipse.jetty.http.{HttpMethod, HttpStatus} import org.eclipse.jetty.server.Server From a0e7b0d78dd8ccc49949e33a77bf4a387d17bd8d Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 11 Jul 2018 13:40:43 +0200 Subject: [PATCH 47/91] convenience apply creators in RestHandler/RestServlet/RestClient --- .../com/avsystem/commons/jetty/rest/RestClient.scala | 6 +++++- .../com/avsystem/commons/jetty/rest/RestHandler.scala | 8 +++++++- .../com/avsystem/commons/jetty/rest/RestServlet.scala | 10 +++++++--- .../avsystem/commons/jetty/rpc/RestHandlerMain.scala | 2 +- .../avsystem/commons/jetty/rpc/RestServletTest.scala | 2 +- 5 files changed, 21 insertions(+), 7 deletions(-) diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala index 02de89d1f..33508abde 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala @@ -4,13 +4,17 @@ package jetty.rest import java.net.URLEncoder import java.nio.charset.StandardCharsets -import com.avsystem.commons.rest.{HeaderValue, HttpBody, QueryValue, RawRest, RestResponse} +import com.avsystem.commons.annotation.explicitGenerics +import com.avsystem.commons.rest.{HeaderValue, HttpBody, QueryValue, RawRest, RestMetadata, RestResponse} import org.eclipse.jetty.client.HttpClient import org.eclipse.jetty.client.api.Result import org.eclipse.jetty.client.util.{BufferingResponseListener, StringContentProvider} import org.eclipse.jetty.http.{HttpHeader, MimeTypes} object RestClient { + def apply[@explicitGenerics Real: RawRest.AsRealRpc : RestMetadata](client: HttpClient, baseUrl: String): Real = + RawRest.fromHandleRequest[Real](asHandleRequest(client, baseUrl)) + def asHandleRequest(client: HttpClient, baseUrl: String): RawRest.HandleRequest = request => { val path = request.headers.path.iterator .map(pv => URLEncoder.encode(pv.value, "utf-8")) diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestHandler.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestHandler.scala index a7add0ec9..4cca73011 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestHandler.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestHandler.scala @@ -1,7 +1,8 @@ package com.avsystem.commons package jetty.rest -import com.avsystem.commons.rest.RawRest +import com.avsystem.commons.annotation.explicitGenerics +import com.avsystem.commons.rest.{RawRest, RestMetadata} import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.server.Request import org.eclipse.jetty.server.handler.AbstractHandler @@ -12,3 +13,8 @@ class RestHandler(handleRequest: RawRest.HandleRequest) extends AbstractHandler RestServlet.handle(handleRequest, request, response) } } + +object RestHandler { + def apply[@explicitGenerics Real: RawRest.AsRawRpc : RestMetadata](real: Real): RestHandler = + new RestHandler(RawRest.asHandleRequest[Real](real)) +} diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala index 6961a92b4..adb73d864 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala @@ -4,7 +4,8 @@ package jetty.rest import java.net.URLDecoder import java.util.regex.Pattern -import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RawRest, RestHeaders, RestRequest} +import com.avsystem.commons.annotation.explicitGenerics +import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RawRest, RestHeaders, RestMetadata, RestRequest} import com.avsystem.commons.rpc.NamedParams import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.http.{HttpStatus, MimeTypes} @@ -16,7 +17,10 @@ class RestServlet(handleRequest: RawRest.HandleRequest) extends HttpServlet { } object RestServlet { - val separatorPattern: Pattern = Pattern.compile("/") + def apply[@explicitGenerics Real: RawRest.AsRawRpc : RestMetadata](real: Real): RestServlet = + new RestServlet(RawRest.asHandleRequest[Real](real)) + + private val SeparatorPattern: Pattern = Pattern.compile("/") def handle( handleRequest: RawRest.HandleRequest, @@ -27,7 +31,7 @@ object RestServlet { // can't use request.getPathInfo because it decodes the URL before we can split it val encodedPath = request.getRequestURI.stripPrefix(request.getServletPath).stripPrefix("/") - val path = separatorPattern + val path = SeparatorPattern .splitAsStream(encodedPath).asScala .map(v => PathValue(URLDecoder.decode(v, "utf-8"))) .to[List] diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala index b83283ef4..813c1caa9 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala @@ -6,7 +6,7 @@ import org.eclipse.jetty.server.Server object RestHandlerMain { def main(args: Array[String]): Unit = { - val handler = new RestHandler(SomeApi.asHandleRequest(SomeApi.impl)) + val handler = RestHandler[SomeApi](SomeApi.impl) val server = new Server(9090) server.setHandler(handler) server.start() diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala index 1fdfed709..2034b190d 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala @@ -12,7 +12,7 @@ import org.scalatest.FunSuite class RestServletTest extends FunSuite with UsesHttpServer with UsesHttpClient { override protected def setupServer(server: Server): Unit = { - val servlet = new RestServlet(SomeApi.asHandleRequest(SomeApi.impl)) + val servlet = RestServlet[SomeApi](SomeApi.impl) val holder = new ServletHolder(servlet) val handler = new ServletHandler handler.addServletWithMapping(holder, "/*") From 0f36c8ce88f0c9886c2bf5717aca304999b820e6 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 11 Jul 2018 13:48:39 +0200 Subject: [PATCH 48/91] fixed bad setting of content length --- .../com/avsystem/commons/rest/AbstractRestCallTest.scala | 6 +++--- .../scala/com/avsystem/commons/jetty/rest/RestServlet.scala | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala index 56ed90084..850c30150 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala @@ -83,15 +83,15 @@ abstract class AbstractRestCallTest extends FunSuite with ScalaFutures { } test("complex GET") { - testCall(_.complexGet(0, "a/+&", 1, "b/+&", 2, "c/+&")) + testCall(_.complexGet(0, "a/+&", 1, "b/+&", 2, "ć/+&")) } test("multi-param body POST") { - testCall(_.multiParamPost(0, "a/+&", 1, "b/+&", 2, "c/+&", 3, "l\"l")) + testCall(_.multiParamPost(0, "a/+&", 1, "b/+&", 2, "ć/+&", 3, "l\"l")) } test("single body PUT") { - testCall(_.singleBodyPut(RestEntity("id", "name"))) + testCall(_.singleBodyPut(RestEntity("id", "señor"))) } test("prefixed GET") { diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala index adb73d864..96b32c471 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala @@ -63,13 +63,11 @@ object RestServlet { case Success(restResponse) => response.setStatus(restResponse.code) restResponse.body.forNonEmpty { (content, mimeType) => - response.setContentLength(content.length) response.setContentType(s"$mimeType;charset=utf-8") response.getWriter.write(content) } case Failure(e) => response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) - response.setContentLength(e.getMessage.length) response.setContentType(MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()) response.getWriter.write(e.getMessage) }.andThenNow { case _ => asyncContext.complete() } From f66791bd5353b6735cce8fb1ac863f849f2e71eb Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 11 Jul 2018 14:23:48 +0200 Subject: [PATCH 49/91] moved jetty rest tests to appropriate package --- .../commons/jetty/{rpc => rest}/HttpRestCallTest.scala | 3 +-- .../avsystem/commons/jetty/{rpc => rest}/RestHandlerMain.scala | 3 +-- .../avsystem/commons/jetty/{rpc => rest}/RestServletTest.scala | 3 +-- .../com/avsystem/commons/jetty/{rpc => rest}/SomeApi.scala | 2 +- .../avsystem/commons/jetty/{rpc => rest}/UsesHttpClient.scala | 2 +- .../avsystem/commons/jetty/{rpc => rest}/UsesHttpServer.scala | 2 +- 6 files changed, 6 insertions(+), 9 deletions(-) rename commons-jetty/src/test/scala/com/avsystem/commons/jetty/{rpc => rest}/HttpRestCallTest.scala (90%) rename commons-jetty/src/test/scala/com/avsystem/commons/jetty/{rpc => rest}/RestHandlerMain.scala (81%) rename commons-jetty/src/test/scala/com/avsystem/commons/jetty/{rpc => rest}/RestServletTest.scala (96%) rename commons-jetty/src/test/scala/com/avsystem/commons/jetty/{rpc => rest}/SomeApi.scala (97%) rename commons-jetty/src/test/scala/com/avsystem/commons/jetty/{rpc => rest}/UsesHttpClient.scala (95%) rename commons-jetty/src/test/scala/com/avsystem/commons/jetty/{rpc => rest}/UsesHttpServer.scala (96%) diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/HttpRestCallTest.scala similarity index 90% rename from commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala rename to commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/HttpRestCallTest.scala index 4e23c1828..37232a4ce 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/HttpRestCallTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/HttpRestCallTest.scala @@ -1,7 +1,6 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest -import com.avsystem.commons.jetty.rest.{RestClient, RestServlet} import com.avsystem.commons.rest.AbstractRestCallTest import com.avsystem.commons.rest.RawRest.HandleRequest import org.eclipse.jetty.server.Server diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestHandlerMain.scala similarity index 81% rename from commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala rename to commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestHandlerMain.scala index 813c1caa9..523686de7 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestHandlerMain.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestHandlerMain.scala @@ -1,7 +1,6 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest -import com.avsystem.commons.jetty.rest.RestHandler import org.eclipse.jetty.server.Server object RestHandlerMain { diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestServletTest.scala similarity index 96% rename from commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala rename to commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestServletTest.scala index 2034b190d..d51ff6e98 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/RestServletTest.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestServletTest.scala @@ -1,9 +1,8 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest import java.nio.charset.StandardCharsets -import com.avsystem.commons.jetty.rest.RestServlet import org.eclipse.jetty.client.util.StringContentProvider import org.eclipse.jetty.http.{HttpMethod, HttpStatus} import org.eclipse.jetty.server.Server diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala similarity index 97% rename from commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala rename to commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala index 753e042ff..4afd84a29 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala @@ -1,5 +1,5 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest import com.avsystem.commons.rest.{GET, POST, RawRest, RestApiCompanion} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpClient.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/UsesHttpClient.scala similarity index 95% rename from commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpClient.scala rename to commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/UsesHttpClient.scala index 621ce02a4..59c271b67 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpClient.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/UsesHttpClient.scala @@ -1,5 +1,5 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest import org.eclipse.jetty.client.HttpClient import org.scalatest.{BeforeAndAfterAll, Suite} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/UsesHttpServer.scala similarity index 96% rename from commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala rename to commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/UsesHttpServer.scala index 0730e880a..dd7fe73d7 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rpc/UsesHttpServer.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/UsesHttpServer.scala @@ -1,5 +1,5 @@ package com.avsystem.commons -package jetty.rpc +package jetty.rest import org.eclipse.jetty.server.Server import org.scalatest.{BeforeAndAfterAll, Suite} From d9a06bf4239dedc26fa70592884a5bc8a1014fae Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 11 Jul 2018 14:56:53 +0200 Subject: [PATCH 50/91] quickstart example in REST documentation --- .../commons/jetty/rest/RestHandlerMain.scala | 14 --- .../jetty/rest/examples/ClientMain.scala | 30 +++++ .../jetty/rest/examples/ServerMain.scala | 18 +++ .../commons/jetty/rest/examples/UserApi.scala | 9 ++ docs/REST.md | 108 ++++++++++++++++++ 5 files changed, 165 insertions(+), 14 deletions(-) delete mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestHandlerMain.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ClientMain.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ServerMain.scala create mode 100644 commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala create mode 100644 docs/REST.md diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestHandlerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestHandlerMain.scala deleted file mode 100644 index 523686de7..000000000 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/RestHandlerMain.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.avsystem.commons -package jetty.rest - -import org.eclipse.jetty.server.Server - -object RestHandlerMain { - def main(args: Array[String]): Unit = { - val handler = RestHandler[SomeApi](SomeApi.impl) - val server = new Server(9090) - server.setHandler(handler) - server.start() - server.join() - } -} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ClientMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ClientMain.scala new file mode 100644 index 000000000..662d3f66b --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ClientMain.scala @@ -0,0 +1,30 @@ +package com.avsystem.commons +package jetty.rest.examples + +import com.avsystem.commons.jetty.rest.RestClient +import org.eclipse.jetty.client.HttpClient + +import scala.concurrent.Await +import scala.concurrent.duration._ + +object ClientMain { + def main(args: Array[String]): Unit = { + val client = new HttpClient + client.start() + + val proxy = RestClient[UserApi](client, "http://localhost:9090/") + + // just for this example, normally not recommended... + import scala.concurrent.ExecutionContext.Implicits.global + + val result = proxy.getUsername("ID") + .andThen({ case _ => client.stop() }) + .andThen { + case Success(name) => println(s"Hello, $name!") + case Failure(cause) => cause.printStackTrace() + } + + // just wait until future is complete so that main thread doesn't finish prematurely + Await.result(result, 10.seconds) + } +} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ServerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ServerMain.scala new file mode 100644 index 000000000..f67caa416 --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ServerMain.scala @@ -0,0 +1,18 @@ +package com.avsystem.commons +package jetty.rest.examples + +import com.avsystem.commons.jetty.rest.RestHandler +import org.eclipse.jetty.server.Server + +class UserApiImpl extends UserApi { + def getUsername(userId: String) = Future.successful(s"$userId-name") +} + +object ServerMain { + def main(args: Array[String]): Unit = { + val server = new Server(9090) + server.setHandler(RestHandler[UserApi](new UserApiImpl)) + server.start() + server.join() + } +} diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala new file mode 100644 index 000000000..a978f0248 --- /dev/null +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala @@ -0,0 +1,9 @@ +package com.avsystem.commons +package jetty.rest.examples + +import com.avsystem.commons.rest._ + +trait UserApi { + @GET def getUsername(userId: String): Future[String] +} +object UserApi extends RestApiCompanion[UserApi] \ No newline at end of file diff --git a/docs/REST.md b/docs/REST.md new file mode 100644 index 000000000..2503986f7 --- /dev/null +++ b/docs/REST.md @@ -0,0 +1,108 @@ +# REST framework + +The commons libary contains an RPC based REST framework for defining REST services using Scala traits. +It may be used for implementing both client and server side and works in both JVM and JS, as long as +appropriate network layer is implemented. For JVM, Jetty-based implementations for client and server +are provided. + +The core or REST framework is platform independent and network-implementation indepenedent and therefore +has no external dependencies. Because of that, it's a part of `commons-core` module. This is enough to +be able to define REST interfaces. But if you want to expose your REST interface through an actual HTTP +server or have an actual HTTP client for that interface, you need separate implementations for that. +The `commons-jetty` module automati + +## Quickstart example + +First, make sure appropriate dependencies are configured for your project (assuming SBT): + +```scala +val commonsVersion: String = ??? // appropriate version of scala-commons here +libraryDependencies ++= Seq( + "com.avsystem.commons" %% "commons-core" % commonsVersion, + "com.avsystem.commons" %% "commons-jetty" % commonsVersion +) +``` + +Then, define some trivial REST interface: + +```scala +import com.avsystem.commons.rest._ + +trait UserApi { + @GET def getUsername(userId: String): Future[String] +} +object UserApi extends RestApiCompanion[UserApi] +``` + +Then, implement it on server side and expose it on localhost port 9090 using Jetty: + +```scala +import com.avsystem.commons.jetty.rest.RestHandler +import org.eclipse.jetty.server.Server + +class UserApiImpl extends UserApi { + def getUsername(userId: String) = Future.successful(s"$userId-name") +} + +object ServerMain { + def main(args: Array[String]): Unit = { + val server = new Server(9090) + server.setHandler(RestHandler[UserApi](new UserApiImpl)) + server.start() + server.join() + } +} +``` + +Finally, obtain a client proxy for your API using Jetty HTTP client and make a call: + +```scala +import com.avsystem.commons.jetty.rest.RestClient +import org.eclipse.jetty.client.HttpClient + +import scala.concurrent.Await +import scala.concurrent.duration._ + +object ClientMain { + def main(args: Array[String]): Unit = { + val client = new HttpClient + client.start() + + val proxy = RestClient[UserApi](client, "http://localhost:9090/") + + // just for this example, normally it's not recommended + import scala.concurrent.ExecutionContext.Implicits.global + + val result = proxy.getUsername("ID") + .andThen({ case _ => client.stop() }) + .andThen { + case Success(name) => println(s"Hello, $name!") + case Failure(cause) => cause.printStackTrace() + } + + // just wait until future is complete so that main thread doesn't finish prematurely + Await.result(result, 10.seconds) + } +} +``` + +If we look at HTTP traffic, that's what we'll see: + +Request: +``` +GET http://localhost:9090/getUsername?userId=ID HTTP/1.1 +Accept-Encoding: gzip +User-Agent: Jetty/9.3.23.v20180228 +Host: localhost:9090 +``` + +Response: +``` +HTTP/1.1 200 OK +Date: Wed, 11 Jul 2018 12:54:21 GMT +Content-Type: application/json;charset=utf-8 +Content-Length: 9 +Server: Jetty(9.3.23.v20180228) + +"ID-name" +``` From d38c50db6f52f26fda1b426e6645bbe02493ade6 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 12 Jul 2018 12:17:59 +0200 Subject: [PATCH 51/91] more meaningful Opt vs Option benchmarks --- .../avsystem/commons/core/OptBenchmarks.scala | 77 ++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/commons-benchmark/jvm/src/main/scala/com/avsystem/commons/core/OptBenchmarks.scala b/commons-benchmark/jvm/src/main/scala/com/avsystem/commons/core/OptBenchmarks.scala index 30ec09fdb..896475bb5 100644 --- a/commons-benchmark/jvm/src/main/scala/com/avsystem/commons/core/OptBenchmarks.scala +++ b/commons-benchmark/jvm/src/main/scala/com/avsystem/commons/core/OptBenchmarks.scala @@ -3,22 +3,79 @@ package core import org.openjdk.jmh.annotations._ -@Warmup(iterations = 5) -@Measurement(iterations = 20) +import scala.annotation.tailrec + +case class NullList(value: Int, tail: NullList) { + @tailrec final def tailrecSum(acc: Int = 0): Int = { + val newAcc = acc + value + if (tail == null) newAcc else tail.tailrecSum(newAcc) + } +} +object NullList { + final val Example = (0 until 1000).foldRight(NullList(1000, null)) { + (value, tail) => NullList(value, tail) + } +} + +case class OptList(value: Int, tail: Opt[OptList]) { + @tailrec final def tailrecSum(acc: Int = 0): Int = { + val newAcc = acc + value + tail match { + case Opt(t) => t.tailrecSum(newAcc) + case Opt.Empty => newAcc + } + } +} +object OptList { + final val Example = (0 until 1000).foldRight(OptList(1000, Opt.Empty)) { + (value, tail) => OptList(value, Opt(tail)) + } +} + +case class OptRefList(value: Int, tail: OptRef[OptRefList]) { + @tailrec final def tailrecSum(acc: Int = 0): Int = { + val newAcc = acc + value + tail match { + case OptRef(t) => t.tailrecSum(newAcc) + case OptRef.Empty => newAcc + } + } +} +object OptRefList { + final val Example = (0 until 1000).foldRight(OptRefList(1000, OptRef.Empty)) { + (value, tail) => OptRefList(value, OptRef(tail)) + } +} + +case class OptionList(value: Int, tail: Option[OptionList]) { + @tailrec final def tailrecSum(acc: Int = 0): Int = { + val newAcc = acc + value + tail match { + case Some(t) => t.tailrecSum(newAcc) + case None => newAcc + } + } +} +object OptionList { + final val Example = (0 until 1000).foldRight(OptionList(1000, None)) { + (value, tail) => OptionList(value, Some(tail)) + } +} + +@Warmup(iterations = 2) +@Measurement(iterations = 5) @Fork(1) @BenchmarkMode(Array(Mode.Throughput)) class OptBenchmarks { - def doSomething(str: String): Unit = () - - def takeOpt(os: Opt[String]): Opt[String] = - os.filter(_.length < 10).map(_.toDouble).map(_.toString) + @Benchmark + def testNull: Int = NullList.Example.tailrecSum() - def takeOption(os: Option[String]): Option[String] = - os.filter(_.length < 10).map(_.toDouble).map(_.toString) + @Benchmark + def testOpt: Int = OptList.Example.tailrecSum() @Benchmark - def testOption: String = takeOption(Option("1234.56")).getOrElse("") + def testOptRef: Int = OptRefList.Example.tailrecSum() @Benchmark - def testOpt: String = takeOpt(Opt("1234.56")).getOrElse("") + def testOption: Int = OptionList.Example.tailrecSum() } From cbcef625da994bc334072f685ce5f97d9826aad2 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 11:10:08 +0200 Subject: [PATCH 52/91] RPC tagging reworked to allow specifying fallback tag values --- .../avsystem/commons/rpc/rpcAnnotations.scala | 70 +++---- .../com/avsystem/commons/rest/RawRest.scala | 16 +- .../avsystem/commons/rest/RestMetadata.scala | 25 +-- .../avsystem/commons/rest/RawRestTest.scala | 23 +++ .../com/avsystem/commons/rpc/NewRawRpc.scala | 12 +- .../commons/macros/MacroCommons.scala | 75 ++++---- .../commons/macros/rpc/RpcMacros.scala | 5 +- .../commons/macros/rpc/RpcMappings.scala | 148 +++++++++------ .../commons/macros/rpc/RpcMetadatas.scala | 134 +++++++------- .../commons/macros/rpc/RpcSymbols.scala | 172 +++++++++--------- 10 files changed, 375 insertions(+), 305 deletions(-) diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala index 376974489..bc2f003b3 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala @@ -226,7 +226,7 @@ sealed trait RpcEncoding extends RawMethodAnnotation with RawParamAnnotation * } * * trait AsyncRawRpc { - * def call(rpcName: String, @multi args: Map[String,Json]): Future[Json] + * def call(@methodName rpcName: String, @multi args: Map[String,Json]): Future[Json] * } * }}} * @@ -236,7 +236,7 @@ sealed trait RpcEncoding extends RawMethodAnnotation with RawParamAnnotation * * {{{ * trait AsyncRawRpc { - * def call(rpcName: String, @multi args: Map[String,String]): Future[String] + * def call(@methodName rpcName: String, @multi args: Map[String,String]): Future[String] * } * object AsyncRawRpc extends RawRpcCompanion[AsyncRawRpc] { * private def readJson[T: GenCodec](json: String): T = @@ -265,7 +265,7 @@ final class encoded extends RpcEncoding * * {{{ * trait VerbatimRawRpc { - * @verbatim def call(rpcName: String, @multi @verbatim args: Map[String,Int]): Double + * @verbatim def call(@methodName rpcName: String, @multi @verbatim args: Map[String,Int]): Double * } * }}} */ @@ -276,44 +276,48 @@ final class verbatim extends RpcEncoding * Example: * * {{{ - * sealed trait RestMethod extends RpcTag + * sealed trait MethodType extends RpcTag * class GET extends RestMethod * class POST extends RestMethod * - * @methodTag[RestMethod,GET] - * trait RestRawRpc { - * @tagged[GET] def get(name: String, @multi args: Map[String,Json]): Future[Json] - * @tagged[POST] def post(name: String, @multi args: Map[String,Json]): Future[Json] + * @methodTag[MethodType](new GET) + * trait ExampleRawRpc { + * @tagged[GET] def get(@methodName name: String, @multi args: Map[String,Json]): Future[Json] + * @tagged[POST] def post(@methodName name: String, @multi args: Map[String,Json]): Future[Json] * } * }}} * - * In the example above, we created a hierarchy of annotations rooted at `RestMethod` which can be used + * In the example above, we created a hierarchy of annotations rooted at `MethodType` which can be used * on real methods in order to explicitly tell the RPC macro which raw methods can match it. - * We also specify `GET` as the default tag that will be assumed for real methods without any tag annotation. - * Then, using `@tagged` we specify that the raw `get` method may only match real methods annotated as `GET` + * We also specify `new GET` as the default tag that will be assumed for real methods without any tag annotation. + * Then, using [[tagged]] we specify that the raw `get` method may only match real methods annotated as `GET` * while `post` raw method may only match real methods annotated as `POST`. - * Raw methods not annotated with `@tagged` have no limitations and may still match any real methods. + * Raw methods not annotated with [[tagged]] have no limitations and may still match any real methods. * - * NOTE: The example above assumes there is a `Json` type defined with appropriate encodings - + * Also, instead of specifying `defaultTag` in `@methodTag` annotation, you may provide the `whenUntagged` + * parameter to [[tagged]] annotation. Raw method annotated as `@tagged[MethodType](whenUntagged = new GET)` + * will match real methods either explicitly tagged with `GET` or untagged. If untagged, `new GET` will be assumed + * as the tag. This is useful when you want to have multiple raw methods with different `whenUntagged` setting. + * + * NOTE: The example above assumes there is a Json` type defined with appropriate encodings - * see [[encoded]] for more details on parameter and method result encoding. * - * An example of real RPC for `RestRawRpc`: + * An example of real RPC for `ExampleRawRpc`: * * {{{ - * trait SomeRestApi { + * trait ExampleApi { * def getUser(id: UserId): Future[User] * @POST def saveUser(user: User): Future[Unit] * } - * object SomeRestApi { - * implicit val AsRawReal: AsRawReal[RestRawRpc,SomeRestApi] = AsRawReal.materializeForRpc + * object ExampleApi { + * implicit val AsRawReal: AsRawReal[ExampleRawRpc,ExampleApi] = AsRawReal.materializeForRpc * } * }}} * - * @tparam BaseTag base type for tags that can be used on real RPC methods - * @tparam DefaultTag the default tag type used for real methods not explicitly tagged - if you don't want to - * introduce any specific default tag, just use the same type as for `BaseTag` + * @tparam BaseTag base type for tags that can be used on real RPC methods + * @param defaultTag default tag value assumed for untagged methods */ -final class methodTag[BaseTag <: RpcTag, DefaultTag <: BaseTag] extends RawRpcAnnotation +final class methodTag[BaseTag <: RpcTag](val defaultTag: BaseTag = null) extends RawRpcAnnotation /** * Parameter tagging lets you have more explicit control over which raw parameters can match which real @@ -328,9 +332,10 @@ final class methodTag[BaseTag <: RpcTag, DefaultTag <: BaseTag] extends RawRpcAn * class Url extends RestParam * class Path extends RestParam * - * @paramTag[RestParam,Body] + * @paramTag[RestParam](new Body) * trait RestRawRpc { - * def get(name: String, + * def get( + * @methodName name: String, * @multi @verbatim @tagged[Path] pathParams: List[String], * @multi @verbatim @tagged[Url] urlParams: Map[String,String], * @multi @tagged[Body] bodyParams: Map[String,Json] @@ -346,16 +351,15 @@ final class methodTag[BaseTag <: RpcTag, DefaultTag <: BaseTag] extends RawRpcAn * * {{{ * trait RestRawRpc { - * @paramTag[RestParam,Body] + * @paramTag[RestParam](new Body) * def get(...) * } * }}} * - * @tparam BaseTag base type for tags that can be used on real RPC parameters - * @tparam DefaultTag the default tag type used for real parameters not explicitly tagged - if you don't want to - * introduce any specific default tag, just use the same type as for `BaseTag` + * @tparam BaseTag base type for tags that can be used on real RPC parameters + * @param defaultTag default tag value assumed for untagged real parameters */ -final class paramTag[BaseTag <: RpcTag, DefaultTag <: BaseTag] extends RawMethodAnnotation +final class paramTag[BaseTag <: RpcTag](val defaultTag: BaseTag = null) extends RawMethodAnnotation /** * Annotation applied on raw methods or raw parameters that limits matching real methods or real parameters to @@ -363,8 +367,12 @@ final class paramTag[BaseTag <: RpcTag, DefaultTag <: BaseTag] extends RawMethod * also be some common supertype of multiple tags which are accepted by this raw method or param. * * @tparam Tag annotation type required to be present on real method or parameter + * @param whenUntagged default tag value assumed for untagged methods/parameters - if specified, this effectively + * means that raw method/parameter will also match untagged real methods/parameters and assume + * the default tag value for them */ -final class tagged[Tag <: RpcTag] extends RawMethodAnnotation with RawParamAnnotation +final class tagged[Tag <: RpcTag](val whenUntagged: Tag = null) + extends RawMethodAnnotation with RawParamAnnotation /** * Raw parameters annotated as `@auxiliary` match real parameters without "consuming" them. This means that @@ -419,11 +427,11 @@ final class infer extends MetadataParamStrategy final class reifyAnnot extends MetadataParamStrategy /** - * Metadata parameter typed as `Boolean` can be annotated with `@hasAnnot[SomeAnnotation]`. Boolean value will then + * Metadata parameter typed as `Boolean` can be annotated with `@isAnnotated[SomeAnnotation]`. Boolean value will then * hold information about whether RPC trait, method or parameter for which metadata is materialized is annotated with * `SomeAnnotation` (or any subtype) or not. */ -final class hasAnnot[T <: StaticAnnotation] extends MetadataParamStrategy +final class isAnnotated[T <: StaticAnnotation] extends MetadataParamStrategy /** * This annotation may only be applied on metadata parameters of type `String` and instructs the macro engine diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index a3cac6ea2..99ad941e4 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -12,27 +12,27 @@ case class ResolvedPath(prefixes: List[RpcWithPath], finalCall: RpcWithPath, sin prefixes.iterator.map(_.rpcName).mkString("", "->", s"->${finalCall.rpcName}") } -@methodTag[RestMethodTag, Prefix] -@paramTag[RestParamTag, RestParamTag] +@methodTag[RestMethodTag] trait RawRest { @multi - @tagged[Prefix] - @paramTag[RestParamTag, Path] + @tagged[Prefix](whenUntagged = new Prefix) + @paramTag[RestParamTag](defaultTag = new Path) def prefix(@methodName name: String, @composite headers: RestHeaders): RawRest @multi @tagged[GET] - @paramTag[RestParamTag, Query] + @paramTag[RestParamTag](defaultTag = new Query) def get(@methodName name: String, @composite headers: RestHeaders): Future[RestResponse] @multi - @tagged[BodyMethodTag] - @paramTag[RestParamTag, JsonBodyParam] + @tagged[BodyMethodTag](whenUntagged = new POST) + @paramTag[RestParamTag](defaultTag = new JsonBodyParam) def handle(@methodName name: String, @composite headers: RestHeaders, @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): Future[RestResponse] @multi - @tagged[BodyMethodTag] + @tagged[BodyMethodTag](whenUntagged = new POST) + @paramTag[RestParamTag] def handleSingle(@methodName name: String, @composite headers: RestHeaders, @encoded @tagged[Body] body: HttpBody): Future[RestResponse] diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index 5ce1895cc..03216a6e0 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -3,13 +3,18 @@ package rest import com.avsystem.commons.rpc._ -@methodTag[RestMethodTag, Prefix] +@methodTag[RestMethodTag] case class RestMetadata[T]( - @paramTag[RestParamTag, Path] @multi @tagged[Prefix] + @multi @tagged[Prefix](whenUntagged = new Prefix) + @paramTag[RestParamTag](defaultTag = new Path) prefixMethods: Map[String, PrefixMetadata[_]], - @paramTag[RestParamTag, Query] @multi @tagged[GET] + + @multi @tagged[GET] + @paramTag[RestParamTag](defaultTag = new Query) httpGetMethods: Map[String, HttpMethodMetadata[_]], - @paramTag[RestParamTag, JsonBodyParam] @multi @tagged[BodyMethodTag] + + @multi @tagged[BodyMethodTag](whenUntagged = new POST) + @paramTag[RestParamTag](defaultTag = new JsonBodyParam) httpBodyMethods: Map[String, HttpMethodMetadata[_]] ) { val httpMethods: Map[String, HttpMethodMetadata[_]] = @@ -148,13 +153,11 @@ sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { } case class PrefixMetadata[T]( - @reifyName name: String, - @optional @reifyAnnot methodTag: Opt[Prefix], + @reifyAnnot methodTag: Prefix, @composite headersMetadata: RestHeadersMetadata, @checked @infer result: RestMetadata.Lazy[T] ) extends RestMethodMetadata[T] { - def methodPath: List[PathValue] = - PathValue.split(methodTag.map(_.path).getOrElse(name)) + def methodPath: List[PathValue] = PathValue.split(methodTag.path) } case class HttpMethodMetadata[T]( @@ -175,11 +178,11 @@ case class RestHeadersMetadata( case class PathParamMetadata[T]( @reifyName(rpcName = true) rpcName: String, - @optional @reifyAnnot pathAnnot: Opt[Path] + @reifyAnnot pathAnnot: Path ) extends TypedMetadata[T] { - val pathSuffix: List[PathValue] = PathValue.split(pathAnnot.fold("")(_.pathSuffix)) + val pathSuffix: List[PathValue] = PathValue.split(pathAnnot.pathSuffix) } case class HeaderParamMetadata[T]() extends TypedMetadata[T] case class QueryParamMetadata[T]() extends TypedMetadata[T] -case class BodyParamMetadata[T](@hasAnnot[Body] singleBody: Boolean) extends TypedMetadata[T] +case class BodyParamMetadata[T](@isAnnotated[Body] singleBody: Boolean) extends TypedMetadata[T] diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index 251659c6b..386fc869b 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -18,6 +18,9 @@ trait UserApi { @Query("f") foo: Int, @Body user: User ): Future[Unit] + + def autopost(bodyarg: String): Future[String] + def singleBodyAutopost(@Body body: String): Future[String] } object UserApi extends RestApiCompanion[UserApi] @@ -51,6 +54,8 @@ class RawRestTest extends FunSuite with ScalaFutures { def subApi(newId: Int, newQuery: String): UserApi = new RootApiImpl(newId, query + newQuery) def user(userId: String): Future[User] = Future.successful(User(userId, s"$userId-$id-$query")) def user(paf: String, awesome: Boolean, f: Int, user: User): Future[Unit] = Future.unit + def autopost(bodyarg: String): Future[String] = Future.successful(bodyarg.toUpperCase) + def singleBodyAutopost(@Body body: String): Future[String] = Future.successful(body.toUpperCase) } var trafficLog: String = _ @@ -89,6 +94,24 @@ class RawRestTest extends FunSuite with ScalaFutures { |""".stripMargin) } + test("auto POST") { + testRestCall(_.self.autopost("bod"), + """-> POST /autopost application/json + |{"bodyarg":"bod"} + |<- 200 application/json + |"BOD" + |""".stripMargin) + } + + test("single body auto POST") { + testRestCall(_.self.singleBodyAutopost("bod"), + """-> POST /singleBodyAutopost application/json + |"bod" + |<- 200 application/json + |"BOD" + |""".stripMargin) + } + test("simple GET after prefix call") { testRestCall(_.subApi(1, "query").user("ID"), """-> GET /subApi/1/user?query=query&userId=ID diff --git a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala index a51b28c6c..02c5f7cac 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rpc/NewRawRpc.scala @@ -22,8 +22,6 @@ case class renamed(int: Int, name: String) extends DummyParamTag { case class suchMeta(intMeta: Int, strMeta: String) extends StaticAnnotation -sealed trait untagged extends DummyParamTag - sealed trait RestMethod extends RpcTag case class POST() extends RestMethod with AnnotationAggregate { @rpcNamePrefix("POST_") type Implied @@ -37,8 +35,8 @@ case class GetterInvocation( @multi tail: List[String] ) -@methodTag[RestMethod, RestMethod] -@paramTag[DummyParamTag, untagged] +@methodTag[RestMethod] +@paramTag[DummyParamTag] trait NewRawRpc { def doSomething(arg: Double): String @optional def doSomethingElse(arg: Double): String @@ -84,8 +82,8 @@ case class DoSomethings( @optional doSomethingElse: Opt[DoSomethingSignature] ) -@methodTag[RestMethod, RestMethod] -@paramTag[DummyParamTag, untagged] +@methodTag[RestMethod] +@paramTag[DummyParamTag] case class NewRpcMetadata[T: TypeName]( @composite doSomethings: DoSomethings, @multi @verbatim procedures: Map[String, FireMetadata], @@ -191,7 +189,7 @@ case class ParameterMetadata[T: TypeName]( @reifyPosition pos: ParamPosition, @reifyFlags flags: ParamFlags, @reifyAnnot @multi metas: List[suchMeta], - @hasAnnot[suchMeta] suchMeta: Boolean + @isAnnotated[suchMeta] suchMeta: Boolean ) extends TypedMetadata[T] { def repr: String = { val flagsStr = if (flags != ParamFlags.Empty) s"[$flags]" else "" diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala index 0dec8bfe0..46d92ac01 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala @@ -69,11 +69,14 @@ trait MacroCommons { bundle => error(msg) } - class Annot(annotTree: Tree, val directSource: Symbol, val aggregate: Option[Annot]) { + class Annot(annotTree: Tree, val subject: Symbol, val directSource: Symbol, val aggregate: Option[Annot]) { def aggregationChain: List[Annot] = aggregate.fold(List.empty[Annot])(a => a :: a.aggregationChain) - def source: Symbol = aggregate.fold(directSource)(_.source) + def aggregationRootSource: Symbol = aggregate.fold(directSource)(_.aggregationRootSource) + + def errorPos: Option[Position] = + List(tree.pos, directSource.pos, aggregationRootSource.pos, subject.pos).find(_ != NoPosition) lazy val tree: Tree = annotTree match { case Apply(constr@Select(New(tpt), termNames.CONSTRUCTOR), args) => @@ -84,7 +87,7 @@ trait MacroCommons { bundle => case (arg, param) if param.asTerm.isParamWithDefault && arg.symbol != null && arg.symbol.isSynthetic && arg.symbol.name.decodedName.toString.contains("$default$") && findAnnotation(param, DefaultsToNameAT).nonEmpty => - q"${source.name.decodedName.toString}" + q"${subject.name.decodedName.toString}" case (arg, _) => arg } @@ -94,7 +97,10 @@ trait MacroCommons { bundle => def tpe: Type = annotTree.tpe - def findArg[T: ClassTag](valSym: Symbol, defaultValue: Option[T] = None): T = tree match { + def findArg[T: ClassTag](valSym: Symbol): T = + findArg[T](valSym, abort(s"(bug) no default value for ${tree.tpe} parameter ${valSym.name} provided by macro")) + + def findArg[T: ClassTag](valSym: Symbol, whenDefault: => T): T = tree match { case Apply(Select(New(tpt), termNames.CONSTRUCTOR), args) => val clsTpe = tpt.tpe val params = primaryConstructorOf(clsTpe).typeSignature.paramLists.head @@ -107,9 +113,8 @@ trait MacroCommons { bundle => case (param, arg) if param.name == subSym.name => arg match { case Literal(Constant(value: T)) => value case t if param.asTerm.isParamWithDefault && t.symbol.isSynthetic && - t.symbol.name.decodedName.toString.contains("$default$") => - defaultValue.getOrElse( - abort(s"(bug) no default value for $clsTpe parameter ${valSym.name} provided by macro")) + t.symbol.name.decodedName.toString.contains("$default$") => whenDefault + case t if classTag[T] == classTag[Tree] => t.asInstanceOf[T] case _ => abort(s"Expected literal ${classTag[T].runtimeClass} as ${valSym.name} parameter of $clsTpe annotation") } @@ -122,7 +127,8 @@ trait MacroCommons { bundle => if (tpe <:< AnnotationAggregateType) { val argsInliner = new AnnotationArgInliner(tree) val impliedMember = tpe.member(TypeName("Implied")) - impliedMember.annotations.map(a => new Annot(argsInliner.transform(a.tree), impliedMember, Some(this))) + impliedMember.annotations.map(a => + new Annot(argsInliner.transform(a.tree), subject, impliedMember, Some(this))) } else Nil } @@ -154,35 +160,38 @@ trait MacroCommons { bundle => private def maybeWithSuperSymbols(s: Symbol, withSupers: Boolean): Iterator[Symbol] = if (withSupers) withSuperSymbols(s) else Iterator(s) - def allAnnotations(s: Symbol, tpeFilter: Type = typeOf[Any], withInherited: Boolean = true): List[Annot] = - maybeWithSuperSymbols(s, withInherited) - .flatMap(ss => ss.annotations.map(a => new Annot(a.tree, ss, None))) + def allAnnotations(s: Symbol, tpeFilter: Type, withInherited: Boolean = true, fallback: List[Tree] = Nil): List[Annot] = { + val nonFallback = maybeWithSuperSymbols(s, withInherited) + .flatMap(ss => ss.annotations.map(a => new Annot(a.tree, s, ss, None))) + + (nonFallback ++ fallback.iterator.map(t => new Annot(t, s, s, None))) .flatMap(_.withAllAggregated).filter(_.tpe <:< tpeFilter).toList + } - def findAnnotation(s: Symbol, tpe: Type, withInherited: Boolean = true): Option[Annot] = - maybeWithSuperSymbols(s, withInherited).map { ss => - def find(annots: List[Annot]): Option[Annot] = annots match { - case head :: tail => - val fromHead = Some(head).filter(_.tpe <:< tpe).orElse(find(head.aggregated)) - for { - found <- fromHead - ignored <- tail.filter(_.tpe <:< tpe) - } { - val errorPos = List(ignored.tree.pos, ignored.directSource.pos, ss.pos, s.pos) - .find(_ != NoPosition).getOrElse(c.enclosingPosition) - val aggInfo = - if (ignored.aggregate.isEmpty) "" - else ignored.aggregationChain.mkString(" (aggregated by ", " aggregated by", ")") - c.error(errorPos, s"Annotation $ignored$aggInfo is ignored because it's overridden by $found") - } - fromHead orElse find(tail) - case Nil => None - } - find(ss.annotations.map(a => new Annot(a.tree, ss, None))) - }.collectFirst { - case Some(annot) => annot + def findAnnotation(s: Symbol, tpe: Type, withInherited: Boolean = true, fallback: List[Tree] = Nil): Option[Annot] = { + def find(annots: List[Annot]): Option[Annot] = annots match { + case head :: tail => + val fromHead = Some(head).filter(_.tpe <:< tpe).orElse(find(head.aggregated)) + for { + found <- fromHead + ignored <- tail.filter(_.tpe <:< tpe) + } { + val errorPos = ignored.errorPos.getOrElse(c.enclosingPosition) + val aggInfo = + if (ignored.aggregate.isEmpty) "" + else ignored.aggregationChain.mkString(" (aggregated by ", " aggregated by", ")") + c.error(errorPos, s"Annotation $ignored$aggInfo is ignored because it's overridden by $found") + } + fromHead orElse find(tail) + case Nil => None } + maybeWithSuperSymbols(s, withInherited) + .map(ss => find(ss.annotations.map(a => new Annot(a.tree, s, ss, None)))) + .collectFirst { case Some(annot) => annot } + .orElse(find(fallback.map(t => new Annot(t, s, s, None)))) + } + private var companionReplacement = Option.empty[(Symbol, TermName)] def mkMacroGenerated(tpe: Type, tree: => Tree): Tree = { diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index 2316f7a4d..d2004fcdc 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -36,9 +36,10 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo val RpcEncodingAT: Type = getType(tq"$RpcPackage.RpcEncoding") val VerbatimAT: Type = getType(tq"$RpcPackage.verbatim") val AuxiliaryAT: Type = getType(tq"$RpcPackage.auxiliary") - val MethodTagAT: Type = getType(tq"$RpcPackage.methodTag[_,_]") - val ParamTagAT: Type = getType(tq"$RpcPackage.paramTag[_,_]") + val MethodTagAT: Type = getType(tq"$RpcPackage.methodTag[_]") + val ParamTagAT: Type = getType(tq"$RpcPackage.paramTag[_]") val TaggedAT: Type = getType(tq"$RpcPackage.tagged[_]") + val WhenUntaggedArg: Symbol = TaggedAT.member(TermName("whenUntagged")) val RpcTagAT: Type = getType(tq"$RpcPackage.RpcTag") val RpcImplicitsSym: Symbol = getType(tq"$RpcPackage.RpcImplicitsProvider").member(TermName("implicits")) val TypedMetadataType: Type = getType(tq"$RpcPackage.TypedMetadata[_]") diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index 85663b033..7529fdc8c 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -10,7 +10,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def collectMethodMappings[R <: RawRpcSymbol with AritySymbol, M]( rawSymbols: List[R], rawShortDesc: String, realMethods: List[RealMethod])( - createMapping: (R, RealMethod) => Res[M]): List[M] = { + createMapping: (R, RealMethod, FallbackTag) => Res[M]): List[M] = { val failedReals = new ListBuffer[String] def addFailure(realMethod: RealMethod, message: String): Unit = { @@ -21,9 +21,9 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => val result = realMethods.flatMap { realMethod => val methodMappings = rawSymbols.map { rawSymbol => val res = for { - _ <- rawSymbol.matchName(realMethod) - _ <- rawSymbol.matchTag(realMethod) - methodMapping <- createMapping(rawSymbol, realMethod) + fallbackTag <- rawSymbol.matchTag(realMethod) + _ <- rawSymbol.matchName(realMethod, fallbackTag) + methodMapping <- createMapping(rawSymbol, realMethod, fallbackTag) } yield (rawSymbol, methodMapping) res.mapFailure(msg => s"${rawSymbol.shortDescription} ${rawSymbol.nameStr} did not match: $msg") } @@ -68,79 +68,104 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def remaining: Seq[RealParam] = realParams.asScala - def extractSingle[B](raw: RealParamTarget, matcher: RealParam => Res[B]): Res[B] = { + def extractSingle[B](raw: RealParamTarget, matcher: (RealParam, FallbackTag) => Res[B]): Res[B] = { val it = realParams.listIterator() def loop(): Res[B] = if (it.hasNext) { val real = it.next() - if (raw.matchesTag(real)) { - if (!raw.auxiliary) { - it.remove() - } - matcher(real) - } else loop() + raw.matchTag(real) match { + case Ok(fallbackTag) => + if (!raw.auxiliary) { + it.remove() + } + matcher(real, fallbackTag) + case Fail(_) => loop() + } } else Fail(s"${raw.shortDescription} ${raw.pathStr} was not matched by real parameter") loop() } - def extractOptional[B](raw: RealParamTarget, matcher: RealParam => Res[B]): Option[B] = { + def extractOptional[B](raw: RealParamTarget, matcher: (RealParam, FallbackTag) => Res[B]): Option[B] = { val it = realParams.listIterator() def loop(): Option[B] = if (it.hasNext) { val real = it.next() - if (raw.matchesTag(real)) { - val res = matcher(real).toOption - if (!raw.auxiliary) { - res.foreach(_ => it.remove()) - } - res - } else loop() + raw.matchTag(real) match { + case Ok(fallbackTag) => + val res = matcher(real, fallbackTag).toOption + if (!raw.auxiliary) { + res.foreach(_ => it.remove()) + } + res + case Fail(_) => loop() + } } else None loop() } - def extractMulti[B](raw: RealParamTarget, matcher: (RealParam, Int) => Res[B], named: Boolean): Res[List[B]] = { + def extractMulti[B](raw: RealParamTarget, matcher: (RealParam, FallbackTag, Int) => Res[B], named: Boolean): Res[List[B]] = { val it = realParams.listIterator() def loop(result: ListBuffer[B]): Res[List[B]] = if (it.hasNext) { val real = it.next() - if (raw.matchesTag(real)) { - if (!raw.auxiliary) { - it.remove() - } - matcher(real, result.size) match { - case Ok(b) => - result += b - loop(result) - case fail: Fail => - fail - } - } else loop(result) + raw.matchTag(real) match { + case Ok(fallbackTag) => + if (!raw.auxiliary) { + it.remove() + } + matcher(real, fallbackTag, result.size) match { + case Ok(b) => + result += b + loop(result) + case fail: Fail => + fail + } + case Fail(_) => loop(result) + } } else Ok(result.result()) loop(new ListBuffer[B]) } } - case class EncodedRealParam(realParam: RealParam, encoding: RpcEncoding) { + case class EncodedRealParam(realParam: RealParam, encoding: RpcEncoding, fallbackTag: FallbackTag) { + val rpcName: String = realParam.rpcName(fallbackTag) + def safeName: TermName = realParam.safeName def rawValueTree: Tree = encoding.applyAsRaw(realParam.safeName) def localValueDecl(body: Tree): Tree = realParam.localValueDecl(body) + + val whenAbsent: Tree = realParam.whenAbsent(fallbackTag) + + val hasDefaultValue: Boolean = + whenAbsent != EmptyTree || realParam.symbol.asTerm.isParamWithDefault + + val transientDefault: Boolean = + hasDefaultValue && realParam.annot(TransientDefaultAT, fallbackTag).nonEmpty + + def fallbackValueTree(ownerRpcName: String): Tree = + if (whenAbsent != EmptyTree) c.untypecheck(whenAbsent) + else if (realParam.symbol.asTerm.isParamWithDefault) realParam.defaultValue(false) + else q"$RpcUtils.missingArg($ownerRpcName, $rpcName)" + + def transientValueTree: Tree = + if (realParam.symbol.asTerm.isParamWithDefault) realParam.defaultValue(true) + else c.untypecheck(whenAbsent) } object EncodedRealParam { - def create(rawParam: RawValueParam, realParam: RealParam): Res[EncodedRealParam] = - RpcEncoding.forParam(rawParam, realParam).map(EncodedRealParam(realParam, _)) + def create(rawParam: RawValueParam, realParam: RealParam, fallbackTag: FallbackTag): Res[EncodedRealParam] = + RpcEncoding.forParam(rawParam, realParam).map(EncodedRealParam(realParam, _, fallbackTag)) } sealed trait ParamMapping { def rawParam: RawValueParam def rawValueTree: Tree - def realDecls: List[Tree] + def realDecls(ownerRpcName: String): List[Tree] } object ParamMapping { case class Single(rawParam: RawValueParam, realParam: EncodedRealParam) extends ParamMapping { def rawValueTree: Tree = realParam.rawValueTree - def realDecls: List[Tree] = + def realDecls(ownerRpcName: String): List[Tree] = List(realParam.localValueDecl(realParam.encoding.applyAsReal(rawParam.safePath))) } case class Optional(rawParam: RawValueParam, wrapped: Option[EncodedRealParam]) extends ParamMapping { @@ -148,13 +173,13 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => val noneRes: Tree = q"${rawParam.optionLike}.none" wrapped.fold(noneRes) { erp => val baseRes = q"${rawParam.optionLike}.some(${erp.rawValueTree})" - if (erp.realParam.transientDefault) - q"if(${erp.realParam.safeName} != ${erp.realParam.transientValueTree}) $baseRes else $noneRes" + if (erp.transientDefault) + q"if(${erp.realParam.safeName} != ${erp.transientValueTree}) $baseRes else $noneRes" else baseRes } } - def realDecls: List[Tree] = wrapped.toList.map { erp => - val defaultValueTree = erp.realParam.fallbackValueTree + def realDecls(ownerRpcName: String): List[Tree] = wrapped.toList.map { erp => + val defaultValueTree = erp.fallbackValueTree(ownerRpcName) erp.realParam.localValueDecl(erp.encoding.foldWithAsReal( rawParam.optionLike, rawParam.safePath, defaultValueTree)) } @@ -164,7 +189,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def rawValueTree: Tree = rawParam.mkMulti(reals.map(_.rawValueTree)) } case class IterableMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ListedMulti { - def realDecls: List[Tree] = if (reals.isEmpty) Nil else { + def realDecls(ownerRpcName: String): List[Tree] = if (reals.isEmpty) Nil else { val itName = c.freshName(TermName("it")) val itDecl = q"val $itName = ${rawParam.safePath}.iterator" itDecl :: reals.map { erp => @@ -174,18 +199,17 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => s"${rawParam.cannotMapClue}: by-name real parameters cannot be extracted from @multi raw parameters") } erp.localValueDecl( - q"if($itName.hasNext) ${erp.encoding.applyAsReal(q"$itName.next()")} else ${rp.fallbackValueTree}") + q"if($itName.hasNext) ${erp.encoding.applyAsReal(q"$itName.next()")} else ${erp.fallbackValueTree(ownerRpcName)}") } } } case class IndexedMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ListedMulti { - def realDecls: List[Tree] = { + def realDecls(ownerRpcName: String): List[Tree] = { reals.zipWithIndex.map { case (erp, idx) => - val rp = erp.realParam erp.realParam.localValueDecl( q""" ${erp.encoding.andThenAsReal(rawParam.safePath)} - .applyOrElse($idx, (_: $IntCls) => ${rp.fallbackValueTree}) + .applyOrElse($idx, (_: $IntCls) => ${erp.fallbackValueTree(ownerRpcName)}) """) } } @@ -195,9 +219,9 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => if (reals.isEmpty) q"$RpcUtils.createEmpty(${rawParam.canBuildFrom})" else { val builderName = c.freshName(TermName("builder")) val addStatements = reals.map { erp => - val baseStat = q"$builderName += ((${erp.realParam.rpcName}, ${erp.rawValueTree}))" - if (erp.realParam.transientDefault) - q"if(${erp.realParam.safeName} != ${erp.realParam.transientValueTree}) $baseStat" + val baseStat = q"$builderName += ((${erp.rpcName}, ${erp.rawValueTree}))" + if (erp.transientDefault) + q"if(${erp.realParam.safeName} != ${erp.transientValueTree}) $baseStat" else baseStat } q""" @@ -206,12 +230,12 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => $builderName.result() """ } - def realDecls: List[Tree] = + def realDecls(ownerRpcName: String): List[Tree] = reals.map { erp => erp.realParam.localValueDecl( q""" ${erp.encoding.andThenAsReal(rawParam.safePath)} - .applyOrElse(${erp.realParam.rpcName}, (_: $StringCls) => ${erp.realParam.fallbackValueTree}) + .applyOrElse(${erp.rpcName}, (_: $StringCls) => ${erp.fallbackValueTree(ownerRpcName)}) """) } } @@ -267,14 +291,16 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } } - case class MethodMapping(realMethod: RealMethod, rawMethod: RawMethod, + case class MethodMapping(realMethod: RealMethod, rawMethod: RawMethod, fallbackTag: FallbackTag, paramMappingList: List[ParamMapping], resultEncoding: RpcEncoding) { + val rpcName: String = realMethod.rpcName(fallbackTag) + val paramMappings: Map[RawValueParam, ParamMapping] = paramMappingList.iterator.map(m => (m.rawParam, m)).toMap private def rawValueTree(rawParam: RawParam): Tree = rawParam match { - case _: MethodNameParam => q"${realMethod.rpcName}" + case _: MethodNameParam => q"$rpcName" case rvp: RawValueParam => paramMappings(rvp).rawValueTree case crp: CompositeRawParam => q""" @@ -293,7 +319,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def rawCaseImpl: Tree = q""" - ..${paramMappings.values.filterNot(_.rawParam.auxiliary).flatMap(_.realDecls)} + ..${paramMappings.values.filterNot(_.rawParam.auxiliary).flatMap(_.realDecls(rpcName))} ${resultEncoding.applyAsRaw(q"${realMethod.owner.safeName}.${realMethod.name}(...${realMethod.argLists})")} """ } @@ -310,12 +336,14 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => registerCompanionImplicits(raw.tpe) private def extractMapping(rawParam: RawValueParam, parser: ParamsParser): Res[ParamMapping] = { - def createErp(realParam: RealParam, index: Int): Res[EncodedRealParam] = EncodedRealParam.create(rawParam, realParam) + def createErp(realParam: RealParam, fallbackTag: FallbackTag, index: Int): Res[EncodedRealParam] = + EncodedRealParam.create(rawParam, realParam, fallbackTag) + rawParam.arity match { case _: RpcParamArity.Single => - parser.extractSingle(rawParam, createErp(_, 0)).map(ParamMapping.Single(rawParam, _)) + parser.extractSingle(rawParam, createErp(_, _, 0)).map(ParamMapping.Single(rawParam, _)) case _: RpcParamArity.Optional => - Ok(ParamMapping.Optional(rawParam, parser.extractOptional(rawParam, createErp(_, 0)))) + Ok(ParamMapping.Optional(rawParam, parser.extractOptional(rawParam, createErp(_, _, 0)))) case RpcParamArity.Multi(_, true) => parser.extractMulti(rawParam, createErp, named = true).map(ParamMapping.NamedMulti(rawParam, _)) case _: RpcParamArity.Multi if rawParam.actualType <:< BIndexedSeqTpe => @@ -325,7 +353,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } } - private def mappingRes(rawMethod: RawMethod, realMethod: RealMethod): Res[MethodMapping] = { + private def mappingRes(rawMethod: RawMethod, realMethod: RealMethod, fallbackTag: FallbackTag): Res[MethodMapping] = { def resultEncoding: Res[RpcEncoding] = if (rawMethod.verbatimResult) { if (rawMethod.resultType =:= realMethod.resultType) @@ -344,7 +372,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => for { resultConv <- resultEncoding paramMappings <- collectParamMappings(rawMethod.allValueParams, "raw parameter", realMethod)(extractMapping) - } yield MethodMapping(realMethod, rawMethod, paramMappings, resultConv) + } yield MethodMapping(realMethod, rawMethod, fallbackTag, paramMappings, resultConv) } lazy val methodMappings: List[MethodMapping] = @@ -360,7 +388,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def asRawImpl: Tree = { val caseImpls = raw.rawMethods.iterator.map(rm => (rm, new mutable.LinkedHashMap[String, Tree])).toMap methodMappings.foreach { mapping => - caseImpls(mapping.rawMethod).put(mapping.realMethod.rpcName, mapping.rawCaseImpl) + caseImpls(mapping.rawMethod).put(mapping.rpcName, mapping.rawCaseImpl) } val rawMethodImpls = raw.rawMethods.map(m => m.rawImpl(caseImpls(m).toList)) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index b77ec0786..fb4db1256 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -66,8 +66,8 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def allowNamedMulti: Boolean = true def allowListedMulti: Boolean = false - def baseTag: Type = owner.baseMethodTag - def defaultTag: Type = owner.defaultMethodTag + def baseTagTpe: Type = owner.baseMethodTag + def fallbackTag: FallbackTag = owner.fallbackMethodTag val verbatimResult: Boolean = annot(RpcEncodingAT).map(_.tpe <:< VerbatimAT).getOrElse(arity.verbatimByDefault) @@ -76,17 +76,16 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem(s"method metadata type must be a subtype TypedMetadata[_]") } - val List(baseParamTag, defaultParamTag) = + val (baseParamTag, fallbackParamTag) = annot(ParamTagAT).orElse(findAnnotation(arity.collectedType.typeSymbol, ParamTagAT)) - .map(_.tpe.baseType(ParamTagAT.typeSymbol).typeArgs) - .getOrElse(List(owner.baseParamTag, owner.defaultParamTag)) + .map(tagSpec).getOrElse(owner.baseParamTag, owner.fallbackParamTag) - def mappingFor(realMethod: RealMethod): Res[MethodMetadataMapping] = for { + def mappingFor(realMethod: RealMethod, fallbackTag: FallbackTag): Res[MethodMetadataMapping] = for { mdType <- actualMetadataType(arity.collectedType, realMethod, verbatimResult) constructor = new MethodMetadataConstructor(mdType, Left(this)) paramMappings <- constructor.paramMappings(realMethod) - tree <- constructor.tryMaterializeFor(realMethod, paramMappings) - } yield MethodMetadataMapping(realMethod, this, tree) + tree <- constructor.tryMaterializeFor(realMethod, fallbackTag, paramMappings) + } yield MethodMetadataMapping(realMethod, fallbackTag, this, tree) } class ParamMetadataParam(owner: MethodMetadataConstructor, symbol: Symbol) @@ -94,8 +93,8 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def pathStr: String = owner.atParam.fold(_ => nameStr, cp => s"${cp.pathStr}.$nameStr") - def baseTag: Type = owner.containingMethodParam.baseParamTag - def defaultTag: Type = owner.containingMethodParam.defaultParamTag + def baseTagTpe: Type = owner.containingMethodParam.baseParamTag + def fallbackTag: FallbackTag = owner.containingMethodParam.fallbackParamTag def cannotMapClue = s"cannot map it to $shortDescription $nameStr of ${owner.ownerType}" @@ -103,21 +102,22 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem(s"type ${arity.collectedType} is not a subtype of TypedMetadata[_]") } - private def metadataTree(realParam: RealParam, indexInRaw: Int): Res[Tree] = { + private def metadataTree(realParam: RealParam, fallbackTag: FallbackTag, indexInRaw: Int): Res[Tree] = { val result = for { mdType <- actualMetadataType(arity.collectedType, realParam, verbatim) - tree <- new ParamMetadataConstructor(mdType, Left(this), indexInRaw).tryMaterializeFor(realParam) + tree <- new ParamMetadataConstructor(mdType, Left(this), indexInRaw).tryMaterializeFor(realParam, fallbackTag) } yield tree result.mapFailure(msg => s"${realParam.problemStr}: $cannotMapClue: $msg") } def metadataFor(parser: ParamsParser): Res[Tree] = arity match { case _: RpcParamArity.Single => - parser.extractSingle(this, metadataTree(_, 0)) + parser.extractSingle(this, metadataTree(_, _, 0)) case _: RpcParamArity.Optional => - Ok(mkOptional(parser.extractOptional(this, metadataTree(_, 0)))) + Ok(mkOptional(parser.extractOptional(this, metadataTree(_, _, 0)))) case RpcParamArity.Multi(_, true) => - parser.extractMulti(this, (rp, i) => metadataTree(rp, i).map(t => q"(${rp.rpcName}, $t)"), named = true).map(mkMulti(_)) + parser.extractMulti(this, (rp, ft, i) => metadataTree(rp, ft, i) + .map(t => q"(${rp.rpcName(ft)}, $t)"), named = true).map(mkMulti(_)) case _: RpcParamArity.Multi => parser.extractMulti(this, metadataTree, named = false).map(mkMulti(_)) } @@ -129,8 +129,8 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => sealed abstract class MetadataConstructor[Real <: RealRpcSymbol](val symbol: Symbol) extends RpcMethod { def ownerType: Type - override def annot(tpe: Type): Option[Annot] = - super.annot(tpe) orElse { + def annot(tpe: Type): Option[Annot] = + findAnnotation(symbol, tpe) orElse { // fallback to annotations on the class itself if (symbol.asMethod.isConstructor) findAnnotation(ownerType.typeSymbol, tpe) @@ -144,7 +144,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case t if t <:< InferAT => new ImplicitParam(this, paramSym) case t if t <:< ReifyAnnotAT => new ReifiedAnnotParam(this, paramSym) case t if t <:< ReifyNameAT => - val useRpcName = annot.findArg[Boolean](ReifyNameAT.member(TermName("rpcName")), Some(false)) + val useRpcName = annot.findArg[Boolean](ReifyNameAT.member(TermName("rpcName")), false) new ReifiedNameParam(this, paramSym, useRpcName) case t if t <:< HasAnnotAT => new HasAnnotParam(this, paramSym, t.typeArgs.head) @@ -169,23 +169,25 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => """ } - case class MethodMetadataMapping(realMethod: RealMethod, mdParam: MethodMetadataParam, tree: Tree) + case class MethodMetadataMapping( + realMethod: RealMethod, fallbackTag: FallbackTag, mdParam: MethodMetadataParam, tree: Tree) { + + val rpcName: String = realMethod.rpcName(fallbackTag) + } class RpcMetadataConstructor(val ownerType: Type, val atParam: Option[RpcCompositeParam]) extends MetadataConstructor[RealRpcTrait](primaryConstructor(ownerType, atParam)) with RawRpcSymbol { - def baseTag: Type = typeOf[Nothing] - def defaultTag: Type = typeOf[Nothing] + def baseTagTpe: Type = NothingTpe + def fallbackTag: FallbackTag = FallbackTag(None) - val List(baseMethodTag, defaultMethodTag) = - annot(MethodTagAT) - .map(_.tpe.baseType(MethodTagAT.typeSymbol).typeArgs) - .getOrElse(List(NothingTpe, NothingTpe)) + override def annot(tpe: Type): Option[Annot] = + super[MetadataConstructor].annot(tpe) - val List(baseParamTag, defaultParamTag) = - annot(ParamTagAT) - .map(_.tpe.baseType(ParamTagAT.typeSymbol).typeArgs) - .getOrElse(List(NothingTpe, NothingTpe)) + val (baseMethodTag, fallbackMethodTag) = + annot(MethodTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag(None))) + val (baseParamTag, fallbackParamTag) = + annot(ParamTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag(None))) lazy val methodMdParams: List[MethodMetadataParam] = paramLists.flatten.flatMap { case mmp: MethodMetadataParam => List(mmp) @@ -200,14 +202,14 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => new RpcCompositeParam(this, paramSym) def methodMappings(rpc: RealRpcTrait): Map[MethodMetadataParam, List[MethodMetadataMapping]] = - collectMethodMappings(methodMdParams, "metadata parameters", rpc.realMethods)(_.mappingFor(_)).groupBy(_.mdParam) + collectMethodMappings(methodMdParams, "metadata parameters", rpc.realMethods)(_.mappingFor(_, _)).groupBy(_.mdParam) def materializeFor(rpc: RealRpcTrait, methodMappings: Map[MethodMetadataParam, List[MethodMetadataMapping]]): Tree = { val argDecls = paramLists.flatten.map { case rcp: RpcCompositeParam => rcp.localValueDecl(rcp.constructor.materializeFor(rpc, methodMappings)) case dmp: DirectMetadataParam[RealRpcTrait] => - dmp.localValueDecl(dmp.materializeFor(rpc)) + dmp.localValueDecl(dmp.materializeFor(rpc, FallbackTag(None))) case mmp: MethodMetadataParam => mmp.localValueDecl { val mappings = methodMappings.getOrElse(mmp, Nil) mmp.arity match { @@ -222,7 +224,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case _ => abort(s"multiple real methods match ${mmp.description}") } case RpcParamArity.Multi(_, _) => - mmp.mkMulti(mappings.map(m => q"(${m.realMethod.rpcName}, ${m.tree})")) + mmp.mkMulti(mappings.map(m => q"(${m.rpcName}, ${m.tree})")) } } } @@ -255,12 +257,14 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => collectParamMappings(paramMdParams, "metadata parameter", realMethod)( (param, parser) => param.metadataFor(parser).map(t => (param, t))).map(_.toMap) - def tryMaterializeFor(realMethod: RealMethod, paramMappings: Map[ParamMetadataParam, Tree]): Res[Tree] = + def tryMaterializeFor(realMethod: RealMethod, fallbackTag: FallbackTag, + paramMappings: Map[ParamMetadataParam, Tree]): Res[Tree] = + Res.traverse(paramLists.flatten) { case cmp: MethodCompositeParam => - cmp.constructor.tryMaterializeFor(realMethod, paramMappings).map(cmp.localValueDecl) + cmp.constructor.tryMaterializeFor(realMethod, fallbackTag, paramMappings).map(cmp.localValueDecl) case dmp: DirectMetadataParam[RealMethod] => - dmp.tryMaterializeFor(realMethod).map(dmp.localValueDecl) + dmp.tryMaterializeFor(realMethod, fallbackTag).map(dmp.localValueDecl) case pmp: ParamMetadataParam => Ok(pmp.localValueDecl(paramMappings(pmp))) }.map(constructorCall) @@ -286,36 +290,36 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def createCompositeParam(paramSym: Symbol): ParamCompositeParam = new ParamCompositeParam(this, paramSym) - def tryMaterializeFor(param: RealParam): Res[Tree] = + def tryMaterializeFor(param: RealParam, fallbackTag: FallbackTag): Res[Tree] = Res.traverse(paramLists.flatten) { case pcp: ParamCompositeParam => - pcp.constructor.tryMaterializeFor(param).map(pcp.localValueDecl) + pcp.constructor.tryMaterializeFor(param, fallbackTag).map(pcp.localValueDecl) case dmp: DirectMetadataParam[RealParam] => - dmp.tryMaterializeFor(param).map(dmp.localValueDecl) + dmp.tryMaterializeFor(param, fallbackTag).map(dmp.localValueDecl) }.map(constructorCall) } sealed abstract class DirectMetadataParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) extends MetadataParam[Real](owner, symbol) { - def materializeFor(rpcSym: Real): Tree - def tryMaterializeFor(rpcSym: Real): Res[Tree] + def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree + def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] } class ImplicitParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) extends DirectMetadataParam[Real](owner, symbol) { - val checked: Boolean = annot(CheckedAT).nonEmpty + val checked: Boolean = findAnnotation(symbol, CheckedAT).nonEmpty - def materializeFor(rpcSym: Real): Tree = + def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = q"${infer(actualType)}" - def tryMaterializeFor(rpcSym: Real): Res[Tree] = + def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = if (checked) tryInferCachedImplicit(actualType).map(n => Ok(q"$n")) .getOrElse(Fail(s"no implicit value $actualType for $description could be found")) else - Ok(materializeFor(rpcSym)) + Ok(materializeFor(rpcSym, fallbackTag)) } class ReifiedAnnotParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) @@ -329,7 +333,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem(s"${arity.collectedType} is not a subtype of StaticAnnotation") } - def materializeFor(rpcSym: Real): Tree = { + def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = { def validated(annot: Annot): Annot = { if (containsInaccessibleThises(annot.tree)) { echo(showCode(annot.tree)) @@ -340,19 +344,19 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => arity match { case RpcParamArity.Single(annotTpe) => - rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree)).getOrElse { + rpcSym.annot(annotTpe, fallbackTag).map(a => c.untypecheck(validated(a).tree)).getOrElse { val msg = s"${rpcSym.problemStr}: cannot materialize value for $description: no annotation of type $annotTpe found" q"$RpcUtils.compilationError(${StringLiteral(msg, rpcSym.pos)})" } case RpcParamArity.Optional(annotTpe) => - mkOptional(rpcSym.annot(annotTpe).map(a => c.untypecheck(validated(a).tree))) + mkOptional(rpcSym.annot(annotTpe, fallbackTag).map(a => c.untypecheck(validated(a).tree))) case RpcParamArity.Multi(annotTpe, _) => mkMulti(allAnnotations(rpcSym.symbol, annotTpe).map(a => c.untypecheck(validated(a).tree))) } } - def tryMaterializeFor(rpcSym: Real): Res[Tree] = - Ok(materializeFor(rpcSym)) + def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = + Ok(materializeFor(rpcSym, fallbackTag)) } class HasAnnotParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol, annotTpe: Type) @@ -362,8 +366,10 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem("@hasAnnot can only be used on Boolean parameters") } - def materializeFor(rpcSym: Real): Tree = q"${allAnnotations(rpcSym.symbol, annotTpe).nonEmpty}" - def tryMaterializeFor(rpcSym: Real): Res[Tree] = Ok(materializeFor(rpcSym)) + def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = + q"${allAnnotations(rpcSym.symbol, annotTpe).nonEmpty}" + def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = + Ok(materializeFor(rpcSym, fallbackTag)) } class ReifiedNameParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol, useRpcName: Boolean) @@ -373,11 +379,11 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem(s"its type is not String") } - def materializeFor(rpcSym: Real): Tree = - q"${if (useRpcName) rpcSym.rpcName else rpcSym.nameStr}" + def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = + q"${if (useRpcName) rpcSym.rpcName(fallbackTag) else rpcSym.nameStr}" - def tryMaterializeFor(rpcSym: Real): Res[Tree] = - Ok(materializeFor(rpcSym)) + def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = + Ok(materializeFor(rpcSym, fallbackTag)) } class ReifiedPositionParam(owner: ParamMetadataConstructor, symbol: Symbol) @@ -387,11 +393,11 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem("its type is not ParamPosition") } - def materializeFor(rpcSym: RealParam): Tree = + def materializeFor(rpcSym: RealParam, fallbackTag: FallbackTag): Tree = q"$ParamPositionObj(${rpcSym.index}, ${rpcSym.indexOfList}, ${rpcSym.indexInList}, ${owner.indexInRaw})" - def tryMaterializeFor(rpcSym: RealParam): Res[Tree] = - Ok(materializeFor(rpcSym)) + def tryMaterializeFor(rpcSym: RealParam, fallbackTag: FallbackTag): Res[Tree] = + Ok(materializeFor(rpcSym, fallbackTag)) } class ReifiedFlagsParam(owner: ParamMetadataConstructor, symbol: Symbol) @@ -401,7 +407,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem("its type is not ParamFlags") } - def materializeFor(rpcSym: RealParam): Tree = { + def materializeFor(rpcSym: RealParam, fallbackTag: FallbackTag): Tree = { def flag(cond: Boolean, bit: Int) = if (cond) 1 << bit else 0 val s = rpcSym.symbol.asTerm val rawFlags = @@ -413,16 +419,16 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => q"new $ParamFlagsTpe($rawFlags)" } - def tryMaterializeFor(rpcSym: RealParam): Res[Tree] = - Ok(materializeFor(rpcSym)) + def tryMaterializeFor(rpcSym: RealParam, fallbackTag: FallbackTag): Res[Tree] = + Ok(materializeFor(rpcSym, fallbackTag)) } class UnknownParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) extends DirectMetadataParam[Real](owner, symbol) { - def materializeFor(rpcSym: Real): Tree = + def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = reportProblem(s"no strategy annotation (e.g. @infer) found") - def tryMaterializeFor(rpcSym: Real): Res[Tree] = - Ok(materializeFor(rpcSym)) + def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = + Ok(materializeFor(rpcSym, fallbackTag)) } } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index 8e95f0384..f79dea184 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -18,7 +18,8 @@ trait RpcSymbols { this: RpcMacroCommons => object RpcParamArity { def fromAnnotation(param: ArityParam, allowMulti: Boolean, allowListed: Boolean, allowNamed: Boolean): RpcParamArity = { - val at = param.annot(RpcArityAT).fold(SingleArityAT)(_.tpe) + + val at = findAnnotation(param.symbol, RpcArityAT).fold(SingleArityAT)(_.tpe) if (at <:< SingleArityAT) RpcParamArity.Single(param.actualType) else if (at <:< OptionalArityAT) { val optionLikeType = typeOfCachedImplicit(param.optionLike) @@ -85,8 +86,6 @@ trait RpcSymbols { this: RpcMacroCommons => val nameStr: String = name.decodedName.toString val encodedNameStr: String = name.encodedName.toString - def annot(tpe: Type): Option[Annot] = findAnnotation(symbol, tpe) - override def equals(other: Any): Boolean = other match { case rpcSym: RpcSymbol => symbol == rpcSym.symbol case _ => false @@ -95,41 +94,57 @@ trait RpcSymbols { this: RpcMacroCommons => override def toString: String = symbol.toString } + case class FallbackTag(annotTree: Option[Tree]) + trait RawRpcSymbol extends RpcSymbol { - def baseTag: Type - def defaultTag: Type + def baseTagTpe: Type + def fallbackTag: FallbackTag + + def annot(tpe: Type): Option[Annot] = + findAnnotation(symbol, tpe) + + def tagSpec(a: Annot): (Type, FallbackTag) = { + val tagType = a.tpe.dealias.typeArgs.head + val defaultTagArg = a.tpe.member(TermName("defaultTag")) + val fallbackTag = FallbackTag(Option(a.findArg[Tree](defaultTagArg, EmptyTree)).filter(_ != EmptyTree)) + (tagType, fallbackTag) + } - lazy val requiredTag: Type = { - val result = annot(TaggedAT).fold(baseTag)(_.tpe.baseType(TaggedAT.typeSymbol).typeArgs.head) - if (!(result <:< baseTag)) { + lazy val (requiredTag, whenUntaggedTag) = { + val taggedAnnot = annot(TaggedAT) + val requiredTagType = taggedAnnot.fold(baseTagTpe)(_.tpe.typeArgs.head) + if (!(requiredTagType <:< baseTagTpe)) { val msg = - if (baseTag =:= NothingTpe) + if (baseTagTpe =:= NothingTpe) "cannot use @tagged, no tag annotation type specified with @methodTag/@paramTag" - else s"tag annotation type $requiredTag specified in @tagged annotation " + - s"must be a subtype of specified base tag $baseTag" + else s"tag annotation type $requiredTagType specified in @tagged annotation " + + s"must be a subtype of specified base tag $baseTagTpe" reportProblem(msg) } - result + val whenUntaggedTagTree = taggedAnnot.map(_.findArg[Tree](WhenUntaggedArg, EmptyTree)).filter(_ != EmptyTree) + (requiredTagType, whenUntaggedTagTree) } - def matchesTag(realSymbol: RealRpcSymbol): Boolean = - realSymbol.tag(baseTag, defaultTag) <:< requiredTag - - def matchTag(realRpcSymbol: RealRpcSymbol): Res[Unit] = - if (matchesTag(realRpcSymbol)) Ok(()) - else { - val tag = realRpcSymbol.tag(baseTag, defaultTag) - Fail(s"it does not accept ${realRpcSymbol.shortDescription}s tagged with $tag") - } + // returns fallback tag tree only IF it was necessary + def matchTag(realRpcSymbol: RealRpcSymbol): Res[FallbackTag] = { + val tagAnnot = findAnnotation(realRpcSymbol.symbol, baseTagTpe) + val fallbackTagTree = whenUntaggedTag orElse fallbackTag.annotTree + val realTagTpe = tagAnnot.map(_.tpe) orElse fallbackTagTree.map(_.tpe) getOrElse baseTagTpe + if (realTagTpe <:< requiredTag) + Ok(FallbackTag(fallbackTagTree.filter(_ => tagAnnot.isEmpty))) + else + Fail(s"it does not accept ${realRpcSymbol.shortDescription}s tagged with $realTagTpe") + } } sealed trait RealRpcSymbol extends RpcSymbol { - def tag(baseTag: Type, defaultTag: Type): Type = - annot(baseTag).fold(defaultTag)(_.tpe) + def annot(tpe: Type, usedFallbackTag: FallbackTag): Option[Annot] = + findAnnotation(symbol, tpe, fallback = usedFallbackTag.annotTree.toList) - lazy val rpcName: String = { - val prefixes = allAnnotations(symbol, RpcNamePrefixAT).map(_.findArg[String](RpcNamePrefixArg)) - val rpcName = annot(RpcNameAT).fold(nameStr)(_.findArg[String](RpcNameArg)) + def rpcName(usedFallbackTag: FallbackTag): String = { + val prefixes = allAnnotations(symbol, RpcNamePrefixAT, fallback = usedFallbackTag.annotTree.toList) + .map(_.findArg[String](RpcNamePrefixArg)) + val rpcName = annot(RpcNameAT, usedFallbackTag).fold(nameStr)(_.findArg[String](RpcNameArg)) prefixes.mkString("", "", rpcName) } } @@ -185,9 +200,9 @@ trait RpcSymbols { this: RpcMacroCommons => // @unchecked because "The outer reference in this type test cannot be checked at runtime" // Srsly scalac, from static types it should be obvious that outer references are the same - def matchName(realRpcSymbol: RealRpcSymbol): Res[Unit] = arity match { + def matchName(realRpcSymbol: RealRpcSymbol, fallbackTag: FallbackTag): Res[Unit] = arity match { case _: RpcArity.Single@unchecked | _: RpcArity.Optional@unchecked => - if (realRpcSymbol.rpcName == nameStr) Ok(()) + if (realRpcSymbol.rpcName(fallbackTag) == nameStr) Ok(()) else Fail(s"it only matches ${realRpcSymbol.shortDescription}s with RPC name $nameStr") case _: RpcArity.Multi@unchecked => Ok(()) } @@ -290,8 +305,8 @@ trait RpcSymbols { this: RpcMacroCommons => case class RawValueParam(owner: Either[RawMethod, CompositeRawParam], symbol: Symbol) extends RawParam with RealParamTarget { - def baseTag: Type = containingRawMethod.baseParamTag - def defaultTag: Type = containingRawMethod.defaultParamTag + def baseTagTpe: Type = containingRawMethod.baseParamTag + def fallbackTag: FallbackTag = containingRawMethod.fallbackParamTag def cannotMapClue = s"cannot map it to $shortDescription $pathStr of ${containingRawMethod.nameStr}" } @@ -302,40 +317,26 @@ trait RpcSymbols { this: RpcMacroCommons => def shortDescription = "real parameter" def description = s"$shortDescription $nameStr of ${owner.description}" - val whenAbsent: Tree = annot(WhenAbsentAT).fold(EmptyTree) { annot => - val annotatedDefault = annot.tree.children.tail.head - if (!(annotatedDefault.tpe <:< actualType)) { - reportProblem(s"expected value of type $actualType in @whenAbsent annotation, got ${annotatedDefault.tpe.widen}") - } - val transformer = new Transformer { - override def transform(tree: Tree): Tree = tree match { - case Super(t@This(_), _) if !enclosingClasses.contains(t.symbol) => - reportProblem(s"illegal super-reference in @whenAbsent annotation") - case This(_) if tree.symbol == owner.owner.symbol => q"${owner.owner.safeName}" - case This(_) if !enclosingClasses.contains(tree.symbol) => - reportProblem(s"illegal this-reference in @whenAbsent annotation") - case t => super.transform(t) + def whenAbsent(fallbackTag: FallbackTag): Tree = + annot(WhenAbsentAT, fallbackTag).fold(EmptyTree) { annot => + val annotatedDefault = annot.tree.children.tail.head + if (!(annotatedDefault.tpe <:< actualType)) { + reportProblem(s"expected value of type $actualType in @whenAbsent annotation, got ${annotatedDefault.tpe.widen}") + } + val transformer = new Transformer { + override def transform(tree: Tree): Tree = tree match { + case Super(t@This(_), _) if !enclosingClasses.contains(t.symbol) => + reportProblem(s"illegal super-reference in @whenAbsent annotation") + case This(_) if tree.symbol == owner.owner.symbol => q"${owner.owner.safeName}" + case This(_) if !enclosingClasses.contains(tree.symbol) => + reportProblem(s"illegal this-reference in @whenAbsent annotation") + case t => super.transform(t) + } } + transformer.transform(annotatedDefault) } - transformer.transform(annotatedDefault) - } - - val hasDefaultValue: Boolean = - whenAbsent != EmptyTree || symbol.asTerm.isParamWithDefault - - val transientDefault: Boolean = - hasDefaultValue && annot(TransientDefaultAT).nonEmpty - def fallbackValueTree: Tree = - if (whenAbsent != EmptyTree) c.untypecheck(whenAbsent) - else if (symbol.asTerm.isParamWithDefault) defaultValue(false) - else q"$RpcUtils.missingArg(${owner.rpcName}, $rpcName)" - - def transientValueTree: Tree = - if (symbol.asTerm.isParamWithDefault) defaultValue(true) - else c.untypecheck(whenAbsent) - - private def defaultValue(useThis: Boolean): Tree = { + def defaultValue(useThis: Boolean): Tree = { val prevListParams = owner.realParams.take(index - indexInList).map(rp => q"${rp.safeName}") val prevListParamss = List(prevListParams).filter(_.nonEmpty) val realInst = if (useThis) q"this" else q"${owner.owner.safeName}" @@ -348,18 +349,16 @@ trait RpcSymbols { this: RpcMacroCommons => def description = s"$shortDescription $nameStr of ${owner.description}" def ownerType: Type = owner.tpe - def baseTag: Type = owner.baseMethodTag - def defaultTag: Type = owner.defaultMethodTag + def baseTagTpe: Type = owner.baseMethodTag + def fallbackTag: FallbackTag = owner.fallbackMethodTag val arity: RpcMethodArity = RpcMethodArity.fromAnnotation(this) val verbatimResult: Boolean = annot(RpcEncodingAT).map(_.tpe <:< VerbatimAT).getOrElse(arity.verbatimByDefault) - val List(baseParamTag, defaultParamTag) = - annot(ParamTagAT) - .map(_.tpe.baseType(ParamTagAT.typeSymbol).typeArgs) - .getOrElse(List(owner.baseParamTag, owner.defaultParamTag)) + val (baseParamTag, fallbackParamTag) = + annot(ParamTagAT).map(tagSpec).getOrElse(owner.baseParamTag, owner.fallbackParamTag) val rawParams: List[RawParam] = sig.paramLists match { case Nil | List(_) => sig.paramLists.flatten.map(RawParam(Left(this), _)) @@ -430,11 +429,11 @@ trait RpcSymbols { this: RpcMacroCommons => val realParams: List[RealParam] = { val result = paramLists.flatten - result.groupBy(_.rpcName).foreach { - case (_, head :: tail) if tail.nonEmpty => - head.reportProblem(s"it has the same RPC name as ${tail.size} other parameters") - case _ => - } + // result.groupBy(_.rpcName).foreach { + // case (_, head :: tail) if tail.nonEmpty => + // head.reportProblem(s"it has the same RPC name as ${tail.size} other parameters") + // case _ => + // } result } } @@ -443,18 +442,13 @@ trait RpcSymbols { this: RpcMacroCommons => def shortDescription = "raw RPC" def description = s"$shortDescription $tpe" - def baseTag: Type = typeOf[Nothing] - def defaultTag: Type = typeOf[Nothing] + def baseTagTpe: Type = NothingTpe + def fallbackTag: FallbackTag = FallbackTag(None) - val List(baseMethodTag, defaultMethodTag) = - annot(MethodTagAT) - .map(_.tpe.baseType(MethodTagAT.typeSymbol).typeArgs) - .getOrElse(List(NothingTpe, NothingTpe)) - - val List(baseParamTag, defaultParamTag) = - annot(ParamTagAT) - .map(_.tpe.baseType(ParamTagAT.typeSymbol).typeArgs) - .getOrElse(List(NothingTpe, NothingTpe)) + val (baseMethodTag, fallbackMethodTag) = + annot(MethodTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag(None))) + val (baseParamTag, fallbackParamTag) = + annot(ParamTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag(None))) lazy val rawMethods: List[RawMethod] = tpe.members.iterator.filter(m => m.isTerm && m.isAbstract).map(RawMethod(this, _)).toList @@ -468,12 +462,12 @@ trait RpcSymbols { this: RpcMacroCommons => lazy val realMethods: List[RealMethod] = { val result = tpe.members.iterator.filter(m => m.isTerm && m.isAbstract).map(RealMethod(this, _)).toList - result.groupBy(_.rpcName).foreach { - case (_, head :: tail) if tail.nonEmpty => - head.reportProblem(s"it has the same RPC name as ${tail.size} other methods - " + - s"if you want to overload RPC methods, disambiguate them with @rpcName") - case _ => - } + // result.groupBy(_.rpcName).foreach { + // case (_, head :: tail) if tail.nonEmpty => + // head.reportProblem(s"it has the same RPC name as ${tail.size} other methods - " + + // s"if you want to overload RPC methods, disambiguate them with @rpcName") + // case _ => + // } result } } From 3562a936f5e7e511e2b259834e010694cfff5e49 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 13:38:41 +0200 Subject: [PATCH 53/91] refactored RPC macros by introducing MatchedRealSymbol --- .../commons/macros/rpc/RpcMacros.scala | 4 +- .../commons/macros/rpc/RpcMappings.scala | 145 ++++++++------- .../commons/macros/rpc/RpcMetadatas.scala | 125 +++++++------ .../commons/macros/rpc/RpcSymbols.scala | 168 ++++++++++-------- 4 files changed, 239 insertions(+), 203 deletions(-) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index d2004fcdc..876ff1389 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -45,7 +45,7 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo val TypedMetadataType: Type = getType(tq"$RpcPackage.TypedMetadata[_]") val MetadataParamStrategyType: Type = getType(tq"$RpcPackage.MetadataParamStrategy") val ReifyAnnotAT: Type = getType(tq"$RpcPackage.reifyAnnot") - val HasAnnotAT: Type = getType(tq"$RpcPackage.hasAnnot[_]") + val IsAnnotatedAT: Type = getType(tq"$RpcPackage.isAnnotated[_]") val InferAT: Type = getType(tq"$RpcPackage.infer") val ReifyNameAT: Type = getType(tq"$RpcPackage.reifyName") val ReifyPositionAT: Type = getType(tq"$RpcPackage.reifyPosition") @@ -102,6 +102,7 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) val raw = RawRpcTrait(weakTypeOf[Raw].dealias) val real = RealRpcTrait(weakTypeOf[Real].dealias) val mapping = RpcMapping(real, raw, forAsRaw = true, forAsReal = false) + mapping.ensureUniqueRpcNames() // must be evaluated before `cachedImplicitDeclarations`, don't inline it into the quasiquote val asRawDef = mapping.asRawImpl @@ -118,6 +119,7 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) val raw = RawRpcTrait(weakTypeOf[Raw].dealias) val real = RealRpcTrait(weakTypeOf[Real].dealias) val mapping = RpcMapping(real, raw, forAsRaw = true, forAsReal = true) + mapping.ensureUniqueRpcNames() // these two must be evaluated before `cachedImplicitDeclarations`, don't inline them into the quasiquote val asRealDef = mapping.asRealImpl diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index 7529fdc8c..5e74a8f98 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -10,7 +10,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def collectMethodMappings[R <: RawRpcSymbol with AritySymbol, M]( rawSymbols: List[R], rawShortDesc: String, realMethods: List[RealMethod])( - createMapping: (R, RealMethod, FallbackTag) => Res[M]): List[M] = { + createMapping: (R, MatchedMethod) => Res[M]): List[M] = { val failedReals = new ListBuffer[String] def addFailure(realMethod: RealMethod, message: String): Unit = { @@ -22,8 +22,9 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => val methodMappings = rawSymbols.map { rawSymbol => val res = for { fallbackTag <- rawSymbol.matchTag(realMethod) - _ <- rawSymbol.matchName(realMethod, fallbackTag) - methodMapping <- createMapping(rawSymbol, realMethod, fallbackTag) + matchedMethod = MatchedMethod(realMethod, fallbackTag) + _ <- rawSymbol.matchName(matchedMethod) + methodMapping <- createMapping(rawSymbol, matchedMethod) } yield (rawSymbol, methodMapping) res.mapFailure(msg => s"${rawSymbol.shortDescription} ${rawSymbol.nameStr} did not match: $msg") } @@ -46,10 +47,10 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => result } - def collectParamMappings[R <: RealParamTarget, M](raws: List[R], rawShortDesc: String, realMethod: RealMethod) + def collectParamMappings[R <: RealParamTarget, M](raws: List[R], rawShortDesc: String, matchedMethod: MatchedMethod) (createMapping: (R, ParamsParser) => Res[M]): Res[List[M]] = { - val parser = new ParamsParser(realMethod) + val parser = new ParamsParser(matchedMethod) Res.traverse(raws)(createMapping(_, parser)).flatMap { result => if (parser.remaining.isEmpty) Ok(result) else { @@ -59,16 +60,16 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } } - class ParamsParser(realMethod: RealMethod) { + class ParamsParser(matchedMethod: MatchedMethod) { import scala.collection.JavaConverters._ private val realParams = new java.util.LinkedList[RealParam] - realParams.addAll(realMethod.realParams.asJava) + realParams.addAll(matchedMethod.real.realParams.asJava) def remaining: Seq[RealParam] = realParams.asScala - def extractSingle[B](raw: RealParamTarget, matcher: (RealParam, FallbackTag) => Res[B]): Res[B] = { + def extractSingle[B](raw: RealParamTarget, matcher: MatchedParam => Res[B]): Res[B] = { val it = realParams.listIterator() def loop(): Res[B] = if (it.hasNext) { @@ -78,21 +79,21 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => if (!raw.auxiliary) { it.remove() } - matcher(real, fallbackTag) + matcher(MatchedParam(real, fallbackTag, matchedMethod)) case Fail(_) => loop() } } else Fail(s"${raw.shortDescription} ${raw.pathStr} was not matched by real parameter") loop() } - def extractOptional[B](raw: RealParamTarget, matcher: (RealParam, FallbackTag) => Res[B]): Option[B] = { + def extractOptional[B](raw: RealParamTarget, matcher: MatchedParam => Res[B]): Option[B] = { val it = realParams.listIterator() def loop(): Option[B] = if (it.hasNext) { val real = it.next() raw.matchTag(real) match { case Ok(fallbackTag) => - val res = matcher(real, fallbackTag).toOption + val res = matcher(MatchedParam(real, fallbackTag, matchedMethod)).toOption if (!raw.auxiliary) { res.foreach(_ => it.remove()) } @@ -103,7 +104,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => loop() } - def extractMulti[B](raw: RealParamTarget, matcher: (RealParam, FallbackTag, Int) => Res[B], named: Boolean): Res[List[B]] = { + def extractMulti[B](raw: RealParamTarget, matcher: (MatchedParam, Int) => Res[B], named: Boolean): Res[List[B]] = { val it = realParams.listIterator() def loop(result: ListBuffer[B]): Res[List[B]] = if (it.hasNext) { @@ -113,7 +114,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => if (!raw.auxiliary) { it.remove() } - matcher(real, fallbackTag, result.size) match { + matcher(MatchedParam(real, fallbackTag, matchedMethod), result.size) match { case Ok(b) => result += b loop(result) @@ -127,45 +128,34 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } } - case class EncodedRealParam(realParam: RealParam, encoding: RpcEncoding, fallbackTag: FallbackTag) { - val rpcName: String = realParam.rpcName(fallbackTag) - - def safeName: TermName = realParam.safeName - def rawValueTree: Tree = encoding.applyAsRaw(realParam.safeName) - def localValueDecl(body: Tree): Tree = realParam.localValueDecl(body) - - val whenAbsent: Tree = realParam.whenAbsent(fallbackTag) - - val hasDefaultValue: Boolean = - whenAbsent != EmptyTree || realParam.symbol.asTerm.isParamWithDefault - - val transientDefault: Boolean = - hasDefaultValue && realParam.annot(TransientDefaultAT, fallbackTag).nonEmpty - - def fallbackValueTree(ownerRpcName: String): Tree = - if (whenAbsent != EmptyTree) c.untypecheck(whenAbsent) - else if (realParam.symbol.asTerm.isParamWithDefault) realParam.defaultValue(false) - else q"$RpcUtils.missingArg($ownerRpcName, $rpcName)" - - def transientValueTree: Tree = - if (realParam.symbol.asTerm.isParamWithDefault) realParam.defaultValue(true) - else c.untypecheck(whenAbsent) + case class EncodedRealParam(matchedParam: MatchedParam, encoding: RpcEncoding) { + def realParam: RealParam = matchedParam.real + def rpcName: String = matchedParam.rpcName + def safeName: TermName = matchedParam.real.safeName + def rawValueTree: Tree = encoding.applyAsRaw(matchedParam.real.safeName) + def localValueDecl(body: Tree): Tree = matchedParam.real.localValueDecl(body) } object EncodedRealParam { - def create(rawParam: RawValueParam, realParam: RealParam, fallbackTag: FallbackTag): Res[EncodedRealParam] = - RpcEncoding.forParam(rawParam, realParam).map(EncodedRealParam(realParam, _, fallbackTag)) + def create(rawParam: RawValueParam, matchedParam: MatchedParam): Res[EncodedRealParam] = + RpcEncoding.forParam(rawParam, matchedParam.real).map(EncodedRealParam(matchedParam, _)) } sealed trait ParamMapping { def rawParam: RawValueParam def rawValueTree: Tree - def realDecls(ownerRpcName: String): List[Tree] + def realDecls: List[Tree] + + def allMatchedParams: List[MatchedParam] = this match { + case ParamMapping.Single(_, erp) => List(erp.matchedParam) + case ParamMapping.Optional(_, erpOpt) => erpOpt.map(_.matchedParam).toList + case multi: ParamMapping.Multi => multi.reals.map(_.matchedParam) + } } object ParamMapping { case class Single(rawParam: RawValueParam, realParam: EncodedRealParam) extends ParamMapping { def rawValueTree: Tree = realParam.rawValueTree - def realDecls(ownerRpcName: String): List[Tree] = + def realDecls: List[Tree] = List(realParam.localValueDecl(realParam.encoding.applyAsReal(rawParam.safePath))) } case class Optional(rawParam: RawValueParam, wrapped: Option[EncodedRealParam]) extends ParamMapping { @@ -173,23 +163,25 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => val noneRes: Tree = q"${rawParam.optionLike}.none" wrapped.fold(noneRes) { erp => val baseRes = q"${rawParam.optionLike}.some(${erp.rawValueTree})" - if (erp.transientDefault) - q"if(${erp.realParam.safeName} != ${erp.transientValueTree}) $baseRes else $noneRes" + if (erp.matchedParam.transientDefault) + q"if(${erp.safeName} != ${erp.matchedParam.transientValueTree}) $baseRes else $noneRes" else baseRes } } - def realDecls(ownerRpcName: String): List[Tree] = wrapped.toList.map { erp => - val defaultValueTree = erp.fallbackValueTree(ownerRpcName) + def realDecls: List[Tree] = wrapped.toList.map { erp => + val defaultValueTree = erp.matchedParam.fallbackValueTree erp.realParam.localValueDecl(erp.encoding.foldWithAsReal( rawParam.optionLike, rawParam.safePath, defaultValueTree)) } } - abstract class ListedMulti extends ParamMapping { - protected def reals: List[EncodedRealParam] + abstract class Multi extends ParamMapping { + def reals: List[EncodedRealParam] + } + abstract class ListedMulti extends Multi { def rawValueTree: Tree = rawParam.mkMulti(reals.map(_.rawValueTree)) } case class IterableMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ListedMulti { - def realDecls(ownerRpcName: String): List[Tree] = if (reals.isEmpty) Nil else { + def realDecls: List[Tree] = if (reals.isEmpty) Nil else { val itName = c.freshName(TermName("it")) val itDecl = q"val $itName = ${rawParam.safePath}.iterator" itDecl :: reals.map { erp => @@ -199,29 +191,29 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => s"${rawParam.cannotMapClue}: by-name real parameters cannot be extracted from @multi raw parameters") } erp.localValueDecl( - q"if($itName.hasNext) ${erp.encoding.applyAsReal(q"$itName.next()")} else ${erp.fallbackValueTree(ownerRpcName)}") + q"if($itName.hasNext) ${erp.encoding.applyAsReal(q"$itName.next()")} else ${erp.matchedParam.fallbackValueTree}") } } } case class IndexedMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ListedMulti { - def realDecls(ownerRpcName: String): List[Tree] = { + def realDecls: List[Tree] = { reals.zipWithIndex.map { case (erp, idx) => - erp.realParam.localValueDecl( + erp.localValueDecl( q""" ${erp.encoding.andThenAsReal(rawParam.safePath)} - .applyOrElse($idx, (_: $IntCls) => ${erp.fallbackValueTree(ownerRpcName)}) + .applyOrElse($idx, (_: $IntCls) => ${erp.matchedParam.fallbackValueTree}) """) } } } - case class NamedMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends ParamMapping { + case class NamedMulti(rawParam: RawValueParam, reals: List[EncodedRealParam]) extends Multi { def rawValueTree: Tree = if (reals.isEmpty) q"$RpcUtils.createEmpty(${rawParam.canBuildFrom})" else { val builderName = c.freshName(TermName("builder")) val addStatements = reals.map { erp => val baseStat = q"$builderName += ((${erp.rpcName}, ${erp.rawValueTree}))" - if (erp.transientDefault) - q"if(${erp.realParam.safeName} != ${erp.transientValueTree}) $baseStat" + if (erp.matchedParam.transientDefault) + q"if(${erp.safeName} != ${erp.matchedParam.transientValueTree}) $baseStat" else baseStat } q""" @@ -230,12 +222,12 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => $builderName.result() """ } - def realDecls(ownerRpcName: String): List[Tree] = + def realDecls: List[Tree] = reals.map { erp => - erp.realParam.localValueDecl( + erp.localValueDecl( q""" ${erp.encoding.andThenAsReal(rawParam.safePath)} - .applyOrElse(${erp.rpcName}, (_: $StringCls) => ${erp.fallbackValueTree(ownerRpcName)}) + .applyOrElse(${erp.rpcName}, (_: $StringCls) => ${erp.matchedParam.fallbackValueTree}) """) } } @@ -291,14 +283,22 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } } - case class MethodMapping(realMethod: RealMethod, rawMethod: RawMethod, fallbackTag: FallbackTag, + case class MethodMapping(matchedMethod: MatchedMethod, rawMethod: RawMethod, paramMappingList: List[ParamMapping], resultEncoding: RpcEncoding) { - val rpcName: String = realMethod.rpcName(fallbackTag) + def realMethod: RealMethod = matchedMethod.real + def rpcName: String = matchedMethod.rpcName val paramMappings: Map[RawValueParam, ParamMapping] = paramMappingList.iterator.map(m => (m.rawParam, m)).toMap + def ensureUniqueRpcNames(): Unit = + paramMappings.values.toList.flatMap(_.allMatchedParams).groupBy(_.rpcName).foreach { + case (rpcName, head :: tail) if tail.nonEmpty => + head.real.reportProblem(s"it has the same RPC name ($rpcName) as ${tail.size} other parameters") + case _ => + } + private def rawValueTree(rawParam: RawParam): Tree = rawParam match { case _: MethodNameParam => q"$rpcName" case rvp: RawValueParam => paramMappings(rvp).rawValueTree @@ -319,7 +319,7 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => def rawCaseImpl: Tree = q""" - ..${paramMappings.values.filterNot(_.rawParam.auxiliary).flatMap(_.realDecls(rpcName))} + ..${paramMappings.values.filterNot(_.rawParam.auxiliary).flatMap(_.realDecls)} ${resultEncoding.applyAsRaw(q"${realMethod.owner.safeName}.${realMethod.name}(...${realMethod.argLists})")} """ } @@ -336,14 +336,14 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => registerCompanionImplicits(raw.tpe) private def extractMapping(rawParam: RawValueParam, parser: ParamsParser): Res[ParamMapping] = { - def createErp(realParam: RealParam, fallbackTag: FallbackTag, index: Int): Res[EncodedRealParam] = - EncodedRealParam.create(rawParam, realParam, fallbackTag) + def createErp(matchedParam: MatchedParam, index: Int): Res[EncodedRealParam] = + EncodedRealParam.create(rawParam, matchedParam) rawParam.arity match { case _: RpcParamArity.Single => - parser.extractSingle(rawParam, createErp(_, _, 0)).map(ParamMapping.Single(rawParam, _)) + parser.extractSingle(rawParam, createErp(_, 0)).map(ParamMapping.Single(rawParam, _)) case _: RpcParamArity.Optional => - Ok(ParamMapping.Optional(rawParam, parser.extractOptional(rawParam, createErp(_, _, 0)))) + Ok(ParamMapping.Optional(rawParam, parser.extractOptional(rawParam, createErp(_, 0)))) case RpcParamArity.Multi(_, true) => parser.extractMulti(rawParam, createErp, named = true).map(ParamMapping.NamedMulti(rawParam, _)) case _: RpcParamArity.Multi if rawParam.actualType <:< BIndexedSeqTpe => @@ -353,7 +353,8 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } } - private def mappingRes(rawMethod: RawMethod, realMethod: RealMethod, fallbackTag: FallbackTag): Res[MethodMapping] = { + private def mappingRes(rawMethod: RawMethod, matchedMethod: MatchedMethod): Res[MethodMapping] = { + val realMethod = matchedMethod.real def resultEncoding: Res[RpcEncoding] = if (rawMethod.verbatimResult) { if (rawMethod.resultType =:= realMethod.resultType) @@ -371,13 +372,23 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => for { resultConv <- resultEncoding - paramMappings <- collectParamMappings(rawMethod.allValueParams, "raw parameter", realMethod)(extractMapping) - } yield MethodMapping(realMethod, rawMethod, fallbackTag, paramMappings, resultConv) + paramMappings <- collectParamMappings(rawMethod.allValueParams, "raw parameter", matchedMethod)(extractMapping) + } yield MethodMapping(matchedMethod, rawMethod, paramMappings, resultConv) } lazy val methodMappings: List[MethodMapping] = collectMethodMappings(raw.rawMethods, "raw methods", real.realMethods)(mappingRes) + def ensureUniqueRpcNames(): Unit = + methodMappings.groupBy(_.matchedMethod.rpcName).foreach { + case (_, single :: Nil) => + single.ensureUniqueRpcNames() + case (rpcName, head :: tail) => + head.realMethod.reportProblem(s"it has the same RPC name ($rpcName) as ${tail.size} other methods - " + + s"if you want to overload RPC methods, disambiguate them with @rpcName") + case _ => + } + def asRealImpl: Tree = q""" def asReal(${raw.safeName}: ${raw.tpe}): ${real.tpe} = new ${real.tpe} { diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala index fb4db1256..5c1d2a466 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMetadatas.scala @@ -80,12 +80,12 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => annot(ParamTagAT).orElse(findAnnotation(arity.collectedType.typeSymbol, ParamTagAT)) .map(tagSpec).getOrElse(owner.baseParamTag, owner.fallbackParamTag) - def mappingFor(realMethod: RealMethod, fallbackTag: FallbackTag): Res[MethodMetadataMapping] = for { - mdType <- actualMetadataType(arity.collectedType, realMethod, verbatimResult) + def mappingFor(matchedMethod: MatchedMethod): Res[MethodMetadataMapping] = for { + mdType <- actualMetadataType(arity.collectedType, matchedMethod.real, verbatimResult) constructor = new MethodMetadataConstructor(mdType, Left(this)) - paramMappings <- constructor.paramMappings(realMethod) - tree <- constructor.tryMaterializeFor(realMethod, fallbackTag, paramMappings) - } yield MethodMetadataMapping(realMethod, fallbackTag, this, tree) + paramMappings <- constructor.paramMappings(matchedMethod) + tree <- constructor.tryMaterializeFor(matchedMethod, paramMappings) + } yield MethodMetadataMapping(matchedMethod, this, tree) } class ParamMetadataParam(owner: MethodMetadataConstructor, symbol: Symbol) @@ -102,22 +102,23 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem(s"type ${arity.collectedType} is not a subtype of TypedMetadata[_]") } - private def metadataTree(realParam: RealParam, fallbackTag: FallbackTag, indexInRaw: Int): Res[Tree] = { + private def metadataTree(matchedParam: MatchedParam, indexInRaw: Int): Res[Tree] = { + val realParam = matchedParam.real val result = for { mdType <- actualMetadataType(arity.collectedType, realParam, verbatim) - tree <- new ParamMetadataConstructor(mdType, Left(this), indexInRaw).tryMaterializeFor(realParam, fallbackTag) + tree <- new ParamMetadataConstructor(mdType, Left(this), indexInRaw).tryMaterializeFor(matchedParam) } yield tree result.mapFailure(msg => s"${realParam.problemStr}: $cannotMapClue: $msg") } def metadataFor(parser: ParamsParser): Res[Tree] = arity match { case _: RpcParamArity.Single => - parser.extractSingle(this, metadataTree(_, _, 0)) + parser.extractSingle(this, metadataTree(_, 0)) case _: RpcParamArity.Optional => - Ok(mkOptional(parser.extractOptional(this, metadataTree(_, _, 0)))) + Ok(mkOptional(parser.extractOptional(this, metadataTree(_, 0)))) case RpcParamArity.Multi(_, true) => - parser.extractMulti(this, (rp, ft, i) => metadataTree(rp, ft, i) - .map(t => q"(${rp.rpcName(ft)}, $t)"), named = true).map(mkMulti(_)) + parser.extractMulti(this, (mp, i) => metadataTree(mp, i) + .map(t => q"(${mp.rpcName}, $t)"), named = true).map(mkMulti(_)) case _: RpcParamArity.Multi => parser.extractMulti(this, metadataTree, named = false).map(mkMulti(_)) } @@ -146,8 +147,8 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case t if t <:< ReifyNameAT => val useRpcName = annot.findArg[Boolean](ReifyNameAT.member(TermName("rpcName")), false) new ReifiedNameParam(this, paramSym, useRpcName) - case t if t <:< HasAnnotAT => - new HasAnnotParam(this, paramSym, t.typeArgs.head) + case t if t <:< IsAnnotatedAT => + new IsAnnotatedParam(this, paramSym, t.typeArgs.head) case t => reportProblem(s"metadata param strategy $t is not allowed here") } @@ -169,25 +170,21 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => """ } - case class MethodMetadataMapping( - realMethod: RealMethod, fallbackTag: FallbackTag, mdParam: MethodMetadataParam, tree: Tree) { - - val rpcName: String = realMethod.rpcName(fallbackTag) - } + case class MethodMetadataMapping(matchedMethod: MatchedMethod, mdParam: MethodMetadataParam, tree: Tree) class RpcMetadataConstructor(val ownerType: Type, val atParam: Option[RpcCompositeParam]) extends MetadataConstructor[RealRpcTrait](primaryConstructor(ownerType, atParam)) with RawRpcSymbol { def baseTagTpe: Type = NothingTpe - def fallbackTag: FallbackTag = FallbackTag(None) + def fallbackTag: FallbackTag = FallbackTag.Empty override def annot(tpe: Type): Option[Annot] = super[MetadataConstructor].annot(tpe) val (baseMethodTag, fallbackMethodTag) = - annot(MethodTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag(None))) + annot(MethodTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag.Empty)) val (baseParamTag, fallbackParamTag) = - annot(ParamTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag(None))) + annot(ParamTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag.Empty)) lazy val methodMdParams: List[MethodMetadataParam] = paramLists.flatten.flatMap { case mmp: MethodMetadataParam => List(mmp) @@ -202,14 +199,14 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => new RpcCompositeParam(this, paramSym) def methodMappings(rpc: RealRpcTrait): Map[MethodMetadataParam, List[MethodMetadataMapping]] = - collectMethodMappings(methodMdParams, "metadata parameters", rpc.realMethods)(_.mappingFor(_, _)).groupBy(_.mdParam) + collectMethodMappings(methodMdParams, "metadata parameters", rpc.realMethods)(_.mappingFor(_)).groupBy(_.mdParam) def materializeFor(rpc: RealRpcTrait, methodMappings: Map[MethodMetadataParam, List[MethodMetadataMapping]]): Tree = { val argDecls = paramLists.flatten.map { case rcp: RpcCompositeParam => rcp.localValueDecl(rcp.constructor.materializeFor(rpc, methodMappings)) case dmp: DirectMetadataParam[RealRpcTrait] => - dmp.localValueDecl(dmp.materializeFor(rpc, FallbackTag(None))) + dmp.localValueDecl(dmp.materializeFor(MatchedRpcTrait(rpc))) case mmp: MethodMetadataParam => mmp.localValueDecl { val mappings = methodMappings.getOrElse(mmp, Nil) mmp.arity match { @@ -224,7 +221,7 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => case _ => abort(s"multiple real methods match ${mmp.description}") } case RpcParamArity.Multi(_, _) => - mmp.mkMulti(mappings.map(m => q"(${m.rpcName}, ${m.tree})")) + mmp.mkMulti(mappings.map(m => q"(${m.matchedMethod.rpcName}, ${m.tree})")) } } } @@ -253,18 +250,16 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def createCompositeParam(paramSym: Symbol): MethodCompositeParam = new MethodCompositeParam(this, paramSym) - def paramMappings(realMethod: RealMethod): Res[Map[ParamMetadataParam, Tree]] = - collectParamMappings(paramMdParams, "metadata parameter", realMethod)( + def paramMappings(matchedMethod: MatchedMethod): Res[Map[ParamMetadataParam, Tree]] = + collectParamMappings(paramMdParams, "metadata parameter", matchedMethod)( (param, parser) => param.metadataFor(parser).map(t => (param, t))).map(_.toMap) - def tryMaterializeFor(realMethod: RealMethod, fallbackTag: FallbackTag, - paramMappings: Map[ParamMetadataParam, Tree]): Res[Tree] = - + def tryMaterializeFor(matchedMethod: MatchedMethod, paramMappings: Map[ParamMetadataParam, Tree]): Res[Tree] = Res.traverse(paramLists.flatten) { case cmp: MethodCompositeParam => - cmp.constructor.tryMaterializeFor(realMethod, fallbackTag, paramMappings).map(cmp.localValueDecl) + cmp.constructor.tryMaterializeFor(matchedMethod, paramMappings).map(cmp.localValueDecl) case dmp: DirectMetadataParam[RealMethod] => - dmp.tryMaterializeFor(realMethod, fallbackTag).map(dmp.localValueDecl) + dmp.tryMaterializeFor(matchedMethod).map(dmp.localValueDecl) case pmp: ParamMetadataParam => Ok(pmp.localValueDecl(paramMappings(pmp))) }.map(constructorCall) @@ -290,20 +285,20 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => def createCompositeParam(paramSym: Symbol): ParamCompositeParam = new ParamCompositeParam(this, paramSym) - def tryMaterializeFor(param: RealParam, fallbackTag: FallbackTag): Res[Tree] = + def tryMaterializeFor(matchedParam: MatchedParam): Res[Tree] = Res.traverse(paramLists.flatten) { case pcp: ParamCompositeParam => - pcp.constructor.tryMaterializeFor(param, fallbackTag).map(pcp.localValueDecl) + pcp.constructor.tryMaterializeFor(matchedParam).map(pcp.localValueDecl) case dmp: DirectMetadataParam[RealParam] => - dmp.tryMaterializeFor(param, fallbackTag).map(dmp.localValueDecl) + dmp.tryMaterializeFor(matchedParam).map(dmp.localValueDecl) }.map(constructorCall) } sealed abstract class DirectMetadataParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) extends MetadataParam[Real](owner, symbol) { - def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree - def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] + def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree + def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] } class ImplicitParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) @@ -311,15 +306,15 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => val checked: Boolean = findAnnotation(symbol, CheckedAT).nonEmpty - def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = + def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree = q"${infer(actualType)}" - def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = + def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] = if (checked) tryInferCachedImplicit(actualType).map(n => Ok(q"$n")) .getOrElse(Fail(s"no implicit value $actualType for $description could be found")) else - Ok(materializeFor(rpcSym, fallbackTag)) + Ok(materializeFor(matchedSymbol)) } class ReifiedAnnotParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) @@ -333,43 +328,44 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem(s"${arity.collectedType} is not a subtype of StaticAnnotation") } - def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = { + def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree = { def validated(annot: Annot): Annot = { if (containsInaccessibleThises(annot.tree)) { echo(showCode(annot.tree)) - rpcSym.reportProblem(s"reified annotation contains this-references inaccessible outside RPC trait") + matchedSymbol.real.reportProblem(s"reified annotation contains this-references inaccessible outside RPC trait") } annot } + val rpcSym = matchedSymbol.real arity match { case RpcParamArity.Single(annotTpe) => - rpcSym.annot(annotTpe, fallbackTag).map(a => c.untypecheck(validated(a).tree)).getOrElse { + matchedSymbol.annot(annotTpe).map(a => c.untypecheck(validated(a).tree)).getOrElse { val msg = s"${rpcSym.problemStr}: cannot materialize value for $description: no annotation of type $annotTpe found" q"$RpcUtils.compilationError(${StringLiteral(msg, rpcSym.pos)})" } case RpcParamArity.Optional(annotTpe) => - mkOptional(rpcSym.annot(annotTpe, fallbackTag).map(a => c.untypecheck(validated(a).tree))) + mkOptional(matchedSymbol.annot(annotTpe).map(a => c.untypecheck(validated(a).tree))) case RpcParamArity.Multi(annotTpe, _) => mkMulti(allAnnotations(rpcSym.symbol, annotTpe).map(a => c.untypecheck(validated(a).tree))) } } - def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = - Ok(materializeFor(rpcSym, fallbackTag)) + def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] = + Ok(materializeFor(matchedSymbol)) } - class HasAnnotParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol, annotTpe: Type) + class IsAnnotatedParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol, annotTpe: Type) extends DirectMetadataParam[Real](owner, symbol) { if (!(actualType =:= typeOf[Boolean])) { reportProblem("@hasAnnot can only be used on Boolean parameters") } - def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = - q"${allAnnotations(rpcSym.symbol, annotTpe).nonEmpty}" - def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = - Ok(materializeFor(rpcSym, fallbackTag)) + def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree = + q"${matchedSymbol.allAnnots(annotTpe).nonEmpty}" + def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] = + Ok(materializeFor(matchedSymbol)) } class ReifiedNameParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol, useRpcName: Boolean) @@ -379,11 +375,11 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem(s"its type is not String") } - def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = - q"${if (useRpcName) rpcSym.rpcName(fallbackTag) else rpcSym.nameStr}" + def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree = + q"${if (useRpcName) matchedSymbol.rpcName else matchedSymbol.real.nameStr}" - def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = - Ok(materializeFor(rpcSym, fallbackTag)) + def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] = + Ok(materializeFor(matchedSymbol)) } class ReifiedPositionParam(owner: ParamMetadataConstructor, symbol: Symbol) @@ -393,11 +389,13 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem("its type is not ParamPosition") } - def materializeFor(rpcSym: RealParam, fallbackTag: FallbackTag): Tree = + def materializeFor(matchedParam: MatchedRealSymbol[RealParam]): Tree = { + val rpcSym = matchedParam.real q"$ParamPositionObj(${rpcSym.index}, ${rpcSym.indexOfList}, ${rpcSym.indexInList}, ${owner.indexInRaw})" + } - def tryMaterializeFor(rpcSym: RealParam, fallbackTag: FallbackTag): Res[Tree] = - Ok(materializeFor(rpcSym, fallbackTag)) + def tryMaterializeFor(matchedParam: MatchedRealSymbol[RealParam]): Res[Tree] = + Ok(materializeFor(matchedParam)) } class ReifiedFlagsParam(owner: ParamMetadataConstructor, symbol: Symbol) @@ -407,7 +405,8 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => reportProblem("its type is not ParamFlags") } - def materializeFor(rpcSym: RealParam, fallbackTag: FallbackTag): Tree = { + def materializeFor(matchedParam: MatchedRealSymbol[RealParam]): Tree = { + val rpcSym = matchedParam.real def flag(cond: Boolean, bit: Int) = if (cond) 1 << bit else 0 val s = rpcSym.symbol.asTerm val rawFlags = @@ -419,16 +418,16 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => q"new $ParamFlagsTpe($rawFlags)" } - def tryMaterializeFor(rpcSym: RealParam, fallbackTag: FallbackTag): Res[Tree] = - Ok(materializeFor(rpcSym, fallbackTag)) + def tryMaterializeFor(matchedParam: MatchedRealSymbol[RealParam]): Res[Tree] = + Ok(materializeFor(matchedParam)) } class UnknownParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) extends DirectMetadataParam[Real](owner, symbol) { - def materializeFor(rpcSym: Real, fallbackTag: FallbackTag): Tree = + def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree = reportProblem(s"no strategy annotation (e.g. @infer) found") - def tryMaterializeFor(rpcSym: Real, fallbackTag: FallbackTag): Res[Tree] = - Ok(materializeFor(rpcSym, fallbackTag)) + def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] = + Ok(materializeFor(matchedSymbol)) } } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index f79dea184..83eff59b9 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -94,7 +94,84 @@ trait RpcSymbols { this: RpcMacroCommons => override def toString: String = symbol.toString } - case class FallbackTag(annotTree: Option[Tree]) + case class FallbackTag(annotTree: Tree) { + def asList: List[Tree] = List(annotTree).filter(_ != EmptyTree) + def orElse(other: FallbackTag): FallbackTag = FallbackTag(annotTree orElse other.annotTree) + } + object FallbackTag { + final val Empty = FallbackTag(EmptyTree) + } + + sealed trait MatchedRealSymbol[+Real <: RealRpcSymbol] { + def real: Real + def fallbackTagUsed: FallbackTag + + def annot(tpe: Type): Option[Annot] = + findAnnotation(real.symbol, tpe, fallback = fallbackTagUsed.asList) + + def allAnnots(tpe: Type): List[Annot] = + allAnnotations(real.symbol, tpe, fallback = fallbackTagUsed.asList) + + val rpcName: String = { + val prefixes = allAnnotations(real.symbol, RpcNamePrefixAT, fallback = fallbackTagUsed.asList) + .map(_.findArg[String](RpcNamePrefixArg)) + val rpcName = annot(RpcNameAT).fold(real.nameStr)(_.findArg[String](RpcNameArg)) + prefixes.mkString("", "", rpcName) + } + } + + case class MatchedRpcTrait(real: RealRpcTrait) extends MatchedRealSymbol[RealRpcTrait] { + def fallbackTagUsed: FallbackTag = FallbackTag.Empty + } + + case class MatchedMethod(real: RealMethod, fallbackTagUsed: FallbackTag) + extends MatchedRealSymbol[RealMethod] + + case class MatchedParam(real: RealParam, fallbackTagUsed: FallbackTag, matchedOwner: MatchedMethod) + extends MatchedRealSymbol[RealParam] { + + val whenAbsent: Tree = + annot(WhenAbsentAT).fold(EmptyTree) { annot => + val annotatedDefault = annot.tree.children.tail.head + if (!(annotatedDefault.tpe <:< real.actualType)) { + real.reportProblem(s"expected value of type ${real.actualType} in @whenAbsent annotation, " + + s"got ${annotatedDefault.tpe.widen}") + } + val transformer = new Transformer { + override def transform(tree: Tree): Tree = tree match { + case Super(t@This(_), _) if !enclosingClasses.contains(t.symbol) => + real.reportProblem(s"illegal super-reference in @whenAbsent annotation") + case This(_) if tree.symbol == real.owner.owner.symbol => q"${real.owner.owner.safeName}" + case This(_) if !enclosingClasses.contains(tree.symbol) => + real.reportProblem(s"illegal this-reference in @whenAbsent annotation") + case t => super.transform(t) + } + } + transformer.transform(annotatedDefault) + } + + val hasDefaultValue: Boolean = + whenAbsent != EmptyTree || real.symbol.asTerm.isParamWithDefault + + val transientDefault: Boolean = + hasDefaultValue && annot(TransientDefaultAT).nonEmpty + + def fallbackValueTree: Tree = + if (whenAbsent != EmptyTree) c.untypecheck(whenAbsent) + else if (real.symbol.asTerm.isParamWithDefault) defaultValue(false) + else q"$RpcUtils.missingArg(${matchedOwner.rpcName}, $rpcName)" + + def transientValueTree: Tree = + if (real.symbol.asTerm.isParamWithDefault) defaultValue(true) + else c.untypecheck(whenAbsent) + + private def defaultValue(useThis: Boolean): Tree = { + val prevListParams = real.owner.realParams.take(real.index - real.indexInList).map(rp => q"${rp.safeName}") + val prevListParamss = List(prevListParams).filter(_.nonEmpty) + val realInst = if (useThis) q"this" else q"${real.owner.owner.safeName}" + q"$realInst.${TermName(s"${real.owner.encodedNameStr}$$default$$${real.index + 1}")}(...$prevListParamss)" + } + } trait RawRpcSymbol extends RpcSymbol { def baseTagTpe: Type @@ -106,7 +183,7 @@ trait RpcSymbols { this: RpcMacroCommons => def tagSpec(a: Annot): (Type, FallbackTag) = { val tagType = a.tpe.dealias.typeArgs.head val defaultTagArg = a.tpe.member(TermName("defaultTag")) - val fallbackTag = FallbackTag(Option(a.findArg[Tree](defaultTagArg, EmptyTree)).filter(_ != EmptyTree)) + val fallbackTag = FallbackTag(a.findArg[Tree](defaultTagArg, EmptyTree)) (tagType, fallbackTag) } @@ -121,34 +198,23 @@ trait RpcSymbols { this: RpcMacroCommons => s"must be a subtype of specified base tag $baseTagTpe" reportProblem(msg) } - val whenUntaggedTagTree = taggedAnnot.map(_.findArg[Tree](WhenUntaggedArg, EmptyTree)).filter(_ != EmptyTree) - (requiredTagType, whenUntaggedTagTree) + val whenUntagged = FallbackTag(taggedAnnot.map(_.findArg[Tree](WhenUntaggedArg, EmptyTree)).getOrElse(EmptyTree)) + (requiredTagType, whenUntagged) } // returns fallback tag tree only IF it was necessary def matchTag(realRpcSymbol: RealRpcSymbol): Res[FallbackTag] = { val tagAnnot = findAnnotation(realRpcSymbol.symbol, baseTagTpe) - val fallbackTagTree = whenUntaggedTag orElse fallbackTag.annotTree - val realTagTpe = tagAnnot.map(_.tpe) orElse fallbackTagTree.map(_.tpe) getOrElse baseTagTpe - if (realTagTpe <:< requiredTag) - Ok(FallbackTag(fallbackTagTree.filter(_ => tagAnnot.isEmpty))) - else - Fail(s"it does not accept ${realRpcSymbol.shortDescription}s tagged with $realTagTpe") - } - } + val fallbackTagUsed = if (tagAnnot.isEmpty) whenUntaggedTag orElse fallbackTag else FallbackTag.Empty + val realTagTpe = tagAnnot.map(_.tpe).getOrElse(NoType) orElse fallbackTagUsed.annotTree.tpe orElse baseTagTpe - sealed trait RealRpcSymbol extends RpcSymbol { - def annot(tpe: Type, usedFallbackTag: FallbackTag): Option[Annot] = - findAnnotation(symbol, tpe, fallback = usedFallbackTag.annotTree.toList) - - def rpcName(usedFallbackTag: FallbackTag): String = { - val prefixes = allAnnotations(symbol, RpcNamePrefixAT, fallback = usedFallbackTag.annotTree.toList) - .map(_.findArg[String](RpcNamePrefixArg)) - val rpcName = annot(RpcNameAT, usedFallbackTag).fold(nameStr)(_.findArg[String](RpcNameArg)) - prefixes.mkString("", "", rpcName) + if (realTagTpe <:< requiredTag) Ok(fallbackTagUsed) + else Fail(s"it does not accept ${realRpcSymbol.shortDescription}s tagged with $realTagTpe") } } + sealed trait RealRpcSymbol extends RpcSymbol + abstract class RpcTrait(val symbol: Symbol) extends RpcSymbol { def tpe: Type @@ -200,10 +266,10 @@ trait RpcSymbols { this: RpcMacroCommons => // @unchecked because "The outer reference in this type test cannot be checked at runtime" // Srsly scalac, from static types it should be obvious that outer references are the same - def matchName(realRpcSymbol: RealRpcSymbol, fallbackTag: FallbackTag): Res[Unit] = arity match { + def matchName(matchedReal: MatchedRealSymbol[RealRpcSymbol]): Res[Unit] = arity match { case _: RpcArity.Single@unchecked | _: RpcArity.Optional@unchecked => - if (realRpcSymbol.rpcName(fallbackTag) == nameStr) Ok(()) - else Fail(s"it only matches ${realRpcSymbol.shortDescription}s with RPC name $nameStr") + if (matchedReal.rpcName == nameStr) Ok(()) + else Fail(s"it only matches ${matchedReal.real.shortDescription}s with RPC name $nameStr") case _: RpcArity.Multi@unchecked => Ok(()) } } @@ -316,32 +382,6 @@ trait RpcSymbols { this: RpcMacroCommons => def shortDescription = "real parameter" def description = s"$shortDescription $nameStr of ${owner.description}" - - def whenAbsent(fallbackTag: FallbackTag): Tree = - annot(WhenAbsentAT, fallbackTag).fold(EmptyTree) { annot => - val annotatedDefault = annot.tree.children.tail.head - if (!(annotatedDefault.tpe <:< actualType)) { - reportProblem(s"expected value of type $actualType in @whenAbsent annotation, got ${annotatedDefault.tpe.widen}") - } - val transformer = new Transformer { - override def transform(tree: Tree): Tree = tree match { - case Super(t@This(_), _) if !enclosingClasses.contains(t.symbol) => - reportProblem(s"illegal super-reference in @whenAbsent annotation") - case This(_) if tree.symbol == owner.owner.symbol => q"${owner.owner.safeName}" - case This(_) if !enclosingClasses.contains(tree.symbol) => - reportProblem(s"illegal this-reference in @whenAbsent annotation") - case t => super.transform(t) - } - } - transformer.transform(annotatedDefault) - } - - def defaultValue(useThis: Boolean): Tree = { - val prevListParams = owner.realParams.take(index - indexInList).map(rp => q"${rp.safeName}") - val prevListParamss = List(prevListParams).filter(_.nonEmpty) - val realInst = if (useThis) q"this" else q"${owner.owner.safeName}" - q"$realInst.${TermName(s"${owner.encodedNameStr}$$default$$${index + 1}")}(...$prevListParamss)" - } } case class RawMethod(owner: RawRpcTrait, symbol: Symbol) extends RpcMethod with RawRpcSymbol with AritySymbol { @@ -427,15 +467,7 @@ trait RpcSymbols { this: RpcMacroCommons => } } - val realParams: List[RealParam] = { - val result = paramLists.flatten - // result.groupBy(_.rpcName).foreach { - // case (_, head :: tail) if tail.nonEmpty => - // head.reportProblem(s"it has the same RPC name as ${tail.size} other parameters") - // case _ => - // } - result - } + val realParams: List[RealParam] = paramLists.flatten } case class RawRpcTrait(tpe: Type) extends RpcTrait(tpe.typeSymbol) with RawRpcSymbol { @@ -443,12 +475,12 @@ trait RpcSymbols { this: RpcMacroCommons => def description = s"$shortDescription $tpe" def baseTagTpe: Type = NothingTpe - def fallbackTag: FallbackTag = FallbackTag(None) + def fallbackTag: FallbackTag = FallbackTag.Empty val (baseMethodTag, fallbackMethodTag) = - annot(MethodTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag(None))) + annot(MethodTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag.Empty)) val (baseParamTag, fallbackParamTag) = - annot(ParamTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag(None))) + annot(ParamTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag.Empty)) lazy val rawMethods: List[RawMethod] = tpe.members.iterator.filter(m => m.isTerm && m.isAbstract).map(RawMethod(this, _)).toList @@ -460,15 +492,7 @@ trait RpcSymbols { this: RpcMacroCommons => def shortDescription = "real RPC" def description = s"$shortDescription $tpe" - lazy val realMethods: List[RealMethod] = { - val result = tpe.members.iterator.filter(m => m.isTerm && m.isAbstract).map(RealMethod(this, _)).toList - // result.groupBy(_.rpcName).foreach { - // case (_, head :: tail) if tail.nonEmpty => - // head.reportProblem(s"it has the same RPC name as ${tail.size} other methods - " + - // s"if you want to overload RPC methods, disambiguate them with @rpcName") - // case _ => - // } - result - } + lazy val realMethods: List[RealMethod] = + tpe.members.iterator.filter(m => m.isTerm && m.isAbstract).map(RealMethod(this, _)).toList } } From 0301bcf6ed7f2e58b7d6fc5b328fe902f140f510 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 14:21:34 +0200 Subject: [PATCH 54/91] fixed param uniqueness checking to exclude auxiliary raw params --- .../avsystem/commons/macros/rpc/RpcMappings.scala | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index 5e74a8f98..3104f7fc7 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -293,11 +293,13 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => paramMappingList.iterator.map(m => (m.rawParam, m)).toMap def ensureUniqueRpcNames(): Unit = - paramMappings.values.toList.flatMap(_.allMatchedParams).groupBy(_.rpcName).foreach { - case (rpcName, head :: tail) if tail.nonEmpty => - head.real.reportProblem(s"it has the same RPC name ($rpcName) as ${tail.size} other parameters") - case _ => - } + paramMappings.valuesIterator.filterNot(_.rawParam.auxiliary).toList + .flatMap(_.allMatchedParams).groupBy(_.rpcName) + .foreach { + case (rpcName, head :: tail) if tail.nonEmpty => + head.real.reportProblem(s"it has the same RPC name ($rpcName) as ${tail.size} other parameters") + case _ => + } private def rawValueTree(rawParam: RawParam): Tree = rawParam match { case _: MethodNameParam => q"$rpcName" From 479b2b379ac7ecedf86552ef90271086684f2e9a Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 16:21:16 +0200 Subject: [PATCH 55/91] introduced machinery for injecting custom implicits into RPC companions --- .../com/avsystem/commons/rest/RawRest.scala | 54 +++++++++++++---- .../commons/rest/RestApiCompanion.scala | 44 ++++++++++---- .../com/avsystem/commons/rest/data.scala | 30 +++------- .../commons/rpc/RpcMacroInstances.scala | 22 +++++++ .../commons/macros/rest/RestMacros.scala | 39 ------------ .../commons/macros/rpc/RpcMacros.scala | 59 +++++++++++++++++++ 6 files changed, 161 insertions(+), 87 deletions(-) create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala delete mode 100644 commons-macros/src/main/scala/com/avsystem/commons/macros/rest/RestMacros.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 99ad941e4..484fe5fed 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -104,19 +104,49 @@ object RawRest extends RawRpcCompanion[RawRest] { } } - trait ClientMacroInstances[Real] { - def metadata: RestMetadata[Real] - def asReal: AsRealRpc[Real] + trait ClientInstances[Real, I] extends RpcMacroInstances[Real] { + type Implicits = I + def metadata(implicits: I): RestMetadata[Real] + def asReal(implicits: I): AsRealRpc[Real] } - - trait ServerMacroInstances[Real] { - def metadata: RestMetadata[Real] - def asRaw: AsRawRpc[Real] + trait ServerInstances[Real, I] extends RpcMacroInstances[Real] { + type Implicits = I + def metadata(implicits: I): RestMetadata[Real] + def asRaw(implicits: I): AsRawRpc[Real] + } + trait FullInstances[Real, I] extends RpcMacroInstances[Real] { + type Implicits = I + def metadata(implicits: I): RestMetadata[Real] + def asRawReal(implicits: I): AsRawRealRpc[Real] } - trait FullMacroInstances[Real] extends ClientMacroInstances[Real] with ServerMacroInstances[Real] - - implicit def clientInstances[Real]: ClientMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] - implicit def serverInstances[Real]: ServerMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] - implicit def fullInstances[Real]: FullMacroInstances[Real] = macro macros.rest.RestMacros.instances[Real] + // I have no idea why I can't just create one macro in `RpcMacroInstances` companion to rule them all + implicit def clientInstances[Real, I]: ClientInstances[Real, I] = + macro macros.rpc.RpcMacros.macroInstances[ClientInstances[Real, I], Real] + implicit def serverInstances[Real, I]: ServerInstances[Real, I] = + macro macros.rpc.RpcMacros.macroInstances[ServerInstances[Real, I], Real] + implicit def clientServerInstances[Real, I]: FullInstances[Real, I] = + macro macros.rpc.RpcMacros.macroInstances[FullInstances[Real, I], Real] + + /** @see [[FullApiCompanion]]*/ + abstract class ClientApiCompanion[Real, I](implicit instances: ClientInstances[Real, I]) { this: I => + implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) + implicit final lazy val restAsReal: AsRealRpc[Real] = instances.asReal(this) + } + /** @see [[FullApiCompanion]]*/ + abstract class ServerApiCompanion[Real, I](implicit instances: ServerInstances[Real, I]) { this: I => + implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) + implicit final lazy val restAsRaw: AsRawRpc[Real] = instances.asRaw(this) + } + /** + * Base class for REST trait companions. Reduces boilerplate needed in order to define appropriate instances + * of `AsRawReal` and `RestMetadata` for given trait. The `I` type parameter lets you inject additional implicits + * into macro materialization of these instances, e.g. [[DefaultRestImplicits]]. + * Usually, for even less boilerplate, this base class is extended by yet another abstract class which fixes + * the `I` type, e.g. [[RestApiCompanion]]. + */ + abstract class FullApiCompanion[Real, I](implicit instances: FullInstances[Real, I]) { this: I => + implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) + implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = instances.asRawReal(this) + } } diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala index 6b6e026bc..1fa7dfa69 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala @@ -1,27 +1,45 @@ package com.avsystem.commons package rest +import com.avsystem.commons.rpc.AsRawReal +import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput} +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} + /** - * Base class for companions of REST API traits used only for REST clients to external services. + * Defines [[GenCodec]] and [[GenKeyCodec]] based serialization for REST API traits. */ -abstract class RestClientApiCompanion[Real](implicit instances: RawRest.ClientMacroInstances[Real]) { - implicit def restMetadata: RestMetadata[Real] = instances.metadata - implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal +trait DefaultRestImplicits { + implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = + AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = + AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[QueryValue, T] = + AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) + implicit def jsonValueDefaultAsRealRaw[T: GenCodec]: AsRawReal[JsonValue, T] = + AsRawReal.create(v => JsonValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) } +object DefaultRestImplicits extends DefaultRestImplicits + +/** + * Base class for companions of REST API traits used only for REST clients to external services. + * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. + */ +abstract class RestClientApiCompanion[Real]( + implicit instances: RawRest.ClientInstances[Real, DefaultRestImplicits] +) extends RawRest.ClientApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits /** * Base class for companions of REST API traits used only for REST servers exposed to external world. + * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. */ -abstract class RestServerApiCompanion[Real](implicit instances: RawRest.ServerMacroInstances[Real]) { - implicit def restMetadata: RestMetadata[Real] = instances.metadata - implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw -} +abstract class RestServerApiCompanion[Real]( + implicit instances: RawRest.ServerInstances[Real, DefaultRestImplicits] +) extends RawRest.ServerApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits /** * Base class for companions of REST API traits used for both REST clients and servers. + * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. */ -abstract class RestApiCompanion[Real](implicit instances: RawRest.FullMacroInstances[Real]) { - implicit def restMetadata: RestMetadata[Real] = instances.metadata - implicit def rawRestAsReal: RawRest.AsRealRpc[Real] = instances.asReal - implicit def realAsRawRest: RawRest.AsRawRpc[Real] = instances.asRaw -} +abstract class RestApiCompanion[Real]( + implicit instances: RawRest.FullInstances[Real, DefaultRestImplicits] +) extends RawRest.FullApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala index 0a66d0492..a0cf0d19a 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala @@ -5,7 +5,6 @@ import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, import com.avsystem.commons.rpc._ import com.avsystem.commons.serialization.GenCodec.ReadFailure import com.avsystem.commons.serialization.json.{JsonReader, JsonStringInput, JsonStringOutput} -import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} import scala.util.control.NoStackTrace @@ -14,8 +13,7 @@ sealed trait RestValue extends Any { } /** - * Value used as encoding of [[Path]] parameters. Types that have `GenKeyCodec` instance have automatic encoding - * to [[PathValue]]. + * Value used as encoding of [[Path]] parameters. */ case class PathValue(value: String) extends AnyVal with RestValue object PathValue { @@ -24,39 +22,25 @@ object PathValue { } /** - * Value used as encoding of [[Header]] parameters. Types that have `GenKeyCodec` instance have automatic encoding - * to [[HeaderValue]]. + * Value used as encoding of [[Header]] parameters. */ case class HeaderValue(value: String) extends AnyVal with RestValue /** - * Value used as encoding of [[Query]] parameters. Types that have `GenKeyCodec` instance have automatic encoding - * to [[QueryValue]]. + * Value used as encoding of [[Query]] parameters. */ case class QueryValue(value: String) extends AnyVal with RestValue /** - * Value used as encoding of [[JsonBodyParam]] parameters. Types that have `GenCodec` instance have automatic encoding - * to [[JsonValue]]. + * Value used as encoding of [[JsonBodyParam]] parameters. */ case class JsonValue(value: String) extends AnyVal with RestValue -object RestValue { - implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = - AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = - AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[QueryValue, T] = - AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def jsonValueDefaultAsRealRaw[T: GenCodec]: AsRawReal[JsonValue, T] = - AsRawReal.create(v => JsonValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) -} - /** * Value used to represent HTTP body. Also used as direct encoding of [[Body]] parameters. Types that have - * encoding to [[JsonValue]] (e.g. types that have `GenCodec` instance) automatically have encoding to [[HttpBody]] - * which uses application/json MIME type. There is also a specialized encoding provided for `Unit` which maps it - * to empty HTTP body instead of JSON containing "null". + * encoding to [[JsonValue]] automatically have encoding to [[HttpBody]] which uses application/json MIME type. + * There is also a specialized encoding provided for `Unit` which returns empty HTTP body when writing and ignores + * the body when reading. */ sealed trait HttpBody { def contentOpt: Opt[String] = this match { diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala new file mode 100644 index 000000000..ce30e9ca3 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala @@ -0,0 +1,22 @@ +package com.avsystem.commons +package rpc + +/** + * Base for traits that aggregate multiple RPC-related typeclass instances (e.g. `AsReal` + metadata). + * Typically such aggregating type is declared as an implicit constructor parameter of an abstract class that + * serves as a base class for RPC trait companions. Example: [[com.avsystem.commons.rest.RestApiCompanion]]. + * This is all in order to reduce boilerplate needed to define an RPC trait. + * + * A trait that extends `RpcMacroInstances[Real]` must declare abstract methods, each method taking exactly + * one parameter of type `Implicits` and returning either an instance of `AsReal`, `AsRaw`, `AsRawReal` or + * some RPC metadata class for RPC trait `Real`. + * + * If instances-trait is defined according to these rules, `RpcMacros.macroInstances` will be able to + * automatically materialize an implementation using `AsReal.materializeForRpc`, `AsRaw.materializeForRpc`, + * `AsRawReal.materializeForRpc` and `RpcMetadata.materializeForRpc`. The `Implicits` parameter taken by every method + * serves to inject additional implicits into macro-materialization. The `RpcMacros.macroInstances` macro will import + * contents of that parameter inside every method implementation. + */ +trait RpcMacroInstances[Real] { + type Implicits +} diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rest/RestMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rest/RestMacros.scala deleted file mode 100644 index 991166aaf..000000000 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rest/RestMacros.scala +++ /dev/null @@ -1,39 +0,0 @@ -package com.avsystem.commons -package macros.rest - -import com.avsystem.commons.macros.AbstractMacroCommons - -import scala.reflect.macros.blackbox - -class RestMacros(val ctx: blackbox.Context) extends AbstractMacroCommons(ctx) { - - import c.universe._ - - val RestPkg: Tree = q"$CommonsPkg.rest" - val RawRestObj: Tree = q"$RestPkg.RawRest" - val RestMetadataObj: Tree = q"$RestPkg.RestMetadata" - val RestMetadataCls: Tree = tq"$RestPkg.RestMetadata" - - def instances[Real: WeakTypeTag]: Tree = { - val realTpe = weakTypeOf[Real] - val instancesTpe = c.macroApplication.tpe - - val asRawTpe: Type = getType(tq"$RawRestObj.AsRawRpc[$realTpe]") - val asRealTpe: Type = getType(tq"$RawRestObj.AsRealRpc[$realTpe]") - val asRawRealTpe: Type = getType(tq"$RawRestObj.AsRawRealRpc[$realTpe]") - val metadataTpe: Type = getType(tq"$RestMetadataCls[$realTpe]") - - val memberDefs = instancesTpe.members.iterator.filter(m => m.isAbstract && m.isMethod).map { m => - val resultTpe = m.typeSignatureIn(instancesTpe).finalResultType - val body = - if (resultTpe <:< asRawRealTpe) q"$RawRestObj.materializeAsRawReal[$realTpe]" - else if (resultTpe <:< asRawTpe) q"$RawRestObj.materializeAsRaw[$realTpe]" - else if (resultTpe <:< asRealTpe) q"$RawRestObj.materializeAsReal[$realTpe]" - else if (resultTpe <:< metadataTpe) q"$RestMetadataObj.materializeForRpc[$realTpe]" - else abort(s"Unexpected result type: $resultTpe") - q"lazy val ${m.name.toTermName} = $body" - }.toList - - q"new $instancesTpe { ..$memberDefs }" - } -} diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index 876ff1389..6ebfd4b96 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -21,6 +21,8 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo val CanBuildFromCls = tq"$CollectionPkg.generic.CanBuildFrom" val ParamPositionObj = q"$RpcPackage.ParamPosition" + val AsRealTpe: Type = getType(tq"$AsRealCls[_,_]") + val AsRawTpe: Type = getType(tq"$AsRawCls[_,_]") val RpcNameAT: Type = getType(tq"$RpcPackage.rpcName") val RpcNameArg: Symbol = RpcNameAT.member(TermName("name")) val RpcNamePrefixAT: Type = getType(tq"$RpcPackage.rpcNamePrefix") @@ -179,6 +181,63 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) } } + def macroInstances[I: WeakTypeTag, Real: WeakTypeTag]: Tree = { + val realTpe = weakTypeOf[Real] + + val TypeApply(_, macroTypeArgs) = c.macroApplication + val macroTparams = c.macroApplication.symbol.typeSignature.typeParams + val macroTargsMap: Map[Name, Type] = + (macroTypeArgs zip macroTparams).map { case (targ, sym) => (sym.name, targ.tpe) }.toMap + + // Scala... srsly... why? + val instancesTpe = weakTypeOf[I].dealias.map { + case t@TypeRef(NoPrefix, sym, Nil) => macroTargsMap.getOrElse(sym.name, t) + case t => t + } + + val asRawTpe = getType(tq"$AsRawCls[_,$realTpe]") + val asRealTpe = getType(tq"$AsRealCls[_,$realTpe]") + val asRawRealTpe = getType(tq"$AsRawRealCls[_,$realTpe]") + + val instTs = instancesTpe.typeSymbol + if (!(instTs.isClass && instTs.isAbstract)) { + abort(s"Expected trait or abstract class type, got $instancesTpe") + } + + val implicitsTmem = instancesTpe.member(TypeName("Implicits")) + if (implicitsTmem.isAbstract) { + abort(s"Could not determine concrete Implicits type for $instancesTpe") + } + val implicitsTpe = implicitsTmem.typeSignatureIn(instancesTpe) + + val impls = instancesTpe.members.iterator.filter(m => m.isAbstract && m.isMethod).map { m => + val sig = m.typeSignatureIn(instancesTpe) + val body = (sig.typeParams, sig.paramLists) match { + case (Nil, List(List(single))) if single.typeSignature =:= implicitsTpe => + val resultTpe = sig.finalResultType.dealias + if (resultTpe <:< asRawRealTpe) + q"$AsRawRealObj.materializeForRpc[..${resultTpe.typeArgs}]" + else if (resultTpe <:< asRawTpe) + q"$AsRawObj.materializeForRpc[..${resultTpe.typeArgs}]" + else if (resultTpe <:< asRealTpe) + q"$AsRealObj.materializeForRpc[..${resultTpe.typeArgs}]" + else resultTpe.typeArgs match { + case List(st) if st =:= realTpe => + q"$RpcPackage.RpcMetadata.materializeForRpc[${resultTpe.typeConstructor}, $realTpe]" + case _ => abort(s"Bad result type $resultTpe of $m: " + + s"it must be an AsReal/AsRaw/AsRealRaw or RPC metadata instance for $realTpe") + } + case _ => + abort(s"Problem with $m: expected non-generic method that takes exactly one parameter of type $implicitsTpe") + } + + val implicitsName = c.freshName(TermName("implicits")) + q"def ${m.name.toTermName}($implicitsName: $implicitsTpe) = { import $implicitsName._; $body }" + } + + q"new $instancesTpe { ..${impls.toList}; () }" + } + def lazyMetadata(metadata: Tree): Tree = q"${c.prefix}($metadata)" } From e85ab6dd465f213a94c6c69067cc94d8dc09d021 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 16:26:40 +0200 Subject: [PATCH 56/91] disallowed materialization of RpcMacroInstances outside of trait's file --- .../scala/com/avsystem/commons/macros/rpc/RpcMacros.scala | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index 6ebfd4b96..db01b7336 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -184,6 +184,10 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) def macroInstances[I: WeakTypeTag, Real: WeakTypeTag]: Tree = { val realTpe = weakTypeOf[Real] + if (c.macroApplication.symbol.isImplicit && c.enclosingPosition.source != realTpe.typeSymbol.pos.source) { + abort(s"Implicit materialization of RpcMacroInstances is only allowed in the same file where RPC trait is defined") + } + val TypeApply(_, macroTypeArgs) = c.macroApplication val macroTparams = c.macroApplication.symbol.typeSignature.typeParams val macroTargsMap: Map[Name, Type] = From 2746a4908e4dbe7ab58950b5fab30305b07b79ec Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 17:04:51 +0200 Subject: [PATCH 57/91] refactored REST macro instances and companions out of RawRest --- .../com/avsystem/commons/rest/RawRest.scala | 46 ------------- .../commons/rest/RestApiCompanion.scala | 65 +++++++++++++++++-- 2 files changed, 58 insertions(+), 53 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 484fe5fed..0c9d2c123 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -103,50 +103,4 @@ object RawRest extends RawRpcCompanion[RawRest] { handleRequest(RestRequest(methodMeta.method, newHeaders, body)) } } - - trait ClientInstances[Real, I] extends RpcMacroInstances[Real] { - type Implicits = I - def metadata(implicits: I): RestMetadata[Real] - def asReal(implicits: I): AsRealRpc[Real] - } - trait ServerInstances[Real, I] extends RpcMacroInstances[Real] { - type Implicits = I - def metadata(implicits: I): RestMetadata[Real] - def asRaw(implicits: I): AsRawRpc[Real] - } - trait FullInstances[Real, I] extends RpcMacroInstances[Real] { - type Implicits = I - def metadata(implicits: I): RestMetadata[Real] - def asRawReal(implicits: I): AsRawRealRpc[Real] - } - - // I have no idea why I can't just create one macro in `RpcMacroInstances` companion to rule them all - implicit def clientInstances[Real, I]: ClientInstances[Real, I] = - macro macros.rpc.RpcMacros.macroInstances[ClientInstances[Real, I], Real] - implicit def serverInstances[Real, I]: ServerInstances[Real, I] = - macro macros.rpc.RpcMacros.macroInstances[ServerInstances[Real, I], Real] - implicit def clientServerInstances[Real, I]: FullInstances[Real, I] = - macro macros.rpc.RpcMacros.macroInstances[FullInstances[Real, I], Real] - - /** @see [[FullApiCompanion]]*/ - abstract class ClientApiCompanion[Real, I](implicit instances: ClientInstances[Real, I]) { this: I => - implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) - implicit final lazy val restAsReal: AsRealRpc[Real] = instances.asReal(this) - } - /** @see [[FullApiCompanion]]*/ - abstract class ServerApiCompanion[Real, I](implicit instances: ServerInstances[Real, I]) { this: I => - implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) - implicit final lazy val restAsRaw: AsRawRpc[Real] = instances.asRaw(this) - } - /** - * Base class for REST trait companions. Reduces boilerplate needed in order to define appropriate instances - * of `AsRawReal` and `RestMetadata` for given trait. The `I` type parameter lets you inject additional implicits - * into macro materialization of these instances, e.g. [[DefaultRestImplicits]]. - * Usually, for even less boilerplate, this base class is extended by yet another abstract class which fixes - * the `I` type, e.g. [[RestApiCompanion]]. - */ - abstract class FullApiCompanion[Real, I](implicit instances: FullInstances[Real, I]) { this: I => - implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) - implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = instances.asRawReal(this) - } } diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala index 1fa7dfa69..3fbe76302 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala @@ -1,10 +1,61 @@ package com.avsystem.commons package rest -import com.avsystem.commons.rpc.AsRawReal +import com.avsystem.commons.rest.RawRest.{AsRawRealRpc, AsRawRpc, AsRealRpc} +import com.avsystem.commons.rpc.{AsRawReal, RpcMacroInstances} import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput} import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} +trait ClientInstances[Real, I] extends RpcMacroInstances[Real] { + type Implicits = I + def metadata(implicits: I): RestMetadata[Real] + def asReal(implicits: I): AsRealRpc[Real] +} +object ClientInstances { + implicit def materialize[Real, I]: ClientInstances[Real, I] = + macro macros.rpc.RpcMacros.macroInstances[ClientInstances[Real, I], Real] +} +trait ServerInstances[Real, I] extends RpcMacroInstances[Real] { + type Implicits = I + def metadata(implicits: I): RestMetadata[Real] + def asRaw(implicits: I): AsRawRpc[Real] +} +object ServerInstances { + implicit def materialize[Real, I]: ServerInstances[Real, I] = + macro macros.rpc.RpcMacros.macroInstances[ServerInstances[Real, I], Real] +} +trait FullInstances[Real, I] extends RpcMacroInstances[Real] { + type Implicits = I + def metadata(implicits: I): RestMetadata[Real] + def asRawReal(implicits: I): AsRawRealRpc[Real] +} +object FullInstances { + implicit def clientServerInstances[Real, I]: FullInstances[Real, I] = + macro macros.rpc.RpcMacros.macroInstances[FullInstances[Real, I], Real] +} + +/** @see [[FullApiCompanion]] */ +abstract class ClientApiCompanion[Real, I](implicit instances: ClientInstances[Real, I]) { this: I => + implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) + implicit final lazy val restAsReal: AsRealRpc[Real] = instances.asReal(this) +} +/** @see [[FullApiCompanion]] */ +abstract class ServerApiCompanion[Real, I](implicit instances: ServerInstances[Real, I]) { this: I => + implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) + implicit final lazy val restAsRaw: AsRawRpc[Real] = instances.asRaw(this) +} +/** + * Base class for REST trait companions. Reduces boilerplate needed in order to define appropriate instances + * of `AsRawReal` and `RestMetadata` for given trait. The `I` type parameter lets you inject additional implicits + * into macro materialization of these instances, e.g. [[DefaultRestImplicits]]. + * Usually, for even less boilerplate, this base class is extended by yet another abstract class which fixes + * the `I` type, e.g. [[RestApiCompanion]]. + */ +abstract class FullApiCompanion[Real, I](implicit instances: FullInstances[Real, I]) { this: I => + implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) + implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = instances.asRawReal(this) +} + /** * Defines [[GenCodec]] and [[GenKeyCodec]] based serialization for REST API traits. */ @@ -25,21 +76,21 @@ object DefaultRestImplicits extends DefaultRestImplicits * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. */ abstract class RestClientApiCompanion[Real]( - implicit instances: RawRest.ClientInstances[Real, DefaultRestImplicits] -) extends RawRest.ClientApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits + implicit instances: ClientInstances[Real, DefaultRestImplicits] +) extends ClientApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits /** * Base class for companions of REST API traits used only for REST servers exposed to external world. * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. */ abstract class RestServerApiCompanion[Real]( - implicit instances: RawRest.ServerInstances[Real, DefaultRestImplicits] -) extends RawRest.ServerApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits + implicit instances: ServerInstances[Real, DefaultRestImplicits] +) extends ServerApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits /** * Base class for companions of REST API traits used for both REST clients and servers. * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. */ abstract class RestApiCompanion[Real]( - implicit instances: RawRest.FullInstances[Real, DefaultRestImplicits] -) extends RawRest.FullApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits + implicit instances: FullInstances[Real, DefaultRestImplicits] +) extends FullApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits From 75819605234e47733f3376e71622d2ec53d9c6dc Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 17:07:50 +0200 Subject: [PATCH 58/91] cosmetic --- .../main/scala/com/avsystem/commons/rest/RawRest.scala | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 0c9d2c123..e9a62a1cb 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -89,12 +89,8 @@ object RawRest extends RawRpcCompanion[RawRest] { def get(name: String, headers: RestHeaders): Future[RestResponse] = handleSingle(name, headers, HttpBody.Empty) - def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Future[RestResponse] = { - val methodMeta = metadata.httpMethods.getOrElse(name, - throw new IllegalArgumentException(s"no such HTTP method: $name")) - val newHeaders = prefixHeaders.append(methodMeta, headers) - handleRequest(RestRequest(methodMeta.method, newHeaders, HttpBody.createJsonBody(body))) - } + def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Future[RestResponse] = + handleSingle(name, headers, HttpBody.createJsonBody(body)) def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Future[RestResponse] = { val methodMeta = metadata.httpMethods.getOrElse(name, From 5c3a35037b29c57243d92f856576a8ccae9a72c0 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 17:51:54 +0200 Subject: [PATCH 59/91] scaladoc fix --- .../avsystem/commons/rest/RestApiCompanion.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala index 3fbe76302..c1b28b60c 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala @@ -34,12 +34,12 @@ object FullInstances { macro macros.rpc.RpcMacros.macroInstances[FullInstances[Real, I], Real] } -/** @see [[FullApiCompanion]] */ +/** @see [[FullApiCompanion]]*/ abstract class ClientApiCompanion[Real, I](implicit instances: ClientInstances[Real, I]) { this: I => implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) implicit final lazy val restAsReal: AsRealRpc[Real] = instances.asReal(this) } -/** @see [[FullApiCompanion]] */ +/** @see [[FullApiCompanion]]*/ abstract class ServerApiCompanion[Real, I](implicit instances: ServerInstances[Real, I]) { this: I => implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) implicit final lazy val restAsRaw: AsRawRpc[Real] = instances.asRaw(this) @@ -57,7 +57,8 @@ abstract class FullApiCompanion[Real, I](implicit instances: FullInstances[Real, } /** - * Defines [[GenCodec]] and [[GenKeyCodec]] based serialization for REST API traits. + * Defines [[com.avsystem.commons.serialization.GenCodec GenCodec]] and + * [[com.avsystem.commons.serialization.GenKeyCodec GenKeyCodec]] based serialization for REST API traits. */ trait DefaultRestImplicits { implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = @@ -73,7 +74,7 @@ object DefaultRestImplicits extends DefaultRestImplicits /** * Base class for companions of REST API traits used only for REST clients to external services. - * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. + * Injects `GenCodec` and `GenKeyCodec` based serialization. */ abstract class RestClientApiCompanion[Real]( implicit instances: ClientInstances[Real, DefaultRestImplicits] @@ -81,7 +82,7 @@ abstract class RestClientApiCompanion[Real]( /** * Base class for companions of REST API traits used only for REST servers exposed to external world. - * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. + * Injects `GenCodec` and `GenKeyCodec` based serialization. */ abstract class RestServerApiCompanion[Real]( implicit instances: ServerInstances[Real, DefaultRestImplicits] @@ -89,7 +90,7 @@ abstract class RestServerApiCompanion[Real]( /** * Base class for companions of REST API traits used for both REST clients and servers. - * Injects [[GenCodec]] and [[GenKeyCodec]] based serialization. + * Injects `GenCodec` and `GenKeyCodec` based serialization. */ abstract class RestApiCompanion[Real]( implicit instances: FullInstances[Real, DefaultRestImplicits] From 885eeaf1ee5d27b33a2c8135123622e92cecd7e3 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 17:56:02 +0200 Subject: [PATCH 60/91] changing mima config --- build.sbt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index b56400e14..53ff33127 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ cancelable in Global := true val forIdeaImport = System.getProperty("idea.managed", "false").toBoolean && System.getProperty("idea.runid") == null // for binary compatibility checking -val previousVersion = "1.27.3" +val previousCompatibleVersions = Set.empty[String] val silencerVersion = "1.1" val guavaVersion = "23.0" @@ -100,8 +100,8 @@ val jvmCommonSettings = commonSettings ++ Seq( libraryDependencies ++= Seq( "org.apache.commons" % "commons-io" % commonsIoVersion % Test, ), - mimaPreviousArtifacts := { - Set(organization.value % s"${name.value}_${scalaBinaryVersion.value}" % previousVersion) + mimaPreviousArtifacts := previousCompatibleVersions.map { previousVersion => + organization.value % s"${name.value}_${scalaBinaryVersion.value}" % previousVersion }, ) From 529b478154a5b8b3a145cf8f8d85742e8aafc73d Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 17 Jul 2018 18:02:43 +0200 Subject: [PATCH 61/91] renaming companions --- ...on.scala => DefaultRestApiCompanion.scala} | 24 +++++++++---------- .../commons/rpc/RpcMacroInstances.scala | 2 +- .../commons/rest/AbstractRestCallTest.scala | 4 ++-- .../avsystem/commons/rest/RawRestTest.scala | 4 ++-- .../avsystem/commons/jetty/rest/SomeApi.scala | 4 ++-- .../commons/jetty/rest/examples/UserApi.scala | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) rename commons-core/src/main/scala/com/avsystem/commons/rest/{RestApiCompanion.scala => DefaultRestApiCompanion.scala} (83%) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala similarity index 83% rename from commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala rename to commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala index c1b28b60c..7fc31c5b5 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestApiCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala @@ -34,13 +34,13 @@ object FullInstances { macro macros.rpc.RpcMacros.macroInstances[FullInstances[Real, I], Real] } -/** @see [[FullApiCompanion]]*/ -abstract class ClientApiCompanion[Real, I](implicit instances: ClientInstances[Real, I]) { this: I => +/** @see [[RestApiCompanion]] */ +abstract class RestClientApiCompanion[Real, I](implicit instances: ClientInstances[Real, I]) { this: I => implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) implicit final lazy val restAsReal: AsRealRpc[Real] = instances.asReal(this) } -/** @see [[FullApiCompanion]]*/ -abstract class ServerApiCompanion[Real, I](implicit instances: ServerInstances[Real, I]) { this: I => +/** @see [[RestApiCompanion]] */ +abstract class RestServerApiCompanion[Real, I](implicit instances: ServerInstances[Real, I]) { this: I => implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) implicit final lazy val restAsRaw: AsRawRpc[Real] = instances.asRaw(this) } @@ -49,9 +49,9 @@ abstract class ServerApiCompanion[Real, I](implicit instances: ServerInstances[R * of `AsRawReal` and `RestMetadata` for given trait. The `I` type parameter lets you inject additional implicits * into macro materialization of these instances, e.g. [[DefaultRestImplicits]]. * Usually, for even less boilerplate, this base class is extended by yet another abstract class which fixes - * the `I` type, e.g. [[RestApiCompanion]]. + * the `I` type, e.g. [[DefaultRestApiCompanion]]. */ -abstract class FullApiCompanion[Real, I](implicit instances: FullInstances[Real, I]) { this: I => +abstract class RestApiCompanion[Real, I](implicit instances: FullInstances[Real, I]) { this: I => implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = instances.asRawReal(this) } @@ -76,22 +76,22 @@ object DefaultRestImplicits extends DefaultRestImplicits * Base class for companions of REST API traits used only for REST clients to external services. * Injects `GenCodec` and `GenKeyCodec` based serialization. */ -abstract class RestClientApiCompanion[Real]( +abstract class DefaultRestClientApiCompanion[Real]( implicit instances: ClientInstances[Real, DefaultRestImplicits] -) extends ClientApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits +) extends RestClientApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits /** * Base class for companions of REST API traits used only for REST servers exposed to external world. * Injects `GenCodec` and `GenKeyCodec` based serialization. */ -abstract class RestServerApiCompanion[Real]( +abstract class DefaultRestServerApiCompanion[Real]( implicit instances: ServerInstances[Real, DefaultRestImplicits] -) extends ServerApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits +) extends RestServerApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits /** * Base class for companions of REST API traits used for both REST clients and servers. * Injects `GenCodec` and `GenKeyCodec` based serialization. */ -abstract class RestApiCompanion[Real]( +abstract class DefaultRestApiCompanion[Real]( implicit instances: FullInstances[Real, DefaultRestImplicits] -) extends FullApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits +) extends RestApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala index ce30e9ca3..d569edcb1 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala @@ -4,7 +4,7 @@ package rpc /** * Base for traits that aggregate multiple RPC-related typeclass instances (e.g. `AsReal` + metadata). * Typically such aggregating type is declared as an implicit constructor parameter of an abstract class that - * serves as a base class for RPC trait companions. Example: [[com.avsystem.commons.rest.RestApiCompanion]]. + * serves as a base class for RPC trait companions. Example: [[com.avsystem.commons.rest.DefaultRestApiCompanion]]. * This is all in order to reduce boilerplate needed to define an RPC trait. * * A trait that extends `RpcMacroInstances[Real]` must declare abstract methods, each method taking exactly diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala index 850c30150..b790b9641 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala @@ -38,7 +38,7 @@ trait RestTestApi { @Query q0: String ): RestTestSubApi } -object RestTestApi extends RestApiCompanion[RestTestApi] { +object RestTestApi extends DefaultRestApiCompanion[RestTestApi] { val Impl: RestTestApi = new RestTestApi { def trivialGet: Future[Unit] = Future.unit def failingGet: Future[Unit] = Future.failed(HttpErrorException(503, "nie")) @@ -56,7 +56,7 @@ object RestTestApi extends RestApiCompanion[RestTestApi] { trait RestTestSubApi { @GET def subget(@Path p1: Int, @Header("X-H1") h1: Int, q1: Int): Future[String] } -object RestTestSubApi extends RestApiCompanion[RestTestSubApi] { +object RestTestSubApi extends DefaultRestApiCompanion[RestTestSubApi] { def impl(arg: String): RestTestSubApi = new RestTestSubApi { def subget(p1: Int, h1: Int, q1: Int): Future[String] = Future.successful(s"$arg-$p1-$h1-$q1") } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index 386fc869b..88adc323e 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -22,13 +22,13 @@ trait UserApi { def autopost(bodyarg: String): Future[String] def singleBodyAutopost(@Body body: String): Future[String] } -object UserApi extends RestApiCompanion[UserApi] +object UserApi extends DefaultRestApiCompanion[UserApi] trait RootApi { @Prefix("") def self: UserApi def subApi(id: Int, @Query query: String): UserApi } -object RootApi extends RestApiCompanion[RootApi] +object RootApi extends DefaultRestApiCompanion[RootApi] class RawRestTest extends FunSuite with ScalaFutures { def repr(body: HttpBody, inNewLine: Boolean = true): String = body match { diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala index 4afd84a29..e7fc85f8f 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala @@ -1,7 +1,7 @@ package com.avsystem.commons package jetty.rest -import com.avsystem.commons.rest.{GET, POST, RawRest, RestApiCompanion} +import com.avsystem.commons.rest.{GET, POST, RawRest, DefaultRestApiCompanion} trait SomeApi { @GET @@ -11,7 +11,7 @@ trait SomeApi { def helloThere(who: String): Future[String] } -object SomeApi extends RestApiCompanion[SomeApi] { +object SomeApi extends DefaultRestApiCompanion[SomeApi] { def asHandleRequest(real: SomeApi): RawRest.HandleRequest = RawRest.asHandleRequest(real) diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala index a978f0248..e7dd7598d 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala @@ -6,4 +6,4 @@ import com.avsystem.commons.rest._ trait UserApi { @GET def getUsername(userId: String): Future[String] } -object UserApi extends RestApiCompanion[UserApi] \ No newline at end of file +object UserApi extends DefaultRestApiCompanion[UserApi] \ No newline at end of file From c5afd4152cd50389deb65b9b7b5d509a9b64cf27 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 18 Jul 2018 10:44:54 +0200 Subject: [PATCH 62/91] simplified RpcMacroInstances machinery --- .../rest/DefaultRestApiCompanion.scala | 78 ++++++++--------- .../commons/rpc/RpcMacroInstances.scala | 74 +++++++++++++---- .../commons/macros/rpc/RpcMacros.scala | 83 +++++++++---------- docs/REST.md | 2 +- 4 files changed, 134 insertions(+), 103 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala index 7fc31c5b5..a9815ac0a 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala @@ -6,54 +6,47 @@ import com.avsystem.commons.rpc.{AsRawReal, RpcMacroInstances} import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput} import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} -trait ClientInstances[Real, I] extends RpcMacroInstances[Real] { - type Implicits = I - def metadata(implicits: I): RestMetadata[Real] - def asReal(implicits: I): AsRealRpc[Real] +trait ClientInstances[Real] { + def metadata: RestMetadata[Real] + def asReal: AsRealRpc[Real] } -object ClientInstances { - implicit def materialize[Real, I]: ClientInstances[Real, I] = - macro macros.rpc.RpcMacros.macroInstances[ClientInstances[Real, I], Real] +trait ServerInstances[Real] { + def metadata: RestMetadata[Real] + def asRaw: AsRawRpc[Real] } -trait ServerInstances[Real, I] extends RpcMacroInstances[Real] { - type Implicits = I - def metadata(implicits: I): RestMetadata[Real] - def asRaw(implicits: I): AsRawRpc[Real] -} -object ServerInstances { - implicit def materialize[Real, I]: ServerInstances[Real, I] = - macro macros.rpc.RpcMacros.macroInstances[ServerInstances[Real, I], Real] -} -trait FullInstances[Real, I] extends RpcMacroInstances[Real] { - type Implicits = I - def metadata(implicits: I): RestMetadata[Real] - def asRawReal(implicits: I): AsRawRealRpc[Real] -} -object FullInstances { - implicit def clientServerInstances[Real, I]: FullInstances[Real, I] = - macro macros.rpc.RpcMacros.macroInstances[FullInstances[Real, I], Real] +trait FullInstances[Real] { + def metadata: RestMetadata[Real] + def asRawReal: AsRawRealRpc[Real] } /** @see [[RestApiCompanion]] */ -abstract class RestClientApiCompanion[Real, I](implicit instances: ClientInstances[Real, I]) { this: I => - implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) - implicit final lazy val restAsReal: AsRealRpc[Real] = instances.asReal(this) +abstract class RestClientApiCompanion[Implicits, Real](implicits: Implicits)( + implicit inst: RpcMacroInstances[Implicits, ClientInstances, Real] +) { + implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits).metadata + implicit final lazy val restAsReal: AsRealRpc[Real] = inst(implicits).asReal } + /** @see [[RestApiCompanion]] */ -abstract class RestServerApiCompanion[Real, I](implicit instances: ServerInstances[Real, I]) { this: I => - implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) - implicit final lazy val restAsRaw: AsRawRpc[Real] = instances.asRaw(this) +abstract class RestServerApiCompanion[Implicits, Real](implicits: Implicits)( + implicit inst: RpcMacroInstances[Implicits, ServerInstances, Real] +) { + implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits).metadata + implicit final lazy val restAsRaw: AsRawRpc[Real] = inst(implicits).asRaw } + /** * Base class for REST trait companions. Reduces boilerplate needed in order to define appropriate instances - * of `AsRawReal` and `RestMetadata` for given trait. The `I` type parameter lets you inject additional implicits + * of `AsRawReal` and `RestMetadata` for given trait. The `Implicits` type parameter lets you inject additional implicits * into macro materialization of these instances, e.g. [[DefaultRestImplicits]]. * Usually, for even less boilerplate, this base class is extended by yet another abstract class which fixes - * the `I` type, e.g. [[DefaultRestApiCompanion]]. + * the `Implicits` type, e.g. [[DefaultRestApiCompanion]]. */ -abstract class RestApiCompanion[Real, I](implicit instances: FullInstances[Real, I]) { this: I => - implicit final lazy val restMetadata: RestMetadata[Real] = instances.metadata(this) - implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = instances.asRawReal(this) +abstract class RestApiCompanion[Implicits, Real](implicits: Implicits)( + implicit inst: RpcMacroInstances[Implicits, FullInstances, Real] +) { + implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits).metadata + implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = inst(implicits).asRawReal } /** @@ -76,22 +69,19 @@ object DefaultRestImplicits extends DefaultRestImplicits * Base class for companions of REST API traits used only for REST clients to external services. * Injects `GenCodec` and `GenKeyCodec` based serialization. */ -abstract class DefaultRestClientApiCompanion[Real]( - implicit instances: ClientInstances[Real, DefaultRestImplicits] -) extends RestClientApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits +abstract class DefaultRestClientApiCompanion[Real](implicit inst: RpcMacroInstances[DefaultRestImplicits, ClientInstances, Real]) + extends RestClientApiCompanion[DefaultRestImplicits, Real](DefaultRestImplicits) /** * Base class for companions of REST API traits used only for REST servers exposed to external world. * Injects `GenCodec` and `GenKeyCodec` based serialization. */ -abstract class DefaultRestServerApiCompanion[Real]( - implicit instances: ServerInstances[Real, DefaultRestImplicits] -) extends RestServerApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits +abstract class DefaultRestServerApiCompanion[Real](implicit inst: RpcMacroInstances[DefaultRestImplicits, ServerInstances, Real]) + extends RestServerApiCompanion[DefaultRestImplicits, Real](DefaultRestImplicits) /** * Base class for companions of REST API traits used for both REST clients and servers. * Injects `GenCodec` and `GenKeyCodec` based serialization. */ -abstract class DefaultRestApiCompanion[Real]( - implicit instances: FullInstances[Real, DefaultRestImplicits] -) extends RestApiCompanion[Real, DefaultRestImplicits] with DefaultRestImplicits +abstract class DefaultRestApiCompanion[Real](implicit inst: RpcMacroInstances[DefaultRestImplicits, FullInstances, Real]) + extends RestApiCompanion[DefaultRestImplicits, Real](DefaultRestImplicits) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala index d569edcb1..02588b44f 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala @@ -2,21 +2,67 @@ package com.avsystem.commons package rpc /** - * Base for traits that aggregate multiple RPC-related typeclass instances (e.g. `AsReal` + metadata). - * Typically such aggregating type is declared as an implicit constructor parameter of an abstract class that - * serves as a base class for RPC trait companions. Example: [[com.avsystem.commons.rest.DefaultRestApiCompanion]]. - * This is all in order to reduce boilerplate needed to define an RPC trait. + * Intermediate factory that creates an `InstancesTrait` for given `Real` RPC trait, based on provided `Implicits`. + * Normally, this factory is used as implicit constructor parameter of base classes for companion objects + * of RPC traits (e.g. [[com.avsystem.commons.rest.DefaultRestApiCompanion DefaultRestApiCompanion]]). + * This all serves to reduce boilerplate associated with RPC trait companion declarations and makes RPC trait + * definitions as concise as possible. It also lets the programmer easily inject additional implicits into + * macro-materialization of RPC-related typeclasses (`AsReal`, `AsRaw`, metadata, etc.). * - * A trait that extends `RpcMacroInstances[Real]` must declare abstract methods, each method taking exactly - * one parameter of type `Implicits` and returning either an instance of `AsReal`, `AsRaw`, `AsRawReal` or - * some RPC metadata class for RPC trait `Real`. + * An `InstancesTrait` is a trait that aggregates multiple RPC related typeclass instances for given `Real` RPC trait. + * There is no fixed interface for `InstancesTrait`, its members are inspected by `materialize` macro and implemented + * automatically. `InstancesTrait` must contain only parameterless abstract methods that return either + * `AsRaw[Raw,Real]`, `AsReal[Raw,Real]`, `AsRawReal[Raw,Real]` or some RPC metadata class for `Real`. + * The `Raw` type is arbitrary and may be different for every method. However, it must be a concrete raw RPC trait + * so that it's further understood by the macro engine. All methods of the `InstancesTrait` are macro-implemented + * using `AsRaw/AsReal/AsRawReal/RpcMetadata.materializeForRpc` macros. * - * If instances-trait is defined according to these rules, `RpcMacros.macroInstances` will be able to - * automatically materialize an implementation using `AsReal.materializeForRpc`, `AsRaw.materializeForRpc`, - * `AsRawReal.materializeForRpc` and `RpcMetadata.materializeForRpc`. The `Implicits` parameter taken by every method - * serves to inject additional implicits into macro-materialization. The `RpcMacros.macroInstances` macro will import - * contents of that parameter inside every method implementation. + * Example of `InstancesTrait`: [[com.avsystem.commons.rest.ClientInstances ClientInstances]] + * + * The `Implicits` type is typically a trait with a collection of implicit definitions whose companion object + * implements that trait, e.g. [[com.avsystem.commons.rest.DefaultRestImplicits DefaultRestImplicits]]. + * When the macro implements `apply` method of `RpcMacroInstances` contents of `Implicits` are imported into the + * body of `apply` and visible further by macros that materialize `InstancesTrait`. */ -trait RpcMacroInstances[Real] { - type Implicits +trait RpcMacroInstances[Implicits, InstancesTrait[_], Real] { + def apply(implicits: Implicits): InstancesTrait[Real] +} +object RpcMacroInstances { + /** + * Materializes an instance of `RpcMacroInstances[Implicits, InstancesTrait, Real]`. This macro should not be + * invoked directly, it should only be used to materialize implicit parameters of RPC companion base classes, + * e.g. [[com.avsystem.commons.rest.DefaultRestApiCompanion DefaultRestApiCompanion]]. + * + * @example + * {{{ + * trait SomeRawRpc { ... } + * class SomeMetadata[Real](...) + * + * trait SomeInstances[Real} { + * def asReal: AsReal[SomeRawRpc, Real] + * def metadata: SomeMetadata[Real] + * } + * + * trait SomeImplicits { ... } + * object SomeImplicits extends SomeImplicits + * + * trait SomeRealRpc { ... } + * }}} + * + * `RpcMacroInstances.materialize[SomeImplicits, SomeInstances, SomeRealRpc]` would generate: + * + * {{{ + * new RpcMacroInstances[SomeImplicits, SomeInstances, SomeRealRpc] { + * def apply(implicits: SomeImplicits): SomeInstances[SomeRealRpc] = { + * import implicits._ + * new SomeInstances[SomeRealRpc] { + * def asReal: AsReal[SomeRawRpc, SomeRealRpc] = AsReal.materializeForRpc + * def metadata: SomeMetadata[Real] = RpcMetadata.materializeForRpc + * } + * } + * } + * }}} + */ + implicit def materialize[Implicits, InstancesTrait[_], Real]: RpcMacroInstances[Implicits, InstancesTrait, Real] = + macro macros.rpc.RpcMacros.macroInstances } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index db01b7336..15576bb75 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -181,65 +181,60 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) } } - def macroInstances[I: WeakTypeTag, Real: WeakTypeTag]: Tree = { - val realTpe = weakTypeOf[Real] + def macroInstances: Tree = { + val resultTpe = c.macroApplication.tpe + val realTpe = resultTpe.typeArgs.last + val applySig = resultTpe.member(TermName("apply")).typeSignatureIn(resultTpe) + val implicitsTpe = applySig.paramLists.head.head.typeSignature + val instancesTpe = applySig.finalResultType if (c.macroApplication.symbol.isImplicit && c.enclosingPosition.source != realTpe.typeSymbol.pos.source) { - abort(s"Implicit materialization of RpcMacroInstances is only allowed in the same file where RPC trait is defined") + abort(s"Implicit materialization of RpcMacroInstances is only allowed in the same file where RPC trait is defined ($realTpe)") } - val TypeApply(_, macroTypeArgs) = c.macroApplication - val macroTparams = c.macroApplication.symbol.typeSignature.typeParams - val macroTargsMap: Map[Name, Type] = - (macroTypeArgs zip macroTparams).map { case (targ, sym) => (sym.name, targ.tpe) }.toMap - - // Scala... srsly... why? - val instancesTpe = weakTypeOf[I].dealias.map { - case t@TypeRef(NoPrefix, sym, Nil) => macroTargsMap.getOrElse(sym.name, t) - case t => t - } - - val asRawTpe = getType(tq"$AsRawCls[_,$realTpe]") - val asRealTpe = getType(tq"$AsRealCls[_,$realTpe]") - val asRawRealTpe = getType(tq"$AsRawRealCls[_,$realTpe]") - val instTs = instancesTpe.typeSymbol if (!(instTs.isClass && instTs.isAbstract)) { abort(s"Expected trait or abstract class type, got $instancesTpe") } - val implicitsTmem = instancesTpe.member(TypeName("Implicits")) - if (implicitsTmem.isAbstract) { - abort(s"Could not determine concrete Implicits type for $instancesTpe") - } - val implicitsTpe = implicitsTmem.typeSignatureIn(instancesTpe) + val asRawTpe = getType(tq"$AsRawCls[_,$realTpe]") + val asRealTpe = getType(tq"$AsRealCls[_,$realTpe]") + val asRawRealTpe = getType(tq"$AsRawRealCls[_,$realTpe]") val impls = instancesTpe.members.iterator.filter(m => m.isAbstract && m.isMethod).map { m => val sig = m.typeSignatureIn(instancesTpe) - val body = (sig.typeParams, sig.paramLists) match { - case (Nil, List(List(single))) if single.typeSignature =:= implicitsTpe => - val resultTpe = sig.finalResultType.dealias - if (resultTpe <:< asRawRealTpe) - q"$AsRawRealObj.materializeForRpc[..${resultTpe.typeArgs}]" - else if (resultTpe <:< asRawTpe) - q"$AsRawObj.materializeForRpc[..${resultTpe.typeArgs}]" - else if (resultTpe <:< asRealTpe) - q"$AsRealObj.materializeForRpc[..${resultTpe.typeArgs}]" - else resultTpe.typeArgs match { - case List(st) if st =:= realTpe => - q"$RpcPackage.RpcMetadata.materializeForRpc[${resultTpe.typeConstructor}, $realTpe]" - case _ => abort(s"Bad result type $resultTpe of $m: " + - s"it must be an AsReal/AsRaw/AsRealRaw or RPC metadata instance for $realTpe") - } - case _ => - abort(s"Problem with $m: expected non-generic method that takes exactly one parameter of type $implicitsTpe") + val resultTpe = sig.finalResultType.dealias + if (sig.typeParams.nonEmpty || sig.paramLists.nonEmpty) { + abort(s"Problem with $m: expected non-generic, parameterless method") } - val implicitsName = c.freshName(TermName("implicits")) - q"def ${m.name.toTermName}($implicitsName: $implicitsTpe) = { import $implicitsName._; $body }" - } + val body = + if (resultTpe <:< asRawRealTpe) + q"$AsRawRealObj.materializeForRpc[..${resultTpe.typeArgs}]" + else if (resultTpe <:< asRawTpe) + q"$AsRawObj.materializeForRpc[..${resultTpe.typeArgs}]" + else if (resultTpe <:< asRealTpe) + q"$AsRealObj.materializeForRpc[..${resultTpe.typeArgs}]" + else resultTpe.typeArgs match { + case List(st) if st =:= realTpe => + q"$RpcPackage.RpcMetadata.materializeForRpc[${resultTpe.typeConstructor}, $realTpe]" + case _ => abort(s"Bad result type $resultTpe of $m: " + + s"it must be an AsReal/AsRaw/AsRealRaw or RPC metadata instance for $realTpe") + } + + q"def ${m.name.toTermName} = $body" + }.toList + + val implicitsName = c.freshName(TermName("implicits")) - q"new $instancesTpe { ..${impls.toList}; () }" + q""" + new $resultTpe { + def apply($implicitsName: $implicitsTpe): $instancesTpe = { + import $implicitsName._ + new $instancesTpe { ..$impls; () } + } + } + """ } def lazyMetadata(metadata: Tree): Tree = diff --git a/docs/REST.md b/docs/REST.md index 2503986f7..780f172e8 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -31,7 +31,7 @@ import com.avsystem.commons.rest._ trait UserApi { @GET def getUsername(userId: String): Future[String] } -object UserApi extends RestApiCompanion[UserApi] +object UserApi extends DefaultRestApiCompanion[UserApi] ``` Then, implement it on server side and expose it on localhost port 9090 using Jetty: From 888416bb6ee80d13638592e31ec2447a11231499 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 18 Jul 2018 11:36:31 +0200 Subject: [PATCH 63/91] added unique param validation for REST --- .../com/avsystem/commons/rest/RawRest.scala | 1 + .../avsystem/commons/rest/RestMetadata.scala | 33 ++++++++- .../commons/rest/RestPathValidationTest.scala | 38 ---------- .../commons/rest/RestValidationTest.scala | 74 +++++++++++++++++++ 4 files changed, 106 insertions(+), 40 deletions(-) delete mode 100644 commons-core/src/test/scala/com/avsystem/commons/rest/RestPathValidationTest.scala create mode 100644 commons-core/src/test/scala/com/avsystem/commons/rest/RestValidationTest.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index e9a62a1cb..730b3077e 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -38,6 +38,7 @@ trait RawRest { def asHandleRequest(metadata: RestMetadata[_]): RawRest.HandleRequest = { metadata.ensureUnambiguousPaths() + metadata.ensureUniqueParams(Nil) locally[RawRest.HandleRequest] { case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { case List(ResolvedPath(prefixes, RpcWithPath(finalRpcName, finalPathParams), singleBody)) => diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index 03216a6e0..6d00ce838 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -20,6 +20,33 @@ case class RestMetadata[T]( val httpMethods: Map[String, HttpMethodMetadata[_]] = httpGetMethods ++ httpBodyMethods + def ensureUniqueParams(prefixes: List[(String, PrefixMetadata[_])]): Unit = { + def ensureUniqueParams(methodName: String, method: RestMethodMetadata[_]): Unit = { + for { + (prefixName, prefix) <- prefixes + headerParam <- method.headersMetadata.headers.keys + if prefix.headersMetadata.headers.contains(headerParam) + } throw new InvalidRestApiException( + s"Header parameter $headerParam of $methodName collides with header parameter of the same name in prefix $prefixName") + + for { + (prefixName, prefix) <- prefixes + queryParam <- method.headersMetadata.query.keys + if prefix.headersMetadata.query.contains(queryParam) + } throw new InvalidRestApiException( + s"Query parameter $queryParam of $methodName collides with query parameter of the same name in prefix $prefixName") + } + + prefixMethods.foreach { + case (name, prefix) => + ensureUniqueParams(name, prefix) + prefix.result.value.ensureUniqueParams((name, prefix) :: prefixes) + } + (httpGetMethods ++ httpBodyMethods).foreach { + case (name, method) => ensureUniqueParams(name, method) + } + } + def ensureUnambiguousPaths(): Unit = { val trie = new RestMetadata.Trie trie.fillWith(this) @@ -30,7 +57,7 @@ case class RestMetadata[T]( val problems = ambiguities.map { case (path, chains) => s"$path may result from multiple calls:\n ${chains.mkString("\n ")}" } - throw new IllegalArgumentException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") + throw new InvalidRestApiException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") } } @@ -71,7 +98,7 @@ object RestMetadata extends RpcMetadataCompanion[RestMetadata] { metadata.prefixMethods.foreach { case entry@(rpcName, pm) => if (prefixStack.contains(entry)) { - throw new IllegalArgumentException( + throw new InvalidRestApiException( s"call chain $prefixChain$rpcName is recursive, recursively defined server APIs are forbidden") } forPattern(pm.pathPattern).fillWith(pm.result.value, entry :: prefixStack) @@ -186,3 +213,5 @@ case class PathParamMetadata[T]( case class HeaderParamMetadata[T]() extends TypedMetadata[T] case class QueryParamMetadata[T]() extends TypedMetadata[T] case class BodyParamMetadata[T](@isAnnotated[Body] singleBody: Boolean) extends TypedMetadata[T] + +class InvalidRestApiException(msg: String) extends RuntimeException(msg) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RestPathValidationTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RestPathValidationTest.scala deleted file mode 100644 index 591ca065e..000000000 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RestPathValidationTest.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.avsystem.commons -package rest - -import org.scalatest.FunSuite - -class RestPathValidationTest extends FunSuite { - trait Api2 { - def self: Api2 - } - object Api2 { - implicit val metadata: RestMetadata[Api2] = RestMetadata.materializeForRpc[Api2] - } - - test("recursive API") { - val failure = intercept[IllegalArgumentException](Api2.metadata.ensureUnambiguousPaths()) - assert(failure.getMessage == "call chain self->self is recursive, recursively defined server APIs are forbidden") - } - - trait Api1 { - @GET("p") def g1: Future[String] - @GET("p") def g2: Future[String] - @GET("") def g3(@Path("p") arg: String): Future[String] - - @POST("p") def p1: Future[String] - } - - test("simple ambiguous paths") { - val failure = intercept[IllegalArgumentException] { - RestMetadata.materializeForRpc[Api1].ensureUnambiguousPaths() - } - assert(failure.getMessage == - """REST API has ambiguous paths: - |GET /p may result from multiple calls: - | g2 - | g1""".stripMargin - ) - } -} diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RestValidationTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RestValidationTest.scala new file mode 100644 index 000000000..b956cff8d --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RestValidationTest.scala @@ -0,0 +1,74 @@ +package com.avsystem.commons +package rest + +import org.scalatest.FunSuite + +class RestValidationTest extends FunSuite { + trait Api2 { + def self: Api2 + } + object Api2 { + implicit val metadata: RestMetadata[Api2] = RestMetadata.materializeForRpc[Api2] + } + + test("recursive API") { + val failure = intercept[InvalidRestApiException](Api2.metadata.ensureUnambiguousPaths()) + assert(failure.getMessage == "call chain self->self is recursive, recursively defined server APIs are forbidden") + } + + trait Api1 { + @GET("p") def g1: Future[String] + @GET("p") def g2: Future[String] + @GET("") def g3(@Path("p") arg: String): Future[String] + + @POST("p") def p1: Future[String] + } + + test("simple ambiguous paths") { + val failure = intercept[InvalidRestApiException] { + RestMetadata.materializeForRpc[Api1].ensureUnambiguousPaths() + } + assert(failure.getMessage == + """REST API has ambiguous paths: + |GET /p may result from multiple calls: + | g2 + | g1""".stripMargin + ) + } + + trait PrefixApi1 { + def prefix(@Header("X-Lol") lol: String): SuffixApi1 + } + trait SuffixApi1 { + def post(@Header("X-Lol") lol: String): Future[String] + } + object SuffixApi1 { + implicit val metadata: RestMetadata[SuffixApi1] = RestMetadata.materializeForRpc + } + + test("conflicting header params") { + val failure = intercept[InvalidRestApiException] { + RestMetadata.materializeForRpc[PrefixApi1].ensureUniqueParams(Nil) + } + assert(failure.getMessage == + "Header parameter X-Lol of POST_post collides with header parameter of the same name in prefix prefix") + } + + trait PrefixApi2 { + def prefix(@Query lol: String): SuffixApi2 + } + trait SuffixApi2 { + def post(@Query lol: String): Future[String] + } + object SuffixApi2 { + implicit val metadata: RestMetadata[SuffixApi2] = RestMetadata.materializeForRpc + } + + test("conflicting query params") { + val failure = intercept[InvalidRestApiException] { + RestMetadata.materializeForRpc[PrefixApi2].ensureUniqueParams(Nil) + } + assert(failure.getMessage == + "Query parameter lol of POST_post collides with query parameter of the same name in prefix prefix") + } +} From 6c3f69c504a8237399a416708148584cbc597ec8 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 18 Jul 2018 11:58:18 +0200 Subject: [PATCH 64/91] added missing REST implicits for Float/Double --- .../rest/DefaultRestApiCompanion.scala | 6 +++--- .../rest/FloatingPointRestImplicits.scala | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 commons-core/src/main/scala/com/avsystem/commons/rest/FloatingPointRestImplicits.scala diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala index a9815ac0a..92fb316f6 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala @@ -19,7 +19,7 @@ trait FullInstances[Real] { def asRawReal: AsRawRealRpc[Real] } -/** @see [[RestApiCompanion]] */ +/** @see [[RestApiCompanion]]*/ abstract class RestClientApiCompanion[Implicits, Real](implicits: Implicits)( implicit inst: RpcMacroInstances[Implicits, ClientInstances, Real] ) { @@ -27,7 +27,7 @@ abstract class RestClientApiCompanion[Implicits, Real](implicits: Implicits)( implicit final lazy val restAsReal: AsRealRpc[Real] = inst(implicits).asReal } -/** @see [[RestApiCompanion]] */ +/** @see [[RestApiCompanion]]*/ abstract class RestServerApiCompanion[Implicits, Real](implicits: Implicits)( implicit inst: RpcMacroInstances[Implicits, ServerInstances, Real] ) { @@ -53,7 +53,7 @@ abstract class RestApiCompanion[Implicits, Real](implicits: Implicits)( * Defines [[com.avsystem.commons.serialization.GenCodec GenCodec]] and * [[com.avsystem.commons.serialization.GenKeyCodec GenKeyCodec]] based serialization for REST API traits. */ -trait DefaultRestImplicits { +trait DefaultRestImplicits extends FloatingPointRestImplicits { implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/FloatingPointRestImplicits.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/FloatingPointRestImplicits.scala new file mode 100644 index 000000000..590294f3c --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/FloatingPointRestImplicits.scala @@ -0,0 +1,21 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.rpc.AsRawReal + +trait FloatingPointRestImplicits { + implicit final val floatPathValueAsRealRaw: AsRawReal[PathValue, Float] = + AsRawReal.create(v => PathValue(v.toString), _.value.toFloat) + implicit final val floatHeaderValueAsRealRaw: AsRawReal[HeaderValue, Float] = + AsRawReal.create(v => HeaderValue(v.toString), _.value.toFloat) + implicit final val floatQueryValueAsRealRaw: AsRawReal[QueryValue, Float] = + AsRawReal.create(v => QueryValue(v.toString), _.value.toFloat) + + implicit final val doublePathValueAsRealRaw: AsRawReal[PathValue, Double] = + AsRawReal.create(v => PathValue(v.toString), _.value.toDouble) + implicit final val doubleHeaderValueAsRealRaw: AsRawReal[HeaderValue, Double] = + AsRawReal.create(v => HeaderValue(v.toString), _.value.toDouble) + implicit final val doubleQueryValueAsRealRaw: AsRawReal[QueryValue, Double] = + AsRawReal.create(v => QueryValue(v.toString), _.value.toDouble) +} +object FloatingPointRestImplicits extends FloatingPointRestImplicits From 3622f67245fee3f23ace91b6c8306516a459eb94 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 18 Jul 2018 12:01:14 +0200 Subject: [PATCH 65/91] identity for AsReal/AsRaw/AsRawReal is implicit --- .../src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala index 9afafabd7..478c21d06 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala @@ -14,7 +14,7 @@ object AsRaw { new AsRaw[Raw, Real] { def asRaw(real: Real): Raw = asRawFun(real) } - def identity[A]: AsRaw[A, A] = AsRawReal.identity[A] + implicit def identity[A]: AsRaw[A, A] = AsRawReal.identity[A] def materializeForRpc[Raw, Real]: AsRaw[Raw, Real] = macro macros.rpc.RpcMacros.rpcAsRaw[Raw, Real] } @@ -29,7 +29,7 @@ object AsReal { new AsReal[Raw, Real] { def asReal(raw: Raw): Real = asRealFun(raw) } - def identity[A]: AsReal[A, A] = AsRawReal.identity[A] + implicit def identity[A]: AsReal[A, A] = AsRawReal.identity[A] def materializeForRpc[Raw, Real]: AsReal[Raw, Real] = macro macros.rpc.RpcMacros.rpcAsReal[Raw, Real] } @@ -49,7 +49,7 @@ object AsRawReal { def asRaw(real: Any): Any = real } - def identity[A]: AsRawReal[A, A] = + implicit def identity[A]: AsRawReal[A, A] = reusableIdentity.asInstanceOf[AsRawReal[A, A]] def materializeForRpc[Raw, Real]: AsRawReal[Raw, Real] = macro macros.rpc.RpcMacros.rpcAsRawReal[Raw, Real] From 36c1b4e13867a5845d93aef4e0d9f7f98c2283d7 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 18 Jul 2018 17:29:30 +0200 Subject: [PATCH 66/91] REST docs --- README.md | 1 + .../avsystem/commons/rest/annotations.scala | 4 +- .../jetty/rest/examples/ClientMain.scala | 6 +- .../jetty/rest/examples/ServerMain.scala | 2 +- .../commons/jetty/rest/examples/UserApi.scala | 3 +- docs/REST.md | 373 +++++++++++++++++- 6 files changed, 375 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 712e7a90d..81d70bbd5 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ derivation for case classes and sealed hierarchies * [RPC framework](docs/RPCFramework.md): **typesafe** RPC/proxy framework used in particular by [Udash Framework](http://guide.udash.io/#/rpc) for client-server communication + * [REST framework](docs/REST.md) based on RPC framework * Better enumeration support for Scala - [`ValueEnum`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/ValueEnum.html), [`SealedEnumCompanion`](http://avsystem.github.io/scala-commons/api/com/avsystem/commons/misc/SealedEnumCompanion.html), diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala index 47fa6b37c..7db962fef 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala @@ -18,8 +18,8 @@ sealed trait RestMethodTag extends RpcTag { /** * HTTP URL path segment associated with REST method annotated with this tag. This path may be multipart * (i.e. contain slashes). It may also be empty which means that this particular REST method does not contribute - * anything to URL path. If path is not specified explicitly, method name is used (the actual method name, not - * `rpcName`). + * anything to URL path. Any special characters will be URL-encoded when creating HTTP request. + * If path is not specified explicitly, method name is used (the actual method name, not `rpcName`). * * @example * {{{ diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ClientMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ClientMain.scala index 662d3f66b..377b9b999 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ClientMain.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ClientMain.scala @@ -14,13 +14,13 @@ object ClientMain { val proxy = RestClient[UserApi](client, "http://localhost:9090/") - // just for this example, normally not recommended... + // just for this example, normally it's not recommended import scala.concurrent.ExecutionContext.Implicits.global - val result = proxy.getUsername("ID") + val result = proxy.createUser("Fred", 1990) .andThen({ case _ => client.stop() }) .andThen { - case Success(name) => println(s"Hello, $name!") + case Success(id) => println(s"User $id created") case Failure(cause) => cause.printStackTrace() } diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ServerMain.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ServerMain.scala index f67caa416..d699e4e7f 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ServerMain.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/ServerMain.scala @@ -5,7 +5,7 @@ import com.avsystem.commons.jetty.rest.RestHandler import org.eclipse.jetty.server.Server class UserApiImpl extends UserApi { - def getUsername(userId: String) = Future.successful(s"$userId-name") + def createUser(name: String, birthYear: Int): Future[String] = Future.successful(s"$name-ID") } object ServerMain { diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala index e7dd7598d..4f928cea2 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/examples/UserApi.scala @@ -4,6 +4,7 @@ package jetty.rest.examples import com.avsystem.commons.rest._ trait UserApi { - @GET def getUsername(userId: String): Future[String] + /** Returns ID of newly created user */ + def createUser(name: String, birthYear: Int): Future[String] } object UserApi extends DefaultRestApiCompanion[UserApi] \ No newline at end of file diff --git a/docs/REST.md b/docs/REST.md index 780f172e8..14d50557f 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -29,7 +29,8 @@ Then, define some trivial REST interface: import com.avsystem.commons.rest._ trait UserApi { - @GET def getUsername(userId: String): Future[String] + /** Returns ID of newly created user */ + def createUser(name: String, birthYear: Int): Future[String] } object UserApi extends DefaultRestApiCompanion[UserApi] ``` @@ -41,7 +42,7 @@ import com.avsystem.commons.jetty.rest.RestHandler import org.eclipse.jetty.server.Server class UserApiImpl extends UserApi { - def getUsername(userId: String) = Future.successful(s"$userId-name") + def createUser(name: String, birthYear: Int) = Future.successful(s"$name-ID") } object ServerMain { @@ -73,10 +74,10 @@ object ClientMain { // just for this example, normally it's not recommended import scala.concurrent.ExecutionContext.Implicits.global - val result = proxy.getUsername("ID") + val result = proxy.createUser("Fred", 1990) .andThen({ case _ => client.stop() }) .andThen { - case Success(name) => println(s"Hello, $name!") + case Success(id) => println(s"User $id created") case Failure(cause) => cause.printStackTrace() } @@ -90,19 +91,377 @@ If we look at HTTP traffic, that's what we'll see: Request: ``` -GET http://localhost:9090/getUsername?userId=ID HTTP/1.1 +POST http://localhost:9090/createUser HTTP/1.1 Accept-Encoding: gzip User-Agent: Jetty/9.3.23.v20180228 Host: localhost:9090 +Content-Type: application/json;charset=utf-8 +Content-Length: 32 + +{"name":"Fred","birthYear":1990} ``` Response: ``` HTTP/1.1 200 OK -Date: Wed, 11 Jul 2018 12:54:21 GMT +Date: Wed, 18 Jul 2018 11:43:08 GMT Content-Type: application/json;charset=utf-8 Content-Length: 9 Server: Jetty(9.3.23.v20180228) -"ID-name" +"Fred-ID" +``` + +## REST API traits + +As we saw in the quickstart example, REST API is defined by a Scala trait adjusted with annotations. +This approach is analogous to various well-established REST frameworks for other languages, e.g. JAX-RS for Java. +However, such frameworks are usually based on runtime reflection while in Scala it can be +done using compile-time reflection through macros which offers several advantages: + +* platform independency - REST traits are understood by both ScalaJVM and ScalaJS +* full type information - compile-time reflection is not limited by type erasure +* type safety - compile-time reflection can perform thorough validation of REST traits and + raise compilation errors in case anything is wrong +* pluggable typeclass based serialization - for serialization of REST parameters and results, + typeclasses are used which also offers strong compile-time safety. If any of your parameters or + method results cannot be serialized, a detailed compilation error will be raised. + +### Companion objects + +In order for a trait to be understood as REST API, it must have a well defined companion object that contains +appropriate implicits: + +* in order to expose REST API on a server, implicit instances of `RawRest.AsRawRpc` and `RestMetadata` for API trait are required. +* in order to use REST API client, implicit instances of `RawRest.AsRealRpc` and `RestMetadata` for API trait are required. +* when API trait is used by both client and server, `RawRest.AsRawRpc` and `RawRest.AsRealRpc` may be provided by a single + combined instance of `RawRest.AsRawRealRpc` for API trait. + +Usually there is no need to declare these implicit instances manually because you can use one of the convenience +base classes for REST API companion objects, e.g. + +```scala +import com.avsystem.commons.rest._ + +trait MyApi { ... } +object MyApi extends DefaultRestApiCompanion[MyApi] +``` + +`DefaultRestApiCompanion` takes a magic implicit parameter generated by a macro which will effectively +materialize all the necessary typeclass instances mentioned earlier. The "`Default`" in its name means that +`DefaultRestImplicits` is used as a provider of serialization-related implicits. See [Serialization](#serialization) +for more details on customizing serialization. + +`DefaultRestApiCompanion` provides all the implicit instances necessary for both the client and server. +If you intend to use your API trait only on the server or only on the client, you may want to use more lightweight +`DefaultRestClientApiCompanion` or `DefaultRestServerApiCompanion`. This may help you reduce the amount of macro +generated code and make compilation faster. + +#### Manual declaration of implicits + +On less frequent occasions you might be unable to use one of the companion base classes. In such situations you must +declare all the implicit instances manually (however, they will still be implemented with a macro). +For example: + +```scala +import com.avsystem.commons.rest._ + +trait MyApi { ... } +object GenericApi { + import DefaultRestImplicits._ + implicit val restAsRawReal: RawRest.AsRawRealRpc[MyApi] = RawRest.materializeAsRawReal + implicit val restMetadata: RestMetadata[MyApi] = RestMetadata.materializeForRpc +} +``` + +This is usually necessary when implicit instances need some additional implicit dependencies or when +the API trait is generic (has type parameters). + +### HTTP REST methods + +REST macro engine inspects API trait and looks for all abstract methods. It then tries to translate every abstract +method into a HTTP REST call. + +* By default (if not annotated explicitly) each method is interpreted as HTTP `POST`. +* Method name is appended to the URL path. +* Every parameter is interpreted as part of the body - all the body parameters will be + combined into a JSON object sent through HTTP body. However, for `GET` methods, every parameter + is interpreted as URL query parameter while the body is empty. +* Result type of each method is typically expected to be a `Future` wrapping some + arbitrary response type. This response type will be serialized into HTTP response which + by default translates it into JSON and creates a `200 OK` response with `application/json` + content type. If response type is `Unit` (method result type is `Future[Unit]`) then empty + body is created when serializing and body is ignored when deseriarlizing. + +For details on how exactly serialization works and how to customize it, see [Serialization](#serialization). +Note that if you don't want to use `Future`, this customization also allows you to use other wrappers for method result types. + +### Choosing HTTP method + +As mentioned earlier, each trait method is by default translated into a `POST` request. +You can specify which HTTP method you want by explicitly annotating trait method as +`@GET`/`@POST`/`@PATCH`/`@PUT` or `@DELETE` (from `com.avsystem.commons.rest` package). + +```scala +@DELETE def deleteUser(id: String): Future[Unit] +``` + +#### `GET` methods + +Trait method annotated as `@GET` is interpreted somewhat differently from other HTTP methods. +Its parameters are interpreted as _query_ parameters rather than _body_ parameters. For example: + +```scala +@GET def getUsername(id: String): Future[String] +``` + +Calling `getUsername("ID")` on the client will result in HTTP request: + +``` +GET http://localhost:9090/getUsername?userId=ID HTTP/1.1 +Accept-Encoding: gzip +User-Agent: Jetty/9.3.23.v20180228 +Host: localhost:9090 + +``` + +### Customizing paths + +By default, method name is appended to URL path when translating method call to HTTP request. +This can be customized. Every annotation specifying HTTP method (e.g. `GET`) takes an optional +`path` argument that you can use to customize your path: + +```scala +@GET("username") def getUsername(id: String): Future[String] +``` + +The specified path may be multipart (it may contain slashes) or it may even be empty. +However, for server-side APIs all paths must be unambiguous, i.e. there must not be more +than one method translating to the same path. This is validated in runtime, upon +creating a server. + +Empty paths may be especially useful for [prefix methods](#prefix-methods). + +### Path parameters + +If a parameter of REST API trait method is annotated as `@Path`, its value is +appended to URL path rather than translated into query parameter or body part. + +```scala +@GET("username") def getUsername(@Path id: String): Future[String] +``` + +Calling `getUsername("ID")` will make a HTTP request on path `username/ID`. + +If there are multiple `@Path` parameters, their values are appended to the path in the +order of declaration. Each path parameters may also optionally specify a _path suffix_ that +will be appended to path after value of each parameter: + +```scala +@GET("users") def getUsername(@Path(pathSuffix = "name") id: String): Future[String] +``` + +Calling `getUsername("ID")` will make a HTTP request on path `users/ID/name`. + +This way you can model completely arbitrary path patterns. + +Values of path parameters are serialized into `PathValue` objects, +see [Serialization](#serializaton) for more details. + +### Query parameters + +You may explicitly request that some parameter is translated into URL query parameter +using `@Query` annotation. As mentioned earlier, parameters of `GET` methods are treated +as query parameters by default, so this is only strictly necessary for non-`GET` methods. + +`@Query` annotation also takes optional `name` parameter which may be specified to customize +URL parameter name. If not specified, trait method parameter name is used. + +Values of query parameters are serialized into `QueryValue` objects, +see [Serialization](#serializaton) for more details. + +### Header parameters + +You may also request that some parameter is translated into a HTTP header using `@Header` annotation. +It takes an obligatory `name` argument that specifies HTTP header name. + +Values of header parameters are serialized into `HeaderValue` objects, +see [Serialization](#serializaton) for more details. + +### JSON Body parameters + +As mentioned earlier, every parameter of non-`GET` API trait method is interpreted as a field +of a JSON object sent as HTTP body. Just like for path, query and header parameters, there is a +`@JsonBodyParam` annotation which requests this explicitly. However, it only makes sense to use this +annotation when one wants to customize the field name because `GET` methods do not accept body parameters. +A method annotated as `@GET` having a parameter annotated as `@JsonBodyParam` will be rejected by REST +macro engine. + +JSON body parameters are serialized into `JsonValue` objects, +see [Serialization](#serializaton) for more details. + +### Single body parameters + +Every non-`GET` API method may also take a single parameter annotated as `@Body` in place of multiple +unannotated parameters (implicitly interpreted as `@JsonBodyParam`s). This way the value of that parameter +will be serialized straight into HTTP body. This gives you full control over the contents sent in HTTP body +and their format (i.e. it no longer needs to be `application/json`). + +Single body parameters are serialized into `HttpBody` objects, +see [Serialization](#serializaton) for more details. + +### Prefix methods + +If a method in REST API trait doesn't return `Future[T]` (or other type that +properly translates to asynchronous HTTP response) then it may also be interpreted as _prefix_ method. + +Prefix methods are methods that return other REST API traits. They are useful for: + +* capturing common path or path/query/header parameters in a single prefix call +* splitting your REST API into multiple traits in order to better organize it + +Just like HTTP API methods (`GET`, `POST`, etc.), prefix methods have their own +annotation that can be used explicitly when you want your trait method to be treated as +a prefix method. This annotation is `@Prefix` and just like HTTP method annotations, it +takes an optional `path` parameter. If you don't need to specify path explicitly then +annotation is not necessary as long as your method returns a valid REST API trait +(where "valid" is determined by presence of appropriate implicits - +see [companion objects](#companion-objects)). + +Prefix methods may take parameters. They are interpreted as path parameters by default, +but they may also be annotated as `@Query` or `@Header`. Prefix methods must not take +body parameters. + +Path and parameters collected by a prefix method will be prepended/added +to the HTTP request generated by a HTTP method call on the API trait returned by this +prefix method. This way prefix methods "contribute" to the final HTTP requests. + +However, sometimes it may also be useful to create completely "transparent" prefix methods - +prefix methods with empty path and no parameters. This is useful when you want to refactor your +REST API trait by grouping methods into multiple, separate traits without changing the +format of HTTP requests. + +Example of prefix method that adds authentication header to the overall API: + +```scala +trait UserApi { ... } +object UserApi extends DefaultRestApiCompanion[UserApi] + +trait RootApi { + @Prefix("") def auth(@Header("Authorization") token: String): UserApi +} +object RootApi extends DefaultRestApiCompanion[RootApi] ``` + +## Serialization + +REST macro engine must be able to generate code that serializes and deserializes +every parameter value and every method result into appropriate raw values which can +be easily sent through network. Serialization in REST framework is typeclass based, +which is a typical, functional and typesafe approach to serialization in Scala. + +Examples of typeclass based serialization libraries include [GenCodec](docs/GenCodec.md) +(which is the default serialization used by this REST framework), [circe](https://circe.github.io/circe/) +(one of the most popular JSON libraries for Scala) or [µPickle](http://www.lihaoyi.com/upickle/). +Any of these solutions can be plugged into REST framework. + +### Real and raw values + +Depending on the context where a type is used in a REST API trait, it will be serialized to a different +_raw value_: + +* path/query/header parameters are serialized as `PathValue`/`QueryValue`/`HeaderValue` +* JSON body parameters are serialized as `JsonValue` +* Single body parameters are serialized as `HttpBody` +* Response types are serialized as `RestResponse` +* Prefix result types (other REST API traits) are "serialized" as `RawRest` + +When a macro needs to serialize a value of some type (let's call it `Real`) to one of these raw types +listed above (let's call it `Raw`) then it looks for an implicit instance of `AsRaw[Raw, Real]`. +In the same manner, an implicit instance of `AsReal[Raw, Real]` is used for deserialization. +Additionally, if there is an implicit instance of `AsRawReal[Raw, Real]` then it serves both purposes. + +These implicit instances may come from multiple sources: + +* implicit scope of the `Raw` type (e.g. its companion object) +* implicit scope of the `Real` type (e.g. its companion object) +* implicits plugged by REST API trait companion + (e.g. `DefaultRestApiCompanion` plugs in `DefaultRestImplicits`) +* imports + +Of course, these implicits may also depend on other implicits which effectively means that +you can use whatever typeclass-based serialization library you want. +For example, you can define an instance of `AsRaw[JsonValue, Real]` which actually uses +`Encoder[Real]` from [circe](https://circe.github.io/circe/). See [Customizing serialization](#customizing-serialization) +for more details. + +### Path, query and header serialization + +Path, query and header parameter values are serialized into `PathValue`/`QueryValue`/`HeaderValue`. +These three classes are all simple `String` wrappers. Thanks to the fact that they are distinct types, +you can have completely different serialization defined for each one of them if you need. + +There are no "global" implicits defined for these raw types. They must be either imported, defined by each +"real" type or plugged in by REST API trait companion. For example, the `DefaultRestApiCompanion` and its +variations automatically provide serialization to `PathValue`/`QueryValue`/`HeaderValue` based on `GenKeyCodec` +and additional instances for `Float` and `Double`. This effectively provides serialization for all the +primitive types, its Java boxed counterparts, `String`, all `NamedEnum`s, Java enums and `Timestamp`. +It's also easy to provide path/query/header serialization for any type which has a natural, unambiguous textual +representation. + +Serialized values of path & query parameters are automatically URL-encoded when being embedded into +HTTP requests. This means that serialization should not worry about that. + +### JSON body parameter serialization + +JSON body parameters are serialized into `JsonValue` which is also a simple wrapper class over `String`, +but is importantly distinct from `PathValue`/`QueryValue`/`HeaderValue` because it must always contain +a valid JSON string. This is required because JSON body parameters are ultimately composed into a single +JSON object sent as HTTP body. + +There are no "global" implicits defined for `JsonValue` - JSON serialization must be either imported, +defined by each "real" type manually or plugged in by REST API trait companion. For example, +`DefaultRestApiCompanion` and its variations automatically provide serialization to `JsonValue` based +on `GenCodec`, which means that if `DefaultRestApiCompanion` is used then every type used in REST API trait +that has a `GenCodec` instance will be serializable to `JsonValue`. + +### Single body serialization + +Single body parameters (annotated as `@Body`) are serialized straight into `HttpBody`, which encapsulates +not only raw content but also MIME type. This way you can define custom body serializations for your types and +you are not limited to `application/json`. + +By default (if there are no more specific implicit instances defined), +serialization to `HttpBody` falls back to `JsonValue` and simply wraps JSON string into HTTP body +with `application/json` type. This means that all types serializable to `JsonValue` are automatically +serializable to `HttpBody`. + +### Result serialization + +Result type of every REST API method is "serialized" into `Future[RestResponse]`, which means that +macro engine looks for an implicit instance of `AsRaw/AsReal[Future[RestResponse], MethodResultType]`. +`RestResponse` is a simple type that encapsulates HTTP status code and body. + +However, `RestResponse` companion object defines a default implicit instance of `AsRaw/Real[Future[RestResponse], Future[T]]` +which depends on implicit `AsRaw/Real[HttpBody, T]`. This default serialization always uses 200 as a status code +for successful `Future[T]` values. In practice this means that if your REST API method returns +`Future[T]` then your `T` type must be serializable to `HttpBody`. Additionally, remember that all types serializable to +`JsonValue` are automatically serializable to `HttpBody`. + +You might want to define custom serialization straight into `Future[RestResponse]` when: + +* You don't want to use `Future` in your REST API traits but some other type that encapsulates asynchronous + computation, e.g. [Monix Task](https://monix.io/docs/2x/eval/task.html) +* Result of your HTTP method determines HTTP status code that you want to be sent back to the client. + +### Customizing serialization + +#### Plugging in entirely custom serialization + +#### Providing serialization for your own type + +#### Providing serialization for third party type + +## API evolution + +## Implementing backends From 9aa29049b41c5c105e1ca47f2cedc737a02d6635 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 18 Jul 2018 17:44:38 +0200 Subject: [PATCH 67/91] typos --- docs/REST.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/REST.md b/docs/REST.md index 14d50557f..7d79085b7 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -1,15 +1,15 @@ # REST framework -The commons libary contains an RPC based REST framework for defining REST services using Scala traits. +The commons library contains an RPC based REST framework for defining REST services using Scala traits. It may be used for implementing both client and server side and works in both JVM and JS, as long as appropriate network layer is implemented. For JVM, Jetty-based implementations for client and server are provided. -The core or REST framework is platform independent and network-implementation indepenedent and therefore +The core of REST framework is platform independent and network-implementation indepenedent and therefore has no external dependencies. Because of that, it's a part of `commons-core` module. This is enough to be able to define REST interfaces. But if you want to expose your REST interface through an actual HTTP server or have an actual HTTP client for that interface, you need separate implementations for that. -The `commons-jetty` module automati +The `commons-jetty` module provides Jetty-based implementations for JVM. ## Quickstart example @@ -75,7 +75,7 @@ object ClientMain { import scala.concurrent.ExecutionContext.Implicits.global val result = proxy.createUser("Fred", 1990) - .andThen({ case _ => client.stop() }) + .andThen { case _ => client.stop() } .andThen { case Success(id) => println(s"User $id created") case Failure(cause) => cause.printStackTrace() From 01a5ad8d73c15cf8f46b4e87645182d507c1f6d2 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 10:19:49 +0200 Subject: [PATCH 68/91] FindUsages analyzer rule --- .../commons/analyzer/AnalyzerPlugin.scala | 41 ++++++++++++------- .../commons/analyzer/AnalyzerRule.scala | 5 ++- .../commons/analyzer/FindUsages.scala | 20 +++++++++ .../commons/analyzer/AnalyzerTest.scala | 13 ++++-- .../commons/analyzer/FindUsagesTest.scala | 18 ++++++++ .../json/JsonStringInputOutputTest.scala | 5 +++ 6 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/FindUsages.scala create mode 100644 commons-analyzer/src/test/scala/com/avsystem/commons/analyzer/FindUsagesTest.scala diff --git a/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala b/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala index b2a160840..9d41fc7ed 100644 --- a/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala +++ b/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerPlugin.scala @@ -6,34 +6,45 @@ import scala.tools.nsc.{Global, Phase} final class AnalyzerPlugin(val global: Global) extends Plugin { plugin => - val rules = List[AnalyzerRule[global.type]]( - new ImportJavaUtil[global.type](global), - new VarargsAtLeast[global.type](global), - new CheckMacroPrivate[global.type](global), - new ExplicitGenerics[global.type](global), - new ValueEnumExhaustiveMatch[global.type](global), - new ShowAst[global.type](global) - ) - val rulesByName = rules.map(r => (r.name, r)).toMap - override def init(options: List[String], error: String => Unit): Boolean = { options.foreach { option => val level = option.charAt(0) match { case '-' => Level.Off + case '*' => Level.Info case '+' => Level.Error case _ => Level.Warn } - val name = if (level != Level.Warn) option.drop(1) else option - if (name == "_") { + val nameArg = if (level != Level.Warn) option.drop(1) else option + if (nameArg == "_") { rules.foreach(_.level = level) - } else rulesByName.get(name) match { - case Some(rule) => rule.level = level - case None => error(s"Unrecognized AVS analyzer rule: $name") + } else { + val (name, arg) = nameArg.split(":", 2) match { + case Array(n, a) => (n, a) + case Array(n) => (n, null) + } + rulesByName.get(name) match { + case Some(rule) => + rule.level = level + rule.argument = arg + case None => + error(s"Unrecognized AVS analyzer rule: $name") + } } } true } + private lazy val rules = List[AnalyzerRule[global.type]]( + new ImportJavaUtil[global.type](global), + new VarargsAtLeast[global.type](global), + new CheckMacroPrivate[global.type](global), + new ExplicitGenerics[global.type](global), + new ValueEnumExhaustiveMatch[global.type](global), + new ShowAst[global.type](global), + new FindUsages[global.type](global) + ) + private lazy val rulesByName = rules.map(r => (r.name, r)).toMap + val name = "AVSystemAnalyzer" val description = "AVSystem custom Scala static analyzer" val components: List[PluginComponent] = List(component) diff --git a/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerRule.scala b/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerRule.scala index 366720bf8..fb85e24a5 100644 --- a/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerRule.scala +++ b/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/AnalyzerRule.scala @@ -12,8 +12,9 @@ abstract class AnalyzerRule[C <: Global with Singleton]( import global._ var level: Level = defaultLevel + var argument: String = _ - protected def classType(fullName: String) = + protected def classType(fullName: String): Type = try rootMirror.staticClass(fullName).asType.toType.erasure catch { case _: ScalaReflectionException => NoType } @@ -29,6 +30,7 @@ abstract class AnalyzerRule[C <: Global with Singleton]( protected def report(pos: Position, message: String): Unit = level match { case Level.Off => + case Level.Info => reporter.info(pos, message, force = true) case Level.Warn => reporter.warning(pos, message) case Level.Error => reporter.error(pos, message) } @@ -41,6 +43,7 @@ abstract class AnalyzerRule[C <: Global with Singleton]( sealed trait Level object Level { case object Off extends Level + case object Info extends Level case object Warn extends Level case object Error extends Level } diff --git a/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/FindUsages.scala b/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/FindUsages.scala new file mode 100644 index 000000000..7d708b1d6 --- /dev/null +++ b/commons-analyzer/src/main/scala/com/avsystem/commons/analyzer/FindUsages.scala @@ -0,0 +1,20 @@ +package com.avsystem.commons +package analyzer + +import scala.tools.nsc.Global + +class FindUsages[C <: Global with Singleton](g: C) extends AnalyzerRule(g, "findUsages") { + + import global._ + + lazy val rejectedSymbols: Set[String] = + if (argument == null) Set.empty else argument.split(";").toSet + + override def analyze(unit: CompilationUnit): Unit = if (rejectedSymbols.nonEmpty) { + unit.body.foreach { tree => + if (tree.symbol != null && rejectedSymbols.contains(tree.symbol.fullName)) { + report(tree.pos, s"found usage of ${tree.symbol.fullName}") + } + } + } +} diff --git a/commons-analyzer/src/test/scala/com/avsystem/commons/analyzer/AnalyzerTest.scala b/commons-analyzer/src/test/scala/com/avsystem/commons/analyzer/AnalyzerTest.scala index 9e1d1aa8d..07204306d 100644 --- a/commons-analyzer/src/test/scala/com/avsystem/commons/analyzer/AnalyzerTest.scala +++ b/commons-analyzer/src/test/scala/com/avsystem/commons/analyzer/AnalyzerTest.scala @@ -1,6 +1,7 @@ package com.avsystem.commons package analyzer +import org.scalactic.source.Position import org.scalatest.Assertions import scala.reflect.internal.util.BatchSourceFile @@ -11,7 +12,8 @@ trait AnalyzerTest { this: Assertions => val settings = new Settings settings.usejavacp.value = true settings.pluginOptions.value ++= List("AVSystemAnalyzer:+_") - val compiler = new Global(settings) { global => + + val compiler: Global = new Global(settings) { global => override protected def loadRoughPluginsList(): List[Plugin] = new AnalyzerPlugin(global) :: super.loadRoughPluginsList() } @@ -22,12 +24,17 @@ trait AnalyzerTest { this: Assertions => run.compileSources(List(new BatchSourceFile("test.scala", source))) } - def assertErrors(source: String): Unit = { + def assertErrors(source: String)(implicit pos: Position): Unit = { compile(source) assert(compiler.reporter.hasErrors) } - def assertNoErrors(source: String): Unit = { + def assertErrors(errors: Int, source: String)(implicit pos: Position): Unit = { + compile(source) + assert(compiler.reporter.errorCount == errors) + } + + def assertNoErrors(source: String)(implicit pos: Position): Unit = { compile(source) assert(!compiler.reporter.hasErrors) } diff --git a/commons-analyzer/src/test/scala/com/avsystem/commons/analyzer/FindUsagesTest.scala b/commons-analyzer/src/test/scala/com/avsystem/commons/analyzer/FindUsagesTest.scala new file mode 100644 index 000000000..57ef23aff --- /dev/null +++ b/commons-analyzer/src/test/scala/com/avsystem/commons/analyzer/FindUsagesTest.scala @@ -0,0 +1,18 @@ +package com.avsystem.commons +package analyzer + +import org.scalatest.FunSuite + +class FindUsagesTest extends FunSuite with AnalyzerTest { + settings.pluginOptions.value ++= List("AVSystemAnalyzer:+findUsages:java.lang.String") + + test("java.lang.String usages should be found") { + assertErrors(2, + """ + |object whatever { + | val x: String = String.valueOf(123) + |} + """.stripMargin + ) + } +} diff --git a/commons-core/src/test/scala/com/avsystem/commons/serialization/json/JsonStringInputOutputTest.scala b/commons-core/src/test/scala/com/avsystem/commons/serialization/json/JsonStringInputOutputTest.scala index 039357d0f..a71bb869d 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/serialization/json/JsonStringInputOutputTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/serialization/json/JsonStringInputOutputTest.scala @@ -131,6 +131,11 @@ class JsonStringInputOutputTest extends FunSuite with SerializationTestUtils wit assert(read[Map[String, List[Int]]](prettyJson, options) == map) } + test("whitespace skipping") { + val json = """ { "a" : [ 1 , 2 ] , "b" : [ ] } """ + assert(read[Map[String, List[Int]]](json) == Map("a" -> List(1, 2), "b" -> Nil)) + } + test("NaN") { val value = Double.NaN From d906a501287a22ed8d2540d5b59120fd171c1485 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 11:12:38 +0200 Subject: [PATCH 69/91] dedicated exceptions for RPC, minor refactors --- .../scala/com/avsystem/commons/rest/RawRest.scala | 6 ++++-- .../com/avsystem/commons/rest/RestMetadata.scala | 6 +++--- .../scala/com/avsystem/commons/rpc/AsRawReal.scala | 2 +- .../scala/com/avsystem/commons/rpc/OptionLike.scala | 2 ++ .../avsystem/commons/rpc/RpcMetadataCompanion.scala | 2 +- .../scala/com/avsystem/commons/rpc/RpcUtils.scala | 12 ++++++------ .../com/avsystem/commons/macros/rpc/RpcMacros.scala | 10 ++-------- 7 files changed, 19 insertions(+), 21 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 730b3077e..ad45cd717 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -62,7 +62,7 @@ trait RawRest { case multiple => val pathStr = headers.path.iterator.map(_.value).mkString("/") val callsRepr = multiple.iterator.map(p => s" ${p.rpcChainRepr}").mkString("\n", "\n", "") - throw new IllegalArgumentException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") + throw new RestException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") } } } @@ -95,9 +95,11 @@ object RawRest extends RawRpcCompanion[RawRest] { def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Future[RestResponse] = { val methodMeta = metadata.httpMethods.getOrElse(name, - throw new IllegalArgumentException(s"no such HTTP method: $name")) + throw new RestException(s"no such HTTP method: $name")) val newHeaders = prefixHeaders.append(methodMeta, headers) handleRequest(RestRequest(methodMeta.method, newHeaders, body)) } } } + +class RestException(msg: String, cause: Throwable = null) extends RpcException(msg, cause) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index 6d00ce838..6de358c98 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -26,14 +26,14 @@ case class RestMetadata[T]( (prefixName, prefix) <- prefixes headerParam <- method.headersMetadata.headers.keys if prefix.headersMetadata.headers.contains(headerParam) - } throw new InvalidRestApiException( + } throw new RestException( s"Header parameter $headerParam of $methodName collides with header parameter of the same name in prefix $prefixName") for { (prefixName, prefix) <- prefixes queryParam <- method.headersMetadata.query.keys if prefix.headersMetadata.query.contains(queryParam) - } throw new InvalidRestApiException( + } throw new RestException( s"Query parameter $queryParam of $methodName collides with query parameter of the same name in prefix $prefixName") } @@ -57,7 +57,7 @@ case class RestMetadata[T]( val problems = ambiguities.map { case (path, chains) => s"$path may result from multiple calls:\n ${chains.mkString("\n ")}" } - throw new InvalidRestApiException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") + throw new RestException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") } } diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala index 478c21d06..334c8a1e2 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala @@ -56,5 +56,5 @@ object AsRawReal { } object RpcMetadata { - def materializeForRpc[M[_], Real]: M[Real] = macro macros.rpc.RpcMacros.rpcMetadata[M[Real], Real] + def materializeForRpc[M[_], Real]: M[Real] = macro macros.rpc.RpcMacros.rpcMetadata[Real] } diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/OptionLike.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/OptionLike.scala index f87156ec2..f37a39ccf 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/OptionLike.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/OptionLike.scala @@ -12,6 +12,8 @@ trait BaseOptionLike[O, A] extends OptionLike[O] { type Value = A } object OptionLike { + type Aux[O, V] = OptionLike[O] {type Value = V} + implicit def optionOptionLike[A]: BaseOptionLike[Option[A], A] = new BaseOptionLike[Option[A], A] { def none: Option[A] = None def some(value: A): Option[A] = Some(value) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala index e5b5b98cd..28b62d55f 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala @@ -6,7 +6,7 @@ import com.avsystem.commons.macros.rpc.RpcMacros trait RpcMetadataCompanion[M[_]] extends RpcImplicitsProvider { def apply[Real](implicit metadata: M[Real]): M[Real] = metadata - def materializeForRpc[Real]: M[Real] = macro RpcMacros.rpcMetadata[M[Real], Real] + def materializeForRpc[Real]: M[Real] = macro RpcMacros.rpcMetadata[Real] final class Lazy[Real](metadata: => M[Real]) { lazy val value: M[Real] = metadata diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcUtils.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcUtils.scala index 37ba1e753..0484d4242 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcUtils.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcUtils.scala @@ -6,9 +6,9 @@ import com.avsystem.commons.macros.misc.MiscMacros import scala.collection.generic.CanBuildFrom import scala.collection.mutable -/** - * @author ghik - */ +class RpcException(msg: String, cause: Throwable = null) + extends RuntimeException(msg, cause) + object RpcUtils { def createEmpty[Coll](cbf: CanBuildFrom[Nothing, Nothing, Coll]): Coll = createBuilder[Nothing, Coll](cbf, 0).result() @@ -20,13 +20,13 @@ object RpcUtils { } def missingArg(rpcName: String, argName: String): Nothing = - throw new IllegalArgumentException(s"Can't interpret raw RPC call $rpcName: argument $argName is missing") + throw new RpcException(s"Can't interpret raw RPC call $rpcName: argument $argName is missing") def unknownRpc(rpcName: String, rawMethodName: String): Nothing = - throw new IllegalArgumentException(s"RPC $rpcName does not map to raw method $rawMethodName") + throw new RpcException(s"RPC $rpcName does not map to raw method $rawMethodName") def missingOptionalRpc(rawMethodName: String): Nothing = - throw new IllegalArgumentException(s"no matching real method for optional raw method $rawMethodName") + throw new RpcException(s"no matching real method for optional raw method $rawMethodName") def compilationError(error: String): Nothing = macro MiscMacros.compilationError } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index 15576bb75..2c8eee7e1 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -136,15 +136,9 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) """ } - def rpcMetadata[M: WeakTypeTag, Real: WeakTypeTag]: Tree = { + def rpcMetadata[Real: WeakTypeTag]: Tree = { val realRpc = RealRpcTrait(weakTypeOf[Real].dealias) - val metadataTpe = weakTypeOf[M] match { // scalac, why do I have to do this? - case TypeRef(pre, sym, Nil) => - internal.typeRef(pre, sym, List(realRpc.tpe)) - case TypeRef(pre, sym, List(TypeRef(NoPrefix, wc, Nil))) if wc.name == typeNames.WILDCARD => - internal.typeRef(pre, sym, List(realRpc.tpe)) - case t => t - } + val metadataTpe = c.macroApplication.tpe.dealias val constructor = new RpcMetadataConstructor(metadataTpe, None) // separate object for cached implicits so that lazy vals are members instead of local variables From 798daf95c4bf0dcaa00b70e0c584e23cb02a4f3c Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 11:40:06 +0200 Subject: [PATCH 70/91] rest exceptions fixed --- .../scala/com/avsystem/commons/rest/RestMetadata.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index 6de358c98..8d5853367 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -26,14 +26,14 @@ case class RestMetadata[T]( (prefixName, prefix) <- prefixes headerParam <- method.headersMetadata.headers.keys if prefix.headersMetadata.headers.contains(headerParam) - } throw new RestException( + } throw new InvalidRestApiException( s"Header parameter $headerParam of $methodName collides with header parameter of the same name in prefix $prefixName") for { (prefixName, prefix) <- prefixes queryParam <- method.headersMetadata.query.keys if prefix.headersMetadata.query.contains(queryParam) - } throw new RestException( + } throw new InvalidRestApiException( s"Query parameter $queryParam of $methodName collides with query parameter of the same name in prefix $prefixName") } @@ -57,7 +57,7 @@ case class RestMetadata[T]( val problems = ambiguities.map { case (path, chains) => s"$path may result from multiple calls:\n ${chains.mkString("\n ")}" } - throw new RestException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") + throw new InvalidRestApiException(s"REST API has ambiguous paths:\n${problems.mkString("\n")}") } } @@ -214,4 +214,4 @@ case class HeaderParamMetadata[T]() extends TypedMetadata[T] case class QueryParamMetadata[T]() extends TypedMetadata[T] case class BodyParamMetadata[T](@isAnnotated[Body] singleBody: Boolean) extends TypedMetadata[T] -class InvalidRestApiException(msg: String) extends RuntimeException(msg) +class InvalidRestApiException(msg: String) extends RestException(msg) From 182df946adda43617966c7b0afc77da8c68e5df8 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 12:18:31 +0200 Subject: [PATCH 71/91] added method metadata to REST call resolution --- .../scala/com/avsystem/commons/rest/RawRest.scala | 12 ++++++------ .../com/avsystem/commons/rest/RestMetadata.scala | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index ad45cd717..6abbb9cb6 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -3,10 +3,10 @@ package rest import com.avsystem.commons.rpc._ -case class RpcWithPath(rpcName: String, pathParams: List[PathValue]) -case class ResolvedPath(prefixes: List[RpcWithPath], finalCall: RpcWithPath, singleBody: Boolean) { - def prepend(rpcName: String, pathParams: List[PathValue]): ResolvedPath = - copy(prefixes = RpcWithPath(rpcName, pathParams) :: prefixes) +case class RestMethodCall(rpcName: String, pathParams: List[PathValue], metadata: RestMethodMetadata[_]) +case class ResolvedPath(prefixes: List[RestMethodCall], finalCall: RestMethodCall, singleBody: Boolean) { + def prepend(rpcName: String, pathParams: List[PathValue], metadata: PrefixMetadata[_]): ResolvedPath = + copy(prefixes = RestMethodCall(rpcName, pathParams, metadata) :: prefixes) def rpcChainRepr: String = prefixes.iterator.map(_.rpcName).mkString("", "->", s"->${finalCall.rpcName}") @@ -41,9 +41,9 @@ trait RawRest { metadata.ensureUniqueParams(Nil) locally[RawRest.HandleRequest] { case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { - case List(ResolvedPath(prefixes, RpcWithPath(finalRpcName, finalPathParams), singleBody)) => + case List(ResolvedPath(prefixes, RestMethodCall(finalRpcName, finalPathParams, _), singleBody)) => val finalRawRest = prefixes.foldLeft(this) { - case (rawRest, RpcWithPath(rpcName, pathParams)) => + case (rawRest, RestMethodCall(rpcName, pathParams, _)) => rawRest.prefix(rpcName, headers.copy(path = pathParams)) } val finalHeaders = headers.copy(path = finalPathParams) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index 8d5853367..93e824cd7 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -65,13 +65,13 @@ case class RestMetadata[T]( val asFinalCall = for { (rpcName, m) <- httpMethods.iterator if m.method == method (pathParams, Nil) <- m.extractPathParams(path) - } yield ResolvedPath(Nil, RpcWithPath(rpcName, pathParams), m.singleBody) + } yield ResolvedPath(Nil, RestMethodCall(rpcName, pathParams, m), m.singleBody) val usingPrefix = for { (rpcName, prefix) <- prefixMethods.iterator (pathParams, pathTail) <- prefix.extractPathParams(path).iterator suffixPath <- prefix.result.value.resolvePath(method, pathTail) - } yield suffixPath.prepend(rpcName, pathParams) + } yield suffixPath.prepend(rpcName, pathParams, prefix) asFinalCall ++ usingPrefix } From 51a8fa52099471a0d9b609d556a96cd8297176fe Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 12:19:27 +0200 Subject: [PATCH 72/91] fixed one more exception --- .../src/main/scala/com/avsystem/commons/rest/RawRest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 6abbb9cb6..88dc87d38 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -82,7 +82,7 @@ object RawRest extends RawRpcCompanion[RawRest] { def prefix(name: String, headers: RestHeaders): RawRest = { val prefixMeta = metadata.prefixMethods.getOrElse(name, - throw new IllegalArgumentException(s"no such prefix method: $name")) + throw new RestException(s"no such prefix method: $name")) val newHeaders = prefixHeaders.append(prefixMeta, headers) new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest) } From bf34140f30f999dd40167cce383cc50dc1658151 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 13:29:06 +0200 Subject: [PATCH 73/91] introduced Fallback for cancelling higher priority of imported implicits --- .../rest/DefaultRestApiCompanion.scala | 20 ++++++++++--------- .../com/avsystem/commons/rpc/AsRawReal.scala | 4 ++++ .../commons/rpc/RpcMacroInstances.scala | 17 ++++++++++++++++ .../commons/rpc/RpcMetadataCompanion.scala | 2 ++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala index 92fb316f6..944fb17cf 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala @@ -2,7 +2,7 @@ package com.avsystem.commons package rest import com.avsystem.commons.rest.RawRest.{AsRawRealRpc, AsRawRpc, AsRealRpc} -import com.avsystem.commons.rpc.{AsRawReal, RpcMacroInstances} +import com.avsystem.commons.rpc.{AsRawReal, Fallback, RpcMacroInstances} import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput} import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} @@ -54,14 +54,16 @@ abstract class RestApiCompanion[Implicits, Real](implicits: Implicits)( * [[com.avsystem.commons.serialization.GenKeyCodec GenKeyCodec]] based serialization for REST API traits. */ trait DefaultRestImplicits extends FloatingPointRestImplicits { - implicit def pathValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[PathValue, T] = - AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[HeaderValue, T] = - AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: AsRawReal[QueryValue, T] = - AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value)) - implicit def jsonValueDefaultAsRealRaw[T: GenCodec]: AsRawReal[JsonValue, T] = - AsRawReal.create(v => JsonValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value)) + // Implicits wrapped into `Fallback` so that they don't get higher priority just because they're imported + // This way concrete classes may override these implicits with implicits in their companion objects + implicit def pathValueFallbackAsRealRaw[T: GenKeyCodec]: Fallback[AsRawReal[PathValue, T]] = + Fallback(AsRawReal.create(v => PathValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value))) + implicit def headerValueDefaultAsRealRaw[T: GenKeyCodec]: Fallback[AsRawReal[HeaderValue, T]] = + Fallback(AsRawReal.create(v => HeaderValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value))) + implicit def queryValueDefaultAsRealRaw[T: GenKeyCodec]: Fallback[AsRawReal[QueryValue, T]] = + Fallback(AsRawReal.create(v => QueryValue(GenKeyCodec.write[T](v)), v => GenKeyCodec.read[T](v.value))) + implicit def jsonValueDefaultAsRealRaw[T: GenCodec]: Fallback[AsRawReal[JsonValue, T]] = + Fallback(AsRawReal.create(v => JsonValue(JsonStringOutput.write[T](v)), v => JsonStringInput.read[T](v.value))) } object DefaultRestImplicits extends DefaultRestImplicits diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala index 334c8a1e2..2d4686240 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala @@ -15,6 +15,7 @@ object AsRaw { def asRaw(real: Real): Raw = asRawFun(real) } implicit def identity[A]: AsRaw[A, A] = AsRawReal.identity[A] + implicit def fromFallback[Raw, Real](implicit fallback: Fallback[AsRaw[Raw, Real]]): AsRaw[Raw, Real] = fallback.value def materializeForRpc[Raw, Real]: AsRaw[Raw, Real] = macro macros.rpc.RpcMacros.rpcAsRaw[Raw, Real] } @@ -30,6 +31,7 @@ object AsReal { def asReal(raw: Raw): Real = asRealFun(raw) } implicit def identity[A]: AsReal[A, A] = AsRawReal.identity[A] + implicit def fromFallback[Raw, Real](implicit fallback: Fallback[AsReal[Raw, Real]]): AsReal[Raw, Real] = fallback.value def materializeForRpc[Raw, Real]: AsReal[Raw, Real] = macro macros.rpc.RpcMacros.rpcAsReal[Raw, Real] } @@ -52,6 +54,8 @@ object AsRawReal { implicit def identity[A]: AsRawReal[A, A] = reusableIdentity.asInstanceOf[AsRawReal[A, A]] + implicit def fromFallback[Raw, Real](implicit fallback: Fallback[AsRawReal[Raw, Real]]): AsRawReal[Raw, Real] = fallback.value + def materializeForRpc[Raw, Real]: AsRawReal[Raw, Real] = macro macros.rpc.RpcMacros.rpcAsRawReal[Raw, Real] } diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala index 02588b44f..e0ed1cf41 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMacroInstances.scala @@ -66,3 +66,20 @@ object RpcMacroInstances { implicit def materialize[Implicits, InstancesTrait[_], Real]: RpcMacroInstances[Implicits, InstancesTrait, Real] = macro macros.rpc.RpcMacros.macroInstances } + +/** + * Wrap your implicit instance of [[AsReal]], [[AsRaw]], [[AsRawReal]] or RPC metadata (with companion that extends + * [[RpcMetadataCompanion]] into `Fallback` in order to lower its implicit priority. + * Useful when some implicit must be imported but we don't want it to get higher priority that imports normally + * have over implicit scope (e.g. implicits from companion objects). + * + * NOTE: `Fallback` does not work for *all* typeclasses, only RPC-related ones (`AsReal`, `AsRaw`, etc). + * You can make it work with your own typeclass, but you must define appropriate forwarder in its companion, e.g. + * {{{ + * trait FallbackAwareTC[T] { ... } + * object FallbackAwareTC { + * implicit def fromFallback[T](implicit f: Fallback[FallbackAwareTC[T]]): FallbackAwareTC[T] = f.value + * } + * }}} + */ +case class Fallback[+T](value: T) extends AnyVal diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala index 28b62d55f..209d812af 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala @@ -8,6 +8,8 @@ trait RpcMetadataCompanion[M[_]] extends RpcImplicitsProvider { def materializeForRpc[Real]: M[Real] = macro RpcMacros.rpcMetadata[Real] + implicit def fromFallback[Real](implicit fallback: Fallback[M[Real]]): M[Real] = fallback.value + final class Lazy[Real](metadata: => M[Real]) { lazy val value: M[Real] = metadata } From 76f33ca560bc50f2683fa51c49133237de59be65 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 13:38:27 +0200 Subject: [PATCH 74/91] cosmetic finals --- .../scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala index 209d812af..0af6c493a 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/RpcMetadataCompanion.scala @@ -4,11 +4,11 @@ package rpc import com.avsystem.commons.macros.rpc.RpcMacros trait RpcMetadataCompanion[M[_]] extends RpcImplicitsProvider { - def apply[Real](implicit metadata: M[Real]): M[Real] = metadata + final def apply[Real](implicit metadata: M[Real]): M[Real] = metadata def materializeForRpc[Real]: M[Real] = macro RpcMacros.rpcMetadata[Real] - implicit def fromFallback[Real](implicit fallback: Fallback[M[Real]]): M[Real] = fallback.value + implicit final def fromFallback[Real](implicit fallback: Fallback[M[Real]]): M[Real] = fallback.value final class Lazy[Real](metadata: => M[Real]) { lazy val value: M[Real] = metadata From f6bd2b92c1c13b0b68826a4d932838dc16188561 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 14:55:28 +0200 Subject: [PATCH 75/91] REST serialization documentation --- .../commons/ser/CirceRestImplicits.scala | 16 +++ docs/REST.md | 112 +++++++++++++++++- 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 commons-benchmark/src/main/scala/com/avsystem/commons/ser/CirceRestImplicits.scala diff --git a/commons-benchmark/src/main/scala/com/avsystem/commons/ser/CirceRestImplicits.scala b/commons-benchmark/src/main/scala/com/avsystem/commons/ser/CirceRestImplicits.scala new file mode 100644 index 000000000..85f72061c --- /dev/null +++ b/commons-benchmark/src/main/scala/com/avsystem/commons/ser/CirceRestImplicits.scala @@ -0,0 +1,16 @@ +package com.avsystem.commons +package ser + +import com.avsystem.commons.rest._ +import com.avsystem.commons.rpc._ +import io.circe._ +import io.circe.parser._ +import io.circe.syntax._ + +trait CirceRestImplicits { + implicit def encoderBasedAsRawJson[T: Encoder]: Fallback[AsRaw[JsonValue, T]] = + Fallback(AsRaw.create(v => JsonValue(v.asJson.noSpaces))) + implicit def decoderBasedJsonAsReal[T: Decoder]: Fallback[AsReal[JsonValue, T]] = + Fallback(AsReal.create(json => decode(json.value).fold(throw _, identity))) +} +object CirceRestImplicits extends CirceRestImplicits diff --git a/docs/REST.md b/docs/REST.md index 7d79085b7..45c6e8af1 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -360,7 +360,7 @@ every parameter value and every method result into appropriate raw values which be easily sent through network. Serialization in REST framework is typeclass based, which is a typical, functional and typesafe approach to serialization in Scala. -Examples of typeclass based serialization libraries include [GenCodec](docs/GenCodec.md) +Examples of typeclass based serialization libraries include [GenCodec](GenCodec.md) (which is the default serialization used by this REST framework), [circe](https://circe.github.io/circe/) (one of the most popular JSON libraries for Scala) or [µPickle](http://www.lihaoyi.com/upickle/). Any of these solutions can be plugged into REST framework. @@ -456,12 +456,120 @@ You might want to define custom serialization straight into `Future[RestResponse ### Customizing serialization +#### Introduction + +When Scala compiler needs to find an implicit, it searches two scopes: _lexical_ scope and _implicit_ scope. + +_Lexical scope_ is made of locally visible and imported implicits. It has priority over implicit scope - +implicit scope is searched only when implicit could not be found in lexical scope. + +_Implicit scope_ is made of companion objects of all traits and classes _associated_ with the +type of implicit being searched for. Consult [Scala Language Specification](https://www.scala-lang.org/files/archive/spec/2.12/07-implicits.html) +for precise definition of the word "_associated_". As an example, implicit scope of type `AsRaw[JsonValue,MyClass]` is +made of companion objects of `AsRaw`, `JsonValue`, `MyClass` + companion objects of all supertraits, superclasses and +enclosing traits/classes of `MyClass`. + +Implicits defined in _implicit scope_ are effectively global and don't need to be imported. + #### Plugging in entirely custom serialization -#### Providing serialization for your own type +REST framework deliberately provides **no** default implicits for serialization and deserialization. +Instead, it introduces a mechanism through which serialization implicits are injected by +[companion objects](#companion-objects) of REST API traits. Thanks to this mechanism REST framework is +not bound to any specific serialization library. At the same time it provides a concise method to inject +serialization implicits that does not require importing them explicitly. + +An example usage of this mechanism is `DefaultRestApiCompanion` which injects [`GenCodec`](GenCodec.md)-based +serialization. + +Let's say you want to use e.g. [circe](https://circe.github.io/circe/) for serialization to `JsonValue`. + +First, define a trait that contains implicits which translate Circe's `Encoder` and `Decoder` into +appropriate instances of `AsReal`/`AsRaw`: + +```scala +import com.avsystem.commons.rest._ +import com.avsystem.commons.rpc._ +import io.circe._ +import io.circe.parser._ +import io.circe.syntax._ + +trait CirceRestImplicits { + implicit def encoderBasedAsRawJson[T: Encoder]: Fallback[AsRaw[JsonValue, T]] = + Fallback(AsRaw.create(v => JsonValue(v.asJson.noSpaces))) + implicit def decoderBasedJsonAsReal[T: Decoder]: Fallback[AsReal[JsonValue, T]] = + Fallback(AsReal.create(json => decode(json.value).fold(throw _, identity))) +} +object CirceRestImplicits extends CirceRestImplicits +``` + +Note that implicits are wrapped into `Fallback`. This is not strictly required, but it's recommended +because these implicits ultimately will have to be imported into lexical scope, even if a macro does it +for us. However, we don't want these implicits to have higher priority than implicits from companion objects +of some concrete classes which need custom serialization. Because of that, we wrap our implicits into +`Fallback` which keeps them visible but without elevated priority. + +Now, in order to define a REST API trait that uses Circe-based serialization, you must appropriately +inject it into its companion object: + +```scala +trait MyRestApi { ... } +object MyRestApi extends RestApiCompanion[CirceRestImplicits, MyRestApi](CirceRestImplicits) +``` + +If you happen to use this often (e.g. because you always want to use Circe) then it may be useful +to create convenience companion base class just for Circe: + +```scala +abstract class CirceRestApiCompanion[Real]( + implicit inst: RpcMacroInstances[CirceRestImplicits, FullInstances, Real]) +) extends RestApiCompanion[CirceRestImplicits, Real](CirceRestImplicits) +``` + +Now you can define your trait more concisely as: + +```scala +trait MyRestApi { ... } +object MyRestApi extends CirceRestApiCompanion[MyRestApi] +``` + +#### Customizing serialization for your own type + +If you need to write manual serialization for your own type, the easiest way to do this is to +provide appropriate implicit in its companion object: + +```scala +class MyClass { ... } +object MyClass { + implicit val jsonAsRawReal: AsRawReal[JsonValue, MyClass] = AsRawReal.create(...) +} +``` #### Providing serialization for third party type +If you need to define serialization implicits for a third party type, you can't do it through +implicit scope because you can't modify its companion object. Instead, you can adjust implicits injected +into REST API trait companion object. + +Assume that companion objects of your REST API traits normally extend `DefaultRestApiCompanion`, i.e. +`GenCodec`-based serialization is used. Now, you can extend `DefaultRestImplicits` to add serialization for +third party types: + +```scala +trait EnhancedRestImplicits extends DefaultRestImplicits { + implicit val thirdPartyJsonAsRawReal: AsRawReal[JsonValue, ThirdParty] = + AsRawReal.create(...) +} +object EnhancedRestImplicits extends EnhancedRestImplicits +``` + +Then, you need to define your REST API trait as: + +```scala +trait MyRestApi { ... } +object MyRestApi extends RestApiCompanion[EnhancedRestImplicits, MyRestApi](EnhancedRestImplicits) +``` + ## API evolution ## Implementing backends From d33ed748e18837eb893e2168df064bce126fff81 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 16:06:51 +0200 Subject: [PATCH 76/91] more REST documentation and some helper methods --- .../rest/DefaultRestApiCompanion.scala | 15 ++- docs/REST.md | 122 +++++++++++++++++- 2 files changed, 128 insertions(+), 9 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala index 944fb17cf..72fd80dd9 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala @@ -19,20 +19,26 @@ trait FullInstances[Real] { def asRawReal: AsRawRealRpc[Real] } -/** @see [[RestApiCompanion]]*/ +/** @see [[RestApiCompanion]] */ abstract class RestClientApiCompanion[Implicits, Real](implicits: Implicits)( implicit inst: RpcMacroInstances[Implicits, ClientInstances, Real] ) { implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits).metadata implicit final lazy val restAsReal: AsRealRpc[Real] = inst(implicits).asReal + + final def fromHandleRequest(handleRequest: RawRest.HandleRequest): Real = + RawRest.fromHandleRequest(handleRequest) } -/** @see [[RestApiCompanion]]*/ +/** @see [[RestApiCompanion]] */ abstract class RestServerApiCompanion[Implicits, Real](implicits: Implicits)( implicit inst: RpcMacroInstances[Implicits, ServerInstances, Real] ) { implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits).metadata implicit final lazy val restAsRaw: AsRawRpc[Real] = inst(implicits).asRaw + + final def asHandleRequest(real: Real): RawRest.HandleRequest = + RawRest.asHandleRequest(real) } /** @@ -47,6 +53,11 @@ abstract class RestApiCompanion[Implicits, Real](implicits: Implicits)( ) { implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits).metadata implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = inst(implicits).asRawReal + + final def fromHandleRequest(handleRequest: RawRest.HandleRequest): Real = + RawRest.fromHandleRequest(handleRequest) + final def asHandleRequest(real: Real): RawRest.HandleRequest = + RawRest.asHandleRequest(real) } /** diff --git a/docs/REST.md b/docs/REST.md index 45c6e8af1..140a8438b 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -1,3 +1,41 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [REST framework](#rest-framework) + - [Quickstart example](#quickstart-example) + - [REST API traits](#rest-api-traits) + - [Companion objects](#companion-objects) + - [Manual declaration of implicits](#manual-declaration-of-implicits) + - [HTTP REST methods](#http-rest-methods) + - [Choosing HTTP method](#choosing-http-method) + - [`GET` methods](#get-methods) + - [Customizing paths](#customizing-paths) + - [Path parameters](#path-parameters) + - [Query parameters](#query-parameters) + - [Header parameters](#header-parameters) + - [JSON Body parameters](#json-body-parameters) + - [Single body parameters](#single-body-parameters) + - [Prefix methods](#prefix-methods) + - [Serialization](#serialization) + - [Real and raw values](#real-and-raw-values) + - [Path, query and header serialization](#path-query-and-header-serialization) + - [JSON body parameter serialization](#json-body-parameter-serialization) + - [Single body serialization](#single-body-serialization) + - [Result serialization](#result-serialization) + - [Customizing serialization](#customizing-serialization) + - [Introduction](#introduction) + - [Plugging in entirely custom serialization](#plugging-in-entirely-custom-serialization) + - [Customizing serialization for your own type](#customizing-serialization-for-your-own-type) + - [Providing serialization for third party type](#providing-serialization-for-third-party-type) + - [API evolution](#api-evolution) + - [Implementing backends](#implementing-backends) + - [Handler function](#handler-function) + - [Implementing a server](#implementing-a-server) + - [Implementing a client](#implementing-a-client) + + + # REST framework The commons library contains an RPC based REST framework for defining REST services using Scala traits. @@ -149,7 +187,7 @@ object MyApi extends DefaultRestApiCompanion[MyApi] `DefaultRestApiCompanion` takes a magic implicit parameter generated by a macro which will effectively materialize all the necessary typeclass instances mentioned earlier. The "`Default`" in its name means that -`DefaultRestImplicits` is used as a provider of serialization-related implicits. See [Serialization](#serialization) +`DefaultRestImplicits` is used as a provider of serialization-related implicits. See [serialization](#serialization) for more details on customizing serialization. `DefaultRestApiCompanion` provides all the implicit instances necessary for both the client and server. @@ -193,7 +231,7 @@ method into a HTTP REST call. content type. If response type is `Unit` (method result type is `Future[Unit]`) then empty body is created when serializing and body is ignored when deseriarlizing. -For details on how exactly serialization works and how to customize it, see [Serialization](#serialization). +For details on how exactly serialization works and how to customize it, see [serialization](#serialization). Note that if you don't want to use `Future`, this customization also allows you to use other wrappers for method result types. ### Choosing HTTP method @@ -266,7 +304,7 @@ Calling `getUsername("ID")` will make a HTTP request on path `users/ID/name`. This way you can model completely arbitrary path patterns. Values of path parameters are serialized into `PathValue` objects, -see [Serialization](#serializaton) for more details. +see [serialization](#path-query-and-header-serialization) for more details. ### Query parameters @@ -278,7 +316,7 @@ as query parameters by default, so this is only strictly necessary for non-`GET` URL parameter name. If not specified, trait method parameter name is used. Values of query parameters are serialized into `QueryValue` objects, -see [Serialization](#serializaton) for more details. +see [serialization](#path-query-and-header-serialization) for more details. ### Header parameters @@ -286,7 +324,7 @@ You may also request that some parameter is translated into a HTTP header using It takes an obligatory `name` argument that specifies HTTP header name. Values of header parameters are serialized into `HeaderValue` objects, -see [Serialization](#serializaton) for more details. +see [serialization](#path-query-and-header-serialization) for more details. ### JSON Body parameters @@ -298,7 +336,7 @@ A method annotated as `@GET` having a parameter annotated as `@JsonBodyParam` wi macro engine. JSON body parameters are serialized into `JsonValue` objects, -see [Serialization](#serializaton) for more details. +see [serialization](#json-body-parameter-serialization) for more details. ### Single body parameters @@ -307,8 +345,15 @@ unannotated parameters (implicitly interpreted as `@JsonBodyParam`s). This way t will be serialized straight into HTTP body. This gives you full control over the contents sent in HTTP body and their format (i.e. it no longer needs to be `application/json`). +```scala +case class User(id: String, login: String) +object User extends HasGenCodec[User] + +@PUT def updateUser(@Body user: User): Future[Unit] +``` + Single body parameters are serialized into `HttpBody` objects, -see [Serialization](#serializaton) for more details. +see [serialization](#single-body-serialization) for more details. ### Prefix methods @@ -572,4 +617,67 @@ object MyRestApi extends RestApiCompanion[EnhancedRestImplicits, MyRestApi](Enha ## API evolution +REST framework gives you a certain amount of guarantees about backwards compatibility of your API. +Here's a list of changes that you may safely do to your REST API traits without breaking clients +that still use the old version: + +* Adding new REST methods, as long as paths are still the same and unambiguous. +* Renaming REST methods, as long as old `path` is configured for them explicitly (e.g. `@GET("oldname") def newname(...)`) +* Reordering parameters of your REST methods, except for `@Path` parameters which may be freely intermixed + with other parameters but they must retain the same order relative to each other. +* Splitting parameters into multiple parameter lists or making them `implicit`. +* Renaming `@Path` parameters - their names are not used in REST requests +* Renaming non-`@Path` parameters, as long as the previous name is explicitly configured by + `@Query`, `@Header` or `@JsonBodyParam` annotation. +* Removing non-`@Path` parameters - even if the client sends them, the server will just ignore them. +* Adding new non-`@Path` parameters, as long as default value is provided for them - either as + Scala-level default parameter value or by using `@whenAbsent` annotation. The server will simply + use the default value if parameter is missing in incoming HTTP request. +* Changing parameter or result types or their serialization - as long as serialized formats of new and old type + are compatible. This depends on on the serialization library you're using. If you're using `GenCodec`, consult + [its documentation on retaining backwards compatibility](GenCodec.md#safely-introducing-changes-to-serialized-classes-retaining-backwards-compatibility). + ## Implementing backends + +Core REST framework (`commons-core` module only) does not contain any backends, i.e. there is no actual network +client or server implementation. However, it's fairly easy to build them as most of the heavy-lifting related to +REST is already done by the core framework (its macro engine, in particular). + +Jetty-based `RestClient` and `RestHandler` are provided by `commons-jetty` module. They may serve as simple +reference backend implementation. + +### Handler function + +`RawRest` object defines a type alias: + +```scala +type HandleRequest = RestRequest => Future[RestResponse] +``` + +`RestRequest` is a simple, immutable representation of HTTP request. It contains HTTP method, path, URL +parameters, HTTP header values and a body (`HttpBody`). All data is already in serialized form so it can be +easily sent through network. + +`RestResponse` is, similarly, a simple representation of HTTP response. `RestResponse` is made of HTTP status +code and HTTP body (`HttpBody`, which also contains MIME type). + +### Implementing a server + +An existing implementation of REST API trait can be easily turned into a `HandleRequest` function using `RawRest.asHandleRequest`. + +Therefore, the only thing you need to do to expose your REST API trait as an actual web service it to turn +`HandleRequest` function into a server. This is usually just a matter of translating native HTTP request into `RestRequest`, +passing them to `HandleRequest` function and translating resulting `RestResponse` to native HTTP response. + +See Jetty-based `RestHandler` for an example implementation. + +### Implementing a client + +If you already have a `HandleRequest` function, you can easily turn it into an implementation of desired REST API trait +using `RawRest.fromHandleRequest`. This implementation is a macro-generated proxy which translates actual +method calls into invocations of provided `HandleRequest` function. + +Therefore, the only thing you need to to in order to wrap a native HTTP client into a REST API trait instance is +to turn this native HTTP client into a `HandleRequest` function. + +See Jetty-based `RestClient` for an example implementation. From 59fdb30d686328068ab88f5731510951790f718c Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 16:08:12 +0200 Subject: [PATCH 77/91] table of contents fixed --- docs/REST.md | 65 ++++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/docs/REST.md b/docs/REST.md index 140a8438b..b6c1c617c 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -1,43 +1,42 @@ +# REST framework + **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [REST framework](#rest-framework) - - [Quickstart example](#quickstart-example) - - [REST API traits](#rest-api-traits) - - [Companion objects](#companion-objects) - - [Manual declaration of implicits](#manual-declaration-of-implicits) - - [HTTP REST methods](#http-rest-methods) - - [Choosing HTTP method](#choosing-http-method) - - [`GET` methods](#get-methods) - - [Customizing paths](#customizing-paths) - - [Path parameters](#path-parameters) - - [Query parameters](#query-parameters) - - [Header parameters](#header-parameters) - - [JSON Body parameters](#json-body-parameters) - - [Single body parameters](#single-body-parameters) - - [Prefix methods](#prefix-methods) - - [Serialization](#serialization) - - [Real and raw values](#real-and-raw-values) - - [Path, query and header serialization](#path-query-and-header-serialization) - - [JSON body parameter serialization](#json-body-parameter-serialization) - - [Single body serialization](#single-body-serialization) - - [Result serialization](#result-serialization) - - [Customizing serialization](#customizing-serialization) - - [Introduction](#introduction) - - [Plugging in entirely custom serialization](#plugging-in-entirely-custom-serialization) - - [Customizing serialization for your own type](#customizing-serialization-for-your-own-type) - - [Providing serialization for third party type](#providing-serialization-for-third-party-type) - - [API evolution](#api-evolution) - - [Implementing backends](#implementing-backends) - - [Handler function](#handler-function) - - [Implementing a server](#implementing-a-server) - - [Implementing a client](#implementing-a-client) +- [Quickstart example](#quickstart-example) +- [REST API traits](#rest-api-traits) + - [Companion objects](#companion-objects) + - [Manual declaration of implicits](#manual-declaration-of-implicits) + - [HTTP REST methods](#http-rest-methods) + - [Choosing HTTP method](#choosing-http-method) + - [`GET` methods](#get-methods) + - [Customizing paths](#customizing-paths) + - [Path parameters](#path-parameters) + - [Query parameters](#query-parameters) + - [Header parameters](#header-parameters) + - [JSON Body parameters](#json-body-parameters) + - [Single body parameters](#single-body-parameters) + - [Prefix methods](#prefix-methods) +- [Serialization](#serialization) + - [Real and raw values](#real-and-raw-values) + - [Path, query and header serialization](#path-query-and-header-serialization) + - [JSON body parameter serialization](#json-body-parameter-serialization) + - [Single body serialization](#single-body-serialization) + - [Result serialization](#result-serialization) + - [Customizing serialization](#customizing-serialization) + - [Introduction](#introduction) + - [Plugging in entirely custom serialization](#plugging-in-entirely-custom-serialization) + - [Customizing serialization for your own type](#customizing-serialization-for-your-own-type) + - [Providing serialization for third party type](#providing-serialization-for-third-party-type) +- [API evolution](#api-evolution) +- [Implementing backends](#implementing-backends) + - [Handler function](#handler-function) + - [Implementing a server](#implementing-a-server) + - [Implementing a client](#implementing-a-client) -# REST framework - The commons library contains an RPC based REST framework for defining REST services using Scala traits. It may be used for implementing both client and server side and works in both JVM and JS, as long as appropriate network layer is implemented. For JVM, Jetty-based implementations for client and server From ceb435505431872975eb01bb3b0ed686ac63baa6 Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 16:08:53 +0200 Subject: [PATCH 78/91] minor --- docs/REST.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/REST.md b/docs/REST.md index b6c1c617c..029e526bf 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -1,5 +1,16 @@ # REST framework +The commons library contains an RPC based REST framework for defining REST services using Scala traits. +It may be used for implementing both client and server side and works in both JVM and JS, as long as +appropriate network layer is implemented. For JVM, Jetty-based implementations for client and server +are provided. + +The core of REST framework is platform independent and network-implementation indepenedent and therefore +has no external dependencies. Because of that, it's a part of `commons-core` module. This is enough to +be able to define REST interfaces. But if you want to expose your REST interface through an actual HTTP +server or have an actual HTTP client for that interface, you need separate implementations for that. +The `commons-jetty` module provides Jetty-based implementations for JVM. + **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* @@ -37,17 +48,6 @@ -The commons library contains an RPC based REST framework for defining REST services using Scala traits. -It may be used for implementing both client and server side and works in both JVM and JS, as long as -appropriate network layer is implemented. For JVM, Jetty-based implementations for client and server -are provided. - -The core of REST framework is platform independent and network-implementation indepenedent and therefore -has no external dependencies. Because of that, it's a part of `commons-core` module. This is enough to -be able to define REST interfaces. But if you want to expose your REST interface through an actual HTTP -server or have an actual HTTP client for that interface, you need separate implementations for that. -The `commons-jetty` module provides Jetty-based implementations for JVM. - ## Quickstart example First, make sure appropriate dependencies are configured for your project (assuming SBT): From 37b3b2823ab0a4a514d367e09203351647fd701f Mon Sep 17 00:00:00 2001 From: ghik Date: Thu, 19 Jul 2018 16:16:12 +0200 Subject: [PATCH 79/91] doc -> source links --- docs/REST.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/REST.md b/docs/REST.md index 029e526bf..e12962b87 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -668,7 +668,7 @@ Therefore, the only thing you need to do to expose your REST API trait as an act `HandleRequest` function into a server. This is usually just a matter of translating native HTTP request into `RestRequest`, passing them to `HandleRequest` function and translating resulting `RestResponse` to native HTTP response. -See Jetty-based `RestHandler` for an example implementation. +See [`RestServlet`](../commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala) for an example implementation. ### Implementing a client @@ -679,4 +679,4 @@ method calls into invocations of provided `HandleRequest` function. Therefore, the only thing you need to to in order to wrap a native HTTP client into a REST API trait instance is to turn this native HTTP client into a `HandleRequest` function. -See Jetty-based `RestClient` for an example implementation. +See Jetty-based [`RestClient`](../commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala) for an example implementation. From 9ead1e4693041d8ad310ee35b33fe95d58b20de1 Mon Sep 17 00:00:00 2001 From: ghik Date: Fri, 20 Jul 2018 16:34:33 +0200 Subject: [PATCH 80/91] decoupled REST framework from usage of Futures, updated docs --- .../avsystem/commons/SharedExtensions.scala | 22 +++++-- .../com/avsystem/commons/rest/RawRest.scala | 54 ++++++++++----- .../com/avsystem/commons/rest/data.scala | 54 ++++++++++----- .../commons/rest/AbstractRestCallTest.scala | 9 ++- .../avsystem/commons/rest/RawRestTest.scala | 36 ++++++++-- .../commons/jetty/rest/RestClient.scala | 66 +++++++++---------- .../commons/jetty/rest/RestServlet.scala | 6 +- .../avsystem/commons/jetty/rest/SomeApi.scala | 8 +-- docs/REST.md | 64 ++++++++++++++---- 9 files changed, 218 insertions(+), 101 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index d7872caca..f992de052 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -23,7 +23,7 @@ trait SharedExtensions extends CompatSharedExtensions { implicit def futureOps[A](fut: Future[A]): FutureOps[A] = new FutureOps(fut) - implicit def lazyFutureOps[A](fut: => Future[A]): LazyFutureOps[A] = new LazyFutureOps(fut) + implicit def lazyFutureOps[A](fut: => Future[A]): LazyFutureOps[A] = new LazyFutureOps(() => fut) implicit def futureCompanionOps(fut: Future.type): FutureCompanionOps.type = FutureCompanionOps @@ -31,6 +31,8 @@ trait SharedExtensions extends CompatSharedExtensions { implicit def tryOps[A](tr: Try[A]): TryOps[A] = new TryOps(tr) + implicit def lazyTryOps[A](tr: => Try[A]): LazyTryOps[A] = new LazyTryOps(() => tr) + implicit def tryCompanionOps(trc: Try.type): TryCompanionOps.type = TryCompanionOps implicit def partialFunctionOps[A, B](pf: PartialFunction[A, B]): PartialFunctionOps[A, B] = new PartialFunctionOps(pf) @@ -212,9 +214,7 @@ object SharedExtensions extends SharedExtensions { fut.transformWith(f)(RunNowEC) def wrapToTry: Future[Try[A]] = - fut.mapNow(Success(_)).recoverNow { - case NonFatal(t) => Failure(t) - } + fut.transformNow(Success(_)) /** * Maps a `Future` using [[concurrent.RunNowEC RunNowEC]]. @@ -265,7 +265,7 @@ object SharedExtensions extends SharedExtensions { thenReturn(Future.successful {}) } - class LazyFutureOps[A](fut: => Future[A]) { + class LazyFutureOps[A](private val fut: () => Future[A]) extends AnyVal { /** * Evaluates a left-hand-side expression that returns a `Future` and ensures that all exceptions thrown by * that expression are converted to a failed `Future`. @@ -273,7 +273,7 @@ object SharedExtensions extends SharedExtensions { * `NullPointerException`. */ def catchFailures: Future[A] = { - val result = try fut catch { + val result = try fut() catch { case NonFatal(t) => Future.failed(t) } if (result != null) result else Future.failed(new NullPointerException("null Future")) @@ -356,6 +356,16 @@ object SharedExtensions extends SharedExtensions { if (tr.isFailure) OptArg.Empty else OptArg(tr.get) } + class LazyTryOps[A](private val tr: () => Try[A]) extends AnyVal { + /** + * Evaluates a left-hand side expression that return `Try`, + * catches all exceptions and converts them into a `Failure`. + */ + def catchFailures: Try[A] = try tr() catch { + case NonFatal(t) => Failure(t) + } + } + object TryCompanionOps { import scala.language.higherKinds diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 88dc87d38..3274ec508 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -22,25 +22,25 @@ trait RawRest { @multi @tagged[GET] @paramTag[RestParamTag](defaultTag = new Query) - def get(@methodName name: String, @composite headers: RestHeaders): Future[RestResponse] + def get(@methodName name: String, @composite headers: RestHeaders): RawRest.Async[RestResponse] @multi @tagged[BodyMethodTag](whenUntagged = new POST) @paramTag[RestParamTag](defaultTag = new JsonBodyParam) def handle(@methodName name: String, @composite headers: RestHeaders, - @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): Future[RestResponse] + @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): RawRest.Async[RestResponse] @multi @tagged[BodyMethodTag](whenUntagged = new POST) @paramTag[RestParamTag] def handleSingle(@methodName name: String, @composite headers: RestHeaders, - @encoded @tagged[Body] body: HttpBody): Future[RestResponse] + @encoded @tagged[Body] body: HttpBody): RawRest.Async[RestResponse] def asHandleRequest(metadata: RestMetadata[_]): RawRest.HandleRequest = { metadata.ensureUnambiguousPaths() metadata.ensureUniqueParams(Nil) - locally[RawRest.HandleRequest] { - case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { + RawRest.safeHandle { case RestRequest(method, headers, body) => + metadata.resolvePath(method, headers.path).toList match { case List(ResolvedPath(prefixes, RestMethodCall(finalRpcName, finalPathParams, _), singleBody)) => val finalRawRest = prefixes.foldLeft(this) { case (rawRest, RestMethodCall(rpcName, pathParams, _)) => @@ -48,16 +48,13 @@ trait RawRest { } val finalHeaders = headers.copy(path = finalPathParams) - def result: Future[RestResponse] = - if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) - else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) - else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) - - result.catchFailures + if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) + else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) + else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) case Nil => val pathStr = headers.path.iterator.map(_.value).mkString("/") - Future.successful(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) + RawRest.successfulAsync(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) case multiple => val pathStr = headers.path.iterator.map(_.value).mkString("/") @@ -69,7 +66,32 @@ trait RawRest { } object RawRest extends RawRpcCompanion[RawRest] { - type HandleRequest = RestRequest => Future[RestResponse] + /** + * The most low-level realization of asynchronous computation: + * something that accepts a callback on a value or failure. + */ + type Callback[T] = Try[T] => Unit + type Async[T] = Callback[T] => Unit + type HandleRequest = RestRequest => Async[RestResponse] + + def safeHandle(handleRequest: HandleRequest): HandleRequest = + request => try handleRequest(request) catch { + case HttpErrorException(code, payload) => + successfulAsync(RestResponse(code, payload.fold(HttpBody.empty)(HttpBody.plain))) + case NonFatal(t) => + failingAsync(t) + } + + def safeAsync[T](async: => Async[T]): Async[T] = + try async catch { + case NonFatal(t) => failingAsync(t) + } + + def successfulAsync[T](value: T): Async[T] = + callback => callback(Success(value)) + + def failingAsync[T](cause: Throwable): Async[T] = + callback => callback(Failure(cause)) def fromHandleRequest[Real: AsRealRpc : RestMetadata](handleRequest: HandleRequest): Real = RawRest.asReal(new DefaultRawRest(RestMetadata[Real], RestHeaders.Empty, handleRequest)) @@ -87,13 +109,13 @@ object RawRest extends RawRpcCompanion[RawRest] { new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest) } - def get(name: String, headers: RestHeaders): Future[RestResponse] = + def get(name: String, headers: RestHeaders): Async[RestResponse] = handleSingle(name, headers, HttpBody.Empty) - def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Future[RestResponse] = + def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Async[RestResponse] = handleSingle(name, headers, HttpBody.createJsonBody(body)) - def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Future[RestResponse] = { + def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Async[RestResponse] = { val methodMeta = metadata.httpMethods.getOrElse(name, throw new RestException(s"no such HTTP method: $name")) val newHeaders = prefixHeaders.append(methodMeta, headers) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala index a0cf0d19a..a27f49c59 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala @@ -142,22 +142,46 @@ object RestHeaders { } case class HttpErrorException(code: Int, payload: OptArg[String] = OptArg.Empty) - extends RuntimeException(s"HTTP ERROR $code${payload.fold("")(p => s": $p")}") with NoStackTrace + extends RuntimeException(s"HTTP ERROR $code${payload.fold("")(p => s": $p")}") with NoStackTrace { + def toResponse: RestResponse = + RestResponse(code, payload.fold(HttpBody.empty)(HttpBody.plain)) +} case class RestRequest(method: HttpMethod, headers: RestHeaders, body: HttpBody) -case class RestResponse(code: Int, body: HttpBody) +case class RestResponse(code: Int, body: HttpBody) { + def toHttpError: HttpErrorException = + HttpErrorException(code, body.contentOpt.toOptArg) + def ensure200OK: RestResponse = + if (code == 200) this else throw toHttpError +} + object RestResponse { - implicit def defaultFutureAsRaw[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[Future[RestResponse], Future[T]] = - AsRaw.create(_.transformNow { - case Success(v) => - Success(RestResponse(200, bodyAsRaw.asRaw(v))) - case Failure(HttpErrorException(code, payload)) => - Success(RestResponse(code, payload.fold(HttpBody.empty)(HttpBody.plain))) - case Failure(cause) => Failure(cause) - }) - implicit def defaultFutureAsReal[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[Future[RestResponse], Future[T]] = - AsReal.create(_.mapNow { - case RestResponse(200, body) => bodyAsReal.asReal(body) - case RestResponse(code, body) => throw HttpErrorException(code, body.contentOpt.toOptArg) - }) + class LazyOps(private val resp: () => RestResponse) extends AnyVal { + def recoverHttpError: RestResponse = try resp() catch { + case e: HttpErrorException => e.toResponse + } + } + implicit def lazyOps(resp: => RestResponse): LazyOps = new LazyOps(() => resp) + + def tryToResponse[T](tr: Try[T])(implicit asResponse: AsRaw[RestResponse, T]): Try[RestResponse] = tr match { + case Success(value) => Success(asResponse.asRaw(value)) + case Failure(e: HttpErrorException) => Success(e.toResponse) + case Failure(cause) => Failure(cause) + } + + implicit def bodyBasedFromResponse[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[RestResponse, T] = + AsReal.create(resp => bodyAsReal.asReal(resp.ensure200OK.body)) + + implicit def bodyBasedToResponse[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[RestResponse, T] = + AsRaw.create(value => RestResponse(200, bodyAsRaw.asRaw(value)).recoverHttpError) + + implicit def futureToAsyncResp[T](implicit respAsRaw: AsRaw[RestResponse, T]): AsRaw[RawRest.Async[RestResponse], Future[T]] = + AsRaw.create(f => callback => f.onCompleteNow(t => callback(tryToResponse(t)))) + + implicit def futureFromAsyncResp[T](implicit respAsReal: AsReal[RestResponse, T]): AsReal[RawRest.Async[RestResponse], Future[T]] = + AsReal.create { async => + val promise = Promise[T] + async(t => promise.complete(t.map(respAsReal.asReal))) + promise.future + } } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala index b790b9641..39c272ea9 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/AbstractRestCallTest.scala @@ -12,8 +12,8 @@ object RestEntity extends HasGenCodec[RestEntity] trait RestTestApi { @GET def trivialGet: Future[Unit] - @GET def failingGet: Future[Unit] + @GET def moreFailingGet: Future[Unit] @GET("a/b") def complexGet( @Path("p1") p1: Int, @Path p2: String, @@ -42,6 +42,7 @@ object RestTestApi extends DefaultRestApiCompanion[RestTestApi] { val Impl: RestTestApi = new RestTestApi { def trivialGet: Future[Unit] = Future.unit def failingGet: Future[Unit] = Future.failed(HttpErrorException(503, "nie")) + def moreFailingGet: Future[Unit] = throw HttpErrorException(503, "nie") def complexGet(p1: Int, p2: String, h1: Int, h2: String, q1: Int, q2: String): Future[RestEntity] = Future.successful(RestEntity(s"$p1-$h1-$q1", s"$p2-$h2-$q2")) def multiParamPost(p1: Int, p2: String, h1: Int, h2: String, q1: Int, q2: String, b1: Int, b2: String): Future[RestEntity] = @@ -72,7 +73,7 @@ abstract class AbstractRestCallTest extends FunSuite with ScalaFutures { RawRest.fromHandleRequest[RestTestApi](clientHandle) def testCall[T](call: RestTestApi => Future[T])(implicit pos: Position): Unit = - assert(call(proxy).wrapToTry.futureValue == call(RestTestApi.Impl).wrapToTry.futureValue) + assert(call(proxy).wrapToTry.futureValue == call(RestTestApi.Impl).catchFailures.wrapToTry.futureValue) test("trivial GET") { testCall(_.trivialGet) @@ -82,6 +83,10 @@ abstract class AbstractRestCallTest extends FunSuite with ScalaFutures { testCall(_.failingGet) } + test("more failing GET") { + testCall(_.moreFailingGet) + } + test("complex GET") { testCall(_.complexGet(0, "a/+&", 1, "b/+&", 2, "ć/+&")) } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index 88adc323e..3baba5979 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -27,6 +27,8 @@ object UserApi extends DefaultRestApiCompanion[UserApi] trait RootApi { @Prefix("") def self: UserApi def subApi(id: Int, @Query query: String): UserApi + def fail: Future[Unit] + def failMore: Future[Unit] } object RootApi extends DefaultRestApiCompanion[RootApi] @@ -56,22 +58,28 @@ class RawRestTest extends FunSuite with ScalaFutures { def user(paf: String, awesome: Boolean, f: Int, user: User): Future[Unit] = Future.unit def autopost(bodyarg: String): Future[String] = Future.successful(bodyarg.toUpperCase) def singleBodyAutopost(@Body body: String): Future[String] = Future.successful(body.toUpperCase) + def fail: Future[Unit] = Future.failed(HttpErrorException(400, "zuo")) + def failMore: Future[Unit] = throw HttpErrorException(400, "ZUO") } var trafficLog: String = _ val real: RootApi = new RootApiImpl(0, "") - val serverHandle: RawRest.HandleRequest = request => { - RawRest.asHandleRequest(real).apply(request).andThenNow { - case Success(response) => - trafficLog = s"${repr(request)}\n${repr(response)}\n" + val serverHandle: RawRest.HandleRequest = request => callback => { + RawRest.asHandleRequest(real).apply(request) { result => + callback(result) + result match { + case Success(response) => + trafficLog = s"${repr(request)}\n${repr(response)}\n" + case _ => + } } } val realProxy: RootApi = RawRest.fromHandleRequest[RootApi](serverHandle) def testRestCall[T](call: RootApi => Future[T], expectedTraffic: String)(implicit pos: Position): Unit = { - assert(call(realProxy).futureValue == call(real).futureValue) + assert(call(realProxy).wrapToTry.futureValue == call(real).catchFailures.wrapToTry.futureValue) assert(trafficLog == expectedTraffic) } @@ -120,4 +128,22 @@ class RawRestTest extends FunSuite with ScalaFutures { |""".stripMargin ) } + + test("failing POST") { + testRestCall(_.fail, + """-> POST /fail + |<- 400 text/plain + |zuo + |""".stripMargin + ) + } + + test("throwing POST") { + testRestCall(_.failMore, + """-> POST /failMore + |<- 400 text/plain + |ZUO + |""".stripMargin + ) + } } diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala index 33508abde..de49489ad 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala @@ -15,40 +15,38 @@ object RestClient { def apply[@explicitGenerics Real: RawRest.AsRealRpc : RestMetadata](client: HttpClient, baseUrl: String): Real = RawRest.fromHandleRequest[Real](asHandleRequest(client, baseUrl)) - def asHandleRequest(client: HttpClient, baseUrl: String): RawRest.HandleRequest = request => { - val path = request.headers.path.iterator - .map(pv => URLEncoder.encode(pv.value, "utf-8")) - .mkString(baseUrl.ensureSuffix("/"), "/", "") - - val httpReq = client.newRequest(baseUrl).method(request.method.toString) - - httpReq.path(path) - request.headers.query.foreach { - case (name, QueryValue(value)) => httpReq.param(name, value) - } - request.headers.headers.foreach { - case (name, HeaderValue(value)) => httpReq.header(name, value) - } - - request.body.forNonEmpty { (content, mimeType) => - httpReq.content(new StringContentProvider(s"$mimeType;charset=utf-8", content, StandardCharsets.UTF_8)) - } - - val promise = Promise[RestResponse] - httpReq.send(new BufferingResponseListener() { - override def onComplete(result: Result): Unit = - if (result.isSucceeded) { - val httpResp = result.getResponse - val body = httpResp.getHeaders.get(HttpHeader.CONTENT_TYPE).opt.fold(HttpBody.empty) { contentType => - HttpBody(getContentAsString(), MimeTypes.getContentTypeWithoutCharset(contentType)) + def asHandleRequest(client: HttpClient, baseUrl: String): RawRest.HandleRequest = + RawRest.safeHandle(request => callback => { + val path = request.headers.path.iterator + .map(pv => URLEncoder.encode(pv.value, "utf-8")) + .mkString(baseUrl.ensureSuffix("/"), "/", "") + + val httpReq = client.newRequest(baseUrl).method(request.method.toString) + + httpReq.path(path) + request.headers.query.foreach { + case (name, QueryValue(value)) => httpReq.param(name, value) + } + request.headers.headers.foreach { + case (name, HeaderValue(value)) => httpReq.header(name, value) + } + + request.body.forNonEmpty { (content, mimeType) => + httpReq.content(new StringContentProvider(s"$mimeType;charset=utf-8", content, StandardCharsets.UTF_8)) + } + + httpReq.send(new BufferingResponseListener() { + override def onComplete(result: Result): Unit = + if (result.isSucceeded) { + val httpResp = result.getResponse + val body = httpResp.getHeaders.get(HttpHeader.CONTENT_TYPE).opt.fold(HttpBody.empty) { contentType => + HttpBody(getContentAsString(), MimeTypes.getContentTypeWithoutCharset(contentType)) + } + val response = RestResponse(httpResp.getStatus, body) + callback(Success(response)) + } else { + callback(Failure(result.getFailure)) } - val response = RestResponse(httpResp.getStatus, body) - promise.success(response) - } else { - promise.failure(result.getFailure) - } + }) }) - - promise.future - } } diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala index 96b32c471..b1aec495b 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala @@ -59,17 +59,19 @@ object RestServlet { val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) val asyncContext = request.startAsync() - handleRequest(restRequest).catchFailures.andThenNow { + RawRest.safeAsync(handleRequest(restRequest)) { case Success(restResponse) => response.setStatus(restResponse.code) restResponse.body.forNonEmpty { (content, mimeType) => response.setContentType(s"$mimeType;charset=utf-8") response.getWriter.write(content) } + asyncContext.complete() case Failure(e) => response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500) response.setContentType(MimeTypes.Type.TEXT_PLAIN_UTF_8.asString()) response.getWriter.write(e.getMessage) - }.andThenNow { case _ => asyncContext.complete() } + asyncContext.complete() + } } } diff --git a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala index e7fc85f8f..ac884055f 100644 --- a/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala +++ b/commons-jetty/src/test/scala/com/avsystem/commons/jetty/rest/SomeApi.scala @@ -1,7 +1,7 @@ package com.avsystem.commons package jetty.rest -import com.avsystem.commons.rest.{GET, POST, RawRest, DefaultRestApiCompanion} +import com.avsystem.commons.rest.{DefaultRestApiCompanion, GET, POST} trait SomeApi { @GET @@ -12,12 +12,6 @@ trait SomeApi { } object SomeApi extends DefaultRestApiCompanion[SomeApi] { - def asHandleRequest(real: SomeApi): RawRest.HandleRequest = - RawRest.asHandleRequest(real) - - def fromHandleRequest(handle: RawRest.HandleRequest): SomeApi = - RawRest.fromHandleRequest[SomeApi](handle) - def format(who: String) = s"Hello, $who!" val poison: String = "poison" diff --git a/docs/REST.md b/docs/REST.md index e12962b87..9a525aaf6 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -482,21 +482,29 @@ serializable to `HttpBody`. ### Result serialization -Result type of every REST API method is "serialized" into `Future[RestResponse]`, which means that -macro engine looks for an implicit instance of `AsRaw/AsReal[Future[RestResponse], MethodResultType]`. -`RestResponse` is a simple type that encapsulates HTTP status code and body. +Result type of every REST API method is "serialized" into `RawRest.Async[RestResponse]`. +This means that macro engine looks for an implicit instance of `AsRaw/AsReal[RawRest.Async[RestResponse], R]` +for every HTTP method with result type `R`. -However, `RestResponse` companion object defines a default implicit instance of `AsRaw/Real[Future[RestResponse], Future[T]]` -which depends on implicit `AsRaw/Real[HttpBody, T]`. This default serialization always uses 200 as a status code -for successful `Future[T]` values. In practice this means that if your REST API method returns -`Future[T]` then your `T` type must be serializable to `HttpBody`. Additionally, remember that all types serializable to -`JsonValue` are automatically serializable to `HttpBody`. +`RestResponse` itself is a simple class that aggregates HTTP status code and body. -You might want to define custom serialization straight into `Future[RestResponse]` when: +`RestResponse` companion object defines default implicit instances of `AsRaw/Real[RawRest.Async[RestResponse], Future[R]]` +which depends on implicit `AsRaw/Real[RestResponse, R]`. This effectively means that if your method returns `Future[R]` then +it's enough if `R` is serializable as `RestResponse`. -* You don't want to use `Future` in your REST API traits but some other type that encapsulates asynchronous - computation, e.g. [Monix Task](https://monix.io/docs/2x/eval/task.html) -* Result of your HTTP method determines HTTP status code that you want to be sent back to the client. +However, there are even more defaults provided: if `R` is serializable as `HttpBody` then it's automatically serializable +as `RestResponse`. This default translation of `HttpBody` into `RestResponse` always uses 200 as a status code. +When translating `RestResponse` into `HttpBody` and response contains other status code than 200, `HttpErrorException` is thrown +(which will be subsequently captured into failed `Future`). + +Going even further with defaults, all types serializable as `JsonValue` are serializable as `HttpBody`. +This effectively means that when your method returns `Future[R]` then you can provide serialization +of `R` into any of the following: `JsonValue`, `HttpBody`, `RestResponse` - depending on how much control +you need. + +Ultimately, if you don't want to use `Future`s, you may replace it with some other asynchronous wrapper type, +e.g. Monix Task or some IO monad. +See [supporting result containers other than `Future`](#supporting-result-containers-other-than-future). ### Customizing serialization @@ -614,6 +622,32 @@ trait MyRestApi { ... } object MyRestApi extends RestApiCompanion[EnhancedRestImplicits, MyRestApi](EnhancedRestImplicits) ``` +#### Supporting result containers other than `Future` + +By default, every HTTP method in REST API trait must return its return wrapped into a `Future`. +However, `Future` is low level and limited in many ways (e.g. there is no way to control when the actual +asynchronous computation starts). It is possible to use other task-like containers, e.g. +[Monix Task](https://monix.io/docs/2x/eval/task.html) or [Cats IO](https://typelevel.org/cats-effect/). + +In order to do that, you must provide some additional "serialization" implicits which will translate your +wrapped results into `RawRest.Async[RestResponse]`. Just like when +[providing serialization for third party type](#providing-serialization-for-third-party-type), +you should put that implicit into a trait and inject it into REST API trait's companion object. + +`Async` is defined as: + +```scala +type Callback[T] = Try[T] => Unit +type Async[T] => Callback[T] => Unit +``` + +In other words, `Async[T]` is a consumer of a callback on value of type `Try[T]`. +When `Async[T]` is passed a callback, it should start asynchronous computation of value of type `T` +and notify the callback when its ready (or failed). + +For an example on how to implement these transformations, you can look at how it's done for +`Future`s - see `RestResponse` companion object. + ## API evolution REST framework gives you a certain amount of guarantees about backwards compatibility of your API. @@ -647,10 +681,12 @@ reference backend implementation. ### Handler function -`RawRest` object defines a type alias: +`RawRest` object defines following type aliases: ```scala -type HandleRequest = RestRequest => Future[RestResponse] +type Callback[T] = Try[T] => Unit +type Async[T] = Callback[T] => Unit +type HandleRequest = RestRequest => Async[RestResponse] ``` `RestRequest` is a simple, immutable representation of HTTP request. It contains HTTP method, path, URL From 70fe83459e068f9c8c63621f5da170376151abed Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 23 Jul 2018 11:02:55 +0200 Subject: [PATCH 81/91] introduced HttpResponseType to uncouple RestMetadata from FUture --- .../com/avsystem/commons/rest/RestMetadata.scala | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index 93e824cd7..2f967ea46 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -190,13 +190,25 @@ case class PrefixMetadata[T]( case class HttpMethodMetadata[T]( @reifyAnnot methodTag: HttpMethodTag, @composite headersMetadata: RestHeadersMetadata, - @multi @tagged[BodyTag] bodyParams: Map[String, BodyParamMetadata[_]] -) extends RestMethodMetadata[Future[T]] { + @multi @tagged[BodyTag] bodyParams: Map[String, BodyParamMetadata[_]], + @checked @infer responseType: HttpResponseType[T] +) extends RestMethodMetadata[T] { val method: HttpMethod = methodTag.method val singleBody: Boolean = bodyParams.values.exists(_.singleBody) def methodPath: List[PathValue] = PathValue.split(methodTag.path) } +/** + * Currently just a marker typeclass used by [[RestMetadata]] materialization to distinguish between + * prefix methods and HTTP methods. In the future this typeclass may contain some additional information, e.g. + * type metadata for generating swagger definitions. + */ +trait HttpResponseType[T] +object HttpResponseType { + implicit def forFuture[T]: HttpResponseType[Future[T]] = + new HttpResponseType[Future[T]] {} +} + case class RestHeadersMetadata( @multi @tagged[Path] path: List[PathParamMetadata[_]], @multi @tagged[Header] headers: Map[String, HeaderParamMetadata[_]], From 9292d041bb9e293a79d5bb8482055194827f7138 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 23 Jul 2018 11:03:03 +0200 Subject: [PATCH 82/91] docs updated --- .../com/avsystem/commons/rest/RawRest.scala | 35 +++++++++++++++---- docs/REST.md | 30 ++++++++++------ 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 3274ec508..3b729c37d 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -67,19 +67,42 @@ trait RawRest { object RawRest extends RawRpcCompanion[RawRest] { /** - * The most low-level realization of asynchronous computation: - * something that accepts a callback on a value or failure. + * A callback that gets notified when value of type `T` gets computed or when computation of that value fails. + * Callbacks should never throw exceptions. Preferably, they should be simple notifiers that delegate the real + * work somewhere else, e.g. schedule some handling code on a separate executor + * (e.g. [[scala.concurrent.ExecutionException ExecutionContext]]). */ type Callback[T] = Try[T] => Unit + + /** + * The most low-level, raw type representing an asynchronous, possibly side-effecting operation that yields a + * value of type `T` as a result. + * `Async` is a consumer of a callback. When a callback is passed to `Async`, it should start the operation + * and ultimately notify the callback about the result. Each time the callback is passed, the + * entire operation should be repeated, involving all possible side effects. Operation should never be started + * without the callback being passed (i.e. there should be no observable side effects before a callback is passed). + * Implementation of `Async` should also be prepared to accept a callback before the previous one was notified + * about the result (i.e. it should support concurrent execution). + */ type Async[T] = Callback[T] => Unit + + /** + * Raw type of an operation that executes a [[RestRequest]]. The operation should be run every time the + * resulting `Async` value is passed a callback. It should not be run before that. Each run may involve side + * effects, network communication, etc. Runs may be concurrent. + * Request handlers should never throw exceptions but rather convert them into failing implementation of + * `Async`. One way to do this is by wrapping the handler with [[safeHandle]]. + */ type HandleRequest = RestRequest => Async[RestResponse] + /** + * Ensures that all possible exceptions thrown by a request handler are not propagated but converted into + * an instance of `Async` that notifies its callbacks about the failure. + */ def safeHandle(handleRequest: HandleRequest): HandleRequest = request => try handleRequest(request) catch { - case HttpErrorException(code, payload) => - successfulAsync(RestResponse(code, payload.fold(HttpBody.empty)(HttpBody.plain))) - case NonFatal(t) => - failingAsync(t) + case e: HttpErrorException => successfulAsync(e.toResponse) + case NonFatal(t) => failingAsync(t) } def safeAsync[T](async: => Async[T]): Async[T] = diff --git a/docs/REST.md b/docs/REST.md index 9a525aaf6..394b2431c 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -634,20 +634,27 @@ wrapped results into `RawRest.Async[RestResponse]`. Just like when [providing serialization for third party type](#providing-serialization-for-third-party-type), you should put that implicit into a trait and inject it into REST API trait's companion object. -`Async` is defined as: +Additionally, you should provide an implicit instance of `HttpResponseType`, similar to the one defined +in its companion object for `Future`s. This drives materialization of `RestMetadata` in a similar way +`AsRaw` and `AsReal` drive materialization of real<->raw interface translation. + +Here's an example that shows what exactly must be implemented to add Monix Task support: ```scala -type Callback[T] = Try[T] => Unit -type Async[T] => Callback[T] => Unit +import monix.eval.Task +import com.avsystem.commons.rest.RawRest + +trait MonixTaskRestImplicits { + implicit def taskAsAsync[T](implicit asResp: AsRaw[RestResponse, T]): AsRaw[RawRest.Async[RestResponse], Task[T]] = + AsRaw.create(...) + implicit def asyncAsTask[T](implicit fromResp: AsReal[RestResponse, T]): AsReal[RawRest.Async[RestResponse], Task[T]] = + AsReal.create(...) + implicit def taskResponseType[T]: HttpResponseType[Task[T]] = + new HttpResponseType[Task[T]] {} +} +object MonixTaskRestImplicits ``` -In other words, `Async[T]` is a consumer of a callback on value of type `Try[T]`. -When `Async[T]` is passed a callback, it should start asynchronous computation of value of type `T` -and notify the callback when its ready (or failed). - -For an example on how to implement these transformations, you can look at how it's done for -`Future`s - see `RestResponse` companion object. - ## API evolution REST framework gives you a certain amount of guarantees about backwards compatibility of your API. @@ -715,4 +722,5 @@ method calls into invocations of provided `HandleRequest` function. Therefore, the only thing you need to to in order to wrap a native HTTP client into a REST API trait instance is to turn this native HTTP client into a `HandleRequest` function. -See Jetty-based [`RestClient`](../commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala) for an example implementation. +See Jetty-based [`RestClient`](../commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala) for +an example implementation. From bf5b5f6518fd987baf7af06fb3dc95c44ed3219a Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 23 Jul 2018 11:57:31 +0200 Subject: [PATCH 83/91] scala 2.11 bump in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5fadbc5b8..1379a9930 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: scala scala: - - 2.11.11 + - 2.11.12 - 2.12.6 jdk: From 9611c688421566a06c45ca665399c87a2f52e845 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 23 Jul 2018 12:01:38 +0200 Subject: [PATCH 84/91] cosmetic --- .../scala/com/avsystem/commons/redis/util/SingletonSeq.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commons-redis/src/main/scala/com/avsystem/commons/redis/util/SingletonSeq.scala b/commons-redis/src/main/scala/com/avsystem/commons/redis/util/SingletonSeq.scala index 7a10459b2..cc9d48c2b 100644 --- a/commons-redis/src/main/scala/com/avsystem/commons/redis/util/SingletonSeq.scala +++ b/commons-redis/src/main/scala/com/avsystem/commons/redis/util/SingletonSeq.scala @@ -3,8 +3,8 @@ package redis.util final class SingletonSeq[+A](value: A) extends IIndexedSeq[A] { - def length = 1 - def apply(idx: Int) = idx match { + def length: Int = 1 + def apply(idx: Int): A = idx match { case 0 => value case _ => throw new IndexOutOfBoundsException } From 54f0faee3db3fb4f48022f04ca2f6a16300f2236 Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 23 Jul 2018 15:09:04 +0200 Subject: [PATCH 85/91] REST doctoc update --- docs/REST.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/REST.md b/docs/REST.md index 394b2431c..d90e9a708 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -40,6 +40,7 @@ The `commons-jetty` module provides Jetty-based implementations for JVM. - [Plugging in entirely custom serialization](#plugging-in-entirely-custom-serialization) - [Customizing serialization for your own type](#customizing-serialization-for-your-own-type) - [Providing serialization for third party type](#providing-serialization-for-third-party-type) + - [Supporting result containers other than `Future`](#supporting-result-containers-other-than-future) - [API evolution](#api-evolution) - [Implementing backends](#implementing-backends) - [Handler function](#handler-function) From c91b3b3189c5a2272710196337dbadd4fe7444df Mon Sep 17 00:00:00 2001 From: ghik Date: Mon, 23 Jul 2018 17:22:55 +0200 Subject: [PATCH 86/91] improved exception handling in REST --- .../avsystem/commons/SharedExtensions.scala | 5 +++ .../com/avsystem/commons/rest/RawRest.scala | 34 ++++++++++++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala index f992de052..40619732f 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -415,6 +415,11 @@ object SharedExtensions extends SharedExtensions { case NoValueMarker => Opt.Empty case rawValue => Opt(rawValue.asInstanceOf[B]) } + + def fold[C](a: A)(forEmpty: A => C, forNonEmpty: B => C): C = pf.applyOrElse(a, NoValueMarkerFunc) match { + case NoValueMarker => forEmpty(a) + case rawValue => forNonEmpty(rawValue.asInstanceOf[B]) + } } object PartialFunctionOps { private object NoValueMarker diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 3b729c37d..371ec33ef 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package rest +import java.util.concurrent.atomic.AtomicBoolean + import com.avsystem.commons.rpc._ case class RestMethodCall(rpcName: String, pathParams: List[PathValue], metadata: RestMethodMetadata[_]) @@ -100,21 +102,37 @@ object RawRest extends RawRpcCompanion[RawRest] { * an instance of `Async` that notifies its callbacks about the failure. */ def safeHandle(handleRequest: HandleRequest): HandleRequest = - request => try handleRequest(request) catch { - case e: HttpErrorException => successfulAsync(e.toResponse) - case NonFatal(t) => failingAsync(t) + request => safeAsync(handleRequest(request), { + case e: HttpErrorException => Success(e.toResponse) + case t => Failure(t) + }) + + private def guardedAsync[T](async: Async[T], recovery: Throwable => Try[T]): Async[T] = callback => { + val called = new AtomicBoolean + val guardedCallback: Callback[T] = result => + if (!called.getAndSet(true)) { + callback(result) // may possibly throw but better let it fly rather than catch and ignore + } + try async(guardedCallback) catch { + case NonFatal(t) => + // if callback was already called then we can't do much with the failure, rethrow it + if (!called.getAndSet(true)) callback(recovery(t)) else throw t } + } - def safeAsync[T](async: => Async[T]): Async[T] = - try async catch { - case NonFatal(t) => failingAsync(t) + def safeAsync[T](async: => Async[T], recovery: Throwable => Try[T] = Failure(_: Throwable)): Async[T] = + try guardedAsync(async, recovery) catch { + case NonFatal(t) => readyAsync(recovery(t)) } + def readyAsync[T](result: Try[T]): Async[T] = + callback => callback(result) + def successfulAsync[T](value: T): Async[T] = - callback => callback(Success(value)) + readyAsync(Success(value)) def failingAsync[T](cause: Throwable): Async[T] = - callback => callback(Failure(cause)) + readyAsync(Failure(cause)) def fromHandleRequest[Real: AsRealRpc : RestMetadata](handleRequest: HandleRequest): Real = RawRest.asReal(new DefaultRawRest(RestMetadata[Real], RestHeaders.Empty, handleRequest)) From 0c034b3c3e30f0de40c1daa5f253d0a26cf74f09 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 24 Jul 2018 15:59:54 +0200 Subject: [PATCH 87/91] introduced tried annotation to catch exceptions thrown by real methods --- .../avsystem/commons/rpc/rpcAnnotations.scala | 9 +++ .../com/avsystem/commons/rest/RawRest.scala | 79 +++++++++++-------- .../com/avsystem/commons/rest/data.scala | 24 ++++-- .../com/avsystem/commons/rpc/AsRawReal.scala | 4 + .../commons/macros/MacroCommons.scala | 2 + .../commons/macros/rpc/RpcMacros.scala | 1 + .../commons/macros/rpc/RpcMappings.scala | 20 +++-- .../commons/macros/rpc/RpcSymbols.scala | 1 + docs/REST.md | 17 ++-- 9 files changed, 103 insertions(+), 54 deletions(-) diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala index bc2f003b3..10bdfaf73 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala @@ -271,6 +271,15 @@ final class encoded extends RpcEncoding */ final class verbatim extends RpcEncoding +/** + * When raw method is annotated as `@tried`, invocations of real methods matching that raw method will be + * automatically wrapped into `Try`. Consequently, all real methods will be treated as if their result + * type was `Try[Result]` instead of actual `Result`. For example, if raw method is [[encoded]] and its + * (raw) result is `Raw` then macro engine will search for implicit `AsRaw/Real[Raw,Try[Result]]` instead of just + * `AsRaw/Real[Raw,Result]` + */ +final class tried extends RawMethodAnnotation + /** * Method tagging lets you have more explicit control over which raw methods can match which real methods. * Example: diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 371ec33ef..3468836f8 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -16,44 +16,58 @@ case class ResolvedPath(prefixes: List[RestMethodCall], finalCall: RestMethodCal @methodTag[RestMethodTag] trait RawRest { + + import RawRest._ + @multi + @tried @tagged[Prefix](whenUntagged = new Prefix) @paramTag[RestParamTag](defaultTag = new Path) - def prefix(@methodName name: String, @composite headers: RestHeaders): RawRest + def prefix(@methodName name: String, @composite headers: RestHeaders): Try[RawRest] @multi + @tried @tagged[GET] @paramTag[RestParamTag](defaultTag = new Query) - def get(@methodName name: String, @composite headers: RestHeaders): RawRest.Async[RestResponse] + def get(@methodName name: String, @composite headers: RestHeaders): Async[RestResponse] @multi + @tried @tagged[BodyMethodTag](whenUntagged = new POST) @paramTag[RestParamTag](defaultTag = new JsonBodyParam) def handle(@methodName name: String, @composite headers: RestHeaders, - @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): RawRest.Async[RestResponse] + @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): Async[RestResponse] @multi + @tried @tagged[BodyMethodTag](whenUntagged = new POST) @paramTag[RestParamTag] def handleSingle(@methodName name: String, @composite headers: RestHeaders, - @encoded @tagged[Body] body: HttpBody): RawRest.Async[RestResponse] + @encoded @tagged[Body] body: HttpBody): Async[RestResponse] - def asHandleRequest(metadata: RestMetadata[_]): RawRest.HandleRequest = { + def asHandleRequest(metadata: RestMetadata[_]): HandleRequest = { metadata.ensureUnambiguousPaths() metadata.ensureUniqueParams(Nil) RawRest.safeHandle { case RestRequest(method, headers, body) => metadata.resolvePath(method, headers.path).toList match { case List(ResolvedPath(prefixes, RestMethodCall(finalRpcName, finalPathParams, _), singleBody)) => - val finalRawRest = prefixes.foldLeft(this) { - case (rawRest, RestMethodCall(rpcName, pathParams, _)) => - rawRest.prefix(rpcName, headers.copy(path = pathParams)) + def resolveCall(rawRest: RawRest, prefixes: List[RestMethodCall]): Async[RestResponse] = prefixes match { + case RestMethodCall(rpcName, pathParams, _) :: tail => + rawRest.prefix(rpcName, headers.copy(path = pathParams)) match { + case Success(nextRawRest) => resolveCall(nextRawRest, tail) + case Failure(e: HttpErrorException) => RawRest.successfulAsync(e.toResponse) + case Failure(cause) => RawRest.failingAsync(cause) + } + case Nil => + val finalHeaders = headers.copy(path = finalPathParams) + if (method == HttpMethod.GET) + rawRest.get(finalRpcName, finalHeaders) + else if (singleBody) + rawRest.handleSingle(finalRpcName, finalHeaders, body) + else + rawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) } - val finalHeaders = headers.copy(path = finalPathParams) - - if (method == HttpMethod.GET) finalRawRest.get(finalRpcName, finalHeaders) - else if (singleBody) finalRawRest.handleSingle(finalRpcName, finalHeaders, body) - else finalRawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) - + resolveCall(this, prefixes) case Nil => val pathStr = headers.path.iterator.map(_.value).mkString("/") RawRest.successfulAsync(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) @@ -102,12 +116,9 @@ object RawRest extends RawRpcCompanion[RawRest] { * an instance of `Async` that notifies its callbacks about the failure. */ def safeHandle(handleRequest: HandleRequest): HandleRequest = - request => safeAsync(handleRequest(request), { - case e: HttpErrorException => Success(e.toResponse) - case t => Failure(t) - }) + request => safeAsync(handleRequest(request)) - private def guardedAsync[T](async: Async[T], recovery: Throwable => Try[T]): Async[T] = callback => { + private def guardedAsync[T](async: Async[T]): Async[T] = callback => { val called = new AtomicBoolean val guardedCallback: Callback[T] = result => if (!called.getAndSet(true)) { @@ -116,13 +127,13 @@ object RawRest extends RawRpcCompanion[RawRest] { try async(guardedCallback) catch { case NonFatal(t) => // if callback was already called then we can't do much with the failure, rethrow it - if (!called.getAndSet(true)) callback(recovery(t)) else throw t + if (!called.getAndSet(true)) callback(Failure(t)) else throw t } } - def safeAsync[T](async: => Async[T], recovery: Throwable => Try[T] = Failure(_: Throwable)): Async[T] = - try guardedAsync(async, recovery) catch { - case NonFatal(t) => readyAsync(recovery(t)) + def safeAsync[T](async: => Async[T]): Async[T] = + try guardedAsync(async) catch { + case NonFatal(t) => failingAsync(t) } def readyAsync[T](result: Try[T]): Async[T] = @@ -143,12 +154,11 @@ object RawRest extends RawRpcCompanion[RawRest] { private final class DefaultRawRest(metadata: RestMetadata[_], prefixHeaders: RestHeaders, handleRequest: HandleRequest) extends RawRest { - def prefix(name: String, headers: RestHeaders): RawRest = { - val prefixMeta = metadata.prefixMethods.getOrElse(name, - throw new RestException(s"no such prefix method: $name")) - val newHeaders = prefixHeaders.append(prefixMeta, headers) - new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest) - } + def prefix(name: String, headers: RestHeaders): Try[RawRest] = + metadata.prefixMethods.get(name).map { prefixMeta => + val newHeaders = prefixHeaders.append(prefixMeta, headers) + Success(new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest)) + } getOrElse Failure(new RestException(s"no such prefix method: $name")) def get(name: String, headers: RestHeaders): Async[RestResponse] = handleSingle(name, headers, HttpBody.Empty) @@ -156,12 +166,11 @@ object RawRest extends RawRpcCompanion[RawRest] { def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Async[RestResponse] = handleSingle(name, headers, HttpBody.createJsonBody(body)) - def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Async[RestResponse] = { - val methodMeta = metadata.httpMethods.getOrElse(name, - throw new RestException(s"no such HTTP method: $name")) - val newHeaders = prefixHeaders.append(methodMeta, headers) - handleRequest(RestRequest(methodMeta.method, newHeaders, body)) - } + def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Async[RestResponse] = + metadata.httpMethods.get(name).map { methodMeta => + val newHeaders = prefixHeaders.append(methodMeta, headers) + handleRequest(RestRequest(methodMeta.method, newHeaders, body)) + } getOrElse RawRest.failingAsync(new RestException(s"no such HTTP method: $name")) } } diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala index a27f49c59..652b8d40f 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala @@ -163,10 +163,11 @@ object RestResponse { } implicit def lazyOps(resp: => RestResponse): LazyOps = new LazyOps(() => resp) - def tryToResponse[T](tr: Try[T])(implicit asResponse: AsRaw[RestResponse, T]): Try[RestResponse] = tr match { - case Success(value) => Success(asResponse.asRaw(value)) - case Failure(e: HttpErrorException) => Success(e.toResponse) - case Failure(cause) => Failure(cause) + implicit class TryOps(private val respTry: Try[RestResponse]) extends AnyVal { + def recoverHttpError: Try[RestResponse] = respTry match { + case Failure(e: HttpErrorException) => Success(e.toResponse) + case _ => respTry + } } implicit def bodyBasedFromResponse[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[RestResponse, T] = @@ -175,13 +176,20 @@ object RestResponse { implicit def bodyBasedToResponse[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[RestResponse, T] = AsRaw.create(value => RestResponse(200, bodyAsRaw.asRaw(value)).recoverHttpError) - implicit def futureToAsyncResp[T](implicit respAsRaw: AsRaw[RestResponse, T]): AsRaw[RawRest.Async[RestResponse], Future[T]] = - AsRaw.create(f => callback => f.onCompleteNow(t => callback(tryToResponse(t)))) + implicit def futureToAsyncResp[T]( + implicit respAsRaw: AsRaw[RestResponse, T] + ): AsRaw[RawRest.Async[RestResponse], Try[Future[T]]] = + AsRaw.create { triedFuture => + val future = triedFuture.fold(Future.failed, identity) + callback => future.onCompleteNow(t => callback(t.map(respAsRaw.asRaw).recoverHttpError)) + } - implicit def futureFromAsyncResp[T](implicit respAsReal: AsReal[RestResponse, T]): AsReal[RawRest.Async[RestResponse], Future[T]] = + implicit def futureFromAsyncResp[T]( + implicit respAsReal: AsReal[RestResponse, T] + ): AsReal[RawRest.Async[RestResponse], Try[Future[T]]] = AsReal.create { async => val promise = Promise[T] async(t => promise.complete(t.map(respAsReal.asReal))) - promise.future + Success(promise.future) } } diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala index 2d4686240..9181394db 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rpc/AsRawReal.scala @@ -15,6 +15,8 @@ object AsRaw { def asRaw(real: Real): Raw = asRawFun(real) } implicit def identity[A]: AsRaw[A, A] = AsRawReal.identity[A] + implicit def forTry[Raw, Real](implicit asRaw: AsRaw[Raw, Real]): AsRaw[Try[Raw], Try[Real]] = + AsRaw.create(_.map(asRaw.asRaw)) implicit def fromFallback[Raw, Real](implicit fallback: Fallback[AsRaw[Raw, Real]]): AsRaw[Raw, Real] = fallback.value def materializeForRpc[Raw, Real]: AsRaw[Raw, Real] = macro macros.rpc.RpcMacros.rpcAsRaw[Raw, Real] } @@ -31,6 +33,8 @@ object AsReal { def asReal(raw: Raw): Real = asRealFun(raw) } implicit def identity[A]: AsReal[A, A] = AsRawReal.identity[A] + implicit def forTry[Raw, Real](implicit asReal: AsReal[Raw, Real]): AsReal[Try[Raw], Try[Real]] = + AsReal.create(_.map(asReal.asReal)) implicit def fromFallback[Raw, Real](implicit fallback: Fallback[AsReal[Raw, Real]]): AsReal[Raw, Real] = fallback.value def materializeForRpc[Raw, Real]: AsReal[Raw, Real] = macro macros.rpc.RpcMacros.rpcAsReal[Raw, Real] } diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala index 46d92ac01..3e4d2e42c 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/MacroCommons.scala @@ -35,6 +35,8 @@ trait MacroCommons { bundle => final val NilObj = q"$CollectionPkg.immutable.Nil" final val MapObj = q"$CollectionPkg.immutable.Map" final val MapCls = tq"$CollectionPkg.immutable.Map" + final val TryCls = tq"$ScalaPkg.util.Try" + final val TryObj = q"$ScalaPkg.util.Try" final val MapSym = typeOf[scala.collection.immutable.Map[_, _]].typeSymbol final val FutureSym = typeOf[scala.concurrent.Future[_]].typeSymbol final val OptionClass = definitions.OptionClass diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala index 2c8eee7e1..6a20de8cb 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMacros.scala @@ -38,6 +38,7 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo val RpcEncodingAT: Type = getType(tq"$RpcPackage.RpcEncoding") val VerbatimAT: Type = getType(tq"$RpcPackage.verbatim") val AuxiliaryAT: Type = getType(tq"$RpcPackage.auxiliary") + val TriedAT: Type = getType(tq"$RpcPackage.tried") val MethodTagAT: Type = getType(tq"$RpcPackage.methodTag[_]") val ParamTagAT: Type = getType(tq"$RpcPackage.paramTag[_]") val TaggedAT: Type = getType(tq"$RpcPackage.tagged[_]") diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala index 3104f7fc7..a3ea3552f 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcMappings.scala @@ -311,18 +311,24 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => """ } + private def maybeTry(tree: Tree): Tree = + if (rawMethod.tried) q"$TryObj($tree)" else tree + + private def maybeUntry(tree: Tree): Tree = + if (rawMethod.tried) q"$tree.get" else tree + def realImpl: Tree = q""" def ${realMethod.name}(...${realMethod.paramDecls}): ${realMethod.resultType} = { ..${rawMethod.rawParams.map(rp => rp.localValueDecl(rawValueTree(rp)))} - ${resultEncoding.applyAsReal(q"${rawMethod.owner.safeName}.${rawMethod.name}(...${rawMethod.argLists})")} + ${maybeUntry(resultEncoding.applyAsReal(q"${rawMethod.owner.safeName}.${rawMethod.name}(...${rawMethod.argLists})"))} } """ def rawCaseImpl: Tree = q""" ..${paramMappings.values.filterNot(_.rawParam.auxiliary).flatMap(_.realDecls)} - ${resultEncoding.applyAsRaw(q"${realMethod.owner.safeName}.${realMethod.name}(...${realMethod.argLists})")} + ${resultEncoding.applyAsRaw(maybeTry(q"${realMethod.owner.safeName}.${realMethod.name}(...${realMethod.argLists})"))} """ } @@ -357,19 +363,21 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => private def mappingRes(rawMethod: RawMethod, matchedMethod: MatchedMethod): Res[MethodMapping] = { val realMethod = matchedMethod.real + val realResultType = + if (rawMethod.tried) getType(tq"$TryCls[${realMethod.resultType}]") else realMethod.resultType def resultEncoding: Res[RpcEncoding] = if (rawMethod.verbatimResult) { - if (rawMethod.resultType =:= realMethod.resultType) + if (rawMethod.resultType =:= realResultType) Ok(RpcEncoding.Verbatim(rawMethod.resultType)) else - Fail(s"real result type ${realMethod.resultType} does not match raw result type ${rawMethod.resultType}") + Fail(s"real result type $realResultType does not match raw result type ${rawMethod.resultType}") } else { - val e = RpcEncoding.RealRawEncoding(realMethod.resultType, rawMethod.resultType, None) + val e = RpcEncoding.RealRawEncoding(realResultType, rawMethod.resultType, None) if ((!forAsRaw || e.asRawName != termNames.EMPTY) && (!forAsReal || e.asRealName != termNames.EMPTY)) Ok(e) else Fail(s"no encoding/decoding found between real result type " + - s"${realMethod.resultType} and raw result type ${rawMethod.resultType}") + s"$realResultType and raw result type ${rawMethod.resultType}") } for { diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala index 83eff59b9..70270ca5c 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/RpcSymbols.scala @@ -393,6 +393,7 @@ trait RpcSymbols { this: RpcMacroCommons => def fallbackTag: FallbackTag = owner.fallbackMethodTag val arity: RpcMethodArity = RpcMethodArity.fromAnnotation(this) + val tried: Boolean = annot(TriedAT).nonEmpty val verbatimResult: Boolean = annot(RpcEncodingAT).map(_.tpe <:< VerbatimAT).getOrElse(arity.verbatimByDefault) diff --git a/docs/REST.md b/docs/REST.md index d90e9a708..3b392ff1d 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -230,6 +230,9 @@ method into a HTTP REST call. by default translates it into JSON and creates a `200 OK` response with `application/json` content type. If response type is `Unit` (method result type is `Future[Unit]`) then empty body is created when serializing and body is ignored when deseriarlizing. +* Each method may also throw a `HttpErrorException` (or return failed `Future`). It will be + automatically translated into appropriate HTTP error response with given status code and + plaintext message. For details on how exactly serialization works and how to customize it, see [serialization](#serialization). Note that if you don't want to use `Future`, this customization also allows you to use other wrappers for method result types. @@ -483,13 +486,14 @@ serializable to `HttpBody`. ### Result serialization -Result type of every REST API method is "serialized" into `RawRest.Async[RestResponse]`. -This means that macro engine looks for an implicit instance of `AsRaw/AsReal[RawRest.Async[RestResponse], R]` +Result type of every REST API method is wrapped into `Try` (in case the method throws an exception) +and "serialized" into `RawRest.Async[RestResponse]`. +This means that macro engine looks for an implicit instance of `AsRaw/AsReal[RawRest.Async[RestResponse], Try[R]]` for every HTTP method with result type `R`. `RestResponse` itself is a simple class that aggregates HTTP status code and body. -`RestResponse` companion object defines default implicit instances of `AsRaw/Real[RawRest.Async[RestResponse], Future[R]]` +`RestResponse` companion object defines default implicit instances of `AsRaw/Real[RawRest.Async[RestResponse], Try[Future[R]]]` which depends on implicit `AsRaw/Real[RestResponse, R]`. This effectively means that if your method returns `Future[R]` then it's enough if `R` is serializable as `RestResponse`. @@ -635,6 +639,9 @@ wrapped results into `RawRest.Async[RestResponse]`. Just like when [providing serialization for third party type](#providing-serialization-for-third-party-type), you should put that implicit into a trait and inject it into REST API trait's companion object. +Also note that the macro engine re-wraps the already wrapped result into `Try` in case some REST API method throws an +exception. Therefore, if you want your methods to return `Task[T]` then you need serialization for `Try[Task[T]]`. + Additionally, you should provide an implicit instance of `HttpResponseType`, similar to the one defined in its companion object for `Future`s. This drives materialization of `RestMetadata` in a similar way `AsRaw` and `AsReal` drive materialization of real<->raw interface translation. @@ -646,9 +653,9 @@ import monix.eval.Task import com.avsystem.commons.rest.RawRest trait MonixTaskRestImplicits { - implicit def taskAsAsync[T](implicit asResp: AsRaw[RestResponse, T]): AsRaw[RawRest.Async[RestResponse], Task[T]] = + implicit def taskAsAsync[T](implicit asResp: AsRaw[RestResponse, T]): AsRaw[RawRest.Async[RestResponse], Try[Task[T]]] = AsRaw.create(...) - implicit def asyncAsTask[T](implicit fromResp: AsReal[RestResponse, T]): AsReal[RawRest.Async[RestResponse], Task[T]] = + implicit def asyncAsTask[T](implicit fromResp: AsReal[RestResponse, T]): AsReal[RawRest.Async[RestResponse], Try[Task[T]]] = AsReal.create(...) implicit def taskResponseType[T]: HttpResponseType[Task[T]] = new HttpResponseType[Task[T]] {} From aa59e01fef8386874424364e98534134d795b51a Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 24 Jul 2018 16:19:51 +0200 Subject: [PATCH 88/91] Try.fold in compat extensions --- .../com/avsystem/commons/CompatSharedExtensions.scala | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/commons-core/src/main/scala-2.12/com/avsystem/commons/CompatSharedExtensions.scala b/commons-core/src/main/scala-2.12/com/avsystem/commons/CompatSharedExtensions.scala index 2d11453d6..58d8c39b8 100644 --- a/commons-core/src/main/scala-2.12/com/avsystem/commons/CompatSharedExtensions.scala +++ b/commons-core/src/main/scala-2.12/com/avsystem/commons/CompatSharedExtensions.scala @@ -1,13 +1,22 @@ package com.avsystem.commons -import com.avsystem.commons.CompatSharedExtensions.FutureCompatOps +import com.avsystem.commons.CompatSharedExtensions.{FutureCompatOps, TryCompatOps} trait CompatSharedExtensions { implicit def futureCompatOps[A](fut: Future[A]): FutureCompatOps[A] = new FutureCompatOps(fut) + + implicit def tryCompatOps[A](tr: Try[A]): TryCompatOps[A] = new TryCompatOps(tr) } object CompatSharedExtensions { final class FutureCompatOps[A](private val fut: Future[A]) extends AnyVal { def transformTry[S](f: Try[A] => Try[S])(implicit ec: ExecutionContext): Future[S] = fut.transform(f) } + + final class TryCompatOps[A](private val tr: Try[A]) extends AnyVal { + def fold[U](ft: Throwable => U, fa: A => U): U = tr match { + case Success(a) => fa(a) + case Failure(t) => ft(t) + } + } } From 6b62f786bf54720c7f651de27447baab981a3639 Mon Sep 17 00:00:00 2001 From: ghik Date: Tue, 24 Jul 2018 16:28:38 +0200 Subject: [PATCH 89/91] fixed CompatSharedExtensions --- .../com/avsystem/commons/CompatSharedExtensions.scala | 10 ++++++++++ .../com/avsystem/commons/CompatSharedExtensions.scala | 11 +---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/commons-core/src/main/scala-2.11/com/avsystem/commons/CompatSharedExtensions.scala b/commons-core/src/main/scala-2.11/com/avsystem/commons/CompatSharedExtensions.scala index 6bfe5b344..a94a77e12 100644 --- a/commons-core/src/main/scala-2.11/com/avsystem/commons/CompatSharedExtensions.scala +++ b/commons-core/src/main/scala-2.11/com/avsystem/commons/CompatSharedExtensions.scala @@ -3,11 +3,14 @@ package com.avsystem.commons import com.avsystem.commons.concurrent.RunNowEC trait CompatSharedExtensions { + import CompatSharedExtensions._ implicit def futureCompatOps[A](fut: Future[A]): FutureCompatOps[A] = new FutureCompatOps(fut) implicit def futureCompanionCompatOps(fut: Future.type): FutureCompanionCompatOps.type = FutureCompanionCompatOps + + implicit def tryCompatOps[A](tr: Try[A]): TryCompatOps[A] = new TryCompatOps(tr) } object CompatSharedExtensions { @@ -35,4 +38,11 @@ object CompatSharedExtensions { object FutureCompanionCompatOps { final val unit: Future[Unit] = Future.successful(()) } + + final class TryCompatOps[A](private val tr: Try[A]) extends AnyVal { + def fold[U](ft: Throwable => U, fa: A => U): U = tr match { + case Success(a) => fa(a) + case Failure(t) => ft(t) + } + } } diff --git a/commons-core/src/main/scala-2.12/com/avsystem/commons/CompatSharedExtensions.scala b/commons-core/src/main/scala-2.12/com/avsystem/commons/CompatSharedExtensions.scala index 58d8c39b8..2d11453d6 100644 --- a/commons-core/src/main/scala-2.12/com/avsystem/commons/CompatSharedExtensions.scala +++ b/commons-core/src/main/scala-2.12/com/avsystem/commons/CompatSharedExtensions.scala @@ -1,22 +1,13 @@ package com.avsystem.commons -import com.avsystem.commons.CompatSharedExtensions.{FutureCompatOps, TryCompatOps} +import com.avsystem.commons.CompatSharedExtensions.FutureCompatOps trait CompatSharedExtensions { implicit def futureCompatOps[A](fut: Future[A]): FutureCompatOps[A] = new FutureCompatOps(fut) - - implicit def tryCompatOps[A](tr: Try[A]): TryCompatOps[A] = new TryCompatOps(tr) } object CompatSharedExtensions { final class FutureCompatOps[A](private val fut: Future[A]) extends AnyVal { def transformTry[S](f: Try[A] => Try[S])(implicit ec: ExecutionContext): Future[S] = fut.transform(f) } - - final class TryCompatOps[A](private val tr: Try[A]) extends AnyVal { - def fold[U](ft: Throwable => U, fa: A => U): U = tr match { - case Success(a) => fa(a) - case Failure(t) => ft(t) - } - } } From 21e6e172a619b0681c929fabf436b46e9dc7a7f0 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 25 Jul 2018 12:23:01 +0200 Subject: [PATCH 90/91] refactored resolvePath and renamed RestHeaders to RestParameters --- .../com/avsystem/commons/rest/RawRest.scala | 53 +++++++++---------- .../avsystem/commons/rest/RestMetadata.scala | 52 ++++++++++-------- .../com/avsystem/commons/rest/data.scala | 19 +++---- .../avsystem/commons/rest/RawRestTest.scala | 8 +-- .../commons/jetty/rest/RestClient.scala | 6 +-- .../commons/jetty/rest/RestServlet.scala | 4 +- 6 files changed, 74 insertions(+), 68 deletions(-) diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala index 3468836f8..b57caeede 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RawRest.scala @@ -23,59 +23,54 @@ trait RawRest { @tried @tagged[Prefix](whenUntagged = new Prefix) @paramTag[RestParamTag](defaultTag = new Path) - def prefix(@methodName name: String, @composite headers: RestHeaders): Try[RawRest] + def prefix(@methodName name: String, @composite parameters: RestParameters): Try[RawRest] @multi @tried @tagged[GET] @paramTag[RestParamTag](defaultTag = new Query) - def get(@methodName name: String, @composite headers: RestHeaders): Async[RestResponse] + def get(@methodName name: String, @composite parameters: RestParameters): Async[RestResponse] @multi @tried @tagged[BodyMethodTag](whenUntagged = new POST) @paramTag[RestParamTag](defaultTag = new JsonBodyParam) - def handle(@methodName name: String, @composite headers: RestHeaders, + def handle(@methodName name: String, @composite parameters: RestParameters, @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): Async[RestResponse] @multi @tried @tagged[BodyMethodTag](whenUntagged = new POST) @paramTag[RestParamTag] - def handleSingle(@methodName name: String, @composite headers: RestHeaders, + def handleSingle(@methodName name: String, @composite parameters: RestParameters, @encoded @tagged[Body] body: HttpBody): Async[RestResponse] def asHandleRequest(metadata: RestMetadata[_]): HandleRequest = { metadata.ensureUnambiguousPaths() metadata.ensureUniqueParams(Nil) - RawRest.safeHandle { case RestRequest(method, headers, body) => - metadata.resolvePath(method, headers.path).toList match { - case List(ResolvedPath(prefixes, RestMethodCall(finalRpcName, finalPathParams, _), singleBody)) => + RawRest.safeHandle { case RestRequest(method, parameters, body) => + metadata.resolvePath(method, parameters.path) match { + case Opt(ResolvedPath(prefixes, RestMethodCall(finalRpcName, finalPathParams, _), singleBody)) => def resolveCall(rawRest: RawRest, prefixes: List[RestMethodCall]): Async[RestResponse] = prefixes match { case RestMethodCall(rpcName, pathParams, _) :: tail => - rawRest.prefix(rpcName, headers.copy(path = pathParams)) match { + rawRest.prefix(rpcName, parameters.copy(path = pathParams)) match { case Success(nextRawRest) => resolveCall(nextRawRest, tail) case Failure(e: HttpErrorException) => RawRest.successfulAsync(e.toResponse) case Failure(cause) => RawRest.failingAsync(cause) } case Nil => - val finalHeaders = headers.copy(path = finalPathParams) + val finalParameters = parameters.copy(path = finalPathParams) if (method == HttpMethod.GET) - rawRest.get(finalRpcName, finalHeaders) + rawRest.get(finalRpcName, finalParameters) else if (singleBody) - rawRest.handleSingle(finalRpcName, finalHeaders, body) + rawRest.handleSingle(finalRpcName, finalParameters, body) else - rawRest.handle(finalRpcName, finalHeaders, HttpBody.parseJsonBody(body)) + rawRest.handle(finalRpcName, finalParameters, HttpBody.parseJsonBody(body)) } resolveCall(this, prefixes) - case Nil => - val pathStr = headers.path.iterator.map(_.value).mkString("/") + case Opt.Empty => + val pathStr = parameters.path.iterator.map(_.value).mkString("/") RawRest.successfulAsync(RestResponse(404, HttpBody.plain(s"path $pathStr not found"))) - - case multiple => - val pathStr = headers.path.iterator.map(_.value).mkString("/") - val callsRepr = multiple.iterator.map(p => s" ${p.rpcChainRepr}").mkString("\n", "\n", "") - throw new RestException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") } } } @@ -146,29 +141,29 @@ object RawRest extends RawRpcCompanion[RawRest] { readyAsync(Failure(cause)) def fromHandleRequest[Real: AsRealRpc : RestMetadata](handleRequest: HandleRequest): Real = - RawRest.asReal(new DefaultRawRest(RestMetadata[Real], RestHeaders.Empty, handleRequest)) + RawRest.asReal(new DefaultRawRest(RestMetadata[Real], RestParameters.Empty, handleRequest)) def asHandleRequest[Real: AsRawRpc : RestMetadata](real: Real): HandleRequest = RawRest.asRaw(real).asHandleRequest(RestMetadata[Real]) - private final class DefaultRawRest(metadata: RestMetadata[_], prefixHeaders: RestHeaders, handleRequest: HandleRequest) + private final class DefaultRawRest(metadata: RestMetadata[_], prefixHeaders: RestParameters, handleRequest: HandleRequest) extends RawRest { - def prefix(name: String, headers: RestHeaders): Try[RawRest] = + def prefix(name: String, parameters: RestParameters): Try[RawRest] = metadata.prefixMethods.get(name).map { prefixMeta => - val newHeaders = prefixHeaders.append(prefixMeta, headers) + val newHeaders = prefixHeaders.append(prefixMeta, parameters) Success(new DefaultRawRest(prefixMeta.result.value, newHeaders, handleRequest)) } getOrElse Failure(new RestException(s"no such prefix method: $name")) - def get(name: String, headers: RestHeaders): Async[RestResponse] = - handleSingle(name, headers, HttpBody.Empty) + def get(name: String, parameters: RestParameters): Async[RestResponse] = + handleSingle(name, parameters, HttpBody.Empty) - def handle(name: String, headers: RestHeaders, body: NamedParams[JsonValue]): Async[RestResponse] = - handleSingle(name, headers, HttpBody.createJsonBody(body)) + def handle(name: String, parameters: RestParameters, body: NamedParams[JsonValue]): Async[RestResponse] = + handleSingle(name, parameters, HttpBody.createJsonBody(body)) - def handleSingle(name: String, headers: RestHeaders, body: HttpBody): Async[RestResponse] = + def handleSingle(name: String, parameters: RestParameters, body: HttpBody): Async[RestResponse] = metadata.httpMethods.get(name).map { methodMeta => - val newHeaders = prefixHeaders.append(methodMeta, headers) + val newHeaders = prefixHeaders.append(methodMeta, parameters) handleRequest(RestRequest(methodMeta.method, newHeaders, body)) } getOrElse RawRest.failingAsync(new RestException(s"no such HTTP method: $name")) } diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala index 2f967ea46..76f33d6d6 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/RestMetadata.scala @@ -24,15 +24,15 @@ case class RestMetadata[T]( def ensureUniqueParams(methodName: String, method: RestMethodMetadata[_]): Unit = { for { (prefixName, prefix) <- prefixes - headerParam <- method.headersMetadata.headers.keys - if prefix.headersMetadata.headers.contains(headerParam) + headerParam <- method.parametersMetadata.headers.keys + if prefix.parametersMetadata.headers.contains(headerParam) } throw new InvalidRestApiException( s"Header parameter $headerParam of $methodName collides with header parameter of the same name in prefix $prefixName") for { (prefixName, prefix) <- prefixes - queryParam <- method.headersMetadata.query.keys - if prefix.headersMetadata.query.contains(queryParam) + queryParam <- method.parametersMetadata.query.keys + if prefix.parametersMetadata.query.contains(queryParam) } throw new InvalidRestApiException( s"Query parameter $queryParam of $methodName collides with query parameter of the same name in prefix $prefixName") } @@ -61,19 +61,29 @@ case class RestMetadata[T]( } } - def resolvePath(method: HttpMethod, path: List[PathValue]): Iterator[ResolvedPath] = { - val asFinalCall = for { - (rpcName, m) <- httpMethods.iterator if m.method == method - (pathParams, Nil) <- m.extractPathParams(path) - } yield ResolvedPath(Nil, RestMethodCall(rpcName, pathParams, m), m.singleBody) + def resolvePath(method: HttpMethod, path: List[PathValue]): Opt[ResolvedPath] = { + def resolve(method: HttpMethod, path: List[PathValue]): Iterator[ResolvedPath] = { + val asFinalCall = for { + (rpcName, m) <- httpMethods.iterator if m.method == method + (pathParams, Nil) <- m.extractPathParams(path) + } yield ResolvedPath(Nil, RestMethodCall(rpcName, pathParams, m), m.singleBody) - val usingPrefix = for { - (rpcName, prefix) <- prefixMethods.iterator - (pathParams, pathTail) <- prefix.extractPathParams(path).iterator - suffixPath <- prefix.result.value.resolvePath(method, pathTail) - } yield suffixPath.prepend(rpcName, pathParams, prefix) + val usingPrefix = for { + (rpcName, prefix) <- prefixMethods.iterator + (pathParams, pathTail) <- prefix.extractPathParams(path).iterator + suffixPath <- prefix.result.value.resolvePath(method, pathTail) + } yield suffixPath.prepend(rpcName, pathParams, prefix) - asFinalCall ++ usingPrefix + asFinalCall ++ usingPrefix + } + resolve(method, path).toList match { + case Nil => Opt.Empty + case single :: Nil => Opt(single) + case multiple => + val pathStr = path.iterator.map(_.value).mkString("/") + val callsRepr = multiple.iterator.map(p => s" ${p.rpcChainRepr}").mkString("\n", "\n", "") + throw new RestException(s"path $pathStr is ambiguous, it could map to following calls:$callsRepr") + } } } object RestMetadata extends RpcMetadataCompanion[RestMetadata] { @@ -148,10 +158,10 @@ case class PathParam(parameter: PathParamMetadata[_]) extends PathPatternElement sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { def methodPath: List[PathValue] - def headersMetadata: RestHeadersMetadata + def parametersMetadata: RestParametersMetadata val pathPattern: List[PathPatternElement] = - methodPath.map(PathName) ++ headersMetadata.path.flatMap(pp => PathParam(pp) :: pp.pathSuffix.map(PathName)) + methodPath.map(PathName) ++ parametersMetadata.path.flatMap(pp => PathParam(pp) :: pp.pathSuffix.map(PathName)) def applyPathParams(params: List[PathValue]): List[PathValue] = { def loop(params: List[PathValue], pattern: List[PathPatternElement]): List[PathValue] = @@ -160,7 +170,7 @@ sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { case (_, PathName(patternHead) :: patternTail) => patternHead :: loop(params, patternTail) case (param :: paramsTail, PathParam(_) :: patternTail) => param :: loop(paramsTail, patternTail) case _ => throw new IllegalArgumentException( - s"got ${params.size} path params, expected ${headersMetadata.path.size}") + s"got ${params.size} path params, expected ${parametersMetadata.path.size}") } loop(params, pathPattern) } @@ -181,7 +191,7 @@ sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { case class PrefixMetadata[T]( @reifyAnnot methodTag: Prefix, - @composite headersMetadata: RestHeadersMetadata, + @composite parametersMetadata: RestParametersMetadata, @checked @infer result: RestMetadata.Lazy[T] ) extends RestMethodMetadata[T] { def methodPath: List[PathValue] = PathValue.split(methodTag.path) @@ -189,7 +199,7 @@ case class PrefixMetadata[T]( case class HttpMethodMetadata[T]( @reifyAnnot methodTag: HttpMethodTag, - @composite headersMetadata: RestHeadersMetadata, + @composite parametersMetadata: RestParametersMetadata, @multi @tagged[BodyTag] bodyParams: Map[String, BodyParamMetadata[_]], @checked @infer responseType: HttpResponseType[T] ) extends RestMethodMetadata[T] { @@ -209,7 +219,7 @@ object HttpResponseType { new HttpResponseType[Future[T]] {} } -case class RestHeadersMetadata( +case class RestParametersMetadata( @multi @tagged[Path] path: List[PathParamMetadata[_]], @multi @tagged[Header] headers: Map[String, HeaderParamMetadata[_]], @multi @tagged[Query] query: Map[String, QueryParamMetadata[_]] diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala index 652b8d40f..c092eaeb7 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/data.scala @@ -126,19 +126,20 @@ object HttpMethod extends AbstractValueEnumCompanion[HttpMethod] { final val GET, PUT, POST, PATCH, DELETE: Value = new HttpMethod } -case class RestHeaders( +case class RestParameters( @multi @tagged[Path] path: List[PathValue], @multi @tagged[Header] headers: NamedParams[HeaderValue], @multi @tagged[Query] query: NamedParams[QueryValue] ) { - def append(method: RestMethodMetadata[_], otherHeaders: RestHeaders): RestHeaders = RestHeaders( - path ::: method.applyPathParams(otherHeaders.path), - headers ++ otherHeaders.headers, - query ++ otherHeaders.query - ) + def append(method: RestMethodMetadata[_], otherParameters: RestParameters): RestParameters = + RestParameters( + path ::: method.applyPathParams(otherParameters.path), + headers ++ otherParameters.headers, + query ++ otherParameters.query + ) } -object RestHeaders { - final val Empty = RestHeaders(Nil, NamedParams.empty, NamedParams.empty) +object RestParameters { + final val Empty = RestParameters(Nil, NamedParams.empty, NamedParams.empty) } case class HttpErrorException(code: Int, payload: OptArg[String] = OptArg.Empty) @@ -147,7 +148,7 @@ case class HttpErrorException(code: Int, payload: OptArg[String] = OptArg.Empty) RestResponse(code, payload.fold(HttpBody.empty)(HttpBody.plain)) } -case class RestRequest(method: HttpMethod, headers: RestHeaders, body: HttpBody) +case class RestRequest(method: HttpMethod, parameters: RestParameters, body: HttpBody) case class RestResponse(code: Int, body: HttpBody) { def toHttpError: HttpErrorException = HttpErrorException(code, body.contentOpt.toOptArg) diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala index 3baba5979..725b400d2 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RawRestTest.scala @@ -39,11 +39,11 @@ class RawRestTest extends FunSuite with ScalaFutures { } def repr(req: RestRequest): String = { - val pathRepr = req.headers.path.map(_.value).mkString("/", "/", "") - val queryRepr = req.headers.query.iterator + val pathRepr = req.parameters.path.map(_.value).mkString("/", "/", "") + val queryRepr = req.parameters.query.iterator .map({ case (k, v) => s"$k=${v.value}" }).mkStringOrEmpty("?", "&", "") - val hasHeaders = req.headers.headers.nonEmpty - val headersRepr = req.headers.headers.iterator + val hasHeaders = req.parameters.headers.nonEmpty + val headersRepr = req.parameters.headers.iterator .map({ case (n, v) => s"$n: ${v.value}" }).mkStringOrEmpty("\n", "\n", "\n") s"-> ${req.method} $pathRepr$queryRepr$headersRepr${repr(req.body, hasHeaders)}".trim } diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala index de49489ad..9a16e13a0 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestClient.scala @@ -17,17 +17,17 @@ object RestClient { def asHandleRequest(client: HttpClient, baseUrl: String): RawRest.HandleRequest = RawRest.safeHandle(request => callback => { - val path = request.headers.path.iterator + val path = request.parameters.path.iterator .map(pv => URLEncoder.encode(pv.value, "utf-8")) .mkString(baseUrl.ensureSuffix("/"), "/", "") val httpReq = client.newRequest(baseUrl).method(request.method.toString) httpReq.path(path) - request.headers.query.foreach { + request.parameters.query.foreach { case (name, QueryValue(value)) => httpReq.param(name, value) } - request.headers.headers.foreach { + request.parameters.headers.foreach { case (name, HeaderValue(value)) => httpReq.header(name, value) } diff --git a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala index b1aec495b..0624afe3e 100644 --- a/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala +++ b/commons-jetty/src/main/scala/com/avsystem/commons/jetty/rest/RestServlet.scala @@ -5,7 +5,7 @@ import java.net.URLDecoder import java.util.regex.Pattern import com.avsystem.commons.annotation.explicitGenerics -import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RawRest, RestHeaders, RestMetadata, RestRequest} +import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RawRest, RestParameters, RestMetadata, RestRequest} import com.avsystem.commons.rpc.NamedParams import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.http.{HttpStatus, MimeTypes} @@ -56,7 +56,7 @@ object RestServlet { .foreach(bodyBuilder.appendCodePoint) HttpBody(bodyBuilder.toString, MimeTypes.getContentTypeWithoutCharset(contentType)) } - val restRequest = RestRequest(method, RestHeaders(path, headers, query), body) + val restRequest = RestRequest(method, RestParameters(path, headers, query), body) val asyncContext = request.startAsync() RawRest.safeAsync(handleRequest(restRequest)) { From 5f9211b59550463f03b5982c7cd94f0364effb37 Mon Sep 17 00:00:00 2001 From: ghik Date: Wed, 25 Jul 2018 12:32:10 +0200 Subject: [PATCH 91/91] doc fix --- .../main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala index 10bdfaf73..d3ccd49db 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/rpc/rpcAnnotations.scala @@ -130,8 +130,8 @@ final class optional extends RpcArity /** * When applied on raw method, specifies that this raw method may be matched by many, arbitrarily named real methods. - * In order to distinguish between real methods, multi raw method must take real method's RPC name - * (a `String`) as its first parameter and the only parameter in its first parameter list. + * In order to distinguish between real methods when translating raw call into real call, + * multi raw method must take real method's RPC name (a `String`) as one of its parameters (see [[methodName]]). * By default, result type of multi raw method is [[encoded]] and the macro engine searches for * appropriate `AsRaw` or `AsReal` conversion between real method result type and raw method result type. *