diff --git a/build.sbt b/build.sbt index 53ff33127..4a1827ad1 100644 --- a/build.sbt +++ b/build.sbt @@ -175,11 +175,13 @@ def sourceDirsSettings(baseMapper: File => File) = Seq( ) lazy val `commons-annotations` = project + .dependsOn(`commons-macros`) .settings(jvmCommonSettings) lazy val `commons-annotations-js` = project.in(`commons-annotations`.base / "js") .enablePlugins(ScalaJSPlugin) .configure(p => if (forIdeaImport) p.dependsOn(`commons-annotations`) else p) + .dependsOn(`commons-macros`) .settings( jsCommonSettings, name := (name in `commons-annotations`).value, diff --git a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/AkkaRPCFramework.scala b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/AkkaRPCFramework.scala index 8ba682447..794326306 100644 --- a/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/AkkaRPCFramework.scala +++ b/commons-akka/src/main/scala/com/avsystem/commons/rpc/akka/AkkaRPCFramework.scala @@ -5,9 +5,10 @@ import java.io.{DataInputStream, DataOutputStream} import akka.actor.{ActorRef, ActorSystem} import akka.util.ByteString +import com.avsystem.commons.meta._ +import com.avsystem.commons.rpc._ import com.avsystem.commons.rpc.akka.client.ClientRawRPC import com.avsystem.commons.rpc.akka.server.ServerActor -import com.avsystem.commons.rpc.{FunctionRPCFramework, GetterRPCFramework, MetadataAnnotation, ProcedureRPCFramework, RpcMetadataCompanion, TypedMetadata, infer, multi, reifyAnnot, reifyName, verbatim} import com.avsystem.commons.serialization.{GenCodec, StreamInput, StreamOutput} import monix.reactive.Observable @@ -31,10 +32,10 @@ object AkkaRPCFramework extends GetterRPCFramework with ProcedureRPCFramework wi case class RPCMetadata[T]( @reifyName name: String, @reifyAnnot @multi annotations: List[MetadataAnnotation], - @multi @verbatim procedureSignatures: Map[String, ProcedureSignature], - @multi functionSignatures: Map[String, FunctionSignature[_]], - @multi observeSignatures: Map[String, ObserveSignature[_]], - @multi getterSignatures: Map[String, GetterSignature[_]] + @multi @verbatim @rpcMethodMetadata procedureSignatures: Map[String, ProcedureSignature], + @multi @rpcMethodMetadata functionSignatures: Map[String, FunctionSignature[_]], + @multi @rpcMethodMetadata observeSignatures: Map[String, ObserveSignature[_]], + @multi @rpcMethodMetadata getterSignatures: Map[String, GetterSignature[_]] ) object RPCMetadata extends RpcMetadataCompanion[RPCMetadata] 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 bb402e0c4..e1daf09e4 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 @@ -1,6 +1,7 @@ package com.avsystem.commons package rpc.akka +import com.avsystem.commons.meta._ import com.avsystem.commons.rpc._ import monix.reactive.Observable diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/annotation/NotInheritedFromSealedTypes.scala b/commons-annotations/src/main/scala/com/avsystem/commons/annotation/NotInheritedFromSealedTypes.scala new file mode 100644 index 000000000..9944cfe6d --- /dev/null +++ b/commons-annotations/src/main/scala/com/avsystem/commons/annotation/NotInheritedFromSealedTypes.scala @@ -0,0 +1,11 @@ +package com.avsystem.commons +package annotation + +import scala.annotation.StaticAnnotation + +/** + * Marker trait for annotations which don't want to be inherited by subtypes + * of a sealed trait or class that has this annotation applied. Intended for annotations that should apply + * only to the sealed trait itself. + */ +trait NotInheritedFromSealedTypes extends StaticAnnotation diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/annotation/positioned.scala b/commons-annotations/src/main/scala/com/avsystem/commons/annotation/positioned.scala new file mode 100644 index 000000000..81a01268f --- /dev/null +++ b/commons-annotations/src/main/scala/com/avsystem/commons/annotation/positioned.scala @@ -0,0 +1,16 @@ +package com.avsystem.commons +package annotation + +import scala.annotation.StaticAnnotation + +/** + * Annotate a symbol (i.e. class, method, parameter, etc.) with `@positioned(positioned.here)` to retain source + * position information for that symbol to be available in macro implementations which inspect that symbol. + * This is necessary e.g. for determining declaration order of subtypes of sealed hierarchies in macro implementations. + * This annotation is only needed when macro is invoked in a different source file than the source file of inspected + * symbol. If macro is invoked in the same file, source position is always available. + */ +class positioned(val point: Int) extends StaticAnnotation +object positioned { + def here: Int = macro macros.misc.MiscMacros.posPoint +} diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/meta/metaAnnotations.scala b/commons-annotations/src/main/scala/com/avsystem/commons/meta/metaAnnotations.scala new file mode 100644 index 000000000..102b490de --- /dev/null +++ b/commons-annotations/src/main/scala/com/avsystem/commons/meta/metaAnnotations.scala @@ -0,0 +1,261 @@ +package com.avsystem.commons +package meta + +import scala.annotation.StaticAnnotation + +/** + * For annotations applied on real RPC traits, their methods and parameters. + */ +trait RealSymAnnotation extends StaticAnnotation + +/** + * For annotations applied on raw RPC traits, their methods, parameters and also metadata parameters for + * RPC traits and data types. They specify how real symbols are matched against raw symbols (e.g. real RPC + * method against raw RPC methods). + */ +trait RawSymAnnotation extends StaticAnnotation +trait RawMethodAnnotation extends RawSymAnnotation +trait RawParamAnnotation extends RawSymAnnotation + +/** + * 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: + * + * - raw methods + * - raw parameters + * - metadata parameters for raw methods and raw parameters + * - metadata parameters that reify annotations (i.e. annotated with [[reifyAnnot]]) + * + * See documentation of each arity annotation for more details. + */ +sealed trait SymbolArity extends RawParamAnnotation + +/** + * The default arity annotation. Usually there is no need to use this annotation explicitly. + * + * When applied on raw method, there must be exactly one real method matching this raw method and it must have + * the same name (or [[com.avsystem.commons.rpc.rpcName rpcName]]) as raw method's name. + * + * When applied on raw parameter, specifies that this raw parameter must be matched by exactly one real parameter, + * on the position that matches raw parameter's position. Names are ignored because - unlike methods - parameters are + * identified by their position in parameter list(s) rather than their name. + * + * By default, [[single]] raw methods and parameters are [[com.avsystem.commons.rpc.verbatim verbatim]] which means + * that the real method must have exactly the same return type as raw method and real parameter must have exactly + * the same type as raw parameter. + * + * When applied on method metadata parameter or parameter metadata parameter, the rules above apply in the same way + * except that real method/parameter type is matched against the type passed as `T` to `TypedMetadata[T]` by + * metadata class. For example, if method metadata class extends `TypedMetadata[Unit]` then `Unit` is assumed to be + * the result type of raw method that this metadata class represents. Similarly, when parameter metadata class + * extends `TypedMetadata[Int]` then raw parameter represented by this metadata class is assumed to be of type `Int`. + * + * Finally, when applied on metadata parameter with [[reifyAnnot]], that parameter is supposed to hold exactly one + * instance of an annotation of given type from real trait/method/param. If there is no such annotation available, + * compilation error will be raised. + */ +final class single extends SymbolArity + +/** + * When applied on raw method, it works almost the same way as [[single]] except that it is not required that + * matching real method exists in real RPC trait. If there is no matching real method, macro-materialized `AsRaw` + * implementation will implement the raw method with a code that throws an exception. + * + * When applied on raw parameter, specifies that this raw parameter may be matched by a real parameter but this is + * not required. Whether a real parameter matches an optional raw parameter is determined by its type and/or tag + * (see [[com.avsystem.commons.rpc.paramTag paramTag]] for more information on param tagging). + * + * Raw parameters marked as [[optional]] must be typed as `Option[T]` (or `Opt[T]`, `OptArg[T]` or whatever type that + * has an instance of `OptionLike`). By default, [[optional]] raw parameters are + * [[com.avsystem.commons.rpc.verbatim verbatim]] which means that the option-wrapped type `T` must match exactly + * the type of real parameter. + * + * In the macro generated code that translates a raw call into a real call, when the raw parameter value is absent + * (the `Option[T]` is empty) then real parameter's default value will be used as fallback. This allows introducing + * new parameters into RPC interfaces without breaking backwards compatibility. + * + * [[optional]] may also be used on method metadata parameters or parameter metadata parameters. It works the same + * way as with [[single]] except that metadata class must be wrapped in an `OptionLike` type (`Option`, `Opt`, etc.). + * + * Finally, [[optional]] may also be used for metadata parameters that hold reified annotations (see [[reifyAnnot]]). + * In that case it is not required that the annotation being reified is actually present on the real trait/method/param. + * For that to work, metadata param must be typed as `Option[A]`, `Opt[A]`, etc. where `A` is the type of annotation + * being reified. + */ +final class optional extends SymbolArity + +/** + * 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 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 [[com.avsystem.commons.rpc.methodName methodName]]). + * By default, result type of multi raw method is [[com.avsystem.commons.rpc.encoded encoded]] and the macro engine searches for + * appropriate `AsRaw` or `AsReal` conversion between real method result type and raw method result type. + * + * When applied on raw parameter, specifies that this raw parameter may be matched by arbitrary number of real + * parameters whose values are typically [[com.avsystem.commons.rpc.encoded encoded]] and collected into + * (or extracted from) raw parameter value. + * The way real parameter values are collected and extracted depends on the type of raw parameter which must be + * either: + * + * - an `Iterable[R]` or any subtype (e.g. `List[R]`) + * - a `PartialFunction[String,R]` or any subtype (e.g. `Map[String,R]`) + * + * `R` denotes the type used to represent each real parameter value. Be default (unless + * [[com.avsystem.commons.rpc.verbatim verbatim]] is used) it means + * that each real value will be encoded as `R` and decoded from `R` - see [[com.avsystem.commons.rpc.encoded encoded]] + * for more information about how parameters are encoded and decoded. + * + * If raw parameter is a `Coll <: Iterable[Raw]` then in order to collect real values into raw value, the macro engine + * will search for an instance of `CanBuildFrom[Nothing,R,Coll]` and use it to build the raw value. + * In order to extract real values from raw value, macro generated code will obtain an `Iterator[R]` from the raw value + * and pull and possibly decode all the values. If the iterator is exhausted before all real values could be obtained + * then default parameter values specified on real parameters (if any) will be used as fallback values instead of + * failing. + * + * If raw parameter is an `IndexedSeq[Raw]` then extraction is optimized - instead of using an iterator, + * values are extracted by index (potentially also falling back to default values of real parameters). + * + * Finally, when raw parameter is a `Mapping <: PartialFunction[String,R]` then not only values but also parameter + * names are collected - the macro engine will search for an instance of `CanBuildFrom[Nothing,(String,R),Mapping]` + * in order to collect the mapping. Parameter names may be adjusted with + * [[com.avsystem.commons.rpc.rpcName rpcName]] but must be unique in the + * scope of a single raw parameter. In order to extract real values from mapping, macro generated code will + * call `applyOrElse` on the raw value giving it the name of each real parameter and also falling back to possible + * default parameter values, in case they are missing in the raw call. + * + * Note that when raw parameter is a name-value mapping, you can freely reorder real parameter without fear of + * breaking backwards compatibility. You can also safely add new real parameters as long as you provide default + * values for them. + * + * Just like [[single]] and [[optional]], [[multi]] can also be applied on metadata parameters corresponding to + * raw methods and raw parameters. The type of multi metadata parameter must be a collection, in the same way + * as it's required for multi raw parameters. Metadata classes materialized for raw methods and raw parameters + * must extend `TypedMetadata[T]` where `T` will be matched against each real method result type or each real + * parameter type. + * + * Ultimately, [[multi]] may be specified on metadata parameter that reifies annotations from real trait/method/param + * (see [[reifyAnnot]]). Such metadata parameter must be a collection: any subtype of `Iterable[A]` where `A` is the + * type of annotation being reified. The macro will then reify all matching annotations from real symbol, including + * inherited ones. + */ +final class multi extends SymbolArity + +/** + * Filter applied on raw methods or parameters which tells the macro engine that this raw method or parameter may + * only match real methods or parameters annotated with at least one annotation of given type `A` (or any subtype). + * + * This is similar to [[com.avsystem.commons.rpc.tagged tagged]] but simplier. + * Tagging lets you explicitly specify all possible tag types and provide + * default/fallback tags when no tag is present. + * + * @tparam A type of annotation required to be present on real method or parameter + */ +final class annotated[A <: StaticAnnotation] extends RawSymAnnotation + +/** + * Raw parameters annotated as `@auxiliary` match real parameters without "consuming" them. This means that + * real parameters matched by an auxiliary raw parameter must also be matched by some non-auxiliary raw parameter. + * This way these real params will be matched to more than one raw parameter and effectively duplicated. + * However, auxiliary raw param may use different encoding that the non-auxiliary one. This may be useful for + * implementors of raw RPC traits. + * + * When extracting real parameter values from raw calls, auxiliary parameters are + * completely ignored and only the matching non-auxiliary raw param value is used. + */ +final class auxiliary extends RawParamAnnotation + +/** + * Base trait for annotations applied on RPC metadata parameters which tell the macro engine how to materialize + * their values based on real RPC trait, its methods or their parameters. + */ +trait MetadataParamStrategy extends StaticAnnotation + +/** + * When a metadata parameter is annotated as `@infer`, RPC macro engine will materialize that parameter by searching + * for an implicit value of that parameter's type. `@infer` is the default strategy assumed for implicit parameters + * of metadata classes, so using this annotation explicitly is only needed when you want an implicit search done + * for non-implicit parameter. This may be useful if, e.g. you want an inferred parameter to be a case class field. + * + * NOTE: By default, implicit search for `@infer` parameter does NOT affect the decision about whether some real + * method or real parameter matches a metadata parameter. For example, if an implicit for `@infer` parameter cannot be + * found, you will only know about it *after* the metadata materializing macro has already been expanded. + * This behaviour can be changed with [[checked]] annotation. + */ +final class infer extends MetadataParamStrategy + +/** + * `@adtParamMetadata` applied on metadata parameter of metadata class for case class or object indicates that + * this parameter holds metadata for ADT parameter(s) (one, some or all, depending on [[SymbolArity]], tagging, etc.). + **/ +final class adtParamMetadata extends MetadataParamStrategy + +/** + * `@adtCaseMetadata` applied on metadata parameter of ADT hierarchy (sealed trait) metadata class indicates that + * this parameter holds metadata for its case classes (one, some or all, depending on [[SymbolArity]], tagging, etc.). + **/ +final class adtCaseMetadata extends MetadataParamStrategy + +/** + * Metadata parameter annotated as `@reifyAnnot` is intended to hold annotation(s) that must or may be present on the real + * RPC trait, method or parameter. `@reifyAnnot` parameters may have arity, which means that they may be annotated as + * [[single]] (the default), [[optional]] or [[multi]]. Arity annotation determines what parameter type the macro + * engine expects: + * + * - for [[single]], metadata parameter type must extend `StaticAnnotation` and an annotation of that type must be + * present on the real symbol or compilation error will be raised + * - for [[optional]], metadata parameter type must be an `Option`/`Opt`/etc. that wraps some `StaticAnnotation`. + * If that annotation is present on the real symbol, it will be reified as metadata value. + * - for [[multi]], metadata parameter type must be a subtype of `Iterable[StaticAnnotation]`, e.g. `List[SomeAnnot]`. + * The macro will then reify all annotations of that particular type present on the real symbol as metadata value. + * + * NOTE: all annotations are inherited from super/overridden symbols, i.e. + * - for RPC traits, annotations are inherited from all superclasses and supertraits + * - for RPC methods, annotations are inherited from all overridden or implemented abstract methods + * - for RPC parameters, annotations are inherited from all corresponding parameters (by index, not name) from + * all methods overridden or implemented by method containing the parameter for which metadata is being reified. + */ +final class reifyAnnot extends MetadataParamStrategy + +/** + * 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 isAnnotated[T <: StaticAnnotation] extends MetadataParamStrategy + +/** + * This annotation may only be applied on metadata parameters of type `String` and instructs the macro engine + * to reify the name of real RPC trait/method/parameter. Depending on the value of `useRawName` flag, the macro + * will either take into account or ignore potential [[com.avsystem.commons.rpc.rpcName rpcName]] annotation. + */ +final class reifyName(val useRawName: Boolean = false) extends MetadataParamStrategy + +/** + * Metadata parameter annotated with this annotation must be of type `ParamPosition` - a class that holds + * parameter index information - see scaladoc for `ParamPosition` for more details. + */ +final class reifyPosition extends MetadataParamStrategy + +/** + * Metadata parameter annotated with this annotation must be of type `ParamFlags` - a class that holds + * parameter flags information - see scaladoc for `ParamFlags` for more details. + */ +final class reifyFlags extends MetadataParamStrategy + +/** + * May be applied on metadata parameters with [[infer]] annotation (or just implicit metadata parameters - + * they have [[infer]] strategy by default). Metadata parameter annotated as [[checked]] makes the implicit search + * for that metadata parameter influence the decision about whether some metadata parameter matches real method or + * param or not. Without [[checked]] annotation, when implicit search for metadata parameter fails, the macro engine + * ignores that fact and error is only reported after macro is fully expanded. + */ +final class checked extends StaticAnnotation \ No newline at end of file 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 899d08e1d..65128baba 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 @@ -1,27 +1,14 @@ package com.avsystem.commons package rpc -import scala.annotation.StaticAnnotation - -/** - * For annotations applied on real RPC traits, their methods and parameters. - */ -trait RpcAnnotation extends StaticAnnotation - -/** - * For annotations applied on raw RPC traits, their methods and parameters. - * They specify how real methods are matched against raw methods and real parameters against raw parameters. - */ -sealed trait RawRpcAnnotation extends StaticAnnotation -sealed trait RawMethodAnnotation extends RawRpcAnnotation -sealed trait RawParamAnnotation extends RawRpcAnnotation +import com.avsystem.commons.meta._ /** * You can use this annotation on overloaded RPC methods to give them unique identifiers for RPC serialization. * You can also subclass this annotation provided that you always override the `name` parameter with another * constructor parameter. */ -class rpcName(val name: String) extends RpcAnnotation +class rpcName(val name: String) extends RealSymAnnotation /** * You can use this annotation on real RPC methods to instruct macro engine to prepend method name (or [[rpcName]] if @@ -34,7 +21,7 @@ class rpcName(val name: String) extends RpcAnnotation * } * }}} */ -class rpcNamePrefix(val prefix: String) extends RpcAnnotation +class rpcNamePrefix(val prefix: String) extends RealSymAnnotation /** * Base trait for RPC tag annotations. Tagging gives more direct control over how real methods @@ -42,152 +29,43 @@ class rpcNamePrefix(val prefix: String) extends RpcAnnotation * For more information about method tagging, see documentation of [[methodTag]]. * For more information about parameter tagging, see documentation of [[paramTag]]. */ -trait RpcTag extends RpcAnnotation +trait RpcTag extends RealSymAnnotation /** * 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). + * Macro generation of `AsRaw` implementations require that raw methods annotated as + * [[com.avsystem.commons.meta.multi multi]] must take at least + * one raw parameter annotated as [[methodName]] (it may also be aggregated into some + * [[com.avsystem.commons.meta.composite 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: - * - * - raw methods - * - raw parameters - * - metadata parameters for raw methods and raw parameters - * - metadata parameters that reify annotations (i.e. annotated with [[reifyAnnot]]) - * - * See documentation of each arity annotation for more details. - */ -sealed trait RpcArity extends RawParamAnnotation - -/** - * The default arity annotation. Usually there is no need to use this annotation explicitly. - * - * When applied on raw method, there must be exactly one real method matching this raw method and it must have - * the same name (or [[rpcName]]) as raw method's name. - * - * When applied on raw parameter, specifies that this raw parameter must be matched by exactly one real parameter, - * on the position that matches raw parameter's position. Names are ignored because - unlike methods - parameters are - * identified by their position in parameter list(s) rather than their name. - * - * By default, [[single]] raw methods and parameters are [[verbatim]] which means that the real method must have - * exactly the same return type as raw method and real parameter must have exactly the same type as raw parameter. - * - * When applied on method metadata parameter or parameter metadata parameter, the rules above apply in the same way - * except that real method/parameter type is matched against the type passed as `T` to `TypedMetadata[T]` by - * metadata class. For example, if method metadata class extends `TypedMetadata[Unit]` then `Unit` is assumed to be - * the result type of raw method that this metadata class represents. Similarly, when parameter metadata class - * extends `TypedMetadata[Int]` then raw parameter represented by this metadata class is assumed to be of type `Int`. - * - * Finally, when applied on metadata parameter with [[reifyAnnot]], that parameter is supposed to hold exactly one - * instance of an annotation of given type from real trait/method/param. If there is no such annotation available, - * compilation error will be raised. - */ -final class single extends RpcArity - -/** - * When applied on raw method, it works almost the same way as [[single]] except that it is not required that - * matching real method exists in real RPC trait. If there is no matching real method, macro-materialized `AsRaw` - * implementation will implement the raw method with a code that throws an exception. - * - * When applied on raw parameter, specifies that this raw parameter may be matched by a real parameter but this is - * not required. Whether a real parameter matches an optional raw parameter is determined by its type and/or tag - * (see [[paramTag]] for more information on param tagging). - * - * Raw parameters marked as [[optional]] must be typed as `Option[T]` (or `Opt[T]`, `OptArg[T]` or whatever type that - * has an instance of `OptionLike`). By default, [[optional]] raw parameters are [[verbatim]] which means that the - * option-wrapped type `T` must match exactly the type of real parameter. - * - * In the macro generated code that translates a raw call into a real call, when the raw parameter value is absent - * (the `Option[T]` is empty) then real parameter's default value will be used as fallback. This allows introducing - * new parameters into RPC interfaces without breaking backwards compatibility. - * - * [[optional]] may also be used on method metadata parameters or parameter metadata parameters. It works the same - * way as with [[single]] except that metadata class must be wrapped in an `OptionLike` type (`Option`, `Opt`, etc.). - * - * Finally, [[optional]] may also be used for metadata parameters that hold reified annotations (see [[reifyAnnot]]). - * In that case it is not required that the annotation being reified is actually present on the real trait/method/param. - * For that to work, metadata param must be typed as `Option[A]`, `Opt[A]`, etc. where `A` is the type of annotation - * being reified. - */ -final class optional extends RpcArity + * `@rpcMethodMetadata` applied on metadata parameter of RPC trait metadata class indicates that this parameter holds + * metadata for RPC method(s) (one, some or all, depending on [[com.avsystem.commons.meta.SymbolArity SymbolArity]], + * tagging, etc.). + **/ +final class rpcMethodMetadata extends MetadataParamStrategy /** - * 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 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. - * - * When applied on raw parameter, specifies that this raw parameter may be matched by arbitrary number of real - * parameters whose values are typically [[encoded]] and collected into (or extracted from) raw parameter value. - * The way real parameter values are collected and extracted depends on the type of raw parameter which must be - * either: - * - * - an `Iterable[R]` or any subtype (e.g. `List[R]`) - * - a `PartialFunction[String,R]` or any subtype (e.g. `Map[String,R]`) - * - * `R` denotes the type used to represent each real parameter value. Be default (unless [[verbatim]] is used) it means - * that each real value will be encoded as `R` and decoded from `R` - see [[encoded]] for more information about - * how parameters are encoded and decoded. - * - * If raw parameter is a `Coll <: Iterable[Raw]` then in order to collect real values into raw value, the macro engine - * will search for an instance of `CanBuildFrom[Nothing,R,Coll]` and use it to build the raw value. - * In order to extract real values from raw value, macro generated code will obtain an `Iterator[R]` from the raw value - * and pull and possibly decode all the values. If the iterator is exhausted before all real values could be obtained - * then default parameter values specified on real parameters (if any) will be used as fallback values instead of - * failing. - * - * If raw parameter is an `IndexedSeq[Raw]` then extraction is optimized - instead of using an iterator, - * values are extracted by index (potentially also falling back to default values of real parameters). - * - * Finally, when raw parameter is a `Mapping <: PartialFunction[String,R]` then not only values but also parameter - * names are collected - the macro engine will search for an instance of `CanBuildFrom[Nothing,(String,R),Mapping]` - * in order to collect the mapping. Parameter names may be adjusted with [[rpcName]] but must be unique in the - * scope of a single raw parameter. In order to extract real values from mapping, macro generated code will - * call `applyOrElse` on the raw value giving it the name of each real parameter and also falling back to possible - * default parameter values, in case they are missing in the raw call. - * - * Note that when raw parameter is a name-value mapping, you can freely reorder real parameter without fear of - * breaking backwards compatibility. You can also safely add new real parameters as long as you provide default - * values for them. - * - * Just like [[single]] and [[optional]], [[multi]] can also be applied on metadata parameters corresponding to - * raw methods and raw parameters. The type of multi metadata parameter must be a collection, in the same way - * as it's required for multi raw parameters. Metadata classes materialized for raw methods and raw parameters - * must extend `TypedMetadata[T]` where `T` will be matched against each real method result type or each real - * parameter type. - * - * Ultimately, [[multi]] may be specified on metadata parameter that reifies annotations from real trait/method/param - * (see [[reifyAnnot]]). Such metadata parameter must be a collection: any subtype of `Iterable[A]` where `A` is the - * type of annotation being reified. The macro will then reify all matching annotations from real symbol, including - * inherited ones. - */ -final class multi extends RpcArity + * `@rpcParamMetadata` applied on metadata parameter of RPC method metadata class indicates that this parameter holds + * metadata for RPC parameter(s) (one, some or all, depending on [[com.avsystem.commons.meta.SymbolArity SymbolArity]]], + * tagging, etc.). + **/ +final class rpcParamMetadata extends MetadataParamStrategy /** * 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 * as raw values. * Currently there are two possible cases: [[verbatim]] (no encoding) and [[encoded]] (encoding using `AsRaw` and - * `AsReal` typeclasses). By default, method return values and [[multi]] parameters are [[encoded]] while - * [[single]] and [[optional]] parameters are [[verbatim]]. + * `AsReal` typeclasses). By default, method return values and [[com.avsystem.commons.meta.multi multi]] + * parameters are [[encoded]] while [[com.avsystem.commons.meta.single single]] and + * [[com.avsystem.commons.meta.optional optional]] parameters are [[verbatim]]. * See documentation of [[verbatim]] and [[encoded]] for more details. */ sealed trait RpcEncoding extends RawMethodAnnotation with RawParamAnnotation @@ -256,9 +134,10 @@ sealed trait RpcEncoding extends RawMethodAnnotation with RawParamAnnotation final class encoded extends RpcEncoding /** - * Turns off raw value encoding as specified by [[encoded]]. By default, [[single]] and [[optional]] raw parameters - * are already [[verbatim]], so using [[verbatim]] only makes sense on [[multi]] raw parameters or - * raw methods themselves, which means turning off encoding of method's result. + * Turns off raw value encoding as specified by [[encoded]]. By default, [[com.avsystem.commons.meta.single single]] + * and [[com.avsystem.commons.meta.optional optional]] raw parameters are already [[verbatim]], so using [[verbatim]] + * only makes sense on [[com.avsystem.commons.meta.multi multi]] raw parameters or raw methods themselves, + * which means turning off encoding of method's result. * * When encoding is turned off, raw and real types must be exactly the same types. For example, the following raw RPC * definition will match only raw RPC traits whose methods take `Int`s as parameters and return `Double`s as values: @@ -326,13 +205,13 @@ final class tried extends RawMethodAnnotation * @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](val defaultTag: BaseTag = null) extends RawRpcAnnotation +final class methodTag[BaseTag <: RpcTag](val defaultTag: BaseTag = null) extends RawSymAnnotation /** * Parameter tagging lets you have more explicit control over which raw parameters can match which real * parameters. This way you can have some of the parameters annotated in order to treat them differently, e.g. * they may be [[verbatim]], encoded in a different way or collected to a different raw container (e.g. - * `Map[String,Raw]` vs `List[Raw]` - see [[multi]] for more details). + * `Map[String,Raw]` vs `List[Raw]` - see [[com.avsystem.commons.meta.multi multi]] for more details). * * Example: * {{{ @@ -382,101 +261,3 @@ final class paramTag[BaseTag <: RpcTag](val defaultTag: BaseTag = null) extends */ final class tagged[Tag <: RpcTag](val whenUntagged: Tag = null) extends RawMethodAnnotation with RawParamAnnotation - -/** - * Filter applied on raw methods or parameters which tells the macro engine that this raw method or parameter may - * only match real methods or parameters annotated with at least one annotation of given type `A` (or any subtype). - * - * This is similar to [[tagged]] but simplier. Tagging lets you explicitly specify all possible tag types and provide - * default/fallback tags when no tag is present. - * - * @tparam A type of annotation required to be present on real method or parameter - */ -final class annotated[A <: StaticAnnotation] extends RawRpcAnnotation - -/** - * Raw parameters annotated as `@auxiliary` match real parameters without "consuming" them. This means that - * real parameters matched by an auxiliary raw parameter must also be matched by some non-auxiliary raw parameter. - * This way these real params will be matched to more than one raw parameter and effectively duplicated. - * However, auxiliary raw param may use different encoding that the non-auxiliary one. This may be useful for - * implementors of raw RPC traits. - * - * When extracting real parameter values from raw calls, auxiliary parameters are - * completely ignored and only the matching non-auxiliary raw param value is used. - */ -final class auxiliary extends RawParamAnnotation - -/** - * Base trait for annotations applied on RPC metadata parameters which tell the macro engine how to materialize - * their values based on real RPC trait, its methods or their parameters. - */ -sealed trait MetadataParamStrategy extends StaticAnnotation - -/** - * When a metadata parameter is annotated as `@infer`, RPC macro engine will materialize that parameter by searching - * for an implicit value of that parameter's type. `@infer` is the default strategy assumed for implicit parameters - * of metadata classes, so using this annotation explicitly is only needed when you want an implicit search done - * for non-implicit parameter. This may be useful if, e.g. you want an inferred parameter to be a case class field. - * - * NOTE: By default, implicit search for `@infer` parameter does NOT affect the decision about whether some real - * method or real parameter matches a metadata parameter. For example, if an implicit for `@infer` parameter cannot be - * found, you will only know about it *after* the metadata materializing macro has already been expanded. - * This behaviour can be changed with [[checked]] annotation. - */ -final class infer extends MetadataParamStrategy - -/** - * Metadata parameter annotated as `@reifyAnnot` is intended to hold annotation(s) that must or may be present on the real - * RPC trait, method or parameter. `@reifyAnnot` parameters may have arity, which means that they may be annotated as - * [[single]] (the default), [[optional]] or [[multi]]. Arity annotation determines what parameter type the macro - * engine expects: - * - * - for [[single]], metadata parameter type must extend `StaticAnnotation` and an annotation of that type must be - * present on the real symbol or compilation error will be raised - * - for [[optional]], metadata parameter type must be an `Option`/`Opt`/etc. that wraps some `StaticAnnotation`. - * If that annotation is present on the real symbol, it will be reified as metadata value. - * - for [[multi]], metadata parameter type must be a subtype of `Iterable[StaticAnnotation]`, e.g. `List[SomeAnnot]`. - * The macro will then reify all annotations of that particular type present on the real symbol as metadata value. - * - * NOTE: all annotations are inherited from super/overridden symbols, i.e. - * - for RPC traits, annotations are inherited from all superclasses and supertraits - * - for RPC methods, annotations are inherited from all overridden or implemented abstract methods - * - for RPC parameters, annotations are inherited from all corresponding parameters (by index, not name) from - * all methods overridden or implemented by method containing the parameter for which metadata is being reified. - */ -final class reifyAnnot extends MetadataParamStrategy - -/** - * 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 isAnnotated[T <: StaticAnnotation] extends MetadataParamStrategy - -/** - * This annotation may only be applied on metadata parameters of type `String` and instructs the macro engine - * to reify the name of real RPC trait/method/parameter. Depending on the value of `rpcName` flag, the macro - * will either take into account or ignore potential [[rpcName]] annotation. - */ -final class reifyName(val rpcName: Boolean = false) extends MetadataParamStrategy - -/** - * Metadata parameter annotated with this annotation must be of type `ParamPosition` - a class that holds - * parameter index information - see scaladoc for `ParamPosition` for more details. - */ -final class reifyPosition extends MetadataParamStrategy - -/** - * Metadata parameter annotated with this annotation must be of type `ParamFlags` - a class that holds - * parameter flags information - see scaladoc for `ParamFlags` for more details. - */ -final class reifyFlags extends MetadataParamStrategy - -/** - * May be applied on metadata parameters with [[infer]] annotation (or just implicit metadata parameters - - * they have [[infer]] strategy by default). Metadata parameter annotated as [[checked]] makes the implicit search - * for that metadata parameter influence the decision about whether some metadata parameter matches real method or - * param or not. Without [[checked]] annotation, when implicit search for metadata parameter fails, the macro engine - * ignores that fact and error is only reported after macro is fully expanded. - */ -final class checked extends StaticAnnotation diff --git a/commons-annotations/src/main/scala/com/avsystem/commons/serialization/name.scala b/commons-annotations/src/main/scala/com/avsystem/commons/serialization/name.scala index 74c7e9484..62683fce7 100644 --- a/commons-annotations/src/main/scala/com/avsystem/commons/serialization/name.scala +++ b/commons-annotations/src/main/scala/com/avsystem/commons/serialization/name.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package serialization +import com.avsystem.commons.annotation.NotInheritedFromSealedTypes + import scala.annotation.StaticAnnotation /** @@ -24,4 +26,5 @@ import scala.annotation.StaticAnnotation * For instance, if a case class field overrides a method of some base trait, the `@name` annotation may * be used on that method and will affect the case class field. */ -class name(val name: String) extends StaticAnnotation +class name(val name: String) extends StaticAnnotation with NotInheritedFromSealedTypes + 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 b085334c2..61d6e8182 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala @@ -176,12 +176,35 @@ object SharedExtensions extends SharedExtensions { def optRef: OptRef[A] = OptRef(a) } + private val RemovableLineBreak = "\\n+".r + class StringOps(private val str: String) extends AnyVal { def ensureSuffix(suffix: String): String = if (str.endsWith(suffix)) str else str + suffix def ensurePrefix(prefix: String): String = if (str.startsWith(prefix)) str else prefix + str + + def uncapitalize: String = + if (str.isEmpty || str.charAt(0).isLower) str + else str.charAt(0).toLower + str.substring(1) + + /** + * Removes a newline character from every sequence of consecutive newline characters. If the sequence contained + * just one newline character without any whitespace before and after it, a space is inserted. + * + * e.g. `My hovercraft\nis full of eels.\n\nMy hovercraft is\n full of eels.` becomes + * `My hovercraft is full of eels.\nMy hovercraft is full of eels.` + * + * Useful for multi-line string literals with lines wrapped in source code but without intention of including + * these line breaks in actual runtime string. + */ + def unwrapLines: String = + RemovableLineBreak.replaceAllIn(str, { m => + val insertSpace = m.end == m.start + 1 && m.start - 1 >= 0 && m.end < str.length && + !Character.isWhitespace(str.charAt(m.start - 1)) && !Character.isWhitespace(str.charAt(m.end)) + if (insertSpace) " " else m.matched.substring(1) + }) } class IntOps(private val int: Int) extends AnyVal { diff --git a/commons-core/src/main/scala/com/avsystem/commons/meta/AdtMetadataCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/meta/AdtMetadataCompanion.scala new file mode 100644 index 000000000..eb8ec02e6 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/meta/AdtMetadataCompanion.scala @@ -0,0 +1,11 @@ +package com.avsystem.commons +package meta + +import com.avsystem.commons.macros.meta.AdtMetadataMacros +import com.avsystem.commons.misc.MacroGenerated + +trait AdtMetadataCompanion[M[_]] extends MetadataCompanion[M] { + def materialize[T]: M[T] = macro AdtMetadataMacros.materialize[T] + + implicit def materializeMacroGenerated[T]: MacroGenerated[M[T]] = macro AdtMetadataMacros.materializeMacroGenerated[T] +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala b/commons-core/src/main/scala/com/avsystem/commons/meta/Mapping.scala similarity index 50% rename from commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala rename to commons-core/src/main/scala/com/avsystem/commons/meta/Mapping.scala index 61d491d9f..51c48426c 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/NamedParams.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/meta/Mapping.scala @@ -1,55 +1,63 @@ package com.avsystem.commons -package rpc +package meta -import com.avsystem.commons.rpc.NamedParams.ConcatIterable +import com.avsystem.commons.meta.Mapping.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. + * Simple immutable structure to collect named values while retaining their order and + * providing fast, hashed lookup by name when necessary. * Intended to be used for [[multi]] raw parameters. */ -final class NamedParams[+V](private val wrapped: IIterable[(String, V)]) +final class Mapping[+V](private val wrapped: IIterable[(String, V)]) extends IIterable[(String, V)] with PartialFunction[String, V] { private[this] lazy val hashMap = new MLinkedHashMap[String, V].setup(_ ++= wrapped) def iterator: Iterator[(String, V)] = hashMap.iterator + def valuesIterator: Iterator[V] = + hashMap.valuesIterator + def keys: Iterable[String] = + hashMap.keys + def contains(key: String): Boolean = + hashMap.contains(key) 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 get(key: String): Opt[V] = + hashMap.getOpt(key) - def ++[V0 >: V](other: NamedParams[V0]): NamedParams[V0] = + def ++[V0 >: V](other: Mapping[V0]): Mapping[V0] = if (wrapped.isEmpty) other else if (other.wrapped.isEmpty) this - else new NamedParams(ConcatIterable(wrapped, other.wrapped)) + else new Mapping(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(_)) +object Mapping { + def empty[V]: Mapping[V] = new Mapping(Nil) + def newBuilder[V]: mutable.Builder[(String, V), Mapping[V]] = + new MListBuffer[(String, V)].mapResult(new Mapping(_)) 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] + private val reusableCBF = new CanBuildFrom[Nothing, (String, Any), Mapping[Any]] { + def apply(from: Nothing): mutable.Builder[(String, Any), Mapping[Any]] = newBuilder[Any] + def apply(): mutable.Builder[(String, Any), Mapping[Any]] = newBuilder[Any] } - implicit def canBuildFrom[V]: CanBuildFrom[Nothing, (String, V), NamedParams[V]] = - reusableCBF.asInstanceOf[CanBuildFrom[Nothing, (String, V), NamedParams[V]]] + implicit def canBuildFrom[V]: CanBuildFrom[Nothing, (String, V), Mapping[V]] = + reusableCBF.asInstanceOf[CanBuildFrom[Nothing, (String, V), Mapping[V]]] - implicit def genCodec[V: GenCodec]: GenCodec[NamedParams[V]] = GenCodec.createNullableObject( - oi => new NamedParams(oi.iterator(GenCodec.read[V]).toList), + implicit def genCodec[V: GenCodec]: GenCodec[Mapping[V]] = GenCodec.createNullableObject( + oi => new Mapping(oi.iterator(GenCodec.read[V]).toList), (oo, np) => np.foreach({ case (k, v) => GenCodec.write[V](oo.writeField(k), v) }) ) } diff --git a/commons-core/src/main/scala/com/avsystem/commons/meta/MetadataCompanion.scala b/commons-core/src/main/scala/com/avsystem/commons/meta/MetadataCompanion.scala new file mode 100644 index 000000000..9059e2543 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/meta/MetadataCompanion.scala @@ -0,0 +1,21 @@ +package com.avsystem.commons +package meta + +import com.avsystem.commons.macros.misc.MiscMacros +import com.avsystem.commons.rpc.Fallback + +trait MetadataCompanion[M[_]] { + final def apply[Real](implicit metadata: M[Real]): M[Real] = metadata + + 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 + } + object Lazy { + def apply[Real](metadata: => M[Real]): Lazy[Real] = new Lazy(metadata) + + // macro effectively turns `metadata` param into by-name param (implicit params by themselves cannot be by-name) + implicit def lazyMetadata[Real](implicit metadata: M[Real]): Lazy[Real] = macro MiscMacros.lazyMetadata + } +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/OptionLike.scala b/commons-core/src/main/scala/com/avsystem/commons/meta/OptionLike.scala similarity index 99% rename from commons-core/src/main/scala/com/avsystem/commons/rpc/OptionLike.scala rename to commons-core/src/main/scala/com/avsystem/commons/meta/OptionLike.scala index f37a39ccf..192d25363 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/OptionLike.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/meta/OptionLike.scala @@ -1,5 +1,5 @@ package com.avsystem.commons -package rpc +package meta sealed trait OptionLike[O] { type Value diff --git a/commons-core/src/main/scala/com/avsystem/commons/rpc/rpcMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/meta/rpcMetadata.scala similarity index 61% rename from commons-core/src/main/scala/com/avsystem/commons/rpc/rpcMetadata.scala rename to commons-core/src/main/scala/com/avsystem/commons/meta/rpcMetadata.scala index 455223fed..a15898aaf 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/rpc/rpcMetadata.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/meta/rpcMetadata.scala @@ -1,5 +1,5 @@ package com.avsystem.commons -package rpc +package meta import com.avsystem.commons.serialization.{HasGenCodec, transparent} @@ -77,3 +77,57 @@ final case class ParamPosition( indexInRaw: Int ) object ParamPosition extends HasGenCodec[ParamPosition] + +/** + * Information about real parameter flags and modifiers as defined in Scala code. + */ +@transparent +final case class TypeFlags(rawFlags: Int) extends AnyVal { + + import TypeFlags._ + + def |(other: TypeFlags): TypeFlags = new TypeFlags(rawFlags | other.rawFlags) + def &(other: TypeFlags): TypeFlags = new TypeFlags(rawFlags & other.rawFlags) + def ^(other: TypeFlags): TypeFlags = new TypeFlags(rawFlags ^ other.rawFlags) + def unary_~ : TypeFlags = new TypeFlags(~rawFlags) + + def hasFlags(flags: TypeFlags): Boolean = (this & flags) == flags + + def isAbstract: Boolean = hasFlags(Abstract) + def isFinal: Boolean = hasFlags(Final) + def isSealed: Boolean = hasFlags(Sealed) + def isCase: Boolean = hasFlags(Case) + def isTrait: Boolean = hasFlags(Trait) + def isObject: Boolean = hasFlags(Object) + + override def toString: String = { + def repr(flags: TypeFlags, r: String): Opt[String] = + r.opt.filter(_ => hasFlags(flags)) + + List( + repr(Abstract, "abstract"), + repr(Final, "final"), + repr(Sealed, "sealed"), + repr(Case, "case"), + repr(Trait, "trait"), + repr(Object, "object") + ).flatten.mkString(",") + } +} + +object TypeFlags extends HasGenCodec[TypeFlags] { + private[this] var currentFlag: Int = 1 + private[this] def nextFlag(): TypeFlags = { + val flag = currentFlag + currentFlag = currentFlag << 1 + new TypeFlags(flag) + } + + final val Empty: TypeFlags = new TypeFlags(0) + final val Abstract: TypeFlags = nextFlag() + final val Final: TypeFlags = nextFlag() + final val Sealed: TypeFlags = nextFlag() + final val Case: TypeFlags = nextFlag() + final val Trait: TypeFlags = nextFlag() + final val Object: TypeFlags = nextFlag() +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/misc/ValueOf.scala b/commons-core/src/main/scala/com/avsystem/commons/misc/ValueOf.scala new file mode 100644 index 000000000..05dc40125 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/misc/ValueOf.scala @@ -0,0 +1,11 @@ +package com.avsystem.commons +package misc + +import com.avsystem.commons.macros.misc.MiscMacros + +class ValueOf[T](val value: T) extends AnyVal +object ValueOf { + def apply[T](implicit vof: ValueOf[T]): T = vof.value + + implicit def mkValueOf[T]: ValueOf[T] = macro MiscMacros.mkValueOf[T] +} 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 72fd80dd9..3d15a8c93 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 @@ -1,10 +1,31 @@ package com.avsystem.commons package rest +import com.avsystem.commons.misc.MacroGenerated import com.avsystem.commons.rest.RawRest.{AsRawRealRpc, AsRawRpc, AsRealRpc} +import com.avsystem.commons.rest.openapi.{OpenApiMetadata, RestSchema, RestStructure} import com.avsystem.commons.rpc.{AsRawReal, Fallback, RpcMacroInstances} import com.avsystem.commons.serialization.json.{JsonStringInput, JsonStringOutput} -import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, HasGenCodec} + +/** + * Base class for companion objects of ADTs (case classes, objects, sealed hierarchies) which are used as + * parameter or result types in REST API traits. Automatically provides instances of + * [[com.avsystem.commons.serialization.GenCodec GenCodec]] and + * [[com.avsystem.commons.rest.openapi.RestSchema RestSchema]]. + * + * @example + * {{{ + * case class User(id: String, name: String, birthYear: Int) + * object User extends RestDataCompanion[User] + * }}} + */ +abstract class RestDataCompanion[T]( + implicit macroRestStructure: MacroGenerated[RestStructure[T]], macroCodec: MacroGenerated[GenCodec[T]] +) extends HasGenCodec[T] { + implicit lazy val restStructure: RestStructure[T] = macroRestStructure.forCompanion(this) + implicit lazy val restSchema: RestSchema[T] = restStructure.standaloneSchema // lazy on restStructure +} trait ClientInstances[Real] { def metadata: RestMetadata[Real] @@ -14,28 +35,46 @@ trait ServerInstances[Real] { def metadata: RestMetadata[Real] def asRaw: AsRawRpc[Real] } +trait OpenApiServerInstances[Real] extends ServerInstances[Real] { + def openapiMetadata: OpenApiMetadata[Real] +} trait FullInstances[Real] { def metadata: RestMetadata[Real] def asRawReal: AsRawRealRpc[Real] } +trait OpenApiFullInstances[Real] extends FullInstances[Real] { + def openapiMetadata: OpenApiMetadata[Real] +} /** @see [[RestApiCompanion]] */ -abstract class RestClientApiCompanion[Implicits, Real](implicits: Implicits)( +abstract class RestClientApiCompanion[Implicits, Real](protected val 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 + implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits, this).metadata + implicit final lazy val restAsReal: AsRealRpc[Real] = inst(implicits, this).asReal final def fromHandleRequest(handleRequest: RawRest.HandleRequest): Real = RawRest.fromHandleRequest(handleRequest) } /** @see [[RestApiCompanion]] */ -abstract class RestServerApiCompanion[Implicits, Real](implicits: Implicits)( +abstract class RestServerApiCompanion[Implicits, Real](protected val 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 + implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits, this).metadata + implicit final lazy val restAsRaw: AsRawRpc[Real] = inst(implicits, this).asRaw + + final def asHandleRequest(real: Real): RawRest.HandleRequest = + RawRest.asHandleRequest(real) +} + +/** @see [[RestApiCompanion]] */ +abstract class RestServerOpenApiCompanion[Implicits, Real](protected val implicits: Implicits)( + implicit inst: RpcMacroInstances[Implicits, OpenApiServerInstances, Real] +) { + implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits, this).metadata + implicit final lazy val restAsRaw: AsRawRpc[Real] = inst(implicits, this).asRaw + implicit final lazy val openapiMetadata: OpenApiMetadata[Real] = inst(implicits, this).openapiMetadata final def asHandleRequest(real: Real): RawRest.HandleRequest = RawRest.asHandleRequest(real) @@ -48,11 +87,25 @@ abstract class RestServerApiCompanion[Implicits, Real](implicits: Implicits)( * Usually, for even less boilerplate, this base class is extended by yet another abstract class which fixes * the `Implicits` type, e.g. [[DefaultRestApiCompanion]]. */ -abstract class RestApiCompanion[Implicits, Real](implicits: Implicits)( +abstract class RestApiCompanion[Implicits, Real](protected val 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 + implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits, this).metadata + implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = inst(implicits, this).asRawReal + + final def fromHandleRequest(handleRequest: RawRest.HandleRequest): Real = + RawRest.fromHandleRequest(handleRequest) + final def asHandleRequest(real: Real): RawRest.HandleRequest = + RawRest.asHandleRequest(real) +} + +/** @see [[RestApiCompanion]] */ +abstract class RestOpenApiCompanion[Implicits, Real](protected val implicits: Implicits)( + implicit inst: RpcMacroInstances[Implicits, OpenApiFullInstances, Real] +) { + implicit final lazy val restMetadata: RestMetadata[Real] = inst(implicits, this).metadata + implicit final lazy val restAsRawReal: AsRawRealRpc[Real] = inst(implicits, this).asRawReal + implicit final lazy val openapiMetadata: OpenApiMetadata[Real] = inst(implicits, this).openapiMetadata final def fromHandleRequest(handleRequest: RawRest.HandleRequest): Real = RawRest.fromHandleRequest(handleRequest) @@ -82,19 +135,24 @@ 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 inst: RpcMacroInstances[DefaultRestImplicits, ClientInstances, Real]) - extends RestClientApiCompanion[DefaultRestImplicits, Real](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. + * Injects `GenCodec` and `GenKeyCodec` based serialization and forces derivation of + * [[com.avsystem.commons.rest.openapi.OpenApiMetadata OpenApiMetadata]]. */ -abstract class DefaultRestServerApiCompanion[Real](implicit inst: RpcMacroInstances[DefaultRestImplicits, ServerInstances, Real]) - extends RestServerApiCompanion[DefaultRestImplicits, Real](DefaultRestImplicits) +abstract class DefaultRestServerApiCompanion[Real](implicit + inst: RpcMacroInstances[DefaultRestImplicits, OpenApiServerInstances, Real] +) extends RestServerOpenApiCompanion[DefaultRestImplicits, Real](DefaultRestImplicits) /** * 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 and forces derivation of + * [[com.avsystem.commons.rest.openapi.OpenApiMetadata OpenApiMetadata]]. */ -abstract class DefaultRestApiCompanion[Real](implicit inst: RpcMacroInstances[DefaultRestImplicits, FullInstances, Real]) - extends RestApiCompanion[DefaultRestImplicits, Real](DefaultRestImplicits) +abstract class DefaultRestApiCompanion[Real](implicit + inst: RpcMacroInstances[DefaultRestImplicits, OpenApiFullInstances, Real] +) extends RestOpenApiCompanion[DefaultRestImplicits, Real](DefaultRestImplicits) 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 b57caeede..9821317db 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,6 +3,7 @@ package rest import java.util.concurrent.atomic.AtomicBoolean +import com.avsystem.commons.meta._ import com.avsystem.commons.rpc._ case class RestMethodCall(rpcName: String, pathParams: List[PathValue], metadata: RestMethodMetadata[_]) @@ -36,7 +37,7 @@ trait RawRest { @tagged[BodyMethodTag](whenUntagged = new POST) @paramTag[RestParamTag](defaultTag = new JsonBodyParam) def handle(@methodName name: String, @composite parameters: RestParameters, - @multi @tagged[JsonBodyParam] body: NamedParams[JsonValue]): Async[RestResponse] + @multi @tagged[JsonBodyParam] body: Mapping[JsonValue]): Async[RestResponse] @multi @tried @@ -158,7 +159,7 @@ object RawRest extends RawRpcCompanion[RawRest] { def get(name: String, parameters: RestParameters): Async[RestResponse] = handleSingle(name, parameters, HttpBody.Empty) - def handle(name: String, parameters: RestParameters, body: NamedParams[JsonValue]): Async[RestResponse] = + def handle(name: String, parameters: RestParameters, body: Mapping[JsonValue]): Async[RestResponse] = handleSingle(name, parameters, HttpBody.createJsonBody(body)) def handleSingle(name: String, parameters: RestParameters, body: HttpBody): Async[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 76f33d6d6..2f4a948f1 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 @@ -1,23 +1,28 @@ package com.avsystem.commons package rest +import com.avsystem.commons.meta._ import com.avsystem.commons.rpc._ +import scala.annotation.implicitNotFound + +@implicitNotFound("RestMetadata for ${T} not found. The easiest way to provide it is to make companion object of " + + "${T} extend one of the convenience base companion classes, e.g. DefaultRestApiCompanion") @methodTag[RestMethodTag] case class RestMetadata[T]( @multi @tagged[Prefix](whenUntagged = new Prefix) @paramTag[RestParamTag](defaultTag = new Path) - prefixMethods: Map[String, PrefixMetadata[_]], + @rpcMethodMetadata prefixMethods: Mapping[PrefixMetadata[_]], @multi @tagged[GET] @paramTag[RestParamTag](defaultTag = new Query) - httpGetMethods: Map[String, HttpMethodMetadata[_]], + @rpcMethodMetadata httpGetMethods: Mapping[HttpMethodMetadata[_]], @multi @tagged[BodyMethodTag](whenUntagged = new POST) @paramTag[RestParamTag](defaultTag = new JsonBodyParam) - httpBodyMethods: Map[String, HttpMethodMetadata[_]] + @rpcMethodMetadata httpBodyMethods: Mapping[HttpMethodMetadata[_]] ) { - val httpMethods: Map[String, HttpMethodMetadata[_]] = + val httpMethods: Mapping[HttpMethodMetadata[_]] = httpGetMethods ++ httpBodyMethods def ensureUniqueParams(prefixes: List[(String, PrefixMetadata[_])]): Unit = { @@ -98,7 +103,7 @@ object RestMetadata extends RpcMetadataCompanion[RestMetadata] { case Nil => this case PathName(PathValue(pathName)) :: tail => byName.getOrElseUpdate(pathName, new Trie).forPattern(tail) - case PathParam(_) :: tail => + case PathParam(_, _) :: tail => wildcard.getOrElse(new Trie().setup(t => wildcard = Opt(t))).forPattern(tail) } @@ -114,7 +119,7 @@ object RestMetadata extends RpcMetadataCompanion[RestMetadata] { 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}_")}" + forPattern(hm.pathPattern).rpcChains(hm.method) += s"$prefixChain${rpcName.stripPrefix(s"${hm.method.name.toLowerCase}_")}" } } @@ -154,21 +159,23 @@ object RestMetadata extends RpcMetadataCompanion[RestMetadata] { sealed trait PathPatternElement case class PathName(value: PathValue) extends PathPatternElement -case class PathParam(parameter: PathParamMetadata[_]) extends PathPatternElement +case class PathParam(name: String, parameter: PathParamMetadata[_]) extends PathPatternElement sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { def methodPath: List[PathValue] def parametersMetadata: RestParametersMetadata val pathPattern: List[PathPatternElement] = - methodPath.map(PathName) ++ parametersMetadata.path.flatMap(pp => PathParam(pp) :: pp.pathSuffix.map(PathName)) + methodPath.map(PathName) ++ parametersMetadata.path.flatMap { + case (name, pp) => PathParam(name, pp) :: pp.pathSuffix.map(PathName) + } def applyPathParams(params: List[PathValue]): List[PathValue] = { def loop(params: List[PathValue], pattern: List[PathPatternElement]): List[PathValue] = (params, pattern) match { case (Nil, Nil) => Nil case (_, PathName(patternHead) :: patternTail) => patternHead :: loop(params, patternTail) - case (param :: paramsTail, PathParam(_) :: patternTail) => param :: loop(paramsTail, patternTail) + case (param :: paramsTail, PathParam(_, _) :: patternTail) => param :: loop(paramsTail, patternTail) case _ => throw new IllegalArgumentException( s"got ${params.size} path params, expected ${parametersMetadata.path.size}") } @@ -179,7 +186,7 @@ sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { 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, PathParam(_) :: patternTail) => + case (param :: pathTail, PathParam(_, _) :: patternTail) => loop(pathTail, patternTail).map { case (params, tail) => (param :: params, tail) } case (pathHead :: pathTail, PathName(patternHead) :: patternTail) if pathHead == patternHead => loop(pathTail, patternTail) @@ -192,7 +199,7 @@ sealed abstract class RestMethodMetadata[T] extends TypedMetadata[T] { case class PrefixMetadata[T]( @reifyAnnot methodTag: Prefix, @composite parametersMetadata: RestParametersMetadata, - @checked @infer result: RestMetadata.Lazy[T] + @infer @checked result: RestMetadata.Lazy[T] ) extends RestMethodMetadata[T] { def methodPath: List[PathValue] = PathValue.split(methodTag.path) } @@ -200,40 +207,30 @@ case class PrefixMetadata[T]( case class HttpMethodMetadata[T]( @reifyAnnot methodTag: HttpMethodTag, @composite parametersMetadata: RestParametersMetadata, - @multi @tagged[BodyTag] bodyParams: Map[String, BodyParamMetadata[_]], - @checked @infer responseType: HttpResponseType[T] + @multi @tagged[JsonBodyParam] @rpcParamMetadata bodyParams: Mapping[ParamMetadata[_]], + @optional @encoded @tagged[Body] @rpcParamMetadata singleBodyParam: Opt[ParamMetadata[_]], + @infer @checked responseType: HttpResponseType[T] ) extends RestMethodMetadata[T] { val method: HttpMethod = methodTag.method - val singleBody: Boolean = bodyParams.values.exists(_.singleBody) + val singleBody: Boolean = singleBodyParam.isDefined 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] +@implicitNotFound("${T} is not a valid result type of HTTP operation (it would be valid when e.g. wrapped in a Future)") +case class HttpResponseType[T]() object HttpResponseType { - implicit def forFuture[T]: HttpResponseType[Future[T]] = - new HttpResponseType[Future[T]] {} + implicit def forFuture[T]: HttpResponseType[Future[T]] = HttpResponseType[Future[T]]() } case class RestParametersMetadata( - @multi @tagged[Path] path: List[PathParamMetadata[_]], - @multi @tagged[Header] headers: Map[String, HeaderParamMetadata[_]], - @multi @tagged[Query] query: Map[String, QueryParamMetadata[_]] + @multi @tagged[Path] @rpcParamMetadata path: Mapping[PathParamMetadata[_]], + @multi @tagged[Header] @rpcParamMetadata headers: Mapping[ParamMetadata[_]], + @multi @tagged[Query] @rpcParamMetadata query: Mapping[ParamMetadata[_]] ) -case class PathParamMetadata[T]( - @reifyName(rpcName = true) rpcName: String, - @reifyAnnot pathAnnot: Path -) extends TypedMetadata[T] { +case class ParamMetadata[T]() extends TypedMetadata[T] +case class PathParamMetadata[T](@reifyAnnot pathAnnot: Path) extends TypedMetadata[T] { 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](@isAnnotated[Body] singleBody: Boolean) extends TypedMetadata[T] - class InvalidRestApiException(msg: String) extends RestException(msg) 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 7db962fef..43ce7d7bf 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 @@ -67,24 +67,24 @@ sealed abstract class BodyMethodTag(method: HttpMethod) extends HttpMethodTag(me * @param path see [[RestMethodTag.path]] */ class GET(val path: String = null) extends HttpMethodTag(HttpMethod.GET) { - @rpcNamePrefix("GET_") type Implied + @rpcNamePrefix("get_") type Implied } /** See [[BodyMethodTag]] */ class POST(val path: String = null) extends BodyMethodTag(HttpMethod.POST) { - @rpcNamePrefix("POST_") type Implied + @rpcNamePrefix("post_") type Implied } /** See [[BodyMethodTag]] */ class PATCH(val path: String = null) extends BodyMethodTag(HttpMethod.PATCH) { - @rpcNamePrefix("PATCH_") type Implied + @rpcNamePrefix("patch_") type Implied } /** See [[BodyMethodTag]] */ class PUT(val path: String = null) extends BodyMethodTag(HttpMethod.PUT) { - @rpcNamePrefix("PUT_") type Implied + @rpcNamePrefix("put_") type Implied } /** See [[BodyMethodTag]] */ class DELETE(val path: String = null) extends BodyMethodTag(HttpMethod.DELETE) { - @rpcNamePrefix("DELETE_") type Implied + @rpcNamePrefix("delete_") type Implied } /** @@ -103,28 +103,28 @@ class DELETE(val path: String = null) extends BodyMethodTag(HttpMethod.DELETE) { class Prefix(val path: String = null) extends RestMethodTag sealed trait RestParamTag extends RpcTag +sealed trait NonBodyTag extends RestParamTag +sealed trait BodyTag extends RestParamTag /** * 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. */ -class Path(val pathSuffix: String = "") extends RestParamTag +class Path(val pathSuffix: String = "") extends NonBodyTag /** * 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. */ class Header(override val name: String) - extends rpcName(name) with RestParamTag + extends rpcName(name) with NonBodyTag /** * 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. */ class Query(@defaultsToName override val name: String = null) - extends rpcName(name) with RestParamTag - -sealed trait BodyTag extends RestParamTag + extends rpcName(name) with NonBodyTag /** * REST method parameters annotated as [[JsonBodyParam]] will be encoded as [[JsonValue]] and combined into 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 c092eaeb7..c1537ed19 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 @@ -1,6 +1,7 @@ package com.avsystem.commons package rest +import com.avsystem.commons.meta._ import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx} import com.avsystem.commons.rpc._ import com.avsystem.commons.serialization.GenCodec.ReadFailure @@ -43,28 +44,36 @@ case class JsonValue(value: String) extends AnyVal with RestValue * the body when reading. */ sealed trait HttpBody { - def contentOpt: Opt[String] = this match { + final 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 { + final def forNonEmpty(consumer: (String, String) => Unit): Unit = this match { case HttpBody(content, mimeType) => consumer(content, mimeType) case HttpBody.Empty => } - def readContent(): String = this match { + final def readContent(): String = this match { case HttpBody(content, _) => content case HttpBody.Empty => throw new ReadFailure("Expected non-empty body") } - def readJson(): JsonValue = this match { + final 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") } + + final def defaultStatus: Int = this match { + case HttpBody.Empty => 204 + case _ => 200 + } + + final def defaultResponse: RestResponse = + RestResponse(defaultStatus, this) } object HttpBody { object Empty extends HttpBody @@ -86,7 +95,7 @@ object HttpBody { def plain(value: String): HttpBody = HttpBody(value, PlainType) def json(json: JsonValue): HttpBody = HttpBody(json.value, JsonType) - def createJsonBody(fields: NamedParams[JsonValue]): HttpBody = + def createJsonBody(fields: Mapping[JsonValue]): HttpBody = if (fields.isEmpty) HttpBody.Empty else { val sb = new JStringBuilder val oo = new JsonStringOutput(sb).writeObject() @@ -98,11 +107,11 @@ object HttpBody { HttpBody.json(JsonValue(sb.toString)) } - def parseJsonBody(body: HttpBody): NamedParams[JsonValue] = body match { - case HttpBody.Empty => NamedParams.empty + def parseJsonBody(body: HttpBody): Mapping[JsonValue] = body match { + case HttpBody.Empty => Mapping.empty case _ => val oi = new JsonStringInput(new JsonReader(body.readJson().value)).readObject() - val builder = NamedParams.newBuilder[JsonValue] + val builder = Mapping.newBuilder[JsonValue] while (oi.hasNext) { val fi = oi.nextField() builder += ((fi.fieldName, JsonValue(fi.readRawJson()))) @@ -128,8 +137,8 @@ object HttpMethod extends AbstractValueEnumCompanion[HttpMethod] { case class RestParameters( @multi @tagged[Path] path: List[PathValue], - @multi @tagged[Header] headers: NamedParams[HeaderValue], - @multi @tagged[Query] query: NamedParams[QueryValue] + @multi @tagged[Header] headers: Mapping[HeaderValue], + @multi @tagged[Query] query: Mapping[QueryValue] ) { def append(method: RestMethodMetadata[_], otherParameters: RestParameters): RestParameters = RestParameters( @@ -139,7 +148,7 @@ case class RestParameters( ) } object RestParameters { - final val Empty = RestParameters(Nil, NamedParams.empty, NamedParams.empty) + final val Empty = RestParameters(Nil, Mapping.empty, Mapping.empty) } case class HttpErrorException(code: Int, payload: OptArg[String] = OptArg.Empty) @@ -152,8 +161,8 @@ case class RestRequest(method: HttpMethod, parameters: RestParameters, body: Htt 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 + def ensureNonError: RestResponse = + if (code >= 200 && code < 300) this else throw toHttpError } object RestResponse { @@ -172,10 +181,10 @@ object RestResponse { } implicit def bodyBasedFromResponse[T](implicit bodyAsReal: AsReal[HttpBody, T]): AsReal[RestResponse, T] = - AsReal.create(resp => bodyAsReal.asReal(resp.ensure200OK.body)) + AsReal.create(resp => bodyAsReal.asReal(resp.ensureNonError.body)) implicit def bodyBasedToResponse[T](implicit bodyAsRaw: AsRaw[HttpBody, T]): AsRaw[RestResponse, T] = - AsRaw.create(value => RestResponse(200, bodyAsRaw.asRaw(value)).recoverHttpError) + AsRaw.create(value => bodyAsRaw.asRaw(value).defaultResponse.recoverHttpError) implicit def futureToAsyncResp[T]( implicit respAsRaw: AsRaw[RestResponse, T] diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/Adjuster.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/Adjuster.scala new file mode 100644 index 000000000..f0f9ad84e --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/Adjuster.scala @@ -0,0 +1,78 @@ +package com.avsystem.commons +package rest.openapi + +import com.avsystem.commons.annotation.NotInheritedFromSealedTypes + +import scala.annotation.StaticAnnotation + +sealed trait Adjuster extends StaticAnnotation + +/** + * Base trait for annotations which may adjust [[Schema]] derived for various symbols in REST API traits. + * Schema adjusters may be applied on: + * + * - Types for which [[RestStructure]] is macro materialized and [[RestSchema]] derived from it. + * This includes all types with companion extending [[RestDataCompanion]]. + * + * - Fields of case classes for which [[RestStructure]] is macro materialized. + * + * - Body parameters of REST methods (parameters tagged with [[com.avsystem.commons.rest.JsonBodyParam JsonBodyParam]] + * or [[com.avsystem.commons.rest.Body Body]] annotations) + * + * Schema adjusters DO NOT WORK on REST methods themselves and their path/header/query parameters. + * Instead, use [[OperationAdjuster]] and [[ParameterAdjuster]]. + * + * Also, be aware that schema adjusters may also be applied on schema references. In such cases, the schema reference + * is wrapped into a [[Schema]] object with `allOf` property containing the original reference. This effectively + * allows you to extend the referenced schema but you cannot inspect it in the process. + */ +trait SchemaAdjuster extends Adjuster with NotInheritedFromSealedTypes { + def adjustSchema(schema: Schema): Schema +} +object SchemaAdjuster { + def adjustRef(adjusters: List[SchemaAdjuster], schema: RefOr[Schema]): RefOr[Schema] = + if (adjusters.nonEmpty) + adjusters.foldRight(schema.rewrapRefToAllOf)(_ adjustSchema _).unwrapSingleRefAllOf + else schema +} + +/** + * Base trait for annotation which may adjust [[Parameter]] generated for path, query or header parameters + * of REST RPC methods. + */ +trait ParameterAdjuster extends Adjuster { + def adjustParameter(parameter: Parameter): Parameter +} + +/** + * Base trait for annotation which may adjust [[Operation]] generated for REST RPC methods which translate to + * HTTP operations (i.e. it doesn't work for prefix methods). + */ +trait OperationAdjuster extends Adjuster { + def adjustOperation(operation: Operation): Operation +} + +/** Convenience implementation of [[SchemaAdjuster]] */ +class adjustSchema(f: Schema => Schema) extends SchemaAdjuster { + def adjustSchema(value: Schema): Schema = f(value) +} +/** Convenience implementation of [[ParameterAdjuster]] */ +class adjustParameter(f: Parameter => Parameter) extends ParameterAdjuster { + def adjustParameter(value: Parameter): Parameter = f(value) +} +/** Convenience implementation of [[OperationAdjuster]] */ +class adjustOperation(f: Operation => Operation) extends OperationAdjuster { + def adjustOperation(value: Operation): Operation = f(value) +} + +/** + * Annotation that specifies description that will be included into generated OpenAPI specification. + * It can be applied on REST methods ([[OperationAdjuster]]), path/header/query parameters ([[ParameterAdjuster]]), + * body parameters ([[SchemaAdjuster]]), case class fields ([[SchemaAdjuster]]) and ADTs for which [[RestStructure]] + * is macro generated ([[SchemaAdjuster]]). + */ +class description(desc: String) extends SchemaAdjuster with ParameterAdjuster with OperationAdjuster { + def adjustSchema(schema: Schema): Schema = schema.copy(description = desc) + def adjustParameter(parameter: Parameter): Parameter = parameter.copy(description = desc) + def adjustOperation(operation: Operation): Operation = operation.copy(description = desc) +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/OpenApi.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/OpenApi.scala new file mode 100644 index 000000000..a6d49186f --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/OpenApi.scala @@ -0,0 +1,581 @@ +package com.avsystem.commons +package rest.openapi + +import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx} +import com.avsystem.commons.serialization.{transientDefault => td, _} + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#openapi-object OpenAPI Object]] + * from [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md OpenAPI 3.0 specification]]. + * It may be serialized to OpenAPI 3.0 compliant JSON using + * [[com.avsystem.commons.serialization.json.JsonStringOutput JsonStringOutput]]. + * This JSON can then be consumed by tools that support OpenAPI 3.0, e.g. + * [[https://swagger.io/tools/swagger-ui/ Swagger UI]]. + */ +case class OpenApi( + openapi: String = OpenApi.Version, + info: Info, + paths: Paths, + @td servers: List[Server] = Nil, + @td components: OptArg[Components] = OptArg.Empty, + @td security: List[SecurityRequirement] = Nil, + @td tags: List[Tag] = Nil, + @td externalDocs: OptArg[ExternalDocumentation] = OptArg.Empty +) +object OpenApi extends HasGenCodec[OpenApi] { + final val Version = "3.0.1" +} + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#infoObject Info Object]] + */ +case class Info( + title: String, + version: String, + @td license: OptArg[License] = OptArg.Empty, + @td description: OptArg[String] = OptArg.Empty, + @td termsOfService: OptArg[String] = OptArg.Empty, + @td contact: OptArg[Contact] = OptArg.Empty +) +object Info extends HasGenCodec[Info] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#contactObject Contact Object]] + */ +case class Contact( + @td name: OptArg[String] = OptArg.Empty, + @td url: OptArg[String] = OptArg.Empty, + @td email: OptArg[String] = OptArg.Empty +) +object Contact extends HasGenCodec[Contact] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#licenseObject License Object]] + */ +case class License( + name: String, + @td url: OptArg[String] = OptArg.Empty +) +object License extends HasGenCodec[License] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#serverObject Server Object]] + */ +case class Server( + url: String, + @td description: OptArg[String] = OptArg.Empty, + @td serverVariables: Map[String, ServerVariable] = Map.empty +) +object Server extends HasGenCodec[Server] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#serverVariableObject Server Variable Object]] + */ +case class ServerVariable( + default: String, + @td enum: List[String] = Nil, + @td description: OptArg[String] = OptArg.Empty +) +object ServerVariable extends HasGenCodec[ServerVariable] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#pathsObject Paths Object]] + */ +@transparent case class Paths(paths: Map[String, RefOr[PathItem]]) +object Paths extends HasGenCodec[Paths] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#pathItemObject Path Item Object]] + */ +case class PathItem( + @td summary: OptArg[String] = OptArg.Empty, + @td description: OptArg[String] = OptArg.Empty, + @td get: OptArg[Operation] = OptArg.Empty, + @td put: OptArg[Operation] = OptArg.Empty, + @td post: OptArg[Operation] = OptArg.Empty, + @td delete: OptArg[Operation] = OptArg.Empty, + @td options: OptArg[Operation] = OptArg.Empty, + @td head: OptArg[Operation] = OptArg.Empty, + @td patch: OptArg[Operation] = OptArg.Empty, + @td trace: OptArg[Operation] = OptArg.Empty, + @td servers: List[Server] = Nil, + @td parameters: List[RefOr[Parameter]] = Nil +) +object PathItem extends HasGenCodec[PathItem] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#operationObject Operation Object]] + */ +case class Operation( + responses: Responses, + @td tags: List[String] = Nil, + @td summary: OptArg[String] = OptArg.Empty, + @td description: OptArg[String] = OptArg.Empty, + @td externalDocs: OptArg[ExternalDocumentation] = OptArg.Empty, + @td operationId: OptArg[String] = OptArg.Empty, + @td parameters: List[RefOr[Parameter]] = Nil, + @td requestBody: OptArg[RefOr[RequestBody]] = OptArg.Empty, + @td callbacks: Map[String, RefOr[Callback]] = Map.empty, + @td deprecated: Boolean = false, + @td security: List[SecurityRequirement] = Nil, + @td servers: List[Server] = Nil +) +object Operation extends HasGenCodec[Operation] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#responsesObject Responses Object]] + */ +case class Responses( + byStatusCode: Map[Int, RefOr[Response]] = Map.empty, + default: OptArg[RefOr[Response]] = OptArg.Empty +) +object Responses { + final val DefaultField = "default" + + implicit val codec: GenCodec[Responses] = GenCodec.createNullableObject( + oi => { + var default = OptArg.empty[RefOr[Response]] + val byStatusCode = Map.newBuilder[Int, RefOr[Response]] + while (oi.hasNext) { + val fi = oi.nextField() + fi.fieldName match { + case DefaultField => + default = GenCodec.read[RefOr[Response]](fi) + case status => + byStatusCode += ((status.toInt, GenCodec.read[RefOr[Response]](fi))) + } + } + Responses(byStatusCode.result(), default) + }, + (oo, v) => { + v.default.foreach(resp => GenCodec.write[RefOr[Response]](oo.writeField(DefaultField), resp)) + v.byStatusCode.foreach { + case (status, resp) => + GenCodec.write[RefOr[Response]](oo.writeField(status.toString), resp) + } + } + ) +} + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#componentsObject Components Object]] + */ +case class Components( + @td schemas: Map[String, RefOr[Schema]] = Map.empty, + @td responses: Map[String, RefOr[Response]] = Map.empty, + @td parameters: Map[String, RefOr[Parameter]] = Map.empty, + @td examples: Map[String, RefOr[Example]] = Map.empty, + @td requestBodies: Map[String, RefOr[RequestBody]] = Map.empty, + @td headers: Map[String, RefOr[Header]] = Map.empty, + @td securitySchemes: Map[String, RefOr[SecurityScheme]] = Map.empty, + @td links: Map[String, RefOr[Link]] = Map.empty, + @td callbacks: Map[String, RefOr[Callback]] = Map.empty +) +object Components extends HasGenCodec[Components] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#securityRequirementObject Security Requirement Object]] + */ +@transparent case class SecurityRequirement(schemes: Map[String, List[String]]) +object SecurityRequirement extends HasGenCodec[SecurityRequirement] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#tagObject Tag Object]] + */ +case class Tag( + name: String, + @td description: OptArg[String] = OptArg.Empty, + @td externalDocs: OptArg[ExternalDocumentation] = OptArg.Empty +) +object Tag extends HasGenCodec[Tag] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#externalDocumentationObject External Documentation Object]] + */ +case class ExternalDocumentation( + url: String, + @td description: OptArg[String] = OptArg.Empty +) +object ExternalDocumentation extends HasGenCodec[ExternalDocumentation] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject Schema Object]] + */ +case class Schema( + @td `type`: OptArg[DataType] = OptArg.Empty, + @td format: OptArg[String] = OptArg.Empty, + @td title: OptArg[String] = OptArg.Empty, + @td description: OptArg[String] = OptArg.Empty, + @td nullable: Boolean = false, + @td readOnly: Boolean = false, + @td writeOnly: Boolean = false, + @td xml: OptArg[Xml] = OptArg.Empty, + @td externalDocs: OptArg[ExternalDocumentation] = OptArg.Empty, + @td deprecated: Boolean = false, + + @td multipleOf: OptArg[BigDecimal] = OptArg.Empty, + @td maximum: OptArg[BigDecimal] = OptArg.Empty, + @td exclusiveMaximum: Boolean = false, + @td minimum: OptArg[BigDecimal] = OptArg.Empty, + @td exclusiveMinimum: Boolean = false, + + @td maxLength: OptArg[Int] = OptArg.Empty, + @td minLength: OptArg[Int] = OptArg.Empty, + @td pattern: OptArg[String] = OptArg.Empty, + + @td items: OptArg[RefOr[Schema]] = OptArg.Empty, + @td maxItems: OptArg[Int] = OptArg.Empty, + @td minItems: OptArg[Int] = OptArg.Empty, + @td uniqueItems: Boolean = false, + + @td properties: Map[String, RefOr[Schema]] = Map.empty, + @td additionalProperties: OptArg[RefOr[Schema]] = OptArg.Empty, //TODO: boolean value support + @td maxProperties: OptArg[Int] = OptArg.Empty, + @td minProperties: OptArg[Int] = OptArg.Empty, + @td required: List[String] = Nil, + + @td allOf: List[RefOr[Schema]] = Nil, + @td oneOf: List[RefOr[Schema]] = Nil, + @td anyOf: List[RefOr[Schema]] = Nil, + @td not: OptArg[RefOr[Schema]] = OptArg.Empty, + @td discriminator: OptArg[Discriminator] = OptArg.Empty, + + @td enum: List[String] = Nil //TODO: other values than strings + + //TODO: default +) { + def unwrapSingleRefAllOf: RefOr[Schema] = allOf match { + case List(ref: RefOr.Ref) if this == Schema(allOf = List(ref)) => ref + case _ => RefOr(this) + } +} +object Schema extends HasGenCodec[Schema] { + final val Boolean = Schema(`type` = DataType.Boolean) + final val Char = Schema(`type` = DataType.String, minLength = 1, maxLength = 1) + final val Byte = Schema(`type` = DataType.Integer, format = Format.Int32, + minimum = BigDecimal(scala.Byte.MinValue), maximum = BigDecimal(scala.Byte.MaxValue)) + final val Short = Schema(`type` = DataType.Integer, format = Format.Int32, + minimum = BigDecimal(scala.Short.MinValue), maximum = BigDecimal(scala.Short.MaxValue)) + final val Int = Schema(`type` = DataType.Integer, format = Format.Int32) + final val Long = Schema(`type` = DataType.Integer, format = Format.Int64) + final val Float = Schema(`type` = DataType.Number, format = Format.Float) + final val Double = Schema(`type` = DataType.Number, format = Format.Double) + final val Integer = Schema(`type` = DataType.Integer) + final val Number = Schema(`type` = DataType.Number) + final val String = Schema(`type` = DataType.String) + final val Date = Schema(`type` = DataType.String, format = Format.Date) + final val DateTime = Schema(`type` = DataType.String, format = Format.DateTime) + final val Uuid = Schema(`type` = DataType.String, format = Format.Uuid) + final val Password = Schema(`type` = DataType.String, format = Format.Password) + final val Binary = Schema(`type` = DataType.String, format = Format.Binary) + final val Email = Schema(`type` = DataType.String, format = Format.Email) + + def arrayOf(items: RefOr[Schema], uniqueItems: Boolean = false): Schema = + Schema(`type` = DataType.Array, items = items, uniqueItems = uniqueItems) + + def mapOf(properties: RefOr[Schema]): Schema = + Schema(`type` = DataType.Object, additionalProperties = properties) + + def enumOf(values: List[String]): Schema = + Schema(`type` = DataType.String, enum = values) + + def nullable(schema: RefOr[Schema]): Schema = + schema.rewrapRefToAllOf.copy(nullable = true) + + implicit class RefOrOps(private val refOrSchema: RefOr[Schema]) extends AnyVal { + /** + * Transforms a potential schema reference into an actual [[Schema]] by wrapping the reference into + * `allOf` property of the new schema, e.g. `{"$$ref": "#/components/schemas/Entity"}` becomes + * `{"allOf": [{"$$ref": "#/components/schemas/Entity"}]}`. + */ + def rewrapRefToAllOf: Schema = refOrSchema match { + case RefOr.Value(schema) => schema + case ref => Schema(allOf = List(ref)) + } + } +} + +object Format { + final val Int32 = "int32" + final val Int64 = "int64" + final val Float = "float" + final val Double = "double" + final val Byte = "byte" + final val Binary = "binary" + final val Date = "date" + final val DateTime = "date-time" + final val Password = "password" + final val Email = "email" + final val Uuid = "uuid" +} + +final class DataType(implicit enumCtx: EnumCtx) extends AbstractValueEnum { + override val name: String = enumCtx.valName.uncapitalize +} +object DataType extends AbstractValueEnumCompanion[DataType] { + final val String, Number, Integer, Boolean, Array, Object: Value = new DataType +} + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#discriminatorObject Discriminator Object]] + */ +case class Discriminator( + propertyName: String, + @td mapping: Map[String, String] = Map.empty +) +object Discriminator extends HasGenCodec[Discriminator] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#xmlObject Xml Object]] + */ +case class Xml( + @td name: OptArg[String] = OptArg.Empty, + @td namespace: OptArg[String] = OptArg.Empty, + @td prefix: OptArg[String] = OptArg.Empty, + @td attribute: Boolean = false, + @td wrapped: Boolean = false +) +object Xml extends HasGenCodec[Xml] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#responseObject Response Object]] + */ +case class Response( + @td description: OptArg[String] = OptArg.Empty, + @td headers: Map[String, RefOr[Header]] = Map.empty, + @td content: Map[String, MediaType] = Map.empty, + @td links: Map[String, RefOr[Link]] = Map.empty +) +object Response extends HasGenCodec[Response] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#parameterObject Parameter Object]] + */ +case class Parameter( + name: String, + in: Location, + @td description: OptArg[String] = OptArg.Empty, + @td required: Boolean = false, + @td deprecated: Boolean = false, + @td allowEmptyValue: Boolean = false, + @td style: OptArg[Style] = OptArg.Empty, + @td explode: OptArg[Boolean] = OptArg.Empty, + @td allowReserved: Boolean = false, + @td schema: OptArg[RefOr[Schema]] = OptArg.Empty, + @td content: OptArg[Entry[String, MediaType]] = OptArg.Empty + //TODO example/examples +) +object Parameter extends HasGenCodec[Parameter] + +case class Entry[K, V](key: K, value: V) +object Entry { + implicit def codec[K: GenKeyCodec, V: GenCodec]: GenCodec[Entry[K, V]] = + GenCodec.createNullableObject( + oi => { + val fi = oi.nextField() + Entry(GenKeyCodec.read[K](fi.fieldName), GenCodec.read[V](fi)) + }, + (oo, entry) => + GenCodec.write[V](oo.writeField(GenKeyCodec.write[K](entry.key)), entry.value) + ) +} + +final class Location(implicit enumCtx: EnumCtx) extends AbstractValueEnum { + override val name: String = enumCtx.valName.uncapitalize +} +object Location extends AbstractValueEnumCompanion[Location] { + final val Query, Header, Path, Cookie: Value = new Location +} + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#style-values parameter style]] + */ +final class Style(implicit enumCtx: EnumCtx) extends AbstractValueEnum { + override val name: String = enumCtx.valName.uncapitalize +} +object Style extends AbstractValueEnumCompanion[Style] { + final val Matrix, Label, Form, Simple, SpaceDelimited, PipeDelimited, DeepObject: Value = new Style +} + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#mediaTypeObject Media Type Object]] + */ +case class MediaType( + @td schema: OptArg[RefOr[Schema]] = OptArg.Empty, + @td encoding: Map[String, Encoding] = Map.empty + //TODO: example/examples +) +object MediaType extends HasGenCodec[MediaType] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#encodingObject Encoding Object]] + */ +case class Encoding( + @td contentType: OptArg[String] = OptArg.Empty, + @td headers: Map[String, RefOr[Header]] = Map.empty, + @td style: OptArg[Style] = OptArg.Empty, + @td explode: OptArg[Boolean] = OptArg.Empty, + @td allowReserved: Boolean = false +) +object Encoding extends HasGenCodec[Encoding] + +case class Example() //TODO +object Example extends HasGenCodec[Example] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#requestBodyObject Request Body Object]] + */ +case class RequestBody( + content: Map[String, MediaType], + @td description: OptArg[String] = OptArg.Empty, + @td required: Boolean = false +) +object RequestBody extends HasGenCodec[RequestBody] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#headerObject Header Object]] + */ +case class Header( + @td description: OptArg[String] = OptArg.Empty, + @td required: Boolean = false, + @td deprecated: Boolean = false, + @td allowEmptyValue: Boolean = false, + @td style: OptArg[Style] = OptArg.Empty, + @td explode: OptArg[Boolean] = OptArg.Empty, + @td allowReserved: Boolean = false, + @td schema: OptArg[RefOr[Schema]] = OptArg.Empty, + @td content: OptArg[Entry[String, MediaType]] = OptArg.Empty + //TODO example/examples +) +object Header extends HasGenCodec[Header] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#securitySchemeObject Security Scheme Object]] + */ +@flatten("type") sealed trait SecurityScheme { + def description: OptArg[String] +} +object SecurityScheme { + @name("apiKey") case class ApiKey( + name: String, + in: Location, + @td description: OptArg[String] = OptArg.Empty + ) extends SecurityScheme + + @name("http") case class Http( + scheme: String, + @td bearerFormat: OptArg[String] = OptArg.Empty, + @td description: OptArg[String] = OptArg.Empty + ) extends SecurityScheme + + @name("oauth2") case class OAuth2( + flows: OAuthFlows, + @td description: OptArg[String] = OptArg.Empty + ) extends SecurityScheme + + @name("openIdConnect") case class OpenIdConnect( + openIdConnectUrl: String, + @td description: OptArg[String] = OptArg.Empty + ) extends SecurityScheme + + implicit val codec: GenCodec[SecurityScheme] = GenCodec.materialize +} + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#oauthFlowsObject OAuth Flows Object]] + */ +case class OAuthFlows( + @td `implicit`: OptArg[OAuthFlow] = OptArg.Empty, + @td password: OptArg[OAuthFlow] = OptArg.Empty, + @td clientCredentials: OptArg[OAuthFlow] = OptArg.Empty, + @td authorizationCode: OptArg[OAuthFlow] = OptArg.Empty +) +object OAuthFlows extends HasGenCodec[OAuthFlows] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#oauthFlowObject OAuth Flow Object]] + */ +case class OAuthFlow( + scopes: Map[String, String], + @td authorizationUrl: OptArg[String] = OptArg.Empty, + @td tokenUrl: OptArg[String] = OptArg.Empty, + @td refreshUrl: OptArg[String] = OptArg.Empty +) +object OAuthFlow extends HasGenCodec[OAuthFlow] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#linkObject Link Object]] + */ +case class Link( + @td operationRef: OptArg[String] = OptArg.Empty, + @td operationId: OptArg[String] = OptArg.Empty, + @td description: OptArg[String] = OptArg.Empty, + @td server: OptArg[Server] = OptArg.Empty + //TODO parameters, requestBody +) +object Link extends HasGenCodec[Link] + +/** + * Representation of + * [[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#callbackObject Callback Object]] + */ +@transparent case class Callback(byExpression: Map[String, PathItem]) +object Callback extends HasGenCodec[Callback] + +sealed trait RefOr[+A] +object RefOr { + case class Ref(ref: String) extends RefOr[Nothing] + case class Value[+A](value: A) extends RefOr[A] + + final val RefField = "$ref" + + def apply[A](value: A): RefOr[A] = Value(value) + def ref[A](ref: String): RefOr[A] = Ref(ref) + + implicit def codec[A: GenCodec]: GenCodec[RefOr[A]] = + GenCodec.createNullableObject( + oi => { + val poi = new PeekingObjectInput(oi) + val refFieldInput = poi.peekField(RefField).orElse { + if (poi.peekNextFieldName.contains(RefField)) poi.nextField().opt + else Opt.Empty + } + val res = refFieldInput.map(fi => Ref(fi.readString())) + .getOrElse(Value(GenCodec.read[A](new ObjectInputAsInput(poi)))) + poi.skipRemaining() + res + }, + (oo, value) => value match { + case Ref(refstr) => oo.writeField(RefField).writeString(refstr) + case Value(v) => GenCodec.write[A](new ObjectOutputAsOutput(oo, forwardFinish = false), v) + } + ) +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/OpenApiMetadata.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/OpenApiMetadata.scala new file mode 100644 index 000000000..88ff8c8b8 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/OpenApiMetadata.scala @@ -0,0 +1,204 @@ +package com.avsystem.commons +package rest.openapi + +import com.avsystem.commons.meta.{multi, reifyAnnot, _} +import com.avsystem.commons.rest.{Header => HeaderAnnot, _} +import com.avsystem.commons.rpc._ +import com.avsystem.commons.serialization.{transientDefault, whenAbsent} + +import scala.annotation.implicitNotFound + +@implicitNotFound("OpenApiMetadata for ${T} not found. The easiest way to provide it is to make companion object of " + + "${T} extend one of the convenience base companion classes, e.g. DefaultRestApiCompanion") +@methodTag[RestMethodTag] +case class OpenApiMetadata[T]( + @multi @tagged[Prefix](whenUntagged = new Prefix) + @paramTag[RestParamTag](defaultTag = new Path) + @rpcMethodMetadata + prefixes: List[OpenApiPrefix[_]], + + @multi @tagged[GET] + @paramTag[RestParamTag](defaultTag = new Query) + @rpcMethodMetadata + gets: List[OpenApiGetOperation[_]], + + @multi @tagged[BodyMethodTag](whenUntagged = new POST) + @paramTag[RestParamTag](defaultTag = new JsonBodyParam) + @rpcMethodMetadata + bodyMethods: List[OpenApiBodyOperation[_]] +) { + val httpMethods: List[OpenApiOperation[_]] = (gets: List[OpenApiOperation[_]]) ++ bodyMethods + + def operations(resolver: SchemaResolver): Iterator[(String, HttpMethod, Operation)] = + prefixes.iterator.flatMap(_.operations(resolver)) ++ + httpMethods.iterator.map(m => (m.pathPattern, m.methodTag.method, m.operation(resolver))) + + def paths(resolver: SchemaResolver): Paths = { + val pathsMap = new MLinkedHashMap[String, MLinkedHashMap[HttpMethod, Operation]] + operations(resolver).foreach { + case (path, httpMethod, operation) => + val opsMap = pathsMap.getOrElseUpdate(path, new MLinkedHashMap) + opsMap(httpMethod) = operation + } + Paths(pathsMap.iterator.map { case (path, ops) => + val pathItem = PathItem( + get = ops.getOpt(HttpMethod.GET).toOptArg, + put = ops.getOpt(HttpMethod.PUT).toOptArg, + post = ops.getOpt(HttpMethod.POST).toOptArg, + patch = ops.getOpt(HttpMethod.PATCH).toOptArg, + delete = ops.getOpt(HttpMethod.DELETE).toOptArg + ) + (path, RefOr(pathItem)) + }.toMap) + } + + def openapi( + info: Info, + servers: List[Server] = Nil, + security: List[SecurityRequirement] = Nil, + tags: List[Tag] = Nil, + externalDocs: OptArg[ExternalDocumentation] = OptArg.Empty + ): OpenApi = { + val registry = new SchemaRegistry(n => s"#/components/schemas/$n") + OpenApi(OpenApi.Version, + info, + paths(registry), + components = Components(schemas = registry.registeredSchemas), + servers = servers, + security = security, + tags = tags, + externalDocs = externalDocs + ) + } +} +object OpenApiMetadata extends RpcMetadataCompanion[OpenApiMetadata] + +sealed trait OpenApiMethod[T] extends TypedMetadata[T] { + @reifyName(useRawName = true) def name: String + @reifyAnnot def methodTag: RestMethodTag + @multi + @rpcParamMetadata + @tagged[NonBodyTag] def parameters: List[OpenApiParameter[_]] + + val pathPattern: String = { + val pathParts = methodTag.path :: parameters.flatMap { + case OpenApiParameter(path: Path, info, _) => + s"{${info.name}}" :: path.pathSuffix :: Nil + case _ => Nil + } + pathParts.iterator.map(_.stripPrefix("/").stripSuffix("/")).filter(_.nonEmpty).mkString("/", "/", "") + } +} + +case class OpenApiPrefix[T]( + name: String, + methodTag: Prefix, + parameters: List[OpenApiParameter[_]], + @infer @checked result: OpenApiMetadata.Lazy[T] +) extends OpenApiMethod[T] { + + def operations(resolver: SchemaResolver): Iterator[(String, HttpMethod, Operation)] = { + val prefixParams = parameters.map(_.parameter(resolver)) + result.value.operations(resolver).map { case (path, httpMethod, operation) => + (pathPattern + path, httpMethod, operation.copy( + operationId = operation.operationId.toOpt.map(oid => s"${name}_$oid").toOptArg, + parameters = prefixParams ++ operation.parameters + )) + } + } +} + +sealed trait OpenApiOperation[T] extends OpenApiMethod[T] { + @infer + @checked def resultType: RestResultType[T] + @multi + @reifyAnnot def adjusters: List[OperationAdjuster] + def methodTag: HttpMethodTag + def requestBody(resolver: SchemaResolver): Opt[RefOr[RequestBody]] + + def operation(resolver: SchemaResolver): Operation = { + val op = Operation( + resultType.responses(resolver), + operationId = name, + parameters = parameters.map(_.parameter(resolver)), + requestBody = requestBody(resolver).toOptArg + ) + adjusters.foldRight(op)(_ adjustOperation _) + } +} + +case class OpenApiGetOperation[T]( + name: String, + methodTag: HttpMethodTag, + parameters: List[OpenApiParameter[_]], + resultType: RestResultType[T], + adjusters: List[OperationAdjuster] +) extends OpenApiOperation[T] { + def requestBody(resolver: SchemaResolver): Opt[RefOr[RequestBody]] = Opt.Empty +} + +case class OpenApiBodyOperation[T]( + name: String, + methodTag: HttpMethodTag, + parameters: List[OpenApiParameter[_]], + @multi @rpcParamMetadata @tagged[JsonBodyParam] bodyParams: List[OpenApiBodyField[_]], + @optional @encoded @rpcParamMetadata @tagged[Body] singleBody: Opt[OpenApiBody[_]], + resultType: RestResultType[T], + adjusters: List[OperationAdjuster] +) extends OpenApiOperation[T] { + + def requestBody(resolver: SchemaResolver): Opt[RefOr[RequestBody]] = + singleBody.map(_.requestBody(resolver).opt).getOrElse { + if (bodyParams.isEmpty) Opt.Empty else Opt { + val schema = Schema(`type` = DataType.Object, + properties = bodyParams.iterator.map(p => (p.info.name, p.schema(resolver))).toMap, + required = bodyParams.collect { case p if !p.info.hasFallbackValue => p.info.name } + ) + RefOr(RestRequestBody.jsonRequestBody(RefOr(schema))) + } + } +} + +case class OpenApiParamInfo[T]( + @reifyName(useRawName = true) name: String, + @optional @reifyAnnot whenAbsent: Opt[whenAbsent[T]], + @isAnnotated[transientDefault] transientDefault: Boolean, + @reifyFlags flags: ParamFlags, + @infer restSchema: RestSchema[T] +) extends TypedMetadata[T] { + val hasFallbackValue: Boolean = + whenAbsent.fold(flags.hasDefaultValue)(wa => Try(wa.value).isSuccess) +} + +case class OpenApiParameter[T]( + @reifyAnnot paramTag: NonBodyTag, + @composite info: OpenApiParamInfo[T], + @multi @reifyAnnot adjusters: List[ParameterAdjuster] +) extends TypedMetadata[T] { + + def parameter(resolver: SchemaResolver): RefOr[Parameter] = { + val in = paramTag match { + case _: Path => Location.Path + case _: HeaderAnnot => Location.Header + case _: Query => Location.Query + } + val param = Parameter(info.name, in, required = !info.hasFallbackValue, schema = resolver.resolve(info.restSchema)) + RefOr(adjusters.foldRight(param)(_ adjustParameter _)) + } +} + +case class OpenApiBodyField[T]( + @composite info: OpenApiParamInfo[T], + @multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster] +) extends TypedMetadata[T] { + def schema(resolver: SchemaResolver): RefOr[Schema] = + SchemaAdjuster.adjustRef(schemaAdjusters, resolver.resolve(info.restSchema)) +} + +case class OpenApiBody[T]( + @infer restRequestBody: RestRequestBody[T], + @multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster] +) extends TypedMetadata[T] { + def requestBody(resolver: SchemaResolver): RefOr[RequestBody] = + restRequestBody.requestBody(resolver, schemaAdjusters) +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/RestSchema.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/RestSchema.scala new file mode 100644 index 000000000..afb892ddb --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/RestSchema.scala @@ -0,0 +1,216 @@ +package com.avsystem.commons +package rest.openapi + +import java.util.UUID + +import com.avsystem.commons.misc.{NamedEnum, NamedEnumCompanion, Timestamp} +import com.avsystem.commons.rest.HttpBody + +import scala.annotation.implicitNotFound + +@implicitNotFound("RestSchema for ${T} not found. You may provide it by making companion object of ${T} " + + "extend RestDataCompanion[${T}] (if it is a case class, object or sealed hierarchy). " + + "Note that RestDataCompanion also automatically provides a GenCodec instance.") +trait RestSchema[T] { self => + def createSchema(resolver: SchemaResolver): RefOr[Schema] + def name: Opt[String] + + def map[S](fun: RefOr[Schema] => Schema, newName: OptArg[String] = OptArg.Empty): RestSchema[S] = + RestSchema.create(resolver => RefOr(fun(resolver.resolve(self))), newName) + def named(name: String): RestSchema[T] = + RestSchema.create(createSchema, name) + def unnamed: RestSchema[T] = + RestSchema.create(createSchema) +} +object RestSchema { + def apply[T](implicit rt: RestSchema[T]): RestSchema[T] = rt + + def create[T](creator: SchemaResolver => RefOr[Schema], schemaName: OptArg[String] = OptArg.Empty): RestSchema[T] = + new RestSchema[T] { + def createSchema(resolver: SchemaResolver): RefOr[Schema] = creator(resolver) + def name: Opt[String] = schemaName.toOpt + } + + def plain[T](schema: Schema): RestSchema[T] = + RestSchema.create(_ => RefOr(schema)) + + def ref[T](refstr: String): RestSchema[T] = + RestSchema.create(_ => RefOr.ref(refstr)) + + implicit lazy val NothingSchema: RestSchema[Nothing] = + RestSchema.create(_ => throw new NotImplementedError("RestSchema[Nothing]")) + + implicit lazy val UnitSchema: RestSchema[Unit] = plain(Schema(nullable = true)) + implicit lazy val NullSchema: RestSchema[Null] = plain(Schema(nullable = true)) + implicit lazy val VoidSchema: RestSchema[Void] = plain(Schema(nullable = true)) + + implicit lazy val BooleanSchema: RestSchema[Boolean] = plain(Schema.Boolean) + implicit lazy val CharSchema: RestSchema[Char] = plain(Schema.Char) + implicit lazy val ByteSchema: RestSchema[Byte] = plain(Schema.Byte) + implicit lazy val ShortSchema: RestSchema[Short] = plain(Schema.Short) + implicit lazy val IntSchema: RestSchema[Int] = plain(Schema.Int) + implicit lazy val LongSchema: RestSchema[Long] = plain(Schema.Long) + implicit lazy val FloatSchema: RestSchema[Float] = plain(Schema.Float) + implicit lazy val DoubleSchema: RestSchema[Double] = plain(Schema.Double) + implicit lazy val BigIntSchema: RestSchema[BigInt] = plain(Schema.Integer) + implicit lazy val BigDecimalSchema: RestSchema[BigDecimal] = plain(Schema.Number) + + implicit lazy val JBooleanSchema: RestSchema[JBoolean] = plain(Schema.Boolean.copy(nullable = true)) + implicit lazy val JCharacterSchema: RestSchema[JCharacter] = plain(Schema.Char.copy(nullable = true)) + implicit lazy val JByteSchema: RestSchema[JByte] = plain(Schema.Byte.copy(nullable = true)) + implicit lazy val JShortSchema: RestSchema[JShort] = plain(Schema.Short.copy(nullable = true)) + implicit lazy val JIntegerSchema: RestSchema[JInteger] = plain(Schema.Int.copy(nullable = true)) + implicit lazy val JLongSchema: RestSchema[JLong] = plain(Schema.Long.copy(nullable = true)) + implicit lazy val JFloatSchema: RestSchema[JFloat] = plain(Schema.Float.copy(nullable = true)) + implicit lazy val JDoubleSchema: RestSchema[JDouble] = plain(Schema.Double.copy(nullable = true)) + implicit lazy val JBigIntegerSchema: RestSchema[JBigInteger] = plain(Schema.Integer) + implicit lazy val JBigDecimalSchema: RestSchema[JBigDecimal] = plain(Schema.Number) + + implicit lazy val TimestampSchema: RestSchema[Timestamp] = plain(Schema.DateTime) + implicit lazy val JDateSchema: RestSchema[JDate] = plain(Schema.DateTime) + implicit lazy val StringSchema: RestSchema[String] = plain(Schema.String) + implicit lazy val SymbolSchema: RestSchema[Symbol] = plain(Schema.String) + implicit lazy val UuidSchema: RestSchema[UUID] = plain(Schema.Uuid) + + implicit def arraySchema[T: RestSchema]: RestSchema[Array[T]] = + RestSchema[T].map(Schema.arrayOf(_)) + implicit def seqSchema[C[X] <: BSeq[X], T: RestSchema]: RestSchema[C[T]] = + RestSchema[T].map(Schema.arrayOf(_)) + implicit def setSchema[C[X] <: BSet[X], T: RestSchema]: RestSchema[C[T]] = + RestSchema[T].map(Schema.arrayOf(_, uniqueItems = true)) + implicit def jCollectionSchema[C[X] <: JCollection[X], T: RestSchema]: RestSchema[C[T]] = + RestSchema[T].map(Schema.arrayOf(_)) + implicit def jSetSchema[C[X] <: JSet[X], T: RestSchema]: RestSchema[C[T]] = + RestSchema[T].map(Schema.arrayOf(_, uniqueItems = true)) + implicit def mapSchema[M[X, Y] <: BMap[X, Y], K, V: RestSchema]: RestSchema[M[K, V]] = + RestSchema[V].map(Schema.mapOf) + implicit def jMapSchema[M[X, Y] <: JMap[X, Y], K, V: RestSchema]: RestSchema[M[K, V]] = + RestSchema[V].map(Schema.mapOf) + + implicit def optionSchema[T: RestSchema]: RestSchema[Option[T]] = + RestSchema[T].map(Schema.nullable) + implicit def optSchema[T: RestSchema]: RestSchema[Opt[T]] = + RestSchema[T].map(Schema.nullable) + implicit def optArgSchema[T: RestSchema]: RestSchema[OptArg[T]] = + RestSchema[T].map(Schema.nullable) + implicit def optRefSchema[T >: Null : RestSchema]: RestSchema[OptRef[T]] = + RestSchema[T].map(Schema.nullable) + implicit def nOptSchema[T: RestSchema]: RestSchema[NOpt[T]] = + RestSchema[T].map(Schema.nullable) + + implicit def namedEnumSchema[E <: NamedEnum](implicit comp: NamedEnumCompanion[E]): RestSchema[E] = + RestSchema.plain(Schema.enumOf(comp.values.iterator.map(_.name).toList)) + implicit def jEnumSchema[E <: Enum[E]](implicit ct: ClassTag[E]): RestSchema[E] = + RestSchema.plain(Schema.enumOf(ct.runtimeClass.getEnumConstants.iterator.map(_.asInstanceOf[E].name).toList)) +} + +@implicitNotFound("RestResultType for ${T} not found. It may be provided by appropriate RestSchema or " + + "RestResponses instance (e.g. RestSchema[T] implies RestResponses[T] which implies RestResultType[Future[T]]). " + + "RestSchema is usually provided by making companion object of your data type extend RestDataCompanion.") +case class RestResultType[T](responses: SchemaResolver => Responses) +object RestResultType { + implicit def forFuture[T: RestResponses]: RestResultType[Future[T]] = + RestResultType[Future[T]](RestResponses[T].responses) +} + +@implicitNotFound("RestResponses for ${T} not found. You may provide it by defining an instance of RestSchema[${T}] " + + "which is usually done by making companion object of your data type extend RestDataCompanion") +case class RestResponses[T](responses: SchemaResolver => Responses) +object RestResponses { + def apply[T](implicit r: RestResponses[T]): RestResponses[T] = r + + implicit val emptyResponseForUnit: RestResponses[Unit] = + RestResponses(_ => Responses(byStatusCode = Map( + 204 -> RefOr(Response()) + ))) + + implicit def fromSchema[T: RestSchema]: RestResponses[T] = + RestResponses(resolver => Responses(byStatusCode = Map( + 200 -> RefOr(Response(content = Map( + HttpBody.JsonType -> MediaType(schema = resolver.resolve(RestSchema[T]))) + )) + ))) +} + +@implicitNotFound("RestRequestBody for ${T} not found. You may provide it by defining an instance of RestSchema[${T}] " + + "which is usually done by making companion object of your data type extend RestDataCompanion") +trait RestRequestBody[T] { + def requestBody(resolver: SchemaResolver, schemaAdjusters: List[SchemaAdjuster]): RefOr[RequestBody] +} +object RestRequestBody { + def apply[T](implicit r: RestRequestBody[T]): RestRequestBody[T] = r + + def jsonRequestBody(schema: RefOr[Schema]): RequestBody = + RequestBody( + content = Map( + HttpBody.JsonType -> MediaType(schema = schema) + ), + required = true + ) + + implicit def fromSchema[T: RestSchema]: RestRequestBody[T] = + new RestRequestBody[T] { + def requestBody(resolver: SchemaResolver, schemaAdjusters: List[SchemaAdjuster]): RefOr[RequestBody] = + RefOr(jsonRequestBody(SchemaAdjuster.adjustRef(schemaAdjusters, resolver.resolve(RestSchema[T])))) + } +} + +trait SchemaResolver { + def resolve(schema: RestSchema[_]): RefOr[Schema] +} + +final class InliningResolver extends SchemaResolver { + private[this] val resolving = new MHashSet[String] + + def resolve(schema: RestSchema[_]): RefOr[Schema] = + try { + schema.name.foreach { n => + if (!resolving.add(n)) { + throw new IllegalArgumentException(s"Recursive schema reference: $n") + } + } + schema.createSchema(this) + } + finally { + schema.name.foreach(resolving.remove) + } +} + +final class SchemaRegistry(nameToRef: String => String, initial: Iterable[(String, RefOr[Schema])] = Map.empty) + extends SchemaResolver { + + private[this] case class Entry(source: Opt[RestSchema[_]], schema: RefOr[Schema]) + + private[this] val resolving = new MHashSet[String] + private[this] val registry = new MLinkedHashMap[String, MListBuffer[Entry]] + .setup(_ ++= initial.iterator.map { case (n, s) => (n, MListBuffer[Entry](Entry(Opt.Empty, s))) }) + + def registeredSchemas: Map[String, RefOr[Schema]] = + registry.iterator.map { case (k, entries) => + entries.result() match { + case Entry(_, schema) :: Nil => (k, schema) + case _ => throw new IllegalArgumentException( + s"Multiple schemas named $k detected - you may want to disambiguate them using @name annotation" + ) + } + }.toMap + + def resolve(restSchema: RestSchema[_]): RefOr[Schema] = restSchema.name match { + case Opt(name) => + if (!resolving.contains(name)) { // handling recursive schemas + val entries = registry.getOrElseUpdate(name, new MListBuffer) + if (!entries.exists(_.source.contains(restSchema))) { + resolving += name + val newSchema = try restSchema.createSchema(this) finally { + resolving -= name + } + if (!entries.exists(_.schema == newSchema)) { + entries += Entry(Opt(restSchema), newSchema) + } + } + } + RefOr.ref(nameToRef(name)) + case Opt.Empty => + restSchema.createSchema(this) + } +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/RestStructure.scala b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/RestStructure.scala new file mode 100644 index 000000000..bc4347311 --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/rest/openapi/RestStructure.scala @@ -0,0 +1,152 @@ +package com.avsystem.commons +package rest.openapi + +import com.avsystem.commons.annotation.positioned +import com.avsystem.commons.meta._ +import com.avsystem.commons.misc.ValueOf +import com.avsystem.commons.serialization._ + +sealed trait RestStructure[T] extends TypedMetadata[T] { + def schemaAdjusters: List[SchemaAdjuster] + def info: GenInfo[T] + + protected def applyAdjusters(schema: Schema): Schema = + schemaAdjusters.foldRight(schema)(_ adjustSchema _) +} +object RestStructure extends AdtMetadataCompanion[RestStructure] { + implicit class LazyRestStructureOps[T](restStructure: => RestStructure[T]) { + def standaloneSchema: RestSchema[T] = new RestSchema[T] { + def createSchema(resolver: SchemaResolver): RefOr[Schema] = restStructure match { + case union: Union[T] => union.createSchema(resolver) + case record: Record[T] => record.createSchema(resolver, Opt.Empty) + case singleton: Singleton[T] => singleton.createSchema(resolver, Opt.Empty) + } + def name: Opt[String] = restStructure match { + case _: Singleton[_] => Opt.Empty + case s => s.info.rawName.opt + } + } + } + + @positioned(positioned.here) case class Union[T]( + @multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster], + @adtCaseMetadata @multi cases: List[Case[_]], + @composite info: GenUnionInfo[T] + ) extends RestStructure[T] { + + def createSchema(resolver: SchemaResolver): RefOr[Schema] = { + val caseFieldOpt = info.flatten.map(_.caseFieldName) + val caseSchemas = caseFieldOpt match { + case Opt(caseFieldName) => cases.map { cs => + val caseName = cs.info.rawName + val caseRestSchema = cs match { + case record: Record[_] => RestSchema.create(record.createSchema(_, caseFieldOpt), caseName) + case singleton: Singleton[_] => RestSchema.create(singleton.createSchema(_, caseFieldOpt), caseName) + case custom: CustomCase[_] => + val caseFieldSchema = RefOr(Schema.enumOf(List(caseName))) + custom.restSchema.map({ + case RefOr.Value(caseSchema) => caseSchema.copy( + properties = caseSchema.properties.updated(caseFieldName, caseFieldSchema), + required = caseFieldName :: caseSchema.required + ) + case ref => Schema(allOf = List(RefOr(Schema( + `type` = DataType.Object, + properties = Map(caseFieldName -> caseFieldSchema), + required = List(caseFieldName) + )), ref)) + }, custom.taggedName) + } + resolver.resolve(caseRestSchema) + } + case Opt.Empty => cases.map { cs => + val caseName = cs.info.rawName + val caseSchema = cs match { + case record: Record[_] => record.createSchema(resolver, Opt.Empty) + case singleton: Singleton[_] => singleton.createSchema(resolver, Opt.Empty) + case custom: CustomCase[_] => resolver.resolve(custom.restSchema) + } + RefOr(Schema( + `type` = DataType.Object, + properties = Map(caseName -> caseSchema), + required = List(caseName) + )) + } + } + val disc = caseFieldOpt.map { caseFieldName => + val mapping = cases.collect { + case custom: CustomCase[_] if custom.taggedName != custom.info.rawName => + (custom.info.rawName, custom.taggedName) + }.toMap + Discriminator(caseFieldName, mapping) + } + RefOr(applyAdjusters(Schema(oneOf = caseSchemas, discriminator = disc.toOptArg))) + } + } + object Union extends AdtMetadataCompanion[Union] + + sealed trait Case[T] extends TypedMetadata[T] { + def info: GenCaseInfo[T] + } + object Case extends AdtMetadataCompanion[Case] + + /** + * Will be inferred for case types that already have [[RestSchema]] defined directly. + */ + @positioned(positioned.here) case class CustomCase[T]( + @checked @infer restSchema: RestSchema[T], + @composite info: GenCaseInfo[T] + ) extends Case[T] { + def taggedName: String = + if (restSchema.name.contains(info.rawName)) s"tagged${info.rawName}" + else info.rawName + } + + /** + * Will be inferred for types having apply/unapply(Seq) pair in their companion. + */ + @positioned(positioned.here) case class Record[T]( + @multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster], + @adtParamMetadata @multi fields: List[Field[_]], + @composite info: GenCaseInfo[T] + ) extends RestStructure[T] with Case[T] { + + def createSchema(resolver: SchemaResolver, caseFieldName: Opt[String]): RefOr[Schema] = + (fields, caseFieldName) match { + case (single :: Nil, Opt.Empty) if info.transparent => + SchemaAdjuster.adjustRef(schemaAdjusters, resolver.resolve(single.restSchema)) + case _ => + val props = caseFieldName.map(cfn => (cfn, RefOr(Schema.enumOf(List(info.rawName))))).iterator ++ + fields.iterator.map(f => (f.info.rawName, f.resolveSchema(resolver))) + val required = caseFieldName.iterator ++ + fields.iterator.filterNot(_.info.hasFallbackValue).map(_.info.rawName) + RefOr(applyAdjusters(Schema(`type` = DataType.Object, properties = props.toMap, required = required.toList))) + } + } + object Record extends AdtMetadataCompanion[Record] + + /** + * Will be inferred for singleton types (objects). + */ + @positioned(positioned.here) case class Singleton[T]( + @multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster], + @infer @checked value: ValueOf[T], + @composite info: GenCaseInfo[T] + ) extends RestStructure[T] with Case[T] { + + def createSchema(resolver: SchemaResolver, caseFieldName: Opt[String]): RefOr[Schema] = + RefOr(applyAdjusters(Schema(`type` = DataType.Object, + properties = caseFieldName.map(cfn => (cfn, RefOr(Schema.enumOf(List(info.rawName))))).toMap, + required = caseFieldName.toList + ))) + } + object Singleton extends AdtMetadataCompanion[Singleton] + + case class Field[T]( + @composite info: GenParamInfo[T], + @infer restSchema: RestSchema[T], + @multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster] + ) extends TypedMetadata[T] { + def resolveSchema(resolver: SchemaResolver): RefOr[Schema] = + SchemaAdjuster.adjustRef(schemaAdjusters, resolver.resolve(restSchema)) + } +} 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 5d34d2100..75dfbe3bd 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,7 @@ package com.avsystem.commons package rpc +import com.avsystem.commons.meta._ import com.avsystem.commons.serialization.GenCodec import scala.language.higherKinds @@ -67,7 +68,7 @@ trait RPCFramework { trait Signature { @reifyName def name: String - @multi def paramMetadata: List[ParamMetadata[_]] + @multi @rpcParamMetadata def paramMetadata: List[ParamMetadata[_]] @reifyAnnot @multi def annotations: List[MetadataAnnotation] } 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 e0ed1cf41..cc84b96fd 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 @@ -23,9 +23,13 @@ package rpc * 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`. + * + * If `RpcMacroInstances` is accepted as implicit super constructor parameter of a companion object + * (which is the typical situation) then `this` reference should be passed as `companion`. + * This is in order to work around https://github.com/scala/bug/issues/7666 */ trait RpcMacroInstances[Implicits, InstancesTrait[_], Real] { - def apply(implicits: Implicits): InstancesTrait[Real] + def apply(implicits: Implicits, companion: Any): InstancesTrait[Real] } object RpcMacroInstances { /** 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 0af6c493a..10315c67c 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 @@ -2,21 +2,8 @@ package com.avsystem.commons package rpc import com.avsystem.commons.macros.rpc.RpcMacros +import com.avsystem.commons.meta.MetadataCompanion -trait RpcMetadataCompanion[M[_]] extends RpcImplicitsProvider { - final def apply[Real](implicit metadata: M[Real]): M[Real] = metadata - +trait RpcMetadataCompanion[M[_]] extends MetadataCompanion[M] with RpcImplicitsProvider { def materializeForRpc[Real]: M[Real] = macro RpcMacros.rpcMetadata[Real] - - 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 - } - object Lazy { - def apply[Real](metadata: => M[Real]): Lazy[Real] = new Lazy(metadata) - - // macro effectively turns `metadata` param into by-name param (implicit params by themselves cannot be by-name) - implicit def lazyMetadata[Real](implicit metadata: M[Real]): Lazy[Real] = macro RpcMacros.lazyMetadata - } } 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 c1e0bc321..64ede9745 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 @@ -1,6 +1,8 @@ package com.avsystem.commons package rpc +import com.avsystem.commons.meta._ + /** * Mix in this trait into your RPC framework to support remote procedures, i.e. fire-and-forget methods * with `Unit` return type. @@ -74,9 +76,9 @@ trait StandardRPCFramework extends GetterRPCFramework with FunctionRPCFramework case class RPCMetadata[T]( @reifyName name: String, @reifyAnnot @multi annotations: List[MetadataAnnotation], - @multi @verbatim procedureSignatures: Map[String, ProcedureSignature], - @multi functionSignatures: Map[String, FunctionSignature[_]], - @multi getterSignatures: Map[String, GetterSignature[_]] + @multi @verbatim @rpcMethodMetadata procedureSignatures: Map[String, ProcedureSignature], + @multi @rpcMethodMetadata functionSignatures: Map[String, FunctionSignature[_]], + @multi @rpcMethodMetadata getterSignatures: Map[String, GetterSignature[_]] ) object RPCMetadata extends RpcMetadataCompanion[RPCMetadata] } @@ -90,8 +92,8 @@ trait OneWayRPCFramework extends GetterRPCFramework with ProcedureRPCFramework { case class RPCMetadata[T]( @reifyName name: String, @reifyAnnot @multi annotations: List[MetadataAnnotation], - @multi @verbatim procedureSignatures: Map[String, ProcedureSignature], - @multi getterSignatures: Map[String, GetterSignature[_]] + @multi @verbatim @rpcMethodMetadata procedureSignatures: Map[String, ProcedureSignature], + @multi @rpcMethodMetadata getterSignatures: Map[String, GetterSignature[_]] ) object RPCMetadata extends RpcMetadataCompanion[RPCMetadata] } diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/DefaultCaseObjectInput.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/DefaultCaseObjectInput.scala index fb66c0b96..9c429408e 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/serialization/DefaultCaseObjectInput.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/DefaultCaseObjectInput.scala @@ -3,7 +3,9 @@ package serialization import com.avsystem.commons.serialization.GenCodec.ReadFailure -final class DefaultCaseObjectInput(firstField: FieldInput, actualInput: ObjectInput, caseFieldName: String) extends ObjectInput { +final class DefaultCaseObjectInput(firstField: FieldInput, actualInput: ObjectInput, caseFieldName: String) + extends ObjectInput { + private[this] var atFirstField = true def hasNext = atFirstField || actualInput.hasNext diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala index 8714d75fd..63bf9f9f8 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodec.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package serialization +import java.util.UUID + import com.avsystem.commons.annotation.explicitGenerics import com.avsystem.commons.derivation.{AllowImplicitMacro, DeferredInstance} import com.avsystem.commons.jiop.JCanBuildFrom @@ -274,7 +276,8 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { final val DefaultCaseField = "_case" - implicit lazy val NothingCodec: GenCodec[Nothing] = create[Nothing](_ => throw new ReadFailure("read Nothing"), (_, _) => throw new WriteFailure("write Nothing")) + implicit lazy val NothingCodec: GenCodec[Nothing] = + create[Nothing](_ => throw new ReadFailure("read Nothing"), (_, _) => throw new WriteFailure("write Nothing")) implicit lazy val NullCodec: GenCodec[Null] = create[Null](_.readNull(), (o, _) => o.writeNull()) implicit lazy val UnitCodec: GenCodec[Unit] = create[Unit](_.readUnit(), (o, _) => o.writeUnit()) implicit lazy val VoidCodec: GenCodec[Void] = create[Void](_.readNull(), (o, _) => o.writeNull()) @@ -303,10 +306,16 @@ object GenCodec extends RecursiveAutoCodecs with TupleGenCodecs { implicit lazy val JBigDecimalCodec: GenCodec[JBigDecimal] = createNullable(_.readBigDecimal().bigDecimal, (o, v) => o.writeBigDecimal(BigDecimal(v))) - implicit lazy val JDateCodec: GenCodec[JDate] = createNullable(i => new JDate(i.readTimestamp()), (o, d) => o.writeTimestamp(d.getTime)) - implicit lazy val StringCodec: GenCodec[String] = createNullable(_.readString(), _ writeString _) - implicit lazy val SymbolCodec: GenCodec[Symbol] = createNullable(i => Symbol(i.readString()), (o, s) => o.writeString(s.name)) - implicit lazy val ByteArrayCodec: GenCodec[Array[Byte]] = createNullable(_.readBinary(), _ writeBinary _) + implicit lazy val JDateCodec: GenCodec[JDate] = + createNullable(i => new JDate(i.readTimestamp()), (o, d) => o.writeTimestamp(d.getTime)) + implicit lazy val StringCodec: GenCodec[String] = + createNullable(_.readString(), _ writeString _) + implicit lazy val SymbolCodec: GenCodec[Symbol] = + createNullable(i => Symbol(i.readString()), (o, s) => o.writeString(s.name)) + implicit lazy val ByteArrayCodec: GenCodec[Array[Byte]] = + createNullable(_.readBinary(), _ writeBinary _) + implicit lazy val UuidCodec: GenCodec[UUID] = + createNullable(i => UUID.fromString(i.readString()), (o, v) => o.writeString(v.toString)) private implicit class TraversableOnceOps[A](private val coll: TraversableOnce[A]) extends AnyVal { def writeToList(lo: ListOutput)(implicit writer: GenCodec[A]): Unit = diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodecStructure.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodecStructure.scala new file mode 100644 index 000000000..72346fc7f --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/GenCodecStructure.scala @@ -0,0 +1,44 @@ +package com.avsystem.commons +package serialization + +import com.avsystem.commons.annotation.positioned +import com.avsystem.commons.meta._ + +sealed trait GenInfo[T] extends TypedMetadata[T] { + def sourceName: String + def annotName: Opt[name] + def rawName: String = annotName.fold(sourceName)(_.name) +} + +case class GenParamInfo[T]( + @reifyName sourceName: String, + @optional @reifyAnnot annotName: Opt[name], + @optional @reifyAnnot whenAbsent: Opt[whenAbsent[T]], + @isAnnotated[transientDefault] transientDefault: Boolean, + @isAnnotated[outOfOrder] outOfOrder: Boolean, + @reifyFlags flags: ParamFlags +) extends GenInfo[T] { + val hasFallbackValue: Boolean = + whenAbsent.fold(flags.hasDefaultValue)(wa => Try(wa.value).isSuccess) +} + +sealed trait GenCodecStructure[T] extends GenInfo[T] { + def flags: TypeFlags +} + +@positioned(positioned.here) case class GenUnionInfo[T]( + @reifyFlags flags: TypeFlags, + @reifyName sourceName: String, + @optional @reifyAnnot annotName: Opt[name], + @optional @reifyAnnot flatten: Opt[flatten] +) extends GenCodecStructure[T] +object GenUnionInfo extends AdtMetadataCompanion[GenUnionInfo] + +@positioned(positioned.here) case class GenCaseInfo[T]( + @reifyFlags flags: TypeFlags, + @reifyName sourceName: String, + @optional @reifyAnnot annotName: Opt[name], + @isAnnotated[transparent] transparent: Boolean, + @isAnnotated[defaultCase] defaultCase: Boolean +) extends GenCodecStructure[T] +object GenCaseInfo extends AdtMetadataCompanion[GenCaseInfo] diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/GenKeyCodec.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/GenKeyCodec.scala index 392ccbdc3..6bc362da1 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/serialization/GenKeyCodec.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/GenKeyCodec.scala @@ -1,6 +1,8 @@ package com.avsystem.commons package serialization +import java.util.UUID + import com.avsystem.commons.annotation.explicitGenerics import scala.annotation.implicitNotFound @@ -52,6 +54,7 @@ object GenKeyCodec { implicit lazy val StringKeyCodec: GenKeyCodec[String] = create(identity, identity) implicit lazy val SymbolKeyCodec: GenKeyCodec[Symbol] = create(Symbol(_), _.name) + implicit lazy val UuidCodec: GenKeyCodec[UUID] = create(UUID.fromString, _.toString) implicit def jEnumKeyCodec[E <: Enum[E]](implicit ct: ClassTag[E]): GenKeyCodec[E] = GenKeyCodec.create( diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/PeekingObjectInput.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/PeekingObjectInput.scala new file mode 100644 index 000000000..99453a31a --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/PeekingObjectInput.scala @@ -0,0 +1,31 @@ +package com.avsystem.commons +package serialization + +/** + * Wrapper over [[ObjectInput]] that lets you peek next field name without advancing the input. + */ +final class PeekingObjectInput(original: ObjectInput) extends ObjectInput { + private[this] var peekedField: FieldInput = _ + + def peekNextFieldName: Opt[String] = peekedField match { + case null if original.hasNext => + peekedField = original.nextField() + peekedField.fieldName.opt + case null => Opt.Empty + case fi => fi.fieldName.opt + } + + def nextField(): FieldInput = + peekedField match { + case null => original.nextField() + case fi => + peekedField = null + fi + } + + def hasNext: Boolean = + peekedField != null || original.hasNext + + override def peekField(name: String): Opt[FieldInput] = + original.peekField(name) +} diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/SimpleValueInputOutput.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/SimpleValueInputOutput.scala index b511db729..0d4551bdf 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/serialization/SimpleValueInputOutput.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/SimpleValueInputOutput.scala @@ -111,7 +111,8 @@ class SimpleValueInput(value: Any) extends Input { case (k, v) => new SimpleValueFieldInput(k, v) } def nextField(): SimpleValueFieldInput = it.next() - override def peekField(name: String): Opt[SimpleValueFieldInput] = map.getOpt(name).map(new SimpleValueFieldInput(name, _)) + override def peekField(name: String): Opt[SimpleValueFieldInput] = + map.getOpt(name).map(new SimpleValueFieldInput(name, _)) def hasNext: Boolean = it.hasNext } diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/json/JsonOptions.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/json/JsonOptions.scala index 977c34490..2697bc570 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/serialization/json/JsonOptions.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/json/JsonOptions.scala @@ -43,7 +43,7 @@ object JsonDateFormat { * @param binaryFormat format used to represent binary data (byte arrays) */ case class JsonOptions( - indentSize: OptArg[Int] = OptArg.Empty, + formatting: JsonFormatting = JsonFormatting.Compact, asciiOutput: Boolean = false, mathContext: MathContext = BigDecimal.defaultMathContext, dateFormat: JsonDateFormat = JsonDateFormat.IsoInstant, @@ -51,4 +51,14 @@ case class JsonOptions( ) object JsonOptions { final val Default = JsonOptions() + final val Pretty = JsonOptions(formatting = JsonFormatting.Pretty) +} + +case class JsonFormatting( + indentSize: OptArg[Int] = OptArg.Empty, + afterColon: Int = 0 +) +object JsonFormatting { + final val Compact = JsonFormatting() + final val Pretty = JsonFormatting(indentSize = 2, afterColon = 1) } diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/json/JsonStringOutput.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/json/JsonStringOutput.scala index c7054bdd6..d58ff5def 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/serialization/json/JsonStringOutput.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/json/JsonStringOutput.scala @@ -9,6 +9,9 @@ object JsonStringOutput { GenCodec.write[T](new JsonStringOutput(sb, options), value) sb.toString } + + def writePretty[T: GenCodec](value: T): String = + write[T](value, JsonOptions.Pretty) } trait BaseJsonOutput { @@ -102,17 +105,19 @@ final class JsonListOutput(builder: JStringBuilder, options: JsonOptions, depth: extends BaseJsonOutput with ListOutput { private[this] var first = true + def writeElement(): JsonStringOutput = { builder.append(if (first) '[' else ',') - indent(builder, options.indentSize, depth) + indent(builder, options.formatting.indentSize, depth) first = false new JsonStringOutput(builder, options, depth) } + def finish(): Unit = { if (first) { builder.append('[') } else { - indent(builder, options.indentSize, depth - 1) + indent(builder, options.formatting.indentSize, depth - 1) } builder.append(']') } @@ -122,19 +127,21 @@ final class JsonObjectOutput(builder: JStringBuilder, options: JsonOptions, dept extends BaseJsonOutput with ObjectOutput { private[this] var first = true + def writeField(key: String): JsonStringOutput = { builder.append(if (first) '{' else ',') - indent(builder, options.indentSize, depth) + indent(builder, options.formatting.indentSize, depth) first = false writeJsonString(builder, key, options.asciiOutput) - builder.append(':') + builder.append(':').append(" " * options.formatting.afterColon) new JsonStringOutput(builder, options, depth) } + def finish(): Unit = { if (first) { builder.append('{') } else { - indent(builder, options.indentSize, depth - 1) + indent(builder, options.formatting.indentSize, depth - 1) } builder.append('}') } diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala index ec26cfec3..7d7e6cfcc 100644 --- a/commons-core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/macroCodecs.scala @@ -28,6 +28,11 @@ abstract class ApplyUnapplyCodec[T]( protected final def writeField[A](output: ObjectOutput, idx: Int, value: A): Unit = writeField(fieldNames(idx), output, value, deps(idx).asInstanceOf[GenCodec[A]]) + protected final def writeField[A](output: ObjectOutput, idx: Int, value: A, transient: A): Unit = + if (value != transient) { + writeField(output, idx, value) + } + protected final def getField[A](fieldValues: FieldValues, idx: Int, default: => A): A = fieldValues.getOrElse[A](idx, default) diff --git a/commons-core/src/main/scala/com/avsystem/commons/serialization/wrappers.scala b/commons-core/src/main/scala/com/avsystem/commons/serialization/wrappers.scala new file mode 100644 index 000000000..8f95fb9fd --- /dev/null +++ b/commons-core/src/main/scala/com/avsystem/commons/serialization/wrappers.scala @@ -0,0 +1,45 @@ +package com.avsystem.commons +package serialization + +import com.avsystem.commons.serialization.GenCodec.{ReadFailure, WriteFailure} + +final class ObjectInputAsInput(objectInput: ObjectInput) extends Input { + private def fail(expected: String): Nothing = + throw new ReadFailure(s"expected $expected, got object") + + def isNull: Boolean = false + def readNull(): Null = fail("null") + def readString(): String = fail("string") + def readBoolean(): Boolean = fail("boolean") + def readInt(): Int = fail("int") + def readLong(): Long = fail("long") + def readDouble(): Double = fail("double") + def readBigInt(): BigInt = fail("bigInt") + def readBigDecimal(): BigDecimal = fail("bigDecimal") + def readBinary(): Array[Byte] = fail("binary") + def readList(): ListInput = fail("list") + def readObject(): ObjectInput = objectInput + def skip(): Unit = objectInput.skipRemaining() +} + +final class ObjectOutputAsOutput(objectOutput: ObjectOutput, forwardFinish: Boolean) extends Output { + private def fail(what: String): Nothing = + throw new WriteFailure(s"could not write $what, can write only object") + + def writeNull(): Unit = fail("null") + def writeString(str: String): Unit = fail("string") + def writeBoolean(boolean: Boolean): Unit = fail("boolean") + def writeInt(int: Int): Unit = fail("int") + def writeLong(long: Long): Unit = fail("long") + def writeDouble(double: Double): Unit = fail("double") + def writeBigInt(bigInt: BigInt): Unit = fail("bigInt") + def writeBigDecimal(bigDecimal: BigDecimal): Unit = fail("bigDecimal") + def writeBinary(binary: Array[Byte]): Unit = fail("binary") + def writeList(): ListOutput = fail("list") + def writeObject(): ObjectOutput = + if (forwardFinish) objectOutput + else new ObjectOutput { + def writeField(key: String): Output = objectOutput.writeField(key) + def finish(): Unit = () + } +} diff --git a/commons-core/src/test/scala/com/avsystem/commons/misc/AdtMetadataTest.scala b/commons-core/src/test/scala/com/avsystem/commons/misc/AdtMetadataTest.scala new file mode 100644 index 000000000..4a571aed1 --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/misc/AdtMetadataTest.scala @@ -0,0 +1,82 @@ +package com.avsystem.commons +package misc + +import com.avsystem.commons.annotation.positioned +import com.avsystem.commons.meta._ +import com.avsystem.commons.serialization.{GenCaseInfo, GenCodec, GenParamInfo, GenUnionInfo, name} + +abstract class HasGenCodecStructure[T]( + implicit gc: MacroGenerated[GenCodec[T]], gs: MacroGenerated[GenStructure[T]]) { + implicit val genCodec: GenCodec[T] = gc.forCompanion(this) + implicit val genStructure: GenStructure[T] = gs.forCompanion(this) +} + +sealed trait GenStructure[T] extends TypedMetadata[T] { + def repr: String +} +object GenStructure extends AdtMetadataCompanion[GenStructure] + +case class GenField[T]( + @infer ts: TypeString[T], + @infer codec: GenCodec[T], + @composite info: GenParamInfo[T] +) extends TypedMetadata[T] { + def rawName: String = info.rawName + def repr: String = s"[$info.flags]${info.annotName.fold("")(n => s"<${n.name}> ")}$ts" +} + +@positioned(positioned.here) case class GenUnion[T]( + @composite info: GenUnionInfo[T], + @multi @adtCaseMetadata cases: Map[String, GenCase[_]] +) extends GenStructure[T] { + def repr: String = cases.iterator.map { + case (name, gr) => s"case $name:${gr.repr}" + }.mkString(s"[${info.flags}]\n", "\n", "") +} + +sealed trait GenCase[T] extends TypedMetadata[T] { + def repr: String + def info: GenCaseInfo[T] +} + +@positioned(positioned.here) case class GenCustomCase[T]( + @composite info: GenCaseInfo[T], + @checked @infer structure: GenStructure.Lazy[T] +) extends GenCase[T] { + def repr: String = structure.value.repr +} + +@positioned(positioned.here) case class GenRecord[T]( + @composite info: GenCaseInfo[T], + @multi @adtParamMetadata fields: Map[String, GenField[_]] +) extends GenCase[T] with GenStructure[T] { + + def repr(indent: Int): String = fields.iterator.map { + case (name, gf) => s"${" " * indent}$name: ${gf.repr}" + }.mkString(s"[${info.flags}]\n", "\n", "") + + def repr: String = repr(0) +} + +@positioned(positioned.here) case class GenSingleton[T]( + @composite info: GenCaseInfo[T], + @checked @infer valueOf: ValueOf[T] +) extends GenCase[T] with GenStructure[T] { + def repr: String = valueOf.value.toString +} + +sealed trait Being +object Being extends HasGenCodecStructure[Being] + +case class Person(name: String, @name("raw_age") age: Int) extends Being +object Person extends HasGenCodecStructure[Person] + +case class Galaxy(name: String, distance: Long) extends Being + +class Peculiarity extends Being +object Peculiarity { + implicit val codec: GenCodec[Peculiarity] = null + implicit val structure: GenStructure[Peculiarity] = null +} + +case object God extends Being diff --git a/commons-core/src/test/scala/com/avsystem/commons/misc/SharedExtensionsTest.scala b/commons-core/src/test/scala/com/avsystem/commons/misc/SharedExtensionsTest.scala index 68efa269c..92ec1e3a3 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/misc/SharedExtensionsTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/misc/SharedExtensionsTest.scala @@ -93,4 +93,17 @@ class SharedExtensionsTest extends FunSuite with Matchers { assert(o.compare(CC(0, 1), CC(0, 0)) > 0) assert(o.compare(CC(0, 0), CC(0, 0)) == 0) } + + test("String.unwrapLines") { + assert("".unwrapLines == "") + assert("\n".unwrapLines == "") + assert("a\n".unwrapLines == "a") + assert("\na".unwrapLines == "a") + assert("\n\n".unwrapLines == "\n") + assert("a\n\nb".unwrapLines == "a\nb") + assert("a\nb".unwrapLines == "a b") + assert("a \nb".unwrapLines == "a b") + assert("a\n b".unwrapLines == "a b") + assert("a \n b".unwrapLines == "a b") + } } diff --git a/commons-core/src/test/scala/com/avsystem/commons/misc/ValueOfTest.scala b/commons-core/src/test/scala/com/avsystem/commons/misc/ValueOfTest.scala new file mode 100644 index 000000000..2ae4eafbe --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/misc/ValueOfTest.scala @@ -0,0 +1,36 @@ +package com.avsystem.commons +package misc + +import org.scalatest.FunSuite + +object Obj { + val x: String = "fuu" + + class Inner { + val x: String = "fuu" + + def valueOfX: x.type = ValueOf[x.type] + def valueOfThis: this.type = ValueOf[this.type] + } +} + +class ValueOfTest extends FunSuite { + test("object") { + assert(ValueOf[Obj.type] == Obj) + } + + test("static val") { + assert(ValueOf[Obj.x.type] == Obj.x) + } + + test("inner val of local") { + val i = new Obj.Inner + assert(ValueOf[i.x.type] == i.x) + assert(i.valueOfX == i.x) + } + + test("this") { + val i = new Obj.Inner + assert(i.valueOfThis == i) + } +} 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 39c272ea9..fa90089e5 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 @@ -2,67 +2,10 @@ 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 def moreFailingGet: 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 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] = - 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 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") - } -} - abstract class AbstractRestCallTest extends FunSuite with ScalaFutures { final val serverHandle: RawRest.HandleRequest = RawRest.asHandleRequest[RestTestApi](RestTestApi.Impl) @@ -106,4 +49,4 @@ abstract class AbstractRestCallTest extends FunSuite with ScalaFutures { class DirectRestCallTest extends AbstractRestCallTest { def clientHandle: HandleRequest = serverHandle -} +} \ No newline at end of file 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 725b400d2..6bf2c2d56 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 @@ -1,13 +1,12 @@ 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] +object User extends RestDataCompanion[User] trait UserApi { @GET def user(userId: String): Future[User] @@ -98,7 +97,7 @@ class RawRestTest extends FunSuite with ScalaFutures { |X-Awesome: true |application/json |{"id":"ID","name":"Fred"} - |<- 200 + |<- 204 |""".stripMargin) } 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..2e855e2b7 --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RestTestApi.scala @@ -0,0 +1,81 @@ +package com.avsystem.commons +package rest + +import com.avsystem.commons.rest.openapi.description +import com.avsystem.commons.serialization.flatten + +sealed trait BaseEntity +object BaseEntity extends RestDataCompanion[BaseEntity] + +@description("Flat sealed entity with some serious cases") +@flatten sealed trait FlatBaseEntity extends BaseEntity +object FlatBaseEntity extends RestDataCompanion[FlatBaseEntity] + +@description("REST entity") +case class RestEntity( + @description("entity id") id: String, + name: String, + @description("recursive optional subentity") subentity: OptArg[RestEntity] = OptArg.Empty +) extends FlatBaseEntity +object RestEntity extends RestDataCompanion[RestEntity] + +case class RestOtherEntity(fuu: Boolean, kek: List[String]) extends FlatBaseEntity + +case object SingletonEntity extends FlatBaseEntity + +trait RestTestApi { + @GET def trivialGet: Future[Unit] + @GET def failingGet: Future[Unit] + @GET def moreFailingGet: Future[Unit] + + @description("A really complex GET operation") + @GET("a/b") def complexGet( + @Path("p1") p1: Int, @description("Very serious path parameter") @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") @description("weird body field") b2: String + ): Future[RestEntity] + + @PUT("") def singleBodyPut( + @Body @description("REST entity description") entity: RestEntity + ): Future[String] + + def prefix( + p0: String, + @Header("X-H0") h0: String, + @Query q0: String + ): RestTestSubApi + + def complexParams(baseEntity: BaseEntity, flatBaseEntity: Opt[FlatBaseEntity] = Opt.Empty): Future[Unit] +} +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] = + 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") + def complexParams(baseEntity: BaseEntity, flatBaseEntity: Opt[FlatBaseEntity]): Future[Unit] = Future.unit + } +} + +trait RestTestSubApi { + @GET def subget(@Path p1: Int, @Header("X-H1") h1: Int, q1: Int): Future[String] +} +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") + } +} \ No newline at end of file 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 index 17b9cbcae..9d0fdb02d 100644 --- a/commons-core/src/test/scala/com/avsystem/commons/rest/RestValidationTest.scala +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/RestValidationTest.scala @@ -51,7 +51,7 @@ class RestValidationTest extends FunSuite { 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") + "Header parameter X-Lol of post_post collides with header parameter of the same name in prefix prefix") } trait PrefixApi2 { @@ -69,6 +69,6 @@ class RestValidationTest extends FunSuite { 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") + "Query parameter lol of post_post collides with query parameter of the same name in prefix prefix") } } diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/WriteOpenApi.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/WriteOpenApi.scala new file mode 100644 index 000000000..6c85d3819 --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/WriteOpenApi.scala @@ -0,0 +1,20 @@ +package com.avsystem.commons +package rest + +import java.io.FileWriter + +import com.avsystem.commons.rest.openapi.{Info, Server} +import com.avsystem.commons.serialization.json.{JsonOptions, JsonStringOutput} + +object WriteOpenApi { + def main(args: Array[String]): Unit = { + val openapi = RestTestApi.openapiMetadata.openapi( + Info("Test API", "0.1", description = "Some test REST API"), + servers = List(Server("http://localhost")) + ) + val fw = new FileWriter("/home/ghik/api.js") + fw.write("apiSpec = ") + fw.write(JsonStringOutput.write(openapi, JsonOptions.Pretty)) + fw.close() + } +} diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/openapi/OpenApiGenerationTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/openapi/OpenApiGenerationTest.scala new file mode 100644 index 000000000..77361738a --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/openapi/OpenApiGenerationTest.scala @@ -0,0 +1,516 @@ +package com.avsystem.commons +package rest.openapi + +import com.avsystem.commons.rest.RestTestApi +import com.avsystem.commons.serialization.json.JsonStringOutput +import org.scalatest.FunSuite + +class OpenApiGenerationTest extends FunSuite { + test("openapi for RestTestApi") { + val openapi = RestTestApi.openapiMetadata.openapi( + Info("Test API", "0.1", description = "Some test REST API"), + servers = List(Server("http://localhost")) + ) + assert(JsonStringOutput.writePretty(openapi) == + """{ + | "openapi": "3.0.1", + | "info": { + | "title": "Test API", + | "version": "0.1", + | "description": "Some test REST API" + | }, + | "paths": { + | "/prefix/{p0}/subget/{p1}": { + | "get": { + | "responses": { + | "200": { + | "content": { + | "application/json": { + | "schema": { + | "type": "string" + | } + | } + | } + | } + | }, + | "operationId": "prefix_get_subget", + | "parameters": [ + | { + | "name": "p0", + | "in": "path", + | "required": true, + | "schema": { + | "type": "string" + | } + | }, + | { + | "name": "X-H0", + | "in": "header", + | "required": true, + | "schema": { + | "type": "string" + | } + | }, + | { + | "name": "q0", + | "in": "query", + | "required": true, + | "schema": { + | "type": "string" + | } + | }, + | { + | "name": "p1", + | "in": "path", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | }, + | { + | "name": "X-H1", + | "in": "header", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | }, + | { + | "name": "q1", + | "in": "query", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | } + | ] + | } + | }, + | "/trivialGet": { + | "get": { + | "responses": { + | "204": {} + | }, + | "operationId": "get_trivialGet" + | } + | }, + | "/complexParams": { + | "post": { + | "responses": { + | "204": {} + | }, + | "operationId": "post_complexParams", + | "requestBody": { + | "content": { + | "application/json": { + | "schema": { + | "type": "object", + | "properties": { + | "baseEntity": { + | "$ref": "#/components/schemas/BaseEntity" + | }, + | "flatBaseEntity": { + | "nullable": true, + | "allOf": [ + | { + | "$ref": "#/components/schemas/FlatBaseEntity" + | } + | ] + | } + | }, + | "required": [ + | "baseEntity" + | ] + | } + | } + | }, + | "required": true + | } + | } + | }, + | "/moreFailingGet": { + | "get": { + | "responses": { + | "204": {} + | }, + | "operationId": "get_moreFailingGet" + | } + | }, + | "/failingGet": { + | "get": { + | "responses": { + | "204": {} + | }, + | "operationId": "get_failingGet" + | } + | }, + | "/multiParamPost/{p1}/p1/{p2}": { + | "post": { + | "responses": { + | "200": { + | "content": { + | "application/json": { + | "schema": { + | "$ref": "#/components/schemas/RestEntity" + | } + | } + | } + | } + | }, + | "operationId": "post_multiParamPost", + | "parameters": [ + | { + | "name": "p1", + | "in": "path", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | }, + | { + | "name": "p2", + | "in": "path", + | "required": true, + | "schema": { + | "type": "string" + | } + | }, + | { + | "name": "X-H1", + | "in": "header", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | }, + | { + | "name": "X-H2", + | "in": "header", + | "required": true, + | "schema": { + | "type": "string" + | } + | }, + | { + | "name": "q1", + | "in": "query", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | }, + | { + | "name": "q=2", + | "in": "query", + | "required": true, + | "schema": { + | "type": "string" + | } + | } + | ], + | "requestBody": { + | "content": { + | "application/json": { + | "schema": { + | "type": "object", + | "properties": { + | "b1": { + | "type": "integer", + | "format": "int32" + | }, + | "b\"2": { + | "type": "string", + | "description": "weird body field" + | } + | }, + | "required": [ + | "b1", + | "b\"2" + | ] + | } + | } + | }, + | "required": true + | } + | } + | }, + | "/a/b/{p1}/p1/{p2}": { + | "get": { + | "responses": { + | "200": { + | "content": { + | "application/json": { + | "schema": { + | "$ref": "#/components/schemas/RestEntity" + | } + | } + | } + | } + | }, + | "description": "A really complex GET operation", + | "operationId": "get_complexGet", + | "parameters": [ + | { + | "name": "p1", + | "in": "path", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | }, + | { + | "name": "p2", + | "in": "path", + | "description": "Very serious path parameter", + | "required": true, + | "schema": { + | "type": "string" + | } + | }, + | { + | "name": "X-H1", + | "in": "header", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | }, + | { + | "name": "X-H2", + | "in": "header", + | "required": true, + | "schema": { + | "type": "string" + | } + | }, + | { + | "name": "q1", + | "in": "query", + | "required": true, + | "schema": { + | "type": "integer", + | "format": "int32" + | } + | }, + | { + | "name": "q=2", + | "in": "query", + | "required": true, + | "schema": { + | "type": "string" + | } + | } + | ] + | } + | }, + | "/": { + | "put": { + | "responses": { + | "200": { + | "content": { + | "application/json": { + | "schema": { + | "type": "string" + | } + | } + | } + | } + | }, + | "operationId": "put_singleBodyPut", + | "requestBody": { + | "content": { + | "application/json": { + | "schema": { + | "description": "REST entity description", + | "allOf": [ + | { + | "$ref": "#/components/schemas/RestEntity" + | } + | ] + | } + | } + | }, + | "required": true + | } + | } + | } + | }, + | "servers": [ + | { + | "url": "http://localhost" + | } + | ], + | "components": { + | "schemas": { + | "RestOtherEntity": { + | "type": "object", + | "properties": { + | "_case": { + | "type": "string", + | "enum": [ + | "RestOtherEntity" + | ] + | }, + | "fuu": { + | "type": "boolean" + | }, + | "kek": { + | "type": "array", + | "items": { + | "type": "string" + | } + | } + | }, + | "required": [ + | "_case", + | "fuu", + | "kek" + | ] + | }, + | "BaseEntity": { + | "oneOf": [ + | { + | "type": "object", + | "properties": { + | "RestEntity": { + | "$ref": "#/components/schemas/RestEntity" + | } + | }, + | "required": [ + | "RestEntity" + | ] + | }, + | { + | "type": "object", + | "properties": { + | "RestOtherEntity": { + | "type": "object", + | "properties": { + | "fuu": { + | "type": "boolean" + | }, + | "kek": { + | "type": "array", + | "items": { + | "type": "string" + | } + | } + | }, + | "required": [ + | "fuu", + | "kek" + | ] + | } + | }, + | "required": [ + | "RestOtherEntity" + | ] + | }, + | { + | "type": "object", + | "properties": { + | "SingletonEntity": { + | "type": "object" + | } + | }, + | "required": [ + | "SingletonEntity" + | ] + | } + | ] + | }, + | "FlatBaseEntity": { + | "description": "Flat sealed entity with some serious cases", + | "oneOf": [ + | { + | "$ref": "#/components/schemas/taggedRestEntity" + | }, + | { + | "$ref": "#/components/schemas/RestOtherEntity" + | }, + | { + | "$ref": "#/components/schemas/SingletonEntity" + | } + | ], + | "discriminator": { + | "propertyName": "_case", + | "mapping": { + | "RestEntity": "taggedRestEntity" + | } + | } + | }, + | "RestEntity": { + | "type": "object", + | "description": "REST entity", + | "properties": { + | "id": { + | "type": "string", + | "description": "entity id" + | }, + | "name": { + | "type": "string" + | }, + | "subentity": { + | "description": "recursive optional subentity", + | "nullable": true, + | "allOf": [ + | { + | "$ref": "#/components/schemas/RestEntity" + | } + | ] + | } + | }, + | "required": [ + | "id", + | "name" + | ] + | }, + | "taggedRestEntity": { + | "allOf": [ + | { + | "type": "object", + | "properties": { + | "_case": { + | "type": "string", + | "enum": [ + | "RestEntity" + | ] + | } + | }, + | "required": [ + | "_case" + | ] + | }, + | { + | "$ref": "#/components/schemas/RestEntity" + | } + | ] + | }, + | "SingletonEntity": { + | "type": "object", + | "properties": { + | "_case": { + | "type": "string", + | "enum": [ + | "SingletonEntity" + | ] + | } + | }, + | "required": [ + | "_case" + | ] + | } + | } + | } + |}""".stripMargin + ) + } +} diff --git a/commons-core/src/test/scala/com/avsystem/commons/rest/openapi/RestSchemaTest.scala b/commons-core/src/test/scala/com/avsystem/commons/rest/openapi/RestSchemaTest.scala new file mode 100644 index 000000000..15115d1db --- /dev/null +++ b/commons-core/src/test/scala/com/avsystem/commons/rest/openapi/RestSchemaTest.scala @@ -0,0 +1,71 @@ +package com.avsystem.commons +package rest.openapi + +import com.avsystem.commons.rest.RestDataCompanion +import com.avsystem.commons.serialization.json.JsonStringOutput +import com.avsystem.commons.serialization.{GenCodec, name, transparent} +import org.scalatest.FunSuite + +class Fuu[T](thing: T) + +class RestSchemaTest extends FunSuite { + private def schemaStr[T](implicit schema: RestSchema[T]): String = + JsonStringOutput.writePretty(new InliningResolver().resolve(schema)) + + trait Dependency + object Dependency { + implicit val codec: GenCodec[Dependency] = null + implicit val schema: RestSchema[Dependency] = RestSchema.ref("Dependency.json") + } + + @description("kejs klass") + case class KejsKlass( + @name("integer") int: Int, + @description("serious dependency") dep: Dependency, + @description("serious string") str: Opt[String] = Opt.Empty + ) + object KejsKlass extends RestDataCompanion[KejsKlass] + + test("case class") { + assert(schemaStr[KejsKlass] == + """{ + | "type": "object", + | "description": "kejs klass", + | "properties": { + | "integer": { + | "type": "integer", + | "format": "int32" + | }, + | "dep": { + | "description": "serious dependency", + | "allOf": [ + | { + | "$ref": "Dependency.json" + | } + | ] + | }, + | "str": { + | "type": "string", + | "description": "serious string", + | "nullable": true + | } + | }, + | "required": [ + | "integer", + | "dep" + | ] + |}""".stripMargin) + } + + @description("wrapped string") + @transparent case class Wrap(str: String) + object Wrap extends RestDataCompanion[Wrap] + + test("transparent wrapper") { + assert(schemaStr[Wrap] == + """{ + | "type": "string", + | "description": "wrapped string" + |}""".stripMargin) + } +} 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 36b060f10..432bc637b 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 @@ -4,6 +4,7 @@ package rpc import java.util.regex.Matcher import com.avsystem.commons.annotation.AnnotationAggregate +import com.avsystem.commons.meta._ import com.avsystem.commons.serialization.GenCodec import scala.annotation.StaticAnnotation @@ -80,19 +81,19 @@ object Utils { import com.avsystem.commons.rpc.Utils._ case class DoSomethings( - doSomething: DoSomethingSignature, - @optional doSomethingElse: Opt[DoSomethingSignature] + @rpcMethodMetadata doSomething: DoSomethingSignature, + @optional @rpcMethodMetadata doSomethingElse: Opt[DoSomethingSignature] ) @methodTag[RestMethod] @paramTag[DummyParamTag] case class NewRpcMetadata[T: TypeName]( @composite doSomethings: DoSomethings, - @multi @verbatim procedures: Map[String, FireMetadata], - @multi functions: Map[String, CallMetadata[_]], - @multi getters: Map[String, GetterMetadata[_]], - @multi @tagged[POST] posters: Map[String, PostMetadata[_]], - @multi prefixers: Map[String, PrefixMetadata[_]] + @multi @verbatim @rpcMethodMetadata procedures: Map[String, FireMetadata], + @multi @rpcMethodMetadata functions: Map[String, CallMetadata[_]], + @multi @rpcMethodMetadata getters: Map[String, GetterMetadata[_]], + @multi @tagged[POST] @rpcMethodMetadata posters: Map[String, PostMetadata[_]], + @multi @rpcMethodMetadata prefixers: Map[String, PrefixMetadata[_]] ) { def repr(open: List[NewRpcMetadata[_]]): String = if (open.contains(this)) "\n" else { @@ -110,7 +111,7 @@ case class NewRpcMetadata[T: TypeName]( } object NewRpcMetadata extends RpcMetadataCompanion[NewRpcMetadata] -case class DoSomethingSignature(arg: ArgMetadata) extends TypedMetadata[String] +case class DoSomethingSignature(@rpcParamMetadata arg: ArgMetadata) extends TypedMetadata[String] case class ArgMetadata() extends TypedMetadata[Double] trait MethodMetadata[T] { @@ -123,8 +124,8 @@ trait MethodMetadata[T] { case class FireMetadata( nameInfo: NameInfo, - @optional @auxiliary ajdi: Opt[ParameterMetadata[Int]], - @multi args: Map[String, ParameterMetadata[_]] + @optional @auxiliary @rpcParamMetadata ajdi: Opt[ParameterMetadata[Int]], + @multi @rpcParamMetadata args: Map[String, ParameterMetadata[_]] ) extends TypedMetadata[Unit] with MethodMetadata[Unit] { def typeName: TypeName[Unit] = TypeName("void") def repr: String = @@ -135,8 +136,8 @@ case class FireMetadata( case class CallMetadata[T]( nameInfo: NameInfo, - @tagged[renamed] @multi renamed: Map[String, ParameterMetadata[_]], - @multi args: Map[String, ParameterMetadata[_]] + @tagged[renamed] @multi @rpcParamMetadata renamed: Map[String, ParameterMetadata[_]], + @multi @rpcParamMetadata args: Map[String, ParameterMetadata[_]] )(implicit val typeName: TypeName[T]) extends TypedMetadata[Future[T]] with MethodMetadata[T] { def repr: String = @@ -146,8 +147,8 @@ case class CallMetadata[T]( } case class GetterParams( - @encoded head: ParameterMetadata[_], - @multi tail: List[ParameterMetadata[_]] + @encoded @rpcParamMetadata head: ParameterMetadata[_], + @multi @rpcParamMetadata tail: List[ParameterMetadata[_]] ) { def repr: String = (head :: tail).map(_.repr).mkString("ARGS:\n", "\n", "") } @@ -166,8 +167,8 @@ case class GetterMetadata[T]( case class PostMetadata[T: TypeName]( nameInfo: NameInfo, @reifyAnnot post: POST, - @tagged[header] @multi @verbatim headers: Vector[ParameterMetadata[String]], - @multi body: MLinkedHashMap[String, ParameterMetadata[_]] + @tagged[header] @multi @verbatim @rpcParamMetadata headers: Vector[ParameterMetadata[String]], + @multi @rpcParamMetadata body: MLinkedHashMap[String, ParameterMetadata[_]] )(implicit val typeName: TypeName[T]) extends TypedMetadata[T] with MethodMetadata[T] { def repr: String = @@ -203,7 +204,7 @@ case class ParameterMetadata[T: TypeName]( case class NameInfo( @reifyName name: String, - @reifyName(rpcName = true) rpcName: String + @reifyName(useRawName = true) rpcName: String ) { def repr: String = name + (if (rpcName != name) s"<$rpcName>" else "") } 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 45c57ae7b..4972e8147 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 @@ -113,17 +113,17 @@ class JsonStringInputOutputTest extends FunSuite with SerializationTestUtils wit intercept[ReadFailure](read[String]("\"\\x0105\"")) } - test("indentation") { - val options = JsonOptions(indentSize = 2) + test("indentation and spacing") { + val options = JsonOptions.Pretty val map = Map("a" -> List(1, 2), "b" -> List(3, 4, 5)) val prettyJson = write[Map[String, List[Int]]](map, options) assert(prettyJson == """{ - | "a":[ + | "a": [ | 1, | 2 | ], - | "b":[ + | "b": [ | 3, | 4, | 5 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 0624afe3e..435d544d3 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,8 +5,8 @@ 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, RestParameters, RestMetadata, RestRequest} -import com.avsystem.commons.rpc.NamedParams +import com.avsystem.commons.meta.Mapping +import com.avsystem.commons.rest.{HeaderValue, HttpBody, HttpMethod, PathValue, QueryValue, RawRest, RestMetadata, RestParameters, RestRequest} import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse} import org.eclipse.jetty.http.{HttpStatus, MimeTypes} @@ -36,13 +36,13 @@ object RestServlet { .map(v => PathValue(URLDecoder.decode(v, "utf-8"))) .to[List] - val headersBuilder = NamedParams.newBuilder[HeaderValue] + val headersBuilder = Mapping.newBuilder[HeaderValue] request.getHeaderNames.asScala.foreach { headerName => headersBuilder += headerName -> HeaderValue(request.getHeader(headerName)) } val headers = headersBuilder.result() - val queryBuilder = NamedParams.newBuilder[QueryValue] + val queryBuilder = Mapping.newBuilder[QueryValue] request.getParameterNames.asScala.foreach { parameterName => queryBuilder += parameterName -> QueryValue(request.getParameter(parameterName)) } 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 377b9b999..c5eb9818c 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 @@ -20,7 +20,7 @@ object ClientMain { val result = proxy.createUser("Fred", 1990) .andThen({ case _ => client.stop() }) .andThen { - case Success(id) => println(s"User $id created") + case Success(user) => println(s"User ${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 d699e4e7f..f136e5442 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,8 @@ import com.avsystem.commons.jetty.rest.RestHandler import org.eclipse.jetty.server.Server class UserApiImpl extends UserApi { - def createUser(name: String, birthYear: Int): Future[String] = Future.successful(s"$name-ID") + def createUser(name: String, birthYear: Int): Future[User] = + Future.successful(User(s"$name-ID", name, birthYear)) } 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 4f928cea2..15f230798 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 @@ -3,8 +3,11 @@ package jetty.rest.examples import com.avsystem.commons.rest._ +case class User(id: String, name: String, birthYear: Int) +object User extends RestDataCompanion[User] + trait UserApi { - /** Returns ID of newly created user */ - def createUser(name: String, birthYear: Int): Future[String] + /** Returns newly created user */ + def createUser(name: String, birthYear: Int): Future[User] } object UserApi extends DefaultRestApiCompanion[UserApi] \ No newline at end of file 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 c16416585..53439a26e 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 @@ -4,7 +4,6 @@ package macros import scala.collection.mutable import scala.collection.mutable.ListBuffer import scala.reflect.macros.{TypecheckException, blackbox} -import scala.reflect.{ClassTag, classTag} import scala.util.control.NoStackTrace abstract class AbstractMacroCommons(val c: blackbox.Context) extends MacroCommons @@ -14,6 +13,8 @@ trait MacroCommons { bundle => import c.universe._ + import scala.reflect.{ClassTag, classTag} + final val ScalaPkg = q"_root_.scala" final val JavaLangPkg = q"_root_.java.lang" final val StringCls = tq"$JavaLangPkg.String" @@ -43,7 +44,18 @@ trait MacroCommons { bundle => final val ImplicitsObj = q"$CommonsPkg.misc.Implicits" final val AnnotationAggregateType = getType(tq"$CommonsPkg.annotation.AnnotationAggregate") final val DefaultsToNameAT = getType(tq"$CommonsPkg.annotation.defaultsToName") + final val NotInheritedFromSealedTypes = getType(tq"$CommonsPkg.annotation.NotInheritedFromSealedTypes") final val SeqCompanionSym = typeOf[scala.collection.Seq.type].termSymbol + final val PositionedAT = getType(tq"$CommonsPkg.annotation.positioned") + + final val NothingTpe: Type = typeOf[Nothing] + final val StringPFTpe: Type = typeOf[PartialFunction[String, Any]] + final val BIterableTpe: Type = typeOf[Iterable[Any]] + final val BIndexedSeqTpe: Type = typeOf[IndexedSeq[Any]] + + final val PartialFunctionClass: Symbol = StringPFTpe.typeSymbol + final val BIterableClass: Symbol = BIterableTpe.typeSymbol + final val BIndexedSeqClass: Symbol = BIndexedSeqTpe.typeSymbol final lazy val isScalaJs = definitions.ScalaPackageClass.toType.member(TermName("scalajs")) != NoSymbol @@ -71,6 +83,14 @@ trait MacroCommons { bundle => error(msg) } + def containsInaccessibleThises(tree: Tree): Boolean = tree.exists { + case t@This(_) if !t.symbol.isPackageClass && !enclosingClasses.contains(t.symbol) => true + case _ => false + } + + def indent(str: String, indent: String): String = + str.replaceAllLiterally("\n", s"\n$indent") + 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) @@ -163,8 +183,11 @@ trait MacroCommons { bundle => if (withSupers) withSuperSymbols(s) else Iterator(s) def allAnnotations(s: Symbol, tpeFilter: Type, withInherited: Boolean = true, fallback: List[Tree] = Nil): List[Annot] = { + def inherited(annot: Annotation, superSym: Symbol): Boolean = + !(s.isClass && superSym != s && annot.tree.tpe <:< NotInheritedFromSealedTypes) + val nonFallback = maybeWithSuperSymbols(s, withInherited) - .flatMap(ss => ss.annotations.map(a => new Annot(a.tree, s, ss, None))) + .flatMap(ss => ss.annotations.filter(inherited(_, ss)).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 @@ -188,50 +211,65 @@ trait MacroCommons { bundle => case Nil => None } + def inherited(annot: Annotation, superSym: Symbol): Boolean = + !(superSym != s && isSealedHierarchyRoot(superSym) && annot.tree.tpe <:< NotInheritedFromSealedTypes) + maybeWithSuperSymbols(s, withInherited) - .map(ss => find(ss.annotations.map(a => new Annot(a.tree, s, ss, None)))) + .map(ss => find(ss.annotations.filter(inherited(_, ss)).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)] + final val companionReplacementName = TermName("$companion$replacement") + final var forceCompanionReplace: Boolean = false - def mkMacroGenerated(tpe: Type, tree: => Tree): Tree = { - def fail() = - abort(s"invocation of this macro is allowed only to be passed as super constructor parameter of an object") + def enclosingConstructorCompanion: Symbol = + ownerChain.filter(_.isConstructor).map(_.owner.asClass.module).find(_ != NoSymbol).getOrElse(NoSymbol) - val ownerConstr = c.internal.enclosingOwner - if (!ownerConstr.isConstructor) { - fail() - } - val companionSym = ownerConstr.owner.asClass.module.asModule - if (companionSym == NoSymbol) { - fail() + lazy val companionReplacement: Symbol = + if (forceCompanionReplace) enclosingConstructorCompanion + else c.typecheck(q"$companionReplacementName", silent = true) match { + case EmptyTree => NoSymbol + case _ => enclosingConstructorCompanion } - val compName = c.freshName(TermName("companion")) - companionReplacement = Some((companionSym, compName)) - q"new $CommonsPkg.misc.MacroGenerated[$tpe](($compName: $ScalaPkg.Any) => $tree)" - } - - // 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 + // Replace references to companion object being constructed with casted reference to + // `$companion$replacement`. All this horrible wiring is to workaround stupid overzealous Scala validation of // self-reference being passed to super constructor parameter (https://github.com/scala/bug/issues/7666) def replaceCompanion(typedTree: Tree): Tree = { - val symToCheck = typedTree match { + def symToCheck: Symbol = typedTree match { case This(_) => typedTree.symbol.asClass.module case t => t.symbol } companionReplacement match { - case None => typedTree - case Some((s, name)) if s == symToCheck => q"$name.asInstanceOf[${typedTree.tpe}]" + case NoSymbol => typedTree + case s if s == symToCheck => q"$companionReplacementName.asInstanceOf[${typedTree.tpe}]" case _ => typedTree match { case Select(prefix, name) => Select(replaceCompanion(prefix), name) + case Apply(fun, args) => Apply(replaceCompanion(fun), args.map(replaceCompanion)) + case TypeApply(fun, args) => TypeApply(replaceCompanion(fun), args) case t => t } } } + def mkMacroGenerated(tpe: Type, tree: => Tree): Tree = { + def fail() = + abort(s"invocation of this macro is allowed only to be passed as super constructor parameter of an object") + + val ownerConstr = c.internal.enclosingOwner + if (!ownerConstr.isConstructor) { + fail() + } + val companionSym = ownerConstr.owner.asClass.module.asModule + if (companionSym == NoSymbol) { + fail() + } + + forceCompanionReplace = true + q"new $CommonsPkg.misc.MacroGenerated[$tpe](($companionReplacementName: $ScalaPkg.Any) => $tree)" + } + // simplified representation of trees of implicits, used to remove duplicated implicits, // i.e. implicits that were found for different types but turned out to be identical private sealed trait ImplicitTrace @@ -278,7 +316,7 @@ trait MacroCommons { bundle => def newCachedImplicit(): TermName = { val name = c.freshName(TermName("cachedImplicit")) inferredImplicitTypes(name) = t.tpe - implicitsToDeclare += q"private lazy val $name = $t" + implicitsToDeclare += q"private lazy val $name = ${replaceCompanion(t)}" name } ImplicitTrace(t).fold(newCachedImplicit()) { tr => @@ -509,6 +547,13 @@ trait MacroCommons { bundle => } } + object MaybeTyped { + def unapply(t: Tree): Some[(Tree, Option[Tree])] = t match { + case Typed(expr, tpt) => Some((expr, Some(tpt))) + case _ => Some(t, None) + } + } + object MaybeTypeApply { def unapply(tree: Tree): Some[(Tree, List[Tree])] = tree match { case TypeApply(fun, args) => Some((fun, args)) @@ -798,6 +843,8 @@ trait MacroCommons { bundle => singleValueFor(internal.thisType(sym.owner)).map(pre => Select(pre, tpe.termSymbol)) case ThisType(sym) => Some(This(sym)) + case SingleType(NoPrefix, sym) => + Some(Ident(sym)) case SingleType(pre, sym) => singleValueFor(pre).map(prefix => Select(prefix, sym)) case ConstantType(value) => @@ -839,17 +886,37 @@ trait MacroCommons { bundle => directSubclasses.flatMap(allCurrentlyKnownSubclasses) + sym } else Set.empty - def knownSubtypes(tpe: Type): Option[List[Type]] = { + private val ownersCache = new mutable.OpenHashMap[Symbol, List[Symbol]] + def ownersOf(sym: Symbol): List[Symbol] = + ownersCache.getOrElseUpdate(sym, Iterator.iterate(sym)(_.owner).takeWhile(_ != NoSymbol).toList.reverse) + + private val positionCache = new mutable.OpenHashMap[Symbol, Int] + def positionPoint(sym: Symbol): Int = + if (c.enclosingPosition.source == sym.pos.source) sym.pos.point + else positionCache.getOrElseUpdate(sym, + sym.annotations.find(_.tree.tpe <:< PositionedAT).map(_.tree).map { + case Apply(_, List(MaybeTyped(Lit(point: Int), _))) => point + case t => abort(s"expected literal int as argument of @positioned annotation on $sym, got $t") + } getOrElse { + abort(s"Could not determine source position of $sym - " + + s"it resides in separate file than macro invocation and has no @positioned annotation") + }) + + def knownSubtypes(tpe: Type, ordered: Boolean = false): Option[List[Type]] = { val dtpe = tpe.dealias val (tpeSym, refined) = dtpe match { case RefinedType(List(single), scope) => (single.typeSymbol, scope.filter(ts => ts.isType && !ts.isAbstract).toList) case _ => (dtpe.typeSymbol, Nil) } + + def sort(subclasses: List[Symbol]): List[Symbol] = + if (ordered || c.enclosingPosition.source == tpeSym.pos.source) + subclasses.sortBy(positionPoint) + else subclasses + Option(tpeSym).filter(isSealedHierarchyRoot).map { sym => - val subclasses = knownNonAbstractSubclasses(sym).toList - val sortedSubclasses = if (tpeSym.pos != NoPosition) subclasses.sortBy(_.pos.point) else subclasses - sortedSubclasses.flatMap { subSym => + sort(knownNonAbstractSubclasses(sym).toList).flatMap { subSym => val undetTpe = typeOfTypeSymbol(subSym.asType) val refinementSignatures = refined.map(rs => undetTpe.member(rs.name).typeSignatureIn(undetTpe)) val undetBaseTpe = undetTpe.baseType(dtpe.typeSymbol) @@ -875,7 +942,7 @@ trait MacroCommons { bundle => val tree = c.typecheck( q""" def $methodName[..$typeDefs](f: ${treeForType(undetTpe)} => $UnitCls): $UnitCls = () - $methodName((_: ${treeForType(detTpe)}) => ()) + $methodName((_: $detTpe) => ()) """, silent = true ) diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/AdtMetadataMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/AdtMetadataMacros.scala new file mode 100644 index 000000000..4a352dac6 --- /dev/null +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/AdtMetadataMacros.scala @@ -0,0 +1,277 @@ +package com.avsystem.commons +package macros.meta + +import com.avsystem.commons.macros.AbstractMacroCommons +import com.avsystem.commons.macros.misc.{Fail, Ok, Res} + +import scala.collection.mutable.ListBuffer +import scala.reflect.macros.blackbox + +class AdtMetadataMacros(ctx: blackbox.Context) extends AbstractMacroCommons(ctx) with MacroMetadatas { + + import c.universe._ + + val AdtParamMetadataAT: Type = getType(tq"$MetaPackage.adtParamMetadata") + val AdtCaseMetadataAT: Type = getType(tq"$MetaPackage.adtCaseMetadata") + + sealed trait AdtSymbol extends MacroSymbol with SelfMatchedSymbol { + def tpe: Type + lazy val symbol: Symbol = tpe.dealias.typeSymbol + + def cases: List[AdtSymbol] + def params: List[AdtParam] + } + + class AdtHierarchy(val tpe: Type, knownSubtypes: List[Type]) extends AdtSymbol { + def shortDescription: String = "ADT hierarchy" + def description: String = s"$shortDescription $tpe" + + val cases: List[AdtSymbol] = knownSubtypes.map { st => + singleValueFor(st).map(sv => new AdtObject(st, sv)) orElse + applyUnapplyFor(st).map(au => new AdtClass(st, au)) getOrElse + new AdtOtherCase(st) + } + + def params: List[AdtParam] = Nil + } + + class AdtClass(val tpe: Type, applyUnapply: ApplyUnapply) extends AdtSymbol { + def shortDescription: String = "ADT class" + def description: String = s"$shortDescription $tpe" + + def cases: List[AdtSymbol] = Nil + + val params: List[AdtParam] = + applyUnapply.params.map { case (sym, _) => new AdtParam(this, sym) } + } + + class AdtObject(val tpe: Type, val singleValue: Tree) extends AdtSymbol { + def shortDescription: String = "ADT object" + def description: String = s"$shortDescription $nameStr" + + def cases: List[AdtSymbol] = Nil + def params: List[AdtParam] = Nil + } + + class AdtOtherCase(val tpe: Type) extends AdtSymbol { + def shortDescription: String = "ADT case type" + def description: String = s"$shortDescription $nameStr" + + def cases: List[AdtSymbol] = Nil + def params: List[AdtParam] = Nil + } + + class AdtParam(owner: AdtClass, val symbol: Symbol) extends MacroParam { + def shortDescription: String = "ADT parameter" + def description: String = s"$shortDescription $nameStr of ${owner.description}" + } + + case class MatchedAdtParam(param: AdtParam, mdParam: AdtParamMetadataParam, indexInRaw: Int) extends MatchedSymbol { + def real: MacroSymbol = param + def annot(tpe: Type): Option[Annot] = findAnnotation(real.symbol, tpe) + def allAnnots(tpe: Type): List[Annot] = allAnnotations(real.symbol, tpe) + def rawName: String = param.nameStr + } + + case class AdtCaseMapping(adtCase: AdtSymbol, param: AdtCaseMetadataParam, tree: Tree) + + class AdtMetadataConstructor(ownerType: Type, atParam: Option[CompositeParam]) + extends MetadataConstructor(ownerType, atParam) { + + def compositeConstructor(param: CompositeParam): MetadataConstructor = + new AdtMetadataConstructor(param.actualType, Some(param)) + + val paramMdParams: List[AdtParamMetadataParam] = collectParams[AdtParamMetadataParam] + val caseMdParams: List[AdtCaseMetadataParam] = collectParams[AdtCaseMetadataParam] + + if (paramMdParams.nonEmpty && caseMdParams.nonEmpty) { + reportProblem(s"having both @adtParamMetadata and @adtCaseMetadata parameters in the same class doesn't make sense") + } + + override def paramByStrategy(paramSym: Symbol, annot: Annot): MetadataParam = + if (annot.tpe <:< AdtParamMetadataAT) new AdtParamMetadataParam(this, paramSym) + else if (annot.tpe <:< AdtCaseMetadataAT) new AdtCaseMetadataParam(this, paramSym) + else if (annot.tpe <:< ReifyFlagsAT) new ReifiedTypeFlagsParam(this, paramSym) + else super.paramByStrategy(paramSym, annot) + + def paramMappings(params: List[AdtParam]): Res[Map[AdtParamMetadataParam, Tree]] = + if (paramMdParams.isEmpty) Ok(Map.empty) + else collectParamMappings(params, paramMdParams, "metadata parameter")( + (param, parser) => param.metadataFor(parser).map(t => (param, t))).map(_.toMap) + + def collectCaseMappings(cases: List[AdtSymbol]): Res[Map[AdtCaseMetadataParam, List[AdtCaseMapping]]] = + if (caseMdParams.isEmpty) Ok(Map.empty) else { + val errors = new ListBuffer[String] + def addFailure(adtCase: AdtSymbol, message: String): Unit = { + errors += s" * ${adtCase.description}: ${indent(message, " ")}" + } + + val mappings = cases.flatMap { adtCase => + Res.firstOk(caseMdParams) { mdParam => + mdParam.tryMaterializeFor(adtCase).map(t => AdtCaseMapping(adtCase, mdParam, t)) + } { errors => + val unmatchedReport = errors.map { case (mdParam, err) => + s" * ${mdParam.shortDescription} ${mdParam.nameStr} did not match: ${indent(err, " ")}" + }.mkString("\n") + s"it has no matching metadata parameters:\n$unmatchedReport" + } match { + case Ok(m) => Some(m) + case Fail(err) => + addFailure(adtCase, err) + None + } + } + + if (errors.isEmpty) Ok(mappings.groupBy(_.param)) + else Fail(s"some ADT cases could not be mapped to metadata parameters:\n${errors.mkString("\n")}") + } + + def tryMaterializeFor(sym: AdtSymbol, + caseMappings: Map[AdtCaseMetadataParam, List[AdtCaseMapping]], + paramMappings: Map[AdtParamMetadataParam, Tree] + ): Res[Tree] = tryMaterialize(sym) { + case acp: AdtCaseMetadataParam => + val mappings = caseMappings.getOrElse(acp, Nil) + acp.arity match { + case ParamArity.Single(_) => mappings match { + case Nil => Fail(s"no ADT case found that would match ${acp.description}") + case List(m) => Ok(m.tree) + case _ => Fail(s"multiple ADT cases match ${acp.description}") + } + case ParamArity.Optional(_) => mappings match { + case Nil => Ok(acp.mkOptional[Tree](None)) + case List(m) => Ok(acp.mkOptional(Some(m.tree))) + case _ => Fail(s"multiple ADT cases match ${acp.description}") + } + case ParamArity.Multi(_, named) => + Ok(acp.mkMulti(mappings.map(m => if (named) q"(${m.adtCase.rawName}, ${m.tree})" else m.tree))) + } + case app: AdtParamMetadataParam => + Ok(paramMappings(app)) + } + + def tryMaterializeFor(adtSymbol: AdtSymbol): Res[Tree] = + for { + _ <- if (paramMdParams.isEmpty) Ok(()) else adtSymbol match { + case _: AdtClass => Ok(()) + case _ => Fail(s"${adtSymbol.nameStr} is not a case class or case class like type") + } + _ <- if (caseMdParams.isEmpty) Ok(()) else adtSymbol match { + case _: AdtHierarchy => Ok(()) + case _ => Fail(s"${adtSymbol.nameStr} is not a sealed hierarchy root") + } + pmappings <- paramMappings(adtSymbol.params) + cmappings <- collectCaseMappings(adtSymbol.cases) + tree <- tryMaterializeFor(adtSymbol, cmappings, pmappings) + } yield tree + } + + class AdtParamMetadataConstructor(ownerType: Type, atParam: Option[CompositeParam]) + extends MetadataConstructor(ownerType, atParam) { + + def compositeConstructor(param: CompositeParam): MetadataConstructor = + new AdtParamMetadataConstructor(param.actualType, Some(param)) + + override def paramByStrategy(paramSym: Symbol, annot: Annot): MetadataParam = annot.tpe match { + case t if t <:< ReifyPositionAT => new ReifiedPositionParam(this, paramSym) + case t if t <:< ReifyFlagsAT => new ReifiedFlagsParam(this, paramSym) + case _ => super.paramByStrategy(paramSym, annot) + } + + def tryMaterializeFor(matchedParam: MatchedAdtParam): Res[Tree] = + tryMaterialize(matchedParam)(p => Fail(s"unexpected metadata parameter $p")) + } + + class AdtCaseMetadataParam(owner: AdtMetadataConstructor, symbol: Symbol) + extends MetadataParam(owner, symbol) with ArityParam with FilteringSymbol { + + def allowMulti: Boolean = true + def allowNamedMulti: Boolean = true + def allowListedMulti: Boolean = true + + def tryMaterializeFor(adtCase: AdtSymbol): Res[Tree] = for { + _ <- matchFilters(adtCase) + mdType <- actualMetadataType(arity.collectedType, adtCase.tpe, "data type", verbatim = false) + tree <- materializeOneOf(mdType)(t => new AdtMetadataConstructor(t, None).tryMaterializeFor(adtCase)) + } yield tree + } + + class AdtParamMetadataParam(owner: AdtMetadataConstructor, symbol: Symbol) + extends MetadataParam(owner, symbol) with ArityParam with FilteringSymbol { + + def allowMulti: Boolean = true + def allowNamedMulti: Boolean = true + def allowListedMulti: Boolean = true + + val auxiliary: Boolean = + findAnnotation(symbol, AuxiliaryAT).nonEmpty + + private def metadataTree(adtParam: AdtParam, indexInRaw: Int): Option[Res[Tree]] = + Some(MatchedAdtParam(adtParam, this, indexInRaw)).filter(m => matchFilters(m).isOk).map { matched => + val result = for { + mdType <- actualMetadataType(arity.collectedType, adtParam.actualType, "parameter type", verbatim = false) + tree <- materializeOneOf(mdType)(t => new AdtParamMetadataConstructor(t, None).tryMaterializeFor(matched)) + } yield tree + result.mapFailure(msg => s"${adtParam.problemStr}: cannot map it to $shortDescription $pathStr: $msg") + } + + def metadataFor(parser: ParamsParser[AdtParam]): Res[Tree] = arity match { + case _: ParamArity.Single => + val unmatchedError = s"$shortDescription $pathStr was not matched by ADT parameter" + parser.extractSingle(!auxiliary, metadataTree(_, 0), unmatchedError) + case _: ParamArity.Optional => + Ok(mkOptional(parser.extractOptional(!auxiliary, metadataTree(_, 0)))) + case ParamArity.Multi(_, true) => + parser.extractMulti(!auxiliary, (adtp, i) => + metadataTree(adtp, i).map(_.map(t => q"(${adtp.nameStr}, $t)"))).map(mkMulti(_)) + case _: ParamArity.Multi => + parser.extractMulti(!auxiliary, metadataTree).map(mkMulti(_)) + } + } + + class ReifiedTypeFlagsParam(owner: MetadataConstructor, symbol: Symbol) extends DirectMetadataParam(owner, symbol) { + if (!(actualType =:= TypeFlagsTpe)) { + reportProblem("its type is not TypeFlags") + } + + def tryMaterializeFor(matchedSymbol: MatchedSymbol): Res[Tree] = Ok { + def flag(cond: Boolean, bit: Int) = if (cond) 1 << bit else 0 + val s = matchedSymbol.real.symbol.asType + val rawFlags = + flag(s.isAbstract, 0) | + flag(s.isFinal, 1) | + flag(s.isClass && s.asClass.isSealed, 2) | + flag(s.isClass && s.asClass.isCaseClass, 3) | + flag(s.isClass && s.asClass.isTrait, 4) | + flag(s.isModuleClass, 5) + q"new $TypeFlagsTpe($rawFlags)" + } + } + + def materialize[Real: WeakTypeTag]: Tree = { + val adtTpe = weakTypeOf[Real].dealias + val metadataTpe = c.macroApplication.tpe.dealias + materializeMetadata(adtTpe, metadataTpe) + } + + def materializeMetadata(adtTpe: Type, metadataTpe: Type): Tree = { + val adtSymbol = + singleValueFor(adtTpe).map(sv => new AdtObject(adtTpe, sv)) orElse + applyUnapplyFor(adtTpe).map(au => new AdtClass(adtTpe, au)) orElse + knownSubtypes(adtTpe, ordered = true).map(st => new AdtHierarchy(adtTpe, st)) getOrElse + new AdtOtherCase(adtTpe) + + def tryMaterialize(metadataTpe: Type): Res[Tree] = + new AdtMetadataConstructor(metadataTpe, None).tryMaterializeFor(adtSymbol) + + guardedMetadata(metadataTpe, adtTpe) { + materializeOneOf(metadataTpe)(tryMaterialize).getOrElse(abort) + } + } + + def materializeMacroGenerated[Real: WeakTypeTag]: Tree = { + val adtTpe = weakTypeOf[Real].dealias + val metadataTpe = c.macroApplication.tpe.dealias.typeArgs.head + mkMacroGenerated(metadataTpe, materializeMetadata(adtTpe, metadataTpe)) + } +} diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/MacroMetadatas.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/MacroMetadatas.scala new file mode 100644 index 000000000..d5f8e9dfa --- /dev/null +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/MacroMetadatas.scala @@ -0,0 +1,298 @@ +package com.avsystem.commons +package macros.meta + +import com.avsystem.commons.macros.misc.{Fail, Ok, Res} + +import scala.annotation.StaticAnnotation +import scala.reflect.{ClassTag, classTag} + +trait MacroMetadatas extends MacroSymbols { + + import c.universe._ + + val ParamPositionObj: Tree = q"$MetaPackage.ParamPosition" + val TypedMetadataType: Type = getType(tq"$MetaPackage.TypedMetadata[_]") + val MetadataParamStrategyType: Type = getType(tq"$MetaPackage.MetadataParamStrategy") + val ReifyAnnotAT: Type = getType(tq"$MetaPackage.reifyAnnot") + val IsAnnotatedAT: Type = getType(tq"$MetaPackage.isAnnotated[_]") + val InferAT: Type = getType(tq"$MetaPackage.infer") + val ReifyNameAT: Type = getType(tq"$MetaPackage.reifyName") + val ReifyPositionAT: Type = getType(tq"$MetaPackage.reifyPosition") + val ReifyFlagsAT: Type = getType(tq"$MetaPackage.reifyFlags") + val CheckedAT: Type = getType(tq"$MetaPackage.checked") + val ParamPositionTpe: Type = getType(tq"$MetaPackage.ParamPosition") + val ParamFlagsTpe: Type = getType(tq"$MetaPackage.ParamFlags") + val TypeFlagsTpe: Type = getType(tq"$MetaPackage.TypeFlags") + + def actualMetadataType(baseMetadataType: Type, realType: Type, realTypeDesc: String, verbatim: Boolean): Res[Type] = { + val (wildcards, underlying) = baseMetadataType match { + case ExistentialType(wc, u) if !verbatim => (wc, u) + case t => (Nil, t) + } + val asTypedMetadata = underlying.baseType(TypedMetadataType.typeSymbol) + if (asTypedMetadata == NoType) { + abort(s"$baseMetadataType is not a subtype of TypedMetadata") + } + val baseMethodResultType = asTypedMetadata.typeArgs.head + val result = if (wildcards.isEmpty) + Some(baseMetadataType).filter(_ => baseMethodResultType =:= realType) + else determineTypeParams(baseMethodResultType, realType, wildcards) + .map(typeArgs => underlying.substituteTypes(wildcards, typeArgs)) + + result.map(Ok(_)).getOrElse(Fail( + s"$realTypeDesc $realType is incompatible with required metadata type $baseMetadataType")) + } + + def materializeOneOf(mdType: Type)(materialize: Type => Res[Tree]): Res[Tree] = + knownSubtypes(mdType, ordered = true) match { + case Some(subtypes) => Res.firstOk(subtypes)(materialize) { errorsByType => + s"none of the case types of $mdType could be materialized:\n" + + errorsByType.iterator.map { + case (st, err) => s" * $st failed because: ${indent(err, " ")}" + }.mkString("\n") + } + case None => materialize(mdType) + } + + abstract class MetadataParam(val owner: MetadataConstructor, val symbol: Symbol) extends MacroParam { + def shortDescription = "metadata parameter" + def description = s"$shortDescription $nameStr of ${owner.description}" + def pathStr: String = owner.atParam.fold(nameStr)(cp => s"${cp.pathStr}.$nameStr") + } + + class CompositeParam(owner: MetadataConstructor, symbol: Symbol) extends MetadataParam(owner, symbol) { + val constructor: MetadataConstructor = owner.compositeConstructor(this) + override def description: String = s"${super.description} at ${owner.description}" + } + + abstract class MetadataConstructor(val ownerType: Type, val atParam: Option[CompositeParam]) + extends MacroMethod { this: MetadataConstructor => + + lazy val symbol: Symbol = primaryConstructor(ownerType, atParam) + + // fallback to annotations on the class itself + def annot(tpe: Type): Option[Annot] = + findAnnotation(symbol, tpe) orElse findAnnotation(ownerType.typeSymbol, tpe) + + def shortDescription = "metadata class" + def description = s"$shortDescription $ownerType" + + def paramByStrategy(paramSym: Symbol, annot: Annot): MetadataParam = annot.tpe match { + case t if t <:< InferAT => new ImplicitParam(this, paramSym) + case t if t <:< ReifyAnnotAT => new ReifiedAnnotParam(this, paramSym) + case t if t <:< ReifyNameAT => + val useRawName = annot.findArg[Boolean](ReifyNameAT.member(TermName("useRawName")), false) + new ReifiedNameParam(this, paramSym, useRawName) + case t if t <:< IsAnnotatedAT => + new IsAnnotatedParam(this, paramSym, t.typeArgs.head) + case t => reportProblem(s"metadata param strategy $t is not allowed here") + } + + def compositeConstructor(param: CompositeParam): MetadataConstructor + + lazy val paramLists: List[List[MetadataParam]] = + symbol.typeSignatureIn(ownerType).paramLists.map(_.map { ps => + if (findAnnotation(ps, CompositeAT).nonEmpty) + new CompositeParam(this, ps) + else findAnnotation(ps, MetadataParamStrategyType).map(paramByStrategy(ps, _)).getOrElse { + if (ps.isImplicit) new ImplicitParam(this, ps) + else reportProblem("no metadata param strategy annotation found") + } + }) + + protected def collectParams[P <: MetadataParam : ClassTag]: List[P] = + paramLists.flatten.flatMap { + case cp: CompositeParam => cp.constructor.collectParams[P] + case p: P => List(p) + case _ => Nil + } + + def constructorCall(argDecls: List[Tree]): Tree = + q""" + ..$argDecls + new $ownerType(...$argLists) + """ + + protected def cast[C <: MetadataConstructor : ClassTag]: C = this match { + case c: C => c + case _ => throw new Exception(s"Metadata constructor $this is not a ${classTag[C].runtimeClass.getSimpleName}") + } + + protected def tryMaterialize(symbol: MatchedSymbol)(paramMaterialize: MetadataParam => Res[Tree]): Res[Tree] = + Res.traverse(paramLists.flatten) { mp => + val res = mp match { + case cp: CompositeParam => cp.constructor.tryMaterialize(symbol)(paramMaterialize) + case dmp: DirectMetadataParam => dmp.tryMaterializeFor(symbol) + case _ => paramMaterialize(mp) + } + res.map(mp.localValueDecl) + }.map(constructorCall) + } + + abstract class DirectMetadataParam(owner: MetadataConstructor, symbol: Symbol) + extends MetadataParam(owner, symbol) { + + def tryMaterializeFor(matchedSymbol: MatchedSymbol): Res[Tree] + } + + class ImplicitParam(owner: MetadataConstructor, symbol: Symbol) + extends DirectMetadataParam(owner, symbol) with ArityParam { + + def allowMulti: Boolean = false + def allowNamedMulti: Boolean = false + def allowListedMulti: Boolean = false + + val checked: Boolean = findAnnotation(symbol, CheckedAT).nonEmpty + + def tryMaterializeFor(matchedSymbol: MatchedSymbol): Res[Tree] = + arity match { + case ParamArity.Single(tpe) => + if (checked) + tryInferCachedImplicit(tpe).map(n => Ok(q"$n")) + .getOrElse(Fail(s"no implicit value $tpe for $description could be found")) + else + Ok(q"${infer(tpe)}") + case ParamArity.Optional(tpe) => + Ok(mkOptional(tryInferCachedImplicit(tpe).map(n => q"$n"))) + case _: ParamArity.Multi => + Fail("@multi arity not allowed on @infer params") + } + + } + + class ReifiedAnnotParam(owner: MetadataConstructor, symbol: Symbol) + extends DirectMetadataParam(owner, symbol) with ArityParam { + + def allowMulti: Boolean = true + def allowNamedMulti: Boolean = false + def allowListedMulti: Boolean = true + + if (!(arity.collectedType <:< typeOf[StaticAnnotation])) { + reportProblem(s"${arity.collectedType} is not a subtype of StaticAnnotation") + } + + def tryMaterializeFor(matchedSymbol: MatchedSymbol): Res[Tree] = Ok { + def validated(annot: Annot): Annot = { + if (containsInaccessibleThises(annot.tree)) { + matchedSymbol.real.reportProblem(s"reified annotation contains this-references inaccessible outside RPC trait") + } + annot + } + + val rpcSym = matchedSymbol.real + arity match { + case ParamArity.Single(annotTpe) => + 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 ParamArity.Optional(annotTpe) => + mkOptional(matchedSymbol.annot(annotTpe).map(a => c.untypecheck(validated(a).tree))) + case ParamArity.Multi(annotTpe, _) => + mkMulti(allAnnotations(rpcSym.symbol, annotTpe).map(a => c.untypecheck(validated(a).tree))) + } + } + } + + class IsAnnotatedParam(owner: MetadataConstructor, symbol: Symbol, annotTpe: Type) + extends DirectMetadataParam(owner, symbol) { + + if (!(actualType =:= typeOf[Boolean])) { + reportProblem("@hasAnnot can only be used on Boolean parameters") + } + + def tryMaterializeFor(matchedSymbol: MatchedSymbol): Res[Tree] = + Ok(q"${matchedSymbol.allAnnots(annotTpe).nonEmpty}") + } + + class ReifiedNameParam(owner: MetadataConstructor, symbol: Symbol, useRawName: Boolean) + extends DirectMetadataParam(owner, symbol) { + + if (!(actualType =:= typeOf[String])) { + reportProblem(s"its type is not String") + } + + def tryMaterializeFor(matchedSymbol: MatchedSymbol): Res[Tree] = + Ok(q"${if (useRawName) matchedSymbol.rawName else matchedSymbol.real.nameStr}") + } + + class ReifiedPositionParam(owner: MetadataConstructor, symbol: Symbol) + extends DirectMetadataParam(owner, symbol) { + + if (!(actualType =:= ParamPositionTpe)) { + reportProblem("its type is not ParamPosition") + } + + def tryMaterializeFor(matchedParam: MatchedSymbol): Res[Tree] = Ok { + val sym = matchedParam.real.symbol + def loop(index: Int, indexInList: Int, indexOfList: Int, paramLists: List[List[Symbol]]): Tree = + paramLists match { + case (`sym` :: _) :: _ => + q"$ParamPositionObj($index, $indexOfList, $indexInList, ${matchedParam.indexInRaw})" + case (_ :: rest) :: tail => + loop(index + 1, indexInList + 1, indexOfList, rest :: tail) + case Nil :: rest => + loop(index, 0, indexOfList + 1, rest) + case Nil => + abort(s"$sym not found in its owner param lists") + } + loop(0, 0, 0, sym.owner.typeSignature.paramLists) + } + } + + class ReifiedFlagsParam(owner: MetadataConstructor, symbol: Symbol) + extends DirectMetadataParam(owner, symbol) { + + if (!(actualType =:= ParamFlagsTpe)) { + reportProblem("its type is not ParamFlags") + } + + def tryMaterializeFor(matchedParam: MatchedSymbol): Res[Tree] = Ok { + val rpcSym = matchedParam.real + def flag(cond: Boolean, bit: Int) = if (cond) 1 << bit else 0 + val s = rpcSym.symbol.asTerm + val rawFlags = + flag(s.isImplicit, 0) | + flag(s.isByNameParam, 1) | + flag(isRepeated(s), 2) | + flag(s.isParamWithDefault, 3) | + flag(s.isSynthetic, 4) + q"new $ParamFlagsTpe($rawFlags)" + } + } + + def guardedMetadata(metadataTpe: Type, realTpe: Type)(materialize: => Tree): Tree = { + // 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")) + + typedCompanionOf(metadataTpe) match { + case Some(comp) => + // short circuit recursive implicit searches for M.Lazy[Real] + val lazyMetadataTpe = getType(tq"$comp.Lazy[$realTpe]") + val lazySelfName = c.freshName(TermName("lazySelf")) + registerImplicit(lazyMetadataTpe, lazySelfName) + val tree = materialize + + q""" + object $depsObj { + var $selfName: $metadataTpe = _ + private val $lazySelfName = $comp.Lazy($selfName) + ..$cachedImplicitDeclarations + $selfName = $tree + } + $depsObj.$selfName + """ + + case None => + val tree = materialize + q""" + object $depsObj { + ..$cachedImplicitDeclarations + val $selfName = $tree + } + $depsObj.$selfName + """ + } + } +} diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/MacroSymbols.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/MacroSymbols.scala new file mode 100644 index 000000000..608f63bfa --- /dev/null +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/meta/MacroSymbols.scala @@ -0,0 +1,359 @@ +package com.avsystem.commons +package macros.meta + +import com.avsystem.commons.macros.MacroCommons +import com.avsystem.commons.macros.misc.{Fail, Ok, Res} + +import scala.collection.mutable.ListBuffer + +trait MacroSymbols extends MacroCommons { + + import c.universe._ + + val RpcPackage = q"$CommonsPkg.rpc" + val MetaPackage = q"$CommonsPkg.meta" + val RpcUtils = q"$RpcPackage.RpcUtils" + val OptionLikeCls = tq"$MetaPackage.OptionLike" + val CanBuildFromCls = tq"$CollectionPkg.generic.CanBuildFrom" + val RpcArityAT: Type = getType(tq"$MetaPackage.SymbolArity") + val SingleArityAT: Type = getType(tq"$MetaPackage.single") + val OptionalArityAT: Type = getType(tq"$MetaPackage.optional") + val MultiArityAT: Type = getType(tq"$MetaPackage.multi") + val CompositeAT: Type = getType(tq"$MetaPackage.composite") + val AuxiliaryAT: Type = getType(tq"$MetaPackage.auxiliary") + val AnnotatedAT: Type = getType(tq"$MetaPackage.annotated[_]") + val TaggedAT: Type = getType(tq"$RpcPackage.tagged[_]") + val WhenUntaggedArg: Symbol = TaggedAT.member(TermName("whenUntagged")) + + def primaryConstructor(ownerType: Type, ownerParam: Option[MacroSymbol]): Symbol = + primaryConstructorOf(ownerType, ownerParam.fold("")(p => s"${p.problemStr}: ")) + + sealed abstract class Arity + object Arity { + trait Single extends Arity + trait Optional extends Arity + trait Multi extends Arity + } + + sealed abstract class ParamArity(val verbatimByDefault: Boolean) extends Arity { + def collectedType: Type + } + object ParamArity { + def fromAnnotation(param: ArityParam, + allowMulti: Boolean, allowListed: Boolean, allowNamed: Boolean): ParamArity = { + + val at = findAnnotation(param.symbol, RpcArityAT).fold(SingleArityAT)(_.tpe) + if (at <:< SingleArityAT) ParamArity.Single(param.actualType) + else if (at <:< OptionalArityAT) { + val optionLikeType = typeOfCachedImplicit(param.optionLike) + val valueMember = optionLikeType.member(TypeName("Value")) + if (valueMember.isAbstract) + param.reportProblem("could not determine actual value of optional parameter type") + else + ParamArity.Optional(valueMember.typeSignatureIn(optionLikeType)) + } + else if (allowMulti && at <:< MultiArityAT) { + if (allowNamed && param.actualType <:< StringPFTpe) + Multi(param.actualType.baseType(PartialFunctionClass).typeArgs(1), named = true) + else if (allowListed && param.actualType <:< BIterableTpe) + Multi(param.actualType.baseType(BIterableClass).typeArgs.head, named = false) + else if (allowNamed && allowListed) + param.reportProblem(s"@multi ${param.shortDescription} must be a PartialFunction of String " + + s"(for by-name mapping) or Iterable (for sequence)") + else if (allowListed) + param.reportProblem(s"@multi ${param.shortDescription} must be an Iterable") + else + param.reportProblem(s"@multi ${param.shortDescription} must be a PartialFunction of String") + } + else param.reportProblem(s"forbidden RPC arity annotation: $at") + } + + case class Single(collectedType: Type) extends ParamArity(true) with Arity.Single + case class Optional(collectedType: Type) extends ParamArity(true) with Arity.Optional + case class Multi(collectedType: Type, named: Boolean) extends ParamArity(false) with Arity.Multi + } + + sealed abstract class MethodArity(val verbatimByDefault: Boolean) extends Arity + object MethodArity { + def fromAnnotation(method: MacroMethod): MethodArity = { + val at = findAnnotation(method.symbol, RpcArityAT).fold(SingleArityAT)(_.tpe) + 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 MethodArity(true) with Arity.Single + case object Optional extends MethodArity(true) with Arity.Optional + case object Multi extends MethodArity(false) with Arity.Multi + } + + abstract class MacroSymbol { + def symbol: Symbol + def pos: Position = symbol.pos + def shortDescription: String + def description: String + def problemStr: String = s"problem with $description" + + def reportProblem(msg: String, detailPos: Position = NoPosition): Nothing = + abortAt(s"$problemStr: $msg", if (detailPos != NoPosition) detailPos else pos) + + def infer(tpt: Tree): TermName = + infer(getType(tpt)) + + def infer(tpe: Type): TermName = + inferCachedImplicit(tpe, s"$problemStr: ", pos) + + val name: TermName = symbol.name.toTermName + val safeName: TermName = c.freshName(symbol.name.toTermName) + val nameStr: String = name.decodedName.toString + val encodedNameStr: String = name.encodedName.toString + + override def equals(other: Any): Boolean = other match { + case rpcSym: MacroSymbol => symbol == rpcSym.symbol + case _ => false + } + override def hashCode: Int = symbol.hashCode + override def toString: String = symbol.toString + } + + abstract class MacroMethod extends MacroSymbol { + def ownerType: Type + + if (!symbol.isMethod) { + abortAt(s"problem with member $nameStr of type $ownerType: it must be a method (def)", pos) + } + + val sig: Type = symbol.typeSignatureIn(ownerType) + def paramLists: List[List[MacroParam]] + val resultType: Type = sig.finalResultType + + def argLists: List[List[Tree]] = paramLists.map(_.map(_.argToPass)) + def paramDecls: List[List[Tree]] = paramLists.map(_.map(_.paramDecl)) + } + + abstract class MacroParam extends MacroSymbol { + val actualType: Type = actualParamType(symbol) + + def localValueDecl(body: Tree): Tree = + if (symbol.asTerm.isByNameParam) + q"def $safeName = $body" + else + q"val $safeName = $body" + + def paramDecl: Tree = { + val implicitFlag = if (symbol.isImplicit) Flag.IMPLICIT else NoFlags + ValDef(Modifiers(Flag.PARAM | implicitFlag), safeName, TypeTree(symbol.typeSignature), EmptyTree) + } + + def argToPass: Tree = + if (isRepeated(symbol)) q"$safeName: _*" else q"$safeName" + } + + trait AritySymbol extends MacroSymbol { + val arity: Arity + + // @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(shortDescr: String, name: String): Res[Unit] = arity match { + case _: Arity.Single@unchecked | _: Arity.Optional@unchecked => + if (name == nameStr) Ok(()) + else Fail(s"it only matches ${shortDescr}s named $nameStr") + case _: Arity.Multi@unchecked => Ok(()) + } + } + + trait FilteringSymbol extends MacroSymbol { + lazy val requiredAnnots: List[Type] = + allAnnotations(symbol, AnnotatedAT).map(_.tpe.dealias.typeArgs.head) + + def matchFilters(realSymbol: MatchedSymbol): Res[Unit] = + Res.traverse(requiredAnnots) { annotTpe => + if (realSymbol.annot(annotTpe).nonEmpty) Ok(()) + else Fail(s"no annotation of type $annotTpe found on ${realSymbol.real.shortDescription}") + }.map(_ => ()) + } + + 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) + } + + trait TagMatchingSymbol extends MacroSymbol with FilteringSymbol { + def baseTagTpe: Type + def fallbackTag: FallbackTag + + def annot(tpe: Type): Option[Annot] = + findAnnotation(symbol, tpe) + + def tagAnnot(tpe: Type): Option[Annot] = + annot(tpe) + + def tagSpec(a: Annot): (Type, FallbackTag) = { + val tagType = a.tpe.dealias.typeArgs.head + val defaultTagArg = a.tpe.member(TermName("defaultTag")) + val fallbackTag = FallbackTag(a.findArg[Tree](defaultTagArg, EmptyTree)) + (tagType, fallbackTag) + } + + lazy val (requiredTag, whenUntaggedTag) = { + val taggedAnnot = annot(TaggedAT) + val requiredTagType = taggedAnnot.fold(baseTagTpe)(_.tpe.typeArgs.head) + if (!(requiredTagType <:< baseTagTpe)) { + val msg = + if (baseTagTpe =:= NothingTpe) + "cannot use @tagged, no tag annotation type specified" + else s"tag annotation type $requiredTagType specified in @tagged annotation " + + s"must be a subtype of specified base tag $baseTagTpe" + reportProblem(msg) + } + val whenUntagged = FallbackTag(taggedAnnot.map(_.findArg[Tree](WhenUntaggedArg, EmptyTree)).getOrElse(EmptyTree)) + (requiredTagType, whenUntagged) + } + + // returns fallback tag tree only IF it was necessary + def matchTag(realSymbol: MacroSymbol): Res[FallbackTag] = { + val tagAnnot = findAnnotation(realSymbol.symbol, baseTagTpe) + val fallbackTagUsed = if (tagAnnot.isEmpty) whenUntaggedTag orElse fallbackTag else FallbackTag.Empty + val realTagTpe = tagAnnot.map(_.tpe).getOrElse(NoType) orElse fallbackTagUsed.annotTree.tpe orElse baseTagTpe + + if (realTagTpe <:< requiredTag) Ok(fallbackTagUsed) + else Fail(s"it does not accept ${realSymbol.shortDescription}s tagged with $realTagTpe") + } + } + + trait ArityParam extends MacroParam with AritySymbol { + def allowMulti: Boolean + def allowNamedMulti: Boolean + def allowListedMulti: Boolean + + val arity: ParamArity = + ParamArity.fromAnnotation(this, allowMulti, allowListedMulti, allowNamedMulti) + + lazy val optionLike: TermName = infer(tq"$OptionLikeCls[$actualType]") + + lazy val canBuildFrom: TermName = arity match { + case _: ParamArity.Multi if allowNamedMulti && actualType <:< StringPFTpe => + infer(tq"$CanBuildFromCls[$NothingCls,($StringCls,${arity.collectedType}),$actualType]") + case _: ParamArity.Multi => + infer(tq"$CanBuildFromCls[$NothingCls,${arity.collectedType},$actualType]") + case _ => abort(s"(bug) CanBuildFrom computed for non-multi $shortDescription") + } + + 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 = + 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 MatchedSymbol { + def real: MacroSymbol + def annot(tpe: Type): Option[Annot] + def allAnnots(tpe: Type): List[Annot] + def rawName: String + def indexInRaw: Int + } + + trait SelfMatchedSymbol extends MacroSymbol with MatchedSymbol { + def real: MacroSymbol = this + def annot(tpe: Type): Option[Annot] = findAnnotation(symbol, tpe) + def allAnnots(tpe: Type): List[Annot] = allAnnotations(symbol, tpe) + def indexInRaw: Int = 0 + def rawName: String = nameStr + } + + def collectParamMappings[Real <: MacroParam, Raw <: MacroParam, M]( + reals: List[Real], raws: List[Raw], rawShortDesc: String)( + createMapping: (Raw, ParamsParser[Real]) => Res[M]): Res[List[M]] = { + + val parser = new ParamsParser(reals) + Res.traverse(raws)(createMapping(_, parser)).flatMap { result => + 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") + } + } + } + + class ParamsParser[Real <: MacroParam](reals: Seq[Real]) { + + import scala.collection.JavaConverters._ + + private val realParams = new java.util.LinkedList[Real] + realParams.addAll(reals.asJava) + + def remaining: Seq[Real] = realParams.asScala + + def extractSingle[M](consume: Boolean, matcher: Real => Option[Res[M]], unmatchedError: String): Res[M] = { + val it = realParams.listIterator() + def loop(): Res[M] = + if (it.hasNext) { + val real = it.next() + matcher(real) match { + case Some(res) => + if (consume) { + it.remove() + } + res + case None => + loop() + } + } else Fail(unmatchedError) + loop() + } + + def extractOptional[M](consume: Boolean, matcher: Real => Option[Res[M]]): Option[M] = { + val it = realParams.listIterator() + def loop(): Option[M] = + if (it.hasNext) { + val real = it.next() + matcher(real) match { + case Some(Ok(res)) => + if (consume) { + it.remove() + } + Some(res) + case Some(Fail(_)) => None + case None => loop() + } + } else None + loop() + } + + def extractMulti[M](consume: Boolean, matcher: (Real, Int) => Option[Res[M]]): Res[List[M]] = { + val it = realParams.listIterator() + def loop(result: ListBuffer[M]): Res[List[M]] = + if (it.hasNext) { + val real = it.next() + matcher(real, result.size) match { + case Some(res) => + if (consume) { + it.remove() + } + res match { + case Ok(value) => + result += value + loop(result) + case fail: Fail => + fail + } + case None => loop(result) + } + } else Ok(result.result()) + loop(new ListBuffer[M]) + } + } +} diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/misc/MiscMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/misc/MiscMacros.scala index 0973b80a5..8504852ba 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/misc/MiscMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/misc/MiscMacros.scala @@ -331,4 +331,18 @@ class MiscMacros(ctx: blackbox.Context) extends AbstractMacroCommons(ctx) { val selection = if (singletonPrefix) "." else "#" mkTypeString(pre, parens = !singletonPrefix) :+ lit(mkNameString(name, prefix = selection)) } + + def lazyMetadata(metadata: Tree): Tree = + q"${c.prefix}($metadata)" + + def mkValueOf[T: WeakTypeTag]: Tree = { + val tpe = weakTypeOf[T].dealias + singleValueFor(tpe) match { + case Some(sv) => q"new $CommonsPkg.misc.ValueOf[$tpe]($sv)" + case None => abort(s"$tpe is not a singleton type") + } + } + + def posPoint: Tree = + q"${c.enclosingPosition.point}" } 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/misc/Res.scala similarity index 75% rename from commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/Res.scala rename to commons-macros/src/main/scala/com/avsystem/commons/macros/misc/Res.scala index 74505d3f7..0eb661e3e 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/rpc/Res.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/misc/Res.scala @@ -1,8 +1,9 @@ package com.avsystem.commons -package macros.rpc +package macros.misc import scala.collection.generic.CanBuildFrom import scala.collection.mutable +import scala.collection.mutable.ListBuffer sealed trait Res[+A] { def isOk: Boolean = this match { @@ -54,4 +55,19 @@ object Res { } else Ok(builder.result()) loop(cbf(in)) } + + def firstOk[A, B](coll: Iterable[A])(f: A => Res[B])(combineErrors: List[(A, String)] => String): Res[B] = { + val errors = new ListBuffer[(A, String)] + def loop(it: Iterator[A]): Res[B] = + if (it.hasNext) { + val el = it.next() + f(el) match { + case Ok(value) => Ok(value) + case Fail(error) => + errors += ((el, error)) + loop(it) + } + } else Fail(combineErrors(errors.result())) + loop(coll.iterator) + } } 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 5c8b08649..73b78df33 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 @@ -2,24 +2,20 @@ package com.avsystem.commons package macros.rpc import com.avsystem.commons.macros.AbstractMacroCommons +import com.avsystem.commons.macros.meta.MacroSymbols import scala.reflect.macros.blackbox -abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommons(ctx) { +abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommons(ctx) with MacroSymbols { 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" val AsRawObj = q"$RpcPackage.AsRaw" val AsRawRealCls = tq"$RpcPackage.AsRawReal" val AsRawRealObj = q"$RpcPackage.AsRawReal" - val OptionLikeCls = tq"$RpcPackage.OptionLike" - val CanBuildFromCls = tq"$CollectionPkg.generic.CanBuildFrom" - val ParamPositionObj = q"$RpcPackage.ParamPosition" val AsRealTpe: Type = getType(tq"$AsRealCls[_,_]") val AsRawTpe: Type = getType(tq"$AsRawCls[_,_]") @@ -30,42 +26,15 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo 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") - val SingleArityAT: Type = getType(tq"$RpcPackage.single") - val OptionalArityAT: Type = getType(tq"$RpcPackage.optional") - val MultiArityAT: Type = getType(tq"$RpcPackage.multi") + val RpcMethodMetadataAT: Type = getType(tq"$RpcPackage.rpcMethodMetadata") + val RpcParamMetadataAT: Type = getType(tq"$RpcPackage.rpcParamMetadata") 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[_]") - val WhenUntaggedArg: Symbol = TaggedAT.member(TermName("whenUntagged")) val RpcTagAT: Type = getType(tq"$RpcPackage.RpcTag") - val AnnotatedAT: Type = getType(tq"$RpcPackage.annotated[_]") val RpcImplicitsSym: Symbol = getType(tq"$RpcPackage.RpcImplicitsProvider").member(TermName("implicits")) - val TypedMetadataType: Type = getType(tq"$RpcPackage.TypedMetadata[_]") - val MetadataParamStrategyType: Type = getType(tq"$RpcPackage.MetadataParamStrategy") - val ReifyAnnotAT: Type = getType(tq"$RpcPackage.reifyAnnot") - 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") - val ReifyFlagsAT: Type = getType(tq"$RpcPackage.reifyFlags") - val CheckedAT: Type = getType(tq"$RpcPackage.checked") - val ParamPositionTpe: Type = getType(tq"$RpcPackage.ParamPosition") - val ParamFlagsTpe: Type = getType(tq"$RpcPackage.ParamFlags") - - val NothingTpe: Type = typeOf[Nothing] - val StringPFTpe: Type = typeOf[PartialFunction[String, Any]] - val BIterableTpe: Type = typeOf[Iterable[Any]] - val BIndexedSeqTpe: Type = typeOf[IndexedSeq[Any]] - - val PartialFunctionClass: Symbol = StringPFTpe.typeSymbol - val BIterableClass: Symbol = BIterableTpe.typeSymbol - val BIndexedSeqClass: Symbol = BIndexedSeqTpe.typeSymbol def registerCompanionImplicits(rawTpe: Type): Unit = typedCompanionOf(rawTpe).filter { companion => @@ -74,11 +43,6 @@ abstract class RpcMacroCommons(ctx: blackbox.Context) extends AbstractMacroCommo }.foreach { companion => registerImplicitImport(q"import $companion.implicits._") } - - def containsInaccessibleThises(tree: Tree): Boolean = tree.exists { - case t@This(_) if !t.symbol.isPackageClass && !enclosingClasses.contains(t.symbol) => true - case _ => false - } } class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) @@ -141,40 +105,9 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) def rpcMetadata[Real: WeakTypeTag]: Tree = { val realRpc = RealRpcTrait(weakTypeOf[Real].dealias) 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 - val depsObj = c.freshName(TermName("deps")) - val selfName = c.freshName(TermName("self")) - - typedCompanionOf(metadataTpe) match { - case Some(comp) => - // short circuit recursive implicit searches for M.Lazy[Real] - val lazyMetadataTpe = getType(tq"$comp.Lazy[${realRpc.tpe}]") - val lazySelfName = c.freshName(TermName("lazySelf")) - registerImplicit(lazyMetadataTpe, lazySelfName) - val tree = constructor.materializeFor(realRpc, constructor.methodMappings(realRpc)) - - q""" - object $depsObj { - var $selfName: $metadataTpe = _ - private val $lazySelfName = $comp.Lazy($selfName) - ..$cachedImplicitDeclarations - $selfName = $tree - } - $depsObj.$selfName - """ - - case None => - val tree = constructor.materializeFor(realRpc, constructor.methodMappings(realRpc)) - q""" - object $depsObj { - ..$cachedImplicitDeclarations - val $selfName = $tree - } - $depsObj.$selfName - """ - } + val constructor = new RpcTraitMetadataConstructor(metadataTpe, None) + guardedMetadata(metadataTpe, realRpc.tpe)( + constructor.tryMaterializeFor(realRpc, constructor.methodMappings(realRpc)).getOrElse(abort)) } def macroInstances: Tree = { @@ -184,6 +117,8 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) val implicitsTpe = applySig.paramLists.head.head.typeSignature val instancesTpe = applySig.finalResultType + forceCompanionReplace = true + 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 ($realTpe)") } @@ -201,7 +136,7 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) val sig = m.typeSignatureIn(instancesTpe) val resultTpe = sig.finalResultType.dealias if (sig.typeParams.nonEmpty || sig.paramLists.nonEmpty) { - abort(s"Problem with $m: expected non-generic, parameterless method") + abort(s"Problem with $m of $instancesTpe: expected non-generic, parameterless method") } val body = @@ -225,14 +160,11 @@ class RpcMacros(ctx: blackbox.Context) extends RpcMacroCommons(ctx) q""" new $resultTpe { - def apply($implicitsName: $implicitsTpe): $instancesTpe = { + def apply($implicitsName: $implicitsTpe, $companionReplacementName: Any): $instancesTpe = { import $implicitsName._ new $instancesTpe { ..$impls; () } } } """ } - - def lazyMetadata(metadata: Tree): Tree = - q"${c.prefix}($metadata)" } 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 e29a7eb2c..48789b33a 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 @@ -1,6 +1,8 @@ package com.avsystem.commons package macros.rpc +import com.avsystem.commons.macros.misc.{Fail, Ok, Res} + import scala.collection.mutable import scala.collection.mutable.ListBuffer @@ -8,9 +10,9 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => import c.universe._ - def collectMethodMappings[R <: RawRpcSymbol with AritySymbol, M]( - rawSymbols: List[R], rawShortDesc: String, realMethods: List[RealMethod])( - createMapping: (R, MatchedMethod) => Res[M]): List[M] = { + def collectMethodMappings[Raw <: TagMatchingSymbol with AritySymbol, M]( + rawSymbols: List[Raw], rawShortDesc: String, realMethods: List[RealMethod])( + createMapping: (Raw, MatchedMethod) => Res[M]): List[M] = { val failedReals = new ListBuffer[String] def addFailure(realMethod: RealMethod, message: String): Unit = { @@ -19,25 +21,23 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } val result = realMethods.flatMap { realMethod => - def findMethodMapping(errorsAcc: List[Fail], raws: List[R]): Option[M] = raws match { - case rawSymbol :: rawSymbolsRest => - val mappingRes = for { - fallbackTag <- rawSymbol.matchTag(realMethod) - matchedMethod = MatchedMethod(realMethod, fallbackTag) - _ <- rawSymbol.matchName(matchedMethod) - _ <- rawSymbol.matchFilters(matchedMethod) - methodMapping <- createMapping(rawSymbol, matchedMethod) - } yield methodMapping - mappingRes.mapFailure(msg => s"${rawSymbol.shortDescription} ${rawSymbol.nameStr} did not match: $msg") match { - case Ok(m) => Some(m) - case f: Fail => findMethodMapping(f :: errorsAcc, rawSymbolsRest) - } - case Nil => - val unmatchedReport = errorsAcc.reverseIterator.map(f => s" * ${f.message}").mkString("\n") - addFailure(realMethod, s"it has no matching $rawShortDesc:\n$unmatchedReport") + Res.firstOk(rawSymbols)(rawSymbol => for { + fallbackTag <- rawSymbol.matchTag(realMethod) + matchedMethod = MatchedMethod(realMethod, fallbackTag) + _ <- rawSymbol.matchName(matchedMethod.real.shortDescription, matchedMethod.rpcName) + _ <- rawSymbol.matchFilters(matchedMethod) + methodMapping <- createMapping(rawSymbol, matchedMethod) + } yield methodMapping) { errors => + val unmatchedReport = errors.map { case (raw, err) => + s" * ${raw.shortDescription} ${raw.nameStr} did not match: ${indent(err, " ")}" + }.mkString("\n") + s"it has no matching $rawShortDesc:\n$unmatchedReport" + } match { + case Ok(v) => Some(v) + case Fail(msg) => + addFailure(realMethod, msg) None } - findMethodMapping(Nil, rawSymbols) } if (failedReals.nonEmpty) { @@ -47,87 +47,6 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => result } - def collectParamMappings[R <: RealParamTarget, M](raws: List[R], rawShortDesc: String, matchedMethod: MatchedMethod) - (createMapping: (R, ParamsParser) => Res[M]): Res[List[M]] = { - - val parser = new ParamsParser(matchedMethod) - Res.traverse(raws)(createMapping(_, parser)).flatMap { result => - 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") - } - } - } - - class ParamsParser(matchedMethod: MatchedMethod) { - - import scala.collection.JavaConverters._ - - private val realParams = new java.util.LinkedList[RealParam] - realParams.addAll(matchedMethod.real.realParams.asJava) - - def remaining: Seq[RealParam] = realParams.asScala - - def extractSingle[B](raw: RealParamTarget, matcher: MatchedParam => Res[B]): Res[B] = { - val it = realParams.listIterator() - def loop(): Res[B] = - if (it.hasNext) { - val real = it.next() - raw.matchRealParam(matchedMethod, real) match { - case Ok(matchedParam) => - if (!raw.auxiliary) { - it.remove() - } - matcher(matchedParam) - case Fail(_) => loop() - } - } else Fail(s"${raw.shortDescription} ${raw.pathStr} was not matched by real parameter") - loop() - } - - 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.matchRealParam(matchedMethod, real) match { - case Ok(matchedParam) => - val res = matcher(matchedParam).toOption - if (!raw.auxiliary) { - res.foreach(_ => it.remove()) - } - res - case Fail(_) => loop() - } - } else None - loop() - } - - 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) { - val real = it.next() - raw.matchRealParam(matchedMethod, real) match { - case Ok(matchedParam) => - if (!raw.auxiliary) { - it.remove() - } - matcher(matchedParam, 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(matchedParam: MatchedParam, encoding: RpcEncoding) { def realParam: RealParam = matchedParam.real def rpcName: String = matchedParam.rpcName @@ -343,21 +262,23 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => } registerCompanionImplicits(raw.tpe) - private def extractMapping(rawParam: RawValueParam, parser: ParamsParser): Res[ParamMapping] = { - def createErp(matchedParam: MatchedParam, index: Int): Res[EncodedRealParam] = - EncodedRealParam.create(rawParam, matchedParam) + private def extractMapping(method: MatchedMethod, rawParam: RawValueParam, parser: ParamsParser[RealParam]): Res[ParamMapping] = { + def createErp(real: RealParam, indexInRaw: Int): Option[Res[EncodedRealParam]] = + rawParam.matchRealParam(method, real, indexInRaw).toOption.map(EncodedRealParam.create(rawParam, _)) + val consume = !rawParam.auxiliary rawParam.arity match { - case _: RpcParamArity.Single => - parser.extractSingle(rawParam, createErp(_, 0)).map(ParamMapping.Single(rawParam, _)) - case _: RpcParamArity.Optional => - 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 => - parser.extractMulti(rawParam, createErp, named = false).map(ParamMapping.IndexedMulti(rawParam, _)) - case _: RpcParamArity.Multi => - parser.extractMulti(rawParam, createErp, named = false).map(ParamMapping.IterableMulti(rawParam, _)) + case _: ParamArity.Single => + val unmatchedError = s"${raw.shortDescription} ${rawParam.pathStr} was not matched by real parameter" + parser.extractSingle(consume, createErp(_, 0), unmatchedError).map(ParamMapping.Single(rawParam, _)) + case _: ParamArity.Optional => + Ok(ParamMapping.Optional(rawParam, parser.extractOptional(consume, createErp(_, 0)))) + case ParamArity.Multi(_, true) => + parser.extractMulti(consume, createErp).map(ParamMapping.NamedMulti(rawParam, _)) + case _: ParamArity.Multi if rawParam.actualType <:< BIndexedSeqTpe => + parser.extractMulti(consume, createErp).map(ParamMapping.IndexedMulti(rawParam, _)) + case _: ParamArity.Multi => + parser.extractMulti(consume, createErp).map(ParamMapping.IterableMulti(rawParam, _)) } } @@ -375,14 +296,14 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => 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"$realResultType and raw result type ${rawMethod.resultType}") + else Fail(s"no encoding/decoding found between real result type " + + s"$realResultType and raw result type ${rawMethod.resultType}") } for { resultConv <- resultEncoding - paramMappings <- collectParamMappings(rawMethod.allValueParams, "raw parameter", matchedMethod)(extractMapping) + paramMappings <- collectParamMappings( + matchedMethod.real.realParams, rawMethod.allValueParams, "raw parameter")(extractMapping(matchedMethod, _, _)) } yield MethodMapping(matchedMethod, rawMethod, paramMappings, resultConv) } @@ -393,8 +314,8 @@ trait RpcMappings { this: RpcMacroCommons with RpcSymbols => 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 - " + + 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 _ => } 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 5c1d2a466..3c9416e5f 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 @@ -1,70 +1,19 @@ package com.avsystem.commons package macros.rpc -import scala.annotation.StaticAnnotation +import com.avsystem.commons.macros.meta.MacroMetadatas +import com.avsystem.commons.macros.misc.{Fail, Ok, Res} -trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => +trait RpcMetadatas extends MacroMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => import c.universe._ - def actualMetadataType(baseMetadataType: Type, realRpcSymbol: RealRpcSymbol, verbatim: Boolean): Res[Type] = { - val (wildcards, underlying) = baseMetadataType match { - case ExistentialType(wc, u) if !verbatim => (wc, u) - case t => (Nil, t) - } - val baseMethodResultType = underlying.baseType(TypedMetadataType.typeSymbol).typeArgs.head - val (realType, realTypeDesc) = realRpcSymbol match { - case rpc: RealRpcTrait => (rpc.tpe, "RPC type") - case method: RealMethod => (method.resultType, "method result type") - case param: RealParam => (param.actualType, "parameter type") - } - - val result = if (wildcards.isEmpty) - Some(baseMetadataType).filter(_ => baseMethodResultType =:= realType) - else determineTypeParams(baseMethodResultType, realType, wildcards) - .map(typeArgs => underlying.substituteTypes(wildcards, typeArgs)) - - result.map(Ok(_)).getOrElse(Fail( - s"$realTypeDesc $realType is incompatible with required metadata type $baseMetadataType")) - } - - sealed abstract class MetadataParam[Real <: RealRpcSymbol]( - val owner: MetadataConstructor[Real], val symbol: Symbol) extends RpcParam { - - def shortDescription = "metadata parameter" - 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)) - - def pathStr: String = owner.atParam.fold(_ => nameStr, cp => s"${cp.pathStr}.$nameStr") - } - - 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 { + class MethodMetadataParam(owner: RpcTraitMetadataConstructor, symbol: Symbol) + extends MetadataParam(owner, symbol) with TagMatchingSymbol with ArityParam { def allowMulti: Boolean = true def allowNamedMulti: Boolean = true - def allowListedMulti: Boolean = false + def allowListedMulti: Boolean = true def baseTagTpe: Type = owner.baseMethodTag def fallbackTag: FallbackTag = owner.fallbackMethodTag @@ -81,99 +30,62 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => .map(tagSpec).getOrElse(owner.baseParamTag, owner.fallbackParamTag) def mappingFor(matchedMethod: MatchedMethod): Res[MethodMetadataMapping] = for { - mdType <- actualMetadataType(arity.collectedType, matchedMethod.real, verbatimResult) - constructor = new MethodMetadataConstructor(mdType, Left(this)) - paramMappings <- constructor.paramMappings(matchedMethod) - tree <- constructor.tryMaterializeFor(matchedMethod, paramMappings) + mdType <- actualMetadataType(arity.collectedType, matchedMethod.real.resultType, "method result type", verbatimResult) + tree <- materializeOneOf(mdType) { t => + val constructor = new MethodMetadataConstructor(t, this, None) + for { + paramMappings <- constructor.paramMappings(matchedMethod) + tree <- constructor.tryMaterializeFor(matchedMethod, paramMappings) + } yield tree + } } yield MethodMetadataMapping(matchedMethod, this, tree) } class ParamMetadataParam(owner: MethodMetadataConstructor, symbol: Symbol) - extends MetadataParam[RealMethod](owner, symbol) with RealParamTarget { - - def pathStr: String = owner.atParam.fold(_ => nameStr, cp => s"${cp.pathStr}.$nameStr") + extends MetadataParam(owner, symbol) with RealParamTarget { def baseTagTpe: Type = owner.containingMethodParam.baseParamTag def fallbackTag: FallbackTag = owner.containingMethodParam.fallbackParamTag - def cannotMapClue = s"cannot map it to $shortDescription $nameStr of ${owner.ownerType}" + def cannotMapClue: String = s"cannot map it to $shortDescription $nameStr of ${owner.ownerType}" if (!(arity.collectedType <:< TypedMetadataType)) { reportProblem(s"type ${arity.collectedType} is not a subtype of TypedMetadata[_]") } + private def metadataTree(matchedMethod: MatchedMethod, realParam: RealParam, indexInRaw: Int): Option[Res[Tree]] = + matchRealParam(matchedMethod, realParam, indexInRaw).toOption.map(metadataTree(_, indexInRaw)) + 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(matchedParam) + mdType <- actualMetadataType(arity.collectedType, realParam.actualType, "parameter type", verbatim) + tree <- materializeOneOf(mdType)(t => + new ParamMetadataConstructor(t, None, 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)) - case _: RpcParamArity.Optional => - Ok(mkOptional(parser.extractOptional(this, metadataTree(_, 0)))) - case RpcParamArity.Multi(_, true) => - 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(_)) + def metadataFor(matchedMethod: MatchedMethod, parser: ParamsParser[RealParam]): Res[Tree] = arity match { + case _: ParamArity.Single => + val unmatchedError = s"$shortDescription $pathStr was not matched by real parameter" + parser.extractSingle(!auxiliary, metadataTree(matchedMethod, _, 0), unmatchedError) + case _: ParamArity.Optional => + Ok(mkOptional(parser.extractOptional(!auxiliary, metadataTree(matchedMethod, _, 0)))) + case ParamArity.Multi(_, true) => + parser.extractMulti(!auxiliary, (rp, i) => matchRealParam(matchedMethod, rp, i) + .toOption.map(mp => metadataTree(mp, i).map(t => q"(${mp.rpcName}, $t)"))).map(mkMulti(_)) + case _: ParamArity.Multi => + parser.extractMulti(!auxiliary, metadataTree(matchedMethod, _, _)).map(mkMulti(_)) } } - private def primaryConstructor(ownerType: Type, ownerParam: Option[RpcSymbol]): Symbol = - primaryConstructorOf(ownerType, ownerParam.fold("")(p => s"${p.problemStr}: ")) - - sealed abstract class MetadataConstructor[Real <: RealRpcSymbol](val symbol: Symbol) extends RpcMethod { - def ownerType: Type - - 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) - else None - } - - def shortDescription = "metadata class" - def description = s"$shortDescription $ownerType" - - def createDirectParam(paramSym: Symbol, annot: Annot): DirectMetadataParam[Real] = annot.tpe match { - 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")), false) - new ReifiedNameParam(this, paramSym, useRpcName) - case t if t <:< IsAnnotatedAT => - new IsAnnotatedParam(this, paramSym, t.typeArgs.head) - 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 => - 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)) - }) - - def constructorCall(argDecls: List[Tree]): Tree = - q""" - ..$argDecls - new $ownerType(...$argLists) - """ + case class MethodMetadataMapping(matchedMethod: MatchedMethod, mdParam: MethodMetadataParam, tree: Tree) { + def collectedTree(named: Boolean): Tree = if (named) q"(${matchedMethod.rpcName}, $tree)" else tree } - 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 { + class RpcTraitMetadataConstructor(ownerType: Type, atParam: Option[CompositeParam]) + extends MetadataConstructor(ownerType, atParam) with TagMatchingSymbol { def baseTagTpe: Type = NothingTpe def fallbackTag: FallbackTag = FallbackTag.Empty @@ -186,248 +98,77 @@ trait RpcMetadatas { this: RpcMacroCommons with RpcSymbols with RpcMappings => val (baseParamTag, fallbackParamTag) = annot(ParamTagAT).map(tagSpec).getOrElse((NothingTpe, FallbackTag.Empty)) - lazy val methodMdParams: List[MethodMetadataParam] = paramLists.flatten.flatMap { - case mmp: MethodMetadataParam => List(mmp) - case rcp: RpcCompositeParam => rcp.constructor.methodMdParams - case _ => Nil - } + lazy val methodMdParams: List[MethodMetadataParam] = collectParams[MethodMetadataParam] - def createDefaultParam(paramSym: Symbol): MethodMetadataParam = - new MethodMetadataParam(this, paramSym) + override def paramByStrategy(paramSym: Symbol, annot: Annot): MetadataParam = + if (annot.tpe <:< RpcMethodMetadataAT) new MethodMetadataParam(this, paramSym) + else super.paramByStrategy(paramSym, annot) - def createCompositeParam(paramSym: Symbol): RpcCompositeParam = - new RpcCompositeParam(this, paramSym) + def compositeConstructor(param: CompositeParam): MetadataConstructor = + new RpcTraitMetadataConstructor(param.actualType, Some(param)) 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 { - case rcp: RpcCompositeParam => - rcp.localValueDecl(rcp.constructor.materializeFor(rpc, methodMappings)) - case dmp: DirectMetadataParam[RealRpcTrait] => - dmp.localValueDecl(dmp.materializeFor(MatchedRpcTrait(rpc))) - case mmp: MethodMetadataParam => mmp.localValueDecl { - 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}") - case List(m) => m.tree - case _ => abort(s"multiple real methods match ${mmp.description}") - } - case RpcParamArity.Optional(_) => mappings match { - case Nil => mmp.mkOptional[Tree](None) - case List(m) => mmp.mkOptional(Some(m.tree)) - case _ => abort(s"multiple real methods match ${mmp.description}") - } - case RpcParamArity.Multi(_, _) => - mmp.mkMulti(mappings.map(m => q"(${m.matchedMethod.rpcName}, ${m.tree})")) + def tryMaterializeFor(rpc: RealRpcTrait, methodMappings: Map[MethodMetadataParam, List[MethodMetadataMapping]]): Res[Tree] = + tryMaterialize(MatchedRpcTrait(rpc)) { case mmp: MethodMetadataParam => + val mappings = methodMappings.getOrElse(mmp, Nil) + mmp.arity match { + case ParamArity.Single(_) => mappings match { + case Nil => Fail(s"no real method found that would match ${mmp.description}") + case List(m) => Ok(m.tree) + case _ => Fail(s"multiple real methods match ${mmp.description}") } + case ParamArity.Optional(_) => mappings match { + case Nil => Ok(mmp.mkOptional[Tree](None)) + case List(m) => Ok(mmp.mkOptional(Some(m.tree))) + case _ => Fail(s"multiple real methods match ${mmp.description}") + } + case ParamArity.Multi(_, named) => + Ok(mmp.mkMulti(mappings.map(_.collectedTree(named)))) } } - constructorCall(argDecls) - } } class MethodMetadataConstructor( - val ownerType: Type, - val atParam: Either[MethodMetadataParam, MethodCompositeParam] - ) extends MetadataConstructor[RealMethod]( - primaryConstructor(ownerType, Some(atParam.fold[RpcSymbol](identity, identity)))) { - - val containingMethodParam: MethodMetadataParam = - atParam.fold(identity, _.owner.containingMethodParam) - - lazy val paramMdParams: List[ParamMetadataParam] = paramLists.flatten.flatMap { - case pmp: ParamMetadataParam => List(pmp) - case mcp: MethodCompositeParam => mcp.constructor.paramMdParams - case _ => Nil - } + ownerType: Type, + val containingMethodParam: MethodMetadataParam, + atParam: Option[CompositeParam] + ) extends MetadataConstructor(ownerType, atParam) { + + lazy val paramMdParams: List[ParamMetadataParam] = collectParams[ParamMetadataParam] - def createDefaultParam(paramSym: Symbol): ParamMetadataParam = - new ParamMetadataParam(this, paramSym) + override def paramByStrategy(paramSym: Symbol, annot: Annot): MetadataParam = + if (annot.tpe <:< RpcParamMetadataAT) new ParamMetadataParam(this, paramSym) + else super.paramByStrategy(paramSym, annot) - def createCompositeParam(paramSym: Symbol): MethodCompositeParam = - new MethodCompositeParam(this, paramSym) + def compositeConstructor(param: CompositeParam): MetadataConstructor = + new MethodMetadataConstructor(param.actualType, containingMethodParam, Some(param)) def paramMappings(matchedMethod: MatchedMethod): Res[Map[ParamMetadataParam, Tree]] = - collectParamMappings(paramMdParams, "metadata parameter", matchedMethod)( - (param, parser) => param.metadataFor(parser).map(t => (param, t))).map(_.toMap) + collectParamMappings(matchedMethod.real.realParams, paramMdParams, "metadata parameter")( + (param, parser) => param.metadataFor(matchedMethod, parser).map(t => (param, t))).map(_.toMap) def tryMaterializeFor(matchedMethod: MatchedMethod, paramMappings: Map[ParamMetadataParam, Tree]): Res[Tree] = - Res.traverse(paramLists.flatten) { - case cmp: MethodCompositeParam => - cmp.constructor.tryMaterializeFor(matchedMethod, paramMappings).map(cmp.localValueDecl) - case dmp: DirectMetadataParam[RealMethod] => - dmp.tryMaterializeFor(matchedMethod).map(dmp.localValueDecl) - case pmp: ParamMetadataParam => - Ok(pmp.localValueDecl(paramMappings(pmp))) - }.map(constructorCall) + tryMaterialize(matchedMethod) { + case pmp: ParamMetadataParam => Ok(paramMappings(pmp)) + } } - class ParamMetadataConstructor( - val ownerType: Type, - val atParam: Either[ParamMetadataParam, ParamCompositeParam], - val indexInRaw: Int - ) extends MetadataConstructor[RealParam]( - primaryConstructor(ownerType, Some(atParam.fold[RpcSymbol](identity, identity)))) { + class ParamMetadataConstructor(ownerType: Type, atParam: Option[CompositeParam], val indexInRaw: Int) + extends MetadataConstructor(ownerType, atParam) { - override def createDirectParam(paramSym: Symbol, annot: Annot): DirectMetadataParam[RealParam] = + override def paramByStrategy(paramSym: Symbol, annot: Annot): MetadataParam = annot.tpe match { case t if t <:< ReifyPositionAT => new ReifiedPositionParam(this, paramSym) case t if t <:< ReifyFlagsAT => new ReifiedFlagsParam(this, paramSym) - case _ => super.createDirectParam(paramSym, annot) + case _ => super.paramByStrategy(paramSym, annot) } - def createDefaultParam(paramSym: Symbol): UnknownParam[RealParam] = - new UnknownParam(this, paramSym) - - def createCompositeParam(paramSym: Symbol): ParamCompositeParam = - new ParamCompositeParam(this, paramSym) + def compositeConstructor(param: CompositeParam): MetadataConstructor = + new ParamMetadataConstructor(param.actualType, Some(param), indexInRaw) def tryMaterializeFor(matchedParam: MatchedParam): Res[Tree] = - Res.traverse(paramLists.flatten) { - case pcp: ParamCompositeParam => - pcp.constructor.tryMaterializeFor(matchedParam).map(pcp.localValueDecl) - case dmp: DirectMetadataParam[RealParam] => - 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(matchedSymbol: MatchedRealSymbol[Real]): Tree - def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] - } - - class ImplicitParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) - extends DirectMetadataParam[Real](owner, symbol) { - - val checked: Boolean = findAnnotation(symbol, CheckedAT).nonEmpty - - def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree = - q"${infer(actualType)}" - - 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(matchedSymbol)) - } - - class ReifiedAnnotParam[Real <: RealRpcSymbol](owner: MetadataConstructor[Real], symbol: Symbol) - extends DirectMetadataParam[Real](owner, symbol) with ArityParam { - - def allowMulti: Boolean = true - def allowNamedMulti: Boolean = false - def allowListedMulti: Boolean = true - - if (!(arity.collectedType <:< typeOf[StaticAnnotation])) { - reportProblem(s"${arity.collectedType} is not a subtype of StaticAnnotation") - } - - def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree = { - def validated(annot: Annot): Annot = { - if (containsInaccessibleThises(annot.tree)) { - echo(showCode(annot.tree)) - matchedSymbol.real.reportProblem(s"reified annotation contains this-references inaccessible outside RPC trait") - } - annot - } - - val rpcSym = matchedSymbol.real - arity match { - case RpcParamArity.Single(annotTpe) => - 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(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(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] = - Ok(materializeFor(matchedSymbol)) - } - - 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(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) - extends DirectMetadataParam[Real](owner, symbol) { - - if (!(actualType =:= typeOf[String])) { - reportProblem(s"its type is not String") - } - - def materializeFor(matchedSymbol: MatchedRealSymbol[Real]): Tree = - q"${if (useRpcName) matchedSymbol.rpcName else matchedSymbol.real.nameStr}" - - def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] = - Ok(materializeFor(matchedSymbol)) - } - - class ReifiedPositionParam(owner: ParamMetadataConstructor, symbol: Symbol) - extends DirectMetadataParam[RealParam](owner, symbol) { - - if (!(actualType =:= ParamPositionTpe)) { - reportProblem("its type is not ParamPosition") - } - - def materializeFor(matchedParam: MatchedRealSymbol[RealParam]): Tree = { - val rpcSym = matchedParam.real - q"$ParamPositionObj(${rpcSym.index}, ${rpcSym.indexOfList}, ${rpcSym.indexInList}, ${owner.indexInRaw})" - } - - def tryMaterializeFor(matchedParam: MatchedRealSymbol[RealParam]): Res[Tree] = - Ok(materializeFor(matchedParam)) - } - - class ReifiedFlagsParam(owner: ParamMetadataConstructor, symbol: Symbol) - extends DirectMetadataParam[RealParam](owner, symbol) { - - if (!(actualType =:= ParamFlagsTpe)) { - reportProblem("its type is not ParamFlags") - } - - 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 = - flag(s.isImplicit, 0) | - flag(s.isByNameParam, 1) | - flag(isRepeated(s), 2) | - flag(s.isParamWithDefault, 3) | - flag(s.isSynthetic, 4) - q"new $ParamFlagsTpe($rawFlags)" - } - - 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(matchedSymbol: MatchedRealSymbol[Real]): Tree = - reportProblem(s"no strategy annotation (e.g. @infer) found") - def tryMaterializeFor(matchedSymbol: MatchedRealSymbol[Real]): Res[Tree] = - Ok(materializeFor(matchedSymbol)) + tryMaterialize(matchedParam)(p => Fail(s"unexpected metadata parameter $p")) } } 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 94e9154b7..cff95f73c 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 @@ -1,109 +1,15 @@ package com.avsystem.commons package macros.rpc -trait RpcSymbols { this: RpcMacroCommons => +import com.avsystem.commons.macros.meta.MacroSymbols +import com.avsystem.commons.macros.misc.{Fail, Ok, Res} - import c.universe._ - - sealed abstract class RpcArity - object RpcArity { - trait Single extends RpcArity - trait Optional extends RpcArity - trait Multi extends RpcArity - } - - sealed abstract class RpcParamArity(val verbatimByDefault: Boolean) extends RpcArity { - def collectedType: Type - } - object RpcParamArity { - def fromAnnotation(param: ArityParam, - allowMulti: Boolean, allowListed: Boolean, allowNamed: Boolean): RpcParamArity = { - - 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) - val valueMember = optionLikeType.member(TypeName("Value")) - if (valueMember.isAbstract) - param.reportProblem("could not determine actual value of optional parameter type") - else - RpcParamArity.Optional(valueMember.typeSignatureIn(optionLikeType)) - } - else if (allowMulti && at <:< MultiArityAT) { - if (allowNamed && param.actualType <:< StringPFTpe) - Multi(param.actualType.baseType(PartialFunctionClass).typeArgs(1), named = true) - else if (allowListed && param.actualType <:< BIterableTpe) - Multi(param.actualType.baseType(BIterableClass).typeArgs.head, named = false) - else if (allowNamed && allowListed) - param.reportProblem(s"@multi ${param.shortDescription} must be a PartialFunction of String " + - s"(for by-name mapping) or Iterable (for sequence)") - else if (allowListed) - param.reportProblem(s"@multi ${param.shortDescription} must be an Iterable") - else - param.reportProblem(s"@multi ${param.shortDescription} must be a PartialFunction of String") - } - else param.reportProblem(s"forbidden RPC arity annotation: $at") - } - - case class Single(collectedType: Type) extends RpcParamArity(true) with RpcArity.Single - case class Optional(collectedType: Type) extends RpcParamArity(true) with RpcArity.Optional - case class Multi(collectedType: Type, named: Boolean) extends RpcParamArity(false) with RpcArity.Multi - } - - sealed abstract class RpcMethodArity(val verbatimByDefault: Boolean) extends RpcArity - object RpcMethodArity { - def fromAnnotation(method: RawMethod): RpcMethodArity = { - val at = method.annot(RpcArityAT).fold(SingleArityAT)(_.tpe) - 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 object Multi extends RpcMethodArity(false) with RpcArity.Multi - } - - abstract class RpcSymbol { - def symbol: Symbol - def pos: Position = symbol.pos - def shortDescription: String - def description: String - def problemStr: String = s"problem with $description" - - def reportProblem(msg: String, detailPos: Position = NoPosition): Nothing = - abortAt(s"$problemStr: $msg", if (detailPos != NoPosition) detailPos else pos) - - def infer(tpt: Tree): TermName = - infer(getType(tpt)) +trait RpcSymbols extends MacroSymbols { this: RpcMacroCommons => - def infer(tpe: Type): TermName = - inferCachedImplicit(tpe, s"$problemStr: ", pos) - - val name: TermName = symbol.name.toTermName - val safeName: TermName = c.freshName(symbol.name.toTermName) - val nameStr: String = name.decodedName.toString - val encodedNameStr: String = name.encodedName.toString - - override def equals(other: Any): Boolean = other match { - case rpcSym: RpcSymbol => symbol == rpcSym.symbol - case _ => false - } - override def hashCode: Int = symbol.hashCode - override def toString: String = symbol.toString - } - - 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) - } + import c.universe._ - sealed trait MatchedRealSymbol[+Real <: RealRpcSymbol] { - def real: Real + sealed trait Matched extends MatchedSymbol { + def real: RealRpcSymbol def fallbackTagUsed: FallbackTag def annot(tpe: Type): Option[Annot] = @@ -118,17 +24,21 @@ trait RpcSymbols { this: RpcMacroCommons => val rpcName = annot(RpcNameAT).fold(real.nameStr)(_.findArg[String](RpcNameArg)) prefixes.mkString("", "", rpcName) } + + def rawName: String = rpcName } - case class MatchedRpcTrait(real: RealRpcTrait) extends MatchedRealSymbol[RealRpcTrait] { + case class MatchedRpcTrait(real: RealRpcTrait) extends Matched { def fallbackTagUsed: FallbackTag = FallbackTag.Empty + def indexInRaw: Int = 0 } - case class MatchedMethod(real: RealMethod, fallbackTagUsed: FallbackTag) - extends MatchedRealSymbol[RealMethod] + case class MatchedMethod(real: RealMethod, fallbackTagUsed: FallbackTag) extends Matched { + def indexInRaw: Int = 0 + } - case class MatchedParam(real: RealParam, fallbackTagUsed: FallbackTag, matchedOwner: MatchedMethod) - extends MatchedRealSymbol[RealParam] { + case class MatchedParam(real: RealParam, fallbackTagUsed: FallbackTag, matchedOwner: MatchedMethod, indexInRaw: Int) + extends Matched { val whenAbsent: Tree = annot(WhenAbsentAT).fold(EmptyTree) { annot => @@ -173,58 +83,9 @@ trait RpcSymbols { this: RpcMacroCommons => } } - trait RawRpcSymbol extends RpcSymbol { - 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(a.findArg[Tree](defaultTagArg, EmptyTree)) - (tagType, fallbackTag) - } - - lazy val (requiredTag, whenUntaggedTag) = { - val taggedAnnot = annot(TaggedAT) - val requiredTagType = taggedAnnot.fold(baseTagTpe)(_.tpe.typeArgs.head) - if (!(requiredTagType <:< baseTagTpe)) { - val msg = - if (baseTagTpe =:= NothingTpe) - "cannot use @tagged, no tag annotation type specified with @methodTag/@paramTag" - else s"tag annotation type $requiredTagType specified in @tagged annotation " + - s"must be a subtype of specified base tag $baseTagTpe" - reportProblem(msg) - } - val whenUntagged = FallbackTag(taggedAnnot.map(_.findArg[Tree](WhenUntaggedArg, EmptyTree)).getOrElse(EmptyTree)) - (requiredTagType, whenUntagged) - } + trait RealRpcSymbol extends MacroSymbol - // returns fallback tag tree only IF it was necessary - def matchTag(realRpcSymbol: RealRpcSymbol): Res[FallbackTag] = { - val tagAnnot = findAnnotation(realRpcSymbol.symbol, baseTagTpe) - val fallbackTagUsed = if (tagAnnot.isEmpty) whenUntaggedTag orElse fallbackTag else FallbackTag.Empty - val realTagTpe = tagAnnot.map(_.tpe).getOrElse(NoType) orElse fallbackTagUsed.annotTree.tpe orElse baseTagTpe - - if (realTagTpe <:< requiredTag) Ok(fallbackTagUsed) - else Fail(s"it does not accept ${realRpcSymbol.shortDescription}s tagged with $realTagTpe") - } - - lazy val requiredAnnots: List[Type] = - allAnnotations(symbol, AnnotatedAT).map(_.tpe.dealias.typeArgs.head) - - def matchFilters(realSymbol: MatchedRealSymbol[RealRpcSymbol]): Res[Unit] = - Res.traverse(requiredAnnots) { annotTpe => - if (realSymbol.annot(annotTpe).nonEmpty) Ok(()) - else Fail(s"no annotation of type $annotTpe found on ${realSymbol.real.shortDescription}") - }.map(_ => ()) - } - - sealed trait RealRpcSymbol extends RpcSymbol - - abstract class RpcTrait(val symbol: Symbol) extends RpcSymbol { + abstract class RpcTrait(val symbol: Symbol) extends MacroSymbol { def tpe: Type if (!symbol.isAbstract || !symbol.isClass) { @@ -232,91 +93,14 @@ trait RpcSymbols { this: RpcMacroCommons => } } - abstract class RpcMethod extends RpcSymbol { - def ownerType: Type - - if (!symbol.isMethod) { - abortAt(s"problem with member $nameStr of type $ownerType: it must be a method (def)", pos) - } - - val sig: Type = symbol.typeSignatureIn(ownerType) + abstract class RpcMethod extends MacroMethod { if (sig.typeParams.nonEmpty) { // can we relax this? reportProblem("RPC methods must not be generic") } - - def paramLists: List[List[RpcParam]] - val resultType: Type = sig.finalResultType - - def argLists: List[List[Tree]] = paramLists.map(_.map(_.argToPass)) - def paramDecls: List[List[Tree]] = paramLists.map(_.map(_.paramDecl)) } - abstract class RpcParam extends RpcSymbol { - val actualType: Type = actualParamType(symbol) - - def localValueDecl(body: Tree): Tree = - if (symbol.asTerm.isByNameParam) - q"def $safeName = $body" - else - q"val $safeName = $body" - - def paramDecl: Tree = { - val implicitFlag = if (symbol.isImplicit) Flag.IMPLICIT else NoFlags - ValDef(Modifiers(Flag.PARAM | implicitFlag), safeName, TypeTree(symbol.typeSignature), EmptyTree) - } - - def argToPass: Tree = - if (isRepeated(symbol)) q"$safeName: _*" else q"$safeName" - } - - trait AritySymbol extends RpcSymbol { - val arity: RpcArity - - // @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(matchedReal: MatchedRealSymbol[RealRpcSymbol]): Res[Unit] = arity match { - case _: RpcArity.Single@unchecked | _: RpcArity.Optional@unchecked => - if (matchedReal.rpcName == nameStr) Ok(()) - else Fail(s"it only matches ${matchedReal.real.shortDescription}s with RPC name $nameStr") - case _: RpcArity.Multi@unchecked => Ok(()) - } - } - - trait ArityParam extends RpcParam with AritySymbol { - def allowMulti: Boolean - def allowNamedMulti: Boolean - def allowListedMulti: Boolean - - val arity: RpcParamArity = - RpcParamArity.fromAnnotation(this, allowMulti, allowListedMulti, allowNamedMulti) - - lazy val optionLike: TermName = infer(tq"$OptionLikeCls[$actualType]") - - lazy val canBuildFrom: TermName = arity match { - case _: RpcParamArity.Multi if allowNamedMulti && actualType <:< StringPFTpe => - infer(tq"$CanBuildFromCls[$NothingCls,($StringCls,${arity.collectedType}),$actualType]") - case _: RpcParamArity.Multi => - infer(tq"$CanBuildFromCls[$NothingCls,${arity.collectedType},$actualType]") - case _ => abort(s"(bug) CanBuildFrom computed for non-multi $shortDescription") - } - - 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 = - 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 { + trait RealParamTarget extends ArityParam with TagMatchingSymbol { def allowMulti: Boolean = true def allowNamedMulti: Boolean = true def allowListedMulti: Boolean = true @@ -331,11 +115,12 @@ trait RpcSymbols { this: RpcMacroCommons => def cannotMapClue: String - def matchRealParam(matchedMethod: MatchedMethod, realParam: RealParam): Res[MatchedParam] = for { - fallbackTag <- matchTag(realParam) - matchedParam = MatchedParam(realParam, fallbackTag, matchedMethod) - _ <- matchFilters(matchedParam) - } yield matchedParam + def matchRealParam(matchedMethod: MatchedMethod, realParam: RealParam, indexInRaw: Int): Res[MatchedParam] = + for { + fallbackTag <- matchTag(realParam) + matchedParam = MatchedParam(realParam, fallbackTag, matchedMethod, indexInRaw) + _ <- matchFilters(matchedParam) + } yield matchedParam } object RawParam { @@ -344,10 +129,11 @@ trait RpcSymbols { this: RpcMacroCommons => MethodNameParam(owner, symbol) else if (findAnnotation(symbol, CompositeAT).nonEmpty) CompositeRawParam(owner, symbol) - else RawValueParam(owner, symbol) + else + RawValueParam(owner, symbol) } - sealed trait RawParam extends RpcParam { + sealed trait RawParam extends MacroParam { val owner: Either[RawMethod, CompositeRawParam] val containingRawMethod: RawMethod = owner.fold(identity, _.containingRawMethod) @@ -393,13 +179,15 @@ trait RpcSymbols { this: RpcMacroCommons => } case class RealParam(owner: RealMethod, symbol: Symbol, index: Int, indexOfList: Int, indexInList: Int) - extends RpcParam with RealRpcSymbol { + extends MacroParam with RealRpcSymbol { def shortDescription = "real parameter" def description = s"$shortDescription $nameStr of ${owner.description}" } - case class RawMethod(owner: RawRpcTrait, symbol: Symbol) extends RpcMethod with RawRpcSymbol with AritySymbol { + case class RawMethod(owner: RawRpcTrait, symbol: Symbol) + extends RpcMethod with TagMatchingSymbol with AritySymbol { + def shortDescription = "raw method" def description = s"$shortDescription $nameStr of ${owner.description}" @@ -407,7 +195,7 @@ trait RpcSymbols { this: RpcMacroCommons => def baseTagTpe: Type = owner.baseMethodTag def fallbackTag: FallbackTag = owner.fallbackMethodTag - val arity: RpcMethodArity = RpcMethodArity.fromAnnotation(this) + val arity: MethodArity = MethodArity.fromAnnotation(this) val tried: Boolean = annot(TriedAT).nonEmpty val verbatimResult: Boolean = @@ -438,17 +226,17 @@ trait RpcSymbols { this: RpcMacroCommons => def rawImpl(caseDefs: List[(String, Tree)]): Tree = { val body = arity match { - case RpcMethodArity.Single => caseDefs match { + case MethodArity.Single => caseDefs match { case Nil => abort(s"no real method found that would match $description") case List((_, single)) => single case _ => abort(s"multiple real methods match $description") } - case RpcMethodArity.Optional => caseDefs match { + case MethodArity.Optional => caseDefs match { case Nil => q"$RpcUtils.missingOptionalRpc($nameStr)" case List((_, single)) => single case _ => abort(s"multiple real methods match $description") } - case RpcMethodArity.Multi => + case MethodArity.Multi => val methodNameName = c.freshName(TermName("methodName")) q""" ${methodNameParam.safePath} match { @@ -486,7 +274,7 @@ trait RpcSymbols { this: RpcMacroCommons => val realParams: List[RealParam] = paramLists.flatten } - case class RawRpcTrait(tpe: Type) extends RpcTrait(tpe.typeSymbol) with RawRpcSymbol { + case class RawRpcTrait(tpe: Type) extends RpcTrait(tpe.typeSymbol) with TagMatchingSymbol { def shortDescription = "raw RPC" def description = s"$shortDescription $tpe" diff --git a/commons-macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala b/commons-macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala index 59163188c..3cc29a735 100644 --- a/commons-macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala +++ b/commons-macros/src/main/scala/com/avsystem/commons/macros/serialization/GenCodecMacros.scala @@ -170,11 +170,9 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with """ case List(p) => def writeField(value: Tree) = { - val baseWrite = q"writeField[${p.valueType}](output, ${p.idx}, $value)" - if (isTransientDefault(p)) - q"if($value != ${p.defaultValue}) { $baseWrite }" - else - baseWrite + val writeArgs = q"output" :: q"${p.idx}" :: value :: + (if (isTransientDefault(p)) List(p.defaultValue) else Nil) + q"writeField[${p.valueType}](..$writeArgs)" } if (canUseFields) @@ -186,11 +184,9 @@ class GenCodecMacros(ctx: blackbox.Context) extends CodecMacroCommons(ctx) with """ case _ => def writeField(p: ApplyParam, value: Tree) = { - val baseWrite = q"writeField[${p.valueType}](output, ${p.idx}, $value)" - if (isTransientDefault(p)) - q"if($value != ${p.defaultValue}) { $baseWrite }" - else - baseWrite + val writeArgs = q"output" :: q"${p.idx}" :: value :: + (if (isTransientDefault(p)) List(p.defaultValue) else Nil) + q"writeField[${p.valueType}](..$writeArgs)" } if (canUseFields) diff --git a/docs/REST.md b/docs/REST.md index 3b392ff1d..c65932908 100644 --- a/docs/REST.md +++ b/docs/REST.md @@ -46,6 +46,19 @@ The `commons-jetty` module provides Jetty-based implementations for JVM. - [Handler function](#handler-function) - [Implementing a server](#implementing-a-server) - [Implementing a client](#implementing-a-client) +- [Generating OpenAPI 3.0 specifications](#generating-openapi-30-specifications) + - [`RestSchema` typeclass](#restschema-typeclass) + - [Macro materialized ADT schemas](#macro-materialized-adt-schemas) + - [Registered schemas](#registered-schemas) + - [Adjusting macro materialized schemas](#adjusting-macro-materialized-schemas) + - [Manually defined schemas](#manually-defined-schemas) + - [`RestResponses` typeclass](#restresponses-typeclass) + - [`RestRequestBody` typeclass](#restrequestbody-typeclass) + - [Adjusting generated OpenAPI documents with annotations](#adjusting-generated-openapi-documents-with-annotations) + - [Adjusting schemas](#adjusting-schemas) + - [Adjusting parameters](#adjusting-parameters) + - [Adjusting operations](#adjusting-operations) + - [Limitations](#limitations) @@ -66,9 +79,12 @@ Then, define some trivial REST interface: ```scala import com.avsystem.commons.rest._ +case class User(id: String, name: String, birthYear: Int) +object User extends RestDataCompanion[User] + trait UserApi { - /** Returns ID of newly created user */ - def createUser(name: String, birthYear: Int): Future[String] + /** Returns newly created user */ + def createUser(name: String, birthYear: Int): Future[User] } object UserApi extends DefaultRestApiCompanion[UserApi] ``` @@ -80,7 +96,8 @@ import com.avsystem.commons.jetty.rest.RestHandler import org.eclipse.jetty.server.Server class UserApiImpl extends UserApi { - def createUser(name: String, birthYear: Int) = Future.successful(s"$name-ID") + def createUser(name: String, birthYear: Int): Future[User] = + Future.successful(User(s"$name-ID", name, birthYear)) } object ServerMain { @@ -91,6 +108,7 @@ object ServerMain { server.join() } } + ``` Finally, obtain a client proxy for your API using Jetty HTTP client and make a call: @@ -113,9 +131,9 @@ 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 Success(user) => println(s"User ${user.id} created") case Failure(cause) => cause.printStackTrace() } @@ -144,10 +162,10 @@ Response: HTTP/1.1 200 OK Date: Wed, 18 Jul 2018 11:43:08 GMT Content-Type: application/json;charset=utf-8 -Content-Length: 9 +Content-Length: 47 Server: Jetty(9.3.23.v20180228) -"Fred-ID" +{"id":"Fred-ID","name":"Fred","birthYear":1990} ``` ## REST API traits @@ -209,6 +227,9 @@ object GenericApi { import DefaultRestImplicits._ implicit val restAsRawReal: RawRest.AsRawRealRpc[MyApi] = RawRest.materializeAsRawReal implicit val restMetadata: RestMetadata[MyApi] = RestMetadata.materializeForRpc + + import openapi._ + implicit val openApiMetadata: OpenApiMetadata[MyApi] = OpenApiMetadata.materializeForRpc } ``` @@ -228,8 +249,8 @@ method into a HTTP REST call. * 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. + content type. If response type is `Unit` (method result type is `Future[Unit]`) then a `204 No Content` + response with 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. @@ -350,7 +371,7 @@ 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] +object User extends RestDataCompanion[User] @PUT def updateUser(@Body user: User): Future[Unit] ``` @@ -498,9 +519,9 @@ which depends on implicit `AsRaw/Real[RestResponse, R]`. This effectively means it's enough if `R` is serializable as `RestResponse`. 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`). +as `RestResponse`. This default translation of `HttpBody` into `RestResponse` always uses 200 as a status code +(or 204 for empty body). When translating `RestResponse` into `HttpBody` and response contains erroneous status code, +`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 @@ -590,6 +611,10 @@ trait MyRestApi { ... } object MyRestApi extends CirceRestApiCompanion[MyRestApi] ``` +**WARNING**: if you also generate [OpenAPI documents](#generating-openapi-30-specifications) for your +REST API, then along from custom serialization you must provide customized instances of +[`RestSchema`](#rest-schema-typeclass) that will adequately describe your new serialization format. + #### 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 @@ -602,6 +627,10 @@ object MyClass { } ``` +**WARNING**: Remember that if you generate [OpenAPI documents](#generating-openapi-30-specifications) for your +REST API then you must also provide custom [`RestSchema`](#rest-schema-typeclass) instance for your type that +will match its serialization format. + #### Providing serialization for third party type If you need to define serialization implicits for a third party type, you can't do it through @@ -627,6 +656,10 @@ trait MyRestApi { ... } object MyRestApi extends RestApiCompanion[EnhancedRestImplicits, MyRestApi](EnhancedRestImplicits) ``` +**WARNING**: Remember that if you generate [OpenAPI documents](#generating-openapi-30-specifications) for your +REST API then you must also provide custom [`RestSchema`](#rest-schema-typeclass) instance for your type that +will match its serialization format. + #### Supporting result containers other than `Future` By default, every HTTP method in REST API trait must return its return wrapped into a `Future`. @@ -663,6 +696,10 @@ trait MonixTaskRestImplicits { object MonixTaskRestImplicits ``` +**WARNING**: If you generate [OpenAPI documents](#generating-openapi-30-specifications) for your +REST API then you must also provide appropriate instance of `RestResultType` typeclass which by default +is only defined for `Future`. + ## API evolution REST framework gives you a certain amount of guarantees about backwards compatibility of your API. @@ -732,3 +769,245 @@ 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. + +## Generating OpenAPI 3.0 specifications + +[OpenAPI](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md) is an open standard for describing +REST endpoints using JSON or YAML. It can be consumed by e.g. [Swagger UI](https://swagger.io/tools/swagger-ui/) in +order to generate nicely looking, human-readable documentation of a REST endpoint. + +REST framework provides automatic generation of OpenAPI documents based on REST API traits. +In order to do this, `OpenApiMetadata` typeclass must be available for an API trait. If your trait's companion +extends `DefaultRestApiCompanion` or `DefaultRestServerApiCompanion` then `OpenApiMetadata` is automatically +materialized by a macro. You can then use it to generate OpenAPI specification document like this: + +```scala +import com.avsystem.commons.rest._ + +trait MyRestApi { + ... +} +object MyRestApi extends DefaultRestApiCompanion[MyRestApi] + +object PrintOpenApiJson { + def main(args: Array[String]): Unit = { + val openapi = MyRestApi.openapiMetadata.openapi( + Info("Some REST API", "0.1", description = "Some example REST API"), + servers = List(Server("http://localhost")) + ) + println(JsonStringOutput.writePretty(openapi)) + } +} +``` + +### `RestSchema` typeclass + +In order to macro-materialize `OpenApiMetadata` for your REST API, you need to provide an instance of `RestSchema` typeclass +for every type used by your API (as a parameter or result type, i.e. when your method returns `Future[T]` then +`RestSchema[T]` will be needed). `RestSchema` contains a description of data type that is later translated into +[OpenAPI Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject). + +Most of the primitive types, collection types, `Option`, `Opt`, etc. already have an appropriate `RestSchema` instance defined +(roughly the same set of simple types that have `GenCodec` automatically available). If `RestSchema` is not defined, there are +usually two ways to provide it: + +#### Macro materialized ADT schemas + +If your data type is an ADT (algebraic data type) - which means case class, object or sealed hierarchy - the easiest way +to provide schema is by making companion object of your data type extend `RestDataCompanion` - this will automatically +materialize a `RestStructure` instance for your data type. `RestSchema` is then built based on that `RestStructure`. +`RestDataCompanion` also automatically materializes a `GenCodec` instance. + +```scala +case class User(id: String, name: String, birthYear: String) +object User extends RestDataCompanion[User] // gives GenCodec + RestStructure + RestSchema +``` + +[Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject) generated for +`User` class will look like this: + +```json +{ + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "birthYear": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "name", + "birthYear" + ] +} +``` + +It's also possible to macro materialize schema without using `RestDataCompanion`: + +```scala +object User { + implicit lazy val schema: RestSchema[User] = RestStructure.materialize[User].standaloneSchema +} +``` + +Schema derived for an ADT from macro materialized `RestStructure` will describe the JSON format used by +`GenCodec` macro materialized for that type. It will take into account all the annotations, e.g. +`@flatten`, `@name`, `@transparent`, etc. + +##### Registered schemas + +By default, schemas macro materialized for case classes and sealed hierarchies will be _named_. +This means they will not be inlined but rather registered under their name in +[Components Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#componentsObject). +By default, the name that will be used will be the simple (unqualified) name of the data type, e.g. "User". +When referring to registered schema (e.g. in +[Media Type Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#mediaTypeObject)), +a [Reference Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#referenceObject) +will be inserted, e.g. `{"$ref": "#/components/schemas/User"}`. This is good for schema reuse but may lead to name +conflicts if you have multiple data types with the same name but in different packages. In such situations, you must +disambiguate names of your data types with `@name` annotation. Unfortunately, such conflicts cannot be detected in compile +time and will only be reported in runtime, when trying to generate OpenAPI document. + +##### Adjusting macro materialized schemas + +It's possible to adjust schemas macro materialized for ADTs using annotations. For example, you can use +`@description` annotation on data types and case class fields to inject description into materialized schemas, e.g. + +```scala +import com.avsystem.commons.rest._ +import openapi._ + +@description("data type for users") +case class User(id: String, @description("user name") name: User, birthYear: Int) +object User extends RestDataCompanion[User] +``` + +You can also use more general `@adjustSchema` annotation which lets you define +completely arbitrary schema transformations. +See [Adjusting generated OpenAPI documents with annotations](#adjusting-generated-openapi-documents-with-annotations) +for more details on this mechanism. + +#### Manually defined schemas + +You may also define `RestSchema` completely by hand. This is usually done for primitive types or types with custom +serialization, different from macro materialized `GenCodec`. You can also insert references to externally defined +schemas. + +```scala +class CustomStringType { ... } +object CustomStringType { + implicit val restSchema: RestSchema[CustomType] = + RestSchema.plain(Schema(`type` = DataType.String, description = "custom string type")) +} +``` + +### `RestResponses` typeclass + +`RestResponses` is an auxiliary typeclass which is needed for result type of every HTTP REST method +in your REST API trait. For example, if your method returns `Future[User]` then you need an instance +of `RestResponses[User]` (this transformation is modeled by yet another intermediate typeclass, `RestResultType`). +This typeclass governs generation of +[Responses Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#responsesObject) + +By default, if no specific `RestResponses` instance is provided, it is created based on `RestSchema`. +The resulting [Responses](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#responsesObject) +will contain exactly one +[Response](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#responseObject) +for HTTP status code `200 OK` with a single +[Media Type](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#mediaTypeObject) +for `application/json` media type and +[Schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject) +inferred from `RestSchema` instance. + +You may want to define `RestResponses` instance manually if you want to use other media types than +`application/json` or you want to define multiple possible responses for different HTTP status codes. +You may also modify responses for particular method - see [Adjusting operations](#adjusting-operations). +Normally, manual `RestResponses` instance is needed when a type has custom `AsRaw/AsReal[RestResponse, T]` +instance which defines custom serialization to HTTP response. + +### `RestRequestBody` typeclass + +`RestRequestBody` typeclass is an auxiliary typeclass analogous to `RestResponses`. It's necessary +for `@Body` parameters of REST methods and governs generation of +[Request Body Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#requestBodyObject). +By default, if not defined explicitly, it's also derived from `RestSchema` and contains single +[Media Type](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#mediaTypeObject) +for `application/json` media type and +[Schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject) +inferred from `RestSchema` instance. + +You may want to provide custom instance of `RestRequestBody` for your type if that type is serialized +to different media type than `application/json` (which can be done by providing appropriate instance +of `AsReal/AsRaw[HttpBody, T]`. + +### Adjusting generated OpenAPI documents with annotations + +The way OpenAPI documents are generated for your REST API can be influenced with annotations +applied on REST methods, parameters and data types. The most common example is the `@description` annotation +which may be applied on data types, case class fields, REST methods and parameters. +It causes the description to be injected into appropriate +[Schema](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject), +[Parameter](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#parameterObject) or +[Operation](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#operationObject) +objects. + +However, `@description` is just an example of more general mechanism - schemas, parameters and operations +can be modified arbitrarily. + +#### Adjusting schemas + +In order to adjust schemas, one can define arbitrary annotations that extend `SchemaAdjuster`. +There is also a default implementation of `SchemaAdjuster`, the `@adjustSchema` annotation which +takes a lambda parameter which defines the schema transformation. + +Annotations extending `SchemaAdjuster` can arbitrarily transform a +[Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject) +and can be applied on: + +* data types with macro-generated `RestSchema` +* case class fields of data types with macro generated `RestSchema` +* `@JsonBodyParam` and `@Body` parameters of REST methods + +Schema adjusters do **NOT** work on path/header/query parameters and REST methods +themselves. Instead use [parameter adjusters](#adjusting-parameters) and +[operation adjusters](#adjusting-operations) which can also modify schemas +used by `Parameter` and `Operation` objects. + +Also, be aware that a `SchemaAdjuster` may be passed a schema reference instead of actual schema object. +This reference is then wrapped into a +[Schema Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject) +object defined as `{"allOf": [{"$ref": }]}`. +Therefore, a schema adjuster may extend the referenced schema using +[Composition and Inheritance](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaComposition) +but it should not rely on the ability to inspect the referenced schema. + +#### Adjusting parameters + +Similar to `SchemaAdjuster` there is a `ParameterAdjuster` annotation trait. Its default implementation +is `@adjustParameter` which takes transformation lambda as its parameter. +Schema adjusters can arbitrarily transform +[Parameter Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#parameterObject) +which are generated for path, header and query parameters of REST methods. + +#### Adjusting operations + +Finally, there is `OperationAdjuster` annotation trait with default implementation `@adjustOperation`. +Operation adjuster can be applied on REST HTTP methods in order to transform +[Operation Objects](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#operationObject) +generated for them. This in particular means that operation adjusters can modify request body and responses +of an operation. + +### Limitations + +Currently, it's not possible to include examples, default values and arbitrarily-valued enums (with values other than strings) +in OpenAPI documents and schemas generated by AVSystem REST framework. + +Also, current representation of OpenAPI document does not support +[specification extensions](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#specificationExtensions). diff --git a/project/plugins.sbt b/project/plugins.sbt index 13c2f523a..ee5d104dc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ resolvers += Resolver.url("jetbrains-bintray", url("http://dl.bintray.com/jetbrains/sbt-plugins/"))(Resolver.ivyStylePatterns) val scalaJSVersion = - Option(System.getenv("SCALAJS_VERSION")).getOrElse("0.6.23") + Option(System.getenv("SCALAJS_VERSION")).getOrElse("0.6.25") addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) addSbtPlugin("org.jetbrains" % "sbt-ide-settings" % "1.0.0") @@ -13,5 +13,5 @@ addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.1") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") addSbtPlugin("com.eed3si9n" % "sbt-unidoc" % "0.4.1") addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.3.0") -addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.3") +addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.1.0-M7") addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.4")