diff --git a/modules/bootstrapped/src/generated/smithy4s/example/LeafNode.scala b/modules/bootstrapped/src/generated/smithy4s/example/LeafNode.scala new file mode 100644 index 000000000..dfc708875 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/LeafNode.scala @@ -0,0 +1,23 @@ +package smithy4s.example + +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.int +import smithy4s.schema.Schema.struct + +final case class LeafNode(value: Int) + +object LeafNode extends ShapeTag.Companion[LeafNode] { + val id: ShapeId = ShapeId("smithy4s.example", "LeafNode") + + val hints: Hints = Hints.empty + + // constructor using the original order from the spec + private def make(value: Int): LeafNode = LeafNode(value) + + implicit val schema: Schema[LeafNode] = struct( + int.required[LeafNode]("value", _.value), + )(make).withId(id).addHints(hints) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/Tree.scala b/modules/bootstrapped/src/generated/smithy4s/example/Tree.scala new file mode 100644 index 000000000..0e87e9c4a --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/Tree.scala @@ -0,0 +1,67 @@ +package smithy4s.example + +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.bijection +import smithy4s.schema.Schema.recursive +import smithy4s.schema.Schema.union + +sealed trait Tree extends scala.Product with scala.Serializable { self => + @inline final def widen: Tree = this + def $ordinal: Int + + object project { + def tree: Option[TreeNode] = Tree.TreeCase.alt.project.lift(self).map(_.tree) + def leaf: Option[LeafNode] = Tree.LeafCase.alt.project.lift(self).map(_.leaf) + } + + def accept[A](visitor: Tree.Visitor[A]): A = this match { + case value: Tree.TreeCase => visitor.tree(value.tree) + case value: Tree.LeafCase => visitor.leaf(value.leaf) + } +} +object Tree extends ShapeTag.Companion[Tree] { + + def tree(tree: TreeNode): Tree = TreeCase(tree) + def leaf(leaf: LeafNode): Tree = LeafCase(leaf) + + val id: ShapeId = ShapeId("smithy4s.example", "Tree") + + val hints: Hints = Hints.empty + + final case class TreeCase(tree: TreeNode) extends Tree { final def $ordinal: Int = 0 } + final case class LeafCase(leaf: LeafNode) extends Tree { final def $ordinal: Int = 1 } + + object TreeCase { + val hints: Hints = Hints.empty + val schema: Schema[Tree.TreeCase] = bijection(TreeNode.schema.addHints(hints), Tree.TreeCase(_), _.tree) + val alt = schema.oneOf[Tree]("tree") + } + object LeafCase { + val hints: Hints = Hints.empty + val schema: Schema[Tree.LeafCase] = bijection(LeafNode.schema.addHints(hints), Tree.LeafCase(_), _.leaf) + val alt = schema.oneOf[Tree]("leaf") + } + + trait Visitor[A] { + def tree(value: TreeNode): A + def leaf(value: LeafNode): A + } + + object Visitor { + trait Default[A] extends Visitor[A] { + def default: A + def tree(value: TreeNode): A = default + def leaf(value: LeafNode): A = default + } + } + + implicit val schema: Schema[Tree] = recursive(union( + Tree.TreeCase.alt, + Tree.LeafCase.alt, + ){ + _.$ordinal + }.withId(id).addHints(hints)) +} diff --git a/modules/bootstrapped/src/generated/smithy4s/example/TreeNode.scala b/modules/bootstrapped/src/generated/smithy4s/example/TreeNode.scala new file mode 100644 index 000000000..c3daae4f3 --- /dev/null +++ b/modules/bootstrapped/src/generated/smithy4s/example/TreeNode.scala @@ -0,0 +1,24 @@ +package smithy4s.example + +import smithy4s.Hints +import smithy4s.Schema +import smithy4s.ShapeId +import smithy4s.ShapeTag +import smithy4s.schema.Schema.recursive +import smithy4s.schema.Schema.struct + +final case class TreeNode(left: Tree, right: Tree) + +object TreeNode extends ShapeTag.Companion[TreeNode] { + val id: ShapeId = ShapeId("smithy4s.example", "TreeNode") + + val hints: Hints = Hints.empty + + // constructor using the original order from the spec + private def make(left: Tree, right: Tree): TreeNode = TreeNode(left, right) + + implicit val schema: Schema[TreeNode] = recursive(struct( + Tree.schema.required[TreeNode]("left", _.left), + Tree.schema.required[TreeNode]("right", _.right), + )(make).withId(id).addHints(hints)) +} diff --git a/modules/bootstrapped/test/src/smithy4s/RecursiveSpec.scala b/modules/bootstrapped/test/src/smithy4s/RecursiveSpec.scala new file mode 100644 index 000000000..d9a9cbc58 --- /dev/null +++ b/modules/bootstrapped/test/src/smithy4s/RecursiveSpec.scala @@ -0,0 +1,139 @@ +package smithy4s + +import munit.FunSuite + +import smithy4s.example.{Tree, TreeNode, LeafNode, Foo} +import cats.Hash +import smithy4s.schema._ +import scala.annotation.tailrec +import smithy4s.internals.maps.MMap +import smithy4s.interopcats.SchemaVisitorHash +import smithy4s.schema.Schema.recursive +import smithy4s.schema.Schema._ + +class RecursiveSpec extends FunSuite { + + case class Recurse(n: Option[Recurse]) + + object Recurse { + implicit val schema: Schema[Recurse] = recursive { + struct( + schema.optional[Recurse]("n", _.n) + )(Recurse.apply) + } + } + + def buildTree(size: Int): Tree = { + val seed = Math.round(Math.random() * 1000).toInt + val nodes = (1 to size).map(i => Tree.leaf(LeafNode(i * seed))).toList + + @tailrec() + def recursiveFold(els: List[Tree]): Tree = + els match { + case head :: Nil => head + case x => { + val joined: List[Tree] = x + .sliding(2, 2) + .flatMap { + case left :: right :: Nil => + List(Tree.tree(TreeNode(left, right))) + case x => x + } + .toList + recursiveFold(joined) + } + } + + recursiveFold(nodes) + } + + def buildRecursive(size: Int): Recurse = + (1 to size).foldLeft(Recurse(None))((tail, i) => Recurse(Some(tail))) + + def useLazyTestCache[F[_]](store: MMap[Any, Any]) = new CompilationCache[F] { + override def getOrElseUpdate[A]( + schema: Schema[A], + fetch: Schema[A] => F[A] + ): F[A] = { + store.getOrElseUpdate(schema, fetch(schema)).asInstanceOf[F[A]] + } + } + + def ignoreLazyTestCache[F[_]](store: MMap[Any, Any]) = + new CompilationCache[F] { + override def getOrElseUpdate[A]( + schema: Schema[A], + fetch: Schema[A] => F[A] + ): F[A] = { + if (schema.isInstanceOf[Schema.LazySchema[_]]) { fetch(schema) } + else store.getOrElseUpdate(schema, fetch(schema)).asInstanceOf[F[A]] + } + } + + def runTest[A]( + caseString: String, + value: Int => A, + buildCache: MMap[Any, Any] => CompilationCache[Hash], + transformSchema: Schema[A] => Schema[A] = (x: Schema[A]) => x + )(implicit schema: Schema[A]) = + test(s"$caseString") { + val store: MMap[Any, Any] = MMap.empty + + val updatedSchema = transformSchema(schema) + + val hashVisitor: Hash[A] = + SchemaVisitorHash.fromSchema(updatedSchema, buildCache(store)) + + // Invoke hash with a size that will have some recursion so that the build cache an be materialized + hashVisitor.hash(value(2)) + val sizeAfterInitializing = store.size + + val sizes = List(10, 100, 256) + sizes.foreach(i => hashVisitor.hash(value(i))) + val sizeAfterHashing = store.size + + assertEquals( + sizeAfterHashing, + sizeAfterInitializing, + "cache store size has grown after initialization" + ) + } + + def addHints[A](schema: Schema[A]): Schema[A] = { + schema.transformHintsTransitively( + _.add( + smithy.api.Documentation("Adding some hints") + ) + ) + } + + def runTestCases[A: Schema](caseString: String, value: Int => A) = { + runTest( + s"$caseString: current cache, hints unchanged", + value, + buildCache = ignoreLazyTestCache + ) + runTest( + s"$caseString: current cache, hints transformed", + value, + buildCache = ignoreLazyTestCache, + transformSchema = addHints[A] + ) + runTest( + s"$caseString: updated cache, hints unchanged", + value, + buildCache = useLazyTestCache + ) + runTest( + s"$caseString: updated cache, hints transformed", + value, + buildCache = useLazyTestCache, + transformSchema = addHints[A] + ) + } + + runTestCases("Foo", Foo.int) + runTestCases("Tree", buildTree) + runTestCases("Recurse", buildRecursive) + +} diff --git a/modules/bootstrapped/test/src/smithy4s/http/AcceptHeaderSpec.scala b/modules/bootstrapped/test/src/smithy4s/http/AcceptHeaderSpec.scala index 09d61f3ba..0a4c6f4b8 100644 --- a/modules/bootstrapped/test/src/smithy4s/http/AcceptHeaderSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/http/AcceptHeaderSpec.scala @@ -33,7 +33,7 @@ final class AcceptHeaderSpec extends FunSuite { try fa catch { case e: Throwable => f(e) } def pure[A](a: A): Id[A] = a - def zipMapAll[A](seq: IndexedSeq[Id[Any]])(f: IndexedSeq[Any] => A): Id[A] = + def zipMapAll[A,B](seq: IndexedSeq[Id[A]])(f: IndexedSeq[A] => B): Id[B] = f(seq) } diff --git a/modules/bootstrapped/test/src/smithy4s/schematests/CachingSpec.scala b/modules/bootstrapped/test/src/smithy4s/schematests/CachingSpec.scala new file mode 100644 index 000000000..96a617b4e --- /dev/null +++ b/modules/bootstrapped/test/src/smithy4s/schematests/CachingSpec.scala @@ -0,0 +1,145 @@ +package smithy4s.schematests + +import smithy4s.schema.Compilation +import smithy4s.schema._ +import smithy4s.Lazy +import smithy4s.Refinement +import smithy4s.{Hints, ShapeId} +import smithy4s.schema.Alt +import smithy4s.Bijection +import smithy4s.schema.Schema +import smithy4s.schema.Schema._ +import munit.FunSuite + +final class CachingSpec extends FunSuite { + + test("Caching works as intended for normal schemas"){ + case class Foo(int: Int, str: String) + object Foo { + val schema: Schema[Foo] = { + val int = Schema.int.required[Foo]("foo", _.int) + val str = Schema.string.required[Foo]("str", _.str) + struct(int, str)(Foo.apply) + } + } + val treeCompilation = TreeVisitor.compile(Foo.schema) + val tree = Compilation.expensiveRun(treeCompilation) + assertEquals(tree.size, 2) + } + + test("Caching works as intended for cyclic schemas".only){ + case class Foo(foo: Foo) + object Foo { + val schema: Schema[Foo] = recursive { + val foos = schema.required[Foo]("foo", _.foo) + struct(foos)(Foo.apply) + } + } + val treeCompilation = TreeVisitor.compile(Foo.schema.transformHintsLocally(_.add(smithy.api.Documentation("foo")))) + val tree = Compilation.expensiveRun(treeCompilation) + assertEquals(tree.size, 3) + } + +} + +sealed trait Tree { + def size = Tree.flatten(this, Set.empty).size +} + +object Tree { + type Const[A] = Tree + + case class Node(children : IndexedSeq[Tree]) extends Tree + case class Cycle(f : Lazy[Tree]) extends Tree + + val empty: Tree = Node(IndexedSeq.empty) + def apply[A](trees: Tree*): Tree = Node(trees.toIndexedSeq) + def flatten(tree: Tree, acc: Set[Tree]) : Set[Tree] = { + if (acc(tree)) acc + else tree match { + case n @ Node(children) => + children.foldLeft(acc + n){(currentAcc, child) => + currentAcc ++ flatten(child, currentAcc) + } + case c @ Cycle(lt) => + flatten(lt.value, acc + c) + } + } +} + +object TreeVisitor extends Compilation.Visitor[Tree.Const] { + def primitive[P]( + shapeId: ShapeId, + hints: Hints, + tag: Primitive[P] + ): Compilation[Tree] = leaf(Tree.empty) + + def collection[C[_], A]( + shapeId: ShapeId, + hints: Hints, + tag: CollectionTag[C], + member: Schema[A] + ): Compilation[Tree] = + compile(member).map(Tree(_)) + + def map[K, V]( + shapeId: ShapeId, + hints: Hints, + key: Schema[K], + value: Schema[V] + ): Compilation[Tree] = + compile(key).zip(compile(value)).map { case (kt, vt) => Tree(kt, vt) } + + def enumeration[E]( + shapeId: ShapeId, + hints: Hints, + tag: EnumTag[E], + values: List[EnumValue[E]], + total: E => EnumValue[E] + ): Compilation[Tree] = leaf(Tree.empty) + + def struct[S]( + shapeId: ShapeId, + hints: Hints, + fields: Vector[Field[S, _]], + make: IndexedSeq[Any] => S + ): Compilation[Tree] = + Compilation + .sequence( + fields + .map(f => compile(f.schema.asInstanceOf[Schema[Any]])) + .toIndexedSeq + ) + .map(Tree.Node(_)) + + def union[U]( + shapeId: ShapeId, + hints: Hints, + alternatives: Vector[Alt[U, _]], + ordinal: U => Int + ): Compilation[Tree] = Compilation + .sequence( + alternatives + .map(f => compile(f.schema.asInstanceOf[Schema[Any]])) + .toIndexedSeq + ) + .map(Tree.Node(_)) + + def biject[A, B]( + schema: Schema[A], + bijection: Bijection[A, B] + ): Compilation[Tree] = compile(schema).map(Tree(_)) + + def refine[A, B]( + schema: Schema[A], + refinement: Refinement[A, B] + ): Compilation[Tree] = compile(schema).map(Tree(_)) + + def lazily[A](suspend: Lazy[Schema[A]]): Compilation[Tree] = { + buildRecursive(suspend)(Tree.Cycle(_)) + } + + def option[A](schema: Schema[A]): Compilation[Tree] = + compile(schema).map(Tree(_)) + +} diff --git a/modules/bootstrapped/test/src/smithy4s/schematests/HintsTransformationSpec.scala b/modules/bootstrapped/test/src/smithy4s/schematests/HintsTransformationSpec.scala index 05cf1d251..d9c7beafc 100644 --- a/modules/bootstrapped/test/src/smithy4s/schematests/HintsTransformationSpec.scala +++ b/modules/bootstrapped/test/src/smithy4s/schematests/HintsTransformationSpec.scala @@ -127,9 +127,14 @@ class HintsTransformationSpec() extends FunSuite { struct(foos)(Foo.apply) } } + + def buildFoo(size: Int): Foo = + (1 until size).foldLeft(Foo(None))((foo, _) => Foo(Some(foo))) + checkSchema(Foo(None), 1) checkSchema(Foo(Some(Foo(None))), 2) checkSchema(Foo(Some(Foo(Some(Foo(None))))), 3) + checkSchema(buildFoo(256), 256) } test(header("nullable")) { diff --git a/modules/cats/src/smithy4s/interopcats/package.scala b/modules/cats/src/smithy4s/interopcats/package.scala index 49cad10e8..6bc28d8d9 100644 --- a/modules/cats/src/smithy4s/interopcats/package.scala +++ b/modules/cats/src/smithy4s/interopcats/package.scala @@ -27,8 +27,8 @@ package object interopcats { implicit def monadThrowShim[F[_]: MonadThrow]: MonadThrowLike[F] = new MonadThrowLike[F] { def pure[A](a: A): F[A] = MonadThrow[F].pure(a) - def zipMapAll[A](seq: IndexedSeq[F[Any]])(f: IndexedSeq[Any] => A): F[A] = - seq.toVector.asInstanceOf[Vector[F[Any]]].sequence.map(f) + def zipMapAll[A, B](seq: IndexedSeq[F[A]])(f: IndexedSeq[A] => B): F[B] = + seq.toVector.sequence.map(f) def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = MonadThrow[F].flatMap(fa)(f) def raiseError[A](e: Throwable): F[A] = MonadThrow[F].raiseError(e) diff --git a/modules/core/src/smithy4s/Lazy.scala b/modules/core/src/smithy4s/Lazy.scala index 9665d664c..efa9073e8 100644 --- a/modules/core/src/smithy4s/Lazy.scala +++ b/modules/core/src/smithy4s/Lazy.scala @@ -16,17 +16,24 @@ package smithy4s -final class Lazy[A](make: () => A) { - private[this] var thunk: () => A = make - lazy val value: A = { - val result = thunk() - thunk = null - result - } - - def map[B](f: A => B): Lazy[B] = new Lazy(() => f(make())) +sealed trait Lazy[A] { + def value: A + final def map[B](f: A => B): Lazy[B] = Lazy.Mapped(this, f) } object Lazy { - def apply[A](a: => A): Lazy[A] = new Lazy(() => a) + def apply[A](a: => A): Lazy[A] = new Root(() => a) + + final class Root[A](make: () => A) extends Lazy[A] { + protected var thunk: () => A = make + lazy val value: A = { + val result = thunk() + thunk = null + result + } + } + + final case class Mapped[A, B](left: Lazy[A], f: A => B) extends Lazy[B] { + lazy val value = f(left.value) + } } diff --git a/modules/core/src/smithy4s/capability/Zipper.scala b/modules/core/src/smithy4s/capability/Zipper.scala index 9fd175221..81873f448 100644 --- a/modules/core/src/smithy4s/capability/Zipper.scala +++ b/modules/core/src/smithy4s/capability/Zipper.scala @@ -26,7 +26,7 @@ package smithy4s.capability trait Zipper[F[_]] extends Covariant[F] { def pure[A](a: A): F[A] - def zipMapAll[A](seq: IndexedSeq[F[Any]])(f: IndexedSeq[Any] => A): F[A] + def zipMapAll[A, B](seq: IndexedSeq[F[A]])(f: IndexedSeq[A] => B): F[B] def zipMap[A, B, C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C] = zipMapAll(IndexedSeq(fa, fb).asInstanceOf[IndexedSeq[F[Any]]])(seq => diff --git a/modules/core/src/smithy4s/capability/instances/either.scala b/modules/core/src/smithy4s/capability/instances/either.scala index 8b2da3b2f..f2c4523dc 100644 --- a/modules/core/src/smithy4s/capability/instances/either.scala +++ b/modules/core/src/smithy4s/capability/instances/either.scala @@ -31,20 +31,20 @@ object either { case (Right(a), Right(b)) => Right(f(a, b)) } - override def zipMapAll[A]( - seq: IndexedSeq[Either[E, Any]] - )(f: IndexedSeq[Any] => A): Either[E, A] = { - val builder = IndexedSeq.newBuilder[Any] + override def zipMapAll[A, B]( + seq: IndexedSeq[Either[E, A]] + )(f: IndexedSeq[A] => B): Either[E, B] = { + val builder = IndexedSeq.newBuilder[A] var i = 0 - var error: Left[E, Any] = null + var error: Left[E, B] = null while (error == null && i < seq.size) { seq(i) match { - case l @ Left(_) => error = l.asInstanceOf[Left[E, Any]] + case l @ Left(_) => error = l.asInstanceOf[Left[E, B]] case Right(r) => builder += r } i += 1 } - if (error != null) error.asInstanceOf[Left[E, A]] + if (error != null) error.asInstanceOf[Left[E, B]] else Right(f(builder.result())) } } diff --git a/modules/core/src/smithy4s/capability/instances/option.scala b/modules/core/src/smithy4s/capability/instances/option.scala index 070e3b427..a9806dd78 100644 --- a/modules/core/src/smithy4s/capability/instances/option.scala +++ b/modules/core/src/smithy4s/capability/instances/option.scala @@ -31,10 +31,10 @@ object option { case (Some(a), Some(b)) => Some(f(a, b)) } - override def zipMapAll[A]( - seq: IndexedSeq[Option[Any]] - )(f: IndexedSeq[Any] => A): Option[A] = { - val builder = IndexedSeq.newBuilder[Any] + override def zipMapAll[A, B]( + seq: IndexedSeq[Option[A]] + )(f: IndexedSeq[A] => B): Option[B] = { + val builder = IndexedSeq.newBuilder[A] var i = 0 var error: Boolean = false while (!error && i < seq.size) { diff --git a/modules/core/src/smithy4s/codecs/Decoder.scala b/modules/core/src/smithy4s/codecs/Decoder.scala index e784c2ecc..6edab0524 100644 --- a/modules/core/src/smithy4s/codecs/Decoder.scala +++ b/modules/core/src/smithy4s/codecs/Decoder.scala @@ -108,15 +108,11 @@ object Decoder { def decode(in: In): F[A] = Zipper[F].pure(a) } - def zipMapAll[A](seq: IndexedSeq[Decoder[F, In, Any]])( - f: IndexedSeq[Any] => A - ): Decoder[F, In, A] = new Decoder[F, In, A] { - def decode(in: In): F[A] = { - Zipper[F].zipMapAll( - seq - .asInstanceOf[IndexedSeq[Decoder[F, In, Any]]] - .map(_.decode(in)) - )(f) + def zipMapAll[A, B](seq: IndexedSeq[Decoder[F, In, A]])( + f: IndexedSeq[A] => B + ): Decoder[F, In, B] = new Decoder[F, In, B] { + def decode(in: In): F[B] = { + Zipper[F].zipMapAll[A, B](seq.map(_.decode(in)))(f) } } } diff --git a/modules/core/src/smithy4s/internals/DocumentEncoderCompiler.scala b/modules/core/src/smithy4s/internals/DocumentEncoderCompiler.scala new file mode 100644 index 000000000..30da9e92c --- /dev/null +++ b/modules/core/src/smithy4s/internals/DocumentEncoderCompiler.scala @@ -0,0 +1,273 @@ +/* + * Copyright 2021-2026 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 internals + +import smithy.api.JsonName +import smithy.api.TimestampFormat +import smithy.api.TimestampFormat.DATE_TIME +import smithy.api.TimestampFormat.EPOCH_SECONDS +import smithy.api.TimestampFormat.HTTP_DATE +import alloy.Discriminated +import alloy.JsonUnknown +import smithy4s.schema._ + +import scala.collection.mutable.Builder + +import Document._ +import smithy4s.schema.Primitive._ +import alloy.Untagged +import smithy4s.schema.FieldFilter + +class DocumentEncoderCompiler(fieldFilter: FieldFilter) + extends Compilation.Visitor[DocumentEncoder] { self => + + override def primitive[P]( + shapeId: ShapeId, + hints: Hints, + tag: Primitive[P] + ) = leaf { + tag match { + case PShort => from(short => DNumber(BigDecimal(short.toInt))) + case PBigInt => from(bigInt => DNumber(BigDecimal(bigInt))) + case PBoolean => from(DBoolean(_)) + case PByte => from(byte => DNumber(BigDecimal(byte.toInt))) + case PBigDecimal => from(DNumber(_)) + case PInt => from(int => DNumber(BigDecimal(int))) + case PBlob => + from(bytes => DString(bytes.toBase64String)) + case PTimestamp => + hints + .get(TimestampFormat) + .getOrElse(TimestampFormat.EPOCH_SECONDS) match { + case DATE_TIME => ts => DString(ts.format(DATE_TIME)) + case HTTP_DATE => ts => DString(ts.format(HTTP_DATE)) + case EPOCH_SECONDS => + ts => + DNumber( + BigDecimal({ + val es = java.math.BigDecimal.valueOf(ts.epochSecond) + if (ts.nano == 0) es + else + es.add( + java.math.BigDecimal + .valueOf(ts.nano.toLong, 9) + .stripTrailingZeros + ) + }) + ) + } + case PDocument => from(identity) + case PFloat => from(float => DNumber(BigDecimal(float.toDouble))) + case PUUID => from(uuid => DString(uuid.toString())) + case PDouble => from(double => DNumber(BigDecimal(double))) + case PLong => from(long => DNumber(BigDecimal(long))) + case PString => from(DString(_)) + } + } + + override def collection[C[_], A]( + shapeId: ShapeId, + hints: Hints, + tag: CollectionTag[C], + member: Schema[A] + ) = compile(member).map { encoderS => + from[C[A]](c => DArray(tag.iterator(c).map(encoderS.apply).toIndexedSeq)) + } + + override def option[A](schema: Schema[A]) = + compile(schema).map { encoder => + locally { + case Some(a) => encoder.apply(a) + case None => Document.DNull + } + } + + override def map[K, V]( + shapeId: ShapeId, + hints: Hints, + key: Schema[K], + value: Schema[V] + ) = Compilation + .zipN( + KeyEncoderCompiler.compile(key), + this.compile(key), + this.compile(value) + ) + .map { + case (Some(keyEncoder), _, valueEncoder) => + from[Map[K, V]] { map => + val mapBuilder = Map.newBuilder[String, Document] + map.foreach { case (k, v) => + val key = keyEncoder.apply(k) + val value = valueEncoder.apply(v) + mapBuilder.+=((key, value)) + } + DObject(mapBuilder.result()) + } + case (None, keyAsValueEncoder, valueEncoder) => + from[Map[K, V]] { map => + val arrayBuilder = IndexedSeq.newBuilder[Document] + map.map { case (k, v) => + arrayBuilder.+=( + DObject( + Map( + "key" -> keyAsValueEncoder.apply(k), + "value" -> valueEncoder.apply(v) + ) + ) + ) + } + DArray(arrayBuilder.result()) + } + } + + override def enumeration[E]( + shapeId: ShapeId, + hints: Hints, + tag: EnumTag[E], + values: List[EnumValue[E]], + total: E => EnumValue[E] + ) = leaf { + tag match { + case EnumTag.IntEnum() => + from(e => Document.fromInt(total(e).intValue)) + case _ => + from(e => DString(total(e).stringValue)) + } + } + + private def isForJsonUnknown(field: Field[_, _]): Boolean = + field.hints.has(JsonUnknown) + + override def struct[S]( + shapeId: ShapeId, + hints: Hints, + fields: Vector[Field[S, _]], + make: IndexedSeq[Any] => S + ) = { + val discriminator = + hints.get(DiscriminatedUnionMember).map { discriminated => + (discriminated.propertyName -> Document.fromString( + discriminated.alternativeLabel + )) + } + def fieldEncoder[A]( + field: Field[S, A] + ): Compilation[ + (S, Builder[(String, Document), Map[String, Document]]) => Unit + ] = compile(field.schema).map { encoder => + val jsonLabel = field.hints + .get(JsonName) + .map(_.value) + .getOrElse(field.label) + val shouldRender = fieldFilter.compile(field) + (s, builder) => + val value = field.get(s) + if (shouldRender(value)) { + builder.+=(jsonLabel -> encoder.apply(value)) + } + } + + def jsonUnknownFieldEncoder[A]( + field: Field[S, A] + ): Compilation[ + (S, Builder[(String, Document), Map[String, Document]]) => Unit + ] = compile(field.schema).map { encoder => + val shouldRender = fieldFilter.compile(field) + (s, builder) => { + val value = field.get(s) + if (shouldRender(value)) { + encoder(value) match { + case Document.DObject(value) => value.foreach(builder += _) + case _ => + throw new IllegalArgumentException( + s"Failed encoding field ${field.label} because it cannot be converted to a JSON object" + ) + } + } + } + } + + val (fieldsForUnknown, knownFields) = fields.partition(isForJsonUnknown) + val knownFieldsC = knownFields.map(fieldEncoder(_)) + val unknownFieldsC = fieldsForUnknown.map(jsonUnknownFieldEncoder(_)) + Compilation + .sequence(knownFieldsC ++ unknownFieldsC) + .map { encoders => + new DocumentEncoder[S] { + def apply(s: S): Document = { + val builder = Map.newBuilder[String, Document] + encoders.foreach(_(s, builder)) + DObject(builder.result() ++ discriminator) + } + } + } + } + + override def union[U]( + shapeId: ShapeId, + hints: Hints, + alternatives: Vector[Alt[U, _]], + ordinal: U => Int + ) = { + object precompile extends Compilation.Precompiler[DocumentEncoder] { + override def apply[A]( + label: String, + schema: Schema[A] + ): Compilation[DocumentEncoder[A]] = { + val jsonLabel = + schema.hints.get(JsonName).map(_.value).getOrElse(label) + hints match { + case Untagged.hint(_) | JsonUnknown.hint(_) => compile(schema) + + case Discriminated.hint(discriminated) => + val unionMemberHint = DiscriminatedUnionMember( + discriminated.value, + jsonLabel + ) + compile(schema.addHints(unionMemberHint)) + case _ => + compile(schema).map(_.mapDocument(_.nest(jsonLabel))) + } + } + } + dispatch(alternatives, ordinal, precompile) + } + + override def biject[A, B]( + schema: Schema[A], + bijection: Bijection[A, B] + ) = compile(schema).map(_.contramap(bijection.from)) + + override def refine[A, B]( + schema: Schema[A], + refinement: Refinement[A, B] + ) = compile(schema).map(_.contramap(refinement.from)) + + override def lazily[A](suspend: Lazy[Schema[A]]) = + buildRecursive(suspend) { lazyCodec => + new DocumentEncoder[A] { + def apply(a: A): Document = lazyCodec.value(a) + } + } + + def from[A](f: A => Document): DocumentEncoder[A] = + new DocumentEncoder[A] { + def apply(a: A): Document = f(a) + } +} diff --git a/modules/core/src/smithy4s/internals/KeyEncoderCompiler.scala b/modules/core/src/smithy4s/internals/KeyEncoderCompiler.scala new file mode 100644 index 000000000..6451e862e --- /dev/null +++ b/modules/core/src/smithy4s/internals/KeyEncoderCompiler.scala @@ -0,0 +1,127 @@ +/* + * Copyright 2021-2026 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.internals + +import smithy4s._ +import smithy4s.schema.EnumValue +import smithy4s.schema.Primitive +import smithy4s.schema.Schema +import smithy4s.schema.EnumTag +import smithy4s.schema.Compilation +import smithy4s.schema.CollectionTag +import smithy4s.schema.Field +import smithy4s.schema.Alt +import Primitive._ +import smithy.api.TimestampFormat +import smithy.api.TimestampFormat._ + +object KeyEncoderCompiler extends Compilation.Visitor[MaybeKeyEncoder] { + + private def forBigDecimal[A](f: A => BigDecimal): MaybeKeyEncoder[A] = + Some(_.toString()) + private def fromToString[A]: MaybeKeyEncoder[A] = Some(_.toString()) + + def primitive[P]( + shapeId: ShapeId, + hints: Hints, + tag: Primitive[P] + ): Compilation[MaybeKeyEncoder[P]] = leaf { + tag match { + case PBoolean => fromToString + case PBigDecimal => fromToString + case PUUID => fromToString + case PString => fromToString + case PShort => forBigDecimal { a => BigDecimal(a.toInt) } + case PBigInt => forBigDecimal { BigDecimal(_) } + case PInt => forBigDecimal { BigDecimal(_) } + case PDouble => forBigDecimal { BigDecimal(_) } + case PLong => forBigDecimal { BigDecimal(_) } + case PByte => forBigDecimal { a => BigDecimal(a.toInt) } + case PFloat => forBigDecimal { a => BigDecimal(a.toDouble) } + case PBlob => Some(_.toBase64String) + case PTimestamp => + hints + .get(TimestampFormat) + .getOrElse(DATE_TIME) match { + case DATE_TIME => Some { ts => ts.format(DATE_TIME) } + case HTTP_DATE => Some { ts => ts.format(HTTP_DATE) } + case EPOCH_SECONDS => + forBigDecimal { ts => BigDecimal(ts.epochSecond) } + } + case PDocument => None + } + } + + def biject[A, B]( + schema: Schema[A], + bijection: Bijection[A, B] + ): Compilation[MaybeKeyEncoder[B]] = + compile(schema).map(_.map(_.compose(bijection.from))) + + def refine[A, B]( + schema: Schema[A], + refinement: Refinement[A, B] + ): Compilation[MaybeKeyEncoder[B]] = + compile(schema).map(_.map(_.compose(refinement.from))) + + def collection[C[_], A]( + shapeId: ShapeId, + hints: Hints, + tag: CollectionTag[C], + member: Schema[A] + ): Compilation[MaybeKeyEncoder[C[A]]] = leaf(None) + + def map[K, V]( + shapeId: ShapeId, + hints: Hints, + key: Schema[K], + value: Schema[V] + ): Compilation[MaybeKeyEncoder[Map[K, V]]] = leaf(None) + + def enumeration[E]( + shapeId: ShapeId, + hints: Hints, + tag: EnumTag[E], + values: List[EnumValue[E]], + total: E => EnumValue[E] + ): Compilation[MaybeKeyEncoder[E]] = tag match { + case EnumTag.IntEnum() => + leaf(Some { a => total(a).intValue.toString }) + case _ => + leaf(Some { a => total(a).stringValue }) + } + + def struct[S]( + shapeId: ShapeId, + hints: Hints, + fields: Vector[Field[S, _]], + make: IndexedSeq[Any] => S + ): Compilation[MaybeKeyEncoder[S]] = leaf(None) + + def union[U]( + shapeId: ShapeId, + hints: Hints, + alternatives: Vector[Alt[U, _]], + ordinal: U => Int + ): Compilation[MaybeKeyEncoder[U]] = leaf(None) + + def lazily[A](suspend: Lazy[Schema[A]]): Compilation[MaybeKeyEncoder[A]] = + leaf(None) + def option[A](schema: Schema[A]): Compilation[MaybeKeyEncoder[Option[A]]] = + leaf(None) + +} diff --git a/modules/core/src/smithy4s/internals/package.scala b/modules/core/src/smithy4s/internals/package.scala index ff977b816..901f2d146 100644 --- a/modules/core/src/smithy4s/internals/package.scala +++ b/modules/core/src/smithy4s/internals/package.scala @@ -19,6 +19,9 @@ package smithy4s import scala.util.control.NoStackTrace package object internals { + type KeyEncoder[A] = A => String + type MaybeKeyEncoder[A] = Option[KeyEncoder[A]] + type SchemaDescription[A] = String val SchemaDescriptionDetailed: Schema ~> SchemaDescription = SchemaDescriptionDetailedImpl.andThen( diff --git a/modules/core/src/smithy4s/schema/Compilation.scala b/modules/core/src/smithy4s/schema/Compilation.scala new file mode 100644 index 000000000..8a76c08c6 --- /dev/null +++ b/modules/core/src/smithy4s/schema/Compilation.scala @@ -0,0 +1,176 @@ +package smithy4s.schema + +import smithy4s.{Lazy, ShapeId, Hints, Bijection, Refinement} +import smithy4s.schema.Schema.LazySchema +import Compilation.Visitor +import Schema._ +import scala.collection.mutable.{Map => MMap} +import smithy4s.capability.EncoderK + +/** + * Applicative construct that allows to compositionally create programs that express schema compilation (ie, the process of creating codecs + * described from schemas). + * + * The Applicative nature of this construct allows interesting patterns, such as: + * * aggressive caching of already-computed codecs, + * * control over recursions, as recursion is inherently deferred to the interpretation of the tree in this pattern. + */ +sealed trait Compilation[A] { + def map[B](f: A => B) : Compilation[B] = Compilation.Mapped(this, f) + def zip[B](other: Compilation[B]): Compilation[(A, B)] = + Compilation.Sequenced(IndexedSeq(this.asInstanceOf[Compilation[Any]], other.asInstanceOf[Compilation[Any]])).map(seq => (seq(0).asInstanceOf[A], seq(1).asInstanceOf[B])) + + /** + * Within a compilation tree, replaces occurrences of a visitor by another. It is helpful for overriding behaviour, as we are given + * control over the delegating calls. + */ + def replace[F[_]](initial: Visitor[F], replacement: Visitor[F]) : Compilation[A] = Compilation.replace(this, initial, replacement) +} + +object Compilation { + + abstract class Deriving[F[_]](compiler: Visitor[F]) { + private val cache = new MCache(scala.collection.concurrent.TrieMap.empty[Any, Any]) + def fromSchema[A](schema: Schema[A]) = unsafeInterpret(compiler.compile(schema), cache) + implicit def derivedInstance[A](implicit schema: Schema[A]): F[A] = fromSchema(schema) + } + + trait Visitor[F[_]] { self => + def primitive[P](shapeId: ShapeId, hints: Hints, tag: Primitive[P]): Compilation[F[P]] + def collection[C[_], A](shapeId: ShapeId, hints: Hints, tag: CollectionTag[C], member: Schema[A]): Compilation[F[C[A]]] + def map[K, V](shapeId: ShapeId, hints: Hints, key: Schema[K], value: Schema[V]): Compilation[F[Map[K, V]]] + def enumeration[E](shapeId: ShapeId, hints: Hints, tag: EnumTag[E], values: List[EnumValue[E]], total: E => EnumValue[E]): Compilation[F[E]] + def struct[S](shapeId: ShapeId, hints: Hints, fields: Vector[Field[S, _]], make: IndexedSeq[Any] => S): Compilation[F[S]] + def union[U](shapeId: ShapeId, hints: Hints, alternatives: Vector[Alt[U, _]], ordinal: U => Int): Compilation[F[U]] + def biject[A, B](schema: Schema[A], bijection: Bijection[A, B]): Compilation[F[B]] + def refine[A, B](schema: Schema[A], refinement: Refinement[A, B]): Compilation[F[B]] + def lazily[A](suspend: Lazy[Schema[A]]): Compilation[F[A]] + def option[A](schema: Schema[A]): Compilation[F[Option[A]]] + + final def compile[A](schema: Schema[A]) : Compilation[F[A]] = Delegate(schema, self) + /** + * Creates a leaf expression containing a codec. Typically used for terminal nodes of a codec (such as primitives/enumerations) + */ + final def leaf[A](fa: F[A]) : Compilation[F[A]] = Pure(fa) + + /** + * Helper that must be used for the compilation of recursive codecs. It takes care of the safe traversal of the recursive tree + * and prevents a number of foot-guns related to the lack of referentially-transparent computations in Scala. + */ + final def buildRecursive[A](lazySchema: Lazy[Schema[A]])(buildRecursive: Lazy[F[A]] => F[A]) : Compilation[F[A]] = Cyclic(lazySchema, self, buildRecursive) + + /** + * Allows to call upon a separate compiler to assist the implementation of this compiler. + */ + final def delegate[G[_], A](schema: Schema[A], otherCompiler: Visitor[G]) : Compilation[G[A]] = Delegate(schema, otherCompiler) + + /** + * Helper that should be used for the compilation of union codecs. + */ + final def dispatch[U, Result](alternatives: Vector[Alt[U, _]], ordinal: U => Int, precompiler: Precompiler[F])(implicit encoderK: EncoderK[F, Result]) : Compilation[F[U]] = { + Compilation.sequence(alternatives.map(alt => precompiler(alt.label, alt.schema.asInstanceOf[Schema[Any]]))).map { codecs => + encoderK.absorb { (u: U) => + encoderK.apply(codecs(ordinal(u)), u) + } + } + } + } + + trait Precompiler[F[_]]{ + def apply[A](label: String, schema: Schema[A]) : Compilation[F[A]] + } + + object Visitor { + private[Compilation] def run[F[_], A](schema: Schema[A], visitor: Visitor[F]) : Compilation[F[A]] = { + import visitor._ + schema match { + case PrimitiveSchema(shapeId, hints, tag) => primitive(shapeId, hints, tag) + case s: CollectionSchema[c, a] => collection[c,a](s.shapeId, s.hints, s.tag, s.member) + case MapSchema(shapeId, hints, key, value) => map(shapeId, hints, key, value) + case EnumerationSchema(shapeId, hints, tag, values, total) => enumeration(shapeId, hints, tag, values, total) + case StructSchema(shapeId, hints, fields, make) => struct(shapeId, hints, fields, make) + case UnionSchema(shapeId, hints, alts, ordinal) => union(shapeId, hints, alts, ordinal) + case BijectionSchema(schema, bijection) => biject(schema, bijection) + case RefinementSchema(schema, refinement) => refine(schema, refinement) + case LazySchema(make) => lazily(make) + case OptionSchema(a) => option(a) + } + }} + + + def pure[A](a: A) : Compilation[A] = Pure(a) + def sequence[A, B](seq: IndexedSeq[Compilation[A]]) : Compilation[IndexedSeq[A]] = Sequenced(seq) + def zipN[A1, A2, A3](c1 : Compilation[A1], c2: Compilation[A2], c3: Compilation[A3]) : Compilation[(A1, A2, A3)] = { + Compilation.Sequenced(IndexedSeq(c1.asInstanceOf[Compilation[Any]], c2.asInstanceOf[Compilation[Any]], c3.asInstanceOf[Compilation[Any]])).map(seq => (seq(0).asInstanceOf[A1], seq(1).asInstanceOf[A2], seq(2).asInstanceOf[A3])) + } + + def compileSchema[F[_], A](schema: Schema[A], compiler: Visitor[F]) : Compilation[F[A]] = Compilation.Delegate(schema, compiler) + + /** + * Runs the compilation by traversing its tree, producing and caching the necessary intermediate constructs that + * participate in the construction of the value. + * + * This operation is inherently expensive in terms of allocations, and should be run wisely. + */ + def expensiveRun[A](compilation: Compilation[A]) : A = { + val mutableCache = new MCache(MMap.empty) + unsafeInterpret(compilation, mutableCache) + } + + private def replace[F[_], A](compilation: Compilation[A], initial: Visitor[F], replacement: Visitor[F]) : Compilation[A] = compilation match { + case Pure(a) => Pure(a) + case d : Delegate[f, a] if d.compiler == replacement => Delegate(d.schema, replacement.asInstanceOf[Visitor[f]]) + case d: Delegate[f, a] => d + case c: Cyclic[f, a] if c.compiler == replacement => Cyclic(c.schema, replacement.asInstanceOf[Visitor[f]], c.buildRecursive) + case c: Cyclic[f, a] => c + case Mapped(ca, f) => Mapped(replace(ca, initial, replacement), f) + case Sequenced(seq) => Sequenced(seq.map(replace(_, initial, replacement))) + } + + private final case class Pure[A](a: A) extends Compilation[A] + private final case class Delegate[F[_], A](schema: Schema[A], compiler: Visitor[F]) extends Compilation[F[A]] + private final case class Cyclic[F[_], A](schema: Lazy[Schema[A]], compiler: Visitor[F], buildRecursive : Lazy[F[A]] => F[A]) extends Compilation[F[A]] + private final case class Mapped[A, B](ca: Compilation[A], f: A => B) extends Compilation[B] + private final case class Sequenced[A, B](seq: IndexedSeq[Compilation[A]]) extends Compilation[IndexedSeq[A]] + + + private final class MCache(private[Compilation] val map: MMap[Any, Any]){ + def add[F[_], A](schema: Schema[A], compiler: Visitor[F], staged: F[A]) : Unit = { + val _ = map.put((schema, compiler), staged) + } + def get[F[_], A](schema: Schema[A], compiler: Visitor[F]) : Option[F[A]] = { + map.get((schema, compiler)).asInstanceOf[Option[F[A]]] + } + } + + /** + * This runs a compilation against a mutable cache, storing the compiled codecs as the tree gets traversed. + */ + private def unsafeInterpret[A](compilation: Compilation[A], mutableCache: MCache) : A = compilation match { + case Pure(a) => a + case Delegate(schema, compiler) => + mutableCache.get(schema, compiler) match { + case Some(value) => value + case None => + val result = unsafeInterpret(Visitor.run(schema, compiler), mutableCache) + mutableCache.add(schema, compiler, result) + result + } + case Cyclic(lschema, compiler, buildRecursive) => + val outerSchema = LazySchema(lschema) + val innerSchema = lschema.value + mutableCache.get(outerSchema, compiler) match { + case Some(value) => value + case None => + // We're creating an entry that contains a deferred codec, that will inspect the final cache when it's instantiated. + val deferredCodec = buildRecursive(Lazy(mutableCache.get(innerSchema, compiler).get)) + mutableCache.add(outerSchema, compiler, deferredCodec) + // At this point, the compilation cache contains the deferred entry. We can recurse safely, as the next traversal of + // the `LazySchema` layer in the cycle will result in a cache-hit. + unsafeInterpret(Delegate(innerSchema, compiler), mutableCache) + } + case Mapped(ca, f) => f(unsafeInterpret(ca, mutableCache)) + case Sequenced(seq) => seq.map(unsafeInterpret(_, mutableCache)) + } + +} diff --git a/modules/core/src/smithy4s/schema/Example.scala b/modules/core/src/smithy4s/schema/Example.scala new file mode 100644 index 000000000..5ec4d27de --- /dev/null +++ b/modules/core/src/smithy4s/schema/Example.scala @@ -0,0 +1,2 @@ +package smithy4s.schema + diff --git a/modules/core/src/smithy4s/schema/Schema.scala b/modules/core/src/smithy4s/schema/Schema.scala index 020865604..829a2a9fe 100644 --- a/modules/core/src/smithy4s/schema/Schema.scala +++ b/modules/core/src/smithy4s/schema/Schema.scala @@ -17,6 +17,7 @@ package smithy4s package schema +import smithy4s.internals.maps.MMap import Schema._ import scala.reflect.ClassTag @@ -214,6 +215,7 @@ object Schema { private final class TransitiveCompiler( underlying: Schema ~> Schema ) extends (Schema ~> Schema) { + val lazyCompileCache: MMap[Any, Any] = MMap.empty def apply[A]( fa: Schema[A] @@ -224,8 +226,14 @@ object Schema { underlying(u.copy(alternatives = u.alternatives.map(handleAlt(_)))) case BijectionSchema(s, bijection) => underlying(BijectionSchema(this(s), bijection)) - case LazySchema(suspend) => - underlying(LazySchema(suspend.map(this.apply))) + case l @ LazySchema(suspend) => { + lazyCompileCache + .getOrElseUpdate( + l, + LazySchema(suspend.map(this.apply)) + ) + .asInstanceOf[Schema[A]] + } case RefinementSchema(s, refinement) => underlying(RefinementSchema(this(s), refinement)) case c: CollectionSchema[c, a] => diff --git a/modules/core/src/smithy4s/schema/SchemaVisitorBase.scala b/modules/core/src/smithy4s/schema/SchemaVisitorBase.scala new file mode 100644 index 000000000..09c79524e --- /dev/null +++ b/modules/core/src/smithy4s/schema/SchemaVisitorBase.scala @@ -0,0 +1,54 @@ +/* + * Copyright 2021-2026 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 schema + +import Schema._ + +// format: off +trait SchemaVisitorBase[F[_]] { + def primitive[P](shapeId: ShapeId, hints: Hints, tag: Primitive[P]): F[P] + def collection[C[_], A](shapeId: ShapeId, hints: Hints, tag: CollectionTag[C], member: Schema[A]): F[C[A]] + def map[K, V](shapeId: ShapeId, hints: Hints, key: Schema[K], value: Schema[V]): F[Map[K, V]] + def enumeration[E](shapeId: ShapeId, hints: Hints, tag: EnumTag[E], values: List[EnumValue[E]], total: E => EnumValue[E]): F[E] + def struct[S](shapeId: ShapeId, hints: Hints, fields: Vector[Field[S, _]], make: IndexedSeq[Any] => S): F[S] + def union[U](shapeId: ShapeId, hints: Hints, alternatives: Vector[Alt[U, _]], dispatch: Alt.Dispatcher[U]): F[U] + def biject[A, B](schema: Schema[A], bijection: Bijection[A, B]): F[B] + def refine[A, B](schema: Schema[A], refinement: Refinement[A, B]): F[B] + def lazily[A](suspend: Lazy[Schema[A]]): F[A] + def option[A](schema: Schema[A]): F[Option[A]] +} + +object SchemaVisitorBase { + + private[schema] def run[F[_], A](schema: Schema[A], visitor: SchemaVisitorBase[F]) : F[A] = { + import visitor._ + schema match { + case PrimitiveSchema(shapeId, hints, tag) => primitive(shapeId, hints, tag) + case s: CollectionSchema[c, a] => collection[c,a](s.shapeId, s.hints, s.tag, s.member) + case MapSchema(shapeId, hints, key, value) => map(shapeId, hints, key, value) + case EnumerationSchema(shapeId, hints, tag, values, total) => enumeration(shapeId, hints, tag, values, total) + case StructSchema(shapeId, hints, fields, make) => struct(shapeId, hints, fields, make) + case u@UnionSchema(shapeId, hints, alts, _) => union(shapeId, hints, alts, Alt.Dispatcher.fromUnion(u)) + case BijectionSchema(schema, bijection) => biject(schema, bijection) + case RefinementSchema(schema, refinement) => refine(schema, refinement) + case LazySchema(make) => lazily(make) + case OptionSchema(a) => option(a) + } + } + +} diff --git a/sampleSpecs/recursive.smithy b/sampleSpecs/recursive.smithy index 522ce5e9a..cebe8869b 100644 --- a/sampleSpecs/recursive.smithy +++ b/sampleSpecs/recursive.smithy @@ -15,3 +15,20 @@ structure RecursiveListWrapper { @required items: RecursiveList } + +union Tree { + tree: TreeNode + leaf: LeafNode +} + +structure TreeNode { + @required + left: Tree + @required + right: Tree +} + +structure LeafNode { + @required + value: Integer +}