diff --git a/modules/core/src/main/scala/pillars/ApiServer.scala b/modules/core/src/main/scala/pillars/ApiServer.scala index ffb7f2444..da2cb6bed 100644 --- a/modules/core/src/main/scala/pillars/ApiServer.scala +++ b/modules/core/src/main/scala/pillars/ApiServer.scala @@ -15,6 +15,7 @@ trait ApiServer[F[_]]: def start(endpoints: List[HttpEndpoint[F]]): F[Unit] object ApiServer: + def apply[F[_]]: Run[F, ApiServer[F]] = summon[Pillars[F]].apiServer def init[F[_]: Async](config: Config, observability: Observability[F], logger: Scribe[F]): ApiServer[F] = (endpoints: List[HttpEndpoint[F]]) => Async[F].whenA(config.enabled): diff --git a/modules/core/src/main/scala/pillars/config.scala b/modules/core/src/main/scala/pillars/Config.scala similarity index 97% rename from modules/core/src/main/scala/pillars/config.scala rename to modules/core/src/main/scala/pillars/Config.scala index 5862b130b..0e9d6b8bf 100644 --- a/modules/core/src/main/scala/pillars/config.scala +++ b/modules/core/src/main/scala/pillars/Config.scala @@ -21,7 +21,8 @@ import scala.io.Source import scala.util.matching.Regex import scodec.bits.ByteVector -object config: +object Config: + def apply[F[_]]: Run[F, PillarsConfig] = summon[Pillars[F]].config case class PillarsConfig( name: App.Name, log: Logging.Config = Logging.Config(), @@ -98,4 +99,4 @@ object config: case ConfigError.MissingEnvironmentVariable(name) => Message(s"Missing environment variable $name".assume) case ConfigError.ParsingError(cause) => Message(s"Failed to parse configuration: ${cause.getMessage}".assume) end ConfigError -end config +end Config diff --git a/modules/core/src/main/scala/pillars/Logging.scala b/modules/core/src/main/scala/pillars/Logging.scala index 6c2148c15..6d9cddcfe 100644 --- a/modules/core/src/main/scala/pillars/Logging.scala +++ b/modules/core/src/main/scala/pillars/Logging.scala @@ -14,17 +14,21 @@ import io.github.iltotore.iron.constraint.all.* import java.nio.file.Path import scribe.Level import scribe.Logger +import scribe.Scribe import scribe.file.PathBuilder import scribe.format.Formatter import scribe.json.ScribeCirceJsonSupport import scribe.writer.ConsoleWriter import scribe.writer.Writer +object Logger: + def apply[F[_]: Pillars]: Run[F, Scribe[F]] = summon[Pillars[F]].logger + object Logging: def init[F[_]: Sync](config: Config): F[Unit] = Sync[F] .delay( - Logger.root + scribe.Logger.root .clearHandlers() .clearModifiers() .withHandler( diff --git a/modules/core/src/main/scala/pillars/Pillars.scala b/modules/core/src/main/scala/pillars/Pillars.scala index 70feff396..3518aebcd 100644 --- a/modules/core/src/main/scala/pillars/Pillars.scala +++ b/modules/core/src/main/scala/pillars/Pillars.scala @@ -8,8 +8,8 @@ import io.circe.Decoder import java.nio.file.Path import java.util.ServiceLoader import org.typelevel.otel4s.trace.Tracer -import pillars.config.PillarsConfig -import pillars.config.Reader +import pillars.Config.PillarsConfig +import pillars.Config.Reader import pillars.probes.ProbeManager import pillars.probes.ProbesController import scala.jdk.CollectionConverters.IterableHasAsScala @@ -20,11 +20,40 @@ import scribe.* * The Pillars trait defines the main components of the application. */ trait Pillars[F[_]]: + /** + * Component for observability. It allows you to create spans and metrics. + */ def observability: Observability[F] + + /** + * The configuration for the application. + */ def config: PillarsConfig + + /** + * The API server for the application. + * + * It has to be manually started by calling the `start` method in the application. + */ def apiServer: ApiServer[F] + + /** + * The logger for the application. + */ def logger: Scribe[F] + + /** + * Reads a configuration from the configuration. + * + * @return the configuration. + */ def readConfig[T](using Decoder[T]): F[T] + + /** + * Gets a module from the application. + * + * @return the module. + */ def module[T <: Module[F]: ClassTag]: T end Pillars diff --git a/modules/core/src/main/scala/pillars/app.scala b/modules/core/src/main/scala/pillars/app.scala index 22f0f3ab8..f63a44400 100644 --- a/modules/core/src/main/scala/pillars/app.scala +++ b/modules/core/src/main/scala/pillars/app.scala @@ -16,7 +16,7 @@ trait App[F[_]]: def infos: AppInfo def probes: List[Probe[F]] = Nil def adminControllers: List[Controller[F]] = Nil - def run(using p: Pillars[F]): F[Unit] + def run: Run[F, F[Unit]] end App object App: @@ -50,12 +50,10 @@ trait EntryPoint extends IOApp: override final def run(args: List[String]): IO[ExitCode] = Command(app.infos.name, app.infos.description)( Opts.option[Path]("config", "Path to the configuration file") - ).parse( - args, - sys.env - ) match + ).parse(args, sys.env) match case Left(help) => Console[IO].errorln(help).as(ExitCode.Error) case Right(configPath) => Pillars(configPath).use: pillars => - app.run(using pillars).as(ExitCode.Success) + given Pillars[IO] = pillars + app.run.as(ExitCode.Success) end EntryPoint diff --git a/modules/core/src/main/scala/pillars/modules.scala b/modules/core/src/main/scala/pillars/modules.scala index 7158872a7..00373cac2 100644 --- a/modules/core/src/main/scala/pillars/modules.scala +++ b/modules/core/src/main/scala/pillars/modules.scala @@ -5,7 +5,7 @@ import cats.effect.Resource import cats.effect.std.Console import fs2.io.net.Network import org.typelevel.otel4s.trace.Tracer -import pillars.config.Reader +import pillars.Config.Reader import pillars.probes.Probe import scala.reflect.ClassTag import scribe.Scribe diff --git a/modules/core/src/main/scala/pillars/package.scala b/modules/core/src/main/scala/pillars/package.scala index 75a5698fa..a55a159c7 100644 --- a/modules/core/src/main/scala/pillars/package.scala +++ b/modules/core/src/main/scala/pillars/package.scala @@ -1,4 +1,12 @@ package object pillars: + /** + * Type alias for a Pillars[F] context bound. + * + * @tparam F The effect type. + * @tparam A The type of the value that is being computed. + */ + type Run[F[_], A] = Pillars[F] ?=> A + extension [T](items: Iterable[T]) /** * Extension method for Iterable[T] to perform topological sorting. diff --git a/modules/db/src/main/scala/pillars/db/db.scala b/modules/db/src/main/scala/pillars/db/db.scala index 642612852..712a21042 100644 --- a/modules/db/src/main/scala/pillars/db/db.scala +++ b/modules/db/src/main/scala/pillars/db/db.scala @@ -11,12 +11,12 @@ import io.github.iltotore.iron.* import io.github.iltotore.iron.circe.given import io.github.iltotore.iron.constraint.all.* import org.typelevel.otel4s.trace.Tracer +import pillars.Config.* import pillars.Loader import pillars.Module import pillars.Modules import pillars.Pillars import pillars.codec.given -import pillars.config.* import pillars.probes.* import skunk.* import skunk.codec.all.* diff --git a/modules/example/src/main/rest/admin.http b/modules/example/src/main/rest/admin.http new file mode 100644 index 000000000..17904ee7c --- /dev/null +++ b/modules/example/src/main/rest/admin.http @@ -0,0 +1,17 @@ +@host = http://localhost:19876 +### +# @name: List all flags +# +GET {{ host }}/admin/flags/ + +### +# @name: Get flag by id + +@featureId = feature-1 + +GET {{ host }}/admin/flags/{{featureId}} + +### +# + +GET {{ host }}/admin/probes/health diff --git a/modules/example/src/main/scala/example/app.scala b/modules/example/src/main/scala/example/app.scala index d72d6dc8f..d42f71ca3 100644 --- a/modules/example/src/main/scala/example/app.scala +++ b/modules/example/src/main/scala/example/app.scala @@ -13,22 +13,21 @@ import skunk.implicits.* // tag::quick-start[] object app extends pillars.EntryPoint: // // <1> - def app: pillars.App[IO] = new pillars.App[IO]: // // <2> + def app: pillars.App[IO] = new: // // <2> def infos: AppInfo = BuildInfo.toAppInfo // // <3> - def run(using p: Pillars[IO]): IO[Unit] = // // <4> - import p.* + def run: Run[IO, IO[Unit]] = // // <4> for - _ <- logger.info(s"📚 Welcome to ${config.name}!") + _ <- Logger[IO].info(s"📚 Welcome to ${Config[IO].name}!") _ <- flag"feature-1".whenEnabled: DB[IO].use: session => for date <- session.unique(sql"select now()".query(timestamptz)) - _ <- logger.info(s"The current date is $date.") + _ <- Logger[IO].info(s"The current date is $date.") yield () _ <- HttpClient[IO].get("https://pillars.rlemaitre.com"): response => - logger.info(s"Response: ${response.status}") - _ <- apiServer.start(endpoints.all) + Logger[IO].info(s"Response: ${response.status}") + _ <- ApiServer[IO].start(endpoints.all) yield () end for end run diff --git a/modules/http-client/src/main/scala/pillars/httpclient/httpclient.scala b/modules/http-client/src/main/scala/pillars/httpclient/httpclient.scala index 383223b6c..f42b0dbef 100644 --- a/modules/http-client/src/main/scala/pillars/httpclient/httpclient.scala +++ b/modules/http-client/src/main/scala/pillars/httpclient/httpclient.scala @@ -26,6 +26,6 @@ final case class HttpClient[F[_]: Async](client: org.http4s.client.Client[F]) object HttpClient: def apply[F[_]](using p: Pillars[F]): Client[F] = p.module[HttpClient[F]].client -final case class Config(followRedirect: Boolean) +private[httpclient] final case class Config(followRedirect: Boolean) extension [F[_]](p: Pillars[F]) def httpClient: Client[F] = p.module[HttpClient[F]].client