form( string(email, required, validate(EmailAddress)) string(name, required) date(birthDate) )
def form(builderFoo: FormBuilder => FormBuilder)
form(_ .string(...) .string(...) .extend(commonFields) .date(...) )
def form(fields: FormBuilder => FormBuilder*)
form( _.string(...), _.string(...), _.date(...), commonFields:_* )
def form(fields: => ())
: form{ string(...) date(...) commonFields() }
def string(fieldFoo: FieldBuilder => FieldBuilder): FormBuilder
.string(_.name("email").required.validate(EmailAddress))
case class
with its automatically defined copy
method is ideal for implementing Scala in Scala. This method copies the current object, modifying the desired attributes. A builder for a particular field might look like this: case class FieldBuilderImpl(fieldType: String, label: String, isRequired: Boolean, validators: Seq[Validator]) extends FieldBuilder { def fieldType(t: String): FieldBuilder = copy(fieldType = t) def label(l: String): FieldBuilder = copy(label = l) def required: FieldBuilder = copy(isRequired = true) def validate(vs: Validator*): FieldBuilder = copy(validators = validators ++ vs) def build: FieldDescription = ??? }
case class FormBuilderImpl(fields: Map[String, FieldDescription]) extends FormBuilder { def field[F](name: String)(foo: FieldBuilder[F] => FieldBuilder[F]) = copy(fields = fields ++ Map(name -> foo(newFieldBuilder[F]).asInstanceOf[FieldBuilderImpl[F]].build)) def string(name: String)(foo: FieldBuilder[String] => FieldBuilder[String]) = field[String](name)(foo andThen (_.fieldType("string"))) def newFieldBuilder[F]: FieldBuilder[F] = ??? def build: FormDescription = ??? }
case class
. For brevity, we will call it “form data object” or simply “form data”. Form fields will be represented by typed fields of this class. In this case, we can identify the form fields with a closure — by calling the getter of the desired field of the form data object (for example, a closure (_.someField)
will correspond to a form field called "someField"
).UserFormData => String
we can determine that the form field should be only string. form[FormData](_.string( _.someField )(_.someProperty))
_.someField
closure get the name "someField"
.fieldName[T:Manifest](fieldFoo: T => Any): String
[T:Manifest]
entry means that an additional, implicit parameter list will be added to the method signature, expecting a Manifest [T] argument from the compiler. In fact, this is nothing more than a crutch to overcome such a phenomenon in the JVM as Type Erasure . With the manifest, we get the opportunity to get type parameter (similar to the type variable from the Generic class in Java) at run time.proxyObject
: an object that satisfies the type that we transferred, the only task of which is to communicate the name of the first method called. If the closure (_: T).someField
is passed as fieldFoo: T => Any
, then a call to fieldFoo(proxyObject)
will give us the string "someField"
. These actions were moved to the scala-reflective-tools library. case class MyClass(fieldA: String) import FieldNameGetter._ assertTrue $[MyClass](_.fieldA) == "fieldA"
case class FormBuilderImpl[T:Manifest](fields: Map[String, FieldDescription]) extends FormBuilder[T] with FieldNameGetter { def field[F](fieldFoo: T => F)(foo: FieldBuilder[F] => FieldBuilder[F]) = copy(fields = fields ++ Map($[T](fieldFoo) -> foo(newFieldBuilder[F]).asInstanceOf[FieldBuilderImpl[F]].build)) def string(fieldFoo: T => String)(foo: FieldBuilder[String] => FieldBuilder[String]) = field[String](fieldFoo)(foo andThen (_.fieldType("string"))) ... }
case class RegistrationFormData( name: String, surname: Option[String], email: String, birthDate: Option[Date] )
name
field is of type RegistrationFormData => String
, and the getter surname
is of type RegistrationFormData => Option[String]
. Accordingly, we must have two methods for defining fields.(def string(fieldFoo: T => String)(foo: FieldBuilder[String] => FieldBuilder[String])
def stringOpt(fieldFoo: T => Option[String])(foo: FieldBuilder[String] => FieldBuilder[String])
string
method by declaring the required
property in it. With the help of currying it was possible to get a rather compact code: case class FormBuilderImpl[T: Manifest](fields: Map[String, FieldDescription]) extends FormBuilder[T] with FieldNameGetter { // : type FieldFoo[F] = FieldBuilder[F] => FieldBuilder[F] type FormField[F] = FieldFoo[F] => FormBuilder[T] // def string(fieldFoo: T => String): FormField[String] = fieldBase[String](fieldFoo)(_.required) // def stringOpt(fieldFoo: T => Option[String]): FormField[String] = fieldBase[String](fieldFoo)(identity) def field[F](fieldName: String)(foo: FieldFoo[F]) = copy(fields = fields ++ Map(fieldName -> foo(newFieldBuilder[F]).asInstanceOf[FieldBuilderImpl[F]].build)) def fieldBase[F: Manifest](fieldFoo: T => Any) (innerConfigFoo: FieldFoo[F]) (userConfigFoo: FieldFoo[F]) = field($[T](fieldFoo))(innerConfigFoo andThen (_.fieldType(fieldTypeBy[F])) andThen userConfigFoo) ... }
fieldType
property using the type parameter.FieldBuilder
we can only specify the label
and required
properties, and this is not enough. Developers should be able to extend the set of field properties if required by the task. In addition, fields of different types need to specify different properties.FieldBuilder
:def addProperty(key: String, value: Any): FieldBuilder[T]
object FieldDslExtenders { import FieldAttributes._ implicit class StringFieldBuilderExtender(val fb: FieldBuilder[String]) extends AnyVal { def minLength(length: Int) = fb.addProperty(MinLength, length) def maxLength(length: Int) = fb.addProperty(MaxLength, length) } implicit class SeqFieldBuilderExtender[A](val fb: FieldBuilder[Seq[A]]) extends AnyVal { ... } implicit class DateFieldBuilderExtender(val fb: FieldBuilder[Date]) extends AnyVal { def format(datePattern: String) = fb.addProperty(DatePattern, datePattern) } }
AnyVal
. This is necessary so that at run time an object of this wrapper class is not instantiated, but instead a static method is called on the companion object, which will be implicitly created by the compiler.FormBuilder
: with its help, so far you can add a rather limited set of field types. It is clear that adding all the most common views to it is not difficult, but what if users need a new field type that is not in FormBuilder
? This problem can be solved with the help of the same implicit converters that access the base fieldBase
method. With their help, we can extend FormBuilder
- for example, we could add a method to create a field with a date: object FormDslExtensions extends FieldNameGetter { val defaultDateFormat = new SimpleDateFormat("dd.MM.yyyy").toPattern import FieldDslExtenders._ implicit class DateFormExtension[T: Manifest](val fb: FormBuilder[T]) extends AnyVal { def dateOpt(fieldFoo: T => Option[Date]) = fb.fieldBase[Date](fieldFoo)(_.format(defaultDateFormat)) _ def date(fieldFoo: T => Date) = fb.fieldBase[Date](fieldFoo)(_.required.format(defaultDateFormat)) _ } }
String -> FieldDescription
. In turn, the FieldDescription
contains the map String -> Any
. Thus, we can set the fields for any attributes that may be required. The resulting description of the form, in my opinion, is quite framework-agnostic, i.e. can be used with various frameworks. All that is needed is to implement the conversion into the form representation of the framework used. Next we look at how this was done for the Play Framework.Map[String, Seq[String]]
. Further, they are expected to be validated and converted into a suitable representation for custom code. Built-in Play allows you to convert this data either into a Tuple or into an arbitrary object - for this, however, you must provide the function of its creation. If the form has enough fields, it can lead to code that is potentially rich in errors. Imagine: you need to make sure that the 18 function arguments are in the correct order.FormMapping
not intended to work with fields manually. Field typing is guaranteed by DSL, and the conversion to a form data object and vice versa occurs automatically using reflection.attributes
- with its help additional parameters will be passed. PlayFormFactory.form[FormData](_.string(_.name)(_.label("").someAttribute(42))
@(form: com.naumen.scala.forms.play.ExtendedForm[RegistrationFormData]) <div> .... @myComponent(form(_.name)) .... </div>
@(customField: com.naumen.scala.forms.play.ExtendedField) <div> .... @customField.ext.attrs("someAttribute") .... </div>
Source: https://habr.com/ru/post/206996/
All Articles