Basic Validation Framework

From Lift

Jump to: navigation, search
  •  ! It's a suggestion !
  • It's a work in progress (a brain storming, paper POC)
  • I don't know the current Liftweb architecture

Contents

Requirements/Features

  • usable outside web context, in example in test context
  • simple to write validators, convertors
  • manage (add/update/remove) message to clients (feebabck in Wicket, error_list in TG, FacesMessages in JSF,...)
  • a validation stack/tree must be readable by third party consumers :
    • HtmlBuilder : to add some tags, attributes (eg: maxlength in input tag), content (eg : * to required field's label)
    • CustomClientSide : to mimic configuration into a javascript framework
  • a validation stack/tree must be modifiable by third party providers :
    • ORM : to mirror DB contrains for binded field
    • programmer : to add what he wich
    • DSL : to translate "user friendly" DSL into
    • Widgets : to add conversion (ex: date field splitted into 3 inputs) and controls
  • use to process input request : conversion from string and validation
  • use to process output response : conversion into strings
  • special traitements on first display (avoid some validations, like required)


Proposal

Pseudo types

/**
 * Logger where to put messages for Client/Browser.
 * (similar to Feebback in wicket, or FacesMessages in JSF)
 */
abstract class ClientLogger {
  def info(fieldName: String, message:String, params: Any*)
  def warn(fieldName: String, message:String, params: Any*)
  def error(fieldName: String, message:String, params: Any*)
  def hasWarnOrError() : boolean
  def hasWarnOrErrorOn(fieldName:String)
}

/**
 * A DataPreparator, is a basic
abstract class DataPreparator {
  /**
   * Convert and/or Validate the data from Web (simple String, group of string) into other object (Date, Number, phone number,...).
   * Input strings value could be removed.
   * The convertion could be applied to 0-n entries.
   * @param data in/out, set of data to transform
   * @param logger logger where to put message for Client/Browser
   */
  abstract def fromWeb(data: Map[String, Any], logger: ClientLogger);

  /**
   * Convert the data into String (or group of string) for a web displaying.
   * Input non-strings value should be removed.
   * The convertion could be applied to 0-n entries.
   * @param data in/out, set of data to transform
   * @param logger logger where to put message for Client/Browser
   */
  abstract def toWeb(data: Map[String, Any], logger: ClientLogger);
}

abstract class SingleDataPreparator(fieldname: String)[T] extends DataPrepartor {
  override def fromWeb(data: Map[String, Any], logger: ClientLogger) {
    val value = data.get(fieldname)
    if (!StringUtils.isBlank(value) && !logger.hasWarnOrErrors(fieldname)) {
      fromWeb(data.get(fieldname), logger)
    }
  }

  override def toWeb(data: Map[String, Any], logger: ClientLogger) {
    val value = data.get(fieldname)
    if (!StringUtils.isBlank(value) && !logger.hasWarnOrErrors(fieldname)) {
      toWeb(data.get(fieldname).asInstanceOf[T] logger)
    }
  }

  abstract def fromWeb(value: String, logger: ClientLogger);
  abstract def toWeb(value: T, logger: ClientLogger);
}

case RequiredValidator(fieldname:String)  extends DataPreparator{
  override def fromWeb(data: Map[String, Any], logger: ClientLogger) = {
    if (StringUtils.isBlank(data.get(fieldname)) {
      logger.warn(fieldname, "required")
    }
  }
}

case StringLengthValidator(fieldname:String, max: Int, min: Int)  extends SingleDataPreparator(fieldname) {
  override def fromWeb(value: String, logger: ClientLogger) = {
    if ((max > 0) && (value.length > max)) {
      logger.warn(fieldname, "maximum length is %d", max)
    }
    if ((min > 0) && (value.length < min)) {
      logger.warn(fieldname, "minimum length is %d", min)
    }
  }
}

...


Validation phases

  var stringValidators : List(DataPreparator) = _;
  def converters : List(DataPreparator) = _;
  def anyValidators : List(DataPreparator) = _;

  def processRequest(params: Map[String,String], logger : Clientlogger) : Map[String, Any] = {
    var data = new Map[String, Any](params)
    stringValidators.forEach(fromWeb(data, clogger))
    converters.forEach(fromWeb(data, clogger))
    anyValidators.forEach(fromWeb(data, clogger))
    data
  }

  def processReponse(data: Map[String,Any], logger : Clientlogger) : Map[String, String] = {
    var params = new Map[String, String](data)
    anyValidators.forEach(toWeb(data, clogger))
    converters.forEach(toWeb(data, clogger))
    stringValidators.forEach(toWeb(data, clogger))
    params
  }

Personal tools