Thorium Server

Import

The data-mapping library is similar to the Play Framework's Forms.

To use data-mapping, import the following package:

import com.greenfossil.data.mapping.Mapping.*
import com.greenfossil.data.mapping.Mapping
Last modified on 05/12/2022, 3:00 pm

Types of mapping

Single field mappings can be declared like below:

val textMapping = Mapping("text", text)
val nonEmptyTextMapping = Mapping("text", nonEmptyText)
val boolMapping = Mapping("bool", boolean)
val intMapping = Mapping("int", number)
val localDateMapping = Mapping("date", localDateUsing("yyyy-MM-dd"))
Mappings can also contain multiple fields:
val tupleMapping = tuple(
  "text" -> text,
  "date" -> localDateUsing("yyyy-MM-dd"),
  "int" -> number,
  "bool" -> boolean
)
Mappings can be mapped to a case class:
case class User(firstname: String, lastname: String)
val userForm = mapping[User](
  "firstname" -> text,
  "lastname" -> text
)
Mappings can also be defined to have repeated values
val namesForm: Mapping[Seq[String]] = Mapping("name", seq(text))
val tuplesForm: Mapping[Seq[(String, String)]] = repeatedTuple(
  "firstname" -> text,
  "lastname" -> text
)
case class User(firstname: String, lastname: String)
val usersForm: Mapping[Seq[User]] = repeatedMapping[User](
  "firstname" -> text,
  "lastname" -> text
)

A field can sometime be optional: 

val optionalTextMapping: Mapping[Option[String]] = Mapping("optional", optional(text))
A field can have a default value. Default values are used when there is no corresponding data for the field from the request.
val numberMapping = Mapping("number", default(1))
val dateMapping = Mapping("date", default(localDateUsing("yyyy-MM-dd"), LocalDate.now))
A field can also be made to have a static value using ignored

case class User(id: Long, firstname: String, lastname: String)
val usersForm: Mapping[Seq[User]] = repeatedMapping[User](
  "id" -> ignored(1),
  "firstname" -> text,
  "lastname" -> text
)

Form Constraints

Individual fields can have constraints. By default, nonEmptyText field requires a non empty strings while text field accepts empty strings. Other fields, such as number or localDate requires the submitted data to be of a certain format.

Text fields can be defined to require a certain length:

val textMapping = Mapping("text", text(minLength = 10, maxLength = 30, trim = true))

Similarly, number fields can have min or max value:

val numberMapping = Mapping("number", number(min = 10, max = 30))]
A field can also have ad-hoc constraints:
val dobMapping = Mapping(
  "dob", 
  localDate.verifying("Date should not be in the future", !_.isAfter(LocalDate.now))
)
Multiple fields can be verified as a whole:
val tupleMapping = tuple(
  "min" -> number,
  "max" -> number
).verifying("Min has to be smaller or equal to max", (min, max) => min <= max)

Transformation

Fields can be transformed from one type to another. 

In the example below, a comma separated text is transformed to a sequence of String.

val form: Mapping[Seq[String]] = Mapping(
  "values", text.transform[Seq[String]](_.split(","), _.mkString(","))
)

Handling filling a form with initial values

You can fill the form using the fill method:

val form = Mapping("name", nonEmptyText)

form.fill("John Doe")

Handling binding

Data can be retrieved from the request using the bindFromRequest() method.

@Post("/submit")
def submitForm = Action:
  implicit request =>
  form.bindFromRequest().fold(
    errorForm => ???, // form errors can be obtained from errorForm.errors
    data => ??? // successfully bound data
  )
}
The form can also be bound manually using key-value pairs:

form.bind("firstname" -> "john", "lastname" -> "doe")
Last modified on 22/11/2023, 3:40 pm

Accessing field values and errors

A bound form can have individual field errors.

To access the individual fields, we can use the apply methods:

val form = tuple(
  "name" -> nonEmptyText.transform[String](_.capitalize, _.capitalize),
  "age" -> number
)
val boundForm = form.bind("name" -> "john")
val nameValue = boundForm[String]("name").value // Some("John")
val nameBindingValue = boundForm[String]("name").bindingValue // Some("john")
val ageError = boundForm[Int]("age").errors.map(_.message) // List("error.required")