I have created tapir endpoint:
val getEndpoint = endpoint.get
.securityIn(auth.bearer[String]())
.in("players" / path[PlayerId]("playerId"))
.in(query[PlayerRequest]("query"))
.errorOut(someErrors)
Now, I would like to read all passed values: bearer token, playerId and query. So I created ZIO server logic:
PlayersEndpoint.getEndpoint.zServerLogic { case (playerId, query) =>
//some logic to do...
}
It works fine, but whithout bearer token. Here I could not read bearer token. I tried to change it to something like:
PlayersEndpoint.getEndpoint.zServerSecurityLogic{ case (token) =>
//do smth with token
}.zServerLogic { case (playerId, query) =>
//some logic to do...
}
But it did not work. I would like to read all 3 values and decide about what to do after checking token. Docs and examples are very poor and do not show how to read tokens from tapir. Do you know how I should do it correctly?
You can try this:
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.server.Router
import sttp.tapir.server.http4s.ztapir.ZHttp4sServerInterpreter
import sttp.tapir.ztapir._
import zio._
import zio.interop.catz._
import cats.implicits._
object TapirExample extends ZIOAppDefault {
type Env = Any
// fake type
type PlayerId = Int
type PlayerRequest = Long
def authLogic(token: String): ZIO[Any, String, String] = {
if (token != "secret") ZIO.fail("user not login")
else ZIO.succeed(token)
}
val authEndpoint: ZPartialServerEndpoint[Any, String, String, Unit, String, Unit, Any] =
endpoint
.securityIn(auth.bearer[String]())
.errorOut(stringBody)
.zServerSecurityLogic(authLogic)
val getEndpoint =
authEndpoint
.get
.in("players" / path[PlayerId]("playerId"))
.in(query[PlayerRequest]("query"))
.out(stringBody)
.serverLogic(token => queryTuple => getLogic(token, queryTuple._1, queryTuple._2))
val getRoute =
ZHttp4sServerInterpreter()
.from(
List(
getEndpoint.widen[Env]
)
).toRoutes
def getLogic(token: String, playerId: PlayerId, request: PlayerRequest): ZIO[Any, Nothing, String] = {
ZIO.succeed(
s"""
| token: $token
| id: $playerId
| request: $request
|""".stripMargin)
}
override def run: ZIO[Any with ZIOAppArgs with Scope, Any, Any] = {
val io = ZIO.runtime[Env].flatMap { _ =>
for {
_ <- ZIO.executor.flatMap(executor =>
BlazeServerBuilder[ZIO[Env, Throwable, *]]
.withExecutionContext(executor.asExecutionContext)
.bindHttp(9090, "0.0.0.0")
.withHttpApp(
Router(
"/" -> (
getRoute
)
).orNotFound
)
.serve
.compile
.drain)
} yield ()
}
io
}
}
And the library version is
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := "2.13.8"
lazy val root = (project in file("."))
.settings(
name := "playground"
)
val tapirVersion = "1.2.4"
val allDependency = Seq(
"com.softwaremill.sttp.tapir" %% "tapir-http4s-server-zio" % tapirVersion,
"org.http4s" %% "http4s-blaze-server" % "0.23.13",
)
libraryDependencies ++= allDependency
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full)
Test by curl
$ curl --location --request GET 'localhost:9090/players/233?query=334' \
--header 'Authorization: Bearer wrongtoken'
user not login
$ curl --location --request GET 'localhost:9090/players/233?query=334' \
--header 'Authorization: Bearer secret'
token: secret
id: 233
request: 334
refer:
https://tapir.softwaremill.com/en/latest/server/logic.html#re-usable-security-logic
https://github.com/softwaremill/tapir/blob/5332cf64d2ae912146e281e759b750d21bd1db55/examples/src/main/scala/sttp/tapir/examples/ZioPartialServerLogicHttp4s.scala#L22-L39
I don't have IDE to give you the exact code but the idea is that the return of your zServerSecurityLogic can be used in the following zServerLogic.
Usually you'd validate the token in the zServerSecurityLogic and return some kind of User value that you can use as input in zServerLogic.
But you can also "do nothing" in the security logic and just passthrough the token so that it's available in the main logic.
Related
I tried directly getting body of JSON in code which I then want to convert to Avro to write to a kafka topic.
Here is my code with case class:
import zhttp.http._
import zio._
import zhttp.http.{Http, Method, Request, Response, Status}
import zhttp.service.Server
import zio.json._
import zio.kafka._
import zio.kafka.serde.Serde
import zio.schema._
case class Experiments(experimentId: String,
variantId: String,
accountId: String,
deviceId: String,
date: Int)
//case class RootInterface (events: Seq[Experiments])
object Experiments {
implicit val encoder: JsonEncoder[Experiments] = DeriveJsonEncoder.gen[Experiments]
implicit val decoder: JsonDecoder[Experiments] = DeriveJsonDecoder.gen[Experiments]
implicit val codec: JsonCodec[Experiments] = DeriveJsonCodec.gen[Experiments]
implicit val schema: Schema[Experiments] = DeriveSchema.gen
}
object HttpService {
def apply(): Http[ExpEnvironment, Throwable, Request, Response] =
Http.collectZIO[Request] {
case req#(Method.POST -> !! / "zioCollector") =>
val c = req.body.asString.map(_.fromJson[Experiments])
for {
u <- req.body.asString.map(_.fromJson[Experiments])
r <- u match {
case Left(e) =>
ZIO.debug(s"Failed to parse the input: $e").as(
Response.text(e).setStatus(Status.BadRequest)
)
case Right(u) =>
println(s"$u + =====")
ExpEnvironment.register(u)
.map(id => Response.text(id))
}
}
yield r
}
}
// val experimentsSerde: Serde[Any, Experiments] = Serde.string.inmapM { string =>
// //desericalization
// ZIO.fromEither(string.fromJson[Experiments].left.map(errorMessage => new RuntimeException(errorMessage)))
// } { theMatch =>
// ZIO.effect(theMatch.toJson)
//
// }
object ZioCollectorMain extends ZIOAppDefault {
def run: ZIO[Environment with ZIOAppArgs with Scope, Any, Any] = {
Server.start(
port = 9001,
http = HttpService()).provide(ZLayerExp.layer)
}
}
I'm looking into Zio-Json but no success yet, any help is appreciated !
We could also schema something to get the avro generic record
here's my json :
{
"experimentId": "abc",
"variantId": "123",
"accountId": "123",
"deviceId": "123",
"date": 1664544365
}
This function works for me in Scala 3 (sorry, I didn't include all the code but it should be enough):
import zio.*
import zio.Console.printLine
import zhttp.http.*
import zhttp.service.Server
import zio.json.*
...
case class Experiments(experimentId: String,
variantId: String,
accountId: String,
deviceId: String,
date: Int)
//case class RootInterface (events: Seq[Experiments])
object Experiments:
implicit val encoder: JsonEncoder[Experiments] = DeriveJsonEncoder.gen[Experiments]
implicit val decoder: JsonDecoder[Experiments] = DeriveJsonDecoder.gen[Experiments]
implicit val codec: JsonCodec[Experiments] = DeriveJsonCodec.gen[Experiments]
val postingExperiment: Http[Any, Throwable, Request, Response] =
Http.collectZIO[Request] {
case req#(Method.POST -> !! / "zioCollector") =>
//val c = req.body.asString.map(_.fromJson[Experiments])
val experimentsZIO = req.body.asString.map(_.fromJson[Experiments])
for {
experimentsOrError <- experimentsZIO
response <- experimentsOrError match {
case Left(e) => ZIO.debug(s"Failed to parse the input: $e").as(
Response.text(e).setStatus(Status.BadRequest)
)
case Right(experiments) => ZIO.succeed(Response.json(experiments.toJson))
}
} yield response
}
I modified your code slightly (you didn't post your ExpEnvironment class), and it returns back the object posted to the url.
and the test code is:
import sttp.client3.{SimpleHttpClient, UriContext, basicRequest}
object TestExperiments:
def main(args: Array[String]): Unit =
val client = SimpleHttpClient()
//post request
val request = basicRequest
.post(uri"http://localhost:9009/zioCollector")
.body("{ \"experimentId\": \"abc\", \"variantId\": \"123\", \"accountId\": \"123\", \"deviceId\": \"123\", \"date\": 1664544365 }")
val response = client.send(request)
println(response.body)
val invalidJsonRequest = basicRequest
.post(uri"http://localhost:9009/zioCollector")
.body("{ \"experimentId\": \"abc\", \"variantId\": \"123\", \"accountId\": \"123\", \"deviceId\": \"123\", \"date\": 1664544365 ") // missing the closing bracket
val invalidJsonResponse = client.send(invalidJsonRequest)
println(invalidJsonResponse.body)
You have to add: "com.softwaremill.sttp.client3" %% "core" % "3.8.3" to your sbt file.
build.sbt:
ThisBuild / scalaVersion := "3.2.0"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "TestSpeed"
ThisBuild / organizationName := "example"
lazy val root = (project in file("."))
.settings(
name := "TestZio",
libraryDependencies ++= Seq(
"dev.zio" %% "zio" % "2.0.2",
"dev.zio" %% "zio-json" % "0.3.0-RC11",
"io.d11" %% "zhttp" % "2.0.0-RC11",
"dev.zio" %% "zio-test" % "2.0.2" % Test,
"com.softwaremill.sttp.client3" %% "core" % "3.8.3" % Test
),
testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework")
)
I didn't include anything related to avro because I am not familiar with it.
Suppose we have the following request:
curl --location --request POST 'localhost:8080/api' \
--header 'Content-Type: multipart/form-data' \
--form 'field1=value1' \
--form 'field2=value2'
The request handler below gets the whole entity, but I am struggling to see how I could instead get value1 and value2.
val requestHandler: Flow[HttpRequest, HttpResponse, _] = Flow[HttpRequest].mapAsync(1) {
case HttpRequest(HttpMethods.POST, Uri.Path("/api"), _, entity, _) =>
val entityTextFuture: Future[String] = entity.toStrict(3 seconds).map(_.data.utf8String)
entityTextFuture.flatMap { text =>
Future(HttpResponse(
StatusCodes.OK,
entity = text
))
}
}
Important: I have to use the Akka HTTP low-level server API, so I cannot use routes.
Many thanks for your time and help in advance!
If all you want are the string values in the form data, you just need to unmarshal to StrictForm, and then unmarshal each of the field values as strings.
Here's a proof of concept Ammonite script that responds to your curl request with value1 & value2:
import $ivy.`com.typesafe.akka::akka-actor:2.6.3`
import $ivy.`com.typesafe.akka::akka-stream:2.6.3`
import $ivy.`com.typesafe.akka::akka-http:10.1.11`
import scala.concurrent.Future
import akka.actor.ActorSystem
import akka.stream.scaladsl.Flow
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.http.scaladsl.common.StrictForm
implicit val system = ActorSystem()
implicit val ec = system.dispatcher
val requestHandler: Flow[HttpRequest, HttpResponse, _] = Flow[HttpRequest].mapAsync(1) {
case HttpRequest(HttpMethods.POST, Uri.Path("/api"), _, entity, _) =>
for {
strictForm <- Unmarshal(entity).to[StrictForm]
fieldsSeq <- Future.traverse(strictForm.fields) {
case (n, v) => Unmarshal(v).to[String].map(n -> _)
}
fields = fieldsSeq.toMap
response = fields("field1") + " & " + fields("field2")
} yield HttpResponse(StatusCodes.OK, entity = response)
}
Http().bindAndHandle(requestHandler, "localhost", 8080)
As I'm delving more into FP, I'm curious about the 'best' way to store settings that are loaded from config files. I've just been creating a case class with all the necessary config variables and setting that on app start. I then pass that case class into whatever function requires info from it.
However, it seems quite annoying especially when that settings case class has to propagate through many functions. Is there a better way to do this?
Reader monad provides a way of propagating configuration without having to pass it as a parameter throughout all functions that need it. Contrast the following two implementations:
Config available from context via Reader[Config, String]
object ConfigFunctional extends App {
case class Config(username: String, password: String, host: String)
def encodeCredentials: Reader[Config, String] = Reader { config =>
Base64.getEncoder.encodeToString(s"${config.username}:${config.password}".getBytes())
}
def basicAuth(credentials: String): Reader[Config, String] = Reader { config =>
Http(s"${config.host}/HTTP/Basic/")
.header("Authorization", s"Basic $credentials")
.asString
.body
}
def validateResponse(body: String): Reader[Config, Either[String, String]] = Reader { _ =>
if (body.contains("Your browser made it"))
Right("Credentials are valid!")
else
Left("Wrong credentials")
}
def program: Reader[Config, Either[String, String]] = for {
credentials <- encodeCredentials
response <- basicAuth(credentials)
validation <- validateResponse(response)
} yield validation
val config = Config("guest", "guest", "https://jigsaw.w3.org")
println(program.run(config))
}
Config passed in as an argument
object ConfigImperative extends App {
case class Config(username: String, password: String, host: String)
def encodeCredentials(config: Config): String = {
Base64.getEncoder.encodeToString(s"${config.username}:${config.password}".getBytes())
}
def basicAuth(credentials: String, config: Config): String = {
Http(s"${config.host}/HTTP/Basic/")
.header("Authorization", s"Basic $credentials")
.asString
.body
}
def validateResponse(body: String): Either[String, String] = {
if (body.contains("Your browser made it"))
Right("Credentials are valid!")
else
Left("Wrong credentials")
}
def program(config: Config): Either[String, String] = {
val credentials = encodeCredentials(config)
val response = basicAuth(credentials, config)
val validation = validateResponse(response)
validation
}
val config = Config("guest", "guest", "https://jigsaw.w3.org")
println(program(config))
}
Both implementations should output Right(Credentials are valid!), however notice how in the first implementation config: Config is not a method parameter, for example, contrast encodeCredentials:
def encodeCredentials: Reader[Config, String]
def encodeCredentials(config: Config): String
Config appears in the return type instead of being a parameter. We can interpret this as meaning
"When encodeCredentials runs in the context that provides a
Config, then it will produce a String result."
The "context" here is represented by Reader monad.
Furthermore, notice how Config is not a parameter even in the main business logic
def program: Reader[Config, Either[String, String]] = for {
credentials <- encodeCredentials
response <- basicAuth(credentials)
validation <- validateResponse(response)
} yield validation
We let the methods evaluate in the context containing Config via run function:
program.run(config)
To run above examples we need the following dependencies
scalacOptions += "-Ypartial-unification",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "1.6.0",
"org.scalaj" %% "scalaj-http" % "2.4.1"
)
and imports
import cats.data.Reader
import java.util.Base64
import scalaj.http.Http
i have to check either the password field is alphanumeric or not and if not it will throw custom Validation error i am using play-framework but getting compile time error
value checkAlphanumeric is not a member of
play.api.libs.json.Reads[String]
- value checkAlphanumeric is not a member of
play.api.libs.json.Reads[String]
i am unable to achive my desired outcome i am doing it wrong that's why i need here is the code
case class userUserSignUpValidation(firstName: String,
lastName: String,
email: String,
password: String) extends Serializable
object UserSignUpValidation {
val allNumbers = """\d*""".r
val allLetters = """[A-Za-z]*""".r
var validationErrorMsg=""
implicit val readDirectUser: Reads[DirectUserSignUpValidation] = (
(JsPath \ "firstName").read(minLength[String](1)) and
(JsPath \ "lastName").read(minLength[String](1)) and
(JsPath \ "email").read(email) and
(JsPath \ "password").read(minLength[String](8)checkAlphanumeric))(UserSignUpValidation.apply _)
def checkAlphanumeric(password:String)={
val allNumbers = """\d*""".r
val allLetters = """[A-Za-z]*""".r
val errors = password match {
case allNumbers() => Seq(ValidationError("Password is all numbers"))
case allLetters() => Seq(ValidationError("Password is all letters"))
case _ => Nil
}
}
i am getting the error on this line
(JsPath \ "password").read(minLength[String](8)checkAlphanumeric))(UserSignUpValidation.apply _)
what is the right way to implement an above scenario
Your problem is that you cannot use your checkAlphanumeric method that way. What you probably want is a filter on the Reads, so I would suggest doing something as follow (I changed the implementation for the check, using pre-existing methods):
implicit val readDirectUser: Reads[DirectUserSignUpValidation] = (
(JsPath \ "firstName").read(minLength[String](1)) and
(JsPath \ "lastName").read(minLength[String](1)) and
(JsPath \ "email").read(email) and
(JsPath \ "password").read(minLength[String](8).
filterNot(ValidationError("Password is all numbers"))(_.forall(_.isDigit)).
filterNot(ValidationError("Password is all letters"))(_.forall(_.isLetter))
)) (UserSignUpValidation.apply _)
Well I wonder why don't you run validations inside case class?
case class userUserSignUpValidation(firstName: String,
lastName: String,
email: String,
password: String) {
assert(!password.matches("""[A-Za-z]*""") && !password.matches("""\d*"""), "Invalid password")
// have other field validations here
}
And in you UserSignUpValidation use a implicit formatter like this:
object UserSignUpValidation {
implicit val userFormatter = JSON.format[userUserSignUpValidation]
// de-serialization code here
}
I want to hit GET api request using latest play framework in scala. I think use case for ws service is changed in Play 2.5+. I am using following code.
class ApiResult #Inject() (ws: WSClient) {
def getApiResult(param1: String, param2: String)= {
var response = ws.url(s"ip-address/getApiResult/${param1}/${param2}").withRequestTimeout(5000.millis).get()
var i = 0
while(i < 2 && !response.isCompleted ){
response = ws.url(s"ip-address/getSmsCredit/${param1}/${param2}").withRequestTimeout(5000.millis).get()
i += 1
}
val result = response.onComplete {
case Success(jsonOutput) =>
val x= (jsonOutput.json \ "x").getOrElse(Json.toJson(-1)).as[Double]
val y= (jsonOutput.json \ "y").getOrElse(Json.toJson(-1)).as[Double]
val z= (jsonOutput.json \ "z").getOrElse(Json.toJson(-1)).as[Double]
SomeCaseClass(x, y, z)
case _ =>
Logger.info("Error")
SomeCaseClass(0.00, 0.00, 0.00)
}
result
}
}
I want to basically return SomeCaseClass , when I will call getApiResult in some other function. Also, how do I call this function there, since this function using param WSClient
There are so many wrong things in your code, but let's assume you want a Future to return from this method you should do something like this:
def getApiResult(param1: String, param2: String): Future[SomeCaseClass] = {
ws.url(s"ip-address/getApiResult/${param1}/${param2}")
.withRequestTimeout(5000.millis).get()
.flatMap(_ => //Will get here after the first request finished
ws.url(s"ip-address/getSmsCredit/${param1}/${param2}")
.withRequestTimeout(5000.millis).get()
.map(_.body.validate[SomeCaseClass]
.asOpt
.getOrElse(SomeCaseClass(0.00, 0.00, 0.00)))
)
}
You will need to implement an implicit reader:
object SomeCaseClass{
implicit val reads: Reads[SomeCaseClass] = (
(__ \ "x").read[Double] and
(__ \ "y").read[Double] and
(__ \ "z").read[Double]
)(SomeCaseClass.apply _)
}
If you want the actual SomeCaseClass, you whould do Await on getApiResult.