diff --git a/build.sbt b/build.sbt index ee4de1d..a96350a 100644 --- a/build.sbt +++ b/build.sbt @@ -123,8 +123,7 @@ IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.http4sPac4J, - IWDeps.pac4jOIDC, - IWDeps.logbackClassic + IWDeps.pac4jOIDC ) .dependsOn(core.jvm, codecs.jvm, `tapir-support`.jvm) diff --git a/build.sbt b/build.sbt index ee4de1d..a96350a 100644 --- a/build.sbt +++ b/build.sbt @@ -123,8 +123,7 @@ IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.http4sPac4J, - IWDeps.pac4jOIDC, - IWDeps.logbackClassic + IWDeps.pac4jOIDC ) .dependsOn(core.jvm, codecs.jvm, `tapir-support`.jvm) diff --git a/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala b/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala new file mode 100644 index 0000000..af25609 --- /dev/null +++ b/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala @@ -0,0 +1,54 @@ +package works.iterative.server.http + +import zio.* +import zio.config.* + +/** Configuration for the single page application (SPA) endpoints. + * + * By default, the application will be served from prefix/app path, static + * resources from prefix/? path. + * + * Under "app", only index.html will be served. + * + * As this has a catch-all route, it should be the last route in the list. + */ +final case class SPAConfig( + /** The path under which the application HTML is always served. */ + appPath: String = "app", + /** The filename for the app */ + appIndex: String = "app.html", + /** Optional prefix for the SPA application. + * + * If not set, the application will be served from the root. + */ + prefix: Option[String] = None, + /** Path to the files of the SPA application. + * + * If not set, the application will be served from the classpath under + * "app" package. + */ + filePath: Option[String] = None, + /** Path in the resources to get the files from. + * + * If filePath is set, this is ignored. + */ + resourcePath: String = "app" +) + +object SPAConfig: + val configDesc: ConfigDescriptor[SPAConfig] = + import ConfigDescriptor.* + nested("SPA")( + string("APPPATH").default("app") zip + string("APPINDEX").default("index.html") zip + string("PREFIX").optional zip + string("FILEPATH").optional zip + string("RESOURCEPATH").default("app") + ).to[SPAConfig] + + val fromEnv: ZLayer[Any, ReadError[String], SPAConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/build.sbt b/build.sbt index ee4de1d..a96350a 100644 --- a/build.sbt +++ b/build.sbt @@ -123,8 +123,7 @@ IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.http4sPac4J, - IWDeps.pac4jOIDC, - IWDeps.logbackClassic + IWDeps.pac4jOIDC ) .dependsOn(core.jvm, codecs.jvm, `tapir-support`.jvm) diff --git a/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala b/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala new file mode 100644 index 0000000..af25609 --- /dev/null +++ b/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala @@ -0,0 +1,54 @@ +package works.iterative.server.http + +import zio.* +import zio.config.* + +/** Configuration for the single page application (SPA) endpoints. + * + * By default, the application will be served from prefix/app path, static + * resources from prefix/? path. + * + * Under "app", only index.html will be served. + * + * As this has a catch-all route, it should be the last route in the list. + */ +final case class SPAConfig( + /** The path under which the application HTML is always served. */ + appPath: String = "app", + /** The filename for the app */ + appIndex: String = "app.html", + /** Optional prefix for the SPA application. + * + * If not set, the application will be served from the root. + */ + prefix: Option[String] = None, + /** Path to the files of the SPA application. + * + * If not set, the application will be served from the classpath under + * "app" package. + */ + filePath: Option[String] = None, + /** Path in the resources to get the files from. + * + * If filePath is set, this is ignored. + */ + resourcePath: String = "app" +) + +object SPAConfig: + val configDesc: ConfigDescriptor[SPAConfig] = + import ConfigDescriptor.* + nested("SPA")( + string("APPPATH").default("app") zip + string("APPINDEX").default("index.html") zip + string("PREFIX").optional zip + string("FILEPATH").optional zip + string("RESOURCEPATH").default("app") + ).to[SPAConfig] + + val fromEnv: ZLayer[Any, ReadError[String], SPAConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/http/src/main/scala/works/iterative/server/http/SPAEndpoints.scala b/server/http/src/main/scala/works/iterative/server/http/SPAEndpoints.scala new file mode 100644 index 0000000..655bbef --- /dev/null +++ b/server/http/src/main/scala/works/iterative/server/http/SPAEndpoints.scala @@ -0,0 +1,33 @@ +package works.iterative.server.http + +import works.iterative.tapir.CustomTapir.* +import sttp.tapir.files.* +import sttp.tapir.Tapir +import sttp.tapir.EndpointInput + +class SPAEndpoints[Env](config: SPAConfig): + private val prefix: EndpointInput[Unit] = + config.prefix.toSeq + .flatMap(_.split("/").toSeq) + .foldLeft(emptyInput)((i, p) => i / p) + + val serverEndpoints: List[ZServerEndpoint[Env, Any]] = + config.filePath match + case Some(filePath) => + List( + staticFileGetServerEndpoint(prefix / config.appPath)( + s"${filePath}/${config.appIndex}" + ), + staticFilesGetServerEndpoint(prefix)(filePath) + ) + case _ => + List( + staticResourceGetServerEndpoint(prefix / config.appPath)( + classOf[Tapir].getClassLoader, + s"${config.resourcePath}/${config.appIndex}" + ), + staticResourcesGetServerEndpoint(prefix)( + classOf[Tapir].getClassLoader, + config.resourcePath + ) + ) diff --git a/build.sbt b/build.sbt index ee4de1d..a96350a 100644 --- a/build.sbt +++ b/build.sbt @@ -123,8 +123,7 @@ IWDeps.tapirZIOHttp4sServer, IWDeps.http4sBlazeServer, IWDeps.http4sPac4J, - IWDeps.pac4jOIDC, - IWDeps.logbackClassic + IWDeps.pac4jOIDC ) .dependsOn(core.jvm, codecs.jvm, `tapir-support`.jvm) diff --git a/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala b/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala new file mode 100644 index 0000000..af25609 --- /dev/null +++ b/server/http/src/main/scala/works/iterative/server/http/SPAConfig.scala @@ -0,0 +1,54 @@ +package works.iterative.server.http + +import zio.* +import zio.config.* + +/** Configuration for the single page application (SPA) endpoints. + * + * By default, the application will be served from prefix/app path, static + * resources from prefix/? path. + * + * Under "app", only index.html will be served. + * + * As this has a catch-all route, it should be the last route in the list. + */ +final case class SPAConfig( + /** The path under which the application HTML is always served. */ + appPath: String = "app", + /** The filename for the app */ + appIndex: String = "app.html", + /** Optional prefix for the SPA application. + * + * If not set, the application will be served from the root. + */ + prefix: Option[String] = None, + /** Path to the files of the SPA application. + * + * If not set, the application will be served from the classpath under + * "app" package. + */ + filePath: Option[String] = None, + /** Path in the resources to get the files from. + * + * If filePath is set, this is ignored. + */ + resourcePath: String = "app" +) + +object SPAConfig: + val configDesc: ConfigDescriptor[SPAConfig] = + import ConfigDescriptor.* + nested("SPA")( + string("APPPATH").default("app") zip + string("APPINDEX").default("index.html") zip + string("PREFIX").optional zip + string("FILEPATH").optional zip + string("RESOURCEPATH").default("app") + ).to[SPAConfig] + + val fromEnv: ZLayer[Any, ReadError[String], SPAConfig] = + ZConfig.fromSystemEnv( + configDesc, + keyDelimiter = Some('_'), + valueDelimiter = Some(',') + ) diff --git a/server/http/src/main/scala/works/iterative/server/http/SPAEndpoints.scala b/server/http/src/main/scala/works/iterative/server/http/SPAEndpoints.scala new file mode 100644 index 0000000..655bbef --- /dev/null +++ b/server/http/src/main/scala/works/iterative/server/http/SPAEndpoints.scala @@ -0,0 +1,33 @@ +package works.iterative.server.http + +import works.iterative.tapir.CustomTapir.* +import sttp.tapir.files.* +import sttp.tapir.Tapir +import sttp.tapir.EndpointInput + +class SPAEndpoints[Env](config: SPAConfig): + private val prefix: EndpointInput[Unit] = + config.prefix.toSeq + .flatMap(_.split("/").toSeq) + .foldLeft(emptyInput)((i, p) => i / p) + + val serverEndpoints: List[ZServerEndpoint[Env, Any]] = + config.filePath match + case Some(filePath) => + List( + staticFileGetServerEndpoint(prefix / config.appPath)( + s"${filePath}/${config.appIndex}" + ), + staticFilesGetServerEndpoint(prefix)(filePath) + ) + case _ => + List( + staticResourceGetServerEndpoint(prefix / config.appPath)( + classOf[Tapir].getClassLoader, + s"${config.resourcePath}/${config.appIndex}" + ), + staticResourcesGetServerEndpoint(prefix)( + classOf[Tapir].getClassLoader, + config.resourcePath + ) + ) diff --git a/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala b/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala index 53d3033..645add1 100644 --- a/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala +++ b/server/http/src/main/scala/works/iterative/server/http/impl/blaze/BlazeHttpServer.scala @@ -4,11 +4,17 @@ import zio.* import zio.interop.catz.* import org.http4s.blaze.server.BlazeServerBuilder +import works.iterative.tapir.Http4sCustomTapir +import org.http4s.HttpRoutes class BlazeHttpServer(config: BlazeServerConfig) extends HttpServer: override def serve[Env](app: HttpApplication[Env]): URIO[Env, Nothing] = - BlazeServerBuilder[RIO[Env, *]] + type AppEnv[A] = RIO[Env, A] + val interpreter = new Http4sCustomTapir[Env] {} + val routes: HttpRoutes[AppEnv] = interpreter.from(app.endpoints).toRoutes + BlazeServerBuilder[AppEnv] .bindHttp(config.port, config.host) + .withHttpApp(routes.orNotFound) .serve .compile .drain