From e20d7e48001aaf2532d617d240f3e0f089aacbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Mon, 27 Dec 2021 16:16:46 +0100 Subject: [PATCH 01/28] Adds a "dynamic" module to dynamically load services from smithy modules This relies on mutability + suspension to build a map of schemas that work against dynamic data, backing product types with arrays. The goal is notably to get to a state where services could be proxied dynamically, whether for actual proxyfication purposes, or to create CLIs that do not need recompilation, or create UIs/playgrounds dynamically. --- build.sbt | 23 ++- .../smithy4s/dynamic/DynamicEndpoint.scala | 21 +++ .../src/smithy4s/dynamic/DynamicModel.scala | 20 ++ .../dynamic/DynamicModelCompiler.scala | 171 ++++++++++++++++++ .../src/smithy4s/dynamic/DynamicOp.scala | 7 + .../src/smithy4s/dynamic/DynamicService.scala | 38 ++++ .../src/smithy4s/dynamic/KeyedSchema.scala | 13 ++ .../dynamic/src/smithy4s/dynamic/Name.scala | 10 + .../src/smithy4s/dynamic/NodeToDoc.scala | 37 ++++ .../src/smithy4s/dynamic/package.scala | 10 + 10 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/DynamicService.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/Name.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/NodeToDoc.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/package.scala diff --git a/build.sbt b/build.sbt index 9c6ac5da1..e103acc90 100644 --- a/build.sbt +++ b/build.sbt @@ -58,7 +58,7 @@ lazy val allModules = Seq( lazy val docs = projectMatrix - .in(file("module/docs")) + .in(file("modules/docs")) .enablePlugins(MdocPlugin) .jvmPlatform(List(Scala213)) .dependsOn( @@ -391,6 +391,27 @@ lazy val protocol = projectMatrix ) ) +/** + * This modules contains utilities to dynamically instantiate + * the interfaces provide by smithy4s, based on data from dynamic + * Model instances. + */ +lazy val dynamic = projectMatrix + .in(file("modules/dynamic")) + .dependsOn(core) + .settings( + isCE3 := true, + libraryDependencies ++= Seq( + Dependencies.Smithy.model, + "org.scala-lang.modules" %% "scala-collection-compat" % "2.6.0", + Dependencies.Weaver.cats.value % Test + ), + Test / fork := true, + testFrameworks += new TestFramework("weaver.framework.CatsEffect") + ) + .jvmPlatform(allJvmScalaVersions, jvmDimSettings) + .jsPlatform(allJsScalaVersions, jsDimSettings) + /** * Module that contains the logic for generating "openapi views" of the * services that abide by some custom protocols provided by this library. diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala new file mode 100644 index 000000000..5dbb8a190 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala @@ -0,0 +1,21 @@ +package smithy4s +package dynamic + +case class DynamicEndpoint( + namespace: String, + name: String, + input: Schema[DynData], + output: Schema[DynData], + hints: Hints +) extends Endpoint[DynamicOp, DynData, DynData, DynData, Nothing, Nothing] { + + def wrap( + input: DynData + ): DynamicOp[DynData, DynData, DynData, Nothing, Nothing] = + DynamicOp(namespace, name, input) + + def streamedInput: StreamingSchema[Nothing] = StreamingSchema.NoStream + + def streamedOutput: StreamingSchema[Nothing] = StreamingSchema.NoStream + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala new file mode 100644 index 000000000..c3b22d8d9 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala @@ -0,0 +1,20 @@ +package smithy4s +package dynamic + +import software.amazon.smithy.model.shapes.ShapeId + +class DynamicModel( + serviceMap: Map[ShapeId, DynamicService], + schemaMap: Map[ShapeId, Schema[DynData]] +) { + + def allServices: List[DynamicService] = serviceMap.values.toList + + def getSchema( + namespace: String, + name: String + ): Option[Schema[DynData]] = { + val shapeId = ShapeId.fromParts(namespace, name) + schemaMap.get(shapeId) + } +} diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala new file mode 100644 index 000000000..83a8e0843 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -0,0 +1,171 @@ +package smithy4s +package dynamic + +import software.amazon.smithy.model.Model +import scala.collection.mutable.{Map => MMap} +import schematic.OneOf +import schematic.StructureField +import software.amazon.smithy.model.shapes._ +import smithy4s.syntax._ +import scala.jdk.CollectionConverters._ +import software.amazon.smithy.model.traits._ +import software.amazon.smithy.model.traits +import software.amazon.smithy.model.node.Node +import java.util.Optional + +object Compiler { + + type DynSchema = Schema[DynData] + type DynFieldSchema = StructureField[Schematic, DynStruct, DynData] + type DynAltSchema = OneOf[Schematic, DynAlt, DynData] + + def compile(model: Model, knownHints: KeyedSchema[_]*): DynamicModel = { + val schemaMap = MMap.empty[ShapeId, Schema[DynData]] + val endpointMap = MMap.empty[ShapeId, DynamicEndpoint] + val serviceMap = MMap.empty[ShapeId, DynamicService] + + val hintsMap: Map[ShapeId, KeyedSchema[_]] = knownHints.map { ks => + val shapeId: ShapeId = + ShapeId.fromParts(ks.hintKey.namespace, ks.hintKey.name) + shapeId -> ks + }.toMap + + def toHintAux[A](ks: KeyedSchema[A], node: Node): Option[Hint] = { + val documentRepr: Document = NodeToDoc(node) + val decoded: Option[A] = + Document.Decoder.fromSchema(ks.schema).decode(documentRepr).toOption + decoded.map { value => + Hints.Binding(ks.hintKey, value) + } + } + + def toHint(tr: traits.Trait): Option[Hint] = { + val id = tr.toShapeId() + val node = tr.toNode() + hintsMap.get(id).flatMap(toHintAux(_, node)) + } + + val visitor = + new CompileVisitor(model, schemaMap, endpointMap, serviceMap, toHint(_)) + model.shapes().iterator().asScala.foreach(_.accept(visitor)) + new DynamicModel(serviceMap.toMap, schemaMap.toMap) + } + + class CompileVisitor( + model: Model, + schemaMap: MMap[ShapeId, Schema[DynData]], + endpointMap: MMap[ShapeId, DynamicEndpoint], + serviceMap: MMap[ShapeId, DynamicService], + toHint: traits.Trait => Option[Hint] + ) extends ShapeVisitor.Default[Unit] { + + def allHints(shape: Shape): Seq[Hint] = { + shape + .getAllTraits() + .asScala + .values + .map(toHint) + .collect { case Some(h) => + h + } + .toSeq + } + + def allHints(shapeId: ShapeId): Seq[Hint] = + allHints(model.expectShape(shapeId)) + + def update[A](shape: Shape, schema: Schema[A]): Unit = { + schemaMap += (shape.getId -> schema + .withHints(allHints(shape): _*) + .withHints(Name(shape.getId.getName())) + .asInstanceOf[Schema[DynData]]) + } + + def getDefault(shape: Shape): Unit = () + + override def integerShape(shape: IntegerShape): Unit = + update(shape, int) + + override def booleanShape(shape: BooleanShape): Unit = + update(shape, boolean) + + override def stringShape(shape: StringShape): Unit = + update(shape, string) + + override def listShape(shape: ListShape): Unit = + update(shape, list(suspend(schemaMap(shape.getMember.getTarget)))) + + override def setShape(shape: SetShape): Unit = + update(shape, set(suspend(schemaMap(shape.getMember.getTarget)))) + + override def operationShape(shape: OperationShape): Unit = { + def maybeSchema(maybeShapeId: Optional[ShapeId]): Schema[DynData] = + suspend( + maybeShapeId + .map[Schema[DynData]](id => schemaMap(id)) + .orElseGet(() => unit.asInstanceOf[Schema[DynData]]) + ) + + val ep = DynamicEndpoint( + shape.getId.getNamespace(), + shape.getId.getName(), + maybeSchema(shape.getInput()), + maybeSchema(shape.getOutput()), + Hints(allHints(shape): _*) + ) + endpointMap += shape.getId() -> ep + } + + override def serviceShape(shape: ServiceShape): Unit = { + val getEndpoints = () => + shape.getOperations().asScala.map(opId => endpointMap(opId)).toList + val service = DynamicService( + shape.getId().getNamespace(), + shape.getId().getName(), + shape.getVersion(), + getEndpoints, + Hints(allHints(shape): _*) + ) + serviceMap += shape.getId() -> service + } + + // Creates a dynamic structure array, unpacking options + // when needed + private final def dynStruct(fields: Vector[Any]): DynStruct = { + val array = Array.ofDim[Any](fields.size) + var i = 0 + fields.foreach { + case None => i += 1 // leaving value to null + case Some(value) => (array(i) = value); i += 1 + case other => (array(i) = other); i += 1 + } + array + } + + override def structureShape(shape: StructureShape): Unit = + update( + shape, { + val shapeId = shape.getId() + val namespace = shapeId.getNamespace() + val shapeName = shapeId.getName() + val members = shape.getAllMembers().asScala + val fields = + members.zipWithIndex.map { case ((label, mShape), index) => + val memberId = ShapeId.fromParts(namespace, shapeName, label) + val memberHints = allHints(memberId) + val memberSchema = + suspend(schemaMap(mShape.getTarget())) + .withHints(memberHints: _*) + if (mShape.getTrait(classOf[RequiredTrait]).isPresent()) { + memberSchema.required[DynStruct](label, _(index)) + } else { + memberSchema + .optional[DynStruct](label, arr => Option(arr(index))) + } + }.toVector + genericStruct(fields)(dynStruct) + } + ) + } + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala new file mode 100644 index 000000000..d6fa16a5a --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala @@ -0,0 +1,7 @@ +package smithy4s.dynamic + +case class DynamicOp[I, E, O, SI, SO]( + namespace: String, + name: String, + data: I +) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala new file mode 100644 index 000000000..4a7ddbab6 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala @@ -0,0 +1,38 @@ +package smithy4s +package dynamic + +case class DynamicService( + namespace: String, + name: String, + version: String, + getEndpoints: () => List[DynamicEndpoint], + hints: Hints +) extends Service[DynamicAlg, DynamicOp] { + + def transform[F[_, _, _, _, _], G[_, _, _, _, _]]( + alg: DynamicAlg[F], + transformation: Transformation[F, G] + ): DynamicAlg[G] = alg.andThen(transformation) + + def endpoints: List[Endpoint[DynamicOp, _, _, _, _, _]] = getEndpoints() + + def endpoint[I, E, O, SI, SO]( + op: DynamicOp[I, E, O, SI, SO] + ): (I, Endpoint[DynamicOp, I, E, O, SI, SO]) = { + val endpoint = endpoints + .find(ep => ep.name == op.name) + .get + .asInstanceOf[Endpoint[DynamicOp, I, E, O, SI, SO]] + val input = op.data + (input, endpoint) + } + + def transform[P[_, _, _, _, _]]( + transformation: Transformation[DynamicOp, P] + ): DynamicAlg[P] = transformation + + def asTransformationGen[P[_, _, _, _, _]]( + impl: DynamicAlg[P] + ): Transformation[DynamicOp, P] = impl + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala b/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala new file mode 100644 index 000000000..7301b9244 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala @@ -0,0 +1,13 @@ +package smithy4s +package dynamic + +case class KeyedSchema[A](schema: Schema[A], hintKey: Hints.Key[A]) + +case object KeyedSchema { + + implicit def fromKey[A](key: Hints.Key[A])(implicit + schema: Schema[A] + ): KeyedSchema[A] = + KeyedSchema[A](schema, key) + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/Name.scala b/modules/dynamic/src/smithy4s/dynamic/Name.scala new file mode 100644 index 000000000..6c3f7949f --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/Name.scala @@ -0,0 +1,10 @@ +package smithy4s.dynamic + +import smithy4s.Hints + +case class Name(name: String) + +object Name extends Hints.Key.Companion[Name] { + val namespace: String = "smithy4s.dynamic" + val name = "Name" +} diff --git a/modules/dynamic/src/smithy4s/dynamic/NodeToDoc.scala b/modules/dynamic/src/smithy4s/dynamic/NodeToDoc.scala new file mode 100644 index 000000000..a8c031e47 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/NodeToDoc.scala @@ -0,0 +1,37 @@ +package smithy4s.dynamic + +import smithy4s.Document +import software.amazon.smithy.model.node._ +import scala.jdk.CollectionConverters._ + +object NodeToDoc { + + def apply(node: Node): Document = + return node.accept(new NodeVisitor[Document] { + def arrayNode(x: ArrayNode): Document = + Document.array(x.getElements().asScala.map(_.accept(this))) + + def booleanNode(x: BooleanNode): Document = + Document.fromBoolean(x.getValue()) + + def nullNode(x: NullNode): Document = + Document.DNull + + def numberNode(x: NumberNode): Document = + Document.fromDouble(x.getValue().doubleValue()) + + def objectNode(x: ObjectNode): Document = + Document.obj( + x.getMembers() + .asScala + .map { case (key, value) => + key.getValue() -> value.accept(this) + } + .toSeq: _* + ) + + def stringNode(x: StringNode): Document = + Document.fromString(x.getValue()) + }) + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/package.scala b/modules/dynamic/src/smithy4s/dynamic/package.scala new file mode 100644 index 000000000..3ee0a25c1 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/package.scala @@ -0,0 +1,10 @@ +package smithy4s + +package object dynamic { + + type DynData = Any + type DynStruct = Array[DynData] + type DynAlt = (Int, DynData) + + type DynamicAlg[F[_, _, _, _, _]] = Transformation[DynamicOp, F] +} From 217be2636407e65c4c85982681adefc0139a6724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Tue, 28 Dec 2021 17:10:56 +0100 Subject: [PATCH 02/28] Towards cross-compilable dynamic --- build.sbt | 10 +- modules/dynamic/smithy/dynamic.smithy | 179 ++++++++++++++++++ .../src/smithy4s/dynamic/DynamicModel.scala | 2 - .../dynamic/DynamicModelCompiler.scala | 157 ++++++++------- .../src/smithy4s/dynamic/ShapeId.scala | 13 ++ .../src/smithy4s/dynamic/ShapeVisitor.scala | 82 ++++++++ .../src/smithy4s/dynamic/package.scala | 1 + 7 files changed, 375 insertions(+), 69 deletions(-) create mode 100644 modules/dynamic/smithy/dynamic.smithy create mode 100644 modules/dynamic/src/smithy4s/dynamic/ShapeId.scala create mode 100644 modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala diff --git a/build.sbt b/build.sbt index e103acc90..f38c90622 100644 --- a/build.sbt +++ b/build.sbt @@ -407,7 +407,15 @@ lazy val dynamic = projectMatrix Dependencies.Weaver.cats.value % Test ), Test / fork := true, - testFrameworks += new TestFramework("weaver.framework.CatsEffect") + testFrameworks += new TestFramework("weaver.framework.CatsEffect"), + // + Compile / allowedNamespaces := Seq( + "smithy4s.dynamic.model" + ), + Compile / smithySpecs := Seq( + (ThisBuild / baseDirectory).value / "modules" / "dynamic" / "smithy" / "dynamic.smithy" + ), + (Compile / sourceGenerators) := Seq(genSmithyScala(Compile).taskValue) ) .jvmPlatform(allJvmScalaVersions, jvmDimSettings) .jsPlatform(allJsScalaVersions, jsDimSettings) diff --git a/modules/dynamic/smithy/dynamic.smithy b/modules/dynamic/smithy/dynamic.smithy new file mode 100644 index 000000000..18368e08f --- /dev/null +++ b/modules/dynamic/smithy/dynamic.smithy @@ -0,0 +1,179 @@ +metadata suppressions = [ + { + id: "UnreferencedShape", + namespace: "smithy4s.dynamic.model", + reason: "This is a library namespace." + } +] + +namespace smithy4s.dynamic.model + +/// This is a best-effort meta-representation of the smithy-model, that we should be able +/// to deserialise from Json. +structure Model { + smithy: String, + metadata: MetadataMap, + @required + shapes: ShapeMap +} + +string ShapeId + +map MetadataMap { + key: String, + value: Document +} + +map ShapeMap { + key: ShapeId, + value: Shape +} + +map TraitMap { + key: ShapeId, + value: Document +} + +union Shape { + blob: BlobShape, + byte: ByteShape, + string: StringShape, + boolean: BooleanShape, + integer: IntegerShape, + short: ShortShape, + long: LongShape, + double: DoubleShape, + float: FloatShape, + bigDecimal: BigDecimalShape, + bigInteger: BigIntegerShape, + document: DocumentShape, + timestamp: TimestampShape, + list: ListShape, + set: SetShape, + map: MapShape, + structure: StructureShape, + union: UnionShape, + operation: OperationShape, + service: ServiceShape, + resource: ResourceShape +} + +structure StringShape { + traits: TraitMap +} + +structure BlobShape { + traits: TraitMap +} + +structure ByteShape { + traits: TraitMap +} + +structure BooleanShape { + traits: TraitMap +} + +structure IntegerShape { + traits: TraitMap +} + +structure LongShape { + traits: TraitMap +} + +structure ShortShape { + traits: TraitMap +} + +structure FloatShape { + traits: TraitMap +} + +structure DoubleShape { + traits: TraitMap +} + +structure BigDecimalShape { + traits: TraitMap +} + +structure BigIntegerShape { + traits: TraitMap +} + +structure DocumentShape { + traits: TraitMap +} + +structure TimestampShape { + traits: TraitMap +} + +structure ListShape { + @required + member: MemberShape, + traits: TraitMap +} + +structure SetShape { + @required + member: MemberShape, + traits: TraitMap +} + +structure MapShape { + @required + key: MemberShape, + @required + value: MemberShape, + traits: TraitMap +} + +structure MemberShape { + @required + target: ShapeId, + traits: TraitMap, +} + +list MemberList { + member: MemberShape +} + +map MemberMap { + key: String, + value: MemberShape +} + +structure StructureShape { + members: MemberMap, + traits: TraitMap +} + +structure UnionShape { + members: MemberMap, + traits: TraitMap +} + +structure OperationShape { + input: MemberShape, + output: MemberShape, + errors: MemberList, + traits: TraitMap +} + +structure ServiceShape { + version: String, + errors: MemberList, + operations: MemberShape, + traits: TraitMap +} + +/// TODO +structure ResourceShape { +} + + + + + diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala index c3b22d8d9..9958394bc 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala @@ -1,8 +1,6 @@ package smithy4s package dynamic -import software.amazon.smithy.model.shapes.ShapeId - class DynamicModel( serviceMap: Map[ShapeId, DynamicService], schemaMap: Map[ShapeId, Schema[DynData]] diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index 83a8e0843..40b13d23c 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -1,17 +1,12 @@ package smithy4s package dynamic -import software.amazon.smithy.model.Model +// import software.amazon.smithy.model.Model +import smithy4s.dynamic.model.{ShapeId => SID, _} import scala.collection.mutable.{Map => MMap} import schematic.OneOf import schematic.StructureField -import software.amazon.smithy.model.shapes._ import smithy4s.syntax._ -import scala.jdk.CollectionConverters._ -import software.amazon.smithy.model.traits._ -import software.amazon.smithy.model.traits -import software.amazon.smithy.model.node.Node -import java.util.Optional object Compiler { @@ -19,6 +14,19 @@ object Compiler { type DynFieldSchema = StructureField[Schematic, DynStruct, DynData] type DynAltSchema = OneOf[Schematic, DynAlt, DynData] + object ValidSID { + def apply(sid: SID): Option[ShapeId] = { + val segments = sid.value.split('#') + if (segments.length == 2) { + Some(ShapeId(segments(0), segments(1))) + } else None + } + + def unapply(sid: SID): Option[ShapeId] = apply(sid) + } + + def toSID(shapeId: ShapeId): SID = SID(shapeId.namespace + "#" + shapeId.name) + def compile(model: Model, knownHints: KeyedSchema[_]*): DynamicModel = { val schemaMap = MMap.empty[ShapeId, Schema[DynData]] val endpointMap = MMap.empty[ShapeId, DynamicEndpoint] @@ -30,8 +38,10 @@ object Compiler { shapeId -> ks }.toMap - def toHintAux[A](ks: KeyedSchema[A], node: Node): Option[Hint] = { - val documentRepr: Document = NodeToDoc(node) + def toHintAux[A]( + ks: KeyedSchema[A], + documentRepr: Document + ): Option[Hint] = { val decoded: Option[A] = Document.Decoder.fromSchema(ks.schema).decode(documentRepr).toOption decoded.map { value => @@ -39,15 +49,23 @@ object Compiler { } } - def toHint(tr: traits.Trait): Option[Hint] = { - val id = tr.toShapeId() - val node = tr.toNode() - hintsMap.get(id).flatMap(toHintAux(_, node)) + def toHint(id: ShapeId, tr: Document): Option[Hint] = { + hintsMap.get(id).flatMap(toHintAux(_, tr)) } val visitor = - new CompileVisitor(model, schemaMap, endpointMap, serviceMap, toHint(_)) - model.shapes().iterator().asScala.foreach(_.accept(visitor)) + new CompileVisitor( + model, + schemaMap, + endpointMap, + serviceMap, + toHint(_, _) + ) + + model.shapes.foreach { + case (ValidSID(id), shape) => visitor(id, shape) + case _ => () + } new DynamicModel(serviceMap.toMap, schemaMap.toMap) } @@ -56,77 +74,83 @@ object Compiler { schemaMap: MMap[ShapeId, Schema[DynData]], endpointMap: MMap[ShapeId, DynamicEndpoint], serviceMap: MMap[ShapeId, DynamicService], - toHint: traits.Trait => Option[Hint] + toHint: (ShapeId, Document) => Option[Hint] ) extends ShapeVisitor.Default[Unit] { - def allHints(shape: Shape): Seq[Hint] = { - shape - .getAllTraits() - .asScala - .values - .map(toHint) + def schema(sid: SID): Schema[DynData] = schemaMap(ValidSID.unapply(sid).get) + + def allHints(traits: Option[Map[SID, Document]]): Seq[Hint] = { + traits + .getOrElse(Map.empty) + .collect { case (ValidSID(k), v) => + toHint(k, v) + } .collect { case Some(h) => h } .toSeq } - def allHints(shapeId: ShapeId): Seq[Hint] = - allHints(model.expectShape(shapeId)) - - def update[A](shape: Shape, schema: Schema[A]): Unit = { - schemaMap += (shape.getId -> schema - .withHints(allHints(shape): _*) - .withHints(Name(shape.getId.getName())) + def update[A]( + shapeId: ShapeId, + traits: Option[Map[SID, Document]], + schema: Schema[A] + ): Unit = { + schemaMap += (shapeId -> schema + .withHints(allHints(traits): _*) + .withHints(Name(shapeId.name)) .asInstanceOf[Schema[DynData]]) } - def getDefault(shape: Shape): Unit = () + def default: Unit = () - override def integerShape(shape: IntegerShape): Unit = - update(shape, int) + override def integerShape(id: ShapeId, shape: IntegerShape): Unit = + update(id, shape.traits, int) - override def booleanShape(shape: BooleanShape): Unit = - update(shape, boolean) + override def booleanShape(id: ShapeId, shape: BooleanShape): Unit = + update(id, shape.traits, boolean) - override def stringShape(shape: StringShape): Unit = - update(shape, string) + override def stringShape(id: ShapeId, shape: StringShape): Unit = + update(id, shape.traits, string) - override def listShape(shape: ListShape): Unit = - update(shape, list(suspend(schemaMap(shape.getMember.getTarget)))) + override def listShape(id: ShapeId, shape: ListShape): Unit = + update(id, shape.traits, list(suspend(schema(shape.member.target)))) - override def setShape(shape: SetShape): Unit = - update(shape, set(suspend(schemaMap(shape.getMember.getTarget)))) + override def setShape(id: ShapeId, shape: SetShape): Unit = + update(id, shape.traits, set(suspend(schema(shape.member.target)))) - override def operationShape(shape: OperationShape): Unit = { - def maybeSchema(maybeShapeId: Optional[ShapeId]): Schema[DynData] = + override def operationShape(id: ShapeId, shape: OperationShape): Unit = { + def maybeSchema(maybeShapeId: Option[SID]): Schema[DynData] = suspend( maybeShapeId + .flatMap(ValidSID(_)) .map[Schema[DynData]](id => schemaMap(id)) - .orElseGet(() => unit.asInstanceOf[Schema[DynData]]) + .getOrElse(unit.asInstanceOf[Schema[DynData]]) ) val ep = DynamicEndpoint( - shape.getId.getNamespace(), - shape.getId.getName(), - maybeSchema(shape.getInput()), - maybeSchema(shape.getOutput()), - Hints(allHints(shape): _*) + id.namespace, + id.name, + maybeSchema(shape.input.map(_.target)), + maybeSchema(shape.output.map(_.target)), + Hints(allHints(shape.traits): _*) ) - endpointMap += shape.getId() -> ep + endpointMap += id -> ep } - override def serviceShape(shape: ServiceShape): Unit = { + override def serviceShape(id: ShapeId, shape: ServiceShape): Unit = { val getEndpoints = () => - shape.getOperations().asScala.map(opId => endpointMap(opId)).toList + shape.operations.toList.map(_.target).collect { case ValidSID(opId) => + endpointMap(opId) + } val service = DynamicService( - shape.getId().getNamespace(), - shape.getId().getName(), - shape.getVersion(), + id.namespace, + id.name, + shape.version.getOrElse(""), getEndpoints, - Hints(allHints(shape): _*) + Hints(allHints(shape.traits): _*) ) - serviceMap += shape.getId() -> service + serviceMap += id -> service } // Creates a dynamic structure array, unpacking options @@ -142,21 +166,22 @@ object Compiler { array } - override def structureShape(shape: StructureShape): Unit = + override def structureShape(id: ShapeId, shape: StructureShape): Unit = update( - shape, { - val shapeId = shape.getId() - val namespace = shapeId.getNamespace() - val shapeName = shapeId.getName() - val members = shape.getAllMembers().asScala + id, + shape.traits, { + val members = shape.members.getOrElse(Map.empty) val fields = members.zipWithIndex.map { case ((label, mShape), index) => - val memberId = ShapeId.fromParts(namespace, shapeName, label) - val memberHints = allHints(memberId) + val memberHints = allHints(mShape.traits) val memberSchema = - suspend(schemaMap(mShape.getTarget())) + suspend(schema(mShape.target)) .withHints(memberHints: _*) - if (mShape.getTrait(classOf[RequiredTrait]).isPresent()) { + if ( + mShape.traits + .getOrElse(Map.empty) + .contains(SID("smithy.api#required")) + ) { memberSchema.required[DynStruct](label, _(index)) } else { memberSchema diff --git a/modules/dynamic/src/smithy4s/dynamic/ShapeId.scala b/modules/dynamic/src/smithy4s/dynamic/ShapeId.scala new file mode 100644 index 000000000..c25dae250 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/ShapeId.scala @@ -0,0 +1,13 @@ +package smithy4s.dynamic + +case class ShapeId(namespace: String, name: String) { + def withMember(member: String): ShapeId.Member = ShapeId.Member(this, member) +} + +object ShapeId { + + case class Member(shapeId: ShapeId, member: String) + + def fromParts(namespace: String, name: String) = ShapeId(namespace, name) + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala b/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala new file mode 100644 index 000000000..5874213e2 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala @@ -0,0 +1,82 @@ +package smithy4s.dynamic + +import model.{ShapeId => _, _} + +trait ShapeVisitor[T] extends ((ShapeId, Shape) => T) { + + def apply(id: ShapeId, shape: Shape): T = shape match { + case Shape.BlobCase(s) => blobShape(id, s) + case Shape.BooleanCase(s) => booleanShape(id, s) + case Shape.ListCase(s) => listShape(id, s) + case Shape.SetCase(s) => setShape(id, s) + case Shape.MapCase(s) => mapShape(id, s) + case Shape.ByteCase(s) => byteShape(id, s) + case Shape.ShortCase(s) => shortShape(id, s) + case Shape.IntegerCase(s) => integerShape(id, s) + case Shape.LongCase(s) => longShape(id, s) + case Shape.FloatCase(s) => floatShape(id, s) + case Shape.DocumentCase(s) => documentShape(id, s) + case Shape.DoubleCase(s) => doubleShape(id, s) + case Shape.BigIntegerCase(s) => bigIntegerShape(id, s) + case Shape.BigDecimalCase(s) => bigDecimalShape(id, s) + case Shape.OperationCase(s) => operationShape(id, s) + case Shape.ResourceCase(s) => resourceShape(id, s) + case Shape.ServiceCase(s) => serviceShape(id, s) + case Shape.StringCase(s) => stringShape(id, s) + case Shape.StructureCase(s) => structureShape(id, s) + case Shape.UnionCase(s) => unionShape(id, s) + case Shape.TimestampCase(s) => timestampShape(id, s) + } + + def blobShape(id: ShapeId, x: BlobShape): T + def booleanShape(id: ShapeId, x: BooleanShape): T + def listShape(id: ShapeId, x: ListShape): T + def setShape(id: ShapeId, x: SetShape): T + def mapShape(id: ShapeId, x: MapShape): T + def byteShape(id: ShapeId, x: ByteShape): T + def shortShape(id: ShapeId, x: ShortShape): T + def integerShape(id: ShapeId, x: IntegerShape): T + def longShape(id: ShapeId, x: LongShape): T + def floatShape(id: ShapeId, x: FloatShape): T + def documentShape(id: ShapeId, x: DocumentShape): T + def doubleShape(id: ShapeId, x: DoubleShape): T + def bigIntegerShape(id: ShapeId, x: BigIntegerShape): T + def bigDecimalShape(id: ShapeId, x: BigDecimalShape): T + def operationShape(id: ShapeId, x: OperationShape): T + def resourceShape(id: ShapeId, x: ResourceShape): T + def serviceShape(id: ShapeId, x: ServiceShape): T + def stringShape(id: ShapeId, x: StringShape): T + def structureShape(id: ShapeId, x: StructureShape): T + def unionShape(id: ShapeId, x: UnionShape): T + def timestampShape(id: ShapeId, x: TimestampShape): T +} + +object ShapeVisitor { + + trait Default[T] extends ShapeVisitor[T] { + def default: T + + def blobShape(id: ShapeId, x: BlobShape): T = default + def booleanShape(id: ShapeId, x: BooleanShape): T = default + def listShape(id: ShapeId, x: ListShape): T = default + def setShape(id: ShapeId, x: SetShape): T = default + def mapShape(id: ShapeId, x: MapShape): T = default + def byteShape(id: ShapeId, x: ByteShape): T = default + def shortShape(id: ShapeId, x: ShortShape): T = default + def integerShape(id: ShapeId, x: IntegerShape): T = default + def longShape(id: ShapeId, x: LongShape): T = default + def floatShape(id: ShapeId, x: FloatShape): T = default + def documentShape(id: ShapeId, x: DocumentShape): T = default + def doubleShape(id: ShapeId, x: DoubleShape): T = default + def bigIntegerShape(id: ShapeId, x: BigIntegerShape): T = default + def bigDecimalShape(id: ShapeId, x: BigDecimalShape): T = default + def operationShape(id: ShapeId, x: OperationShape): T = default + def resourceShape(id: ShapeId, x: ResourceShape): T = default + def serviceShape(id: ShapeId, x: ServiceShape): T = default + def stringShape(id: ShapeId, x: StringShape): T = default + def structureShape(id: ShapeId, x: StructureShape): T = default + def unionShape(id: ShapeId, x: UnionShape): T = default + def timestampShape(id: ShapeId, x: TimestampShape): T = default + } + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/package.scala b/modules/dynamic/src/smithy4s/dynamic/package.scala index 3ee0a25c1..961da4951 100644 --- a/modules/dynamic/src/smithy4s/dynamic/package.scala +++ b/modules/dynamic/src/smithy4s/dynamic/package.scala @@ -7,4 +7,5 @@ package object dynamic { type DynAlt = (Int, DynData) type DynamicAlg[F[_, _, _, _, _]] = Transformation[DynamicOp, F] + } From 5db12e348dfd21c68f8c33c5c25a6ea00a6b189f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Tue, 4 Jan 2022 18:13:25 +0100 Subject: [PATCH 03/28] Fix model schema (multiple lists), remove old dependency and unused code --- build.sbt | 3 +- modules/dynamic/smithy/dynamic.smithy | 2 +- .../dynamic/DynamicModelCompiler.scala | 5 ++- .../src/smithy4s/dynamic/NodeToDoc.scala | 37 ------------------- 4 files changed, 5 insertions(+), 42 deletions(-) delete mode 100644 modules/dynamic/src/smithy4s/dynamic/NodeToDoc.scala diff --git a/build.sbt b/build.sbt index f38c90622..acd75a1d6 100644 --- a/build.sbt +++ b/build.sbt @@ -402,8 +402,7 @@ lazy val dynamic = projectMatrix .settings( isCE3 := true, libraryDependencies ++= Seq( - Dependencies.Smithy.model, - "org.scala-lang.modules" %% "scala-collection-compat" % "2.6.0", + "org.scala-lang.modules" %%% "scala-collection-compat" % "2.6.0", Dependencies.Weaver.cats.value % Test ), Test / fork := true, diff --git a/modules/dynamic/smithy/dynamic.smithy b/modules/dynamic/smithy/dynamic.smithy index 18368e08f..ccfebcff3 100644 --- a/modules/dynamic/smithy/dynamic.smithy +++ b/modules/dynamic/smithy/dynamic.smithy @@ -165,7 +165,7 @@ structure OperationShape { structure ServiceShape { version: String, errors: MemberList, - operations: MemberShape, + operations: MemberList, traits: TraitMap } diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index 40b13d23c..c987720e7 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -140,8 +140,9 @@ object Compiler { override def serviceShape(id: ShapeId, shape: ServiceShape): Unit = { val getEndpoints = () => - shape.operations.toList.map(_.target).collect { case ValidSID(opId) => - endpointMap(opId) + shape.operations.toList.flatMap(_.map(_.target)).collect { + case ValidSID(opId) => + endpointMap(opId) } val service = DynamicService( id.namespace, diff --git a/modules/dynamic/src/smithy4s/dynamic/NodeToDoc.scala b/modules/dynamic/src/smithy4s/dynamic/NodeToDoc.scala deleted file mode 100644 index a8c031e47..000000000 --- a/modules/dynamic/src/smithy4s/dynamic/NodeToDoc.scala +++ /dev/null @@ -1,37 +0,0 @@ -package smithy4s.dynamic - -import smithy4s.Document -import software.amazon.smithy.model.node._ -import scala.jdk.CollectionConverters._ - -object NodeToDoc { - - def apply(node: Node): Document = - return node.accept(new NodeVisitor[Document] { - def arrayNode(x: ArrayNode): Document = - Document.array(x.getElements().asScala.map(_.accept(this))) - - def booleanNode(x: BooleanNode): Document = - Document.fromBoolean(x.getValue()) - - def nullNode(x: NullNode): Document = - Document.DNull - - def numberNode(x: NumberNode): Document = - Document.fromDouble(x.getValue().doubleValue()) - - def objectNode(x: ObjectNode): Document = - Document.obj( - x.getMembers() - .asScala - .map { case (key, value) => - key.getValue() -> value.accept(this) - } - .toSeq: _* - ) - - def stringNode(x: StringNode): Document = - Document.fromString(x.getValue()) - }) - -} From a4a1cc87731328737a9fac921a8f6fed48d4bac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Tue, 4 Jan 2022 22:17:41 +0100 Subject: [PATCH 04/28] Update after merge --- .../dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala | 1 - modules/dynamic/src/smithy4s/dynamic/DynamicService.scala | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index c987720e7..b1afc01d3 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -1,7 +1,6 @@ package smithy4s package dynamic -// import software.amazon.smithy.model.Model import smithy4s.dynamic.model.{ShapeId => SID, _} import scala.collection.mutable.{Map => MMap} import schematic.OneOf diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala index 4a7ddbab6..003a7e969 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala @@ -31,8 +31,7 @@ case class DynamicService( transformation: Transformation[DynamicOp, P] ): DynamicAlg[P] = transformation - def asTransformationGen[P[_, _, _, _, _]]( + def asTransformation[P[_, _, _, _, _]]( impl: DynamicAlg[P] ): Transformation[DynamicOp, P] = impl - } From 41e5d1fea6e7fc825a397828578c0cc727d8ca28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Tue, 4 Jan 2022 22:18:44 +0100 Subject: [PATCH 05/28] Add I/O hints --- .../dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index b1afc01d3..7fc5fe69c 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -6,6 +6,7 @@ import scala.collection.mutable.{Map => MMap} import schematic.OneOf import schematic.StructureField import smithy4s.syntax._ +import smithy4s.internals.InputOutput object Compiler { @@ -130,8 +131,8 @@ object Compiler { val ep = DynamicEndpoint( id.namespace, id.name, - maybeSchema(shape.input.map(_.target)), - maybeSchema(shape.output.map(_.target)), + maybeSchema(shape.input.map(_.target)).withHints(InputOutput.Input), + maybeSchema(shape.output.map(_.target)).withHints(InputOutput.Output), Hints(allHints(shape.traits): _*) ) endpointMap += id -> ep From a83ae61fec5ece4e80d1ecc568078615aab410cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Wed, 5 Jan 2022 12:32:38 +0100 Subject: [PATCH 06/28] Update dynamic with new ShapeId model --- modules/core/src/smithy4s/Endpoint.scala | 3 +- modules/core/src/smithy4s/ShapeId.scala | 3 ++ modules/dynamic/smithy/dynamic.smithy | 8 ++-- .../smithy4s/dynamic/DynamicEndpoint.scala | 5 +- .../src/smithy4s/dynamic/DynamicModel.scala | 2 +- .../dynamic/DynamicModelCompiler.scala | 47 ++++++++++--------- .../src/smithy4s/dynamic/DynamicOp.scala | 5 +- .../src/smithy4s/dynamic/DynamicService.scala | 15 +++--- .../dynamic/src/smithy4s/dynamic/Name.scala | 10 ---- .../src/smithy4s/dynamic/ShapeId.scala | 13 ----- .../src/smithy4s/dynamic/ShapeVisitor.scala | 5 +- 11 files changed, 51 insertions(+), 65 deletions(-) delete mode 100644 modules/dynamic/src/smithy4s/dynamic/Name.scala delete mode 100644 modules/dynamic/src/smithy4s/dynamic/ShapeId.scala diff --git a/modules/core/src/smithy4s/Endpoint.scala b/modules/core/src/smithy4s/Endpoint.scala index ea3a3ae0b..6aed87896 100644 --- a/modules/core/src/smithy4s/Endpoint.scala +++ b/modules/core/src/smithy4s/Endpoint.scala @@ -40,7 +40,8 @@ package smithy4s * be encoded a great many ways, using a greatt many libraries) */ trait Endpoint[Op[_, _, _, _, _], I, E, O, SI, SO] { outer => - def name: String + def id: ShapeId + final def name: String = id.name def input: Schema[I] def output: Schema[O] def streamedInput: StreamingSchema[SI] diff --git a/modules/core/src/smithy4s/ShapeId.scala b/modules/core/src/smithy4s/ShapeId.scala index 93d7e4564..f7d170d66 100644 --- a/modules/core/src/smithy4s/ShapeId.scala +++ b/modules/core/src/smithy4s/ShapeId.scala @@ -18,9 +18,12 @@ package smithy4s case class ShapeId(namespace: String, name: String) { def show = s"$namespace#$name" + def withMember(member: String): ShapeId.Member = ShapeId.Member(this, member) override def toString = show } object ShapeId extends Hints.Key.Companion[ShapeId] { def id: ShapeId = ShapeId("smithy4s", "ShapeId") + + case class Member(shapeId: ShapeId, member: String) } diff --git a/modules/dynamic/smithy/dynamic.smithy b/modules/dynamic/smithy/dynamic.smithy index ccfebcff3..745cabe8b 100644 --- a/modules/dynamic/smithy/dynamic.smithy +++ b/modules/dynamic/smithy/dynamic.smithy @@ -17,7 +17,7 @@ structure Model { shapes: ShapeMap } -string ShapeId +string IdRef map MetadataMap { key: String, @@ -25,12 +25,12 @@ map MetadataMap { } map ShapeMap { - key: ShapeId, + key: IdRef, value: Shape } map TraitMap { - key: ShapeId, + key: IdRef, value: Document } @@ -132,7 +132,7 @@ structure MapShape { structure MemberShape { @required - target: ShapeId, + target: IdRef, traits: TraitMap, } diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala index 5dbb8a190..16fb33984 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala @@ -2,8 +2,7 @@ package smithy4s package dynamic case class DynamicEndpoint( - namespace: String, - name: String, + id: ShapeId, input: Schema[DynData], output: Schema[DynData], hints: Hints @@ -12,7 +11,7 @@ case class DynamicEndpoint( def wrap( input: DynData ): DynamicOp[DynData, DynData, DynData, Nothing, Nothing] = - DynamicOp(namespace, name, input) + DynamicOp(id, input) def streamedInput: StreamingSchema[Nothing] = StreamingSchema.NoStream diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala index 9958394bc..75e91da2d 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala @@ -12,7 +12,7 @@ class DynamicModel( namespace: String, name: String ): Option[Schema[DynData]] = { - val shapeId = ShapeId.fromParts(namespace, name) + val shapeId = ShapeId(namespace, name) schemaMap.get(shapeId) } } diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index 7fc5fe69c..31b16b42d 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -1,7 +1,7 @@ package smithy4s package dynamic -import smithy4s.dynamic.model.{ShapeId => SID, _} +import smithy4s.dynamic.model._ import scala.collection.mutable.{Map => MMap} import schematic.OneOf import schematic.StructureField @@ -14,18 +14,20 @@ object Compiler { type DynFieldSchema = StructureField[Schematic, DynStruct, DynData] type DynAltSchema = OneOf[Schematic, DynAlt, DynData] - object ValidSID { - def apply(sid: SID): Option[ShapeId] = { - val segments = sid.value.split('#') + object ValidIdRef { + def apply(idRef: IdRef): Option[ShapeId] = { + val segments = idRef.value.split('#') if (segments.length == 2) { Some(ShapeId(segments(0), segments(1))) } else None } - def unapply(sid: SID): Option[ShapeId] = apply(sid) + def unapply(idRef: IdRef): Option[ShapeId] = apply(idRef) } - def toSID(shapeId: ShapeId): SID = SID(shapeId.namespace + "#" + shapeId.name) + def toIdRef(shapeId: ShapeId): IdRef = IdRef( + shapeId.namespace + "#" + shapeId.name + ) def compile(model: Model, knownHints: KeyedSchema[_]*): DynamicModel = { val schemaMap = MMap.empty[ShapeId, Schema[DynData]] @@ -33,8 +35,7 @@ object Compiler { val serviceMap = MMap.empty[ShapeId, DynamicService] val hintsMap: Map[ShapeId, KeyedSchema[_]] = knownHints.map { ks => - val shapeId: ShapeId = - ShapeId.fromParts(ks.hintKey.namespace, ks.hintKey.name) + val shapeId: ShapeId = ks.hintKey.id shapeId -> ks }.toMap @@ -63,8 +64,8 @@ object Compiler { ) model.shapes.foreach { - case (ValidSID(id), shape) => visitor(id, shape) - case _ => () + case (ValidIdRef(id), shape) => visitor(id, shape) + case _ => () } new DynamicModel(serviceMap.toMap, schemaMap.toMap) } @@ -77,12 +78,14 @@ object Compiler { toHint: (ShapeId, Document) => Option[Hint] ) extends ShapeVisitor.Default[Unit] { - def schema(sid: SID): Schema[DynData] = schemaMap(ValidSID.unapply(sid).get) + def schema(idRef: IdRef): Schema[DynData] = schemaMap( + ValidIdRef.unapply(idRef).get + ) - def allHints(traits: Option[Map[SID, Document]]): Seq[Hint] = { + def allHints(traits: Option[Map[IdRef, Document]]): Seq[Hint] = { traits .getOrElse(Map.empty) - .collect { case (ValidSID(k), v) => + .collect { case (ValidIdRef(k), v) => toHint(k, v) } .collect { case Some(h) => @@ -93,12 +96,12 @@ object Compiler { def update[A]( shapeId: ShapeId, - traits: Option[Map[SID, Document]], + traits: Option[Map[IdRef, Document]], schema: Schema[A] ): Unit = { schemaMap += (shapeId -> schema .withHints(allHints(traits): _*) - .withHints(Name(shapeId.name)) + .withHints(shapeId) .asInstanceOf[Schema[DynData]]) } @@ -120,17 +123,16 @@ object Compiler { update(id, shape.traits, set(suspend(schema(shape.member.target)))) override def operationShape(id: ShapeId, shape: OperationShape): Unit = { - def maybeSchema(maybeShapeId: Option[SID]): Schema[DynData] = + def maybeSchema(maybeShapeId: Option[IdRef]): Schema[DynData] = suspend( maybeShapeId - .flatMap(ValidSID(_)) + .flatMap(ValidIdRef(_)) .map[Schema[DynData]](id => schemaMap(id)) .getOrElse(unit.asInstanceOf[Schema[DynData]]) ) val ep = DynamicEndpoint( - id.namespace, - id.name, + id, maybeSchema(shape.input.map(_.target)).withHints(InputOutput.Input), maybeSchema(shape.output.map(_.target)).withHints(InputOutput.Output), Hints(allHints(shape.traits): _*) @@ -141,12 +143,11 @@ object Compiler { override def serviceShape(id: ShapeId, shape: ServiceShape): Unit = { val getEndpoints = () => shape.operations.toList.flatMap(_.map(_.target)).collect { - case ValidSID(opId) => + case ValidIdRef(opId) => endpointMap(opId) } val service = DynamicService( - id.namespace, - id.name, + id, shape.version.getOrElse(""), getEndpoints, Hints(allHints(shape.traits): _*) @@ -181,7 +182,7 @@ object Compiler { if ( mShape.traits .getOrElse(Map.empty) - .contains(SID("smithy.api#required")) + .contains(IdRef("smithy.api#required")) ) { memberSchema.required[DynStruct](label, _(index)) } else { diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala index d6fa16a5a..53f108f86 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala @@ -1,7 +1,8 @@ package smithy4s.dynamic +import smithy4s.ShapeId + case class DynamicOp[I, E, O, SI, SO]( - namespace: String, - name: String, + id: ShapeId, data: I ) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala index 003a7e969..51aff0903 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala @@ -2,8 +2,7 @@ package smithy4s package dynamic case class DynamicService( - namespace: String, - name: String, + id: ShapeId, version: String, getEndpoints: () => List[DynamicEndpoint], hints: Hints @@ -14,14 +13,18 @@ case class DynamicService( transformation: Transformation[F, G] ): DynamicAlg[G] = alg.andThen(transformation) - def endpoints: List[Endpoint[DynamicOp, _, _, _, _, _]] = getEndpoints() + lazy val endpoints: List[Endpoint[DynamicOp, _, _, _, _, _]] = + getEndpoints() + + private lazy val endpointMap + : Map[ShapeId, Endpoint[DynamicOp, _, _, _, _, _]] = + endpoints.map(ep => ep.id -> ep).toMap def endpoint[I, E, O, SI, SO]( op: DynamicOp[I, E, O, SI, SO] ): (I, Endpoint[DynamicOp, I, E, O, SI, SO]) = { - val endpoint = endpoints - .find(ep => ep.name == op.name) - .get + val endpoint = endpointMap + .get(op.id) .asInstanceOf[Endpoint[DynamicOp, I, E, O, SI, SO]] val input = op.data (input, endpoint) diff --git a/modules/dynamic/src/smithy4s/dynamic/Name.scala b/modules/dynamic/src/smithy4s/dynamic/Name.scala deleted file mode 100644 index 6c3f7949f..000000000 --- a/modules/dynamic/src/smithy4s/dynamic/Name.scala +++ /dev/null @@ -1,10 +0,0 @@ -package smithy4s.dynamic - -import smithy4s.Hints - -case class Name(name: String) - -object Name extends Hints.Key.Companion[Name] { - val namespace: String = "smithy4s.dynamic" - val name = "Name" -} diff --git a/modules/dynamic/src/smithy4s/dynamic/ShapeId.scala b/modules/dynamic/src/smithy4s/dynamic/ShapeId.scala deleted file mode 100644 index c25dae250..000000000 --- a/modules/dynamic/src/smithy4s/dynamic/ShapeId.scala +++ /dev/null @@ -1,13 +0,0 @@ -package smithy4s.dynamic - -case class ShapeId(namespace: String, name: String) { - def withMember(member: String): ShapeId.Member = ShapeId.Member(this, member) -} - -object ShapeId { - - case class Member(shapeId: ShapeId, member: String) - - def fromParts(namespace: String, name: String) = ShapeId(namespace, name) - -} diff --git a/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala b/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala index 5874213e2..3fdf4bebe 100644 --- a/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala +++ b/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala @@ -1,6 +1,7 @@ -package smithy4s.dynamic +package smithy4s +package dynamic -import model.{ShapeId => _, _} +import model._ trait ShapeVisitor[T] extends ((ShapeId, Shape) => T) { From 676fa7a34822d55966ae64adb89632daa62c36e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Wed, 5 Jan 2022 13:39:50 +0100 Subject: [PATCH 07/28] do not render operation names --- modules/codegen/src/smithy4s/codegen/Renderer.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/codegen/src/smithy4s/codegen/Renderer.scala b/modules/codegen/src/smithy4s/codegen/Renderer.scala index e7d828c0e..738469b61 100644 --- a/modules/codegen/src/smithy4s/codegen/Renderer.scala +++ b/modules/codegen/src/smithy4s/codegen/Renderer.scala @@ -280,7 +280,6 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => ext = s"$Endpoint_[${traitName}, ${op.renderAlgParams}]$httpEndpoint$errorable" )( - s"""def name: String = "${opName}"""", s"val input: $Schema_[${op.input.render}] = ${op.input.schemaRef}.withHints(smithy4s.internals.InputOutput.Input)", s"val output: $Schema_[${op.output.render}] = ${op.output.schemaRef}.withHints(smithy4s.internals.InputOutput.Output)", renderStreamingSchemaVal("streamedInput", op.streamedInput), From 1d08fb27583489bab63c44db2f939501cc870ffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Tue, 11 Jan 2022 12:52:03 +0100 Subject: [PATCH 08/28] Wire discriminated trait in dynamic model --- build.sbt | 7 +- modules/dynamic/smithy/dynamic.smithy | 4 + .../dynamic/model/DynamicModelSpec.scala | 108 ++++++++++++++++++ .../dynamic/model/NodeToDocument.scala | 37 ++++++ 4 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala create mode 100644 modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala diff --git a/build.sbt b/build.sbt index 891b5dc46..e0e4009fd 100644 --- a/build.sbt +++ b/build.sbt @@ -420,7 +420,12 @@ lazy val dynamic = projectMatrix ), (Compile / sourceGenerators) := Seq(genSmithyScala(Compile).taskValue) ) - .jvmPlatform(allJvmScalaVersions, jvmDimSettings) + .jvmPlatform( + allJvmScalaVersions, + jvmDimSettings ++ Seq( + libraryDependencies += Dependencies.Smithy.model % Test + ) + ) .jsPlatform(allJsScalaVersions, jsDimSettings) /** diff --git a/modules/dynamic/smithy/dynamic.smithy b/modules/dynamic/smithy/dynamic.smithy index 745cabe8b..33ab4d92d 100644 --- a/modules/dynamic/smithy/dynamic.smithy +++ b/modules/dynamic/smithy/dynamic.smithy @@ -6,8 +6,11 @@ metadata suppressions = [ } ] + namespace smithy4s.dynamic.model +use smithy4s.api#discriminated + /// This is a best-effort meta-representation of the smithy-model, that we should be able /// to deserialise from Json. structure Model { @@ -34,6 +37,7 @@ map TraitMap { value: Document } +@discriminated("type") union Shape { blob: BlobShape, byte: ByteShape, diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala new file mode 100644 index 000000000..1c14f5047 --- /dev/null +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala @@ -0,0 +1,108 @@ +package smithy4s.dynamic.model + +import weaver._ +import smithy4s.Document +import software.amazon.smithy.model.{Model => SModel} +import software.amazon.smithy.model.shapes.ModelSerializer +import cats.effect.IO +import cats.syntax.all._ + +object DynamicModelSpec extends SimpleIOSuite { + + val modelString = + """|namespace foo + | + |service Service { + | operations: [Operation] + |} + | + |@readonly + |@http(method: "GET", uri: "/{name}") + |operation Operation { + | input: Input, + | output: Output + |} + | + |structure Input { + | @httpLabel + | @required + | name: String + |} + | + |structure Output { + | greeting: String + |} + |""".stripMargin.trim + + val expected: Model = { + Model( + smithy = Some("1.0"), + shapes = Map( + IdRef("foo#Service") -> Shape.ServiceCase( + ServiceShape(operations = + Some(List(MemberShape(IdRef("foo#Operation")))) + ) + ), + IdRef("foo#Operation") -> Shape.OperationCase( + OperationShape( + input = Some(MemberShape(IdRef("foo#Input"))), + output = Some(MemberShape(IdRef("foo#Output"))), + traits = Some( + Map( + IdRef("smithy.api#http") -> Document.obj( + "method" -> Document.fromString("GET"), + "uri" -> Document.fromString("/{name}"), + "code" -> Document.fromInt(200) + ), + IdRef("smithy.api#readonly") -> Document.obj() + ) + ) + ) + ), + IdRef("foo#Input") -> Shape.StructureCase( + StructureShape( + members = Some( + Map( + "name" -> MemberShape( + IdRef("smithy.api#String"), + traits = Some( + Map( + IdRef("smithy.api#httpLabel") -> Document.obj(), + IdRef("smithy.api#required") -> Document.obj() + ) + ) + ) + ) + ) + ) + ), + IdRef("foo#Output") -> Shape.StructureCase( + StructureShape( + members = Some( + Map( + "greeting" -> MemberShape( + IdRef("smithy.api#String") + ) + ) + ) + ) + ) + ) + ) + } + + test("Decode json representation of models") { + IO( + SModel + .assembler() + .addUnparsedModel("foo.smithy", modelString) + .assemble() + .unwrap() + ).map(ModelSerializer.builder().build.serialize(_)) + .map(NodeToDocument(_)) + .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) + .flatMap(_.liftTo[IO]) + .map(obtained => expect.same(obtained, expected)) + } + +} diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala new file mode 100644 index 000000000..e6b51dd86 --- /dev/null +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala @@ -0,0 +1,37 @@ +package smithy4s.dynamic.model + +import smithy4s.Document +import software.amazon.smithy.model.node._ +import scala.jdk.CollectionConverters._ + +object NodeToDocument { + + def apply(node: Node): Document = + return node.accept(new NodeVisitor[Document] { + def arrayNode(x: ArrayNode): Document = + Document.array(x.getElements().asScala.map(_.accept(this))) + + def booleanNode(x: BooleanNode): Document = + Document.fromBoolean(x.getValue()) + + def nullNode(x: NullNode): Document = + Document.DNull + + def numberNode(x: NumberNode): Document = + Document.fromDouble(x.getValue().doubleValue()) + + def objectNode(x: ObjectNode): Document = + Document.obj( + x.getMembers() + .asScala + .map { case (key, value) => + key.getValue() -> value.accept(this) + } + .toSeq: _* + ) + + def stringNode(x: StringNode): Document = + Document.fromString(x.getValue()) + }) + +} From eeef6b8760d4d458c8f36604c38b8f7f3b98362d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 15 Jan 2022 05:26:33 +0100 Subject: [PATCH 09/28] Add failing test for extracting something from the service --- .../dynamic/model/DynamicModelSpec.scala | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala index 1c14f5047..2dcb8a239 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala @@ -6,6 +6,10 @@ import software.amazon.smithy.model.{Model => SModel} import software.amazon.smithy.model.shapes.ModelSerializer import cats.effect.IO import cats.syntax.all._ +import smithy4s.dynamic +import smithy4s.Service +import smithy4s.Hints +import schematic.Field object DynamicModelSpec extends SimpleIOSuite { @@ -105,4 +109,69 @@ object DynamicModelSpec extends SimpleIOSuite { .map(obtained => expect.same(obtained, expected)) } + object Interpreter { + type ToFieldNames[A] = () => List[String] + + object GetFieldNames extends smithy4s.StubSchematic[ToFieldNames] { + def default[A]: ToFieldNames[A] = () => Nil + + override def withHints[A]( + fa: ToFieldNames[A], + hints: Hints + ): ToFieldNames[A] = fa + + override def genericStruct[S]( + fields: Vector[Field[ToFieldNames, S, _]] + )(const: Vector[Any] => S): ToFieldNames[S] = () => + fields.flatMap { f => + f.label :: f.instance() + }.toList + + override def suspend[A](f: => ToFieldNames[A]): ToFieldNames[A] = + () => f() + + // these will be needed later but are irrelevant for now + // override def union[S]( + // first: Alt[ToFieldNames, S, _], + // rest: Vector[Alt[ToFieldNames, S, _]] + // )(total: S => Alt.WithValue[ToFieldNames, S, _]): ToFieldNames[S] = + // () => + // first.label :: first.instance() ::: rest.flatMap { a => + // a.label :: a.instance() + // }.toList + + // override def list[S](fs: ToFieldNames[S]): ToFieldNames[List[S]] = fs + // override def vector[S](fs: ToFieldNames[S]): ToFieldNames[Vector[S]] = fs + // override def map[K, V]( + // fk: ToFieldNames[K], + // fv: ToFieldNames[V] + // ): ToFieldNames[Map[K, V]] = () => fk() ++ fv() + // override def bijection[A, B]( + // f: ToFieldNames[A], + // to: A => B, + // from: B => A + // ): ToFieldNames[B] = f + + // override def set[S](fs: ToFieldNames[S]): ToFieldNames[Set[S]] = fs + + } + + def toFieldNames[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]]( + svc: Service[Alg, Op] + ): List[String] = + svc.endpoints.flatMap { endpoint => + endpoint.input.compile(GetFieldNames)() ++ + endpoint.output.compile(GetFieldNames)() + } + } + + pureTest( + "Extract field names from all structures in a service's endpoints" + ) { + val svc = dynamic.Compiler.compile(expected).allServices.head + + // NoSuchElementException: key not found: smithy.api#String + val result = Interpreter.toFieldNames(svc) + assert(result == List("name", "greeting")) + } } From 99357f00ebc331ab99549179dcfe5fd725f91309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 16 Jan 2022 02:34:20 +0100 Subject: [PATCH 10/28] Add dynamic to allModules --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index e15ae7f4c..d7a5bfd56 100644 --- a/build.sbt +++ b/build.sbt @@ -53,7 +53,8 @@ lazy val allModules = Seq( `aws-kernel`.projectRefs, aws.projectRefs, `aws-http4s`.projectRefs, - `codegen-cli`.projectRefs + `codegen-cli`.projectRefs, + dynamic.projectRefs ).flatten lazy val docs = From 78e9f50635773272650af5192235a33491d16362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 16 Jan 2022 02:43:17 +0100 Subject: [PATCH 11/28] Visit prelude shapes - quick and dirty --- .../dynamic/DynamicModelCompiler.scala | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index 31b16b42d..a6044bb60 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -63,6 +63,25 @@ object Compiler { toHint(_, _) ) + // Loosely inspired by + // https://awslabs.github.io/smithy/1.0/spec/core/prelude-model.html#prelude-shapes + List( + "String" -> Shape.StringCase(StringShape()), + "Blob" -> Shape.BlobCase(BlobShape()), + "BigInteger" -> Shape.BigIntegerCase(BigIntegerShape()), + "BigDecimal" -> Shape.BigDecimalCase(BigDecimalShape()), + "Timestamp" -> Shape.TimestampCase(TimestampShape()), + "Document" -> Shape.DocumentCase(DocumentShape()), + "Boolean" -> Shape.BooleanCase(BooleanShape()), + "Byte" -> Shape.ByteCase(ByteShape()), + "Short" -> Shape.ShortCase(ShortShape()), + "Integer" -> Shape.IntegerCase(IntegerShape()), + "Long" -> Shape.LongCase(LongShape()), + "Float" -> Shape.FloatCase(FloatShape()), + "Double" -> Shape.DoubleCase(DoubleShape()), + "Unit" -> Shape.StructureCase(StructureShape()) + ).foreach { case (id, shape) => visitor(ShapeId("smithy.api", id), shape) } + model.shapes.foreach { case (ValidIdRef(id), shape) => visitor(id, shape) case _ => () From d24710c4bb0e042366e3a40dd56b11ac4e8b575d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 16 Jan 2022 02:48:35 +0100 Subject: [PATCH 12/28] Support Float --- .../src/smithy4s/dynamic/DynamicModelCompiler.scala | 3 +++ .../src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index a6044bb60..659869bab 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -129,6 +129,9 @@ object Compiler { override def integerShape(id: ShapeId, shape: IntegerShape): Unit = update(id, shape.traits, int) + override def floatShape(id: ShapeId, shape: FloatShape): Unit = + update(id, shape.traits, float) + override def booleanShape(id: ShapeId, shape: BooleanShape): Unit = update(id, shape.traits, boolean) diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala index 2dcb8a239..08a5ef587 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala @@ -34,7 +34,8 @@ object DynamicModelSpec extends SimpleIOSuite { |} | |structure Output { - | greeting: String + | greeting: String, + | someFloat: Float |} |""".stripMargin.trim @@ -86,6 +87,9 @@ object DynamicModelSpec extends SimpleIOSuite { Map( "greeting" -> MemberShape( IdRef("smithy.api#String") + ), + "someFloat" -> MemberShape( + IdRef("smithy.api#Float") ) ) ) @@ -172,6 +176,6 @@ object DynamicModelSpec extends SimpleIOSuite { // NoSuchElementException: key not found: smithy.api#String val result = Interpreter.toFieldNames(svc) - assert(result == List("name", "greeting")) + assert(result == List("name", "greeting", "someFloat")) } } From fd35be6e1f618d9bb5ae6ff18e00715917a391fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 16 Jan 2022 02:54:45 +0100 Subject: [PATCH 13/28] Add new failing spec --- .../dynamic/model/DynamicModelPizzaSpec.scala | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala new file mode 100644 index 000000000..0ebdb57a3 --- /dev/null +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala @@ -0,0 +1,43 @@ +package smithy4s.dynamic.model + +import weaver._ +import software.amazon.smithy.model.{Model => SModel} +import software.amazon.smithy.model.shapes.ModelSerializer +import cats.effect.IO +import cats.syntax.all._ +import java.nio.file.Files +import java.nio.file.Paths + +object DynamicModelPizzaSpec extends SimpleIOSuite { + + val modelString = new String( + Files.readAllBytes( + // quick and dirty + Paths.get("../../../sampleSpecs/pizza.smithy").toAbsolutePath() + ) + ) + + val expected: Model = { + Model( + smithy = Some("1.0"), + shapes = Map( + ) + ) + } + + test("Decode json representation of models") { + // Unable to resolve trait `smithy4s.api#simpleRestJson`. If this is a custom trait, then it must be defined before it can be used in a model. + IO( + SModel + .assembler() + .addUnparsedModel("pizza.smithy", modelString) + .assemble() + .unwrap() + ).map(ModelSerializer.builder().build.serialize(_)) + .map(NodeToDocument(_)) + .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) + .flatMap(_.liftTo[IO]) + .map(obtained => expect.same(obtained, expected)) + } + +} From 32bfcae58b5f873c3f8c244905f271320ad1de58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Sun, 16 Jan 2022 10:12:16 +0100 Subject: [PATCH 14/28] Fix loading of specs in tests --- build.sbt | 7 +++---- .../dynamic/model/DynamicModelPizzaSpec.scala | 16 ++++++++-------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/build.sbt b/build.sbt index d7a5bfd56..d20c8ffa3 100644 --- a/build.sbt +++ b/build.sbt @@ -412,11 +412,10 @@ lazy val dynamic = projectMatrix Dependencies.Weaver.cats.value % Test ), Test / fork := true, + // Forwarding cwd to tests + Test / javaOptions += s"-Duser.dir=${sys.props("user.dir")}", testFrameworks += new TestFramework("weaver.framework.CatsEffect"), - // - Compile / allowedNamespaces := Seq( - "smithy4s.dynamic.model" - ), + Compile / allowedNamespaces := Seq("smithy4s.dynamic.model"), Compile / smithySpecs := Seq( (ThisBuild / baseDirectory).value / "modules" / "dynamic" / "smithy" / "dynamic.smithy" ), diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala index 0ebdb57a3..78ad09adf 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala @@ -5,17 +5,16 @@ import software.amazon.smithy.model.{Model => SModel} import software.amazon.smithy.model.shapes.ModelSerializer import cats.effect.IO import cats.syntax.all._ -import java.nio.file.Files import java.nio.file.Paths object DynamicModelPizzaSpec extends SimpleIOSuite { - val modelString = new String( - Files.readAllBytes( - // quick and dirty - Paths.get("../../../sampleSpecs/pizza.smithy").toAbsolutePath() - ) - ) + // This is not ideal, but it does the job. + val cwd = System.getProperty("user.dir"); + val pizzaSpec = Paths.get(cwd + "/sampleSpecs/pizza.smithy").toAbsolutePath() + val smithy4sPrelude = Paths + .get(cwd + "/modules/protocol/resources/META-INF/smithy/smithy4s.smithy") + .toAbsolutePath() val expected: Model = { Model( @@ -30,7 +29,8 @@ object DynamicModelPizzaSpec extends SimpleIOSuite { IO( SModel .assembler() - .addUnparsedModel("pizza.smithy", modelString) + .addImport(smithy4sPrelude) + .addImport(pizzaSpec) .assemble() .unwrap() ).map(ModelSerializer.builder().build.serialize(_)) From eb05278c93056094b2c11cbd714bac424bf6f303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Sun, 16 Jan 2022 11:07:18 +0100 Subject: [PATCH 15/28] Implement rest of metadamodel in dynamic compiler --- .../dynamic/DynamicModelCompiler.scala | 82 ++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index 659869bab..bf1a58e58 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -29,6 +29,16 @@ object Compiler { shapeId.namespace + "#" + shapeId.name ) + private def getTrait[A: Hints.Key: Document.Decoder]( + traits: Option[Map[IdRef, Document]] + ): Option[A] = + traits.flatMap(dynamicTraits => + dynamicTraits.get(toIdRef(implicitly[Hints.Key[A]].id)).flatMap { + document => + document.decode[A].toOption + } + ) + def compile(model: Model, knownHints: KeyedSchema[_]*): DynamicModel = { val schemaMap = MMap.empty[ShapeId, Schema[DynData]] val endpointMap = MMap.empty[ShapeId, DynamicEndpoint] @@ -132,11 +142,48 @@ object Compiler { override def floatShape(id: ShapeId, shape: FloatShape): Unit = update(id, shape.traits, float) + override def longShape(id: ShapeId, shape: LongShape): Unit = + update(id, shape.traits, long) + + override def doubleShape(id: ShapeId, shape: DoubleShape): Unit = + update(id, shape.traits, double) + + override def shortShape(id: ShapeId, shape: ShortShape): Unit = + update(id, shape.traits, short) + + override def bigIntegerShape(id: ShapeId, shape: BigIntegerShape): Unit = + update(id, shape.traits, bigint) + + override def bigDecimalShape(id: ShapeId, shape: BigDecimalShape): Unit = + update(id, shape.traits, bigdecimal) + + override def byteShape(id: ShapeId, shape: ByteShape): Unit = + update(id, shape.traits, byte) + + override def timestampShape(id: ShapeId, shape: TimestampShape): Unit = + update(id, shape.traits, timestamp) + + override def blobShape(id: ShapeId, shape: BlobShape): Unit = + update(id, shape.traits, bytes) + override def booleanShape(id: ShapeId, shape: BooleanShape): Unit = update(id, shape.traits, boolean) - override def stringShape(id: ShapeId, shape: StringShape): Unit = - update(id, shape.traits, string) + override def stringShape(id: ShapeId, shape: StringShape): Unit = { + val maybeUuid = getTrait[smithy4s.api.UuidFormat](shape.traits) + val maybeEnum = getTrait[smithy.api.Enum](shape.traits) + (maybeUuid, maybeEnum) match { + case (Some(_), _) => update(id, shape.traits, uuid) + case (None, Some(e)) => { + val valuesAndIndexes = e.value.map(_.value.value).zipWithIndex + val fn = valuesAndIndexes.map(tup => tup._1 -> tup).toMap + val fromName = valuesAndIndexes.map(tup => tup._1 -> tup._1).toMap + val fromOrd = valuesAndIndexes.map(tup => tup._2 -> tup._1).toMap + update(id, shape.traits, enumeration(fn, fromName, fromOrd)) + } + case _ => update(id, shape.traits, string) + } + } override def listShape(id: ShapeId, shape: ListShape): Unit = update(id, shape.traits, list(suspend(schema(shape.member.target)))) @@ -144,6 +191,16 @@ object Compiler { override def setShape(id: ShapeId, shape: SetShape): Unit = update(id, shape.traits, set(suspend(schema(shape.member.target)))) + override def mapShape(id: ShapeId, shape: MapShape): Unit = + update( + id, + shape.traits, + map( + suspend(schema(shape.key.target)), + suspend(schema(shape.value.target)) + ) + ) + override def operationShape(id: ShapeId, shape: OperationShape): Unit = { def maybeSchema(maybeShapeId: Option[IdRef]): Schema[DynData] = suspend( @@ -215,6 +272,27 @@ object Compiler { genericStruct(fields)(dynStruct) } ) + + override def unionShape(id: ShapeId, shape: UnionShape): Unit = + shape.members.filter(_.nonEmpty).foreach { members => + update( + id, + shape.traits, { + val alts = + members.zipWithIndex.map { case ((label, mShape), index) => + val memberHints = allHints(mShape.traits) + val memberSchema = + suspend(schema(mShape.target)) + .withHints(memberHints: _*) + memberSchema + .oneOf[DynAlt](label, data => (index, data)) + }.toVector + union(alts.head, alts.drop(1): _*) { case (index, data) => + alts(index).apply(data) + } + } + ) + } } } From 40216d39cb7064544e2213ca33dc33b6958fc08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Sun, 16 Jan 2022 20:56:50 +0100 Subject: [PATCH 16/28] Prepare functions for testing purposes * Adds a protocol for testing purposes * Adds a rendering function for documents * Make the services and schema exposed by the DynamicModel work against existential types. --- modules/core/src/smithy4s/Document.scala | 21 +++++ .../src/smithy4s/dynamic/DynamicModel.scala | 23 ++++- .../dynamic/DynamicModelCompiler.scala | 2 +- .../src/smithy4s/dynamic/DynamicService.scala | 7 +- .../dynamic/model/DynamicModelSpec.scala | 2 +- .../dynamic/model/JsonIOProtocol.scala | 83 +++++++++++++++++++ 6 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala diff --git a/modules/core/src/smithy4s/Document.scala b/modules/core/src/smithy4s/Document.scala index 497aa80ce..72d6bcdeb 100644 --- a/modules/core/src/smithy4s/Document.scala +++ b/modules/core/src/smithy4s/Document.scala @@ -19,6 +19,7 @@ package smithy4s import smithy4s.http.PayloadError import smithy4s.internals.SchematicDocumentDecoder import smithy4s.internals.SchematicDocumentEncoder +import smithy4s.Document._ /** * A json-like free-form structure serving as a model for @@ -31,6 +32,26 @@ sealed trait Document extends Product with Serializable { ): Either[PayloadError, A] = decoder.decode(this) + override def toString(): String = this.show + + /** + * Toy renderer that does not comply the json specification : + * strings aren't escaped and keys aren't quoted. + * Do not use for any real purpose other than debugging. + */ + def show: String = this match { + case DNumber(value) => { + if (value.isValidLong) value.toLong.toString() + else value.toString() + } + case DBoolean(value) => value.toString + case DString(value) => s""""$value"""" + case DNull => "null" + case DArray(value) => value.map(_.show).mkString("[", ", ", "]") + case DObject(value) => + value.map { case (k, v) => k + "=" + v.show }.mkString("{", ", ", "}") + } + } object Document { diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala index 75e91da2d..109d36bad 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala @@ -6,13 +6,32 @@ class DynamicModel( schemaMap: Map[ShapeId, Schema[DynData]] ) { - def allServices: List[DynamicService] = serviceMap.values.toList + def allServices: List[DynamicModel.ServiceWrapper] = + serviceMap.values.toList def getSchema( namespace: String, name: String - ): Option[Schema[DynData]] = { + ): Option[Schema[_]] = { val shapeId = ShapeId(namespace, name) schemaMap.get(shapeId) } } + +object DynamicModel { + + /** + * A construct that hides the types a service instance works, + * virtually turning them into existential types. + * + * This prevents the user from calling the algebra/transformation + * in an unsafe fashion. + */ + trait ServiceWrapper { + type Alg[P[_, _, _, _, _]] + type Op[I, E, O, SI, SO] + + def service: Service[Alg, Op] + } + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index bf1a58e58..148faeece 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -99,7 +99,7 @@ object Compiler { new DynamicModel(serviceMap.toMap, schemaMap.toMap) } - class CompileVisitor( + private class CompileVisitor( model: Model, schemaMap: MMap[ShapeId, Schema[DynData]], endpointMap: MMap[ShapeId, DynamicEndpoint], diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala index 51aff0903..8c179e701 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala @@ -6,7 +6,12 @@ case class DynamicService( version: String, getEndpoints: () => List[DynamicEndpoint], hints: Hints -) extends Service[DynamicAlg, DynamicOp] { +) extends Service[DynamicAlg, DynamicOp] + with DynamicModel.ServiceWrapper { + + type Alg[P[_, _, _, _, _]] = DynamicAlg[P] + type Op[I, E, O, SI, SO] = DynamicOp[I, E, O, SI, SO] + override val service: Service[Alg, Op] = this def transform[F[_, _, _, _, _], G[_, _, _, _, _]]( alg: DynamicAlg[F], diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala index 08a5ef587..cc67340cc 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala @@ -172,7 +172,7 @@ object DynamicModelSpec extends SimpleIOSuite { pureTest( "Extract field names from all structures in a service's endpoints" ) { - val svc = dynamic.Compiler.compile(expected).allServices.head + val svc = dynamic.Compiler.compile(expected).allServices.head.service // NoSuchElementException: key not found: smithy.api#String val result = Interpreter.toFieldNames(svc) diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala new file mode 100644 index 000000000..baa933fc3 --- /dev/null +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala @@ -0,0 +1,83 @@ +package smithy4s +package dynamic.model + +import cats.effect.IO +import cats.syntax.all._ + +/** + * These are toy interpreters that turn services into json-in/json-out + * functions, and vice versa. + * + * Created for testing purposes. + */ +object JsonIOProtocol { + + def fromJsonIO[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]]( + jsonIO: Document => IO[Document] + )(implicit S: Service[Alg, Op]): Monadic[Alg, IO] = { + val kleisliCache = + fromLowLevel(jsonIO).precompute(S.endpoints.map(Kind5.existential(_))) + val transfo = new Transformation[Op, GenLift[IO]#λ] { + def apply[I, E, O, SI, SO](op: Op[I, E, O, SI, SO]): IO[O] = { + val (input, ep) = S.endpoint(op) + kleisliCache(ep).apply(input) + } + } + S.transform(transfo) + } + + def toJsonIO[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]]( + alg: Monadic[Alg, IO] + )(implicit S: Service[Alg, Op]): Document => IO[Document] = { + val transformation = S.asTransformation(alg) + val jsonEndpoints = + S.endpoints.map(ep => ep.name -> toLowLevel(transformation, ep)).toMap + (d: Document) => + d match { + case Document.DObject(m) if m.size == 1 => + val (method, payload) = m.head + jsonEndpoints.get(method) match { + case Some(jsonEndpoint) => jsonEndpoint(payload) + case None => IO.raiseError(NotFound) + } + case _ => IO.raiseError(NotFound) + } + } + + private type KL[I, E, O, SI, SO] = I => IO[O] + + private def fromLowLevel[Op[_, _, _, _, _]]( + jsonIO: Document => IO[Document] + ): Transformation[Endpoint[Op, *, *, *, *, *], KL] = + new Transformation[Endpoint[Op, *, *, *, *, *], KL] { + def apply[I, E, O, SI, SO]( + ep: Endpoint[Op, I, E, O, SI, SO] + ): KL[I, E, O, SI, SO] = { + implicit val encoderI: Document.Encoder[I] = + Document.Encoder.fromSchema(ep.input) + implicit val decoderO: Document.Decoder[O] = + Document.Decoder.fromSchema(ep.output) + + (i: I) => + jsonIO(Document.obj(ep.name -> Document.encode(i))) + .flatMap(_.decode[O].liftTo[IO]) + } + } + + private def toLowLevel[Op[_, _, _, _, _], I, E, O, SI, SO]( + transformation: Transformation[Op, GenLift[IO]#λ], + endpoint: Endpoint[Op, I, E, O, SI, SO] + ): Document => IO[Document] = { + implicit val decoderI = Document.Decoder.fromSchema(endpoint.input) + implicit val encoderO = Document.Encoder.fromSchema(endpoint.output) + (document: Document) => + for { + input <- document.decode[I].liftTo[IO] + op = endpoint.wrap(input) + output <- transformation(op) + } yield encoderO.encode(output) + } + + case object NotFound extends Throwable + +} From 99ed96d36ca38c5adf9dc21e05db8b87004d1546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Thu, 20 Jan 2022 01:01:07 +0100 Subject: [PATCH 17/28] Various fixes to the dynamic compiler (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Olivier Mélois --- build.sbt | 1 + .../smithy4s/codegen/CollisionAvoidance.scala | 24 +- modules/codegen/src/smithy4s/codegen/IR.scala | 11 +- .../src/smithy4s/codegen/Renderer.scala | 37 +-- .../src/smithy4s/codegen/SmithyToIR.scala | 13 +- .../smithy4s/http/internals/PathEncode.scala | 1 + .../src/smithy4s/ShapeIdHintsSmokeSpec.scala | 16 ++ .../smithy4s/dynamic/DynamicEndpoint.scala | 16 ++ .../src/smithy4s/dynamic/DynamicModel.scala | 16 ++ .../dynamic/DynamicModelCompiler.scala | 253 +++++++++++++----- .../src/smithy4s/dynamic/DynamicOp.scala | 16 ++ .../src/smithy4s/dynamic/DynamicService.scala | 23 +- .../src/smithy4s/dynamic/KeyedSchema.scala | 16 ++ .../src/smithy4s/dynamic/ShapeVisitor.scala | 16 ++ .../src/smithy4s/dynamic/package.scala | 16 ++ .../dynamic/model/DynamicModelPizzaSpec.scala | 82 +++++- .../dynamic/model/DynamicModelSpec.scala | 55 +++- .../dynamic/model/JsonIOProtocol.scala | 20 +- .../dynamic/model/NodeToDocument.scala | 16 ++ .../src/smithy4s/example/ArbitraryData.scala | 2 +- 20 files changed, 520 insertions(+), 130 deletions(-) diff --git a/build.sbt b/build.sbt index f3973580c..afd182461 100644 --- a/build.sbt +++ b/build.sbt @@ -410,6 +410,7 @@ lazy val dynamic = projectMatrix isCE3 := true, libraryDependencies ++= Seq( "org.scala-lang.modules" %%% "scala-collection-compat" % "2.6.0", + Dependencies.Cats.core.value, Dependencies.Weaver.cats.value % Test ), Test / fork := true, diff --git a/modules/codegen/src/smithy4s/codegen/CollisionAvoidance.scala b/modules/codegen/src/smithy4s/codegen/CollisionAvoidance.scala index 6d0d7a0c5..9843b4d9a 100644 --- a/modules/codegen/src/smithy4s/codegen/CollisionAvoidance.scala +++ b/modules/codegen/src/smithy4s/codegen/CollisionAvoidance.scala @@ -59,27 +59,39 @@ object CollisionAvoidance { hints.map(modHint), version ) - case Product(name, fields, recursive, hints) => + case Product(name, originalName, fields, recursive, hints) => Product( protect(name.capitalize), + originalName, fields.map(modField), recursive, hints.map(modHint) ) - case Union(name, alts, recursive, hints) => + case Union(name, originalName, alts, recursive, hints) => Union( protect(name.capitalize), + originalName, alts.map(modAlt), recursive, hints.map(modHint) ) - case TypeAlias(name, tpe, hints) => - TypeAlias(protect(name.capitalize), modType(tpe), hints.map(modHint)) - case Enumeration(name, values, hints) => + case TypeAlias(name, originalName, tpe, hints) => + TypeAlias( + protect(name.capitalize), + originalName, + modType(tpe), + hints.map(modHint) + ) + case Enumeration(name, originalName, values, hints) => val newValues = values.map { case EnumValue(value, name, hints) => EnumValue(value, name.map(protect), hints.map(modHint)) } - Enumeration(protect(name.capitalize), newValues, hints.map(modHint)) + Enumeration( + protect(name.capitalize), + originalName, + newValues, + hints.map(modHint) + ) } CompilationUnit(compilationUnit.namespace, declarations) } diff --git a/modules/codegen/src/smithy4s/codegen/IR.scala b/modules/codegen/src/smithy4s/codegen/IR.scala index d5faa0a81..1b5e43e74 100644 --- a/modules/codegen/src/smithy4s/codegen/IR.scala +++ b/modules/codegen/src/smithy4s/codegen/IR.scala @@ -54,6 +54,7 @@ case class Operation( case class Product( name: String, + originalName: String, fields: List[Field], recursive: Boolean = false, hints: List[Hint] = Nil @@ -61,16 +62,22 @@ case class Product( case class Union( name: String, + originalName: String, alts: NonEmptyList[Alt], recursive: Boolean = false, hints: List[Hint] = Nil ) extends Decl -case class TypeAlias(name: String, tpe: Type, hints: List[Hint] = Nil) - extends Decl +case class TypeAlias( + name: String, + originalName: String, + tpe: Type, + hints: List[Hint] = Nil +) extends Decl case class Enumeration( name: String, + originalName: String, values: List[EnumValue], hints: List[Hint] = Nil ) extends Decl diff --git a/modules/codegen/src/smithy4s/codegen/Renderer.scala b/modules/codegen/src/smithy4s/codegen/Renderer.scala index d6d168d34..1e9401f86 100644 --- a/modules/codegen/src/smithy4s/codegen/Renderer.scala +++ b/modules/codegen/src/smithy4s/codegen/Renderer.scala @@ -71,18 +71,20 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => def renderDecl(decl: Decl): RenderResult = decl match { case Service(name, originalName, ops, hints, version) => renderService(name, originalName, ops, hints, version) - case Product(name, fields, recursive, hints) => - renderProduct(name, fields, recursive, hints) - case Union(name, alts, recursive, hints) => - renderUnion(name, alts, recursive, hints) - case TypeAlias(name, tpe, hints) => renderTypeAlias(name, tpe, hints) - case Enumeration(name, values, hints) => renderEnum(name, values, hints) - case _ => RenderResult.empty + case Product(name, originalName, fields, recursive, hints) => + renderProduct(name, originalName, fields, recursive, hints) + case Union(name, originalName, alts, recursive, hints) => + renderUnion(name, originalName, alts, recursive, hints) + case TypeAlias(name, originalName, tpe, hints) => + renderTypeAlias(name, originalName, tpe, hints) + case Enumeration(name, originalName, values, hints) => + renderEnum(name, originalName, values, hints) + case _ => RenderResult.empty } def renderPackageContents: RenderResult = { val typeAliases = compilationUnit.declarations.collect { - case TypeAlias(name, _, _) => + case TypeAlias(name, _, _, _) => s"type $name = ${compilationUnit.namespace}.${name}.Type" } @@ -258,11 +260,12 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => alts <- errorNel.traverse { t => t.name.map(n => Alt(n, t)) } - } yield Union(opName + "Error", alts) + name = opName + "Error" + } yield Union(name, name, alts) val renderedErrorUnion = errorUnion.foldMap { - case Union(name, alts, recursive, hints) => - renderUnion(name, alts, recursive, hints, error = true) + case Union(name, originalName, alts, recursive, hints) => + renderUnion(name, originalName, alts, recursive, hints, error = true) } lines( @@ -301,6 +304,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => private def renderProduct( name: String, + originalName: String, fields: List[Field], recursive: Boolean, hints: List[Hint] @@ -333,7 +337,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => } } else line(decl), obj(name, ext = hintKey(name, hints))( - renderId(name), + renderId(originalName), newline, renderHintsValWithId(hints), newline, @@ -391,6 +395,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => private def renderUnion( name: String, + originalName: String, alts: NonEmptyList[Alt], recursive: Boolean, hints: List[Hint], @@ -403,7 +408,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => lines( s"sealed trait $name", obj(name, ext = hintKey(name, hints))( - renderId(name), + renderId(originalName), newline, renderHintsValWithId(hints), newline, @@ -457,12 +462,13 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => private def renderEnum( name: String, + originalName: String, values: List[EnumValue], hints: List[Hint] ): RenderResult = lines( s"sealed abstract class $name(val value: String, val ordinal: Int) extends Product with Serializable", obj(name, ext = s"$Enumeration_[$name]", w = hintKey(name, hints))( - renderId(name), + renderId(originalName), newline, renderHintsValWithId(hints), newline, @@ -486,6 +492,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => private def renderTypeAlias( name: String, + originalName: String, tpe: Type, hints: List[Hint] ): RenderResult = { @@ -493,7 +500,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => lines( obj(name, extensions = List(s"Newtype[${tpe.render}]"))( - renderId(name), + renderId(originalName), renderHintsValWithId(hints), s"val underlyingSchema : $Schema_[${tpe.render}] = ${tpe.schemaRef}.withHints(hints)", lines( diff --git a/modules/codegen/src/smithy4s/codegen/SmithyToIR.scala b/modules/codegen/src/smithy4s/codegen/SmithyToIR.scala index d30087a60..935682c5a 100644 --- a/modules/codegen/src/smithy4s/codegen/SmithyToIR.scala +++ b/modules/codegen/src/smithy4s/codegen/SmithyToIR.scala @@ -70,9 +70,10 @@ private[codegen] class SmithyToIR(model: Model, namespace: String) { if (shape.isMemberShape()) None else shape.tpe.flatMap { - case Type.Alias(_, name, tpe) => TypeAlias(name, tpe, hints).some - case Type.PrimitiveType(_) => None - case other => TypeAlias(shape.name, other, hints).some + case Type.Alias(_, name, tpe) => + TypeAlias(name, name, tpe, hints).some + case Type.PrimitiveType(_) => None + case other => TypeAlias(shape.name, shape.name, other, hints).some } } @@ -80,7 +81,7 @@ private[codegen] class SmithyToIR(model: Model, namespace: String) { val rec = isRecursive(shape.getId(), Set.empty) val hints = traitsToHints(shape.getAllTraits().asScala.values.toList) - Product(shape.name, shape.fields, rec, hints).some + Product(shape.name, shape.name, shape.fields, rec, hints).some } override def unionShape(shape: UnionShape): Option[Decl] = { @@ -88,7 +89,7 @@ private[codegen] class SmithyToIR(model: Model, namespace: String) { val hints = traitsToHints(shape.getAllTraits().asScala.values.toList) NonEmptyList.fromList(shape.alts).map { case alts => - Union(shape.name, alts, rec, hints) + Union(shape.name, shape.name, alts, rec, hints) } } @@ -101,7 +102,7 @@ private[codegen] class SmithyToIR(model: Model, namespace: String) { EnumValue(value.getValue(), value.getName().asScala) } .toList - Enumeration(shape.name, values).some + Enumeration(shape.name, shape.name, values).some case _ => super.stringShape(shape) } diff --git a/modules/core/src/smithy4s/http/internals/PathEncode.scala b/modules/core/src/smithy4s/http/internals/PathEncode.scala index 8fba64585..086a97171 100644 --- a/modules/core/src/smithy4s/http/internals/PathEncode.scala +++ b/modules/core/src/smithy4s/http/internals/PathEncode.scala @@ -59,6 +59,7 @@ object PathEncode { def fromToString[A]: Make[A] = from(_.toString) def noop[A]: Make[A] = Hinted.static[MaybePathEncode, A](None) + } } diff --git a/modules/core/test/src/smithy4s/ShapeIdHintsSmokeSpec.scala b/modules/core/test/src/smithy4s/ShapeIdHintsSmokeSpec.scala index 168b64787..7198a1706 100644 --- a/modules/core/test/src/smithy4s/ShapeIdHintsSmokeSpec.scala +++ b/modules/core/test/src/smithy4s/ShapeIdHintsSmokeSpec.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 schematic.Field diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala index 16fb33984..dccdebdda 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.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 package dynamic diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala index 109d36bad..51ddc1e5e 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModel.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 package dynamic diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index 148faeece..ce44172e7 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.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 package dynamic @@ -7,6 +23,8 @@ import schematic.OneOf import schematic.StructureField import smithy4s.syntax._ import smithy4s.internals.InputOutput +import cats.Eval +import cats.syntax.all._ object Compiler { @@ -39,10 +57,13 @@ object Compiler { } ) + /** + * @param knownHints hints supported by the caller. + */ def compile(model: Model, knownHints: KeyedSchema[_]*): DynamicModel = { - val schemaMap = MMap.empty[ShapeId, Schema[DynData]] - val endpointMap = MMap.empty[ShapeId, DynamicEndpoint] - val serviceMap = MMap.empty[ShapeId, DynamicService] + val schemaMap = MMap.empty[ShapeId, Eval[Schema[DynData]]] + val endpointMap = MMap.empty[ShapeId, Eval[DynamicEndpoint]] + val serviceMap = MMap.empty[ShapeId, Eval[DynamicService]] val hintsMap: Map[ShapeId, KeyedSchema[_]] = knownHints.map { ks => val shapeId: ShapeId = ks.hintKey.id @@ -60,9 +81,8 @@ object Compiler { } } - def toHint(id: ShapeId, tr: Document): Option[Hint] = { + def toHint(id: ShapeId, tr: Document): Option[Hint] = hintsMap.get(id).flatMap(toHintAux(_, tr)) - } val visitor = new CompileVisitor( @@ -96,20 +116,34 @@ object Compiler { case (ValidIdRef(id), shape) => visitor(id, shape) case _ => () } - new DynamicModel(serviceMap.toMap, schemaMap.toMap) + new DynamicModel( + serviceMap.toMap.fmap(_.value), + schemaMap.toMap.fmap(_.value) + ) } private class CompileVisitor( model: Model, - schemaMap: MMap[ShapeId, Schema[DynData]], - endpointMap: MMap[ShapeId, DynamicEndpoint], - serviceMap: MMap[ShapeId, DynamicService], + schemaMap: MMap[ShapeId, Eval[Schema[DynData]]], + endpointMap: MMap[ShapeId, Eval[DynamicEndpoint]], + serviceMap: MMap[ShapeId, Eval[DynamicService]], toHint: (ShapeId, Document) => Option[Hint] ) extends ShapeVisitor.Default[Unit] { - def schema(idRef: IdRef): Schema[DynData] = schemaMap( - ValidIdRef.unapply(idRef).get - ) + val closureMap: Map[ShapeId, Set[ShapeId]] = model.shapes.collect { + case (ValidIdRef(shapeId), shape) => + shapeId -> ClosureVisitor(shapeId, shape) + } + def isRecursive(id: ShapeId, visited: Set[ShapeId] = Set.empty): Boolean = + visited(id) || closureMap + .getOrElse(id, Set.empty) + .exists(isRecursive(_, visited + id)) + + def schema(idRef: IdRef): Eval[Schema[DynData]] = Eval.defer { + schemaMap( + ValidIdRef.unapply(idRef).get + ) + } def allHints(traits: Option[Map[IdRef, Document]]): Seq[Hint] = { traits @@ -126,14 +160,22 @@ object Compiler { def update[A]( shapeId: ShapeId, traits: Option[Map[IdRef, Document]], - schema: Schema[A] + lSchema: Eval[Schema[A]] ): Unit = { - schemaMap += (shapeId -> schema - .withHints(allHints(traits): _*) - .withHints(shapeId) - .asInstanceOf[Schema[DynData]]) + schemaMap += (shapeId -> lSchema.map { sch => + sch + .withHints(allHints(traits): _*) + .withHints(shapeId) + .asInstanceOf[Schema[DynData]] + }) } + def update[A]( + shapeId: ShapeId, + traits: Option[Map[IdRef, Document]], + schema: Schema[A] + ): Unit = update(shapeId, traits, Eval.now(schema)) + def default: Unit = () override def integerShape(id: ShapeId, shape: IntegerShape): Unit = @@ -169,6 +211,9 @@ object Compiler { override def booleanShape(id: ShapeId, shape: BooleanShape): Unit = update(id, shape.traits, boolean) + override def documentShape(id: ShapeId, shape: DocumentShape): Unit = + update(id, shape.traits, document) + override def stringShape(id: ShapeId, shape: StringShape): Unit = { val maybeUuid = getTrait[smithy4s.api.UuidFormat](shape.traits) val maybeEnum = getTrait[smithy.api.Enum](shape.traits) @@ -186,51 +231,64 @@ object Compiler { } override def listShape(id: ShapeId, shape: ListShape): Unit = - update(id, shape.traits, list(suspend(schema(shape.member.target)))) + update(id, shape.traits, schema(shape.member.target).map(s => list(s))) override def setShape(id: ShapeId, shape: SetShape): Unit = - update(id, shape.traits, set(suspend(schema(shape.member.target)))) + update(id, shape.traits, schema(shape.member.target).map(s => set(s))) override def mapShape(id: ShapeId, shape: MapShape): Unit = update( id, shape.traits, - map( - suspend(schema(shape.key.target)), - suspend(schema(shape.value.target)) - ) + for { + k <- schema(shape.key.target) + v <- schema(shape.value.target) + } yield map(k, v) ) override def operationShape(id: ShapeId, shape: OperationShape): Unit = { - def maybeSchema(maybeShapeId: Option[IdRef]): Schema[DynData] = - suspend( - maybeShapeId - .flatMap(ValidIdRef(_)) - .map[Schema[DynData]](id => schemaMap(id)) - .getOrElse(unit.asInstanceOf[Schema[DynData]]) + def getSchema(maybeShapeId: Option[IdRef]): Eval[Schema[DynData]] = + maybeShapeId + .flatMap(ValidIdRef(_)) + .map[Eval[Schema[DynData]]](id => Eval.defer(schemaMap(id))) + .getOrElse(Eval.now(unit.asInstanceOf[Schema[DynData]])) + + val input = shape.input.map(_.target) + val output = shape.output.map(_.target) + + val ep = for { + inputSchema <- getSchema(input).map(_.withHints(InputOutput.Input)) + outputSchema <- getSchema(output).map(_.withHints(InputOutput.Output)), + } yield { + DynamicEndpoint( + id, + inputSchema, + outputSchema, + Hints(allHints(shape.traits): _*) ) + } - val ep = DynamicEndpoint( - id, - maybeSchema(shape.input.map(_.target)).withHints(InputOutput.Input), - maybeSchema(shape.output.map(_.target)).withHints(InputOutput.Output), - Hints(allHints(shape.traits): _*) - ) endpointMap += id -> ep } override def serviceShape(id: ShapeId, shape: ServiceShape): Unit = { - val getEndpoints = () => - shape.operations.toList.flatMap(_.map(_.target)).collect { - case ValidIdRef(opId) => - endpointMap(opId) - } - val service = DynamicService( - id, - shape.version.getOrElse(""), - getEndpoints, - Hints(allHints(shape.traits): _*) - ) + val lEndpoints = + shape.operations.toList + .flatMap(_.map(_.target)) + .collect { case ValidIdRef(opId) => + Eval.defer(endpointMap(opId)) + } + .sequence + + val service = lEndpoints.map { endpoints => + DynamicService( + id, + shape.version.getOrElse(""), + endpoints, + Hints(allHints(shape.traits): _*) + ) + } + serviceMap += id -> service } @@ -252,24 +310,30 @@ object Compiler { id, shape.traits, { val members = shape.members.getOrElse(Map.empty) - val fields = - members.zipWithIndex.map { case ((label, mShape), index) => - val memberHints = allHints(mShape.traits) - val memberSchema = - suspend(schema(mShape.target)) - .withHints(memberHints: _*) - if ( - mShape.traits - .getOrElse(Map.empty) - .contains(IdRef("smithy.api#required")) - ) { - memberSchema.required[DynStruct](label, _(index)) - } else { - memberSchema - .optional[DynStruct](label, arr => Option(arr(index))) + val lFields = { + members.zipWithIndex + .map { case ((label, mShape), index) => + val memberHints = allHints(mShape.traits) + val lMemberSchema = + schema(mShape.target).map(_.withHints(memberHints: _*)) + if ( + mShape.traits + .getOrElse(Map.empty) + .contains(IdRef("smithy.api#required")) + ) { + lMemberSchema.map(_.required[DynStruct](label, _(index))) + } else { + lMemberSchema.map( + _.optional[DynStruct](label, arr => Option(arr(index))) + ) + } } - }.toVector - genericStruct(fields)(dynStruct) + .toVector + .sequence + } + if (isRecursive(id)) + Eval.later(suspend(genericStruct(lFields.value)(dynStruct))) + else lFields.map(fields => genericStruct(fields)(dynStruct)) } ) @@ -278,21 +342,62 @@ object Compiler { update( id, shape.traits, { - val alts = - members.zipWithIndex.map { case ((label, mShape), index) => - val memberHints = allHints(mShape.traits) - val memberSchema = - suspend(schema(mShape.target)) - .withHints(memberHints: _*) - memberSchema - .oneOf[DynAlt](label, data => (index, data)) - }.toVector - union(alts.head, alts.drop(1): _*) { case (index, data) => - alts(index).apply(data) + val lAlts = + members.zipWithIndex + .map { case ((label, mShape), index) => + val memberHints = allHints(mShape.traits) + schema(mShape.target) + .map(_.withHints(memberHints: _*)) + .map(_.oneOf[DynAlt](label, data => (index, data))) + } + .toVector + .sequence + lAlts.map { alts => + union(alts.head, alts.drop(1): _*) { case (index, data) => + alts(index).apply(data) + } } } ) } } + // A visitor allowing to gather the "closure" of all shapes + object ClosureVisitor extends ShapeVisitor.Default[Set[ShapeId]] { + def default: Set[ShapeId] = Set.empty + + def fromMembers(it: Iterable[MemberShape]): Set[ShapeId] = + it.map(_.target).collect { case ValidIdRef(id) => id }.toSet + + override def structureShape( + id: ShapeId, + shape: StructureShape + ): Set[ShapeId] = + fromMembers(shape.members.foldMap(_.values.toSet)) + + override def unionShape( + id: ShapeId, + shape: UnionShape + ): Set[ShapeId] = + fromMembers(shape.members.foldMap(_.values.toSet)) + + override def listShape( + id: ShapeId, + shape: ListShape + ): Set[ShapeId] = + fromMembers(Set(shape.member)) + + override def setShape( + id: ShapeId, + shape: SetShape + ): Set[ShapeId] = + fromMembers(Set(shape.member)) + + override def mapShape( + id: ShapeId, + shape: MapShape + ): Set[ShapeId] = + fromMembers(Set(shape.key, shape.value)) + } + } diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala index 53f108f86..2a88ad4ef 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicOp.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.dynamic import smithy4s.ShapeId diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala index 8c179e701..a91b6271a 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala @@ -1,10 +1,26 @@ +/* + * 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 dynamic case class DynamicService( id: ShapeId, version: String, - getEndpoints: () => List[DynamicEndpoint], + endpoints: List[DynamicEndpoint], hints: Hints ) extends Service[DynamicAlg, DynamicOp] with DynamicModel.ServiceWrapper { @@ -18,9 +34,6 @@ case class DynamicService( transformation: Transformation[F, G] ): DynamicAlg[G] = alg.andThen(transformation) - lazy val endpoints: List[Endpoint[DynamicOp, _, _, _, _, _]] = - getEndpoints() - private lazy val endpointMap : Map[ShapeId, Endpoint[DynamicOp, _, _, _, _, _]] = endpoints.map(ep => ep.id -> ep).toMap @@ -29,7 +42,7 @@ case class DynamicService( op: DynamicOp[I, E, O, SI, SO] ): (I, Endpoint[DynamicOp, I, E, O, SI, SO]) = { val endpoint = endpointMap - .get(op.id) + .getOrElse(op.id, sys.error("Unknown endpoint: " + op.id)) .asInstanceOf[Endpoint[DynamicOp, I, E, O, SI, SO]] val input = op.data (input, endpoint) diff --git a/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala b/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala index 7301b9244..9108c1f72 100644 --- a/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala +++ b/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.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 package dynamic diff --git a/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala b/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala index 3fdf4bebe..cb5a55370 100644 --- a/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala +++ b/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.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 package dynamic diff --git a/modules/dynamic/src/smithy4s/dynamic/package.scala b/modules/dynamic/src/smithy4s/dynamic/package.scala index 961da4951..6f79b5f1b 100644 --- a/modules/dynamic/src/smithy4s/dynamic/package.scala +++ b/modules/dynamic/src/smithy4s/dynamic/package.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 package object dynamic { diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala index 78ad09adf..392b107e1 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.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.dynamic.model import weaver._ @@ -6,6 +22,10 @@ import software.amazon.smithy.model.shapes.ModelSerializer import cats.effect.IO import cats.syntax.all._ import java.nio.file.Paths +import smithy4s.Document +import smithy4s.dynamic +import smithy4s.http.HttpEndpoint +import smithy.api.Http object DynamicModelPizzaSpec extends SimpleIOSuite { @@ -16,16 +36,51 @@ object DynamicModelPizzaSpec extends SimpleIOSuite { .get(cwd + "/modules/protocol/resources/META-INF/smithy/smithy4s.smithy") .toAbsolutePath() - val expected: Model = { - Model( - smithy = Some("1.0"), - shapes = Map( - ) - ) + test("Decode operation") { + IO( + SModel + .assembler() + .addImport(smithy4sPrelude) + .addImport(pizzaSpec) + .assemble() + .unwrap() + ).map(ModelSerializer.builder().build.serialize(_)) + .map(NodeToDocument(_)) + .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) + .flatMap(_.liftTo[IO]) + .map { model => + expect( + model.shapes(IdRef("smithy4s.example#Health")) == Shape.OperationCase( + OperationShape( + Some(MemberShape(IdRef("smithy4s.example#HealthRequest"), None)), + Some(MemberShape(IdRef("smithy4s.example#HealthResponse"), None)), + Some( + List( + MemberShape( + IdRef("smithy4s.example#UnknownServerError"), + None + ) + ) + ), + Some( + Map( + IdRef( + "smithy.api#http" + ) -> Document.obj( + "code" -> Document.fromInt(200), + "method" -> Document.fromString("GET"), + "uri" -> Document.fromString("/health") + ), + IdRef("smithy.api#readonly") -> Document.obj() + ) + ) + ) + ) + ) + } } - test("Decode json representation of models") { - // Unable to resolve trait `smithy4s.api#simpleRestJson`. If this is a custom trait, then it must be defined before it can be used in a model. + test("Compile HTTP operation") { IO( SModel .assembler() @@ -37,7 +92,16 @@ object DynamicModelPizzaSpec extends SimpleIOSuite { .map(NodeToDocument(_)) .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) .flatMap(_.liftTo[IO]) - .map(obtained => expect.same(obtained, expected)) + .map { model => + val compiled = dynamic.Compiler.compile(model, Http) + + val endpoints = compiled.allServices.head.service.endpoints + val httpEndpoints = endpoints.map(HttpEndpoint.cast(_)) + + expect( + httpEndpoints.forall(_.isDefined) + ) + } } } diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala index cc67340cc..b161e13a1 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.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.dynamic.model import weaver._ @@ -13,6 +29,18 @@ import schematic.Field object DynamicModelSpec extends SimpleIOSuite { + def parse(string: String): IO[Model] = + IO( + SModel + .assembler() + .addUnparsedModel("foo.smithy", string) + .assemble() + .unwrap() + ).map(ModelSerializer.builder().build.serialize(_)) + .map(NodeToDocument(_)) + .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) + .flatMap(_.liftTo[IO]) + val modelString = """|namespace foo | @@ -100,19 +128,26 @@ object DynamicModelSpec extends SimpleIOSuite { } test("Decode json representation of models") { - IO( - SModel - .assembler() - .addUnparsedModel("foo.smithy", modelString) - .assemble() - .unwrap() - ).map(ModelSerializer.builder().build.serialize(_)) - .map(NodeToDocument(_)) - .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) - .flatMap(_.liftTo[IO]) + parse(modelString) .map(obtained => expect.same(obtained, expected)) } + test( + "Compilation does not recurse infinitely in the case of recursive structures".only + ) { + val modelString = + """|namespace foo + | + |structure Foo { + | foo: Foo + |} + |""".stripMargin + + parse(modelString).flatMap { dynamicModel => + IO(dynamic.Compiler.compile(dynamicModel)).as(success) + } + } + object Interpreter { type ToFieldNames[A] = () => List[String] diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala index baa933fc3..83ac99941 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.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 package dynamic.model @@ -23,13 +39,13 @@ object JsonIOProtocol { kleisliCache(ep).apply(input) } } - S.transform(transfo) + S.transform[GenLift[IO]#λ](transfo) } def toJsonIO[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]]( alg: Monadic[Alg, IO] )(implicit S: Service[Alg, Op]): Document => IO[Document] = { - val transformation = S.asTransformation(alg) + val transformation = S.asTransformation[GenLift[IO]#λ](alg) val jsonEndpoints = S.endpoints.map(ep => ep.name -> toLowLevel(transformation, ep)).toMap (d: Document) => diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala index e6b51dd86..aba64cd64 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.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.dynamic.model import smithy4s.Document diff --git a/modules/example/src/smithy4s/example/ArbitraryData.scala b/modules/example/src/smithy4s/example/ArbitraryData.scala index d79d7c933..df1d9ac70 100644 --- a/modules/example/src/smithy4s/example/ArbitraryData.scala +++ b/modules/example/src/smithy4s/example/ArbitraryData.scala @@ -5,7 +5,7 @@ import smithy4s.Newtype import smithy4s.syntax._ object ArbitraryData extends Newtype[Document] { - val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "ArbitraryData") + val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "arbitraryData") val hints : smithy4s.Hints = smithy4s.Hints( id, smithy.api.Trait(None, None, None), From ba66a54a7b19477ea3daf25e6b2b447fe9426ed4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Fri, 21 Jan 2022 14:19:37 +0100 Subject: [PATCH 18/28] wip --- .../dynamic/DynamicModelCompiler.scala | 9 ++- .../dynamic/DynamicJsonServerSpec.scala | 42 ++++++++++ .../{model => }/DynamicModelPizzaSpec.scala | 7 +- .../{model => }/DynamicModelSpec.scala | 29 ++----- .../dynamic/{model => }/NodeToDocument.scala | 2 +- .../test/src-jvm/smithy4s/dynamic/Utils.scala | 29 +++++++ .../smithy4s/dynamic/DefaultSchematic.scala | 80 +++++++++++++++++++ .../src/smithy4s/dynamic/DummyService.scala | 28 +++++++ .../smithy4s/dynamic}/JsonIOProtocol.scala | 17 +++- 9 files changed, 214 insertions(+), 29 deletions(-) create mode 100644 modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala rename modules/dynamic/test/src-jvm/smithy4s/dynamic/{model => }/DynamicModelPizzaSpec.scala (98%) rename modules/dynamic/test/src-jvm/smithy4s/dynamic/{model => }/DynamicModelSpec.scala (87%) rename modules/dynamic/test/src-jvm/smithy4s/dynamic/{model => }/NodeToDocument.scala (98%) create mode 100644 modules/dynamic/test/src-jvm/smithy4s/dynamic/Utils.scala create mode 100644 modules/dynamic/test/src/smithy4s/dynamic/DefaultSchematic.scala create mode 100644 modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala rename modules/dynamic/test/{src-jvm/smithy4s/dynamic/model => src/smithy4s/dynamic}/JsonIOProtocol.scala (86%) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index ce44172e7..5ba6fea20 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -109,7 +109,14 @@ object Compiler { "Long" -> Shape.LongCase(LongShape()), "Float" -> Shape.FloatCase(FloatShape()), "Double" -> Shape.DoubleCase(DoubleShape()), - "Unit" -> Shape.StructureCase(StructureShape()) + "Unit" -> Shape.StructureCase(StructureShape()), + "PrimitiveBoolean" -> Shape.BooleanCase(BooleanShape()), + "PrimitiveByte" -> Shape.ByteCase(ByteShape()), + "PrimitiveShort" -> Shape.ShortCase(ShortShape()), + "PrimitiveInteger" -> Shape.IntegerCase(IntegerShape()), + "PrimitiveLong" -> Shape.LongCase(LongShape()), + "PrimitiveFloat" -> Shape.FloatCase(FloatShape()), + "PrimitiveDouble" -> Shape.DoubleCase(DoubleShape()) ).foreach { case (id, shape) => visitor(ShapeId("smithy.api", id), shape) } model.shapes.foreach { diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala new file mode 100644 index 000000000..b09f2c1d9 --- /dev/null +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala @@ -0,0 +1,42 @@ +package smithy4s.dynamic + +object DynamicJsonServerSpec { + + val modelString = """|namespace foo + | + |service KVStore { + | operations[Get, Set, Delete] + |} + | + |operation Set { + | input: KeyValue + |} + | + |operation Get { + | input: Key, + | output: Value + |} + | + |operation Delete { + | output: + |} + | + |structure Key { + | @required + | key: String + |} + | + |structure KeyValue { + | @required + | key: String, + | @required + | value: String + |} + | + |structure Value { + | @required + | value: String + |} + |""".stripMargin + +} diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicModelPizzaSpec.scala similarity index 98% rename from modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala rename to modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicModelPizzaSpec.scala index 392b107e1..464c80b4b 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicModelPizzaSpec.scala @@ -14,18 +14,19 @@ * limitations under the License. */ -package smithy4s.dynamic.model +package smithy4s +package dynamic +import model._ import weaver._ import software.amazon.smithy.model.{Model => SModel} import software.amazon.smithy.model.shapes.ModelSerializer import cats.effect.IO -import cats.syntax.all._ import java.nio.file.Paths -import smithy4s.Document import smithy4s.dynamic import smithy4s.http.HttpEndpoint import smithy.api.Http +import cats.syntax.all._ object DynamicModelPizzaSpec extends SimpleIOSuite { diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicModelSpec.scala similarity index 87% rename from modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala rename to modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicModelSpec.scala index b161e13a1..3d5a46ab9 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicModelSpec.scala @@ -14,33 +14,17 @@ * limitations under the License. */ -package smithy4s.dynamic.model +package smithy4s.dynamic +import model._ import weaver._ import smithy4s.Document -import software.amazon.smithy.model.{Model => SModel} -import software.amazon.smithy.model.shapes.ModelSerializer -import cats.effect.IO -import cats.syntax.all._ -import smithy4s.dynamic import smithy4s.Service import smithy4s.Hints import schematic.Field object DynamicModelSpec extends SimpleIOSuite { - def parse(string: String): IO[Model] = - IO( - SModel - .assembler() - .addUnparsedModel("foo.smithy", string) - .assemble() - .unwrap() - ).map(ModelSerializer.builder().build.serialize(_)) - .map(NodeToDocument(_)) - .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) - .flatMap(_.liftTo[IO]) - val modelString = """|namespace foo | @@ -128,7 +112,8 @@ object DynamicModelSpec extends SimpleIOSuite { } test("Decode json representation of models") { - parse(modelString) + Utils + .parse(modelString) .map(obtained => expect.same(obtained, expected)) } @@ -143,9 +128,7 @@ object DynamicModelSpec extends SimpleIOSuite { |} |""".stripMargin - parse(modelString).flatMap { dynamicModel => - IO(dynamic.Compiler.compile(dynamicModel)).as(success) - } + Utils.compile(modelString).as(success) } object Interpreter { @@ -207,7 +190,7 @@ object DynamicModelSpec extends SimpleIOSuite { pureTest( "Extract field names from all structures in a service's endpoints" ) { - val svc = dynamic.Compiler.compile(expected).allServices.head.service + val svc = Utils.compile(expected).allServices.head.service // NoSuchElementException: key not found: smithy.api#String val result = Interpreter.toFieldNames(svc) diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/NodeToDocument.scala similarity index 98% rename from modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala rename to modules/dynamic/test/src-jvm/smithy4s/dynamic/NodeToDocument.scala index aba64cd64..16830edea 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/NodeToDocument.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package smithy4s.dynamic.model +package smithy4s.dynamic import smithy4s.Document import software.amazon.smithy.model.node._ diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/Utils.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/Utils.scala new file mode 100644 index 000000000..95bbb1e79 --- /dev/null +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/Utils.scala @@ -0,0 +1,29 @@ +package smithy4s.dynamic + +import model.Model +import software.amazon.smithy.model.{Model => SModel} +import software.amazon.smithy.model.shapes.ModelSerializer +import cats.syntax.all._ +import cats.effect.IO + +object Utils { + + def compile(string: String): IO[DynamicModel] = + parse(string).map(compile) + + def compile(model: Model): DynamicModel = + Compiler.compile(model) + + def parse(string: String): IO[Model] = + IO( + SModel + .assembler() + .addUnparsedModel("dynamic.smithy", string) + .assemble() + .unwrap() + ).map(ModelSerializer.builder().build.serialize(_)) + .map(NodeToDocument(_)) + .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) + .flatMap(_.liftTo[IO]) + +} diff --git a/modules/dynamic/test/src/smithy4s/dynamic/DefaultSchematic.scala b/modules/dynamic/test/src/smithy4s/dynamic/DefaultSchematic.scala new file mode 100644 index 000000000..b37a2a253 --- /dev/null +++ b/modules/dynamic/test/src/smithy4s/dynamic/DefaultSchematic.scala @@ -0,0 +1,80 @@ +package smithy4s +package dynamic + +import cats.Id +import java.util.UUID +import schematic.ByteArray +import schematic.Field +import schematic.Alt + +object DefaultSchematic + extends smithy4s.Schematic[Id] + with schematic.struct.GenericAritySchematic[Id] { + + def short: Id[Short] = 0 + def int: Id[Int] = 0 + def long: Id[Long] = 0 + def double: Id[Double] = 0 + def float: Id[Float] = 0 + def bigint: Id[BigInt] = 0 + + def bigdecimal: Id[BigDecimal] = 0 + + def string: Id[String] = "" + + def boolean: Id[Boolean] = true + + def uuid: Id[UUID] = new UUID(0, 0) + + def byte: Id[Byte] = 0.toByte + + def bytes: Id[ByteArray] = ByteArray(Array.emptyByteArray) + + def unit: Id[Unit] = () + + def list[S](fs: Id[S]): Id[List[S]] = List.empty + + def set[S](fs: Id[S]): Id[Set[S]] = Set.empty + + def vector[S](fs: Id[S]): Id[Vector[S]] = Vector.empty + + def map[K, V](fk: Id[K], fv: Id[V]): Id[Map[K, V]] = Map.empty + + def genericStruct[S](fields: Vector[Field[Id, S, _]])( + const: Vector[Any] => S + ): Id[S] = const(fields.map(_.fold(new Field.Folder[Id, S, Any] { + def onRequired[A](label: String, instance: Id[A], get: S => A): Any = + instance + + def onOptional[A]( + label: String, + instance: Id[A], + get: S => Option[A] + ): Any = + None + }))) + + def union[S](first: Alt[Id, S, _], rest: Vector[Alt[Id, S, _]])( + total: S => Alt.WithValue[Id, S, _] + ): Id[S] = { + def processAlt[A](alt: Alt[Id, S, A]) = alt.inject(alt.instance) + processAlt(first) + } + + def enumeration[A]( + to: A => (String, Int), + fromName: Map[String, A], + fromOrdinal: Map[Int, A] + ): Id[A] = fromName.head._2 + + def suspend[A](f: => Id[A]): Id[A] = f + + def bijection[A, B](f: Id[A], to: A => B, from: B => A): Id[B] = to(f) + + def timestamp: Id[Timestamp] = Timestamp.fromEpochSecond(0L) + + def withHints[A](fa: Id[A], hints: Hints): Id[A] = fa + + def document: Id[Document] = Document.DNull + +} diff --git a/modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala b/modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala new file mode 100644 index 000000000..6cc690c83 --- /dev/null +++ b/modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala @@ -0,0 +1,28 @@ +package smithy4s +package dynamic + +import cats.Applicative + +object DummyService { + + def apply[F[_]]: PartiallyApplied[F] = new PartiallyApplied[F] + + class PartiallyApplied[F[_]] { + def create[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]](implicit + service: Service[Alg, Op], + F: Applicative[F] + ): smithy4s.Monadic[Alg, F] = { + service.transform { + service.opToEndpoint.andThen( + new Transformation[Endpoint[Op, *, *, *, *, *], GenLift[F]#λ] { + def apply[I, E, O, SI, SO]( + ep: Endpoint[Op, I, E, O, SI, SO] + ): F[O] = + F.pure(ep.output.compile(DefaultSchematic)) + } + ) + } + } + } + +} diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala b/modules/dynamic/test/src/smithy4s/dynamic/JsonIOProtocol.scala similarity index 86% rename from modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala rename to modules/dynamic/test/src/smithy4s/dynamic/JsonIOProtocol.scala index 83ac99941..10e57adba 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala +++ b/modules/dynamic/test/src/smithy4s/dynamic/JsonIOProtocol.scala @@ -15,7 +15,7 @@ */ package smithy4s -package dynamic.model +package dynamic import cats.effect.IO import cats.syntax.all._ @@ -28,6 +28,21 @@ import cats.syntax.all._ */ object JsonIOProtocol { + def dummy[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]]( + service: Service.Provider[Alg, Op] + ): Document => IO[Document] = { + implicit val S: Service[Alg, Op] = service.service + toJsonIO[Alg, Op](DummyService[IO].create[Alg, Op]) + } + + def proxy[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]]( + jsonIO: Document => IO[Document], + service: Service.Provider[Alg, Op] + ): Document => IO[Document] = { + implicit val S: Service[Alg, Op] = service.service + toJsonIO[Alg, Op](fromJsonIO[Alg, Op](jsonIO)) + } + def fromJsonIO[Alg[_[_, _, _, _, _]], Op[_, _, _, _, _]]( jsonIO: Document => IO[Document] )(implicit S: Service[Alg, Op]): Monadic[Alg, IO] = { From faa231a5c8a60a27efef38d7d1ffc05198777510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 21 Jan 2022 22:08:38 +0100 Subject: [PATCH 19/28] Move dynamic tests to shared sources --- .../smithy4s/dynamic/model/DynamicModelPizzaSpec.scala | 0 .../smithy4s/dynamic/model/DynamicModelSpec.scala | 0 .../{src-jvm => src}/smithy4s/dynamic/model/JsonIOProtocol.scala | 0 .../{src-jvm => src}/smithy4s/dynamic/model/NodeToDocument.scala | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename modules/dynamic/test/{src-jvm => src}/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala (100%) rename modules/dynamic/test/{src-jvm => src}/smithy4s/dynamic/model/DynamicModelSpec.scala (100%) rename modules/dynamic/test/{src-jvm => src}/smithy4s/dynamic/model/JsonIOProtocol.scala (100%) rename modules/dynamic/test/{src-jvm => src}/smithy4s/dynamic/model/NodeToDocument.scala (100%) diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala b/modules/dynamic/test/src/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala similarity index 100% rename from modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala rename to modules/dynamic/test/src/smithy4s/dynamic/model/DynamicModelPizzaSpec.scala diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala b/modules/dynamic/test/src/smithy4s/dynamic/model/DynamicModelSpec.scala similarity index 100% rename from modules/dynamic/test/src-jvm/smithy4s/dynamic/model/DynamicModelSpec.scala rename to modules/dynamic/test/src/smithy4s/dynamic/model/DynamicModelSpec.scala diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala b/modules/dynamic/test/src/smithy4s/dynamic/model/JsonIOProtocol.scala similarity index 100% rename from modules/dynamic/test/src-jvm/smithy4s/dynamic/model/JsonIOProtocol.scala rename to modules/dynamic/test/src/smithy4s/dynamic/model/JsonIOProtocol.scala diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala b/modules/dynamic/test/src/smithy4s/dynamic/model/NodeToDocument.scala similarity index 100% rename from modules/dynamic/test/src-jvm/smithy4s/dynamic/model/NodeToDocument.scala rename to modules/dynamic/test/src/smithy4s/dynamic/model/NodeToDocument.scala From f80372a6d58eb651197d5c9bc58ab38b05939b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Sat, 22 Jan 2022 12:09:00 +0100 Subject: [PATCH 20/28] More tests --- .../src/smithy4s/http/HttpContractError.scala | 2 + .../dynamic/DynamicJsonServerSpec.scala | 88 ++++++++++++++++++- .../smithy4s/dynamic/PlatformUtils.scala | 4 +- 3 files changed, 88 insertions(+), 6 deletions(-) diff --git a/modules/core/src/smithy4s/http/HttpContractError.scala b/modules/core/src/smithy4s/http/HttpContractError.scala index fb0635078..0625e541d 100644 --- a/modules/core/src/smithy4s/http/HttpContractError.scala +++ b/modules/core/src/smithy4s/http/HttpContractError.scala @@ -42,6 +42,8 @@ case class PayloadError( expected: String, message: String ) extends HttpContractError { + override def toString(): String = + s"PayloadError($path, expected = $expected, message=$message)" override def getMessage(): String = message } diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala index 4dd73898e..3fe118707 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala @@ -14,14 +14,29 @@ * limitations under the License. */ -package smithy4s.dynamic +package smithy4s +package dynamic -object DynamicJsonServerSpec { +import cats.effect._ +import cats.syntax.all._ + +/** + * This spec dynamically compiles a KV Store service, + * creates a dummy instance for it (that returns 0 values for all the fields of outputs), + * and serves it over an example JSON-RPC-like protocol (returning a `Document => IO[Document]`) + */ +object DynamicJsonServerSpec extends weaver.IOSuite { + + case class JsonIO(fn: Document => IO[Document]) { + def apply(json: Document): IO[Document] = fn(json) + } + + type Res = JsonIO val modelString = """|namespace foo | |service KVStore { - | operations[Get, Set, Delete] + | operations: [Get, Set, Delete] |} | |operation Set { @@ -34,7 +49,7 @@ object DynamicJsonServerSpec { |} | |operation Delete { - | output: + | input: Key |} | |structure Key { @@ -55,4 +70,69 @@ object DynamicJsonServerSpec { |} |""".stripMargin + def sharedResource: Resource[IO, Res] = Resource.eval { + Utils + .compile(modelString) + .flatMap { dynamicModel => + dynamicModel.allServices + .find(_.service.id == ShapeId("foo", "KVStore")) + .liftTo[IO](new Throwable("Not found")) + } + .map(service => JsonIOProtocol.dummy(service.service)) + .map(JsonIO(_)) + } + + test("Dynamic service is correctly wired: Set") { jsonIO => + val expected = Document.obj() + + jsonIO( + Document.obj( + "Set" -> Document.obj( + "key" -> Document.fromString("K"), + "value" -> Document.fromString("V") + ) + ) + ).map { result => + expect.same(result, expected) + } + } + + test("Dynamic service is correctly wired: Get") { jsonIO => + val expected = Document.obj("value" -> Document.fromString("")) + + jsonIO( + Document.obj( + "Get" -> Document.obj( + "key" -> Document.fromString("K") + ) + ) + ).map { result => + expect.same(result, expected) + } + } + + test("Dynamic service is correctly wired: Bad Json Input") { jsonIO => + val expected = smithy4s.http.PayloadError( + PayloadPath("key"), + "", + "Required field not found" + ) + + jsonIO( + Document.obj("Get" -> Document.obj()) + ).attempt.map { result => + expect(result == Left(expected)) + } + } + + test("Dynamic service is correctly wired: Bad operation") { jsonIO => + val expected = JsonIOProtocol.NotFound + + jsonIO( + Document.obj("Unknown" -> Document.DNull) + ).attempt.map { result => + expect(result == Left(expected)) + } + } + } diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/PlatformUtils.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/PlatformUtils.scala index 6f584628e..e1d76221f 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/PlatformUtils.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/PlatformUtils.scala @@ -22,10 +22,10 @@ import software.amazon.smithy.model.shapes.ModelSerializer import cats.syntax.all._ import cats.effect.IO -private[dynamic] trait PlatformUtils { +private[dynamic] trait PlatformUtils { self: Utils.type => def compile(string: String): IO[DynamicModel] = - parse(string).map(compile) + parse(string).map(self.compile) def parse(string: String): IO[Model] = IO( From e44dfaa445118ff451af07aa06a808dca4c686a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Sat, 22 Jan 2022 12:15:36 +0100 Subject: [PATCH 21/28] 2.12 --- modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala b/modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala index 9be7cf0ad..7c627d022 100644 --- a/modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala +++ b/modules/dynamic/test/src/smithy4s/dynamic/DummyService.scala @@ -28,8 +28,8 @@ object DummyService { service: Service[Alg, Op], F: Applicative[F] ): smithy4s.Monadic[Alg, F] = { - service.transform { - service.opToEndpoint.andThen( + service.transform[GenLift[F]#λ] { + service.opToEndpoint.andThen[GenLift[F]#λ]( new Transformation[Endpoint[Op, *, *, *, *, *], GenLift[F]#λ] { def apply[I, E, O, SI, SO]( ep: Endpoint[Op, I, E, O, SI, SO] From 56cf25988838164ab1229fb94f2a08de9af27939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 26 Jan 2022 21:13:16 +0100 Subject: [PATCH 22/28] Post-rebase fixes --- .../dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala | 4 ++-- .../test/src/smithy4s/dynamic/DefaultSchematic.scala | 6 ++---- modules/dynamic/test/src/smithy4s/dynamic/FieldsSpec.scala | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index 5ba6fea20..00dfd8965 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -339,8 +339,8 @@ object Compiler { .sequence } if (isRecursive(id)) - Eval.later(suspend(genericStruct(lFields.value)(dynStruct))) - else lFields.map(fields => genericStruct(fields)(dynStruct)) + Eval.later(suspend(struct(lFields.value)(dynStruct))) + else lFields.map(fields => struct(fields)(dynStruct)) } ) diff --git a/modules/dynamic/test/src/smithy4s/dynamic/DefaultSchematic.scala b/modules/dynamic/test/src/smithy4s/dynamic/DefaultSchematic.scala index 61566ef68..0eec0bfde 100644 --- a/modules/dynamic/test/src/smithy4s/dynamic/DefaultSchematic.scala +++ b/modules/dynamic/test/src/smithy4s/dynamic/DefaultSchematic.scala @@ -23,9 +23,7 @@ import schematic.ByteArray import schematic.Field import schematic.Alt -object DefaultSchematic - extends smithy4s.Schematic[Id] - with schematic.struct.GenericAritySchematic[Id] { +object DefaultSchematic extends smithy4s.Schematic[Id] { def short: Id[Short] = 0 def int: Id[Int] = 0 @@ -56,7 +54,7 @@ object DefaultSchematic def map[K, V](fk: Id[K], fv: Id[V]): Id[Map[K, V]] = Map.empty - def genericStruct[S](fields: Vector[Field[Id, S, _]])( + def struct[S](fields: Vector[Field[Id, S, _]])( const: Vector[Any] => S ): Id[S] = const(fields.map(_.fold(new Field.Folder[Id, S, Any] { def onRequired[A](label: String, instance: Id[A], get: S => A): Any = diff --git a/modules/dynamic/test/src/smithy4s/dynamic/FieldsSpec.scala b/modules/dynamic/test/src/smithy4s/dynamic/FieldsSpec.scala index b9253ecd9..24068e0ce 100644 --- a/modules/dynamic/test/src/smithy4s/dynamic/FieldsSpec.scala +++ b/modules/dynamic/test/src/smithy4s/dynamic/FieldsSpec.scala @@ -45,7 +45,7 @@ object FieldsSpec extends SimpleIOSuite { hints: Hints ): ToFieldNames[A] = fa - override def genericStruct[S]( + override def struct[S]( fields: Vector[Field[ToFieldNames, S, _]] )(const: Vector[Any] => S): ToFieldNames[S] = () => fields.flatMap { f => From a68a4a296bb7be120e0d1e9acc3d4f3701766b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Fri, 28 Jan 2022 22:59:13 +0100 Subject: [PATCH 23/28] Update dynamic with SchemaIndex construct --- .../src/smithy4s/codegen/Renderer.scala | 10 +++---- modules/core/src/smithy4s/HintMask.scala | 4 +-- modules/core/src/smithy4s/SchemaIndex.scala | 2 ++ .../dynamic/DynamicModelCompiler.scala | 21 ++++++++------ .../src/smithy4s/dynamic/KeyedSchema.scala | 29 ------------------- .../smithy4s/dynamic/OperationSpec.scala | 2 +- .../test/src/smithy4s/dynamic/Utils.scala | 3 +- .../smithy4s/example/ArbitraryDataTest.scala | 2 +- .../example/src/smithy4s/example/Foo.scala | 2 +- .../src/smithy4s/example/GetFooOutput.scala | 2 +- .../src/smithy4s/example/GetObjectInput.scala | 2 +- .../smithy4s/example/GetObjectOutput.scala | 2 +- .../example/GetStreamedObjectInput.scala | 2 +- .../example/GetStreamedObjectOutput.scala | 2 +- .../src/smithy4s/example/LowHigh.scala | 2 +- .../src/smithy4s/example/NoMoreSpace.scala | 2 +- .../src/smithy4s/example/ObjectService.scala | 4 +-- .../src/smithy4s/example/PutObjectInput.scala | 2 +- .../example/PutStreamedObjectInput.scala | 2 +- .../src/smithy4s/example/ServerError.scala | 2 +- 20 files changed, 38 insertions(+), 61 deletions(-) delete mode 100644 modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala diff --git a/modules/codegen/src/smithy4s/codegen/Renderer.scala b/modules/codegen/src/smithy4s/codegen/Renderer.scala index 5df52de3f..08ed62d53 100644 --- a/modules/codegen/src/smithy4s/codegen/Renderer.scala +++ b/modules/codegen/src/smithy4s/codegen/Renderer.scala @@ -350,7 +350,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => } } } else line(decl), - obj(name, ext = hintKey(name, hints))( + obj(name, ext = shapeTag(name))( renderId(originalName), newline, renderHintsValWithId(hints), @@ -422,7 +422,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => val imports = alts.foldMap(_.tpe.imports) ++ syntaxImport lines( s"sealed trait $name extends scala.Product with scala.Serializable", - obj(name, ext = hintKey(name, hints))( + obj(name, ext = shapeTag(name))( renderId(originalName), newline, renderHintsValWithId(hints), @@ -482,7 +482,7 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => hints: List[Hint] ): RenderResult = lines( s"sealed abstract class $name(val value: String, val ordinal: Int) extends scala.Product with scala.Serializable", - obj(name, ext = s"$Enumeration_[$name]", w = hintKey(name, hints))( + obj(name, ext = s"$Enumeration_[$name]", w = shapeTag(name))( renderId(originalName), newline, renderHintsValWithId(hints), @@ -674,8 +674,8 @@ private[codegen] class Renderer(compilationUnit: CompilationUnit) { self => if (h.isEmpty) "" else h.mkString(", ") } - private def hintKey(name: String, hints: List[Hint]): String = - if (hints.contains(Hint.Trait)) s"$ShapeTag_.Companion[$name]" else "" + private def shapeTag(name: String): String = + s"$ShapeTag_.Companion[$name]" type TopLevel = Boolean type InCollection = Boolean diff --git a/modules/core/src/smithy4s/HintMask.scala b/modules/core/src/smithy4s/HintMask.scala index e6fda4f7e..09ec3ba73 100644 --- a/modules/core/src/smithy4s/HintMask.scala +++ b/modules/core/src/smithy4s/HintMask.scala @@ -27,8 +27,8 @@ object HintMask { def empty: HintMask = apply() - def apply(hintKeys: ShapeTag[_]*): HintMask = { - new Impl(hintKeys.toSet) + def apply(shapeTags: ShapeTag[_]*): HintMask = { + new Impl(shapeTags.toSet) } private[this] case object Permissive extends HintMask { diff --git a/modules/core/src/smithy4s/SchemaIndex.scala b/modules/core/src/smithy4s/SchemaIndex.scala index a16ef5d9f..29c6aedd9 100644 --- a/modules/core/src/smithy4s/SchemaIndex.scala +++ b/modules/core/src/smithy4s/SchemaIndex.scala @@ -33,6 +33,8 @@ sealed trait SchemaIndex { object SchemaIndex { + val empty: SchemaIndex = new Impl(Map.empty) + def apply[S](bindings: Binding[_]*): SchemaIndex = { new Impl(bindings.map(b => b.tuple: (ShapeTag[_], Schema[_])).toMap) } diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala index 00dfd8965..7ce9beaca 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala @@ -47,11 +47,11 @@ object Compiler { shapeId.namespace + "#" + shapeId.name ) - private def getTrait[A: Hints.Key: Document.Decoder]( + private def getTrait[A: ShapeTag: Document.Decoder]( traits: Option[Map[IdRef, Document]] ): Option[A] = traits.flatMap(dynamicTraits => - dynamicTraits.get(toIdRef(implicitly[Hints.Key[A]].id)).flatMap { + dynamicTraits.get(toIdRef(implicitly[ShapeTag[A]].id)).flatMap { document => document.decode[A].toOption } @@ -60,24 +60,27 @@ object Compiler { /** * @param knownHints hints supported by the caller. */ - def compile(model: Model, knownHints: KeyedSchema[_]*): DynamicModel = { + def compile(model: Model, knownHints: SchemaIndex): DynamicModel = { val schemaMap = MMap.empty[ShapeId, Eval[Schema[DynData]]] val endpointMap = MMap.empty[ShapeId, Eval[DynamicEndpoint]] val serviceMap = MMap.empty[ShapeId, Eval[DynamicService]] - val hintsMap: Map[ShapeId, KeyedSchema[_]] = knownHints.map { ks => - val shapeId: ShapeId = ks.hintKey.id - shapeId -> ks - }.toMap + val hintsMap: Map[ShapeId, SchemaIndex.Binding[_]] = { + def binding[A]( + tag: ShapeTag[A] + ): Option[(ShapeId, SchemaIndex.Binding[A])] = + knownHints.get(tag).map(s => tag.id -> SchemaIndex.Binding(tag, s)) + knownHints.tags.flatMap(binding(_)).toMap + } def toHintAux[A]( - ks: KeyedSchema[A], + ks: SchemaIndex.Binding[A], documentRepr: Document ): Option[Hint] = { val decoded: Option[A] = Document.Decoder.fromSchema(ks.schema).decode(documentRepr).toOption decoded.map { value => - Hints.Binding(ks.hintKey, value) + Hints.Binding(ks.tag, value) } } diff --git a/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala b/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala deleted file mode 100644 index 9108c1f72..000000000 --- a/modules/dynamic/src/smithy4s/dynamic/KeyedSchema.scala +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 dynamic - -case class KeyedSchema[A](schema: Schema[A], hintKey: Hints.Key[A]) - -case object KeyedSchema { - - implicit def fromKey[A](key: Hints.Key[A])(implicit - schema: Schema[A] - ): KeyedSchema[A] = - KeyedSchema[A](schema, key) - -} diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/OperationSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/OperationSpec.scala index 8449fbed6..e34adecb3 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/OperationSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/OperationSpec.scala @@ -94,7 +94,7 @@ object OperationSpec extends SimpleIOSuite { .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) .flatMap(_.liftTo[IO]) .map { model => - val compiled = dynamic.Compiler.compile(model, Http) + val compiled = dynamic.Compiler.compile(model, SchemaIndex(Http)) val endpoints = compiled.allServices.head.service.endpoints val httpEndpoints = endpoints.map(HttpEndpoint.cast(_)) diff --git a/modules/dynamic/test/src/smithy4s/dynamic/Utils.scala b/modules/dynamic/test/src/smithy4s/dynamic/Utils.scala index 224203152..76f7a1f1e 100644 --- a/modules/dynamic/test/src/smithy4s/dynamic/Utils.scala +++ b/modules/dynamic/test/src/smithy4s/dynamic/Utils.scala @@ -17,10 +17,11 @@ package smithy4s.dynamic import model.Model +import smithy4s.SchemaIndex object Utils extends PlatformUtils { def compile(model: Model): DynamicModel = - Compiler.compile(model) + Compiler.compile(model, SchemaIndex.empty) } diff --git a/modules/example/src/smithy4s/example/ArbitraryDataTest.scala b/modules/example/src/smithy4s/example/ArbitraryDataTest.scala index c3d10fb51..9a850b206 100644 --- a/modules/example/src/smithy4s/example/ArbitraryDataTest.scala +++ b/modules/example/src/smithy4s/example/ArbitraryDataTest.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ case class ArbitraryDataTest() -object ArbitraryDataTest { +object ArbitraryDataTest extends smithy4s.ShapeTag.Companion[ArbitraryDataTest] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "ArbitraryDataTest") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/Foo.scala b/modules/example/src/smithy4s/example/Foo.scala index 54dedca6f..3d5b2cda2 100644 --- a/modules/example/src/smithy4s/example/Foo.scala +++ b/modules/example/src/smithy4s/example/Foo.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ sealed trait Foo extends scala.Product with scala.Serializable -object Foo { +object Foo extends smithy4s.ShapeTag.Companion[Foo] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "Foo") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/GetFooOutput.scala b/modules/example/src/smithy4s/example/GetFooOutput.scala index feb4099db..64f6cbabe 100644 --- a/modules/example/src/smithy4s/example/GetFooOutput.scala +++ b/modules/example/src/smithy4s/example/GetFooOutput.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ case class GetFooOutput(foo: Option[Foo] = None) -object GetFooOutput { +object GetFooOutput extends smithy4s.ShapeTag.Companion[GetFooOutput] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "GetFooOutput") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/GetObjectInput.scala b/modules/example/src/smithy4s/example/GetObjectInput.scala index 3dff558fc..c2f65d098 100644 --- a/modules/example/src/smithy4s/example/GetObjectInput.scala +++ b/modules/example/src/smithy4s/example/GetObjectInput.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ case class GetObjectInput(key: ObjectKey, bucketName: BucketName) -object GetObjectInput { +object GetObjectInput extends smithy4s.ShapeTag.Companion[GetObjectInput] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "GetObjectInput") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/GetObjectOutput.scala b/modules/example/src/smithy4s/example/GetObjectOutput.scala index 6b8b70139..049d3be24 100644 --- a/modules/example/src/smithy4s/example/GetObjectOutput.scala +++ b/modules/example/src/smithy4s/example/GetObjectOutput.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ case class GetObjectOutput(size: ObjectSize, data: Option[String] = None) -object GetObjectOutput { +object GetObjectOutput extends smithy4s.ShapeTag.Companion[GetObjectOutput] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "GetObjectOutput") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/GetStreamedObjectInput.scala b/modules/example/src/smithy4s/example/GetStreamedObjectInput.scala index 077205327..6db5c5e73 100644 --- a/modules/example/src/smithy4s/example/GetStreamedObjectInput.scala +++ b/modules/example/src/smithy4s/example/GetStreamedObjectInput.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ case class GetStreamedObjectInput(key: String) -object GetStreamedObjectInput { +object GetStreamedObjectInput extends smithy4s.ShapeTag.Companion[GetStreamedObjectInput] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "GetStreamedObjectInput") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/GetStreamedObjectOutput.scala b/modules/example/src/smithy4s/example/GetStreamedObjectOutput.scala index 843dec8a6..aa5950832 100644 --- a/modules/example/src/smithy4s/example/GetStreamedObjectOutput.scala +++ b/modules/example/src/smithy4s/example/GetStreamedObjectOutput.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ case class GetStreamedObjectOutput() -object GetStreamedObjectOutput { +object GetStreamedObjectOutput extends smithy4s.ShapeTag.Companion[GetStreamedObjectOutput] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "GetStreamedObjectOutput") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/LowHigh.scala b/modules/example/src/smithy4s/example/LowHigh.scala index ee8f607b1..d6d7eda69 100644 --- a/modules/example/src/smithy4s/example/LowHigh.scala +++ b/modules/example/src/smithy4s/example/LowHigh.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ sealed abstract class LowHigh(val value: String, val ordinal: Int) extends scala.Product with scala.Serializable -object LowHigh extends smithy4s.Enumeration[LowHigh] { +object LowHigh extends smithy4s.Enumeration[LowHigh] with smithy4s.ShapeTag.Companion[LowHigh] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "LowHigh") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/NoMoreSpace.scala b/modules/example/src/smithy4s/example/NoMoreSpace.scala index 61689467f..0f5a15385 100644 --- a/modules/example/src/smithy4s/example/NoMoreSpace.scala +++ b/modules/example/src/smithy4s/example/NoMoreSpace.scala @@ -5,7 +5,7 @@ import smithy4s.syntax._ case class NoMoreSpace(message: String, foo: Option[Foo] = None) extends Throwable { override def getMessage() : String = message } -object NoMoreSpace { +object NoMoreSpace extends smithy4s.ShapeTag.Companion[NoMoreSpace] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "NoMoreSpace") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/ObjectService.scala b/modules/example/src/smithy4s/example/ObjectService.scala index a982d82f4..1719bbdaa 100644 --- a/modules/example/src/smithy4s/example/ObjectService.scala +++ b/modules/example/src/smithy4s/example/ObjectService.scala @@ -81,7 +81,7 @@ object ObjectServiceGen extends smithy4s.Service[ObjectServiceGen, ObjectService } } sealed trait PutObjectError extends scala.Product with scala.Serializable - object PutObjectError { + object PutObjectError extends smithy4s.ShapeTag.Companion[PutObjectError] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "PutObjectError") val hints : smithy4s.Hints = smithy4s.Hints( @@ -135,7 +135,7 @@ object ObjectServiceGen extends smithy4s.Service[ObjectServiceGen, ObjectService } } sealed trait GetObjectError extends scala.Product with scala.Serializable - object GetObjectError { + object GetObjectError extends smithy4s.ShapeTag.Companion[GetObjectError] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "GetObjectError") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/PutObjectInput.scala b/modules/example/src/smithy4s/example/PutObjectInput.scala index 031c3bdba..363253838 100644 --- a/modules/example/src/smithy4s/example/PutObjectInput.scala +++ b/modules/example/src/smithy4s/example/PutObjectInput.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ case class PutObjectInput(key: ObjectKey, bucketName: BucketName, data: String, foo: Option[LowHigh] = None, someValue: Option[SomeValue] = None) -object PutObjectInput { +object PutObjectInput extends smithy4s.ShapeTag.Companion[PutObjectInput] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "PutObjectInput") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/PutStreamedObjectInput.scala b/modules/example/src/smithy4s/example/PutStreamedObjectInput.scala index dce07b431..ce3479317 100644 --- a/modules/example/src/smithy4s/example/PutStreamedObjectInput.scala +++ b/modules/example/src/smithy4s/example/PutStreamedObjectInput.scala @@ -3,7 +3,7 @@ package smithy4s.example import smithy4s.syntax._ case class PutStreamedObjectInput(key: String) -object PutStreamedObjectInput { +object PutStreamedObjectInput extends smithy4s.ShapeTag.Companion[PutStreamedObjectInput] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "PutStreamedObjectInput") val hints : smithy4s.Hints = smithy4s.Hints( diff --git a/modules/example/src/smithy4s/example/ServerError.scala b/modules/example/src/smithy4s/example/ServerError.scala index 69345d9fa..f1c8e176f 100644 --- a/modules/example/src/smithy4s/example/ServerError.scala +++ b/modules/example/src/smithy4s/example/ServerError.scala @@ -5,7 +5,7 @@ import smithy4s.syntax._ case class ServerError(message: Option[String] = None) extends Throwable { override def getMessage() : String = message.orNull } -object ServerError { +object ServerError extends smithy4s.ShapeTag.Companion[ServerError] { val id: smithy4s.ShapeId = smithy4s.ShapeId("smithy4s.example", "ServerError") val hints : smithy4s.Hints = smithy4s.Hints( From 705b5f3788aece977a5dfa0657020997fbaafa24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Wed, 9 Feb 2022 11:30:35 +0100 Subject: [PATCH 24/28] Cleanup Move most function to the internals package, make most things private --- ...icModel.scala => DynamicSchemaIndex.scala} | 38 +++++++++++-------- .../{ => internals}/DynamicEndpoint.scala | 3 +- .../DynamicModelCompiler.scala | 36 +++++++++--------- .../dynamic/{ => internals}/DynamicOp.scala | 4 +- .../internals/DynamicSchemaIndexImpl.scala | 18 +++++++++ .../{ => internals}/DynamicService.scala | 5 ++- .../{ => internals}/ShapeVisitor.scala | 5 ++- .../dynamic/{ => internals}/package.scala | 12 +++--- .../dynamic/DynamicJsonServerSpec.scala | 4 +- .../smithy4s/dynamic/OperationSpec.scala | 5 +-- .../smithy4s/dynamic/PlatformUtils.scala | 2 +- .../test/src/smithy4s/dynamic/Utils.scala | 4 +- 12 files changed, 82 insertions(+), 54 deletions(-) rename modules/dynamic/src/smithy4s/dynamic/{DynamicModel.scala => DynamicSchemaIndex.scala} (54%) rename modules/dynamic/src/smithy4s/dynamic/{ => internals}/DynamicEndpoint.scala (94%) rename modules/dynamic/src/smithy4s/dynamic/{ => internals}/DynamicModelCompiler.scala (94%) rename modules/dynamic/src/smithy4s/dynamic/{ => internals}/DynamicOp.scala (88%) create mode 100644 modules/dynamic/src/smithy4s/dynamic/internals/DynamicSchemaIndexImpl.scala rename modules/dynamic/src/smithy4s/dynamic/{ => internals}/DynamicService.scala (94%) rename modules/dynamic/src/smithy4s/dynamic/{ => internals}/ShapeVisitor.scala (97%) rename modules/dynamic/src/smithy4s/dynamic/{ => internals}/package.scala (68%) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicSchemaIndex.scala similarity index 54% rename from modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala rename to modules/dynamic/src/smithy4s/dynamic/DynamicSchemaIndex.scala index 51ddc1e5e..ed33c7608 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModel.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicSchemaIndex.scala @@ -17,24 +17,30 @@ package smithy4s package dynamic -class DynamicModel( - serviceMap: Map[ShapeId, DynamicService], - schemaMap: Map[ShapeId, Schema[DynData]] -) { - - def allServices: List[DynamicModel.ServiceWrapper] = - serviceMap.values.toList - - def getSchema( - namespace: String, - name: String - ): Option[Schema[_]] = { - val shapeId = ShapeId(namespace, name) - schemaMap.get(shapeId) - } +/** + * A dynamically loaded schema index, containing existential instances + * of services and schemas, that can be used to wire protocols together + * without requiring code generation. + */ +trait DynamicSchemaIndex { + def allServices: List[DynamicSchemaIndex.ServiceWrapper] + + def getSchema(shapeId: ShapeId): Option[Schema[_]] } -object DynamicModel { +object DynamicSchemaIndex { + + /** + * Loads the model from a dynamic representation of smithy models + * (typically json blobs). This representation is modelled in smithy itself, + * and code generated by smithy4s. + * + * @param model + * @param knownHints an index of schemas of hints that the user would like + * to retrieve from the dynamic representation of the model. + */ + def load(model: dynamic.model.Model, knownHints: SchemaIndex): DynamicSchemaIndex = + internals.Compiler.compile(model, knownHints) /** * A construct that hides the types a service instance works, diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicEndpoint.scala similarity index 94% rename from modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala rename to modules/dynamic/src/smithy4s/dynamic/internals/DynamicEndpoint.scala index dccdebdda..c7bfc3e8b 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicEndpoint.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicEndpoint.scala @@ -16,8 +16,9 @@ package smithy4s package dynamic +package internals -case class DynamicEndpoint( +private[internals] case class DynamicEndpoint( id: ShapeId, input: Schema[DynData], output: Schema[DynData], diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala similarity index 94% rename from modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala rename to modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala index 7ce9beaca..82a89ba59 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala @@ -16,23 +16,18 @@ package smithy4s package dynamic +package internals import smithy4s.dynamic.model._ import scala.collection.mutable.{Map => MMap} -import schematic.OneOf -import schematic.StructureField import smithy4s.syntax._ import smithy4s.internals.InputOutput import cats.Eval import cats.syntax.all._ -object Compiler { +private[dynamic] object Compiler { - type DynSchema = Schema[DynData] - type DynFieldSchema = StructureField[Schematic, DynStruct, DynData] - type DynAltSchema = OneOf[Schematic, DynAlt, DynData] - - object ValidIdRef { + private object ValidIdRef { def apply(idRef: IdRef): Option[ShapeId] = { val segments = idRef.value.split('#') if (segments.length == 2) { @@ -43,7 +38,7 @@ object Compiler { def unapply(idRef: IdRef): Option[ShapeId] = apply(idRef) } - def toIdRef(shapeId: ShapeId): IdRef = IdRef( + private def toIdRef(shapeId: ShapeId): IdRef = IdRef( shapeId.namespace + "#" + shapeId.name ) @@ -60,7 +55,10 @@ object Compiler { /** * @param knownHints hints supported by the caller. */ - def compile(model: Model, knownHints: SchemaIndex): DynamicModel = { + protected[dynamic] def compile( + model: Model, + knownHints: SchemaIndex + ): DynamicSchemaIndex = { val schemaMap = MMap.empty[ShapeId, Eval[Schema[DynData]]] val endpointMap = MMap.empty[ShapeId, Eval[DynamicEndpoint]] val serviceMap = MMap.empty[ShapeId, Eval[DynamicService]] @@ -126,7 +124,7 @@ object Compiler { case (ValidIdRef(id), shape) => visitor(id, shape) case _ => () } - new DynamicModel( + new DynamicSchemaIndexImpl( serviceMap.toMap.fmap(_.value), schemaMap.toMap.fmap(_.value) ) @@ -140,22 +138,26 @@ object Compiler { toHint: (ShapeId, Document) => Option[Hint] ) extends ShapeVisitor.Default[Unit] { - val closureMap: Map[ShapeId, Set[ShapeId]] = model.shapes.collect { + private val closureMap: Map[ShapeId, Set[ShapeId]] = model.shapes.collect { case (ValidIdRef(shapeId), shape) => shapeId -> ClosureVisitor(shapeId, shape) } - def isRecursive(id: ShapeId, visited: Set[ShapeId] = Set.empty): Boolean = + + private def isRecursive( + id: ShapeId, + visited: Set[ShapeId] = Set.empty + ): Boolean = visited(id) || closureMap .getOrElse(id, Set.empty) .exists(isRecursive(_, visited + id)) - def schema(idRef: IdRef): Eval[Schema[DynData]] = Eval.defer { + private def schema(idRef: IdRef): Eval[Schema[DynData]] = Eval.defer { schemaMap( ValidIdRef.unapply(idRef).get ) } - def allHints(traits: Option[Map[IdRef, Document]]): Seq[Hint] = { + private def allHints(traits: Option[Map[IdRef, Document]]): Seq[Hint] = { traits .getOrElse(Map.empty) .collect { case (ValidIdRef(k), v) => @@ -167,7 +169,7 @@ object Compiler { .toSeq } - def update[A]( + private def update[A]( shapeId: ShapeId, traits: Option[Map[IdRef, Document]], lSchema: Eval[Schema[A]] @@ -373,7 +375,7 @@ object Compiler { } // A visitor allowing to gather the "closure" of all shapes - object ClosureVisitor extends ShapeVisitor.Default[Set[ShapeId]] { + private object ClosureVisitor extends ShapeVisitor.Default[Set[ShapeId]] { def default: Set[ShapeId] = Set.empty def fromMembers(it: Iterable[MemberShape]): Set[ShapeId] = diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicOp.scala similarity index 88% rename from modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala rename to modules/dynamic/src/smithy4s/dynamic/internals/DynamicOp.scala index 2a88ad4ef..1390845a7 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicOp.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicOp.scala @@ -14,11 +14,11 @@ * limitations under the License. */ -package smithy4s.dynamic +package smithy4s.dynamic.internals import smithy4s.ShapeId -case class DynamicOp[I, E, O, SI, SO]( +private[internals] case class DynamicOp[I, E, O, SI, SO]( id: ShapeId, data: I ) diff --git a/modules/dynamic/src/smithy4s/dynamic/internals/DynamicSchemaIndexImpl.scala b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicSchemaIndexImpl.scala new file mode 100644 index 000000000..05757c4e2 --- /dev/null +++ b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicSchemaIndexImpl.scala @@ -0,0 +1,18 @@ +package smithy4s +package dynamic +package internals + +private[internals] class DynamicSchemaIndexImpl( + serviceMap: Map[ShapeId, DynamicService], + schemaMap: Map[ShapeId, Schema[DynData]] +) extends DynamicSchemaIndex { + + def allServices: List[DynamicSchemaIndex.ServiceWrapper] = + serviceMap.values.toList + + def getSchema( + shapeId: ShapeId + ): Option[Schema[_]] = + schemaMap.get(shapeId) + +} diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicService.scala similarity index 94% rename from modules/dynamic/src/smithy4s/dynamic/DynamicService.scala rename to modules/dynamic/src/smithy4s/dynamic/internals/DynamicService.scala index a91b6271a..d5bebc7e7 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicService.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicService.scala @@ -16,14 +16,15 @@ package smithy4s package dynamic +package internals -case class DynamicService( +private[internals] case class DynamicService( id: ShapeId, version: String, endpoints: List[DynamicEndpoint], hints: Hints ) extends Service[DynamicAlg, DynamicOp] - with DynamicModel.ServiceWrapper { + with DynamicSchemaIndex.ServiceWrapper { type Alg[P[_, _, _, _, _]] = DynamicAlg[P] type Op[I, E, O, SI, SO] = DynamicOp[I, E, O, SI, SO] diff --git a/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala b/modules/dynamic/src/smithy4s/dynamic/internals/ShapeVisitor.scala similarity index 97% rename from modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala rename to modules/dynamic/src/smithy4s/dynamic/internals/ShapeVisitor.scala index cb5a55370..988adc0e8 100644 --- a/modules/dynamic/src/smithy4s/dynamic/ShapeVisitor.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/ShapeVisitor.scala @@ -16,10 +16,11 @@ package smithy4s package dynamic +package internals import model._ -trait ShapeVisitor[T] extends ((ShapeId, Shape) => T) { +private[internals] trait ShapeVisitor[T] extends ((ShapeId, Shape) => T) { def apply(id: ShapeId, shape: Shape): T = shape match { case Shape.BlobCase(s) => blobShape(id, s) @@ -68,7 +69,7 @@ trait ShapeVisitor[T] extends ((ShapeId, Shape) => T) { def timestampShape(id: ShapeId, x: TimestampShape): T } -object ShapeVisitor { +private[internals] object ShapeVisitor { trait Default[T] extends ShapeVisitor[T] { def default: T diff --git a/modules/dynamic/src/smithy4s/dynamic/package.scala b/modules/dynamic/src/smithy4s/dynamic/internals/package.scala similarity index 68% rename from modules/dynamic/src/smithy4s/dynamic/package.scala rename to modules/dynamic/src/smithy4s/dynamic/internals/package.scala index 6f79b5f1b..dc6dbd6fd 100644 --- a/modules/dynamic/src/smithy4s/dynamic/package.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/package.scala @@ -14,14 +14,14 @@ * limitations under the License. */ -package smithy4s +package smithy4s.dynamic -package object dynamic { +package object internals { - type DynData = Any - type DynStruct = Array[DynData] - type DynAlt = (Int, DynData) + private[internals] type DynData = Any + private[internals] type DynStruct = Array[DynData] + private[internals] type DynAlt = (Int, DynData) - type DynamicAlg[F[_, _, _, _, _]] = Transformation[DynamicOp, F] + private[internals] type DynamicAlg[F[_, _, _, _, _]] = smithy4s.Transformation[DynamicOp, F] } diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala index 3fe118707..059d2506f 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/DynamicJsonServerSpec.scala @@ -73,8 +73,8 @@ object DynamicJsonServerSpec extends weaver.IOSuite { def sharedResource: Resource[IO, Res] = Resource.eval { Utils .compile(modelString) - .flatMap { dynamicModel => - dynamicModel.allServices + .flatMap { DynamicSchemaIndex => + DynamicSchemaIndex.allServices .find(_.service.id == ShapeId("foo", "KVStore")) .liftTo[IO](new Throwable("Not found")) } diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/OperationSpec.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/OperationSpec.scala index e34adecb3..87199af41 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/OperationSpec.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/OperationSpec.scala @@ -23,10 +23,9 @@ import software.amazon.smithy.model.{Model => SModel} import software.amazon.smithy.model.shapes.ModelSerializer import cats.effect.IO import java.nio.file.Paths -import smithy4s.dynamic -import smithy4s.http.HttpEndpoint import smithy.api.Http import cats.syntax.all._ +import http.HttpEndpoint object OperationSpec extends SimpleIOSuite { @@ -94,7 +93,7 @@ object OperationSpec extends SimpleIOSuite { .map(smithy4s.Document.decode[smithy4s.dynamic.model.Model](_)) .flatMap(_.liftTo[IO]) .map { model => - val compiled = dynamic.Compiler.compile(model, SchemaIndex(Http)) + val compiled = DynamicSchemaIndex.load(model, SchemaIndex(Http)) val endpoints = compiled.allServices.head.service.endpoints val httpEndpoints = endpoints.map(HttpEndpoint.cast(_)) diff --git a/modules/dynamic/test/src-jvm/smithy4s/dynamic/PlatformUtils.scala b/modules/dynamic/test/src-jvm/smithy4s/dynamic/PlatformUtils.scala index e1d76221f..d4fecfd7e 100644 --- a/modules/dynamic/test/src-jvm/smithy4s/dynamic/PlatformUtils.scala +++ b/modules/dynamic/test/src-jvm/smithy4s/dynamic/PlatformUtils.scala @@ -24,7 +24,7 @@ import cats.effect.IO private[dynamic] trait PlatformUtils { self: Utils.type => - def compile(string: String): IO[DynamicModel] = + def compile(string: String): IO[DynamicSchemaIndex] = parse(string).map(self.compile) def parse(string: String): IO[Model] = diff --git a/modules/dynamic/test/src/smithy4s/dynamic/Utils.scala b/modules/dynamic/test/src/smithy4s/dynamic/Utils.scala index 76f7a1f1e..af6c7d2b9 100644 --- a/modules/dynamic/test/src/smithy4s/dynamic/Utils.scala +++ b/modules/dynamic/test/src/smithy4s/dynamic/Utils.scala @@ -21,7 +21,7 @@ import smithy4s.SchemaIndex object Utils extends PlatformUtils { - def compile(model: Model): DynamicModel = - Compiler.compile(model, SchemaIndex.empty) + def compile(model: Model): DynamicSchemaIndex = + internals.Compiler.compile(model, SchemaIndex.empty) } From 97b7d034192fcd4cbb75797e82141a77608faf1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Wed, 9 Feb 2022 11:35:30 +0100 Subject: [PATCH 25/28] Headers --- .../internals/DynamicSchemaIndexImpl.scala | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/modules/dynamic/src/smithy4s/dynamic/internals/DynamicSchemaIndexImpl.scala b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicSchemaIndexImpl.scala index 05757c4e2..fd7b58562 100644 --- a/modules/dynamic/src/smithy4s/dynamic/internals/DynamicSchemaIndexImpl.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicSchemaIndexImpl.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 package dynamic package internals From 6c23f9b16a32ff8d6b5146a26c0a4f40c0530d4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Wed, 9 Feb 2022 11:35:43 +0100 Subject: [PATCH 26/28] Fmt --- .../dynamic/src/smithy4s/dynamic/DynamicSchemaIndex.scala | 5 ++++- modules/dynamic/src/smithy4s/dynamic/internals/package.scala | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/dynamic/src/smithy4s/dynamic/DynamicSchemaIndex.scala b/modules/dynamic/src/smithy4s/dynamic/DynamicSchemaIndex.scala index ed33c7608..29b6e0fea 100644 --- a/modules/dynamic/src/smithy4s/dynamic/DynamicSchemaIndex.scala +++ b/modules/dynamic/src/smithy4s/dynamic/DynamicSchemaIndex.scala @@ -39,7 +39,10 @@ object DynamicSchemaIndex { * @param knownHints an index of schemas of hints that the user would like * to retrieve from the dynamic representation of the model. */ - def load(model: dynamic.model.Model, knownHints: SchemaIndex): DynamicSchemaIndex = + def load( + model: dynamic.model.Model, + knownHints: SchemaIndex + ): DynamicSchemaIndex = internals.Compiler.compile(model, knownHints) /** diff --git a/modules/dynamic/src/smithy4s/dynamic/internals/package.scala b/modules/dynamic/src/smithy4s/dynamic/internals/package.scala index dc6dbd6fd..a6da4cf95 100644 --- a/modules/dynamic/src/smithy4s/dynamic/internals/package.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/package.scala @@ -22,6 +22,7 @@ package object internals { private[internals] type DynStruct = Array[DynData] private[internals] type DynAlt = (Int, DynData) - private[internals] type DynamicAlg[F[_, _, _, _, _]] = smithy4s.Transformation[DynamicOp, F] + private[internals] type DynamicAlg[F[_, _, _, _, _]] = + smithy4s.Transformation[DynamicOp, F] } From 4d9dde3b22b469a1ae5fdc1403f4cc75ed6db9c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Wed, 9 Feb 2022 11:44:53 +0100 Subject: [PATCH 27/28] Fix 2.12 --- .../smithy4s/dynamic/internals/DynamicModelCompiler.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala index 82a89ba59..b5c7e9ba8 100644 --- a/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala @@ -68,7 +68,10 @@ private[dynamic] object Compiler { tag: ShapeTag[A] ): Option[(ShapeId, SchemaIndex.Binding[A])] = knownHints.get(tag).map(s => tag.id -> SchemaIndex.Binding(tag, s)) - knownHints.tags.flatMap(binding(_)).toMap + type B = (ShapeId, SchemaIndex.Binding[_]) + knownHints.tags + .flatMap[B, Iterable[B]](binding(_)) + .toMap } def toHintAux[A]( From 0a5860a644353f0fd976feb381a3ab87c3379341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Wed, 9 Feb 2022 11:49:11 +0100 Subject: [PATCH 28/28] Fix 2.12 AND 2.13 --- .../src/smithy4s/dynamic/internals/DynamicModelCompiler.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala index b5c7e9ba8..7d5ca0017 100644 --- a/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala +++ b/modules/dynamic/src/smithy4s/dynamic/internals/DynamicModelCompiler.scala @@ -70,7 +70,7 @@ private[dynamic] object Compiler { knownHints.get(tag).map(s => tag.id -> SchemaIndex.Binding(tag, s)) type B = (ShapeId, SchemaIndex.Binding[_]) knownHints.tags - .flatMap[B, Iterable[B]](binding(_)) + .flatMap(binding(_): Option[B]) .toMap }