From 7894adfa056272bac0b03637ede269b898fe817e Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 11 Dec 2024 08:39:53 +0000 Subject: [PATCH 1/2] Contributor's patch (submitted anonymously) --- .gitignore | 3 ++ README.md | 11 +++--- build.sbt | 29 ++++++++------- modules/app/src/main/scala/app.scala | 2 +- .../src/main/scala/PlatformShLoader.scala | 4 +-- .../src/main/scala/SkunkDatabase.scala | 3 +- modules/backend/src/main/scala/app.scala | 5 +-- .../backend/src/main/scala/auth.crypto.scala | 5 ++- modules/backend/src/main/scala/auth.jwt.scala | 4 --- modules/backend/src/main/scala/config.scala | 5 --- .../src/main/scala/database.codecs.scala | 36 +++++++++---------- .../src/main/scala/database.operations.scala | 18 ++++------ .../backend/src/main/scala/http.auth.scala | 11 +++--- .../backend/src/main/scala/http.routes.scala | 5 ++- .../src/main/scala/service.companies.scala | 9 ++--- .../src/main/scala/service.health.scala | 2 +- .../backend/src/main/scala/service.jobs.scala | 8 ++--- .../src/main/scala/service.users.scala | 19 +++------- .../test/scala/frontend/FrontendSuite.scala | 2 +- .../test/scala/frontend/PageFragments.scala | 3 +- .../src/test/scala/frontend/Users.scala | 2 +- .../src/test/scala/integration/Fixture.scala | 5 +-- .../scala/integration/IntegrationSuite.scala | 3 +- .../backend/src/test/scala/stub/Fixture.scala | 3 +- .../src/test/scala/stub/StubTests.scala | 2 +- 25 files changed, 86 insertions(+), 113 deletions(-) diff --git a/.gitignore b/.gitignore index f23fc69..ecf0587 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .bloop .bsp .sbt +.vscode *.gch .cache target @@ -16,3 +17,5 @@ frontend-build/node_modules node_modules .idea postgres-data + +.history diff --git a/README.md b/README.md index 9ae8eb5..abb6636 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This is a full-stack Scala application (job posting board), made with the follow The app used to be deployed on 1. Platform.sh but I've run out of the free trial plan -2. Heroku but they discontinued the free flans for everyone +2. Heroku but they discontinued the free plans for everyone and now the app is deployed to the lovely Fly.io and here's the [**live version**](https://jobby-web.fly.dev/) @@ -30,10 +30,11 @@ It's a companion repo for my [4-part blog post series](https://blog.indoorvivant Easiest way to run it is: ``` - docker run -p 5432:5432 -e POSTGRES_PASSWORD=mysecretpassword -e POSTGRES_DB=jobby -d postgres + docker run --name jobby -p 5439:5432 -e POSTGRES_USER=jobby -e POSTGRES_PASSWORD=mysecretpassword -e POSTGRES_DB=jobby -d postgres + ``` - Take note of the password (`mysecretpassword`) and database (`jobby`), you will need to configure the app . + Take note of the user, password, database (`jobby`) and host port (`5439`), if you wish to reconfigure the connection . ### Running @@ -52,9 +53,11 @@ The following settings are respected, and can be either be set as environment va deployments), or read from the `jobby.opts` file at the root of the project, for example: ```properties +PG_USER=jobby PG_PASSWORD=mysecretpassword PG_DB=jobby LOCAL_DEPLOYMENT=true +PG_PORT=5439 ``` * `PG_PASSWORD`, `PG_HOST`, `PG_DB`, `PG_USER`, `PG_PORT` - variables to configure access to Postgres @@ -98,4 +101,4 @@ b) Run Vite's development server: ``` And (3) is achieved by the configuration in `frontend-build/vite.config.js`. **Make sure to start -the server on port 9999** +the server on port 9999** \ No newline at end of file diff --git a/build.sbt b/build.sbt index 3026227..4a96e40 100644 --- a/build.sbt +++ b/build.sbt @@ -1,5 +1,4 @@ -import org.scalajs.linker.interface.Report -import org.scalajs.linker.interface.ModuleSplitStyle +import org.scalajs.linker.interface.{ModuleSplitStyle, Report} import smithy4s.codegen.Smithy4sCodegenPlugin Global / onChangedBuildSource := ReloadOnSourceChanges @@ -7,13 +6,13 @@ Global / onChangedBuildSource := ReloadOnSourceChanges val Versions = new { val http4sBlaze = "0.23.14" val http4s = "0.23.18" - val Scala = "3.3.4" - val skunk = "0.5.1" - val upickle = "2.0.0" - val scribe = "3.11.1" + val Scala = "3.6.2" + val skunk = "1.0.0-M8" + val upickle = "3.1.4" + val scribe = "3.15.2" val http4sDom = "0.2.7" - val jwt = "9.2.0" - val Flyway = "9.22.3" + val jwt = "10.0.1" + val FlywayPG = "11.0.1" val Postgres = "42.7.4" val TestContainers = "0.41.4" val Weaver = "0.8.4" @@ -56,7 +55,7 @@ lazy val app = projectMatrix "org.http4s" %% "http4s-blaze-server" % Versions.http4sBlaze, "org.http4s" %% "http4s-ember-server" % Versions.http4s, "org.postgresql" % "postgresql" % Versions.Postgres, - "org.flywaydb" % "flyway-core" % Versions.Flyway + "org.flywaydb" % "flyway-database-postgresql" % Versions.FlywayPG ), Compile / resourceGenerators += { Def.task[Seq[File]] { @@ -90,7 +89,7 @@ lazy val backend = projectMatrix scalaVersion := Versions.Scala, Compile / doc / sources := Seq.empty, libraryDependencies ++= Seq( - ("com.disneystreaming.smithy4s" %% "smithy4s-http4s" % smithy4sVersion.value), + "com.disneystreaming.smithy4s" %% "smithy4s-http4s" % smithy4sVersion.value, "com.disneystreaming.smithy4s" %% "smithy4s-http4s-swagger" % smithy4sVersion.value, "com.github.jwt-scala" %% "jwt-upickle" % Versions.jwt, "com.lihaoyi" %% "upickle" % Versions.upickle, @@ -110,7 +109,7 @@ lazy val backend = projectMatrix "org.http4s" %% "http4s-ember-server" % Versions.http4s, "org.http4s" %% "http4s-ember-client" % Versions.http4s, "org.postgresql" % "postgresql" % Versions.Postgres, - "org.flywaydb" % "flyway-core" % Versions.Flyway + "org.flywaydb" % "flyway-database-postgresql" % Versions.FlywayPG ).map(_ % Test), testFrameworks += new TestFramework("weaver.framework.CatsEffect"), Test / fork := true, @@ -186,7 +185,7 @@ lazy val defaults = Seq(VirtualAxis.scalaABIVersion(Versions.Scala), VirtualAxis.jvm) lazy val frontendModules = taskKey[(Report, File)]("") -ThisBuild / frontendModules := (Def.taskIf { +ThisBuild / frontendModules := Def.taskIf { def proj = frontend.finder(BuildStyle.Modules)( Versions.Scala ) @@ -197,10 +196,10 @@ ThisBuild / frontendModules := (Def.taskIf { else (proj / Compile / fastLinkJS).value.data -> (proj / Compile / fastLinkJS / scalaJSLinkerOutputDirectory).value -}).value +}.value lazy val frontendBundle = taskKey[File]("") -ThisBuild / frontendBundle := (Def.taskIf { +ThisBuild / frontendBundle := Def.taskIf { def proj = frontend.finder(BuildStyle.SingleFile)( Versions.Scala ) @@ -212,7 +211,7 @@ ThisBuild / frontendBundle := (Def.taskIf { val res = (proj / Compile / fastLinkJS).value (proj / Compile / fastLinkJS / scalaJSLinkerOutputDirectory).value } -}).value +}.value lazy val isRelease = sys.env.get("RELEASE").contains("yesh") diff --git a/modules/app/src/main/scala/app.scala b/modules/app/src/main/scala/app.scala index 413a311..4d0c26c 100644 --- a/modules/app/src/main/scala/app.scala +++ b/modules/app/src/main/scala/app.scala @@ -47,7 +47,7 @@ object Main extends IOApp: }.toMap def run(args: List[String]) = - import natchez.Trace.Implicits.noop + import org.typelevel.otel4s.trace.Tracer.Implicits.noop val logger = scribe.cats.io diff --git a/modules/backend/src/main/scala/PlatformShLoader.scala b/modules/backend/src/main/scala/PlatformShLoader.scala index f793e85..a530e56 100644 --- a/modules/backend/src/main/scala/PlatformShLoader.scala +++ b/modules/backend/src/main/scala/PlatformShLoader.scala @@ -1,7 +1,7 @@ package jobby -import scala.util.Try import java.util.Base64 +import scala.util.Try import scala.util.control.NonFatal class PlatformShLoader(env: Map[String, String]): @@ -15,7 +15,7 @@ class PlatformShLoader(env: Map[String, String]): scribe.error("Failed to parse PLATFORM_RELATIONSHIPS", error) Option.empty }, - Option.apply(_) + Option.apply ) } diff --git a/modules/backend/src/main/scala/SkunkDatabase.scala b/modules/backend/src/main/scala/SkunkDatabase.scala index 33b5dc5..16458a1 100644 --- a/modules/backend/src/main/scala/SkunkDatabase.scala +++ b/modules/backend/src/main/scala/SkunkDatabase.scala @@ -10,10 +10,11 @@ import jobby.spec.* import smithy4s.Newtype import database.codecs.* import database.operations.* +import org.typelevel.otel4s.trace.Tracer object SkunkDatabase: def load(postgres: PgCredentials, skunkConfig: SkunkConfig)(using - natchez.Trace[IO] + Tracer[IO] ): Resource[IO, Database] = Session .pooled[IO]( diff --git a/modules/backend/src/main/scala/app.scala b/modules/backend/src/main/scala/app.scala index b6438d0..5efeef4 100644 --- a/modules/backend/src/main/scala/app.scala +++ b/modules/backend/src/main/scala/app.scala @@ -2,19 +2,20 @@ package jobby import cats.effect.* import scribe.Scribe +import org.typelevel.otel4s.trace.Tracer class JobbyApp( val config: AppConfig, db: Database, logger: Scribe[IO], timeCop: TimeCop -)(using natchez.Trace[IO]): +)(using Tracer[IO]): def routes = Routes(db, config, logger, timeCop) end JobbyApp object JobbyApp: def bootstrap(config: AppConfig, logger: Scribe[IO])(using - natchez.Trace[IO] + Tracer[IO] ) = for db <- SkunkDatabase.load(config.postgres, config.skunk) yield JobbyApp(config, db, logger, TimeCop.unsafe) diff --git a/modules/backend/src/main/scala/auth.crypto.scala b/modules/backend/src/main/scala/auth.crypto.scala index e6c1344..490e9d1 100644 --- a/modules/backend/src/main/scala/auth.crypto.scala +++ b/modules/backend/src/main/scala/auth.crypto.scala @@ -3,13 +3,12 @@ package users import jobby.spec.* import cats.effect.* -import java.security.SecureRandom -import cats.effect.std.Random +import cats.effect.std.SecureRandom import java.security.MessageDigest object Crypto: def hashPassword(raw: UserPassword): IO[HashedPassword] = - Random.javaSecuritySecureRandom[IO].flatMap { r => + SecureRandom.javaSecuritySecureRandom[IO].flatMap { r => for seed <- r.nextString(16) seeded = seed + ":" + raw.value diff --git a/modules/backend/src/main/scala/auth.jwt.scala b/modules/backend/src/main/scala/auth.jwt.scala index d09dd0c..c2be580 100644 --- a/modules/backend/src/main/scala/auth.jwt.scala +++ b/modules/backend/src/main/scala/auth.jwt.scala @@ -3,12 +3,8 @@ package jobby import jobby.spec.* import java.util.UUID import scala.util.Try -import cats.effect.IO import scala.util.Failure import scala.util.Success -import pdi.jwt.exceptions.JwtException -import scala.concurrent.duration.FiniteDuration -import scribe.Scribe object JWT: enum Kind: diff --git a/modules/backend/src/main/scala/config.scala b/modules/backend/src/main/scala/config.scala index c14eda6..77c5f5f 100644 --- a/modules/backend/src/main/scala/config.scala +++ b/modules/backend/src/main/scala/config.scala @@ -1,13 +1,8 @@ package jobby -import scala.util.Try -import java.util.Base64 -import scala.util.control.NonFatal import cats.effect.IO import com.comcast.ip4s.Host import com.comcast.ip4s.Port -import com.comcast.ip4s.Literals.host -import scala.concurrent.duration.FiniteDuration import pdi.jwt.JwtAlgorithm import pdi.jwt.algorithms.JwtHmacAlgorithm import scala.concurrent.duration.* diff --git a/modules/backend/src/main/scala/database.codecs.scala b/modules/backend/src/main/scala/database.codecs.scala index fbe36a1..7bd7f79 100644 --- a/modules/backend/src/main/scala/database.codecs.scala +++ b/modules/backend/src/main/scala/database.codecs.scala @@ -1,13 +1,11 @@ package jobby package database +import jobby.spec.* import skunk.Codec import skunk.codec.all.* -import smithy4s.Newtype - -import jobby.spec.* -import smithy4s.Timestamp import skunk.data.Type +import smithy4s.{Newtype, Timestamp} object codecs: extension [T](c: Codec[T]) @@ -31,31 +29,31 @@ object codecs: val currency = `enum`[Currency](_.value, Currency.fromString, Type("currency_enum")) - val salaryRange = (minSalary ~ maxSalary ~ currency).gimap[SalaryRange] + val salaryRange = (minSalary *: maxSalary *: currency).to[SalaryRange] val added = timestamptz .imap(Timestamp.fromOffsetDateTime)(_.toOffsetDateTime) .as(JobAdded) val jobAttributes = - (jobTitle ~ - jobDescription ~ - jobUrl ~ - salaryRange).gimap[JobAttributes] + (jobTitle *: + jobDescription *: + jobUrl *: + salaryRange).to[JobAttributes] val job = - (jobId ~ - companyId ~ - jobAttributes ~ - added).gimap[Job] + (jobId *: + companyId *: + jobAttributes *: + added).to[Job] val companyAttributes = - (companyName ~ - companyDescription ~ - companyUrl).gimap[CompanyAttributes] + (companyName *: + companyDescription *: + companyUrl).to[CompanyAttributes] val company = - (companyId ~ - userId ~ - companyAttributes).gimap[Company] + (companyId *: + userId *: + companyAttributes).to[Company] end codecs diff --git a/modules/backend/src/main/scala/database.operations.scala b/modules/backend/src/main/scala/database.operations.scala index 45a1ceb..ef53fcc 100644 --- a/modules/backend/src/main/scala/database.operations.scala +++ b/modules/backend/src/main/scala/database.operations.scala @@ -2,19 +2,13 @@ package jobby package database package operations -import skunk.Session -import skunk.PreparedCommand import cats.effect.IO -import skunk.PreparedQuery - +import cats.effect.kernel.Resource +import jobby.database.codecs.* +import jobby.spec.* import skunk.* -import skunk.implicits.* - -import codecs.* import skunk.codec.all.* - -import jobby.spec.* -import cats.effect.kernel.Resource +import skunk.implicits.* sealed abstract class SqlQuery[I, O](val input: I, query: skunk.Query[I, O]): def use[A](session: Session[IO])(f: PreparedQuery[IO, I, O] => IO[A]) = @@ -32,7 +26,7 @@ case class GetCredentials(login: UserLogin) sql""" select user_id, salted_hash from users where lower(login) = lower($userLogin) - """.query(userId ~ hashedPassword) + """.query(userId *: hashedPassword) ) case class CreateUser(login: UserLogin, password: HashedPassword) @@ -116,7 +110,7 @@ case class CreateJob( attributes: JobAttributes, jobAdded: JobAdded ) extends SqlQuery( - ((company, attributes), jobAdded), + (company, attributes, jobAdded), sql""" insert into jobs(job_id, company_id, job_title, job_description, job_url, min_salary, max_salary, currency, added) values (gen_random_uuid(), $companyId, $jobAttributes, $added) diff --git a/modules/backend/src/main/scala/http.auth.scala b/modules/backend/src/main/scala/http.auth.scala index 2bad31c..03c2269 100644 --- a/modules/backend/src/main/scala/http.auth.scala +++ b/modules/backend/src/main/scala/http.auth.scala @@ -1,15 +1,12 @@ package jobby -import jobby.spec.* -import java.util.UUID -import scala.util.Try import cats.effect.IO -import scala.util.Failure -import scala.util.Success -import pdi.jwt.exceptions.JwtException -import scala.concurrent.duration.FiniteDuration +import jobby.spec.* import scribe.Scribe +import scala.concurrent.duration.FiniteDuration +import scala.util.{Failure, Success} + class HttpAuth(config: JwtConfig, logger: Scribe[IO]): def accessToken(userId: UserId): (String, FiniteDuration) = diff --git a/modules/backend/src/main/scala/http.routes.scala b/modules/backend/src/main/scala/http.routes.scala index a496bf8..546f7fe 100644 --- a/modules/backend/src/main/scala/http.routes.scala +++ b/modules/backend/src/main/scala/http.routes.scala @@ -3,7 +3,9 @@ package jobby import cats.data.Kleisli import cats.effect.* import cats.implicits.* +import jobby.health.* import jobby.spec.* +import jobby.users.* import org.http4s.* import org.http4s.dsl.io.* import scribe.Scribe @@ -11,9 +13,6 @@ import smithy4s.http4s.SimpleRestJsonBuilder import java.nio.file.Paths -import users.* -import health.* - def Routes( db: Database, config: AppConfig, diff --git a/modules/backend/src/main/scala/service.companies.scala b/modules/backend/src/main/scala/service.companies.scala index 74e2de6..ab3328b 100644 --- a/modules/backend/src/main/scala/service.companies.scala +++ b/modules/backend/src/main/scala/service.companies.scala @@ -1,13 +1,10 @@ package jobby -import jobby.spec.* import cats.effect.* -import java.util.UUID -import jobby.database.{operations as op} -import jobby.spec.CompaniesServiceGen.CreateCompany import cats.syntax.all.* - -import validation.* +import jobby.database.{operations as op} +import jobby.spec.* +import jobby.validation.* class CompaniesServiceImpl(db: Database, httpAuth: HttpAuth) extends CompaniesService[IO]: diff --git a/modules/backend/src/main/scala/service.health.scala b/modules/backend/src/main/scala/service.health.scala index 750a21f..b45f99a 100644 --- a/modules/backend/src/main/scala/service.health.scala +++ b/modules/backend/src/main/scala/service.health.scala @@ -1,8 +1,8 @@ package jobby package health -import spec.* import cats.effect.IO +import jobby.spec.* object HealthServiceImpl extends HealthService[IO]: override def healthCheck() = diff --git a/modules/backend/src/main/scala/service.jobs.scala b/modules/backend/src/main/scala/service.jobs.scala index df29a62..27aba06 100644 --- a/modules/backend/src/main/scala/service.jobs.scala +++ b/modules/backend/src/main/scala/service.jobs.scala @@ -1,14 +1,10 @@ package jobby -import jobby.spec.* import cats.effect.* import cats.implicits.* -import java.util.UUID - +import jobby.spec.* import database.operations as op -import smithy4s.Timestamp - -import validation.* +import jobby.validation.* class JobServiceImpl(db: Database, auth: HttpAuth, timeCop: TimeCop) extends JobService[IO]: diff --git a/modules/backend/src/main/scala/service.users.scala b/modules/backend/src/main/scala/service.users.scala index 92ead6d..23148d8 100644 --- a/modules/backend/src/main/scala/service.users.scala +++ b/modules/backend/src/main/scala/service.users.scala @@ -1,25 +1,16 @@ package jobby package users -import java.security.MessageDigest -import java.security.SecureRandom -import java.time.Instant -import java.util.UUID - import cats.effect.* -import cats.effect.std.Random import cats.implicits.* - -import jobby.database.operations as op import jobby.spec.* - -import org.http4s.HttpDate -import org.http4s.RequestCookie -import org.http4s.ResponseCookie -import org.http4s.SameSite +import jobby.validation.* +import org.http4s.{HttpDate, RequestCookie, ResponseCookie, SameSite} import scribe.Scribe -import validation.* +import jobby.database.operations as op + +import java.time.Instant class UserServiceImpl( db: Database, diff --git a/modules/backend/src/test/scala/frontend/FrontendSuite.scala b/modules/backend/src/test/scala/frontend/FrontendSuite.scala index 552bbd1..09069b7 100644 --- a/modules/backend/src/test/scala/frontend/FrontendSuite.scala +++ b/modules/backend/src/test/scala/frontend/FrontendSuite.scala @@ -5,7 +5,7 @@ package frontend import scala.concurrent.duration.* import com.indoorvivants.weaver.playwright.* import org.http4s.* -import natchez.Trace.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop import cats.syntax.all.* import cats.effect.* import weaver.* diff --git a/modules/backend/src/test/scala/frontend/PageFragments.scala b/modules/backend/src/test/scala/frontend/PageFragments.scala index d76b04c..7c64366 100644 --- a/modules/backend/src/test/scala/frontend/PageFragments.scala +++ b/modules/backend/src/test/scala/frontend/PageFragments.scala @@ -5,7 +5,8 @@ package frontend import scala.concurrent.duration.* import com.indoorvivants.weaver.playwright.* import org.http4s.* -import natchez.Trace.Implicits.noop + +import org.typelevel.otel4s.trace.Tracer.Implicits.noop import cats.syntax.all.* import weaver.* import cats.effect.* diff --git a/modules/backend/src/test/scala/frontend/Users.scala b/modules/backend/src/test/scala/frontend/Users.scala index ab88ffc..212e4c9 100644 --- a/modules/backend/src/test/scala/frontend/Users.scala +++ b/modules/backend/src/test/scala/frontend/Users.scala @@ -7,7 +7,7 @@ import com.indoorvivants.weaver.playwright.* import org.http4s.* -import natchez.Trace.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop import cats.syntax.all.* import weaver.* import cats.effect.* diff --git a/modules/backend/src/test/scala/integration/Fixture.scala b/modules/backend/src/test/scala/integration/Fixture.scala index 8f3da5b..94d5327 100644 --- a/modules/backend/src/test/scala/integration/Fixture.scala +++ b/modules/backend/src/test/scala/integration/Fixture.scala @@ -26,6 +26,7 @@ import org.http4s.server.middleware.ResponseLogger.apply import org.http4s.server.middleware.ResponseLogger import org.http4s.Request import cats.effect.kernel.Ref +import org.typelevel.otel4s.trace.Tracer object Fixture: private def parseJDBC(url: String) = IO(java.net.URI.create(url.substring(5))) @@ -46,7 +47,7 @@ object Fixture: IO(f.migrate()) } - def skunkConnection(using natchez.Trace[IO]) = + def skunkConnection(using Tracer[IO]) = postgresContainer .evalMap(cont => parseJDBC(cont.jdbcUrl).map(cont -> _)) .evalTap { case (cont, _) => @@ -68,7 +69,7 @@ object Fixture: } - def resource(using natchez.Trace[IO]): Resource[cats.effect.IO, Probe] = + def resource(using Tracer[IO]): Resource[cats.effect.IO, Probe] = import scribe.{Logger, Level} val silenceOfTheLogs = Seq( diff --git a/modules/backend/src/test/scala/integration/IntegrationSuite.scala b/modules/backend/src/test/scala/integration/IntegrationSuite.scala index 141e0f4..9d4cd2d 100644 --- a/modules/backend/src/test/scala/integration/IntegrationSuite.scala +++ b/modules/backend/src/test/scala/integration/IntegrationSuite.scala @@ -6,7 +6,8 @@ import cats.effect.* import cats.effect.std.* import cats.syntax.all.* import jobby.spec.* -import natchez.Trace.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop + import weaver.* object Resources extends GlobalResource: diff --git a/modules/backend/src/test/scala/stub/Fixture.scala b/modules/backend/src/test/scala/stub/Fixture.scala index 97161ef..b3db613 100644 --- a/modules/backend/src/test/scala/stub/Fixture.scala +++ b/modules/backend/src/test/scala/stub/Fixture.scala @@ -14,10 +14,11 @@ import org.http4s.client.Client import pdi.jwt.JwtAlgorithm.HS256 import scribe.cats.* import skunk.util.Typer.Strategy +import org.typelevel.otel4s.trace.Tracer object Fixture: - def resource(using natchez.Trace[IO]): Resource[cats.effect.IO, Probe] = + def resource(using Tracer[IO]): Resource[cats.effect.IO, Probe] = for db <- Resource.eval(InMemoryDB.create) timeCop <- Resource.eval(SlowTimeCop.apply) diff --git a/modules/backend/src/test/scala/stub/StubTests.scala b/modules/backend/src/test/scala/stub/StubTests.scala index aee2224..bece6ee 100644 --- a/modules/backend/src/test/scala/stub/StubTests.scala +++ b/modules/backend/src/test/scala/stub/StubTests.scala @@ -4,7 +4,7 @@ package stub import weaver.* -import natchez.Trace.Implicits.noop +import org.typelevel.otel4s.trace.Tracer.Implicits.noop abstract class StubSuite(global: GlobalRead) extends JobbySuite: override def sharedResource = Fixture.resource From 7a910f35c04bfc8558b9ab93c752cab874a60ec5 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 13 Dec 2024 13:56:21 +0000 Subject: [PATCH 2/2] More updates, linters, formatting changes --- .sbtopts | 1 + .scalafix.conf | 12 +++ .scalafmt.conf | 17 ++- build.sbt | 90 +++++++++++----- jobby.opts | 5 + .../backend/src/main/scala/FlyIOLoader.scala | 2 +- .../backend/src/main/scala/HerokuLoader.scala | 2 +- .../src/main/scala/PlatformShLoader.scala | 7 +- .../src/main/scala/SkunkDatabase.scala | 14 +-- modules/backend/src/main/scala/TimeCop.scala | 5 +- modules/backend/src/main/scala/app.scala | 6 +- .../backend/src/main/scala/auth.crypto.scala | 5 +- modules/backend/src/main/scala/auth.jwt.scala | 14 +-- modules/backend/src/main/scala/config.scala | 27 ++--- .../src/main/scala/database.codecs.scala | 3 +- .../src/main/scala/database.operations.scala | 24 ++--- .../backend/src/main/scala/http.auth.scala | 15 +-- .../backend/src/main/scala/http.routes.scala | 10 +- .../src/main/scala/service.companies.scala | 8 +- .../backend/src/main/scala/service.jobs.scala | 17 +-- .../src/main/scala/service.users.scala | 42 ++++---- .../src/main/scala/skunk.extensions.scala | 26 ++--- modules/backend/src/test/scala/Api.scala | 12 +-- .../backend/src/test/scala/Generator.scala | 3 +- .../src/test/scala/InMemoryLogger.scala | 5 +- .../backend/src/test/scala/JobbySuite.scala | 1 - modules/backend/src/test/scala/Probe.scala | 13 +-- .../backend/src/test/scala/SlowTimeCop.scala | 3 +- .../test/scala/frontend/FrontendSuite.scala | 22 ++-- .../test/scala/frontend/PageFragments.scala | 13 +-- .../src/test/scala/frontend/Users.scala | 18 ++-- .../src/test/scala/integration/Fixture.scala | 42 +++----- .../scala/integration/IntegrationSuite.scala | 4 - .../src/test/scala/specs/Companies.scala | 9 +- .../backend/src/test/scala/specs/Health.scala | 2 - .../backend/src/test/scala/specs/Jobs.scala | 14 ++- .../backend/src/test/scala/specs/Users.scala | 7 +- .../src/test/scala/specs/fragments.scala | 6 +- .../backend/src/test/scala/stub/Fixture.scala | 13 +-- .../src/test/scala/stub/InMemoryDB.scala | 26 ++--- .../src/test/scala/stub/StubTests.scala | 3 +- .../src/test/scala/unit/Validation.scala | 23 ++-- .../frontend/src/main/scala/api.client.scala | 18 ++-- modules/frontend/src/main/scala/app.scala | 30 ++---- .../frontend/src/main/scala/app.state.scala | 14 +-- .../src/main/scala/auth.refresh.scala | 22 ++-- .../src/main/scala/page.company.scala | 26 ++--- .../src/main/scala/page.create_company.scala | 11 +- .../src/main/scala/page.create_job.scala | 20 ++-- .../frontend/src/main/scala/page.job.scala | 7 +- .../src/main/scala/page.latest_jobs.scala | 12 +-- .../frontend/src/main/scala/page.login.scala | 20 ++-- .../frontend/src/main/scala/page.logout.scala | 9 +- .../src/main/scala/page.profile.scala | 29 +++-- .../src/main/scala/page.register.scala | 14 +-- modules/frontend/src/main/scala/routes.scala | 71 ++++++------ modules/frontend/src/main/scala/styles.scala | 58 +++++----- .../src/main/scala/views.company.scala | 18 ++-- .../src/main/scala/views.create_company.scala | 32 +++--- .../main/scala/views.create_job_listing.scala | 101 +++++++++--------- .../main/scala/views.credentials_form.scala | 20 ++-- .../frontend/src/main/scala/views.forms.scala | 2 +- .../src/main/scala/views.job_listing.scala | 22 ++-- .../src/main/scala/views.user_toolbar.scala | 18 ++-- .../src/main/scala/validation.jobs.scala | 2 +- project/BuildStyle.scala | 2 +- project/plugins.sbt | 25 +++-- 67 files changed, 586 insertions(+), 608 deletions(-) create mode 100644 .sbtopts create mode 100644 .scalafix.conf create mode 100644 jobby.opts diff --git a/.sbtopts b/.sbtopts new file mode 100644 index 0000000..d56f51e --- /dev/null +++ b/.sbtopts @@ -0,0 +1 @@ +-J-Xmx4g diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 0000000..3c2f090 --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,12 @@ +OrganizeImports { + coalesceToWildcardImportThreshold = 3 # Int.MaxValue + expandRelative = false + groupExplicitlyImportedImplicitsSeparately = false + groupedImports = Explode + groups = ["re:javax?\\.", "scala.", "*"] + importSelectorsOrder = Ascii + importsOrder = Ascii + removeUnused = false +} +OrganizeImports.targetDialect = Scala3 +OrganizeImports.removeUnused = true diff --git a/.scalafmt.conf b/.scalafmt.conf index b147b75..b95029b 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,5 @@ version = "3.8.3" + runner.dialect = scala3 rewrite.scala3.insertEndMarkerMinLines = 10 rewrite.scala3.removeOptionalBraces = true @@ -9,9 +10,21 @@ fileOverride { "glob:**.sbt" { runner.dialect = scala212source3 } - - "glob:**/project/**.*" { + "glob:**/project/*.scala" { runner.dialect = scala212source3 } + + "glob:**/project/plugins.sbt" { + runner.dialect = scala212source3 + newlines.topLevelStatementBlankLines = [ + { + blanks = 1, + minBreaks = 0 + } + ] + } } +rewrite { + trailingCommas.style = "always" +} diff --git a/build.sbt b/build.sbt index 4a96e40..8140b9b 100644 --- a/build.sbt +++ b/build.sbt @@ -17,11 +17,11 @@ val Versions = new { val TestContainers = "0.41.4" val Weaver = "0.8.4" val WeaverPlaywright = "0.0.5" - val Laminar = "15.0.1" - val waypoint = "6.0.0" + val Laminar = "17.2.0" + val waypoint = "9.0.0" val scalacss = "1.0.0" val monocle = "3.2.0" - val circe = "0.14.5" + val circe = "0.14.10" val macroTaskExecutor = "1.1.1" } @@ -52,21 +52,21 @@ lazy val app = projectMatrix dockerBaseImage := Config.DockerBaseImage, Docker / packageName := Config.DockerImageName, libraryDependencies ++= Seq( - "org.http4s" %% "http4s-blaze-server" % Versions.http4sBlaze, - "org.http4s" %% "http4s-ember-server" % Versions.http4s, - "org.postgresql" % "postgresql" % Versions.Postgres, - "org.flywaydb" % "flyway-database-postgresql" % Versions.FlywayPG + "org.http4s" %% "http4s-blaze-server" % Versions.http4sBlaze, + "org.http4s" %% "http4s-ember-server" % Versions.http4s, + "org.postgresql" % "postgresql" % Versions.Postgres, + "org.flywaydb" % "flyway-database-postgresql" % Versions.FlywayPG, ), Compile / resourceGenerators += { Def.task[Seq[File]] { copyAll( frontendBundle.value, - (Compile / resourceManaged).value / "assets" + (Compile / resourceManaged).value / "assets", ) } }, reStart / baseDirectory := (ThisBuild / baseDirectory).value, - run / baseDirectory := (ThisBuild / baseDirectory).value + run / baseDirectory := (ThisBuild / baseDirectory).value, ) def copyAll(location: File, outDir: File) = { @@ -80,13 +80,18 @@ def copyAll(location: File, outDir: File) = { } } +val scalacSettings = Seq( + scalacOptions += "-Wunused:all", +) + lazy val backend = projectMatrix .in(file("modules/backend")) .dependsOn(shared) .defaultAxes(defaults*) .jvmPlatform(Seq(Versions.Scala)) .settings( - scalaVersion := Versions.Scala, + scalaVersion := Versions.Scala, + scalacSettings, Compile / doc / sources := Seq.empty, libraryDependencies ++= Seq( "com.disneystreaming.smithy4s" %% "smithy4s-http4s" % smithy4sVersion.value, @@ -96,7 +101,7 @@ lazy val backend = projectMatrix "com.outr" %% "scribe" % Versions.scribe, "com.outr" %% "scribe-cats" % Versions.scribe, "com.outr" %% "scribe-slf4j" % Versions.scribe, - "org.tpolecat" %% "skunk-core" % Versions.skunk + "org.tpolecat" %% "skunk-core" % Versions.skunk, ), libraryDependencies ++= Seq( @@ -109,7 +114,7 @@ lazy val backend = projectMatrix "org.http4s" %% "http4s-ember-server" % Versions.http4s, "org.http4s" %% "http4s-ember-client" % Versions.http4s, "org.postgresql" % "postgresql" % Versions.Postgres, - "org.flywaydb" % "flyway-database-postgresql" % Versions.FlywayPG + "org.flywaydb" % "flyway-database-postgresql" % Versions.FlywayPG, ).map(_ % Test), testFrameworks += new TestFramework("weaver.framework.CatsEffect"), Test / fork := true, @@ -118,10 +123,10 @@ lazy val backend = projectMatrix Def.task[Seq[File]] { copyAll( frontendBundle.value, - (Test / resourceManaged).value / "assets" + (Test / resourceManaged).value / "assets", ) } - } + }, ) lazy val shared = projectMatrix @@ -131,11 +136,12 @@ lazy val shared = projectMatrix .jsPlatform(Seq(Versions.Scala)) .enablePlugins(Smithy4sCodegenPlugin) .settings( + scalacSettings, libraryDependencies ++= Seq( "com.disneystreaming.smithy4s" %%% "smithy4s-http4s" % smithy4sVersion.value, - "io.lemonlabs" %%% "scala-uri" % "4.0.3" + "com.indoorvivants" %%% "scala-uri" % "4.1.0", ), - Compile / doc / sources := Seq.empty + Compile / doc / sources := Seq.empty, ) lazy val frontend = projectMatrix @@ -143,17 +149,18 @@ lazy val frontend = projectMatrix .customRow( Seq(Versions.Scala), axisValues = Seq(VirtualAxis.js, BuildStyle.SingleFile), - Seq.empty + Seq.empty, ) .customRow( Seq(Versions.Scala), axisValues = Seq(VirtualAxis.js, BuildStyle.Modules), - Seq.empty + Seq.empty, ) .defaultAxes((defaults :+ VirtualAxis.js)*) .dependsOn(shared) .enablePlugins(ScalaJSPlugin, BundleMonPlugin) .settings( + scalacSettings, scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig := { val config = scalaJSLinkerConfig.value @@ -163,7 +170,7 @@ lazy val frontend = projectMatrix config .withModuleSplitStyle( ModuleSplitStyle - .SmallModulesFor(List(s"${Config.BasePackage}.frontend")) + .SmallModulesFor(List(s"${Config.BasePackage}.frontend")), ) .withModuleKind(ModuleKind.ESModule) .withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs")) @@ -177,8 +184,8 @@ lazy val frontend = projectMatrix "io.circe" %%% "circe-core" % Versions.circe, "io.circe" %%% "circe-parser" % Versions.circe, "org.http4s" %%% "http4s-dom" % Versions.http4sDom, - "org.scala-js" %%% "scala-js-macrotask-executor" % Versions.macroTaskExecutor - ) + "org.scala-js" %%% "scala-js-macrotask-executor" % Versions.macroTaskExecutor, + ), ) lazy val defaults = @@ -187,7 +194,7 @@ lazy val defaults = lazy val frontendModules = taskKey[(Report, File)]("") ThisBuild / frontendModules := Def.taskIf { def proj = frontend.finder(BuildStyle.Modules)( - Versions.Scala + Versions.Scala, ) if (isRelease) @@ -201,7 +208,7 @@ ThisBuild / frontendModules := Def.taskIf { lazy val frontendBundle = taskKey[File]("") ThisBuild / frontendBundle := Def.taskIf { def proj = frontend.finder(BuildStyle.SingleFile)( - Versions.Scala + Versions.Scala, ) if (isRelease) { @@ -220,17 +227,46 @@ addCommandAlias("stubTests", "backend/testOnly jobby.tests.stub.*") addCommandAlias("unitTests", "backend/testOnly jobby.tests.unit.*") addCommandAlias( "fastTests", - "backend/testOnly jobby.tests.stub.* jobby.tests.unit.*" + "backend/testOnly jobby.tests.stub.* jobby.tests.unit.*", ) addCommandAlias( "integrationTests", - "backend/testOnly jobby.tests.integration.*" + "backend/testOnly jobby.tests.integration.*", ) addCommandAlias( "frontendTests", - "backend/testOnly jobby.tests.frontend.*" + "backend/testOnly jobby.tests.frontend.*", ) +val scalafixRules = Seq( + "OrganizeImports", + "DisableSyntax", + "LeakingImplicitClassVal", + "NoValInForComprehension", +).mkString(" ") + +val CICommands = Seq( + "clean", + "scalafixEnable", + "compile", + "test", + "scalafmtCheckAll", + "scalafmtSbtCheck", + s"scalafix --check $scalafixRules", +).mkString(";") + +val PrepareCICommands = Seq( + "scalafixEnable", + s"scalafix --rules $scalafixRules", + s"Test/scalafix --rules $scalafixRules", + "scalafmtAll", + "scalafmtSbt", +).mkString(";") + +addCommandAlias("ci", CICommands) + +addCommandAlias("preCI", PrepareCICommands) + lazy val buildFrontend = taskKey[Unit]("") buildFrontend := { @@ -250,7 +286,7 @@ buildFrontend := { ThisBuild / concurrentRestrictions ++= { if (sys.env.contains("CI")) { Seq( - Tags.limitAll(4) + Tags.limitAll(4), ) } else Seq.empty } diff --git a/jobby.opts b/jobby.opts new file mode 100644 index 0000000..3c25f08 --- /dev/null +++ b/jobby.opts @@ -0,0 +1,5 @@ +PG_USER=jobby +PG_PASSWORD=mysecretpassword +PG_DB=jobby +LOCAL_DEPLOYMENT=true +PG_PORT=5439 \ No newline at end of file diff --git a/modules/backend/src/main/scala/FlyIOLoader.scala b/modules/backend/src/main/scala/FlyIOLoader.scala index bc2aad2..c68cf87 100644 --- a/modules/backend/src/main/scala/FlyIOLoader.scala +++ b/modules/backend/src/main/scala/FlyIOLoader.scala @@ -23,7 +23,7 @@ class FlyIOLoader(env: Map[String, String]): user = userName, password = Some(password), database = dbName, - ssl = false + ssl = false, ) }.toOption diff --git a/modules/backend/src/main/scala/HerokuLoader.scala b/modules/backend/src/main/scala/HerokuLoader.scala index 39b73fe..60a283b 100644 --- a/modules/backend/src/main/scala/HerokuLoader.scala +++ b/modules/backend/src/main/scala/HerokuLoader.scala @@ -23,7 +23,7 @@ class HerokuLoader(env: Map[String, String]): user = userName, password = Some(password), database = dbName, - ssl = true + ssl = true, ) }.toOption diff --git a/modules/backend/src/main/scala/PlatformShLoader.scala b/modules/backend/src/main/scala/PlatformShLoader.scala index a530e56..207fa96 100644 --- a/modules/backend/src/main/scala/PlatformShLoader.scala +++ b/modules/backend/src/main/scala/PlatformShLoader.scala @@ -1,6 +1,7 @@ package jobby import java.util.Base64 + import scala.util.Try import scala.util.control.NonFatal @@ -15,7 +16,7 @@ class PlatformShLoader(env: Map[String, String]): scribe.error("Failed to parse PLATFORM_RELATIONSHIPS", error) Option.empty }, - Option.apply + Option.apply, ) } @@ -31,8 +32,8 @@ class PlatformShLoader(env: Map[String, String]): user = db("username").str, database = db("path").str, password = Some(db("password").str), - ssl = false - ) + ssl = false, + ), ) catch case exc => diff --git a/modules/backend/src/main/scala/SkunkDatabase.scala b/modules/backend/src/main/scala/SkunkDatabase.scala index 16458a1..f160b3d 100644 --- a/modules/backend/src/main/scala/SkunkDatabase.scala +++ b/modules/backend/src/main/scala/SkunkDatabase.scala @@ -1,20 +1,14 @@ package jobby import cats.effect.* +import org.typelevel.otel4s.trace.Tracer import skunk.* -import cats.implicits.* -import skunk.* -import skunk.implicits.* -import skunk.codec.all.* -import jobby.spec.* -import smithy4s.Newtype -import database.codecs.* + import database.operations.* -import org.typelevel.otel4s.trace.Tracer object SkunkDatabase: def load(postgres: PgCredentials, skunkConfig: SkunkConfig)(using - Tracer[IO] + Tracer[IO], ): Resource[IO, Database] = Session .pooled[IO]( @@ -26,7 +20,7 @@ object SkunkDatabase: strategy = skunkConfig.strategy, max = skunkConfig.maxSessions, debug = skunkConfig.debug, - ssl = if postgres.ssl then skunk.SSL.Trusted else skunk.SSL.None + ssl = if postgres.ssl then skunk.SSL.Trusted else skunk.SSL.None, ) .map(SkunkDatabase(_)) end load diff --git a/modules/backend/src/main/scala/TimeCop.scala b/modules/backend/src/main/scala/TimeCop.scala index 4800ec7..a37a8ab 100644 --- a/modules/backend/src/main/scala/TimeCop.scala +++ b/modules/backend/src/main/scala/TimeCop.scala @@ -1,10 +1,11 @@ package jobby import java.time.OffsetDateTime -import cats.effect.IO import java.time.ZoneOffset -import smithy4s.Timestamp + +import cats.effect.IO import smithy4s.Newtype +import smithy4s.Timestamp trait TimeCop: def nowODT: IO[OffsetDateTime] diff --git a/modules/backend/src/main/scala/app.scala b/modules/backend/src/main/scala/app.scala index 5efeef4..d567200 100644 --- a/modules/backend/src/main/scala/app.scala +++ b/modules/backend/src/main/scala/app.scala @@ -1,21 +1,21 @@ package jobby import cats.effect.* -import scribe.Scribe import org.typelevel.otel4s.trace.Tracer +import scribe.Scribe class JobbyApp( val config: AppConfig, db: Database, logger: Scribe[IO], - timeCop: TimeCop + timeCop: TimeCop, )(using Tracer[IO]): def routes = Routes(db, config, logger, timeCop) end JobbyApp object JobbyApp: def bootstrap(config: AppConfig, logger: Scribe[IO])(using - Tracer[IO] + Tracer[IO], ) = for db <- SkunkDatabase.load(config.postgres, config.skunk) yield JobbyApp(config, db, logger, TimeCop.unsafe) diff --git a/modules/backend/src/main/scala/auth.crypto.scala b/modules/backend/src/main/scala/auth.crypto.scala index 490e9d1..56c6414 100644 --- a/modules/backend/src/main/scala/auth.crypto.scala +++ b/modules/backend/src/main/scala/auth.crypto.scala @@ -1,10 +1,11 @@ package jobby package users -import jobby.spec.* +import java.security.MessageDigest + import cats.effect.* import cats.effect.std.SecureRandom -import java.security.MessageDigest +import jobby.spec.* object Crypto: def hashPassword(raw: UserPassword): IO[HashedPassword] = diff --git a/modules/backend/src/main/scala/auth.jwt.scala b/modules/backend/src/main/scala/auth.jwt.scala index c2be580..ccccdd1 100644 --- a/modules/backend/src/main/scala/auth.jwt.scala +++ b/modules/backend/src/main/scala/auth.jwt.scala @@ -1,17 +1,19 @@ package jobby -import jobby.spec.* import java.util.UUID -import scala.util.Try + import scala.util.Failure import scala.util.Success +import scala.util.Try + +import jobby.spec.* object JWT: enum Kind: case AccessToken, RefreshToken import java.time.Instant - import pdi.jwt.{JwtUpickle, JwtAlgorithm, JwtClaim} + import pdi.jwt.{JwtUpickle, JwtClaim} def create(kind: Kind, userId: UserId, config: JwtConfig) = val claim = JwtClaim( @@ -19,11 +21,11 @@ object JWT: expiration = Some( Instant.now .plusSeconds(config.expiration(kind).toSeconds) - .getEpochSecond + .getEpochSecond, ), issuedAt = Some(Instant.now.getEpochSecond), audience = Option(Set(config.audience(kind))), - subject = Option(userId.value.toString) + subject = Option(userId.value.toString), ) JwtUpickle.encode(claim, config.secretKey.plaintext, config.algorithm) @@ -38,7 +40,7 @@ object JWT: if aud == expected then Success(claim) else Failure( - new Exception(s"Audience: $aud didn't match expected: $expected") + new Exception(s"Audience: $aud didn't match expected: $expected"), ) } .flatMap { claim => diff --git a/modules/backend/src/main/scala/config.scala b/modules/backend/src/main/scala/config.scala index 77c5f5f..60fc1b1 100644 --- a/modules/backend/src/main/scala/config.scala +++ b/modules/backend/src/main/scala/config.scala @@ -1,25 +1,26 @@ package jobby +import scala.concurrent.duration.* + import cats.effect.IO import com.comcast.ip4s.Host import com.comcast.ip4s.Port import pdi.jwt.JwtAlgorithm import pdi.jwt.algorithms.JwtHmacAlgorithm -import scala.concurrent.duration.* case class AppConfig( postgres: PgCredentials, skunk: SkunkConfig, http: HttpConfig, jwt: JwtConfig, - misc: MiscConfig + misc: MiscConfig, ) object AppConfig: def load( env: Map[String, String], cliArgs: List[String], - opts: Map[String, String] = Map.empty + opts: Map[String, String] = Map.empty, ) = val envWithFallback = @@ -42,7 +43,7 @@ object AppConfig: val skunkConfig = SkunkConfig( maxSessions = 16, strategy = skunk.Strategy.SearchPath, - debug = false + debug = false, ) val http = HttpConfig.fromCliArguments(cliArgs, envWithFallback) @@ -60,11 +61,11 @@ object AppConfig: case JWT.Kind.AccessToken => 15.minutes case JWT.Kind.RefreshToken => 14.days }, - { case _ => "urn:jobby:auth" } + { case _ => "urn:jobby:auth" }, ) val misc = MiscConfig( - latestJobs = 20 + latestJobs = 20, ) IO { @@ -78,11 +79,11 @@ case class JwtConfig( algorithm: JwtHmacAlgorithm, audience: JWT.Kind => String, expiration: JWT.Kind => FiniteDuration, - issuer: JWT.Kind => String + issuer: JWT.Kind => String, ) case class MiscConfig( - latestJobs: Int = 20 + latestJobs: Int = 20, ) enum Deployment: @@ -93,7 +94,7 @@ object HttpConfig: import com.comcast.ip4s.* def fromCliArguments( args: List[String], - env: Map[String, String] = Map.empty + env: Map[String, String] = Map.empty, ) = HttpConfig( port = args.headOption @@ -105,14 +106,14 @@ object HttpConfig: .get("LOCAL_DEPLOYMENT") .filter(_ == "true") .map(_ => Deployment.Local) - .getOrElse(Deployment.Live) + .getOrElse(Deployment.Live), ) end HttpConfig case class SkunkConfig( maxSessions: Int, strategy: skunk.Strategy, - debug: Boolean + debug: Boolean, ) case class PgCredentials( @@ -121,7 +122,7 @@ case class PgCredentials( user: String, database: String, password: Option[String], - ssl: Boolean + ssl: Boolean, ) object PgCredentials: @@ -132,6 +133,6 @@ object PgCredentials: user = mp.getOrElse("PG_USER", "postgres"), database = mp.getOrElse("PG_DB", "postgres"), password = mp.get("PG_PASSWORD"), - ssl = false + ssl = false, ) end PgCredentials diff --git a/modules/backend/src/main/scala/database.codecs.scala b/modules/backend/src/main/scala/database.codecs.scala index 7bd7f79..6a69238 100644 --- a/modules/backend/src/main/scala/database.codecs.scala +++ b/modules/backend/src/main/scala/database.codecs.scala @@ -5,7 +5,8 @@ import jobby.spec.* import skunk.Codec import skunk.codec.all.* import skunk.data.Type -import smithy4s.{Newtype, Timestamp} +import smithy4s.Newtype +import smithy4s.Timestamp object codecs: extension [T](c: Codec[T]) diff --git a/modules/backend/src/main/scala/database.operations.scala b/modules/backend/src/main/scala/database.operations.scala index ef53fcc..d8baab4 100644 --- a/modules/backend/src/main/scala/database.operations.scala +++ b/modules/backend/src/main/scala/database.operations.scala @@ -26,7 +26,7 @@ case class GetCredentials(login: UserLogin) sql""" select user_id, salted_hash from users where lower(login) = lower($userLogin) - """.query(userId *: hashedPassword) + """.query(userId *: hashedPassword), ) case class CreateUser(login: UserLogin, password: HashedPassword) @@ -37,7 +37,7 @@ case class CreateUser(login: UserLogin, password: HashedPassword) users (user_id, login, salted_hash) values (gen_random_uuid(), lower($userLogin), $hashedPassword) returning user_id - """.query(userId) + """.query(userId), ) // company ops @@ -48,7 +48,7 @@ case class GetCompanyById(login: CompanyId) sql""" select company_id, owner_id, name, description, url from companies where company_id = $companyId - """.query(company) + """.query(company), ) case class DeleteCompanyById(company: CompanyId, user: UserId) @@ -57,7 +57,7 @@ case class DeleteCompanyById(company: CompanyId, user: UserId) sql""" delete from companies where company_id = $companyId and owner_id = $userId returning 'ok'::varchar - """.query(varchar) + """.query(varchar), ) case class DeleteJobById(id: JobId) @@ -66,7 +66,7 @@ case class DeleteJobById(id: JobId) sql""" delete from jobs where job_id = $jobId returning 'ok'::varchar - """.query(varchar) + """.query(varchar), ) case class CreateCompany(userId: UserId, attributes: CompanyAttributes) @@ -78,7 +78,7 @@ case class CreateCompany(userId: UserId, attributes: CompanyAttributes) values(gen_random_uuid(), ${codecs.userId}, $companyAttributes) on conflict do nothing returning company_id - """.query(companyId) + """.query(companyId), ) end CreateCompany @@ -88,7 +88,7 @@ case class ListUserCompanies(user: UserId) sql""" select company_id, owner_id, name, description, url from companies where owner_id = $userId - """.query(company) + """.query(company), ) // // job ops @@ -102,20 +102,20 @@ case class GetJob(id: JobId) from jobs where job_id = $jobId - """.query(job) + """.query(job), ) case class CreateJob( company: CompanyId, attributes: JobAttributes, - jobAdded: JobAdded + jobAdded: JobAdded, ) extends SqlQuery( (company, attributes, jobAdded), sql""" insert into jobs(job_id, company_id, job_title, job_description, job_url, min_salary, max_salary, currency, added) values (gen_random_uuid(), $companyId, $jobAttributes, $added) returning job_id - """.query(jobId) + """.query(jobId), ) case object LatestJobs @@ -133,7 +133,7 @@ case object LatestJobs currency, added from jobs order by added desc limit 20 - """.query(job) + """.query(job), ) case class ListJobs(company: CompanyId) @@ -151,5 +151,5 @@ case class ListJobs(company: CompanyId) currency, added from jobs where company_id = $companyId - """.query(job) + """.query(job), ) diff --git a/modules/backend/src/main/scala/http.auth.scala b/modules/backend/src/main/scala/http.auth.scala index 03c2269..01d5cb5 100644 --- a/modules/backend/src/main/scala/http.auth.scala +++ b/modules/backend/src/main/scala/http.auth.scala @@ -1,24 +1,25 @@ package jobby +import scala.concurrent.duration.FiniteDuration +import scala.util.Failure +import scala.util.Success + import cats.effect.IO import jobby.spec.* import scribe.Scribe -import scala.concurrent.duration.FiniteDuration -import scala.util.{Failure, Success} - class HttpAuth(config: JwtConfig, logger: Scribe[IO]): def accessToken(userId: UserId): (String, FiniteDuration) = JWT.create(JWT.Kind.AccessToken, userId, config) -> config.expiration( - JWT.Kind.AccessToken + JWT.Kind.AccessToken, ) def refreshToken(userId: UserId): (String, FiniteDuration) = JWT.create(JWT.Kind.RefreshToken, userId, config) -> config.expiration( - JWT.Kind.RefreshToken + JWT.Kind.RefreshToken, ) def access[A](header: AuthHeader): IO[UserId] = @@ -26,7 +27,7 @@ class HttpAuth(config: JwtConfig, logger: Scribe[IO]): JWT.validate( header.value.drop("Bearer ".length), JWT.Kind.AccessToken, - config + config, ) match case Failure(ex) => logger.error(ex) *> @@ -40,7 +41,7 @@ class HttpAuth(config: JwtConfig, logger: Scribe[IO]): JWT.validate( token.value, JWT.Kind.RefreshToken, - config + config, ) match case Failure(ex) => logger.error(ex) *> IO.raiseError(UnauthorizedError()) diff --git a/modules/backend/src/main/scala/http.routes.scala b/modules/backend/src/main/scala/http.routes.scala index 546f7fe..4f2bf16 100644 --- a/modules/backend/src/main/scala/http.routes.scala +++ b/modules/backend/src/main/scala/http.routes.scala @@ -1,5 +1,7 @@ package jobby +import java.nio.file.Paths + import cats.data.Kleisli import cats.effect.* import cats.implicits.* @@ -11,13 +13,11 @@ import org.http4s.dsl.io.* import scribe.Scribe import smithy4s.http4s.SimpleRestJsonBuilder -import java.nio.file.Paths - def Routes( db: Database, config: AppConfig, logger: Scribe[IO], - timeCop: TimeCop + timeCop: TimeCop, ): Resource[IO, HttpApp[IO]] = def handleErrors(routes: HttpRoutes[IO]) = routes.orNotFound.onError { exc => @@ -50,7 +50,7 @@ object Static: .fromResource[IO]( "index.html", None, - preferGzipped = true + preferGzipped = true, ) .getOrElseF(NotFound()) @@ -61,7 +61,7 @@ object Static: .fromResource[IO]( Paths.get("assets", filename).toString, Some(req), - preferGzipped = true + preferGzipped = true, ) .getOrElseF(NotFound()) case req @ GET -> Root => indexHtml diff --git a/modules/backend/src/main/scala/service.companies.scala b/modules/backend/src/main/scala/service.companies.scala index ab3328b..da25935 100644 --- a/modules/backend/src/main/scala/service.companies.scala +++ b/modules/backend/src/main/scala/service.companies.scala @@ -2,7 +2,7 @@ package jobby import cats.effect.* import cats.syntax.all.* -import jobby.database.{operations as op} +import jobby.database.operations as op import jobby.spec.* import jobby.validation.* @@ -10,18 +10,18 @@ class CompaniesServiceImpl(db: Database, httpAuth: HttpAuth) extends CompaniesService[IO]: override def createCompany( auth: AuthHeader, - attributes: CompanyAttributes + attributes: CompanyAttributes, ): IO[CreateCompanyOutput] = httpAuth.access(auth).flatMap { userId => val validation = List( validateCompanyName(attributes.name), validateCompanyDescription(attributes.description), - validateCompanyUrl(attributes.url) + validateCompanyUrl(attributes.url), ).traverse(IO.fromEither) validation *> db.option( - op.CreateCompany(userId, attributes) + op.CreateCompany(userId, attributes), ).flatMap { case None => IO.raiseError(ValidationError("Company already exists")) case Some(id) => IO.pure(CreateCompanyOutput(id)) diff --git a/modules/backend/src/main/scala/service.jobs.scala b/modules/backend/src/main/scala/service.jobs.scala index 27aba06..360638d 100644 --- a/modules/backend/src/main/scala/service.jobs.scala +++ b/modules/backend/src/main/scala/service.jobs.scala @@ -3,9 +3,10 @@ package jobby import cats.effect.* import cats.implicits.* import jobby.spec.* -import database.operations as op import jobby.validation.* +import database.operations as op + class JobServiceImpl(db: Database, auth: HttpAuth, timeCop: TimeCop) extends JobService[IO]: override def getJob(id: JobId): IO[Job] = @@ -17,13 +18,13 @@ class JobServiceImpl(db: Database, auth: HttpAuth, timeCop: TimeCop) override def createJob( authHeader: AuthHeader, companyId: CompanyId, - attributes: JobAttributes + attributes: JobAttributes, ): IO[CreateJobOutput] = for userId <- auth.access(authHeader) companyLookup <- db.option(op.GetCompanyById(companyId)) company <- IO.fromOption(companyLookup)( - ValidationError("company not found") + ValidationError("company not found"), ) _ <- IO.raiseUnless(company.owner_id == userId)(ForbiddenError()) @@ -32,7 +33,7 @@ class JobServiceImpl(db: Database, auth: HttpAuth, timeCop: TimeCop) validateJobTitle(attributes.title), validateJobDescription(attributes.description), validateJobUrl(attributes.url), - validateSalaryRange(attributes.range) + validateSalaryRange(attributes.range), ).traverse(IO.fromEither) added <- timeCop.timestampNT(JobAdded) @@ -42,11 +43,11 @@ class JobServiceImpl(db: Database, auth: HttpAuth, timeCop: TimeCop) op.CreateJob( companyId, attributes, - added - ) + added, + ), ) jobId <- IO.fromOption(createdJob)( - ValidationError("well you *must have* done something wrong") + ValidationError("well you *must have* done something wrong"), ) yield CreateJobOutput(jobId) end for @@ -66,7 +67,7 @@ class JobServiceImpl(db: Database, auth: HttpAuth, timeCop: TimeCop) job <- getJob(id) companyLookup <- db.option(op.GetCompanyById(job.companyId)) company <- IO.fromOption(companyLookup)( - ValidationError("company not found") + ValidationError("company not found"), ) _ <- IO.raiseUnless(company.owner_id == userId)(ForbiddenError()) _ <- db.option(op.DeleteJobById(id)) diff --git a/modules/backend/src/main/scala/service.users.scala b/modules/backend/src/main/scala/service.users.scala index 23148d8..f069b0b 100644 --- a/modules/backend/src/main/scala/service.users.scala +++ b/modules/backend/src/main/scala/service.users.scala @@ -1,22 +1,24 @@ package jobby package users +import java.time.Instant + import cats.effect.* import cats.implicits.* +import jobby.database.operations as op import jobby.spec.* import jobby.validation.* -import org.http4s.{HttpDate, RequestCookie, ResponseCookie, SameSite} +import org.http4s.HttpDate +import org.http4s.RequestCookie +import org.http4s.ResponseCookie +import org.http4s.SameSite import scribe.Scribe -import jobby.database.operations as op - -import java.time.Instant - class UserServiceImpl( db: Database, auth: HttpAuth, logger: Scribe[IO], - deployment: Deployment + deployment: Deployment, ) extends UserService[IO]: override def login(login: UserLogin, password: UserPassword): IO[Tokens] = @@ -43,11 +45,11 @@ class UserServiceImpl( secureCookie( "refresh_token", refresh, - HttpDate.unsafeFromInstant(instant) - ) - ) + HttpDate.unsafeFromInstant(instant), + ), + ), ), - expires_in = Some(TokenExpiration(maxAgeAccess.toSeconds.toInt)) + expires_in = Some(TokenExpiration(maxAgeAccess.toSeconds.toInt)), ) } end if @@ -72,7 +74,7 @@ class UserServiceImpl( override def refresh( refreshToken: Option[RefreshToken], - logout: Option[Boolean] + logout: Option[Boolean], ): IO[RefreshOutput] = val extractCookie = refreshToken match @@ -93,18 +95,18 @@ class UserServiceImpl( "", maxAge = Some(0L), secure = deployment == Deployment.Live, - path = Some("/api/users/refresh") - ).renderString - ) + path = Some("/api/users/refresh"), + ).renderString, + ), ), - expires_in = TokenExpiration(0) - ) + expires_in = TokenExpiration(0), + ), ) else extractCookie.flatMap { case None => IO.raiseError( - UnauthorizedError(message = Some("Refresh cookie is missing")) + UnauthorizedError(message = Some("Refresh cookie is missing")), ) case Some(tok) => auth.refresh(RefreshToken(tok.content)).flatMap { userId => @@ -113,8 +115,8 @@ class UserServiceImpl( IO.pure( RefreshOutput( access_token = Some(AccessToken(auth_token)), - expires_in = TokenExpiration(expiresIn.toSeconds.toInt) - ) + expires_in = TokenExpiration(expiresIn.toSeconds.toInt), + ), ) } } @@ -129,6 +131,6 @@ class UserServiceImpl( secure = deployment == Deployment.Live, path = Some("/api/users/refresh"), expires = Some(expires), - sameSite = Some(SameSite.Strict) + sameSite = Some(SameSite.Strict), ).renderString end UserServiceImpl diff --git a/modules/backend/src/main/scala/skunk.extensions.scala b/modules/backend/src/main/scala/skunk.extensions.scala index f33f5c3..06c393f 100644 --- a/modules/backend/src/main/scala/skunk.extensions.scala +++ b/modules/backend/src/main/scala/skunk.extensions.scala @@ -1,14 +1,14 @@ package jobby -import skunk.util.Twiddler import scala.deriving.Mirror import skunk.* import skunk.implicits.* +import skunk.util.Twiddler implicit def product7[P <: Product, A, B, C, D, E, F, G](implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G } = new Twiddler[P]: type Out = A ~ B ~ C ~ D ~ E ~ F ~ G @@ -19,7 +19,7 @@ implicit def product7[P <: Product, A, B, C, D, E, F, G](implicit implicit def product8[P <: Product, A, B, C, D, E, F, G, H](implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H } = new Twiddler[P]: type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H @@ -31,7 +31,7 @@ implicit def product8[P <: Product, A, B, C, D, E, F, G, H](implicit implicit def product9[P <: Product, A, B, C, D, E, F, G, H, I](implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I } = new Twiddler[P]: type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I @@ -43,7 +43,7 @@ implicit def product9[P <: Product, A, B, C, D, E, F, G, H, I](implicit implicit def product10[P <: Product, A, B, C, D, E, F, G, H, I, J](implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J } = new Twiddler[P]: type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J @@ -56,7 +56,7 @@ implicit def product10[P <: Product, A, B, C, D, E, F, G, H, I, J](implicit implicit def product11[P <: Product, A, B, C, D, E, F, G, H, I, J, K](implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J ~ K } = new Twiddler[P]: type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J ~ K @@ -70,7 +70,7 @@ implicit def product11[P <: Product, A, B, C, D, E, F, G, H, I, J, K](implicit implicit def product12[P <: Product, A, B, C, D, E, F, G, H, I, J, K, L]( implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J ~ K ~ L } = new Twiddler[P]: type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J ~ K ~ L @@ -85,7 +85,7 @@ implicit def product12[P <: Product, A, B, C, D, E, F, G, H, I, J, K, L]( implicit def product13[P <: Product, A, B, C, D, E, F, G, H, I, J, K, L, Q]( implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L, Q) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L, Q), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J ~ K ~ L ~ Q } = @@ -101,7 +101,7 @@ implicit def product13[P <: Product, A, B, C, D, E, F, G, H, I, J, K, L, Q]( implicit def product14[P <: Product, A, B, C, D, E, F, G, H, I, J, K, L, Q, R]( implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L, Q, R) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L, Q, R), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J ~ K ~ L ~ Q ~ R } = @@ -130,10 +130,10 @@ implicit def product15[ L, Q, R, - S + S, ](implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L, Q, R, S) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L, Q, R, S), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J ~ K ~ L ~ Q ~ R ~ S } = @@ -163,10 +163,10 @@ implicit def product16[ Q, R, S, - T + T, ](implicit m: Mirror.ProductOf[P], - i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L, Q, R, S, T) + i: m.MirroredElemTypes =:= (A, B, C, D, E, F, G, H, I, J, K, L, Q, R, S, T), ): Twiddler[P] { type Out = A ~ B ~ C ~ D ~ E ~ F ~ G ~ H ~ I ~ J ~ K ~ L ~ Q ~ R ~ S ~ T } = diff --git a/modules/backend/src/test/scala/Api.scala b/modules/backend/src/test/scala/Api.scala index f34e7b7..ae1105e 100644 --- a/modules/backend/src/test/scala/Api.scala +++ b/modules/backend/src/test/scala/Api.scala @@ -3,9 +3,7 @@ package tests import cats.effect.* import cats.syntax.all.* - import jobby.spec.* - import org.http4s.* import org.http4s.client.* import smithy4s.http4s.SimpleRestJsonBuilder @@ -14,7 +12,7 @@ case class Api( companies: CompaniesService[IO], jobs: JobService[IO], users: UserService[IO], - health: HealthService[IO] + health: HealthService[IO], ) object Api: @@ -23,27 +21,27 @@ object Api: SimpleRestJsonBuilder(CompaniesService) .client(client) .uri(uri) - .use + .make, ) val jobs = IO.fromEither( SimpleRestJsonBuilder(JobService) .client(client) .uri(uri) - .use + .make, ) val users = IO.fromEither( SimpleRestJsonBuilder(UserService) .client(client) .uri(uri) - .use + .make, ) val health = IO.fromEither( SimpleRestJsonBuilder(HealthService) .client(client) .uri(uri) - .use + .make, ) (companies, jobs, users, health).mapN(Api.apply) diff --git a/modules/backend/src/test/scala/Generator.scala b/modules/backend/src/test/scala/Generator.scala index 96da5ec..17af076 100644 --- a/modules/backend/src/test/scala/Generator.scala +++ b/modules/backend/src/test/scala/Generator.scala @@ -6,7 +6,6 @@ import java.util.UUID import cats.effect.* import cats.effect.std.* import cats.syntax.all.* - import smithy4s.Newtype case class Generator private (random: Random[IO], uuid: UUIDGen[IO]): @@ -23,7 +22,7 @@ case class Generator private (random: Random[IO], uuid: UUIDGen[IO]): def str( toNewType: Newtype[String], - lengthRange: Range = 0 to 100 + lengthRange: Range = 0 to 100, ): IO[toNewType.Type] = for length <- random.betweenInt(lengthRange.start, lengthRange.end) diff --git a/modules/backend/src/test/scala/InMemoryLogger.scala b/modules/backend/src/test/scala/InMemoryLogger.scala index a9b3dae..69fb220 100644 --- a/modules/backend/src/test/scala/InMemoryLogger.scala +++ b/modules/backend/src/test/scala/InMemoryLogger.scala @@ -3,13 +3,12 @@ package tests import cats.effect.* import cats.effect.std.* - import scribe.* import scribe.cats.* class InMemoryLogger private ( val logs: IO[Vector[LogRecord]], - val scribeLogger: Scribe[IO] + val scribeLogger: Scribe[IO], ) object InMemoryLogger: @@ -29,7 +28,7 @@ object InMemoryLogger: new InMemoryLogger( ref.get, - logger + logger, ) } } diff --git a/modules/backend/src/test/scala/JobbySuite.scala b/modules/backend/src/test/scala/JobbySuite.scala index 5bce099..123aca8 100644 --- a/modules/backend/src/test/scala/JobbySuite.scala +++ b/modules/backend/src/test/scala/JobbySuite.scala @@ -3,7 +3,6 @@ package tests import cats.effect.IO import cats.syntax.all.* - import scribe.Level import weaver.* diff --git a/modules/backend/src/test/scala/Probe.scala b/modules/backend/src/test/scala/Probe.scala index 60ead24..36c9b83 100644 --- a/modules/backend/src/test/scala/Probe.scala +++ b/modules/backend/src/test/scala/Probe.scala @@ -2,15 +2,10 @@ package jobby package tests import cats.effect.* - import org.http4s.Uri import org.http4s.client.Client -import scribe.cats.* -import scribe.handler.FunctionalLogHandler -import cats.effect.std.Dispatcher import scribe.LogRecord -import scribe.handler.LogHandler -import scribe.Level +import scribe.cats.* case class Probe( api: Api, @@ -18,7 +13,7 @@ case class Probe( serverUri: Uri, gen: Generator, config: AppConfig, - getLogs: IO[Vector[LogRecord]] + getLogs: IO[Vector[LogRecord]], ): def fragments = Fragments(this) end Probe @@ -28,7 +23,7 @@ object Probe: client: Client[IO], uri: Uri, config: AppConfig, - logger: InMemoryLogger + logger: InMemoryLogger, ) = Resource.eval { for @@ -36,7 +31,7 @@ object Probe: api <- Api.build(client, uri) auth = HttpAuth( config.jwt, - logger.scribeLogger + logger.scribeLogger, ) yield Probe(api, auth, uri, gen, config, logger.logs) } diff --git a/modules/backend/src/test/scala/SlowTimeCop.scala b/modules/backend/src/test/scala/SlowTimeCop.scala index 6af1b6b..e5a4a3f 100644 --- a/modules/backend/src/test/scala/SlowTimeCop.scala +++ b/modules/backend/src/test/scala/SlowTimeCop.scala @@ -1,8 +1,9 @@ package jobby -import cats.effect.* import java.time.OffsetDateTime +import cats.effect.* + object SlowTimeCop: def apply: IO[TimeCop] = Ref.of[IO, Int](0).map { daysRef => val start = OffsetDateTime.now diff --git a/modules/backend/src/test/scala/frontend/FrontendSuite.scala b/modules/backend/src/test/scala/frontend/FrontendSuite.scala index 09069b7..d2f47ff 100644 --- a/modules/backend/src/test/scala/frontend/FrontendSuite.scala +++ b/modules/backend/src/test/scala/frontend/FrontendSuite.scala @@ -2,16 +2,16 @@ package jobby package tests package frontend +import java.nio.file.Paths + import scala.concurrent.duration.* -import com.indoorvivants.weaver.playwright.* -import org.http4s.* -import org.typelevel.otel4s.trace.Tracer.Implicits.noop -import cats.syntax.all.* + import cats.effect.* -import weaver.* +import com.indoorvivants.weaver.playwright.* import com.indoorvivants.weaver.playwright.BrowserConfig.Chromium import com.microsoft.playwright.BrowserType.LaunchOptions -import java.nio.file.Paths +import org.typelevel.otel4s.trace.Tracer.Implicits.noop +import weaver.* abstract class FrontendSuite(global: GlobalRead) extends weaver.IOSuite @@ -26,9 +26,9 @@ abstract class FrontendSuite(global: GlobalRead) Some( LaunchOptions() .setHeadless(sys.env.contains("CI")) - .setSlowMo(sys.env.get("CI").map(_ => 0).getOrElse(1000)) - ) - ) + .setSlowMo(sys.env.get("CI").map(_ => 0).getOrElse(1000)), + ), + ), ) .map { pw => Resources(pb, pw) @@ -48,14 +48,14 @@ abstract class FrontendSuite(global: GlobalRead) pc.page(_.setDefaultTimeout(timeout.toMillis)) def frontendTest( - name: TestName + name: TestName, )(f: (Probe, PageContext, PageFragments) => IO[Expectations]) = test(name) { (res, logs) => getPageContext(res).evalTap(configure).use { pc => def screenshot(pc: PageContext, name: String) = val path = Paths.get("playwright-screenshots", name + ".png") pc.screenshot(path) *> logs.info( - s"Screenshot of last known page state is saved at ${path.toAbsolutePath()}" + s"Screenshot of last known page state is saved at ${path.toAbsolutePath()}", ) def testName = name.name.collect { diff --git a/modules/backend/src/test/scala/frontend/PageFragments.scala b/modules/backend/src/test/scala/frontend/PageFragments.scala index 7c64366..ba4f06a 100644 --- a/modules/backend/src/test/scala/frontend/PageFragments.scala +++ b/modules/backend/src/test/scala/frontend/PageFragments.scala @@ -2,21 +2,16 @@ package jobby package tests package frontend -import scala.concurrent.duration.* +import cats.effect.* import com.indoorvivants.weaver.playwright.* +import jobby.spec.* import org.http4s.* - -import org.typelevel.otel4s.trace.Tracer.Implicits.noop -import cats.syntax.all.* import weaver.* -import cats.effect.* -import java.nio.file.Paths -import jobby.spec.* class PageFragments( pc: PageContext, probe: Probe, - policy: PlaywrightRetry + policy: PlaywrightRetry, ): import pc.* @@ -32,7 +27,7 @@ class PageFragments( _ <- eventually(page(_.url()).map(Uri.unsafeFromString)) { u => expect.same( u.path.dropEndsWithSlash.toAbsolute.renderString, - "/companies/create" + "/companies/create", ) } _ <- locator("#input-company-name").map(_.fill(attributes.name.value)) diff --git a/modules/backend/src/test/scala/frontend/Users.scala b/modules/backend/src/test/scala/frontend/Users.scala index 212e4c9..6ef15b0 100644 --- a/modules/backend/src/test/scala/frontend/Users.scala +++ b/modules/backend/src/test/scala/frontend/Users.scala @@ -2,21 +2,15 @@ package jobby package tests package frontend -import scala.concurrent.duration.* +import cats.syntax.all.* import com.indoorvivants.weaver.playwright.* - +import jobby.spec.* import org.http4s.* - -import org.typelevel.otel4s.trace.Tracer.Implicits.noop -import cats.syntax.all.* import weaver.* -import cats.effect.* -import java.nio.file.Paths -import jobby.spec.* case class Resources( probe: Probe, - pw: PlaywrightRuntime + pw: PlaywrightRuntime, ) class UsersSpec(global: GlobalRead) extends FrontendSuite(global): @@ -56,7 +50,7 @@ class UsersSpec(global: GlobalRead) extends FrontendSuite(global): } frontendTest("add company and render its page") { (probe, pc, pf) => - import pc.*, probe.*, pf.* + import pc.*, pf.* val frg = Fragments(probe) for @@ -82,7 +76,7 @@ class UsersSpec(global: GlobalRead) extends FrontendSuite(global): expect(path.segments.size == 2) && expect(path.segments.headOption.exists(_.encoded == "company")) }.whenA( - !sys.env.contains("CI") + !sys.env.contains("CI"), ) // for some reason redirects don't get registered in headless mode on CI name <- locator("#company-profile-name").map(_.innerText()) @@ -93,7 +87,7 @@ class UsersSpec(global: GlobalRead) extends FrontendSuite(global): .all( name == attributes.name.value, url == attributes.url.value, - description == attributes.description.value + description == attributes.description.value, ) end for } diff --git a/modules/backend/src/test/scala/integration/Fixture.scala b/modules/backend/src/test/scala/integration/Fixture.scala index 94d5327..f20a66a 100644 --- a/modules/backend/src/test/scala/integration/Fixture.scala +++ b/modules/backend/src/test/scala/integration/Fixture.scala @@ -4,29 +4,15 @@ package integration import cats.effect.IO import cats.effect.Resource -import cats.syntax.all.* import com.dimafeng.testcontainers.PostgreSQLContainer import org.flywaydb.core.Flyway -import org.http4s.Uri +import org.http4s.HttpApp import org.http4s.blaze.client.* -import org.http4s.client.Client -import org.http4s.ember.client.* +import org.http4s.blaze.server.* import org.testcontainers.utility.DockerImageName +import org.typelevel.otel4s.trace.Tracer import pdi.jwt.JwtAlgorithm.HS256 import skunk.util.Typer.Strategy -import org.http4s.ember.server.EmberServerBuilder -import scribe.cats.* -import org.http4s.blaze.server.* -import org.http4s.server.middleware.RequestLogger -import org.http4s.HttpRoutes.apply -import org.http4s.HttpRoutes -import org.http4s.HttpApp -import org.http4s.Status.apply -import org.http4s.server.middleware.ResponseLogger.apply -import org.http4s.server.middleware.ResponseLogger -import org.http4s.Request -import cats.effect.kernel.Ref -import org.typelevel.otel4s.trace.Tracer object Fixture: private def parseJDBC(url: String) = IO(java.net.URI.create(url.substring(5))) @@ -35,8 +21,8 @@ object Fixture: val start = IO( PostgreSQLContainer( dockerImageNameOverride = DockerImageName("postgres:14"), - mountPostgresDataToTmpfs = true - ) + mountPostgresDataToTmpfs = true, + ), ).flatTap(cont => IO(cont.start())) Resource.make(start)(cont => IO(cont.stop())) @@ -60,7 +46,7 @@ object Fixture: user = cont.username, password = Some(cont.password), database = cont.databaseName, - ssl = false + ssl = false, ) SkunkDatabase @@ -77,7 +63,7 @@ object Fixture: "org.flywaydb.core", "org.testcontainers", "🐳 [postgres:14]", - "🐳 [testcontainers/ryuk:0.3.3]" + "🐳 [testcontainers/ryuk:0.3.3]", ) silenceOfTheLogs.foreach { log => @@ -96,7 +82,7 @@ object Fixture: appConfig, db, logger.scribeLogger, - timeCop + timeCop, ).routes latchedRoutes = HttpApp[IO] { case req => shutdownLatch.get.flatMap { deadSkunk => @@ -104,9 +90,9 @@ object Fixture: IO.pure( org.http4s .Response[IO]( - org.http4s.Status.InternalServerError + org.http4s.Status.InternalServerError, ) - .withEntity("Skunk is dead, stop sending requests!") + .withEntity("Skunk is dead, stop sending requests!"), ) else routes.run(req) } @@ -118,14 +104,14 @@ object Fixture: .map(_.baseUri) client <- BlazeClientBuilder[IO].resource.onFinalize( - shutdownLatch.set(true) + shutdownLatch.set(true), ) probe <- Probe.build( client, uri, appConfig, - logger + logger, ) yield probe end for @@ -138,13 +124,13 @@ object Fixture: HS256, _ => "jobby:token", _ => 5.minutes, - _ => "jobby:issuer" + _ => "jobby:issuer", ) val skunk = SkunkConfig( maxSessions = 10, strategy = Strategy.SearchPath, - debug = false + debug = false, ) import com.comcast.ip4s.* diff --git a/modules/backend/src/test/scala/integration/IntegrationSuite.scala b/modules/backend/src/test/scala/integration/IntegrationSuite.scala index 9d4cd2d..44e2789 100644 --- a/modules/backend/src/test/scala/integration/IntegrationSuite.scala +++ b/modules/backend/src/test/scala/integration/IntegrationSuite.scala @@ -3,11 +3,7 @@ package tests package integration import cats.effect.* -import cats.effect.std.* -import cats.syntax.all.* -import jobby.spec.* import org.typelevel.otel4s.trace.Tracer.Implicits.noop - import weaver.* object Resources extends GlobalResource: diff --git a/modules/backend/src/test/scala/specs/Companies.scala b/modules/backend/src/test/scala/specs/Companies.scala index bd637f0..2622767 100644 --- a/modules/backend/src/test/scala/specs/Companies.scala +++ b/modules/backend/src/test/scala/specs/Companies.scala @@ -2,7 +2,6 @@ package jobby package tests import jobby.spec.* -import cats.effect.IO trait CompaniesSuite: self: JobbySuite => @@ -17,7 +16,7 @@ trait CompaniesSuite: companyId <- api.companies .createCompany( authHeader, - attributes + attributes, ) .map(_.id) @@ -26,7 +25,7 @@ trait CompaniesSuite: attributes.name == retrieved.attributes.name, attributes.url == retrieved.attributes.url, attributes.description == retrieved.attributes.description, - userId == retrieved.owner_id + userId == retrieved.owner_id, ) end for } @@ -40,7 +39,7 @@ trait CompaniesSuite: company <- api.companies.createCompany( ownerAuth, - attributes + attributes, ) byRando <- api.companies.deleteCompany(rando, company.id).attempt @@ -59,7 +58,7 @@ trait CompaniesSuite: yield expect.all( afterDeletion == Left(CompanyNotFound()), jobsBeforeDeletion.jobs.nonEmpty, - jobsAfterDeletion.jobs.isEmpty + jobsAfterDeletion.jobs.isEmpty, ) end for } diff --git a/modules/backend/src/test/scala/specs/Health.scala b/modules/backend/src/test/scala/specs/Health.scala index d3c5014..71cf4f6 100644 --- a/modules/backend/src/test/scala/specs/Health.scala +++ b/modules/backend/src/test/scala/specs/Health.scala @@ -2,8 +2,6 @@ package jobby package tests import jobby.spec.* -import cats.effect.IO -import org.http4s.ResponseCookie trait HealthSuite: self: JobbySuite => diff --git a/modules/backend/src/test/scala/specs/Jobs.scala b/modules/backend/src/test/scala/specs/Jobs.scala index e21237a..91f1284 100644 --- a/modules/backend/src/test/scala/specs/Jobs.scala +++ b/modules/backend/src/test/scala/specs/Jobs.scala @@ -2,8 +2,6 @@ package jobby package tests import jobby.spec.* -import cats.effect.IO -import cats.syntax.all.* trait JobsSuite: self: JobbySuite => @@ -22,7 +20,7 @@ trait JobsSuite: api.jobs.createJob( authHeader, companyId, - attributes + attributes, ) byCompanyOwner <- create(companyOwner, companyId) @@ -32,7 +30,7 @@ trait JobsSuite: yield expect.all( byRando == Left(ForbiddenError()), wrongCompany == Left(ForbiddenError()), - withoutAuth == Left(UnauthorizedError()) + withoutAuth == Left(UnauthorizedError()), ) end for } @@ -51,7 +49,7 @@ trait JobsSuite: api.jobs.createJob( authHeader, companyId, - attributes + attributes, ) bumBook_Job1 <- createJob(bumBook_Owner, BumBook_Inc) @@ -74,7 +72,7 @@ trait JobsSuite: yield expect.all( nope == Left(ForbiddenError()), nope2 == Left(ForbiddenError()), - nope3 == Left(UnauthorizedError()) + nope3 == Left(UnauthorizedError()), ) end for } @@ -90,7 +88,7 @@ trait JobsSuite: createJob = api.jobs.createJob( companyOwner, companyId, - attributes + attributes, ) _ <- createJob.replicateA_(probe.config.misc.latestJobs * 2) @@ -98,7 +96,7 @@ trait JobsSuite: sorted = jobs.sortBy(_.added.value.epochSecond).reverse yield expect.all( jobs.length == config.misc.latestJobs, - sorted == jobs + sorted == jobs, ) end for } diff --git a/modules/backend/src/test/scala/specs/Users.scala b/modules/backend/src/test/scala/specs/Users.scala index 332c1ef..c5da528 100644 --- a/modules/backend/src/test/scala/specs/Users.scala +++ b/modules/backend/src/test/scala/specs/Users.scala @@ -1,9 +1,8 @@ package jobby package tests -import jobby.spec.* import cats.effect.IO -import org.http4s.ResponseCookie +import jobby.spec.* trait UsersSuite: self: JobbySuite => @@ -18,7 +17,7 @@ trait UsersSuite: refreshCookie <- IO .fromOption(resp.cookie)( - new Exception("Expected a refresh cookie ") + new Exception("Expected a refresh cookie "), ) .map(_.value) accessToken = resp.access_token.value @@ -47,7 +46,7 @@ trait UsersSuite: yield expect.all( wrongLogin.isLeft, wrongPass.isLeft, - everythingWrong.isLeft + everythingWrong.isLeft, ) end for } diff --git a/modules/backend/src/test/scala/specs/fragments.scala b/modules/backend/src/test/scala/specs/fragments.scala index 17d1e7c..ca6a040 100644 --- a/modules/backend/src/test/scala/specs/fragments.scala +++ b/modules/backend/src/test/scala/specs/fragments.scala @@ -19,7 +19,7 @@ class Fragments(probe: Probe): def createCompany( authHeader: AuthHeader, - attributes: Option[CompanyAttributes] = None + attributes: Option[CompanyAttributes] = None, ) = for generatedAttributes <- companyAttributes @@ -27,7 +27,7 @@ class Fragments(probe: Probe): companyId <- api.companies .createCompany( authHeader, - attributes.getOrElse(generatedAttributes) + attributes.getOrElse(generatedAttributes), ) .map(_.id) yield companyId @@ -40,7 +40,7 @@ class Fragments(probe: Probe): attributes = CompanyAttributes( companyName, companyDescription, - companyUrl + companyUrl, ) yield attributes diff --git a/modules/backend/src/test/scala/stub/Fixture.scala b/modules/backend/src/test/scala/stub/Fixture.scala index b3db613..f04fc31 100644 --- a/modules/backend/src/test/scala/stub/Fixture.scala +++ b/modules/backend/src/test/scala/stub/Fixture.scala @@ -6,15 +6,12 @@ import scala.concurrent.duration.* import cats.effect.IO import cats.effect.Resource -import cats.syntax.all.* - import com.comcast.ip4s.* import org.http4s.Uri import org.http4s.client.Client +import org.typelevel.otel4s.trace.Tracer import pdi.jwt.JwtAlgorithm.HS256 -import scribe.cats.* import skunk.util.Typer.Strategy -import org.typelevel.otel4s.trace.Tracer object Fixture: @@ -27,7 +24,7 @@ object Fixture: appConfig, db, logger.scribeLogger, - timeCop + timeCop, ).routes client = Client.fromHttpApp(routes) generator <- Resource.eval(Generator.create) @@ -36,7 +33,7 @@ object Fixture: client, Uri.unsafeFromString("http://localhost"), appConfig, - logger + logger, ) yield probe end for @@ -47,12 +44,12 @@ object Fixture: HS256, _ => "jobby:token", _ => 5.minutes, - _ => "jobby:issuer" + _ => "jobby:issuer", ) val skunk = SkunkConfig( maxSessions = 0, strategy = Strategy.BuiltinsOnly, - debug = false + debug = false, ) val http = HttpConfig(host"localhost", port"9914", Deployment.Local) val misc = MiscConfig(latestJobs = 20) diff --git a/modules/backend/src/test/scala/stub/InMemoryDB.scala b/modules/backend/src/test/scala/stub/InMemoryDB.scala index eaec141..5cd7bd4 100644 --- a/modules/backend/src/test/scala/stub/InMemoryDB.scala +++ b/modules/backend/src/test/scala/stub/InMemoryDB.scala @@ -2,21 +2,15 @@ package jobby package tests package stub -import java.util.UUID - import cats.effect.* -import cats.effect.std.* import cats.syntax.all.* - import jobby.database.operations.* import jobby.spec.* -import com.comcast.ip4s.* - case class InMemoryDB( state: Ref[IO, InMemoryDB.State], gen: Generator, - timecop: TimeCop + timecop: TimeCop, ) extends Database: private def opt[T](s: InMemoryDB.State => Option[T]): fs2.Stream[IO, T] = @@ -67,8 +61,8 @@ case class InMemoryDB( st.copy( companies = st.companies.filterNot(matches), jobs = st.jobs.filterNot( - _.companyId == companyId - ) // simulate cascade delete + _.companyId == companyId, + ), // simulate cascade delete ) -> Seq("ok") else st -> Seq.empty } @@ -91,9 +85,9 @@ case class InMemoryDB( Company( companyId, userId, - attributes - ) - ) + attributes, + ), + ), ) } .as(companyId) @@ -108,7 +102,7 @@ case class InMemoryDB( id = jobId, companyId = companyId, attributes = attributes, - added = ja + added = ja, ) state.update(st => st.copy(jobs = st.jobs.appended(job))).as(jobId) @@ -121,14 +115,14 @@ case class InMemoryDB( fs2.Stream .eval { state.get.map( - _.jobs.sortBy(_.added.value.epochSecond).reverse.take(20) + _.jobs.sortBy(_.added.value.epochSecond).reverse.take(20), ) } .flatMap(fs2.Stream.emits) case other => fs2.Stream.raiseError( - new Exception(s"Operation $other is not implemented in InMemoryDB") + new Exception(s"Operation $other is not implemented in InMemoryDB"), ) end InMemoryDB @@ -137,7 +131,7 @@ object InMemoryDB: case class State( jobs: Vector[Job] = Vector.empty, companies: Vector[Company] = Vector.empty, - users: Vector[(UserId, UserLogin, HashedPassword)] = Vector.empty + users: Vector[(UserId, UserLogin, HashedPassword)] = Vector.empty, ) def create = diff --git a/modules/backend/src/test/scala/stub/StubTests.scala b/modules/backend/src/test/scala/stub/StubTests.scala index bece6ee..56e7e0d 100644 --- a/modules/backend/src/test/scala/stub/StubTests.scala +++ b/modules/backend/src/test/scala/stub/StubTests.scala @@ -2,9 +2,8 @@ package jobby package tests package stub -import weaver.* - import org.typelevel.otel4s.trace.Tracer.Implicits.noop +import weaver.* abstract class StubSuite(global: GlobalRead) extends JobbySuite: override def sharedResource = Fixture.resource diff --git a/modules/backend/src/test/scala/unit/Validation.scala b/modules/backend/src/test/scala/unit/Validation.scala index 457f970..76a1a9b 100644 --- a/modules/backend/src/test/scala/unit/Validation.scala +++ b/modules/backend/src/test/scala/unit/Validation.scala @@ -2,17 +2,16 @@ package jobby package tests package unit -import weaver.* import jobby.spec.* - -import weaver.scalacheck.* import org.scalacheck.Gen +import weaver.* +import weaver.scalacheck.* object ValidationPropertyTests extends SimpleIOSuite with Checkers: override def checkConfig: CheckConfig = CheckConfig.default.copy( minimumSuccessful = 500, - initialSeed = Some(13378008L) + initialSeed = Some(13378008L), ) test("users: username") { @@ -24,7 +23,7 @@ object ValidationPropertyTests extends SimpleIOSuite with Checkers: expect( trimmed.length < 12 || trimmed.length > 50 - || trimmed.isEmpty + || trimmed.isEmpty, ) } } @@ -39,7 +38,7 @@ object ValidationPropertyTests extends SimpleIOSuite with Checkers: expect( trimmed.length < 12 || trimmed.length > 128 - || str.exists(_.isWhitespace) + || str.exists(_.isWhitespace), ) } } @@ -54,7 +53,7 @@ object ValidationPropertyTests extends SimpleIOSuite with Checkers: expect( trimmed.isEmpty || trimmed.length > 100 - || trimmed.length < 3 + || trimmed.length < 3, ) } } @@ -71,7 +70,7 @@ object ValidationPropertyTests extends SimpleIOSuite with Checkers: expect( trimmed.isEmpty || trimmed.length < 100 - || trimmed.length > 5000 + || trimmed.length > 5000, ) } } @@ -86,7 +85,7 @@ object ValidationPropertyTests extends SimpleIOSuite with Checkers: expect( trimmed.isEmpty || trimmed.length < 10 - || trimmed.length > 50 + || trimmed.length > 50, ) } } @@ -101,7 +100,7 @@ object ValidationPropertyTests extends SimpleIOSuite with Checkers: expect( trimmed.trim.isEmpty || trimmed.length < 100 - || trimmed.length > 5000 + || trimmed.length > 5000, ) } } @@ -115,7 +114,7 @@ object ValidationPropertyTests extends SimpleIOSuite with Checkers: yield SalaryRange( min = MinSalary(min), max = MaxSalary(max), - currency = currency + currency = currency, ) given cats.Show[SalaryRange] = cats.Show.fromToString @@ -128,7 +127,7 @@ object ValidationPropertyTests extends SimpleIOSuite with Checkers: expect( range.min.value > range.max.value || range.min.value <= 0 - || range.max.value <= 0 + || range.max.value <= 0, ) } } diff --git a/modules/frontend/src/main/scala/api.client.scala b/modules/frontend/src/main/scala/api.client.scala index 29f71c2..e82bdd7 100644 --- a/modules/frontend/src/main/scala/api.client.scala +++ b/modules/frontend/src/main/scala/api.client.scala @@ -1,26 +1,20 @@ package frontend -import java.util.UUID - import scala.concurrent.Future import cats.effect.IO import cats.effect.unsafe.implicits.global - +import com.raquo.airstream.core.EventStream as LaminarStream import jobby.spec.* - import org.http4s.* import org.http4s.dom.* import org.scalajs.dom import smithy4s.http4s.* -import cats.effect.std.Dispatcher - -import com.raquo.airstream.core.EventStream as LaminarStream class Api private ( val companies: CompaniesService[IO], val jobs: JobService[IO], - val users: UserService[IO] + val users: UserService[IO], ): import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.* def future[A](a: Api => IO[A]): Future[A] = @@ -31,7 +25,7 @@ class Api private ( end Api object Api: - def create(location: String = dom.window.location.origin.get) = + def create(location: String = dom.window.location.origin) = val uri = Uri.unsafeFromString(location) val client = FetchClientBuilder[IO].create @@ -40,21 +34,21 @@ object Api: SimpleRestJsonBuilder(CompaniesService) .client(client) .uri(uri) - .use + .make .fold(throw _, identity) val jobs = SimpleRestJsonBuilder(JobService) .client(client) .uri(uri) - .use + .make .fold(throw _, identity) val users = SimpleRestJsonBuilder(UserService) .client(client) .uri(uri) - .use + .make .fold(throw _, identity) Api(companies, jobs, users) diff --git a/modules/frontend/src/main/scala/app.scala b/modules/frontend/src/main/scala/app.scala index 39fb511..b534ddc 100644 --- a/modules/frontend/src/main/scala/app.scala +++ b/modules/frontend/src/main/scala/app.scala @@ -1,17 +1,9 @@ package frontend import com.raquo.laminar.api.L.* -import org.scalajs.dom import com.raquo.waypoint.Router -import scala.scalajs.js.Date -import cats.effect.IO - -import scala.concurrent.ExecutionContext.Implicits.global -import jobby.spec.Tokens -import jobby.spec.AccessToken -import jobby.spec.CompanyAttributes import com.raquo.waypoint.SplitRender -import com.raquo.waypoint.Router +import org.scalajs.dom enum AuthEvent: case Check, Reset @@ -20,7 +12,7 @@ enum AuthEvent: object Main: def renderPage(using - router: Router[Page] + router: Router[Page], )(using state: AppState, api: Api): Signal[HtmlElement] = SplitRender[Page, HtmlElement](router.currentPageSignal) .collectStatic(Page.Login)(pages.login) @@ -51,25 +43,25 @@ object Main: a(Styles.logo, "Jobby", navigateTo(Page.LatestJobs)), span( Styles.logoTagline, - "because you're worth it (better job that is)" + "because you're worth it (better job that is)", ), p( small( "This is not a real job site, it's a project from a ", a( href := "https://blog.indoorvivants.com/2022-06-10-smithy4s-fullstack-part-1", - "blog post series" - ) - ) - ) + "blog post series", + ), + ), + ), ), - userToolbar.node + userToolbar.node, ), div( Styles.contentContainer, child <-- renderPage, - tokenRefresh.loop - ) + tokenRefresh.loop, + ), ) renderOnDomContentLoaded( @@ -80,7 +72,7 @@ object Main: dom.document.querySelector("head").appendChild(sty.ref) app - } + }, ) end main end Main diff --git a/modules/frontend/src/main/scala/app.state.scala b/modules/frontend/src/main/scala/app.state.scala index d7aba04..bf9002c 100644 --- a/modules/frontend/src/main/scala/app.state.scala +++ b/modules/frontend/src/main/scala/app.state.scala @@ -1,12 +1,12 @@ package frontend -import com.raquo.laminar.api.L.* -import org.scalajs.dom -import jobby.spec.AuthHeader import scala.scalajs.js.Date -import com.raquo.airstream.state.StrictSignal -import com.raquo.airstream.core.Signal + import com.raquo.airstream.core.Observer +import com.raquo.airstream.core.Signal +import com.raquo.airstream.state.StrictSignal +import com.raquo.laminar.api.L.* +import jobby.spec.AuthHeader enum AuthState: case Unauthenticated @@ -14,7 +14,7 @@ enum AuthState: class AppState private ( _authToken: Var[Option[AuthState]], - val events: EventBus[AuthEvent] + val events: EventBus[AuthEvent], ): val $token: StrictSignal[Option[AuthState]] = _authToken.signal @@ -35,7 +35,7 @@ object AppState: def init: AppState = AppState( _authToken = Var(None), - events = EventBus[AuthEvent]() + events = EventBus[AuthEvent](), ) end AppState diff --git a/modules/frontend/src/main/scala/auth.refresh.scala b/modules/frontend/src/main/scala/auth.refresh.scala index a8e2cdf..c668161 100644 --- a/modules/frontend/src/main/scala/auth.refresh.scala +++ b/modules/frontend/src/main/scala/auth.refresh.scala @@ -1,25 +1,21 @@ package frontend +import scala.concurrent.duration.FiniteDuration import scala.scalajs.js.Date import cats.effect.IO -import cats.syntax.all.* - -import jobby.spec.AuthHeader -import jobby.spec.* - -import com.raquo.laminar.api.L.* -import scala.concurrent.duration.FiniteDuration import com.raquo.airstream.core.EventStream +import com.raquo.laminar.api.L.* +import jobby.spec.* class AuthRefresh(bus: EventBus[AuthEvent], period: FiniteDuration)(using state: AppState, - api: Api + api: Api, ): def loop = eventSources .withCurrentValueOf(state.$token) - .flatMap { + .flatMapSwitch { case (_, None) => refresh case (AuthEvent.Reset, _) => logout @@ -39,7 +35,7 @@ class AuthRefresh(bus: EventBus[AuthEvent], period: FiniteDuration)(using else EventStream.empty } --> state.tokenWriter - private def refresh = + private def refresh: EventStream[Option[AuthState]] = api .stream( _.users @@ -53,7 +49,7 @@ class AuthRefresh(bus: EventBus[AuthEvent], period: FiniteDuration)(using api.users .refresh(None, logout = Some(true)) .as(Some(AuthState.Unauthenticated)) - } + }, ) private def logout: EventStream[Some[AuthState]] = @@ -61,14 +57,14 @@ class AuthRefresh(bus: EventBus[AuthEvent], period: FiniteDuration)(using .stream( _.users .refresh(None, logout = Some(true)) - .as(Some(AuthState.Unauthenticated)) + .as(Some(AuthState.Unauthenticated)), ) private val eventSources: EventStream[AuthEvent] = EventStream .merge( bus.events, EventStream.periodic(period.toMillis.toInt).mapTo(AuthEvent.Check), - state.$token.changes.collect { case None => AuthEvent.Reset } + state.$token.changes.collect { case None => AuthEvent.Reset }, ) end AuthRefresh diff --git a/modules/frontend/src/main/scala/page.company.scala b/modules/frontend/src/main/scala/page.company.scala index affd355..efe8ae7 100644 --- a/modules/frontend/src/main/scala/page.company.scala +++ b/modules/frontend/src/main/scala/page.company.scala @@ -1,36 +1,32 @@ package frontend package pages -import views.* - import com.raquo.laminar.api.L.* -import jobby.spec.* -import scala.scalajs.js.Date -import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.waypoint.Router +import jobby.spec.* def company( - page: Signal[Page.CompanyPage] + page: Signal[Page.CompanyPage], )(using state: AppState, api: Api, router: Router[Page]) = val companyInfo = Var(Option.empty[Company]) val fetchCompanyInfo = - page.flatMap { case Page.CompanyPage(id) => + page.flatMapSwitch { case Page.CompanyPage(id) => api .stream( _.companies - .getCompany(id) + .getCompany(id), ) } --> companyInfo.someWriter div( fetchCompanyInfo, child.maybe <-- companyInfo.signal.map( - _.map(CompanyListing.apply(_)).map(_.node) + _.map(CompanyListing.apply(_)).map(_.node), ), div( Styles.latestJobs.container, - children <-- companyInfo.signal.flatMap { + children <-- companyInfo.signal.flatMapSwitch { case None => Signal.fromValue(Nil) case Some(company) => api @@ -42,12 +38,12 @@ def company( _.map( JobListing .apply(_, company.id, company.attributes.name) - .node - ) - ) + .node, + ), + ), ) .startWith(Nil) - } - ) + }, + ), ) end company diff --git a/modules/frontend/src/main/scala/page.create_company.scala b/modules/frontend/src/main/scala/page.create_company.scala index 2e90733..3ac83e9 100644 --- a/modules/frontend/src/main/scala/page.create_company.scala +++ b/modules/frontend/src/main/scala/page.create_company.scala @@ -1,12 +1,11 @@ package frontend package pages -import views.* +import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.laminar.api.L.* -import jobby.spec.* -import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.waypoint.Router +import jobby.spec.* def create_company(using state: AppState, api: Api, router: Router[Page]) = val error = Var(Option.empty[String]) @@ -20,9 +19,9 @@ def create_company(using state: AppState, api: Api, router: Router[Page]) = _.companies .createCompany( auth = h, - attributes = cc + attributes = cc, ) - .attempt + .attempt, ) .collect { case Left(ValidationError(msg)) => @@ -35,6 +34,6 @@ def create_company(using state: AppState, api: Api, router: Router[Page]) = val createCompany = CreateCompanyForm(companyHandler, error.signal) div( - createCompany.node + createCompany.node, ) end create_company diff --git a/modules/frontend/src/main/scala/page.create_job.scala b/modules/frontend/src/main/scala/page.create_job.scala index c52e59b..9d3d4ea 100644 --- a/modules/frontend/src/main/scala/page.create_job.scala +++ b/modules/frontend/src/main/scala/page.create_job.scala @@ -1,13 +1,11 @@ package frontend package pages -import views.* +import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.laminar.api.L.* -import jobby.spec.* -import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.waypoint.Router -import java.util.UUID +import jobby.spec.* def create_job(using state: AppState, api: Api, router: Router[Page]) = val error = Var(Option.empty[String]) @@ -22,9 +20,9 @@ def create_job(using state: AppState, api: Api, router: Router[Page]) = .createJob( auth = h, companyId = companyId, - attributes = cj.attributes + attributes = cj.attributes, ) - .attempt + .attempt, ) .collect { case Left(ValidationError(msg)) => @@ -44,20 +42,20 @@ def create_job(using state: AppState, api: Api, router: Router[Page]) = div( p( Styles.jobListing.sampleText, - "Here's what the listing will look like:" + "Here's what the listing will look like:", ), JobListing( Job( id = JobId(cid.value), companyId = cid, attributes = cj.attributes, - added = JobAdded(smithy4s.Timestamp.nowUTC()) + added = JobAdded(smithy4s.Timestamp.nowUTC()), ), cid, - CompanyName("some company") - ).node + CompanyName("some company"), + ).node, ) } - } + }, ) end create_job diff --git a/modules/frontend/src/main/scala/page.job.scala b/modules/frontend/src/main/scala/page.job.scala index a5c41c9..a9616e0 100644 --- a/modules/frontend/src/main/scala/page.job.scala +++ b/modules/frontend/src/main/scala/page.job.scala @@ -1,16 +1,11 @@ package frontend package pages -import views.* - import com.raquo.laminar.api.L.* -import jobby.spec.* -import scala.scalajs.js.Date -import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.waypoint.Router def job( - page: Signal[Page.JobPage] + page: Signal[Page.JobPage], )(using state: AppState, api: Api, router: Router[Page]) = div("yo") end job diff --git a/modules/frontend/src/main/scala/page.latest_jobs.scala b/modules/frontend/src/main/scala/page.latest_jobs.scala index 140f8bd..ab92090 100644 --- a/modules/frontend/src/main/scala/page.latest_jobs.scala +++ b/modules/frontend/src/main/scala/page.latest_jobs.scala @@ -1,13 +1,7 @@ package frontend package pages -import views.* - import com.raquo.laminar.api.L.* -import jobby.spec.* -import scala.scalajs.js.Date -import scala.concurrent.ExecutionContext.Implicits.global - import com.raquo.waypoint.Router import jobby.spec.GetCompaniesOutput @@ -28,11 +22,11 @@ def latest_jobs(using state: AppState, api: Api, router: Router[Page]) = mapping .get(job.companyId) .map(company => - JobListing(job, company.id, company.attributes.name).node + JobListing(job, company.id, company.attributes.name).node, ) } } - }) - ) + }), + ), ) end latest_jobs diff --git a/modules/frontend/src/main/scala/page.login.scala b/modules/frontend/src/main/scala/page.login.scala index b73a1d9..d0b8cc1 100644 --- a/modules/frontend/src/main/scala/page.login.scala +++ b/modules/frontend/src/main/scala/page.login.scala @@ -1,16 +1,16 @@ package frontend package pages -import views.* +import scala.concurrent.ExecutionContext.Implicits.global +import scala.scalajs.js.Date import com.raquo.laminar.api.L.* +import com.raquo.waypoint.Router import jobby.spec.AccessToken -import jobby.spec.Tokens import jobby.spec.AuthHeader -import scala.scalajs.js.Date -import scala.concurrent.ExecutionContext.Implicits.global +import jobby.spec.Tokens -import com.raquo.waypoint.Router +import views.* def login(using state: AppState, api: Api, router: Router[Page]) = val error = Var(Option.empty[String]) @@ -20,9 +20,9 @@ def login(using state: AppState, api: Api, router: Router[Page]) = _.users .login( login, - password + password, ) - .attempt + .attempt, ) .collect { case l @ Left(_) => @@ -34,9 +34,9 @@ def login(using state: AppState, api: Api, router: Router[Page]) = AuthState.Token( AuthHeader("Bearer " + tok), new Date, - expiresIn.value - ) - ) + expiresIn.value, + ), + ), ) redirectTo(Page.LatestJobs) diff --git a/modules/frontend/src/main/scala/page.logout.scala b/modules/frontend/src/main/scala/page.logout.scala index a0df7b9..bcc4c3a 100644 --- a/modules/frontend/src/main/scala/page.logout.scala +++ b/modules/frontend/src/main/scala/page.logout.scala @@ -1,14 +1,7 @@ package frontend package pages -import views.* - import com.raquo.laminar.api.L.* -import jobby.spec.AccessToken -import jobby.spec.Tokens -import jobby.spec.AuthHeader -import scala.scalajs.js.Date -import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.waypoint.Router def logout(using state: AppState, api: Api, router: Router[Page]) = @@ -19,6 +12,6 @@ def logout(using state: AppState, api: Api, router: Router[Page]) = state.events.emit(AuthEvent.Reset) redirectTo(Page.LatestJobs) None - } + }, ) end logout diff --git a/modules/frontend/src/main/scala/page.profile.scala b/modules/frontend/src/main/scala/page.profile.scala index 4c531fd..fcc52d3 100644 --- a/modules/frontend/src/main/scala/page.profile.scala +++ b/modules/frontend/src/main/scala/page.profile.scala @@ -1,29 +1,24 @@ package frontend package pages -import views.* - -import com.raquo.laminar.api.L.* -import jobby.spec.AccessToken -import jobby.spec.Tokens -import jobby.spec.AuthHeader -import scala.scalajs.js.Date import scala.concurrent.ExecutionContext.Implicits.global -import com.raquo.waypoint.Router import cats.syntax.all.* +import com.raquo.airstream.core.EventStream +import com.raquo.laminar.api.L.* +import com.raquo.waypoint.Router +import jobby.spec.AuthHeader import jobby.spec.Company import jobby.spec.Job -import com.raquo.airstream.core.EventStream def profile(using state: AppState, api: Api, router: Router[Page]) = - val myCompanies = state.$authHeader.flatMap { + val myCompanies = state.$authHeader.flatMapSwitch { case None => Signal.fromValue(List.empty) case Some(tok) => val result: EventStream[List[(Company, List[Job])]] = api.stream { a => a.companies.myCompanies(tok).map(_.companies).flatMap { companies => companies.traverse(company => - a.jobs.listJobs(company.id).map(_.jobs).map(company -> _) + a.jobs.listJobs(company.id).map(_.jobs).map(company -> _), ) } } @@ -38,7 +33,7 @@ def profile(using state: AppState, api: Api, router: Router[Page]) = onDelete = company => api.future(_.companies.deleteCompany(tok, company.id)).map { _ => deletedCompanies.update(_ + company.id) - } + }, ).node inline def renderJob(job: Job, company: Company) = @@ -50,7 +45,7 @@ def profile(using state: AppState, api: Api, router: Router[Page]) = onDelete = job => api.future(_.jobs.deleteJob(tok, job.id)).map { _ => deleted.update(_ + job.id) - } + }, ).node result @@ -67,16 +62,16 @@ def profile(using state: AppState, api: Api, router: Router[Page]) = jobs.filterNot(j => isDeleted(j.id)).map { job => renderJob(job, company) } - } - ) + }, + ), ) - } + }, ) } } .startWith(List(i("You haven't created any companies yet..."))) } div( - children <-- myCompanies + children <-- myCompanies, ) end profile diff --git a/modules/frontend/src/main/scala/page.register.scala b/modules/frontend/src/main/scala/page.register.scala index 8f99e36..de60ab6 100644 --- a/modules/frontend/src/main/scala/page.register.scala +++ b/modules/frontend/src/main/scala/page.register.scala @@ -1,13 +1,13 @@ package frontend package pages -import views.* +import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.laminar.api.L.* -import jobby.spec.* -import scala.scalajs.js.Date -import scala.concurrent.ExecutionContext.Implicits.global import com.raquo.waypoint.Router +import jobby.spec.* + +import views.* def register(using state: AppState, api: Api, router: Router[Page]) = val error = Var(Option.empty[String]) @@ -17,9 +17,9 @@ def register(using state: AppState, api: Api, router: Router[Page]) = _.users .register( login, - password + password, ) - .attempt + .attempt, ) .collect { case Left(ValidationError(msg)) => @@ -34,7 +34,7 @@ def register(using state: AppState, api: Api, router: Router[Page]) = div( guestOnly, h1(Styles.header, "Register"), - form + form, ) end register diff --git a/modules/frontend/src/main/scala/routes.scala b/modules/frontend/src/main/scala/routes.scala index 462eace..9ffc7b8 100644 --- a/modules/frontend/src/main/scala/routes.scala +++ b/modules/frontend/src/main/scala/routes.scala @@ -1,15 +1,16 @@ package frontend -import jobby.spec.* +import java.util.UUID + +import scala.scalajs.js.JSON + import com.raquo.laminar.api.L import com.raquo.laminar.api.L.* import com.raquo.waypoint.* -import java.util.UUID - -import io.circe.{*, given} +import io.circe.* import io.circe.syntax.* +import jobby.spec.* import smithy4s.Newtype -import scala.scalajs.js.JSON def codec[A: Decoder: Encoder](nt: Newtype[A]): Codec[nt.Type] = val decT = summon[Decoder[A]].map(nt.apply) @@ -47,42 +48,42 @@ object Page: val companyPageRoute = Route( encode = (stp: CompanyPage) => stp.id.toString, decode = (arg: String) => CompanyPage(CompanyId(UUID.fromString(arg))), - pattern = root / "company" / segment[String] / endOfSegments + pattern = root / "company" / segment[String] / endOfSegments, ) val jobPageRoute = Route( encode = (stp: JobPage) => stp.id.toString, decode = (arg: String) => JobPage(JobId(UUID.fromString(arg))), - pattern = root / "job" / segment[String] / endOfSegments + pattern = root / "job" / segment[String] / endOfSegments, ) - val router = new Router[Page]( - routes = List( - mainRoute, - profileRoute, - loginRoute, - registerRoute, - companyPageRoute, - jobPageRoute, - createCompanyRoute, - createJobRoute, - logoutRoute - ), - getPageTitle = { - case LatestJobs => "Jobby: latest" - case Login => "Jobby: login" - case Register => "Jobby: register" - case CreateCompany => "Jobby: create company" - case CreateJob => "Jobby: create vacancy" - case _ => "Jobby" - }, - serializePage = pg => pg.asJson.noSpaces, - deserializePage = str => - io.circe.scalajs.decodeJs[Page](JSON.parse(str)).fold(throw _, identity) - )( - popStateEvents = windowEvents(_.onPopState), - owner = L.unsafeWindowOwner - ) + object router + extends Router[Page]( + routes = List( + mainRoute, + profileRoute, + loginRoute, + registerRoute, + companyPageRoute, + jobPageRoute, + createCompanyRoute, + createJobRoute, + logoutRoute, + ), + getPageTitle = { + case LatestJobs => "Jobby: latest" + case Login => "Jobby: login" + case Register => "Jobby: register" + case CreateCompany => "Jobby: create company" + case CreateJob => "Jobby: create vacancy" + case _ => "Jobby" + }, + serializePage = pg => pg.asJson.noSpaces, + deserializePage = str => + io.circe.scalajs + .decodeJs[Page](JSON.parse(str)) + .fold(throw _, identity), + ) end Page def navigateTo(page: Page)(using router: Router[Page]): Binder[HtmlElement] = @@ -95,7 +96,7 @@ def navigateTo(page: Page)(using router: Router[Page]): Binder[HtmlElement] = (onClick .filter(ev => - !(isLinkElement && (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)) + !(isLinkElement && (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey)), ) .preventDefault --> (_ => redirectTo(page))).bind(el) diff --git a/modules/frontend/src/main/scala/styles.scala b/modules/frontend/src/main/scala/styles.scala index b4631a2..748641d 100644 --- a/modules/frontend/src/main/scala/styles.scala +++ b/modules/frontend/src/main/scala/styles.scala @@ -18,7 +18,7 @@ object Styles extends StyleSheet.Inline: backgroundAttachment := "fixed", backgroundRepeat.noRepeat, backgroundSize := "cover", - fontFamily :=! "'Wotfard',Futura,-apple-system,sans-serif" + fontFamily :=! "'Wotfard',Futura,-apple-system,sans-serif", ) "html" - ( height := 100.%% @@ -29,7 +29,7 @@ object Styles extends StyleSheet.Inline: val container = style( display.flex, flexDirection.column, - gap := 15.px + gap := 15.px, ) object jobListing: @@ -37,11 +37,11 @@ object Styles extends StyleSheet.Inline: val container = style( backgroundColor := lightGray, padding := 10.px, - borderRadius := 8.px + borderRadius := 8.px, ) val deleteLink = style( color.darkred, - fontWeight.bold + fontWeight.bold, ) val title = style( fontSize := 1.3.rem, @@ -50,14 +50,14 @@ object Styles extends StyleSheet.Inline: fontStyle.italic, textDecorationLine.none, &.visited(color.black), - color.black + color.black, ) val salaryRange = style( - fontSize := 2.rem + fontSize := 2.rem, ) val description = style( fontSize := 1.5.rem, - whiteSpace := "break-spaces" + whiteSpace := "break-spaces", ) private val curButtonMixin = mixin( @@ -67,14 +67,14 @@ object Styles extends StyleSheet.Inline: borderWidth := 1.px, borderColor := grey, borderStyle.solid, - borderRadius := 3.px + borderRadius := 3.px, ) val currencyButton = styleF.bool { case true => mixin( curButtonMixin, color.white, - backgroundColor.black + backgroundColor.black, ) case false => mixin(curButtonMixin, backgroundColor.white) @@ -84,12 +84,12 @@ object Styles extends StyleSheet.Inline: object company: val deleteLink = style( color.red, - fontWeight.bold + fontWeight.bold, ) val container = style( display.flex, flexDirection.column, - gap := 15.px + gap := 15.px, ) val name = style(color.white, fontSize := 2.rem) @@ -97,16 +97,16 @@ object Styles extends StyleSheet.Inline: color.white, fontSize := 1.3.rem, margin := 5.px, - whiteSpace.preWrap + whiteSpace.preWrap, ) val url = style( color.white, - fontSize := 1.5.rem + fontSize := 1.5.rem, ) val internalUrl = style( textDecorationLine.none, &.visited(color.white), - &.hover(textDecorationLine.underline) + &.hover(textDecorationLine.underline), ) val nameUrl = style( @@ -114,14 +114,14 @@ object Styles extends StyleSheet.Inline: fontStyle.italic, textDecorationLine.none, &.visited(color.black), - &.hover(textDecorationLine.underline) + &.hover(textDecorationLine.underline), ) end company val container = style( padding := 10.px, margin := auto, - maxWidth := 1024.px + maxWidth := 1024.px, ) val contentContainer = style( @@ -131,19 +131,19 @@ object Styles extends StyleSheet.Inline: borderWidth := 2.px, margin := auto, maxWidth := 1024.px, - backgroundColor := darkBlue + backgroundColor := darkBlue, ) val headerContainer = style( display.flex, flexDirection.row, justifyContent.spaceBetween, - alignItems.center + alignItems.center, ) val textInput = style( fontSize := 1.5.rem, - padding := 5.px + padding := 5.px, ) val error = style( @@ -152,11 +152,11 @@ object Styles extends StyleSheet.Inline: fontWeight.bold, color := maroon, backgroundColor.white, - margin := 5.px + margin := 5.px, ) val contentTitle = style( - color.white + color.white, ) val logo = @@ -165,7 +165,7 @@ object Styles extends StyleSheet.Inline: fontSize := 3.rem, textDecorationLine.none, &.visited(color.black), - &.hover(textDecorationLine.underline) + &.hover(textDecorationLine.underline), ) val logoTagline = style(marginBottom := 10.px, display.block) @@ -174,7 +174,7 @@ object Styles extends StyleSheet.Inline: display.flex, alignItems.end, justifyContent.right, - gap(10.px) + gap(10.px), ) val button = style( padding := 4.px, @@ -184,7 +184,7 @@ object Styles extends StyleSheet.Inline: borderBottomWidth := 4.px, borderBottomColor := black, backgroundColor := white, - cursor.pointer + cursor.pointer, ) end userToolbar @@ -195,14 +195,14 @@ object Styles extends StyleSheet.Inline: display.flex, width := 100.%%, flexDirection.column, - gap := 15.px + gap := 15.px, ) val inputGroup = style( display.flex, width := 100.%%, flexDirection.row, alignContent.stretch, - gap := 15.px + gap := 15.px, ) val inputLabel = style( @@ -211,19 +211,19 @@ object Styles extends StyleSheet.Inline: width := 100.%%, fontSize := 2.rem, color.white, - textAlign.right + textAlign.right, ) val inputField = style( flexGrow := "3", padding := 4.px, fontSize := 1.2.rem, width := 100.%%, - backgroundColor.white + backgroundColor.white, ) val submit = style( padding := 5.px, - fontSize := 2.rem + fontSize := 2.rem, ) end form diff --git a/modules/frontend/src/main/scala/views.company.scala b/modules/frontend/src/main/scala/views.company.scala index cc96030..3937ea5 100644 --- a/modules/frontend/src/main/scala/views.company.scala +++ b/modules/frontend/src/main/scala/views.company.scala @@ -10,7 +10,7 @@ object CompanyListing: def apply( company: jobby.spec.Company, allowDelete: Boolean = false, - onDelete: Company => Unit = _ => () + onDelete: Company => Unit = _ => (), )(using Router[Page]): CompanyListing = import company.attributes.* @@ -26,12 +26,12 @@ object CompanyListing: val sure = org.scalajs.dom.window.confirm( s"Are you sure you want to delete ${company.attributes.name}?\n" + - "Note: the company will still exist in the physical world" + "Note: the company will still exist in the physical world", ) if sure then onDelete(company) - } - ) + }, + ), ) } @@ -43,21 +43,21 @@ object CompanyListing: navigateTo(Page.CompanyPage(company.id)), idAttr := "company-profile-name", name.value, - Styles.company.internalUrl + Styles.company.internalUrl, ), - span(" ", deleteLink) + span(" ", deleteLink), ), a( url.value, idAttr := "company-profile-url", href := url.value, - Styles.company.url + Styles.company.url, ), pre( description.value, idAttr := "company-profile-description", - Styles.company.description - ) + Styles.company.description, + ), ) CompanyListing(node) diff --git a/modules/frontend/src/main/scala/views.create_company.scala b/modules/frontend/src/main/scala/views.create_company.scala index ebc8e4a..04583e4 100644 --- a/modules/frontend/src/main/scala/views.create_company.scala +++ b/modules/frontend/src/main/scala/views.create_company.scala @@ -5,7 +5,7 @@ import jobby.spec.* case class CreateCompanyForm private ( node: Node, - stream: Signal[CompanyAttributes] + stream: Signal[CompanyAttributes], ) object CreateCompanyForm: @@ -14,15 +14,15 @@ object CreateCompanyForm: CompanyAttributes( name = CompanyName(""), description = CompanyDescription(""), - url = CompanyUrl("") - ) + url = CompanyUrl(""), + ), ) val nameWriter = stateVar.updater[String]((s, n) => s.copy(name = CompanyName(n))) val descriptionWriter = stateVar.updater[String]((s, n) => - s.copy(description = CompanyDescription(n)) + s.copy(description = CompanyDescription(n)), ) val urlWriter = @@ -40,9 +40,9 @@ object CreateCompanyForm: idAttr := "input-company-name", controlled( value <-- stateVar.signal.map(_.name.value), - onInput.mapToValue --> nameWriter - ) - ) + onInput.mapToValue --> nameWriter, + ), + ), ), inputGroup( "description", @@ -51,9 +51,9 @@ object CreateCompanyForm: rows := 15, controlled( value <-- stateVar.signal.map(_.description.value), - onInput.mapToValue --> descriptionWriter - ) - ) + onInput.mapToValue --> descriptionWriter, + ), + ), ), inputGroup( "url", @@ -61,17 +61,17 @@ object CreateCompanyForm: idAttr := "input-company-url", controlled( value <-- stateVar.signal.map(_.url.value), - onInput.mapToValue --> urlWriter - ) - ) + onInput.mapToValue --> urlWriter, + ), + ), ), button( tpe := "submit", idAttr := "input-company-submit", "Create", - Styles.form.submit - ) - ) + Styles.form.submit, + ), + ), ) new CreateCompanyForm(node, stateVar.signal) diff --git a/modules/frontend/src/main/scala/views.create_job_listing.scala b/modules/frontend/src/main/scala/views.create_job_listing.scala index 2b74fc2..ed405ba 100644 --- a/modules/frontend/src/main/scala/views.create_job_listing.scala +++ b/modules/frontend/src/main/scala/views.create_job_listing.scala @@ -1,26 +1,26 @@ package frontend -import com.raquo.laminar.api.L.* +import java.util.UUID +import com.raquo.laminar.api.L.* import jobby.spec.* -import java.util.UUID -import smithy4s.Newtype import monocle.Lens +import smithy4s.Newtype case class CreateJob( companyId: Option[CompanyId] = None, - attributes: JobAttributes + attributes: JobAttributes, ) case class CreateJobListingForm private ( node: Node, - stream: Signal[CreateJob] + stream: Signal[CreateJob], ) object CreateJobListingForm: def apply(submit: Observer[CreateJob], error: Signal[Option[String]])(using api: Api, - appState: AppState + appState: AppState, ) = val stateVar = Var( CreateJob( @@ -32,10 +32,10 @@ object CreateJobListingForm: range = SalaryRange( min = MinSalary(30000), max = MaxSalary(100000), - currency = Currency.GBP - ) - ) - ) + currency = Currency.GBP, + ), + ), + ), ) import monocle.syntax.all.* @@ -54,11 +54,11 @@ object CreateJobListingForm: def controlledNT( nt: Newtype[String], - f: CreateJob => Lens[CreateJob, nt.Type] + f: CreateJob => Lens[CreateJob, nt.Type], ) = controlled( value <-- stateVar.signal.map(cj => f(cj).get(cj).value), - onInput.mapToValue --> writerNT(nt, f) + onInput.mapToValue --> writerNT(nt, f), ) val currencyToggles = Currency.values.map { c => @@ -71,19 +71,20 @@ object CreateJobListingForm: .map(_.attributes.range.currency == c) .map(Styles.jobListing.currencyButton) .map(_.className.value), - onClick.mapTo(c) --> writer(_.focus(_.attributes.range.currency).optic) + onClick.mapTo(c) --> writer(_.focus(_.attributes.range.currency).optic), ) } - val myCompanies = appState.$authHeader.flatMap { - case None => Signal.fromValue(List.empty) - case Some(tok) => - api - .stream(_.companies.myCompanies(tok)) - .map(_.companies) - .map(_.map(company => company.attributes.name.value -> company.id)) - .startWith(List.empty) - } + val myCompanies = + appState.$authHeader.flatMapSwitch { + case None => Signal.fromValue(List.empty) + case Some(tok) => + api + .stream(_.companies.myCompanies(tok)) + .map(_.companies) + .map(_.map(company => company.attributes.name.value -> company.id)) + .startWith(List.empty) + } val companySelector = select( @@ -93,7 +94,7 @@ object CreateJobListingForm: onChange.mapToValue .map(UUID.fromString) .map(CompanyId.apply) - .map(Option.apply) --> writer(_.focus(_.companyId).optic) + .map(Option.apply) --> writer(_.focus(_.companyId).optic), ) val node = div( @@ -102,7 +103,7 @@ object CreateJobListingForm: Styles.form.container, onSubmit.preventDefault.mapTo(stateVar.now()) --> submit, myCompanies.map( - _.headOption.map(_._2) + _.headOption.map(_._2), ) --> writer(_.focus(_.companyId).optic), inputGroup("company", companySelector), inputGroup( @@ -110,18 +111,18 @@ object CreateJobListingForm: input( controlledNT( JobTitle, - _.focus(_.attributes.title).optic - ) - ) + _.focus(_.attributes.title).optic, + ), + ), ), inputGroup( "url", input( controlledNT( JobUrl, - _.focus(_.attributes.url).optic - ) - ) + _.focus(_.attributes.url).optic, + ), + ), ), inputGroup( "description", @@ -129,9 +130,9 @@ object CreateJobListingForm: rows := 5, controlledNT( JobDescription, - _.focus(_.attributes.description).optic - ) - ) + _.focus(_.attributes.description).optic, + ), + ), ), inputGroup( "minimum salary", @@ -141,24 +142,24 @@ object CreateJobListingForm: tpe := "range", minAttr := "30000", maxAttr <-- stateVar.signal.map( - _.attributes.range.max.value.toString + _.attributes.range.max.value.toString, ), value := "60000", stepAttr := "1000", onInput.mapToValue.map(_.toInt) --> writerNT( MinSalary, - _.focus(_.attributes.range.min).optic - ) + _.focus(_.attributes.range.min).optic, + ), ), p( textAlign := "center", child.text <-- stateVar.signal .map( - _.attributes.range.min.value + _.attributes.range.min.value, ) - .map(format) - ) - ) + .map(format), + ), + ), ), inputGroup( "maximum salary", @@ -167,33 +168,33 @@ object CreateJobListingForm: Styles.form.inputField, tpe := "range", minAttr <-- stateVar.signal.map( - _.attributes.range.min.value.toString + _.attributes.range.min.value.toString, ), maxAttr := "250000", value := "100000", stepAttr := "1000", onInput.mapToValue.map(_.toInt) --> writerNT( MaxSalary, - _.focus(_.attributes.range.max).optic - ) + _.focus(_.attributes.range.max).optic, + ), ), p( textAlign := "center", child.text <-- stateVar.signal .map( - _.attributes.range.max.value + _.attributes.range.max.value, ) - .map(format) - ) - ) + .map(format), + ), + ), ), inputGroup("", p(textAlign := "center", currencyToggles)), button( Styles.form.submit, tpe := "submit", - "Add" - ) - ) + "Add", + ), + ), ) new CreateJobListingForm(node, stateVar.signal) diff --git a/modules/frontend/src/main/scala/views.credentials_form.scala b/modules/frontend/src/main/scala/views.credentials_form.scala index 40f674e..bc98c4a 100644 --- a/modules/frontend/src/main/scala/views.credentials_form.scala +++ b/modules/frontend/src/main/scala/views.credentials_form.scala @@ -6,18 +6,18 @@ import jobby.spec.* case class Credentials( login: UserLogin, - password: UserPassword + password: UserPassword, ) case class CredentialsForm private ( - node: HtmlElement + node: HtmlElement, ) object CredentialsForm: def apply( submitButton: String, submit: Observer[Credentials], - error: Signal[Option[String]] + error: Signal[Option[String]], ) = val credentials = Var(Credentials(UserLogin(""), UserPassword(""))) @@ -39,8 +39,8 @@ object CredentialsForm: Styles.textInput, idAttr := "credentials-login", tpe := "text", - onInput.mapToValue.map(UserLogin.apply) --> loginWriter - ) + onInput.mapToValue.map(UserLogin.apply) --> loginWriter, + ), ), inputGroup( "password", @@ -48,16 +48,16 @@ object CredentialsForm: Styles.textInput, idAttr := "credentials-password", tpe := "password", - onInput.mapToValue.map(UserPassword.apply) --> passwordWriter - ) + onInput.mapToValue.map(UserPassword.apply) --> passwordWriter, + ), ), button( Styles.form.submit, idAttr := "credentials-submit", submitButton, - tpe := "submit" - ) - ) + tpe := "submit", + ), + ), ) new CredentialsForm(node) diff --git a/modules/frontend/src/main/scala/views.forms.scala b/modules/frontend/src/main/scala/views.forms.scala index a1a97a6..d563fdc 100644 --- a/modules/frontend/src/main/scala/views.forms.scala +++ b/modules/frontend/src/main/scala/views.forms.scala @@ -6,5 +6,5 @@ def inputGroup(name: String, el: HtmlElement) = div( Styles.form.inputGroup, label(name, Styles.form.inputLabel), - el.amend(Styles.form.inputField) + el.amend(Styles.form.inputField), ) diff --git a/modules/frontend/src/main/scala/views.job_listing.scala b/modules/frontend/src/main/scala/views.job_listing.scala index 8a92c81..5dfdfc3 100644 --- a/modules/frontend/src/main/scala/views.job_listing.scala +++ b/modules/frontend/src/main/scala/views.job_listing.scala @@ -1,10 +1,10 @@ package frontend -import com.raquo.laminar.api.L.* +import java.text.DecimalFormat -import jobby.spec.* +import com.raquo.laminar.api.L.* import com.raquo.waypoint.Router -import java.text.DecimalFormat +import jobby.spec.* case class JobListing private (node: Node) @@ -24,7 +24,7 @@ object JobListing: companyId: CompanyId, companyName: CompanyName, allowDelete: Boolean = false, - onDelete: Job => Unit = _ => () + onDelete: Job => Unit = _ => (), )(using Router[Page]): JobListing = import job.attributes.* @@ -41,12 +41,12 @@ object JobListing: onClick.preventDefault --> { _ => val sure = org.scalajs.dom.window.confirm( - s"Are you sure you want to delete ${job.attributes.title}?" + s"Are you sure you want to delete ${job.attributes.title}?", ) if sure then onDelete(job) - } - ) + }, + ), ) } @@ -54,19 +54,19 @@ object JobListing: Styles.jobListing.container, h3( a(href := url.value, title.value, Styles.jobListing.title), - deleteLink + deleteLink, ), "at ", a( Styles.company.nameUrl, navigateTo(Page.CompanyPage(companyId)), - companyName.value + companyName.value, ), p(Styles.jobListing.description, code(description.value)), p( Styles.jobListing.salaryRange, - s"$cur${format(range.min.value)} - $cur${format(range.max.value)}" - ) + s"$cur${format(range.min.value)} - $cur${format(range.max.value)}", + ), ) JobListing(node) diff --git a/modules/frontend/src/main/scala/views.user_toolbar.scala b/modules/frontend/src/main/scala/views.user_toolbar.scala index 601debd..231e7da 100644 --- a/modules/frontend/src/main/scala/views.user_toolbar.scala +++ b/modules/frontend/src/main/scala/views.user_toolbar.scala @@ -3,22 +3,20 @@ package frontend import com.raquo.laminar.api.L.* import com.raquo.waypoint.Router -import jobby.spec.* - case class UserToolbar private (node: Node) object UserToolbar: def apply(using api: Api, state: AppState, - router: Router[Page] + router: Router[Page], ): UserToolbar = val logout = button( Styles.userToolbar.button, cls := "nav-link", "Logout", - navigateTo(Page.Logout) + navigateTo(Page.Logout), ) val register = @@ -26,7 +24,7 @@ object UserToolbar: Styles.userToolbar.button, cls := "nav-link", "Register", - navigateTo(Page.Register) + navigateTo(Page.Register), ) val login = @@ -34,7 +32,7 @@ object UserToolbar: Styles.userToolbar.button, cls := "nav-link", "Login", - navigateTo(Page.Login) + navigateTo(Page.Login), ) val addJob = @@ -42,7 +40,7 @@ object UserToolbar: Styles.userToolbar.button, cls := "nav-link", "Add a job", - navigateTo(Page.CreateJob) + navigateTo(Page.CreateJob), ) val profile = @@ -50,7 +48,7 @@ object UserToolbar: Styles.userToolbar.button, cls := "nav-link", "Profile", - navigateTo(Page.Profile) + navigateTo(Page.Profile), ) val addCompany = @@ -58,7 +56,7 @@ object UserToolbar: Styles.userToolbar.button, cls := "nav-link", "Add a company", - navigateTo(Page.CreateCompany) + navigateTo(Page.CreateCompany), ) val buttons = state.$token.map { @@ -70,7 +68,7 @@ object UserToolbar: val node = div( Styles.userToolbar.container, - children <-- buttons + children <-- buttons, ) UserToolbar(node) diff --git a/modules/shared/src/main/scala/validation.jobs.scala b/modules/shared/src/main/scala/validation.jobs.scala index 3e17942..a31b6d5 100644 --- a/modules/shared/src/main/scala/validation.jobs.scala +++ b/modules/shared/src/main/scala/validation.jobs.scala @@ -18,7 +18,7 @@ def validateJobDescription(login: JobDescription) = if str.length == 0 then err("Description cannot be empty") else if str.length < minLength || str.length > maxLength then err( - s"Description cannot be shorter than $minLength or longer than $maxLength characters" + s"Description cannot be shorter than $minLength or longer than $maxLength characters", ) else ok end validateJobDescription diff --git a/project/BuildStyle.scala b/project/BuildStyle.scala index 6d81c42..e761896 100644 --- a/project/BuildStyle.scala +++ b/project/BuildStyle.scala @@ -2,7 +2,7 @@ import sbt.VirtualAxis sealed abstract class BuildStyle( val idSuffix: String, - val directorySuffix: String + val directorySuffix: String, ) extends VirtualAxis.WeakAxis with Product with Serializable diff --git a/project/plugins.sbt b/project/plugins.sbt index 8a92cf4..edf1174 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,13 +1,22 @@ addSbtPlugin( "com.disneystreaming.smithy4s" % "smithy4s-sbt-codegen" % sys.env - .getOrElse("SMITHY_VERSION", "0.17.4") + .getOrElse("SMITHY_VERSION", "0.18.27"), ) -addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") -addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") -addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.1") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("com.armanbilge" % "sbt-bundlemon" % "0.1.4") + +addSbtPlugin("io.spray" % "sbt-revolver" % "0.10.0") + +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") + +addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.1") + +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") + +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") + +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") + +addSbtPlugin("com.armanbilge" % "sbt-bundlemon" % "0.1.4") + +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always