Skip to content

Instantly share code, notes, and snippets.

@Slakah
Last active August 12, 2019 17:34
Show Gist options
  • Select an option

  • Save Slakah/4c689751eca986fb0e0067aae00d6141 to your computer and use it in GitHub Desktop.

Select an option

Save Slakah/4c689751eca986fb0e0067aae00d6141 to your computer and use it in GitHub Desktop.
#!/usr/bin/env amm
import $ivy.{
`com.github.alexarchambault::case-app:2.0.0-M9`,
`com.gubbns::uritemplate4s:0.3.0`,
`io.circe::circe-core:0.12.0-RC2`,
`io.circe::circe-generic-extras:0.12.0-RC2`,
`org.http4s::http4s-circe:0.20.9`,
`org.http4s::http4s-dsl:0.20.9`,
`org.http4s::http4s-blaze-client:0.20.9`,
`org.slf4j:slf4j-nop:1.7.26`
}
import ammonite.ops._
import caseapp._
import cats._
import cats.effect._
import cats.implicits._
import io.circe._
import io.circe.syntax._
import io.circe.generic.extras._
import io.circe.generic.extras.auto._
import org.http4s.{UriTemplate => _, _}
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.client.blaze._
import org.http4s.client._
import org.http4s.client.dsl.io._
import org.http4s.Method._
import org.http4s.headers._
import org.http4s.syntax.all._
import uritemplate4s._
import scala.concurrent.ExecutionContext
implicit val config: Configuration = Configuration.default.withSnakeCaseMemberNames
final case class Options(
@ExtraName("o") owner: String,
@ExtraName("r") repo: List[String],
@ExtraName("l") label: List[String]
)
def logErr[T: Show](m: T) = IO(Console.err.println(Show[T].show(m)))
object App extends CaseApp[Options] {
private implicit val contextShift: ContextShift[IO] = IO.contextShift(ExecutionContext.global)
private implicit val timer: Timer[IO] = IO.timer(ExecutionContext.global)
override def run(opts: Options, arg: RemainingArgs): Unit = {
BlazeClientBuilder[IO](ExecutionContext.global).resource
.use(doRun(_, opts))
.unsafeRunSync()
}
private val apiUrlIo = IO(sys.env.get("GITHUB_HOST")).flatMap {
case Some(host) => IO.fromEither(Uri.fromString(show"https://$host/api/v3"))
case None => IO.pure(uri"https://api.github.com")
}
private def doRun(client: Client[IO], opts: Options) =
for {
token <- ghToken
apiUrl <- apiUrlIo
homePage <- GitHubApi.home(client, token, apiUrl)
prs <- opts.repo.parTraverse(getPrs(homePage, opts.owner, _)).map(_.flatten)
filteredPrs = filterPrs(prs, opts.label)
} yield println(filteredPrs.asJson.noSpaces)
private def ghToken =
IO.fromEither(sys.env.get("GITHUB_TOKEN")
.map(GitHubToken.apply)
.toRight(new Exception("GITHUB_TOKEN is unset")))
private def getPrs(homePage: GitHubApi, owner: String, repo: String) = for {
repoPage <- homePage
.follow("repository_url", _.expand("owner" -> owner, "repo" -> repo))
pullPages <- repoPage.followCollection("pulls_url")
prs <- pullPages.parTraverse { pullPage =>
for {
issuePage <- pullPage.follow("issue_url")
issue <- IO.fromEither(issuePage.page.as[Issue])
pull <- IO.fromEither(pullPage.page.as[Pull])
} yield Pr(pull, issue)
}
} yield prs
private def filterPrs(prs: List[Pr], labelNames: List[String]): List[Pr] =
labelNames match {
case Nil => prs
case _ => prs.filter { case Pr(_, issue) =>
issue.labels
.map(_.name)
.exists(labelNames.contains)
}
}
}
@main
def entrypoint(args: String*) = App.main(args.toArray)
final case class GitHubToken(value: String) extends AnyVal
object GitHubToken {
implicit val gitHubTokenShow: Show[GitHubToken] = Show.show(_.value)
}
implicit val uriShow: Show[Uri] = Show.fromToString
final class GitHubApi(request: Uri => IO[Json], val page: Json) {
private def asCollection: IO[List[GitHubApi]] =
IO.fromEither(page.as[List[Json]].map(_.map(new GitHubApi(request, _))))
def followCollection(key: String): IO[List[GitHubApi]] =
follow(key)
.flatMap(_.asCollection)
def follow(key: String): IO[GitHubApi] =
follow(key, _.expandVars())
def followCollection(key: String, expand: UriTemplate => ExpandResult): IO[List[GitHubApi]] =
follow(key, expand)
.flatMap(_.asCollection)
def follow(key: String, expand: UriTemplate => ExpandResult): IO[GitHubApi] =
follow(_.downField(key).as[String], expand)
def followCollection(
lookup: HCursor => Decoder.Result[String],
expand: UriTemplate => ExpandResult
): IO[List[GitHubApi]] =
follow(lookup, expand)
.flatMap(_.asCollection)
def follow(
lookup: HCursor => Decoder.Result[String],
expand: UriTemplate => ExpandResult
): IO[GitHubApi] = {
val uriE = for {
rawTemplate <- lookup(page.hcursor)
template <- UriTemplate.parse(rawTemplate)
.leftMap(err => new Exception(err.message))
uri <- Uri.fromString(expand(template).value)
} yield uri
for {
uri <- IO.fromTry(uriE.toTry)
_ <- logErr(show"requesting $uri...")
nextPage <- request(uri)
} yield new GitHubApi(request, nextPage)
}
}
object GitHubApi {
def home(client: Client[IO], token: GitHubToken, root: Uri): IO[GitHubApi] = {
val header = Header.Raw("Authorization".ci, show"token $token")
val request = (uri: Uri) => client.expect[Json](GET(uri, header))
request(root).map(new GitHubApi(request, _))
}
}
final case class Pr(pull: Pull, issue: Issue)
final case class Label(name: String)
final case class Repo(fullName: String)
final case class Head(repo: Repo)
final case class Pull(url: String, title: String, number: Long, htmlUrl: String, head: Head)
final case class Issue(number: Long, title: String, labels: List[Label])
object Pull {
implicit val pullShow: Show[Pull] = Show.fromToString
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment