From e3cfa9c61311eb776375f44843458b92f2552501 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Sun, 9 Jun 2024 14:17:32 +0700 Subject: [PATCH] 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 | 5 +- 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, 75 insertions(+), 48 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..b27b6a7 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,8 @@ 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) = line.substring(start, end).trim.some.filter(_.nonEmpty) + def number(start: Int, end: Int): Option[Rating] = string(start, end).flatMap(Rating.fromString) for id <- number(0, 15) name <- string(15, 76).map(_.filterNot(_.isDigit).trim) 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