Skip to content

Commit ed51041

Browse files
authored
Merge pull request #86 from AVSystem/json-defaults
Arbitrary JSON default values for OpenAPI
2 parents 66c6a14 + b7defc8 commit ed51041

File tree

28 files changed

+367
-122
lines changed

28 files changed

+367
-122
lines changed

commons-annotations/src/main/scala/com/avsystem/commons/meta/metaAnnotations.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,16 @@ final class adtCaseMetadata extends MetadataParamStrategy
225225
*/
226226
final class reifyAnnot extends MetadataParamStrategy
227227

228+
/**
229+
* Similar to [[reifyAnnot]] but the annotation is encoded into some raw type (which is the type of the parameter).
230+
* Because the type of the parameter is now the raw type, annotation type must be given as type parameter of
231+
* [[reifyEncodedAnnot]].
232+
*
233+
* Encoding is done by searching and using implicit instance of `AsRaw[ParameterType,AnnotationType]`.
234+
* Arity annotations apply in the same way as for [[reifyAnnot]].
235+
*/
236+
final class reifyEncodedAnnot[T <: StaticAnnotation] extends MetadataParamStrategy
237+
228238
/**
229239
* Metadata parameter typed as `Boolean` can be annotated with `@isAnnotated[SomeAnnotation]`. Boolean value will then
230240
* hold information about whether RPC trait, method or parameter for which metadata is materialized is annotated with

commons-annotations/src/main/scala/com/avsystem/commons/serialization/whenAbsent.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,17 @@ import scala.annotation.StaticAnnotation
2828
class whenAbsent[+T](v: => T) extends StaticAnnotation {
2929
def value: T = v
3030
}
31+
object whenAbsent {
32+
/**
33+
* If you want your parameter to have _both_ a `@whenAbsent` annotation and a Scala-level default value, you can
34+
* use this macro to avoid writing the default value twice:
35+
*
36+
* {{{
37+
* case class Record(@whenAbsent(0) i: Int = whenAbsent.value)
38+
* }}}
39+
*
40+
* This is useful when you want the default value to be collectible by macros (e.g. `OpenApiMetadata` for REST).
41+
* which is possible only with default value in annotation.
42+
*/
43+
def value[T]: T = macro macros.misc.WhiteMiscMacros.whenAbsentValue
44+
}

commons-core/src/main/scala/com/avsystem/commons/SharedExtensions.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ object SharedExtensions extends SharedExtensions {
6363
*/
6464
def |>[B](f: A => B): B = f(a)
6565

66+
def applyIf[A0 >: A](predicate: A => Boolean)(f: A => A0): A0 =
67+
if (predicate(a)) f(a) else a
68+
6669
/**
6770
* Explicit syntax to discard the value of a side-effecting expression.
6871
* Useful when `-Ywarn-value-discard` compiler option is enabled.

commons-core/src/main/scala/com/avsystem/commons/meta/AdtMetadataCompanion.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ import com.avsystem.commons.misc.MacroGenerated
77
trait AdtMetadataCompanion[M[_]] extends MetadataCompanion[M] {
88
def materialize[T]: M[T] = macro AdtMetadataMacros.materialize[T]
99

10-
implicit def materializeMacroGenerated[T]: MacroGenerated[M[T]] = macro AdtMetadataMacros.materializeMacroGenerated[T]
10+
implicit def materializeMacroGenerated[C, T]: MacroGenerated[C, M[T]] = macro AdtMetadataMacros.materializeMacroGenerated[T]
1111
}
Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
package com.avsystem.commons
22
package misc
33

4-
class MacroGenerated[T](val forCompanion: Any => T)
4+
/**
5+
* Wrapper class for macro-generated typeclasses. Usually, a typeclass is wrapped in `MacroGenerated` when
6+
* it's accepted as implicit super constructor parameter of some base class for companion objects of
7+
* types for which the typeclass is being generated.
8+
* Example: [[com.avsystem.commons.serialization.HasGenCodec HasGenCodec]], which is a base class for
9+
* companion objects of classes that want [[com.avsystem.commons.serialization.GenCodec GenCodec]] to be
10+
* macro-generated for them.
11+
*
12+
* Instead of materializing the type class instance directly, a function from some base companion type `C` is
13+
* materialized. To obtain the actual typeclass instance, companion object must be passed as this function's
14+
* argument. This serves two purposes:
15+
*
16+
* - contents of `C` will be wildcard-imported into macro-materialization, allowing injection of additional implicits
17+
* - working around too strict Scala validation of super constructor arguments: https://github.com/scala/bug/issues/7666
18+
*/
19+
class MacroGenerated[C, T](val forCompanion: C => T) extends AnyVal
520
object MacroGenerated {
6-
def apply[T](instance: => T): MacroGenerated[T] =
7-
new MacroGenerated[T](_ => instance)
21+
def apply[C, T](instance: => T): MacroGenerated[C, T] =
22+
new MacroGenerated[C, T](_ => instance)
823
}

commons-core/src/main/scala/com/avsystem/commons/rest/DefaultRestApiCompanion.scala

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec, HasGenCodec}
2020
* object User extends RestDataCompanion[User]
2121
* }}}
2222
*/
23-
abstract class RestDataCompanion[T](
24-
implicit macroRestStructure: MacroGenerated[RestStructure[T]], macroCodec: MacroGenerated[GenCodec[T]]
25-
) extends HasGenCodec[T] {
23+
abstract class RestDataCompanion[T](implicit
24+
macroRestStructure: MacroGenerated[DefaultRestImplicits, RestStructure[T]],
25+
macroCodec: MacroGenerated[Any, GenCodec[T]]
26+
) extends HasGenCodec[T] with DefaultRestImplicits {
2627
implicit lazy val restStructure: RestStructure[T] = macroRestStructure.forCompanion(this)
2728
implicit lazy val restSchema: RestSchema[T] = restStructure.standaloneSchema // lazy on restStructure
2829
}
@@ -46,7 +47,7 @@ trait OpenApiFullInstances[Real] extends FullInstances[Real] {
4647
def openapiMetadata: OpenApiMetadata[Real]
4748
}
4849

49-
/** @see [[RestApiCompanion]] */
50+
/** @see [[RestApiCompanion]]*/
5051
abstract class RestClientApiCompanion[Implicits, Real](protected val implicits: Implicits)(
5152
implicit inst: RpcMacroInstances[Implicits, ClientInstances, Real]
5253
) {
@@ -57,7 +58,7 @@ abstract class RestClientApiCompanion[Implicits, Real](protected val implicits:
5758
RawRest.fromHandleRequest(handleRequest)
5859
}
5960

60-
/** @see [[RestApiCompanion]] */
61+
/** @see [[RestApiCompanion]]*/
6162
abstract class RestServerApiCompanion[Implicits, Real](protected val implicits: Implicits)(
6263
implicit inst: RpcMacroInstances[Implicits, ServerInstances, Real]
6364
) {
@@ -68,7 +69,7 @@ abstract class RestServerApiCompanion[Implicits, Real](protected val implicits:
6869
RawRest.asHandleRequest(real)
6970
}
7071

71-
/** @see [[RestApiCompanion]] */
72+
/** @see [[RestApiCompanion]]*/
7273
abstract class RestServerOpenApiCompanion[Implicits, Real](protected val implicits: Implicits)(
7374
implicit inst: RpcMacroInstances[Implicits, OpenApiServerInstances, Real]
7475
) {
@@ -99,7 +100,7 @@ abstract class RestApiCompanion[Implicits, Real](protected val implicits: Implic
99100
RawRest.asHandleRequest(real)
100101
}
101102

102-
/** @see [[RestApiCompanion]] */
103+
/** @see [[RestApiCompanion]]*/
103104
abstract class RestOpenApiCompanion[Implicits, Real](protected val implicits: Implicits)(
104105
implicit inst: RpcMacroInstances[Implicits, OpenApiFullInstances, Real]
105106
) {

commons-core/src/main/scala/com/avsystem/commons/rest/annotations.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,20 @@ class FormBody extends StaticAnnotation
114114
class Prefix(val path: String = null) extends RestMethodTag
115115

116116
sealed trait RestParamTag extends RpcTag
117-
sealed trait NonBodyTag extends RestParamTag
117+
sealed trait NonBodyTag extends RestParamTag {
118+
def isPath: Boolean = this match {
119+
case _: Path => true
120+
case _ => false
121+
}
122+
def isHeader: Boolean = this match {
123+
case _: Header => true
124+
case _ => false
125+
}
126+
def isQuery: Boolean = this match {
127+
case _: Query => true
128+
case _ => false
129+
}
130+
}
118131
sealed trait BodyTag extends RestParamTag
119132

120133
/**

commons-core/src/main/scala/com/avsystem/commons/rest/data.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion,
66
import com.avsystem.commons.rpc._
77
import com.avsystem.commons.serialization.GenCodec.ReadFailure
88
import com.avsystem.commons.serialization.json.{JsonReader, JsonStringInput, JsonStringOutput}
9+
import com.avsystem.commons.serialization.{GenCodec, whenAbsent}
910

1011
import scala.util.control.NoStackTrace
1112

@@ -55,6 +56,23 @@ object QueryValue {
5556
* Wrapped value MUST be a valid JSON.
5657
*/
5758
case class JsonValue(value: String) extends AnyVal with RestValue
59+
object JsonValue {
60+
// TODO: this is terrible, but GenCodec in general just can't embed arbitrary JSON at this point...
61+
private[rest] implicit val codec: GenCodec[JsonValue] =
62+
GenCodec.create(
63+
{
64+
case ji: JsonStringInput => JsonValue(ji.readRawJson())
65+
case i => JsonValue(i.readString())
66+
},
67+
{
68+
case (jo: JsonStringOutput, JsonValue(json)) => jo.writeRawJson(json)
69+
case (o, JsonValue(json)) => o.writeString(json)
70+
}
71+
)
72+
73+
implicit def whenAbsentAsJson[T](implicit valueAsJson: AsRaw[JsonValue, T]): AsRaw[Try[JsonValue], whenAbsent[T]] =
74+
AsRaw.create(wa => Try(wa.value).map(valueAsJson.asRaw))
75+
}
5876

5977
/**
6078
* Value used to represent HTTP body. Also used as direct encoding of [[Body]] parameters. Types that have

commons-core/src/main/scala/com/avsystem/commons/rest/openapi/OpenApi.scala

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.avsystem.commons
22
package rest.openapi
33

44
import com.avsystem.commons.misc.{AbstractValueEnum, AbstractValueEnumCompanion, EnumCtx}
5+
import com.avsystem.commons.rest.JsonValue
6+
import com.avsystem.commons.serialization.json.JsonStringOutput
57
import com.avsystem.commons.serialization.{transientDefault => td, _}
68

79
/**
@@ -255,8 +257,8 @@ case class Schema(
255257
@td not: OptArg[RefOr[Schema]] = OptArg.Empty,
256258
@td discriminator: OptArg[Discriminator] = OptArg.Empty,
257259

258-
@td enum: List[String] = Nil, //TODO: other values than strings
259-
@td default: OptArg[String] = OptArg.Empty //TODO: other values than strings
260+
@td enum: List[JsonValue] = Nil,
261+
@td default: OptArg[JsonValue] = OptArg.Empty
260262
) {
261263
def unwrapSingleRefAllOf: RefOr[Schema] = allOf match {
262264
case List(ref: RefOr.Ref) if this == Schema(allOf = List(ref)) => ref
@@ -291,7 +293,7 @@ object Schema extends HasGenCodec[Schema] {
291293
Schema(`type` = DataType.Object, additionalProperties = properties)
292294

293295
def enumOf(values: List[String]): Schema =
294-
Schema(`type` = DataType.String, enum = values)
296+
Schema(`type` = DataType.String, enum = values.map(s => JsonValue(JsonStringOutput.write(s))))
295297

296298
def nullable(schema: RefOr[Schema]): Schema =
297299
schema.rewrapRefToAllOf.copy(nullable = true)
@@ -306,6 +308,9 @@ object Schema extends HasGenCodec[Schema] {
306308
case RefOr.Value(schema) => schema
307309
case ref => Schema(allOf = List(ref))
308310
}
311+
312+
def withDefaultValue(dv: Opt[Try[JsonValue]]): RefOr[Schema] =
313+
dv.collect({ case Success(v) => v }).fold(refOrSchema)(v => RefOr(rewrapRefToAllOf.copy(default = v)))
309314
}
310315
}
311316

@@ -380,7 +385,7 @@ case class Parameter(
380385
@td explode: OptArg[Boolean] = OptArg.Empty,
381386
@td allowReserved: Boolean = false,
382387
@td schema: OptArg[RefOr[Schema]] = OptArg.Empty,
383-
@td example: OptArg[String] = OptArg.Empty, //TODO other values than strings
388+
@td example: OptArg[JsonValue] = OptArg.Empty,
384389
@td examples: Map[String, RefOr[Example]] = Map.empty,
385390
@td content: OptArg[Entry[String, MediaType]] = OptArg.Empty
386391
)
@@ -445,7 +450,7 @@ object Encoding extends HasGenCodec[Encoding]
445450
case class Example(
446451
@td summary: OptArg[String] = OptArg.Empty,
447452
@td description: OptArg[String] = OptArg.Empty,
448-
@td value: OptArg[String] = OptArg.Empty, //TODO other values than strings
453+
@td value: OptArg[JsonValue] = OptArg.Empty,
449454
@td externalValue: OptArg[String] = OptArg.Empty
450455
)
451456
object Example extends HasGenCodec[Example]
@@ -474,7 +479,7 @@ case class Header(
474479
@td explode: OptArg[Boolean] = OptArg.Empty,
475480
@td allowReserved: Boolean = false,
476481
@td schema: OptArg[RefOr[Schema]] = OptArg.Empty,
477-
@td example: OptArg[String] = OptArg.Empty, //TODO other values than strings
482+
@td example: OptArg[JsonValue] = OptArg.Empty,
478483
@td examples: Map[String, RefOr[Example]] = Map.empty,
479484
@td content: OptArg[Entry[String, MediaType]] = OptArg.Empty
480485
)
@@ -544,8 +549,8 @@ object OAuthFlow extends HasGenCodec[OAuthFlow]
544549
case class Link(
545550
@td operationRef: OptArg[String] = OptArg.Empty,
546551
@td operationId: OptArg[String] = OptArg.Empty,
547-
@td parameters: Map[String, String] = Map.empty, //TODO: other values than strings
548-
@td requestBody: OptArg[String] = OptArg.Empty, //TODO: other values than strings
552+
@td parameters: Map[String, JsonValue] = Map.empty,
553+
@td requestBody: OptArg[JsonValue] = OptArg.Empty,
549554
@td description: OptArg[String] = OptArg.Empty,
550555
@td server: OptArg[Server] = OptArg.Empty
551556
)

commons-core/src/main/scala/com/avsystem/commons/rest/openapi/OpenApiMetadata.scala

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,20 @@ import scala.annotation.implicitNotFound
1212
"${T} extend one of the convenience base companion classes, e.g. DefaultRestApiCompanion")
1313
@methodTag[RestMethodTag]
1414
case class OpenApiMetadata[T](
15-
@multi @tagged[Prefix](whenUntagged = new Prefix)
15+
@multi @rpcMethodMetadata
16+
@tagged[Prefix](whenUntagged = new Prefix)
1617
@paramTag[RestParamTag](defaultTag = new Path)
17-
@rpcMethodMetadata
1818
prefixes: List[OpenApiPrefix[_]],
1919

20-
@multi @tagged[GET]
20+
@multi @rpcMethodMetadata
21+
@tagged[GET]
2122
@paramTag[RestParamTag](defaultTag = new Query)
2223
@rpcMethodMetadata
2324
gets: List[OpenApiGetOperation[_]],
2425

25-
@multi @tagged[BodyMethodTag](whenUntagged = new POST)
26+
@multi @rpcMethodMetadata
27+
@tagged[BodyMethodTag](whenUntagged = new POST)
2628
@paramTag[RestParamTag](defaultTag = new BodyField)
27-
@rpcMethodMetadata
2829
bodyMethods: List[OpenApiBodyOperation[_]]
2930
) {
3031
val httpMethods: List[OpenApiOperation[_]] = (gets: List[OpenApiOperation[_]]) ++ bodyMethods
@@ -76,9 +77,7 @@ object OpenApiMetadata extends RpcMetadataCompanion[OpenApiMetadata]
7677
sealed trait OpenApiMethod[T] extends TypedMetadata[T] {
7778
@reifyName(useRawName = true) def name: String
7879
@reifyAnnot def methodTag: RestMethodTag
79-
@multi
80-
@rpcParamMetadata
81-
@tagged[NonBodyTag] def parameters: List[OpenApiParameter[_]]
80+
@multi @rpcParamMetadata @tagged[NonBodyTag] def parameters: List[OpenApiParameter[_]]
8281

8382
val pathPattern: String = {
8483
val pathParts = methodTag.path :: parameters.flatMap {
@@ -109,10 +108,8 @@ case class OpenApiPrefix[T](
109108
}
110109

111110
sealed trait OpenApiOperation[T] extends OpenApiMethod[T] {
112-
@infer
113-
@checked def resultType: RestResultType[T]
114-
@multi
115-
@reifyAnnot def adjusters: List[OperationAdjuster]
111+
@infer @checked def resultType: RestResultType[T]
112+
@multi @reifyAnnot def adjusters: List[OperationAdjuster]
116113
def methodTag: HttpMethodTag
117114
def requestBody(resolver: SchemaResolver): Opt[RefOr[RequestBody]]
118115

@@ -151,25 +148,27 @@ case class OpenApiBodyOperation[T](
151148
def requestBody(resolver: SchemaResolver): Opt[RefOr[RequestBody]] =
152149
singleBody.map(_.requestBody(resolver).opt).getOrElse {
153150
if (bodyFields.isEmpty) Opt.Empty else Opt {
154-
val schema = Schema(`type` = DataType.Object,
155-
properties = bodyFields.iterator.map(p => (p.info.name, p.schema(resolver))).toMap,
156-
required = bodyFields.collect { case p if !p.info.hasFallbackValue => p.info.name }
157-
)
151+
val fields = bodyFields.iterator.map(p => (p.info.name, p.schema(resolver))).toMap
152+
val requiredFields = bodyFields.collect { case p if !p.info.hasFallbackValue => p.info.name }
153+
val schema = Schema(`type` = DataType.Object, properties = fields, required = requiredFields)
158154
val mimeType = if (formBody) HttpBody.FormType else HttpBody.JsonType
159-
RefOr(RestRequestBody.simpleRequestBody(mimeType, RefOr(schema)))
155+
RefOr(RestRequestBody.simpleRequestBody(mimeType, RefOr(schema), requiredFields.nonEmpty))
160156
}
161157
}
162158
}
163159

164160
case class OpenApiParamInfo[T](
165161
@reifyName(useRawName = true) name: String,
166-
@optional @reifyAnnot whenAbsent: Opt[whenAbsent[T]],
162+
@optional @reifyEncodedAnnot[whenAbsent[T]] whenAbsent: Opt[Try[JsonValue]],
167163
@isAnnotated[transientDefault] transientDefault: Boolean,
168164
@reifyFlags flags: ParamFlags,
169165
@infer restSchema: RestSchema[T]
170166
) extends TypedMetadata[T] {
171167
val hasFallbackValue: Boolean =
172-
whenAbsent.fold(flags.hasDefaultValue)(wa => Try(wa.value).isSuccess)
168+
whenAbsent.fold(flags.hasDefaultValue)(_.isSuccess)
169+
170+
def schema(resolver: SchemaResolver, withDefaultValue: Boolean): RefOr[Schema] =
171+
resolver.resolve(restSchema) |> (s => if (withDefaultValue) s.withDefaultValue(whenAbsent) else s)
173172
}
174173

175174
case class OpenApiParameter[T](
@@ -184,7 +183,11 @@ case class OpenApiParameter[T](
184183
case _: HeaderAnnot => Location.Header
185184
case _: Query => Location.Query
186185
}
187-
val param = Parameter(info.name, in, required = !info.hasFallbackValue, schema = resolver.resolve(info.restSchema))
186+
val pathParam = in == Location.Path
187+
val param = Parameter(info.name, in,
188+
required = pathParam || !info.hasFallbackValue,
189+
schema = info.schema(resolver, withDefaultValue = !pathParam)
190+
)
188191
RefOr(adjusters.foldRight(param)(_ adjustParameter _))
189192
}
190193
}
@@ -194,7 +197,7 @@ case class OpenApiBodyField[T](
194197
@multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster]
195198
) extends TypedMetadata[T] {
196199
def schema(resolver: SchemaResolver): RefOr[Schema] =
197-
SchemaAdjuster.adjustRef(schemaAdjusters, resolver.resolve(info.restSchema))
200+
SchemaAdjuster.adjustRef(schemaAdjusters, info.schema(resolver, withDefaultValue = true))
198201
}
199202

200203
case class OpenApiBody[T](

0 commit comments

Comments
 (0)