Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default value for enumeratum #1852

Merged
merged 14 commits into from
Feb 18, 2022
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ abstract class EndpointAnnotationsMacro(val c: blackbox.Context) {
.fold(i)(encodedExample => q"$i.schema(_.encodedExample($encodedExample))"),
i =>
util
.extractTreeFromAnnotation(field, schemaDefaultType)
.fold(i)(default => q"$i.default($default)"),
.extractTreeAndOptTreeFromAnnotation(field, schemaDefaultType)
.fold(i)(default => q"$i.default(${default._1})"),
i =>
util
.extractStringArgFromAnnotation(field, schemaFormatType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ trait SchemaMagnoliaDerivation {
annotations.foldLeft(schema) {
case (schema, ann: Schema.annotations.description) => schema.description(ann.text)
case (schema, ann: Schema.annotations.encodedExample) => schema.encodedExample(ann.example)
case (schema, ann: Schema.annotations.default[X]) => schema.default(ann.default)
case (schema, ann: Schema.annotations.default[X]) => schema.default(ann.default, ann.encoded)
case (schema, ann: Schema.annotations.validate[X]) => schema.validate(ann.v)
case (schema, ann: Schema.annotations.format) => schema.format(ann.format)
case (schema, _: Schema.annotations.deprecated) => schema.deprecated(true)
Expand Down
12 changes: 12 additions & 0 deletions core/src/main/scala-2/sttp/tapir/internal/CaseClassUtil.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,16 @@ private[tapir] class CaseClassUtil[C <: blackbox.Context, T: C#WeakTypeTag](val
}
}
}

def extractTreeAndOptTreeFromAnnotation(field: Symbol, annotationType: c.Type): Option[(Tree, Tree)] = {
field.annotations.collectFirst {
case a if a.tree.tpe <:< annotationType =>
a.tree.children.tail match {
case List(t, u @ Apply(TypeApply(name @ Select(_, _), List(TypeTree())), List(_))) if name.toString.startsWith("scala.Some.apply") => (t, u)
case List(t, u @ Select(_, _)) if u.toString.startsWith("scala.None") => (t, u)
case List(t, TypeApply(Select(_, name @ TermName(_)), List(TypeTree()))) if name.decodedName.toString.startsWith("<init>$default") => (t, q"None")
case _ => throw new IllegalStateException(s"Cannot extract annotation argument from: ${c.universe.showRaw(a.tree)}")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -376,8 +376,8 @@ class AnnotationsMacros[T <: Product: Type](using q: Quotes) {
.getOrElse(t),
t =>
field
.extractTreeFromAnnotation(schemaDefaultAnnotationSymbol)
.map(d => addMetadataToAtom(field, t, '{ i => i.default(${ d.asExprOf[f] }) }))
.extractTreeAndOptTreeFromAnnotation(schemaDefaultAnnotationSymbol)
.map(d => addMetadataToAtom(field, t, '{ i => i.default(${ d._1.asExprOf[f] }) }))
.getOrElse(t),
t =>
field
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/scala-3/sttp/tapir/internal/CaseClass.scala
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ class CaseClassField[Q <: Quotes, T](using val q: Q, t: Type[T])(
case _ => report.throwError(s"Cannot extract annotation: @${annSymbol.name}, from field: ${symbol.name}, of type: ${Type.show[T]}")
}

def extractTreeAndOptTreeFromAnnotation(annSymbol: Symbol): Option[(Tree, Tree)] = constructorField.getAnnotation(annSymbol).map {
case Apply(_, List(t, TypeApply(Select(_, "$lessinit$greater$default$2"), _))) => (t, '{ None }.asTerm)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity, why not just return the second element of the list, which will correspond to the encoded annotation parameter?

Copy link
Contributor Author

@mkrzemien mkrzemien Feb 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this method is used in AnnotationMacros (and EndpointAnnotationMacros for Scala 2) where the first parameter is required.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm not sure I understand. Do you mean you needed to cover the cases when there are both 1 or 2 parameters if an annotation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my question is, why wouldn't this work:

def extractTreeAndOptTreeFromAnnotation(annSymbol: Symbol): Option[(Tree, Tree)] = constructorField.getAnnotation(annSymbol).map {
  case Apply(_, List(t, u)) => (t, u)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I've added the method to cover the cases with 1 or 2 parameters. I've described all these cases in a new test-case.

Actually, only the first parameter is currently used, the second one being ignored. So as an alternative I've been considering a different method - returning first arg and simply ignoring the rest.

I've created another PR with this alternative. The patter matching there is indeed much simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my question is, why wouldn't this work: (...)

I wanted to make sure, that the second parameter was Optional. Now I think I might have overcomplicated it :/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happens :)

case Apply(_, List(t, u @ Ident("None"))) => (t, u)
case Apply(_, List(t, NamedArg(_, u @ Ident("None")))) => (t, u)
case Apply(_, List(t, u @ Apply(TypeApply(name @ Select(Ident("Some"), "apply"), List(_)), List(_)))) => (t, u)
case Apply(_, List(t, NamedArg(_, u @ Apply(TypeApply(name @ Select(Ident("Some"), "apply"), List(_)), List(_))))) => (t, u)
case _ => report.throwError(s"Cannot extract annotation: @${annSymbol.name}, from field: ${symbol.name}, of type: ${Type.show[T]}")
}

def annotated(annSymbol: Symbol): Boolean = annotation(annSymbol).isDefined
def annotation(annSymbol: Symbol): Option[Term] = constructorField.getAnnotation(annSymbol)
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ trait SchemaMagnoliaDerivation {
annotations.foldLeft(schema) {
case (schema, ann: Schema.annotations.description) => schema.description(ann.text)
case (schema, ann: Schema.annotations.encodedExample) => schema.encodedExample(ann.example)
case (schema, ann: Schema.annotations.default[X @unchecked]) => schema.default(ann.default)
case (schema, ann: Schema.annotations.default[X @unchecked]) => schema.default(ann.default, ann.encoded)
case (schema, ann: Schema.annotations.validate[X @unchecked]) => schema.validate(ann.v)
case (schema, ann: Schema.annotations.format) => schema.format(ann.format)
case (schema, _: Schema.annotations.deprecated) => schema.deprecated(true)
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/sttp/tapir/EndpointIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ object EndpointIO {
override private[tapir] type ThisType[X] = OneOfBody[O, X]
override def show: String = showOneOf(variants.map { variant =>
val prefix =
if ((ContentTypeRange.exactNoCharset(variant.body.codec.format.mediaType)) == variant.range) "" else s"${variant.range} -> "
if (ContentTypeRange.exactNoCharset(variant.body.codec.format.mediaType) == variant.range) "" else s"${variant.range} -> "
prefix + variant.body.show
})
override def map[U](m: Mapping[T, U]): OneOfBody[O, U] = copy[O, U](mapping = mapping.map(m))
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/scala/sttp/tapir/Schema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ object Schema extends LowPrioritySchema with SchemaCompanionMacros {
object annotations {
class description(val text: String) extends StaticAnnotation
class encodedExample(val example: Any) extends StaticAnnotation
class default[T](val default: T) extends StaticAnnotation
class default[T](val default: T, val encoded: Option[Any] = None) extends StaticAnnotation
class format(val format: String) extends StaticAnnotation
class deprecated extends StaticAnnotation
class encodedName(val name: String) extends StaticAnnotation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,27 @@ final case class TapirResponseTest2(
setCookies: List[CookieWithMeta]
)

final case class TapirRequestTest15(
@query
@Schema.annotations.default(11)
field1: Int,
@query
@Schema.annotations.default(12, None)
field2: Int,
@query
@Schema.annotations.default(13, encoded=None)
field3: Int,
@query
@Schema.annotations.default(14, Some(140))
field4: Int,
@query
@Schema.annotations.default(15, Some("150"))
field5: Int,
@query
@Schema.annotations.default(16, encoded=Some(160))
field6: Int
)

class DeriveEndpointIOTest extends AnyFlatSpec with Matchers with TableDrivenPropertyChecks with Tapir {

"@endpointInput" should "derive correct input for @query, @cookie, @header" in {
Expand Down Expand Up @@ -253,6 +274,18 @@ class DeriveEndpointIOTest extends AnyFlatSpec with Matchers with TableDrivenPro
derived.codec.schema.applyValidation(TapirRequestTest8(1)) shouldBe empty
}

it should "derive default annotation correctly" in {
val expectedInput = query[Int]("field1").default(11)
.and(query[Int]("field2").default(12))
.and(query[Int]("field3").default(13))
.and(query[Int]("field4").default(14))
.and(query[Int]("field5").default(15))
.and(query[Int]("field6").default(16))
.mapTo[TapirRequestTest15]

compareTransputs(EndpointInput.derived[TapirRequestTest15], expectedInput) shouldBe true
}

val bodyInputDerivations =
Table(
("body", "expected", "derived"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
openapi: 3.0.3
info:
title: Fruits
version: '1.0'
paths:
/:
post:
operationId: postRoot
requestBody:
content:
application/json:
schema:
$ref:'#/components/schemas/FruitQueryWithEncoded'
required: true
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/FruitWithEnum'
'400':
description: 'Invalid value for: body'
content:
text/plain:
schema:
type: string
components:
schemas:
FruitQueryWithEncoded:
type: object
properties:
fruitType:
$ref: '#/components/schemas/FruitType'
FruitType:
type: string
default: PEAR
enum:
- APPLE
- PEAR
FruitWithEnum:
required:
- fruit
- amount
type: object
properties:
fruit:
type: string
amount:
type: integer
fruitType:
type: array
items:
$ref: '#/components/schemas/FruitType'
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
openapi: 3.0.3
info:
title: Fruits
version: '1.0'
paths:
/:
get:
operationId: getRoot
parameters:
- name: type
in: query
required: false
schema:
$ref: '#/components/schemas/FruitType'
example: APPLE
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/FruitWithEnum'
'400':
description: 'Invalid value for: query parameter type'
content:
text/plain:
schema:
type: string
components:
schemas:
FruitType:
type: string
default: PEAR
enum:
- APPLE
- PEAR
FruitWithEnum:
required:
- fruit
- amount
type: object
properties:
fruit:
type: string
amount:
type: integer
fruitType:
type: array
items:
$ref: '#/components/schemas/FruitType'
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
openapi: 3.0.3
info:
title: Fruits
version: '1.0'
paths:
/:
post:
operationId: postRoot
requestBody:
content:
application/json:
schema:
$ref:'#/components/schemas/FruitQuery'
required: true
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/FruitWithEnum'
'400':
description: 'Invalid value for: body'
content:
text/plain:
schema:
type: string
components:
schemas:
FruitQuery:
type: object
properties:
fruitType:
$ref: '#/components/schemas/FruitType'
FruitType:
type: string
enum:
- APPLE
- PEAR
FruitWithEnum:
required:
- fruit
- amount
type: object
properties:
fruit:
type: string
amount:
type: integer
fruitType:
type: array
items:
$ref: '#/components/schemas/FruitType'
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
openapi: 3.0.3
info:
title: Fruits
version: '1.0'
paths:
/fruit-by-type1:
get:
operationId: getFruit-by-type1
parameters:
- name: type1
in: query
required: false
schema:
$ref: '#/components/schemas/FruitType'
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/FruitWithEnum'
'400':
description: 'Invalid value for: query parameter type1'
content:
text/plain:
schema:
type: string
/fruit-by-type2:
get:
operationId: getFruit-by-type2
parameters:
- name: type2
in: query
required: false
schema:
$ref: '#/components/schemas/FruitType'
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/FruitWithEnum'
'400':
description: 'Invalid value for: query parameter type2'
content:
text/plain:
schema:
type: string
components:
schemas:
FruitType:
type: string
default: PEAR
enum:
- APPLE
- PEAR
FruitWithEnum:
required:
- fruit
- amount
type: object
properties:
fruit:
type: string
amount:
type: integer
fruitType:
type: array
items:
$ref: '#/components/schemas/FruitType'
Loading