Last active
August 12, 2019 17:34
-
-
Save Slakah/4c689751eca986fb0e0067aae00d6141 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
| #!/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