diff --git a/README.md b/README.md index 90d5dd5..859010c 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Gets the web port from the config file [config file](test/resources/config.edn#L # Helpers ## [helpers/state-flow/server/pedestal](src/parenthesin/helpers/state_flow/server/pedestal.clj) -Extract `io.pedestat.http/service-fn` from state-flow context and calls `io.pedestat.test/response-for` to simulate and http request on the system server. +Extract `io.pedestal.http/service-fn` from state-flow context and calls `io.pedestal.test/response-for` to simulate and http request on the system server. *Check [system integration tests](./test/integration/parenthesin/schema/system_test.clj#L79) to see how to use this function.* ## [helpers/state-flow/db](src/parenthesin/helpers/state_flow/db.clj) @@ -139,6 +139,11 @@ clj -M:clojure-lsp diagnostics - [pg-embedded-clj](https://github.com/Bigsy/pg-embedded-clj) Embedded PostgreSQL for integration tests - [clojure-lsp](https://github.com/clojure-lsp/clojure-lsp/) Code Format, Namespace Check and Diagnosis +# Documentation + +If you want to know more about the components in general and each component or helper implementation, check the [documentation](doc/intro.md). +Contributions and suggestions are welcome. + # License Copyright © 2023 Parenthesin diff --git a/doc/cljdoc.edn b/doc/cljdoc.edn new file mode 100644 index 0000000..8d09fc2 --- /dev/null +++ b/doc/cljdoc.edn @@ -0,0 +1,6 @@ +{:cljdoc.doc/tree + [["Readme" {:file "README.md"}] + ; ["Changes" {:file "CHANGELOG.md"}] ;; todo + ["Intro" {:file "doc/intro.md"}] + ["Components" {:file "doc/components.md"}] + ["Helpers" {:file "doc/helpers.md"}]]} diff --git a/doc/components.md b/doc/components.md new file mode 100644 index 0000000..9128203 --- /dev/null +++ b/doc/components.md @@ -0,0 +1,238 @@ +# Components + +This document provides a full introduction and usage example of each component implemented here. Let's start with it! + +## config/aero +This component is responsible for reading and parsing configuration files from our environment based on a `resources/config.edn` file or getting the current [profile](https://github.com/juxt/aero#profile) on environment var `SYSTEM_ENV`. The used library is defined as `A small library for explicit, intentful configuration` and you can configure multiple environment variables for each specific usage. + +Look below for an example of `resources/config.edn` file: +```clojure +{:webserver/port #long #or [#env PORT 3001] + + :database {:dbtype "postgres" + :dbname #or [#env DB-NAME "postgres"] + :username #or [#env DB-USER "postgres"] + :password #or [#env DB-PASS "postgres"] + :host #or [#env DB-HOST "localhost"] + :port #or [#env DB-PORT 5432]}} +``` +We define a `:webserver/port` for example, that can come from a defined `PORT` in our environment, or if it's not defined, it will assume as `3001`. The same logic is valid for each field in the database configuration. + +To start our component we can start building our system map: +```clojure +(defn- build-system-map [] + (component/system-map + ;; some other components + :config (config/new-config))) +``` +> It's normal to see this component as the first one on the system map because it will start with every configuration that comes from our environment. + +The `dev` profile is the default for `SYSTEM_ENV`, so if you want to set up another profile you can define your `SYSTEM_ENV` to a new value and a new environment will be configured. For example, if we want to use and redefine `:port` for each specific profile, we can implement something like: +```clojure +{:webserver/port #profile {:default 3001 + :dev 8001 + :test 8002}} +``` + +Then, to specify a port you can use: +```clojure +(read-config "config.edn" {:profile :dev}) +``` +> Be free to use any profile that you want! This is helpful to manage custom environments. + +## db/jdbc-hikari +To set up our database that's the component that we use (and it depends on your [connection info data](https://github.com/parenthesin/components/blob/main/test/resources/config.edn#L3) previously configured). This component is using two main libraries to manage our database: [next-jdbc](https://github.com/seancorfield/next-jdbc) and [HikariCP](https://github.com/brettwooldridge/HikariCP). + +### next-jdbc +This library is responsible for managing our access to our database (based on JDBC databases). It is *a new low-level Clojure wrapper for JDBC-based access to databases.* You can check more about [here](https://cljdoc.org/d/com.github.seancorfield/next.jdbc/1.3.955/doc/readme). + +This API is based on using qualified keywords and transducers having a friendly layout to use and handling your SQL queries. If you want to there's also a helper to manage your SQL queries, turning Clojure data structures into SQL called as [honeysql](https://github.com/seancorfield/honeysql) (both of these libraries are maintained by [Sean Corfield](https://github.com/seancorfield)). You can create really awesome SQL queries and use it with the component like the example below: +```clojure +(require '[honey.sql :as sql]) +(require '[honey.sql.helpers :as sql.helpers]) +(require '[parenthesin.components.db.jdbc-hikari :as components.database]) + +(->> (-> (sql.helpers/insert-into :verycooltable) + (sql.helpers/values [{:something "value"}]) + (sql.helpers/returning :*) + sql/format)j + (components.database/execute db)) +``` + +We're also using [jdbc-url](https://github.com/parenthesin/components/blob/main/src/parenthesin/components/db/jdbc_hikari.clj#L17) to build up the connection string and the database configuration map, supporting multiple extra keys if you want to pass them or configure them yourself. That means you can also provide additional data into your `:database` configuration map in your `resources/config.edn` and that will be used! An example of usage is with [useSSL](https://github.com/clj-codes/docs.backend/blob/main/resources/config.edn#L12) - specific for PostgreSQL configuration with SSL. If you want to customize your `db-spec` hash-map there's also great [documentation for next-jdbc](https://github.com/seancorfield/next-jdbc/blob/develop/doc/getting-started.md#the-db-spec-hash-map). + +### HikariCP +Described as *a solid, high-performance, JDBC connection pool at last*, HikariCP manages our connection pool provided by our configuration previously, being a very light library for its amazing usage (at roughly 165Kb). You can check more about these optimizations [here](https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole). + +When the database component is started, a data source will be initialized based on previously configured environment variables for the database, starting a pool using the HikariCP data source. + +### How to use it? +You can build your system map using this component after a `config/aero` load (if you're using it for your environment management). Look at the example below of the component startup: +```clojure +(defn- build-system-map [] + (component/system-map + :config (config/new-config) + ;; some other components + :database (component/using (database/new-database) [:config]))) +``` +> By default your component will try to start using the configuration provided and then if it is not properly set up, it will use the default configuration. + +To run any query you can follow the example above for `honeysql` where you can build an SQL query and execute it with `components.database/execute` function from [parenthesin.components.db.jdbc-hikari](https://github.com/parenthesin/components/blob/main/src/parenthesin/components/db/jdbc_hikari.clj). + +You can also prepare your integration test environment to use this component with [pg-embedded-clj](https://github.com/Bigsy/pg-embedded-clj), a Clojure library that provides embedded PostgreSQL for testing and development purposes. You can see this [example](https://github.com/parenthesin/components/blob/main/test/integration/parenthesin/util.clj#L16) of usage for system initialization to start your database. Check [state-flow.db helper](helpers.md#db) to understand how to use it with your integration tests. + +## http/clj-http +To provide an HTTP client, that's the right component, using [clj-http](https://github.com/dakrone/clj-http) as the main library defined as *an idiomatic clojure http client wrapping the [apache client](https://hc.apache.org/)*. The implementation of this component is divided into two main options: an `Http` component that handles real requests and an `HttpMock` component to help you build some integration tests easily, mocking your requests. + +A `request-fn` is also defined as a function to perform HTTP requests expecting a map containing the keys `:url` and `:method`. You can see an example of the implementation of this component below: +```clojure +(defn- build-system-map [] + (component/system-map + ;; some other components + :http (http/new-http))) +``` + +An example of usage for this component is: +```clojure +(require '[parenthesin.components.http.clj-http :as components.http]) + +(->> {:url "https://api.coindesk.com/v1/bpi/currentprice.json" + :as :json + :methhod :get} + (components.http/request http) ;; http is your component! + :body) +``` +> Then you can easily make general requests and parse these results as you want! + +By default, this component isn't fully prepared to do all HTTP client functionalities. To understand this better and see an example of the reimplementation of this `request` function for test validation check [here](helpers.md#http). + +## routers +We've two implementations of `routers`, and both perform a spec/schema validation for your requests to improve your development in general. As you know, type hints and type inference in Clojure are optional, but we also highly recommend it to provide production-ready code! + +### reitit-malli +To create our router and specifically start routing our app we use [reitit](https://github.com/metosin/reitit), *a fast data-driven routing library for Clojure/Script* with bi-directional routing and also have a [pluggable coercion](https://cljdoc.org/d/metosin/reitit/0.7.2/doc/coercion/coercion-explained). In this component for example we're using [malli](https://github.com/metosin/malli), *high-performance data-driven data specification library for Clojure/Script.* + +This component implements a simple router with some details: +- Exceptions are logged in by default with the log helper; +- [muuntaja](https://github.com/metosin/muuntaja) is used for fast HTTP API format negotiation, encoding and decoding; +- [reitit-swagger](https://github.com/metosin/reitit/tree/master/modules/reitit-swagger) is implemented by default, so you also have a Swagger interface to interact with (and you can configure it with multiple tags if you want to); +- [reitit-pedestal](https://github.com/metosin/reitit/tree/master/modules/reitit-pedestal) is used for routing interceptors and [reitit-ring](https://github.com/metosin/reitit/tree/master/modules/reitit-ring) to manage routes and creating some handlers; + +You can previously define some routes to start your interaction like: +```clojure +(require '[reitit.swagger :as swagger]) + +(def routes + [["/swagger.json" + {:get {:no-doc true + :swagger {:info {:title "example" + :description "small example of usage"}} + :handler (swagger/create-swagger-handler)}}] + + ["/something" + {:swagger {:tags ["something"]}} + + ["/internal" + {:get {:summary "get all wallet entries and current total" + :responses {200 {:body [:map [:something string?]]} + 500 {:body :string}} + :handler ports.http-in/some-handler}}]]]) +``` +> You can just define a vector of values and map your entries! + +And now, you can build your system map to perform a proper router initialization! +```clojure +(defn- build-system-map [] + (component/system-map + ;; some other components + :router (router/new-router routes))) ;; routes are defined above! +``` +> That's it! You only have to pass the `routes` defined above as this amazing vector. + +### reitit-schema +Very similar to `reitit-malli` but using [schema](https://github.com/plumatic/schema) for declarative data description and validation. The main difference between *malli* and *schema* is that *schema* uses macros instead of *annotations* as *malli*! So when you have to define a new schema, you have to import the library manually and use its macro. + +This component implements a simple router with some details: +- Exceptions are logged in by default with the log helper; +- [muuntaja](https://github.com/metosin/muuntaja) is used for fast HTTP API format negotiation, encoding and decoding; +- [reitit-swagger](https://github.com/metosin/reitit/tree/master/modules/reitit-swagger) is implemented by default, so you also have a Swagger interface to interact with (and you can configure it with multiple tags if you want to); +- [reitit-pedestal](https://github.com/metosin/reitit/tree/master/modules/reitit-pedestal) is used for routing interceptors and [reitit-ring](https://github.com/metosin/reitit/tree/master/modules/reitit-ring) to manage routes and create some handlers; +- The only difference in this implementation in comparison to `reitit-malli` is the `coercion` library used: `reitit.schema/coercion` instead of `reitit.malli/coercion` + +You can previously define some routes to start your interaction like: +```clojure +(require '[reitit.swagger :as swagger]) +(require '[schema.core :as s]) + +(s/defschema Something + {:something s/Str}) + +(def routes + [["/swagger.json" + {:get {:no-doc true + :swagger {:info {:title "example" + :description "small example of usage"}} + :handler (swagger/create-swagger-handler)}}] + + ["/something" + {:swagger {:tags ["something"]}} + + ["/internal" + {:get {:summary "get all wallet entries and current total" + :responses {200 {:body Something} + 500 {:body s/Str}} + :handler ports.http-in/some-handler}}]]]) +``` +> You have to define your schema using the macro `defschema`! + +And now, you can build your system map to perform a proper router initialization! +```clojure +(defn- build-system-map [] + (component/system-map + ;; some other components + :router (router/new-router routes))) ;; routes are defined above! +``` +> That's it! You only have to pass the `routes` defined above as this amazing vector. + +## server/reitit-pedestal-jetty +This component directly depends on one of each type of the components defined previously (config, db, http, router) and starts a web server with all components injected in the HTTP context. The web port comes from the `resources/config.edn` like the example below: +```clojure +{:webserver/port #long #or [#env PORT 3001]} +``` +> In the example above the web server port will be loaded in your environment or 3001 by default! + +Using [reitit](https://github.com/metosin/reitit) and [pedestal](https://github.com/pedestal/pedestal) to perform a secure and healthy web server initialization, we have two main functions managing our environment: +- `prod-init`: receives a service map and a router, starting the server with the default interceptors from `io.pedestal.http`; +- `dev-init`: receives a service map and a router, starting the server with the default and dev interceptors from `io.pedestal.http` and setting up manually the `secure-headers` and `allowed-origins`; + +By default, the base service uses `:jetty` for server type, setting `join` to true, defining some secure headers for `content-security-policy-settings`, and also configuring `allowed-origins` if necessary. Logs are also configured to server start and stop. + +To build your system map to perform a proper web server initialization we can define: +```clojure +;; remember: this component directly depends on one of each type of the component +(defn- build-system-map [] + (component/system-map + :config (config/new-config) + :http (http/new-http) + :router (router/new-router routes/routes) + :database (component/using (database/new-database) [:config]) + :webserver (component/using (webserver/new-webserver) [:config :http :router :database]))) +``` + +And now to start completely your system you can define a function: +```clojure +;; this atom will manage our system state +(def system-atom (atom nil)) + +(defn start-system! [system-map] + (->> system-map + component/start + (reset! system-atom))) + +;; then, to start your system +(start-system! (build-system-map)) +``` + +And that's it! Now you can properly understand the base of each component - opening possibilities to start building your Clojure services! Open a parentheses and deep dive into this world. + +If you want, you can check more about some [helpers](helpers.md) (like the logs helper mentioned above)! diff --git a/doc/helpers.md b/doc/helpers.md new file mode 100644 index 0000000..53687ba --- /dev/null +++ b/doc/helpers.md @@ -0,0 +1,154 @@ +# Helpers + +This document provides a full introduction and usage example of each helper implemented here. Let's start with it! + +## state-flow +Integration tests are really important in our development cycle, so this helper aims to help you with [state-flow](https://github.com/nubank/state-flow), an integration testing framework using a state monad in the backend for building and composing flows. You can write multiple flows to build your integration test. By default there are some helpers for `state-flow`, so let's deep dive into each one! + +### server/pedestal +The goal of this helper is to extract `io.pedestal.http/service-fn` from `state-flow` context and call `io.pedestal.test/response-for` to simulate HTTP requests. + +This helper implements a `request!` function that receives a map with `method`, `uri`, `body`, and `headers` (same for composing a request in pedestal) and starts a flow that *makes* an HTTP request and parses the response. You can see an example of this example [here](https://github.com/parenthesin/components/blob/main/test/integration/parenthesin/schema/system_test.clj#L79) and the context down below: +```clojure +;; inside your test +;; don't forget to import corretly the libraries +(flow "should match the response" + (match? {:status 200 + :body "yay!"} + (state-flow.server/request! {:method :post + :uri "/some/url" + :body {:something "yay"}}))) +``` +> This example also uses the [matchers-combinators](https://github.com/nubank/matcher-combinators/) library to validate integration tests. + +By default this implementation for `request!` isn't fully prepared to do all HTTP client functionalities. You can see an [implementation example below](#http) for multipart uploads. +### db +This helper exposes a function to directly execute SQL commands on the `state-flow` context database called `execute!`. An example of this implementation is provided [here](https://github.com/parenthesin/components/blob/main/test/integration/parenthesin/db/jdbc_hikari_test.clj) and an example of usage is: +```clojure +(defflow flow-integration-database-test + {:init (util/start-system! + {:config (components.config/new-config) + :database (component/using (components.db/new-database) [:config])}) + :cleanup util/stop-system! + :fail-fast? true} + (flow "just a simple example" + (match? [#:something{:id 1 + :name "cool example" + :email "example@email.com"}] + (state-flow.db/execute! ["select * from something"])))) +``` + +### http +This helper exposes some functions to set and get an HTTP mock state with `set-http-out--responses!` that receive some responses for mocking, `http-out-requests` to retrieve some HTTP request and `request!` to make an HTTP request based on an HTTP state. You can see an example of implementation [here](https://github.com/parenthesin/components/blob/main/test/integration/parenthesin/http/clj_http_test.clj) and down below: +```clojure + +(defflow flow-integration-database-test + {:init (util/start-system! + {:config (components.config/new-config) + :http (http.clj-http/new-http-mock {"https://duckduckgo.com" {:status 200}}) + :webserver (component/using (components.webserver/new-webserver) [:config :http])}) + :cleanup util/stop-system! + :fail-fast? true} + (flow "start a system with http mock" + (state-flow.http/set-http-out-responses! {"https://goosegoosego.com" + {:body {:msg "quack?"} + :status 200}}) + + (flow "do request in new existing configured mock response" + (match? {:status 200 + :body {:msg "quack?"}} + (state-flow.http/request! {:method :get + :url "https://goosegoosego.com"}))))) +``` +> This example set a mock for `http-out-request` to validate a request. + +In the above example we saw how we can make simple HTTP requests and mock some responses easily, but handling some HTTP client functionalities - like handling multipart upload - can be a little different and needs some custom implementation. Look at the example below: +```clojure +(defn request! + [{:keys [uri body multipart] :as opts}] + (flow "makes http request" + (-> (cond-> opts + body (assoc :form-params body) + (not multipart) (assoc :content-type :json)) + (assoc :as :json + :url (str "http://localhost:3001" uri)) + clj-http.client/request + state-flow.api/return))) +``` +> The same `request!` but with a new validation. The idea here is to perform different scenarios if we have to deal with multipart parameters, handling `clj-http` requests, and returning a state-flow monad for specific usage. + +You can see below an example of the implementation of a multipart parameter route for reitit. There we define which elements we can receive, and handle. +```clojure +["/files" {:swagger {:tags ["files"]}} + ["/upload" + {:post {:summary "upload a file" + :parameters {:multipart [:map-of :string [:or :string malli/temp-file-part]]} + :responses {200 {:body :any}} + :handler (fn [{{multipart :multipart} :parameters}] + {:status 200 + :body {:multipart multipart}})}}]] +``` + +And then if you want to use it in a flow you can simply implement it like that: +```clojure +;; inside a flow! +(request! {:uri "/files/upload" + :as :json + :method :post + :multipart [{:name "fiale" :content "Eggplants"} + {:name "file.jpg" :content (clojure.java.io/file "/Users/rafael.delboni/Downloads/images.jpg")} + {:name "file.csv" :content (clojure.java.io/file "/Users/rafael.delboni/Downloads/file.csv")}]}) +``` +> In this example, we're handling multipart as some different files loaded from our disk, so remember to have these files correctly loaded! + +## logs +Logging is essential to all services, so `components` also implement a helper for logs! This helper uses [timbre](https://github.com/taoensso/timbre), a pure Clojure/Script logging library to provide logging in general with an easy-to-configure interface with pure Clojure data (and that just works). + +By default, this helper implements a setup function that receives two parameters: level and stream (to set a `min-level` and configure a stream into an [appender](https://taoensso.github.io/timbre/taoensso.timbre.appenders.core.html)). This setup can be called into a `start-system!` function like the example above: +```clojure +;; your previous imports +(require '[parenthesin.helpers.logs :as logs]) + +(defn start-system! [system-map] + ;; your previous implementations + (logs/setup :info :auto)) +``` + +This helper also implements a macro wrapping `timbre/log!` for better level context and more. To log something you can follow the example: +```clojure +(require '[parenthesin.helpers.logs :as logs]) + +(logs/log :info :something :received "something!") +``` +> You can first of all pass the level of the log followed by extra arguments to be used in this log information. + +## malli +This helper implements both start and stop functions to instrumentation with [malli](github.com/metosin/malli) to enable runtime validation of arguments and return values. This helps a lot with general testing using `clojure.text/use-fixtures`. You can see an example of usage below: +```clojure +(require '[parenthesin.helpers.malli :as helpers.malli]) + +(use-fixtures :once helpers.malli/with-instrumentation) +``` +> The `with-instrumentation` function wraps `f` ensuring there has malli collect and instrument started before running it. + +## migrations +This helper is a wrapper over [migratus](https://github.com/yogthos/migratus) to create a simple CLI-based API depending on `aero` and `jdbc` to read and connect to the database. The idea is to manage your migrations easily on your service, implementing also functions that can be used inside your `system-start` like the example below: +```clojure +(require '[parenthesin.helpers.migrations :as migrations]) + +(defn start-system! [system-mapa] + ;; some other implementations + (migrations/migrate (migrations/configuration-with-db))) +``` +> This function `configuration-with-db` will get your connection from a map! + +You can also use some CLI commands like `clj -M:migratus create migration-name`, `clj -M:migratus migrate` and `clj -M:migratus rollback` for example. Remember to define this alias on your `deps.edn`: +```clojure +{:paths ["..."] + :deps {com.github.parenthesin/components {:mvn/version "0.3.1"}} + :aliases {:migratus {:main-opts ["-m" "parenthesin.helpers.migrations"]}}} +``` + +And that's it! Now you can use some helpers also implemented here in `parenthesin/components`! Hope this was helpful and see you next time! + +Go [back to main page](intro.md). diff --git a/doc/intro.md b/doc/intro.md index 11fa61d..73fd494 100644 --- a/doc/intro.md +++ b/doc/intro.md @@ -1,3 +1,125 @@ # Introduction to com.github.parenthesin/components -TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) +Hey there, thanks a lot for your interest in learning more about the parenthesis/components library. First of all, we have to talk about "what exactly is a component and why I need it?" + +## Component +Component is defined as a *tiny Clojure framework for managing the lifecycle and dependencies of software components with runtime state*. This means the Component can be seen as a dependency injection using immutable data structures. To give more context about it, you can watch Sierra's talk [Components Just Enough Structure](https://youtu.be/13cmHf_kt-Q?si=VribqpkKYOofgAWz). + +To handle this entire explanation, imagine that you have a database connection to get. If you open so many connections and don't close them, your database will *break* as you increase the opened connections. By default, in object-oriented programming languages we can manage this as an object in memory - only one - that you pass as your parameters by your methods and use that connection - that's where the component came from: you have instanced objects in memory and you have to pass all of them by parameters to access what that object gives you (in our case of a database example, you can make any database operation with it). + +But the Component *magic* is more than that: is how you infer your dependency injection. To understand how it works, see the example below: +```clojure +(defn- build-system-map [] + (component/system-map + :config (config/new-config) + :http (http/new-http) + :router (router/new-router routes/routes) + :database (component/using (database/new-database) [:config]) + :webserver (component/using (webserver/new-webserver) [:config :http :router :database]))) +``` +> The above example is from [microservice-boilerplate](https://github.com/parenthesin/microservice-boilerplate). + +You have some components that are configured: you have a `:config`, `:http`, `:router`, `:database` and `:webserver`. Look at the `:config` component: it only has itself, so this represents that `:config` doesn't have any dependency by default from another component. In another hand, when we look at `:database` we have `(component/using (database/new-database) [:config])`, but what that means? Simple: we have a component that starts with `(database/new-database)` but it depends of another component: `:config`! This means that `:database` will only start when `:config` is properly started! And now look to `:webserver`... It will start only after the initialization of `:config`, `:http`, `:router` and `:database`! + +By default, the Component library implements a protocol that you have to refer to when you're building your components (a protocol is very similar to an interface from object-oriented programming) - the protocol name is Lifecycle, and it needs to have two main functions: a start and a stop function. Look at the example below: +```clojure +(ns com.example.your-application + (:require [com.stuartsierra.component :as component])) + +(defrecord Database [host port connection] + ;; Implement the Lifecycle protocol + component/Lifecycle + + (start [component] + (println ";; Starting database") + ;; In the 'start' method, initialize this component + ;; and start it running. For example, connect to a + ;; database, create thread pools, or initialize shared + ;; state. + (let [conn (connect-to-database host port)] + ;; Return an updated version of the component with + ;; the run-time state assoc'd in. + (assoc component :connection conn))) + + (stop [component] + (println ";; Stopping database") + ;; In the 'stop' method, shut down the running + ;; component and release any external resources it has + ;; acquired. + (.close connection) + ;; Return the component, optionally modified. Remember that if you + ;; dissoc one of a record's base fields, you get a plain map. + (assoc component :connection nil))) +``` +> This example came from the Component library README + +Every time that you want to implement a new component, you will be implementing this protocol. If you want to access your component, you can define a function to start it and update its value, like this example: +```clojure +(defn new-database [host port] + (map->Database {:host host :port port})) +``` + +Well, by default Component uses records instead of classical maps and that's why you use a function named `map->YourComponent` to access a map of properties of your component. You can see more about Records by reaching [defrecord](https://docs.clj.codes/org.clojure/clojure/clojure.core/defrecord/0) and see implementation examples. + +Suppose you don't like the primary idea of using records. In that case, you can check out some Component alternatives, like [Integrant](https://github.com/weavejester/integrant) and [mount](https://github.com/tolitius/mount), and compare them with Component to see the general differences. + +## Mocking components +Sometimes, creating a mock can be helpful with integration tests, so by default, this approach of using records and implementing some protocols can be useful for such cases. Let's dive deep into an example of our HTTP component. + +Inside our components, we have an [HTTP component](https://github.com/parenthesin/components/blob/main/src/parenthesin/components/http/clj_http.clj) using [clj-http](https://github.com/dakrone/clj-http). To manage our tests, it's helpful to configure both an HTTP component to make real requests to the external world and an internal component to mock these requests and make our tests faster and more reliable. + +First of all, we've to define a simple protocol for an HTTP provider that manages a request, like this one: +```clojure +(defprotocol HttpProvider + (request + [self request-input])) +``` +And now, we can implement it for both cases! Look at the example below: +```clojure +(defrecord Http [_] + component/Lifecycle + (start [this] this) + (stop [this] this) + + HttpProvider + (request + [_self {:keys [method url] :as request-input}] + (logs/log :info :http-out-message :method method :url url) + (let [start-time (System/currentTimeMillis) + {:keys [status] :as response} (request-fn request-input) + end-time (System/currentTimeMillis) + total-time (- end-time start-time)] + (logs/log :info :http-out-message-response :response-time-millis total-time + :status status) + response))) + +(defn new-http [] (map->Http {})) +``` +Then, we have a component for HTTP that can handle requests and log them. But now imagine that we have to implement some similar component to handle our mock, but also implementing the `HttpProvider` protocol... How can we do it? +```clojure +(defrecord HttpMock [responses requests] + component/Lifecycle + (start [this] this) + (stop [this] this) + + HttpProvider + (request + [_self {:keys [url] :as req}] + (swap! requests merge + (assoc req :instant (System/currentTimeMillis))) + (get-in @responses + [url] + {:status 500 + :body "Response not set in mocks!"}))) + +(defn new-http-mock + [mocked-responses] + (map->HttpMock {:responses (atom mocked-responses) + :requests (atom [])})) +``` +*Voilà*! Now we have both components for HTTP implementing the same protocol and handling our interactions depending on our usage. By default, you can see this example running at the integration tests from [microservice-boilerplate](https://github.com/parenthesin/microservice-boilerplate/blob/main/test/integration/microservice_boilerplate/wallet_test.clj#L25). + +## How to use it? +Well, now you have a full introduction to the *Component* structure. Now, we can deep dive into each component and understand its usability based on some examples! Check out: +- [Components](components.md) +- [Helpers](helpers.md)