Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use typesafe domain #125

Merged
merged 6 commits into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
28 changes: 18 additions & 10 deletions modules/api/src/main/scala/providers.scala
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
package fide.spec

import cats.syntax.all.*
import fide.spec.*
import fide.types.*
import fide.types.{ NonEmptySet, PageNumber, PageSize }
import smithy4s.*

object providers:
given RefinementProvider[PageFormat, String, Natural] =
Refinement.drivenBy(Natural.fromString, _.toString)
given RefinementProvider[PageFormat, String, PageNumber] =
Refinement.drivenBy(PageNumber.fromString, _.toString)

given RefinementProvider.Simple[smithy.api.Range, Natural] =
RefinementProvider.rangeConstraint(x => x: Int)
given RefinementProvider.Simple[smithy.api.Range, fide.types.PageSize] =
RefinementProvider.rangeConstraint(_.toInt)

given RefinementProvider[PageSizeFormat, Int, Natural] =
Refinement.drivenBy(Natural.either, identity)
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)

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,
(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)
33 changes: 30 additions & 3 deletions modules/api/src/main/smithy/_global.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,49 @@ 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")
@refinement(
targetType: "fide.types.FederationId"
providerImport: "fide.spec.providers.given"
)
structure FederationIdFormat {}

@unwrap
@FederationIdFormat
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")
@refinement(
targetType: "fide.types.Natural"
targetType: "fide.types.PageNumber"
providerImport: "fide.spec.providers.given"
)
structure PageFormat {}

@trait(selector: "integer")
@refinement(
targetType: "fide.types.Natural"
targetType: "fide.types.PageSize"
providerImport: "fide.spec.providers.given"
)
structure PageSizeFormat { }
Expand Down
27 changes: 13 additions & 14 deletions modules/backend/src/main/scala/service.federation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.Natural
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
import org.typelevel.log4cats.syntax.*
Expand All @@ -18,8 +18,8 @@ class FederationServiceImpl(db: Db)(using Logger[IO]) extends FederationService[

override def getFederationPlayersById(
id: FederationId,
page: Natural,
pageSize: Natural,
page: PageNumber,
pageSize: PageSize,
sortBy: Option[SortBy],
order: Option[Order],
isActive: Option[Boolean],
Expand All @@ -31,14 +31,14 @@ 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,
Models.RatingRange(standardMin.map(_.value), standardMax.map(_.value)),
Models.RatingRange(rapidMin.map(_.value), rapidMax.map(_.value)),
Models.RatingRange(blitzMin.map(_.value), blitzMax.map(_.value)),
id.value.some
Models.RatingRange(standardMin, standardMax),
Models.RatingRange(rapidMin, rapidMax),
Models.RatingRange(blitzMin, blitzMax),
id.some
)
name
.fold(db.allPlayers(sorting, paging, filter))(db.playersByName(_, sorting, paging, filter))
Expand All @@ -53,18 +53,18 @@ 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"))
.flatMap:
_.fold(IO.raiseError(FederationNotFound(id)))(_.transform.pure)

override def getFederationsSummary(
page: Natural,
pageSize: Natural
page: PageNumber,
pageSize: PageSize
): 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"))
Expand All @@ -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]
21 changes: 9 additions & 12 deletions modules/backend/src/main/scala/service.player.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.{ FederationId as _, PageNumber as _, PageSize as _, PlayerId as _, Rating as _, * }
import fide.types.*
import io.github.arainko.ducktape.*
import org.typelevel.log4cats.Logger
Expand All @@ -18,8 +18,8 @@ class PlayerServiceImpl(db: Db)(using Logger[IO]) extends PlayerService[IO]:
import PlayerTransformers.*

override def getPlayers(
page: Natural,
pageSize: Natural,
page: PageNumber,
pageSize: PageSize,
sortBy: Option[SortBy],
order: Option[Order],
isActive: Option[Boolean],
Expand All @@ -31,13 +31,13 @@ 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,
Models.RatingRange(standardMin.map(_.value), standardMax.map(_.value)),
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use mixing to avoid duplicated code

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
Expand All @@ -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"))
Expand All @@ -62,17 +62,14 @@ 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"))
.map(_.map(p => p.id.toString -> p.transform).toMap)
.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] =
Transformer.Derived.FromFunction(Timestamp.fromOffsetDateTime)

Expand Down
18 changes: 11 additions & 7 deletions modules/crawler/src/main/scala/Crawler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import cats.effect.IO
import cats.syntax.all.*
import fide.db.{ Db, KVStore }
import fide.domain.*
import fide.types.*
import org.http4s.*
import org.http4s.client.Client
import org.http4s.implicits.*
Expand Down Expand Up @@ -69,10 +70,13 @@ 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)
id <- number(0, 15) >>= PlayerId.option
name <- string(15, 76).map(_.filterNot(_.isDigit).trim)
if name.sizeIs > 2
title = string(84, 89) >>= Title.apply
Expand All @@ -81,16 +85,16 @@ 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,
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
Expand Down
51 changes: 39 additions & 12 deletions modules/db/src/main/scala/Db.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.*

Expand Down Expand Up @@ -98,8 +99,34 @@ 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.*
import io.github.iltotore.iron.constraint.all.*

/** 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 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(
Expand All @@ -111,23 +138,23 @@ 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)
(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] =
(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 *: int4.opt *: int4.opt *: int4.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:
Expand All @@ -153,10 +180,10 @@ 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 = $text".query(playerInfo)
sql"$allPlayersFragment WHERE p.federation_id = $federationIdCodec".query(playerInfo)

lazy val upsertFederation: Command[NewFederation] =
sql"$insertIntoFederation VALUES ($newFederation) $onConflictDoNothing".command
Expand Down Expand Up @@ -188,7 +215,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)
Expand Down Expand Up @@ -240,10 +267,10 @@ 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.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}"
Expand Down
Loading