9. Zmontowanie Aplikacji
Na zakończenie zaaplikujemy zdobytą wiedzę do naszej przykładowej aplikacji i zaimplementujemy klienta oraz serwer HTTP za pomocą czysto funkcyjnej biblioteki http4s.
Kod źródłowy drone-dynamic-agents jest dostępny wraz z kodem źródłowym tej książki na https://github.com/fommil/fpmortals
w folderze examples. Obecność przy komputerze w trakcie lektury tego rozdziału nie jest co prawda obowiązkowa,
ale wielu czytelników może zechcieć śledzić kod źródłowy wraz z tekstem tego rozdziału.
Niektóre części aplikacji pozostały niezaimplementowane i pozostawione jako ćwiczenie dla czytelnika. Więcej
instrukcji znajdziesz w README.
9.1 Przegląd
Nasza główna aplikacja wymaga jedynie implementacji algebry DynAgents.
trait DynAgents[F[_]] {
def initial: F[WorldView]
def update(old: WorldView): F[WorldView]
def act(world: WorldView): F[WorldView]
}
Mamy już taką implementację w postaci DynAgentsModule, ale wymaga ona implementacji algebr Drone i Machines,
które z kolei wymagają algebr JsonClient, LocalClock i Oauth2, itd., itd., itd.
Przydatnym bywa spojrzenie z lotu ptaka na wszystkie algebry, moduły i interpretery naszej aplikacji. Oto jak ułożony jest nasz kod źródłowy:
├── dda
│ ├── algebra.scala
│ ├── DynAgents.scala
│ ├── main.scala
│ └── interpreters
│ ├── DroneModule.scala
│ └── GoogleMachinesModule.scala
├── http
│ ├── JsonClient.scala
│ ├── OAuth2JsonClient.scala
│ ├── encoding
│ │ ├── UrlEncoded.scala
│ │ ├── UrlEncodedWriter.scala
│ │ ├── UrlQuery.scala
│ │ └── UrlQueryWriter.scala
│ ├── oauth2
│ │ ├── Access.scala
│ │ ├── Auth.scala
│ │ ├── Refresh.scala
│ │ └── interpreters
│ │ └── BlazeUserInteraction.scala
│ └── interpreters
│ └── BlazeJsonClient.scala
├── os
│ └── Browser.scala
└── time
├── Epoch.scala
├── LocalClock.scala
└── Sleep.scala
Sygnatury wszystkich algebr możemy podsumować jako
trait Sleep[F[_]] {
def sleep(time: FiniteDuration): F[Unit]
}
trait LocalClock[F[_]] {
def now: F[Epoch]
}
trait JsonClient[F[_]] {
def get[A: JsDecoder](
uri: String Refined Url,
headers: IList[(String, String)]
): F[A]
def post[P: UrlEncodedWriter, A: JsDecoder](
uri: String Refined Url,
payload: P,
headers: IList[(String, String)]
): F[A]
}
trait Auth[F[_]] {
def authenticate: F[CodeToken]
}
trait Access[F[_]] {
def access(code: CodeToken): F[(RefreshToken, BearerToken)]
}
trait Refresh[F[_]] {
def bearer(refresh: RefreshToken): F[BearerToken]
}
trait OAuth2JsonClient[F[_]] {
// same methods as JsonClient, but doing OAuth2 transparently
}
trait UserInteraction[F[_]] {
def start: F[String Refined Url]
def open(uri: String Refined Url): F[Unit]
def stop: F[CodeToken]
}
trait Drone[F[_]] {
def getBacklog: F[Int]
def getAgents: F[Int]
}
trait Machines[F[_]] {
def getTime: F[Epoch]
def getManaged: F[NonEmptyList[MachineNode]]
def getAlive: F[MachineNode ==>> Epoch]
def start(node: MachineNode): F[Unit]
def stop(node: MachineNode): F[Unit]
}
Zauważ, że niektóre sygnatury z poprzednich rozdziałów zostały przerefaktorowane tak, aby używały typów danych ze Scalaz, skoro już wiemy, że są lepsze od tych z biblioteki standardowej.
Definiowane typy danych to:
@xderiving(Order, Arbitrary)
final case class Epoch(millis: Long) extends AnyVal
@deriving(Order, Show)
final case class MachineNode(id: String)
@deriving(Equal, Show)
final case class CodeToken(token: String, redirect_uri: String Refined Url)
@xderiving(Equal, Show, ConfigReader)
final case class RefreshToken(token: String) extends AnyVal
@deriving(Equal, Show, ConfigReader)
final case class BearerToken(token: String, expires: Epoch)
@deriving(ConfigReader)
final case class OAuth2Config(token: RefreshToken, server: ServerConfig)
@deriving(ConfigReader)
final case class AppConfig(drone: BearerToken, machines: OAuth2Config)
@xderiving(UrlEncodedWriter)
final case class UrlQuery(params: IList[(String, String)]) extends AnyVal
Oraz typeklasy:
@typeclass trait UrlEncodedWriter[A] {
def toUrlEncoded(a: A): String Refined UrlEncoded
}
@typeclass trait UrlQueryWriter[A] {
def toUrlQuery(a: A): UrlQuery
}
Derywujemy przydatne typeklasy używając scalaz-deriving oraz Magnolii. ConfigReader pochodzi
z biblioteki pureconfig i służy do odczytywania konfiguracji z plików HOCON.
Przeanalizujmy też, bez zaglądania do implementacji, jak kształtuje się graf zależności w DynAgentsModule.
final class DynAgentsModule[F[_]: Applicative](
D: Drone[F],
M: Machines[F]
) extends DynAgents[F] { ... }
final class DroneModule[F[_]](
H: OAuth2JsonClient[F]
) extends Drone[F] { ... }
final class GoogleMachinesModule[F[_]](
H: OAuth2JsonClient[F]
) extends Machines[F] { ... }
Dwa moduły implementują OAuth2JsonClient, jeden używa algebry Refresh dla usług Google’a, a drugi niewygasającego BearerToken dla `Drone’a.
final class OAuth2JsonClientModule[F[_]](
token: RefreshToken
)(
H: JsonClient[F],
T: LocalClock[F],
A: Refresh[F]
)(
implicit F: MonadState[F, BearerToken]
) extends OAuth2JsonClient[F] { ... }
final class BearerJsonClientModule[F[_]: Monad](
bearer: BearerToken
)(
H: JsonClient[F]
) extends OAuth2JsonClient[F] { ... }
Do tej pory widzieliśmy wymagania względem F mówiące, że musimy dostarczyć Applicative[F], Monad[F]
oraz MonadState[F, BearerToken]. Wszystkie te wymagania spełnia StateT[Task, BearerToken, ?] co pozwala
nam uczynić ten typ kontekstem naszej aplikacji.
Jednak niektóre algebry mają interpretery używające bezpośrednio typu Task
final class LocalClockTask extends LocalClock[Task] { ... }
final class SleepTask extends Sleep[Task] { ... }
Przypomnijmy, że nasze algebry mogą dostarczać liftM w swoich obiektach towarzyszących (patrz rozdział
7.4 na temat Biblioteki Transformatorów Monad), co pozwala nam wynieść LocalClock[Task] do pożądanego
StateT[Task, BearerToken, ?] czyniąc wszystko idealnie spójnym.
Niestety to nie koniec. Sprawy komplikują się na następnej warstwie, gdyż JsonClient posiada interpreter używający
innego kontekstu
final class BlazeJsonClient[F[_]](H: Client[Task])(
implicit
F: MonadError[F, JsonClient.Error],
I: MonadIO[F, Throwable]
) extends JsonClient[F] { ... }
object BlazeJsonClient {
def apply[F[_]](
implicit
F: MonadError[F, JsonClient.Error],
I: MonadIO[F, Throwable]
): Task[JsonClient[F]] = ...
}
Zauważ, że konstruktor BlazeJsonClient zwraca Task[JsonClient[F]], a nie JsonClient[F].
Dzieje się tak, ponieważ stworzenie tego klient powoduje efekt w postaci utworzenia mutowalnej puli połączeń
zarządzanej wewnętrznie przez http4s.
Nie możemy zapomnieć o dostarczeniu RefreshToken dla GoogleMachinesModule. Moglibyśmy zrzucić to zadanie
na użytkownika, ale jesteśmy mili i dostarczamy osobną aplikację, która używając algebr Auth i Access rozwiązuje
ten problem. Implementacje AuthModule i AccessModule niosą ze sobą kolejne wymagania, ale na szczęście żadnych
zmian co do kontekstu F[_].
final class AuthModule[F[_]: Monad](
config: ServerConfig
)(
I: UserInteraction[F]
) extends Auth[F] { ... }
final class AccessModule[F[_]: Monad](
config: ServerConfig
)(
H: JsonClient[F],
T: LocalClock[F]
) extends Access[F] { ... }
final class BlazeUserInteraction private (
pserver: Promise[Void, Server[Task]],
ptoken: Promise[Void, String]
) extends UserInteraction[Task] { ... }
object BlazeUserInteraction {
def apply(): Task[BlazeUserInteraction] = ...
}
Interpreter algebry UserInteraction jest najbardziej skomplikowanym elementem naszego
kodu. Startuje on serwer HTTP, prosi użytkownika o otworzenie strony w przeglądarce,
odbiera wywołanie zwrotne w serwerze i zwraca wynik jednocześnie zakańczając pracę serwera
w bezpieczny sposób.
Zamiast używać StateT do zarządzania tym stanem użyliśmy typu Promise (pochodzącego z ioeffect).
Powinniśmy zawsze używać Promise (lub IORef) zamiast StateT, gdy piszemy interpreter oparty o
IO, gdyż pozwala nam to opanować abstrakcje. Gdybyśmy użyli StateT, to nie tylko miałoby to wpływ
na całą aplikacje, ale również powodowałoby wyciek lokalnego stanu do głównej aplikacji, która musiałaby
przejąc odpowiedzialność za dostarczenie inicjalnej wartości. W tym wypadku nie moglibyśmy użyć StateT
również dlatego, że potrzebujemy możliwości “czekania”, którą daje nam jedynie Promise.
9.2 Main
Najbrzydsza część FP pojawia się, gdy musimy sprawić by wszystkie monady się zgadzały. Najczęściej ma to miejsce
w punkcie wejściowym naszej aplikacji, czyli klasie Main.
Nasza główna pętla wyglądała tak
state = initial()
while True:
state = update(state)
state = act(state)
Dobra wiadomość jest taka, że teraz ten kod będzie wyglądał tak:
for {
old <- F.get
updated <- A.update(old)
changed <- A.act(updated)
_ <- F.put(changed)
_ <- S.sleep(10.seconds)
} yield ()
gdzie F przechowuje stan świata w MonadState[F, WoldView]. Możemy zamknąć ten fragment
w metodzie .step i powtarzać ją w nieskończoność wywołując .step[F].forever[Unit].
W tym momencie mamy do wyboru dwa podejścia i oba omówimy. Pierwszym i jednocześnie najprostszym
jest skonstruowanie stosu monad kompatybilnego ze wszystkimi algebrami, a każda z nich musi definiować liftM, aby
wynieść ją do większego stosu.
Kod, który chcemy napisać dla trybu jednorazowego uwierzytelnienia to
def auth(name: String): Task[Unit] = {
for {
config <- readConfig[ServerConfig](name + ".server")
ui <- BlazeUserInteraction()
auth = new AuthModule(config)(ui)
codetoken <- auth.authenticate
client <- BlazeJsonClient
clock = new LocalClockTask
access = new AccessModule(config)(client, clock)
token <- access.access(codetoken)
_ <- putStrLn(s"got token: $token")
} yield ()
}.run
gdzie .readConfig i .putStrLn to wywołania funkcji z bibliotek. Możemy potraktować je jako interpretery
oparte o Task dla algebr odczytujących konfigurację i wypisująca ciąg znaków.
Ten kod jednak się nie kompiluje z dwóch powodów. Po pierwsze, musimy zdecydować jak będzie wyglądał nasz
stos monad. Konstruktor BlazeJsonClient zwraca Task, ale JsonClientwymaga MonadError[..., JsonClient.Error],
co można rozwiązać za pomocą EitherT. Możemy więc skonstruować nasz stos dla całej konstrukcji for jako
type H[a] = EitherT[Task, JsonClient.Error, a]
Niestety, oznacza to, że musimy wywołać .liftM dla wszystkiego co zwraca Task,
co dodaje dość dużo boilerplate’u. Niestety metoda liftM nie przyjmuje typów o kształcie
H[_] tylko H[_[_]. _], więc musimy stworzyć alias, który pomoże kompilatorowi:
type HT[f[_], a] = EitherT[f, JsonClient.Error, a]
type H[a] = HT[Task, a]
możemy teraz wywołać .liftM[HT] kiedy dostajemy Task
for {
config <- readConfig[ServerConfig](name + ".server").liftM[HT]
ui <- BlazeUserInteraction().liftM[HT]
auth = new AuthModule(config)(ui)
codetoken <- auth.authenticate.liftM[HT]
client <- BlazeJsonClient[H].liftM[HT]
clock = new LocalClockTask
access = new AccessModule(config)(client, clock)
token <- access.access(codetoken)
_ <- putStrLn(s"got token: $token").liftM[HT]
} yield ()
Ale nasz kod nadal się nie kompiluje. Tym razem dlatego, że clock jest typu LocalClock[Task] a AccessModule wymaga LocalClock[H].
Dodajmy więc potrzebny boilerplate .liftM do obiektu towarzyszącego LocalClock i wynieśmy całą algebrę
clock = LocalClock.liftM[Task, HT](new LocalClockTask)
Wreszcie wszystko się kompiluje!
Drugie podejście do zmontowywania aplikacji jest bardziej złożone, ale niezbędne, gdy pojawiają się konflikty w stosie monad, tak jak w naszej głównej pętli. Jeśli przeanalizujemy wymagania, zobaczymy, że potrzebujemy poniższych instancji:
-
MonadError[F, JsonClient.Error]wJsonClient -
MonadState[F, BearerToken]wOAuth2JsonClient -
MonadState[F, WorldView]w głównej pętli
Niestety, dwa wymagania na MonadState są ze sobą sprzeczne. Moglibyśmy
skonstruować typ danych, który przechowuje cały stan aplikacji, ale byłaby to
cieknąca abstrakcja. Zamiast tego zagnieździmy konstrukcję for i dostarczymy stan tam gdzie jest potrzebny
Musimy teraz przemyśleć trzy warstwy, które nazwiemy F, G i H
type HT[f[_], a] = EitherT[f, JsonClient.Error, a]
type GT[f[_], a] = StateT[f, BearerToken, a]
type FT[f[_], a] = StateT[f, WorldView, a]
type H[a] = HT[Task, a]
type G[a] = GT[H, a]
type F[a] = FT[G, a]
Teraz złe wieści: liftM obsługuje tylko jedną warstwę na raz. Jeśli mamy Task[A], a chcemy
uzyskać F[A] to musimy przejść przez wszystkie kroki i wywołać ta.liftM[HT].liftM[GT].liftM[FT].
Podobnie, gdy wynosimy algebry, musimy zawołać liftM wielokrotnie. Aby uzyskać Sleep[F], musimy napisać
val S: Sleep[F] = {
import Sleep.liftM
liftM(liftM(liftM(new SleepTask)))
}
a żeby dostać LocalClock[G] robimy dwa wyniesienia
val T: LocalClock[G] = {
import LocalClock.liftM
liftM(liftM(new LocalClockTask))
}
Główna aplikacja wygląda więc tak:
def agents(bearer: BearerToken): Task[Unit] = {
...
for {
config <- readConfig[AppConfig]
blaze <- BlazeJsonClient[G]
_ <- {
val bearerClient = new BearerJsonClientModule(bearer)(blaze)
val drone = new DroneModule(bearerClient)
val refresh = new RefreshModule(config.machines.server)(blaze, T)
val oauthClient =
new OAuth2JsonClientModule(config.machines.token)(blaze, T, refresh)
val machines = new GoogleMachinesModule(oauthClient)
val agents = new DynAgentsModule(drone, machines)
for {
start <- agents.initial
_ <- {
val fagents = DynAgents.liftM[G, FT](agents)
step(fagents, S).forever[Unit]
}.run(start)
} yield ()
}.eval(bearer).run
} yield ()
}
gdzie zewnętrzna pętla używa Task, środkowa G a wewnętrzna F.
Wywołania .run(start) oraz .eval(bearer) dostarczają inicjalny stan dla części bazujących na StateT.
.run z kolei pokazuje błędy zgromadzone w EitherT.
Na koniec wołamy te dwie aplikacji z naszej instancji SafeApp
object Main extends SafeApp {
def run(args: List[String]): IO[Void, ExitStatus] = {
if (args.contains("--machines")) auth("machines")
else agents(BearerToken("<invalid>", Epoch(0)))
}.attempt[Void].map {
case \/-(_) => ExitStatus.ExitNow(0)
case -\/(err) => ExitStatus.ExitNow(1)
}
}
i uruchamiamy ją!
> runMain fommil.dda.Main --machines
[info] Running (fork) fommil.dda.Main --machines
...
[info] Service bound to address /127.0.0.1:46687
...
[info] Created new window in existing browser session.
...
[info] Headers(Host: localhost:46687, Connection: keep-alive, User-Agent: Mozilla/5.0 ...)
...
[info] POST https://www.googleapis.com/oauth2/v4/token
...
[info] got token: "<elided>"
Hurra!
9.3 Blaze
Server i klienta HTTP zaimplementujemy z użyciem zewnętrznej biblioteki http4s. Interpretery
dla odpowiednich algebr dostaną w związku z tym prefiks Blaze, gdyż tak też nazywa się
właściwy komponent tej biblioteki.
Dodajemy poniższe zależności
val http4sVersion = "0.18.16"
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-blaze-server" % http4sVersion,
"org.http4s" %% "http4s-blaze-client" % http4sVersion
)
9.3.1 BlazeJsonClient
Będziemy potrzebować kilku dodatkowych importów
import org.http4s
import org.http4s.{ EntityEncoder, MediaType }
import org.http4s.headers.`Content-Type`
import org.http4s.client.Client
import org.http4s.client.blaze.{ BlazeClientConfig, Http1Client }
Moduł Client może być podsumowany jako
final class Client[F[_]](
val shutdown: F[Unit]
)(implicit F: MonadError[F, Throwable]) {
def fetch[A](req: Request[F])(f: Response[F] => F[A]): F[A] = ...
...
}
gdzie Request i Response to typy danych:
final case class Request[F[_]](
method: Method
uri: Uri,
headers: Headers,
body: EntityBody[F]
) {
def withBody[A](a: A)
(implicit F: Monad[F], A: EntityEncoder[F, A]): F[Request[F]] = ...
...
}
final case class Response[F[_]](
status: Status,
headers: Headers,
body: EntityBody[F]
)
składające się z
final case class Headers(headers: List[Header])
final case class Header(name: String, value: String)
final case class Uri( ... )
object Uri {
// not total, only use if `s` is guaranteed to be a URL
def unsafeFromString(s: String): Uri = ...
...
}
final case class Status(code: Int) {
def isSuccess: Boolean = ...
...
}
type EntityBody[F[_]] = fs2.Stream[F, Byte]
EntityBody jest aliasem na typ Stream z biblioteki fs2. Możemy rozumieć go jako
leniwy strumień danych wykonujący efekty, bazujący na wyciąganiu danych (pull-based). Zaimplementowany jest jako monada Free z
dodatkowym łapaniem wyjątków i obsługą przerwań. Stream przyjmuje dwa parametry typu: typ efektów i typ zawartości. Dodatkowo posiada
wewnątrz wydajną reprezentację pozwalającą na łączenie danych (batching), więc przykładowo, używając Stream[F, Byte] tak naprawdę mamy do czynienia
z opakowaną tablicą Array[Byte], która przybywa do nas za pośrednictwem sieci.
Musimy przekonwertować nasze reprezentacje nagłówków i URLi na wersje wymagane przez http4s:
def convert(headers: IList[(String, String)]): http4s.Headers =
http4s.Headers(
headers.foldRight(List[http4s.Header]()) {
case ((key, value), acc) => http4s.Header(key, value) :: acc
}
)
def convert(uri: String Refined Url): http4s.Uri =
http4s.Uri.unsafeFromString(uri.value) // we already validated our String
Obie nasze metody .get i .post muszą przekonwertować instancję Response pochodząca z http4s na typ A.
Możemy wydzielić tę logikę do pojedynczej funkcji .handler
import JsonClient.Error
final class BlazeJsonClient[F[_]] private (H: Client[Task])(
implicit
F: MonadError[F, Error],
I: MonadIO[F, Throwable]
) extends JsonClient[F] {
...
def handler[A: JsDecoder](resp: http4s.Response[Task]): Task[Error \/ A] = {
if (!resp.status.isSuccess)
Task.now(JsonClient.ServerError(resp.status.code).left)
else
for {
text <- resp.body.through(fs2.text.utf8Decode).compile.foldMonoid
res = JsParser(text)
.flatMap(_.as[A])
.leftMap(JsonClient.DecodingError(_))
} yield res
}
}
through(fs2.text.utf8Decode) pozwala przekonwertować Stream[Task, Byte] na Stream[Task, String].
compile.foldMonoid interpretuje strumień z użyciem naszego Taska i łączy wyniki przy pomocy
Monoid[String], zwracając Task[String].
Następnie parsujemy string do JSONa, a JsDecoder[A]dostarcza potrzebny rezultat.
Oto nasza implementacja .get
def get[A: JsDecoder](
uri: String Refined Url,
headers: IList[(String, String)]
): F[A] =
I.liftIO(
H.fetch(
http4s.Request[Task](
uri = convert(uri),
headers = convert(headers)
)
)(handler[A])
)
.emap(identity)
Trzeba przyznać, że jest to w 100% łączenie istniejących kawałków. Konwertujemy nasze typy wejściowe
do http4s.Request, wołamy .fetch na kliencie przekazując nasz handler, w odpowiedzi dostajemy
Task[Error \/ A]. Musimy jednak zwrócić F[A], więc używamy MonadIO.liftIO do stworzenia
F[Error \/ ], na którym z kolei wywołujemy emap umieszczając błąd wewnątrz F.
Niestety, próba skompilowania tego kodu zakończy się porażką, a błąd będzie wyglądał mniej więcej tak:
[error] BlazeJsonClient.scala:95:64: could not find implicit value for parameter
[error] F: cats.effect.Sync[scalaz.ioeffect.Task]
Coś na temat zaginionego kota?
Dzieje się tak, gdyż http4s używa innej biblioteki wspomagającej FP niż Scalaz. Na szczęście scalaz-ioeffect dostarcza
warstwę dodającą kompatybilność z tą biblioteką, a projekt shims definiuje
niezauważalne (zazwyczaj) niejawne konwersje. Tak więc możemy sprawić, że nasz kod zacznie się kompilować dodając zależności
libraryDependencies ++= Seq(
"com.codecommit" %% "shims" % "1.4.0",
"org.scalaz" %% "scalaz-ioeffect-cats" % "2.10.1"
)
i importy
import shims._
import scalaz.ioeffect.catz._
Implementacja .post jest podobna, ale musimy jeszcze dostarczyć instancję
EntityEncoder[Task, String Refined UrlEncoded]
Na szczęście typeklasa EntityEncoder pozwala nam łatwo ją wyderywować z istniejącego
enkodera dla typu String
implicit val encoder: EntityEncoder[Task, String Refined UrlEncoded] =
EntityEncoder[Task, String]
.contramap[String Refined UrlEncoded](_.value)
.withContentType(
`Content-Type`(MediaType.`application/x-www-form-urlencoded`)
)
Jedyną różnicą między .get i .post jest sposób w jaki konstruujemy http4s.Request
http4s.Request[Task](
method = http4s.Method.POST,
uri = convert(uri),
headers = convert(headers)
)
.withBody(payload.toUrlEncoded)
Ostatnim fragmentem układanki jest konstruktor, w którym wywołujemy Http1Client przekazując obiekt konfiguracyjny
object BlazeJsonClient {
def apply[F[_]](
implicit
F: MonadError[F, JsonClient.Error],
I: MonadIO[F, Throwable]
): Task[JsonClient[F]] =
Http1Client(BlazeClientConfig.defaultConfig).map(new BlazeJsonClient(_))
}
9.3.2 BlazeUserInteraction
Musimy uruchomić serwer HTTP, co jest dużo łatwiejsze niż może się wydawać. Po pierwsze, importy
import org.http4s._
import org.http4s.dsl._
import org.http4s.server.Server
import org.http4s.server.blaze._
Następnie musimy utworzyć dsl dla naszego typu efektów, z którego zaimportujemy zawartość
private val dsl = new Http4sDsl[Task] {}
import dsl._
Teraz możemy używać dsla http4s do obsługi żądań HTTP. Zamiast opisywać wszystko co jest możliwe zaimplementujemy po prostu pojedynczą końcówkę (endpoint), która przypomina każdy inny DSL HTTP
private object Code extends QueryParamDecoderMatcher[String]("code")
private val service: HttpService[Task] = HttpService[Task] {
case GET -> Root :? Code(code) => ...
}
Każde dopasowanie musi zwrócić Task[Response[Task]]. W naszym przypadku chcemy wziąć code i
ukończyć obietnicę ptoken:
final class BlazeUserInteraction private (
pserver: Promise[Throwable, Server[Task]],
ptoken: Promise[Throwable, String]
) extends UserInteraction[Task] {
...
private val service: HttpService[Task] = HttpService[Task] {
case GET -> Root :? Code(code) =>
ptoken.complete(code) >> Ok(
"That seems to have worked, go back to the console."
)
}
...
}
ale zdefiniowanie logiki nie wystarczy, musimy jeszcze uruchomić nasz serwer, co też zrobimy
używając BlazeBuilder
private val launch: Task[Server[Task]] =
BlazeBuilder[Task].bindHttp(0, "localhost").mountService(service, "/").start
Przypisanie do portu 0 sprawia, że system operacyjny użyje tymczasowego portu, który możemy
odczytać z pola server.address.
Nasza implementacja .start i .stop jest więc bardzo prosta
def start: Task[String Refined Url] =
for {
server <- launch
updated <- pserver.complete(server)
_ <- if (updated) Task.unit
else server.shutdown *> fail("server was already running")
} yield mkUrl(server)
def stop: Task[CodeToken] =
for {
server <- pserver.get
token <- ptoken.get
_ <- IO.sleep(1.second) *> server.shutdown
} yield CodeToken(token, mkUrl(server))
private def mkUrl(s: Server[Task]): String Refined Url = {
val port = s.address.getPort
Refined.unsafeApply(s"http://localhost:${port}/")
}
private def fail[A](s: String): String =
Task.fail(new IOException(s) with NoStackTrace)
Uśpienie wątku na 1.second jest niezbędne, aby uniknąć wyłączenia serwera zanim odpowiedź trafi z powrotem
do przeglądarki. Z wydajnością współbieżności IO nie ma żartów!
W końcu, aby utworzyć BlazeUserInteraction potrzebuje jedynie dwóch niezainicjalizowanych obietnic
object BlazeUserInteraction {
def apply(): Task[BlazeUserInteraction] = {
for {
p1 <- Promise.make[Void, Server[Task]].widenError[Throwable]
p2 <- Promise.make[Void, String].widenError[Throwable]
} yield new BlazeUserInteraction(p1, p2)
}
}
Mogliśmy użyć IO[Void, ?], ale skoro reszta naszej aplikacji używa Task (czyli IO[Throwable, ?]), wywołujemy
.widenError, aby nie wprowadzać zbędnego boilerplate’u.
9.4 Podziękowania
I to tyle! Gratulujemy dotarcia do końca podróży.
Jeśli w trakcie jej trwania nauczyłeś się czegoś, to proszę, powiedz o tym swoim znajomym. Ta książka nie ma działu marketingu, więc jest to jedyny sposób w jaki potencjalni czytelnicy mogą się o niej dowiedzieć.
Aby zaangażować się w rozwój Scalaz wystarczy dołączyć do pokoju na gitterze. Stamtąd możesz zadawać pytania, pomagać innym (teraz jesteś ekspertem!) i pomagać w tworzeniu kolejnych wersji biblioteki.