Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions forms4s-core/src/main/scala/forms4s/DefaultValues.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package forms4s

import scala.quoted.*

object DefaultValues {

inline def extract[T]: Map[String, Any] = ${ extractImpl[T] }

private def extractImpl[T](using quotes: Quotes, tpe: Type[T]): Expr[Map[String, Any]] = {
import quotes.reflect.*

val sym = TypeTree.of[T].symbol

// Get the companion module
val companionSym = sym.companionModule
if (companionSym == Symbol.noSymbol) {
return '{ Map.empty[String, Any] }
}

val comp = sym.companionClass
val mod = Ref(companionSym)

// Get field names that have defaults
val names = sym.caseFields.filter(p => p.flags.is(Flags.HasDefault)).map(_.name)

val namesExpr: Expr[List[String]] = Expr.ofList(names.map(Expr(_)))

// Get the companion class body
val body = comp.tree.asInstanceOf[ClassDef].body

// Find default value methods ($lessinit$greater$default$N)
val idents: List[Ref] = body.collect {
case deff @ DefDef(name, _, _, _) if name.startsWith("$lessinit$greater$default") =>
mod.select(deff.symbol)
}

val identsExpr: Expr[List[Any]] = Expr.ofList(idents.map(_.asExpr))

'{ $namesExpr.zip($identsExpr).toMap }
}
}
2 changes: 1 addition & 1 deletion forms4s-core/src/main/scala/forms4s/FormElement.scala
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ object FormElement {
case class State(selected: Int, states: Vector[FormElementState])
}

case class Core[-T](id: String, label: String, description: Option[String], validators: Seq[Validator[T]])
case class Core[-T](id: String, label: String, description: Option[String], validators: Seq[Validator[T]], defaultValue: Option[Any] = None)

}
29 changes: 25 additions & 4 deletions forms4s-core/src/main/scala/forms4s/FormElementState.scala
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,32 @@ object FormElementState {

def empty[T <: FormElement](elem: T): ForElem[T] = {
def go[T <: FormElement](elem: T, parentPath: FormElementPath): ForElem[T] = elem match {
case x: FormElement.Text => Text(x, "", Nil, parentPath)
case x: FormElement.Select => Select(x, x.options.headOption.getOrElse(""), Nil, parentPath)
case x: FormElement.Checkbox => Checkbox(x, false, Nil, parentPath)
case x: FormElement.Text =>
val defaultValue = x.core.defaultValue.flatMap {
case s: String => Some(s)
case _ => None
}.getOrElse("")
Text(x, defaultValue, Nil, parentPath)
case x: FormElement.Select =>
val defaultValue = x.core.defaultValue.flatMap {
case s: String => Some(s)
case _ => None
}.getOrElse(x.options.headOption.getOrElse(""))
Select(x, defaultValue, Nil, parentPath)
case x: FormElement.Checkbox =>
val defaultValue = x.core.defaultValue.flatMap {
case b: Boolean => Some(b)
case _ => None
}.getOrElse(false)
Checkbox(x, defaultValue, Nil, parentPath)
case x: FormElement.Group => Group(x, x.elements.map(go(_, parentPath / x.core.id)), Nil, parentPath)
case x: FormElement.Number => Number(x, None, Nil, parentPath)
case x: FormElement.Number =>
val defaultValue = x.core.defaultValue.flatMap {
case i: Int => Some(i.toDouble)
case d: Double => Some(d)
case _ => None
}
Number(x, defaultValue, Nil, parentPath)
case x: FormElement.Multivalue => Multivalue(x, Vector(), Nil, parentPath)
case x: FormElement.Alternative =>
Alternative(x, FormElement.Alternative.State(0, x.variants.toVector.map(go(_, parentPath / x.core.id))), Seq(), parentPath)
Expand Down
20 changes: 11 additions & 9 deletions forms4s-core/src/main/scala/forms4s/ToFormElem.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ object ToFormElem {
// get compile-time labels & ToFormElem instances
val labels = elemLabels[p.MirroredElemLabels]
val elems = summonAll[p.MirroredElemTypes]
val defaults = DefaultValues.extract[T]

// build a Group
StaticToFormElem(
Expand All @@ -60,8 +61,9 @@ object ToFormElem {
),
elements = elems.zip(labels).map { (tf, name) =>
val fe = tf.get
val defaultValue = defaults.get(name)
// copy into each element its field‐name as id & human label
updateCore(fe)(id = name, label = name.capitalize)
updateCore(fe)(id = name, label = name.capitalize, defaultValue = defaultValue)
},
),
)
Expand Down Expand Up @@ -101,13 +103,13 @@ object ToFormElem {
}

// –– Helpers: update the Core.id & Core.label of any FormElement –––
private def updateCore(fe: FormElement)(id: String, label: String): FormElement = fe match {
case Text(core, fmt) => Text(core.copy(id = id, label = label), fmt)
case Number(core, isInt) => Number(core.copy(id = id, label = label), isInt)
case Select(core, opts) => Select(core.copy(id = id, label = label), opts)
case Checkbox(core) => Checkbox(core.copy(id = id, label = label))
case Group(core, elems) => Group(core.copy(id = id, label = label), elems)
case Multivalue(core, item) => Multivalue(core.copy(id = id, label = label), item)
case Alternative(core, vs, d) => Alternative(core.copy(id = id, label = label), vs, d)
private def updateCore(fe: FormElement)(id: String, label: String, defaultValue: Option[Any]): FormElement = fe match {
case Text(core, fmt) => Text(core.copy(id = id, label = label, defaultValue = defaultValue), fmt)
case Number(core, isInt) => Number(core.copy(id = id, label = label, defaultValue = defaultValue), isInt)
case Select(core, opts) => Select(core.copy(id = id, label = label, defaultValue = defaultValue), opts)
case Checkbox(core) => Checkbox(core.copy(id = id, label = label, defaultValue = defaultValue))
case Group(core, elems) => Group(core.copy(id = id, label = label, defaultValue = defaultValue), elems)
case Multivalue(core, item) => Multivalue(core.copy(id = id, label = label, defaultValue = defaultValue), item)
case Alternative(core, vs, d) => Alternative(core.copy(id = id, label = label, defaultValue = defaultValue), vs, d)
}
}
25 changes: 25 additions & 0 deletions forms4s-core/src/test/scala/forms4s/ToFormElemTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class ToFormElemTest extends AnyFreeSpec {
case class Card(number: String) extends Payment derives ToFormElem
}

case class PersonWithDefaults(name: String = "John Doe", age: Int = 30, isMember: Boolean = true) derives ToFormElem

"group" in {
assert(
summon[ToFormElem[Person]].get == FormElement.Group(
Expand Down Expand Up @@ -43,4 +45,27 @@ class ToFormElemTest extends AnyFreeSpec {
)
}

"default values in form elements" in {
val formElem = summon[ToFormElem[PersonWithDefaults]].get
formElem match {
case FormElement.Group(_, elements) =>
assert(elements(0).core.defaultValue.contains("John Doe"))
assert(elements(1).core.defaultValue.contains(30))
assert(elements(2).core.defaultValue.contains(true))
case _ => fail("Expected Group")
}
}

"default values in form state" in {
val formElem = summon[ToFormElem[PersonWithDefaults]].get
val state = FormElementState.empty(formElem)
state match {
case FormElementState.Group(_, values, _, _) =>
assert(values(0).asInstanceOf[FormElementState.Text].value == "John Doe")
assert(values(1).asInstanceOf[FormElementState.Number].value.contains(30.0))
assert(values(2).asInstanceOf[FormElementState.Checkbox].value == true)
case _ => fail("Expected Group")
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@ object FormFromJsonSchema {
): Ior[List[String], FormElement] = {
val name = nameOverride.getOrElse(schema.title.getOrElse("unknown"))
val label = schema.title.getOrElse(capitalizeAndSplitWords(name))
def core[T](validators: Seq[Validator[T]]) = FormElement.Core(name, label, schema.description, validators)

val defaultValue = schema.default.flatMap {
case ExampleSingleValue(v) => Some(v)
case _ => None
}

def core[T](validators: Seq[Validator[T]]) = FormElement.Core(name, label, schema.description, validators, defaultValue)

val variants = schema.oneOf ++ schema.anyOf
if (variants.nonEmpty) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,33 @@ class FormFromJsonSchemaSpec extends AnyFreeSpec {
}
}

"default values" in {
import sttp.apispec.{Schema => ASchema, ExampleSingleValue, SchemaType}
import scala.collection.immutable.ListMap

val schema = ASchema(
title = Some("WithDefaults"),
`type` = Some(List(SchemaType.Object)),
properties = ListMap(
"a" -> ASchema(
`type` = Some(List(SchemaType.String)),
default = Some(ExampleSingleValue("default"))
)
)
)

val form = FormFromJsonSchema.convert(schema)

val expected = FormElement.Group(
simpleCore("WithDefaults"),
List(
FormElement.Text(simpleCore("a", "A").copy(defaultValue = Some("default")), Format.Raw),
),
)

assert(form == expected)
}

}

def getForm[T](nullableOptions: Boolean = false)(implicit tschema: TSchema[T]): FormElement = {
Expand Down