diff --git a/build.sbt b/build.sbt index ad658124b..9b08accef 100644 --- a/build.sbt +++ b/build.sbt @@ -284,7 +284,6 @@ lazy val `aws-http4s` = projectMatrix (ThisBuild / baseDirectory).value / "sampleSpecs" / "dynamodb.2012-08-10.json" ) ) - .settings(Smithy4sPlugin.doNotPublishArtifact) .jvmPlatform(latest2ScalaVersions, jvmDimSettings) .jsPlatform(latest2ScalaVersions, jsDimSettings) diff --git a/modules/aws-kernel/src/smithy4s/aws/AwsSignature.scala b/modules/aws-kernel/src/smithy4s/aws/AwsSignature.scala index 246bcf916..b9fb2dd4c 100644 --- a/modules/aws-kernel/src/smithy4s/aws/AwsSignature.scala +++ b/modules/aws-kernel/src/smithy4s/aws/AwsSignature.scala @@ -16,7 +16,7 @@ package smithy4s.aws.kernel -import smithy4s.http.internals.URIEncoderDecoder.{encodeOthers => uriEncode} +import smithy4s.http.internals.URIEncoderDecoder.{encode => uriEncode} import smithy4s.http.CaseInsensitive import smithy4s.http.HttpMethod diff --git a/modules/codegen/src/smithy4s/codegen/Renderer.scala b/modules/codegen/src/smithy4s/codegen/Renderer.scala index 49bb75ab1..d6d168d34 100644 --- a/modules/codegen/src/smithy4s/codegen/Renderer.scala +++ b/modules/codegen/src/smithy4s/codegen/Renderer.scala @@ -245,7 +245,6 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => val opName = op.name val traitName = s"${serviceName}Operation" - val inputName = op.input.render val input = if (op.input == Type.unit) "" else "input" val errorName = if (op.errors.isEmpty) "Nothing" else s"${op.name}Error" @@ -254,13 +253,6 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => s" with $Errorable_[$errorName]" } else "" - val httpEndpoint = - op.hints - .collectFirst { case Hint.Http(_, _, _) => - s" with http.HttpEndpoint[$inputName]" - } - .getOrElse("") - val errorUnion: Option[Union] = for { errorNel <- NonEmptyList.fromList(op.errors) alts <- errorNel.traverse { t => @@ -277,8 +269,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => s"case class $opName($params) extends $traitName[${op.renderAlgParams}]", obj( opName, - ext = - s"$Endpoint_[${traitName}, ${op.renderAlgParams}]$httpEndpoint$errorable" + ext = s"$Endpoint_[${traitName}, ${op.renderAlgParams}]$errorable" )( renderId(op.name, op.originalNamespace), s"val input: $Schema_[${op.input.render}] = ${op.input.schemaRef}.withHints(smithy4s.internals.InputOutput.Input)", @@ -287,8 +278,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => renderStreamingSchemaVal("streamedOutput", op.streamedOutput), renderHintsValWithId(op.hints), s"def wrap(input: ${op.input.render}) = ${opName}($input)", - renderErrorable(op), - renderHttpSpecific(op) + renderErrorable(op) ), renderedErrorUnion ).addImports(op.imports).addImports(syntaxImport) @@ -374,39 +364,6 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => ).addImports(imports) } - private def renderHttpSpecific(op: Operation): RenderResult = lines { - op.hints - .collectFirst { case Hint.Http(method, uriPattern, code) => - val segments = uriPattern - .map { - case Segment.Label(value) => - s"""http.PathSegment.label("$value")""" - case Segment.GreedyLabel(value) => - s"""http.PathSegment.greedy("$value")""" - case Segment.Static(value) => - s"""http.PathSegment.static("$value")""" - } - .mkString("List(", ", ", ")") - - val uriValue = uriPattern - .map { - case Segment.Label(value) => - s"$${smithy4s.segment(input.$value)}" - case Segment.GreedyLabel(value) => - s"$${smithy4s.greedySegment(input.$value)}" - case Segment.Static(value) => value - } - .mkString("s\"", "/", "\"") - lines( - s"def path(input: ${op.input.render}) = $uriValue", - s"val path = $segments", - s"val code: Int = $code", - s"val method: http.HttpMethod = http.HttpMethod.$method" - ).addImports(Set("smithy4s.http")) - } - .getOrElse(empty) - } - private def renderErrorable(op: Operation): RenderResult = { val errorName = op.name + "Error" diff --git a/modules/core/src/smithy4s/http/HttpEndpoint.scala b/modules/core/src/smithy4s/http/HttpEndpoint.scala index d51f3b316..f3c082004 100644 --- a/modules/core/src/smithy4s/http/HttpEndpoint.scala +++ b/modules/core/src/smithy4s/http/HttpEndpoint.scala @@ -17,6 +17,9 @@ package smithy4s package http +import smithy4s.syntax._ +import smithy.api.Http + trait HttpEndpoint[I] { def path(input: I): String def path: List[PathSegment] @@ -36,8 +39,27 @@ object HttpEndpoint { def cast[Op[_, _, _, _, _], I, E, O, SI, SO]( endpoint: Endpoint[Op, I, E, O, SI, SO] - ): Option[HttpEndpoint[I]] = endpoint match { - case he: HttpEndpoint[_] => Some(he.asInstanceOf[HttpEndpoint[I]]) - case _ => None + ): Option[HttpEndpoint[I]] = { + for { + http <- endpoint.hints.get(Http) + httpMethod <- HttpMethod.fromString(http.method.value) + httpPath <- internals.pathSegments(http.uri.value) + encoder <- endpoint.input + .withHints(http) + .compile(internals.SchematicPathEncoder) + .get + } yield { + new HttpEndpoint[I] { + def path(input: I): String = { + val sb = new StringBuilder() + encoder.encode(sb, input) + sb.result() + } + val path: List[PathSegment] = httpPath.toList + val method: HttpMethod = httpMethod + val code: Int = http.code.getOrElse(200) + } + } } + } diff --git a/modules/core/src/smithy4s/http/Method.scala b/modules/core/src/smithy4s/http/Method.scala index a996d7fd8..08ae346fc 100644 --- a/modules/core/src/smithy4s/http/Method.scala +++ b/modules/core/src/smithy4s/http/Method.scala @@ -40,4 +40,10 @@ object HttpMethod { case object DELETE extends HttpMethod case object GET extends HttpMethod case object PATCH extends HttpMethod + + val values = List(PUT, POST, DELETE, GET, PATCH) + + def fromString(s: String): Option[HttpMethod] = values.find { m => + CaseInsensitive(s) == CaseInsensitive(m.showCapitalised) + } } diff --git a/modules/core/src/smithy4s/http/PathSegment.scala b/modules/core/src/smithy4s/http/PathSegment.scala index c5da86edf..ee5cb7a0f 100644 --- a/modules/core/src/smithy4s/http/PathSegment.scala +++ b/modules/core/src/smithy4s/http/PathSegment.scala @@ -26,4 +26,5 @@ object PathSegment { case class StaticSegment(value: String) extends PathSegment case class LabelSegment(value: String) extends PathSegment case class GreedySegment(value: String) extends PathSegment + } diff --git a/modules/core/src/smithy4s/http/internals/PathEncode.scala b/modules/core/src/smithy4s/http/internals/PathEncode.scala new file mode 100644 index 000000000..8fba64585 --- /dev/null +++ b/modules/core/src/smithy4s/http/internals/PathEncode.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2021 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithy4s.http.internals + +import smithy4s.internals.Hinted + +trait PathEncode[A] { self => + def encode(sb: StringBuilder, a: A): Unit + def encodeGreedy(sb: StringBuilder, a: A): Unit + + def contramap[B](from: B => A): PathEncode[B] = new PathEncode[B] { + def encode(sb: StringBuilder, b: B): Unit = self.encode(sb, from(b)) + + def encodeGreedy(sb: StringBuilder, b: B): Unit = + self.encodeGreedy(sb, from(b)) + } +} + +object PathEncode { + + type MaybePathEncode[A] = Option[PathEncode[A]] + type Make[A] = Hinted[MaybePathEncode, A] + + object Make { + def from[A](f: A => String): Make[A] = Hinted.static[MaybePathEncode, A] { + Some { + raw(f) + } + } + + def raw[A](f: A => String): PathEncode[A] = { + new PathEncode[A] { + def encode(sb: StringBuilder, a: A): Unit = { + val _ = sb.append(URIEncoderDecoder.encode(f(a))) + } + def encodeGreedy(sb: StringBuilder, a: A): Unit = { + f(a).split('/').foreach { + case s if s.isEmpty() => () + case s => sb.append('/').append(URIEncoderDecoder.encode(s)) + } + } + } + } + + def fromToString[A]: Make[A] = from(_.toString) + + def noop[A]: Make[A] = Hinted.static[MaybePathEncode, A](None) + } + +} diff --git a/modules/core/src/smithy4s/http/internals/SchematicPathEncoder.scala b/modules/core/src/smithy4s/http/internals/SchematicPathEncoder.scala new file mode 100644 index 000000000..20ba0e342 --- /dev/null +++ b/modules/core/src/smithy4s/http/internals/SchematicPathEncoder.scala @@ -0,0 +1,136 @@ +/* + * Copyright 2021 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithy4s +package http.internals + +import smithy.api.TimestampFormat +import schematic.Field +import smithy4s.internals.Hinted +import smithy.api.Http +import smithy4s.http.PathSegment +import smithy4s.http.PathSegment.StaticSegment +import smithy4s.http.PathSegment.LabelSegment +import smithy4s.http.PathSegment.GreedySegment + +object SchematicPathEncoder + extends Schematic[PathEncode.Make] + with StubSchematic[PathEncode.Make] { + + def default[A]: PathEncode.Make[A] = PathEncode.Make.noop + override def withHints[A]( + fa: PathEncode.Make[A], + hints: smithy4s.Hints + ): PathEncode.Make[A] = + fa.addHints(hints) + + override def bijection[A, B]( + f: PathEncode.Make[A], + to: A => B, + from: B => A + ): PathEncode.Make[B] = + Hinted[PathEncode.MaybePathEncode, B]( + f.hints, + make = hints => f.make(hints).map(_.contramap(from)) + ) + + override val bigdecimal: PathEncode.Make[BigDecimal] = + PathEncode.Make.fromToString + override val bigint: PathEncode.Make[BigInt] = PathEncode.Make.fromToString + override val double: PathEncode.Make[Double] = PathEncode.Make.fromToString + override val int: PathEncode.Make[Int] = PathEncode.Make.fromToString + override val float: PathEncode.Make[Float] = PathEncode.Make.fromToString + override val short: PathEncode.Make[Short] = PathEncode.Make.fromToString + override val long: PathEncode.Make[Long] = PathEncode.Make.fromToString + override val string: PathEncode.Make[String] = PathEncode.Make.fromToString + override val uuid: PathEncode.Make[java.util.UUID] = + PathEncode.Make.fromToString + override val boolean: PathEncode.Make[Boolean] = PathEncode.Make.fromToString + + override val timestamp: PathEncode.Make[Timestamp] = + Hinted[PathEncode.MaybePathEncode].from { hints => + val fmt = hints.get(TimestampFormat).getOrElse(TimestampFormat.DATE_TIME) + + Some(PathEncode.Make.raw(_.format(fmt))) + } + + override val unit: PathEncode.Make[Unit] = + genericStruct(Vector.empty)(_ => ()) + + override def genericStruct[S](fields: Vector[Field[PathEncode.Make, S, _]])( + const: Vector[Any] => S + ): PathEncode.Make[S] = { + type Writer = (StringBuilder, S) => Unit + + def toPathEncoder[A]( + field: Field[PathEncode.Make, S, A], + greedy: Boolean + ): Option[Writer] = { + field.fold(new Field.Folder[PathEncode.Make, S, Option[Writer]] { + def onRequired[AA]( + label: String, + instance: PathEncode.Make[AA], + get: S => AA + ): Option[Writer] = + if (greedy) + instance.get.map(encoder => + (sb, s) => encoder.encodeGreedy(sb, get(s)) + ) + else + instance.get.map(encoder => (sb, s) => encoder.encode(sb, get(s))) + def onOptional[AA]( + label: String, + instance: PathEncode.Make[AA], + get: S => Option[AA] + ): Option[Writer] = None + }) + } + + def compile1(path: PathSegment): Option[Writer] = path match { + case StaticSegment(value) => + Some((sb, _) => { val _ = sb.append(value) }) + case LabelSegment(value) => + fields + .find(_.label == value) + .flatMap(field => toPathEncoder(field, greedy = false)) + case GreedySegment(value) => + fields + .find(_.label == value) + .flatMap(field => toPathEncoder(field, greedy = true)) + } + + def compilePath(path: Vector[PathSegment]): Option[Vector[Writer]] = + path.traverse(compile1(_)) + + Hinted[PathEncode.MaybePathEncode].onHintOpt[Http, S] { maybeHttpHint => + for { + httpHint <- maybeHttpHint + path <- pathSegments(httpHint.uri.value) + writers <- compilePath(path) + } yield new PathEncode[S] { + def encode(sb: StringBuilder, s: S): Unit = { + var first = true + writers.foreach { w => + if (first) { w.apply(sb, s); first = false } + else w.apply(sb.append('/'), s) + } + } + + def encodeGreedy(sb: StringBuilder, s: S) = () + } + } + } +} diff --git a/modules/core/src/smithy4s/http/internals/URIEncoderDecoder.scala b/modules/core/src/smithy4s/http/internals/URIEncoderDecoder.scala index 180cbae19..a52fc8fd0 100644 --- a/modules/core/src/smithy4s/http/internals/URIEncoderDecoder.scala +++ b/modules/core/src/smithy4s/http/internals/URIEncoderDecoder.scala @@ -19,113 +19,66 @@ package http package internals import java.io.ByteArrayOutputStream -import java.net._ +import scala.annotation.tailrec private[smithy4s] object URIEncoderDecoder { val digits: String = "0123456789ABCDEF" - val encoding: String = "UTF8" - def validate(s: String, legal: String): Unit = { - var i: Int = 0 - while (i < s.length) { - var continue = false - val ch: Char = s.charAt(i) - if (ch == '%') { - continue = true - while ({ - if (i + 2 >= s.length) { - throw new URISyntaxException(s, "Incomplete % sequence", i) + def encode(s: String): String = { + if (s == null) { + throw new NullPointerException + } + val buf = new java.lang.StringBuilder(s.length + 16) + var start = -1 + @tailrec + def loop(i: Int): Unit = { + if (i < s.length) { + val ch: Char = s.charAt(i) + if ( + (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || " .-*_" + .indexOf(ch.toInt) > -1 + ) { + if (start >= 0) { + encodeOthers(s.substring(start, i), buf, encoding) + start = -1 } - val d1: Int = java.lang.Character.digit(s.charAt(i + 1), 16) - val d2: Int = java.lang.Character.digit(s.charAt(i + 2), 16) - if (d1 == -1 || d2 == -1) { - throw new URISyntaxException( - s, - "Invalid % sequence (" + s.substring(i, i + 3) + ")", - i - ) + if (ch != ' ') { + buf.append(ch) + } else { + buf.append('+') } - i += 3 - // loop condition - // Scala 3 dropped do-while loops - // this is the recommended rewrite: - // https://docs.scala-lang.org/scala3/reference/dropped-features/do-while.html - (i < s.length && s.charAt(i) == '%') - }) {} - } else if ( - !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || - legal.indexOf(ch.toInt) > -1 || - (ch > 127 && !java.lang.Character.isSpaceChar( - ch - ) && !java.lang.Character - .isISOControl(ch))) - ) { - throw new URISyntaxException(s, "Illegal character", i) - } - if (!continue) i += 1 - } - } - - def validateSimple(s: String, legal: String): Unit = { - var i: Int = 0 - while (i < s.length) { - val ch: Char = s.charAt(i) - if ( - !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || - legal.indexOf(ch.toInt) > -1) - ) { - throw new URISyntaxException(s, "Illegal character", i) + } else if (start < 0) { + start = i + } + loop(i + 1) } - i += 1 } - } + loop(0) - def quoteIllegal(s: String, legal: String): String = { - val buf: StringBuilder = new StringBuilder() - for (i <- 0 until s.length) { - val ch: Char = s.charAt(i) - if ( - (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || - (ch >= '0' && ch <= '9') || - legal.indexOf(ch.toInt) > -1 || - (ch > 127 && !java.lang.Character.isSpaceChar( - ch - ) && !java.lang.Character - .isISOControl(ch)) - ) { - buf.append(ch) - } else { - val bytes: Array[Byte] = new String(Array(ch)).getBytes(encoding) - for (j <- bytes.indices) { - buf.append('%') - buf.append(digits.charAt((bytes(j) & 0xf0) >> 4)) - buf.append(digits.charAt(bytes(j) & 0xf)) - } - } + if (start >= 0) { + encodeOthers(s.substring(start, s.length), buf, encoding) } buf.toString } - def encodeOthers(s: String): String = { - val buf: StringBuilder = new StringBuilder() - for (i <- 0 until s.length) { - val ch: Char = s.charAt(i) - if (ch <= 127) { - buf.append(ch) - } else { - val bytes: Array[Byte] = new String(Array(ch)).getBytes(encoding) - for (j <- bytes.indices) { - buf.append('%') - buf.append(digits.charAt((bytes(j) & 0xf0) >> 4)) - buf.append(digits.charAt(bytes(j) & 0xf)) - } + private def encodeOthers( + s: String, + buf: java.lang.StringBuilder, + enc: String + ): Unit = { + val bytes = s.getBytes(enc) + @tailrec + def loop(j: Int): Unit = { + if (j < bytes.length) { + buf.append('%') + buf.append(digits((bytes(j) & 0xf0) >> 4)) + buf.append(digits(bytes(j) & 0xf)) + loop(j + 1) } } - buf.toString + loop(0) } def decode(s: String): String = { diff --git a/modules/core/src/smithy4s/http/internals/package.scala b/modules/core/src/smithy4s/http/internals/package.scala index 3d39f77fb..a70aa7e13 100644 --- a/modules/core/src/smithy4s/http/internals/package.scala +++ b/modules/core/src/smithy4s/http/internals/package.scala @@ -52,4 +52,23 @@ package object internals { } } + private[smithy4s] def pathSegments( + str: String + ): Option[Vector[PathSegment]] = { + str + .split('/') + .toVector + .filterNot(_.isEmpty()) + .traverse(fromToString(_)) + } + + private def fromToString(str: String): Option[PathSegment] = { + if (str.isEmpty()) None + else if (str.startsWith("{") && str.endsWith("+}")) + Some(PathSegment.greedy(str.substring(1, str.length() - 2))) + else if (str.startsWith("{") && str.endsWith("}")) + Some(PathSegment.label(str.substring(1, str.length() - 1))) + else Some(PathSegment.static(str)) + } + } diff --git a/modules/core/src/smithy4s/package.scala b/modules/core/src/smithy4s/package.scala index 77c200b72..5a63c2d1d 100644 --- a/modules/core/src/smithy4s/package.scala +++ b/modules/core/src/smithy4s/package.scala @@ -14,8 +14,6 @@ * limitations under the License. */ -import smithy4s.http.internals.URIEncoderDecoder - package object smithy4s extends TypeAliases { type Hint = Hints.Binding[_] @@ -27,9 +25,6 @@ package object smithy4s extends TypeAliases { val errorTypeHeader = "X-Error-Type" - def segment(s: Any): String = URIEncoderDecoder.encodeOthers(s.toString()) - def greedySegment(s: String) = s.split("/").map(segment).mkString("/") - // Allows to "inject" F[_] types in places that require F[_,_,_,_,_] type GenLift[F[_]] = { type λ[I, E, O, SI, SO] = F[O] diff --git a/modules/core/test/src-js/smithy4s/JsConvertersSpec.scala b/modules/core/test/src-js/smithy4s/JsConvertersSpec.scala index f3b18fc78..2bf6b48b4 100644 --- a/modules/core/test/src-js/smithy4s/JsConvertersSpec.scala +++ b/modules/core/test/src-js/smithy4s/JsConvertersSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2021 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package smithy4s import weaver._ diff --git a/modules/core/test/src/smithy4s/TypeInferenceSmokeSpec.scala b/modules/core/test/src/smithy4s/TypeInferenceSmokeSpec.scala index e71cdb6f8..d0f2ee0c4 100644 --- a/modules/core/test/src/smithy4s/TypeInferenceSmokeSpec.scala +++ b/modules/core/test/src/smithy4s/TypeInferenceSmokeSpec.scala @@ -16,38 +16,17 @@ package smithy4s -import weaver._ import smithy4s.example._ import cats.Functor import cats.syntax.all._ -import cats.effect.IO -object TypeInferenceSmokeSpec extends SimpleIOSuite { - - test("Type inference works with service calls") { - /* - * Checks that `map` can be called without upcasting the result of - * the service call to F[something]. - */ - def foo[F[_]: Functor](dummyService: DummyService[F]): F[Int] = - dummyService.dummy().map(_ => 1) - - val dummyInstance = new DummyService[IO] { - - override def dummy( - str: Option[String], - int: Option[Int], - ts1: Option[Timestamp], - ts2: Option[Timestamp], - ts3: Option[Timestamp], - ts4: Option[Timestamp], - b: Option[Boolean], - sl: Option[List[String]], - slm: Option[Map[String, String]] - ): IO[Unit] = IO.unit - - } - foo(dummyInstance).map(x => expect(x == 1)) - } +// compile-time tests +object TypeInferenceSmokeSpec { + /* + * Checks that `map` can be called without upcasting the result of + * the service call to F[something]. + */ + def foo[F[_]: Functor](dummyService: DummyService[F]): F[Int] = + dummyService.dummy().map(_ => 1) } diff --git a/modules/core/test/src/smithy4s/http/internals/PathSpec.scala b/modules/core/test/src/smithy4s/http/internals/PathSpec.scala new file mode 100644 index 000000000..2a1e8559a --- /dev/null +++ b/modules/core/test/src/smithy4s/http/internals/PathSpec.scala @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package smithy4s.http.internals + +import smithy4s.Timestamp +import smithy4s.example.DummyServiceGen.DummyPath +import smithy4s.example.PathParams +import smithy4s.http.HttpEndpoint +import smithy4s.http.PathSegment + +object PathSpec extends weaver.FunSuite { + + test("Parse path pattern into path segments") { + val result = pathSegments("/{head}/foo/{tail+}") + expect( + result == Option( + Vector( + PathSegment.label("head"), + PathSegment.static("foo"), + PathSegment.greedy("tail") + ) + ) + ) + } + + test("Write PathParams") { + val result = HttpEndpoint + .cast( + DummyPath + ) + .get + .path( + PathParams( + "example with spaces, %, / and \\", + 10, + Timestamp.fromEpochSecond(0L), + Timestamp.fromEpochSecond(0L), + Timestamp.fromEpochSecond(0L), + Timestamp.fromEpochSecond(0L), + true + ) + ) + + val expected = if (weaver.Platform.isJS) { + "dummy-path/example+with+spaces%2C+%25%2C+%2F+and+%5C/10/1970-01-01T00%3A00%3A00.000Z/1970-01-01T00%3A00%3A00.000Z/0/Thu%2C+01+Jan+1970+00%3A00%3A00+GMT/true" + } else { + "dummy-path/example+with+spaces%2C+%25%2C+%2F+and+%5C/10/1970-01-01T00%3A00%3A00Z/1970-01-01T00%3A00%3A00Z/0/Thu%2C+01+Jan+1970+00%3A00%3A00+GMT/true" + } + + assert.eql(result, expected) + } + +} diff --git a/modules/example/src/smithy4s/example/FooService.scala b/modules/example/src/smithy4s/example/FooService.scala index 1b8774bf5..af88f796d 100644 --- a/modules/example/src/smithy4s/example/FooService.scala +++ b/modules/example/src/smithy4s/example/FooService.scala @@ -1,6 +1,5 @@ package smithy4s.example -import smithy4s.http import smithy4s.syntax._ trait FooServiceGen[F[_, _, _, _, _]] { @@ -48,7 +47,7 @@ object FooServiceGen extends smithy4s.Service[FooServiceGen, FooServiceOperation } } case class GetFoo() extends FooServiceOperation[Unit, Nothing, GetFooOutput, Nothing, Nothing] - object GetFoo extends smithy4s.Endpoint[FooServiceOperation, Unit, Nothing, GetFooOutput, Nothing, Nothing] with http.HttpEndpoint[Unit] { + object GetFoo extends smithy4s.Endpoint[FooServiceOperation, Unit, Nothing, GetFooOutput, Nothing, Nothing] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "GetFoo") val input: smithy4s.Schema[Unit] = unit.withHints(smithy4s.internals.InputOutput.Input) val output: smithy4s.Schema[GetFooOutput] = GetFooOutput.schema.withHints(smithy4s.internals.InputOutput.Output) @@ -60,10 +59,6 @@ object FooServiceGen extends smithy4s.Service[FooServiceGen, FooServiceOperation smithy.api.Readonly(), ) def wrap(input: Unit) = GetFoo() - def path(input: Unit) = s"foo" - val path = List(http.PathSegment.static("foo")) - val code: Int = 200 - val method: http.HttpMethod = http.HttpMethod.GET } } diff --git a/modules/example/src/smithy4s/example/ObjectService.scala b/modules/example/src/smithy4s/example/ObjectService.scala index dc651beff..de620d6f0 100644 --- a/modules/example/src/smithy4s/example/ObjectService.scala +++ b/modules/example/src/smithy4s/example/ObjectService.scala @@ -2,7 +2,6 @@ package smithy4s.example import ObjectServiceGen.GetObjectError import ObjectServiceGen.PutObjectError -import smithy4s.http import smithy4s.syntax._ trait ObjectServiceGen[F[_, _, _, _, _]] { @@ -57,7 +56,7 @@ object ObjectServiceGen extends smithy4s.Service[ObjectServiceGen, ObjectService } } case class PutObject(input: PutObjectInput) extends ObjectServiceOperation[PutObjectInput, PutObjectError, Unit, Nothing, Nothing] - object PutObject extends smithy4s.Endpoint[ObjectServiceOperation, PutObjectInput, PutObjectError, Unit, Nothing, Nothing] with http.HttpEndpoint[PutObjectInput] with smithy4s.Errorable[PutObjectError] { + object PutObject extends smithy4s.Endpoint[ObjectServiceOperation, PutObjectInput, PutObjectError, Unit, Nothing, Nothing] with smithy4s.Errorable[PutObjectError] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "PutObject") val input: smithy4s.Schema[PutObjectInput] = PutObjectInput.schema.withHints(smithy4s.internals.InputOutput.Input) val output: smithy4s.Schema[Unit] = unit.withHints(smithy4s.internals.InputOutput.Output) @@ -80,10 +79,6 @@ object ObjectServiceGen extends smithy4s.Service[ObjectServiceGen, ObjectService case PutObjectError.ServerErrorCase(e) => e case PutObjectError.NoMoreSpaceCase(e) => e } - def path(input: PutObjectInput) = s"${smithy4s.segment(input.bucketName)}/${smithy4s.segment(input.key)}" - val path = List(http.PathSegment.label("bucketName"), http.PathSegment.label("key")) - val code: Int = 200 - val method: http.HttpMethod = http.HttpMethod.PUT } sealed trait PutObjectError object PutObjectError { @@ -117,7 +112,7 @@ object ObjectServiceGen extends smithy4s.Service[ObjectServiceGen, ObjectService implicit val staticSchema : schematic.Static[smithy4s.Schema[PutObjectError]] = schematic.Static(schema) } case class GetObject(input: GetObjectInput) extends ObjectServiceOperation[GetObjectInput, GetObjectError, GetObjectOutput, Nothing, Nothing] - object GetObject extends smithy4s.Endpoint[ObjectServiceOperation, GetObjectInput, GetObjectError, GetObjectOutput, Nothing, Nothing] with http.HttpEndpoint[GetObjectInput] with smithy4s.Errorable[GetObjectError] { + object GetObject extends smithy4s.Endpoint[ObjectServiceOperation, GetObjectInput, GetObjectError, GetObjectOutput, Nothing, Nothing] with smithy4s.Errorable[GetObjectError] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "GetObject") val input: smithy4s.Schema[GetObjectInput] = GetObjectInput.schema.withHints(smithy4s.internals.InputOutput.Input) val output: smithy4s.Schema[GetObjectOutput] = GetObjectOutput.schema.withHints(smithy4s.internals.InputOutput.Output) @@ -138,10 +133,6 @@ object ObjectServiceGen extends smithy4s.Service[ObjectServiceGen, ObjectService def unliftError(e: GetObjectError) : Throwable = e match { case GetObjectError.ServerErrorCase(e) => e } - def path(input: GetObjectInput) = s"${smithy4s.segment(input.bucketName)}/${smithy4s.segment(input.key)}" - val path = List(http.PathSegment.label("bucketName"), http.PathSegment.label("key")) - val code: Int = 200 - val method: http.HttpMethod = http.HttpMethod.GET } sealed trait GetObjectError object GetObjectError { diff --git a/modules/protocol/test/src/smithy4s/api/validation/DiscriminatedUnionValidatorSpec.scala b/modules/protocol/test/src/smithy4s/api/validation/DiscriminatedUnionValidatorSpec.scala index c819e9ea4..e4f615363 100644 --- a/modules/protocol/test/src/smithy4s/api/validation/DiscriminatedUnionValidatorSpec.scala +++ b/modules/protocol/test/src/smithy4s/api/validation/DiscriminatedUnionValidatorSpec.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2021 Disney Streaming + * + * Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://disneystreaming.github.io/TOST-1.0.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package smithy4s.api.validation import software.amazon.smithy.model.shapes.UnionShape diff --git a/sampleSpecs/metadata.smithy b/sampleSpecs/metadata.smithy index 0361bc446..089af2074 100644 --- a/sampleSpecs/metadata.smithy +++ b/sampleSpecs/metadata.smithy @@ -4,13 +4,21 @@ namespace smithy4s.example /// when testing core service DummyService { version: "0.0", - operations: [Dummy] + operations: [Dummy, DummyPath] } +@http(method: "GET", uri: "/dummy") +@readonly operation Dummy { input: Queries } +@http(method: "GET", uri: "/dummy-path/{str}/{int}/{ts1}/{ts2}/{ts3}/{ts4}/{b}") +@readonly +operation DummyPath { + input: PathParams +} + structure Queries { @httpQuery("str") str: String, @@ -59,6 +67,7 @@ structure Headers { slm: StringMap } + structure PathParams { @httpLabel @required diff --git a/sampleSpecs/pizza.smithy b/sampleSpecs/pizza.smithy index f3339588c..ecc95e3cb 100644 --- a/sampleSpecs/pizza.smithy +++ b/sampleSpecs/pizza.smithy @@ -60,6 +60,7 @@ structure AddMenuItemResult { added: Timestamp } +@readonly @http(method: "GET", uri: "/version", code: 200) operation Version { output: VersionOutput