From 56fa1165c528814e6f475436414cf13561bfef6e Mon Sep 17 00:00:00 2001 From: "mr. dima" Date: Fri, 22 Nov 2024 21:28:16 +0100 Subject: [PATCH] Refactor: revise comments V1 (#15) --- src/main/scala/api/ErrorHandler.scala | 29 +++++- src/main/scala/api/ServerBuilder.scala | 19 ++-- .../endpoints/flow/ReaktoroEndpoints.scala | 24 ++++- .../preprocessor/PreprocessorEndpoints.scala | 96 +++++++++---------- src/main/scala/app/Main.scala | 48 +++++++--- .../scala/app/units/ClientResources.scala | 30 +++++- .../scala/app/units/EndpointsResources.scala | 58 ++++++++--- .../scala/app/units/ServerResources.scala | 28 +++++- .../scala/app/units/ServiceResources.scala | 69 +++++++++---- .../scala/app/units/SystemResources.scala | 26 ++++- src/main/scala/config/ConfigLoader.scala | 14 ++- .../cache/DistributedCacheService.scala | 12 ++- .../services/cache/LocalCacheService.scala | 17 +++- .../services/cache/types/CacheService.scala | 6 -- .../core/services/flow/ReaktoroService.scala | 54 +++-------- .../preprocessor/MechanismService.scala | 50 ++++++++++ .../preprocessor/ReactionService.scala | 50 ++++++++++ .../infrastructure/http/HttpClient.scala | 78 +++++++++------ 18 files changed, 501 insertions(+), 207 deletions(-) diff --git a/src/main/scala/api/ErrorHandler.scala b/src/main/scala/api/ErrorHandler.scala index 6eccfd7..a8a1eb9 100644 --- a/src/main/scala/api/ErrorHandler.scala +++ b/src/main/scala/api/ErrorHandler.scala @@ -7,24 +7,45 @@ import io.circe.generic.auto._ import org.http4s.circe.CirceEntityEncoder._ /** - * Represents an error response returned by the server. + * Represents a standard error response returned by the server in JSON format. + * + * This case class is used to encapsulate error details, making it easy to serialise into a consistent JSON structure + * for HTTP error responses. * * @param error - * The type of error (e.g., "NotFound", "BadRequest"). + * A string identifying the type of error (e.g., "NotFound", "BadRequest"). * @param message - * A descriptive message about the error. + * A descriptive message providing additional details about the error. */ final case class ErrorResponse(error: String, message: String) +/** + * Provides error handling for HTTP routes in a standardised way. + * + * The `ErrorHandler` wraps existing HTTP routes and ensures that: + * - `NotFound` errors return a `404` status with a JSON-encoded `ErrorResponse`. + * - Validation errors (e.g., `IllegalArgumentException`) return a `400` status. + * - Unexpected errors return a `500` status with a generic message. + * + * This utility promotes consistent error handling across the application. + */ object ErrorHandler { /** * Wraps the provided HTTP routes with error handling logic. * + * This function ensures that errors raised during route processing are captured and translated into appropriate HTTP + * responses with JSON-encoded error messages. + * + * Error Handling: + * - `NoSuchElementException`: Translates to a `404 Not Found` response. + * - `IllegalArgumentException`: Translates to a `400 Bad Request` response. + * - Any other exception: Translates to a `500 Internal Server Error` response. + * * @param routes * The `HttpRoutes[IO]` to be wrapped with error handling. * @return - * A new `HttpRoutes[IO]` that handles errors and returns appropriate HTTP responses with JSON error messages. + * A new `HttpRoutes[IO]` that handles errors consistently and returns meaningful responses. */ def apply(routes: HttpRoutes[IO]): HttpRoutes[IO] = HttpRoutes.of[IO] { request => routes(request).getOrElseF( diff --git a/src/main/scala/api/ServerBuilder.scala b/src/main/scala/api/ServerBuilder.scala index e6c0a3e..080b566 100644 --- a/src/main/scala/api/ServerBuilder.scala +++ b/src/main/scala/api/ServerBuilder.scala @@ -9,7 +9,10 @@ import org.http4s.ember.server.EmberServerBuilder import org.http4s.HttpRoutes /** - * A class responsible for building and starting an HTTP server. + * A utility class for constructing and starting an HTTP server. + * + * This class uses the `EmberServerBuilder` to set up a server with the provided HTTP routes. It integrates error + * handling for routes and ensures the server is properly managed as a `Resource`, allowing safe startup and shutdown. * * @param routes * The HTTP routes to be served by the server. @@ -19,15 +22,19 @@ class ServerBuilder( ) { /** - * Starts the HTTP server with the specified host and port configuration. + * Configures and starts an HTTP server with the specified host and port. + * + * This method uses the `EmberServerBuilder` to create and manage the server. It wraps the provided routes with the + * `ErrorHandler` to ensure consistent error handling, and builds an HTTP application that listens on the specified + * host and port. * * @param host - * The host address on which the server will listen. + * The host address on which the server will listen (e.g., `Host.fromString("127.0.0.1")`). * @param port - * The port number on which the server will listen. + * The port number on which the server will listen (e.g., `Port.fromInt(8080)`). * @return - * A `Resource[IO, Server]` that represents the running HTTP server. The server will be properly managed and - * terminated when the `Resource` is released. + * A `Resource[IO, Server]` that represents the running HTTP server. The server is automatically cleaned up when the + * `Resource` is released. */ def startServer( host: Host, diff --git a/src/main/scala/api/endpoints/flow/ReaktoroEndpoints.scala b/src/main/scala/api/endpoints/flow/ReaktoroEndpoints.scala index 531e501..ba2cb73 100644 --- a/src/main/scala/api/endpoints/flow/ReaktoroEndpoints.scala +++ b/src/main/scala/api/endpoints/flow/ReaktoroEndpoints.scala @@ -33,23 +33,37 @@ object ComputePropsRequest { /** * Defines the HTTP routes for interacting with the ReaktoroService. * + * This class sets up the routing for the API endpoints that allow clients to compute system properties for chemical + * reactions. It integrates with the `ReaktoroService` to perform the computations and handle requests. + * * @param reaktoroService - * The service handling system property computations. + * The service handling system property computations. This should implement the core logic to process reaction + * properties using the provided inputs. */ + class ReaktoroEndpoints( reaktoroService: ReaktoroService[IO] ) { /** - * HTTP POST route for computing system properties for a reaction. + * HTTP POST route for computing system properties for a chemical reaction. * * Endpoint: `/api/system/properties` * - * @param req - * The HTTP request containing a `ComputePropsRequest` object in the body. + * This endpoint accepts a JSON payload containing a `ComputePropsRequest` object, which includes: + * - `reactionId`: The identifier of the reaction. + * - `database`: The database to use for the computation. + * - `amounts`: The list of molecule amounts relevant to the computation. + * + * The route invokes the `ReaktoroService` to compute the system properties for the given reaction and returns the + * results as a JSON response. If an error occurs during processing, an appropriate error response is sent. + * * @return - * A JSON response containing the computed system properties or an error message. + * An HTTP response: + * - `200 OK`: With a JSON body containing the computed system properties. + * - `500 Internal Server Error`: If the service fails to process the request. */ + private val computeSystemPropsForReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] { case req @ POST -> Root / "system" / "properties" => req.as[ComputePropsRequest].flatMap { diff --git a/src/main/scala/api/endpoints/preprocessor/PreprocessorEndpoints.scala b/src/main/scala/api/endpoints/preprocessor/PreprocessorEndpoints.scala index b193c3f..2d65d69 100644 --- a/src/main/scala/api/endpoints/preprocessor/PreprocessorEndpoints.scala +++ b/src/main/scala/api/endpoints/preprocessor/PreprocessorEndpoints.scala @@ -17,16 +17,20 @@ import core.domain.preprocessor.{MechanismDetails, Reaction, ReactionDetails} import core.errors.http.preprocessor.ReactionError /** - * Defines the HTTP routes for handling reactions and mechanisms. + * Provides HTTP endpoints for managing reactions and mechanisms in the preprocessor module. * - * This class provides endpoints for performing CRUD operations on reactions and retrieving mechanism details. Each - * endpoint calls the appropriate service, and returns a response in JSON format. + * This class defines routes for: + * - Fetching reaction and mechanism details by ID. + * - Creating new reactions. + * - Deleting existing reactions. + * + * All endpoints interact with the `ReactionService` and `MechanismService` for business logic and return appropriate + * JSON responses or error messages. * * @param reactionService - * The service responsible for handling reaction-related operations, such as fetching, creating, and deleting - * reactions. + * Handles CRUD operations related to reactions. * @param mechanismService - * The service responsible for handling mechanism-related operations, such as fetching mechanism details. + * Handles retrieval of mechanism details. */ class PreprocessorEndpoints( reactionService: ReactionService[IO], @@ -34,21 +38,23 @@ class PreprocessorEndpoints( ) { /** - * HTTP GET route for fetching a reaction by its ID. + * HTTP GET route for fetching reaction details by ID. * * Endpoint: `/api/reaction/{id}` * - * This route validates the provided ID, fetches the reaction details from the `ReactionService`, and returns the - * details in JSON format. If the ID is invalid or the reaction is not found, it returns an appropriate error - * response. + * Validates the provided reaction ID, fetches the corresponding details using the `ReactionService`, and returns them + * in JSON format. Returns an appropriate error response if: + * - The ID is invalid. + * - The reaction is not found. + * - An unexpected error occurs during processing. * * @param id - * The string representation of the reaction ID to fetch. + * The string representation of the reaction ID. * @return - * - `200 OK` with the `ReactionDetails` object in JSON format if the reaction is found. - * - `400 Bad Request` if the ID is invalid. - * - `404 Not Found` if no reaction with the given ID exists. - * - `500 Internal Server Error` if an unexpected error occurs. + * - `200 OK`: Contains the `ReactionDetails` in JSON format. + * - `400 Bad Request`: If the ID is invalid. + * - `404 Not Found`: If no reaction exists with the given ID. + * - `500 Internal Server Error`: For unexpected errors. */ private val getReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] { case GET -> Root / "reaction" / id => @@ -69,15 +75,15 @@ class PreprocessorEndpoints( * * Endpoint: `/api/reaction` * - * This route reads a `Reaction` object from the request body, passes it to the `ReactionService` to create a new - * reaction, and returns the created reaction details in JSON format. + * Accepts a `Reaction` object in the request body and uses the `ReactionService` to create a new reaction. Returns + * the created reaction details in JSON format or an error response in case of failure. * * @param req - * The HTTP request containing the `Reaction` object in the body. + * The HTTP request containing the `Reaction` object. * @return - * - `201 Created` with the created `Reaction` object in JSON format on success. - * - `400 Bad Request` if the request body is invalid or creation fails. - * - `500 Internal Server Error` if an unexpected error occurs. + * - `201 Created`: Contains the created `Reaction` in JSON format. + * - `400 Bad Request`: If the request body is invalid or creation fails. + * - `500 Internal Server Error`: For unexpected errors. */ private val postReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] { case req @ POST -> Root / "reaction" => @@ -92,19 +98,19 @@ class PreprocessorEndpoints( } /** - * HTTP DELETE route for deleting a reaction by its ID. + * HTTP DELETE route for deleting a reaction by ID. * * Endpoint: `/api/reaction/{id}` * - * This route validates the provided ID, deletes the reaction using the `ReactionService`, and returns an appropriate - * response. If the ID is invalid or the deletion fails, an error response is returned. + * Validates the provided reaction ID and attempts to delete the reaction using the `ReactionService`. Returns an + * appropriate response based on the success or failure of the operation. * * @param id - * The string representation of the reaction ID to delete. + * The string representation of the reaction ID. * @return - * - `204 No Content` if the reaction is successfully deleted. - * - `400 Bad Request` if the ID is invalid or deletion fails. - * - `500 Internal Server Error` if an unexpected error occurs. + * - `204 No Content`: If the reaction is successfully deleted. + * - `400 Bad Request`: If the ID is invalid or deletion fails. + * - `500 Internal Server Error`: For unexpected errors. */ private val deleteReactionRoute: HttpRoutes[IO] = HttpRoutes.of[IO] { case DELETE -> Root / "reaction" / id => @@ -119,21 +125,23 @@ class PreprocessorEndpoints( } /** - * HTTP GET route for fetching a mechanism by its ID. + * HTTP GET route for fetching mechanism details by ID. * * Endpoint: `/api/mechanism/{id}` * - * This route validates the provided ID, fetches the mechanism details from the `MechanismService`, and returns the - * details in JSON format. If the ID is invalid or the mechanism is not found, it returns an appropriate error - * response. + * Validates the provided mechanism ID, fetches the corresponding details using the `MechanismService`, and returns + * them in JSON format. Returns an appropriate error response if: + * - The ID is invalid. + * - The mechanism is not found. + * - An unexpected error occurs during processing. * * @param id - * The string representation of the mechanism ID to fetch. + * The string representation of the mechanism ID. * @return - * - `200 OK` with the `MechanismDetails` object in JSON format if the mechanism is found. - * - `400 Bad Request` if the ID is invalid. - * - `404 Not Found` if no mechanism with the given ID exists. - * - `500 Internal Server Error` if an unexpected error occurs. + * - `200 OK`: Contains the `MechanismDetails` in JSON format. + * - `400 Bad Request`: If the ID is invalid. + * - `404 Not Found`: If no mechanism exists with the given ID. + * - `500 Internal Server Error`: For unexpected errors. */ private val getMechanismRoute: HttpRoutes[IO] = HttpRoutes.of[IO] { case GET -> Root / "mechanism" / id => @@ -149,22 +157,8 @@ class PreprocessorEndpoints( } } - /** - * Validates the given string as an integer ID. - * - * @param id - * The string to validate. - * @return - * An `Option[Int]` containing the valid integer ID, or `None` if the validation fails. - */ private def validateId(id: String): Option[Int] = id.toIntOption - /** - * Combines all defined HTTP routes and applies middleware for logging. - * - * @return - * A single `HttpRoutes[IO]` instance containing all endpoints. - */ val routes: HttpRoutes[IO] = Logger.httpRoutes(logHeaders = false, logBody = true)( Router( "/api" -> (getReactionRoute <+> postReactionRoute <+> deleteReactionRoute <+> getMechanismRoute) diff --git a/src/main/scala/app/Main.scala b/src/main/scala/app/Main.scala index 7cd6773..d90316a 100644 --- a/src/main/scala/app/Main.scala +++ b/src/main/scala/app/Main.scala @@ -28,30 +28,48 @@ import app.units.ServiceResources.{ } /** - * Entry point for the application. Responsible for configuring and starting all resources and services. + * Main entry point for the application. + * + * This object configures and starts all required resources and services for the application, including: + * - Actor system setup for Akka-based concurrency. + * - HTTP client setup for external API interactions. + * - Distributed cache management with a configurable time-to-live (TTL). + * - Initialisation of core services (`MechanismService`, `ReactionService`, `ReaktoroService`). + * - HTTP server setup for serving API endpoints. + * + * Proper lifecycle management is ensured using `cats.effect.Resource`, which guarantees that all resources are + * initialised and cleaned up correctly. This entry point waits for user input to terminate the application, ensuring a + * controlled shutdown. */ object Main extends IOApp { /** * Configures and manages the lifecycle of all resources and services required for the application. * - * This method initialises resources such as the actor system, HTTP clients, distributed cache, services, and server. - * It ensures proper resource management by leveraging `Resource` for initialisation and cleanup. + * This method integrates the initialisation and cleanup of: + * - Actor system for concurrency and distributed data. + * - HTTP clients for API communication. + * - Distributed cache services with a configurable time-to-live (TTL) for cache expiration. + * - Core application services (`MechanismService`, `ReactionService`, `ReaktoroService`). + * - HTTP server for hosting API endpoints. API routes are combined and served as a single HTTP application. + * + * The method waits for user input to terminate the application, ensuring a controlled shutdown. All resources are + * composed using `cats.effect.Resource`, ensuring proper cleanup on termination. * * @param config - * The configuration loader that provides application-specific settings, such as HTTP endpoints, client - * configurations, and server properties. + * The configuration loader for application-specific settings. * @param ec - * The `ExecutionContext` used to handle asynchronous operations across the application. + * The `ExecutionContext` for handling asynchronous operations. * @param system - * The `ActorSystem` used for Akka-based concurrency and distributed data. + * The Akka `ActorSystem` for managing concurrency. * @param selfUniqueAddress - * The unique address of the current actor system, used for distributed data in a cluster. + * The unique address of the current actor system, used for distributed data. + * @param ttl + * A timeout representing the time-to-live (TTL) for cache expiration. * @param logger - * An implicit logger instance for recording lifecycle events, debugging, and error handling. + * An implicit logger instance for lifecycle logging. * @return - * A `Resource[IO, Unit]` that encapsulates the application's full lifecycle. This includes initialisation, running - * the server, and ensuring all resources are properly cleaned up when the application terminates. + * A `Resource[IO, Unit]` that encapsulates the entire lifecycle of the application. */ private def runApp( config: ConfigLoader @@ -60,7 +78,7 @@ object Main extends IOApp { ec: ExecutionContext, system: ActorSystem, selfUniqueAddress: SelfUniqueAddress, - cacheExpiration: Timeout, + ttl: Timeout, logger: Logger[IO] ): Resource[IO, Unit] = val preprocessorBaseUri = config.preprocessorHttpClientConfig.baseUri @@ -86,6 +104,12 @@ object Main extends IOApp { /** * Main entry point for the application. * + * This method sets up the application by: + * - Initialising implicit dependencies, including the logger, actor system, execution context, distributed data, + * unique address, and cache expiration timeout (TTL). + * - Loading configuration using the `DefaultConfigLoader`. + * - Running the application using `runApp` and ensuring all resources are cleaned up on exit. + * * @param args * The command-line arguments passed to the application. * @return diff --git a/src/main/scala/app/units/ClientResources.scala b/src/main/scala/app/units/ClientResources.scala index 12c1c66..7a305ff 100644 --- a/src/main/scala/app/units/ClientResources.scala +++ b/src/main/scala/app/units/ClientResources.scala @@ -7,18 +7,38 @@ import org.http4s.client.Client import org.http4s.ember.client.EmberClientBuilder /** - * Provides resources related to HTTP clients for the application. + * Provides resources for creating and managing HTTP clients in the application. + * + * This object encapsulates logic for building `Http4s` HTTP clients as managed resources, ensuring proper lifecycle + * management (e.g., cleanup after use). It integrates seamlessly with `Cats Effect` to provide a safe and composable + * way to work with clients. */ object ClientResources { /** - * Creates a managed `Client` resource using the Ember HTTP client. + * Creates a managed HTTP client resource using the Ember HTTP client. + * + * The `EmberClientBuilder` is used to construct a default `Http4s` client. The client is wrapped in a `Resource` to + * ensure proper lifecycle management, including cleanup when the resource is released. The method also supports + * logging via the provided `Logger` instance. + * + * Example usage: + * {{{ + * import org.typelevel.log4cats.slf4j.Slf4jLogger + * import app.units.ClientResources + * + * implicit val logger = Slf4jLogger.getLogger[IO] + * + * val clientResource = ClientResources.clientResource + * clientResource.use { client => + * // Use the client to make HTTP requests + * } + * }}} * * @param logger - * A logger instance used for logging any events or errors. + * A logger instance for logging events or errors. * @return - * A managed `Resource` that encapsulates an `Http4s` `Client[IO]` instance. The `Client` is properly cleaned up - * when the resource is released. + * A managed `Resource` that encapsulates an `Http4s` `Client[IO]` instance. */ def clientResource( implicit logger: Logger[IO] diff --git a/src/main/scala/app/units/EndpointsResources.scala b/src/main/scala/app/units/EndpointsResources.scala index af7e392..60d8ae1 100644 --- a/src/main/scala/app/units/EndpointsResources.scala +++ b/src/main/scala/app/units/EndpointsResources.scala @@ -11,23 +11,40 @@ import core.services.preprocessor.{MechanismService, ReactionService} import org.typelevel.log4cats.Logger /** - * Provides resources for managing API endpoint initialisation. This includes endpoints for the preprocessor and - * Reaktoro services. + * Provides managed resources for API endpoint initialisation. + * + * This object handles the creation and lifecycle management of API endpoints, such as those for preprocessor services + * and Reaktoro services. By encapsulating endpoint initialisation in `Resource`, it ensures proper setup and teardown + * of these components. */ object EndpointResources { /** * Creates a managed resource for the `PreprocessorEndpoints`. * + * This method initialises and manages the lifecycle of the `PreprocessorEndpoints` instance, which handles API routes + * for preprocessor-related services, including reactions and mechanisms. It logs lifecycle events during the + * resource's creation and release for debugging and monitoring purposes. + * + * Example usage: + * {{{ + * val preprocessorResource = EndpointResources.preprocessorEndpointsResource( + * reactionService, + * mechanismService + * ) + * preprocessorResource.use { endpoints => + * // Use the endpoints to serve HTTP routes + * } + * }}} + * * @param reactionService - * An instance of `ReactionService` used by the endpoints to handle reaction-related operations. + * An instance of `ReactionService` for handling reaction-related operations. * @param mechanismService - * An instance of `MechanismService` used by the endpoints to handle mechanism-related operations. + * An instance of `MechanismService` for handling mechanism-related operations. * @param logger - * A logger instance for logging lifecycle events and errors during the resource's creation and release. + * A logger instance for logging lifecycle events. * @return - * A `Resource` encapsulating the `PreprocessorEndpoints` instance. Ensures that the resource is properly - * initialised and cleaned up. + * A `Resource` encapsulating the `PreprocessorEndpoints` instance, ensuring proper initialisation and cleanup. */ def preprocessorEndpointsResource( reactionService: ReactionService[IO], @@ -39,19 +56,32 @@ object EndpointResources { logger.info("Creating Preprocessor Endpoints") *> IO(new PreprocessorEndpoints(reactionService, mechanismService)) )(endpoints => - logger.info("Shutting down Preprocessor Endpoints").handleErrorWith(_ => IO.unit) + logger + .info("Shutting down Preprocessor Endpoints") + .handleErrorWith(_ => IO.unit) ) /** * Creates a managed resource for the `ReaktoroEndpoints`. * + * This method initialises and manages the lifecycle of the `ReaktoroEndpoints` instance, which handles API routes for + * Reaktoro-related services. Lifecycle events are logged for better observability during resource creation and + * release. + * + * Example usage: + * {{{ + * val reaktoroResource = EndpointResources.reaktoroEndpointsResource(reaktoroService) + * reaktoroResource.use { endpoints => + * // Use the endpoints to serve HTTP routes + * } + * }}} + * * @param reaktoroService - * An instance of `ReaktoroService` used by the endpoints for handling Reaktoro-related operations. + * An instance of `ReaktoroService` for handling Reaktoro-related operations. * @param logger - * A logger instance for logging lifecycle events and errors during the resource's creation and release. + * A logger instance for logging lifecycle events. * @return - * A `Resource` encapsulating the `ReaktoroEndpoints` instance. Ensures that the resource is properly initialised - * and cleaned up. + * A `Resource` encapsulating the `ReaktoroEndpoints` instance, ensuring proper initialisation and cleanup. */ def reaktoroEndpointsResource( reaktoroService: ReaktoroService[IO] @@ -62,7 +92,9 @@ object EndpointResources { logger.info("Creating Reaktoro Endpoints") *> IO(new ReaktoroEndpoints(reaktoroService)) )(endpoints => - logger.info("Shutting down Reaktoro Endpoints").handleErrorWith(_ => IO.unit) + logger + .info("Shutting down Reaktoro Endpoints") + .handleErrorWith(_ => IO.unit) ) } diff --git a/src/main/scala/app/units/ServerResources.scala b/src/main/scala/app/units/ServerResources.scala index 0d754a3..1ef3691 100644 --- a/src/main/scala/app/units/ServerResources.scala +++ b/src/main/scala/app/units/ServerResources.scala @@ -8,17 +8,37 @@ import org.typelevel.log4cats.Logger import org.http4s.HttpRoutes /** - * Provides resources related to server management for the application. + * Provides managed resources for server management in the application. + * + * This object encapsulates the creation and lifecycle management of the `ServerBuilder`, ensuring that the server is + * properly initialised and cleaned up as a managed resource. */ object ServerResources { /** * Creates a managed resource for the `ServerBuilder`. * + * This method initialises and manages the lifecycle of a `ServerBuilder` instance, which serves the provided API + * routes. It logs lifecycle events during resource creation and cleanup for better observability. + * + * Example usage: + * {{{ + * import org.typelevel.log4cats.slf4j.Slf4jLogger + * import app.units.ServerResources + * + * implicit val logger = Slf4jLogger.getLogger[IO] + * + * val serverResource = ServerResources.serverBuilderResource(myRoutes) + * serverResource.use { serverBuilder => + * val server = serverBuilder.startServer(Host.fromString("127.0.0.1").get, Port.fromInt(8080).get) + * server.useForever + * } + * }}} + * * @param routes * The `HttpRoutes[IO]` containing the API routes to be served by the server. * @param logger - * An implicit logger instance for logging lifecycle events and errors during the resource's creation and release. + * An implicit logger instance for logging lifecycle events and errors. * @return * A `Resource[IO, ServerBuilder]` that manages the lifecycle of the `ServerBuilder` instance. */ @@ -31,7 +51,9 @@ object ServerResources { logger.info("Creating Server Builder") *> IO(new ServerBuilder(routes)) )(endpoints => - logger.info("Shutting down ServerBuilder").handleErrorWith(_ => IO.unit) + logger + .info("Shutting down ServerBuilder") + .handleErrorWith(_ => IO.unit) ) } diff --git a/src/main/scala/app/units/ServiceResources.scala b/src/main/scala/app/units/ServiceResources.scala index e87c89e..f9a7138 100644 --- a/src/main/scala/app/units/ServiceResources.scala +++ b/src/main/scala/app/units/ServiceResources.scala @@ -18,26 +18,38 @@ import scala.concurrent.ExecutionContext import scala.concurrent.duration.FiniteDuration /** - * Provides resources for service initialisation and lifecycle management. + * Provides managed resources for initialising and managing services in the application. * - * This object contains methods to create managed resources for various application services, ensuring proper - * initialisation and cleanup using the `Resource` abstraction. + * This object encapsulates the lifecycle management of core services like `MechanismService`, `ReactionService`, + * `ReaktoroService`, and various caching services. By using `Resource`, it ensures that resources are properly + * initialised and cleaned up. */ object ServiceResources { /** * Creates a managed resource for the `MechanismService`. * + * The `MechanismService` interacts with caching and HTTP APIs to manage mechanisms. This method ensures that the + * service is initialised and cleaned up correctly, with logging for lifecycle events. + * + * Example usage: + * {{{ + * val mechanismResource = ServiceResources.mechanismServiceResource(cacheService, httpClient, baseUri) + * mechanismResource.use { mechanismService => + * // Use the mechanismService + * } + * }}} + * * @param cacheService * The distributed cache service used for storing and retrieving mechanisms. * @param client - * The HTTP client instance used to make requests to the mechanism service endpoints. + * The HTTP client instance used for making API requests. * @param baseUri * The base URI for the mechanism service's API endpoints. * @param logger - * An implicit logger instance for logging lifecycle events and errors during the resource's creation and release. + * An implicit logger instance for logging lifecycle events. * @return - * A `Resource[IO, MechanismService[IO]]` that ensures the lifecycle of the `MechanismService` is managed correctly. + * A `Resource[IO, MechanismService[IO]]` for the managed lifecycle of the `MechanismService`. */ def mechanismServiceResource( cacheService: DistributedCacheService[IO], @@ -56,16 +68,19 @@ object ServiceResources { /** * Creates a managed resource for the `ReactionService`. * + * The `ReactionService` handles caching and API interactions for reactions. This method manages its lifecycle, + * ensuring proper initialisation and cleanup with appropriate logging. + * * @param cacheService * The distributed cache service used for storing and retrieving reactions. * @param client - * The HTTP client instance used to make requests to the reaction service endpoints. + * The HTTP client instance used for making API requests. * @param baseUri * The base URI for the reaction service's API endpoints. * @param logger - * An implicit logger instance for logging lifecycle events and errors during the resource's creation and release. + * An implicit logger instance for logging lifecycle events. * @return - * A `Resource[IO, ReactionService[IO]]` that ensures the lifecycle of the `ReactionService` is managed correctly. + * A `Resource[IO, ReactionService[IO]]` for the managed lifecycle of the `ReactionService`. */ def reactionServiceResource( cacheService: DistributedCacheService[IO], @@ -84,16 +99,19 @@ object ServiceResources { /** * Creates a managed resource for the `ReaktoroService`. * + * The `ReaktoroService` builds on the `ReactionService` to provide extended functionality for managing reactions. + * This method ensures its lifecycle is properly managed, with detailed logging for creation and shutdown. + * * @param reactionService - * The reaction service used to provide dependencies for the `ReaktoroService`. + * The `ReactionService` used for providing dependencies to the `ReaktoroService`. * @param client - * The HTTP client instance used to make requests to the Reaktoro service endpoints. + * The HTTP client instance used for making API requests. * @param baseUri * The base URI for the Reaktoro service's API endpoints. * @param logger - * An implicit logger instance for logging lifecycle events and errors during the resource's creation and release. + * An implicit logger instance for logging lifecycle events. * @return - * A `Resource[IO, ReaktoroService[IO]]` that ensures the lifecycle of the `ReaktoroService` is managed correctly. + * A `Resource[IO, ReaktoroService[IO]]` for the managed lifecycle of the `ReaktoroService`. */ def reaktoroServiceResource( reactionService: ReactionService[IO], @@ -110,12 +128,17 @@ object ServiceResources { ) /** - * Creates a managed resource for the `CacheService`. + * Creates a managed resource for the `LocalCacheService`. + * + * This method initialises a simple in-memory cache for local caching needs. The cache lifecycle is managed, and + * events are logged during creation and release. * + * @param ttl + * The time-to-live duration for cache entries. * @param logger - * An implicit logger instance for logging lifecycle events and errors during the resource's creation and release. + * An implicit logger instance for logging lifecycle events. * @return - * A `Resource[IO, CacheService[IO]]` that ensures the lifecycle of the `CacheService` is managed correctly. + * A `Resource[IO, LocalCacheService[IO]]` for the managed lifecycle of the `LocalCacheService`. */ def localCacheServiceResource( implicit @@ -132,15 +155,21 @@ object ServiceResources { /** * Creates a managed resource for the `DistributedCacheService`. * + * This method initialises a distributed cache backed by Akka Cluster. It ensures proper integration with the actor + * system and cluster configuration while managing the lifecycle and logging events. + * * @param system * The `ActorSystem` used for Akka-based concurrency and distributed data. * @param selfUniqueAddress - * The unique address of the current actor system instance, used for distributed data in a cluster. + * The unique address of the current actor system instance, used for cluster data. + * @param ex + * An implicit `ExecutionContext` for asynchronous operations. + * @param ttl + * The time-to-live duration for cache entries. * @param logger - * An implicit logger instance for logging lifecycle events and errors during the resource's creation and release. + * An implicit logger instance for logging lifecycle events. * @return - * A `Resource[IO, DistributedCacheService[IO]]` that ensures the lifecycle of the `DistributedCacheService` is - * managed correctly. + * A `Resource[IO, DistributedCacheService[IO]]` for the managed lifecycle of the `DistributedCacheService`. */ def distributedCacheServiceResource( system: ActorSystem, diff --git a/src/main/scala/app/units/SystemResources.scala b/src/main/scala/app/units/SystemResources.scala index 44b63f5..a10ae01 100644 --- a/src/main/scala/app/units/SystemResources.scala +++ b/src/main/scala/app/units/SystemResources.scala @@ -9,22 +9,40 @@ import org.typelevel.log4cats.Logger import scala.concurrent.ExecutionContext /** - * Provides resources for managing system-level components, such as the `ActorSystem`. + * Provides managed resources for system-level components. + * + * This object encapsulates the lifecycle management of system-level components like the `ActorSystem`, ensuring proper + * initialisation and termination using the `Resource` abstraction. */ object SystemResources { /** * Creates a managed resource for the `ActorSystem`. * + * This method manages the lifecycle of an `ActorSystem` instance, ensuring it is properly initialised and terminated. + * Lifecycle events, including creation and termination, are logged for observability. Any errors during termination + * are captured and logged. + * + * Example usage: + * {{{ + * implicit val system: ActorSystem = ActorSystem("my-system") + * implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] + * implicit val ec: ExecutionContext = system.dispatcher + * + * val systemResource = SystemResources.actorSystemResource + * systemResource.use { actorSystem => + * // Use the actor system + * } + * }}} + * * @param ec * The `ExecutionContext` to be used by the `ActorSystem`. * @param system * The `ActorSystem` instance to be managed. * @param logger - * An implicit logger instance for logging lifecycle events and errors during the resource's creation and release. + * An implicit logger instance for logging lifecycle events. * @return - * A `Resource[IO, ActorSystem]` that manages the lifecycle of the `ActorSystem` instance, ensuring proper - * termination. + * A `Resource[IO, ActorSystem]` that ensures proper initialisation and termination of the `ActorSystem`. */ def actorSystemResource( implicit diff --git a/src/main/scala/config/ConfigLoader.scala b/src/main/scala/config/ConfigLoader.scala index 66602f3..6769333 100644 --- a/src/main/scala/config/ConfigLoader.scala +++ b/src/main/scala/config/ConfigLoader.scala @@ -176,6 +176,12 @@ sealed trait ConfigLoader { def engineHttpClientConfig: ChemistEngineHttpClient } +/** + * Provides a loader for application configuration. + * + * The configuration is loaded from `application.conf` and `reference.conf` files, with the former taking precedence. + * This object also sets up logging configurations. + */ object ConfigLoader { System.setProperty("logback.configurationFile", "src/main/scala/resource/logback.xml") @@ -203,6 +209,12 @@ object ConfigLoader { } +/** + * Provides a loader for test configuration. + * + * The configuration is loaded from `application.spec.conf` and `reference.conf` files, with the former taking + * precedence. This loader is used specifically for test environments. + */ object TestConfigLoader { System.setProperty("logback.configurationFile", "src/main/scala/resource/logback.xml") @@ -225,7 +237,7 @@ object TestConfigLoader { override val preprocessorHttpClientConfig: ChemistPreprocessorHttpClient = appConfig.preprocessorHttpClient override val engineHttpClientConfig: ChemistEngineHttpClient = appConfig.engineHttpClient - val pureConfig = config + val pureConfig: Config = config } } diff --git a/src/main/scala/core/services/cache/DistributedCacheService.scala b/src/main/scala/core/services/cache/DistributedCacheService.scala index 4e283c1..283268c 100644 --- a/src/main/scala/core/services/cache/DistributedCacheService.scala +++ b/src/main/scala/core/services/cache/DistributedCacheService.scala @@ -18,14 +18,20 @@ import scala.concurrent.duration._ /** * A distributed cache service for managing mechanisms and reactions using Akka Distributed Data. * - * This service provides caching with consistency guarantees across multiple nodes in a cluster. + * This service provides caching with consistency guarantees across multiple nodes in a cluster. It uses `LWWMap` + * (Last-Write-Wins Map) for conflict resolution and performs distributed read and write operations with configurable + * timeouts. * * @param system - * The ActorSystem for Akka operations. + * The ActorSystem for Akka operations, required to initialise the Distributed Data replicator. * @param selfUniqueAddress * The unique address of the node interacting with the cache. + * @param ec + * The ExecutionContext for handling asynchronous operations within the service. + * @param ttl + * The Timeout for distributed operations like `Get` and `Update`. * @tparam F - * The effect type (e.g., `IO`, `Future`, etc.). + * The effect type (e.g., `IO`, `Future`, etc.) used to encapsulate asynchronous computations. */ class DistributedCacheService[F[_]: Async]( system: ActorSystem, diff --git a/src/main/scala/core/services/cache/LocalCacheService.scala b/src/main/scala/core/services/cache/LocalCacheService.scala index 2199acf..f079765 100644 --- a/src/main/scala/core/services/cache/LocalCacheService.scala +++ b/src/main/scala/core/services/cache/LocalCacheService.scala @@ -9,8 +9,14 @@ import core.services.cache.types.CacheServiceTrait /** * A local, in-memory service for caching mechanisms and reactions with a time-to-live (TTL) mechanism. * + * This service uses a `TrieMap` for thread-safe, concurrent caching. Each cache entry is timestamped, and expired + * entries are removed based on the configured TTL. The service provides CRUD operations for mechanisms and reactions, + * ensuring expired entries are not returned or updated. + * + * @param ttl + * The time-to-live (TTL) for cache entries. Entries older than this duration are considered expired. * @tparam F - * The effect type (e.g., `IO`, `SyncIO`, etc.). + * The effect type (e.g., `IO`, `SyncIO`, etc.) used to encapsulate computations. */ class LocalCacheService[F[_]: Sync]( implicit ttl: FiniteDuration @@ -79,6 +85,15 @@ class LocalCacheService[F[_]: Sync]( } } + /** + * Removes expired entries from all caches. + * + * This method checks the timestamp of each cache entry and removes entries that have exceeded the configured TTL. + * This operation is performed in-memory and is thread-safe. + * + * @return + * An effectful computation that completes when all expired entries have been removed. + */ def cleanExpiredEntries: F[Unit] = Sync[F].delay { cleanCache(mechanismCache) cleanCache(mechanismDetailsCache) diff --git a/src/main/scala/core/services/cache/types/CacheService.scala b/src/main/scala/core/services/cache/types/CacheService.scala index 7a023b8..8adb72a 100644 --- a/src/main/scala/core/services/cache/types/CacheService.scala +++ b/src/main/scala/core/services/cache/types/CacheService.scala @@ -10,8 +10,6 @@ import core.domain.preprocessor.{Mechanism, MechanismDetails, MechanismId, React */ trait CacheServiceTrait[F[_]] { - // Mechanism-related methods - /** * Retrieves a mechanism from the cache. * @@ -68,8 +66,6 @@ trait CacheServiceTrait[F[_]] { */ def createMechanism(id: MechanismId, mechanism: Mechanism): F[Either[String, Mechanism]] - // Reaction-related methods - /** * Retrieves a reaction from the cache. * @@ -126,8 +122,6 @@ trait CacheServiceTrait[F[_]] { */ def createReaction(id: ReactionId, reaction: Reaction): F[Either[String, Reaction]] - // Maintenance methods - /** * Cleans expired entries from the cache. * diff --git a/src/main/scala/core/services/flow/ReaktoroService.scala b/src/main/scala/core/services/flow/ReaktoroService.scala index 103967d..487b87c 100644 --- a/src/main/scala/core/services/flow/ReaktoroService.scala +++ b/src/main/scala/core/services/flow/ReaktoroService.scala @@ -19,6 +19,10 @@ import org.http4s.{Method, Request, Status, Uri} /** * Service for interacting with the Chemist Engine to compute system properties for reactions. * + * This service integrates with the `ReactionService` to fetch reaction details and uses an HTTP client to communicate + * with the Chemist Engine. System properties are computed by creating system states and sending them to the Chemist + * Engine for processing. The service supports parallel processing for improved performance. + * * @param reactionService * The service for managing reaction details. * @param chemistEngineClient @@ -26,7 +30,7 @@ import org.http4s.{Method, Request, Status, Uri} * @param chemistEngineUri * The base URI of the Chemist Engine. * @tparam F - * The effect type (e.g., `IO`, `SyncIO`, etc.). + * The effect type (e.g., `IO`, `SyncIO`, etc.) that supports concurrency. */ class ReaktoroService[F[_]: Concurrent]( reactionService: ReactionService[F], @@ -35,16 +39,18 @@ class ReaktoroService[F[_]: Concurrent]( ) { /** - * Computes system properties for a given reaction ID. + * Computes system properties for a given reaction ID by creating system states and sending them to the Chemist Engine + * for processing. * * @param reactionId - * The ID of the reaction. + * The unique ID of the reaction. * @param database - * The thermodynamic database to use. + * The thermodynamic database to use for the computation. * @param amounts * The molecule amounts for the reaction. * @return - * A list of `Either` containing system properties or errors for each system state. + * An effectful computation yielding a list of `Either[SystemPropsError, SystemProps]`. Each element represents the + * result of computing system properties for a specific system state, with errors captured as `SystemPropsError`. */ def computeSystemPropsForReaction( reactionId: ReactionId, @@ -61,18 +67,6 @@ class ReaktoroService[F[_]: Concurrent]( case Left(error) => Concurrent[F].raiseError(error) } - /** - * Creates a list of system states for a reaction. - * - * @param reactionDetails - * The details of the reaction. - * @param database - * The thermodynamic database to use. - * @param amounts - * The molecule amounts for the reaction. - * @return - * A list of `SystemState` instances. - */ private def createSystemStateList( reactionDetails: ReactionDetails, database: DataBase, @@ -98,16 +92,6 @@ class ReaktoroService[F[_]: Concurrent]( } } - /** - * Computes the molecule amounts for a reaction based on inbound and outbound data. - * - * @param reactionDetails - * The details of the reaction. - * @param amounts - * The molecule amounts for the reaction. - * @return - * A map of molecules to their respective amounts. - */ private def computeMoleculeAmounts( reactionDetails: ReactionDetails, amounts: MoleculeAmountList @@ -123,27 +107,11 @@ class ReaktoroService[F[_]: Concurrent]( (inboundAmounts ++ outboundAmounts).toMap } - /** - * Sends a batch of system states to the Chemist Engine. - * - * @param systemStates - * The list of system states to send. - * @return - * A list of `Either` containing system properties or errors for each system state. - */ private def sendBatchToChemistEngine( systemStates: List[SystemState] ): F[List[Either[SystemPropsError, SystemProps]]] = systemStates.parTraverse(sendToChemistEngine) - /** - * Sends a single system state to the Chemist Engine. - * - * @param systemState - * The system state to send. - * @return - * An `Either` containing the computed system properties or an error. - */ private def sendToChemistEngine( systemState: SystemState ): F[Either[SystemPropsError, SystemProps]] = diff --git a/src/main/scala/core/services/preprocessor/MechanismService.scala b/src/main/scala/core/services/preprocessor/MechanismService.scala index 17b64fd..97ff108 100644 --- a/src/main/scala/core/services/preprocessor/MechanismService.scala +++ b/src/main/scala/core/services/preprocessor/MechanismService.scala @@ -16,18 +16,55 @@ import io.circe.parser.decode import org.http4s.circe._ +/** + * Service for managing mechanisms using both a distributed cache and remote HTTP service. + * + * This service provides methods to fetch, create, and delete mechanisms. It interacts with a distributed cache for + * efficient data retrieval and synchronises with a remote service via HTTP for data persistence and updates. + * + * @param cacheService + * The distributed cache service used for storing and retrieving mechanisms. + * @param client + * The HTTP client for making requests to the remote mechanism service. + * @param baseUri + * The base URI of the remote mechanism service. + * @tparam F + * The effect type (e.g., `IO`, `SyncIO`, etc.) that supports concurrency. + */ class MechanismService[F[_]: Concurrent]( cacheService: DistributedCacheService[F], client: Client[F], baseUri: Uri ) { + /** + * Fetches a mechanism by its ID. + * + * This method first checks the distributed cache for the requested mechanism. If the mechanism is not found in the + * cache, it fetches the data from the remote mechanism service and updates the cache. + * + * @param id + * The unique identifier of the mechanism to fetch. + * @return + * An effectful computation that yields the `MechanismDetails` for the given ID. + */ def getMechanism(id: MechanismId): F[MechanismDetails] = cacheService.getMechanismDetails(id).flatMap { case Some(cachedMechanism) => cachedMechanism.pure[F] case None => fetchMechanismFromRemote(id) } + /** + * Creates a new mechanism. + * + * This method sends a `POST` request to the remote mechanism service to create a new mechanism. The created mechanism + * is then added to the distributed cache. + * + * @param mechanism + * The mechanism to create. + * @return + * An effectful computation that yields the created `Mechanism` upon success. + */ def createMechanism(mechanism: Mechanism): F[Mechanism] = makeRequest[Mechanism]( Request[F](Method.POST, baseUri).withEntity(mechanism.asJson), @@ -39,6 +76,19 @@ class MechanismService[F[_]: Concurrent]( cacheService.putMechanism(createdMechanism.mechanismId, createdMechanism) ) + /** + * Deletes a mechanism by its ID. + * + * This method sends a `DELETE` request to the remote mechanism service. If the deletion is successful, the cache is + * updated to remove any stale data. + * + * @param id + * The unique identifier of the mechanism to delete. + * @return + * An effectful computation that yields: + * - `Right(true)` if the mechanism was successfully deleted. + * - `Left(MechanismError)` if an error occurred during deletion. + */ def deleteMechanism(id: MechanismId): F[Either[MechanismError, Boolean]] = client .run(Request[F](Method.DELETE, baseUri / id.toString)) diff --git a/src/main/scala/core/services/preprocessor/ReactionService.scala b/src/main/scala/core/services/preprocessor/ReactionService.scala index 935a5eb..68d4ca4 100644 --- a/src/main/scala/core/services/preprocessor/ReactionService.scala +++ b/src/main/scala/core/services/preprocessor/ReactionService.scala @@ -12,18 +12,55 @@ import io.circe.syntax._ import io.circe.parser.decode import org.http4s.circe._ +/** + * Service for managing reactions using both a distributed cache and remote HTTP service. + * + * This service provides methods to fetch, create, and delete reactions. It integrates with a distributed cache for + * efficient data retrieval and interacts with a remote service via HTTP for data persistence and updates. + * + * @param distributedCache + * The distributed cache service used for storing and retrieving reactions. + * @param client + * The HTTP client for making requests to the remote reaction service. + * @param baseUri + * The base URI of the remote reaction service. + * @tparam F + * The effect type (e.g., `IO`, `SyncIO`, etc.) that supports concurrency. + */ class ReactionService[F[_]: Concurrent]( distributedCache: DistributedCacheService[F], client: Client[F], baseUri: Uri ) { + /** + * Fetches a reaction by its ID. + * + * This method first checks the distributed cache for the requested reaction. If the reaction is not found in the + * cache, it fetches the data from the remote reaction service and updates the cache. + * + * @param id + * The unique identifier of the reaction to fetch. + * @return + * An effectful computation that yields the `ReactionDetails` for the given ID. + */ def getReaction(id: ReactionId): F[ReactionDetails] = distributedCache.getReactionDetails(id).flatMap { case Some(cachedReaction) => cachedReaction.pure[F] case None => fetchReactionFromRemote(id) } + /** + * Creates a new reaction. + * + * This method sends a `POST` request to the remote reaction service to create a new reaction. The created reaction is + * then added to the distributed cache. + * + * @param reaction + * The reaction to create. + * @return + * An effectful computation that yields the created `Reaction` upon success. + */ def createReaction(reaction: Reaction): F[Reaction] = makeRequest[Reaction]( Request[F](Method.POST, baseUri).withEntity(reaction.asJson), @@ -35,6 +72,19 @@ class ReactionService[F[_]: Concurrent]( distributedCache.putReaction(createdReaction.reactionId, createdReaction) ) + /** + * Deletes a reaction by its ID. + * + * This method sends a `DELETE` request to the remote reaction service. If the deletion is successful, the cache is + * updated to remove any stale data. + * + * @param id + * The unique identifier of the reaction to delete. + * @return + * An effectful computation that yields: + * - `Right(true)` if the reaction was successfully deleted. + * - `Left(ReactionError)` if an error occurred during deletion. + */ def deleteReaction(id: ReactionId): F[Either[ReactionError, Boolean]] = client .run(Request[F](Method.DELETE, baseUri / id.toString)) diff --git a/src/main/scala/infrastructure/http/HttpClient.scala b/src/main/scala/infrastructure/http/HttpClient.scala index e8e77e5..86aaccd 100644 --- a/src/main/scala/infrastructure/http/HttpClient.scala +++ b/src/main/scala/infrastructure/http/HttpClient.scala @@ -14,23 +14,19 @@ import io.circe.parser.decode /** * A generic HTTP client for making RESTful API requests. * + * This client provides methods for performing standard HTTP operations (`GET`, `POST`, `PUT`, `DELETE`) and supports + * JSON encoding/decoding using Circe. Requests are constructed relative to the specified base URI, and responses are + * returned as effectful computations in the specified effect type. + * * @param client - * The underlying HTTP client instance. + * The underlying HTTP client instance used to send requests and receive responses. * @param baseUri - * The base URI for all requests. + * The base URI for all API requests made by this client. * @tparam F - * The effect type (e.g., `IO`, `Future`, etc.). + * The effect type (e.g., `IO`, `Future`, etc.) that supports asynchronous and concurrent computations. */ class HttpClient[F[_]: Async](client: Client[F], baseUri: Uri) extends Http4sClientDsl[F] { - /** - * Sends an HTTP request and returns the response as a string. - * - * @param request - * The HTTP request to send. - * @return - * An effect wrapping the response body as a string. Throws an exception if the response status indicates failure. - */ private def sendRequest(request: Request[F]): F[String] = client.run(request).use { response => response.as[String].flatMap { body => @@ -42,10 +38,12 @@ class HttpClient[F[_]: Async](client: Client[F], baseUri: Uri) extends Http4sCli /** * Sends an HTTP GET request. * + * Constructs a GET request relative to the base URI and sends it to the specified endpoint. + * * @param endpoint - * The URI path of the endpoint. + * The URI path of the endpoint to query. * @return - * An effect wrapping the response body as a string. + * An effectful computation that yields the response body as a string. */ def get(endpoint: Uri.Path): F[String] = { val request = Request[F](method = GET, uri = baseUri.withPath(endpoint)) @@ -55,14 +53,17 @@ class HttpClient[F[_]: Async](client: Client[F], baseUri: Uri) extends Http4sCli /** * Sends an HTTP POST request with a JSON payload. * + * Constructs a POST request relative to the base URI and sends it to the specified endpoint. The payload is + * serialised to JSON using Circe. + * * @param endpoint - * The URI path of the endpoint. + * The URI path of the endpoint to send the request to. * @param payload - * The JSON payload to send. + * The JSON payload to include in the request body. * @tparam T - * The type of the payload, which must have a Circe `Encoder` instance. + * The type of the payload, which must have an implicit Circe `Encoder` instance. * @return - * An effect wrapping the response body as a string. + * An effectful computation that yields the response body as a string. */ def post[T: Encoder](endpoint: Uri.Path, payload: T): F[String] = { val request = Request[F](method = POST, uri = baseUri.withPath(endpoint)) @@ -73,14 +74,17 @@ class HttpClient[F[_]: Async](client: Client[F], baseUri: Uri) extends Http4sCli /** * Sends an HTTP PUT request with a JSON payload. * + * Constructs a PUT request relative to the base URI and sends it to the specified endpoint. The payload is serialised + * to JSON using Circe. + * * @param endpoint - * The URI path of the endpoint. + * The URI path of the endpoint to send the request to. * @param payload - * The JSON payload to send. + * The JSON payload to include in the request body. * @tparam T - * The type of the payload, which must have a Circe `Encoder` instance. + * The type of the payload, which must have an implicit Circe `Encoder` instance. * @return - * An effect wrapping the response body as a string. + * An effectful computation that yields the response body as a string. */ def put[T: Encoder](endpoint: Uri.Path, payload: T): F[String] = { val request = Request[F](method = PUT, uri = baseUri.withPath(endpoint)) @@ -91,10 +95,12 @@ class HttpClient[F[_]: Async](client: Client[F], baseUri: Uri) extends Http4sCli /** * Sends an HTTP DELETE request. * + * Constructs a DELETE request relative to the base URI and sends it to the specified endpoint. + * * @param endpoint - * The URI path of the endpoint. + * The URI path of the endpoint to delete. * @return - * An effect wrapping the response body as a string. + * An effectful computation that yields the response body as a string. */ def delete(endpoint: Uri.Path): F[String] = { val request = Request[F](method = DELETE, uri = baseUri.withPath(endpoint)) @@ -104,12 +110,15 @@ class HttpClient[F[_]: Async](client: Client[F], baseUri: Uri) extends Http4sCli /** * Decodes a JSON response into a specified type. * + * Attempts to decode the JSON string into the specified type using Circe. If decoding fails, this method raises an + * error with the failure details. + * * @param response * The response body as a JSON string. * @tparam T - * The type to decode into, which must have a Circe `Decoder` instance. + * The type to decode into, which must have an implicit Circe `Decoder` instance. * @return - * An effect wrapping the decoded value of type `T`. Throws an exception if the decoding fails. + * An effectful computation that yields the decoded value of type `T`. */ def decodeResponse[T: Decoder](response: String): F[T] = Async[F].fromEither(decode[T](response).left.map(err => new Exception(s"Decoding failed: $err"))) @@ -117,7 +126,16 @@ class HttpClient[F[_]: Async](client: Client[F], baseUri: Uri) extends Http4sCli } /** - * Companion object for `HttpClient` providing a factory method. + * Creates a new `HttpClient` instance. + * + * @param client + * The underlying HTTP client instance used to send requests and receive responses. + * @param baseUri + * The base URI for all API requests made by the client. + * @tparam F + * The effect type (e.g., `IO`, `Future`, etc.) that supports asynchronous computations. + * @return + * A new `HttpClient` instance configured with the given client and base URI. */ object HttpClient { @@ -125,13 +143,13 @@ object HttpClient { * Creates a new `HttpClient` instance. * * @param client - * The underlying HTTP client instance. + * The underlying HTTP client instance used to send requests and receive responses. * @param baseUri - * The base URI for all requests. + * The base URI for all API requests made by the client. * @tparam F - * The effect type (e.g., `IO`, `Future`, etc.). + * The effect type (e.g., `IO`, `Future`, etc.) that supports asynchronous computations. * @return - * A new `HttpClient` instance. + * A new `HttpClient` instance configured with the given client and base URI. */ def resource[F[_]: Async](client: Client[F], baseUri: Uri): HttpClient[F] = { new HttpClient[F](client, baseUri)