Skip to content

Commit

Permalink
Merge pull request #1213 from disneystreaming/endpoint-host-label
Browse files Browse the repository at this point in the history
Endpoint Host Prefix
  • Loading branch information
Baccata authored Sep 27, 2023
2 parents d4d218b + 700f254 commit f8f0889
Show file tree
Hide file tree
Showing 18 changed files with 487 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ trait DummyServiceGen[F[_, _, _, _, _]] {
self =>

def dummy(str: Option[String] = None, int: Option[Int] = None, ts1: Option[Timestamp] = None, ts2: Option[Timestamp] = None, ts3: Option[Timestamp] = None, ts4: Option[Timestamp] = None, b: Option[Boolean] = None, sl: Option[List[String]] = None, ie: Option[Numbers] = None, on: Option[OpenNums] = None, ons: Option[OpenNumsStr] = None, slm: Option[Map[String, String]] = None): F[Queries, Nothing, Unit, Nothing, Nothing]
def dummyHostPrefix(label1: String, label2: String, label3: HostLabelEnum): F[HostLabelInput, Nothing, Unit, Nothing, Nothing]
def dummyPath(str: String, int: Int, ts1: Timestamp, ts2: Timestamp, ts3: Timestamp, ts4: Timestamp, b: Boolean, ie: Numbers): F[PathParams, Nothing, Unit, Nothing, Nothing]

def transform: Transformation.PartiallyApplied[DummyServiceGen[F]] = Transformation.of[DummyServiceGen[F]](this)
Expand All @@ -42,6 +43,7 @@ object DummyServiceGen extends Service.Mixin[DummyServiceGen, DummyServiceOperat

val endpoints: Vector[smithy4s.Endpoint[DummyServiceOperation, _, _, _, _, _]] = Vector(
DummyServiceOperation.Dummy,
DummyServiceOperation.DummyHostPrefix,
DummyServiceOperation.DummyPath,
)

Expand All @@ -68,10 +70,12 @@ object DummyServiceOperation {

object reified extends DummyServiceGen[DummyServiceOperation] {
def dummy(str: Option[String] = None, int: Option[Int] = None, ts1: Option[Timestamp] = None, ts2: Option[Timestamp] = None, ts3: Option[Timestamp] = None, ts4: Option[Timestamp] = None, b: Option[Boolean] = None, sl: Option[List[String]] = None, ie: Option[Numbers] = None, on: Option[OpenNums] = None, ons: Option[OpenNumsStr] = None, slm: Option[Map[String, String]] = None) = Dummy(Queries(str, int, ts1, ts2, ts3, ts4, b, sl, ie, on, ons, slm))
def dummyHostPrefix(label1: String, label2: String, label3: HostLabelEnum) = DummyHostPrefix(HostLabelInput(label1, label2, label3))
def dummyPath(str: String, int: Int, ts1: Timestamp, ts2: Timestamp, ts3: Timestamp, ts4: Timestamp, b: Boolean, ie: Numbers) = DummyPath(PathParams(str, int, ts1, ts2, ts3, ts4, b, ie))
}
class Transformed[P[_, _, _, _, _], P1[_ ,_ ,_ ,_ ,_]](alg: DummyServiceGen[P], f: PolyFunction5[P, P1]) extends DummyServiceGen[P1] {
def dummy(str: Option[String] = None, int: Option[Int] = None, ts1: Option[Timestamp] = None, ts2: Option[Timestamp] = None, ts3: Option[Timestamp] = None, ts4: Option[Timestamp] = None, b: Option[Boolean] = None, sl: Option[List[String]] = None, ie: Option[Numbers] = None, on: Option[OpenNums] = None, ons: Option[OpenNumsStr] = None, slm: Option[Map[String, String]] = None) = f[Queries, Nothing, Unit, Nothing, Nothing](alg.dummy(str, int, ts1, ts2, ts3, ts4, b, sl, ie, on, ons, slm))
def dummyHostPrefix(label1: String, label2: String, label3: HostLabelEnum) = f[HostLabelInput, Nothing, Unit, Nothing, Nothing](alg.dummyHostPrefix(label1, label2, label3))
def dummyPath(str: String, int: Int, ts1: Timestamp, ts2: Timestamp, ts3: Timestamp, ts4: Timestamp, b: Boolean, ie: Numbers) = f[PathParams, Nothing, Unit, Nothing, Nothing](alg.dummyPath(str, int, ts1, ts2, ts3, ts4, b, ie))
}

Expand All @@ -90,9 +94,21 @@ object DummyServiceOperation {
.withHints(smithy.api.Http(method = smithy.api.NonEmptyString("GET"), uri = smithy.api.NonEmptyString("/dummy"), code = 200), smithy.api.Readonly())
def wrap(input: Queries) = Dummy(input)
}
final case class DummyHostPrefix(input: HostLabelInput) extends DummyServiceOperation[HostLabelInput, Nothing, Unit, Nothing, Nothing] {
def run[F[_, _, _, _, _]](impl: DummyServiceGen[F]): F[HostLabelInput, Nothing, Unit, Nothing, Nothing] = impl.dummyHostPrefix(input.label1, input.label2, input.label3)
def ordinal = 1
def endpoint: smithy4s.Endpoint[DummyServiceOperation,HostLabelInput, Nothing, Unit, Nothing, Nothing] = DummyHostPrefix
}
object DummyHostPrefix extends smithy4s.Endpoint[DummyServiceOperation,HostLabelInput, Nothing, Unit, Nothing, Nothing] {
val schema: OperationSchema[HostLabelInput, Nothing, Unit, Nothing, Nothing] = Schema.operation(ShapeId("smithy4s.example", "DummyHostPrefix"))
.withInput(HostLabelInput.schema.addHints(smithy4s.internals.InputOutput.Input.widen))
.withOutput(unit.addHints(smithy4s.internals.InputOutput.Output.widen))
.withHints(smithy.api.Http(method = smithy.api.NonEmptyString("GET"), uri = smithy.api.NonEmptyString("/dummy"), code = 200), smithy.api.Endpoint(hostPrefix = smithy.api.NonEmptyString("foo.{label1}--abc{label2}.{label3}.secure.")))
def wrap(input: HostLabelInput) = DummyHostPrefix(input)
}
final case class DummyPath(input: PathParams) extends DummyServiceOperation[PathParams, Nothing, Unit, Nothing, Nothing] {
def run[F[_, _, _, _, _]](impl: DummyServiceGen[F]): F[PathParams, Nothing, Unit, Nothing, Nothing] = impl.dummyPath(input.str, input.int, input.ts1, input.ts2, input.ts3, input.ts4, input.b, input.ie)
def ordinal = 1
def ordinal = 2
def endpoint: smithy4s.Endpoint[DummyServiceOperation,PathParams, Nothing, Unit, Nothing, Nothing] = DummyPath
}
object DummyPath extends smithy4s.Endpoint[DummyServiceOperation,PathParams, Nothing, Unit, Nothing, Nothing] {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package smithy4s.example

import smithy4s.Enumeration
import smithy4s.Hints
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.schema.EnumTag
import smithy4s.schema.Schema.enumeration

sealed abstract class HostLabelEnum(_value: String, _name: String, _intValue: Int, _hints: Hints) extends Enumeration.Value {
override type EnumType = HostLabelEnum
override val value: String = _value
override val name: String = _name
override val intValue: Int = _intValue
override val hints: Hints = _hints
override def enumeration: Enumeration[EnumType] = HostLabelEnum
@inline final def widen: HostLabelEnum = this
}
object HostLabelEnum extends Enumeration[HostLabelEnum] with ShapeTag.Companion[HostLabelEnum] {
val id: ShapeId = ShapeId("smithy4s.example", "HostLabelEnum")

val hints: Hints = Hints.empty

case object THING1 extends HostLabelEnum("THING1", "THING1", 0, Hints())
case object THING2 extends HostLabelEnum("THING2", "THING2", 1, Hints())

val values: List[HostLabelEnum] = List(
THING1,
THING2,
)
val tag: EnumTag[HostLabelEnum] = EnumTag.ClosedStringEnum
implicit val schema: Schema[HostLabelEnum] = enumeration(tag, values).withId(id).addHints(hints)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package smithy4s.example

import smithy4s.Hints
import smithy4s.Schema
import smithy4s.ShapeId
import smithy4s.ShapeTag
import smithy4s.schema.Schema.string
import smithy4s.schema.Schema.struct

final case class HostLabelInput(label1: String, label2: String, label3: HostLabelEnum)
object HostLabelInput extends ShapeTag.Companion[HostLabelInput] {
val id: ShapeId = ShapeId("smithy4s.example", "HostLabelInput")

val hints: Hints = Hints.empty

implicit val schema: Schema[HostLabelInput] = struct(
string.required[HostLabelInput]("label1", _.label1).addHints(smithy.api.HostLabel()),
string.required[HostLabelInput]("label2", _.label2).addHints(smithy.api.HostLabel()),
HostLabelEnum.schema.required[HostLabelInput]("label3", _.label3).addHints(smithy.api.HostLabel()),
){
HostLabelInput.apply
}.withId(id).addHints(hints)
}
52 changes: 52 additions & 0 deletions modules/bootstrapped/test/src/smithy4s/http/HostPrefixSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2021-2022 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

import smithy4s.example.DummyServiceOperation.DummyHostPrefix
import smithy4s.example.HostLabelInput
import internals.HostPrefixSegment

class HostPrefixSpec() extends munit.FunSuite {

test("Parse host prefix pattern into host prefix segments") {
val result = internals.hostPrefixSegments("{head}--foo{tail}")
val expected =
Vector(
HostPrefixSegment.label("head"),
HostPrefixSegment.static("--foo"),
HostPrefixSegment.label("tail")
)
expect.same(result, expected)
}

// "foo.{label1}--abc{label2}.{label3}.secure
test("Write a valid Host Prefix for DummyHostPrefix") {
val injector = HttpHostPrefix(DummyHostPrefix.schema).get
val input = HostLabelInput(
"mabeline",
"virgo",
smithy4s.example.HostLabelEnum.THING1
)
val result = injector.write(List.empty, input)

val expected =
"foo." :: "mabeline" :: "--abc" :: "virgo" :: "." :: "THING1" :: ".secure." :: Nil
expect.eql(result, expected)
}

}
42 changes: 42 additions & 0 deletions modules/bootstrapped/test/src/smithy4s/http/HttpRequestSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package smithy4s.http

import munit._
import smithy4s.schema.OperationSchema
import smithy4s.schema.Schema
import smithy4s._
import smithy.api

final class HttpRequestSpec extends FunSuite {

test("host prefix") {
case class Foo(foo: String)
val schema =
Schema.struct(Schema.string.required[Foo]("foo", _.foo))(Foo(_))
val endpointHint =
api.Endpoint(hostPrefix = api.NonEmptyString("test.{foo}-other."))
val opSchema = OperationSchema(
ShapeId("test", "Test"),
Hints(endpointHint),
schema,
None,
Schema.unit,
None,
None
)

val writer = HttpRequest.Writer.hostPrefix[String, Foo](opSchema)

val uri = HttpUri(
HttpUriScheme.Https,
"example.com",
None,
Seq.empty,
Map.empty,
None
)
val request = HttpRequest(HttpMethod.GET, uri, Map.empty, "")
val resultUri = writer.write(request, Foo("hello")).uri
assertEquals(resultUri, uri.copy(host = "test.hello-other.example.com"))
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ private[internals] object assert {
}
}

def contains(
result: String,
expected: String,
prefix: String = ""
): ComplianceResult = {
if (result.contains(expected)) {
success
} else {
fail(
s"$prefix the result value: ${pprint.apply(result)} did not contain the expected TestCase value ${pprint
.apply(expected)}."
)
}
}

private def xmlEql[F[_]: Concurrent](
result: String,
testCase: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,20 @@ private[compliancetests] class ClientHttpComplianceTestCase[
)
}

val resolvedHostPrefix =
testCase.resolvedHost
.zip(testCase.host)
.map { case (resolved, host) => resolved.split(host)(0) }

val resolvedHostAssert =
request.uri.host
.map(_.value)
.zip(resolvedHostPrefix)
.map { case (a, b) =>
assert.contains(a, b, "resolved host test :").pure[F]
}
.toList

val receivedPathSegments =
request.uri.path.segments.map(_.decoded())
val expectedPathSegments =
Expand All @@ -87,7 +101,7 @@ private[compliancetests] class ClientHttpComplianceTestCase[
"method test :"
)
val ioAsserts: List[F[ComplianceResult]] =
bodyAssert +: (List(
bodyAssert +: (resolvedHostAssert ++ List(
assert.testCase.checkHeaders(testCase, request.headers),
pathAssert,
queryAssert,
Expand Down
5 changes: 5 additions & 0 deletions modules/core/src/smithy4s/codecs/Writer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ object Writer {
*/
def lift[Message, A](f: (Message, A) => Message): Writer[Message, A] = f(_, _)

/**
* Creates a writer which returns a constant value
*/
def constant[Message](m: Message): Writer[Message, Any] = (_, _) => m

/**
* Creates a writer that returns its input as its output, without taking
* the data into consideration
Expand Down
35 changes: 35 additions & 0 deletions modules/core/src/smithy4s/http/HostPrefix.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2021-2022 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

import smithy4s.http.internals.HostPrefixSchemaVisitor
import smithy4s.codecs.Writer
import smithy4s.schema.OperationSchema

object HttpHostPrefix {
def apply[I, E, O, SI, SO](
endpoint: OperationSchema[I, E, O, SI, SO]
): Option[Writer[List[String], I]] = {
for {
endpointHint <- endpoint.hints.get(smithy.api.Endpoint)
hostPrefixEncoder <- HostPrefixSchemaVisitor(
endpoint.input.addHints(endpointHint)
)
} yield hostPrefixEncoder
}

}
22 changes: 22 additions & 0 deletions modules/core/src/smithy4s/http/HttpRequest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ object HttpRequest {
req.addHeaders(meta.headers).copy(uri = newUri)
}

private[http] def hostPrefix[Body, I](
httpEndpoint: OperationSchema[I, _, _, _, _]
): Writer[Body, I] = {
HttpHostPrefix(httpEndpoint) match {
case Some(prefixEncoder) =>
new Writer[Body, I] {
def write(
request: HttpRequest[Body],
input: I
): HttpRequest[Body] = {
val hostPrefix = prefixEncoder.write(List.empty, input)
val oldUri = request.uri
val newUri =
oldUri.copy(host = s"${hostPrefix.mkString}${oldUri.host}")
request.copy(uri = newUri)
}
}
case None =>
(r, _) => r
}
}

}

object Decoder {
Expand Down
Loading

0 comments on commit f8f0889

Please sign in to comment.