From 4f7fb8d865ff14a8563ff7528655bf91a95fd643 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 9 Jun 2024 10:17:13 +0700 Subject: [PATCH 1/6] Use types from types module in domain And rename Natural to PositiveInt --- build.sbt | 2 +- modules/api/src/main/scala/providers.scala | 12 ++++----- modules/api/src/main/smithy/_global.smithy | 4 +-- .../src/main/scala/service.federation.scala | 10 +++---- .../src/main/scala/service.player.scala | 4 +-- modules/db/src/main/scala/Db.scala | 21 ++++++++++++++- modules/domain/src/main/scala/Models.scala | 26 +++++++++---------- modules/types/src/main/scala/Models.scala | 6 +++++ modules/types/src/main/scala/Natural.scala | 13 ---------- .../types/src/main/scala/PositiveInt.scala | 19 ++++++++++++++ 10 files changed, 73 insertions(+), 44 deletions(-) create mode 100644 modules/types/src/main/scala/Models.scala delete mode 100644 modules/types/src/main/scala/Natural.scala create mode 100644 modules/types/src/main/scala/PositiveInt.scala diff --git a/build.sbt b/build.sbt index 9e67bc9..54d6d42 100644 --- a/build.sbt +++ b/build.sbt @@ -57,7 +57,7 @@ lazy val domain = (project in file("modules/domain")) .settings( name := "domain", commonSettings - ) + ).dependsOn(types) lazy val db = (project in file("modules/db")) .settings( diff --git a/modules/api/src/main/scala/providers.scala b/modules/api/src/main/scala/providers.scala index fafea66..e960e97 100644 --- a/modules/api/src/main/scala/providers.scala +++ b/modules/api/src/main/scala/providers.scala @@ -6,14 +6,14 @@ import fide.types.* import smithy4s.* object providers: - given RefinementProvider[PageFormat, String, Natural] = - Refinement.drivenBy(Natural.fromString, _.toString) + given RefinementProvider[PageFormat, String, PositiveInt] = + Refinement.drivenBy(PositiveInt.fromString, _.toString) - given RefinementProvider.Simple[smithy.api.Range, Natural] = - RefinementProvider.rangeConstraint(x => x: Int) + given RefinementProvider.Simple[smithy.api.Range, PositiveInt] = + RefinementProvider.rangeConstraint(_.value.toInt) - given RefinementProvider[PageSizeFormat, Int, Natural] = - Refinement.drivenBy(Natural.either, identity) + given RefinementProvider[PageSizeFormat, Int, PositiveInt] = + Refinement.drivenBy(PositiveInt.either, _.value) given [A]: RefinementProvider[NonEmptySetFormat, Set[A], NonEmptySet[A]] = Refinement.drivenBy[NonEmptySetFormat]( diff --git a/modules/api/src/main/smithy/_global.smithy b/modules/api/src/main/smithy/_global.smithy index fb528de..0a647b1 100644 --- a/modules/api/src/main/smithy/_global.smithy +++ b/modules/api/src/main/smithy/_global.smithy @@ -34,14 +34,14 @@ integer Rating @trait(selector: "string") @refinement( - targetType: "fide.types.Natural" + targetType: "fide.types.PositiveInt" providerImport: "fide.spec.providers.given" ) structure PageFormat {} @trait(selector: "integer") @refinement( - targetType: "fide.types.Natural" + targetType: "fide.types.PositiveInt" providerImport: "fide.spec.providers.given" ) structure PageSizeFormat { } diff --git a/modules/backend/src/main/scala/service.federation.scala b/modules/backend/src/main/scala/service.federation.scala index 8ab9e08..11b7d63 100644 --- a/modules/backend/src/main/scala/service.federation.scala +++ b/modules/backend/src/main/scala/service.federation.scala @@ -6,7 +6,7 @@ import fide.db.Db import fide.domain.Models.Pagination import fide.domain.{ FederationSummary, Models } import fide.spec.* -import fide.types.Natural +import fide.types.PositiveInt import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger import org.typelevel.log4cats.syntax.* @@ -18,8 +18,8 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ override def getFederationPlayersById( id: FederationId, - page: Natural, - pageSize: Natural, + page: PositiveInt, + pageSize: PositiveInt, sortBy: Option[SortBy], order: Option[Order], isActive: Option[Boolean], @@ -61,8 +61,8 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ _.fold(IO.raiseError(FederationNotFound(id)))(_.transform.pure) override def getFederationsSummary( - page: Natural, - pageSize: Natural + page: PositiveInt, + pageSize: PositiveInt ): IO[GetFederationsSummaryOutput] = db.allFederationsSummary(Pagination.fromPageAndSize(page, pageSize)) .handleErrorWith: e => diff --git a/modules/backend/src/main/scala/service.player.scala b/modules/backend/src/main/scala/service.player.scala index 93d563b..c510a05 100644 --- a/modules/backend/src/main/scala/service.player.scala +++ b/modules/backend/src/main/scala/service.player.scala @@ -18,8 +18,8 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: import PlayerTransformers.* override def getPlayers( - page: Natural, - pageSize: Natural, + page: PositiveInt, + pageSize: PositiveInt, sortBy: Option[SortBy], order: Option[Order], isActive: Option[Boolean], diff --git a/modules/db/src/main/scala/Db.scala b/modules/db/src/main/scala/Db.scala index a55f6ac..a60e445 100644 --- a/modules/db/src/main/scala/Db.scala +++ b/modules/db/src/main/scala/Db.scala @@ -130,6 +130,25 @@ private object Codecs: (int4 *: text *: title.opt *: title.opt *: otherTitles *: int4.opt *: int4.opt *: int4.opt *: sex.opt *: int4.opt *: bool *: timestamptz *: timestamptz *: federationInfo.opt) .to[PlayerInfo] + // copy from https://github.com/Iltotore/iron/blob/main/skunk/src/io.github.iltotore.iron/skunk.scala for skunk 1.0.0 + import io.github.iltotore.iron.* + /** + * Explicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. + * + * @param constraint the [[Constraint]] implementation to test the decoded value + */ + extension [A](codec: Codec[A]) + inline def refined[C](using inline constraint: Constraint[A, C]): Codec[A :| C] = + codec.eimap[A :| C](_.refineEither[C])(_.asInstanceOf[A]) + + /** + * A [[Codec]] for refined types. Decodes to the underlying type then checks the constraint. + * + * @param codec the [[Codec]] of the underlying type + * @param constraint the [[Constraint]] implementation to test the decoded value + */ + inline given [A, C](using inline codec: Codec[A], inline constraint: Constraint[A, C]): Codec[A :| C] = codec.refined + private object Sql: import skunk.codec.all.* @@ -240,7 +259,7 @@ private object Sql: private def pagingFragment(page: Pagination): AppliedFragment = sql""" - LIMIT ${int4} OFFSET ${int4}""".apply(page.limit, page.offset) + LIMIT ${int4} OFFSET ${int4}""".apply(page.limit.toInt, page.offset) private def federationIdFragment(id: FederationId): AppliedFragment = sql"""p.federation_id = $text""".apply(id) diff --git a/modules/domain/src/main/scala/Models.scala b/modules/domain/src/main/scala/Models.scala index 0c55680..ac2f163 100644 --- a/modules/domain/src/main/scala/Models.scala +++ b/modules/domain/src/main/scala/Models.scala @@ -1,6 +1,9 @@ package fide package domain +import fide.types.* +import io.github.iltotore.iron.* + object Models: enum Order(val value: String): case Asc extends Order("ASC") @@ -30,23 +33,18 @@ object Models: Models.Sorting(_sortBy, _order) // start at 1 - case class Pagination(limit: Int, offset: Int): - def next = copy(offset = offset + limit) - def nextPage = (offset / limit) + 2 + case class Pagination(limit: PositiveInt, offset: Int): + def next = copy(offset = offset + limit.toInt) + def nextPage = (offset / limit.toInt) + 2 object Pagination: - val defaultLimit = 30 - val firstPage = 1 - val defaultOffset = 0 - val default = Pagination(defaultLimit, defaultOffset) - - def apply(limit: Option[Int], page: Option[Int]): Pagination = - val _limit = limit.getOrElse(defaultLimit) - val _offset = (page.getOrElse(firstPage) - 1) * _limit - Pagination(_limit, _offset) + val defaultLimit: PositiveInt = PositiveInt(30) + val firstPage: PositiveInt = PositiveInt(1) + val defaultOffset = 0 + val default = Pagination(defaultLimit, defaultOffset) - def fromPageAndSize(page: Int, size: Int): Pagination = - val offset = (math.max(firstPage, page) - 1) * size + def fromPageAndSize(page: PositiveInt, size: PositiveInt): Pagination = + val offset = page.toInt * size.toInt Pagination(size, offset) case class RatingRange(min: Option[Rating], max: Option[Rating]) diff --git a/modules/types/src/main/scala/Models.scala b/modules/types/src/main/scala/Models.scala new file mode 100644 index 0000000..5f3313a --- /dev/null +++ b/modules/types/src/main/scala/Models.scala @@ -0,0 +1,6 @@ +// package fide.types + +// type PlayerId = Int +// type Rating = Int +// type FederationId = String + diff --git a/modules/types/src/main/scala/Natural.scala b/modules/types/src/main/scala/Natural.scala deleted file mode 100644 index 8a1d9a1..0000000 --- a/modules/types/src/main/scala/Natural.scala +++ /dev/null @@ -1,13 +0,0 @@ -package fide.types - -import cats.syntax.all.* -import io.github.iltotore.iron.* -import io.github.iltotore.iron.constraint.all.* - -opaque type Natural <: Int :| Positive = Int :| Positive - -object Natural extends RefinedTypeOps[Int, Positive, Natural]: - def fromString(value: String): Either[String, Natural] = - value.toIntOption.toRight(s"$value is not an int") >>= Natural.either - - extension (self: Natural) def succ: Natural = Natural.applyUnsafe(self + 1) diff --git a/modules/types/src/main/scala/PositiveInt.scala b/modules/types/src/main/scala/PositiveInt.scala new file mode 100644 index 0000000..40ec874 --- /dev/null +++ b/modules/types/src/main/scala/PositiveInt.scala @@ -0,0 +1,19 @@ +package fide.types + +import cats.syntax.all.* +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* + +opaque type PositiveInt <: Int = Int :| Positive + +object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]: + def fromString(value: String): Either[String, PositiveInt] = + value.toIntOption.toRight(s"$value is not an int") >>= PositiveInt.either + + val firstNumber: PositiveInt = 1 + + extension (self: PositiveInt) + inline def succ: PositiveInt = PositiveInt.applyUnsafe(self + 1) + inline def toInt: Int = self + inline def max(other: PositiveInt): PositiveInt = + if self > other then self else other From 1cbd9b502f538edd4205a042f9d00b08e34eb8b3 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 9 Jun 2024 10:31:28 +0700 Subject: [PATCH 2/6] Refactor pagination using PositiveInt --- .../src/main/scala/service.federation.scala | 4 +-- .../src/main/scala/service.player.scala | 2 +- modules/db/src/main/scala/Db.scala | 29 ++++++++++--------- modules/db/src/test/scala/DbSuite.scala | 4 ++- modules/domain/src/main/scala/Models.scala | 18 +++--------- modules/types/src/main/scala/Models.scala | 1 - .../types/src/main/scala/PositiveInt.scala | 2 +- 7 files changed, 27 insertions(+), 33 deletions(-) diff --git a/modules/backend/src/main/scala/service.federation.scala b/modules/backend/src/main/scala/service.federation.scala index 11b7d63..4780af7 100644 --- a/modules/backend/src/main/scala/service.federation.scala +++ b/modules/backend/src/main/scala/service.federation.scala @@ -31,7 +31,7 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ blitzMax: Option[Rating], name: Option[String] ): IO[GetFederationPlayersByIdOutput] = - val paging = Models.Pagination.fromPageAndSize(page, pageSize) + val paging = Models.Pagination(page, pageSize) val sorting = Models.Sorting.fromOption(sortBy.map(_.to[Models.SortBy]), order.map(_.to[Models.Order])) val filter = Models.PlayerFilter( isActive, @@ -64,7 +64,7 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ page: PositiveInt, pageSize: PositiveInt ): IO[GetFederationsSummaryOutput] = - db.allFederationsSummary(Pagination.fromPageAndSize(page, pageSize)) + db.allFederationsSummary(Pagination(page, pageSize)) .handleErrorWith: e => error"Error in getFederationsSummary: $e" *> IO.raiseError(InternalServerError("Internal server error")) diff --git a/modules/backend/src/main/scala/service.player.scala b/modules/backend/src/main/scala/service.player.scala index c510a05..03af560 100644 --- a/modules/backend/src/main/scala/service.player.scala +++ b/modules/backend/src/main/scala/service.player.scala @@ -31,7 +31,7 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: blitzMax: Option[Rating], name: Option[String] ): IO[GetPlayersOutput] = - val paging = Models.Pagination.fromPageAndSize(page, pageSize) + val paging = Models.Pagination(page, pageSize) val sorting = Models.Sorting.fromOption(sortBy.map(_.to[Models.SortBy]), order.map(_.to[Models.Order])) val filter = Models.PlayerFilter( isActive, diff --git a/modules/db/src/main/scala/Db.scala b/modules/db/src/main/scala/Db.scala index a60e445..4dd8e04 100644 --- a/modules/db/src/main/scala/Db.scala +++ b/modules/db/src/main/scala/Db.scala @@ -132,22 +132,25 @@ private object Codecs: // copy from https://github.com/Iltotore/iron/blob/main/skunk/src/io.github.iltotore.iron/skunk.scala for skunk 1.0.0 import io.github.iltotore.iron.* - /** - * Explicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. - * - * @param constraint the [[Constraint]] implementation to test the decoded value - */ + + /** Explicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. + * + * @param constraint + * the [[Constraint]] implementation to test the decoded value + */ extension [A](codec: Codec[A]) inline def refined[C](using inline constraint: Constraint[A, C]): Codec[A :| C] = codec.eimap[A :| C](_.refineEither[C])(_.asInstanceOf[A]) - /** - * A [[Codec]] for refined types. Decodes to the underlying type then checks the constraint. - * - * @param codec the [[Codec]] of the underlying type - * @param constraint the [[Constraint]] implementation to test the decoded value - */ - inline given [A, C](using inline codec: Codec[A], inline constraint: Constraint[A, C]): Codec[A :| C] = codec.refined + /** A [[Codec]] for refined types. Decodes to the underlying type then checks the constraint. + * + * @param codec + * the [[Codec]] of the underlying type + * @param constraint + * the [[Constraint]] implementation to test the decoded value + */ + inline given [A, C](using inline codec: Codec[A], inline constraint: Constraint[A, C]): Codec[A :| C] = + codec.refined private object Sql: @@ -259,7 +262,7 @@ private object Sql: private def pagingFragment(page: Pagination): AppliedFragment = sql""" - LIMIT ${int4} OFFSET ${int4}""".apply(page.limit.toInt, page.offset) + LIMIT ${int4} OFFSET ${int4}""".apply(page.size, page.offset) private def federationIdFragment(id: FederationId): AppliedFragment = sql"""p.federation_id = $text""".apply(id) diff --git a/modules/db/src/test/scala/DbSuite.scala b/modules/db/src/test/scala/DbSuite.scala index 71c9f87..cfaf14c 100644 --- a/modules/db/src/test/scala/DbSuite.scala +++ b/modules/db/src/test/scala/DbSuite.scala @@ -7,7 +7,9 @@ import cats.effect.kernel.Resource import cats.syntax.all.* import fide.domain.* import fide.domain.Models.* +import fide.types.PositiveInt import io.github.arainko.ducktape.* +import io.github.iltotore.iron.* import org.typelevel.log4cats.Logger import org.typelevel.log4cats.noop.NoOpLogger import weaver.* @@ -99,7 +101,7 @@ object DbSuite extends SimpleIOSuite: ) val defaultSorting = Sorting(SortBy.Name, Order.Asc) - val defaultPage = Pagination(10, 0) + val defaultPage = Pagination(PositiveInt(1), PositiveInt(30)) test("search playersByName success"): resource.use: db => diff --git a/modules/domain/src/main/scala/Models.scala b/modules/domain/src/main/scala/Models.scala index ac2f163..e676a25 100644 --- a/modules/domain/src/main/scala/Models.scala +++ b/modules/domain/src/main/scala/Models.scala @@ -32,20 +32,10 @@ object Models: val _order = order.getOrElse(defaultOrder) Models.Sorting(_sortBy, _order) - // start at 1 - case class Pagination(limit: PositiveInt, offset: Int): - def next = copy(offset = offset + limit.toInt) - def nextPage = (offset / limit.toInt) + 2 - - object Pagination: - val defaultLimit: PositiveInt = PositiveInt(30) - val firstPage: PositiveInt = PositiveInt(1) - val defaultOffset = 0 - val default = Pagination(defaultLimit, defaultOffset) - - def fromPageAndSize(page: PositiveInt, size: PositiveInt): Pagination = - val offset = page.toInt * size.toInt - Pagination(size, offset) + case class Pagination(page: PositiveInt, size: PositiveInt): + def next: Pagination = copy(page = page.succ) + def nextPage: PositiveInt = page.succ + def offset: Int = (page - 1) * size case class RatingRange(min: Option[Rating], max: Option[Rating]) object RatingRange: diff --git a/modules/types/src/main/scala/Models.scala b/modules/types/src/main/scala/Models.scala index 5f3313a..b423cf0 100644 --- a/modules/types/src/main/scala/Models.scala +++ b/modules/types/src/main/scala/Models.scala @@ -3,4 +3,3 @@ // type PlayerId = Int // type Rating = Int // type FederationId = String - diff --git a/modules/types/src/main/scala/PositiveInt.scala b/modules/types/src/main/scala/PositiveInt.scala index 40ec874..cb4d2d9 100644 --- a/modules/types/src/main/scala/PositiveInt.scala +++ b/modules/types/src/main/scala/PositiveInt.scala @@ -14,6 +14,6 @@ object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]: extension (self: PositiveInt) inline def succ: PositiveInt = PositiveInt.applyUnsafe(self + 1) - inline def toInt: Int = self + inline def toInt: Int = self inline def max(other: PositiveInt): PositiveInt = if self > other then self else other From cdfb252192c559a4eefdbb676dc8c7011e4842f7 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 9 Jun 2024 14:17:32 +0700 Subject: [PATCH 3/6] Unify Rating type --- modules/api/src/main/scala/providers.scala | 5 +- modules/api/src/main/smithy/_global.smithy | 10 +++- .../src/main/scala/service.federation.scala | 10 ++-- .../src/main/scala/service.player.scala | 9 ++-- modules/crawler/src/main/scala/Crawler.scala | 14 +++-- modules/db/src/main/scala/Db.scala | 54 ++++++++++--------- modules/db/src/test/scala/DbSuite.scala | 14 ++--- modules/domain/src/main/scala/Domain.scala | 2 +- modules/types/src/main/scala/Rating.scala | 14 +++++ 9 files changed, 81 insertions(+), 51 deletions(-) create mode 100644 modules/types/src/main/scala/Rating.scala diff --git a/modules/api/src/main/scala/providers.scala b/modules/api/src/main/scala/providers.scala index e960e97..2a6e33c 100644 --- a/modules/api/src/main/scala/providers.scala +++ b/modules/api/src/main/scala/providers.scala @@ -2,7 +2,7 @@ package fide.spec import cats.syntax.all.* import fide.spec.* -import fide.types.* +import fide.types.{ NonEmptySet, PositiveInt } import smithy4s.* object providers: @@ -15,6 +15,9 @@ object providers: given RefinementProvider[PageSizeFormat, Int, PositiveInt] = Refinement.drivenBy(PositiveInt.either, _.value) + given RefinementProvider[RatingFormat, Int, fide.types.Rating] = + Refinement.drivenBy(fide.types.Rating.either, _.value) + given [A]: RefinementProvider[NonEmptySetFormat, Set[A], NonEmptySet[A]] = Refinement.drivenBy[NonEmptySetFormat]( NonEmptySet.either, diff --git a/modules/api/src/main/smithy/_global.smithy b/modules/api/src/main/smithy/_global.smithy index 0a647b1..b6fafe5 100644 --- a/modules/api/src/main/smithy/_global.smithy +++ b/modules/api/src/main/smithy/_global.smithy @@ -29,7 +29,15 @@ structure InternalServerError { integer PlayerId string FederationId -@range(min: 0, max: 4000) +@trait(selector: "integer") +@refinement( + targetType: "fide.types.Rating" + providerImport: "fide.spec.providers.given" +) +structure RatingFormat {} + +@RatingFormat +@unwrap integer Rating @trait(selector: "string") diff --git a/modules/backend/src/main/scala/service.federation.scala b/modules/backend/src/main/scala/service.federation.scala index 4780af7..d8a7a68 100644 --- a/modules/backend/src/main/scala/service.federation.scala +++ b/modules/backend/src/main/scala/service.federation.scala @@ -5,8 +5,8 @@ import cats.syntax.all.* import fide.db.Db import fide.domain.Models.Pagination import fide.domain.{ FederationSummary, Models } -import fide.spec.* -import fide.types.PositiveInt +import fide.spec.{ Rating as _, * } +import fide.types.* import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger import org.typelevel.log4cats.syntax.* @@ -35,9 +35,9 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ val sorting = Models.Sorting.fromOption(sortBy.map(_.to[Models.SortBy]), order.map(_.to[Models.Order])) val filter = Models.PlayerFilter( isActive, - Models.RatingRange(standardMin.map(_.value), standardMax.map(_.value)), - Models.RatingRange(rapidMin.map(_.value), rapidMax.map(_.value)), - Models.RatingRange(blitzMin.map(_.value), blitzMax.map(_.value)), + Models.RatingRange(standardMin, standardMax), + Models.RatingRange(rapidMin, rapidMax), + Models.RatingRange(blitzMin, blitzMax), id.value.some ) name diff --git a/modules/backend/src/main/scala/service.player.scala b/modules/backend/src/main/scala/service.player.scala index 03af560..1a437d7 100644 --- a/modules/backend/src/main/scala/service.player.scala +++ b/modules/backend/src/main/scala/service.player.scala @@ -4,7 +4,7 @@ import cats.effect.* import cats.syntax.all.* import fide.db.Db import fide.domain.Models -import fide.spec.* +import fide.spec.{ Rating as _, * } import fide.types.* import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger @@ -35,9 +35,9 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: val sorting = Models.Sorting.fromOption(sortBy.map(_.to[Models.SortBy]), order.map(_.to[Models.Order])) val filter = Models.PlayerFilter( isActive, - Models.RatingRange(standardMin.map(_.value), standardMax.map(_.value)), - Models.RatingRange(rapidMin.map(_.value), rapidMax.map(_.value)), - Models.RatingRange(blitzMin.map(_.value), blitzMax.map(_.value)), + Models.RatingRange(standardMin, standardMax), + Models.RatingRange(rapidMin, rapidMax), + Models.RatingRange(blitzMin, blitzMax), None ) name @@ -70,7 +70,6 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: .map(GetPlayerByIdsOutput.apply) object PlayerTransformers: - given Transformer.Derived[Int, Rating] = Transformer.Derived.FromFunction(Rating.apply) given Transformer.Derived[String, FederationId] = Transformer.Derived.FromFunction(FederationId.apply) given Transformer.Derived[Int, PlayerId] = Transformer.Derived.FromFunction(PlayerId.apply) given Transformer.Derived[OffsetDateTime, Timestamp] = diff --git a/modules/crawler/src/main/scala/Crawler.scala b/modules/crawler/src/main/scala/Crawler.scala index 3b4f27a..cab3c6c 100644 --- a/modules/crawler/src/main/scala/Crawler.scala +++ b/modules/crawler/src/main/scala/Crawler.scala @@ -5,6 +5,7 @@ import cats.effect.IO import cats.syntax.all.* import fide.db.{ Db, KVStore } import fide.domain.* +import fide.types.Rating import org.http4s.* import org.http4s.client.Client import org.http4s.implicits.* @@ -69,8 +70,11 @@ object Downloader: .handleErrorWith(e => error"Error while parsing line: $line, error: $e".as(none)) def parse(line: String): Option[(NewPlayer, Option[NewFederation])] = - def string(start: Int, end: Int) = line.substring(start, end).trim.some.filter(_.nonEmpty) - def number(start: Int, end: Int) = string(start, end).flatMap(_.toIntOption) + def string(start: Int, end: Int): Option[String] = line.substring(start, end).trim.some.filter(_.nonEmpty) + + def number(start: Int, end: Int): Option[Int] = string(start, end).flatMap(_.toIntOption) + def rating(start: Int, end: Int): Option[Rating] = string(start, end) >>= Rating.fromString + for id <- number(0, 15) name <- string(15, 76).map(_.filterNot(_.isDigit).trim) @@ -88,9 +92,9 @@ object Downloader: title = title, womenTitle = wTitle, otherTitles = otherTitles, - standard = number(113, 117), - rapid = number(126, 132), - blitz = number(139, 145), + standard = rating(113, 117), + rapid = rating(126, 132), + blitz = rating(139, 145), sex = sex, birthYear = year, active = inactiveFlag.isEmpty diff --git a/modules/db/src/main/scala/Db.scala b/modules/db/src/main/scala/Db.scala index 4dd8e04..a64a1cf 100644 --- a/modules/db/src/main/scala/Db.scala +++ b/modules/db/src/main/scala/Db.scala @@ -5,6 +5,7 @@ import cats.effect.* import cats.syntax.all.* import fide.domain.* import fide.domain.Models.* +import fide.types.* import org.typelevel.log4cats.Logger import skunk.* @@ -98,8 +99,31 @@ private object Codecs: import skunk.codec.all.* import skunk.data.{ Arr, Type } - val title: Codec[Title] = `enum`[Title](_.value, Title.apply, Type("title")) - val sex: Codec[Sex] = `enum`[Sex](_.value, Sex.apply, Type("sex")) + // copy from https://github.com/Iltotore/iron/blob/main/skunk/src/io.github.iltotore.iron/skunk.scala for skunk 1.0.0 + import io.github.iltotore.iron.* + + /** Explicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. + * + * @param constraint + * the [[Constraint]] implementation to test the decoded value + */ + extension [A](codec: Codec[A]) + inline def refined[C](using inline constraint: Constraint[A, C]): Codec[A :| C] = + codec.eimap[A :| C](_.refineEither[C])(_.asInstanceOf[A]) + + /** A [[Codec]] for refined types. Decodes to the underlying type then checks the constraint. + * + * @param codec + * the [[Codec]] of the underlying type + * @param constraint + * the [[Constraint]] implementation to test the decoded value + */ + inline given [A, C](using inline codec: Codec[A], inline constraint: Constraint[A, C]): Codec[A :| C] = + codec.refined + + val title: Codec[Title] = `enum`[Title](_.value, Title.apply, Type("title")) + val sex: Codec[Sex] = `enum`[Sex](_.value, Sex.apply, Type("sex")) + val ratingCodec: Codec[Rating] = int4.refined[RatingConstraint].imap(Rating.apply)(_.value) val otherTitleArr: Codec[Arr[OtherTitle]] = Codec.array( @@ -111,7 +135,7 @@ private object Codecs: val otherTitles: Codec[List[OtherTitle]] = otherTitleArr.opt.imap(_.fold(Nil)(_.toList))(Arr(_*).some) val insertPlayer: Codec[InsertPlayer] = - (int4 *: text *: title.opt *: title.opt *: otherTitles *: int4.opt *: int4.opt *: int4.opt *: sex.opt *: int4.opt *: bool *: text.opt) + (int4 *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: text.opt) .to[InsertPlayer] val newFederation: Codec[NewFederation] = @@ -127,31 +151,9 @@ private object Codecs: (text *: text *: int4 *: stats *: stats *: stats).to[FederationSummary] val playerInfo: Codec[PlayerInfo] = - (int4 *: text *: title.opt *: title.opt *: otherTitles *: int4.opt *: int4.opt *: int4.opt *: sex.opt *: int4.opt *: bool *: timestamptz *: timestamptz *: federationInfo.opt) + (int4 *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: timestamptz *: timestamptz *: federationInfo.opt) .to[PlayerInfo] - // copy from https://github.com/Iltotore/iron/blob/main/skunk/src/io.github.iltotore.iron/skunk.scala for skunk 1.0.0 - import io.github.iltotore.iron.* - - /** Explicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. - * - * @param constraint - * the [[Constraint]] implementation to test the decoded value - */ - extension [A](codec: Codec[A]) - inline def refined[C](using inline constraint: Constraint[A, C]): Codec[A :| C] = - codec.eimap[A :| C](_.refineEither[C])(_.asInstanceOf[A]) - - /** A [[Codec]] for refined types. Decodes to the underlying type then checks the constraint. - * - * @param codec - * the [[Codec]] of the underlying type - * @param constraint - * the [[Constraint]] implementation to test the decoded value - */ - inline given [A, C](using inline codec: Codec[A], inline constraint: Constraint[A, C]): Codec[A :| C] = - codec.refined - private object Sql: import skunk.codec.all.* diff --git a/modules/db/src/test/scala/DbSuite.scala b/modules/db/src/test/scala/DbSuite.scala index cfaf14c..39d38fe 100644 --- a/modules/db/src/test/scala/DbSuite.scala +++ b/modules/db/src/test/scala/DbSuite.scala @@ -7,7 +7,7 @@ import cats.effect.kernel.Resource import cats.syntax.all.* import fide.domain.* import fide.domain.Models.* -import fide.types.PositiveInt +import fide.types.* import io.github.arainko.ducktape.* import io.github.iltotore.iron.* import org.typelevel.log4cats.Logger @@ -28,9 +28,9 @@ object DbSuite extends SimpleIOSuite: Title.GM.some, Title.WGM.some, List(OtherTitle.FI, OtherTitle.LSI), - 2700.some, - 2700.some, - 2700.some, + Rating(2700).some, + Rating(2700).some, + Rating(2700).some, Sex.Male.some, 1990.some, true @@ -42,9 +42,9 @@ object DbSuite extends SimpleIOSuite: Title.GM.some, Title.WGM.some, List(OtherTitle.IA, OtherTitle.DI), - 2700.some, - 2700.some, - 2700.some, + Rating(2700).some, + Rating(2700).some, + Rating(2700).some, Sex.Female.some, 1990.some, true diff --git a/modules/domain/src/main/scala/Domain.scala b/modules/domain/src/main/scala/Domain.scala index 5b2ec8b..e2dc865 100644 --- a/modules/domain/src/main/scala/Domain.scala +++ b/modules/domain/src/main/scala/Domain.scala @@ -2,11 +2,11 @@ package fide package domain import cats.syntax.all.* +import fide.types.* import java.time.OffsetDateTime type PlayerId = Int -type Rating = Int type FederationId = String object FederationId: diff --git a/modules/types/src/main/scala/Rating.scala b/modules/types/src/main/scala/Rating.scala new file mode 100644 index 0000000..ecbb7ac --- /dev/null +++ b/modules/types/src/main/scala/Rating.scala @@ -0,0 +1,14 @@ +package fide.types + +import cats.syntax.all.* +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* + +type RatingConstraint = GreaterEqual[1] & LessEqual[4000] +opaque type Rating <: Int = Int :| RatingConstraint + +object Rating extends RefinedTypeOps[Int, RatingConstraint, Rating]: + def fromString(value: String): Option[Rating] = + value.toIntOption >>= Rating.option + + extension (self: Rating) inline def toInt: Int = self From d3b1332eb2e546ff7da5a00d4965f371716aec71 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 9 Jun 2024 15:11:00 +0700 Subject: [PATCH 4/6] Unify FederationId --- modules/api/src/main/scala/providers.scala | 3 +++ modules/api/src/main/smithy/_global.smithy | 10 ++++++++ .../src/main/scala/service.federation.scala | 7 +++--- .../src/main/scala/service.player.scala | 5 ++-- modules/crawler/src/main/scala/Crawler.scala | 4 ++-- modules/db/src/main/scala/Db.scala | 21 ++++++++-------- modules/db/src/test/scala/DbSuite.scala | 9 +++---- modules/domain/src/main/scala/Domain.scala | 7 ++---- .../types/src/main/scala/FederationId.scala | 24 +++++++++++++++++++ modules/types/src/main/scala/Models.scala | 5 ---- 10 files changed, 62 insertions(+), 33 deletions(-) create mode 100644 modules/types/src/main/scala/FederationId.scala delete mode 100644 modules/types/src/main/scala/Models.scala diff --git a/modules/api/src/main/scala/providers.scala b/modules/api/src/main/scala/providers.scala index 2a6e33c..83096e8 100644 --- a/modules/api/src/main/scala/providers.scala +++ b/modules/api/src/main/scala/providers.scala @@ -18,6 +18,9 @@ object providers: given RefinementProvider[RatingFormat, Int, fide.types.Rating] = Refinement.drivenBy(fide.types.Rating.either, _.value) + given RefinementProvider[FederationIdFormat, String, fide.types.FederationId] = + Refinement.drivenBy(fide.types.FederationId.either, _.value) + given [A]: RefinementProvider[NonEmptySetFormat, Set[A], NonEmptySet[A]] = Refinement.drivenBy[NonEmptySetFormat]( NonEmptySet.either, diff --git a/modules/api/src/main/smithy/_global.smithy b/modules/api/src/main/smithy/_global.smithy index b6fafe5..53428c0 100644 --- a/modules/api/src/main/smithy/_global.smithy +++ b/modules/api/src/main/smithy/_global.smithy @@ -27,6 +27,16 @@ structure InternalServerError { } integer PlayerId + +@trait(selector: "string") +@refinement( + targetType: "fide.types.FederationId" + providerImport: "fide.spec.providers.given" +) +structure FederationIdFormat {} + +@unwrap +@FederationIdFormat string FederationId @trait(selector: "integer") diff --git a/modules/backend/src/main/scala/service.federation.scala b/modules/backend/src/main/scala/service.federation.scala index d8a7a68..8298cb1 100644 --- a/modules/backend/src/main/scala/service.federation.scala +++ b/modules/backend/src/main/scala/service.federation.scala @@ -5,7 +5,7 @@ import cats.syntax.all.* import fide.db.Db import fide.domain.Models.Pagination import fide.domain.{ FederationSummary, Models } -import fide.spec.{ Rating as _, * } +import fide.spec.{ FederationId as _, Rating as _, * } import fide.types.* import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger @@ -38,7 +38,7 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ Models.RatingRange(standardMin, standardMax), Models.RatingRange(rapidMin, rapidMax), Models.RatingRange(blitzMin, blitzMax), - id.value.some + id.some ) name .fold(db.allPlayers(sorting, paging, filter))(db.playersByName(_, sorting, paging, filter)) @@ -53,7 +53,7 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ ) override def getFederationSummaryById(id: FederationId): IO[GetFederationSummaryByIdOutput] = - db.federationSummaryById(id.value) + db.federationSummaryById(id) .handleErrorWith: e => error"Error in getFederationSummaryById: $id, $e" *> IO.raiseError(InternalServerError("Internal server error")) @@ -76,7 +76,6 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ ) object FederationTransformers: - given Transformer.Derived[String, FederationId] = Transformer.Derived.FromFunction(FederationId.apply) extension (p: FederationSummary) def transform: GetFederationSummaryByIdOutput = p.to[GetFederationSummaryByIdOutput] diff --git a/modules/backend/src/main/scala/service.player.scala b/modules/backend/src/main/scala/service.player.scala index 1a437d7..315e833 100644 --- a/modules/backend/src/main/scala/service.player.scala +++ b/modules/backend/src/main/scala/service.player.scala @@ -4,7 +4,7 @@ import cats.effect.* import cats.syntax.all.* import fide.db.Db import fide.domain.Models -import fide.spec.{ Rating as _, * } +import fide.spec.{ FederationId as _, Rating as _, * } import fide.types.* import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger @@ -70,8 +70,7 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: .map(GetPlayerByIdsOutput.apply) object PlayerTransformers: - given Transformer.Derived[String, FederationId] = Transformer.Derived.FromFunction(FederationId.apply) - given Transformer.Derived[Int, PlayerId] = Transformer.Derived.FromFunction(PlayerId.apply) + given Transformer.Derived[Int, PlayerId] = Transformer.Derived.FromFunction(PlayerId.apply) given Transformer.Derived[OffsetDateTime, Timestamp] = Transformer.Derived.FromFunction(Timestamp.fromOffsetDateTime) diff --git a/modules/crawler/src/main/scala/Crawler.scala b/modules/crawler/src/main/scala/Crawler.scala index cab3c6c..31d5090 100644 --- a/modules/crawler/src/main/scala/Crawler.scala +++ b/modules/crawler/src/main/scala/Crawler.scala @@ -5,7 +5,7 @@ import cats.effect.IO import cats.syntax.all.* import fide.db.{ Db, KVStore } import fide.domain.* -import fide.types.Rating +import fide.types.{ FederationId, Rating } import org.http4s.* import org.http4s.client.Client import org.http4s.implicits.* @@ -85,7 +85,7 @@ object Downloader: sex = string(79, 82) >>= Sex.apply year = number(152, 156).filter(_ > 1000) inactiveFlag = string(158, 160) - federationId = string(76, 79).map(FederationId.apply) + federationId = string(76, 79) >>= FederationId.option yield NewPlayer( id = id, name = name, diff --git a/modules/db/src/main/scala/Db.scala b/modules/db/src/main/scala/Db.scala index a64a1cf..02b5d6a 100644 --- a/modules/db/src/main/scala/Db.scala +++ b/modules/db/src/main/scala/Db.scala @@ -121,9 +121,10 @@ private object Codecs: inline given [A, C](using inline codec: Codec[A], inline constraint: Constraint[A, C]): Codec[A :| C] = codec.refined - val title: Codec[Title] = `enum`[Title](_.value, Title.apply, Type("title")) - val sex: Codec[Sex] = `enum`[Sex](_.value, Sex.apply, Type("sex")) - val ratingCodec: Codec[Rating] = int4.refined[RatingConstraint].imap(Rating.apply)(_.value) + val title: Codec[Title] = `enum`[Title](_.value, Title.apply, Type("title")) + val sex: Codec[Sex] = `enum`[Sex](_.value, Sex.apply, Type("sex")) + val ratingCodec: Codec[Rating] = int4.refined[RatingConstraint].imap(Rating.apply)(_.value) + val federationIdCodec: Codec[FederationId] = text.refined[NonEmpty].imap(FederationId.apply)(_.value) val otherTitleArr: Codec[Arr[OtherTitle]] = Codec.array( @@ -135,20 +136,20 @@ private object Codecs: val otherTitles: Codec[List[OtherTitle]] = otherTitleArr.opt.imap(_.fold(Nil)(_.toList))(Arr(_*).some) val insertPlayer: Codec[InsertPlayer] = - (int4 *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: text.opt) + (int4 *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: federationIdCodec.opt) .to[InsertPlayer] val newFederation: Codec[NewFederation] = - (text *: text).to[NewFederation] + (federationIdCodec *: text).to[NewFederation] val federationInfo: Codec[FederationInfo] = - (text *: text).to[FederationInfo] + (federationIdCodec *: text).to[FederationInfo] val stats: Codec[Stats] = (int4 *: int4 *: int4).to[Stats] val federationSummary: Codec[FederationSummary] = - (text *: text *: int4 *: stats *: stats *: stats).to[FederationSummary] + (federationIdCodec *: text *: int4 *: stats *: stats *: stats).to[FederationSummary] val playerInfo: Codec[PlayerInfo] = (int4 *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: timestamptz *: timestamptz *: federationInfo.opt) @@ -180,7 +181,7 @@ private object Sql: sql"$allPlayersFragment WHERE p.id = $int4".query(playerInfo) lazy val playersByFederationId: Query[FederationId, PlayerInfo] = - sql"$allPlayersFragment WHERE p.federation_id = $text".query(playerInfo) + sql"$allPlayersFragment WHERE p.federation_id = $federationIdCodec".query(playerInfo) lazy val upsertFederation: Command[NewFederation] = sql"$insertIntoFederation VALUES ($newFederation) $onConflictDoNothing".command @@ -212,7 +213,7 @@ private object Sql: lazy val federationSummaryById: Query[FederationId, FederationSummary] = sql"""$allFederationsSummaryFragment - WHERE id = $text""".query(federationSummary) + WHERE id = $federationIdCodec""".query(federationSummary) private val void: AppliedFragment = sql"".apply(Void) private val and: AppliedFragment = sql"AND ".apply(Void) @@ -267,7 +268,7 @@ private object Sql: LIMIT ${int4} OFFSET ${int4}""".apply(page.size, page.offset) private def federationIdFragment(id: FederationId): AppliedFragment = - sql"""p.federation_id = $text""".apply(id) + sql"""p.federation_id = $federationIdCodec""".apply(id) private def sortingFragment(sorting: Sorting): AppliedFragment = val column = s"p.${sorting.sortBy.value}" diff --git a/modules/db/src/test/scala/DbSuite.scala b/modules/db/src/test/scala/DbSuite.scala index 39d38fe..172acca 100644 --- a/modules/db/src/test/scala/DbSuite.scala +++ b/modules/db/src/test/scala/DbSuite.scala @@ -50,8 +50,9 @@ object DbSuite extends SimpleIOSuite: true ) + val fedId = FederationId("fide") val newFederation = NewFederation( - "fide", + fedId, "FIDE" ) @@ -89,7 +90,7 @@ object DbSuite extends SimpleIOSuite: test("overwriting player success"): val player2 = newPlayer1.copy(name = "Jane") - val federation2 = NewFederation("Lichess", "lichess") + val federation2 = NewFederation(FederationId("Lichess"), "lichess") resource.use: db => for _ <- db.upsert(newPlayer1, newFederation.some) @@ -126,7 +127,7 @@ object DbSuite extends SimpleIOSuite: resource.use: db => for _ <- db.upsert(newPlayers) - players <- db.playersByFederationId("fide") + players <- db.playersByFederationId(fedId) yield expect( players.length == 1 && players.head.to[NewPlayer] == newPlayer1 && players.head.federation.get .to[NewFederation] == newFederation @@ -154,5 +155,5 @@ object DbSuite extends SimpleIOSuite: for _ <- db.upsert(newPlayer1, newFederation.some) _ <- kv.put("fide_last_update_key", "2021-01-01") - result <- db.federationSummaryById("fide") + result <- db.federationSummaryById(fedId) yield expect(result.isDefined) diff --git a/modules/domain/src/main/scala/Domain.scala b/modules/domain/src/main/scala/Domain.scala index e2dc865..9d583c7 100644 --- a/modules/domain/src/main/scala/Domain.scala +++ b/modules/domain/src/main/scala/Domain.scala @@ -6,11 +6,7 @@ import fide.types.* import java.time.OffsetDateTime -type PlayerId = Int -type FederationId = String - -object FederationId: - def apply(value: String): FederationId = value.toUpperCase +type PlayerId = Int enum Title(val value: String): case GM extends Title("GM") @@ -139,6 +135,7 @@ object Federation: def nameById(id: FederationId): Option[String] = names.get(id) + import io.github.iltotore.iron.* val names: Map[FederationId, String] = Map( FederationId("FID") -> "FIDE", FederationId("USA") -> "United States of America", diff --git a/modules/types/src/main/scala/FederationId.scala b/modules/types/src/main/scala/FederationId.scala new file mode 100644 index 0000000..0635264 --- /dev/null +++ b/modules/types/src/main/scala/FederationId.scala @@ -0,0 +1,24 @@ +package fide.types + +import cats.syntax.all.* +import io.github.iltotore.iron.* +import io.github.iltotore.iron.constraint.all.* + +type NonEmpty = MinLength[1] +opaque type FederationId = String :| NonEmpty + +object FederationId: + private val rtc: RefinedTypeOps[String, NonEmpty, FederationId] = + new RefinedTypeOps[String, NonEmpty, FederationId]() {} + + inline def apply(value: String :| NonEmpty): FederationId = rtc.applyUnsafe(value.toUpperCase) + + def either(value: String): Either[String, FederationId] = + rtc.either(value.toUpperCase) + + def option(value: String): Option[FederationId] = + either(value).toOption + + extension (str: FederationId) + inline def toStr: String = str + inline def value = rtc.value(str) diff --git a/modules/types/src/main/scala/Models.scala b/modules/types/src/main/scala/Models.scala deleted file mode 100644 index b423cf0..0000000 --- a/modules/types/src/main/scala/Models.scala +++ /dev/null @@ -1,5 +0,0 @@ -// package fide.types - -// type PlayerId = Int -// type Rating = Int -// type FederationId = String From 5bbc1e0905e8331fc41ecec42f944fba8eb5f6ff Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 9 Jun 2024 15:43:05 +0700 Subject: [PATCH 5/6] Seperate PositiveInt into PageNumber & PageSize --- modules/api/src/main/scala/providers.scala | 15 +++++++------ modules/api/src/main/smithy/_global.smithy | 4 ++-- .../src/main/scala/service.federation.scala | 10 ++++----- .../src/main/scala/service.player.scala | 6 +++--- modules/db/src/test/scala/DbSuite.scala | 2 +- modules/domain/src/main/scala/Models.scala | 8 +++---- .../types/src/main/scala/PositiveInt.scala | 21 +++++++++++-------- 7 files changed, 34 insertions(+), 32 deletions(-) diff --git a/modules/api/src/main/scala/providers.scala b/modules/api/src/main/scala/providers.scala index 83096e8..1fcb128 100644 --- a/modules/api/src/main/scala/providers.scala +++ b/modules/api/src/main/scala/providers.scala @@ -1,19 +1,18 @@ package fide.spec import cats.syntax.all.* -import fide.spec.* -import fide.types.{ NonEmptySet, PositiveInt } +import fide.types.{ NonEmptySet, PageNumber, PageSize } import smithy4s.* object providers: - given RefinementProvider[PageFormat, String, PositiveInt] = - Refinement.drivenBy(PositiveInt.fromString, _.toString) + given RefinementProvider[PageFormat, String, PageNumber] = + Refinement.drivenBy(PageNumber.fromString, _.toString) - given RefinementProvider.Simple[smithy.api.Range, PositiveInt] = - RefinementProvider.rangeConstraint(_.value.toInt) + given RefinementProvider.Simple[smithy.api.Range, fide.types.PageSize] = + RefinementProvider.rangeConstraint(_.toInt) - given RefinementProvider[PageSizeFormat, Int, PositiveInt] = - Refinement.drivenBy(PositiveInt.either, _.value) + given RefinementProvider[PageSizeFormat, Int, fide.types.PageSize] = + Refinement.drivenBy(PageSize.either, _.toInt) given RefinementProvider[RatingFormat, Int, fide.types.Rating] = Refinement.drivenBy(fide.types.Rating.either, _.value) diff --git a/modules/api/src/main/smithy/_global.smithy b/modules/api/src/main/smithy/_global.smithy index 53428c0..e4a20b6 100644 --- a/modules/api/src/main/smithy/_global.smithy +++ b/modules/api/src/main/smithy/_global.smithy @@ -52,14 +52,14 @@ integer Rating @trait(selector: "string") @refinement( - targetType: "fide.types.PositiveInt" + targetType: "fide.types.PageNumber" providerImport: "fide.spec.providers.given" ) structure PageFormat {} @trait(selector: "integer") @refinement( - targetType: "fide.types.PositiveInt" + targetType: "fide.types.PageSize" providerImport: "fide.spec.providers.given" ) structure PageSizeFormat { } diff --git a/modules/backend/src/main/scala/service.federation.scala b/modules/backend/src/main/scala/service.federation.scala index 8298cb1..50c63b3 100644 --- a/modules/backend/src/main/scala/service.federation.scala +++ b/modules/backend/src/main/scala/service.federation.scala @@ -5,7 +5,7 @@ import cats.syntax.all.* import fide.db.Db import fide.domain.Models.Pagination import fide.domain.{ FederationSummary, Models } -import fide.spec.{ FederationId as _, Rating as _, * } +import fide.spec.{ FederationId as _, PageNumber as _, PageSize as _, Rating as _, * } import fide.types.* import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger @@ -18,8 +18,8 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ override def getFederationPlayersById( id: FederationId, - page: PositiveInt, - pageSize: PositiveInt, + page: PageNumber, + pageSize: PageSize, sortBy: Option[SortBy], order: Option[Order], isActive: Option[Boolean], @@ -61,8 +61,8 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[ _.fold(IO.raiseError(FederationNotFound(id)))(_.transform.pure) override def getFederationsSummary( - page: PositiveInt, - pageSize: PositiveInt + page: PageNumber, + pageSize: PageSize ): IO[GetFederationsSummaryOutput] = db.allFederationsSummary(Pagination(page, pageSize)) .handleErrorWith: e => diff --git a/modules/backend/src/main/scala/service.player.scala b/modules/backend/src/main/scala/service.player.scala index 315e833..b21f79b 100644 --- a/modules/backend/src/main/scala/service.player.scala +++ b/modules/backend/src/main/scala/service.player.scala @@ -4,7 +4,7 @@ import cats.effect.* import cats.syntax.all.* import fide.db.Db import fide.domain.Models -import fide.spec.{ FederationId as _, Rating as _, * } +import fide.spec.{ FederationId as _, PageNumber as _, PageSize as _, Rating as _, * } import fide.types.* import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger @@ -18,8 +18,8 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: import PlayerTransformers.* override def getPlayers( - page: PositiveInt, - pageSize: PositiveInt, + page: PageNumber, + pageSize: PageSize, sortBy: Option[SortBy], order: Option[Order], isActive: Option[Boolean], diff --git a/modules/db/src/test/scala/DbSuite.scala b/modules/db/src/test/scala/DbSuite.scala index 172acca..dfb5442 100644 --- a/modules/db/src/test/scala/DbSuite.scala +++ b/modules/db/src/test/scala/DbSuite.scala @@ -102,7 +102,7 @@ object DbSuite extends SimpleIOSuite: ) val defaultSorting = Sorting(SortBy.Name, Order.Asc) - val defaultPage = Pagination(PositiveInt(1), PositiveInt(30)) + val defaultPage = Pagination(PageNumber(1), PageSize(30)) test("search playersByName success"): resource.use: db => diff --git a/modules/domain/src/main/scala/Models.scala b/modules/domain/src/main/scala/Models.scala index e676a25..49144b6 100644 --- a/modules/domain/src/main/scala/Models.scala +++ b/modules/domain/src/main/scala/Models.scala @@ -32,10 +32,10 @@ object Models: val _order = order.getOrElse(defaultOrder) Models.Sorting(_sortBy, _order) - case class Pagination(page: PositiveInt, size: PositiveInt): - def next: Pagination = copy(page = page.succ) - def nextPage: PositiveInt = page.succ - def offset: Int = (page - 1) * size + case class Pagination(page: PageNumber, size: PageSize): + def next: Pagination = copy(page = page.succ) + def nextPage: PageNumber = page.succ + def offset: Int = (page - 1) * size case class RatingRange(min: Option[Rating], max: Option[Rating]) object RatingRange: diff --git a/modules/types/src/main/scala/PositiveInt.scala b/modules/types/src/main/scala/PositiveInt.scala index cb4d2d9..15eb098 100644 --- a/modules/types/src/main/scala/PositiveInt.scala +++ b/modules/types/src/main/scala/PositiveInt.scala @@ -4,16 +4,19 @@ import cats.syntax.all.* import io.github.iltotore.iron.* import io.github.iltotore.iron.constraint.all.* -opaque type PositiveInt <: Int = Int :| Positive +type PositiveInt = Int :| Positive -object PositiveInt extends RefinedTypeOps[Int, Positive, PositiveInt]: - def fromString(value: String): Either[String, PositiveInt] = - value.toIntOption.toRight(s"$value is not an int") >>= PositiveInt.either +opaque type PageSize <: Int = PositiveInt +object PageSize extends RefinedTypeOps[Int, Positive, PageSize] - val firstNumber: PositiveInt = 1 +opaque type PageNumber <: Int = PositiveInt +object PageNumber extends RefinedTypeOps[Int, Positive, PageNumber]: - extension (self: PositiveInt) - inline def succ: PositiveInt = PositiveInt.applyUnsafe(self + 1) - inline def toInt: Int = self - inline def max(other: PositiveInt): PositiveInt = + def fromString(value: String): Either[String, PageNumber] = + value.toIntOption.toRight(s"$value is not an int") >>= PageNumber.either + + extension (self: PageNumber) + inline def succ: PageNumber = PageNumber.applyUnsafe(self + 1) + inline def toInt: Int = self + inline def max(other: PageNumber): PageNumber = if self > other then self else other From 9ca039d4438546eadda445cea62e7b6d6dace4db Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 9 Jun 2024 16:11:57 +0700 Subject: [PATCH 6/6] Unify PlayerId --- modules/api/src/main/scala/providers.scala | 7 +++++-- modules/api/src/main/smithy/_global.smithy | 9 +++++++++ .../src/main/scala/service.federation.scala | 2 +- .../backend/src/main/scala/service.player.scala | 7 +++---- modules/crawler/src/main/scala/Crawler.scala | 4 ++-- modules/db/src/main/scala/Db.scala | 8 +++++--- modules/db/src/test/scala/DbSuite.scala | 16 ++++++++-------- modules/domain/src/main/scala/Domain.scala | 2 -- modules/types/src/main/scala/FederationId.scala | 4 ++-- modules/types/src/main/scala/PositiveInt.scala | 3 +++ 10 files changed, 38 insertions(+), 24 deletions(-) diff --git a/modules/api/src/main/scala/providers.scala b/modules/api/src/main/scala/providers.scala index 1fcb128..d20df90 100644 --- a/modules/api/src/main/scala/providers.scala +++ b/modules/api/src/main/scala/providers.scala @@ -14,6 +14,9 @@ object providers: given RefinementProvider[PageSizeFormat, Int, fide.types.PageSize] = Refinement.drivenBy(PageSize.either, _.toInt) + given RefinementProvider[PlayerIdFormat, Int, fide.types.PlayerId] = + Refinement.drivenBy(fide.types.PlayerId.either, _.value) + given RefinementProvider[RatingFormat, Int, fide.types.Rating] = Refinement.drivenBy(fide.types.Rating.either, _.value) @@ -26,5 +29,5 @@ object providers: (b: NonEmptySet[A]) => b.value ) - given RefinementProvider.Simple[smithy.api.Length, fide.types.NonEmptySet[fide.spec.PlayerId]] = - RefinementProvider.lengthConstraint(x => x.value.size) + given [A]: RefinementProvider.Simple[smithy.api.Length, fide.types.NonEmptySet[A]] = + RefinementProvider.lengthConstraint(_.value.size) diff --git a/modules/api/src/main/smithy/_global.smithy b/modules/api/src/main/smithy/_global.smithy index e4a20b6..db2e4b3 100644 --- a/modules/api/src/main/smithy/_global.smithy +++ b/modules/api/src/main/smithy/_global.smithy @@ -26,6 +26,15 @@ structure InternalServerError { message: String } +@trait(selector: "integer") +@refinement( + targetType: "fide.types.PlayerId" + providerImport: "fide.spec.providers.given" +) +structure PlayerIdFormat {} + +@PlayerIdFormat +@unwrap integer PlayerId @trait(selector: "string") diff --git a/modules/backend/src/main/scala/service.federation.scala b/modules/backend/src/main/scala/service.federation.scala index 50c63b3..0de1060 100644 --- a/modules/backend/src/main/scala/service.federation.scala +++ b/modules/backend/src/main/scala/service.federation.scala @@ -5,7 +5,7 @@ import cats.syntax.all.* import fide.db.Db import fide.domain.Models.Pagination import fide.domain.{ FederationSummary, Models } -import fide.spec.{ FederationId as _, PageNumber as _, PageSize as _, Rating as _, * } +import fide.spec.{ FederationId as _, PageNumber as _, PageSize as _, PlayerId as _, Rating as _, * } import fide.types.* import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger diff --git a/modules/backend/src/main/scala/service.player.scala b/modules/backend/src/main/scala/service.player.scala index b21f79b..5df912d 100644 --- a/modules/backend/src/main/scala/service.player.scala +++ b/modules/backend/src/main/scala/service.player.scala @@ -4,7 +4,7 @@ import cats.effect.* import cats.syntax.all.* import fide.db.Db import fide.domain.Models -import fide.spec.{ FederationId as _, PageNumber as _, PageSize as _, Rating as _, * } +import fide.spec.{ FederationId as _, PageNumber as _, PageSize as _, PlayerId as _, Rating as _, * } import fide.types.* import io.github.arainko.ducktape.* import org.typelevel.log4cats.Logger @@ -53,7 +53,7 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: ) override def getPlayerById(id: PlayerId): IO[GetPlayerByIdOutput] = - db.playerById(id.value) + db.playerById(id) .handleErrorWith: e => error"Error in getPlayerById: $id, $e" *> IO.raiseError(InternalServerError("Internal server error")) @@ -62,7 +62,7 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: _.transform.pure[IO] override def getPlayerByIds(ids: NonEmptySet[PlayerId]): IO[GetPlayerByIdsOutput] = - db.playersByIds(ids.value.map(_.value)) + db.playersByIds(ids.value) .handleErrorWith: e => error"Error in getPlayersByIds: $ids, $e" *> IO.raiseError(InternalServerError("Internal server error")) @@ -70,7 +70,6 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]: .map(GetPlayerByIdsOutput.apply) object PlayerTransformers: - given Transformer.Derived[Int, PlayerId] = Transformer.Derived.FromFunction(PlayerId.apply) given Transformer.Derived[OffsetDateTime, Timestamp] = Transformer.Derived.FromFunction(Timestamp.fromOffsetDateTime) diff --git a/modules/crawler/src/main/scala/Crawler.scala b/modules/crawler/src/main/scala/Crawler.scala index 31d5090..55e1fda 100644 --- a/modules/crawler/src/main/scala/Crawler.scala +++ b/modules/crawler/src/main/scala/Crawler.scala @@ -5,7 +5,7 @@ import cats.effect.IO import cats.syntax.all.* import fide.db.{ Db, KVStore } import fide.domain.* -import fide.types.{ FederationId, Rating } +import fide.types.* import org.http4s.* import org.http4s.client.Client import org.http4s.implicits.* @@ -76,7 +76,7 @@ object Downloader: def rating(start: Int, end: Int): Option[Rating] = string(start, end) >>= Rating.fromString for - id <- number(0, 15) + id <- number(0, 15) >>= PlayerId.option name <- string(15, 76).map(_.filterNot(_.isDigit).trim) if name.sizeIs > 2 title = string(84, 89) >>= Title.apply diff --git a/modules/db/src/main/scala/Db.scala b/modules/db/src/main/scala/Db.scala index 02b5d6a..7276687 100644 --- a/modules/db/src/main/scala/Db.scala +++ b/modules/db/src/main/scala/Db.scala @@ -101,6 +101,7 @@ private object Codecs: // copy from https://github.com/Iltotore/iron/blob/main/skunk/src/io.github.iltotore.iron/skunk.scala for skunk 1.0.0 import io.github.iltotore.iron.* + import io.github.iltotore.iron.constraint.all.* /** Explicit conversion for refining a [[Codec]]. Decodes to the underlying type then checks the constraint. * @@ -125,6 +126,7 @@ private object Codecs: val sex: Codec[Sex] = `enum`[Sex](_.value, Sex.apply, Type("sex")) val ratingCodec: Codec[Rating] = int4.refined[RatingConstraint].imap(Rating.apply)(_.value) val federationIdCodec: Codec[FederationId] = text.refined[NonEmpty].imap(FederationId.apply)(_.value) + val playerIdCodec: Codec[PlayerId] = int4.refined[Positive].imap(PlayerId.apply)(_.value) val otherTitleArr: Codec[Arr[OtherTitle]] = Codec.array( @@ -136,7 +138,7 @@ private object Codecs: val otherTitles: Codec[List[OtherTitle]] = otherTitleArr.opt.imap(_.fold(Nil)(_.toList))(Arr(_*).some) val insertPlayer: Codec[InsertPlayer] = - (int4 *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: federationIdCodec.opt) + (playerIdCodec *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: federationIdCodec.opt) .to[InsertPlayer] val newFederation: Codec[NewFederation] = @@ -152,7 +154,7 @@ private object Codecs: (federationIdCodec *: text *: int4 *: stats *: stats *: stats).to[FederationSummary] val playerInfo: Codec[PlayerInfo] = - (int4 *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: timestamptz *: timestamptz *: federationInfo.opt) + (playerIdCodec *: text *: title.opt *: title.opt *: otherTitles *: ratingCodec.opt *: ratingCodec.opt *: ratingCodec.opt *: sex.opt *: int4.opt *: bool *: timestamptz *: timestamptz *: federationInfo.opt) .to[PlayerInfo] private object Sql: @@ -178,7 +180,7 @@ private object Sql: """.command lazy val playerById: Query[PlayerId, PlayerInfo] = - sql"$allPlayersFragment WHERE p.id = $int4".query(playerInfo) + sql"$allPlayersFragment WHERE p.id = $playerIdCodec".query(playerInfo) lazy val playersByFederationId: Query[FederationId, PlayerInfo] = sql"$allPlayersFragment WHERE p.federation_id = $federationIdCodec".query(playerInfo) diff --git a/modules/db/src/test/scala/DbSuite.scala b/modules/db/src/test/scala/DbSuite.scala index dfb5442..3529b1a 100644 --- a/modules/db/src/test/scala/DbSuite.scala +++ b/modules/db/src/test/scala/DbSuite.scala @@ -23,7 +23,7 @@ object DbSuite extends SimpleIOSuite: private def resource: Resource[IO, Db] = resourceP.map(_._1) val newPlayer1 = NewPlayer( - 1, + PlayerId(1), "John", Title.GM.some, Title.WGM.some, @@ -37,7 +37,7 @@ object DbSuite extends SimpleIOSuite: ) val newPlayer2 = NewPlayer( - 2, + PlayerId(2), "Jane", Title.GM.some, Title.WGM.some, @@ -63,7 +63,7 @@ object DbSuite extends SimpleIOSuite: .use(_.upsert(newPlayer1, newFederation.some).map(_ => expect(true))) test("create players success"): - val player2 = newPlayer1.copy(name = "Jane", id = 2) + val player2 = newPlayer1.copy(name = "Jane", id = PlayerId(2)) resource .use: _.upsert(List(newPlayer1 -> newFederation.some, player2 -> none)).map(_ => expect(true)) @@ -72,7 +72,7 @@ object DbSuite extends SimpleIOSuite: resource.use: db => for _ <- db.upsert(newPlayers) - result <- db.playerById(1) + result <- db.playerById(PlayerId(1)) found = result.get yield expect( found.to[NewPlayer] == newPlayer1 && found.federation.get.to[NewFederation] == newFederation @@ -82,7 +82,7 @@ object DbSuite extends SimpleIOSuite: resource.use: db => for _ <- db.upsert(newPlayers) - result <- db.playerById(2) + result <- db.playerById(PlayerId(2)) found = result.get yield expect( found.to[NewPlayer] == newPlayer2 && found.federation.isEmpty @@ -95,7 +95,7 @@ object DbSuite extends SimpleIOSuite: for _ <- db.upsert(newPlayer1, newFederation.some) _ <- db.upsert(player2, federation2.some) - result <- db.playerById(1) + result <- db.playerById(PlayerId(1)) found = result.get yield expect( found.to[NewPlayer] == player2 && found.federation.get.to[NewFederation] == federation2 @@ -115,7 +115,7 @@ object DbSuite extends SimpleIOSuite: ) test("allPlayers with default filter"): - val player2 = newPlayer1.copy(id = 2, name = "A") + val player2 = newPlayer1.copy(id = PlayerId(2), name = "A") resource.use: db => for _ <- db.upsert(newPlayer1, none) @@ -137,7 +137,7 @@ object DbSuite extends SimpleIOSuite: resource.use: db => for _ <- db.upsert(newPlayers) - players <- db.playersByIds(Set(2, 3)) + players <- db.playersByIds(Set(PlayerId(2), PlayerId(3))) yield expect( players.length == 1 && players.head.to[NewPlayer] == newPlayer2 && players.head.federation.isEmpty ) diff --git a/modules/domain/src/main/scala/Domain.scala b/modules/domain/src/main/scala/Domain.scala index 9d583c7..bcd5ec2 100644 --- a/modules/domain/src/main/scala/Domain.scala +++ b/modules/domain/src/main/scala/Domain.scala @@ -6,8 +6,6 @@ import fide.types.* import java.time.OffsetDateTime -type PlayerId = Int - enum Title(val value: String): case GM extends Title("GM") case IM extends Title("IM") diff --git a/modules/types/src/main/scala/FederationId.scala b/modules/types/src/main/scala/FederationId.scala index 0635264..e7082eb 100644 --- a/modules/types/src/main/scala/FederationId.scala +++ b/modules/types/src/main/scala/FederationId.scala @@ -20,5 +20,5 @@ object FederationId: either(value).toOption extension (str: FederationId) - inline def toStr: String = str - inline def value = rtc.value(str) + inline def toStr: String = str + inline def value: IronType[String, NonEmpty] = rtc.value(str) diff --git a/modules/types/src/main/scala/PositiveInt.scala b/modules/types/src/main/scala/PositiveInt.scala index 15eb098..26cb1bc 100644 --- a/modules/types/src/main/scala/PositiveInt.scala +++ b/modules/types/src/main/scala/PositiveInt.scala @@ -20,3 +20,6 @@ object PageNumber extends RefinedTypeOps[Int, Positive, PageNumber]: inline def toInt: Int = self inline def max(other: PageNumber): PageNumber = if self > other then self else other + +opaque type PlayerId <: Int = PositiveInt +object PlayerId extends RefinedTypeOps[Int, Positive, PlayerId]