Skip to content

Commit ebeffa5

Browse files
author
Andrea Fiore
committed
Track related refs in definition
1 parent 20ead12 commit ebeffa5

File tree

7 files changed

+90
-34
lines changed

7 files changed

+90
-34
lines changed

Diff for: 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),ResponseContext(Get,/dinos/{id})))))
290290
```
291291

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

Diff for: src/main/scala/com/timeout/docless/schema/JsonSchema.scala

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ trait JsonSchema[A] extends JsonSchema.HasRef {
3131
def asJsonRef: Json = asObjectRef.asJson
3232

3333
lazy val definition: JsonSchema.Definition =
34-
JsonSchema.Definition(id, asJson)
34+
JsonSchema.Definition(id, relatedDefinitions.map(_.asRef), asJson)
3535

3636
def definitions: Set[JsonSchema.Definition] =
3737
relatedDefinitions + definition
@@ -53,7 +53,8 @@ object JsonSchema
5353
def asArrayRef: Ref = ArrayRef(id)
5454
}
5555

56-
case class Definition(id: String, json: Json) extends HasRef
56+
case class Definition(id: String, relatedRefs: Set[Ref], json: Json)
57+
extends HasRef
5758

5859
sealed trait Ref {
5960
def id: String

Diff for: 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

Diff for: 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,11 @@ 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+
s"${d.id}: cannot find a field definition for: '${r.id}'"
16+
case MissingDefinition(RefWithContext(r, ParamContext(param, path))) =>
1517
s"$path: cannot find definition '${r.id}' for parameter name '$param'"
16-
case MissingDefinition(ResponseRef(r, path, method)) =>
18+
case MissingDefinition(RefWithContext(r, ResponseContext(method, path))) =>
1719
s"$path: cannot find response definition '${r.id}' for method '${method.entryName}'"
1820
}
1921
}

Diff for: 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)