Skip to content

Commit

Permalink
dave - rename of Validator to Extractor, as that is really what it does
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddenton committed May 27, 2016
1 parent a3bc915 commit dab78a2
Show file tree
Hide file tree
Showing 12 changed files with 405 additions and 404 deletions.
23 changes: 12 additions & 11 deletions README.md
Expand Up @@ -4,23 +4,24 @@
<a href='https://coveralls.io/github/daviddenton/crossyfield?branch=master'><img src='https://coveralls.io/repos/github/daviddenton/crossyfield/badge.svg?branch=master' alt='Coverage Status' /></a>&nbsp;&nbsp;&nbsp;
<a href='https://bintray.com/daviddenton/maven/crossyfield/_latestVersion'><img src='https://api.bintray.com/packages/daviddenton/maven/crossyfield/images/download.svg' alt='Download' /></a>

This library provides a way of performing generic cross-field validation in Scala `for comprehensions`.
This library attempts to provide a nice way of performing generic cross-field validation in Scala `for comprehensions`.

## Get it
Crossyfield has no dependencies.

Add the following lines to ```build.sbt```:

```scala
resolvers += "JCenter" at "https://jcenter.bintray.com"
libraryDependencies += "io.github.daviddenton" %% "crossyfield" % "1.0.0"
libraryDependencies += "io.github.daviddenton" %% "crossyfield" % "1.2.0"
```

## Learn it
The library provides a single concept, the `Validator`, which exposes a couple of `<--?()` methods to provide validation operations.

```scala

case class Range(startDate: LocalDate, middleDate: Option[LocalDate], endDate: LocalDate)
Crossyfield has no dependencies.

```
## Learn it
The library provides a single concept, the `Extractor`, which exposes a couple of `<--?()` methods to provide extraction/validation operations. The result of
the extraction operation is one of 3 Case classes:
1. `Extracted[T]` - when the object was successfully extracted
2. `NotProvided` - when the object was missing, but was optional
3. `Invalid(Seq(Symbol -> String))` - when the object was invalid or missing when required. Contains a sequence of errors denoting the failures

The `Extractors` can be used in `for comprehensions` and chained in a graph. The first failure in the extraction chain will short-circuit the operations
and return an `Invalid` instance containing the error.
2 changes: 1 addition & 1 deletion build.sbt
Expand Up @@ -7,7 +7,7 @@ organization := orgName

name := projectName

description := "Cross-field object validation for fun and profit"
description := "Cross-field object extraction for fun and profit"

scalaVersion := "2.11.8"

Expand Down
148 changes: 148 additions & 0 deletions src/main/scala/io/github/daviddenton/crossyfield/Extractor.scala
@@ -0,0 +1,148 @@
package io.github.daviddenton.crossyfield

import io.github.daviddenton.crossyfield.Extractor.ExtractionError

import scala.language.implicitConversions
import scala.util.{Failure, Success, Try}

trait Extractor[-From, +T] {
val identifier: Symbol

/**
* Performs extraction
*/
def <--?(from: From): Extraction[T]

/**
* Performs extraction. Synonym for <--?().
*/
final def extract(from: From): Extraction[T] = <--?(from)

/**
* Performs extraction and applies the predicate to achieve a result.
*/
final def <--?(from: From, error: String, predicate: T => Boolean): Extraction[T] =
<--?(from).flatMap[T](v => if (v.map(predicate).getOrElse(true)) Extraction(v) else Invalid((identifier, error)))

/**
* Performs extraction and applies the predicate to achieve a result. Synonym for <--?().
*/
final def extract(from: From, reason: String, predicate: T => Boolean): Extraction[T] = <--?(from, reason, predicate)
}

object Extractor {

type ExtractionError = (Symbol, String)

/**
* Constructs a simple Mandatory Extractor from applying the passed Extractor function
*/
def mk[From, T](id: Symbol)(fn: From => Extraction[T]): Extractor[From, T] = new Extractor[From, T] {
override val identifier = id

override def <--?(from: From): Extraction[T] = fn(from)
}

/**
* Constructs a simple Mandatory Extractor for from a function, returns either Extracted or Invalid upon
* an failure from the function
*/
def mk[From, T](id: Symbol, message: String, fn: From => T): Extractor[From, T] = new Extractor[From, T] {
override val identifier: Symbol = id

override def <--?(from: From): Extraction[T] = Try(fn(from)) match {
case Success(value) => Extracted(value)
case Failure(e) => Invalid(identifier, message)
}
}
}

/**
* Result of an attempt to extract an object from a target
*/
sealed trait Extraction[+T] {
def flatMap[O](f: Option[T] => Extraction[O]): Extraction[O]

def map[O](f: Option[T] => O): Extraction[O]

def orDefault[O >: T](f: => O): Extraction[O]
}

object Extraction {

/**
* Collect errors together from several extractions.
*/
def collectErrors(extractions: Extraction[_]*): Seq[ExtractionError] = <--?(extractions) match {
case Invalid(ip) => ip
case _ => Nil
}

/**
* Utility method for combining the results of many Extraction into a single Extraction, simply to get an overall
* extraction result in the case of failure.
*/
def <--?(extractions: Seq[Extraction[_]]): Extraction[Nothing] = {
val missingOrFailed = extractions.flatMap {
case Invalid(ip) => ip
case _ => Nil
}
if (missingOrFailed.isEmpty) NotProvided else Invalid(missingOrFailed)
}

/**
* Wraps in a successful Extraction - this assumes the object was not mandatory.
*/
def apply[T](t: Option[T]): Extraction[T] = t.map(Extracted(_)).getOrElse(NotProvided)

/**
* For optional cases, you can use this to convert an Extraction(None) -> NotProvided
*/
def flatten[T](extraction: Extraction[Option[T]]): Extraction[T] =
extraction match {
case Extracted(opt) => opt.map(Extracted(_)).getOrElse(NotProvided)
case NotProvided => NotProvided
case Invalid(ip) => Invalid(ip)
}
}

/**
* Represents a object which was provided and extracted successfully.
*/
case class Extracted[T](value: T) extends Extraction[T] {
def flatMap[O](f: Option[T] => Extraction[O]) = f(Some(value))

override def map[O](f: Option[T] => O) = Extracted(f(Some(value)))

override def orDefault[O >: T](f: => O): Extraction[O] = this
}

/**
* Represents an object which was optional and missing. Ie. still a passing case.
*/
object NotProvided extends Extraction[Nothing] {

override def toString = "NotProvided"

def flatMap[O](f: Option[Nothing] => Extraction[O]) = f(None)

override def map[O](f: Option[Nothing] => O) = Extracted(f(None))

override def orDefault[T](f: => T): Extraction[T] = Extracted(f)
}

/**
* Represents a object which could not be extracted due to it being invalid or missing when required.
*/
case class Invalid(invalid: Seq[ExtractionError]) extends Extraction[Nothing] {
def flatMap[O](f: Option[Nothing] => Extraction[O]) = Invalid(invalid)

override def map[O](f: Option[Nothing] => O) = Invalid(invalid)

override def orDefault[T](f: => T): Extraction[T] = this
}

object Invalid {
def apply(p: ExtractionError): Invalid = Invalid(Seq(p))
}

@@ -1,15 +1,15 @@
package io.github.daviddenton.crossyfield

/**
* Convenience Validators
* Convenience Extractors
*/
object Validators {
object Extractors {

/**
* Converting to various primitive types from a String
*/
object string {
val optional = new PrimitiveValidators(false)
val required = new PrimitiveValidators(true)
val optional = new PrimitiveExtractors(false)
val required = new PrimitiveExtractors(true)
}
}
Expand Up @@ -3,10 +3,10 @@ package io.github.daviddenton.crossyfield
import java.time.{LocalDate, LocalDateTime, ZonedDateTime}
import java.util.UUID

class PrimitiveValidators(required: Boolean) {
class PrimitiveExtractors(required: Boolean) {
private def mk[T](id: Symbol, msg: String, required: Boolean, fn: String => T) =
Validator.mk(id) {
in: String => if (in.isEmpty && !required) Ignored else Validator.mk(id, msg, fn) <--? in
Extractor.mk(id) {
in: String => if (in.isEmpty && !required) NotProvided else Extractor.mk(id, msg, fn) <--? in
}

def int(id: Symbol, msg: String = "invalid int") = mk(id, msg, required, (s: String) => s.toInt)
Expand Down
148 changes: 0 additions & 148 deletions src/main/scala/io/github/daviddenton/crossyfield/Validator.scala

This file was deleted.

0 comments on commit dab78a2

Please sign in to comment.