Skip to content

Commit

Permalink
Merge pull request #45 from KacperFKorban/nullability
Browse files Browse the repository at this point in the history
Nullability
  • Loading branch information
KacperFKorban authored Apr 11, 2024
2 parents 1aed401 + f719db8 commit 90336c7
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 41 deletions.
9 changes: 9 additions & 0 deletions guinep/src/main/scala/macros.scala
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ private[guinep] object macros {
FormElement.FloatingNumberInput(paramName, Types.FloatingType.Double)
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] =>
val innerForm = if ltpe =:= TypeRepr.of[Null] then
functionFormElementFromTreeWithCaching(paramName, rtpe)
else
functionFormElementFromTreeWithCaching(paramName, ltpe)
FormElement.Nullable(paramName, innerForm)
case ntpe if isProductTpe(ntpe) =>
val classSymbol = ntpe.typeSymbol
val typeDefParams = classSymbol.primaryConstructor.paramSymss.flatten.filter(_.isTypeParam)
Expand Down Expand Up @@ -255,6 +261,9 @@ private[guinep] object macros {
param.select(s"asInstanceOf").appliedToType(ntpe)
case AppliedType(ntpe: NamedType, List(tpeArg)) if listLikeSymbolsTypes.contains(ntpe.typeSymbol) =>
param.select("asInstanceOf").appliedToType(paramTpe)
case OrType(ltpe, rtpe) if ltpe =:= TypeRepr.of[Null] || rtpe =:= TypeRepr.of[Null] =>
val castedParam = param.select("asInstanceOf").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)
case ntpe if isCaseObjectTpe(ntpe) =>
Expand Down
9 changes: 8 additions & 1 deletion guinep/src/main/scala/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ private[guinep] object model {
case List
case Seq
case Vector
case Array

object ListType:
given ToExpr[ListType] with
Expand All @@ -74,6 +73,7 @@ private[guinep] object model {
case PasswordInput(override val name: String) extends FormElement(name)
case FieldSet(override val name: String, elements: List[FormElement]) extends FormElement(name)
case NamedRef(override val name: String, ref: String) extends FormElement(name)
case Nullable(override val name: String, element: FormElement) extends FormElement(name)

def constrOrd: Int = this match
case TextInput(_) => 0
Expand All @@ -89,6 +89,7 @@ private[guinep] object model {
case PasswordInput(_) => 7
case FieldSet(_, _) => 8
case NamedRef(_, _) => 9
case Nullable(_, elem) => elem.constrOrd

object FormElement:
given ToExpr[FormElement] with
Expand Down Expand Up @@ -119,6 +120,8 @@ private[guinep] object model {
'{ FormElement.PasswordInput(${Expr(name)}) }
case FormElement.NamedRef(name, ref) =>
'{ FormElement.NamedRef(${Expr(name)}, ${Expr(ref)}) }
case FormElement.Nullable(name, element) =>
'{ FormElement.Nullable(${Expr(name)}, ${Expr(element)}) }

// This ordering is a hack to avoid placing recursive constructors as first options in a dropdown
given Ordering[FormElement] = new Ordering[FormElement] {
Expand All @@ -130,6 +133,10 @@ private[guinep] object model {
elems1.size - elems2.size
case (FormElement.Dropdown(_, opts1), FormElement.Dropdown(_, opts2)) =>
opts1.size - opts2.size
case (FormElement.Nullable(_, elem1), FormElement.Nullable(_, elem2)) =>
compare(elem1, elem2)
case (FormElement.ListInput(_, elem1, _), FormElement.ListInput(_, elem2, _)) =>
compare(elem1, elem2)
case _ => 0
}
}
11 changes: 11 additions & 0 deletions guinep/src/test/scala/formgentests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@ class FormGenTests extends munit.FunSuite {
)
)

checkGeneratedFormEquals(
"showNullableInt",
showNullableInt,
Form(
Seq(
FormElement.Nullable("i", FormElement.NumberInput("i", Types.IntType.Int))
),
Map.empty
)
)

checkGeneratedFormEquals(
"isInTree",
isInTree,
Expand Down
14 changes: 14 additions & 0 deletions guinep/src/test/scala/rungentests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,20 @@ class RunGenTests extends munit.FunSuite {
"6.0"
)

checkGeneratedRunResultEquals(
"showNullableInt",
showNullableInt,
List(null),
"null"
)

checkGeneratedRunResultEquals(
"showNullableInt",
showNullableInt,
List(1),
"1"
)

checkGeneratedRunResultEquals(
"isInTree",
isInTree,
Expand Down
3 changes: 3 additions & 0 deletions guinep/src/test/scala/testsdata.scala
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ object TestsData {
def productSeq(s: Seq[Float]): Float =
s.product

def showNullableInt(i: Int | Null): String =
if i == null then "null" else i.toString

enum IntTree:
case Leaf
case Node(left: IntTree, value: Int, right: IntTree)
Expand Down
54 changes: 30 additions & 24 deletions testcases/src/main/scala/main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -102,29 +102,35 @@ def sumVector(v: Vector[Int]): Int =
def seqProduct(seq: Seq[Float]): Float =
seq.product

def showNullableInt(i: Int | Null): String =
if i == null then "null" else i.toString

@main
def run: Unit =
guinep.web(
upperCaseText,
add,
concat,
giveALongText,
addObj,
greetMaybeName,
greetInLanguage,
nameWithPossiblePrefix,
nameWithPossiblePrefix1,
roll20,
roll6(),
concatAll,
showDouble,
divideFloats,
codeOfChar,
isInTree,
listProduct,
sumVector,
seqProduct,
// isInTreeExt
// addManyParamLists
// printsWeirdGADT
)
guinep.web
.withModifyConfig(_.copy(requireNonNullableInputs = true))
.apply(
upperCaseText,
add,
concat,
giveALongText,
addObj,
greetMaybeName,
greetInLanguage,
nameWithPossiblePrefix,
nameWithPossiblePrefix1,
roll20,
roll6(),
concatAll,
showDouble,
divideFloats,
codeOfChar,
isInTree,
listProduct,
sumVector,
seqProduct,
showNullableInt
// isInTreeExt
// addManyParamLists
// printsWeirdGADT
)
4 changes: 2 additions & 2 deletions web/src/main/scala/api.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ object GuinepWebConfig:
GuinepWebConfig()

private case class GuinepWeb(config: GuinepWebConfig = GuinepWebConfig.default) {
def withSetConfig(config: GuinepWebConfig): Unit =
def withSetConfig(config: GuinepWebConfig): GuinepWeb =
this.copy(config = config)

def withModifyConfig(f: GuinepWebConfig => GuinepWebConfig): Unit =
def withModifyConfig(f: GuinepWebConfig => GuinepWebConfig): GuinepWeb =
withSetConfig(f(config))

/**
Expand Down
20 changes: 20 additions & 0 deletions web/src/main/scala/htmlgen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ private[guinep] trait HtmlGen {
input.name = formElem.name;
input.id = formElem.name;
input.placeholder = formElem.name;
if (formElem.nullable) {
input.setAttribute('nullable', formElem.nullable);
}
if (${config.requireNonNullableInputs} && !formElem.nullable) {
input.setAttribute('required', !formElem.nullable);
}
form.insertBefore(input, before);
form.insertBefore(br.cloneNode(), before);
} else if (formElem.type == 'char') {
Expand All @@ -172,6 +178,12 @@ private[guinep] trait HtmlGen {
input.name = formElem.name;
input.id = formElem.name;
input.placeholder = formElem.name;
if (formElem.nullable) {
input.setAttribute('nullable', formElem.nullable);
}
if (${config.requireNonNullableInputs} && !formElem.nullable) {
input.setAttribute('required', !formElem.nullable);
}
form.insertBefore(input, before);
form.insertBefore(br.cloneNode(), before);
} else {
Expand All @@ -184,6 +196,12 @@ private[guinep] trait HtmlGen {
input.name = formElem.name;
input.id = formElem.name;
input.placeholder = formElem.name;
if (formElem.nullable) {
input.setAttribute('nullable', formElem.nullable);
}
if (${config.requireNonNullableInputs} && !formElem.nullable) {
input.setAttribute('required', !formElem.nullable);
}
form.insertBefore(input, before);
form.insertBefore(br.cloneNode(), before);
}
Expand Down Expand Up @@ -240,6 +258,8 @@ private[guinep] trait HtmlGen {
const value = element.value;
if (element.type === 'checkbox') {
return [name, element.checked];
} else if (element.getAttribute('nullable') && value === '') {
return [name, null];
} else {
return [name, value];
}
Expand Down
34 changes: 20 additions & 14 deletions web/src/main/scala/serialization.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,45 +77,51 @@ private[guinep] object serialization:
case guinep.model.Types.ListType.List => res
case guinep.model.Types.ListType.Seq => res.toSeq
case guinep.model.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")

extension (form: Form)
def formElementsJSONRepr =
val elems = form.inputs.map(_.toJSONRepr).mkString(",")
val elems = form.inputs.map(_.toJSONRepr()).mkString(",")
s"[$elems]"
def namedFormElementsJSONRepr: String =
val entries = form.namedFormElements.toList.map { (name, formElement) =>
s""""$name": ${formElement.toJSONRepr}"""
s""""$name": ${formElement.toJSONRepr()}"""
}
.mkString(",")
s"{$entries}"

extension (formElement: FormElement)
def toJSONRepr: String = formElement match
def toJSONRepr(nullable: Boolean = false): String = formElement match
case FormElement.FieldSet(name, elements) =>
s"""{ "name": '$name', "type": 'fieldset', "elements": [${elements.map(_.toJSONRepr).mkString(",")}] }"""
s"""{ "name": '$name', "type": 'fieldset', "elements": [${elements.map(_.toJSONRepr()).mkString(",")}]}"""
case FormElement.TextInput(name) =>
s"""{ "name": '$name', "type": 'text' }"""
s"""{ "name": '$name', "type": 'text', "nullable": $nullable }"""
case FormElement.CharInput(name) =>
s"""{ "name": '$name', "type": 'char' }"""
s"""{ "name": '$name', "type": 'char', "nullable": $nullable }"""
case FormElement.NumberInput(name, _) =>
s"""{ "name": '$name', "type": 'number' }"""
s"""{ "name": '$name', "type": 'number', "nullable": $nullable }"""
case FormElement.FloatingNumberInput(name, _) =>
s"""{ "name": '$name', "type": 'float' }"""
s"""{ "name": '$name', "type": 'float', "nullable": $nullable }"""
case FormElement.CheckboxInput(name) =>
s"""{ "name": '$name', "type": 'checkbox' }"""
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(",")}] }"""
s"""{ "name": '$name', "type": 'dropdown', "options": [${options.sortBy(_._2).map { case (k, v) => s"""{"name": "$k", "value": ${v.toJSONRepr()}}""" }.mkString(",")}] }"""
case FormElement.ListInput(name, element, _) =>
s"""{ "name": '$name', "type": 'list', "element": ${element.toJSONRepr} }"""
s"""{ "name": '$name', "type": 'list', "element": ${element.toJSONRepr()} }"""
case FormElement.TextArea(name, rows, cols) =>
s"""{ "name": '$name', "type": 'textarea', "rows": ${rows.getOrElse("")}, "cols": ${cols.getOrElse("")} }"""
s"""{ "name": '$name', "type": 'textarea', "rows": ${rows.getOrElse("")}, "cols": ${cols.getOrElse("")}, "nullable": $nullable }"""
case FormElement.DateInput(name) =>
s"""{ "name": '$name', "type": 'date' }"""
s"""{ "name": '$name', "type": 'date', "nullable": $nullable }"""
case FormElement.EmailInput(name) =>
s"""{ "name": '$name', "type": 'email' }"""
s"""{ "name": '$name', "type": 'email', "nullable": $nullable }"""
case FormElement.PasswordInput(name) =>
s"""{ "name": '$name', "type": 'password' }"""
s"""{ "name": '$name', "type": 'password', "nullable": $nullable }"""
case FormElement.NamedRef(name, ref) =>
s"""{ "name": '$name', "ref": '$ref', "type": 'namedref' }"""
case FormElement.Nullable(_, element) =>
element.toJSONRepr(nullable = true)

0 comments on commit 90336c7

Please sign in to comment.