Skip to content

Instantly share code, notes, and snippets.

@djspiewak
Created October 17, 2019 17: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 djspiewak/5ecfeb3cf6f04a42b34bf6d6caf40aac to your computer and use it in GitHub Desktop.
Save djspiewak/5ecfeb3cf6f04a42b34bf6d6caf40aac to your computer and use it in GitHub Desktop.
/*
* 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