diff --git a/guinep/src/main/scala/macros.scala b/guinep/src/main/scala/macros.scala index 368e8e0..a135afa 100644 --- a/guinep/src/main/scala/macros.scala +++ b/guinep/src/main/scala/macros.scala @@ -37,13 +37,20 @@ private[guinep] object macros { extension (t: Term) private def select(s: Term): Term = Select(t, s.symbol) private def select(s: String): Term = - t.select( - t.tpe - .typeSymbol - .methodMember(s) - .headOption. - getOrElse(report.errorAndAbort(s"PANIC: No member $s in term ${t.show} with type ${t.tpe.show}")) - ) + val sym = t.tpe + .typeSymbol + .methodMember(s) + .headOption + .getOrElse(report.errorAndAbort(s"PANIC: No member $s in term ${t.show} with type ${t.tpe.show}")) + Select(t, sym) + private def select(s: String, argnum: Int): Term = + val sym = t.tpe + .typeSymbol + .methodMember(s) + .filter(_.paramSymss.flatten.size == argnum) + .headOption + .getOrElse(report.errorAndAbort(s"PANIC: No member $s in term ${t.show} with type ${t.tpe.show}")) + Select(t, sym) extension (s: Symbol) private def prettyName: String = @@ -133,12 +140,18 @@ private[guinep] object macros { FormElement.NumberInput(paramName, Types.IntType.Short) case ntpe: NamedType if ntpe =:= TypeRepr.of[Byte] => FormElement.NumberInput(paramName, Types.IntType.Byte) + case ntpe: NamedType if ntpe =:= TypeRepr.of[BigInt] => + FormElement.NumberInput(paramName, Types.IntType.BigInt) case ntpe: NamedType if ntpe =:= TypeRepr.of[Boolean] => FormElement.CheckboxInput(paramName) case ntpe: NamedType if ntpe =:= TypeRepr.of[Float] => FormElement.FloatingNumberInput(paramName, Types.FloatingType.Float) case ntpe: NamedType if ntpe =:= TypeRepr.of[Double] => FormElement.FloatingNumberInput(paramName, Types.FloatingType.Double) + case ntpe: NamedType if ntpe =:= TypeRepr.of[BigDecimal] => + FormElement.FloatingNumberInput(paramName, Types.FloatingType.BigDecimal) + case ntpe: NamedType if ntpe =:= TypeRepr.of[Unit] => + FormElement.HiddenInput(paramName, "Unit") case AppliedType(ntpe: NamedType, List(tpeArg)) if listLikeSymbolsTypes.contains(ntpe.typeSymbol) => FormElement.ListInput(paramName, functionFormElementFromTreeWithCaching("elem", tpeArg), listLikeSymbolsTypes(ntpe.typeSymbol)) case OrType(ltpe, rtpe) if ltpe =:= TypeRepr.of[Null] || rtpe =:= TypeRepr.of[Null] => @@ -249,20 +262,22 @@ private[guinep] object macros { private def constructArg(paramTpe: TypeRepr, param: Term)(using ConstrContext): Term = { paramTpe match { case ntpe: NamedType if ntpe =:= TypeRepr.of[String] => - param.select("asInstanceOf").appliedToType(ntpe) + param.select("asInstanceOf", 1).appliedToType(ntpe) case ntpe: NamedType if ntpe =:= TypeRepr.of[Char] => - param.select("asInstanceOf").appliedToType(ntpe) + param.select("asInstanceOf", 1).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(ntpe) + if ntpe =:= TypeRepr.of[Int] || ntpe =:= TypeRepr.of[Long] || ntpe =:= TypeRepr.of[Short] || ntpe =:= TypeRepr.of[Byte] || ntpe =:= TypeRepr.of[BigInt] => + param.select(s"asInstanceOf", 1).appliedToType(ntpe) 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(ntpe) + param.select("asInstanceOf", 1).appliedToType(ntpe) + case ntpe: NamedType if ntpe =:= TypeRepr.of[Double] || ntpe =:= TypeRepr.of[Float] || ntpe =:= TypeRepr.of[BigDecimal] => + param.select(s"asInstanceOf", 1).appliedToType(ntpe) + case ntpe: NamedType if ntpe =:= TypeRepr.of[Unit] => + param.select("asInstanceOf", 1).appliedToType(ntpe) case AppliedType(ntpe: NamedType, List(tpeArg)) if listLikeSymbolsTypes.contains(ntpe.typeSymbol) => - param.select("asInstanceOf").appliedToType(paramTpe) + param.select("asInstanceOf", 1).appliedToType(paramTpe) case OrType(ltpe, rtpe) if ltpe =:= TypeRepr.of[Null] || rtpe =:= TypeRepr.of[Null] => - val castedParam = param.select("asInstanceOf").appliedToType(paramTpe) + val castedParam = param.select("asInstanceOf", 1).appliedToType(paramTpe) '{ if ${param.asExpr} == null then null else ${castedParam.asExpr} }.asTerm case ntpe if isCaseObjectTpe(ntpe) && ntpe.typeSymbol.flags.is(Flags.Module) => Ref(ntpe.typeSymbol.companionModule) @@ -275,7 +290,7 @@ private[guinep] object macros { val paramValue = '{ ${param.asExpr}.asInstanceOf[Map[String, Any]] }.asTerm val args = fields.collect { case field: ValDef => val fieldName = field.name - val fieldValue = paramValue.select("apply").appliedTo(Literal(StringConstant(fieldName))) + val fieldValue = paramValue.select("apply", 1).appliedTo(Literal(StringConstant(fieldName))) constructArgWithCaching( field.tpt.tpe.substituteTypes(typeDefParams, ntpe.typeArgs), fieldValue @@ -288,14 +303,14 @@ private[guinep] object macros { val children = classSymbol.children val childrenAppliedTpes = children.map(child => appliedChild(child, classSymbol, ntpe.typeArgs)).map(_.stripAnnots) val paramMap = '{ ${param.asExpr}.asInstanceOf[Map[String, Any]] }.asTerm - val paramName = paramMap.select("apply").appliedTo(Literal(StringConstant("name"))) - val paramValue = paramMap.select("apply").appliedTo(Literal(StringConstant("value"))) + val paramName = paramMap.select("apply", 1).appliedTo(Literal(StringConstant("name"))) + val paramValue = paramMap.select("apply", 1).appliedTo(Literal(StringConstant("value"))) children.zip(childrenAppliedTpes).foldRight[Term]{ '{ throw new RuntimeException(s"Class ${${paramName.asExpr}} is not a child of ${${Expr(className)}}") }.asTerm } { case ((child, childAppliedTpe), acc) => val childName = Literal(StringConstant(child.prettyName)) If( - paramName.select("equals").appliedTo(childName), + paramName.select("equals", 1).appliedTo(childName), constructArgWithCaching(childAppliedTpe, paramValue), acc ) @@ -316,7 +331,7 @@ private[guinep] object macros { { case (sym, List(params: Term)) => val args = functionParams(f).zipWithIndex.map { case (valdef, i) => val paramTpe = valdef.tpt.tpe - val param = params.select("apply").appliedTo(Literal(IntConstant(i))) + val param = params.select("apply", 1).appliedTo(Literal(IntConstant(i))) constructArgWithCaching(paramTpe, param) }.toList val aply = l.select("apply") @@ -325,7 +340,7 @@ private[guinep] object macros { aply.appliedToNone else aply.appliedToArgs(args) - res.select("toString").appliedToNone + res.select("toString", 0).appliedToNone } ) Block( @@ -337,7 +352,7 @@ private[guinep] object macros { Symbol.spliceOwner, MethodType(List("inputs"))(_ => List(TypeRepr.of[List[Any]]), _ => TypeRepr.of[String]), { case (sym, List(params: Term)) => - t.select("toString").appliedToNone + t.select("toString", 0).appliedToNone } ).asExprOf[List[Any] => String] case a@Apply(Ident(_), Nil) => @@ -345,7 +360,7 @@ private[guinep] object macros { Symbol.spliceOwner, MethodType(List("inputs"))(_ => List(TypeRepr.of[List[Any]]), _ => TypeRepr.of[String]), { case (sym, List(params: Term)) => - a.select("toString").appliedToNone + a.select("toString", 0).appliedToNone } ).asExprOf[List[Any] => String] case _ => diff --git a/guinep/src/main/scala/model.scala b/guinep/src/main/scala/model.scala index 26cda6e..e91e9a3 100644 --- a/guinep/src/main/scala/model.scala +++ b/guinep/src/main/scala/model.scala @@ -16,6 +16,7 @@ private[guinep] object model { enum FloatingType: case Double case Float + case BigDecimal object FloatingType: given ToExpr[FloatingType] with @@ -24,12 +25,15 @@ private[guinep] object model { '{ FloatingType.Double } case FloatingType.Float => '{ FloatingType.Float } + case FloatingType.BigDecimal => + '{ FloatingType.BigDecimal } enum IntType: case Int case Long case Byte case Short + case BigInt object IntType: given ToExpr[IntType] with @@ -42,6 +46,8 @@ private[guinep] object model { '{ IntType.Byte } case IntType.Short => '{ IntType.Short } + case IntType.BigInt => + '{ IntType.BigInt } enum ListType: case List @@ -65,6 +71,7 @@ private[guinep] object model { case NumberInput(override val name: String, underlying: Types.IntType) extends FormElement(name) case FloatingNumberInput(override val name: String, underlying: Types.FloatingType) extends FormElement(name) case CheckboxInput(override val name: String) extends FormElement(name) + case HiddenInput(override val name: String, underlying: String) extends FormElement(name) case Dropdown(override val name: String, options: List[(String, FormElement)]) extends FormElement(name) case ListInput(override val name: String, element: FormElement, underlying: Types.ListType) extends FormElement(name) case TextArea(override val name: String, rows: Option[Int] = None, cols: Option[Int] = None) extends FormElement(name) @@ -80,6 +87,7 @@ private[guinep] object model { case CharInput(_) => 0 case NumberInput(_, _) => 1 case FloatingNumberInput(_, _) => 1 + case HiddenInput(_, _) => 1 case CheckboxInput(_) => 2 case Dropdown(_, _) => 3 case ListInput(_, _, _) => 3 @@ -106,6 +114,8 @@ private[guinep] object model { '{ FormElement.FloatingNumberInput(${Expr(name)}, ${Expr(underlying)}) } case FormElement.CheckboxInput(name) => '{ FormElement.CheckboxInput(${Expr(name)}) } + case FormElement.HiddenInput(name, underlying) => + '{ FormElement.HiddenInput(${Expr(name)}, ${Expr(underlying)}) } case FormElement.Dropdown(name, options) => '{ FormElement.Dropdown(${Expr(name)}, ${Expr(options)}) } case FormElement.ListInput(name, element, underlying) => diff --git a/guinep/src/test/scala/formgentests.scala b/guinep/src/test/scala/formgentests.scala index 4e8eb84..bfa7aa5 100644 --- a/guinep/src/test/scala/formgentests.scala +++ b/guinep/src/test/scala/formgentests.scala @@ -276,6 +276,39 @@ class FormGenTests extends munit.FunSuite { ) ) + checkGeneratedFormEquals( + "factorialBigInt", + factorialBigInt, + Form( + Seq( + FormElement.NumberInput("n", Types.IntType.BigInt) + ), + Map.empty + ) + ) + + checkGeneratedFormEquals( + "inverseBigDecimal", + inverseBigDecimal, + Form( + Seq( + FormElement.FloatingNumberInput("bd", Types.FloatingType.BigDecimal) + ), + Map.empty + ) + ) + + checkGeneratedFormEquals( + "sayBye", + sayBye, + Form( + Seq( + FormElement.HiddenInput("unit", "Unit") + ), + Map.empty + ) + ) + checkGeneratedFormEquals( "isInTree", isInTree, diff --git a/guinep/src/test/scala/rungentests.scala b/guinep/src/test/scala/rungentests.scala index 36580d3..22772af 100644 --- a/guinep/src/test/scala/rungentests.scala +++ b/guinep/src/test/scala/rungentests.scala @@ -221,6 +221,27 @@ class RunGenTests extends munit.FunSuite { "1" ) + checkGeneratedRunResultEquals( + "factorialBigInt", + factorialBigInt, + List(BigInt(5)), + "120" + ) + + checkGeneratedRunResultEquals( + "inverseBigDecimal", + inverseBigDecimal, + List(BigDecimal(2)), + "0.5" + ) + + checkGeneratedRunResultEquals( + "sayBye", + sayBye, + List("Unit"), + "Bye!" + ) + checkGeneratedRunResultEquals( "isInTree", isInTree, diff --git a/guinep/src/test/scala/testsdata.scala b/guinep/src/test/scala/testsdata.scala index c3838cc..cf2e75f 100644 --- a/guinep/src/test/scala/testsdata.scala +++ b/guinep/src/test/scala/testsdata.scala @@ -85,6 +85,15 @@ object TestsData { def showNullableInt(i: Int | Null): String = if i == null then "null" else i.toString + def factorialBigInt(n: BigInt): BigInt = + if n == 0 then 1 else n * factorialBigInt(n - 1) + + def inverseBigDecimal(bd: BigDecimal): BigDecimal = + BigDecimal(1) / bd + + def sayBye(unit: Unit): String = + "Bye!" + 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 ec2caca..5e1e494 100644 --- a/testcases/src/main/scala/main.scala +++ b/testcases/src/main/scala/main.scala @@ -105,10 +105,23 @@ def seqProduct(seq: Seq[Float]): Float = def showNullableInt(i: Int | Null): String = if i == null then "null" else i.toString +def factorialBigInt(n: BigInt): BigInt = + if n == 0 then 1 else n * factorialBigInt(n - 1) + +def inverseBigDecimal(bd: BigDecimal): BigDecimal = + BigDecimal(1) / bd + +def sayBye(unit: Unit): String = + "Bye!" + +case class TakesUnit(unit: Unit) + +def runTakesUnit(takesUnit: TakesUnit): Unit = + () + @main def run: Unit = guinep.web - .withModifyConfig(_.copy(requireNonNullableInputs = true)) .apply( upperCaseText, add, @@ -129,7 +142,11 @@ def run: Unit = listProduct, sumVector, seqProduct, - showNullableInt + showNullableInt, + factorialBigInt, + inverseBigDecimal, + sayBye, + runTakesUnit, // isInTreeExt // addManyParamLists // printsWeirdGADT diff --git a/web/src/main/scala/htmlgen.scala b/web/src/main/scala/htmlgen.scala index e32e0ee..7a92dee 100644 --- a/web/src/main/scala/htmlgen.scala +++ b/web/src/main/scala/htmlgen.scala @@ -18,7 +18,7 @@ private[guinep] trait HtmlGen { style( """ body { font-family: Arial, sans-serif; } - .sidebar { position: fixed; left: 0; top: 0; width: 200px; height: 100vh; background-color: #f0f0f0; padding: 20px; } + .sidebar { position: fixed; left: 0; top: 0; width: 200px; height: 100vh; background-color: #f0f0f0; padding: 20px; overflow-y: auto; } .sidebar a { display: block; padding: 10px; margin-bottom: 10px; background-color: #007bff; color: white; text-decoration: none; text-align: center; border-radius: 5px; } .sidebar a:hover { background-color: #0056b3; } .main-content { margin-left: 232px; padding: 40px; display: flex; justify-content: center; padding-top: 20px; } @@ -29,8 +29,8 @@ private[guinep] trait HtmlGen { input[type=submit]:hover { background-color: #45a049; } select { display: inline-block; margin-bottom: 10px; padding: 8px; box-sizing: border-box; } .result { margin-top: 20px; font-weight: bold; } - .button { text-decoration: none; background-color: #AAAAAA; color: white; padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; } - .button:hover { background-color: #BBBBBB; } + .add-button { text-decoration: none; background-color: #AAAAAA; color: white; padding: 5px 10px; border: none; border-radius: 4px; cursor: pointer; } + .add-button:hover { background-color: #BBBBBB; } """ ), script(Dom.raw(jsToChangeFormBasedOnPath)), @@ -138,7 +138,7 @@ private[guinep] trait HtmlGen { const button = document.createElement('a'); button.innerText = '+'; button.href = '#'; - button.classList.add('button'); + button.classList.add('add-button'); button.onclick = () => { addFormElement(fieldset, formElem.element, namedLookup, button); }; @@ -187,13 +187,16 @@ private[guinep] trait HtmlGen { form.insertBefore(input, before); form.insertBefore(br.cloneNode(), before); } else { - const label = document.createElement('label'); - label.innerText = formElem.name + ': '; - label.for = formElem.name; - form.insertBefore(label, before); + if (formElem.type !== 'hidden') { + const label = document.createElement('label'); + label.innerText = formElem.name + ': '; + label.for = formElem.name; + form.insertBefore(label, before); + } const input = document.createElement('input'); input.type = formElem.type; input.name = formElem.name; + input.value = formElem.value; input.id = formElem.name; input.placeholder = formElem.name; if (formElem.nullable) { diff --git a/web/src/main/scala/serialization.scala b/web/src/main/scala/serialization.scala index f74cfd2..e3366a6 100644 --- a/web/src/main/scala/serialization.scala +++ b/web/src/main/scala/serialization.scala @@ -36,23 +36,32 @@ private[guinep] object serialization: m <- value.asObject.toRight(s"Invalid object: $value") res <- elements.parseJSONValue(m) } yield res - case FormElement.TextInput(_) => value.asString.toRight(s"Invalid string: $value") - case FormElement.CharInput(_) => value.asString.flatMap(_.headOption).toRight(s"Invalid char: $value") + case FormElement.TextInput(_) => + value.asString.toRight(s"Invalid string: $value") + case FormElement.CharInput(_) => + value.asString.flatMap(_.headOption).toRight(s"Invalid char: $value") case FormElement.NumberInput(_, tpe) => tpe match - case guinep.model.Types.IntType.Int => + case Types.IntType.Int => value.asString.flatMap(_.toIntOption).toRight(s"Invalid int: $value") - case guinep.model.Types.IntType.Long => + case Types.IntType.Long => value.asString.flatMap(_.toLongOption).toRight(s"Invalid long: $value") - case guinep.model.Types.IntType.Byte => + case Types.IntType.Byte => value.asString.flatMap(_.toByteOption).toRight(s"Invalid byte: $value") - case guinep.model.Types.IntType.Short => + case Types.IntType.Short => value.asString.flatMap(_.toShortOption).toRight(s"Invalid short: $value") + case Types.IntType.BigInt => + value.asString.map(BigInt.apply).toRight(s"Invalid big int: $value") case FormElement.FloatingNumberInput(_, tpe) => tpe match - case guinep.model.Types.FloatingType.Double => + case Types.FloatingType.Double => value.asString.flatMap(_.toDoubleOption).toRight(s"Invalid double: $value") - case guinep.model.Types.FloatingType.Float => + case Types.FloatingType.Float => value.asString.flatMap(_.toFloatOption).toRight(s"Invalid float: $value") - case FormElement.CheckboxInput(_) => value.asBoolean.toRight(s"Invalid boolean: $value") + case Types.FloatingType.BigDecimal => + value.asString.map(BigDecimal.apply).toRight(s"Invalid big decimal: $value") + case FormElement.CheckboxInput(_) => + value.asBoolean.toRight(s"Invalid boolean: $value") + case FormElement.HiddenInput(_, "Unit") => + Right(()) case FormElement.Dropdown(_, options) => for { v <- value.asObject.toRight(s"Invalid object: $value") @@ -65,7 +74,8 @@ private[guinep] object serialization: "name" -> ddName, "value" -> res ) - case FormElement.TextArea(_, _, _) => Right(value) + case FormElement.TextArea(_, _, _) => + Right(value) case FormElement.NamedRef(name, ref) => val formElementFromLookup = formElementLookup(ref).modify(_.name).setTo(name) formElementFromLookup.parseJSONValue(value) @@ -74,14 +84,15 @@ private[guinep] object serialization: jsonLst <- value.asArray.map(_.toList).toRight(s"Invalid array $value") res <- sequenceEither(jsonLst.map(element.parseJSONValue)) } yield tpe match - case guinep.model.Types.ListType.List => res - case guinep.model.Types.ListType.Seq => res.toSeq - case guinep.model.Types.ListType.Vector => res.toVector + case Types.ListType.List => res + case Types.ListType.Seq => res.toSeq + case Types.ListType.Vector => res.toVector case FormElement.Nullable(_, element) => value match case Null => Right(null) case _ => element.parseJSONValue(value) - case _ => Left(s"Unsupported form element: $formElement") + case _ => + Left(s"Unsupported form element: $formElement") extension (form: Form) def formElementsJSONRepr = @@ -108,6 +119,8 @@ private[guinep] object serialization: s"""{ "name": '$name', "type": 'float', "nullable": $nullable }""" case FormElement.CheckboxInput(name) => s"""{ "name": '$name', "type": 'checkbox' }""" + case FormElement.HiddenInput(name, underlying) => + s"""{ "name": '$name', "type": 'hidden', "value": '$underlying' }""" case FormElement.Dropdown(name, options) => // TODO(kπ) this sortBy isn't 100% sure to be working (the only requirement is for the first constructor to not be recursive; this is a graph problem, sorta) s"""{ "name": '$name', "type": 'dropdown', "options": [${options.sortBy(_._2).map { case (k, v) => s"""{"name": "$k", "value": ${v.toJSONRepr()}}""" }.mkString(",")}] }"""