Skip to content

Commit

Permalink
Disable significant indentation
Browse files Browse the repository at this point in the history
  • Loading branch information
kubukoz committed Nov 29, 2023
1 parent 84ff584 commit 1446b71
Show file tree
Hide file tree
Showing 22 changed files with 638 additions and 166 deletions.
3 changes: 2 additions & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
runner.dialect = scala3
version = "3.5.0"
runner.dialectOverride.allowSignificantIndentation = false
version = "3.7.2"
maxColumn = 140
align.preset = some
align.tokens.add = [
Expand Down
37 changes: 29 additions & 8 deletions app/src/main/scala/com/kubukoz/next/Analysis.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,46 @@ import cats.implicits.*
import cats.effect.kernel.MonadCancel

trait Analysis[F[_]] {
def getAnalysis(trackUri: TrackUri): F[GetAudioAnalysisOutput]

def getAnalysis(
trackUri: TrackUri
): F[GetAudioAnalysisOutput]

}

object Analysis {

def apply[F[_]](implicit F: Analysis[F]): Analysis[F] = F
def apply[F[_]](
implicit F: Analysis[F]
): Analysis[F] = F

def instance[F[_]: SpotifyApi]: Analysis[F] = new {

def instance[F[_]: SpotifyApi]: Analysis[F] = new:
def getAnalysis(trackUri: TrackUri): F[GetAudioAnalysisOutput] = SpotifyApi[F].getAudioAnalysis(trackUri.id)
def getAnalysis(
trackUri: TrackUri
): F[GetAudioAnalysisOutput] = SpotifyApi[F].getAudioAnalysis(trackUri.id)

}

// "best-effort" cache of the underlying instance. Only has one slot and no concurrency guarantees.
def cached[F[_]: Ref.Make](underlying: Analysis[F])(using MonadCancel[F, ?]): F[Analysis[F]] = {
def cached[F[_]: Ref.Make](
underlying: Analysis[F]
)(
using MonadCancel[F, ?]
): F[Analysis[F]] = {
enum State {
case Initial
case HasResult(key: TrackUri, result: GetAudioAnalysisOutput)
case HasResult(
key: TrackUri,
result: GetAudioAnalysisOutput
)
}

Ref[F].of(State.Initial: State).map { state =>
new:
def getAnalysis(trackUri: TrackUri): F[GetAudioAnalysisOutput] = MonadCancel[F].uncancelable { poll =>
new {
def getAnalysis(
trackUri: TrackUri
): F[GetAudioAnalysisOutput] = MonadCancel[F].uncancelable { poll =>
poll(state.get.flatMap {
case State.HasResult(`trackUri`, result) => result.pure[F]
case _ => underlying.getAnalysis(trackUri)
Expand All @@ -37,6 +57,7 @@ object Analysis {
.as(analysis)
}
}
}
}
}

Expand Down
41 changes: 32 additions & 9 deletions app/src/main/scala/com/kubukoz/next/ConfigLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,40 @@ import fs2.io.file.Path
import fs2.io.file.Flags

trait ConfigLoader[F[_]] {
def saveConfig(config: Config): F[Unit]

def saveConfig(
config: Config
): F[Unit]

def loadConfig: F[Config]
}

object ConfigLoader {
def apply[F[_]](using F: ConfigLoader[F]): ConfigLoader[F] = F

def apply[F[_]](
using F: ConfigLoader[F]
): ConfigLoader[F] = F

def cached[F[_]: Ref.Make: FlatMap]: ConfigLoader[F] => F[ConfigLoader[F]] =
underlying =>
underlying.loadConfig.flatMap(Ref[F].of(_)).map { ref =>
new ConfigLoader[F] {
def saveConfig(config: Config): F[Unit] = underlying.saveConfig(config) *> ref.set(config)
def saveConfig(
config: Config
): F[Unit] = underlying.saveConfig(config) *> ref.set(config)
val loadConfig: F[Config] = ref.get
}
}

def withCreateFileIfMissing[F[_]: UserOutput: Console: MonadThrow](configPath: Path): ConfigLoader[F] => ConfigLoader[F] = {
def withCreateFileIfMissing[F[_]: UserOutput: Console: MonadThrow](
configPath: Path
): ConfigLoader[F] => ConfigLoader[F] = {

val validInput = "Y"

def askToCreateFile(originalException: NoSuchFileException): F[Config] =
def askToCreateFile(
originalException: NoSuchFileException
): F[Config] =
for {
_ <- UserOutput[F].print(UserMessage.ConfigFileNotFound(configPath, validInput))
_ <- Console[F].readLine.map(_.trim).ensure(originalException)(_.equalsIgnoreCase(validInput))
Expand All @@ -64,11 +77,17 @@ object ConfigLoader {
UserOutput[F].print(UserMessage.SavedConfig(configPath))
}

def saveConfig(config: Config): F[Unit] = underlying.saveConfig(config)
def saveConfig(
config: Config
): F[Unit] = underlying.saveConfig(config)
}
}

def default[F[_]: Files: MonadThrow](configPath: Path)(using fs2.Compiler[F, F]): ConfigLoader[F] =
def default[F[_]: Files: MonadThrow](
configPath: Path
)(
using fs2.Compiler[F, F]
): ConfigLoader[F] =
new ConfigLoader[F] {

private val createOrOverwriteFile: Pipe[F, Byte, Nothing] = bytes =>
Expand All @@ -77,7 +96,9 @@ object ConfigLoader {
Files[F].writeAll(configPath, Flags.Write)
)

def saveConfig(config: Config): F[Unit] =
def saveConfig(
config: Config
): F[Unit] =
fs2
.Stream
.emit(config)
Expand All @@ -97,6 +118,8 @@ object ConfigLoader {

}

extension [F[_]: Applicative](cl: ConfigLoader[F]) def configAsk: Config.Ask[F] = Config.askLiftF(cl.loadConfig)
extension [F[_]: Applicative](
cl: ConfigLoader[F]
) def configAsk: Config.Ask[F] = Config.askLiftF(cl.loadConfig)

}
25 changes: 20 additions & 5 deletions app/src/main/scala/com/kubukoz/next/Login.scala
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,32 @@ import fs2.io.net.Network

trait Login[F[_]] {
def server: F[OAuth.Tokens]
def refreshToken(token: RefreshToken): F[Token]

def refreshToken(
token: RefreshToken
): F[Token]

}

object Login {
def apply[F[_]](using F: Login[F]): Login[F] = F

def apply[F[_]](
using F: Login[F]
): Login[F] = F

def ember[F[_]: UserOutput: Config.Ask: Network: Async](
oauth: OAuth[F]
): Login[F] =
new Login[F] {

def refreshToken(token: RefreshToken): F[Token] = oauth.refreshToken(token)
def refreshToken(
token: RefreshToken
): F[Token] = oauth.refreshToken(token)

def mkServer(config: Config, route: HttpRoutes[F]) =
def mkServer(
config: Config,
route: HttpRoutes[F]
) =
EmberServerBuilder
.default[F]
.withHttpApp(route.orNotFound)
Expand Down Expand Up @@ -57,7 +69,10 @@ object Login {

}

def routes[F[_]: MonadThrow](saveCode: OAuth.Code => F[Unit], finishServer: F[Unit]): HttpRoutes[F] = {
def routes[F[_]: MonadThrow](
saveCode: OAuth.Code => F[Unit],
finishServer: F[Unit]
): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
import dsl.*

Expand Down
32 changes: 26 additions & 6 deletions app/src/main/scala/com/kubukoz/next/LoginProcess.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,20 @@ trait LoginProcess[F[_]] {
}

object LoginProcess {
def apply[F[_]](using F: LoginProcess[F]): LoginProcess[F] = F

def apply[F[_]](
using F: LoginProcess[F]
): LoginProcess[F] = F

def instance[F[_]: UserOutput: ConfigLoader: Monad](
loginAlg: Login[F],
tokensLens: Lens[Config, (Option[Token], Option[RefreshToken])]
tokensLens: Lens[
Config,
(
Option[Token],
Option[RefreshToken]
)
]
): LoginProcess[F] = new LoginProcess[F] {

def login: F[Unit] = for {
Expand All @@ -31,19 +40,28 @@ object LoginProcess {

}

given [F[_]: Applicative]: Monoid[LoginProcess[F]] with
given [F[_]: Applicative]: Monoid[LoginProcess[F]] with {

override val empty: LoginProcess[F] = new LoginProcess[F] {
val login: F[Unit] = Applicative[F].unit
}

override def combine(x: LoginProcess[F], y: LoginProcess[F]): LoginProcess[F] = new LoginProcess[F] {
override def combine(
x: LoginProcess[F],
y: LoginProcess[F]
): LoginProcess[F] = new LoginProcess[F] {
val login: F[Unit] = x.login *> y.login
}

extension [F[_]: Monad](loginProcess: LoginProcess[F])
}

extension [F[_]: Monad](
loginProcess: LoginProcess[F]
) {

def orRefresh(refresh: RefreshTokenProcess[F]): LoginProcess[F] = new LoginProcess[F] {
def orRefresh(
refresh: RefreshTokenProcess[F]
): LoginProcess[F] = new LoginProcess[F] {

override val login: F[Unit] =
refresh
Expand All @@ -55,4 +73,6 @@ object LoginProcess {

}

}

}
18 changes: 15 additions & 3 deletions app/src/main/scala/com/kubukoz/next/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ enum Choice {
case Login
case SkipTrack
case DropTrack
case FastForward(percentage: Int)

case FastForward(
percentage: Int
)

case JumpSection
case Switch
case Move
Expand Down Expand Up @@ -74,7 +78,13 @@ object Main extends CommandIOApp(name = "spotify-next", header = "spotify-next:
rawClient: Client[F],
oauthKernel: OAuth.Kernel[F]
)(
tokensLens: Lens[Config, (Option[Token], Option[RefreshToken])]
tokensLens: Lens[
Config,
(
Option[Token],
Option[RefreshToken]
)
]
) = {
val login = Login.ember[F](OAuth.fromKernel[F](rawClient, oauthKernel))

Expand Down Expand Up @@ -134,7 +144,9 @@ object Main extends CommandIOApp(name = "spotify-next", header = "spotify-next:
.map(_.split("\\s+").toList)
.onComplete(fs2.Stream.exec(IO.println("Bye!")))

def reportError(e: Throwable): IO[Unit] =
def reportError(
e: Throwable
): IO[Unit] =
Console[IO].errorln("Command failed with exception: ") *> IO(e.printStackTrace())

fs2.Stream.exec(IO.println("Loading REPL...")) ++
Expand Down
Loading

0 comments on commit 1446b71

Please sign in to comment.