diff --git a/.scalafmt.conf b/.scalafmt.conf index 3dd7eeb..dec164f 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,5 +1,5 @@ version = "3.7.17" -runner.dialect=scala213 +runner.dialect=scala3 style = defaultWithAlign diff --git a/README.md b/README.md index 3d7f383..030248f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ Here's an example for Typescript: ~~~scala import bridges.typescript._ -import bridges.typescript.syntax._ Typescript.render(List( decl[Color], @@ -109,15 +108,14 @@ for defining structural types directly: ~~~scala import bridges.typescript._ import bridges.typescript.TsType._ -import bridges.typescript.syntax._ val logMessage: TsDecl = decl("LogMessage")(struct( "level" --> union(lit("error"), lit("warning")), - text --> Str + text --> Str ) -Typescript.render(logMessage) + Typescript.render(logMessage) // res0: String = // export interface LogMessage { // level: "error" | "warning"; @@ -131,7 +129,6 @@ which is something the shapeless derivation currently can't handle: ~~~scala import bridges.typescript._ import bridges.typescript.TsType._ -import bridges.typescript.syntax._ val pair: TsDecl = decl("Pair", "A", "B")(struct( @@ -139,7 +136,7 @@ val pair: TsDecl = "tail" --> Ref("B"), ) -Typescript.render(pair) + Typescript.render(pair) // res0: String = // export interface Pair { // head: A; diff --git a/build.sbt b/build.sbt index eecf6b0..7e30e85 100644 --- a/build.sbt +++ b/build.sbt @@ -6,68 +6,20 @@ enablePlugins(GitBranchPrompt) organization := "com.davegurnell" name := "bridges" -ThisBuild / scalaVersion := "3.5.0" - -ThisBuild / crossScalaVersions := Seq("2.13.13", "3.5.0") - -ThisBuild / scalacOptions ++= { - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, 12)) => - Seq( - "-feature", - "-unchecked", - "-deprecation", - "-Xfatal-warnings", - "-Ypartial-unification" - ) - - case Some((2, _)) => - Seq( - "-feature", - "-unchecked", - "-deprecation", - "-Xfatal-warnings", - ) - - case Some((3, _)) => - Seq( - "-feature", - "-unchecked", - "-deprecation", - "-Xfatal-warnings", - "-old-syntax", - ) - - case _ => - Seq( - "-feature", - "-unchecked", - "-deprecation", - "-rewrite", - "-new-syntax", - ) - } -} +ThisBuild / scalaVersion := "3.5.0" -ThisBuild / libraryDependencies ++= Seq( - "com.davegurnell" %% "unindent" % "1.8.0", - "org.apache.commons" % "commons-text" % "1.9", - "org.scalatest" %% "scalatest" % "3.2.13" % Test, - "eu.timepit" %% "refined" % "0.10.1" % Provided, +ThisBuild / scalacOptions ++= Seq( + "-feature", + "-unchecked", + "-deprecation", + "-Xfatal-warnings", ) -ThisBuild / libraryDependencies ++= { - CrossVersion.partialVersion(scalaVersion.value) match { - case Some((2, _)) => - Seq( - "com.chuusai" %% "shapeless" % "2.3.10", - "eu.timepit" %% "refined-shapeless" % "0.10.1" % Provided - ) - - case _ => - Seq.empty - } -} +ThisBuild / libraryDependencies ++= Seq( + "com.davegurnell" %% "unindent" % "1.8.0", + "org.apache.commons" % "commons-text" % "1.9", + "org.scalameta" %% "munit" % "1.0.1" % Test, +) // Versioning ----------------------------------- diff --git a/src/main/scala-2/bridges/core/DerivedEncoderInstances.scala b/src/main/scala-2/bridges/core/DerivedEncoderInstances.scala deleted file mode 100644 index ceed380..0000000 --- a/src/main/scala-2/bridges/core/DerivedEncoderInstances.scala +++ /dev/null @@ -1,62 +0,0 @@ -package bridges.core - -import shapeless._ -import shapeless.labelled.FieldType - -import scala.reflect.runtime.universe.WeakTypeTag - -trait DerivedEncoderInstances extends DerivedEncoderInstances1 { - implicit def valueClassEncoder[A <: AnyVal, B](implicit unwrapped: Unwrapped.Aux[A, B], encoder: BasicEncoder[B]): BasicEncoder[A] = - pure(encoder.encode) -} - -trait DerivedEncoderInstances1 extends DerivedEncoderInstances0 { - import Type._ - - implicit val hnilProdEncoder: ProdEncoder[HNil] = - pureProd(Prod(Nil)) - - implicit def hconsProdEncoder[K <: Symbol, H, T <: HList](implicit - witness: Witness.Aux[K], - hEnc: Lazy[BasicEncoder[H]], - tEnc: ProdEncoder[T] - ): ProdEncoder[FieldType[K, H] :: T] = { - val name = witness.value.name - val head = hEnc.value.encode - val tail = tEnc.encode - pureProd(Prod((name -> head) +: tail.fields)) - } - - implicit def cnilSumEncoder: SumEncoder[CNil] = - pureSum(Sum(Nil)) - - implicit def cconsSumEncoder[K <: Symbol, H, T <: Coproduct](implicit - witness: Witness.Aux[K], - hEnc: Lazy[ProdEncoder[H]], - tEnc: SumEncoder[T] - ): SumEncoder[FieldType[K, H] :+: T] = { - val name = witness.value.name - val product = hEnc.value.encode - val tail = tEnc.encode - pureSum(Sum((name -> product) +: tail.products)) - } - - implicit def genericProdEncoder[A, R](implicit - gen: LabelledGeneric.Aux[A, R], - enc: Lazy[ProdEncoder[R]] - ): ProdEncoder[A] = - pureProd(enc.value.encode) - - implicit def genericSumEncoder[A, R](implicit - gen: LabelledGeneric.Aux[A, R], - enc: Lazy[SumEncoder[R]] - ): SumEncoder[A] = - pureSum(enc.value.encode) -} - -trait DerivedEncoderInstances0 extends EncoderConstructors { - import Type._ - - implicit def genericBasicEncoder[A](implicit low: LowPriority, tpeTag: WeakTypeTag[A]): BasicEncoder[A] = - pure(Ref(TypeName.getTypeName[A])) -} diff --git a/src/main/scala-2/bridges/core/TypeName.scala b/src/main/scala-2/bridges/core/TypeName.scala deleted file mode 100644 index b1f120a..0000000 --- a/src/main/scala-2/bridges/core/TypeName.scala +++ /dev/null @@ -1,15 +0,0 @@ -package bridges.core - -import scala.reflect.runtime.universe.WeakTypeTag - -object TypeName { - // NOTE: we can't use `shapeless.Typeable` in here as it breaks the code for recursive types like - // final case class Recursive(head: Int, tail: Option[Recursive]) - // - // The only solution I found is to use a `WeakTypeTag` from scala runtime, - // which seems to manage the recursivity OK. - def getTypeName[A](implicit tpeTag: WeakTypeTag[A]): String = { - val fullName = tpeTag.tpe.typeSymbol.fullName - fullName.split('.').last - } -} diff --git a/src/main/scala-2/bridges/core/syntax.scala b/src/main/scala-2/bridges/core/syntax.scala deleted file mode 100644 index 7675e16..0000000 --- a/src/main/scala-2/bridges/core/syntax.scala +++ /dev/null @@ -1,33 +0,0 @@ -package bridges.core - -import shapeless.Lazy - -import scala.reflect.runtime.universe.WeakTypeTag - -object syntax extends RenamableSyntax { - import Type._ - - def encode[A: Encoder]: Type = - Encoder[A].encode - - def decl[A](implicit tpeTag: WeakTypeTag[A], encoder: Lazy[Encoder[A]]): Decl = - DeclF(TypeName.getTypeName[A], encoder.value.encode) - - // To be used for classes with generic parameters as we can't use shapeless to derive them - def decl(name: String, params: String*)(tpe: Type): Decl = - DeclF(name, params.toList, tpe) - - def prod(fields: (String, Type)*): Prod = - Prod(fields.toList) - - def sum(products: (String, Prod)*): Sum = - Sum(products.toList) - - def dict(keyType: Type, valueType: Type): Type = - Dict(keyType, valueType) - - implicit class StringDeclOps(str: String) { - def :=[A](tpe: A): DeclF[A] = - DeclF(str, tpe) - } -} diff --git a/src/main/scala-2/bridges/flow/syntax.scala b/src/main/scala-2/bridges/flow/syntax.scala deleted file mode 100644 index d65e38f..0000000 --- a/src/main/scala-2/bridges/flow/syntax.scala +++ /dev/null @@ -1,66 +0,0 @@ -package bridges.flow - -import bridges.core._ -import shapeless.Lazy - -import scala.language.implicitConversions -import scala.reflect.runtime.universe.WeakTypeTag - -object syntax extends RenamableSyntax { - import FlowType._ - - def encode[A](implicit encoder: FlowEncoder[A]): FlowType = - encoder.encode - - def decl[A](implicit tpeTag: WeakTypeTag[A], encoder: Lazy[FlowEncoder[A]]): FlowDecl = - DeclF(TypeName.getTypeName[A], encoder.value.encode) - - def decl(name: String, params: String*)(tpe: FlowType): FlowDecl = - DeclF(name, params.toList, tpe) - - def struct(fields: FlowField*): Struct = - Struct(fields.toList) - - implicit class StringFieldOps(name: String) { - def -->(tpe: FlowType): FlowField = - FlowField(name, tpe, optional = false) - - def -?>(tpe: FlowType): FlowField = - FlowField(name, tpe, optional = true) - } - - @deprecated("Use --> instead of ->", "0.16.0") - implicit def pairToField(pair: (String, FlowType)): FlowField = { - val (name, tpe) = pair - FlowField(name, tpe, optional = false) - } - - def field(name: String, optional: Boolean = false)(tpe: FlowType): FlowField = - FlowField(name, tpe, optional) - - def restField(name: String, keyType: FlowType)(valueType: FlowType): FlowRestField = - FlowRestField(name, keyType, valueType) - - def tuple(types: FlowType*): FlowType = - Tuple(types.toList) - - def union(types: FlowType*): FlowType = - Union(types.toList) - - def inter(types: FlowType*): FlowType = - Inter(types.toList) - - def arr(tpe: FlowType): FlowType = - Arr(tpe) - - def dict(keyType: FlowType, valueType: FlowType): FlowType = - Struct(Nil, Some(FlowRestField("key", keyType, valueType))) - - def ref(name: String, params: FlowType*): Ref = - Ref(name, params.toList) - - implicit class StringDeclOps(str: String) { - def :=[A](tpe: A): DeclF[A] = - DeclF(str, tpe) - } -} diff --git a/src/main/scala-2/bridges/typescript/syntax.scala b/src/main/scala-2/bridges/typescript/syntax.scala deleted file mode 100644 index 3e0ece9..0000000 --- a/src/main/scala-2/bridges/typescript/syntax.scala +++ /dev/null @@ -1,66 +0,0 @@ -package bridges.typescript - -import bridges.core.{ DeclF, RenamableSyntax, TypeName } -import shapeless.Lazy - -import scala.language.implicitConversions -import scala.reflect.runtime.universe.WeakTypeTag - -object syntax extends RenamableSyntax { - import TsType._ - - def encode[A](implicit encoder: TsEncoder[A]): TsType = - encoder.encode - - def decl[A](implicit tpeTag: WeakTypeTag[A], encoder: Lazy[TsEncoder[A]]): TsDecl = - DeclF(TypeName.getTypeName[A], encoder.value.encode) - - def decl(name: String, params: String*)(tpe: TsType): TsDecl = - DeclF(name, params.toList, tpe) - - def struct(fields: TsField*): Struct = - Struct(fields.toList) - - def dict(keyType: TsType, valueType: TsType): Struct = - Struct(Nil, Some(TsRestField("key", keyType, valueType))) - - implicit class StringFieldOps(name: String) { - def -->(tpe: TsType): TsField = - TsField(name, tpe, optional = false) - - def -?>(tpe: TsType): TsField = - TsField(name, tpe, optional = true) - } - - @deprecated("Use --> instead of ->", "0.16.0") - implicit def pairToField(pair: (String, TsType)): TsField = { - val (name, tpe) = pair - TsField(name, tpe, optional = false) - } - - def field(name: String, optional: Boolean = false)(tpe: TsType): TsField = - TsField(name, tpe, optional) - - def restField(name: String, keyType: TsType)(valueType: TsType): TsRestField = - TsRestField(name, keyType, valueType) - - def tuple(types: TsType*): Tuple = - Tuple(types.toList) - - def union(types: TsType*): Union = - Union(types.toList) - - def inter(types: TsType*): Inter = - Inter(types.toList) - - def ref(name: String, params: TsType*): Ref = - Ref(name, params.toList) - - implicit class StringDeclOps(str: String) { - def :=[A](tpe: A): DeclF[A] = - DeclF(str, tpe) - } - - def func(args: (String, TsType)*)(ret: TsType): Func = - Func(args.toList, ret) -} diff --git a/src/main/scala-3/bridges/core/DerivedEncoderInstances.scala b/src/main/scala-3/bridges/core/DerivedEncoderInstances.scala deleted file mode 100644 index 1c0590a..0000000 --- a/src/main/scala-3/bridges/core/DerivedEncoderInstances.scala +++ /dev/null @@ -1,64 +0,0 @@ -package bridges.core - -import scala.compiletime._ -import scala.deriving._ - -trait DerivedEncoderInstances extends DerivedEncoderInstances1 { - // given valueClassEncoder[A <: AnyVal, B](implicit encoder: BasicEncoder[B]): BasicEncoder[A] = - // ??? -} - -trait DerivedEncoderInstances1 extends DerivedEncoderInstances0 { - inline given allEncoders[A <: Tuple]: List[Encoder[Any]] = { - inline erasedValue[A] match { - case _: EmptyTuple => Nil - case _: (h *: t) => summonInline[Encoder[h]].asInstanceOf[Encoder[Any]] :: allEncoders[t] - } - } - - inline given allProdEncoders[A <: Tuple]: List[ProdEncoder[Any]] = { - inline erasedValue[A] match { - case _: EmptyTuple => Nil - case _: (h *: t) => summonInline[ProdEncoder[h]].asInstanceOf[ProdEncoder[Any]] :: allProdEncoders[t] - } - } - - private inline def allLabels[A <: Tuple]: List[String] = { - inline erasedValue[A] match { - case _: EmptyTuple => Nil - case _: (h *: t) => constValue[h].asInstanceOf[String] :: allLabels[t] - } - } - - inline given deriveEncoder[A](using mirror: Mirror.Of[A]): Encoder[A] = { - lazy val name = TypeName.getTypeName[A] - - println(s"derivedEncoder[${name}]") - - lazy val labels: List[String] = - allLabels[mirror.MirroredElemLabels] - - inline mirror match { - case mirror: Mirror.ProductOf[A] => prodEncoder(mirror, labels) - case mirror: Mirror.SumOf[A] => sumEncoder(mirror, labels) - } - } - - private inline def prodEncoder[A](mirror: Mirror.ProductOf[A], labels: List[String]): Encoder[A] = { - lazy val types: List[Type] = allEncoders[mirror.MirroredElemTypes].map(_.encode) - pureProd(Type.Prod(labels.zip(types))) - } - - private inline def sumEncoder[A](mirror: Mirror.SumOf[A], labels: List[String]): Encoder[A] = { - lazy val types: List[Type.Prod] = allProdEncoders[mirror.MirroredElemTypes].map(_.encode) - pure(Type.Sum(labels.zip(types))) - } -} - -trait DerivedEncoderInstances0 extends EncoderConstructors { - inline given genericBasicEncoder[A]: BasicEncoder[A] = { - lazy val name = TypeName.getTypeName[A] - println(s"genericBasicEncoder[${name}]") - pure(Type.Ref(name)) - } -} diff --git a/src/main/scala-3/bridges/core/TypeName.scala b/src/main/scala-3/bridges/core/TypeName.scala deleted file mode 100644 index 0ef73d2..0000000 --- a/src/main/scala-3/bridges/core/TypeName.scala +++ /dev/null @@ -1,14 +0,0 @@ -package bridges.core - -import scala.quoted.* - -object TypeName { - inline def getTypeName[T]: String = - ${ getTypeNameImpl[T] } - - private def getTypeNameImpl[T: Type](using Quotes): Expr[String] = { - import quotes.reflect.* - val typeRepr = TypeRepr.of[T] - Expr(typeRepr.show.split("\\.").last) - } -} diff --git a/src/main/scala-3/bridges/core/syntax.scala b/src/main/scala-3/bridges/core/syntax.scala deleted file mode 100644 index 5f9fe65..0000000 --- a/src/main/scala-3/bridges/core/syntax.scala +++ /dev/null @@ -1,28 +0,0 @@ -package bridges.core - -object syntax extends RenamableSyntax { - import Type._ - - def encode[A: Encoder]: Type = - Encoder[A].encode - - inline def decl[A](implicit encoder: => Encoder[A]): Decl = - DeclF(TypeName.getTypeName[A], encoder.encode) - - def decl(name: String, params: String*)(tpe: Type): Decl = - DeclF(name, params.toList, tpe) - - def prod(fields: (String, Type)*): Prod = - Prod(fields.toList) - - def sum(products: (String, Prod)*): Sum = - Sum(products.toList) - - def dict(keyType: Type, valueType: Type): Type = - Dict(keyType, valueType) - - implicit class StringDeclOps(str: String) { - def :=[A](tpe: A): DeclF[A] = - DeclF(str, tpe) - } -} diff --git a/src/main/scala-3/bridges/flow/syntax.scala b/src/main/scala-3/bridges/flow/syntax.scala deleted file mode 100644 index 6db31f0..0000000 --- a/src/main/scala-3/bridges/flow/syntax.scala +++ /dev/null @@ -1,56 +0,0 @@ -package bridges.flow - -import bridges.core._ - -object syntax extends RenamableSyntax { - import FlowType._ - - def encode[A](implicit encoder: FlowEncoder[A]): FlowType = - encoder.encode - - inline def decl[A](implicit encoder: => FlowEncoder[A]): FlowDecl = - DeclF(TypeName.getTypeName[A], encoder.encode) - - def decl(name: String, params: String*)(tpe: FlowType): FlowDecl = - DeclF(name, params.toList, tpe) - - def struct(fields: FlowField*): Struct = - Struct(fields.toList) - - implicit class StringFieldOps(name: String) { - def -->(tpe: FlowType): FlowField = - FlowField(name, tpe, optional = false) - - def -?>(tpe: FlowType): FlowField = - FlowField(name, tpe, optional = true) - } - - def field(name: String, optional: Boolean = false)(tpe: FlowType): FlowField = - FlowField(name, tpe, optional) - - def restField(name: String, keyType: FlowType)(valueType: FlowType): FlowRestField = - FlowRestField(name, keyType, valueType) - - def tuple(types: FlowType*): FlowType = - Tuple(types.toList) - - def union(types: FlowType*): FlowType = - Union(types.toList) - - def inter(types: FlowType*): FlowType = - Inter(types.toList) - - def arr(tpe: FlowType): FlowType = - Arr(tpe) - - def dict(keyType: FlowType, valueType: FlowType): FlowType = - Struct(Nil, Some(FlowRestField("key", keyType, valueType))) - - def ref(name: String, params: FlowType*): Ref = - Ref(name, params.toList) - - implicit class StringDeclOps(str: String) { - def :=[A](tpe: A): DeclF[A] = - DeclF(str, tpe) - } -} diff --git a/src/main/scala-3/bridges/typescript/syntax.scala b/src/main/scala-3/bridges/typescript/syntax.scala deleted file mode 100644 index eb9fa8d..0000000 --- a/src/main/scala-3/bridges/typescript/syntax.scala +++ /dev/null @@ -1,56 +0,0 @@ -package bridges.typescript - -import bridges.core.{ DeclF, RenamableSyntax, TypeName } - -object syntax extends RenamableSyntax { - import TsType._ - - def encode[A](implicit encoder: TsEncoder[A]): TsType = - encoder.encode - - inline def decl[A](implicit encoder: => TsEncoder[A]): TsDecl = - DeclF(TypeName.getTypeName[A], encoder.encode) - - def decl(name: String, params: String*)(tpe: TsType): TsDecl = - DeclF(name, params.toList, tpe) - - def struct(fields: TsField*): Struct = - Struct(fields.toList) - - def dict(keyType: TsType, valueType: TsType): Struct = - Struct(Nil, Some(TsRestField("key", keyType, valueType))) - - implicit class StringFieldOps(name: String) { - def -->(tpe: TsType): TsField = - TsField(name, tpe, optional = false) - - def -?>(tpe: TsType): TsField = - TsField(name, tpe, optional = true) - } - - def field(name: String, optional: Boolean = false)(tpe: TsType): TsField = - TsField(name, tpe, optional) - - def restField(name: String, keyType: TsType)(valueType: TsType): TsRestField = - TsRestField(name, keyType, valueType) - - def tuple(types: TsType*): Tuple = - Tuple(types.toList) - - def union(types: TsType*): Union = - Union(types.toList) - - def inter(types: TsType*): Inter = - Inter(types.toList) - - def ref(name: String, params: TsType*): Ref = - Ref(name, params.toList) - - implicit class StringDeclOps(str: String) { - def :=[A](tpe: A): DeclF[A] = - DeclF(str, tpe) - } - - def func(args: (String, TsType)*)(ret: TsType): Func = - Func(args.toList, ret) -} diff --git a/src/main/scala/bridges/core/DeclF.scala b/src/main/scala/bridges/core/DeclF.scala deleted file mode 100644 index 3e60ba6..0000000 --- a/src/main/scala/bridges/core/DeclF.scala +++ /dev/null @@ -1,25 +0,0 @@ -package bridges.core - -/** A named declaration. Either top-level or a field in a sum, product, or struct. - * - * We define type aliases for DeclF in - * the package objects for bridges.core, bridges.flow, and bridges.typescript. - */ -final case class DeclF[+A](name: String, params: List[String], tpe: A) { - def map[B](func: A => B): DeclF[B] = - copy(tpe = func(tpe)) -} - -object DeclF { - def apply[A](name: String, tpe: A): DeclF[A] = - DeclF(name, Nil, tpe) - - implicit def rename[A](implicit rename: Rename[A]): Rename[DeclF[A]] = - Rename.pure[DeclF[A]] { (decl, from, to) => - DeclF( - if (decl.name == from) to else decl.name, - decl.params, - if (decl.params.contains(from)) decl.tpe else rename(decl.tpe, from, to) - ) - } -} diff --git a/src/main/scala/bridges/core/Encoder.scala b/src/main/scala/bridges/core/Encoder.scala deleted file mode 100644 index f18a5ed..0000000 --- a/src/main/scala/bridges/core/Encoder.scala +++ /dev/null @@ -1,74 +0,0 @@ -package bridges.core - -import eu.timepit.refined.api._ - -trait Encoder[A] { - def encode: Type -} - -trait ProdEncoder[A] extends Encoder[A] { - override def encode: Type.Prod -} - -trait SumEncoder[A] extends Encoder[A] { - override def encode: Type.Sum -} - -trait BasicEncoder[A] extends Encoder[A] - -object Encoder extends DerivedEncoderInstances1 { - import Type._ - - implicit val stringEncoder: BasicEncoder[String] = - pure(Str) - - implicit val charEncoder: BasicEncoder[Char] = - pure(Chr) - - implicit val intEncoder: BasicEncoder[Int] = - pure(Intr) - - implicit val longEncoder: BasicEncoder[Long] = - pure(Intr) - - implicit val bigDecimalEncoder: BasicEncoder[BigDecimal] = - pure(Real) - - implicit val doubleEncoder: BasicEncoder[Double] = - pure(Real) - - implicit val floatEncoder: BasicEncoder[Float] = - pure(Real) - - implicit val booleanEncoder: BasicEncoder[Boolean] = - pure(Bool) - - implicit def optionEncoder[A](implicit enc: BasicEncoder[A]): BasicEncoder[Option[A]] = - pure(Opt(enc.encode)) - - implicit def mapEncoder[A, B](implicit aEnc: BasicEncoder[A], bEnc: Encoder[B]): BasicEncoder[Map[A, B]] = - pure(Dict(aEnc.encode, bEnc.encode)) - - implicit def traversableEncoder[F[_] <: Iterable[?], A](implicit enc: BasicEncoder[A]): BasicEncoder[F[A]] = - pure(Arr(enc.encode)) - - implicit def refinedEncoder[A, B](implicit enc: BasicEncoder[A]): BasicEncoder[Refined[A, B]] = - pure(enc.encode) - -} - -trait EncoderConstructors { - import Type._ - - def apply[A](implicit enc: Encoder[A]): Encoder[A] = - enc - - def pure[A](tpe: Type): BasicEncoder[A] = - new BasicEncoder[A] { def encode: Type = tpe } - - def pureProd[A](tpe: Prod): ProdEncoder[A] = - new ProdEncoder[A] { def encode: Prod = tpe } - - def pureSum[A](tpe: Sum): SumEncoder[A] = - new SumEncoder[A] { def encode: Sum = tpe } -} diff --git a/src/main/scala/bridges/core/Rename.scala b/src/main/scala/bridges/core/Rename.scala deleted file mode 100644 index a2234d8..0000000 --- a/src/main/scala/bridges/core/Rename.scala +++ /dev/null @@ -1,30 +0,0 @@ -package bridges.core - -trait Rename[A] { - def apply(value: A, from: String, to: String): A -} - -object Rename { - def apply[A](implicit rename: Rename[A]): Rename[A] = - rename - - def pure[A](func: (A, String, String) => A): Rename[A] = - new Rename[A] { - override def apply(value: A, from: String, to: String): A = - func(value, from, to) - } - - implicit def pairRename[A](implicit aRename: Rename[A]): Rename[(String, A)] = - Rename.pure { (pair, from, to) => - pair match { - case (name, a) => (if (name == from) to else name, aRename(a, from, to)) - } - } -} - -trait RenamableSyntax { - implicit class RenamableOps[A](value: A) { - def rename(from: String, to: String)(implicit rename: Rename[A]): A = - rename(value, from, to) - } -} diff --git a/src/main/scala/bridges/core/Renderer.scala b/src/main/scala/bridges/core/Renderer.scala deleted file mode 100644 index 06234bc..0000000 --- a/src/main/scala/bridges/core/Renderer.scala +++ /dev/null @@ -1,8 +0,0 @@ -package bridges.core - -trait Renderer[A] { - def render(decl: DeclF[A]): String - - def render(decls: List[DeclF[A]]): String = - decls.map(render).mkString("\n\n") -} diff --git a/src/main/scala/bridges/core/Type.scala b/src/main/scala/bridges/core/Type.scala deleted file mode 100644 index e5bb8cb..0000000 --- a/src/main/scala/bridges/core/Type.scala +++ /dev/null @@ -1,59 +0,0 @@ -package bridges.core - -import bridges.core.syntax._ - -/** Representation of a nominal, sum-of-products style type. - * - * We can encode Scala ADTs to this representation: - * - * - sealed traits become instances of Sum; - * - case classes become instances of Prod; - * - references to other types in the body of a Sum or Prod become Refs; - * - we have special encodings for Options and sequences - * (which are normally handled specially in the target language). - * - * We can generate Elm bindings directly from this representation. For Flow and Typescript bindings we translate to other intermediate - * representations. - */ -sealed abstract class Type extends Product with Serializable - -object Type { - final case class Ref(id: String, params: List[Type] = Nil) extends Type - case object Str extends Type - case object Chr extends Type - case object Intr extends Type - case object Real extends Type - case object Bool extends Type - final case class Opt(tpe: Type) extends Type - final case class Arr(tpe: Type) extends Type - final case class Dict(keys: Type, values: Type) extends Type - final case class Prod(fields: List[(String, Type)]) extends Type - final case class Sum(products: List[(String, Prod)]) extends Type - - implicit private val prodRename: Rename[Prod] = - Rename.pure { (tpe, from, to) => - tpe match { - case Prod(fields) => Prod(fields.map(_.rename(from, to))) - } - } - - implicit val rename: Rename[Type] = - Rename.pure { (tpe, from, to) => - def renameId(id: String): String = - if (id == from) to else id - - tpe match { - case Ref(id, params) => Ref(renameId(id), params.map(_.rename(from, to))) - case tpe @ Str => tpe - case tpe @ Chr => tpe - case tpe @ Intr => tpe - case tpe @ Real => tpe - case tpe @ Bool => tpe - case Opt(tpe) => Opt(tpe.rename(from, to)) - case Arr(tpe) => Arr(tpe.rename(from, to)) - case Dict(kTpe, vTpe) => Dict(kTpe.rename(from, to), vTpe.rename(from, to)) - case Prod(fields) => Prod(fields.map(_.rename(from, to))) - case Sum(products) => Sum(products.map(_.rename(from, to))) - } - } -} diff --git a/src/main/scala/bridges/core/package.scala b/src/main/scala/bridges/core/package.scala deleted file mode 100644 index 4d1aa2c..0000000 --- a/src/main/scala/bridges/core/package.scala +++ /dev/null @@ -1,5 +0,0 @@ -package bridges - -package object core { - type Decl = DeclF[Type] -} diff --git a/src/main/scala/bridges/elm/Elm.scala b/src/main/scala/bridges/elm/Elm.scala deleted file mode 100644 index ee11251..0000000 --- a/src/main/scala/bridges/elm/Elm.scala +++ /dev/null @@ -1,3 +0,0 @@ -package bridges.elm - -object Elm extends ElmRenderer with ElmJsonDecoder with ElmJsonEncoder with ElmFileBuilder diff --git a/src/main/scala/bridges/elm/ElmFileBuilder.scala b/src/main/scala/bridges/elm/ElmFileBuilder.scala deleted file mode 100644 index ed6884a..0000000 --- a/src/main/scala/bridges/elm/ElmFileBuilder.scala +++ /dev/null @@ -1,86 +0,0 @@ -package bridges.elm - -import bridges.core.Type._ -import bridges.core._ -import unindent._ - -trait ElmFileBuilder { - // Given a declaration, returns a tuple with file name and file contents: - def buildFile(module: String, decl: Decl, customTypeReplacements: Map[Ref, TypeReplacement] = Map.empty): (String, String) = - buildFile(module, List(decl), customTypeReplacements) - - def buildFile(module: String, decls: List[Decl], customTypeReplacements: Map[Ref, TypeReplacement]): (String, String) = { - val fileName = decls.headOption.map(_.name).getOrElse("") - val foldZero = ("", "", "") - - val typesInFile = decls.map(_.name) - val replacementTypes = customTypeReplacements.keySet - val typeImports = decls - .flatMap(d => getDeclarationTypes(d.tpe, d.name)) - .distinct - .filterNot(r => typesInFile.contains(r.id) || replacementTypes.contains(r)) - .map(r => s"import $module.${r.id} exposing (..)") - .mkString("\n") - - val (declarations, decoders, encoders) = - decls.map(getFileComponents(module, customTypeReplacements, _)).foldLeft(foldZero) { case (acc, (t, d, e)) => - ( - s"${acc._1}\n$t", - s"${acc._2}\n\n$d", - s"${acc._3}\n\n$e" - ) - } - - val pipelineImport = - "import Json.Decode.Pipeline exposing (..)" - - val customImports = customTypeReplacements.values.filter(td => decoders.contains(td.newType)).flatMap(_.imports).mkString("\n") - - val imports = typeImports + customImports - - val content = - i""" - module $module.$fileName exposing (..) - - import Json.Decode as Decode - $pipelineImport - import Json.Encode as Encode - $imports - $declarations - $decoders - $encoders - """ - - (s"$fileName.elm", content) - } - - private def getFileComponents( - module: String, - customTypeReplacements: Map[Ref, TypeReplacement], - decl: Decl - ): (String, String, String) = { - val declaration = Elm.render(decl, customTypeReplacements) - val decoder = Elm.decoder(decl, customTypeReplacements) - val encoder = Elm.encoder(decl, customTypeReplacements) - - (declaration, decoder, encoder) - } - - private def getDeclarationTypes(tpe: Type, parentType: String): List[Ref] = { - def getIncludeTypes(tpe: Type): List[Ref] = - tpe match { - case r: Ref => r :: Nil - case Opt(optTpe) => getIncludeTypes(optTpe) - case Arr(arrTpe) => getIncludeTypes(arrTpe) - case Dict(kTpe, vTpe) => Ref("Dict") +: (getIncludeTypes(kTpe) ++ getIncludeTypes(vTpe)) - case Prod(fields) => fields.map { case (_, tpe) => tpe }.flatMap(getIncludeTypes) - case Sum(products) => products.map { case (_, tpe) => tpe }.flatMap(getIncludeTypes) - case _ => Nil - } - - val exclude = Ref(parentType) - val include = getIncludeTypes(tpe) - - include.distinct.filterNot(_ == exclude) - } -} diff --git a/src/main/scala/bridges/elm/ElmJsonDecoder.scala b/src/main/scala/bridges/elm/ElmJsonDecoder.scala deleted file mode 100644 index 9317b59..0000000 --- a/src/main/scala/bridges/elm/ElmJsonDecoder.scala +++ /dev/null @@ -1,84 +0,0 @@ -package bridges.elm - -import bridges.core._ -import bridges.core.Type._ -import unindent._ - -trait ElmJsonDecoder extends ElmUtils { - def decoder(decls: List[Decl], customTypeReplacements: Map[Ref, TypeReplacement]): String = - decls.map(decoder(_, customTypeReplacements)).mkString("\n\n") - - def decoder(decl: Decl, customTypeReplacements: Map[Ref, TypeReplacement] = Map.empty): String = { - val (newTypeReplacements, genericsDefinition) = mergeGenericsAndTypes(decl, customTypeReplacements) - val genericsInType = genericsDefinition.foldLeft("")((acc, b) => s"$acc $b") - val nameWithGenerics = if (genericsInType.isEmpty) decl.name else s"(${decl.name}$genericsInType)" - val definitionsForGenerics = genericsDefinition.map(s => s"(Decode.Decoder $s) -> ").foldLeft("")((acc, b) => s"$acc$b") - val methodsForGenerics = genericsDefinition.map(s => s"decoder${s.toUpperCase}").foldLeft("")((acc, b) => s"$acc $b") - - decl.tpe match { - case Sum(products) => - // DO NOT REMOVE SPACE AT END - needed for Elm compiler and to pass tests. Yup, dirty, I know! - val body = products.map { case (name, prod) => decodeSumType(name, prod, newTypeReplacements) }.mkString("\n ") - i""" - decoder${decl.name} : ${definitionsForGenerics}Decode.Decoder $nameWithGenerics - decoder${decl.name}$methodsForGenerics = Decode.field "type" Decode.string |> Decode.andThen decoder${decl.name}Tpe$methodsForGenerics - - decoder${decl.name}Tpe : ${definitionsForGenerics}String -> Decode.Decoder $nameWithGenerics - decoder${decl.name}Tpe$methodsForGenerics tpe = - case tpe of - $body - _ -> Decode.fail ("Unexpected type for ${decl.name}: " ++ tpe) - """ - case other => - val body = decodeType(other, newTypeReplacements) - - i""" - decoder${decl.name} : ${definitionsForGenerics}Decode.Decoder $nameWithGenerics - decoder${decl.name}$methodsForGenerics = Decode.succeed ${decl.name} $body - """ - } - } - - private def decodeSumType(name: String, prod: Prod, customTypeReplacements: Map[Ref, TypeReplacement]): String = { - val refName = Ref(name) - val mainType = customTypeReplacements.get(refName).map(_.newType).getOrElse(name) - - val paramsDecoder = - prod.fields.map { case (name, tpe) => decodeField(name, tpe, customTypeReplacements) }.mkString(" |> ") - - // consider case objects vs case classes - val bodyDecoder = - if (paramsDecoder.isEmpty) s"Decode.succeed $mainType" - else s"Decode.succeed $mainType |> $paramsDecoder" - - s""""$mainType" -> $bodyDecoder""" - } - - private def decodeType(tpe: Type, customTypeReplacements: Map[Ref, TypeReplacement]): String = - tpe match { - case r @ Ref(id, _) => customTypeReplacements.get(r).flatMap(_.decoder).getOrElse(s"""(Decode.lazy (\\_ -> decoder$id))""") - case Str => "Decode.string" - case Chr => "Decode.string" - case Intr => "Decode.int" - case Real => "Decode.float" - case Bool => "Decode.bool" - case Opt(optTpe) => "(Decode.maybe " + decodeType(optTpe, customTypeReplacements) + ")" - case Arr(arrTpe) => "(Decode.list " + decodeType(arrTpe, customTypeReplacements) + ")" - case Dict(Str, vTpe) => "(Decode.dict " + decodeType(vTpe, customTypeReplacements) + ")" - // The Elm standard library only provides JSON decoders for dictionaries with string keys: - case _: Dict => throw new IllegalArgumentException("Cannot create a JsonDecoder for a Dict with anything other than String keys") - case Prod(fields) => fields.map { case (name, tpe) => decodeField(name, tpe, customTypeReplacements) }.mkString("|> ", " |> ", "") - case _: Sum => throw new IllegalArgumentException("SumOfProducts jsonEncoder: we should never be here") - } - - private def decodeField(name: String, tpe: Type, customTypeReplacements: Map[Ref, TypeReplacement]): String = { - def decode(tpe: Type) = - s"""required "${name}" ${decodeType(tpe, customTypeReplacements)}""" - - tpe match { - case Opt(optTpe) => - s"""optional "${name}" (Decode.maybe ${decodeType(optTpe, customTypeReplacements)}) Nothing""" - case other => decode(other) - } - } -} diff --git a/src/main/scala/bridges/elm/ElmJsonEncoder.scala b/src/main/scala/bridges/elm/ElmJsonEncoder.scala deleted file mode 100644 index a0fc613..0000000 --- a/src/main/scala/bridges/elm/ElmJsonEncoder.scala +++ /dev/null @@ -1,87 +0,0 @@ -package bridges.elm - -import bridges.core._ -import bridges.core.Type._ -import unindent._ - -trait ElmJsonEncoder extends ElmUtils { - def encoder(decls: List[Decl], customTypeReplacements: Map[Ref, TypeReplacement]): String = - decls.map(encoder(_, customTypeReplacements)).mkString("\n\n") - - def encoder(decl: Decl, customTypeReplacements: Map[Ref, TypeReplacement] = Map.empty): String = { - val (newTypeReplacements, genericsDefinition) = mergeGenericsAndTypes(decl, customTypeReplacements) - val genericsInType = genericsDefinition.foldLeft("")((acc, b) => s"$acc $b") - val definitionsForGenerics = genericsDefinition.map(s => s"($s -> Encode.Value) -> ").foldLeft("")((acc, b) => s"$acc$b") - val methodsForGenerics = genericsDefinition.map(s => s"encoder${s.toUpperCase}").foldLeft("")((acc, b) => s"$acc $b") - decl.tpe match { - case Sum(products) => - // DO NOT REMOVE SPACE AT END - needed for Elm compiler and to pass tests. Yup, dirty, I know! - val body = - products.map { case (name, prod) => encodeSumType(name, prod, newTypeReplacements) }.mkString("\n ") - - i""" - encoder${decl.name} : $definitionsForGenerics${decl.name}$genericsInType -> Encode.Value - encoder${decl.name}$methodsForGenerics tpe = - case tpe of - $body - """ - case other => - val body = encodeType(other, "obj", decl.name, newTypeReplacements) - - i""" - encoder${decl.name} : $definitionsForGenerics${decl.name}$genericsInType -> Encode.Value - encoder${decl.name}$methodsForGenerics obj = $body - """ - } - } - - private def encodeSumType(name: String, tpe: Prod, customTypeReplacements: Map[Ref, TypeReplacement]): String = { - val refName = Ref(name) - val mainType = customTypeReplacements.get(refName).map(_.newType).getOrElse(name) - - val params = tpe.fields.map { case (name, tpe) => name }.mkString(" ") - val paramsEncoder = tpe.fields.map { case (name, tpe) => encodeField(name, tpe, "", customTypeReplacements) } - - val caseEncoder = if (params.isEmpty) mainType else s"$mainType $params" - val bodyEncoder = - (paramsEncoder :+ s"""("type", Encode.string "$mainType")""") - .mkString("Encode.object [ ", ", ", " ]") - - s"""$caseEncoder -> $bodyEncoder""" - } - - private def encodeType(tpe: Type, objectName: String, fieldName: String, customTypeReplacements: Map[Ref, TypeReplacement]): String = - tpe match { - case r @ Ref(id, _) => customTypeReplacements.get(r).flatMap(_.encoder).getOrElse(s"encoder$id") + s" $fieldName" - case Str => s"Encode.string $fieldName" - case Chr => s"Encode.string $fieldName" - case Intr => s"Encode.int $fieldName" - case Real => s"Encode.float $fieldName" - case Bool => s"Encode.bool $fieldName" - case Opt(optTpe) => - "Maybe.withDefault Encode.null (Maybe.map " + - encodeType(optTpe, objectName, fieldName, customTypeReplacements) + ")" - case Arr(arrTpe) => - "Encode.list " + - encodeType(arrTpe, objectName, fieldName, customTypeReplacements) - case Dict(kTpe, vTpe) => - "(Encode.dict " + - encodeType(kTpe, objectName, fieldName, customTypeReplacements) + " " + - encodeType(vTpe, objectName, fieldName, customTypeReplacements) + ")" - case Prod(fields) => - fields - .map { case (name, tpe) => encodeField(name, tpe, objectName, customTypeReplacements) } - .mkString("Encode.object [ ", ", ", " ]") - case _: Sum => throw new IllegalArgumentException("SumOfProducts jsonEncoder: we should never be here") - } - - private def encodeField(name: String, tpe: Type, objectName: String, customTypeReplacements: Map[Ref, TypeReplacement]): String = { - val typeFieldName = - if (objectName.isEmpty) name else s"$objectName.$name" - - val encoding = encodeType(tpe, objectName, typeFieldName, customTypeReplacements) - - s"""("$name", $encoding)""" - } - -} diff --git a/src/main/scala/bridges/elm/ElmRenderer.scala b/src/main/scala/bridges/elm/ElmRenderer.scala deleted file mode 100644 index df4debd..0000000 --- a/src/main/scala/bridges/elm/ElmRenderer.scala +++ /dev/null @@ -1,45 +0,0 @@ -package bridges.elm - -import bridges.core._ -import bridges.core.Type._ - -trait ElmRenderer extends Renderer[Type] with ElmUtils { - def render(decl: Decl): String = - render(decl, Map.empty) - - def render(decl: Decl, customTypeReplacements: Map[Ref, TypeReplacement]): String = { - val (newTypeReplacements, genericsDefinition) = mergeGenericsAndTypes(decl, customTypeReplacements) - val genericsInType = genericsDefinition.foldLeft("")((acc, b) => s"$acc $b") - decl.tpe match { - case Sum(products) => - s"type ${decl.name}$genericsInType = ${products.map { case (name, prod) => renderSumType(name, prod, newTypeReplacements) }.mkString(" | ")}" - case other => s"type alias ${decl.name}$genericsInType = ${renderType(other, newTypeReplacements)}" - } - } - - private def renderSumType(name: String, prod: Prod, customTypeReplacements: Map[Ref, TypeReplacement]) = { - val refName = Ref(name) - val mainType = customTypeReplacements.get(refName).map(_.newType).getOrElse(name) - val params = prod.fields.map { case (name, tpe) => renderType(tpe, customTypeReplacements) }.mkString(" ") - // We trim in case we have no params (case object) as tests don't like extra spaces: - s"$mainType $params".trim - } - - private def renderType(tpe: Type, customTypeReplacements: Map[Ref, TypeReplacement]): String = - tpe match { - case r @ Ref(id, _) => customTypeReplacements.get(r).map(_.newType).getOrElse(id) - case Str => "String" - case Chr => "Char" - case Intr => "Int" - case Real => "Float" - case Bool => "Bool" - case Opt(optTpe) => "(Maybe " + renderType(optTpe, customTypeReplacements) + ")" - case Arr(arrTpe) => "(List " + renderType(arrTpe, customTypeReplacements) + ")" - case Dict(kTpe, vTpe) => "(Dict " + renderType(kTpe, customTypeReplacements) + " " + renderType(vTpe, customTypeReplacements) + ")" - case Prod(fields) => fields.map { case (name, tpe) => renderField(name, tpe, customTypeReplacements) }.mkString("{ ", ", ", " }") - case _: Sum => throw new IllegalArgumentException("SumOfProducts Renderer: we should never be here") - } - - private def renderField(name: String, tpe: Type, customTypeReplacements: Map[Ref, TypeReplacement]): String = - s"""$name: ${renderType(tpe, customTypeReplacements)}""" -} diff --git a/src/main/scala/bridges/elm/ElmUtils.scala b/src/main/scala/bridges/elm/ElmUtils.scala deleted file mode 100644 index 4056560..0000000 --- a/src/main/scala/bridges/elm/ElmUtils.scala +++ /dev/null @@ -1,17 +0,0 @@ -package bridges.elm - -import bridges.core.Decl -import bridges.core.Type.Ref - -trait ElmUtils { - - // helper method intended to be used to unify how we work with hand made generics and custom type replacements in Elm - def mergeGenericsAndTypes(decl: Decl, customTypeReplacements: Map[Ref, TypeReplacement]): (Map[Ref, TypeReplacement], List[String]) = { - val genericsAsElmReplacement = decl.params.map(k => Ref(k) -> TypeReplacement(k.toLowerCase)).toMap - val newTypeReplacements = customTypeReplacements ++ genericsAsElmReplacement - val genericsDefinition = genericsAsElmReplacement.valuesIterator.map(_.newType).toList - - (newTypeReplacements, genericsDefinition) - } - -} diff --git a/src/main/scala/bridges/elm/TypeReplacement.scala b/src/main/scala/bridges/elm/TypeReplacement.scala deleted file mode 100644 index 5f1da3f..0000000 --- a/src/main/scala/bridges/elm/TypeReplacement.scala +++ /dev/null @@ -1,10 +0,0 @@ -package bridges.elm - -final case class TypeReplacement(newType: String, imports: Option[String], decoder: Option[String], encoder: Option[String]) - -object TypeReplacement { - def apply(newType: String): TypeReplacement = TypeReplacement(newType, None, None, None) - - def apply(newType: String, imports: String, decoder: String, encoder: String): TypeReplacement = - new TypeReplacement(newType, Some(imports), Some(decoder), Some(encoder)) -} diff --git a/src/main/scala/bridges/flow/Flow.scala b/src/main/scala/bridges/flow/Flow.scala deleted file mode 100644 index b4f902d..0000000 --- a/src/main/scala/bridges/flow/Flow.scala +++ /dev/null @@ -1,3 +0,0 @@ -package bridges.flow - -object Flow extends FlowRenderer diff --git a/src/main/scala/bridges/flow/FlowEncoder.scala b/src/main/scala/bridges/flow/FlowEncoder.scala deleted file mode 100644 index a58864d..0000000 --- a/src/main/scala/bridges/flow/FlowEncoder.scala +++ /dev/null @@ -1,20 +0,0 @@ -package bridges.flow - -import bridges.core.Encoder - -trait FlowEncoder[A] { - def encode: FlowType -} - -object FlowEncoder { - def apply[A](implicit encoder: FlowEncoder[A]): FlowEncoder[A] = - encoder - - def pure[A](tpe: FlowType): FlowEncoder[A] = - new FlowEncoder[A] { - override val encode = tpe - } - - implicit def from[A](implicit encoder: Encoder[A]): FlowEncoder[A] = - pure(FlowType.from(encoder.encode)) -} diff --git a/src/main/scala/bridges/flow/FlowEncoderConfig.scala b/src/main/scala/bridges/flow/FlowEncoderConfig.scala deleted file mode 100644 index 9add6de..0000000 --- a/src/main/scala/bridges/flow/FlowEncoderConfig.scala +++ /dev/null @@ -1,8 +0,0 @@ -package bridges.flow - -case class FlowEncoderConfig(optionalFields: Boolean) - -object FlowEncoderConfig { - implicit val default: FlowEncoderConfig = - FlowEncoderConfig(optionalFields = false) -} diff --git a/src/main/scala/bridges/flow/FlowField.scala b/src/main/scala/bridges/flow/FlowField.scala deleted file mode 100644 index 703db61..0000000 --- a/src/main/scala/bridges/flow/FlowField.scala +++ /dev/null @@ -1,34 +0,0 @@ -package bridges.flow - -import bridges.core._ -import bridges.flow.syntax._ - -final case class FlowField(name: String, valueType: FlowType, optional: Boolean = false) - -object FlowField { - implicit val rename: Rename[FlowField] = - Rename.pure { (field, from, to) => - val FlowField(name, valueType, optional) = field - - FlowField( - name = if (field.name == from) to else field.name, - valueType = valueType.rename(from, to), - optional = optional - ) - } -} - -final case class FlowRestField(name: String, keyType: FlowType, valueType: FlowType) - -object FlowRestField { - implicit val rename: Rename[FlowRestField] = - Rename.pure { (field, from, to) => - val FlowRestField(name, keyType, valueType) = field - - FlowRestField( - name = if (field.name == from) to else field.name, - keyType = keyType.rename(from, to), - valueType = valueType.rename(from, to) - ) - } -} diff --git a/src/main/scala/bridges/flow/FlowRenderer.scala b/src/main/scala/bridges/flow/FlowRenderer.scala deleted file mode 100644 index f00a639..0000000 --- a/src/main/scala/bridges/flow/FlowRenderer.scala +++ /dev/null @@ -1,87 +0,0 @@ -package bridges.flow - -import bridges.core.Renderer -import org.apache.commons.text.StringEscapeUtils.{ escapeJava => escape } - -trait FlowRenderer extends Renderer[FlowType] { - import FlowType._ - - def render(decl: FlowDecl): String = - s"""export type ${renderParams(decl.name, decl.params)} = ${renderType(decl.tpe)};""" - - private def renderParams(name: String, params: List[String]): String = - if (params.isEmpty) name else params.mkString(s"$name<", ", ", ">") - - private def renderType(tpe: FlowType): String = - tpe match { - case Ref(id, params) => renderRef(id, params) - case Str => "string" - case Chr => "string" - case Intr => "number" - case Real => "number" - case Bool => "boolean" - case Null => "null" - case Undefined => "undefined" - case StrLit(value) => s""""${escape(value)}"""" - case ChrLit(value) => s""""${escape(value.toString)}"""" - case IntrLit(value) => value.toString - case RealLit(value) => value.toString - case BoolLit(value) => value.toString - case tpe @ Opt(arg) => s"""?${renderParens(tpe)(arg)}""" - case tpe @ Arr(arg) => s"""${renderParens(tpe)(arg)}[]""" - case Tuple(types) => types.map(renderType).mkString("[", ", ", "]") - case Struct(fields, rest) => renderStruct(fields, rest) - case tpe @ Inter(types) => types.map(renderParens(tpe)).mkString(" & ") - case tpe @ Union(types) => types.map(renderParens(tpe)).mkString(" | ") - } - - private def renderRef(name: String, params: List[FlowType]): String = - if (params.isEmpty) name else params.map(renderType).mkString(s"$name<", ", ", ">") - - private def renderStruct(fields: List[FlowField], rest: Option[FlowRestField]): String = - (fields.map(renderField) ++ rest.toList.map(renderRestField)).mkString("{ ", ", ", " }") - - private def renderField(field: FlowField): String = - field match { - case FlowField(name, tpe, false) => - s"""${name}: ${renderType(tpe)}""" - - case FlowField(name, tpe, true) => - s"""${name}?: ${renderType(tpe)}""" - } - - private def renderRestField(field: FlowRestField): String = { - val FlowRestField(name, keyType, valueType) = field - s"""[${name}: ${renderType(keyType)}]: ${renderType(valueType)}""" - } - - private def renderParens(outer: FlowType)(inner: FlowType): String = - if (precedence(outer) > precedence(inner)) { - s"(${renderType(inner)})" - } else { - renderType(inner) - } - - private def precedence(tpe: FlowType): Int = - tpe match { - case _: Ref => 1000 - case _ @Str => 1000 - case _ @Chr => 1000 - case _ @Intr => 1000 - case _ @Real => 1000 - case _ @Bool => 1000 - case _ @Null => 1000 - case _ @Undefined => 1000 - case _: StrLit => 1000 - case _: ChrLit => 1000 - case _: IntrLit => 1000 - case _: RealLit => 1000 - case _: BoolLit => 1000 - case _: Arr => 900 - case _: Tuple => 900 - case _: Opt => 800 - case _: Struct => 600 - case _: Union => 400 - case _: Inter => 200 - } -} diff --git a/src/main/scala/bridges/flow/FlowType.scala b/src/main/scala/bridges/flow/FlowType.scala deleted file mode 100644 index ea1fab4..0000000 --- a/src/main/scala/bridges/flow/FlowType.scala +++ /dev/null @@ -1,102 +0,0 @@ -package bridges.flow - -import bridges.core._ -import bridges.flow.syntax._ - -sealed abstract class FlowType extends Product with Serializable { - import FlowType._ - - def |(that: FlowType): FlowType = - Union(List(this, that)) - - def &(that: FlowType): FlowType = - Inter(List(this, that)) - - def ? : FlowType = - Opt(this) -} - -object FlowType { - final case class Ref(id: String, params: List[FlowType] = Nil) extends FlowType - - case object Str extends FlowType - case object Chr extends FlowType - case object Intr extends FlowType - case object Real extends FlowType - case object Bool extends FlowType - case object Null extends FlowType - case object Undefined extends FlowType - - final case class StrLit(value: String) extends FlowType - final case class ChrLit(value: Char) extends FlowType - final case class IntrLit(value: Int) extends FlowType - final case class RealLit(value: Double) extends FlowType - final case class BoolLit(value: Boolean) extends FlowType - final case class Opt(tpe: FlowType) extends FlowType - final case class Arr(tpe: FlowType) extends FlowType - final case class Tuple(types: List[FlowType]) extends FlowType - - final case class Struct(fields: List[FlowField], rest: Option[FlowRestField] = None) extends FlowType { - def withRest(keyType: FlowType, valueType: FlowType, keyName: String = "key"): Struct = - copy(rest = Some(FlowRestField(keyName, keyType, valueType))) - } - final case class Inter(types: List[FlowType]) extends FlowType - final case class Union(types: List[FlowType]) extends FlowType - - def from(tpe: Type)(implicit config: FlowEncoderConfig): FlowType = - tpe match { - case Type.Ref(id, params) => Ref(id, params.map(from)) - case Type.Str => Str - case Type.Chr => Chr - case Type.Intr => Intr - case Type.Real => Real - case Type.Bool => Bool - case Type.Opt(tpe) => Opt(from(tpe)) - case Type.Arr(tpe) => Arr(from(tpe)) - case Type.Dict(kTpe, vTpe) => Struct(Nil, Some(FlowRestField("key", from(kTpe), from(vTpe)))) - case Type.Prod(fields) => translateProd(fields) - case Type.Sum(products) => translateSum(products) - } - - private def translateProd(fields: List[(String, Type)])(implicit config: FlowEncoderConfig): Struct = - Struct(fields.map { case (name, tpe) => FlowField(name, from(tpe), keyIsOptional(tpe)) }) - - private def translateSum(products: List[(String, Type.Prod)])(implicit config: FlowEncoderConfig): Union = - Union(products.map { case (name, prod) => - Struct(FlowField("type", StrLit(name)) +: translateProd(prod.fields).fields) - }) - - private def keyIsOptional(tpe: Type)(implicit config: FlowEncoderConfig): Boolean = - tpe match { - case _: Type.Opt if config.optionalFields => true - case _ => false - } - - implicit val rename: Rename[FlowType] = - Rename.pure { (value, from, to) => - def renameId(id: String): String = - if (id == from) to else id - - value match { - case Ref(id, params) => Ref(renameId(id), params.map(_.rename(from, to))) - case tpe @ Str => tpe - case tpe @ Chr => tpe - case tpe @ Intr => tpe - case tpe @ Real => tpe - case tpe @ Bool => tpe - case tpe @ Null => tpe - case tpe @ Undefined => tpe - case tpe: StrLit => tpe - case tpe: ChrLit => tpe - case tpe: IntrLit => tpe - case tpe: RealLit => tpe - case tpe: BoolLit => tpe - case Opt(tpe) => Opt(tpe.rename(from, to)) - case Arr(tpe) => Arr(tpe.rename(from, to)) - case Tuple(types) => Tuple(types.map(_.rename(from, to))) - case Struct(fields, rest) => Struct(fields.map(_.rename(from, to)), rest.map(_.rename(from, to))) - case Inter(types) => Inter(types.map(_.rename(from, to))) - case Union(types) => Union(types.map(_.rename(from, to))) - } - } -} diff --git a/src/main/scala/bridges/flow/package.scala b/src/main/scala/bridges/flow/package.scala deleted file mode 100644 index f2e9368..0000000 --- a/src/main/scala/bridges/flow/package.scala +++ /dev/null @@ -1,7 +0,0 @@ -package bridges - -import bridges.core.DeclF - -package object flow { - type FlowDecl = DeclF[FlowType] -} diff --git a/src/main/scala/bridges/typescript/Decl.scala b/src/main/scala/bridges/typescript/Decl.scala new file mode 100644 index 0000000..155b121 --- /dev/null +++ b/src/main/scala/bridges/typescript/Decl.scala @@ -0,0 +1,7 @@ +package bridges.typescript + +final case class Decl(name: String, params: List[String], tpe: TsType) + +object Decl: + def apply(name: String, tpe: TsType): Decl = + Decl(name, Nil, tpe) diff --git a/src/main/scala/bridges/typescript/TsEncoder.scala b/src/main/scala/bridges/typescript/TsEncoder.scala index ba7e49c..9579bf6 100644 --- a/src/main/scala/bridges/typescript/TsEncoder.scala +++ b/src/main/scala/bridges/typescript/TsEncoder.scala @@ -1,20 +1,140 @@ package bridges.typescript -import bridges.core.Encoder +import bridges.typescript.syntax.* +import scala.compiletime.* +import scala.deriving.* +import scala.quoted.* +import scala.util.NotGiven -trait TsEncoder[A] { +trait TsEncoder[A]: def encode: TsType -} -object TsEncoder { +trait BasicEncoder[A] extends TsEncoder[A] + +trait AdtEncoder[A] extends TsEncoder[A] + +trait StructEncoder[A] extends AdtEncoder[A]: + override def encode: TsType.Struct + +trait UnionEncoder[A] extends AdtEncoder[A]: + override def encode: TsType.Union + +object TsEncoder extends TsEncoderInstances, TsEncoderConstructors + +trait TsEncoderInstances extends DerivedTsEncoderInstances: + self: TsEncoderConstructors => + + given stringEncoder: BasicEncoder[String] = + basicEnc(TsType.Str) + + given charEncoder: BasicEncoder[Char] = + basicEnc(TsType.Chr) + + given intEncoder: BasicEncoder[Int] = + basicEnc(TsType.Intr) + + given longEncoder: BasicEncoder[Long] = + basicEnc(TsType.Intr) + + given bigDecimalEncoder: BasicEncoder[BigDecimal] = + basicEnc(TsType.Real) + + given doubleEncoder: BasicEncoder[Double] = + basicEnc(TsType.Real) + + given floatEncoder: BasicEncoder[Float] = + basicEnc(TsType.Real) + + given booleanEncoder: BasicEncoder[Boolean] = + basicEnc(TsType.Bool) + + given optionEncoder[A](using enc: BasicEncoder[A]): BasicEncoder[Option[A]] = + basicEnc(TsType.nullable(enc.encode)) + + given mapEncoder[A, B](using aEnc: BasicEncoder[A], bEnc: TsEncoder[B]): BasicEncoder[Map[A, B]] = + basicEnc(TsType.Struct(Nil, Some(TsRestField("rest", aEnc.encode, bEnc.encode)))) + + given traversableEncoder[F[_] <: Iterable[?], A](using enc: BasicEncoder[A]): BasicEncoder[F[A]] = + basicEnc(TsType.Arr(enc.encode)) + +trait DerivedTsEncoderInstances extends LowPriorityEncoderInstances: + self: TsEncoderConstructors => + + inline given allBasicEncoders[A <: Tuple]: List[BasicEncoder[Any]] = + inline erasedValue[A] match + case _: EmptyTuple => Nil + case _: (h *: t) => summonInline[BasicEncoder[h]].asInstanceOf[BasicEncoder[Any]] :: allBasicEncoders[t] + + inline given allStructEncoders[A <: Tuple]: List[StructEncoder[Any]] = + inline erasedValue[A] match + case _: EmptyTuple => Nil + case _: (h *: t) => summonInline[StructEncoder[h]].asInstanceOf[StructEncoder[Any]] :: allStructEncoders[t] + + private inline def allLabels[A <: Tuple]: List[String] = + inline erasedValue[A] match + case _: EmptyTuple => Nil + case _: (h *: t) => constValue[h].asInstanceOf[String] :: allLabels[t] + + inline given deriveStructEncoder[A](using mirror: Mirror.ProductOf[A], config: TsEncoderConfig): StructEncoder[A] = + lazy val labels: List[String] = allLabels[mirror.MirroredElemLabels] + lazy val types: List[TsType] = allBasicEncoders[mirror.MirroredElemTypes].map(_.encode) + structEnc { + TsType.Struct( + labels.zip(types).map { (name, tpe) => + TsField(name, tpe, fieldIsOptional(config, tpe)) + } + ) + } + + inline given deriveUnionEncoder[A](using mirror: Mirror.SumOf[A], config: TsEncoderConfig): UnionEncoder[A] = + import TsType.* + + lazy val labels: List[String] = allLabels[mirror.MirroredElemLabels] + lazy val types: List[TsType.Struct] = allStructEncoders[mirror.MirroredElemTypes].map(_.encode) + + unionEnc { + val products: List[TsType] = + labels.zip(types).map { (label, tpe) => + if config.refsInUnions then + intersect( + TsType.struct(TsField("type", StrLit(label))), + Ref(label) + ) + else + TsType.Struct( + TsField("type", StrLit(label)) :: + tpe.fields.map(field => field.copy(optional = fieldIsOptional(config, field.valueType))) + ) + } + + TsType.Union(products) + } + + private def fieldIsOptional(config: TsEncoderConfig, tpe: TsType) = + config.optionalFields && tpe.isNullable + +trait LowPriorityEncoderInstances: + self: TsEncoderConstructors => + + inline given basicRefEncoder[A]: BasicEncoder[A] = + lazy val name = TypeName.of[A] + basicEnc(TsType.Ref(name)) + +trait TsEncoderConstructors: def apply[A](implicit encoder: TsEncoder[A]): TsEncoder[A] = encoder - def pure[A](tpe: TsType): TsEncoder[A] = - new TsEncoder[A] { - override val encode = tpe - } + def basicEnc[A](tpe: TsType): BasicEncoder[A] = + new BasicEncoder[A]: + override def encode: TsType = + tpe + + def structEnc[A](tpe: TsType.Struct): StructEncoder[A] = + new StructEncoder[A]: + override def encode: TsType.Struct = + tpe - implicit def from[A](implicit encoder: Encoder[A], config: TsEncoderConfig): TsEncoder[A] = - pure(TsType.from(encoder.encode)) -} + def unionEnc[A](tpe: TsType.Union): UnionEncoder[A] = + new UnionEncoder[A]: + override def encode: TsType.Union = + tpe diff --git a/src/main/scala/bridges/typescript/TsEncoderConfig.scala b/src/main/scala/bridges/typescript/TsEncoderConfig.scala index 2b424eb..e99e729 100644 --- a/src/main/scala/bridges/typescript/TsEncoderConfig.scala +++ b/src/main/scala/bridges/typescript/TsEncoderConfig.scala @@ -6,6 +6,6 @@ case class TsEncoderConfig( ) object TsEncoderConfig { - implicit val default: TsEncoderConfig = + given default: TsEncoderConfig = TsEncoderConfig() } diff --git a/src/main/scala/bridges/typescript/TsField.scala b/src/main/scala/bridges/typescript/TsField.scala index f69469f..25bd648 100644 --- a/src/main/scala/bridges/typescript/TsField.scala +++ b/src/main/scala/bridges/typescript/TsField.scala @@ -1,34 +1,17 @@ package bridges.typescript -import bridges.core._ -import bridges.typescript.syntax._ +final case class TsField(name: String, valueType: TsType, optional: Boolean = false): + def rename(from: String, to: String): TsField = + TsField( + name = if name == from then to else name, + valueType = valueType.rename(from, to), + optional = optional + ) -final case class TsField(name: String, valueType: TsType, optional: Boolean = false) - -object TsField { - implicit val rename: Rename[TsField] = - Rename.pure { (field, from, to) => - val TsField(name, valueType, optional) = field - - TsField( - name = if (field.name == from) to else field.name, - valueType = valueType.rename(from, to), - optional = optional - ) - } -} - -final case class TsRestField(name: String, keyType: TsType, valueType: TsType) - -object TsRestField { - implicit val rename: Rename[TsRestField] = - Rename.pure { (field, from, to) => - val TsRestField(name, keyType, valueType) = field - - TsRestField( - name = if (field.name == from) to else field.name, - keyType = keyType.rename(from, to), - valueType = valueType.rename(from, to) - ) - } -} +final case class TsRestField(name: String, keyType: TsType, valueType: TsType): + def rename(from: String, to: String): TsRestField = + TsRestField( + name = if name == from then to else name, + keyType = keyType.rename(from, to), + valueType = valueType.rename(from, to) + ) diff --git a/src/main/scala/bridges/typescript/TsGuardExpr.scala b/src/main/scala/bridges/typescript/TsGuardExpr.scala index 89d1eac..89112ab 100644 --- a/src/main/scala/bridges/typescript/TsGuardExpr.scala +++ b/src/main/scala/bridges/typescript/TsGuardExpr.scala @@ -2,26 +2,25 @@ package bridges.typescript import org.apache.commons.text.StringEscapeUtils.{ escapeJava => escape } -sealed abstract class TsGuardExpr - -object TsGuardExpr { - final case class Ref(name: String) extends TsGuardExpr - final case class Dot(obj: TsGuardExpr, name: String) extends TsGuardExpr - final case class Arr(exprs: List[TsGuardExpr]) extends TsGuardExpr - final case class Index(arr: TsGuardExpr, index: TsGuardExpr) extends TsGuardExpr - final case class Lit(name: String) extends TsGuardExpr - final case class Typeof(expr: TsGuardExpr) extends TsGuardExpr - final case class Call(func: TsGuardExpr, args: List[TsGuardExpr]) extends TsGuardExpr - final case class Func(args: List[String], body: TsGuardExpr) extends TsGuardExpr - final case class Guard(arg: String, retType: TsType, body: TsGuardExpr) extends TsGuardExpr - final case class Cond(test: TsGuardExpr, trueArm: TsGuardExpr, falseArm: TsGuardExpr) extends TsGuardExpr - final case class IsNull(expr: TsGuardExpr) extends TsGuardExpr - final case class Not(expr: TsGuardExpr) extends TsGuardExpr - final case class And(lhs: TsGuardExpr, rhs: TsGuardExpr) extends TsGuardExpr - final case class Or(lhs: TsGuardExpr, rhs: TsGuardExpr) extends TsGuardExpr - final case class Eql(lhs: TsGuardExpr, rhs: TsGuardExpr) extends TsGuardExpr - final case class In(key: String, expr: TsGuardExpr) extends TsGuardExpr - +enum TsGuardExpr: + case Ref(name: String) + case Dot(obj: TsGuardExpr, name: String) + case Arr(exprs: List[TsGuardExpr]) + case Index(arr: TsGuardExpr, index: TsGuardExpr) + case Lit(name: String) + case Typeof(expr: TsGuardExpr) + case Call(func: TsGuardExpr, args: List[TsGuardExpr]) + case Func(args: List[String], body: TsGuardExpr) + case Guard(arg: String, retType: TsType, body: TsGuardExpr) + case Cond(test: TsGuardExpr, trueArm: TsGuardExpr, falseArm: TsGuardExpr) + case IsNull(expr: TsGuardExpr) + case Not(expr: TsGuardExpr) + case And(lhs: TsGuardExpr, rhs: TsGuardExpr) + case Or(lhs: TsGuardExpr, rhs: TsGuardExpr) + case Eql(lhs: TsGuardExpr, rhs: TsGuardExpr) + case In(key: String, expr: TsGuardExpr) + +object TsGuardExpr: def ref(name: String): TsGuardExpr = Ref(name) @@ -88,10 +87,9 @@ object TsGuardExpr { def in(key: String, expr: TsGuardExpr): TsGuardExpr = In(key, expr) - def render(expr: TsGuardExpr): String = { - val r = renderParens(expr)(_) - - expr match { + def render(expr: TsGuardExpr): String = + val r = renderParens(expr) + expr match case Ref(name) => name case Dot(obj, name) => s"""${r(obj)}.${name}""" case Arr(exprs) => exprs.map(r).mkString("[", ", ", "]") @@ -109,18 +107,14 @@ object TsGuardExpr { case Or(lhs, rhs) => s"""${r(lhs)} || ${r(rhs)}""" case Eql(lhs, rhs) => s"""${r(lhs)} === ${r(rhs)}""" case In(key, expr) => s"""${r(lit(key))} in ${r(expr)}""" - } - } private def renderParens(outer: TsGuardExpr)(inner: TsGuardExpr): String = - if (precedence(outer) > precedence(inner)) { - s"(${render(inner)})" - } else { - render(inner) - } + if precedence(outer) > precedence(inner) + then s"(${render(inner)})" + else render(inner) private def precedence(tpe: TsGuardExpr): Int = - tpe match { + tpe match case _: Ref => 1000 case _: Dot => 1000 case _: Arr => 1000 @@ -138,5 +132,3 @@ object TsGuardExpr { case _: Cond => 400 case _: Func => 300 case _: Guard => 300 - } -} diff --git a/src/main/scala/bridges/typescript/TsGuardRenderer.scala b/src/main/scala/bridges/typescript/TsGuardRenderer.scala index fede5e7..f1bde49 100644 --- a/src/main/scala/bridges/typescript/TsGuardRenderer.scala +++ b/src/main/scala/bridges/typescript/TsGuardRenderer.scala @@ -1,25 +1,24 @@ package bridges.typescript -import bridges.core.{ DeclF, Renderer } -import unindent._ +import unindent.* -abstract class TsGuardRenderer( - predName: String => String = id => s"""is${id}""", - guardName: String => String = id => s"""as${id}""" -) extends Renderer[TsType] { - import TsType._ - import TsGuardExpr._ +abstract class TsGuardRenderer(predName: String => String = id => s"""is${id}"""): + import TsGuardExpr.* + import TsType.{ func => _, ref => _, * } - def render(decl: TsDecl): String = + def render(decls: List[Decl]): String = + decls.map(render).mkString("\n\n") + + def render(decl: Decl): String = decl match { - case DeclF(name, Nil, tpe) => + case Decl(name, Nil, tpe) => i""" export const ${predName(decl.name)} = (v: any): v is ${name} => { return ${TsGuardExpr.render(isType(ref("v"), decl.tpe))}; } """ - case DeclF(name, params, tpe) => + case Decl(name, params, tpe) => val tparams = renderParamTypes(params) val vparams = renderParamPreds(params) i""" @@ -30,31 +29,24 @@ abstract class TsGuardRenderer( } def renderParamTypes(params: List[String]): String = - if (params.isEmpty) { - "" - } else { - params.mkString("<", ", ", ">") - } + if params.isEmpty + then "" + else params.mkString("<", ", ", ">") def renderParamPreds(params: List[String]): String = params.map(param => s"${predName(param)}: (${param.toLowerCase}: any) => ${param.toLowerCase} is ${param}").mkString(", ") - import TsGuardExpr._ + import TsGuardExpr.* - def guardFunc(pair: (TsType, Int)): TsGuardExpr = { + def guardFunc(pair: (TsType, Int)): TsGuardExpr = val (tpe, index) = pair val arg = "a" + index guard(arg, tpe)(isType(ref(arg), tpe)) - } def isType(arg: TsGuardExpr, tpe: TsType): TsGuardExpr = - tpe match { - case TsType.Ref(id, Nil) => - call(ref(predName(id)), arg) - - case TsType.Ref(id, params) => - call(Call(ref(predName(id)), params.zipWithIndex.map(guardFunc)), arg) - + tpe match + case TsType.Ref(id, Nil) => call(ref(predName(id)), arg) + case TsType.Ref(id, params) => call(Call(ref(predName(id)), params.zipWithIndex.map(guardFunc)), arg) case TsType.Any => lit(true) case TsType.Unknown => lit(true) case TsType.Str => eql(typeof(arg), lit("string")) @@ -74,7 +66,6 @@ abstract class TsGuardRenderer( case TsType.Struct(fields, rest) => isStruct(arg, fields, rest) case TsType.Inter(types) => isAll(arg, types) case TsType.Union(types) => isUnion(arg, types) - } private def isArray(expr: TsGuardExpr, tpe: TsType): TsGuardExpr = and( @@ -82,16 +73,16 @@ abstract class TsGuardRenderer( call(dot(expr, "every"), func("i")(isType(ref("i"), tpe))) ) - private def isTuple(expr: TsGuardExpr, types: List[TsType]): TsGuardExpr = { + private def isTuple(expr: TsGuardExpr, types: List[TsType]): TsGuardExpr = val baseChecks = List( call(dot(ref("Array"), "isArray"), expr), eql(dot(expr, "length"), lit(types.length)) ) - val itemChecks = types.zipWithIndex.map { case (tpe, idx) => isType(index(expr, idx), tpe) } + val itemChecks: List[TsGuardExpr] = + types.zipWithIndex.map((tpe, idx) => isType(index(expr, idx), tpe)) (baseChecks ++ itemChecks).reduceLeft(and(_, _)) - } private def isStruct(expr: TsGuardExpr, fields: List[TsField], rest: Option[TsRestField]): TsGuardExpr = { val seed = and(eql(typeof(expr), lit("object")), not(isnull(expr))) @@ -101,11 +92,9 @@ abstract class TsGuardRenderer( .map { field => val TsField(name, tpe, optional) = field - if (optional) { - or(not(in(name, expr)), isType(dot(expr, name), tpe)) - } else { - and(in(name, expr), isType(dot(expr, name), tpe)) - } + if optional + then or(not(in(name, expr)), isType(dot(expr, name), tpe)) + else and(in(name, expr), isType(dot(expr, name), tpe)) } .foldLeft(seed)(and(_, _)) @@ -150,21 +139,29 @@ abstract class TsGuardRenderer( } private def isUnion(expr: TsGuardExpr, types: List[TsType]): TsGuardExpr = - types.collectAll { case tpe @ DiscriminatedBy(name, rest) => name -> rest } match { + types.collectAll { case tpe @ DiscriminatedBy(name, rest) => name -> rest } match case Some(pairs) => - and(eql(typeof(expr), lit("object")), not(isnull(expr)), in("type", expr), isDiscriminated(expr, pairs)) + and( + eql(typeof(expr), lit("object")), + not(isnull(expr)), + in("type", expr), + isDiscriminated(expr, pairs) + ) + case None => isAny(expr, types) - } private def isDiscriminated(expr: TsGuardExpr, types: List[(String, TsType.Struct)]): TsGuardExpr = - types match { + types match case Nil => lit(false) case (name, head) :: tail => - cond(eql(dot(expr, "type"), lit(name)), isType(expr, head), isDiscriminated(expr, tail)) - } + cond( + eql(dot(expr, "type"), lit(name)), + isType(expr, head), + isDiscriminated(expr, tail) + ) private def isAny(expr: TsGuardExpr, types: List[TsType]): TsGuardExpr = types @@ -178,16 +175,14 @@ abstract class TsGuardRenderer( .reduceLeftOption(and(_, _)) .getOrElse(lit(true)) - private implicit class ListOps[A](list: List[A]) { - def collectAll[B](func: PartialFunction[A, B]): Option[List[B]] = { + extension [A](list: List[A]) + def collectAll[B](func: PartialFunction[A, B]): Option[List[B]] = val temp = list.collect(func) if (temp.length == list.length) Some(temp) else None - } - } - private object DiscriminatedBy { + private object DiscriminatedBy: def unapply(tpe: TsType): Option[(String, TsType.Struct)] = - tpe match { + tpe match case TsType.Struct(fields, _) => fields.collectFirst { case decl @ TsField("type", TsType.StrLit(name), _) => (name, TsType.Struct(fields.filterNot(_ == decl))) @@ -195,6 +190,5 @@ abstract class TsGuardRenderer( case _ => None - } - } -} + end DiscriminatedBy +end TsGuardRenderer diff --git a/src/main/scala/bridges/typescript/TsType.scala b/src/main/scala/bridges/typescript/TsType.scala index dbf79e4..832535c 100644 --- a/src/main/scala/bridges/typescript/TsType.scala +++ b/src/main/scala/bridges/typescript/TsType.scala @@ -1,106 +1,98 @@ package bridges.typescript -import bridges.core._ -import bridges.typescript.syntax._ - -sealed abstract class TsType extends Product with Serializable { +enum TsType: import TsType._ - def |(that: TsType): TsType = - Union(List(this, that)) - - def &(that: TsType): TsType = - Inter(List(this, that)) -} - -object TsType { - final case class Ref(id: String, params: List[TsType] = Nil) extends TsType - - case object Any extends TsType - case object Unknown extends TsType - case object Str extends TsType - case object Chr extends TsType - case object Intr extends TsType - case object Real extends TsType - case object Bool extends TsType - case object Null extends TsType - - final case class StrLit(value: String) extends TsType - final case class ChrLit(value: Char) extends TsType - final case class IntrLit(value: Int) extends TsType - final case class RealLit(value: Double) extends TsType - final case class BoolLit(value: Boolean) extends TsType - final case class Arr(tpe: TsType) extends TsType - final case class Tuple(types: List[TsType]) extends TsType - final case class Func(args: List[(String, TsType)], ret: TsType) extends TsType - - final case class Struct(fields: List[TsField], rest: Option[TsRestField] = None) extends TsType { - def withRest(keyType: TsType, valueType: TsType, keyName: String = "key"): Struct = - copy(rest = Some(TsRestField(keyName, keyType, valueType))) - } - - final case class Inter(types: List[TsType]) extends TsType - final case class Union(types: List[TsType]) extends TsType - - def from(tpe: Type)(implicit config: TsEncoderConfig): TsType = - tpe match { - case Type.Ref(id, params) => Ref(id, params.map(from)) - case Type.Str => Str - case Type.Chr => Chr - case Type.Intr => Intr - case Type.Real => Real - case Type.Bool => Bool - case Type.Opt(tpe) => from(tpe) | Null - case Type.Arr(tpe) => Arr(from(tpe)) - case Type.Dict(kTpe, vTpe) => Struct(Nil, Some(TsRestField("key", from(kTpe), from(vTpe)))) - case Type.Prod(fields) => translateProd(fields) - case Type.Sum(products) => translateSum(products) - } - - private def translateProd(fields: List[(String, Type)])(implicit config: TsEncoderConfig): Struct = - Struct(fields.map { case (name, tpe) => TsField(name, from(tpe), keyIsOptional(tpe)) }) - - private def translateSum(products: List[(String, Type.Prod)])(implicit config: TsEncoderConfig): Union = - Union(products.map { case (name, tpe) => - if (config.refsInUnions) { - Inter(List(Struct(List(TsField("type", StrLit(name)))), Ref(name))) - } else { - Struct(TsField("type", StrLit(name)) +: translateProd(tpe.fields).fields) - } - }) - - private def keyIsOptional(tpe: Type)(implicit config: TsEncoderConfig): Boolean = - tpe match { - case _: Type.Opt if config.optionalFields => true - case _ => false - } - - implicit val rename: Rename[TsType] = - Rename.pure { (value, from, to) => - def renameId(id: String): String = - if (id == from) to else id - - value match { - case Ref(id, params) => Ref(renameId(id), params.map(_.rename(from, to))) - case Any => Any - case tpe @ Unknown => tpe - case tpe @ Str => tpe - case tpe @ Chr => tpe - case tpe @ Intr => tpe - case tpe @ Real => tpe - case tpe @ Bool => tpe - case tpe @ Null => tpe - case tpe: StrLit => tpe - case tpe: ChrLit => tpe - case tpe: IntrLit => tpe - case tpe: RealLit => tpe - case tpe: BoolLit => tpe - case Arr(tpe) => Arr(tpe.rename(from, to)) - case Tuple(types) => Tuple(types.map(_.rename(from, to))) - case Func(args, ret) => Func(args.map(_.rename(from, to)), ret.rename(from, to)) - case Struct(fields, rest) => Struct(fields.map(_.rename(from, to)), rest.map(_.rename(from, to))) - case Inter(types) => Inter(types.map(_.rename(from, to))) - case Union(types) => Union(types.map(_.rename(from, to))) - } - } -} + case Any + case Unknown + case Str + case Chr + case Intr + case Real + case Bool + case Null + + case Ref(id: String, params: List[TsType] = Nil) + case StrLit(value: String) + case ChrLit(value: Char) + case IntrLit(value: Int) + case RealLit(value: Double) + case BoolLit(value: Boolean) + case Arr(tpe: TsType) + case Tuple(types: List[TsType]) + case Func(args: List[(String, TsType)], ret: TsType) + + case Struct(fields: List[TsField], rest: Option[TsRestField] = None) + + case Inter(types: List[TsType]) + case Union(types: List[TsType]) + + def isNullable: Boolean = + this match + case Null => true + case Union(types) => types.exists(_.isNullable) + case _ => false + + def rename(from: String, to: String): TsType = + def renameId(id: String): String = + if (id == from) to else id + + this match + case Ref(id, params) => Ref(renameId(id), params.map(_.rename(from, to))) + case Any => Any + case tpe @ Unknown => tpe + case tpe @ Str => tpe + case tpe @ Chr => tpe + case tpe @ Intr => tpe + case tpe @ Real => tpe + case tpe @ Bool => tpe + case tpe @ Null => tpe + case tpe: StrLit => tpe + case tpe: ChrLit => tpe + case tpe: IntrLit => tpe + case tpe: RealLit => tpe + case tpe: BoolLit => tpe + case Arr(tpe) => Arr(tpe.rename(from, to)) + case Tuple(types) => Tuple(types.map(_.rename(from, to))) + case Func(args, ret) => Func(args.map((name, tpe) => (name, tpe.rename(from, to))), ret.rename(from, to)) + case Struct(fields, rest) => Struct(fields.map(_.rename(from, to)), rest.map(_.rename(from, to))) + case Inter(types) => Inter(types.map(_.rename(from, to))) + case Union(types) => Union(types.map(_.rename(from, to))) + +object TsType: + def ref(name: String, params: TsType*): Ref = + Ref(name, params.toList) + + def tuple(types: TsType*): Tuple = + Tuple(types.toList) + + def union(types: TsType*): Union = + Union(types.toList) + + def intersect(types: TsType*): TsType = + Inter(types.toList) + + def nullable(tpe: TsType): TsType = + union(tpe, Null) + + def dict(keyType: TsType, valueType: TsType): Struct = + Struct(Nil, Some(TsRestField("key", keyType, valueType))) + + def struct(fields: TsField*): Struct = + Struct(fields.toList) + + def labelled(name: String, tpe: TsType): TsType = + val discriminator = TsField("type", StrLit(name)) + tpe match + case Struct(fields, rest) => Struct(discriminator :: fields, rest) + case tpe => intersect(struct(discriminator), tpe) + + def discriminated(cases: (String, TsType)*): Union = + union(cases.map(labelled)*) + + def func(args: (String, TsType)*)(ret: TsType): Func = + Func(args.toList, ret) + +extension (struct: TsType.Struct) + def withRest(keyType: TsType, valueType: TsType, keyName: String = "key"): TsType.Struct = + struct.copy(rest = Some(TsRestField(keyName, keyType, valueType))) diff --git a/src/main/scala/bridges/typescript/TsTypeRenderer.scala b/src/main/scala/bridges/typescript/TsTypeRenderer.scala index 8ee90d5..249833a 100644 --- a/src/main/scala/bridges/typescript/TsTypeRenderer.scala +++ b/src/main/scala/bridges/typescript/TsTypeRenderer.scala @@ -1,22 +1,23 @@ package bridges.typescript -import bridges.core.{ DeclF, Renderer } import org.apache.commons.text.StringEscapeUtils.{ escapeJava => escape } -abstract class TsTypeRenderer(exportAll: Boolean) extends Renderer[TsType] { +abstract class TsTypeRenderer(exportAll: Boolean): import TsType._ - def render(decl: TsDecl): String = - decl match { - case DeclF(name, params, TsType.Struct(fields, rest)) => + def render(decls: List[Decl]): String = + decls.map(render).mkString("\n\n") + + def render(decl: Decl): String = + decl match + case Decl(name, params, TsType.Struct(fields, rest)) => s"${if (exportAll) "export interface" else "interface"} ${renderParams(name, params)} ${renderStructAsInterface(fields, rest)}" - case DeclF(name, params, tpe) => + case Decl(name, params, tpe) => s"${if (exportAll) "export type" else "type"} ${renderParams(name, params)} = ${renderType(tpe)};" - } def renderType(tpe: TsType): String = - tpe match { + tpe match case Ref(id, params) => renderRef(id, params) case Any => "any" case Str => "string" @@ -37,7 +38,6 @@ abstract class TsTypeRenderer(exportAll: Boolean) extends Renderer[TsType] { case Struct(fields, rest) => renderStruct(fields, rest) case tpe @ Inter(types) => types.map(renderParens(tpe)).mkString(" & ") case tpe @ Union(types) => types.map(renderParens(tpe)).mkString(" | ") - } private def renderParams(name: String, params: List[String]): String = if (params.isEmpty) name else params.mkString(s"$name<", ", ", ">") @@ -55,33 +55,29 @@ abstract class TsTypeRenderer(exportAll: Boolean) extends Renderer[TsType] { .mkString("{\n", "", "}") private def renderField(field: TsField): String = - field match { + field match case TsField(name, valueType, false) => - s"""${name}: ${renderType(valueType)}""" + s"""$name: ${renderType(valueType)}""" case TsField(name, valueType, true) => - s"""${name}?: ${renderType(valueType)}""" - } + s"""$name?: ${renderType(valueType)}""" private def renderArgs(args: List[(String, TsType)]): String = args - .map { case (name, tpe) => s"""${name}: ${renderType(tpe)}""" } + .map { case (name, tpe) => s"""$name: ${renderType(tpe)}""" } .mkString("(", ", ", ")") - private def renderRestField(field: TsRestField): String = { + private def renderRestField(field: TsRestField): String = val TsRestField(name, keyType, valueType) = field - s"""[${name}: ${renderType(keyType)}]: ${renderType(valueType)}""" - } + s"""[$name: ${renderType(keyType)}]: ${renderType(valueType)}""" private def renderParens(outer: TsType)(inner: TsType): String = - if (precedence(outer) > precedence(inner)) { - s"(${renderType(inner)})" - } else { - renderType(inner) - } + if precedence(outer) > precedence(inner) + then s"(${renderType(inner)})" + else renderType(inner) private def precedence(tpe: TsType): Int = - tpe match { + tpe match case _: Ref => 1000 case _ @Any => 1000 case _ @Unknown => 1000 @@ -102,5 +98,3 @@ abstract class TsTypeRenderer(exportAll: Boolean) extends Renderer[TsType] { case _: Union => 400 case _: Inter => 200 case _: Func => 100 - } -} diff --git a/src/main/scala/bridges/typescript/TypeName.scala b/src/main/scala/bridges/typescript/TypeName.scala new file mode 100644 index 0000000..ba7b91f --- /dev/null +++ b/src/main/scala/bridges/typescript/TypeName.scala @@ -0,0 +1,13 @@ +package bridges.typescript + +import scala.compiletime.* +import scala.quoted.* + +object TypeName: + inline given of[A]: String = + ${ nameImpl[A] } + + private def nameImpl[A: Type](using Quotes): Expr[String] = + import quotes.reflect.* + val typeRepr = TypeRepr.of[A] + Expr(typeRepr.show.split("\\.").last) diff --git a/src/main/scala/bridges/typescript/package.scala b/src/main/scala/bridges/typescript/package.scala deleted file mode 100644 index 2a8764a..0000000 --- a/src/main/scala/bridges/typescript/package.scala +++ /dev/null @@ -1,7 +0,0 @@ -package bridges - -import bridges.core.DeclF - -package object typescript { - type TsDecl = DeclF[TsType] -} diff --git a/src/main/scala/bridges/typescript/syntax.scala b/src/main/scala/bridges/typescript/syntax.scala new file mode 100644 index 0000000..dd3f084 --- /dev/null +++ b/src/main/scala/bridges/typescript/syntax.scala @@ -0,0 +1,16 @@ +package bridges.typescript + +object syntax: + inline def decl[A](using enc: AdtEncoder[A]): Decl = + lazy val name = TypeName.of[A] + Decl(name, enc.encode) + + def decl(name: String, params: String*)(tpe: TsType): Decl = + Decl(name, params.toList, tpe) + + extension (name: String) + def -->(tpe: TsType): TsField = + TsField(name, tpe) + + def -?>(tpe: TsType): TsField = + TsField(name, tpe, true) diff --git a/src/test/scala/bridges/core/EncoderSpec.scala b/src/test/scala/bridges/core/EncoderSpec.scala deleted file mode 100644 index 3871f7f..0000000 --- a/src/test/scala/bridges/core/EncoderSpec.scala +++ /dev/null @@ -1,347 +0,0 @@ -package bridges.core - -import bridges.SampleTypes._ -import bridges.core.Type._ -import bridges.core.syntax._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class EncoderSpec extends AnyFreeSpec with Matchers { - "encode[A]" - { - "primitive types" in { - encode[String] should be(Str) - encode[Char] should be(Chr) - encode[Int] should be(Intr) - encode[Float] should be(Real) - encode[Double] should be(Real) - encode[Boolean] should be(Bool) - } - - "options" in { - encode[Option[String]] should be(Opt(Str)) - encode[Option[Int]] should be(Opt(Intr)) - } - - "sequences" in { - encode[Seq[String]] should be(Arr(Str)) - encode[Set[Set[Int]]] should be(Arr(Arr(Intr))) - } - - "value classes" in { - encode[Value] should be(Str) - } - - "a class with UUID member" in { - encode[ClassUUID] should be( - prod( - "a" -> Ref("UUID") - ) - ) - } - - "a class with Date member" in { - encode[ClassDate] should be( - prod( - "a" -> Ref("Date") - ) - ) - } - - "case classes" in { - encode[Pair] should be( - prod( - "a" -> Str, - "b" -> Intr - ) - ) - } - - "sealed types" in { - encode[OneOrOther] should be( - sum( - "One" -> prod("value" -> Str), - "Other" -> prod("value" -> Intr) - ) - ) - } - - "sealed types with objects" in { - encode[ClassOrObject] should be( - sum( - "MyClass" -> prod("value" -> Intr), - "MyObject" -> prod() - ) - ) - } - - "sealed types with objects in nested objects" in { - encode[NestedClassOrObject] should be( - sum( - "MyClass" -> prod("value" -> Intr), - "MyObject" -> prod() - ) - ) - } - - "overridden defaults" in { - implicit val oneEncoder: BasicEncoder[One] = - Encoder.pure(Str) - - encode[One] should be(Str) - - encode[OneOrOther] should be( - sum( - "One" -> prod("value" -> Str), - "Other" -> prod("value" -> Intr) - ) - ) - } - - "sealed types with intermediate types and indirect recursion" in { - encode[Shape] should be( - sum( - "Circle" -> prod( - "radius" -> Real, - "color" -> Ref("Color") - ), - "Rectangle" -> prod( - "width" -> Real, - "height" -> Real, - "color" -> Ref("Color") - ), - "ShapeGroup" -> prod( - "leftShape" -> Ref("Shape"), - "rightShape" -> Ref("Shape") - ) - ) - ) - - encode[Circle] should be( - prod( - "radius" -> Real, - "color" -> Ref("Color") - ) - ) - - encode[Rectangle] should be( - prod( - "width" -> Real, - "height" -> Real, - "color" -> Ref("Color") - ) - ) - - encode[ShapeGroup] should be( - prod( - "leftShape" -> Ref("Shape"), - "rightShape" -> Ref("Shape") - ) - ) - } - - "recursive types with direct recursion on same type" in { - encode[Navigation] should be( - sum( - "Node" -> prod( - "name" -> Str, - "children" -> Arr(Ref("Navigation")) - ), - "NodeList" -> prod( - "all" -> Arr(Ref("Navigation")) - ) - ) - ) - - encode[NodeList] should be( - prod( - "all" -> Arr(Ref("Navigation")) - ) - ) - - encode[Node] should be( - prod( - "name" -> Str, - "children" -> Arr(Ref("Navigation")) - ) - ) - } - - "types with specific parameters" in { - encode[Alpha] should be( - prod( - "name" -> Str, - "char" -> Chr, - "bool" -> Bool - ) - ) - - encode[ArrayClass] should be( - prod( - "aList" -> Arr(Str), - "optField" -> Opt(Real) - ) - ) - encode[Numeric] should be( - prod( - "double" -> Real, - "float" -> Real, - "int" -> Intr - ) - ) - } - - "class that references other case classes" in { - encode[ExternalReferences] should be( - prod( - "color" -> Ref("Color"), - "nav" -> Ref("Navigation") - ) - ) - } - - "mutually recursive types" in { - encode[TypeOne] should be( - prod( - "name" -> Str, - "values" -> Arr(Ref("TypeTwo")) - ) - ) - - encode[TypeTwo] should be( - sum( - "OptionOne" -> prod("value" -> Intr), - "OptionTwo" -> prod("value" -> Ref("TypeOne")) - ) - ) - } - - "self-recursive type" in { - encode[Recursive] should be( - prod( - "head" -> Intr, - "tail" -> Opt(Ref("Recursive")) - ) - ) - - encode[Recursive2] should be( - prod( - "head" -> Intr, - "tail" -> Arr(Ref("Recursive2")) - ) - ) - } - - "pure objects ADT" in { - encode[ObjectsOnly] should be( - sum( - "ObjectOne" -> prod(), - "ObjectTwo" -> prod() - ) - ) - } - - "refined types and class containing them" in { - encode[RefinedString] should be(Str) - encode[RefinedInt] should be(Intr) - encode[RefinedChar] should be(Chr) - - // Note that the import is required or it fails! - // import eu.timepit.refined.shapeless.typeable._ - - encode[ClassWithRefinedType] should be( - prod("name" -> Str) - ) - } - - "we can override uuid as string" in { - implicit val uuidEncoder: BasicEncoder[java.util.UUID] = - Encoder.pure(Str) - - encode[ClassUUID] should be( - prod("a" -> Str) - ) - } - } - - "decl[A]" - { - "value classes" in { - decl[Value] should be( - "Value" := Str - ) - } - - "case classes" in { - decl[Pair] should be( - "Pair" := prod( - "a" -> Str, - "b" -> Intr - ) - ) - } - - "sealed types" in { - decl[OneOrOther] should be( - "OneOrOther" := sum( - "One" -> prod("value" -> Str), - "Other" -> prod("value" -> Intr) - ) - ) - } - - "overridden defaults" in { - implicit val oneEncoder: BasicEncoder[One] = - Encoder.pure(Str) - - encode[One] should be(Str) - - decl[OneOrOther] should be( - "OneOrOther" := sum( - "One" -> prod( - "value" -> Str - ), - "Other" -> prod( - "value" -> Intr - ) - ) - ) - } - - "class with refined type" in { - // Note that the import is required or it fails! - // import eu.timepit.refined.shapeless.typeable._ - - decl[ClassWithRefinedType] should be( - "ClassWithRefinedType" := prod( - "name" -> Str - ) - ) - } - } - - "Numeric types" in { - decl[NumericTypes] shouldBe { - decl("NumericTypes")( - prod( - "int" -> Intr, - "long" -> Intr, - "float" -> Real, - "double" -> Real, - "bigDecimal" -> Real - ) - ) - } - } - - "Map" in { - decl[Map[String, Int]] shouldBe decl("Map")(dict(Str, Intr)) - decl[Map[String, Pair]] shouldBe decl("Map")( - dict( - Str, - prod( - "a" -> Str, - "b" -> Intr - ) - ) - ) - } -} diff --git a/src/test/scala/bridges/core/TypeSpec.scala b/src/test/scala/bridges/core/TypeSpec.scala deleted file mode 100644 index 520ceac..0000000 --- a/src/test/scala/bridges/core/TypeSpec.scala +++ /dev/null @@ -1,121 +0,0 @@ -package bridges.core - -import bridges.core.syntax._ -import org.scalatest._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class TypeSpec extends AnyFreeSpec with Matchers { - import Type._ - - def t[A <: Type](a: A): Type = a - - "type.rename" - { - "matching Ref" in { - val actual = t(Ref("foo")).rename("foo", "bar") - val expected = t(Ref("bar")) - actual should equal(expected) - } - - "non-matching Ref" in { - val actual = t(Ref("baz")).rename("foo", "bar") - val expected = t(Ref("baz")) - actual should equal(expected) - } - - "Optional" in { - val actual = t(Opt(Ref("foo"))).rename("foo", "bar") - val expected = t(Opt(Ref("bar"))) - actual should equal(expected) - } - - "Array" in { - val actual = t(Arr(Ref("foo"))).rename("foo", "bar") - val expected = t(Arr(Ref("bar"))) - actual should equal(expected) - } - - "Prod" in { - val actual = t( - prod( - "a" -> Ref("foo"), - "b" -> Ref("baz") - ) - ).rename("foo", "bar") - - val expected = t( - prod( - "a" -> Ref("bar"), - "b" -> Ref("baz") - ) - ) - - actual should equal(expected) - } - - "Sum" - { - "renames members " in { - val actual = t( - sum( - "typeA" -> prod( - "a" -> Ref("foo"), - "b" -> Ref("baz") - ), - "typeB" -> prod( - "a" -> Ref("foo"), - "b" -> Ref("baz") - ) - ) - ).rename("foo", "bar") - - val expected = t( - sum( - "typeA" -> Prod( - List( - "a" -> Ref("bar"), - "b" -> Ref("baz") - ) - ), - "typeB" -> Prod( - List( - "a" -> Ref("bar"), - "b" -> Ref("baz") - ) - ) - ) - ) - - actual should equal(expected) - } - "renames type name of members" in { - val actual = t( - sum( - "typeA" -> prod( - "a" -> Ref("foo"), - "b" -> Ref("baz") - ), - "typeA" -> prod( - "a" -> Ref("foo"), - "b" -> Ref("baz") - ) - ) - ).rename("typeA", "typeC") - - val expected = t( - sum( - "typeC" -> prod( - "a" -> Ref("foo"), - "b" -> Ref("baz") - ), - "typeC" -> prod( - "a" -> Ref("foo"), - "b" -> Ref("baz") - ) - ) - ) - - actual should equal(expected) - } - } - } -} diff --git a/src/test/scala/bridges/elm/ElmFileBuilderSpec.scala b/src/test/scala/bridges/elm/ElmFileBuilderSpec.scala deleted file mode 100644 index 12e96c5..0000000 --- a/src/test/scala/bridges/elm/ElmFileBuilderSpec.scala +++ /dev/null @@ -1,353 +0,0 @@ -package bridges.elm - -import bridges.SampleTypes._ -import bridges.core.Type._ -import bridges.core.syntax._ -import org.scalatest._ -import unindent._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class ElmFileBuilderSpec extends AnyFreeSpec with Matchers { - "for a single case class" in { - val fileContent = - i""" - module CustomModule.Color exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - - - type alias Color = { red: Int, green: Int, blue: Int } - - - decoderColor : Decode.Decoder Color - decoderColor = Decode.succeed Color |> required "red" Decode.int |> required "green" Decode.int |> required "blue" Decode.int - - - encoderColor : Color -> Encode.Value - encoderColor obj = Encode.object [ ("red", Encode.int obj.red), ("green", Encode.int obj.green), ("blue", Encode.int obj.blue) ] - """ - val expected = ("Color.elm", fileContent) - - Elm.buildFile("CustomModule", decl[Color]) shouldBe expected - } - - "for a single case class with complex types" in { - val fileContent = - i""" - module CustomModule.ExternalReferences exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - import CustomModule.Color exposing (..) - import CustomModule.Navigation exposing (..) - - type alias ExternalReferences = { color: Color, nav: Navigation } - - - decoderExternalReferences : Decode.Decoder ExternalReferences - decoderExternalReferences = Decode.succeed ExternalReferences |> required "color" (Decode.lazy (\\_ -> decoderColor)) |> required "nav" (Decode.lazy (\\_ -> decoderNavigation)) - - - encoderExternalReferences : ExternalReferences -> Encode.Value - encoderExternalReferences obj = Encode.object [ ("color", encoderColor obj.color), ("nav", encoderNavigation obj.nav) ] - """ - val expected = ("ExternalReferences.elm", fileContent) - - Elm.buildFile("CustomModule", decl[ExternalReferences]) shouldBe expected - } - - "for a trait" in { - val fileContent = - i""" - module CustomModule.Shape exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - import CustomModule.Color exposing (..) - - type Shape = Circle Float Color | Rectangle Float Float Color | ShapeGroup Shape Shape - - - decoderShape : Decode.Decoder Shape - decoderShape = Decode.field "type" Decode.string |> Decode.andThen decoderShapeTpe - - decoderShapeTpe : String -> Decode.Decoder Shape - decoderShapeTpe tpe = - case tpe of - "Circle" -> Decode.succeed Circle |> required "radius" Decode.float |> required "color" (Decode.lazy (\\_ -> decoderColor)) - "Rectangle" -> Decode.succeed Rectangle |> required "width" Decode.float |> required "height" Decode.float |> required "color" (Decode.lazy (\\_ -> decoderColor)) - "ShapeGroup" -> Decode.succeed ShapeGroup |> required "leftShape" (Decode.lazy (\\_ -> decoderShape)) |> required "rightShape" (Decode.lazy (\\_ -> decoderShape)) - _ -> Decode.fail ("Unexpected type for Shape: " ++ tpe) - - - encoderShape : Shape -> Encode.Value - encoderShape tpe = - case tpe of - Circle radius color -> Encode.object [ ("radius", Encode.float radius), ("color", encoderColor color), ("type", Encode.string "Circle") ] - Rectangle width height color -> Encode.object [ ("width", Encode.float width), ("height", Encode.float height), ("color", encoderColor color), ("type", Encode.string "Rectangle") ] - ShapeGroup leftShape rightShape -> Encode.object [ ("leftShape", encoderShape leftShape), ("rightShape", encoderShape rightShape), ("type", Encode.string "ShapeGroup") ] - """ - val expected = ("Shape.elm", fileContent) - - Elm.buildFile("CustomModule", decl[Shape]) shouldBe expected - } - - "for a recursive trait" in { - val fileContent = - i""" - module CustomModule.Navigation exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - - - type Navigation = Node String (List Navigation) | NodeList (List Navigation) - - - decoderNavigation : Decode.Decoder Navigation - decoderNavigation = Decode.field "type" Decode.string |> Decode.andThen decoderNavigationTpe - - decoderNavigationTpe : String -> Decode.Decoder Navigation - decoderNavigationTpe tpe = - case tpe of - "Node" -> Decode.succeed Node |> required "name" Decode.string |> required "children" (Decode.list (Decode.lazy (\\_ -> decoderNavigation))) - "NodeList" -> Decode.succeed NodeList |> required "all" (Decode.list (Decode.lazy (\\_ -> decoderNavigation))) - _ -> Decode.fail ("Unexpected type for Navigation: " ++ tpe) - - - encoderNavigation : Navigation -> Encode.Value - encoderNavigation tpe = - case tpe of - Node name children -> Encode.object [ ("name", Encode.string name), ("children", Encode.list encoderNavigation children), ("type", Encode.string "Node") ] - NodeList all -> Encode.object [ ("all", Encode.list encoderNavigation all), ("type", Encode.string "NodeList") ] - """ - val expected = ("Navigation.elm", fileContent) - - Elm.buildFile("CustomModule", decl[Navigation]) shouldBe expected - } - - "UUID" - { - "without overrides" in { - val fileContent = - i""" - module CustomModule2.ClassUUID exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - import CustomModule2.UUID exposing (..) - - type alias ClassUUID = { a: UUID } - - - decoderClassUUID : Decode.Decoder ClassUUID - decoderClassUUID = Decode.succeed ClassUUID |> required "a" (Decode.lazy (\\_ -> decoderUUID)) - - - encoderClassUUID : ClassUUID -> Encode.Value - encoderClassUUID obj = Encode.object [ ("a", encoderUUID obj.a) ] - """ - val expected = ("ClassUUID.elm", fileContent) - - Elm.buildFile("CustomModule2", decl[ClassUUID]) shouldBe expected - } - - "with overrides" in { - val customTypeReplacements: Map[Ref, TypeReplacement] = Map( - Ref("UUID") -> TypeReplacement("Uuid", "import Uuid exposing (Uuid)", "Uuid.decoder", "Uuid.encode") - ) - - val fileContent = - i""" - module CustomModule2.ClassUUID exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - import Uuid exposing (Uuid) - - type alias ClassUUID = { a: Uuid } - - - decoderClassUUID : Decode.Decoder ClassUUID - decoderClassUUID = Decode.succeed ClassUUID |> required "a" Uuid.decoder - - - encoderClassUUID : ClassUUID -> Encode.Value - encoderClassUUID obj = Encode.object [ ("a", Uuid.encode obj.a) ] - """ - val expected = ("ClassUUID.elm", fileContent) - - Elm.buildFile("CustomModule2", decl[ClassUUID], customTypeReplacements) shouldBe expected - } - } - - "objects only" in { - val fileContent = - i""" - module CustomModule2.ObjectsOnly exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - - - type ObjectsOnly = ObjectOne | ObjectTwo - - - decoderObjectsOnly : Decode.Decoder ObjectsOnly - decoderObjectsOnly = Decode.field "type" Decode.string |> Decode.andThen decoderObjectsOnlyTpe - - decoderObjectsOnlyTpe : String -> Decode.Decoder ObjectsOnly - decoderObjectsOnlyTpe tpe = - case tpe of - "ObjectOne" -> Decode.succeed ObjectOne - "ObjectTwo" -> Decode.succeed ObjectTwo - _ -> Decode.fail ("Unexpected type for ObjectsOnly: " ++ tpe) - - - encoderObjectsOnly : ObjectsOnly -> Encode.Value - encoderObjectsOnly tpe = - case tpe of - ObjectOne -> Encode.object [ ("type", Encode.string "ObjectOne") ] - ObjectTwo -> Encode.object [ ("type", Encode.string "ObjectTwo") ] - """ - val expected = ("ObjectsOnly.elm", fileContent) - - Elm.buildFile("CustomModule2", decl[ObjectsOnly]) shouldBe expected - } - - "for several classes at once" - { - "without overrides" in { - val fileContent = - i""" - module CustomModule2.Color exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - import CustomModule2.UUID exposing (..) - - type alias Color = { red: Int, green: Int, blue: Int } - type alias ClassUUID = { a: UUID } - - - decoderColor : Decode.Decoder Color - decoderColor = Decode.succeed Color |> required "red" Decode.int |> required "green" Decode.int |> required "blue" Decode.int - - decoderClassUUID : Decode.Decoder ClassUUID - decoderClassUUID = Decode.succeed ClassUUID |> required "a" (Decode.lazy (\\_ -> decoderUUID)) - - - encoderColor : Color -> Encode.Value - encoderColor obj = Encode.object [ ("red", Encode.int obj.red), ("green", Encode.int obj.green), ("blue", Encode.int obj.blue) ] - - encoderClassUUID : ClassUUID -> Encode.Value - encoderClassUUID obj = Encode.object [ ("a", encoderUUID obj.a) ] - """ - - val expected = ("Color.elm", fileContent) - - Elm.buildFile( - "CustomModule2", - List(decl[Color], decl[ClassUUID]), - Map.empty[Ref, TypeReplacement] - ) shouldBe expected - } - - "with custom mappings" in { - - val customTypeReplacements: Map[Ref, TypeReplacement] = Map( - Ref("UUID") -> TypeReplacement("Uuid", "import Uuid exposing (Uuid)", "Uuid.decoder", "Uuid.encode") - ) - - val fileContent = - i""" - module CustomModule2.Color exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - import Uuid exposing (Uuid) - - type alias Color = { red: Int, green: Int, blue: Int } - type alias ClassUUID = { a: Uuid } - - - decoderColor : Decode.Decoder Color - decoderColor = Decode.succeed Color |> required "red" Decode.int |> required "green" Decode.int |> required "blue" Decode.int - - decoderClassUUID : Decode.Decoder ClassUUID - decoderClassUUID = Decode.succeed ClassUUID |> required "a" Uuid.decoder - - - encoderColor : Color -> Encode.Value - encoderColor obj = Encode.object [ ("red", Encode.int obj.red), ("green", Encode.int obj.green), ("blue", Encode.int obj.blue) ] - - encoderClassUUID : ClassUUID -> Encode.Value - encoderClassUUID obj = Encode.object [ ("a", Uuid.encode obj.a) ] - """ - - val expected = ("Color.elm", fileContent) - - Elm.buildFile( - "CustomModule2", - List(decl[Color], decl[ClassUUID]), - customTypeReplacements - ) shouldBe expected - } - } - - "for mutually recursive classes" in { - val fileContent = - i""" - module CustomModule2.TypeOne exposing (..) - - import Json.Decode as Decode - import Json.Decode.Pipeline exposing (..) - import Json.Encode as Encode - - - type alias TypeOne = { name: String, values: (List TypeTwo) } - type TypeTwo = OptionOne Int | OptionTwo TypeOne - - - decoderTypeOne : Decode.Decoder TypeOne - decoderTypeOne = Decode.succeed TypeOne |> required "name" Decode.string |> required "values" (Decode.list (Decode.lazy (\\_ -> decoderTypeTwo))) - - decoderTypeTwo : Decode.Decoder TypeTwo - decoderTypeTwo = Decode.field "type" Decode.string |> Decode.andThen decoderTypeTwoTpe - - decoderTypeTwoTpe : String -> Decode.Decoder TypeTwo - decoderTypeTwoTpe tpe = - case tpe of - "OptionOne" -> Decode.succeed OptionOne |> required "value" Decode.int - "OptionTwo" -> Decode.succeed OptionTwo |> required "value" (Decode.lazy (\\_ -> decoderTypeOne)) - _ -> Decode.fail ("Unexpected type for TypeTwo: " ++ tpe) - - - encoderTypeOne : TypeOne -> Encode.Value - encoderTypeOne obj = Encode.object [ ("name", Encode.string obj.name), ("values", Encode.list encoderTypeTwo obj.values) ] - - encoderTypeTwo : TypeTwo -> Encode.Value - encoderTypeTwo tpe = - case tpe of - OptionOne value -> Encode.object [ ("value", Encode.int value), ("type", Encode.string "OptionOne") ] - OptionTwo value -> Encode.object [ ("value", encoderTypeOne value), ("type", Encode.string "OptionTwo") ] - """ - - val expected = ("TypeOne.elm", fileContent) - - Elm.buildFile( - "CustomModule2", - List(decl[TypeOne], decl[TypeTwo]), - Map.empty[Ref, TypeReplacement] - ) shouldBe expected - } -} diff --git a/src/test/scala/bridges/elm/ElmJsonDecoderSpec.scala b/src/test/scala/bridges/elm/ElmJsonDecoderSpec.scala deleted file mode 100644 index c063174..0000000 --- a/src/test/scala/bridges/elm/ElmJsonDecoderSpec.scala +++ /dev/null @@ -1,324 +0,0 @@ -package bridges.elm - -import bridges.SampleTypes._ -import bridges.core.Type._ -import bridges.core._ -import bridges.core.syntax._ -import org.scalatest._ -import unindent._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class ElmJsonDecoderSpec extends AnyFreeSpec with Matchers { - "Color" in { - Elm.decoder(decl[Color]) shouldBe { - i""" - decoderColor : Decode.Decoder Color - decoderColor = Decode.succeed Color |> required "red" Decode.int |> required "green" Decode.int |> required "blue" Decode.int - """ - } - } - - "Circle" in { - Elm.decoder(decl[Circle]) shouldBe { - i""" - decoderCircle : Decode.Decoder Circle - decoderCircle = Decode.succeed Circle |> required "radius" Decode.float |> required "color" (Decode.lazy (\\_ -> decoderColor)) - """ - } - } - - "Rectangle" in { - Elm.decoder(decl[Rectangle]) shouldBe { - i""" - decoderRectangle : Decode.Decoder Rectangle - decoderRectangle = Decode.succeed Rectangle |> required "width" Decode.float |> required "height" Decode.float |> required "color" (Decode.lazy (\\_ -> decoderColor)) - """ - } - } - - "Shape" in { - Elm.decoder(decl[Shape]) shouldBe { - i""" - decoderShape : Decode.Decoder Shape - decoderShape = Decode.field "type" Decode.string |> Decode.andThen decoderShapeTpe - - decoderShapeTpe : String -> Decode.Decoder Shape - decoderShapeTpe tpe = - case tpe of - "Circle" -> Decode.succeed Circle |> required "radius" Decode.float |> required "color" (Decode.lazy (\\_ -> decoderColor)) - "Rectangle" -> Decode.succeed Rectangle |> required "width" Decode.float |> required "height" Decode.float |> required "color" (Decode.lazy (\\_ -> decoderColor)) - "ShapeGroup" -> Decode.succeed ShapeGroup |> required "leftShape" (Decode.lazy (\\_ -> decoderShape)) |> required "rightShape" (Decode.lazy (\\_ -> decoderShape)) - _ -> Decode.fail ("Unexpected type for Shape: " ++ tpe) - """ - } - } - - "Alpha" in { - Elm.decoder(decl[Alpha]) shouldBe { - i""" - decoderAlpha : Decode.Decoder Alpha - decoderAlpha = Decode.succeed Alpha |> required "name" Decode.string |> required "char" Decode.string |> required "bool" Decode.bool - """ - } - } - - "ArrayClass" in { - Elm.decoder(decl[ArrayClass]) shouldBe { - i""" - decoderArrayClass : Decode.Decoder ArrayClass - decoderArrayClass = Decode.succeed ArrayClass |> required "aList" (Decode.list Decode.string) |> optional "optField" (Decode.maybe Decode.float) Nothing - """ - } - } - - "Numeric" in { - Elm.decoder(decl[Numeric]) shouldBe { - i""" - decoderNumeric : Decode.Decoder Numeric - decoderNumeric = Decode.succeed Numeric |> required "double" Decode.float |> required "float" Decode.float |> required "int" Decode.int - """ - } - } - - "ClassOrObject" in { - Elm.decoder(decl[ClassOrObject]) shouldBe { - i""" - decoderClassOrObject : Decode.Decoder ClassOrObject - decoderClassOrObject = Decode.field "type" Decode.string |> Decode.andThen decoderClassOrObjectTpe - - decoderClassOrObjectTpe : String -> Decode.Decoder ClassOrObject - decoderClassOrObjectTpe tpe = - case tpe of - "MyClass" -> Decode.succeed MyClass |> required "value" Decode.int - "MyObject" -> Decode.succeed MyObject - _ -> Decode.fail ("Unexpected type for ClassOrObject: " ++ tpe) - """ - } - } - - "Navigation" in { - Elm.decoder(decl[Navigation]) shouldBe { - i""" - decoderNavigation : Decode.Decoder Navigation - decoderNavigation = Decode.field "type" Decode.string |> Decode.andThen decoderNavigationTpe - - decoderNavigationTpe : String -> Decode.Decoder Navigation - decoderNavigationTpe tpe = - case tpe of - "Node" -> Decode.succeed Node |> required "name" Decode.string |> required "children" (Decode.list (Decode.lazy (\\_ -> decoderNavigation))) - "NodeList" -> Decode.succeed NodeList |> required "all" (Decode.list (Decode.lazy (\\_ -> decoderNavigation))) - _ -> Decode.fail ("Unexpected type for Navigation: " ++ tpe) - """ - } - } - - "ExternalReferences" in { - Elm.decoder(decl[ExternalReferences]) shouldBe { - i""" - decoderExternalReferences : Decode.Decoder ExternalReferences - decoderExternalReferences = Decode.succeed ExternalReferences |> required "color" (Decode.lazy (\\_ -> decoderColor)) |> required "nav" (Decode.lazy (\\_ -> decoderNavigation)) - """ - } - } - - "TypeOne and TypeTwo" in { - Elm.decoder(List(decl[TypeOne], decl[TypeTwo]), Map.empty[Ref, TypeReplacement]) shouldBe { - i""" - decoderTypeOne : Decode.Decoder TypeOne - decoderTypeOne = Decode.succeed TypeOne |> required "name" Decode.string |> required "values" (Decode.list (Decode.lazy (\\_ -> decoderTypeTwo))) - - decoderTypeTwo : Decode.Decoder TypeTwo - decoderTypeTwo = Decode.field "type" Decode.string |> Decode.andThen decoderTypeTwoTpe - - decoderTypeTwoTpe : String -> Decode.Decoder TypeTwo - decoderTypeTwoTpe tpe = - case tpe of - "OptionOne" -> Decode.succeed OptionOne |> required "value" Decode.int - "OptionTwo" -> Decode.succeed OptionTwo |> required "value" (Decode.lazy (\\_ -> decoderTypeOne)) - _ -> Decode.fail ("Unexpected type for TypeTwo: " ++ tpe) - """ - } - } - - "ClassUUID" - { - "by default we treat UUID as a normal type we created" in { - // this is the case when we don't import any Elm specific UUID library and we will create our own UUID type there - - Elm.decoder(decl[ClassUUID]) shouldBe { - i""" - decoderClassUUID : Decode.Decoder ClassUUID - decoderClassUUID = Decode.succeed ClassUUID |> required "a" (Decode.lazy (\\_ -> decoderUUID)) - """ - } - } - - "we can provide a map to substitute UUID decoding with a custom decoding logic" in { - // this is the case when we import Elm specific UUID types we want to use in our decoder, but Scala can't know about them without extra hints - val customTypeReplacements: Map[Ref, TypeReplacement] = Map( - Ref("UUID") -> TypeReplacement("Uuid", "import Uuid exposing (Uuid)", "Uuid.decoder", "Uuid.encode") - ) - - Elm.decoder(decl[ClassUUID], customTypeReplacements) shouldBe { - i""" - decoderClassUUID : Decode.Decoder ClassUUID - decoderClassUUID = Decode.succeed ClassUUID |> required "a" Uuid.decoder - """ - } - } - - "we can override the Encoder so we treat UUID as another basic type, like String, and Decode.succeed it accordingly" in { - // probably not recommended, better to use a mapping as in other tests, but it is supported - implicit val uuidEncoder: BasicEncoder[java.util.UUID] = - Encoder.pure(Str) - - Elm.decoder(decl[ClassUUID]) shouldBe { - i""" - decoderClassUUID : Decode.Decoder ClassUUID - decoderClassUUID = Decode.succeed ClassUUID |> required "a" Decode.string - """ - } - } - } - - "ClassDate" - { - "by default we treat Date as a normal type we created" in { - // this is the case when we don't import any Elm specific Date library and we will create our own Date type there - - Elm.decoder(decl[ClassDate]) shouldBe { - i""" - decoderClassDate : Decode.Decoder ClassDate - decoderClassDate = Decode.succeed ClassDate |> required "a" (Decode.lazy (\\_ -> decoderDate)) - """ - } - } - - "we can provide a map to substitute Date decoding with a custom decoding logic" in { - // this is the case when we import Elm specific UUID types we want to use in our decoder, but Scala can't know about them without extra hints - val customTypeReplacements: Map[Ref, TypeReplacement] = Map( - Ref("Date") -> TypeReplacement("Date", "import Date exposing (Date)", "Date.decoder", "Date.encode") - ) - - Elm.decoder(decl[ClassDate], customTypeReplacements) shouldBe { - i""" - decoderClassDate : Decode.Decoder ClassDate - decoderClassDate = Decode.succeed ClassDate |> required "a" Date.decoder - """ - } - } - - "we can override the Encoder so we treat Date as another basic type, like String, and Decode.succeed it accordingly" in { - // probably not recommended, better to use a mapping as in other tests, but it is supported - implicit val dateEncoder: BasicEncoder[java.util.Date] = - Encoder.pure(Str) - - Elm.decoder(decl[ClassDate]) shouldBe { - i""" - decoderClassDate : Decode.Decoder ClassDate - decoderClassDate = Decode.succeed ClassDate |> required "a" Decode.string - """ - } - } - } - - "Recursive" in { - Elm.decoder(decl[Recursive]) shouldBe - i""" - decoderRecursive : Decode.Decoder Recursive - decoderRecursive = Decode.succeed Recursive |> required "head" Decode.int |> optional "tail" (Decode.maybe (Decode.lazy (\\_ -> decoderRecursive))) Nothing - """ - } - - "Recursive2" in { - Elm.decoder(decl[Recursive2]) shouldBe { - i""" - decoderRecursive2 : Decode.Decoder Recursive2 - decoderRecursive2 = Decode.succeed Recursive2 |> required "head" Decode.int |> required "tail" (Decode.list (Decode.lazy (\\_ -> decoderRecursive2))) - """ - } - } - - "ObjectsOnly" in { - Elm.decoder(decl[ObjectsOnly]) shouldBe { - i""" - decoderObjectsOnly : Decode.Decoder ObjectsOnly - decoderObjectsOnly = Decode.field "type" Decode.string |> Decode.andThen decoderObjectsOnlyTpe - - decoderObjectsOnlyTpe : String -> Decode.Decoder ObjectsOnly - decoderObjectsOnlyTpe tpe = - case tpe of - "ObjectOne" -> Decode.succeed ObjectOne - "ObjectTwo" -> Decode.succeed ObjectTwo - _ -> Decode.fail ("Unexpected type for ObjectsOnly: " ++ tpe) - """ - } - } - - "ClassWithGeneric" in { - val productDef = prod("first" -> Ref("A"), "second" -> Ref("B"), "third" -> Ref("C")) - val declaration = decl("ClassWithGeneric", "A", "B", "C")(productDef) - Elm.decoder(declaration) shouldBe { - i""" - decoderClassWithGeneric : (Decode.Decoder a) -> (Decode.Decoder b) -> (Decode.Decoder c) -> Decode.Decoder (ClassWithGeneric a b c) - decoderClassWithGeneric decoderA decoderB decoderC = Decode.succeed ClassWithGeneric |> required "first" (Decode.lazy (\\_ -> decoderA)) |> required "second" (Decode.lazy (\\_ -> decoderB)) |> required "third" (Decode.lazy (\\_ -> decoderC)) - """ - } - } - - "ClassWithGeneric2" in { - val productDef = prod("first" -> Ref("A")) - val declaration = decl("ClassWithGeneric2", "A")(productDef) - Elm.decoder(declaration) shouldBe { - i""" - decoderClassWithGeneric2 : (Decode.Decoder a) -> Decode.Decoder (ClassWithGeneric2 a) - decoderClassWithGeneric2 decoderA = Decode.succeed ClassWithGeneric2 |> required "first" (Decode.lazy (\\_ -> decoderA)) - """ - } - } - - "SumWithGeneric" in { - val sumDef = sum( - "First" -> prod("f" -> Ref("A")), - "Second" -> prod("s" -> Ref("B")), - "Third" -> prod("t" -> Ref("C")) - ) - val declaration = decl("SumWithGeneric", "A", "B", "C")(sumDef) - Elm.decoder(declaration) shouldBe { - i""" - decoderSumWithGeneric : (Decode.Decoder a) -> (Decode.Decoder b) -> (Decode.Decoder c) -> Decode.Decoder (SumWithGeneric a b c) - decoderSumWithGeneric decoderA decoderB decoderC = Decode.field "type" Decode.string |> Decode.andThen decoderSumWithGenericTpe decoderA decoderB decoderC - - decoderSumWithGenericTpe : (Decode.Decoder a) -> (Decode.Decoder b) -> (Decode.Decoder c) -> String -> Decode.Decoder (SumWithGeneric a b c) - decoderSumWithGenericTpe decoderA decoderB decoderC tpe = - case tpe of - "First" -> Decode.succeed First |> required "f" (Decode.lazy (\\_ -> decoderA)) - "Second" -> Decode.succeed Second |> required "s" (Decode.lazy (\\_ -> decoderB)) - "Third" -> Decode.succeed Third |> required "t" (Decode.lazy (\\_ -> decoderC)) - _ -> Decode.fail ("Unexpected type for SumWithGeneric: " ++ tpe) - """ - } - } - - "Numeric types" in { - Elm.decoder(decl[NumericTypes]) shouldBe { - i""" - decoderNumericTypes : Decode.Decoder NumericTypes - decoderNumericTypes = Decode.succeed NumericTypes |> required "int" Decode.int |> required "long" Decode.int |> required "float" Decode.float |> required "double" Decode.float |> required "bigDecimal" Decode.float - """ - } - } - - "Dictionary types" in { - Elm.decoder(decl[Map[String, Int]]) shouldBe { - i""" - decoderMap : Decode.Decoder Map - decoderMap = Decode.succeed Map (Decode.dict Decode.int) - """ - } - - // The Elm standard library only provides JSON decoders for dictionaries with string keys: - intercept[IllegalArgumentException] { - Elm.decoder(decl[Map[Int, Int]]) - } - } -} diff --git a/src/test/scala/bridges/elm/ElmJsonEncoderSpec.scala b/src/test/scala/bridges/elm/ElmJsonEncoderSpec.scala deleted file mode 100644 index 5ab499c..0000000 --- a/src/test/scala/bridges/elm/ElmJsonEncoderSpec.scala +++ /dev/null @@ -1,295 +0,0 @@ -package bridges.elm - -import bridges.SampleTypes._ -import bridges.core._ -import bridges.core.Type._ -import bridges.core.syntax._ -import org.scalatest._ -import unindent._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class ElmJsonEncoderSpec extends AnyFreeSpec with Matchers { - "Color" in { - Elm.encoder(decl[Color]) shouldBe { - i""" - encoderColor : Color -> Encode.Value - encoderColor obj = Encode.object [ ("red", Encode.int obj.red), ("green", Encode.int obj.green), ("blue", Encode.int obj.blue) ] - """ - } - } - - "Circle" in { - Elm.encoder(decl[Circle]) shouldBe { - i""" - encoderCircle : Circle -> Encode.Value - encoderCircle obj = Encode.object [ ("radius", Encode.float obj.radius), ("color", encoderColor obj.color) ] - """ - } - } - - "Rectangle" in { - Elm.encoder(decl[Rectangle]) shouldBe { - i""" - encoderRectangle : Rectangle -> Encode.Value - encoderRectangle obj = Encode.object [ ("width", Encode.float obj.width), ("height", Encode.float obj.height), ("color", encoderColor obj.color) ] - """ - } - } - - "Shape" in { - Elm.encoder(decl[Shape]) shouldBe { - i""" - encoderShape : Shape -> Encode.Value - encoderShape tpe = - case tpe of - Circle radius color -> Encode.object [ ("radius", Encode.float radius), ("color", encoderColor color), ("type", Encode.string "Circle") ] - Rectangle width height color -> Encode.object [ ("width", Encode.float width), ("height", Encode.float height), ("color", encoderColor color), ("type", Encode.string "Rectangle") ] - ShapeGroup leftShape rightShape -> Encode.object [ ("leftShape", encoderShape leftShape), ("rightShape", encoderShape rightShape), ("type", Encode.string "ShapeGroup") ] - """ - } - } - - "Alpha" in { - Elm.encoder(decl[Alpha]) shouldBe { - i""" - encoderAlpha : Alpha -> Encode.Value - encoderAlpha obj = Encode.object [ ("name", Encode.string obj.name), ("char", Encode.string obj.char), ("bool", Encode.bool obj.bool) ] - """ - } - } - - "ArrayClass" in { - Elm.encoder(decl[ArrayClass]) shouldBe { - i""" - encoderArrayClass : ArrayClass -> Encode.Value - encoderArrayClass obj = Encode.object [ ("aList", Encode.list Encode.string obj.aList), ("optField", Maybe.withDefault Encode.null (Maybe.map Encode.float obj.optField)) ] - """ - } - } - - "Numeric" in { - Elm.encoder(decl[Numeric]) shouldBe { - i""" - encoderNumeric : Numeric -> Encode.Value - encoderNumeric obj = Encode.object [ ("double", Encode.float obj.double), ("float", Encode.float obj.float), ("int", Encode.int obj.int) ] - """ - } - } - - "ClassOrObject" in { - Elm.encoder(decl[ClassOrObject]) shouldBe - i""" - encoderClassOrObject : ClassOrObject -> Encode.Value - encoderClassOrObject tpe = - case tpe of - MyClass value -> Encode.object [ ("value", Encode.int value), ("type", Encode.string "MyClass") ] - MyObject -> Encode.object [ ("type", Encode.string "MyObject") ] - """ - } - - "Navigation" in { - Elm.encoder(decl[Navigation]) shouldBe { - i""" - encoderNavigation : Navigation -> Encode.Value - encoderNavigation tpe = - case tpe of - Node name children -> Encode.object [ ("name", Encode.string name), ("children", Encode.list encoderNavigation children), ("type", Encode.string "Node") ] - NodeList all -> Encode.object [ ("all", Encode.list encoderNavigation all), ("type", Encode.string "NodeList") ] - """ - } - } - - "ExternalReferences" in { - Elm.encoder(decl[ExternalReferences]) shouldBe { - i""" - encoderExternalReferences : ExternalReferences -> Encode.Value - encoderExternalReferences obj = Encode.object [ ("color", encoderColor obj.color), ("nav", encoderNavigation obj.nav) ] - """ - } - } - - "TypeOne and TypeTwo" in { - Elm.encoder(List(decl[TypeOne], decl[TypeTwo]), Map.empty[Ref, TypeReplacement]) shouldBe { - i""" - encoderTypeOne : TypeOne -> Encode.Value - encoderTypeOne obj = Encode.object [ ("name", Encode.string obj.name), ("values", Encode.list encoderTypeTwo obj.values) ] - - encoderTypeTwo : TypeTwo -> Encode.Value - encoderTypeTwo tpe = - case tpe of - OptionOne value -> Encode.object [ ("value", Encode.int value), ("type", Encode.string "OptionOne") ] - OptionTwo value -> Encode.object [ ("value", encoderTypeOne value), ("type", Encode.string "OptionTwo") ] - """ - } - } - - "ClassUUID" - { - "by default we treat UUID as a normal type we created" in { - // this is the case when we don't import any Elm specific UUID library and we will create our own UUID type there - - Elm.encoder(decl[ClassUUID]) shouldBe { - i""" - encoderClassUUID : ClassUUID -> Encode.Value - encoderClassUUID obj = Encode.object [ ("a", encoderUUID obj.a) ] - """ - } - } - - "we can provide a map to substitute UUID decoding with a custom encoding logic" in { - // this is the case when we import Elm specific UUID types we want to use in our decoder, but Scala can't know about them without extra hints - val customTypeReplacements: Map[Ref, TypeReplacement] = Map( - Ref("UUID") -> TypeReplacement("Uuid", "import Uuid exposing (Uuid)", "Uuid.decoder", "Uuid.encode") - ) - - Elm.encoder(decl[ClassUUID], customTypeReplacements) shouldBe { - i""" - encoderClassUUID : ClassUUID -> Encode.Value - encoderClassUUID obj = Encode.object [ ("a", Uuid.encode obj.a) ] - """ - } - } - - "we can override the Encoder so we treat UUID as another basic type, like String, and encode it accordingly" in { - // probably not recommended, better to use a mapping as in other tests, but it is supported - implicit val uuidEncoder: BasicEncoder[java.util.UUID] = - Encoder.pure(Str) - - Elm.encoder(decl[ClassUUID]) shouldBe { - i""" - encoderClassUUID : ClassUUID -> Encode.Value - encoderClassUUID obj = Encode.object [ ("a", Encode.string obj.a) ] - """ - } - } - } - - "ClassDate" - { - "by default we treat Date as a normal type we created" in { - // this is the case when we don't import any Elm specific Date library and we will create our own Date type there - - Elm.encoder(decl[ClassDate]) shouldBe { - i""" - encoderClassDate : ClassDate -> Encode.Value - encoderClassDate obj = Encode.object [ ("a", encoderDate obj.a) ] - """ - } - } - - "we can provide a map to substitute Date decoding with a custom encoding logic" in { - // this is the case when we import Elm specific Date types we want to use in our decoder, but Scala can't know about them without extra hints - val customTypeReplacements: Map[Ref, TypeReplacement] = Map( - Ref("Date") -> TypeReplacement("Date", "import Date exposing (Date)", "Date.decoder", "Date.encode") - ) - - Elm.encoder(decl[ClassDate], customTypeReplacements) shouldBe { - i""" - encoderClassDate : ClassDate -> Encode.Value - encoderClassDate obj = Encode.object [ ("a", Date.encode obj.a) ] - """ - } - } - - "we can override the Encoder so we treat Date as another basic type, like String, and encode it accordingly" in { - // probably not recommended, better to use a mapping as in other tests, but it is supported - implicit val dateEncoder: BasicEncoder[java.util.Date] = - Encoder.pure(Str) - - Elm.encoder(decl[ClassDate]) shouldBe { - i""" - encoderClassDate : ClassDate -> Encode.Value - encoderClassDate obj = Encode.object [ ("a", Encode.string obj.a) ] - """ - } - } - } - - "Recursive" in { - Elm.encoder(decl[Recursive]) shouldBe { - i""" - encoderRecursive : Recursive -> Encode.Value - encoderRecursive obj = Encode.object [ ("head", Encode.int obj.head), ("tail", Maybe.withDefault Encode.null (Maybe.map encoderRecursive obj.tail)) ] - """ - } - } - - "Recursive2" in { - Elm.encoder(decl[Recursive2]) shouldBe { - i""" - encoderRecursive2 : Recursive2 -> Encode.Value - encoderRecursive2 obj = Encode.object [ ("head", Encode.int obj.head), ("tail", Encode.list encoderRecursive2 obj.tail) ] - """ - } - } - - "ObjectsOnly" in { - Elm.encoder(decl[ObjectsOnly]) shouldBe { - i""" - encoderObjectsOnly : ObjectsOnly -> Encode.Value - encoderObjectsOnly tpe = - case tpe of - ObjectOne -> Encode.object [ ("type", Encode.string "ObjectOne") ] - ObjectTwo -> Encode.object [ ("type", Encode.string "ObjectTwo") ] - """ - } - } - - "ClassWithGeneric" in { - val productDef = prod("first" -> Ref("A"), "second" -> Ref("B"), "third" -> Ref("C")) - val declaration = decl("ClassWithGeneric", "A", "B", "C")(productDef) - Elm.encoder(declaration) shouldBe { - i""" - encoderClassWithGeneric : (a -> Encode.Value) -> (b -> Encode.Value) -> (c -> Encode.Value) -> ClassWithGeneric a b c -> Encode.Value - encoderClassWithGeneric encoderA encoderB encoderC obj = Encode.object [ ("first", encoderA obj.first), ("second", encoderB obj.second), ("third", encoderC obj.third) ] - """ - } - } - - "ClassWithGeneric2" in { - val productDef = prod("first" -> Ref("A")) - val declaration = decl("ClassWithGeneric2", "A")(productDef) - Elm.encoder(declaration) shouldBe { - i""" - encoderClassWithGeneric2 : (a -> Encode.Value) -> ClassWithGeneric2 a -> Encode.Value - encoderClassWithGeneric2 encoderA obj = Encode.object [ ("first", encoderA obj.first) ] - """ - } - } - - "SumWithGeneric" in { - val sumDef = sum( - "First" -> prod("f" -> Ref("A")), - "Second" -> prod("s" -> Ref("B")), - "Third" -> prod("t" -> Ref("C")) - ) - val declaration = decl("SumWithGeneric", "A", "B", "C")(sumDef) - Elm.encoder(declaration) shouldBe { - i""" - encoderSumWithGeneric : (a -> Encode.Value) -> (b -> Encode.Value) -> (c -> Encode.Value) -> SumWithGeneric a b c -> Encode.Value - encoderSumWithGeneric encoderA encoderB encoderC tpe = - case tpe of - First f -> Encode.object [ ("f", encoderA f), ("type", Encode.string "First") ] - Second s -> Encode.object [ ("s", encoderB s), ("type", Encode.string "Second") ] - Third t -> Encode.object [ ("t", encoderC t), ("type", Encode.string "Third") ] - """ - } - } - - "Numeric types" in { - Elm.encoder(decl[NumericTypes]) shouldBe { - i""" - encoderNumericTypes : NumericTypes -> Encode.Value - encoderNumericTypes obj = Encode.object [ ("int", Encode.int obj.int), ("long", Encode.int obj.long), ("float", Encode.float obj.float), ("double", Encode.float obj.double), ("bigDecimal", Encode.float obj.bigDecimal) ] - """ - } - } - - "Dictionary types" in { - Elm.encoder(decl[Map[String, Int]]) shouldBe { - i""" - encoderMap : Map -> Encode.Value - encoderMap obj = (Encode.dict Encode.string Map Encode.int Map) - """ - } - } -} diff --git a/src/test/scala/bridges/elm/ElmRendererSpec.scala b/src/test/scala/bridges/elm/ElmRendererSpec.scala deleted file mode 100644 index b9dd841..0000000 --- a/src/test/scala/bridges/elm/ElmRendererSpec.scala +++ /dev/null @@ -1,153 +0,0 @@ -package bridges.elm - -import bridges.SampleTypes._ -import bridges.core.Type._ -import bridges.core._ -import bridges.core.syntax._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class ElmRendererSpec extends AnyFreeSpec with Matchers { - "Color" in { - Elm.render(decl[Color]) shouldBe "type alias Color = { red: Int, green: Int, blue: Int }" - } - - "Circle" in { - Elm.render(decl[Circle]) shouldBe "type alias Circle = { radius: Float, color: Color }" - } - - "Rectangle" in { - Elm.render(decl[Rectangle]) shouldBe "type alias Rectangle = { width: Float, height: Float, color: Color }" - } - - "Shape" in { - Elm.render(decl[Shape]) shouldBe """type Shape = Circle Float Color | Rectangle Float Float Color | ShapeGroup Shape Shape""" - } - - "Alpha" in { - Elm.render(decl[Alpha]) shouldBe "type alias Alpha = { name: String, char: Char, bool: Bool }" - } - - "ArrayClass" in { - Elm.render(decl[ArrayClass]) shouldBe """type alias ArrayClass = { aList: (List String), optField: (Maybe Float) }""" - } - - "Numeric" in { - Elm.render(decl[Numeric]) shouldBe """type alias Numeric = { double: Float, float: Float, int: Int }""" - } - - "ClassOrObject" in { - Elm.render(decl[ClassOrObject]) shouldBe """type ClassOrObject = MyClass Int | MyObject""" - } - - "NestedClassOrObject" in { - Elm.render(decl[NestedClassOrObject]) shouldBe """type NestedClassOrObject = MyClass Int | MyObject""" - } - - "Navigation" in { - Elm.render(decl[Navigation]) shouldBe """type Navigation = Node String (List Navigation) | NodeList (List Navigation)""" - } - - "TypeOne and TypeTwo" in { - Elm.render(decl[TypeOne]) shouldBe """type alias TypeOne = { name: String, values: (List TypeTwo) }""" - Elm.render(decl[TypeTwo]) shouldBe """type TypeTwo = OptionOne Int | OptionTwo TypeOne""" - } - - "Recursive" in { - Elm.render(decl[Recursive]) shouldBe """type alias Recursive = { head: Int, tail: (Maybe Recursive) }""" - } - - "Recursive2" in { - Elm.render(decl[Recursive2]) shouldBe """type alias Recursive2 = { head: Int, tail: (List Recursive2) }""" - } - - "ExternalReferences" in { - Elm.render(decl[ExternalReferences]) shouldBe """type alias ExternalReferences = { color: Color, nav: Navigation }""" - } - - "ClassUUID" - { - "Without any specific override" in { - Elm.render(decl[ClassUUID]) shouldBe """type alias ClassUUID = { a: UUID }""" - } - - "providing a type override map" in { - val customTypeReplacements: Map[Ref, TypeReplacement] = Map( - Ref("UUID") -> TypeReplacement("Uuid", "import Uuid exposing (Uuid)", "Uuid.decoder", "Uuid.encode") - ) - - Elm.render(decl[ClassUUID], customTypeReplacements) shouldBe """type alias ClassUUID = { a: Uuid }""" - } - - "using override to treat UUID as a String" in { - implicit val uuidEncoder: BasicEncoder[java.util.UUID] = - Encoder.pure(Str) - - Elm.render(decl[ClassUUID]) shouldBe """type alias ClassUUID = { a: String }""" - } - } - - "ClassDate" - { - "Without any specific override" in { - Elm.render(decl[ClassDate]) shouldBe """type alias ClassDate = { a: Date }""" - } - - "providing a type override map" in { - val customTypeReplacements: Map[Ref, TypeReplacement] = Map( - Ref("Date") -> TypeReplacement("Date", "import Date exposing (Date)", "Date.decoder", "Date.encode") - ) - - Elm.render(decl[ClassDate], customTypeReplacements) shouldBe """type alias ClassDate = { a: Date }""" - } - - "using override to treat Date as a String" in { - implicit val dateEncoder: BasicEncoder[java.util.Date] = - Encoder.pure(Str) - - Elm.render(decl[ClassDate]) shouldBe """type alias ClassDate = { a: String }""" - } - } - - "ObjectsOnly" in { - Elm.render(decl[ObjectsOnly]) shouldBe """type ObjectsOnly = ObjectOne | ObjectTwo""" - } - - "ClassWithRefinedType" in { - // import eu.timepit.refined.shapeless.typeable._ - - Elm.render(decl[ClassWithRefinedType]) shouldBe """type alias ClassWithRefinedType = { name: String }""" - } - - "ClassWithGeneric" in { - val productDef = prod("first" -> Ref("A"), "second" -> Ref("B"), "third" -> Ref("C")) - val declaration = decl("ClassWithGeneric", "A", "B", "C")(productDef) - Elm.render(declaration) shouldBe """type alias ClassWithGeneric a b c = { first: a, second: b, third: c }""" - } - - "ClassWithGeneric2" in { - val productDef = prod("first" -> Ref("A")) - val declaration = decl("ClassWithGeneric2", "A")(productDef) - Elm.render(declaration) shouldBe """type alias ClassWithGeneric2 a = { first: a }""" - } - - "SumWithGeneric" in { - val sumDef = sum( - "First" -> prod("f" -> Ref("A")), - "Second" -> prod("s" -> Ref("B")), - "Third" -> prod("t" -> Ref("C")) - ) - val declaration = decl("SumWithGeneric", "A", "B", "C")(sumDef) - Elm.render(declaration) shouldBe """type SumWithGeneric a b c = First a | Second b | Third c""" - } - - "Numeric types" in { - Elm.render(decl[NumericTypes]) shouldBe { - """type alias NumericTypes = { int: Int, long: Int, float: Float, double: Float, bigDecimal: Float }""" - } - } - - "Dictionary types" in { - Elm.render(decl[Map[String, Int]]) shouldBe { - """type alias Map = (Dict String Int)""" - } - } -} diff --git a/src/test/scala/bridges/flow/FlowRenameSpec.scala b/src/test/scala/bridges/flow/FlowRenameSpec.scala deleted file mode 100644 index f667411..0000000 --- a/src/test/scala/bridges/flow/FlowRenameSpec.scala +++ /dev/null @@ -1,21 +0,0 @@ -package bridges.flow - -import bridges.SampleTypes._ -import bridges.flow.FlowType._ -import bridges.flow.syntax._ -import org.scalatest._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class FlowRenameSpec extends AnyFreeSpec with Matchers { - "decl" in { - val actual = decl[Color].rename("red", "r") - val expected = "Color" := struct( - "r" --> Intr, - "green" --> Intr, - "blue" --> Intr - ) - - actual shouldBe expected - } -} diff --git a/src/test/scala/bridges/flow/FlowRendererSpec.scala b/src/test/scala/bridges/flow/FlowRendererSpec.scala deleted file mode 100644 index 8312094..0000000 --- a/src/test/scala/bridges/flow/FlowRendererSpec.scala +++ /dev/null @@ -1,233 +0,0 @@ -package bridges.flow - -import bridges.SampleTypes._ -import bridges.core.DeclF -import bridges.flow.FlowType._ -import bridges.flow.syntax._ -import org.scalatest._ -import unindent._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class FlowRendererSpec extends AnyFreeSpec with Matchers { - "Color" in { - Flow.render(decl[Color]) shouldBe { - i""" - export type Color = { red: number, green: number, blue: number }; - """ - } - } - - "Circle" in { - Flow.render(decl[Circle]) shouldBe { - i""" - export type Circle = { radius: number, color: Color }; - """ - } - } - - "Rectangle" in { - Flow.render(decl[Rectangle]) shouldBe { - i""" - export type Rectangle = { width: number, height: number, color: Color }; - """ - } - } - - "Shape" in { - Flow.render(decl[Shape]) shouldBe { - i""" - export type Shape = { type: "Circle", radius: number, color: Color } | { type: "Rectangle", width: number, height: number, color: Color } | { type: "ShapeGroup", leftShape: Shape, rightShape: Shape }; - """ - } - } - - "Alpha" in { - Flow.render(decl[Alpha]) shouldBe { - i""" - export type Alpha = { name: string, char: string, bool: boolean }; - """ - } - } - - "ArrayClass" in { - Flow.render(decl[ArrayClass]) shouldBe { - i""" - export type ArrayClass = { aList: string[], optField: ?number }; - """ - } - } - - "Numeric" in { - Flow.render(decl[Numeric]) shouldBe { - i""" - export type Numeric = { double: number, float: number, int: number }; - """ - } - } - - "ClassOrObject" in { - Flow.render(decl[ClassOrObject]) shouldBe { - i""" - export type ClassOrObject = { type: "MyClass", value: number } | { type: "MyObject" }; - """ - } - } - - "NestedClassOrObject" in { - Flow.render(decl[NestedClassOrObject]) shouldBe { - i""" - export type NestedClassOrObject = { type: "MyClass", value: number } | { type: "MyObject" }; - """ - } - } - - "Navigation" in { - Flow.render(decl[Navigation]) shouldBe { - i""" - export type Navigation = { type: "Node", name: string, children: Navigation[] } | { type: "NodeList", all: Navigation[] }; - """ - } - } - - "ClassUUID" in { - Flow.render(decl[ClassUUID]) shouldBe { - i""" - export type ClassUUID = { a: UUID }; - """ - } - } - - "ClassDate" in { - Flow.render(decl[ClassDate]) shouldBe { - i""" - export type ClassDate = { a: Date }; - """ - } - } - - "ExternalReferences" in { - Flow.render(decl[ExternalReferences]) shouldBe { - i""" - export type ExternalReferences = { color: Color, nav: Navigation }; - """ - } - } - - "Recursive" in { - Flow.render(decl[Recursive]) shouldBe { - i""" - export type Recursive = { head: number, tail: ?Recursive }; - """ - } - } - - "Recursive2" in { - Flow.render(decl[Recursive2]) shouldBe { - i""" - export type Recursive2 = { head: number, tail: Recursive2[] }; - """ - } - } - - "ObjectsOnly" in { - Flow.render(decl[ObjectsOnly]) shouldBe { - i""" - export type ObjectsOnly = { type: "ObjectOne" } | { type: "ObjectTwo" }; - """ - } - } - - "Optional of Array" in { - Flow.render("Foo" := Arr(Str).?) shouldBe { - i""" - export type Foo = ?string[]; - """ - } - } - - "Array of Optional" in { - Flow.render("Foo" := Arr(Str.?)) shouldBe { - i""" - export type Foo = (?string)[]; - """ - } - } - - "Union of Union" in { - Flow.render("A" := Ref("B") | Ref("C") | Ref("D")) shouldBe { - i""" - export type A = B | C | D; - """ - } - } - - "Inter of Inter" in { - Flow.render("A" := Ref("B") & Ref("C") & Ref("D")) shouldBe { - i""" - export type A = B & C & D; - """ - } - } - - "Generic Decl" in { - Flow.render(decl("Pair", "A", "B")(struct("a" --> Ref("A"), "b" --> Ref("B")))) shouldBe { - i""" - export type Pair = { a: A, b: B }; - """ - } - } - - "Generic Ref" in { - Flow.render(decl("Numbers")(ref("Pair", Real, Real))) shouldBe { - i""" - export type Numbers = Pair; - """ - } - } - - "Numeric types" in { - Flow.render(decl[NumericTypes]) shouldBe { - i""" - export type NumericTypes = { int: number, long: number, float: number, double: number, bigDecimal: number }; - """ - } - } - - "Tuple" in { - Flow.render(decl("Cell")(tuple(Str, Intr))) shouldBe { - i""" - export type Cell = [string, number]; - """ - } - } - - "Empty tuple" in { - Flow.render(decl("Empty")(tuple())) shouldBe { - i""" - export type Empty = []; - """ - } - } - - "Structs with rest fields" in { - Flow.render(decl("Dict")(dict(Str, Intr))) shouldBe { - i""" - export type Dict = { [key: string]: number }; - """ - } - - Flow.render( - decl("Dict")( - struct( - "a" --> Str, - "b" -?> Intr - ).withRest(Str, Bool, "c") - ) - ) shouldBe { - i""" - export type Dict = { a: string, b?: number, [c: string]: boolean }; - """ - } - } -} diff --git a/src/test/scala/bridges/SampleTypes.scala b/src/test/scala/bridges/typescript/SampleTypes.scala similarity index 80% rename from src/test/scala/bridges/SampleTypes.scala rename to src/test/scala/bridges/typescript/SampleTypes.scala index b3e03dc..b8c4491 100644 --- a/src/test/scala/bridges/SampleTypes.scala +++ b/src/test/scala/bridges/typescript/SampleTypes.scala @@ -1,20 +1,9 @@ -package bridges +package bridges.typescript import java.util.{ Date, UUID } -import bridges.core._ -import bridges.core.Type._ -import bridges.core.syntax._ -import eu.timepit.refined.api.Refined -import eu.timepit.refined.collection.Size -import eu.timepit.refined.generic.Equal -import eu.timepit.refined.numeric.Greater -import eu.timepit.refined.numeric.Interval.ClosedOpen - object SampleTypes { - type RefinedString = String Refined Size[ClosedOpen[1, 100]] - type RefinedInt = Int Refined Greater[6] - type RefinedChar = Char Refined Equal['3'] + import syntax.* // Sample product case class Pair(a: String, b: Int) @@ -54,8 +43,8 @@ object SampleTypes { // Recursive structure sealed trait Navigation - final case class NodeList(all: List[Navigation]) extends Navigation final case class Node(name: String, children: List[Navigation]) extends Navigation + final case class NodeList(all: List[Navigation]) extends Navigation // case classes with specific values (list, Float, Option, Char, etc) final case class Alpha(name: String, char: Char, bool: Boolean) @@ -82,13 +71,14 @@ object SampleTypes { // Custom declaration of a intermediate structure val customDeclaration: Decl = - "Message" := sum( - "ErrorMessage" -> prod("error" -> Ref("ErrorMessage")), - "WarningMessage" -> prod("warning" -> Ref("WarningMessage")) + Decl( + "Message", + TsType.discriminated( + "ErrorMessage" -> TsType.struct("error" --> TsType.Ref("ErrorMessage")), + "WarningMessage" -> TsType.struct("warning" --> TsType.Ref("WarningMessage")) + ) ) - final case class ClassWithRefinedType(name: RefinedString) - final case class NumericTypes( int: Int, long: Long, diff --git a/src/test/scala/bridges/typescript/TsEncoderSpec.scala b/src/test/scala/bridges/typescript/TsEncoderSpec.scala index 58d747e..6fed232 100644 --- a/src/test/scala/bridges/typescript/TsEncoderSpec.scala +++ b/src/test/scala/bridges/typescript/TsEncoderSpec.scala @@ -1,82 +1,80 @@ -package bridges.typescript - -import bridges.SampleTypes._ -import bridges.core.DeclF -import bridges.typescript.TsType._ -import bridges.typescript.syntax._ -import org.scalatest._ -import unindent._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class TsEncoderSpec extends AnyFreeSpec with Matchers { - "TsEncoderConfig.optionalFields" - { - "render optional fields by default" in { - decl[Recursive] shouldBe { - decl("Recursive")( - struct( - "head" --> Intr, - "tail" -?> union(ref("Recursive"), Null) - ) - ) - } - } - - "override setting" in { - implicit val config: TsEncoderConfig = - TsEncoderConfig(optionalFields = false) - - decl[Recursive] shouldBe { - decl("Recursive")( - struct( - "head" --> Intr, - "tail" --> union(ref("Recursive"), Null) - ) - ) - } - } - } - - "TsEncoderConfig.refsUnions" - { - "render type names in unions by default" in { - decl[OneOrOther] shouldBe { - decl("OneOrOther")( - struct( - "type" --> StrLit("One"), - "value" --> Str - ) | - struct( - "type" --> StrLit("Other"), - "value" --> Intr - ) - ) - } - } - - "override setting" in { - implicit val config: TsEncoderConfig = - TsEncoderConfig(refsInUnions = true) - - decl[OneOrOther] shouldBe { - decl("OneOrOther")( - (struct("type" --> StrLit("One")) & ref("One")) | - (struct("type" --> StrLit("Other")) & ref("Other")) - ) - } - } - } - - "Numeric types" in { - decl[NumericTypes] shouldBe { - decl("NumericTypes")( - struct( - "int" --> Intr, - "long" --> Intr, - "float" --> Real, - "double" --> Real, - "bigDecimal" --> Real - ) - ) - } - } -} +// package bridges.typescript +// +// import bridges.SampleTypes._ +// import bridges.typescript.TsType._ +// import org.scalatest._ +// import unindent._ +// import org.scalatest.freespec.AnyFreeSpec +// import org.scalatest.matchers.should.Matchers +// +// class TsEncoderSpec extends AnyFreeSpec with Matchers { +// "TsEncoderConfig.optionalFields" - { +// "render optional fields by default" in { +// decl[Recursive] shouldBe { +// decl("Recursive")( +// struct( +// "head" --> Intr, +// "tail" -?> union(ref("Recursive"), Null) +// ) +// ) +// } +// } +// +// "override setting" in { +// implicit val config: TsEncoderConfig = +// TsEncoderConfig(optionalFields = false) +// +// decl[Recursive] shouldBe { +// decl("Recursive")( +// struct( +// "head" --> Intr, +// "tail" --> union(ref("Recursive"), Null) +// ) +// ) +// } +// } +// } +// +// "TsEncoderConfig.refsUnions" - { +// "render type names in unions by default" in { +// decl[OneOrOther] shouldBe { +// decl("OneOrOther")( +// struct( +// "type" --> StrLit("One"), +// "value" --> Str +// ) | +// struct( +// "type" --> StrLit("Other"), +// "value" --> Intr +// ) +// ) +// } +// } +// +// "override setting" in { +// implicit val config: TsEncoderConfig = +// TsEncoderConfig(refsInUnions = true) +// +// decl[OneOrOther] shouldBe { +// decl("OneOrOther")( +// (struct("type" --> StrLit("One")) & ref("One")) | +// (struct("type" --> StrLit("Other")) & ref("Other")) +// ) +// } +// } +// } +// +// "Numeric types" in { +// decl[NumericTypes] shouldBe { +// decl("NumericTypes")( +// struct( +// "int" --> Intr, +// "long" --> Intr, +// "float" --> Real, +// "double" --> Real, +// "bigDecimal" --> Real +// ) +// ) +// } +// } +// } diff --git a/src/test/scala/bridges/typescript/TsGuardRendererSpec.scala b/src/test/scala/bridges/typescript/TsGuardRendererSpec.scala index 95b782f..4228806 100644 --- a/src/test/scala/bridges/typescript/TsGuardRendererSpec.scala +++ b/src/test/scala/bridges/typescript/TsGuardRendererSpec.scala @@ -1,322 +1,321 @@ -package bridges.typescript - -import bridges.SampleTypes._ -import bridges.typescript.TsType._ -import bridges.typescript.syntax._ -import org.scalatest._ -import unindent._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class TsGuardRendererSpec extends AnyFreeSpec with Matchers { - "Color" in { - TypescriptGuard.render(decl[Color]) shouldBe { - i""" - export const isColor = (v: any): v is Color => { - return typeof v === "object" && v != null && "red" in v && typeof v.red === "number" && "green" in v && typeof v.green === "number" && "blue" in v && typeof v.blue === "number"; - } - """ - } - } - - "Circle" in { - TypescriptGuard.render(decl[Circle]) shouldBe { - i""" - export const isCircle = (v: any): v is Circle => { - return typeof v === "object" && v != null && "radius" in v && typeof v.radius === "number" && "color" in v && isColor(v.color); - } - """ - } - } - - "Rectangle" in { - TypescriptGuard.render(decl[Rectangle]) shouldBe { - i""" - export const isRectangle = (v: any): v is Rectangle => { - return typeof v === "object" && v != null && "width" in v && typeof v.width === "number" && "height" in v && typeof v.height === "number" && "color" in v && isColor(v.color); - } - """ - } - } - - "Shape" in { - TypescriptGuard.render(decl[Shape]) shouldBe { - i""" - export const isShape = (v: any): v is Shape => { - return typeof v === "object" && v != null && "type" in v && (v.type === "Circle" ? typeof v === "object" && v != null && "radius" in v && typeof v.radius === "number" && "color" in v && isColor(v.color) : v.type === "Rectangle" ? typeof v === "object" && v != null && "width" in v && typeof v.width === "number" && "height" in v && typeof v.height === "number" && "color" in v && isColor(v.color) : v.type === "ShapeGroup" ? typeof v === "object" && v != null && "leftShape" in v && isShape(v.leftShape) && "rightShape" in v && isShape(v.rightShape) : false); - } - """ - } - } - - "Alpha" in { - TypescriptGuard.render(decl[Alpha]) shouldBe { - i""" - export const isAlpha = (v: any): v is Alpha => { - return typeof v === "object" && v != null && "name" in v && typeof v.name === "string" && "char" in v && typeof v.char === "string" && "bool" in v && typeof v.bool === "boolean"; - } - """ - } - } - - "ArrayClass" in { - TypescriptGuard.render(decl[ArrayClass]) shouldBe { - i""" - export const isArrayClass = (v: any): v is ArrayClass => { - return typeof v === "object" && v != null && "aList" in v && Array.isArray(v.aList) && v.aList.every((i: any) => typeof i === "string") && (!("optField" in v) || typeof v.optField === "number" || v.optField === null); - } - """ - } - } - - "Numeric" in { - TypescriptGuard.render(decl[Numeric]) shouldBe { - i""" - export const isNumeric = (v: any): v is Numeric => { - return typeof v === "object" && v != null && "double" in v && typeof v.double === "number" && "float" in v && typeof v.float === "number" && "int" in v && typeof v.int === "number"; - } - """ - } - } - - "ClassOrObject" in { - TypescriptGuard.render(decl[ClassOrObject]) shouldBe { - i""" - export const isClassOrObject = (v: any): v is ClassOrObject => { - return typeof v === "object" && v != null && "type" in v && (v.type === "MyClass" ? typeof v === "object" && v != null && "value" in v && typeof v.value === "number" : v.type === "MyObject" ? typeof v === "object" && v != null : false); - } - """ - } - } - - "NestedClassOrObject" in { - TypescriptGuard.render(decl[NestedClassOrObject]) shouldBe { - i""" - export const isNestedClassOrObject = (v: any): v is NestedClassOrObject => { - return typeof v === "object" && v != null && "type" in v && (v.type === "MyClass" ? typeof v === "object" && v != null && "value" in v && typeof v.value === "number" : v.type === "MyObject" ? typeof v === "object" && v != null : false); - } - """ - } - } - - "Navigation" in { - TypescriptGuard.render(decl[Navigation]) shouldBe { - i""" - export const isNavigation = (v: any): v is Navigation => { - return typeof v === "object" && v != null && "type" in v && (v.type === "Node" ? typeof v === "object" && v != null && "name" in v && typeof v.name === "string" && "children" in v && Array.isArray(v.children) && v.children.every((i: any) => isNavigation(i)) : v.type === "NodeList" ? typeof v === "object" && v != null && "all" in v && Array.isArray(v.all) && v.all.every((i: any) => isNavigation(i)) : false); - } - """ - } - } - - "ClassUUID" in { - TypescriptGuard.render(decl[ClassUUID]) shouldBe { - i""" - export const isClassUUID = (v: any): v is ClassUUID => { - return typeof v === "object" && v != null && "a" in v && isUUID(v.a); - } - """ - } - } - - "ClassDate" in { - TypescriptGuard.render(decl[ClassDate]) shouldBe { - i""" - export const isClassDate = (v: any): v is ClassDate => { - return typeof v === "object" && v != null && "a" in v && isDate(v.a); - } - """ - } - } - - "Recursive" in { - TypescriptGuard.render(decl[Recursive]) shouldBe { - i""" - export const isRecursive = (v: any): v is Recursive => { - return typeof v === "object" && v != null && "head" in v && typeof v.head === "number" && (!("tail" in v) || isRecursive(v.tail) || v.tail === null); - } - """ - } - } - - "Recursive2" in { - TypescriptGuard.render(decl[Recursive2]) shouldBe { - i""" - export const isRecursive2 = (v: any): v is Recursive2 => { - return typeof v === "object" && v != null && "head" in v && typeof v.head === "number" && "tail" in v && Array.isArray(v.tail) && v.tail.every((i: any) => isRecursive2(i)); - } - """ - } - } - - "ExternalReferences" in { - TypescriptGuard.render(decl[ExternalReferences]) shouldBe { - i""" - export const isExternalReferences = (v: any): v is ExternalReferences => { - return typeof v === "object" && v != null && "color" in v && isColor(v.color) && "nav" in v && isNavigation(v.nav); - } - """ - } - } - - "ObjectsOnly" in { - TypescriptGuard.render(decl[ObjectsOnly]) shouldBe { - i""" - export const isObjectsOnly = (v: any): v is ObjectsOnly => { - return typeof v === "object" && v != null && "type" in v && (v.type === "ObjectOne" ? typeof v === "object" && v != null : v.type === "ObjectTwo" ? typeof v === "object" && v != null : false); - } - """ - } - } - - "Union of Union" in { - TypescriptGuard.render("A" := Ref("B") | Ref("C") | Ref("D")) shouldBe { - i""" - export const isA = (v: any): v is A => { - return isB(v) || isC(v) || isD(v); - } - """ - } - } - - "Inter of Inter" in { - TypescriptGuard.render("A" := Ref("B") & Ref("C") & Ref("D")) shouldBe { - i""" - export const isA = (v: any): v is A => { - return isB(v) && isC(v) && isD(v); - } - """ - } - } - - "Generic Decl" in { - TypescriptGuard.render( - decl("Pair", "A", "B")( - struct( - "a" --> Ref("A"), - "b" -?> Ref("B") - ) - ) - ) shouldBe { - i""" - export const isPair = (isA: (a: any) => a is A, isB: (b: any) => b is B) => (v: any): v is Pair => { - return typeof v === "object" && v != null && "a" in v && isA(v.a) && (!("b" in v) || isB(v.b)); - } - """ - } - } - - "Applications of Generics" in { - TypescriptGuard.render(decl("Cell")(ref("Pair", Str, Intr))) shouldBe { - i""" - export const isCell = (v: any): v is Cell => { - return isPair((a0: any): a0 is string => typeof a0 === "string", (a1: any): a1 is number => typeof a1 === "number")(v); - } - """ - } - - TypescriptGuard.render(decl("Same", "A")(ref("Pair", ref("A"), ref("A")))) shouldBe { - i""" - export const isSame = (isA: (a: any) => a is A) => (v: any): v is Same => { - return isPair((a0: any): a0 is A => isA(a0), (a1: any): a1 is A => isA(a1))(v); - } - """ - } - - TypescriptGuard.render(decl("AnyPair")(ref("Pair", Any, Any))) shouldBe { - i""" - export const isAnyPair = (v: any): v is AnyPair => { - return isPair((a0: any): a0 is any => true, (a1: any): a1 is any => true)(v); - } - """ - } - } - - "Numeric types" in { - TypescriptGuard.render(decl[NumericTypes]) shouldBe { - i""" - export const isNumericTypes = (v: any): v is NumericTypes => { - return typeof v === "object" && v != null && "int" in v && typeof v.int === "number" && "long" in v && typeof v.long === "number" && "float" in v && typeof v.float === "number" && "double" in v && typeof v.double === "number" && "bigDecimal" in v && typeof v.bigDecimal === "number"; - } - """ - } - } - - "Tuple" in { - TypescriptGuard.render(decl("Cell")(tuple(Str, Intr))) shouldBe { - i""" - export const isCell = (v: any): v is Cell => { - return Array.isArray(v) && v.length === 2 && typeof v[0] === "string" && typeof v[1] === "number"; - } - """ - } - } - - "Empty tuple" in { - TypescriptGuard.render(decl("Empty")(tuple())) shouldBe { - i""" - export const isEmpty = (v: any): v is Empty => { - return Array.isArray(v) && v.length === 0; - } - """ - } - } - - "Structs with rest fields" in { - TypescriptGuard.render(decl("Dict")(dict(Str, Intr))) shouldBe { - i""" - export const isDict = (v: any): v is Dict => { - return typeof v === "object" && v != null && Object.keys(v).every((k: any) => typeof k === "string" && typeof v[k] === "number"); - } - """ - } - - TypescriptGuard.render( - decl("Dict")( - struct( - "a" --> Str, - "b" -?> Intr - ).withRest(Str, Bool, "c") - ) - ) shouldBe { - i""" - export const isDict = (v: any): v is Dict => { - return typeof v === "object" && v != null && "a" in v && typeof v.a === "string" && (!("b" in v) || typeof v.b === "number") && Object.keys(v).every((k: any) => ["a", "b"].includes(k) || typeof k === "string" && typeof v[k] === "boolean"); - } - """ - } - } - - "Function types" in { - TypescriptGuard.render( - decl("Rule")( - struct( - "message" --> Str, - "apply" --> func("value" -> Unknown)(Bool) - ) - ) - ) shouldBe { - i""" - export const isRule = (v: any): v is Rule => { - return typeof v === "object" && v != null && "message" in v && typeof v.message === "string" && "apply" in v && typeof v.apply === "function"; - } - """ - } - - TypescriptGuard.render( - decl("Funcy")( - tuple( - func("arg" -> tuple(Str))(tuple(Str)), - func("arg" -> tuple(Intr))(tuple(Intr)) - ) - ) - ) shouldBe { - i""" - export const isFuncy = (v: any): v is Funcy => { - return Array.isArray(v) && v.length === 2 && typeof v[0] === "function" && typeof v[1] === "function"; - } - """ - } - } - -} +// package bridges.typescript +// +// import bridges.SampleTypes._ +// import bridges.typescript.TsType._ +// import org.scalatest._ +// import unindent._ +// import org.scalatest.freespec.AnyFreeSpec +// import org.scalatest.matchers.should.Matchers +// +// class TsGuardRendererSpec extends AnyFreeSpec with Matchers { +// "Color" in { +// TypescriptGuard.render(decl[Color]) shouldBe { +// i""" +// export const isColor = (v: any): v is Color => { +// return typeof v === "object" && v != null && "red" in v && typeof v.red === "number" && "green" in v && typeof v.green === "number" && "blue" in v && typeof v.blue === "number"; +// } +// """ +// } +// } +// +// "Circle" in { +// TypescriptGuard.render(decl[Circle]) shouldBe { +// i""" +// export const isCircle = (v: any): v is Circle => { +// return typeof v === "object" && v != null && "radius" in v && typeof v.radius === "number" && "color" in v && isColor(v.color); +// } +// """ +// } +// } +// +// "Rectangle" in { +// TypescriptGuard.render(decl[Rectangle]) shouldBe { +// i""" +// export const isRectangle = (v: any): v is Rectangle => { +// return typeof v === "object" && v != null && "width" in v && typeof v.width === "number" && "height" in v && typeof v.height === "number" && "color" in v && isColor(v.color); +// } +// """ +// } +// } +// +// "Shape" in { +// TypescriptGuard.render(decl[Shape]) shouldBe { +// i""" +// export const isShape = (v: any): v is Shape => { +// return typeof v === "object" && v != null && "type" in v && (v.type === "Circle" ? typeof v === "object" && v != null && "radius" in v && typeof v.radius === "number" && "color" in v && isColor(v.color) : v.type === "Rectangle" ? typeof v === "object" && v != null && "width" in v && typeof v.width === "number" && "height" in v && typeof v.height === "number" && "color" in v && isColor(v.color) : v.type === "ShapeGroup" ? typeof v === "object" && v != null && "leftShape" in v && isShape(v.leftShape) && "rightShape" in v && isShape(v.rightShape) : false); +// } +// """ +// } +// } +// +// "Alpha" in { +// TypescriptGuard.render(decl[Alpha]) shouldBe { +// i""" +// export const isAlpha = (v: any): v is Alpha => { +// return typeof v === "object" && v != null && "name" in v && typeof v.name === "string" && "char" in v && typeof v.char === "string" && "bool" in v && typeof v.bool === "boolean"; +// } +// """ +// } +// } +// +// "ArrayClass" in { +// TypescriptGuard.render(decl[ArrayClass]) shouldBe { +// i""" +// export const isArrayClass = (v: any): v is ArrayClass => { +// return typeof v === "object" && v != null && "aList" in v && Array.isArray(v.aList) && v.aList.every((i: any) => typeof i === "string") && (!("optField" in v) || typeof v.optField === "number" || v.optField === null); +// } +// """ +// } +// } +// +// "Numeric" in { +// TypescriptGuard.render(decl[Numeric]) shouldBe { +// i""" +// export const isNumeric = (v: any): v is Numeric => { +// return typeof v === "object" && v != null && "double" in v && typeof v.double === "number" && "float" in v && typeof v.float === "number" && "int" in v && typeof v.int === "number"; +// } +// """ +// } +// } +// +// "ClassOrObject" in { +// TypescriptGuard.render(decl[ClassOrObject]) shouldBe { +// i""" +// export const isClassOrObject = (v: any): v is ClassOrObject => { +// return typeof v === "object" && v != null && "type" in v && (v.type === "MyClass" ? typeof v === "object" && v != null && "value" in v && typeof v.value === "number" : v.type === "MyObject" ? typeof v === "object" && v != null : false); +// } +// """ +// } +// } +// +// "NestedClassOrObject" in { +// TypescriptGuard.render(decl[NestedClassOrObject]) shouldBe { +// i""" +// export const isNestedClassOrObject = (v: any): v is NestedClassOrObject => { +// return typeof v === "object" && v != null && "type" in v && (v.type === "MyClass" ? typeof v === "object" && v != null && "value" in v && typeof v.value === "number" : v.type === "MyObject" ? typeof v === "object" && v != null : false); +// } +// """ +// } +// } +// +// "Navigation" in { +// TypescriptGuard.render(decl[Navigation]) shouldBe { +// i""" +// export const isNavigation = (v: any): v is Navigation => { +// return typeof v === "object" && v != null && "type" in v && (v.type === "Node" ? typeof v === "object" && v != null && "name" in v && typeof v.name === "string" && "children" in v && Array.isArray(v.children) && v.children.every((i: any) => isNavigation(i)) : v.type === "NodeList" ? typeof v === "object" && v != null && "all" in v && Array.isArray(v.all) && v.all.every((i: any) => isNavigation(i)) : false); +// } +// """ +// } +// } +// +// "ClassUUID" in { +// TypescriptGuard.render(decl[ClassUUID]) shouldBe { +// i""" +// export const isClassUUID = (v: any): v is ClassUUID => { +// return typeof v === "object" && v != null && "a" in v && isUUID(v.a); +// } +// """ +// } +// } +// +// "ClassDate" in { +// TypescriptGuard.render(decl[ClassDate]) shouldBe { +// i""" +// export const isClassDate = (v: any): v is ClassDate => { +// return typeof v === "object" && v != null && "a" in v && isDate(v.a); +// } +// """ +// } +// } +// +// "Recursive" in { +// TypescriptGuard.render(decl[Recursive]) shouldBe { +// i""" +// export const isRecursive = (v: any): v is Recursive => { +// return typeof v === "object" && v != null && "head" in v && typeof v.head === "number" && (!("tail" in v) || isRecursive(v.tail) || v.tail === null); +// } +// """ +// } +// } +// +// "Recursive2" in { +// TypescriptGuard.render(decl[Recursive2]) shouldBe { +// i""" +// export const isRecursive2 = (v: any): v is Recursive2 => { +// return typeof v === "object" && v != null && "head" in v && typeof v.head === "number" && "tail" in v && Array.isArray(v.tail) && v.tail.every((i: any) => isRecursive2(i)); +// } +// """ +// } +// } +// +// "ExternalReferences" in { +// TypescriptGuard.render(decl[ExternalReferences]) shouldBe { +// i""" +// export const isExternalReferences = (v: any): v is ExternalReferences => { +// return typeof v === "object" && v != null && "color" in v && isColor(v.color) && "nav" in v && isNavigation(v.nav); +// } +// """ +// } +// } +// +// "ObjectsOnly" in { +// TypescriptGuard.render(decl[ObjectsOnly]) shouldBe { +// i""" +// export const isObjectsOnly = (v: any): v is ObjectsOnly => { +// return typeof v === "object" && v != null && "type" in v && (v.type === "ObjectOne" ? typeof v === "object" && v != null : v.type === "ObjectTwo" ? typeof v === "object" && v != null : false); +// } +// """ +// } +// } +// +// "Union of Union" in { +// TypescriptGuard.render("A" := Ref("B") union Ref("C") union Ref("D")) shouldBe { +// i""" +// export const isA = (v: any): v is A => { +// return isB(v) || isC(v) || isD(v); +// } +// """ +// } +// } +// +// "Inter of Inter" in { +// TypescriptGuard.render("A" := Ref("B") intersect Ref("C") intersect Ref("D")) shouldBe { +// i""" +// export const isA = (v: any): v is A => { +// return isB(v) && isC(v) && isD(v); +// } +// """ +// } +// } +// +// "Generic Decl" in { +// TypescriptGuard.render( +// decl("Pair", "A", "B")( +// struct( +// "a" --> Ref("A"), +// "b" -?> Ref("B") +// ) +// ) +// ) shouldBe { +// i""" +// export const isPair = (isA: (a: any) => a is A, isB: (b: any) => b is B) => (v: any): v is Pair => { +// return typeof v === "object" && v != null && "a" in v && isA(v.a) && (!("b" in v) || isB(v.b)); +// } +// """ +// } +// } +// +// "Applications of Generics" in { +// TypescriptGuard.render(decl("Cell")(ref("Pair", Str, Intr))) shouldBe { +// i""" +// export const isCell = (v: any): v is Cell => { +// return isPair((a0: any): a0 is string => typeof a0 === "string", (a1: any): a1 is number => typeof a1 === "number")(v); +// } +// """ +// } +// +// TypescriptGuard.render(decl("Same", "A")(ref("Pair", ref("A"), ref("A")))) shouldBe { +// i""" +// export const isSame = (isA: (a: any) => a is A) => (v: any): v is Same => { +// return isPair((a0: any): a0 is A => isA(a0), (a1: any): a1 is A => isA(a1))(v); +// } +// """ +// } +// +// TypescriptGuard.render(decl("AnyPair")(ref("Pair", Any, Any))) shouldBe { +// i""" +// export const isAnyPair = (v: any): v is AnyPair => { +// return isPair((a0: any): a0 is any => true, (a1: any): a1 is any => true)(v); +// } +// """ +// } +// } +// +// "Numeric types" in { +// TypescriptGuard.render(decl[NumericTypes]) shouldBe { +// i""" +// export const isNumericTypes = (v: any): v is NumericTypes => { +// return typeof v === "object" && v != null && "int" in v && typeof v.int === "number" && "long" in v && typeof v.long === "number" && "float" in v && typeof v.float === "number" && "double" in v && typeof v.double === "number" && "bigDecimal" in v && typeof v.bigDecimal === "number"; +// } +// """ +// } +// } +// +// "Tuple" in { +// TypescriptGuard.render(decl("Cell")(tuple(Str, Intr))) shouldBe { +// i""" +// export const isCell = (v: any): v is Cell => { +// return Array.isArray(v) && v.length === 2 && typeof v[0] === "string" && typeof v[1] === "number"; +// } +// """ +// } +// } +// +// "Empty tuple" in { +// TypescriptGuard.render(decl("Empty")(tuple())) shouldBe { +// i""" +// export const isEmpty = (v: any): v is Empty => { +// return Array.isArray(v) && v.length === 0; +// } +// """ +// } +// } +// +// "Structs with rest fields" in { +// TypescriptGuard.render(decl("Dict")(dict(Str, Intr))) shouldBe { +// i""" +// export const isDict = (v: any): v is Dict => { +// return typeof v === "object" && v != null && Object.keys(v).every((k: any) => typeof k === "string" && typeof v[k] === "number"); +// } +// """ +// } +// +// TypescriptGuard.render( +// decl("Dict")( +// struct( +// "a" --> Str, +// "b" -?> Intr +// ).withRest(Str, Bool, "c") +// ) +// ) shouldBe { +// i""" +// export const isDict = (v: any): v is Dict => { +// return typeof v === "object" && v != null && "a" in v && typeof v.a === "string" && (!("b" in v) || typeof v.b === "number") && Object.keys(v).every((k: any) => ["a", "b"].includes(k) || typeof k === "string" && typeof v[k] === "boolean"); +// } +// """ +// } +// } +// +// "Function types" in { +// TypescriptGuard.render( +// decl("Rule")( +// struct( +// "message" --> Str, +// "apply" --> func("value" -> Unknown)(Bool) +// ) +// ) +// ) shouldBe { +// i""" +// export const isRule = (v: any): v is Rule => { +// return typeof v === "object" && v != null && "message" in v && typeof v.message === "string" && "apply" in v && typeof v.apply === "function"; +// } +// """ +// } +// +// TypescriptGuard.render( +// decl("Funcy")( +// tuple( +// func("arg" -> tuple(Str))(tuple(Str)), +// func("arg" -> tuple(Intr))(tuple(Intr)) +// ) +// ) +// ) shouldBe { +// i""" +// export const isFuncy = (v: any): v is Funcy => { +// return Array.isArray(v) && v.length === 2 && typeof v[0] === "function" && typeof v[1] === "function"; +// } +// """ +// } +// } +// +// } diff --git a/src/test/scala/bridges/typescript/TsRenameSpec.scala b/src/test/scala/bridges/typescript/TsRenameSpec.scala index 9043455..38a09f9 100644 --- a/src/test/scala/bridges/typescript/TsRenameSpec.scala +++ b/src/test/scala/bridges/typescript/TsRenameSpec.scala @@ -1,22 +1,21 @@ -package bridges.typescript - -import bridges.SampleTypes._ -import bridges.typescript.TsType._ -import bridges.typescript.syntax._ -import org.scalatest._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class TsRenameSpec extends AnyFreeSpec with Matchers { - "decl" in { - val actual = decl[Color].rename("red", "r") - - val expected = "Color" := struct( - "r" --> Intr, - "green" --> Intr, - "blue" --> Intr - ) - - actual shouldBe expected - } -} +// package bridges.typescript +// +// import bridges.SampleTypes._ +// import bridges.typescript.TsType._ +// import org.scalatest._ +// import org.scalatest.freespec.AnyFreeSpec +// import org.scalatest.matchers.should.Matchers +// +// class TsRenameSpec extends AnyFreeSpec with Matchers { +// "decl" in { +// val actual = decl[Color].rename("red", "r") +// +// val expected = "Color" := struct( +// "r" --> Intr, +// "green" --> Intr, +// "blue" --> Intr +// ) +// +// actual shouldBe expected +// } +// } diff --git a/src/test/scala/bridges/typescript/TsTypeRendererSpec.scala b/src/test/scala/bridges/typescript/TsTypeRendererSpec.scala index 3b90329..ff034e6 100644 --- a/src/test/scala/bridges/typescript/TsTypeRendererSpec.scala +++ b/src/test/scala/bridges/typescript/TsTypeRendererSpec.scala @@ -1,17 +1,16 @@ package bridges.typescript -import bridges.SampleTypes._ -import bridges.core.DeclF -import bridges.typescript.TsType._ -import bridges.typescript.syntax._ -import org.scalatest._ -import unindent._ -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - -class TsTypeRendererSpec extends AnyFreeSpec with Matchers { - "Color" in { - Typescript.render(decl[Color]) shouldBe { +import munit.FunSuite +import unindent.* + +class TsTypeRendererSpec extends FunSuite: + import SampleTypes.* + import TsType.* + import syntax.* + + test("Color") { + assertEquals( + Typescript.render(decl[Color]), i""" export interface Color { red: number; @@ -19,22 +18,24 @@ class TsTypeRendererSpec extends AnyFreeSpec with Matchers { blue: number; } """ - } + ) } - "Circle" in { - Typescript.render(decl[Circle]) shouldBe { + test("Circle") { + assertEquals( + Typescript.render(decl[Circle]), i""" export interface Circle { radius: number; color: Color; } """ - } + ) } - "Rectangle" in { - Typescript.render(decl[Rectangle]) shouldBe { + test("Rectangle") { + assertEquals( + Typescript.render(decl[Rectangle]), i""" export interface Rectangle { width: number; @@ -42,19 +43,21 @@ class TsTypeRendererSpec extends AnyFreeSpec with Matchers { color: Color; } """ - } + ) } - "Shape" in { - Typescript.render(decl[Shape]) shouldBe { + test("Shape") { + assertEquals( + Typescript.render(decl[Shape]), i""" export type Shape = { type: "Circle", radius: number, color: Color } | { type: "Rectangle", width: number, height: number, color: Color } | { type: "ShapeGroup", leftShape: Shape, rightShape: Shape }; """ - } + ) } - "Alpha" in { - Typescript.render(decl[Alpha]) shouldBe { + test("Alpha") { + assertEquals( + Typescript.render(decl[Alpha]), i""" export interface Alpha { name: string; @@ -62,22 +65,24 @@ class TsTypeRendererSpec extends AnyFreeSpec with Matchers { bool: boolean; } """ - } + ) } - "ArrayClass" in { - Typescript.render(decl[ArrayClass]) shouldBe { + test("ArrayClass") { + assertEquals( + Typescript.render(decl[ArrayClass]), i""" export interface ArrayClass { aList: string[]; optField?: number | null; } """ - } + ) } - "Numeric" in { - Typescript.render(decl[Numeric]) shouldBe { + test("Numeric") { + assertEquals( + Typescript.render(decl[Numeric]), i""" export interface Numeric { double: number; @@ -85,159 +90,176 @@ class TsTypeRendererSpec extends AnyFreeSpec with Matchers { int: number; } """ - } + ) } - "Singleton object" in { - Typescript.render(decl[MyObject.type]) shouldBe { + test("Singleton object") { + assertEquals( + Typescript.render(decl[MyObject.type]), i""" export interface MyObject { } """ - } + ) } - "ClassOrObject" in { - Typescript.render(decl[ClassOrObject]) shouldBe { + test("ClassOrObject") { + assertEquals( + Typescript.render(decl[ClassOrObject]), i""" export type ClassOrObject = { type: "MyClass", value: number } | { type: "MyObject" }; """ - } + ) } - "NestedClassOrObject" in { - Typescript.render(decl[NestedClassOrObject]) shouldBe { + test("NestedClassOrObject") { + assertEquals( + Typescript.render(decl[NestedClassOrObject]), i""" export type NestedClassOrObject = { type: "MyClass", value: number } | { type: "MyObject" }; """ - } + ) } - "Navigation" in { - Typescript.render(decl[Navigation]) shouldBe { + test("Navigation") { + assertEquals( + Typescript.render(decl[Navigation]), i""" export type Navigation = { type: "Node", name: string, children: Navigation[] } | { type: "NodeList", all: Navigation[] }; """ - } + ) } - "ClassUUID" in { - Typescript.render(decl[ClassUUID]) shouldBe { + test("ClassUUID") { + assertEquals( + Typescript.render(decl[ClassUUID]), i""" export interface ClassUUID { a: UUID; } """ - } + ) } - "ClassDate" in { - Typescript.render(decl[ClassDate]) shouldBe { + test("ClassDate") { + assertEquals( + Typescript.render(decl[ClassDate]), i""" export interface ClassDate { a: Date; } """ - } + ) } - "Recursive" in { - Typescript.render(decl[Recursive]) shouldBe { + test("Recursive") { + assertEquals( + Typescript.render(decl[Recursive]), i""" export interface Recursive { head: number; tail?: Recursive | null; } """ - } + ) } - "Recursive2" in { - Typescript.render(decl[Recursive2]) shouldBe { + test("Recursive2") { + assertEquals( + Typescript.render(decl[Recursive2]), i""" export interface Recursive2 { head: number; tail: Recursive2[]; } """ - } + ) } - "ExternalReferences" in { - Typescript.render(decl[ExternalReferences]) shouldBe { + test("ExternalReferences") { + assertEquals( + Typescript.render(decl[ExternalReferences]), i""" export interface ExternalReferences { color: Color; nav: Navigation; } """ - } + ) } - "ObjectsOnly" in { - Typescript.render(decl[ObjectsOnly]) shouldBe { + test("ObjectsOnly") { + assertEquals( + Typescript.render(decl[ObjectsOnly]), i""" export type ObjectsOnly = { type: "ObjectOne" } | { type: "ObjectTwo" }; """ - } + ) } - "Union of Union" in { - Typescript.render("A" := Ref("B") | Ref("C") | Ref("D")) shouldBe { + test("Union of Union") { + assertEquals( + Typescript.render(decl("A")(union(Ref("B"), Ref("C"), Ref("D")))), i""" export type A = B | C | D; """ - } + ) } - "Inter of Inter" in { - Typescript.render("A" := Ref("B") & Ref("C") & Ref("D")) shouldBe { + test("Inter of Inter") { + assertEquals( + Typescript.render(decl("A")(intersect(Ref("B"), Ref("C"), Ref("D")))), i""" export type A = B & C & D; """ - } + ) } - "Generic Decl" in { - Typescript.render( - decl("Pair", "A", "B")( - struct( - "a" --> Ref("A"), - "b" -?> Ref("B") + test("Generic Decl") { + assertEquals( + Typescript.render( + decl("Pair", "A", "B")( + struct( + "a" --> Ref("A"), + "b" -?> Ref("B") + ) ) - ) - ) shouldBe { + ), i""" export interface Pair { a: A; b?: B; } """ - } + ) } - "Applications of Generics" in { - Typescript.render(decl("Cell")(ref("Pair", Str, Intr))) shouldBe { + test("Applications of Generics") { + assertEquals( + Typescript.render(decl("Cell")(ref("Pair", Str, Intr))), i""" export type Cell = Pair; """ - } + ) - Typescript.render(decl("Same", "A")(ref("Pair", ref("A"), ref("A")))) shouldBe { + assertEquals( + Typescript.render(decl("Same", "A")(ref("Pair", ref("A"), ref("A")))), i""" export type Same = Pair; """ - } + ) - Typescript.render(decl("AnyPair")(ref("Pair", Any, Any))) shouldBe { + assertEquals( + Typescript.render(decl("AnyPair")(ref("Pair", Any, Any))), i""" export type AnyPair = Pair; """ - } + ) } - "Numeric types" in { - Typescript.render(decl[NumericTypes]) shouldBe { + test("Numeric types") { + assertEquals( + Typescript.render(decl[NumericTypes]), i""" export interface NumericTypes { int: number; @@ -247,42 +269,46 @@ class TsTypeRendererSpec extends AnyFreeSpec with Matchers { bigDecimal: number; } """ - } + ) } - "Tuple" in { - Typescript.render(decl("Cell")(tuple(Str, Intr))) shouldBe { + test("Tuple") { + assertEquals( + Typescript.render(decl("Cell")(tuple(Str, Intr))), i""" export type Cell = [string, number]; """ - } + ) } - "Empty tuple" in { - Typescript.render(decl("Empty")(tuple())) shouldBe { + test("Empty tuple") { + assertEquals( + Typescript.render(decl("Empty")(tuple())), i""" export type Empty = []; """ - } + ) } - "Structs with rest fields" in { - Typescript.render(decl("Dict")(dict(Str, Intr))) shouldBe { + test("Structs with rest fields") { + assertEquals( + Typescript.render(decl("Dict")(dict(Str, Intr))), i""" export interface Dict { [key: string]: number; } """ - } - - Typescript.render( - decl("Dict")( - struct( - "a" --> Str, - "b" -?> Intr - ).withRest(Str, Bool, "c") - ) - ) shouldBe { + ) + + assertEquals( + Typescript.render( + decl("Dict")( + struct( + "a" --> Str, + "b" -?> Intr + ).withRest(Str, Bool, "c") + ) + ), i""" export interface Dict { a: string; @@ -290,41 +316,43 @@ class TsTypeRendererSpec extends AnyFreeSpec with Matchers { [c: string]: boolean; } """ - } + ) } - "Unknown and any" in { - Typescript.render(decl("UnknownAndAny")(struct("foo" --> Any, "bar" --> Unknown))) shouldBe { + test("Unknown and any") { + assertEquals( + Typescript.render(decl("UnknownAndAny")(struct("foo" --> Any, "bar" --> Unknown))), i""" export interface UnknownAndAny { foo: any; bar: unknown; } """ - } + ) } - "Function types" in { - Typescript.render( - decl("Rule")( - struct( - "message" --> Str, - "apply" --> func("value" -> Unknown)(Bool) + test("Function types") { + assertEquals( + Typescript.render( + decl("Rule")( + struct( + "message" --> Str, + "apply" --> func("value" -> Unknown)(Bool) + ) ) - ) - ) shouldBe { + ), i""" export interface Rule { message: string; apply: (value: unknown) => boolean; } """ - } + ) - Typescript.render(decl("Funcy")(tuple(func("arg" -> tuple(Str))(tuple(Str)), func("arg" -> tuple(Intr))(tuple(Intr))))) shouldBe { + assertEquals( + Typescript.render(decl("Funcy")(tuple(func("arg" -> tuple(Str))(tuple(Str)), func("arg" -> tuple(Intr))(tuple(Intr))))), i""" export type Funcy = [(arg: [string]) => [string], (arg: [number]) => [number]]; """ - } + ) } -}