Skip to content

Commit

Permalink
Option to generate examples for OpenAPI (zio#3196)
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil committed Jan 17, 2025
1 parent 6d9dce1 commit 14b657d
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2398,6 +2398,100 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
|}""".stripMargin
assertTrue(json == toJsonAst(expected))
},
test("generated example") {
val endpoint = Endpoint(GET / "static").in[NestedProduct]
val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", genExamples = true, endpoint)
val json = toJsonAst(generated)
println(json.toJsonPretty)
val expected = """{
| "openapi" : "3.1.0",
| "info" : {
| "title" : "Simple Endpoint",
| "version" : "1.0"
| },
| "paths" : {
| "/static" : {
| "get" : {
| "requestBody" : {
| "content" : {
| "application/json" : {
| "schema" : {
| "$ref" : "#/components/schemas/NestedProduct"
| },
| "examples" : {
| "generated" : {
| "value" : {
| "imageMetadata" : {
| "name" : "",
| "size" : 0
| },
| "withOptionalField" : {
| "name" : "",
| "age" : 0
| }
| }
| }
| }
| }
| },
| "required" : true
| }
| }
| }
| },
| "components" : {
| "schemas" : {
| "ImageMetadata" : {
| "type" : "object",
| "properties" : {
| "name" : {
| "type" : "string"
| },
| "size" : {
| "type" : "integer",
| "format" : "int32"
| }
| },
| "required" : [
| "name",
| "size"
| ]
| },
| "NestedProduct" : {
| "type" : "object",
| "properties" : {
| "imageMetadata" : {
| "$ref" : "#/components/schemas/ImageMetadata"
| },
| "withOptionalField" : {
| "$ref" : "#/components/schemas/WithOptionalField"
| }
| },
| "required" : [
| "imageMetadata",
| "withOptionalField"
| ]
| },
| "WithOptionalField" : {
| "type" : "object",
| "properties" : {
| "name" : {
| "type" : "string"
| },
| "age" : {
| "type" : "integer",
| "format" : "int32"
| }
| },
| "required" : [
| "name"
| ]
| }
| }
| }
|}""".stripMargin
assertTrue(json == toJsonAst(expected))
},
test("enum") {
val endpoint = Endpoint(GET / "static").in[SimpleEnum]
val generated = OpenAPIGen.fromEndpoints("Simple Endpoint", "1.0", endpoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ object OpenAPIGen {
case (examples, _) => examples
}

def examples(schema: Schema[_]): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
examples.map { case (k, v) =>
def examples(schema: Schema[_], generate: Boolean): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
(if (generate && examples.isEmpty) generateExamples(schema) else examples).map { case (k, v) =>
k -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(toJsonAst(schema, v)))
}

Expand Down Expand Up @@ -132,13 +132,13 @@ object OpenAPIGen {
status ++ that.status,
)

def contentExamples: Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
def contentExamples(genExamples: Boolean): Map[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] =
content.flatMap {
case mc @ MetaCodec(HttpCodec.Content(codec, _, _), _) if codec.lookup(MediaType.application.json).isDefined =>
mc.examples(codec.lookup(MediaType.application.json).get._2.schema)
mc.examples(codec.lookup(MediaType.application.json).get._2.schema, genExamples)
case mc @ MetaCodec(HttpCodec.ContentStream(codec, _, _), _)
if codec.lookup(MediaType.application.json).isDefined =>
mc.examples(codec.lookup(MediaType.application.json).get._2.schema)
mc.examples(codec.lookup(MediaType.application.json).get._2.schema, genExamples)
case _ =>
Map.empty[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]]
}.toMap
Expand Down Expand Up @@ -224,6 +224,57 @@ object OpenAPIGen {
}
}

private def generateExamples(schema: Schema[_], name: String = "generated"): Map[String, Any] =
schema match {
case collection: Schema.Collection[_, _] =>
collection match {
case Schema.Sequence(elementSchema, fromChunk, _, _, _) =>
elementSchema.defaultValue.map(v => Map(name -> fromChunk(Chunk.single(v)))).getOrElse(Map.empty)
case map @ Schema.Map(_, _, _) =>
val example = for {
k <- map.keySchema.defaultValue
v <- map.valueSchema.defaultValue
} yield map.fromChunk(Chunk.single(k -> v))
example.map(v => Map(name -> v)).getOrElse(Map.empty)
case map @ Schema.NonEmptyMap(_, _, _) =>
val example = for {
k <- map.keySchema.defaultValue
v <- map.valueSchema.defaultValue
} yield map.fromChunk(Chunk.single(k -> v))
example.map(v => Map(name -> v)).getOrElse(Map.empty)
case Schema.NonEmptySequence(elementSchema, fromChunkOption, _, _, _) =>
elementSchema.defaultValue.map(v => Map(name -> fromChunkOption(Chunk.single(v)))).getOrElse(Map.empty)
case set @ Schema.Set(elementSchema, _) =>
elementSchema.defaultValue
.map(v => set.fromChunk(Chunk.single(v)))
.map(v => Map(name -> v))
.getOrElse(Map.empty)

}
case Transform(schema, _, _, _, _) =>
generateExamples(schema, name)
case Schema.Optional(schema, _) =>
generateExamples(schema, name).map { case (k, v) => k -> Some(v) }
case Schema.Fail(_, _) =>
Map.empty
case Schema.Either(left, right, _) =>
generateExamples(left, "generatedLeft") ++ generateExamples(right, "generatedRight")
case Schema.Fallback(left, right, _, _) =>
generateExamples(left, "generatedLeft") ++ generateExamples(right, "generatedRight")
case Schema.Lazy(schema0) =>
generateExamples(schema0())
case Schema.Dynamic(_) =>
Map.empty
case s @ Schema.Primitive(_, _) =>
s.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty)
case enum: Schema.Enum[_] =>
enum.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty)
case record: Record[_] =>
record.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty)
case t: Schema.Tuple2[_, _] =>
t.defaultValue.map(v => Map(name -> v)).getOrElse(Map.empty)
}

def method(in: Chunk[MetaCodec[SimpleCodec[Method, _]]]): Method = {
if (in.size > 1) throw new Exception("Multiple methods not supported")
in.collectFirst { case MetaCodec(SimpleCodec.Specified(method: Method), _) => method }
Expand Down Expand Up @@ -483,13 +534,27 @@ object OpenAPIGen {
endpoints: Endpoint[_, _, _, _, _]*,
): OpenAPI = fromEndpoints(endpoint1 +: endpoints)

def fromEndpoints(
genExamples: Boolean,
endpoint1: Endpoint[_, _, _, _, _],
endpoints: Endpoint[_, _, _, _, _]*,
): OpenAPI = fromEndpoints(genExamples, endpoint1 +: endpoints)

def fromEndpoints(
title: String,
version: String,
endpoint1: Endpoint[_, _, _, _, _],
endpoints: Endpoint[_, _, _, _, _]*,
): OpenAPI = fromEndpoints(title, version, endpoint1 +: endpoints)

def fromEndpoints(
title: String,
version: String,
genExamples: Boolean,
endpoint1: Endpoint[_, _, _, _, _],
endpoints: Endpoint[_, _, _, _, _]*,
): OpenAPI = fromEndpoints(title, version, genExamples, endpoint1 +: endpoints)

def fromEndpoints(
title: String,
version: String,
Expand All @@ -498,31 +563,78 @@ object OpenAPIGen {
endpoints: Endpoint[_, _, _, _, _]*,
): OpenAPI = fromEndpoints(title, version, referenceType, endpoint1 +: endpoints)

def fromEndpoints(
title: String,
version: String,
referenceType: SchemaStyle,
genExamples: Boolean,
endpoint1: Endpoint[_, _, _, _, _],
endpoints: Endpoint[_, _, _, _, _]*,
): OpenAPI = fromEndpoints(title, version, referenceType, genExamples, endpoint1 +: endpoints)

def fromEndpoints(
referenceType: SchemaStyle,
endpoints: Iterable[Endpoint[_, _, _, _, _]],
): OpenAPI = if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, referenceType)).reduce(_ ++ _)

def fromEndpoints(
referenceType: SchemaStyle,
genExamples: Boolean,
endpoints: Iterable[Endpoint[_, _, _, _, _]],
): OpenAPI =
if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, referenceType, genExamples)).reduce(_ ++ _)

def fromEndpoints(
endpoints: Iterable[Endpoint[_, _, _, _, _]],
): OpenAPI = if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, SchemaStyle.Compact)).reduce(_ ++ _)

def fromEndpoints(
genExamples: Boolean,
endpoints: Iterable[Endpoint[_, _, _, _, _]],
): OpenAPI =
if (endpoints.isEmpty) OpenAPI.empty else endpoints.map(gen(_, SchemaStyle.Compact, genExamples)).reduce(_ ++ _)

def fromEndpoints(
title: String,
version: String,
endpoints: Iterable[Endpoint[_, _, _, _, _]],
): OpenAPI = fromEndpoints(endpoints).title(title).version(version)

def fromEndpoints(
title: String,
version: String,
genExamples: Boolean,
endpoints: Iterable[Endpoint[_, _, _, _, _]],
): OpenAPI = fromEndpoints(genExamples, endpoints).title(title).version(version)

def fromEndpoints(
title: String,
version: String,
referenceType: SchemaStyle,
endpoints: Iterable[Endpoint[_, _, _, _, _]],
): OpenAPI = fromEndpoints(referenceType, endpoints).title(title).version(version)

def fromEndpoints(
title: String,
version: String,
referenceType: SchemaStyle,
genExamples: Boolean,
endpoints: Iterable[Endpoint[_, _, _, _, _]],
): OpenAPI = fromEndpoints(referenceType, genExamples, endpoints).title(title).version(version)

def gen(
endpoint: Endpoint[_, _, _, _, _],
): OpenAPI = gen(endpoint, SchemaStyle.Compact)

def gen(
endpoint: Endpoint[_, _, _, _, _],
referenceType: SchemaStyle,
): OpenAPI = gen(endpoint, referenceType, genExamples = false)

def gen(
endpoint: Endpoint[_, _, _, _, _],
referenceType: SchemaStyle = SchemaStyle.Compact,
genExamples: Boolean,
): OpenAPI = {
val inAtoms = AtomizedMetaCodecs.flatten(endpoint.input)
val outs: Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]] =
Expand Down Expand Up @@ -610,7 +722,7 @@ object OpenAPIGen {
val mediaTypeResponses = mediaTypes.map { case (mediaType, (schema, atomized)) =>
mediaType.fullType -> OpenAPI.MediaType(
schema = OpenAPI.ReferenceOr.Or(schema),
examples = atomized.contentExamples,
examples = atomized.contentExamples(genExamples),
encoding = Map.empty,
)
}
Expand All @@ -627,7 +739,7 @@ object OpenAPIGen {
})

def responses: OpenAPI.Responses =
responsesForAlternatives(outs)
responsesForAlternatives(outs, genExamples)

def parameters: Set[OpenAPI.ReferenceOr[OpenAPI.Parameter]] =
queryParams ++ pathParams ++ headerParams
Expand Down Expand Up @@ -992,13 +1104,14 @@ object OpenAPIGen {

private def responsesForAlternatives(
codecs: Map[OpenAPI.StatusOrDefault, Map[MediaType, (JsonSchema, AtomizedMetaCodecs)]],
genExamples: Boolean,
): Map[OpenAPI.StatusOrDefault, OpenAPI.ReferenceOr[OpenAPI.Response]] =
codecs.map { case (status, mediaTypes) =>
val combinedAtomizedCodecs = mediaTypes.map { case (_, (_, atomized)) => atomized }.reduce(_ ++ _)
val mediaTypeResponses = mediaTypes.map { case (mediaType, (schema, atomized)) =>
mediaType.fullType -> OpenAPI.MediaType(
schema = OpenAPI.ReferenceOr.Or(schema),
examples = atomized.contentExamples,
examples = atomized.contentExamples(genExamples),
encoding = Map.empty,
)
}
Expand Down

0 comments on commit 14b657d

Please sign in to comment.