app/lib/Config.scala (95 lines of code) (raw):

package lib import java.time.Instant import com.madgag.git._ import com.madgag.scalagithub.model.PullRequest import com.madgag.time.Implicits._ import io.lemonlabs.uri.Uri import lib.Config.{Checkpoint, CheckpointDetails, Sentry} import lib.labels.{Overdue, PullRequestCheckpointStatus, Seen} import org.eclipse.jgit.lib.ObjectId import org.joda import org.joda.time.Period import play.api.data.validation.ValidationError import play.api.libs.json.{Json, _} import play.api.libs.functional.syntax._ import com.madgag.scala.collection.decorators._ case class ConfigFile(checkpoints: Map[String, CheckpointDetails], sentry: Option[Sentry] = None) { lazy val checkpointsByName: Map[String, Checkpoint] = checkpoints.map { case (name, details) => name -> Checkpoint(name, details) } lazy val checkpointSet = checkpointsByName.values.toSet } object Config { def readsParseableString[T](parser: String => T): Reads[T] = new Reads[T] { def reads(json: JsValue): JsResult[T] = json match { case JsString(s) => parse(s) match { case Some(d) => JsSuccess(d) case None => JsError(Seq(JsPath() -> Seq(JsonValidationError("Error parsing string")))) } case _ => JsError(Seq(JsPath() -> Seq(JsonValidationError("Expected string")))) } private def parse(input: String): Option[T] = scala.util.control.Exception.allCatch[T] opt (parser(input)) } implicit val readsPeriod: Reads[Period] = readsParseableString(input => Period.parse("PT"+input)) implicit val readsUri: Reads[Uri] = readsParseableString(input => Uri.parse(input)) case class Sentry(projects: Seq[String]) case class CheckpointMessages(filePaths: Map[PullRequestCheckpointStatus, String]) { def filePathforStatus(status: PullRequestCheckpointStatus): Option[String] = filePaths.get(status) } object Sentry { implicit val readsSentry = Json.reads[Sentry] } object CheckpointMessages { def apply(messages: (PullRequestCheckpointStatus, String)*): CheckpointMessages = CheckpointMessages(messages.toMap) implicit val readsMessages: Reads[CheckpointMessages] = ( (JsPath \ "seen").readNullable[String].map(_.map(Seen -> _)) and (JsPath \ "overdue").readNullable[String].map(_.map(Overdue -> _)) ) { (seen, overdue) => val messages = Map.empty[PullRequestCheckpointStatus, String] ++ seen.toMap ++ overdue.toMap CheckpointMessages(messages) } val defaults: Map[PullRequestCheckpointStatus, String] = Map( Seen -> "Please check your changes!", Overdue -> "What's gone wrong?" ) } implicit val readsCheckpointDetails = Json.reads[CheckpointDetails] implicit val readsConfig = Json.reads[ConfigFile] def readConfigFrom(configFileObjectId: ObjectId)(implicit repoThreadLocal: ThreadLocalObjectDatabaseResources): JsResult[ConfigFile] = { implicit val reader = repoThreadLocal.reader() val fileJson = Json.parse(configFileObjectId.open.getCachedBytes(4096)) Json.fromJson[ConfigFile](fileJson) } case class CheckpointDetails( url: Uri, overdue: joda.time.Period, disableSSLVerification: Option[Boolean] = None, messages: Option[CheckpointMessages] = None ) { val sslVerification = !disableSSLVerification.contains(true) def overdueInstantFor(pr: PullRequest): Option[Instant] = pr.merged_at.map(_.plus(overdue).toInstant) } object Checkpoint { implicit def checkpointToDetails(c: Checkpoint) = c.details } case class Checkpoint(name: String, details: CheckpointDetails) { lazy val nameMarkdown = s"[$name](${details.url})" } case class RepoConfig( configByFolder: Map[String, JsResult[ConfigFile]] ) { val validConfigByFolder: Map[String, ConfigFile] = configByFolder.collect { case (folder, JsSuccess(config, _)) => folder -> config } val foldersWithValidConfig: Set[String] = validConfigByFolder.keySet val foldersByCheckpointName: Map[String, Seq[String]] = (for { (folder, checkpointNames) <- validConfigByFolder.mapV(_.checkpoints.keySet).toSeq checkpointName <- checkpointNames } yield checkpointName -> folder).groupBy(_._1).mapV(_.map(_._2)) val checkpointsNamedInMultipleFolders: Map[String, Seq[String]] = foldersByCheckpointName.filter(_._2.size > 1) require(checkpointsNamedInMultipleFolders.isEmpty, s"Duplicate checkpoints defined in multiple config files: ${checkpointsNamedInMultipleFolders.mapV(_.mkString("(",", ",")"))}") val checkpointsByName: Map[String, Checkpoint] = validConfigByFolder.values.map(_.checkpointsByName).fold(Map.empty)(_ ++ _) } }