Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add hint mask construct #70

Merged
merged 26 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0f140d5
add hint mask construct
lewisjkl Jan 21, 2022
36d6370
add mask syntax
lewisjkl Jan 21, 2022
1f247d2
add second remove method to Hints src-3
lewisjkl Jan 21, 2022
e0da2d6
work on applying hint mask to schematics from protocol builders
lewisjkl Jan 21, 2022
46e8159
update renderer to render hint masks
lewisjkl Jan 24, 2022
0fd0120
create missing headers
lewisjkl Jan 24, 2022
1898930
remove Http specific hint traits from codegen
lewisjkl Jan 24, 2022
42d92a2
unify error hint in codegen
lewisjkl Jan 24, 2022
790786b
get rid of mask syntax
lewisjkl Jan 24, 2022
b2bfd03
fun with structs
lewisjkl Jan 24, 2022
97d9d3c
relocate newTypeToHintKey conversion
lewisjkl Jan 24, 2022
464129c
add smithy4s.api traits to simpleRestJson protocol
lewisjkl Jan 24, 2022
43bf07b
implement hints without having remove method
lewisjkl Jan 24, 2022
8ff34da
update AWS module with hintMask
lewisjkl Jan 24, 2022
7e92aec
Merge remote-tracking branch 'origin/main' into hint-masks
Baccata Jan 25, 2022
95b5534
Only 1 struct method to implement now
Baccata Jan 25, 2022
e9753bd
collectFirst + foldMap
Baccata Jan 25, 2022
ec2facf
Add HintMask.allAllowed, revert Aws changes
Baccata Jan 25, 2022
bad5119
Fix benchmarks
Baccata Jan 25, 2022
33d28d9
remove hintMask as parameter from CodecAPI
lewisjkl Jan 25, 2022
fc6c1cd
fix aws module compilation
lewisjkl Jan 25, 2022
d99d85b
generate headers
lewisjkl Jan 25, 2022
80e5d96
fix json module compilation
lewisjkl Jan 25, 2022
189e5d3
fix json benchmark
lewisjkl Jan 25, 2022
f11cda8
remove hintmask from AwsProtocol
lewisjkl Jan 25, 2022
bd33588
clean up simpleRestJson protocolDefinition traits
lewisjkl Jan 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion modules/aws-kernel/src/smithy4s/aws/AwsProtocol.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ private[aws] object AwsProtocol {
hints
.get(AwsJson1_0)
.map(AWS_JSON_1_0.apply)
.orElse(hints.get(AwsJson1_1).map(AWS_JSON_1_1.apply))
.orElse(
hints
.get(AwsJson1_1)
.map(AWS_JSON_1_1.apply)
)

// See https://awslabs.github.io/smithy/1.0/spec/aws/aws-json-1_0-protocol.html#differences-between-awsjson1-0-and-awsjson1-1
final case class AWS_JSON_1_0(value: AwsJson1_0) extends AwsProtocol
Expand Down
14 changes: 11 additions & 3 deletions modules/benchmark/src/main/scala/JsonBenchmark.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.scalacheck.Gen
import org.scalacheck.rng.Seed

import java.util.concurrent.TimeUnit
import smithy4s.HintMask

@BenchmarkMode(Array(Mode.AverageTime))
@OutputTimeUnit(TimeUnit.MILLISECONDS)
Expand All @@ -36,7 +37,10 @@ class JsonBenchmark {
val s3objectGen = S3Object.schema.compile(smithy4s.scalacheck.SchematicGen)
val schema = S3Object.schema
val jsonCodecs = smithy4s.http.json.codecs
val jsonCodec = smithy4s.http.json.codecs.compileCodec(schema)
val jsonCodec =
smithy4s.http.json
.codecs(hintMask = HintMask.allAllowed)
.compileCodec(schema)

val original = s3objectGen(Gen.Parameters.default, Seed(2048)).get

Expand All @@ -50,8 +54,12 @@ class JsonBenchmark {
@Benchmark
def measureSmithy4sJson(): Unit = {
val bytes =
smithy4s.http.json.codecs.writeToArray[S3Object](jsonCodec, original)
smithy4s.http.json
.codecs(hintMask = HintMask.allAllowed)
.writeToArray[S3Object](jsonCodec, original)
val _ =
smithy4s.http.json.codecs.decodeFromByteArray(jsonCodec, bytes)
smithy4s.http.json
.codecs(hintMask = HintMask.allAllowed)
.decodeFromByteArray(jsonCodec, bytes)
}
}
6 changes: 2 additions & 4 deletions modules/codegen/src/smithy4s/codegen/IR.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,8 @@ sealed trait Hint

object Hint {
case object Trait extends Hint
case object ClientError extends Hint
case object ServerError extends Hint
case class Http(method: String, uri: List[Segment], code: Int) extends Hint
case class HttpError(code: Int) extends Hint
case object Error extends Hint
case class Protocol(traits: List[Type.Ref]) extends Hint
// traits that get rendered generically
case class Native(typedNode: Fix[TypedNode]) extends Hint
}
Expand Down
21 changes: 18 additions & 3 deletions modules/codegen/src/smithy4s/codegen/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,22 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
)
}

private def renderProtocol(name: String, hints: List[Hint]): RenderResult = {
hints.collectFirst({ case p: Hint.Protocol => p }).foldMap { protocol =>
val protocolTraits = protocol.traits
.map(t => s"${t.namespace}.${t.name.capitalize}")
.mkString(", ")
lines(
newline,
block(
s"implicit val protocol: smithy4s.Protocol[$name] = new smithy4s.Protocol[$name]"
) {
s"def hintMask: smithy4s.HintMask = smithy4s.HintMask($protocolTraits)"
}
)
}
}

private def renderProduct(
name: String,
fields: List[Field],
Expand All @@ -308,9 +324,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
val decl = s"case class $name(${renderArgs(fields)})"
val imports = fields.foldMap(_.tpe.imports) ++ syntaxImport
lines(
if (
hints.contains(Hint.ClientError) || hints.contains(Hint.ServerError)
) {
if (hints.contains(Hint.Error)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

block(s"$decl extends Throwable") {
fields
.find(_.name == "message")
Expand All @@ -336,6 +350,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self =>
renderId(name),
newline,
renderHintsValWithId(hints),
renderProtocol(name, hints),
newline,
if (fields.nonEmpty) {
val definition = if (recursive) "recursive(struct" else "struct"
Expand Down
23 changes: 7 additions & 16 deletions modules/codegen/src/smithy4s/codegen/SmithyToIR.scala
Original file line number Diff line number Diff line change
Expand Up @@ -365,29 +365,20 @@ private[codegen] class SmithyToIR(model: Model, namespace: String) {
)

private val traitToHint: PartialFunction[Trait, Hint] = {
case t: ErrorTrait if t.isClientError => Hint.ClientError
case t: ErrorTrait if t.isServerError => Hint.ServerError
case t: HttpErrorTrait => Hint.HttpError(t.getCode())
case t: HttpTrait => Hint.Http(t.getMethod(), segments(t), t.getCode())
case _: ErrorTrait => Hint.Error
case t: ProtocolDefinitionTrait =>
val shapeIds = t.getTraits()
val refs = shapeIds.asScala.map(shapeId =>
Type.Ref(shapeId.getNamespace(), shapeId.getName())
)
Hint.Protocol(refs.toList)
case t if t.toShapeId() == ShapeId.fromParts("smithy.api", "trait") =>
Hint.Trait
}

private def traitsToHints(traits: List[Trait]): List[Hint] =
traits.collect(traitToHint) ++ traits.map(unfoldTrait)

private def segments(httpTrait: HttpTrait): List[Segment] =
httpTrait
.getUri()
.getSegments()
.asScala
.map {
case s if s.isGreedyLabel() => Segment.GreedyLabel(s.getContent())
case s if s.isLabel() => Segment.Label(s.getContent())
case s => Segment.Static(s.getContent())
}
.toList

implicit class ShapeExt(shape: Shape) {
def name = shape.getId().getName()

Expand Down
4 changes: 3 additions & 1 deletion modules/core/src-2/Hints.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package smithy4s

/**
* A hing is an arbitrary piece of data that can be added to a schema,
* A hint is an arbitrary piece of data that can be added to a schema,
* at the struct level, or at the field/member level.
*
* You can think of it as an annotation that can communicate
Expand Down Expand Up @@ -77,6 +77,8 @@ object Hints {
implicit val keyInstance: Key[A] = this
final override def getKey: Key[A] = this
}

implicit def newTypeToHintKey[A](a: Newtype[A]): Hints.Key[_] = a.key
}

private[smithy4s] class Impl(
Expand Down
4 changes: 3 additions & 1 deletion modules/core/src-3/Hints.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package smithy4s

/**
* A hing is an arbitrary piece of data that can be added to a schema,
* A hint is an arbitrary piece of data that can be added to a schema,
* at the struct level, or at the field/member level.
*
* You can think of it as an annotation that can communicate
Expand Down Expand Up @@ -79,6 +79,8 @@ object Hints {
implicit val keyInstance: Key[A] = this
final override def getKey: Key[A] = this
}

implicit def newTypeToHintKey[A](a: Newtype[A]): Hints.Key[_] = a.key
}

private[smithy4s] class Impl(
Expand Down
62 changes: 62 additions & 0 deletions modules/core/src/smithy4s/HintMask.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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

sealed abstract class HintMask {
def ++(other: HintMask): HintMask
def apply(hints: Hints): Hints
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice :)

}

object HintMask {

def allAllowed: HintMask = Permissive

def empty: HintMask = apply()

def apply(hintKeys: Hints.Key[_]*): HintMask = {
new Impl(hintKeys.toSet)
}

private[this] case object Permissive extends HintMask {
def ++(other: HintMask): HintMask = this
def apply(hints: Hints): Hints = hints
}

private[this] final class Impl(val toSet: Set[Hints.Key[_]])
extends HintMask {
def ++(other: HintMask): HintMask = other match {
case i: Impl => new Impl(toSet ++ i.toSet)
case Permissive => Permissive
}

def apply(hints: Hints): Hints = {
val hintsToKeep = hints.all.filter(h => toSet.contains(h.key)).toSeq
Hints(hintsToKeep: _*)
}
}

private[this] final class MaskSchematic[F[_]](
schematic: Schematic[F],
mask: HintMask
) extends PassthroughSchematic[F](schematic) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea !

override def withHints[A](fa: F[A], hints: Hints): F[A] =
schematic.withHints(fa, mask(hints))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

}

def mask[F[_]](schematic: Schematic[F], mask: HintMask): Schematic[F] =
new MaskSchematic[F](schematic, mask)
}
83 changes: 83 additions & 0 deletions modules/core/src/smithy4s/PassthroughSchematic.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* 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 schematic._
import java.util.UUID

class PassthroughSchematic[F[_]](schematic: Schematic[F]) extends Schematic[F] {
def short: F[Short] = schematic.short

def int: F[Int] = schematic.int

def long: F[Long] = schematic.long

def double: F[Double] = schematic.double

def float: F[Float] = schematic.float

def bigint: F[BigInt] = schematic.bigint

def bigdecimal: F[BigDecimal] = schematic.bigdecimal

def string: F[String] = schematic.string

def boolean: F[Boolean] = schematic.boolean

def uuid: F[UUID] = schematic.uuid

def byte: F[Byte] = schematic.byte

def bytes: F[ByteArray] = schematic.bytes

def unit: F[Unit] = schematic.unit

def list[S](fs: F[S]): F[List[S]] = schematic.list(fs)

def set[S](fs: F[S]): F[Set[S]] = schematic.set(fs)

def vector[S](fs: F[S]): F[Vector[S]] = schematic.vector(fs)

def map[K, V](fk: F[K], fv: F[V]): F[Map[K, V]] = schematic.map(fk, fv)

def struct[S](fields: Vector[Field[F, S, _]])(
const: Vector[Any] => S
): F[S] = schematic.struct(fields)(const)

def union[S](first: Alt[F, S, _], rest: Vector[Alt[F, S, _]])(
total: S => Alt.WithValue[F, S, _]
): F[S] = schematic.union(first, rest)(total)

def enumeration[A](
to: A => (String, Int),
fromName: Map[String, A],
fromOrdinal: Map[Int, A]
): F[A] = schematic.enumeration(to, fromName, fromOrdinal)

def suspend[A](f: => F[A]): F[A] = schematic.suspend(f)

def bijection[A, B](f: F[A], to: A => B, from: B => A): F[B] =
schematic.bijection(f, to, from)

def timestamp: F[Timestamp] = schematic.timestamp

def withHints[A](fa: F[A], hints: Hints): F[A] =
schematic.withHints(fa, hints)

def document: F[Document] = schematic.document

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@
*/

package smithy4s
package http

package object json {

private[smithy4s] val schematicJCodec: Schematic[JCodec.JCodecMake] =
new SchematicJCodec(Constraints.defaultConstraints, maxArity = 1024)
object codecs extends JsonCodecAPI(schematicJCodec)

trait Protocol[A] {
def hintMask: HintMask
}
5 changes: 4 additions & 1 deletion modules/core/src/smithy4s/http/CodecAPI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ object CodecAPI {
constraints: Constraints
): CodecAPI =
new DelegatingCodecAPI {
def compileCodec[A](schema: Schema[A]): this.Codec[A] = {

def compileCodec[A](
schema: Schema[A]
): this.Codec[A] = {
val stringAndBlobResult = schema.compile(
new internals.StringAndBlobCodecSchematic(constraints)
)
Expand Down
48 changes: 48 additions & 0 deletions modules/core/test/src/smithy4s/HintMaskSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package smithy4s

import smithy4s.api.Discriminated
import smithy.api._
import cats.kernel.Eq
import smithy4s.syntax._

object HintMaskSpec extends weaver.FunSuite {

private implicit val hintsEq: Eq[Hints] = (x: Hints, y: Hints) =>
x.toMap == y.toMap

test("Hints are masked") {
val hints = Hints(Discriminated("type"), Required(), HttpError(404))
val mask = HintMask(Discriminated, Required)
val result = mask(hints)
val expected = Hints(Discriminated("type"), Required())
expect.eql(expected, result)
}

test("empty mask masks all hints") {
val hints = Hints(Discriminated("type"), Required(), HttpError(404))
val mask = HintMask.empty
val result = mask(hints)
expect.eql(Hints(), result)
}

type ToHints[A] = Hints

object TestCompiler extends StubSchematic[ToHints] {
def default[A]: Hints = Hints()

override def withHints[A](fa: ToHints[A], hints: Hints): ToHints[A] =
fa ++ hints
}

test("hint mask is applied in schematic mask") {
val schema = string.withHints(Readonly(), Paginated())
val mask = HintMask(Readonly)
val newSchematic = HintMask.mask(TestCompiler, mask)
val result = schema.compile(newSchematic)
val expected = Hints(Readonly())
expect.eql(
expected,
result
)
}
}
Loading