-
-
Save rockymadden/9557583 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scala.collection.immutable.ListMap | |
sealed abstract class Validated[+T] | |
case class Valid[+T](value:T) extends Validated[T] | |
case class Error(message:String) extends Validated[Nothing] | |
class ValidationException(s:String) extends Exception(s) | |
implicit def toValue[T](v:Validated[T]):T = { | |
v match { | |
case Valid(v) => v | |
case Error(e) => throw new ValidationException(e) | |
} | |
} | |
trait FieldType[+T] { | |
def clean(v:Any):Validated[T] | |
} | |
object StringField extends FieldType[String] { | |
def clean(v:Any) = Valid(v.toString) | |
override def toString() = "StringField" | |
} | |
object IntField extends FieldType[Int] { | |
def clean(v:Any) = { | |
v match { | |
case v:Int => Valid(v) | |
case v:String => { | |
try { | |
Valid(v.toInt) | |
} catch { | |
case e:NumberFormatException => Error("Please enter a whole number") | |
} | |
} | |
case _ => Error("Please enter a whole number") | |
} | |
} | |
override def toString() = "IntField" | |
} | |
sealed abstract class Fallback[+T] | |
case class Default[+T](value:T) extends Fallback[T] | |
object Required extends Fallback[Nothing] { | |
override def toString() = "Required" | |
} | |
implicit def toFallback[T](a:T):Fallback[T] = Default(a) | |
type RawData = Map[String,Any] | |
case class Field[+T](name:String, fieldType:FieldType[T], default:Fallback[T]) | |
class DuplicateFieldException(val field:Field[_], msg:String) extends Exception(msg) | |
case class FieldValue[+T](value:Validated[T], field:Field[T]) | |
implicit def toValue[T](fv:FieldValue[T]):T = fv.value | |
trait Entity { | |
val data:RawData | |
private[this] var errorDict = ListMap[String,String]() | |
def errors() = errorDict | |
private[this] var fieldDict = ListMap[String,Field[_]]() | |
def fields() = fieldDict | |
def valid():Boolean = errors.size == 0 | |
protected def field[T](name:String, fieldType:FieldType[T], default:Fallback[T] = Required):FieldValue[T] = { | |
// Shortcut to log and return an error. | |
def error(message:String):Error = { | |
errorDict += (name -> message) | |
Error(message) | |
} | |
// Information about the field. | |
val field = Field(name, fieldType, default) | |
// Ensure that a field is not declared twice using a different signature. | |
fieldDict.get(name) match { | |
case Some(f) if f != field => throw new DuplicateFieldException(field, "A field named %s has already been defined for %s".format(name, getClass.getSimpleName)) | |
case _ => { | |
// Store the Field. | |
fieldDict += (name -> field) | |
// Attempt to retrieve the data. | |
val value = data.get(name) match { | |
case Some(v) => { | |
fieldType.clean(v) match { | |
case Valid(v) => Valid(v) | |
case Error(e) => error(e) | |
} | |
} | |
case None => { | |
default match { | |
case Default(v) => Valid(v) | |
case Required => error("This field is required") | |
} | |
} | |
} | |
FieldValue(value, field) | |
} | |
} | |
} | |
} | |
// Now for the pure functional API. | |
sealed abstract class CreationResult[+T] | |
case class Created[+T](entity:T) extends CreationResult[T] | |
case class Errors(errors:Map[String,String]) extends CreationResult[Nothing] | |
object Entity { | |
def create[T <: Entity](factory: (RawData) => T, rawData:RawData):CreationResult[T] = { | |
val entity = factory(rawData) | |
if (entity.valid) { | |
Created(entity) | |
} else { | |
Errors(entity.errors) | |
} | |
} | |
} | |
// Now for a demonstration! | |
case class Person(val data:RawData) extends Entity { | |
val name = field("name", StringField) | |
val age = field("age", IntField) | |
val happiness = field("happiness", IntField, default=5) | |
} | |
val david = Person(Map("name" -> "Dave", "age" -> 26)) | |
// Access the field values. | |
println(david.name) | |
println(david.age) | |
println(david.happiness) | |
// Introspect the person's state. | |
println(david.fields) | |
println(david.errors) // Map(age -> Please enter a whole number) | |
// Try a functional approach. | |
println(Entity.create(Person, Map("name" -> "Jenny", "age" -> 25))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment