All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ph.samson.atbp.jira.Client.scala Maven / Gradle / Ivy

The newest version!
package ph.samson.atbp.jira

import ph.samson.atbp.http.StatusCheck
import ph.samson.atbp.jira.model.EditIssueRequest
import ph.samson.atbp.jira.model.Issue
import ph.samson.atbp.jira.model.SearchRequest
import ph.samson.atbp.jira.model.SearchResults
import zio.LogLevel
import zio.Scope
import zio.Task
import zio.ZIO
import zio.ZLayer
import zio.http
import zio.http.Body
import zio.http.Client as HttpClient
import zio.http.Header.Accept
import zio.http.Header.Authorization
import zio.http.Header.ContentType
import zio.http.Header.UserAgent
import zio.http.Header.UserAgent.ProductOrComment
import zio.http.Headers
import zio.http.MediaType
import zio.http.Response
import zio.http.Status
import zio.http.URL
import zio.http.ZClient
import zio.http.ZClientAspect

/** Jira REST API client.
  *
  * @see
  *   https://developer.atlassian.com/cloud/jira/platform/rest/
  */
trait Client {
  def getIssue(key: String): Task[Issue]
  def search(jql: String): Task[List[Issue]]
  def addLabel(key: String, label: String): Task[Unit]
  def removeLabel(key: String, label: String): Task[Unit]
}

object Client {

  private class LiveImpl(client: HttpClient) extends Client {

    override def search(jql: String): Task[List[Issue]] = {
      val request = SearchRequest(jql, Issue.FieldNames, maxResults = 100)

      def tail(head: SearchResults): List[SearchRequest] = {

        def tailRequests(
            reqs: List[SearchRequest] = Nil
        ): List[SearchRequest] = {
          val lastRequest = reqs match
            case Nil       => request
            case last :: _ => last

          val nextStart = lastRequest.startAt + lastRequest.maxResults

          if nextStart < head.total then
            val nextRequest = lastRequest.copy(startAt = nextStart)
            tailRequests(nextRequest :: reqs)
          else reqs
        }

        if head.length < head.total then tailRequests() else Nil
      }

      ZIO.scoped(ZIO.logSpan("search") {
        for {
          _ <- ZIO.logDebug(s"jql: $jql")
          headResult <- doSearch(request)
          _ <- ZIO.logDebug(
            s"Got ${headResult.issues.length} of ${headResult.total} results in head"
          )
          tailResults <- ZIO.foreachPar(tail(headResult))(doSearch)
          tailIssues = tailResults.flatMap(_.issues)
          _ <- ZIO.logDebug(s"Got ${tailIssues.length} results in tails")
        } yield {
          headResult.issues ++ tailIssues
        }
      })
    }

    private def doSearch(request: SearchRequest) = {
      for {
        _ <- ZIO.logDebug(
          s"Requesting ${request.startAt + 1} to ${request.startAt + request.maxResults}"
        )
        res <- client
          .addHeader(ContentType(MediaType.application.json))
          .post("/search")(Body.from(request))
        results <- res.body.to[SearchResults]
        _ <- ZIO.logDebug(
          s"Got results ${results.startAt + 1} to ${results.startAt + results.length}"
        )
      } yield {
        results
      }
    }

    override def getIssue(key: String): Task[Issue] =
      ZIO.scoped(ZIO.logSpan(s"getIssue($key)") {
        for {
          response <- client.addPath("issue").get(key)
          result <- response.body.to[Issue]
        } yield result
      })

    override def addLabel(key: String, label: String): Task[Unit] =
      ZIO.scoped(ZIO.logSpan("addLabel") {
        for {
          _ <- client
            .addHeader(ContentType(MediaType.application.json))
            .addPath("issue")
            .put(key)(Body.from(EditIssueRequest.addLabel(label)))
        } yield ()
      })

    override def removeLabel(key: String, label: String): Task[Unit] =
      ZIO.scoped(ZIO.logSpan("removeLabel") {
        for {
          _ <- client
            .addHeader(ContentType(MediaType.application.json))
            .addPath("issue")
            .put(key)(Body.from(EditIssueRequest.removeLabel(label)))
        } yield ()
      })
  }

  def layer(conf: Conf) = ZLayer {
    val loggingAspect = ZClientAspect.requestLogging(
      level = {
        case _: Status.Error => LogLevel.Warning
        case _               => LogLevel.Debug
      },
      logRequestBody = true,
      logResponseBody = true
    )

    for {
      version <- ZIO
        .attempt({
          // read version info from JAR manifest
          val pak = Client.getClass.getPackage
          Option(pak.getImplementationVersion)
        })
        .orElseSucceed(None)
      headers = Headers(
        UserAgent(ProductOrComment.Product("atbp", version)),
        Authorization.Basic(conf.user, conf.token),
        Accept(MediaType.application.json)
      )
      url <- ZIO.fromEither(URL.decode(s"https://${conf.site}/rest/api/3"))
      client <- ZIO.serviceWith[HttpClient](
        _.addHeaders(headers).url(
          url
        ) @@ loggingAspect @@ StatusCheck.successOnly()
      )
    } yield {
      LiveImpl(client): Client
    }
    end for
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy