diff --git a/guinep/src/main/scala/macros.scala b/guinep/src/main/scala/macros.scala index 59cb223..5e18d7f 100644 --- a/guinep/src/main/scala/macros.scala +++ b/guinep/src/main/scala/macros.scala @@ -121,9 +121,13 @@ private[guinep] object macros { formElement private def functionFormElementFromTree(paramName: String, paramType: TypeRepr)(using FormConstrContext): FormElement = paramType match { - case ntpe: NamedType if ntpe.name == "String" => FormElement.TextInput(paramName) - case ntpe: NamedType if ntpe.name == "Int" => FormElement.NumberInput(paramName) - case ntpe: NamedType if ntpe.name == "Boolean" => FormElement.CheckboxInput(paramName) + case ntpe: NamedType if ntpe =:= TypeRepr.of[String] => FormElement.TextInput(paramName) + case ntpe: NamedType if ntpe =:= TypeRepr.of[Char] => FormElement.CharInput(paramName) + case ntpe: NamedType + if ntpe =:= TypeRepr.of[Int] || ntpe =:= TypeRepr.of[Long] || ntpe =:= TypeRepr.of[Short] || ntpe =:= TypeRepr.of[Byte] => + FormElement.NumberInput(paramName) + case ntpe: NamedType if ntpe =:= TypeRepr.of[Boolean] => FormElement.CheckboxInput(paramName) + case ntpe: NamedType if ntpe =:= TypeRepr.of[Double] || ntpe =:= TypeRepr.of[Float] => FormElement.FloatingNumberInput(paramName) case ntpe if isProductTpe(ntpe) => val classSymbol = ntpe.typeSymbol val typeDefParams = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isTypeParam) @@ -218,9 +222,14 @@ private[guinep] object macros { private def constructArg(paramTpe: TypeRepr, param: Term)(using ConstrContext): Term = { paramTpe match { - case ntpe: NamedType if ntpe.name == "String" => param.select("asInstanceOf").appliedToType(ntpe) - case ntpe: NamedType if ntpe.name == "Int" => param.select("asInstanceOf").appliedToType(ntpe) - case ntpe: NamedType if ntpe.name == "Boolean" => param.select("asInstanceOf").appliedToType(ntpe) + case ntpe: NamedType if ntpe =:= TypeRepr.of[String] => param.select("asInstanceOf").appliedToType(ntpe) + case ntpe: NamedType if ntpe =:= TypeRepr.of[Char] => param.select("asInstanceOf").appliedToType(ntpe) + case ntpe: NamedType + if ntpe =:= TypeRepr.of[Int] || ntpe =:= TypeRepr.of[Long] || ntpe =:= TypeRepr.of[Short] || ntpe =:= TypeRepr.of[Byte] => + param.select(s"asInstanceOf").appliedToType(TypeRepr.of[Long]).select(s"to${ntpe.name}") + case ntpe: NamedType if ntpe =:= TypeRepr.of[Boolean] => param.select("asInstanceOf").appliedToType(ntpe) + case ntpe: NamedType if ntpe =:= TypeRepr.of[Double] || ntpe =:= TypeRepr.of[Float] => + param.select(s"asInstanceOf").appliedToType(TypeRepr.of[Double]).select(s"to${ntpe.name}") case ntpe if isCaseObjectTpe(ntpe) && ntpe.typeSymbol.flags.is(Flags.Module) => Ref(ntpe.typeSymbol.companionModule) case ntpe if isCaseObjectTpe(ntpe) => diff --git a/guinep/src/main/scala/model.scala b/guinep/src/main/scala/model.scala index 47ce4be..552827e 100644 --- a/guinep/src/main/scala/model.scala +++ b/guinep/src/main/scala/model.scala @@ -24,7 +24,9 @@ private[guinep] object model { enum FormElement(val name: String): case TextInput(override val name: String) extends FormElement(name) + case CharInput(override val name: String) extends FormElement(name) case NumberInput(override val name: String) extends FormElement(name) + case FloatingNumberInput(override val name: String) extends FormElement(name) case CheckboxInput(override val name: String) extends FormElement(name) case Dropdown(override val name: String, options: List[(String, FormElement)]) extends FormElement(name) case TextArea(override val name: String, rows: Option[Int] = None, cols: Option[Int] = None) extends FormElement(name) @@ -36,7 +38,9 @@ private[guinep] object model { def constrOrd: Int = this match case TextInput(_) => 0 + case CharInput(_) => 0 case NumberInput(_) => 1 + case FloatingNumberInput(_) => 1 case CheckboxInput(_) => 2 case Dropdown(_, _) => 3 case TextArea(_, _, _) => 4 @@ -51,8 +55,12 @@ private[guinep] object model { s"""{ "name": '$name', "type": 'fieldset', "elements": [${elements.map(_.toJSONRepr).mkString(",")}] }""" case FormElement.TextInput(name) => s"""{ "name": '$name', "type": 'text' }""" + case FormElement.CharInput(name) => + s"""{ "name": '$name', "type": 'char' }""" case FormElement.NumberInput(name) => s"""{ "name": '$name', "type": 'number' }""" + case FormElement.FloatingNumberInput(name) => + s"""{ "name": '$name', "type": 'float' }""" case FormElement.CheckboxInput(name) => s"""{ "name": '$name', "type": 'checkbox' }""" case FormElement.Dropdown(name, options) => @@ -76,8 +84,12 @@ private[guinep] object model { '{ FormElement.FieldSet(${Expr(name)}, ${Expr(elements)}) } case FormElement.TextInput(name) => '{ FormElement.TextInput(${Expr(name)}) } + case FormElement.CharInput(name) => + '{ FormElement.CharInput(${Expr(name)}) } case FormElement.NumberInput(name) => '{ FormElement.NumberInput(${Expr(name)}) } + case FormElement.FloatingNumberInput(name) => + '{ FormElement.FloatingNumberInput(${Expr(name)}) } case FormElement.CheckboxInput(name) => '{ FormElement.CheckboxInput(${Expr(name)}) } case FormElement.Dropdown(name, options) => @@ -93,6 +105,7 @@ private[guinep] object model { case FormElement.NamedRef(name, ref) => '{ FormElement.NamedRef(${Expr(name)}, ${Expr(ref)}) } + // This ordering is a hack to avoid placing recursive constructors as first options in a dropdown given Ordering[FormElement] = new Ordering[FormElement] { def compare(x: FormElement, y: FormElement): Int = if x.constrOrd < y.constrOrd then -1 diff --git a/guinep/src/test/scala/formgentests.scala b/guinep/src/test/scala/formgentests.scala index 9b8c19c..2e11d6f 100644 --- a/guinep/src/test/scala/formgentests.scala +++ b/guinep/src/test/scala/formgentests.scala @@ -199,6 +199,70 @@ class FormGenTests extends munit.FunSuite { ) ) + checkGeneratedFormEquals( + "showDouble", + showDouble, + Form( + Seq( + FormElement.FloatingNumberInput("d") + ), + Map.empty + ) + ) + + checkGeneratedFormEquals( + "multiplyShorts", + multiplyShorts, + Form( + Seq( + FormElement.NumberInput("a"), + FormElement.NamedRef("b", "scala.Short") + ), + Map( + "scala.Short" -> FormElement.NumberInput("value") + ) + ) + ) + + checkGeneratedFormEquals( + "divideFloats", + divideFloats, + Form( + Seq( + FormElement.FloatingNumberInput("a"), + FormElement.NamedRef("b", "scala.Float") + ), + Map( + "scala.Float" -> FormElement.FloatingNumberInput("value") + ) + ) + ) + + checkGeneratedFormEquals( + "subtractLongs", + subtractLongs, + Form( + Seq( + FormElement.NumberInput("a"), + FormElement.NamedRef("b", "scala.Long") + ), + Map( + "scala.Long" -> FormElement.NumberInput("value") + ) + ) + ) + + checkGeneratedFormEquals( + "codeOfChar", + codeOfChar, + Form( + Seq( + FormElement.CharInput("c") + ), + Map.empty + ) + ) + checkGeneratedFormEquals( "isInTree", isInTree, diff --git a/guinep/src/test/scala/rungentests.scala b/guinep/src/test/scala/rungentests.scala index b05ead5..7a94de5 100644 --- a/guinep/src/test/scala/rungentests.scala +++ b/guinep/src/test/scala/rungentests.scala @@ -27,7 +27,7 @@ class RunGenTests extends munit.FunSuite { checkGeneratedRunResultEquals( "add", add, - List(1, 2), + List(1l, 2l), "3" ) @@ -55,7 +55,7 @@ class RunGenTests extends munit.FunSuite { checkGeneratedRunResultEquals( "addObj", addObj, - List(Map("a" -> 1, "b" -> 2)), + List(Map("a" -> 1l, "b" -> 2l)), "3" ) @@ -158,17 +158,52 @@ class RunGenTests extends munit.FunSuite { "hello" ) + checkGeneratedRunResultEquals( + "showDouble", + showDouble, + List(1.0d), + "1.0" + ) + + checkGeneratedRunResultEquals( + "multiplyShorts", + multiplyShorts, + List(1l, 2l), + "2" + ) + + checkGeneratedRunResultEquals( + "divideFloats", + divideFloats, + List(1.0d, 2.0d), + "0.5" + ) + + checkGeneratedRunResultEquals( + "subtractLongs", + subtractLongs, + List(2l, 1l), + "1" + ) + + checkGeneratedRunResultEquals( + "codeOfChar", + codeOfChar, + List('a'), + "97" + ) + checkGeneratedRunResultEquals( "isInTree", isInTree, - List(1, Map("name" -> "Node", "value" -> Map("left" -> Map("name" -> "Leaf", "value" -> Map.empty), "value" -> 1, "right" -> Map("name" -> "Leaf", "value" -> Map.empty)))), + List(1l, Map("name" -> "Node", "value" -> Map("left" -> Map("name" -> "Leaf", "value" -> Map.empty), "value" -> 1l, "right" -> Map("name" -> "Leaf", "value" -> Map.empty)))), "true" ) checkGeneratedRunResultEquals( "isInTree", isInTree, - List(1, Map("name" -> "Leaf", "value" -> Map.empty)), + List(1l, Map("name" -> "Leaf", "value" -> Map.empty)), "false" ) diff --git a/guinep/src/test/scala/testsdata.scala b/guinep/src/test/scala/testsdata.scala index 9681251..71fbeec 100644 --- a/guinep/src/test/scala/testsdata.scala +++ b/guinep/src/test/scala/testsdata.scala @@ -61,6 +61,21 @@ object TestsData { def concatAll(elems: List[String]): String = elems.mkString + def showDouble(d: Double): String = + d.toString + + def multiplyShorts(a: Short, b: Short): Int = + a * b + + def divideFloats(a: Float, b: Float): Float = + a / b + + def subtractLongs(a: Long, b: Long): Long = + a - b + + def codeOfChar(c: Char): Int = + c.toInt + enum IntTree: case Leaf case Node(left: IntTree, value: Int, right: IntTree) diff --git a/testcases/src/main/scala/main.scala b/testcases/src/main/scala/main.scala index dc3c215..c1f5c3c 100644 --- a/testcases/src/main/scala/main.scala +++ b/testcases/src/main/scala/main.scala @@ -65,6 +65,15 @@ def printsWeirdGADT(g: WeirdGADT[String]): String = g match def concatAll(elems: List[String]): String = elems.mkString +def showDouble(d: Double): String = + d.toString + +def divideFloats(a: Float, b: Float): Float = + a / b + +def codeOfChar(c: Char): Int = + c.toInt + enum IntTree: case Leaf case Node(left: IntTree, value: Int, right: IntTree) @@ -99,6 +108,9 @@ def run: Unit = roll20, roll6(), concatAll, + showDouble, + divideFloats, + codeOfChar, isInTree, // isInTreeExt // addManyParamLists diff --git a/web/src/main/scala/api.scala b/web/src/main/scala/api.scala index 4fd8729..5165366 100644 --- a/web/src/main/scala/api.scala +++ b/web/src/main/scala/api.scala @@ -20,4 +20,4 @@ inline def web(inline functions: Any*): Unit = |Ignoring duplicates""".stripMargin ) println("Starting GUInep web server at http://localhost:8090/") - webgen.genWeb(functionsInfosMap.view.mapValues(_.head).toMap) + webgen.genWeb(functionsInfos.distinct.map(fun => fun.name -> fun)) diff --git a/web/src/main/scala/htmlgen.scala b/web/src/main/scala/htmlgen.scala index e7f51c5..f7de7bc 100644 --- a/web/src/main/scala/htmlgen.scala +++ b/web/src/main/scala/htmlgen.scala @@ -8,7 +8,7 @@ import zio.http.template.* import zio.http.codec.* private[guinep] trait HtmlGen { - val funs: Map[String, Fun] + val funs: Seq[(String, Fun)] def generateHtml = html( head( @@ -125,6 +125,32 @@ private[guinep] trait HtmlGen { const formElemFromLookup = namedLookup[formElem.ref]; formElemFromLookup.name = formElem.name; addFormElement(form, formElemFromLookup, namedLookup); + } else if (formElem.type == 'float') { + const label = document.createElement('label'); + label.innerText = formElem.name + ': '; + label.for = formElem.name; + form.appendChild(label); + const input = document.createElement('input'); + input.type = 'number'; + input.step = 'any'; + input.name = formElem.name; + input.id = formElem.name; + input.placeholder = formElem.name; + form.appendChild(input); + form.appendChild(br.cloneNode()); + } else if (formElem.type == 'char') { + const label = document.createElement('label'); + label.innerText = formElem.name + ': '; + label.for = formElem.name; + form.appendChild(label); + const input = document.createElement('input'); + input.type = 'text'; + input.maxLength = '1'; + input.name = formElem.name; + input.id = formElem.name; + input.placeholder = formElem.name; + form.appendChild(input); + form.appendChild(br.cloneNode()); } else { const label = document.createElement('label'); label.innerText = formElem.name + ': '; diff --git a/web/src/main/scala/serialization.scala b/web/src/main/scala/serialization.scala index 0c45beb..c6c59e0 100644 --- a/web/src/main/scala/serialization.scala +++ b/web/src/main/scala/serialization.scala @@ -37,7 +37,9 @@ private[guinep] object serialization: res <- elements.parseJSONValue(m) } yield res case FormElement.TextInput(_) => value.asString.toRight(s"Invalid string: $value") - case FormElement.NumberInput(_) => value.asString.flatMap(_.toIntOption).toRight(s"Invalid number: $value") + case FormElement.CharInput(_) => value.asString.flatMap(_.headOption).toRight(s"Invalid char: $value") + case FormElement.NumberInput(_) => value.asString.flatMap(_.toLongOption).toRight(s"Invalid number: $value") + case FormElement.FloatingNumberInput(_) => value.asString.flatMap(_.toDoubleOption).toRight(s"Invalid float: $value") case FormElement.CheckboxInput(_) => value.asBoolean.toRight(s"Invalid boolean: $value") case FormElement.Dropdown(_, options) => for { diff --git a/web/src/main/scala/webgen.scala b/web/src/main/scala/webgen.scala index 6486a76..177485d 100644 --- a/web/src/main/scala/webgen.scala +++ b/web/src/main/scala/webgen.scala @@ -13,7 +13,7 @@ import scala.util.chaining.* private[guinep] object webgen { - def genWeb(funs: Map[String, Fun]): Unit = { + def genWeb(funs: Seq[(String, Fun)]): Unit = { val ws = WebServer(funs) val runtime = Runtime.default Unsafe.unsafe { implicit unsafe => @@ -24,7 +24,8 @@ private[guinep] object webgen { } } - class WebServer(val funs: Map[String, Fun]) extends HtmlGen { + class WebServer(val funs: Seq[(String, Fun)]) extends HtmlGen { + val funsMap = funs.toMap val app: HttpApp[Any] = Routes( Method.GET / PathCodec.empty -> handler(Response.html(generateHtml)), @@ -35,7 +36,7 @@ private[guinep] object webgen { (for { str <- req.body.asString obj <- ZIO.fromEither(str.fromJson[Obj]) - fun = funs(name) + fun = funsMap(name) given Map[String, FormElement] = fun.form.namedFormElements inputsValuesMap <- ZIO.fromEither(fun.form.inputs.toList.parseJSONValue(obj)) inputsValues = fun.form.inputs.toList.sortByArgs(inputsValuesMap)