diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..9fa06984 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Scala Steward: Reformat with scalafmt 3.7.2 +554a8897231b562317c80e615ce41fef92bfef2f diff --git a/.scalafmt.conf b/.scalafmt.conf index 0bb5f198..51a0f995 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,6 +1,6 @@ runner.dialect=scala3 -version = "3.5.9" +version = "3.7.2" maxColumn = 140 align.preset = some align.tokens."+" = [ diff --git a/bootstrap/src/main/scala/org/polyvariant/Args.scala b/bootstrap/src/main/scala/org/polyvariant/Args.scala index 6c22803d..b8ee3e4a 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Args.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Args.scala @@ -4,7 +4,10 @@ object Args { private val switch = "-(\\w+)".r private val option = "--(\\w+)".r - private def parseNext(pendingArguments: List[String], previousResult: Map[String, String]): Map[String, String] = + private def parseNext( + pendingArguments: List[String], + previousResult: Map[String, String] + ): Map[String, String] = pendingArguments match { case Nil => previousResult case option(opt) :: value :: tail => parseNext(tail, previousResult ++ Map(opt -> value)) @@ -14,7 +17,9 @@ object Args { } // TODO: Consider switching to https://ben.kirw.in/decline/ after https://github.com/bkirwi/decline/pull/293 - def parse(args: List[String]): Map[String, String] = + def parse( + args: List[String] + ): Map[String, String] = parseNext(args.toList, Map()) } diff --git a/bootstrap/src/main/scala/org/polyvariant/Config.scala b/bootstrap/src/main/scala/org/polyvariant/Config.scala index 838b9132..f377eacf 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Config.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Config.scala @@ -15,7 +15,9 @@ final case class Config( object Config { - def fromArgs[F[_]: MonadThrow](args: Map[String, String]): F[Config] = + def fromArgs[F[_]: MonadThrow]( + args: Map[String, String] + ): F[Config] = MonadThrow[F] .catchNonFatal { Config( diff --git a/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala b/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala index c633e317..0ba72206 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Gitlab.scala @@ -24,15 +24,32 @@ import cats.MonadThrow import io.circe.* trait Gitlab[F[_]] { - def mergeRequests(projectId: Long): F[List[Gitlab.MergeRequestInfo]] - def deleteMergeRequest(projectId: Long, mergeRequestId: Long): F[Unit] - def createWebhook(projectId: Long, pitgullUrl: Uri): F[Unit] - def listWebhooks(projectId: Long): F[List[Gitlab.Webhook]] + + def mergeRequests( + projectId: Long + ): F[List[Gitlab.MergeRequestInfo]] + + def deleteMergeRequest( + projectId: Long, + mergeRequestId: Long + ): F[Unit] + + def createWebhook( + projectId: Long, + pitgullUrl: Uri + ): F[Unit] + + def listWebhooks( + projectId: Long + ): F[List[Gitlab.Webhook]] + } object Gitlab { - def apply[F[_]](using ev: Gitlab[F]): Gitlab[F] = ev + def apply[F[_]]( + using ev: Gitlab[F] + ): Gitlab[F] = ev def sttpInstance[F[_]: Logger: MonadThrow]( baseUri: Uri, @@ -40,18 +57,24 @@ object Gitlab { )( using backend: SttpBackend[Identity, Any] // FIXME: https://github.com/polyvariant/pitgull/issues/265 ): Gitlab[F] = { - def runRequest[O](request: Request[O, Any]): F[O] = + def runRequest[O]( + request: Request[O, Any] + ): F[O] = request .header("Private-Token", accessToken) .send(backend) .pure[F] .map(_.body) // FIXME - change in https://github.com/polyvariant/pitgull/issues/265 - def runGraphQLQuery[A: IsOperation, B](a: SelectionBuilder[A, B]): F[B] = + def runGraphQLQuery[A: IsOperation, B]( + a: SelectionBuilder[A, B] + ): F[B] = runRequest(a.toRequest(baseUri.addPath("api", "graphql"))).rethrow new Gitlab[F] { - def mergeRequests(projectId: Long): F[List[MergeRequestInfo]] = + def mergeRequests( + projectId: Long + ): F[List[MergeRequestInfo]] = Logger[F].info(s"Looking up merge requests for project: $projectId") *> mergeRequestsQuery(projectId) .mapEither(_.toRight(DecodingError("Project not found"))) @@ -60,7 +83,10 @@ object Gitlab { Logger[F].info(s"Found merge requests. Size: ${result.size}") } - def deleteMergeRequest(projectId: Long, mergeRequestId: Long): F[Unit] = for { + def deleteMergeRequest( + projectId: Long, + mergeRequestId: Long + ): F[Unit] = for { _ <- Logger[F].debug(s"Request to remove $mergeRequestId") result <- runRequest( basicRequest.delete( @@ -79,7 +105,10 @@ object Gitlab { ) } yield () - def createWebhook(projectId: Long, pitgullUrl: Uri): F[Unit] = for { + def createWebhook( + projectId: Long, + pitgullUrl: Uri + ): F[Unit] = for { _ <- Logger[F].debug(s"Creating webhook to $pitgullUrl") result <- runRequest( basicRequest @@ -100,7 +129,9 @@ object Gitlab { ) } yield () - def listWebhooks(projectId: Long): F[List[Webhook]] = for { + def listWebhooks( + projectId: Long + ): F[List[Webhook]] = for { _ <- Logger[F].debug(s"Listing webhooks for $projectId") response <- runRequest( basicRequest @@ -141,7 +172,9 @@ object Gitlab { private def flattenTheEarth[A]: Option[List[Option[Option[Option[List[Option[A]]]]]]] => List[A] = _.toList.flatten.flatten.flatten.flatten.flatten.flatten - private def mergeRequestInfoSelection(projectId: Long): SelectionBuilder[MergeRequest, MergeRequestInfo] = ( + private def mergeRequestInfoSelection( + projectId: Long + ): SelectionBuilder[MergeRequest, MergeRequestInfo] = ( MergeRequest.iid.mapEither(_.toLongOption.toRight(DecodingError("MR IID wasn't a Long"))) ~ MergeRequest .author(UserCore.username) @@ -168,7 +201,9 @@ object Gitlab { hasConflicts = hasConflicts ) - private def mergeRequestsQuery(projectId: Long) = + private def mergeRequestsQuery( + projectId: Long + ) = Query .projects(ids = List(show"gid://gitlab/Project/$projectId").some)( ProjectConnection diff --git a/bootstrap/src/main/scala/org/polyvariant/Logger.scala b/bootstrap/src/main/scala/org/polyvariant/Logger.scala index 47274391..535ab938 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Logger.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Logger.scala @@ -5,25 +5,64 @@ import cats.effect.kernel.Sync import scala.io.AnsiColor._ trait Logger[F[_]] { - def debug(msg: String): F[Unit] - def success(msg: String): F[Unit] - def info(msg: String): F[Unit] - def warn(msg: String): F[Unit] - def error(msg: String): F[Unit] + + def debug( + msg: String + ): F[Unit] + + def success( + msg: String + ): F[Unit] + + def info( + msg: String + ): F[Unit] + + def warn( + msg: String + ): F[Unit] + + def error( + msg: String + ): F[Unit] + } object Logger { - def apply[F[_]](using ev: Logger[F]): Logger[F] = ev + + def apply[F[_]]( + using ev: Logger[F] + ): Logger[F] = ev def wrappedPrint[F[_]: Sync] = new Logger[F] { - private def colorPrinter(color: String)(msg: String): F[Unit] = + + private def colorPrinter( + color: String + )( + msg: String + ): F[Unit] = Sync[F].delay(println(s"$color$msg$RESET")) - override def debug(msg: String): F[Unit] = colorPrinter(CYAN)(msg) - override def success(msg: String): F[Unit] = colorPrinter(GREEN)(msg) - override def info(msg: String): F[Unit] = colorPrinter(WHITE)(msg) - override def warn(msg: String): F[Unit] = colorPrinter(YELLOW)(msg) - override def error(msg: String): F[Unit] = colorPrinter(RED)(msg) + override def debug( + msg: String + ): F[Unit] = colorPrinter(CYAN)(msg) + + override def success( + msg: String + ): F[Unit] = colorPrinter(GREEN)(msg) + + override def info( + msg: String + ): F[Unit] = colorPrinter(WHITE)(msg) + + override def warn( + msg: String + ): F[Unit] = colorPrinter(YELLOW)(msg) + + override def error( + msg: String + ): F[Unit] = colorPrinter(RED)(msg) + } } diff --git a/bootstrap/src/main/scala/org/polyvariant/Main.scala b/bootstrap/src/main/scala/org/polyvariant/Main.scala index 8afbbab5..75e58102 100644 --- a/bootstrap/src/main/scala/org/polyvariant/Main.scala +++ b/bootstrap/src/main/scala/org/polyvariant/Main.scala @@ -16,7 +16,9 @@ import cats.Monad object Main extends IOApp { - private def printMergeRequests[F[_]: Logger: Applicative](mergeRequests: List[MergeRequestInfo]): F[Unit] = + private def printMergeRequests[F[_]: Logger: Applicative]( + mergeRequests: List[MergeRequestInfo] + ): F[Unit] = mergeRequests.traverse { mr => Logger[F].info(s"ID: ${mr.mergeRequestIid} by: ${mr.authorUsername}") }.void @@ -28,18 +30,30 @@ object Main extends IOApp { ifFalse = MonadThrow[F].raiseError(new Exception("User rejected deletion")) ) - private def qualifyMergeRequestsForDeletion(botUserName: String, mergeRequests: List[MergeRequestInfo]): List[MergeRequestInfo] = + private def qualifyMergeRequestsForDeletion( + botUserName: String, + mergeRequests: List[MergeRequestInfo] + ): List[MergeRequestInfo] = mergeRequests.filter(_.authorUsername == botUserName) - private def deleteMergeRequests[F[_]: Gitlab: Logger: Applicative](project: Long, mergeRequests: List[MergeRequestInfo]): F[Unit] = + private def deleteMergeRequests[F[_]: Gitlab: Logger: Applicative]( + project: Long, + mergeRequests: List[MergeRequestInfo] + ): F[Unit] = mergeRequests.traverse(mr => Gitlab[F].deleteMergeRequest(project, mr.mergeRequestIid)).void - private def createWebhook[F[_]: Gitlab: Logger: Applicative](project: Long, webhook: Uri): F[Unit] = + private def createWebhook[F[_]: Gitlab: Logger: Applicative]( + project: Long, + webhook: Uri + ): F[Unit] = Logger[F].info("Creating webhook") *> Gitlab[F].createWebhook(project, webhook) *> Logger[F].info("Webhook created") - private def configureWebhooks[F[_]: Gitlab: Logger: Monad](project: Long, webhook: Uri): F[Unit] = for { + private def configureWebhooks[F[_]: Gitlab: Logger: Monad]( + project: Long, + webhook: Uri + ): F[Unit] = for { hooks <- Gitlab[F].listWebhooks(project).map(_.filter(_.url == webhook.toString)) _ <- Monad[F] .ifM(hooks.nonEmpty.pure[F])( @@ -48,7 +62,9 @@ object Main extends IOApp { ) } yield () - private def program[F[_]: Logger: Console: Async](args: List[String]): F[Unit] = { + private def program[F[_]: Logger: Console: Async]( + args: List[String] + ): F[Unit] = { given SttpBackend[Identity, Any] = HttpURLConnectionBackend() val parsedArgs = Args.parse(args) for { @@ -70,7 +86,9 @@ object Main extends IOApp { } yield () } - override def run(args: List[String]): IO[ExitCode] = { + override def run( + args: List[String] + ): IO[ExitCode] = { given Logger[IO] = Logger.wrappedPrint[IO] program[IO](args).recoverWith { case Config.ArgumentsParsingException => diff --git a/build.sbt b/build.sbt index 709cd7ae..9905f010 100644 --- a/build.sbt +++ b/build.sbt @@ -65,7 +65,9 @@ ThisBuild / libraryDependencySchemes ++= Seq( "io.circe" %% "circe-parser" % "early-semver" ) -def crossPlugin(x: sbt.librarymanagement.ModuleID) = +def crossPlugin( + x: sbt.librarymanagement.ModuleID +) = compilerPlugin(x.cross(CrossVersion.full)) val compilerPlugins = List( diff --git a/core/src/main/scala/io/pg/TextUtils.scala b/core/src/main/scala/io/pg/TextUtils.scala index 8715f65a..9322478f 100644 --- a/core/src/main/scala/io/pg/TextUtils.scala +++ b/core/src/main/scala/io/pg/TextUtils.scala @@ -2,13 +2,19 @@ package io.pg object TextUtils { - def trim(maxChars: Int)(s: String): String = { + def trim( + maxChars: Int + )( + s: String + ): String = { val ellipsis = "." * 3 if (s.lengthIs > maxChars) s.take(maxChars - ellipsis.length) ++ ellipsis else s } - def inline(s: String): String = + def inline( + s: String + ): String = s.replaceAll("\n", " ") } diff --git a/core/src/main/scala/io/pg/messaging/messaging.scala b/core/src/main/scala/io/pg/messaging/messaging.scala index 169fa60f..9f67821f 100644 --- a/core/src/main/scala/io/pg/messaging/messaging.scala +++ b/core/src/main/scala/io/pg/messaging/messaging.scala @@ -10,10 +10,16 @@ import cats.Invariant import cats.ApplicativeThrow trait Publisher[F[_], -A] { - def publish(a: A): F[Unit] + + def publish( + a: A + ): F[Unit] + } -final case class Processor[F[_], -A](process: fs2.Pipe[F, A, Unit]) +final case class Processor[F[_], -A]( + process: fs2.Pipe[F, A, Unit] +) object Processor { @@ -26,7 +32,9 @@ object Processor { } } - def logError[F[_]: Logger, A](msg: A): Throwable => F[Unit] = + def logError[F[_]: Logger, A]( + msg: A + ): Throwable => F[Unit] = e => Logger[F].error( "Encountered error while processing message", @@ -44,27 +52,53 @@ object Channel { given [F[_]]: Invariant[Channel[F, *]] with { - def imap[A, B](chan: Channel[F, A])(f: A => B)(g: B => A): Channel[F, B] = new { + def imap[A, B]( + chan: Channel[F, A] + )( + f: A => B + )( + g: B => A + ): Channel[F, B] = new { def consume: fs2.Stream[F, B] = chan.consume.map(f) - def publish(b: B): F[Unit] = chan.publish(g(b)) + + def publish( + b: B + ): F[Unit] = chan.publish(g(b)) + } } - def fromQueue[F[_]: Functor, A](q: Queue[F, A]): Channel[F, A] = + def fromQueue[F[_]: Functor, A]( + q: Queue[F, A] + ): Channel[F, A] = new Channel[F, A] { - def publish(a: A): F[Unit] = q.offer(a) + + def publish( + a: A + ): F[Unit] = q.offer(a) + val consume: fs2.Stream[F, A] = fs2.Stream.fromQueueUnterminated(q) } - implicit class ChannelOpticsSyntax[F[_], A](val ch: Channel[F, A]) extends AnyVal { + implicit class ChannelOpticsSyntax[F[_], A]( + val ch: Channel[F, A] + ) extends AnyVal { /** Transforms a channel into one that forwards everything to the publisher, but only consumes a subset of the original channel's * messages (the ones that match `f`). */ - def prism[B](f: PartialFunction[A, B])(g: B => A): Channel[F, B] = + def prism[B]( + f: PartialFunction[A, B] + )( + g: B => A + ): Channel[F, B] = new Channel[F, B] { - def publish(a: B): F[Unit] = ch.publish(g(a)) + + def publish( + a: B + ): F[Unit] = ch.publish(g(a)) + val consume: fs2.Stream[F, B] = ch.consume.collect(f) } diff --git a/gitlab/src/main/scala/io/pg/gitlab/Gitlab.scala b/gitlab/src/main/scala/io/pg/gitlab/Gitlab.scala index beaaed68..256c857c 100644 --- a/gitlab/src/main/scala/io/pg/gitlab/Gitlab.scala +++ b/gitlab/src/main/scala/io/pg/gitlab/Gitlab.scala @@ -36,15 +36,33 @@ import io.pg.TextUtils import cats.MonadThrow trait Gitlab[F[_]] { - def mergeRequests(projectId: Long): F[List[MergeRequestInfo]] - def acceptMergeRequest(projectId: Long, mergeRequestIid: Long): F[Unit] - def rebaseMergeRequest(projectId: Long, mergeRequestIid: Long): F[Unit] - def forceApprove(projectId: Long, mergeRequestIid: Long): F[Unit] + + def mergeRequests( + projectId: Long + ): F[List[MergeRequestInfo]] + + def acceptMergeRequest( + projectId: Long, + mergeRequestIid: Long + ): F[Unit] + + def rebaseMergeRequest( + projectId: Long, + mergeRequestIid: Long + ): F[Unit] + + def forceApprove( + projectId: Long, + mergeRequestIid: Long + ): F[Unit] + } object Gitlab { - def apply[F[_]](using F: Gitlab[F]): Gitlab[F] = F + def apply[F[_]]( + using F: Gitlab[F] + ): Gitlab[F] = F // VCS-specific MR information // Not specific to the method of fetching (no graphql model references etc.) @@ -63,7 +81,10 @@ object Gitlab { enum Status { case Success - case Other(value: String) + + case Other( + value: String + ) implicit val eq: Eq[Status] = Eq.fromUniversalEquals } @@ -80,7 +101,9 @@ object Gitlab { SC: fs2.Compiler[F, F] ): Gitlab[F] = { - def runRequest[O](request: Request[O, Any]): F[O] = + def runRequest[O]( + request: Request[O, Any] + ): F[O] = // todo multiple possible header names... request.header("Private-Token", accessToken.value).send(backend).map(_.body) @@ -96,11 +119,15 @@ object Gitlab { ): I => F[O] = runEndpoint[I, Nothing, O](endpoint).nested.map(_.merge).value - def runGraphQLQuery[A: IsOperation, B](a: SelectionBuilder[A, B]): F[B] = + def runGraphQLQuery[A: IsOperation, B]( + a: SelectionBuilder[A, B] + ): F[B] = runRequest(a.toRequest(baseUri.addPath("api", "graphql"))).rethrow new Gitlab[F] { - def mergeRequests(projectId: Long): F[List[MergeRequestInfo]] = + def mergeRequests( + projectId: Long + ): F[List[MergeRequestInfo]] = Logger[F].info( "Finding merge requests", Map( @@ -132,7 +159,9 @@ object Gitlab { private def flattenTheEarth[A]: Option[List[Option[Option[Option[List[Option[A]]]]]]] => List[A] = _.toList.flatten.flatten.flatten.flatten.flatten.flatten - private def mergeRequestInfoSelection(projectId: Long): SelectionBuilder[MergeRequest, MergeRequestInfo] = ( + private def mergeRequestInfoSelection( + projectId: Long + ): SelectionBuilder[MergeRequest, MergeRequestInfo] = ( MergeRequest.iid.mapEither(_.toLongOption.toRight(DecodingError("MR IID wasn't a Long"))) ~ MergeRequest.headPipeline(Pipeline.status.map(convertPipelineStatus)) ~ MergeRequest @@ -167,33 +196,57 @@ object Gitlab { case other => MergeRequestInfo.Status.Other(other.toString) } - def acceptMergeRequest(projectId: Long, mergeRequestIid: Long): F[Unit] = + def acceptMergeRequest( + projectId: Long, + mergeRequestIid: Long + ): F[Unit] = runInfallibleEndpoint(GitlabEndpoints.acceptMergeRequest) .apply((projectId, mergeRequestIid)) .void - def rebaseMergeRequest(projectId: Long, mergeRequestIid: Long): F[Unit] = + def rebaseMergeRequest( + projectId: Long, + mergeRequestIid: Long + ): F[Unit] = runInfallibleEndpoint(GitlabEndpoints.rebaseMergeRequest) .apply((projectId, mergeRequestIid)) .void - private def listMRApprovalRules(projectId: Long, mergeRequestIid: Long): F[List[ApprovalRule]] = + private def listMRApprovalRules( + projectId: Long, + mergeRequestIid: Long + ): F[List[ApprovalRule]] = runInfallibleEndpoint(GitlabEndpoints.listMRApprovaRules) .apply((projectId, mergeRequestIid)) - private def setMrRuleApprovals(projectId: Long, mergeRequestIid: Long, ruleId: Long, amount: Int): F[Unit] = + private def setMrRuleApprovals( + projectId: Long, + mergeRequestIid: Long, + ruleId: Long, + amount: Int + ): F[Unit] = runInfallibleEndpoint(GitlabEndpoints.setMRRuleApprovalRequirement) .apply((projectId, mergeRequestIid, ruleId, amount)) - private def getMrApprovals(projectId: Long, mergeRequestIid: Long): F[MergeRequestApprovals] = + private def getMrApprovals( + projectId: Long, + mergeRequestIid: Long + ): F[MergeRequestApprovals] = runInfallibleEndpoint(GitlabEndpoints.getMergeRequestApprovals) .apply((projectId, mergeRequestIid)) - private def setMrApprovals(projectId: Long, mergeRequestIid: Long, amount: Int): F[Unit] = + private def setMrApprovals( + projectId: Long, + mergeRequestIid: Long, + amount: Int + ): F[Unit] = runInfallibleEndpoint(GitlabEndpoints.setMergeRequestApprovals) .apply((projectId, mergeRequestIid, amount)) - def forceApprove(projectId: Long, mergeRequestIid: Long): F[Unit] = { + def forceApprove( + projectId: Long, + mergeRequestIid: Long + ): F[Unit] = { val clearDirectApprovals = Stream .eval(getMrApprovals(projectId, mergeRequestIid)) .filter(_.approvalsRequired > 0) @@ -216,7 +269,10 @@ object Gitlab { } } - final case class GitlabError(msg: String) extends Throwable(s"Gitlab error: $msg") + final case class GitlabError( + msg: String + ) extends Throwable(s"Gitlab error: $msg") + } object GitlabEndpoints { @@ -224,7 +280,15 @@ object GitlabEndpoints { private val baseEndpoint = infallibleEndpoint.in("api" / "v4") - val acceptMergeRequest: Endpoint[(Long, Long), Nothing, Unit, Any] = + val acceptMergeRequest: Endpoint[ + ( + Long, + Long + ), + Nothing, + Unit, + Any + ] = baseEndpoint // hehe putin .put @@ -232,7 +296,15 @@ object GitlabEndpoints { .in("merge_requests" / path[Long]("merge_request_iid")) .in("merge") - val rebaseMergeRequest: Endpoint[(Long, Long), Nothing, Unit, Any] = + val rebaseMergeRequest: Endpoint[ + ( + Long, + Long + ), + Nothing, + Unit, + Any + ] = baseEndpoint .put .in("projects" / path[Long]("projectId")) @@ -240,7 +312,15 @@ object GitlabEndpoints { .in("rebase") // Legacy methods, still in use though - val getMergeRequestApprovals: Endpoint[(Long, Long), Nothing, MergeRequestApprovals, Any] = + val getMergeRequestApprovals: Endpoint[ + ( + Long, + Long + ), + Nothing, + MergeRequestApprovals, + Any + ] = baseEndpoint .get .in("projects" / path[Long]("projectId")) @@ -248,7 +328,16 @@ object GitlabEndpoints { .in("approvals") .out(jsonBody[MergeRequestApprovals]) - val setMergeRequestApprovals: Endpoint[(Long, Long, Int), Nothing, Unit, Any] = + val setMergeRequestApprovals: Endpoint[ + ( + Long, + Long, + Int + ), + Nothing, + Unit, + Any + ] = baseEndpoint .post .in("projects" / path[Long]("projectId")) @@ -256,7 +345,15 @@ object GitlabEndpoints { .in("approvals") .in(query[Int]("approvals_required")) - val listMRApprovaRules: Endpoint[(Long, Long), Nothing, List[ApprovalRule], Any] = + val listMRApprovaRules: Endpoint[ + ( + Long, + Long + ), + Nothing, + List[ApprovalRule], + Any + ] = baseEndpoint .get .in("projects" / path[Long]("projectId")) @@ -266,7 +363,17 @@ object GitlabEndpoints { jsonBody[List[ApprovalRule]] ) - val setMRRuleApprovalRequirement: Endpoint[(Long, Long, Long, Int), Nothing, Unit, Any] = + val setMRRuleApprovalRequirement: Endpoint[ + ( + Long, + Long, + Long, + Int + ), + Nothing, + Unit, + Any + ] = baseEndpoint .put .in("projects" / path[Long]("projectId")) @@ -276,7 +383,11 @@ object GitlabEndpoints { object transport { - final case class ApprovalRule(id: Long, name: String, ruleType: String) { + final case class ApprovalRule( + id: Long, + name: String, + ruleType: String + ) { val isMutable: Boolean = ruleType != "code_owner" } @@ -285,7 +396,9 @@ object GitlabEndpoints { given CirceCodec[ApprovalRule] = CirceCodec.forProduct3("id", "name", "rule_type")(apply)(r => (r.id, r.name, r.ruleType)) } - final case class MergeRequestApprovals(approvalsRequired: Int) + final case class MergeRequestApprovals( + approvalsRequired: Int + ) object MergeRequestApprovals { // todo: use configured codec when https://github.com/circe/circe/pull/1800 is available diff --git a/gitlab/src/main/scala/io/pg/gitlab/webhook/webhook.scala b/gitlab/src/main/scala/io/pg/gitlab/webhook/webhook.scala index e8a638e6..872efe63 100644 --- a/gitlab/src/main/scala/io/pg/gitlab/webhook/webhook.scala +++ b/gitlab/src/main/scala/io/pg/gitlab/webhook/webhook.scala @@ -2,7 +2,10 @@ package io.pg.gitlab.webhook import io.circe.Codec -final case class WebhookEvent(project: Project, objectKind: String /* for logs */ ) +final case class WebhookEvent( + project: Project, + objectKind: String /* for logs */ +) object WebhookEvent { // todo: use configured codec when https://github.com/circe/circe/pull/1800 is available diff --git a/src/main/scala/io/pg/Application.scala b/src/main/scala/io/pg/Application.scala index 3cdb4d14..ae968ec4 100644 --- a/src/main/scala/io/pg/Application.scala +++ b/src/main/scala/io/pg/Application.scala @@ -23,7 +23,11 @@ import sttp.client3.http4s.Http4sBackend sealed trait Event extends Product with Serializable object Event { - final case class Webhook(value: WebhookEvent) extends Event + + final case class Webhook( + value: WebhookEvent + ) extends Event + } final class Application[F[_]]( diff --git a/src/main/scala/io/pg/Main.scala b/src/main/scala/io/pg/Main.scala index 7ba8559f..1d4a00b1 100644 --- a/src/main/scala/io/pg/Main.scala +++ b/src/main/scala/io/pg/Main.scala @@ -22,7 +22,9 @@ import cats.arrow.FunctionK object Main extends IOApp { - def mkLogger[F[_]: Async](fToIO: F ~> IO): Resource[F, Logger[F]] = { + def mkLogger[F[_]: Async]( + fToIO: F ~> IO + ): Resource[F, Logger[F]] = { val console = io.odin.consoleLogger[F](formatter = Formatter.colorful).withMinimalLevel(Level.Info).pure[Resource[F, *]] @@ -60,7 +62,9 @@ object Main extends IOApp { .httpApp( logHeaders = true, logBody = true, - logAction = ((msg: String) => Logger[F].debug(msg)).some + logAction = ( + (msg: String) => Logger[F].debug(msg) + ).some )(routes) BlazeServerBuilder[F] @@ -70,13 +74,21 @@ object Main extends IOApp { .resource } - def logStarting[F[_]: Logger](meta: MetaConfig) = + def logStarting[F[_]: Logger]( + meta: MetaConfig + ) = Logger[F].info("Starting application", Map("version" -> meta.version, "scalaVersion" -> meta.scalaVersion)) - def logStarted[F[_]: Logger](meta: MetaConfig) = + def logStarted[F[_]: Logger]( + meta: MetaConfig + ) = Logger[F].info("Started application", Map("version" -> meta.version, "scalaVersion" -> meta.scalaVersion)) - def serve[F[_]: Async](fToIO: F ~> IO)(config: AppConfig) = + def serve[F[_]: Async]( + fToIO: F ~> IO + )( + config: AppConfig + ) = for { given Logger[F] <- mkLogger[F](fToIO) _ <- logStarting(config.meta).toResource @@ -86,7 +98,9 @@ object Main extends IOApp { _ <- logStarted(config.meta).toResource } yield () - def run(args: List[String]): IO[ExitCode] = + def run( + args: List[String] + ): IO[ExitCode] = AppConfig .appConfig .resource[IO] diff --git a/src/main/scala/io/pg/MergeRequests.scala b/src/main/scala/io/pg/MergeRequests.scala index 1f9faaf6..e8989bd4 100644 --- a/src/main/scala/io/pg/MergeRequests.scala +++ b/src/main/scala/io/pg/MergeRequests.scala @@ -16,11 +16,18 @@ import io.pg.config.ProjectConfigReader import io.pg.gitlab.webhook.Project trait MergeRequests[F[_]] { - def build(project: Project): F[List[MergeRequestState]] + + def build( + project: Project + ): F[List[MergeRequestState]] + } object MergeRequests { - def apply[F[_]](using F: MergeRequests[F]): MergeRequests[F] = F + + def apply[F[_]]( + using F: MergeRequests[F] + ): MergeRequests[F] = F import scala.util.chaining._ @@ -41,7 +48,9 @@ object MergeRequests { )( implicit SC: fs2.Compiler[F, F] ): F[List[B]] = { - def tapLeftAndDrop[L, R](log: L => F[Unit]): Pipe[F, Either[L, R], R] = + def tapLeftAndDrop[L, R]( + log: L => F[Unit] + ): Pipe[F, Either[L, R], R] = _.evalTap(_.leftTraverse(log)).map(_.toOption).unNone val logMismatches: NonEmptyList[E] => F[Unit] = e => diff --git a/src/main/scala/io/pg/actions.scala b/src/main/scala/io/pg/actions.scala index d780a7a6..1039cd0a 100644 --- a/src/main/scala/io/pg/actions.scala +++ b/src/main/scala/io/pg/actions.scala @@ -23,14 +23,26 @@ import cats.Contravariant trait ProjectActions[F[_]] { type Action - def resolve(mr: MergeRequestState): F[Option[Action]] - def execute(action: Action): F[Unit] + + def resolve( + mr: MergeRequestState + ): F[Option[Action]] + + def execute( + action: Action + ): F[Unit] + } object ProjectActions { - def apply[F[_]](implicit F: ProjectActions[F]): F.type = F - def defaultResolve[F[_]: Applicative: Logger](mr: MergeRequestState): F[Option[ProjectAction]] = mr.mergeability match { + def apply[F[_]]( + implicit F: ProjectActions[F] + ): F.type = F + + def defaultResolve[F[_]: Applicative: Logger]( + mr: MergeRequestState + ): F[Option[ProjectAction]] = mr.mergeability match { case CanMerge => ProjectAction .Merge(projectId = mr.projectId, mergeRequestIid = mr.mergeRequestIid) @@ -58,9 +70,13 @@ object ProjectActions { type Action = ProjectAction - def resolve(mr: MergeRequestState): F[Option[ProjectAction]] = defaultResolve[F](mr) + def resolve( + mr: MergeRequestState + ): F[Option[ProjectAction]] = defaultResolve[F](mr) - def execute(action: ProjectAction): F[Unit] = { + def execute( + action: ProjectAction + ): F[Unit] = { val logBefore = Logger[F].info("About to execute action", Map("action" -> action.toString)) val approve = action match { @@ -98,22 +114,45 @@ object ProjectActions { } trait MatcherFunction[-In] { - def matches(in: In): Matched[Unit] - def atPath(path: String): MatcherFunction[In] = mapFailures(_.map(_.atPath(path))) - def mapResult(f: Matched[Unit] => Matched[Unit]): MatcherFunction[In] = f.compose(matches).apply(_) - def mapFailures(f: NonEmptyList[Mismatch] => NonEmptyList[Mismatch]): MatcherFunction[In] = mapResult(_.leftMap(f)) + def matches( + in: In + ): Matched[Unit] + + def atPath( + path: String + ): MatcherFunction[In] = mapFailures(_.map(_.atPath(path))) + + def mapResult( + f: Matched[Unit] => Matched[Unit] + ): MatcherFunction[In] = f.compose(matches).apply(_) + + def mapFailures( + f: NonEmptyList[Mismatch] => NonEmptyList[Mismatch] + ): MatcherFunction[In] = mapResult(_.leftMap(f)) + } object MatcherFunction { implicit val contravariantMatcherFunction: Contravariant[MatcherFunction] = new Contravariant[MatcherFunction] { - def contramap[A, B](fa: MatcherFunction[A])(f: B => A): MatcherFunction[B] = b => fa.matches(f(b)) + + def contramap[A, B]( + fa: MatcherFunction[A] + )( + f: B => A + ): MatcherFunction[B] = b => fa.matches(f(b)) + } implicit val monoidK: MonoidK[MatcherFunction] = new MonoidK[MatcherFunction] { - override def combineK[A](x: MatcherFunction[A], y: MatcherFunction[A]): MatcherFunction[A] = + + override def combineK[A]( + x: MatcherFunction[A], + y: MatcherFunction[A] + ): MatcherFunction[A] = in => (x.matches(in).toValidated |+| y.matches(in).toValidated).toEither + override def empty[A]: MatcherFunction[A] = success } @@ -128,14 +167,34 @@ object ProjectActions { } sealed trait Mismatch extends Product with Serializable { - def atPath(path: String): Mismatch = Mismatch.AtPath(path, this) + + def atPath( + path: String + ): Mismatch = Mismatch.AtPath(path, this) + } object Mismatch { - final case class AtPath(path: String, mismatch: Mismatch) extends Mismatch - final case class ValueMismatch(expected: String, actual: String) extends Mismatch - final case class RegexMismatch(pattern: Regex, actual: String) extends Mismatch - final case class ManyFailed(incompleteMatches: List[NonEmptyList[Mismatch]]) extends Mismatch + + final case class AtPath( + path: String, + mismatch: Mismatch + ) extends Mismatch + + final case class ValueMismatch( + expected: String, + actual: String + ) extends Mismatch + + final case class RegexMismatch( + pattern: Regex, + actual: String + ) extends Mismatch + + final case class ManyFailed( + incompleteMatches: List[NonEmptyList[Mismatch]] + ) extends Mismatch + case object ValueEmpty extends Mismatch case object NegationFailed extends Mismatch @@ -144,7 +203,9 @@ object ProjectActions { type Matched[A] = EitherNel[Mismatch, A] - def statusMatches(expectedStatus: String): MatcherFunction[MergeRequestState] = + def statusMatches( + expectedStatus: String + ): MatcherFunction[MergeRequestState] = MatcherFunction .fromPredicate[MergeRequestInfo.Status]( { @@ -169,25 +230,34 @@ object ProjectActions { ) } - def exists[A](base: MatcherFunction[A]): MatcherFunction[Option[A]] = + def exists[A]( + base: MatcherFunction[A] + ): MatcherFunction[Option[A]] = _.fold[Matched[Unit]](Mismatch.ValueEmpty.leftNel)(base.matches) - def oneOf[A](matchers: List[MatcherFunction[A]]): MatcherFunction[A] = input => + def oneOf[A]( + matchers: List[MatcherFunction[A]] + ): MatcherFunction[A] = input => matchers .traverse(_.matches(input).swap) .swap .leftMap(Mismatch.ManyFailed.apply) .toEitherNel - def not[A](matcher: MatcherFunction[A]): MatcherFunction[A] = input => - matcher.matches(input).swap.leftMap(_ => Mismatch.NegationFailed).void.toEitherNel + def not[A]( + matcher: MatcherFunction[A] + ): MatcherFunction[A] = input => matcher.matches(input).swap.leftMap(_ => Mismatch.NegationFailed).void.toEitherNel - def autorMatches(matcher: TextMatcher): MatcherFunction[MergeRequestState] = + def autorMatches( + matcher: TextMatcher + ): MatcherFunction[MergeRequestState] = matchTextMatcher(matcher) .atPath(".author") .contramap(_.authorUsername) - def descriptionMatches(matcher: TextMatcher): MatcherFunction[MergeRequestState] = + def descriptionMatches( + matcher: TextMatcher + ): MatcherFunction[MergeRequestState] = exists(matchTextMatcher(matcher)) .atPath(".description") .contramap(_.description) @@ -212,6 +282,15 @@ object ProjectActions { } enum ProjectAction { - case Merge(projectId: Long, mergeRequestIid: Long) - case Rebase(projectId: Long, mergeRequestIid: Long) + + case Merge( + projectId: Long, + mergeRequestIid: Long + ) + + case Rebase( + projectId: Long, + mergeRequestIid: Long + ) + } diff --git a/src/main/scala/io/pg/appconfig.scala b/src/main/scala/io/pg/appconfig.scala index 2272878c..4cbcb092 100644 --- a/src/main/scala/io/pg/appconfig.scala +++ b/src/main/scala/io/pg/appconfig.scala @@ -41,10 +41,14 @@ object AppConfig { ).parMapN(MetaConfig.apply) implicit val decodeUri: ConfigDecoder[String, Uri] = - ConfigDecoder[String, String].mapEither { (key, value) => - Uri - .parse(value) - .leftMap(e => ConfigError(s"Invalid URI ($value at $key), error: $e")) + ConfigDecoder[String, String].mapEither { + ( + key, + value + ) => + Uri + .parse(value) + .leftMap(e => ConfigError(s"Invalid URI ($value at $key), error: $e")) } val gitConfig: ConfigValue[ciris.Effect, Git] = @@ -64,7 +68,9 @@ object AppConfig { } -final case class HttpConfig(port: Int) +final case class HttpConfig( + port: Int +) final case class MetaConfig( banner: String, @@ -72,7 +78,11 @@ final case class MetaConfig( scalaVersion: String ) -final case class Git(host: Git.Host, apiUrl: Uri, apiToken: Secret[String]) +final case class Git( + host: Git.Host, + apiUrl: Uri, + apiToken: Secret[String] +) object Git { sealed trait Host extends Product with Serializable @@ -83,6 +93,10 @@ object Git { } -final case class Queues(maxSize: Int) +final case class Queues( + maxSize: Int +) -final case class MiddlewareConfig(sensitiveHeaders: Set[CIString]) +final case class MiddlewareConfig( + sensitiveHeaders: Set[CIString] +) diff --git a/src/main/scala/io/pg/config/DiscriminatedCodec.scala b/src/main/scala/io/pg/config/DiscriminatedCodec.scala index cb7a7e3f..f25170fd 100644 --- a/src/main/scala/io/pg/config/DiscriminatedCodec.scala +++ b/src/main/scala/io/pg/config/DiscriminatedCodec.scala @@ -23,7 +23,11 @@ object DiscriminatedCodec { ) :: deriveAll[t] } - inline def derived[A](discriminator: String)(using inline m: Mirror.SumOf[A]): Codec.AsObject[A] = { + inline def derived[A]( + discriminator: String + )( + using inline m: Mirror.SumOf[A] + ): Codec.AsObject[A] = { val codecs: List[Codec.AsObject[A]] = deriveAll[m.MirroredElemTypes].map(_.asInstanceOf[Codec.AsObject[A]]) diff --git a/src/main/scala/io/pg/config/ProjectConfig.scala b/src/main/scala/io/pg/config/ProjectConfig.scala index 8a6c87b5..08ad3d7b 100644 --- a/src/main/scala/io/pg/config/ProjectConfig.scala +++ b/src/main/scala/io/pg/config/ProjectConfig.scala @@ -11,19 +11,30 @@ import java.nio.file.Paths import scala.util.chaining._ trait ProjectConfigReader[F[_]] { - def readConfig(project: Project): F[ProjectConfig] + + def readConfig( + project: Project + ): F[ProjectConfig] + } object ProjectConfigReader { - def apply[F[_]](using F: ProjectConfigReader[F]): ProjectConfigReader[F] = F + + def apply[F[_]]( + using F: ProjectConfigReader[F] + ): ProjectConfigReader[F] = F def test[F[_]: Applicative]: ProjectConfigReader[F] = new ProjectConfigReader[F] { - def semver(level: String) = Matcher.Description(TextMatcher.Matches(s"(?s).*labels:.*semver-$level.*".r)) + def semver( + level: String + ) = Matcher.Description(TextMatcher.Matches(s"(?s).*labels:.*semver-$level.*".r)) // todo: dhall needs to be updated - def steward(extra: Matcher) = Rule( + def steward( + extra: Matcher + ) = Rule( "scala-steward", Matcher.Many( List( @@ -50,7 +61,10 @@ object ProjectConfigReader { ) ) - def readConfig(project: Project): F[ProjectConfig] = config.pure[F] + def readConfig( + project: Project + ): F[ProjectConfig] = config.pure[F] + } def dhallJsonStringConfig[F[_]: ProxFS2: MonadThrow]: F[ProjectConfigReader[F]] = { @@ -70,7 +84,9 @@ object ProjectConfigReader { val instance: ProjectConfigReader[F] = new ProjectConfigReader[F] { - def readConfig(project: Project): F[ProjectConfig] = + def readConfig( + project: Project + ): F[ProjectConfig] = Process(dhallCommand) .`with`("TOKEN" -> "demo-token") .fromFile(Paths.get(filePath)) diff --git a/src/main/scala/io/pg/config/format.scala b/src/main/scala/io/pg/config/format.scala index fb859f91..edf6f48d 100644 --- a/src/main/scala/io/pg/config/format.scala +++ b/src/main/scala/io/pg/config/format.scala @@ -25,10 +25,18 @@ object circe { import circe.regexCodec enum TextMatcher { - case Equals(value: String) - case Matches(regex: Regex) - override def equals(another: Any) = (this, another) match { + case Equals( + value: String + ) + + case Matches( + regex: Regex + ) + + override def equals( + another: Any + ) = (this, another) match { // Regex uses reference equality by default. // By using `.regex` we convert it back to a pattern string for better comparison. case (Matches(p1), Matches(p2)) => p1.regex == p2.regex @@ -43,14 +51,35 @@ object TextMatcher { } enum Matcher { - def and(another: Matcher): Matcher = Matcher.Many(List(this, another)) - - case Author(email: TextMatcher) - case Description(text: TextMatcher) - case PipelineStatus(status: String) - case Many(values: List[Matcher]) - case OneOf(values: List[Matcher]) - case Not(underlying: Matcher) + + def and( + another: Matcher + ): Matcher = Matcher.Many(List(this, another)) + + case Author( + email: TextMatcher + ) + + case Description( + text: TextMatcher + ) + + case PipelineStatus( + status: String + ) + + case Many( + values: List[Matcher] + ) + + case OneOf( + values: List[Matcher] + ) + + case Not( + underlying: Matcher + ) + } object Matcher { @@ -73,13 +102,19 @@ object Action { } -final case class Rule(name: String, matcher: Matcher, action: Action) derives Codec.AsObject +final case class Rule( + name: String, + matcher: Matcher, + action: Action +) derives Codec.AsObject object Rule { val mergeAnything = Rule("anything", Matcher.Many(Nil), Action.Merge) } -final case class ProjectConfig(rules: List[Rule]) derives Codec.AsObject +final case class ProjectConfig( + rules: List[Rule] +) derives Codec.AsObject object ProjectConfig { val empty = ProjectConfig(Nil) diff --git a/src/main/scala/io/pg/resolver.scala b/src/main/scala/io/pg/resolver.scala index c39ae189..313778a9 100644 --- a/src/main/scala/io/pg/resolver.scala +++ b/src/main/scala/io/pg/resolver.scala @@ -12,18 +12,27 @@ import cats.MonadThrow import monocle.syntax.all._ trait StateResolver[F[_]] { - def resolve(project: Project): F[List[MergeRequestState]] + + def resolve( + project: Project + ): F[List[MergeRequestState]] + } object StateResolver { - def apply[F[_]](using F: StateResolver[F]): StateResolver[F] = F + + def apply[F[_]]( + using F: StateResolver[F] + ): StateResolver[F] = F def instance[F[_]: Gitlab: Logger: MonadThrow]( implicit SC: fs2.Compiler[F, F] ): StateResolver[F] = new StateResolver[F] { - private def findMergeRequests(project: Project): F[List[MergeRequestState]] = + private def findMergeRequests( + project: Project + ): F[List[MergeRequestState]] = Gitlab[F] .mergeRequests(projectId = project.id) .nested @@ -47,7 +56,9 @@ object StateResolver { ) ) - def resolve(project: Project): F[List[MergeRequestState]] = + def resolve( + project: Project + ): F[List[MergeRequestState]] = findMergeRequests(project) .flatTap { state => Logger[F].info("Resolved MR state", Map("state" -> state.show)) @@ -76,7 +87,10 @@ object MergeRequestState { case object NeedsRebase extends Mergeability case object HasConflicts extends Mergeability - def fromFlags(hasConflicts: Boolean, needsRebase: Boolean): Mergeability = + def fromFlags( + hasConflicts: Boolean, + needsRebase: Boolean + ): Mergeability = if (hasConflicts) HasConflicts else if (needsRebase) NeedsRebase else CanMerge diff --git a/src/main/scala/io/pg/transport/transport.scala b/src/main/scala/io/pg/transport/transport.scala index e3cc068b..4beed5ae 100644 --- a/src/main/scala/io/pg/transport/transport.scala +++ b/src/main/scala/io/pg/transport/transport.scala @@ -15,7 +15,11 @@ object MergeRequestState { enum Status derives Codec.AsObject { case Success - case Other(value: String) + + case Other( + value: String + ) + } enum Mergeability derives Codec.AsObject { diff --git a/src/main/scala/io/pg/webhook/webhook.scala b/src/main/scala/io/pg/webhook/webhook.scala index 70171f0a..9539fca1 100644 --- a/src/main/scala/io/pg/webhook/webhook.scala +++ b/src/main/scala/io/pg/webhook/webhook.scala @@ -44,7 +44,9 @@ object WebhookRouter { } - private def mergeRequestToTransport(mr: MergeRequestState): io.pg.transport.MergeRequestState = transport.MergeRequestState( + private def mergeRequestToTransport( + mr: MergeRequestState + ): io.pg.transport.MergeRequestState = transport.MergeRequestState( projectId = mr.projectId, mergeRequestIid = mr.mergeRequestIid, description = mr.description, @@ -64,11 +66,7 @@ object WebhookRouter { object WebhookProcessor { - def instance[ - F[ - _ - ]: MergeRequests: ProjectActions: Logger: MonadThrow - ]: WebhookEvent => F[Unit] = { ev => + def instance[F[_]: MergeRequests: ProjectActions: Logger: MonadThrow]: WebhookEvent => F[Unit] = { ev => for { _ <- Logger[F].info("Received event", Map("event" -> ev.toString())) states <- MergeRequests[F].build(ev.project) diff --git a/src/test/scala/io/pg/WebhookProcessorTest.scala b/src/test/scala/io/pg/WebhookProcessorTest.scala index ce265e04..5bc54b2c 100644 --- a/src/test/scala/io/pg/WebhookProcessorTest.scala +++ b/src/test/scala/io/pg/WebhookProcessorTest.scala @@ -51,8 +51,13 @@ object WebhookProcessorTest extends SimpleIOSuite { } .toResource - def testWithResources(name: String)(use: Resources[IO] => IO[Expectations]) = + def testWithResources( + name: String + )( + use: Resources[IO] => IO[Expectations] + ) = test(name)(mkResources.use(use)) + /* testWithResources("unknown project") { resources => import resources._ diff --git a/src/test/scala/io/pg/fakes/FakeUtils.scala b/src/test/scala/io/pg/fakes/FakeUtils.scala index dfa2b852..9abca032 100644 --- a/src/test/scala/io/pg/fakes/FakeUtils.scala +++ b/src/test/scala/io/pg/fakes/FakeUtils.scala @@ -6,12 +6,21 @@ import cats.effect.Ref object FakeUtils { - def statefulRef[F[_]: Monad, A](ref: Ref[F, A]): Stateful[F, A] = + def statefulRef[F[_]: Monad, A]( + ref: Ref[F, A] + ): Stateful[F, A] = new Stateful[F, A] { def monad: Monad[F] = implicitly def get: F[A] = ref.get - def set(s: A): F[Unit] = ref.set(s) - override def modify(f: A => A): F[Unit] = ref.update(f) + + def set( + s: A + ): F[Unit] = ref.set(s) + + override def modify( + f: A => A + ): F[Unit] = ref.update(f) + } } diff --git a/src/test/scala/io/pg/fakes/ProjectActionsStateFake.scala b/src/test/scala/io/pg/fakes/ProjectActionsStateFake.scala index ea342f18..e61d9e86 100644 --- a/src/test/scala/io/pg/fakes/ProjectActionsStateFake.scala +++ b/src/test/scala/io/pg/fakes/ProjectActionsStateFake.scala @@ -18,7 +18,11 @@ import io.pg.gitlab.webhook.Project import monocle.syntax.all._ object ProjectActionsStateFake { - sealed case class MergeRequestDescription(projectId: Long, mergeRequestIid: Long) + + sealed case class MergeRequestDescription( + projectId: Long, + mergeRequestIid: Long + ) object MergeRequestDescription { val fromMergeAction: ProjectAction.Merge => MergeRequestDescription = merge => @@ -36,36 +40,66 @@ object ProjectActionsStateFake { /** A collection of modifiers on the state, which will be provided together with the instance using it. */ trait Modifiers[F[_]] { + // returns Iid of created MR - def open(projectId: Long, authorUsername: String, description: Option[String]): F[Long] - def finishPipeline(projectId: Long, mergeRequestIid: Long): F[Unit] - def setMergeability(projectId: Long, mergeRequestIid: Long, mergeability: Mergeability): F[Unit] + def open( + projectId: Long, + authorUsername: String, + description: Option[String] + ): F[Long] + + def finishPipeline( + projectId: Long, + mergeRequestIid: Long + ): F[Unit] + + def setMergeability( + projectId: Long, + mergeRequestIid: Long, + mergeability: Mergeability + ): F[Unit] + def getActionLog: F[List[ProjectAction]] } private[ProjectActionsStateFake] object modifications { - def logAction(action: ProjectAction): State => State = + def logAction( + action: ProjectAction + ): State => State = _.focus(_.actionLog).modify(_.append(action)) - def merge(action: ProjectAction.Merge): State => State = + def merge( + action: ProjectAction.Merge + ): State => State = _.focus(_.mergeRequests).modify(_ - MergeRequestDescription.fromMergeAction(action)) - def rebase(action: ProjectAction.Rebase): State => State = + def rebase( + action: ProjectAction.Rebase + ): State => State = // Note: this doesn't check for conflicts setMergeabilityInternal(action.projectId, action.mergeRequestIid, Mergeability.CanMerge) - def setMergeabilityInternal(projectId: Long, mergeRequestIid: Long, mergeability: Mergeability): State => State = + def setMergeabilityInternal( + projectId: Long, + mergeRequestIid: Long, + mergeability: Mergeability + ): State => State = _.focus(_.mergeRequests).modify { mrs => val key = MergeRequestDescription(projectId, mergeRequestIid) mrs ++ mrs.get(key).map(_.copy(mergeability = mergeability)).tupleLeft(key) } - def save(key: MergeRequestDescription, state: MergeRequestState) = (_: State).focus(_.mergeRequests).modify { + def save( + key: MergeRequestDescription, + state: MergeRequestState + ) = (_: State).focus(_.mergeRequests).modify { _ + (key -> state) } - def finishPipeline(key: MergeRequestDescription) = (_: State).focus(_.mergeRequests).modify { + def finishPipeline( + key: MergeRequestDescription + ) = (_: State).focus(_.mergeRequests).modify { _.updatedWith(key) { _.map { state => state.copy(status = MergeRequestInfo.Status.Success) @@ -86,14 +120,19 @@ object ProjectActionsStateFake { /** This instance has both the capabilities of ProjectActions and StateResolver, because they operate on the same state, and the state is * sealed by convention. */ - def instance[ - F[_]: Data: Monad: Logger - ]: ProjectActions[F] with StateResolver[F] with State.Modifiers[F] = new ProjectActions[F] with StateResolver[F] with State.Modifiers[F] { + def instance[F[_]: Data: Monad: Logger]: ProjectActions[F] with StateResolver[F] with State.Modifiers[F] = new ProjectActions[F] + with StateResolver[F] + with State.Modifiers[F] { type Action = ProjectAction - def resolve(mr: MergeRequestState): F[Option[ProjectAction]] = ProjectActions.defaultResolve[F](mr) - def execute(action: ProjectAction): F[Unit] = Data[F].modify { + def resolve( + mr: MergeRequestState + ): F[Option[ProjectAction]] = ProjectActions.defaultResolve[F](mr) + + def execute( + action: ProjectAction + ): F[Unit] = Data[F].modify { val actionChange = action match { case m: Merge => State.modifications.merge(m) case r: Rebase => State.modifications.rebase(r) @@ -102,10 +141,16 @@ object ProjectActionsStateFake { actionChange >>> State.modifications.logAction(action) } - def resolve(project: Project): F[List[MergeRequestState]] = + def resolve( + project: Project + ): F[List[MergeRequestState]] = Data[F].get.map(_.mergeRequests).map(_.values.toList) - def open(projectId: Long, authorUsername: String, description: Option[String]): F[Long] = { + def open( + projectId: Long, + authorUsername: String, + description: Option[String] + ): F[Long] = { val getNextId = Data[F] .get @@ -134,11 +179,18 @@ object ProjectActionsStateFake { } } - def setMergeability(projectId: Long, mergeRequestIid: Long, mergeability: Mergeability): F[Unit] = Data[F].modify { + def setMergeability( + projectId: Long, + mergeRequestIid: Long, + mergeability: Mergeability + ): F[Unit] = Data[F].modify { State.modifications.setMergeabilityInternal(projectId, mergeRequestIid, mergeability) } - def finishPipeline(projectId: Long, mergeRequestIid: Long): F[Unit] = + def finishPipeline( + projectId: Long, + mergeRequestIid: Long + ): F[Unit] = Data[F].modify { State.modifications.finishPipeline(MergeRequestDescription(projectId, mergeRequestIid)) } diff --git a/src/test/scala/io/pg/fakes/ProjectConfigReaderFake.scala b/src/test/scala/io/pg/fakes/ProjectConfigReaderFake.scala index 863a22c2..e8036b23 100644 --- a/src/test/scala/io/pg/fakes/ProjectConfigReaderFake.scala +++ b/src/test/scala/io/pg/fakes/ProjectConfigReaderFake.scala @@ -23,7 +23,12 @@ object ProjectConfigReaderFake { /** A collection of modifiers on the state, which will be provided together with the instance using it. */ trait Modifiers[F[_]] { - def register(projectId: Long, config: ProjectConfig): F[Unit] + + def register( + projectId: Long, + config: ProjectConfig + ): F[Unit] + } } @@ -37,12 +42,17 @@ object ProjectConfigReaderFake { def instance[F[_]: Data: MonadThrow]: ProjectConfigReader[F] with State.Modifiers[F] = new ProjectConfigReader[F] with State.Modifiers[F] { - def readConfig(project: Project): F[ProjectConfig] = + def readConfig( + project: Project + ): F[ProjectConfig] = Data[F] .get .flatMap(_.configs.get(project.id).liftTo[F](new Throwable(s"Unknown project: $project"))) - def register(projectId: Long, config: ProjectConfig): F[Unit] = + def register( + projectId: Long, + config: ProjectConfig + ): F[Unit] = Data[F].modify(_.focus(_.configs).modify(_ + (projectId -> config))) }