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

Clarify 'required: false' and 'default: ...' both being present #1534

Merged
merged 11 commits into from
Jul 10, 2022
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ vendor/
.idea
.bundle/
metals.sbt
.sbtopts
1 change: 1 addition & 0 deletions .sbtopts-default
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-J-Xmx4G
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ object SwaggerUtil {
import Sc._
def buildResolve[B: Extractable, A <: Schema[_]: Default.GetDefault](transformLit: B => F[L#Term]): Tracker[A] => F[core.ResolvedType[L]] = { a =>
for {
default <- Default(a.unwrapTracker).extract[B].traverse(transformLit(_))
default <- Default(a).extract[B].traverse(transformLit(_))
} yield resolved match {
case x: core.Resolved[L] => x.copy(defaultValue = default)
case other => other
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,18 +125,11 @@ object LanguageParameter {

log.function(s"fromParameter")(
for {
_ <- log.debug(parameter.unwrapTracker.showNotNull)
(meta, required) <- paramMeta(parameter)
core.Resolved(paramType, _, baseDefaultValue, reifiedRawType) <- core.ResolvedType.resolve[L, F](meta, protocolElems)
_ <- log.debug(parameter.unwrapTracker.showNotNull)
(meta, required) <- paramMeta(parameter)
core.Resolved(paramType, _, rawDefaultType, reifiedRawType) <- core.ResolvedType.resolve[L, F](meta, protocolElems)

declType <-
if (!required) {
liftOptionalType(paramType)
} else {
paramType.pure[F]
}

enumDefaultValue <- extractTypeName(paramType).flatMap(_.fold(baseDefaultValue.traverse(_.pure[F])) { tpe =>
baseDefaultValue <- extractTypeName(paramType).flatMap(_.fold(rawDefaultType.traverse(_.pure[F])) { tpe =>
protocolElems
.flatTraverse {
case x @ EnumDefinition(_, _tpeName, _, _, _, _) =>
Expand All @@ -145,16 +138,21 @@ object LanguageParameter {
} yield if (areEqual) List(x) else List.empty[EnumDefinition[L]]
case _ => List.empty[EnumDefinition[L]].pure[F]
}
.flatMap(_.headOption.fold[F[Option[L#Term]]](baseDefaultValue.traverse(_.pure[F])) { x =>
baseDefaultValue.traverse(lookupEnumDefaultValue(tpe, _, x.elems).flatMap(widenTermSelect))
.flatMap(_.headOption.fold[F[Option[L#Term]]](rawDefaultType.traverse(_.pure[F])) { x =>
rawDefaultType.traverse(lookupEnumDefaultValue(tpe, _, x.elems).flatMap(widenTermSelect))
})
})

defaultValue <-
(declType, defaultValue) <-
if (!required) {
(enumDefaultValue.traverse(liftOptionalTerm), emptyOptionalTerm().map(Option.apply _)).mapN(_.orElse(_))
baseDefaultValue match {
case None =>
(liftOptionalType(paramType), emptyOptionalTerm().map(Option.apply _)).mapN((_, _))
case Some(value) =>
(paramType, baseDefaultValue).pure[F]
}
} else {
enumDefaultValue.pure[F]
(paramType, baseDefaultValue).pure[F]
}

name <- getParameterName(parameter)
Expand Down
13 changes: 7 additions & 6 deletions modules/core/src/main/scala/dev/guardrail/terms/Responses.scala
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,14 @@ object Responses {
core.Resolved(baseType, _, baseDefaultValue, _) = resolved // TODO: ReifiedRawType is just dropped, should it be considered?
} yield (contentType, baseType, baseDefaultValue)
}
headers <- resp.downField("headers", _.getHeaders()).unwrapTracker.value.toList.traverse { case (name, header) =>
headers <- resp.downField("headers", _.getHeaders()).indexedDistribute.value.traverse { case (name, header) =>
for {
argName <- formatMethodArgName(s"${name}Header")
termName <- pureTermName(argName)
typeName <- pureTypeName("String").flatMap(widenTypeName)
resultType <- if (header.getRequired) typeName.pure[F] else liftOptionalType(typeName)
} yield new Header(name, header.getRequired, resultType, termName)
argName <- formatMethodArgName(s"${name}Header")
termName <- pureTermName(argName)
typeName <- pureTypeName("String").flatMap(widenTypeName)
required = header.downField("required", _.getRequired).unwrapTracker.getOrElse(false)
resultType <- if (required) typeName.pure[F] else liftOptionalType(typeName)
} yield new Header(name, required, resultType, termName)
}
} yield new Response[L](statusCodeName, statusCode, valueTypes.headOption, new Headers(headers)) // FIXME: headOption
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -896,18 +896,18 @@ class JacksonGenerator private (implicit Cl: CollectionsLibTerms[JavaLanguage, T
Target.log.warning(s"Can't generate default value for class $clsName and property $name.") >> Target.pure(None)
case None => Target.pure(None)
}): Target[Option[Expression]]
finalDefaultTypeValue <- Option(requirement)
.filter {
case PropertyRequirement.Required => true
case _ => false
}
.fold[Target[(Type, Option[Expression])]](
for {
optionalTpe <- Cl.liftOptionalType(tpe)
defaultValueExpr <- defaultValue.fold(Target.pure(Option.empty[Expression]))(dv => dv.toExpression.map(Option.apply))
} yield (optionalTpe, defaultValueExpr)
)(Function.const(Target.pure((tpe, expressionDefaultValue))) _)
(finalDeclType, finalDefaultValue) = finalDefaultTypeValue
(finalDeclType, finalDefaultValue) <- requirement match {
case PropertyRequirement.Required =>
Target.pure((tpe, expressionDefaultValue))
case _ =>
defaultValue match {
case Some(value) => value.toExpression.map(Option.apply).map(tpe -> _)
case None =>
for {
optionalTpe <- Cl.liftOptionalType(tpe)
} yield (optionalTpe, Option.empty[Expression])
}
}
term <- safeParseParameter(s"final ${finalDeclType} $fieldName")
dep = classDep.filterNot(_.asString == clsName) // Filter out our own class name
} yield ProtocolParameter[JavaLanguage](
Expand Down
17 changes: 17 additions & 0 deletions modules/sample-akkaHttp/src/test/scala/core/issues/Issue1532.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package core.issues

import issues.issue1532.client.akkaHttp.Client
import issues.issue1532.client.akkaHttp.definitions.Foo
import issues.issue1532.server.akkaHttp.{ Handler, Resource }

class Issue1532CompileTests {
// No need to actually run the tests, we just want to be sure that these expressions actually compile
// As a result, we stub out the Client and Handler with ???, then call the functions inside a def as well.
def client: Client = ???
def clientTest = client.doFoo(p1 = 123L)

def handler: Handler = ???
def handlerTest = handler.doFoo(Resource.DoFooResponse)(p1 = 234L)

def foo = Foo(bar = 345)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,47 @@ class ValidationTest extends AnyFreeSpec with Matchers with EitherValues {

"maximum validation" - {
"should be inclusive" in {
Validated.decodeValidated(parse("""{ "max_validation": 100 }""").right.get.hcursor).right.value shouldBe Validated(Some(100), None, None)
Validated.decodeValidated(parse("""{ "max_validation": 100 }""").value.hcursor).value shouldBe Validated(Some(100), None, None)
}
"should succeed" in {
Validated.decodeValidated(parse("""{ "max_validation": 10 }""").right.get.hcursor).right.value shouldBe Validated(Some(10), None, None)
Validated.decodeValidated(parse("""{ "max_validation": 10 }""").value.hcursor).value shouldBe Validated(Some(10), None, None)
}
"should fail on incorrect input" in {
Validated.decodeValidated(parse("""{ "max_validation": 101 }""").right.get.hcursor).isLeft shouldBe true
Validated.decodeValidated(parse("""{ "max_validation": 101 }""").value.hcursor).isLeft shouldBe true
}
}

"minimum validation" - {
"should be inclusive" in {
Validated.decodeValidated(parse("""{ "min_validation": 1 }""").right.get.hcursor).right.value shouldBe Validated(None, Some(1), None)
Validated.decodeValidated(parse("""{ "min_validation": 1 }""").value.hcursor).value shouldBe Validated(None, Some(1), None)
}
"should succeed" in {
Validated.decodeValidated(parse("""{ "min_validation": 10 }""").right.get.hcursor).right.value shouldBe Validated(None, Some(10), None)
Validated.decodeValidated(parse("""{ "min_validation": 10 }""").value.hcursor).value shouldBe Validated(None, Some(10), None)
}
"should fail on incorrect input" in {
Validated.decodeValidated(parse("""{ "min_validation": 0 }""").right.get.hcursor).isLeft shouldBe true
Validated.decodeValidated(parse("""{ "min_validation": 0 }""").value.hcursor).isLeft shouldBe true
}
}

"range validation" - {
"should succeed within the range" in {
Validated.decodeValidated(parse("""{ "range_validation": 10 }""").right.get.hcursor).right.value shouldBe Validated(None, None, Some(10))
Validated.decodeValidated(parse("""{ "range_validation": 10 }""").value.hcursor).value shouldBe Validated(None, None, Some(10))
}

"should be inclusive - higher bound" in {
Validated.decodeValidated(parse("""{ "range_validation": 100 }""").right.get.hcursor).right.value shouldBe Validated(None, None, Some(100))
Validated.decodeValidated(parse("""{ "range_validation": 100 }""").value.hcursor).value shouldBe Validated(None, None, Some(100))
}

"should be inclusive - lower bound" in {
Validated.decodeValidated(parse("""{ "range_validation": 0 }""").right.get.hcursor).right.value shouldBe Validated(None, None, Some(0))
Validated.decodeValidated(parse("""{ "range_validation": 0 }""").value.hcursor).value shouldBe Validated(None, None, Some(0))
}

"should fail on incorrect input - lower bound" in {
Validated.decodeValidated(parse("""{ "range_validation": -1 }""").right.get.hcursor).isLeft shouldBe true
Validated.decodeValidated(parse("""{ "range_validation": -1 }""").value.hcursor).isLeft shouldBe true
}

"should fail on incorrect input - higher bound" in {
Validated.decodeValidated(parse("""{ "range_validation": 101 }""").right.get.hcursor).isLeft shouldBe true
Validated.decodeValidated(parse("""{ "range_validation": 101 }""").value.hcursor).isLeft shouldBe true
}

}
Expand Down
28 changes: 28 additions & 0 deletions modules/sample/src/main/resources/issues/issue1532.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
openapi: 3.0.2
info:
title: Ensure required+default work together
version: 1.0.0
paths:
/foo:
post:
operationId: doFoo
parameters:
- name: p1
in: query
required: false
schema:
type: integer
format: int64
default: 300
responses:
200:
description: OK
components:
schemas:
Foo:
type: object
properties:
bar:
type: integer
format: int32
default: 400
1 change: 0 additions & 1 deletion modules/sample/src/main/resources/validation/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ definitions:
format: int32
minimum: 0
maximum: 100
default: 0
paths:
/user/{id}:
get:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ class Issue43 extends AnyFunSpec with Matchers with SwaggerSpecRunner {

it("should generate right case class") {
clsDog.structure shouldBe q"""case class Dog(name: String, packSize: Int = 0) extends Pet""".structure
clsPersianCat.structure shouldBe q"""case class PersianCat(name: String, huntingSkill: Cat.HuntingSkill = Cat.HuntingSkill.Lazy, wool: Option[Int] = Option(10)) extends Cat""".structure
clsPersianCat.structure shouldBe q"""case class PersianCat(name: String, huntingSkill: Cat.HuntingSkill = Cat.HuntingSkill.Lazy, wool: Int = 10) extends Cat""".structure
}

it("should generate right companion object (Dog)") {
Expand All @@ -334,7 +334,7 @@ class Issue43 extends AnyFunSpec with Matchers with SwaggerSpecRunner {
val readOnlyKeys = _root_.scala.Predef.Set[_root_.scala.Predef.String]()
_root_.io.circe.Encoder.AsObject.instance[PersianCat](a => _root_.io.circe.JsonObject.fromIterable(_root_.scala.Vector(("name", a.name.asJson), ("huntingSkill", a.huntingSkill.asJson), ("wool", a.wool.asJson), ("petType", Json.fromString("PersianCat"))))).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key)))
}
implicit val decodePersianCat: _root_.io.circe.Decoder[PersianCat] = new _root_.io.circe.Decoder[PersianCat] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[PersianCat] = for (v0 <- c.downField("name").as[String]; v1 <- c.downField("huntingSkill").as[Cat.HuntingSkill]; v2 <- c.downField("wool").as[Option[Int]]) yield PersianCat(v0, v1, v2) }
implicit val decodePersianCat: _root_.io.circe.Decoder[PersianCat] = new _root_.io.circe.Decoder[PersianCat] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[PersianCat] = for (v0 <- c.downField("name").as[String]; v1 <- c.downField("huntingSkill").as[Cat.HuntingSkill]; v2 <- c.downField("wool").as[Int]) yield PersianCat(v0, v1, v2) }
}
"""
companionPersianCat.structure shouldBe companion.structure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,15 @@ class DefinitionSpec extends AnyFunSuite with Matchers with SwaggerSpecRunner {
val cmp = companionForStaticDefns(staticDefns)

val definition = q"""
case class Sixth(defval: Int = 1, defvalOpt: Option[Long] = Option(2L))
case class Sixth(defval: Int = 1, defvalOpt: Long = 2L)
"""
val companion = q"""
object Sixth {
implicit val encodeSixth: _root_.io.circe.Encoder.AsObject[Sixth] = {
val readOnlyKeys = _root_.scala.Predef.Set[_root_.scala.Predef.String]()
_root_.io.circe.Encoder.AsObject.instance[Sixth](a => _root_.io.circe.JsonObject.fromIterable(_root_.scala.Vector(("defval", a.defval.asJson), ("defval_opt", a.defvalOpt.asJson)))).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key)))
}
implicit val decodeSixth: _root_.io.circe.Decoder[Sixth] = new _root_.io.circe.Decoder[Sixth] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Sixth] = for (v0 <- c.downField("defval").as[Int]; v1 <- c.downField("defval_opt").as[Option[Long]]) yield Sixth(v0, v1) }
implicit val decodeSixth: _root_.io.circe.Decoder[Sixth] = new _root_.io.circe.Decoder[Sixth] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Sixth] = for (v0 <- c.downField("defval").as[Int]; v1 <- c.downField("defval_opt").as[Long]) yield Sixth(v0, v1) }
}
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class DefaultParametersTest extends AnyFunSuite with Matchers with SwaggerSpecRu
val getOrderByIdOKDecoder = {
structuredJsonEntityUnmarshaller.flatMap(_ => _ => json => io.circe.Decoder[Order].decodeJson(json).fold(FastFuture.failed, FastFuture.successful))
}
def getOrderById(orderId: Long, defparmOpt: Option[Int] = Option(1), defparm: Int = 2, headerMeThis: String, headers: List[HttpHeader] = Nil): EitherT[Future, Either[Throwable, HttpResponse], GetOrderByIdResponse] = {
def getOrderById(orderId: Long, defparmOpt: Int = 1, defparm: Int = 2, headerMeThis: String, headers: List[HttpHeader] = Nil): EitherT[Future, Either[Throwable, HttpResponse], GetOrderByIdResponse] = {
val allHeaders = headers ++ scala.collection.immutable.Seq[Option[HttpHeader]](Some(RawHeader("HeaderMeThis", Formatter.show(headerMeThis)))).flatten
makeRequest(HttpMethods.GET, host + basePath + "/store/order/" + Formatter.addPath(orderId) + "?" + Formatter.addArg("defparm_opt", defparmOpt) + Formatter.addArg("defparm", defparm), allHeaders, HttpEntity.Empty, HttpProtocols.`HTTP/1.1`).flatMap(req => EitherT(httpClient(req).flatMap(resp => resp.status match {
case StatusCodes.OK =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Issue370 extends AnyFunSuite with Matchers with SwaggerSpecRunner {
val readOnlyKeys = _root_.scala.Predef.Set[_root_.scala.Predef.String]()
_root_.io.circe.Encoder.AsObject.instance[Foo](a => _root_.io.circe.JsonObject.fromIterable(_root_.scala.Vector(("value", a.value.asJson), ("value2", a.value2.asJson), ("nested", a.nested.asJson)))).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key)))
}
implicit val decodeFoo: _root_.io.circe.Decoder[Foo] = new _root_.io.circe.Decoder[Foo] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Foo] = for (v0 <- c.downField("value").as[Option[Foo.Value]]; v1 <- c.downField("value2").as[Baz]; v2 <- c.downField("nested").as[Option[Foo.Nested]]) yield Foo(v0, v1, v2) }
implicit val decodeFoo: _root_.io.circe.Decoder[Foo] = new _root_.io.circe.Decoder[Foo] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Foo] = for (v0 <- c.downField("value").as[Foo.Value]; v1 <- c.downField("value2").as[Baz]; v2 <- c.downField("nested").as[Option[Foo.Nested]]) yield Foo(v0, v1, v2) }
sealed abstract class Value(val value: String) extends _root_.scala.Product with _root_.scala.Serializable { override def toString: String = value.toString }
object Value {
object members {
Expand All @@ -86,13 +86,13 @@ class Issue370 extends AnyFunSuite with Matchers with SwaggerSpecRunner {
def from(value: String): _root_.scala.Option[Value] = values.find(_.value == value)
implicit val order: cats.Order[Value] = cats.Order.by[Value, Int](values.indexOf)
}
case class Nested(value: Option[Foo.Nested.Value] = Option(Foo.Nested.Value.C))
case class Nested(value: Foo.Nested.Value = Foo.Nested.Value.C)
object Nested {
implicit val encodeNested: _root_.io.circe.Encoder.AsObject[Nested] = {
val readOnlyKeys = _root_.scala.Predef.Set[_root_.scala.Predef.String]()
_root_.io.circe.Encoder.AsObject.instance[Nested](a => _root_.io.circe.JsonObject.fromIterable(_root_.scala.Vector(("value", a.value.asJson)))).mapJsonObject(_.filterKeys(key => !(readOnlyKeys contains key)))
}
implicit val decodeNested: _root_.io.circe.Decoder[Nested] = new _root_.io.circe.Decoder[Nested] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Nested] = for (v0 <- c.downField("value").as[Option[Foo.Nested.Value]]) yield Nested(v0) }
implicit val decodeNested: _root_.io.circe.Decoder[Nested] = new _root_.io.circe.Decoder[Nested] { final def apply(c: _root_.io.circe.HCursor): _root_.io.circe.Decoder.Result[Nested] = for (v0 <- c.downField("value").as[Foo.Nested.Value]) yield Nested(v0) }
sealed abstract class Value(val value: String) extends _root_.scala.Product with _root_.scala.Serializable { override def toString: String = value.toString }
object Value {
object members {
Expand All @@ -112,7 +112,7 @@ class Issue370 extends AnyFunSuite with Matchers with SwaggerSpecRunner {
}
"""

c1.structure shouldEqual q"case class Foo(value: Option[Foo.Value] = Option(Foo.Value.A), value2: Baz = Baz.X, nested: Option[Foo.Nested] = None)".structure
c1.structure shouldEqual q"case class Foo(value: Foo.Value = Foo.Value.A, value2: Baz = Baz.X, nested: Option[Foo.Nested] = None)".structure
companion.structure shouldEqual cmp.structure
}

Expand Down
Loading