Created
October 17, 2019 17:41
-
-
Save djspiewak/5ecfeb3cf6f04a42b34bf6d6caf40aac to your computer and use it in GitHub Desktop.
This file contains 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
/* | |
* Copyright 2014–2019 SlamData Inc. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package quasar.api.destination.param | |
import argonaut._, Argonaut._ | |
import cats.{Eq, Show} | |
import cats.implicits._ | |
import scala.{Boolean, Int, List, Option, Predef, Product, Serializable, StringContext}, Predef._ | |
import scala.util.{Either, Left, Right} | |
sealed trait Param[A] | |
extends Product | |
with Serializable | |
with (Json => Either[ParamError, A]) | |
private[param] trait LowPriorityInstances { | |
import Param.{Boolean, Integer, Enum} | |
implicit def codecJson[A: CodecJson]: CodecJson[Param[A]] = | |
CodecJson( | |
{ | |
case Boolean => | |
Json("type" -> "boolean".asJson) | |
case Integer(min, max, step) => | |
Json( | |
"type" -> "integer".asJson, | |
"min" -> min.asJson, | |
"max" -> max.asJson, | |
"step" -> step.asJson) | |
case Enum(possibilities) => | |
Json( | |
"type" -> "enum".asJson, | |
"possibilities" -> possibilities.asJson) | |
}, | |
{ cursor => | |
(cursor --\ "type").as[String] flatMap { | |
case "enum" => | |
(cursor --\ "possibilities").as[List[A]].map(Enum(_)) | |
case t => | |
DecodeResult.fail[Param[A]](s"unrecognized param type '$t' (for unknown expected type)", cursor.history) | |
} | |
}) | |
} | |
object Param extends LowPriorityInstances { | |
implicit def equal[A: Eq]: Eq[Param[A]] = | |
Eq instance { | |
case (Boolean, Boolean) => | |
true | |
case (Integer(min1, max1, step1), Integer(min2, max2, step2)) => | |
min1 === min2 && max1 === max2 && step1 === step2 | |
case (Enum(pos1), Enum(pos2)) => | |
pos1 === pos2 | |
} | |
implicit def show[A: Show]: Show[Param[A]] = | |
Show show { | |
case Boolean => "Boolean" | |
case Integer(min, max, step) => s"Integer(${min.show}, ${max.show}, ${step.show})" | |
case Enum(possibilities) => s"Enum(${possibilities.show})" | |
} | |
// we need to shade the CodecJson[Boolean | Int] implicit | |
// this is actually quite tricky because we're causing | |
// decoding to fail on {"type":"boolean"} when the expected | |
// Param type is Int (or other), and analogously for {"type":"integer"}. | |
// This makes sense, but it's also a rather interesting | |
// collision of invariant functor codecs and gadts. Note | |
// that the *encoder* works just fine | |
implicit val decodeBoolean: DecodeJson[Param[Boolean]] = | |
DecodeJson { cursor => | |
(cursor --\ "type").as[String] flatMap { | |
case "boolean" => | |
DecodeResult.ok[Param[Boolean]](Boolean) | |
case "enum" => | |
(cursor --\ "possibilities").as[List[A]].map(Enum(_)) | |
case t => | |
DecodeResult.fail[Param[Boolean]](s"unrecognized param type '$t' (for expected type Boolean)", cursor.history) | |
} | |
} | |
implicit val decodeInteger: DecodeJson[Param[Int]] = | |
DecodeJson { cursor => | |
(cursor --\ "type").as[String] flatMap { | |
case "integer" => | |
for { | |
min <- (cursor --\ "min").as[Option[Int]] | |
max <- (cursor --\ "max").as[Option[Int]] | |
step <- (cursor --\ "step").as[Option[IntegerStep]] | |
} yield Integer(min, max, step) | |
case "enum" => | |
(cursor --\ "possibilities").as[List[A]].map(Enum(_)) | |
case t => | |
DecodeResult.fail[Param[Int]](s"unrecognized param type '$t' (for expected type Int)", cursor.history) | |
} | |
} | |
case object Boolean extends Param[Boolean] { | |
def apply(json: Json): Either[ParamError, scala.Boolean] = | |
json.bool.map(Right(_)).getOrElse(Left(ParamError.InvalidBoolean(json))) | |
} | |
final case class Integer( | |
min: Option[Int], | |
max: Option[Int], | |
step: Option[IntegerStep]) | |
extends Param[Int] { | |
def apply(json: Json): Either[ParamError, Int] = | |
for { | |
i <- json.as[Int].toEither.leftMap(_ => ParamError.InvalidInt(json)) | |
_ <- if (min.map(i >= _).getOrElse(true) && max.map(i < _).getOrElse(true)) | |
Right(()) | |
else | |
Left(ParamError.IntOutOfRange(i, min, max)) | |
_ <- if (step(i)) | |
Right(()) | |
else | |
Left(ParamError.IntOutOfStep(i, step)) | |
} yield i | |
} | |
final case class Enum[A: DecodeJson](possibilities: List[A]) extends Param[A] { | |
def apply(json: Json): Either[ParamError, A] = | |
for { | |
a <- json.as[A].toEither.leftMap(_ => ParamError.InvalidEnum(json)) | |
_ <- if (possibilities.contains(a)) | |
Right(()) | |
else | |
Left(ParamError.ValueNotInEnum(a, possibilities)) | |
} yield a | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment