-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial commit * Hello World and Echo server * Hello World and Echo Server * Create API for receiving POST request #2 Multiple API endpoints with Tapir #3 * Stream Endpoint with fs2 #4 * Include error code other than 404 #5 #5 * #5 and #6 Include error code other than 404 #5 Include Swagger API Docs #6 * Removing unncessary import * Add asserts for testing the result of endpoint request Add asserts for testing the result of endpoint request * #7 Add Error Code for Stream Endpoint Implement the same error code from normal endpoint to the stream endpoint * Update gitignore * Update .gitignore Part 2 * Create package com.headstorm and Test directory #9 * #9, #10, #11, #12 Co-authored-by: plee <[email protected]> Co-authored-by: plee <[email protected]>
- Loading branch information
1 parent
3eacec4
commit bebf933
Showing
7 changed files
with
466 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,5 @@ | ||
*.class | ||
*.log | ||
target/ | ||
.idea/ | ||
project/target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
name := "Truss" | ||
|
||
version := "0.1" | ||
|
||
scalaVersion := "2.12.10" | ||
|
||
val Http4sVersion = "0.21.0" | ||
val CirceVersion = "0.13.0" | ||
val Specs2Version = "4.8.3" | ||
val LogbackVersion = "1.2.3" | ||
|
||
libraryDependencies ++= Seq( | ||
|
||
// Start with this one | ||
"org.tpolecat" %% "doobie-core" % "0.8.6", | ||
|
||
// And add any of these as needed | ||
"org.tpolecat" %% "doobie-h2" % "0.8.6", // H2 driver 1.4.200 + type mappings. | ||
"org.tpolecat" %% "doobie-hikari" % "0.8.6", // HikariCP transactor. | ||
"org.tpolecat" %% "doobie-postgres" % "0.8.6", // Postgres driver 42.2.9 + type mappings. | ||
"org.tpolecat" %% "doobie-quill" % "0.8.6", // Support for Quill 3.4.10 | ||
"org.tpolecat" %% "doobie-specs2" % "0.8.6" % "test", // Specs2 support for typechecking statements. | ||
"org.tpolecat" %% "doobie-scalatest" % "0.8.6" % "test" // ScalaTest support for typechecking statements. | ||
|
||
) | ||
|
||
libraryDependencies ++= Seq( | ||
"org.http4s" %% "http4s-blaze-server" % Http4sVersion, | ||
"org.http4s" %% "http4s-blaze-client" % Http4sVersion, | ||
"org.http4s" %% "http4s-circe" % Http4sVersion, | ||
"org.http4s" %% "http4s-dsl" % Http4sVersion, | ||
"io.circe" %% "circe-generic" % CirceVersion, | ||
"org.specs2" %% "specs2-core" % Specs2Version % "test", | ||
"ch.qos.logback" % "logback-classic" % LogbackVersion | ||
) | ||
|
||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-core" % "0.12.23" | ||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-akka-http-server" % "0.12.23" | ||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % "0.12.23" | ||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % "0.12.23" | ||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-openapi-docs" % "0.12.23" | ||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-openapi-circe-yaml" % "0.12.23" | ||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-akka-http" % "0.12.23" | ||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-sttp-client" % "0.12.23" | ||
libraryDependencies += "com.softwaremill.sttp.client" %% "core" % "2.0.4" | ||
libraryDependencies += "com.softwaremill.sttp" %% "core" % "1.0.2" | ||
libraryDependencies += "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-http4s" % "0.12.23" | ||
|
||
val circeVersion = "0.12.3" | ||
|
||
libraryDependencies ++= Seq( | ||
"io.circe" %% "circe-core", | ||
"io.circe" %% "circe-generic", | ||
"io.circe" %% "circe-parser" | ||
).map(_ % circeVersion) | ||
|
||
val silencerVersion = "1.4.2" | ||
|
||
libraryDependencies += "com.github.ghik" % "silencer-plugin_2.12" % "1.4.2" | ||
libraryDependencies += "net.liftweb" %% "lift-json" % "3.4.1" | ||
|
||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test" | ||
|
||
libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.12.3" | ||
|
||
libraryDependencies ++= Seq( | ||
|
||
// Start with this one | ||
"org.tpolecat" %% "doobie-core" % "0.8.6", | ||
|
||
// And add any of these as needed | ||
"org.tpolecat" %% "doobie-h2" % "0.8.6", // H2 driver 1.4.200 + type mappings. | ||
"org.tpolecat" %% "doobie-hikari" % "0.8.6", // HikariCP transactor. | ||
"org.tpolecat" %% "doobie-postgres" % "0.8.6", // Postgres driver 42.2.9 + type mappings. | ||
"org.tpolecat" %% "doobie-quill" % "0.8.6", // Support for Quill 3.4.10 | ||
"org.tpolecat" %% "doobie-specs2" % "0.8.6" % "test", // Specs2 support for typechecking statements. | ||
"org.tpolecat" %% "doobie-scalatest" % "0.8.6" % "test" // ScalaTest support for typechecking statements. | ||
|
||
) | ||
|
||
scalacOptions += "-Ypartial-unification" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
sbt.version = 1.3.8 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
development { | ||
database { | ||
data-source = "slick.jdbc.DatabaseUrlDataSource" | ||
driver ="org.postgresql.Driver" | ||
url = "jdbc:postgresql:world" | ||
user = "postgres" | ||
password = "*H1m9r4*" | ||
} | ||
server { | ||
host = "localhost" | ||
port = 8888 | ||
} | ||
} | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package com.headstorm | ||
|
||
import RouteGenerator._ | ||
import cats.effect._ | ||
import cats.implicits._ | ||
import com.typesafe.config.ConfigFactory | ||
import javax.sound.sampled.Port | ||
import org.http4s.HttpRoutes | ||
import org.http4s.server.Router | ||
import org.http4s.server.blaze.BlazeServerBuilder | ||
import org.http4s.syntax.kleisli._ | ||
import sttp.tapir.docs.openapi._ | ||
import sttp.tapir.openapi.OpenAPI | ||
import sttp.tapir.openapi.circe.yaml._ | ||
import sttp.tapir.swagger.http4s.SwaggerHttp4s | ||
import pureconfig.generic.auto._ | ||
import pureconfig._ | ||
import sttp.tapir.server.http4s.Http4sServerOptions | ||
|
||
|
||
case class DBDetail(dataSource: String, driver: String, url: String, user: String, password: String) | ||
case class ServerDetail(host: String, port: Int) | ||
|
||
case class Config(database: DBDetail, server: ServerDetail) | ||
|
||
case class DevConfig(development: Config) | ||
|
||
object Main extends App { | ||
|
||
val config = ConfigSource.default.load[DevConfig] | ||
|
||
private val configuration = { | ||
ConfigSource.default.load[DevConfig] match { | ||
case Left(error) => | ||
println(s"There was an error loading the config, shutting down: ${error.toString}") | ||
System.exit(1) | ||
case Right(config) => config | ||
} | ||
} | ||
|
||
val (serverHost: String, | ||
serverPort: Int, | ||
databaseDataSource: String, | ||
databaseDriver: String, | ||
databaseURL: String, | ||
databaseUser: String, | ||
databasePassword: String) = configuration match { | ||
case DevConfig(r) => (r.server.host.toString, | ||
r.server.port, | ||
r.database.dataSource, | ||
r.database.driver, | ||
r.database.url, | ||
r.database.user, | ||
r.database.password) | ||
case _ => println("Error Loading Config") | ||
} | ||
|
||
// the endpoints' routes | ||
val helloWorldInit = new Endpoints("name", "hello", "get") | ||
val helloWorldEndpoint = helloWorldInit.aEndpoint | ||
val helloWorldRoute = helloWorldInit.aRoute | ||
|
||
val byeWorldInit = new Endpoints("name", "bye", "post") | ||
val byeWorldEndpoint = byeWorldInit.aEndpoint | ||
val byeWorldRoute = byeWorldInit.aRoute | ||
|
||
val databaseSelectInit = new DatabaseEndpoints(databaseDataSource, databaseDriver, databaseURL, databaseUser, databasePassword, "name", "dbselect", "select") | ||
val databaseSelectEndpoint = databaseSelectInit.aEndpoint | ||
def databaseSelectRoute[F[_]: Sync](implicit serverOption: Http4sServerOptions[F], fcs: ContextShift[F]) = databaseSelectInit.aRoute | ||
|
||
val databaseInsertInit = new DatabaseEndpoints(databaseDataSource, databaseDriver, databaseURL, databaseUser, databasePassword, "name", "dbinsert", "insert") | ||
val databaseInsertEndpoint = databaseInsertInit.aEndpoint | ||
def databaseInsertRoute[F[_]: Sync](implicit serverOption: Http4sServerOptions[F], fcs: ContextShift[F]) = databaseInsertInit.aRoute | ||
|
||
val databaseUpdateInit = new DatabaseEndpoints(databaseDataSource, databaseDriver, databaseURL, databaseUser, databasePassword, "name", "dbupdate", "update") | ||
val databaseUpdateEndpoint = databaseUpdateInit.aEndpoint | ||
def databaseUpdateRoute[F[_]: Sync](implicit serverOption: Http4sServerOptions[F], fcs: ContextShift[F]) = databaseUpdateInit.aRoute | ||
|
||
val streamInit = new StreamEndpoints("text", "stream", "get") | ||
val streamEndpoint = streamInit.streamingEndpoint | ||
val streamRoute = streamInit.streamingRoute | ||
|
||
// generating the documentation in yml; extension methods come from imported packages | ||
val routes: HttpRoutes[IO] = helloWorldRoute <+> byeWorldRoute <+> streamRoute <+> databaseSelectRoute <+> databaseInsertRoute <+> databaseUpdateRoute | ||
|
||
// generating the documentation in yml; extension methods come from imported packages | ||
val openApiDocs: OpenAPI = List(helloWorldEndpoint, byeWorldEndpoint, streamEndpoint, databaseSelectEndpoint, databaseInsertEndpoint, databaseUpdateEndpoint).toOpenAPI("The tapir library", "1.0.0") | ||
val openApiYml: String = openApiDocs.toYaml | ||
|
||
// starting the server | ||
BlazeServerBuilder[IO] | ||
.bindHttp(serverPort, serverHost) | ||
.withHttpApp(Router("/" -> (routes <+> new SwaggerHttp4s(openApiYml).routes[IO])).orNotFound) | ||
.resource | ||
.use { _ => | ||
IO { | ||
println(s"Go to: http://$serverHost:$serverPort/docs") | ||
println("Press any key to exit ...") | ||
scala.io.StdIn.readLine() | ||
} | ||
} | ||
.unsafeRunSync() | ||
|
||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
package com.headstorm | ||
|
||
import sttp.tapir.Endpoint | ||
import sttp.model.StatusCode | ||
import io.circe.generic.auto._ | ||
import io.circe.parser._ | ||
import io.circe.syntax._ | ||
import sttp.tapir.json.circe._ | ||
import cats.effect._ | ||
import cats.implicits._ | ||
import org.http4s.HttpRoutes | ||
import sttp.tapir._ | ||
import sttp.tapir.server.http4s._ | ||
import fs2._ | ||
import sttp.model.HeaderNames | ||
import doobie._ | ||
import doobie.implicits._ | ||
import cats.effect.IO | ||
import scala.concurrent.ExecutionContext | ||
|
||
object RouteGenerator { | ||
|
||
sealed trait ErrorInfo | ||
case class NotFound(what: String) extends ErrorInfo | ||
case class Unauthorized(realm: String) extends ErrorInfo | ||
case class Unknown(code: Int, msg: String) extends ErrorInfo | ||
case object NoContent extends ErrorInfo | ||
// | ||
case class Messages(code: String, msg: String) | ||
val msgCodec = jsonBody[Messages] | ||
|
||
implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global | ||
implicit val contextShift: ContextShift[IO] = IO.contextShift(ec) | ||
implicit val timer: Timer[IO] = IO.timer(ec) | ||
|
||
class Endpoints(paramName: String, endpointName: String, method: String) { | ||
|
||
val baseEndpoint = endpoint.errorOut( | ||
oneOf( | ||
statusMapping(StatusCode.NotFound, jsonBody[NotFound].description("not found")), | ||
statusMapping(StatusCode.Unauthorized, jsonBody[Unauthorized].description("unauthorized")), | ||
statusMapping(StatusCode.NoContent, emptyOutput.map(_ => NoContent)(_ => ())), | ||
statusDefaultMapping(jsonBody[Unknown].description("unknown")) | ||
) | ||
) | ||
|
||
val aEndpoint: Endpoint[String, ErrorInfo with Product with Serializable, Messages, Nothing] = { | ||
method match { | ||
case "get" => baseEndpoint.get.in(endpointName).in(query[String](paramName)).out(jsonBody[Messages]) | ||
case "post" => baseEndpoint.post.in(endpointName).in(query[String](paramName)).out(jsonBody[Messages]) | ||
} | ||
|
||
} | ||
|
||
val aRoute = aEndpoint.toRoutes(in => IO(Messages("200", s"$endpointName : $paramName : $in").asRight[ErrorInfo with Product with Serializable])) | ||
} | ||
|
||
class StreamEndpoints(paramName: String, endpointName: String, method: String) { | ||
|
||
val baseEndpoint = endpoint.errorOut( | ||
oneOf( | ||
statusMapping(StatusCode.NotFound, jsonBody[NotFound].description("not found")), | ||
statusMapping(StatusCode.Unauthorized, jsonBody[Unauthorized].description("unauthorized")), | ||
statusMapping(StatusCode.NoContent, emptyOutput.map(_ => NoContent)(_ => ())), | ||
statusDefaultMapping(jsonBody[Unknown].description("unknown")) | ||
) | ||
) | ||
|
||
val streamingEndpoint = { | ||
method match { | ||
case "get" => baseEndpoint.get.in(endpointName).in(query[String](paramName)).out(header[Long](HeaderNames.ContentLength)).out(streamBody[Stream[IO, Byte]](schemaFor[String], CodecFormat.TextPlain())) | ||
case "post" => baseEndpoint.post.in(endpointName).in(query[String](paramName)).out(header[Long](HeaderNames.ContentLength)).out(streamBody[Stream[IO, Byte]](schemaFor[String], CodecFormat.TextPlain())) | ||
} | ||
} | ||
|
||
def createStream(in: String) = { | ||
val size = 100L | ||
val responseMsg = Messages("200", in) | ||
val responseMsgJson = responseMsg.asJson.toString() | ||
val listChar = responseMsgJson.toList | ||
val streamProcess = Stream | ||
.emit(listChar) | ||
// .repeat | ||
.flatMap(list => Stream.chunk(Chunk.seq(list))) | ||
// .metered[IO](50.millis) | ||
.take(size) | ||
.covary[IO] | ||
.map(_.toByte) | ||
.pure[IO] | ||
.map(s => Right((size, s))) | ||
streamProcess | ||
} | ||
|
||
val streamingRoute: HttpRoutes[IO] = streamingEndpoint.toRoutes {createStream _} | ||
|
||
} | ||
|
||
class DatabaseEndpoints(dataSource: String, driver: String, url: String, user: String, password: String, paramName: String, endpointName: String, method: String) { | ||
|
||
val xa = Transactor.fromDriverManager[IO](driver, url, user, password) | ||
case class Country(code: String, name: String, pop: Int, gnp: Option[Double]) | ||
case class City(id: Int, name: String, countryCode: String, district: String, population: Int) | ||
case class UpdateCity(idUpdate: Int, name: String, countryCode: String, district: String, population: Int) | ||
|
||
def select(n: String): ConnectionIO[Option[Country]] = | ||
sql"select code, name, population, gnp from country where name = $n".query[Country].option | ||
|
||
def insert(c: City) = | ||
sql"insert into city (id, name, countrycode, district, population) values (${c.id}, ${c.name}, ${c.countryCode}, ${c.district}, ${c.population})".update | ||
|
||
def update(c: UpdateCity) = | ||
sql"update city set name = ${c.name}, countrycode = ${c.countryCode}, district = ${c.district}, population = ${c.population} where id = ${c.idUpdate}".update | ||
|
||
val baseEndpoint = endpoint.errorOut( | ||
oneOf( | ||
statusMapping(StatusCode.NotFound, jsonBody[NotFound].description("not found")), | ||
statusMapping(StatusCode.Unauthorized, jsonBody[Unauthorized].description("unauthorized")), | ||
statusMapping(StatusCode.NoContent, emptyOutput.map(_ => NoContent)(_ => ())), | ||
statusDefaultMapping(jsonBody[Unknown].description("unknown")) | ||
) | ||
) | ||
|
||
val aEndpoint: Endpoint[String, ErrorInfo with Product with Serializable, Messages, Nothing] = { | ||
method match { | ||
case "select" => baseEndpoint.get.in(endpointName).in(query[String](paramName)).out(jsonBody[Messages]) | ||
case "insert" => baseEndpoint.put.in(endpointName).in(stringBody).out(jsonBody[Messages]) | ||
case "update" => baseEndpoint.post.in(endpointName).in(stringBody).out(jsonBody[Messages]) | ||
} | ||
|
||
} | ||
|
||
def selectLogic(n: String) = { | ||
val qResult = select(n).transact(xa).unsafeRunSync match { | ||
case Some(x) => | ||
x match { | ||
case in: Country => Messages("200", s"${in.asJson}") | ||
} | ||
case _ => Messages("400", "Error") | ||
} | ||
qResult | ||
} | ||
|
||
def insertLogic(n: String) = { | ||
val qResult = decode[City](n) match { | ||
case Right(r) => insert(r).run.transact(xa).unsafeRunSync match { | ||
case x: Int => Messages("200", s"Updated $x record(s).") | ||
case _ => Messages("400", "Error") | ||
} | ||
} | ||
qResult | ||
} | ||
|
||
def updateLogic(n: String) = { | ||
val qResult = decode[UpdateCity](n) match { | ||
case Right(r) => update(r).run.transact(xa).unsafeRunSync match { | ||
case x: Int => Messages("200", s"Updated $x record(s).") | ||
case _ => Messages("400", "Error") | ||
} | ||
} | ||
qResult | ||
} | ||
|
||
def aRoute[F[_]: Sync](implicit serverOption: Http4sServerOptions[F], fcs: ContextShift[F]) = { | ||
method match { | ||
case "select" => aEndpoint.toRoutes ( in => selectLogic(in).asRight[ErrorInfo with Product with Serializable].pure[F] ) | ||
case "insert" => aEndpoint.toRoutes ( in => insertLogic(in).asRight[ErrorInfo with Product with Serializable].pure[F] ) | ||
case "update" => aEndpoint.toRoutes ( in => updateLogic(in).asRight[ErrorInfo with Product with Serializable].pure[F] ) | ||
} | ||
} | ||
|
||
} | ||
} |
Oops, something went wrong.