From c4c8eccf23f5bcc12f7de0e9c0733734b6c1b1a7 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Sat, 14 Oct 2023 20:41:52 +0200 Subject: [PATCH] Fix exhaustive matching --- .../zio/http/endpoint/cli/HttpOptions.scala | 19 +++-- .../zio/http/endpoint/cli/CommandGen.scala | 21 +++-- .../main/scala/zio/http/codec/PathCodec.scala | 25 +++--- .../http/endpoint/openapi/JsonSchema.scala | 35 ++++++-- .../zio/http/endpoint/openapi/OpenAPI.scala | 82 +++++++++---------- .../http/endpoint/openapi/OpenAPIGen.scala | 74 +++++++++-------- 6 files changed, 150 insertions(+), 106 deletions(-) diff --git a/zio-http-cli/src/main/scala/zio/http/endpoint/cli/HttpOptions.scala b/zio-http-cli/src/main/scala/zio/http/endpoint/cli/HttpOptions.scala index fad9f366fc..ce3d0d4655 100644 --- a/zio-http-cli/src/main/scala/zio/http/endpoint/cli/HttpOptions.scala +++ b/zio-http-cli/src/main/scala/zio/http/endpoint/cli/HttpOptions.scala @@ -1,5 +1,6 @@ package zio.http.endpoint.cli +import scala.annotation.tailrec import scala.language.implicitConversions import scala.util.Try @@ -310,9 +311,10 @@ private[cli] object HttpOptions { } private[cli] def optionsFromSegment(segment: SegmentCodec[_]): Options[String] = { + @tailrec def fromSegment[A](segment: SegmentCodec[A]): Options[String] = segment match { - case SegmentCodec.UUID(name) => + case SegmentCodec.UUID(name) => Options .text(name) .mapOrFail(str => @@ -324,13 +326,14 @@ private[cli] object HttpOptions { }, ) .map(_.toString) - case SegmentCodec.Text(name) => Options.text(name) - case SegmentCodec.IntSeg(name) => Options.integer(name).map(_.toInt).map(_.toString) - case SegmentCodec.LongSeg(name) => Options.integer(name).map(_.toInt).map(_.toString) - case SegmentCodec.BoolSeg(name) => Options.boolean(name).map(_.toString) - case SegmentCodec.Literal(value) => Options.Empty.map(_ => value) - case SegmentCodec.Trailing => Options.none.map(_.toString) - case SegmentCodec.Empty => Options.none.map(_.toString) + case SegmentCodec.Text(name) => Options.text(name) + case SegmentCodec.IntSeg(name) => Options.integer(name).map(_.toInt).map(_.toString) + case SegmentCodec.LongSeg(name) => Options.integer(name).map(_.toInt).map(_.toString) + case SegmentCodec.BoolSeg(name) => Options.boolean(name).map(_.toString) + case SegmentCodec.Literal(value) => Options.Empty.map(_ => value) + case SegmentCodec.Trailing => Options.none.map(_.toString) + case SegmentCodec.Empty => Options.none.map(_.toString) + case SegmentCodec.Annotated(codec, _) => fromSegment(codec) } fromSegment(segment) diff --git a/zio-http-cli/src/test/scala/zio/http/endpoint/cli/CommandGen.scala b/zio-http-cli/src/test/scala/zio/http/endpoint/cli/CommandGen.scala index 8486a937ea..53334db6c0 100644 --- a/zio-http-cli/src/test/scala/zio/http/endpoint/cli/CommandGen.scala +++ b/zio-http-cli/src/test/scala/zio/http/endpoint/cli/CommandGen.scala @@ -1,5 +1,7 @@ package zio.http.endpoint.cli +import scala.annotation.tailrec + import zio.cli._ import zio.test._ @@ -18,17 +20,20 @@ import zio.http.endpoint.cli.EndpointGen._ object CommandGen { def getSegment(segment: SegmentCodec[_]): (String, String) = { + @tailrec def fromSegment[A](segment: SegmentCodec[A]): (String, String) = segment match { - case SegmentCodec.UUID(name) => (name, "text") - case SegmentCodec.Text(name) => (name, "text") - case SegmentCodec.IntSeg(name) => (name, "integer") - case SegmentCodec.LongSeg(name) => (name, "integer") - case SegmentCodec.BoolSeg(name) => (name, "boolean") - case SegmentCodec.Literal(_) => ("", "") - case SegmentCodec.Trailing => ("", "") - case SegmentCodec.Empty => ("", "") + case SegmentCodec.UUID(name) => (name, "text") + case SegmentCodec.Text(name) => (name, "text") + case SegmentCodec.IntSeg(name) => (name, "integer") + case SegmentCodec.LongSeg(name) => (name, "integer") + case SegmentCodec.BoolSeg(name) => (name, "boolean") + case SegmentCodec.Literal(_) => ("", "") + case SegmentCodec.Trailing => ("", "") + case SegmentCodec.Empty => ("", "") + case SegmentCodec.Annotated(codec, _) => fromSegment(codec) } + fromSegment(segment) } diff --git a/zio-http/src/main/scala/zio/http/codec/PathCodec.scala b/zio-http/src/main/scala/zio/http/codec/PathCodec.scala index 7b19cf90dc..85e74f8a15 100644 --- a/zio-http/src/main/scala/zio/http/codec/PathCodec.scala +++ b/zio-http/src/main/scala/zio/http/codec/PathCodec.scala @@ -16,12 +16,10 @@ package zio.http.codec -import scala.annotation.tailrec import scala.collection.immutable.ListMap import scala.language.implicitConversions -import zio.stacktracer.TracingImplicits.disableAutoTrace -import zio.{Chunk, NonEmptyChunk} +import zio._ import zio.http.Path @@ -266,14 +264,15 @@ sealed trait PathCodec[A] { self => pattern match { case PathCodec.Segment(segment) => Chunk(segment.asInstanceOf[SegmentCodec[_]] match { - case SegmentCodec.Empty => Opt.Unit - case SegmentCodec.Literal(value) => Opt.Match(value) - case SegmentCodec.IntSeg(_) => Opt.IntOpt - case SegmentCodec.LongSeg(_) => Opt.LongOpt - case SegmentCodec.Text(_) => Opt.StringOpt - case SegmentCodec.UUID(_) => Opt.UUIDOpt - case SegmentCodec.BoolSeg(_) => Opt.BoolOpt - case SegmentCodec.Trailing => Opt.TrailingOpt + case SegmentCodec.Empty => Opt.Unit + case SegmentCodec.Literal(value) => Opt.Match(value) + case SegmentCodec.IntSeg(_) => Opt.IntOpt + case SegmentCodec.LongSeg(_) => Opt.LongOpt + case SegmentCodec.Text(_) => Opt.StringOpt + case SegmentCodec.UUID(_) => Opt.UUIDOpt + case SegmentCodec.BoolSeg(_) => Opt.BoolOpt + case SegmentCodec.Trailing => Opt.TrailingOpt + case SegmentCodec.Annotated(codec, _) => loop(PathCodec.Segment(codec)).head }) case Concat(left, right, combiner, _) => @@ -305,7 +304,7 @@ sealed trait PathCodec[A] { self => loop(self) } - def renderIgnoreTrailing: String = { + private[zio] def renderIgnoreTrailing: String = { def loop(path: PathCodec[_]): String = path match { case PathCodec.Concat(left, right, _, _) => loop(left) + loop(right) @@ -313,6 +312,8 @@ sealed trait PathCodec[A] { self => case PathCodec.Segment(SegmentCodec.Trailing) => "" case PathCodec.Segment(segment) => segment.render + + case PathCodec.TransformOrFail(api, _, _) => loop(api) } loop(self) diff --git a/zio-http/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala b/zio-http/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala index 3656304830..64f409bd09 100644 --- a/zio-http/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala +++ b/zio-http/src/main/scala/zio/http/endpoint/openapi/JsonSchema.scala @@ -292,22 +292,47 @@ object JsonSchema { case TextCodec.Constant(string) => JsonSchema.Enum(Chunk(EnumValue.Str(string))) case TextCodec.StringCodec => JsonSchema.String case TextCodec.IntCodec => JsonSchema.Integer(JsonSchema.IntegerFormat.Int32) + case TextCodec.LongCodec => JsonSchema.Integer(JsonSchema.IntegerFormat.Int64) case TextCodec.BooleanCodec => JsonSchema.Boolean case TextCodec.UUIDCodec => JsonSchema.String } def fromSegmentCodec(codec: SegmentCodec[_]): JsonSchema = codec match { - case SegmentCodec.BoolSeg(_) => JsonSchema.Boolean - case SegmentCodec.IntSeg(_) => JsonSchema.Integer(JsonSchema.IntegerFormat.Int32) - case SegmentCodec.LongSeg(_) => JsonSchema.Integer(JsonSchema.IntegerFormat.Int64) - case SegmentCodec.Text(_) => JsonSchema.String - case SegmentCodec.UUID(_) => JsonSchema.String + case SegmentCodec.BoolSeg(_) => JsonSchema.Boolean + case SegmentCodec.IntSeg(_) => JsonSchema.Integer(JsonSchema.IntegerFormat.Int32) + case SegmentCodec.LongSeg(_) => JsonSchema.Integer(JsonSchema.IntegerFormat.Int64) + case SegmentCodec.Text(_) => JsonSchema.String + case SegmentCodec.UUID(_) => JsonSchema.String + case SegmentCodec.Annotated(codec, annotations) => + fromSegmentCodec(codec).description(segmentDoc(annotations)).examples(segmentExamples(codec, annotations)) case SegmentCodec.Literal(_) => throw new IllegalArgumentException("Literal segment is not supported.") case SegmentCodec.Empty => throw new IllegalArgumentException("Empty segment is not supported.") case SegmentCodec.Trailing => throw new IllegalArgumentException("Trailing segment is not supported.") } + private def segmentDoc(annotations: Chunk[SegmentCodec.MetaData[_]]) = + annotations.collect { case SegmentCodec.MetaData.Documented(doc) => doc }.reduceOption(_ + _).map(_.toCommonMark) + + private def segmentExamples(codec: SegmentCodec[_], annotations: Chunk[SegmentCodec.MetaData[_]]) = + Chunk.fromIterable( + annotations.collect { case SegmentCodec.MetaData.Examples(example) => example.values }.flatten.map { value => + codec match { + case SegmentCodec.Empty => throw new IllegalArgumentException("Empty segment is not supported.") + case SegmentCodec.Literal(_) => throw new IllegalArgumentException("Literal segment is not supported.") + case SegmentCodec.BoolSeg(_) => Json.Bool(value.asInstanceOf[Boolean]) + case SegmentCodec.IntSeg(_) => Json.Num(value.asInstanceOf[Int]) + case SegmentCodec.LongSeg(_) => Json.Num(value.asInstanceOf[Long]) + case SegmentCodec.Text(_) => Json.Str(value.asInstanceOf[String]) + case SegmentCodec.UUID(_) => Json.Str(value.asInstanceOf[java.util.UUID].toString) + case SegmentCodec.Trailing => + throw new IllegalArgumentException("Trailing segment is not supported.") + case SegmentCodec.Annotated(_, _) => + throw new IllegalStateException("Annotated SegmentCodec should never be nested.") + } + }, + ) + def fromZSchema(schema: Schema[_], refType: SchemaStyle = SchemaStyle.Inline): JsonSchema = schema match { case enum0: Schema.Enum[_] if refType != SchemaStyle.Inline && nominal(enum0).isDefined => diff --git a/zio-http/src/main/scala/zio/http/endpoint/openapi/OpenAPI.scala b/zio-http/src/main/scala/zio/http/endpoint/openapi/OpenAPI.scala index d07886ab94..23904c5cff 100644 --- a/zio-http/src/main/scala/zio/http/endpoint/openapi/OpenAPI.scala +++ b/zio-http/src/main/scala/zio/http/endpoint/openapi/OpenAPI.scala @@ -445,21 +445,21 @@ object OpenAPI { * path. * @param description * A description, intended to apply to all operations in this path. - * @param getOp + * @param get * A definition of a GET operation on this path. - * @param putOp + * @param put * A definition of a PUT operation on this path. - * @param postOp + * @param post * A definition of a POST operation on this path. - * @param deleteOp + * @param delete * A definition of a DELETE operation on this path. - * @param optionsOp + * @param options * A definition of a OPTIONS operation on this path. - * @param headOp + * @param head * A definition of a HEAD operation on this path. - * @param patchOp + * @param patch * A definition of a PATCH operation on this path. - * @param traceOp + * @param trace * A definition of a TRACE operation on this path. * @param servers * An alternative server List to service all operations in this path. @@ -474,35 +474,35 @@ object OpenAPI { @fieldName("$ref") ref: Option[String], summary: Option[String], description: Option[Doc], - getOp: Option[Operation], - putOp: Option[Operation], - postOp: Option[Operation], - deleteOp: Option[Operation], - optionsOp: Option[Operation], - headOp: Option[Operation], - patchOp: Option[Operation], - traceOp: Option[Operation], + get: Option[Operation], + put: Option[Operation], + post: Option[Operation], + delete: Option[Operation], + options: Option[Operation], + head: Option[Operation], + patch: Option[Operation], + trace: Option[Operation], servers: List[Server] = List.empty, parameters: Set[ReferenceOr[Parameter]] = Set.empty, ) { - def get(operation: Operation): PathItem = copy(getOp = Some(operation)) - def put(operation: Operation): PathItem = copy(putOp = Some(operation)) - def post(operation: Operation): PathItem = copy(postOp = Some(operation)) - def delete(operation: Operation): PathItem = copy(deleteOp = Some(operation)) - def options(operation: Operation): PathItem = copy(optionsOp = Some(operation)) - def head(operation: Operation): PathItem = copy(headOp = Some(operation)) - def patch(operation: Operation): PathItem = copy(patchOp = Some(operation)) - def trace(operation: Operation): PathItem = copy(traceOp = Some(operation)) - def any(operation: Operation): PathItem = + def addGet(operation: Operation): PathItem = copy(get = Some(operation)) + def addPut(operation: Operation): PathItem = copy(put = Some(operation)) + def addPost(operation: Operation): PathItem = copy(post = Some(operation)) + def addDelete(operation: Operation): PathItem = copy(delete = Some(operation)) + def addOptions(operation: Operation): PathItem = copy(options = Some(operation)) + def addHead(operation: Operation): PathItem = copy(head = Some(operation)) + def addPatch(operation: Operation): PathItem = copy(patch = Some(operation)) + def addTrace(operation: Operation): PathItem = copy(trace = Some(operation)) + def any(operation: Operation): PathItem = copy( - getOp = Some(operation), - putOp = Some(operation), - postOp = Some(operation), - deleteOp = Some(operation), - optionsOp = Some(operation), - headOp = Some(operation), - patchOp = Some(operation), - traceOp = Some(operation), + get = Some(operation), + put = Some(operation), + post = Some(operation), + delete = Some(operation), + options = Some(operation), + head = Some(operation), + patch = Some(operation), + trace = Some(operation), ) } @@ -514,14 +514,14 @@ object OpenAPI { ref = None, summary = None, description = None, - getOp = None, - putOp = None, - postOp = None, - deleteOp = None, - optionsOp = None, - headOp = None, - patchOp = None, - traceOp = None, + get = None, + put = None, + post = None, + delete = None, + options = None, + head = None, + patch = None, + trace = None, servers = List.empty, parameters = Set.empty, ) diff --git a/zio-http/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala b/zio-http/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala index 0a5a34b3fd..dfcbf512e7 100644 --- a/zio-http/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala +++ b/zio-http/src/main/scala/zio/http/endpoint/openapi/OpenAPIGen.scala @@ -2,6 +2,7 @@ package zio.http.endpoint.openapi import java.util.UUID +import scala.annotation.tailrec import scala.collection.{immutable, mutable} import zio.Chunk @@ -100,20 +101,23 @@ object OpenAPIGen { status: Chunk[MetaCodec[HttpCodec.Status[_]]], ) { def append(metaCodec: MetaCodec[_]): AtomizedMetaCodecs = metaCodec match { - case MetaCodec(codec: HttpCodec.Method[_], annotations) => + case MetaCodec(codec: HttpCodec.Method[_], annotations) => copy(method = (method :+ MetaCodec(codec.codec, annotations)).asInstanceOf[Chunk[MetaCodec[SimpleCodec[Method, _]]]], ) - case MetaCodec(_: SegmentCodec[_], _) => + case MetaCodec(_: SegmentCodec[_], _) => copy(path = path :+ metaCodec.asInstanceOf[MetaCodec[SegmentCodec[_]]]) - case MetaCodec(_: HttpCodec.Query[_], _) => + case MetaCodec(_: HttpCodec.Query[_], _) => copy(query = query :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Query[_]]]) - case MetaCodec(_: HttpCodec.Header[_], _) => + case MetaCodec(_: HttpCodec.Header[_], _) => copy(header = header :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Header[_]]]) - case MetaCodec(_: HttpCodec.Status[_], _) => + case MetaCodec(_: HttpCodec.Status[_], _) => copy(status = status :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Status[_]]]) - case MetaCodec(_: HttpCodec.Atom[HttpCodecType.Content, _], _) => + case MetaCodec(_: HttpCodec.Content[_], _) => copy(content = content :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Atom[HttpCodecType.Content, _]]]) + case MetaCodec(_: HttpCodec.ContentStream[_], _) => + copy(content = content :+ metaCodec.asInstanceOf[MetaCodec[HttpCodec.Atom[HttpCodecType.Content, _]]]) + case _ => this } def ++(that: AtomizedMetaCodecs): AtomizedMetaCodecs = @@ -136,6 +140,8 @@ object OpenAPIGen { mc.examples.map { case (name, value) => name -> OpenAPI.ReferenceOr.Or(OpenAPI.Example(toJsonAst(schema, value))) } + case _ => + Map.empty[String, OpenAPI.ReferenceOr.Or[OpenAPI.Example]] }.toMap // in case of alternatives, @@ -292,7 +298,7 @@ object OpenAPIGen { .nullable(optional(metadata)) .description(description(metadata)) .annotate(annotations) - + case _ => throw new IllegalArgumentException("Multipart content without name.") } } @@ -313,6 +319,8 @@ object OpenAPIGen { .deprecated(deprecated(metadata)) .nullable(optional(metadata)) .description(description(metadata)) + case _ => + throw new IllegalStateException("A non multipart combine, should lead to at least one null schema.") } case HttpCodec.Fallback(_, _, _) => throw new IllegalArgumentException("Fallback not supported at this point") } @@ -337,21 +345,21 @@ object OpenAPIGen { def status[R, A](codec: HttpCodec[R, A]): Option[Status] = codec match { - case HttpCodec.Status(simpleCodec, _) if simpleCodec.isInstanceOf[SimpleCodec.Specified[Status]] => + case HttpCodec.Status(simpleCodec, _) if simpleCodec.isInstanceOf[SimpleCodec.Specified[_]] => Some(simpleCodec.asInstanceOf[SimpleCodec.Specified[Status]].value) - case HttpCodec.Annotated(codec, _) => + case HttpCodec.Annotated(codec, _) => status(codec) - case HttpCodec.TransformOrFail(api, _, _) => + case HttpCodec.TransformOrFail(api, _, _) => status(api) - case HttpCodec.Empty => + case HttpCodec.Empty => None - case HttpCodec.Halt => + case HttpCodec.Halt => None - case HttpCodec.Combine(left, right, _) => + case HttpCodec.Combine(left, right, _) => status(left).orElse(status(right)) - case HttpCodec.Fallback(left, right, _) => + case HttpCodec.Fallback(left, right, _) => status(left).orElse(status(right)) - case _ => + case _ => None } @@ -444,14 +452,14 @@ object OpenAPIGen { val pathItem = OpenAPI.PathItem.empty .copy(description = Some(endpoint.doc + endpoint.input.doc.getOrElse(Doc.empty)).filter(!_.isEmpty)) val pathItemWithOp = method0 match { - case Method.OPTIONS => pathItem.options(operation(endpoint)) - case Method.GET => pathItem.get(operation(endpoint)) - case Method.HEAD => pathItem.head(operation(endpoint)) - case Method.POST => pathItem.post(operation(endpoint)) - case Method.PUT => pathItem.put(operation(endpoint)) - case Method.PATCH => pathItem.patch(operation(endpoint)) - case Method.DELETE => pathItem.delete(operation(endpoint)) - case Method.TRACE => pathItem.trace(operation(endpoint)) + case Method.OPTIONS => pathItem.addOptions(operation(endpoint)) + case Method.GET => pathItem.addGet(operation(endpoint)) + case Method.HEAD => pathItem.addHead(operation(endpoint)) + case Method.POST => pathItem.addPost(operation(endpoint)) + case Method.PUT => pathItem.addPut(operation(endpoint)) + case Method.PATCH => pathItem.addPatch(operation(endpoint)) + case Method.DELETE => pathItem.addDelete(operation(endpoint)) + case Method.TRACE => pathItem.addTrace(operation(endpoint)) case Method.ANY => pathItem.any(operation(endpoint)) case method => throw new IllegalArgumentException(s"OpenAPI does not support method $method") } @@ -634,16 +642,18 @@ object OpenAPIGen { callbacks = Map.empty, ) - def segmentToJson(codec: SegmentCodec[_], value: Any) = { + @tailrec + def segmentToJson(codec: SegmentCodec[_], value: Any): Json = { codec match { - case SegmentCodec.Empty => throw new Exception("Empty segment not allowed") - case SegmentCodec.Literal(_) => throw new Exception("Literal segment not allowed") - case SegmentCodec.BoolSeg(_) => Json.Bool(value.asInstanceOf[Boolean]) - case SegmentCodec.IntSeg(_) => Json.Num(value.asInstanceOf[Int]) - case SegmentCodec.LongSeg(_) => Json.Num(value.asInstanceOf[Long]) - case SegmentCodec.Text(_) => Json.Str(value.asInstanceOf[String]) - case SegmentCodec.UUID(_) => Json.Str(value.asInstanceOf[UUID].toString) - case SegmentCodec.Trailing => throw new Exception("Trailing segment not allowed") + case SegmentCodec.Empty => throw new Exception("Empty segment not allowed") + case SegmentCodec.Literal(_) => throw new Exception("Literal segment not allowed") + case SegmentCodec.BoolSeg(_) => Json.Bool(value.asInstanceOf[Boolean]) + case SegmentCodec.IntSeg(_) => Json.Num(value.asInstanceOf[Int]) + case SegmentCodec.LongSeg(_) => Json.Num(value.asInstanceOf[Long]) + case SegmentCodec.Text(_) => Json.Str(value.asInstanceOf[String]) + case SegmentCodec.UUID(_) => Json.Str(value.asInstanceOf[UUID].toString) + case SegmentCodec.Annotated(codec, _) => segmentToJson(codec, value) + case SegmentCodec.Trailing => throw new Exception("Trailing segment not allowed") } }