Class FormField<I,O>

java.lang.Object
google.registry.ui.forms.FormField<I,O>
Type Parameters:
I - input value type
O - output value type

@Immutable public final class FormField<I,O> extends Object
Declarative functional fluent form field converter / validator.

This class is responsible for converting arbitrary data, sent to us by the web browser, into validated data structures that the server-side code can use. For example:


 private enum Gender { MALE, FEMALE }

 private static final FormField<String, String> NAME_FIELD = FormField.named("name")
     .matches("[a-z]+")
     .range(atMost(16))
     .required()
     .build();

 private static final FormField<String, Gender> GENDER_FIELD = FormField.named("gender")
     .asEnum(Gender.class)
     .required()
     .build();

 public Person makePerson(Map<String, String> params) {
   Person.Builder person = new Person.Builder();
   for (String name : NAME_FIELD.extract(params).asSet()) {
     person.setName(name);
   }
   for (Gender name : GENDER_FIELD.extract(params).asSet()) {
     person.setGender(name);
   }
   return person.build();
 }
 

This class provides full type-safety if and only if you statically initialize your FormField objects and write a unit test that causes the class to be loaded.

Exception Handling

When values passed to convert(I) or extract(java.util.Map<java.lang.String, I>) don't meet the contract, FormFieldException will be thrown, which provides the field name and a short error message that's safe to pass along to the client.

You can safely throw FormFieldException from within your validator functions, and the field name will automatically be propagated into the exception object for you.

In situations when you're validating lists or maps, you'll end up with a hierarchical field naming structure. For example, if you were validating a list of maps, an error generated by the bar field of the fifth item in the foo field would have a fully-qualified field name of: foo[5][bar].

Library Definitions

You should never assign a partially constructed FormField.Builder to a variable or constant. Instead, you should use asBuilder() or asBuilderNamed(String).

Here is an example of how you might go about defining library definitions:


 final class FormFields {
   private static final FormField<String, String> COUNTRY_CODE =
       FormField.named("countryCode")
           .range(Range.singleton(2))
           .uppercased()
           .in(ImmutableSet.copyOf(Locale.getISOCountries()))
           .build();
 }

 final class Form {
   private static final FormField<String, String> COUNTRY_CODE_FIELD =
       FormFields.COUNTRY_CODE.asBuilder()
           .required()
           .build();
 }
 
  • Method Details

    • named

      public static FormField.Builder<String,String> named(String name)
      Returns an optional string form field named name.
    • named

      public static <T> FormField.Builder<T,T> named(String name, Class<T> typeIn)
      Returns an optional form field named name with a specific inputType.
    • mapNamed

      public static FormField.Builder<Map<String,?>,Map<String,?>> mapNamed(String name)
      Returns a form field builder for validating JSON nested maps.

      Here's an example of how you'd use this feature:

         private static final FormField<String, String> REGISTRAR_NAME_FIELD =
             FormField.named("name")
                 .emptyToNull()
                 .required()
                 .build();
      
         private static final FormField<Map<String, ?>, Registrar> REGISTRAR_FIELD =
             FormField.mapNamed("registrar")
                 .transform(Registrar.class, new Function<Map<String, ?>, Registrar>() {
                   @Nullable
                   @Override
                   public Registrar apply(@Nullable Map<String, ?> params) {
                     Registrar.Builder builder = new Registrar.Builder();
                     for (String name : REGISTRAR_NAME_FIELD.extractUntyped(params).asSet()) {
                      builder.setName(name);
                     }
                     return builder.build();
                   }})
                 .build();

      When a FormFieldException is thrown, it'll be propagated to create a fully-qualified field name. For example, if the JSON input is

      {registrar: {name: ""}}
      then the field name will be registrar.name.
    • name

      public String name()
      Returns the name of this field.
    • convert

      @Detainted public Optional<O> convert(@Tainted @Nullable I value)
      Convert and validate a raw user-supplied value.
      Throws:
      FormFieldException - if value does not meet expected contracts.
    • extract

      @Detainted public Optional<O> extract(@Tainted Map<String,I> valueMap)
      Convert and validate a raw user-supplied value from a map.

      This is the same as saying: field.convert(valueMap.get(field.name())

      Throws:
      FormFieldException - if value does not meet expected contracts.
    • extractUntyped

      @Detainted public Optional<O> extractUntyped(@Tainted Map<String,?> jsonMap)
      Convert and validate a raw user-supplied value from an untyped JSON map.
      Throws:
      FormFieldException - if value is wrong type or does not meet expected contracts.
    • asBuilder

      public FormField.Builder<I,O> asBuilder()
      Returns a builder of this object, which can be used to further restrict validation.
      See Also:
    • asBuilderNamed

      public FormField.Builder<I,O> asBuilderNamed(String newName)
      Same as asBuilder() but changes the field name.