Skip to content

Instantly share code, notes, and snippets.

@rockymadden
Forked from etianen/EntityV3.scala
Created March 14, 2014 21:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rockymadden/9557583 to your computer and use it in GitHub Desktop.
Save rockymadden/9557583 to your computer and use it in GitHub Desktop.
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