Skip to content

Instantly share code, notes, and snippets.

@afsalthaj
Last active April 1, 2019 10:50
Show Gist options
  • Save afsalthaj/74c91b2aef2a253d2126ad8b2eafb277 to your computer and use it in GitHub Desktop.
Save afsalthaj/74c91b2aef2a253d2126ad8b2eafb277 to your computer and use it in GitHub Desktop.
// Thanks to @adam_evans for this.. and cats documentation as well
// This can be a very simple open source... its far better than just about everything out there!
def fromCommandLineArguments(args: List[String]) = {
args.sliding(2, 2).flatMap(t => (
(t.headOption.flatMap(str => str.startsWith("--").option(str.replace("--", ""))) |@|
t.lift(1).flatMap(str => (!str.startsWith("--")).option(str))){(_, _)}).toList).toMap
import Types._
import scalaz.syntax.std.option._
import scalaz.ValidationNel
import scalaz.NonEmptyList
import scalaz.Reader
import scalaz.\/
import scalaz.Validation
import scalaz.Monad
final case class ConfigAction[A](run: EnvReader[ValidationNel[ConfigError, A]]) {
def map[B](f: A ⇒ B): ConfigAction[B] =
ConfigAction { run.map { _.map(f) } }
def disjunctioned: EnvReader[NonEmptyList[ConfigError] \/ A] =
run.map { _.disjunction }
def flatMap[B](f: A => ConfigAction[B]): ConfigAction[B] =
ConfigAction(Reader(env => run(env).andThen(a => f(a).run(env))))
}
object ConfigAction extends ConfigActionInstances {
// Safely parse a environment variable to a given type
def read[A: Unmarshaller](key: String): ConfigAction[A] =
fromUnmarshaller(key) { Unmarshaller[A] }
def readWithDefault[A: Unmarshaller](key: String)(default: A): ConfigAction[A] =
fromUnmarshaller(key) {
Unmarshaller[A].recover { case Unmarshaller.Error.MissingEnvValue ⇒ default }
}
def fromUnmarshaller[A](key: String)(unmarshaller: Unmarshaller[A]): ConfigAction[A] =
ConfigAction {
Reader { (env: Env) ⇒
unmarshaller
.read { env.get(key).toMaybe }
.leftMap(ConfigError(key, _))
.validationNel
}
}
}
trait ConfigActionInstances {
implicit val monad: Monad[ConfigAction] = new Monad[ConfigAction] {
override def point[A](a: => A): ConfigAction[A] =
ConfigAction(Reader((_: Env) ⇒ Validation.success[NonEmptyList[ConfigError], A](a)))
override def ap[A, B](c1: ⇒ ConfigAction[A])(c2: ⇒ ConfigAction[(A) ⇒ B]): ConfigAction[B] =
ConfigAction(c1.run.flatMap(validation ⇒ c2.run.map(validation.ap(_))))
override def bind[A, B](fa: ConfigAction[A])(f: A => ConfigAction[B]): ConfigAction[B] =
fa.flatMap(f)
}
}
import scalaz.Show
final case class ConfigError(key: String, value: Unmarshaller.Error)
object ConfigError {
implicit val configErrorShow: Show[ConfigError] = Show.show {
case ConfigError(key, Unmarshaller.Error.MissingEnvValue) ⇒ s"Config error, required environment variable ${key} is missing"
case ConfigError(key, Unmarshaller.Error.InvalidEnvValue(provided, expected)) ⇒ s"Config error, invalid ${key} environment variable. Expected ${expected}, got ${provided}"
}
}
import java.net.URI
import Unmarshaller.Error
import scalaz.syntax.either._
import scalaz.syntax.id._
import scalaz.syntax.maybe._
import scalaz.{-\/, Maybe, NonEmptyList, \/, \/-}
import scala.annotation.tailrec
final case class Unmarshaller[A](run: Maybe[String] => Unmarshaller.Error \/ A) {
def read(s: Maybe[String]): Unmarshaller.Error \/ A =
run(s)
def map[B](f: A => B): Unmarshaller[B] =
Unmarshaller { run(_).map(f) }
def mapError[B](f: A => Unmarshaller.Error \/ B): Unmarshaller[B] =
Unmarshaller { run(_).flatMap(f) }
def flatMap[B](f: A => Unmarshaller[B]): Unmarshaller[B] =
Unmarshaller { str => run(str).flatMap { f(_).run(str) }}
def recover(err: PartialFunction[Unmarshaller.Error, A]): Unmarshaller[A] =
Unmarshaller { run(_).recover(err) }
}
object Unmarshaller extends UnmarshallerInstances {
sealed trait Error
object Error {
case object MissingEnvValue extends Error
final case class InvalidEnvValue(provided: String, expected: String) extends Error
}
@SuppressWarnings(Array("org.wartremover.warts.Overloading"))
def apply[A: Unmarshaller]: Unmarshaller[A] =
implicitly[Unmarshaller[A]]
}
trait UnmarshallerInstances {
implicit val stringValueUnmarshaller: Unmarshaller[String] =
Unmarshaller { _ \/> Error.MissingEnvValue }
implicit val boolValueUnmarshaller: Unmarshaller[Boolean] =
Unmarshaller[String].mapError { value ⇒ \/.fromTryCatchNonFatal(value.toBoolean).leftMap(_ => Error.InvalidEnvValue(value, "boolean"))}
implicit val intValueUnmarshaller: Unmarshaller[Int] =
Unmarshaller[String].mapError { value ⇒ \/.fromTryCatchNonFatal(value.toInt).leftMap(_ => Error.InvalidEnvValue(value, "integer")) }
implicit val longValueUnmarshaller: Unmarshaller[Long] =
Unmarshaller[String].mapError { value ⇒ \/.fromTryCatchNonFatal(value.toLong).leftMap(_ => Error.InvalidEnvValue(value, "long"))}
implicit def maybeValueUnmarshaller[A: Unmarshaller]: Unmarshaller[Maybe[A]] =
Unmarshaller {
Unmarshaller[A].read(_) match {
case -\/(Error.MissingEnvValue) ⇒ \/.right[Unmarshaller.Error, Maybe[A]](Maybe.empty)
case other ⇒ other.map(Maybe.just)
}
}
implicit def nonEmptyListValueUnmarshaller[A: Unmarshaller]: Unmarshaller[NonEmptyList[A]] =
Unmarshaller { value ⇒
val list = value.getOrElse("").split(",").map(_.trim).filter(_.nonEmpty).toList
list match {
case x :: xs ⇒
NonEmptyList(x, xs: _*).traverse1[Unmarshaller.Error \/ ?, A](_.just |> Unmarshaller[A].read)
case Nil ⇒
Unmarshaller.Error.InvalidEnvValue(value.getOrElse(""), "nonemptylist").left[NonEmptyList[A]]
}
}
implicit def listValueUnmarshaller[A: Unmarshaller]: Unmarshaller[List[A]] =
Unmarshaller { value =>
@tailrec
def loop(items: List[String], accum: List[A]): Unmarshaller.Error \/ List[A] =
items match {
case x :: xs =>
Unmarshaller[A].read(x.just) match {
case \/-(a) =>
loop(xs, a +: accum)
case -\/(_) =>
Unmarshaller.Error.InvalidEnvValue(value.getOrElse(""), "list").left[List[A]]
}
case Nil =>
accum.right[Unmarshaller.Error]
}
val list = value.getOrElse("").split(",").map(_.trim).filter(_.nonEmpty).toList
loop(list, List.empty[A])
}
}
@forficate
Copy link

👍 Looks familiar ;)

I have also been playing with the pattern https://github.com/ajevans85/scala-simple-env-config although I aim to keep that purely environment variable based or anything which can be represented as a Map[String, String] as input with no intention of supporting HOCON other than someone parsing HOCON => Map[String, String].

One thing I've updated is to change the EnvReader stack to also have state type EnvReader[A] = Reader[Env, State[ConfigReport, A]] where ConfigReport is just a wrapper of Map[String, String] of raw successfully read values that can be used to generate a naive/simplistic config report for free and then printed to stdout.

@afsalthaj
Copy link
Author

Added the library https://github.com/ajevans85/scala-simple-env-config as a reference point.
I will be very happy to push on with the idealogies kept in the library. As far as I can say, if HOCON can be flattened this will be the best one existing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment