From 6603d7884ffc0b50adfe48185f4b7a0ca645ec62 Mon Sep 17 00:00:00 2001 From: Olivier NOUGUIER Date: Fri, 27 Sep 2024 18:24:42 +0200 Subject: [PATCH] Refactor form derivation in Tree and ListElement samples --- .../src/main/scala/samples/EitherSample.scala | 2 +- .../src/main/scala/samples/EnumSample.scala | 6 +- .../src/main/scala/samples/ListElement.scala | 4 +- .../src/main/scala/samples/Persons.scala | 6 +- .../src/main/scala/samples/Sealed.scala | 10 +- .../client/src/main/scala/samples/Tree.scala | 4 +- .../src/main/scala/samples/Validation.scala | 2 +- .../src/main/scala/samples/package.scala | 6 +- .../scala/dev/cheleb/scalamigen/Form.scala | 180 +++++++++++------- .../scala/dev/cheleb/scalamigen/package.scala | 12 ++ 10 files changed, 144 insertions(+), 88 deletions(-) diff --git a/examples/client/src/main/scala/samples/EitherSample.scala b/examples/client/src/main/scala/samples/EitherSample.scala index ec26743..bf040bd 100644 --- a/examples/client/src/main/scala/samples/EitherSample.scala +++ b/examples/client/src/main/scala/samples/EitherSample.scala @@ -12,7 +12,7 @@ val either = { either: Either[Cat, Dog], primitiveEither: Either[Cat, String], optionalInt: Option[Int] - ) derives Form + ) val eitherVar = Var( EitherSample( diff --git a/examples/client/src/main/scala/samples/EnumSample.scala b/examples/client/src/main/scala/samples/EnumSample.scala index 9e4087d..9a51404 100644 --- a/examples/client/src/main/scala/samples/EnumSample.scala +++ b/examples/client/src/main/scala/samples/EnumSample.scala @@ -9,12 +9,12 @@ import com.raquo.airstream.state.Var import com.raquo.laminar.api.L val enums = { - enum Color(val code: String) derives Form: + enum Color(val code: String): case Black extends Color("000") case White extends Color("FFF") case Isabelle extends Color("???") - case class Basket(color: Color, cat: Cat) derives Form + case class Basket(color: Color, cat: Cat) given colorForm: Form[Color] = enumForm(Color.values, Color.fromOrdinal) @@ -22,7 +22,7 @@ val enums = { name: String, age: Int, color: Color - ) derives Form + ) val eitherVar = Var( Basket(Color.Black, Cat("Scala", 10, Color.White)) diff --git a/examples/client/src/main/scala/samples/ListElement.scala b/examples/client/src/main/scala/samples/ListElement.scala index a63de7d..10f13a1 100644 --- a/examples/client/src/main/scala/samples/ListElement.scala +++ b/examples/client/src/main/scala/samples/ListElement.scala @@ -5,11 +5,11 @@ import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* val list = { - case class Person2(id: Int, name: String, age: Int) derives Form + case class Person2(id: Int, name: String, age: Int) case class ListElement( ints: List[Person2] - ) derives Form + ) given (Person2 => Int) = _.id diff --git a/examples/client/src/main/scala/samples/Persons.scala b/examples/client/src/main/scala/samples/Persons.scala index 13a127e..71c0506 100644 --- a/examples/client/src/main/scala/samples/Persons.scala +++ b/examples/client/src/main/scala/samples/Persons.scala @@ -23,15 +23,15 @@ val person = { email: Option[String], age: BigInt, size: Double - ) derives Form + ) case class Pet( name: String, age: BigInt, House: House, size: Double :| Positive - ) derives Form + ) - case class House(capacity: Int) derives Form + case class House(capacity: Int) // Provide default for optional given Defaultable[Pet] with diff --git a/examples/client/src/main/scala/samples/Sealed.scala b/examples/client/src/main/scala/samples/Sealed.scala index 24f772a..c2cd50d 100644 --- a/examples/client/src/main/scala/samples/Sealed.scala +++ b/examples/client/src/main/scala/samples/Sealed.scala @@ -8,14 +8,14 @@ import com.raquo.airstream.state.Var import com.raquo.laminar.nodes.ReactiveHtmlElement val sealedClasses = { - sealed trait Animal derives Form + sealed trait Animal - case class Horse(name: String, age: Int) extends Animal derives Form + case class Horse(name: String, age: Int) extends Animal case class Lama(name: String, age: Int, splitDistance: Int) extends Animal - derives Form - case class Otter(name: String, age: Int) extends Animal derives Form - case class Owner(name: String, pet: Animal) derives Form + case class Otter(name: String, age: Int) extends Animal + + case class Owner(name: String, pet: Animal) Sample( "Sealed", { diff --git a/examples/client/src/main/scala/samples/Tree.scala b/examples/client/src/main/scala/samples/Tree.scala index 40a4290..fb90d68 100644 --- a/examples/client/src/main/scala/samples/Tree.scala +++ b/examples/client/src/main/scala/samples/Tree.scala @@ -5,10 +5,10 @@ import dev.cheleb.scalamigen.* import com.raquo.laminar.api.L.* import com.raquo.airstream.state.Var -import dev.cheleb.scalamigen.FormDerive.autoDerived + val tree = { - enum Tree[+T] derives Form: + enum Tree[+T]: case Empty extends Tree[Nothing] case Node(value: T, left: Tree[T], right: Tree[T]) object Tree: diff --git a/examples/client/src/main/scala/samples/Validation.scala b/examples/client/src/main/scala/samples/Validation.scala index 8b8bf6e..83d4926 100644 --- a/examples/client/src/main/scala/samples/Validation.scala +++ b/examples/client/src/main/scala/samples/Validation.scala @@ -19,7 +19,7 @@ val validation = { optionalInt: Option[Int], doubleGreaterThanEight: Double :| GreaterEqual[8.0], optionalDoublePositive: Option[Double :| Positive] - ) derives Form + ) given IronTypeValidator[Double, GreaterEqual[8.0]] = _.toDoubleOption match diff --git a/examples/client/src/main/scala/samples/package.scala b/examples/client/src/main/scala/samples/package.scala index f314b35..a0c82b7 100644 --- a/examples/client/src/main/scala/samples/package.scala +++ b/examples/client/src/main/scala/samples/package.scala @@ -11,10 +11,10 @@ object CurrencyCode: opaque type Password = String object Password: def apply(password: String): Password = password - given Form[Password] = Form.secretForm(apply) + given Form[Password] = secretForm(apply) -case class Cat(name: String, weight: Int, kind: Boolean = true) derives Form -case class Dog(name: String, weight: Int) derives Form +case class Cat(name: String, weight: Int, kind: Boolean = true) +case class Dog(name: String, weight: Int) given Defaultable[Cat] with def default = Cat("", 0) diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala index fbddb7c..d226ece 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/Form.scala @@ -9,7 +9,6 @@ import org.scalajs.dom.HTMLDivElement import org.scalajs.dom.HTMLElement import com.raquo.laminar.nodes.ReactiveHtmlElement import magnolia1.SealedTrait.Subtype -import scala.deriving.Mirror import java.time.LocalDate import io.github.iltotore.iron.* @@ -58,6 +57,16 @@ trait Form[A] { self => given Owner = unsafeWindowOwner + /* + + +
+
Left
+
Right
+
+ + + */ def labelled(name: String, required: Boolean): Form[A] = new Form[A] { override def render( variable: Var[A], @@ -83,7 +92,24 @@ trait Form[A] { self => } -object Form { +object Form extends AutoDerivation[Form] { + + type Typeclass[T] = Form[T] + + /** Render a variable with a form. + * + * @param v + * the variable to render + * @param syncParent + * a function to sync the parent state + * @param factory + * the widget factory + * @param fa + * the form for the variable, either given or derived by magnolia <3 + * @tparam A + * the type of the variable + * @return + */ def renderVar[A](v: Var[A], syncParent: () => Unit = () => ())(using WidgetFactory )(using @@ -91,14 +117,19 @@ object Form { ): ReactiveHtmlElement[HTMLElement] = fa.render(v, syncParent) - /** Use this form to render a string that can be converted to A, can be used - * for Opaque types. - */ - /** Form for an Iron type. This is a form for a type that can be validated * with an Iron type. + * @param validator + * the Iron type validator + * @tparam T + * the base type of the Iron type + * @tparam C + * the type of the Iron type contraint */ - given [T, C](using fv: IronTypeValidator[T, C]): Form[IronType[T, C]] = + given [T, C](using + validator: IronTypeValidator[T, C], + widgetFactory: WidgetFactory + ): Form[IronType[T, C]] = new Form[IronType[T, C]] { override def render( @@ -113,18 +144,19 @@ object Form { s"$item" ) }), - input( - // _.showClearIcon := true, - backgroundColor <-- errorVar.signal.map { - case "" => "white" - case _ => "red" - }, - value <-- variable.signal.map(toString(_)), - onInput.mapToValue --> { str => - fromString(str, variable, errorVar) - - } - ) + widgetFactory.renderText + .amend( + // _.showClearIcon := true, + backgroundColor <-- errorVar.signal.map { + case "" => "white" + case _ => "red" + }, + value <-- variable.signal.map(toString(_)), + onInput.mapToValue --> { str => + fromString(str, variable, errorVar) + + } + ) ) override def fromString( @@ -132,7 +164,7 @@ object Form { variable: Var[IronType[T, C]], errorVar: Var[String] ): Unit = - fv.validate(str) match + validator.validate(str) match case Left(error) => errorVar.set(error) case Right(value) => @@ -156,6 +188,8 @@ object Form { } ) + /** Form for a Nothing, not sure it is still really needed :-/ + */ given Form[Nothing] = new Form[Nothing] { override def render( variable: Var[Nothing], @@ -164,6 +198,10 @@ object Form { div() } + /** Form for a Boolean. + * + * Basically a checkbox. + */ given Form[Boolean] = new Form[Boolean] { override def render( variable: Var[Boolean], @@ -180,16 +218,44 @@ object Form { ) ) } + + /** Form for an Double. + */ given Form[Double] = numericForm(_.toDoubleOption, 0) + + /** Form for an Int. + */ given Form[Int] = numericForm(_.toIntOption, 0) + + /** Form for an Float. + */ given Form[Float] = numericForm(_.toFloatOption, 0) + + /** Form for an Long. + */ + given Form[Long] = numericForm(_.toLongOption, 0) + + /** Form for a BigInt. + */ given Form[BigInt] = numericForm(str => Try(BigInt(str)).toOption, BigInt(0)) + /** Form for a BigDecimal. + */ given Form[BigDecimal] = numericForm(str => Try(BigDecimal(str)).toOption, BigDecimal(0)) - // given - + /** Form for a either of L or R + * + * @param lf + * the left form for a L, given or derived by magnolia + * @param rf + * the right form for a R, given or derived by magnolia + * @param ld + * the default value for a L + * @param rd + * the default value for a R + * @return + */ given eitherOf[L, R](using lf: Form[L], rf: Form[R], @@ -241,9 +307,19 @@ object Form { } + /** Form for an Option[A] + * + * Render with clear button if the value is Some, else render with a set new + * value button. + * @param fa + * the form for A + * @param d + * the default value for A + * @return + */ given optionOfA[A](using - d: Defaultable[A], - fa: Form[A] + fa: Form[A], + d: Defaultable[A] ): Form[Option[A]] = new Form[Option[A]] { override def render( @@ -293,6 +369,14 @@ object Form { ) } + /** Form for a List[A] + * @param fa + * the form for A + * @param idOf + * a function to get the id of an A, important for the split function. + * @return + */ + given listOfA[A, K](using fa: Form[A], idOf: A => K): Form[List[A]] = new Form[List[A]] { @@ -312,6 +396,10 @@ object Form { ) } + /** Form for a LocalDate + * + * Render a date picker. // FIXME should be able to set the format + */ given Form[LocalDate] = new Form[LocalDate] { override def render( variable: Var[LocalDate], @@ -329,24 +417,6 @@ object Form { ) } - def secretForm[A <: String](to: String => A) = new Form[A]: - override def render( - variable: Var[A], - syncParent: () => Unit - )(using factory: WidgetFactory): HtmlElement = - factory.renderSecret.amend( - value <-- variable.signal, - onInput.mapToValue.map(to) --> { v => - variable.set(v) - syncParent() - } - ) - -} - -object FormDerive extends AutoDerivation[Form] { - - type Typeclass[T] = Form[T] def join[A]( caseClass: CaseClass[Typeclass, A] ): Form[A] = new Form[A] { @@ -403,11 +473,6 @@ object FormDerive extends AutoDerivation[Form] { } } - /** Split a sealed trait into a form - * - * @param sealedTrait - * @return - */ def split[A](sealedTrait: SealedTrait[Form, A]): Form[A] = new Form[A] { override def render( @@ -415,24 +480,6 @@ object FormDerive extends AutoDerivation[Form] { syncParent: () => Unit )(using factory: WidgetFactory): HtmlElement = val a = variable.now() - // val values = sealedTrait.subtypes.map(_.typeInfo.short) - // div( - // factory - // .renderSelect(_ => ()) - // .amend( - // sealedTrait.subtypes - // .map(_.typeInfo.short) - // .map { label => - // factory.renderOption( - // label, - // values - // .map(_.toString) - // .indexOf(label), - // label == variable.now().toString - // ) - // } - // .toSeq - // ), sealedTrait.choose(a) { sub => val va = Var(sub.cast(a)) sub.typeclass @@ -461,6 +508,3 @@ object FormDerive extends AutoDerivation[Form] { string.split("(?=[A-Z])").map(_.capitalize).mkString(" ") } - -extension [A]($ : Form.type)(using Mirror.Of[A]) - inline def derived: Form[A] = FormDerive.derived[A] diff --git a/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala b/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala index 4bcef64..ca4fff7 100644 --- a/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala +++ b/modules/core/src/main/scala/dev/cheleb/scalamigen/package.scala @@ -16,6 +16,18 @@ def stringForm[A](to: String => A) = new Form[A]: syncParent() } ) +def secretForm[A <: String](to: String => A) = new Form[A]: + override def render( + variable: Var[A], + syncParent: () => Unit + )(using factory: WidgetFactory): HtmlElement = + factory.renderSecret.amend( + value <-- variable.signal, + onInput.mapToValue.map(to) --> { v => + variable.set(v) + syncParent() + } + ) /** Form for a numeric type. */