Skip to content

Commit dca8d32

Browse files
author
Andrea Fiore
committed
Merge branch 'extend-definitions'
2 parents 20ead12 + 7a4a4f9 commit dca8d32

File tree

10 files changed

+145
-58
lines changed

10 files changed

+145
-58
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ groups into a single Swagger API description.
286286

287287
``` {.scala}
288288
scala> PathGroup.aggregate(apiInfo, List(PetsRoute, DinosRoute))
289-
res13: cats.data.ValidatedNel[com.timeout.docless.swagger.SchemaError,com.timeout.docless.swagger.APISchema] = Invalid(NonEmptyList(MissingDefinition(ResponseRef(TypeRef(Dino),/dinos/{id},Get))))
289+
res13: cats.data.ValidatedNel[com.timeout.docless.swagger.SchemaError,com.timeout.docless.swagger.APISchema] = Invalid(NonEmptyList(MissingDefinition(RefWithContext(TypeRef(Dino,None),ResponseContext(Get,/dinos/{id})))))
290290
```
291291

292292
The `aggregate` method will also verify that the schema definitions

src/main/scala/com/timeout/docless/encoders/Swagger.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ object Swagger {
7575
}
7676

7777
implicit val schemaRefEnc = Encoder.instance[JsonSchema.Ref] {
78-
case ArrayRef(id) =>
78+
case ArrayRef(id, _) =>
7979
Json.obj(
8080
"type" -> Json.fromString("array"),
8181
"items" -> Json.obj(
8282
"$ref" -> Json.fromString(s"#/definitions/$id")
8383
)
8484
)
85-
case TypeRef(id) =>
85+
case TypeRef(id, _) =>
8686
Json.obj("$ref" -> Json.fromString(s"#/definitions/$id"))
8787
}
8888

src/main/scala/com/timeout/docless/schema/JsonSchema.scala

+40-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.timeout.docless.schema
22

3+
import com.timeout.docless.schema.JsonSchema.NamedDefinition
34
import com.timeout.docless.swagger.Responses.Response
45
import io.circe._
56
import io.circe.syntax._
@@ -19,6 +20,9 @@ trait JsonSchema[A] extends JsonSchema.HasRef {
1920

2021
def relatedDefinitions: Set[JsonSchema.Definition]
2122

23+
def fieldDefinitions: Set[JsonSchema.NamedDefinition] =
24+
relatedDefinitions.collect { case d: NamedDefinition => d }
25+
2226
def jsonObject: JsonObject
2327

2428
def asJson: Json = jsonObject.asJson
@@ -30,8 +34,16 @@ trait JsonSchema[A] extends JsonSchema.HasRef {
3034

3135
def asJsonRef: Json = asObjectRef.asJson
3236

33-
lazy val definition: JsonSchema.Definition =
34-
JsonSchema.Definition(id, asJson)
37+
def namedDefinition(fieldName: String): NamedDefinition =
38+
JsonSchema.NamedDefinition(
39+
id,
40+
fieldName,
41+
relatedDefinitions.map(_.asRef),
42+
asJson
43+
)
44+
45+
lazy val definition: JsonSchema.UnnamedDefinition =
46+
JsonSchema.UnnamedDefinition(id, relatedDefinitions.map(_.asRef), asJson)
3547

3648
def definitions: Set[JsonSchema.Definition] =
3749
relatedDefinitions + definition
@@ -49,26 +61,43 @@ object JsonSchema
4961
with derive.CoprodInstances {
5062
trait HasRef {
5163
def id: String
52-
def asRef: Ref = TypeRef(id)
53-
def asArrayRef: Ref = ArrayRef(id)
64+
def asRef: Ref = TypeRef(id, None)
65+
def asArrayRef: Ref = ArrayRef(id, None)
5466
}
5567

56-
case class Definition(id: String, json: Json) extends HasRef
68+
sealed trait Definition extends HasRef {
69+
def id: String
70+
def json: Json
71+
def relatedRefs: Set[Ref]
72+
}
73+
74+
case class UnnamedDefinition(id: String, relatedRefs: Set[Ref], json: Json)
75+
extends Definition
76+
77+
case class NamedDefinition(id: String,
78+
fieldName: String,
79+
relatedRefs: Set[Ref],
80+
json: Json)
81+
extends Definition {
82+
override def asRef = TypeRef(id, Some(fieldName))
83+
override def asArrayRef = ArrayRef(id, Some(fieldName))
84+
}
5785

5886
sealed trait Ref {
5987
def id: String
88+
def fieldName: Option[String]
6089
}
6190

62-
case class TypeRef(id: String) extends Ref
91+
case class TypeRef(id: String, fieldName: Option[String]) extends Ref
6392
object TypeRef {
64-
def apply(definition: Definition): TypeRef = TypeRef(definition.id)
65-
def apply(schema: JsonSchema[_]): TypeRef = TypeRef(schema.id)
93+
def apply(definition: Definition): TypeRef = TypeRef(definition.id, None)
94+
def apply(schema: JsonSchema[_]): TypeRef = TypeRef(schema.id, None)
6695
}
6796

68-
case class ArrayRef(id: String) extends Ref
97+
case class ArrayRef(id: String, fieldName: Option[String]) extends Ref
6998
object ArrayRef {
70-
def apply(definition: Definition): ArrayRef = ArrayRef(definition.id)
71-
def apply(schema: JsonSchema[_]): ArrayRef = ArrayRef(schema.id)
99+
def apply(definition: Definition): ArrayRef = ArrayRef(definition.id, None)
100+
def apply(schema: JsonSchema[_]): ArrayRef = ArrayRef(schema.id, None)
72101
}
73102

74103
trait PatternProperty[K] {

src/main/scala/com/timeout/docless/schema/derive/HListInstances.scala

+6-4
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@ trait HListInstances {
1818
lazyHSchema: Lazy[JsonSchema[H]],
1919
lazyTSchema: Lazy[JsonSchema[T]]
2020
): JsonSchema[FieldType[K, H] :: T] = instanceAndRelated {
21-
val hSchema = lazyHSchema.value
22-
val tSchema = lazyTSchema.value
21+
val fieldName = witness.value.name
22+
val hSchema = lazyHSchema.value
23+
val tSchema = lazyTSchema.value
2324
val (hValue, related) =
2425
if (hSchema.inline)
2526
hSchema.asJson -> tSchema.relatedDefinitions
2627
else
27-
hSchema.asJsonRef -> (tSchema.relatedDefinitions + hSchema.definition)
28+
hSchema.asJsonRef -> (tSchema.relatedDefinitions + hSchema
29+
.namedDefinition(fieldName))
2830

29-
val hField = witness.value.name -> hValue
31+
val hField = fieldName -> hValue
3032
val tFields = tSchema.jsonObject.toList
3133

3234
JsonObject.fromIterable(hField :: tFields) -> related

src/main/scala/com/timeout/docless/swagger/Path.scala

+3-18
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,16 @@
11
package com.timeout.docless.swagger
22

3-
import com.timeout.docless.schema.JsonSchema.Ref
43
import cats.syntax.foldable._
54
import cats.instances.list._
65
import cats.instances.map._
7-
import com.timeout.docless.swagger.Path._
8-
9-
object Path {
10-
sealed trait RefWithContext {
11-
def path: String
12-
def ref: Ref
13-
}
14-
15-
case class ParamRef(ref: Ref, path: String, param: String)
16-
extends RefWithContext
17-
18-
case class ResponseRef(ref: Ref, path: String, method: Method)
19-
extends RefWithContext
20-
}
216

227
case class Path(id: String,
238
parameters: List[OperationParameter] = Nil,
249
operations: Map[Method, Operation] = Map.empty)
2510
extends ParamSetters[Path] {
2611

27-
private def paramRef(p: OperationParameter): Option[ParamRef] =
28-
p.schema.map(ParamRef(_, id, p.name))
12+
private def paramRef(p: OperationParameter): Option[RefWithContext] =
13+
p.schema.map(RefWithContext.param(_, id, p.name))
2914

3015
def paramRefs: Set[RefWithContext] =
3116
parameters.flatMap(paramRef).toSet ++
@@ -35,7 +20,7 @@ case class Path(id: String,
3520
operations.flatMap {
3621
case (m, op) =>
3722
val resps = op.responses.default :: op.responses.byStatusCode.values.toList
38-
resps.flatMap(_.schema.map(ResponseRef(_, id, m)))
23+
resps.flatMap { _.schema.map(RefWithContext.response(_, m, id)) }
3924
}.toSet
4025

4126
def refs: Set[RefWithContext] = responseRefs ++ paramRefs

src/main/scala/com/timeout/docless/swagger/PathGroup.scala

+14-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import cats.syntax.eq._
66
import cats.syntax.foldable._
77
import cats.syntax.monoid._
88
import cats.{Eq, Monoid}
9-
import com.timeout.docless.schema.JsonSchema.Definition
10-
import com.timeout.docless.swagger.Path.RefWithContext
9+
import com.timeout.docless.schema.JsonSchema.{Definition, TypeRef}
1110

1211
trait PathGroup {
1312
def params: List[OperationParameter] = Nil
@@ -24,16 +23,26 @@ object PathGroup {
2423
info: Info,
2524
groups: List[PathGroup]
2625
): ValidatedNel[SchemaError, APISchema] = {
27-
val g = groups.combineAll
26+
val g = groups.combineAll
27+
val allDefs = g.definitions
28+
val definedIds = allDefs.map(_.id).toSet
2829

2930
def isDefined(ctx: RefWithContext): Boolean =
30-
g.definitions.exists(_.id === ctx.ref.id)
31+
allDefs.exists(_.id === ctx.ref.id)
32+
33+
val missingDefinitions =
34+
allDefs.foldMap { d =>
35+
d.relatedRefs.collect {
36+
case r @ TypeRef(id, _) if !definedIds.exists(_ === id) =>
37+
SchemaError.missingDefinition(RefWithContext.definition(r, d))
38+
}
39+
}
3140

3241
val errors =
3342
g.paths
3443
.foldMap(_.refs.filterNot(isDefined))
3544
.map(SchemaError.missingDefinition)
36-
.toList
45+
.toList ++ missingDefinitions
3746

3847
if (errors.nonEmpty)
3948
Validated.invalid[NonEmptyList[SchemaError], APISchema](
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.timeout.docless.swagger
2+
3+
import com.timeout.docless.schema.JsonSchema.{Definition, Ref}
4+
5+
object RefWithContext {
6+
trait PathContext {
7+
def path: String
8+
}
9+
10+
sealed trait Context
11+
case class DefinitionContext(definition: Definition) extends Context
12+
case class ParamContext(param: String, path: String)
13+
extends Context
14+
with PathContext
15+
case class ResponseContext(method: Method, path: String)
16+
extends Context
17+
with PathContext
18+
19+
def definition(ref: Ref, d: Definition) =
20+
RefWithContext(ref, DefinitionContext(d))
21+
def param(ref: Ref, param: String, path: String) =
22+
RefWithContext(ref, ParamContext(param, path))
23+
def response(ref: Ref, method: Method, path: String) =
24+
RefWithContext(ref, ResponseContext(method, path))
25+
}
26+
27+
import RefWithContext.Context
28+
case class RefWithContext(ref: Ref, context: Context)
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.timeout.docless.swagger
22

33
import cats.Show
4-
import com.timeout.docless.swagger.Path.{ParamRef, RefWithContext, ResponseRef}
4+
import RefWithContext._
55

66
sealed trait SchemaError
77

@@ -11,9 +11,12 @@ object SchemaError {
1111
MissingDefinition(ctx)
1212

1313
implicit val mdShow: Show[SchemaError] = Show.show {
14-
case MissingDefinition(ParamRef(r, path, param)) =>
14+
case MissingDefinition(RefWithContext(r, DefinitionContext(d))) =>
15+
val fieldName = r.fieldName.fold("")(fld => s"(in field name: $fld)")
16+
s"${d.id}: cannot find a definition for '${r.id}' $fieldName"
17+
case MissingDefinition(RefWithContext(r, ParamContext(param, path))) =>
1518
s"$path: cannot find definition '${r.id}' for parameter name '$param'"
16-
case MissingDefinition(ResponseRef(r, path, method)) =>
19+
case MissingDefinition(RefWithContext(r, ResponseContext(method, path))) =>
1720
s"$path: cannot find response definition '${r.id}' for method '${method.entryName}'"
1821
}
1922
}

src/test/scala/com/timeout/docless/schema/JsonSchemaTest.scala

+9-9
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class JsonSchemaTest extends FreeSpec {
111111
""".stripMargin) should ===(Right(schema.asJson))
112112

113113
schema.id should ===(id[Nested])
114-
schema.relatedDefinitions should ===(Set(fs.definition))
114+
schema.relatedDefinitions should ===(Set(fs.namedDefinition("foo")))
115115
}
116116

117117
"with types extending enumeratum.EnumEntry" - {
@@ -175,18 +175,18 @@ class JsonSchemaTest extends FreeSpec {
175175

176176
"provides JSON definitions of the coproduct" in {
177177
implicit val fs: JsonSchema[Foo] = fooSchema
178-
val schema = JsonSchema.deriveFor[ADT]
179-
val ySchema = JsonSchema.deriveFor[A]
180-
val zSchema = JsonSchema.deriveFor[B]
181178

182-
val z1Schema = JsonSchema.deriveFor[C]
179+
val schema = JsonSchema.deriveFor[ADT]
180+
val aSchema = JsonSchema.deriveFor[A]
181+
val bSchema = JsonSchema.deriveFor[B]
182+
val cSchema = JsonSchema.deriveFor[C]
183183

184184
schema.relatedDefinitions should ===(
185185
Set(
186-
ySchema.definition,
187-
zSchema.definition,
188-
z1Schema.definition,
189-
fooSchema.definition
186+
aSchema.definition,
187+
bSchema.definition,
188+
cSchema.definition,
189+
fooSchema.namedDefinition("foo")
190190
)
191191
)
192192
}

src/test/scala/com/timeout/docless/swagger/PathGroupTest.scala

+36-5
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,29 @@ import org.scalatest.{FreeSpec, Inside, Matchers}
44
import cats.data.NonEmptyList
55
import cats.data.Validated
66
import SchemaError._
7+
import com.timeout.docless.schema.JsonSchema
78
import com.timeout.docless.schema.JsonSchema._
89
import com.timeout.docless.swagger.Method._
9-
import com.timeout.docless.swagger.Path._
1010

1111
class PathGroupTest extends FreeSpec with Matchers {
1212
"PathGroup" - {
1313
val petstore = PetstoreSchema()
1414
val pet = PetstoreSchema.Schemas.pet
1515

16-
val paths = Path("example") :: petstore.paths.get.toList
16+
val paths = Path("/example") :: petstore.paths.get.toList
1717
val defs = petstore.definitions.get.toList
1818
val defsNoPet = defs.filterNot(_.id === pet.id)
1919
val params = petstore.parameters.get.toList
2020

2121
val group1 = PathGroup(paths, defs, params)
22-
val group2 = PathGroup(List(Path("extra")), Nil, Nil)
22+
val group2 = PathGroup(List(Path("/extra")), Nil, Nil)
2323
val groupMissingErr = PathGroup(paths, defsNoPet, params)
2424

2525
def err(path: String, m: Method, f: Definition => Ref): SchemaError =
26-
missingDefinition(ResponseRef(f(pet.definition), path, m))
26+
missingDefinition(RefWithContext.response(f(pet.definition), m, path))
2727

2828
"aggregate" - {
29-
"when some definitions are missing" - {
29+
"when some top level definitions are missing" - {
3030
"returns the missing refs" in {
3131
PathGroup.aggregate(petstore.info, List(groupMissingErr)) should ===(
3232
Validated.invalid[NonEmptyList[SchemaError], APISchema](
@@ -40,6 +40,37 @@ class PathGroupTest extends FreeSpec with Matchers {
4040
)
4141
}
4242
}
43+
"when some nested definitions are missing" - {
44+
val info = Info("example")
45+
case class Nested(name: String)
46+
case class TopLevel(nested: Nested)
47+
48+
val schema = JsonSchema.deriveFor[TopLevel]
49+
val nested = schema.relatedDefinitions.head
50+
51+
val paths = List(
52+
"/example".Post(
53+
Operation('_, "...")
54+
.withParams(BodyParameter(schema = Some(schema.asRef)))
55+
)
56+
)
57+
58+
val withNested = PathGroup(paths, schema.definitions.toList, Nil)
59+
val withoutNested = PathGroup(paths, List(schema.definition), Nil)
60+
61+
"returns the missing refs" in {
62+
PathGroup.aggregate(info, List(withNested)).isValid shouldBe true
63+
PathGroup.aggregate(info, List(withoutNested)) should ===(
64+
Validated.invalid[NonEmptyList[SchemaError], APISchema](
65+
NonEmptyList.of(
66+
MissingDefinition(
67+
RefWithContext.definition(nested.asRef, schema.definition)
68+
)
69+
)
70+
)
71+
)
72+
}
73+
}
4374
"when no definition is missing" - {
4475
"returns a valid api schema" in new Inside {
4576
inside(PathGroup.aggregate(petstore.info, List(group1, group2))) {

0 commit comments

Comments
 (0)