Caching tools for use with Pharmacist prescriptions
Caching tools for use with Pharmacist prescriptions
A data source is the declarative description of a single remote source of data. It must include a reference to a function (or an id if using multi-method dispatch), and can include additional information about parameters, dependencies on other sources/initial parameters, the maximum number of retries, and how to fetch child sources.
(require '[pharmacist.data-source :as data-source]
'[pharmacist.result :as result])
(defn fetch-playlist [source]
(result/success {:id (-> source ::data-source/params :playlist-id)}))
(def playlist-data-source
{::data-source/fn #'fetch-playlist
::data-source/params {:playlist-id 42}})
Pharmacist will try the following three ways to fetch your source, in order of preference:
:pharmacist.data-source/fn
:pharmacist.data-source/async-fn
fetch
on the source's :pharmacist.data-source/id
All of these functions accept the fully resolved source as their only
argument. The return value from the function passed to
:pharmacist.data-source/fn
should be a pharmacist.result
. The return
value from :pharmacist.data-source/async-fn
should be a clojure.core.async
channel that emits a single message which should be a pharmacist.result
.
If the fetch function throws an exception, or otherwise fails to meet its contract, the error will be wrapped in a descriptive [[parmacist.result]].
In its simplest form, the parameter map is just a map of values to pass to the
fetch function. To further parameterize the data source, the parameter map can
depend on values that will be provided as the full prescription is filled, or
even resulting values from other sources. Dependencies are declared with a
vector that has the :pharmacist.data-source/dep
keyword set as meta-data:
(require '[pharmacist.data-source :as data-source])
(def playlist
{::data-source/fn #'fetch-playlist
::data-source/params {:id ^::data-source/dep [:playlist-id]}})
Now Pharmacist will look for :playlist-id
in the initial parameters passed
to pharmacist.prescription/fill
or other sources in the prescription. It
will then retrieve the source and pass the full result as the :id
parameter
to the fetch-playlist
source. If you just need to pick a single value from
the dependency, use a path:
(def playlist
{::data-source/fn #'fetch-playlist
::data-source/params {:id ^::data-source/dep [:user :id]}})
In this updated example, the :id
parameter will receive the value of :id
in the result of the :user
data source - or the :id
key of the map passed
as :user
in the initial parameters:
(pharmacist.prescription/fill
{:playlist playlist}
{:params {:user {:id 42}}})
When the keys of the dependency are the same as the parameters your source
expects, like above (:id => [:user :id]
), you can just make the whole
parameters map a dependency:
(def playlist
{::data-source/fn #'fetch-playlist
::data-source/params ^::data-source/dep [:user]})
Pharmacist can fetch collections where you don't know upfront how many sources you'll find. To do this you must provide two source definitions: one that defines a single source in the collection, and another that specifies how many times to fetch that source, and parameters for each individual fetch.
Imagine that you wanted to fetch all of a user's playlists. First define a function that receives a user and returns a vector of all their playlists:
(require '[pharmacist.result :as result])
(defn all-playlists [{::data-source/keys [params]}]
;; params is now the user - find their playlists from the map, over HTTP, from
;; disk, or whatever
(result/success (:playlists params)))
Then define the prescription with the collection and the item source:
(require '[pharmacist.data-source :as data-source])
(def prescription
{:playlist {::data-source/fn #'fetch-playlist
::data-source/params {:id ^::data-source/dep [:playlist-id]}}
:user-playlists {::data-source/fn #'all-playlists
::data-source/params {:user ^::data-source/dep [:user]}
::data-source/coll-of :playlist}})
You can now fill this with an initial user (you could of course also get the user from yet another data source):
(pharmacist.prescription/fill
prescription
{:params {:user {:playlists [{:id 1}
{:id 2}]}}})
The :playlist
source will be fetched twice - once with {:id 1}
as its
parameters, and once with {:id 2}
. The parameters returned from the
selection function are merged into the map in the prescription, so in the
above example, {:id ^::data-source/dep [:playlist-id]}
isn't strictly
necessary, but it is recommended, because it documents the expected parameters
and it makes the source usable outside of the collection as well.
In order to load a different selection of the collection, define another
selection function, and make it a ::data-source/coll-of
the same item
source.
key | description |
---|---|
::data-source/id | Keyword identifying this source. Can be inferred from ::fn or ::async-fn , see id |
::data-source/fn | Function that fetches this source. Should return a pharmacist.result |
::data-source/async-fn | Function that fetches this source asynchronously. Should return a core.async channel that emits a single pharmacist.result |
::data-source/params | Parameter map to pass to the fetch function |
::data-source/retries | The number of times ::result/retryable? results from this source can be retried |
::data-source/timeout | The maximum number of milliseconds to wait for this source to be fetched. |
::data-source/coll-of | The type of source this source is a collection of |
::data-source/cache-params | A collection of paths to extract as the cache key, see [[pharmacist.cache/cache-params]] |
::data-source/cache-deps | The dependencies required to compute the cache key. Usually inferred from ::data-source/cache-params , see [[pharmacist.cache/cache-deps]] |
A data source is the declarative description of a single remote source of data. It must include a reference to a function (or an id if using multi-method dispatch), and can include additional information about parameters, dependencies on other sources/initial parameters, the maximum number of retries, and how to fetch child sources. ```clj (require '[pharmacist.data-source :as data-source] '[pharmacist.result :as result]) (defn fetch-playlist [source] (result/success {:id (-> source ::data-source/params :playlist-id)})) (def playlist-data-source {::data-source/fn #'fetch-playlist ::data-source/params {:playlist-id 42}}) ``` ## Fetch functions Pharmacist will try the following three ways to fetch your source, in order of preference: 1. Call the function in `:pharmacist.data-source/fn` 2. Call the function in `:pharmacist.data-source/async-fn` 3. Dispatch [[fetch]] on the source's `:pharmacist.data-source/id` All of these functions accept the fully resolved source as their only argument. The return value from the function passed to `:pharmacist.data-source/fn` should be a [[pharmacist.result]]. The return value from `:pharmacist.data-source/async-fn` should be a clojure.core.async channel that emits a single message which should be a [[pharmacist.result]]. If the fetch function throws an exception, or otherwise fails to meet its contract, the error will be wrapped in a descriptive [[parmacist.result]]. ## Parameters and dependencies In its simplest form, the parameter map is just a map of values to pass to the fetch function. To further parameterize the data source, the parameter map can depend on values that will be provided as the full prescription is filled, or even resulting values from other sources. Dependencies are declared with a vector that has the `:pharmacist.data-source/dep` keyword set as meta-data: ```clojure (require '[pharmacist.data-source :as data-source]) (def playlist {::data-source/fn #'fetch-playlist ::data-source/params {:id ^::data-source/dep [:playlist-id]}}) ``` Now Pharmacist will look for `:playlist-id` in the initial parameters passed to [[pharmacist.prescription/fill]] or other sources in the prescription. It will then retrieve the source and pass the full result as the `:id` parameter to the `fetch-playlist` source. If you just need to pick a single value from the dependency, use a path: ```clj (def playlist {::data-source/fn #'fetch-playlist ::data-source/params {:id ^::data-source/dep [:user :id]}}) ``` In this updated example, the `:id` parameter will receive the value of `:id` in the result of the `:user` data source - or the `:id` key of the map passed as `:user` in the initial parameters: ```clj (pharmacist.prescription/fill {:playlist playlist} {:params {:user {:id 42}}}) ``` When the keys of the dependency are the same as the parameters your source expects, like above (`:id => [:user :id]`), you can just make the whole parameters map a dependency: ```clj (def playlist {::data-source/fn #'fetch-playlist ::data-source/params ^::data-source/dep [:user]}) ``` ## Collection sources Pharmacist can fetch collections where you don't know upfront how many sources you'll find. To do this you must provide two source definitions: one that defines a single source in the collection, and another that specifies how many times to fetch that source, and parameters for each individual fetch. Imagine that you wanted to fetch all of a user's playlists. First define a function that receives a user and returns a vector of all their playlists: ```clj (require '[pharmacist.result :as result]) (defn all-playlists [{::data-source/keys [params]}] ;; params is now the user - find their playlists from the map, over HTTP, from ;; disk, or whatever (result/success (:playlists params))) ``` Then define the prescription with the collection and the item source: ```clj (require '[pharmacist.data-source :as data-source]) (def prescription {:playlist {::data-source/fn #'fetch-playlist ::data-source/params {:id ^::data-source/dep [:playlist-id]}} :user-playlists {::data-source/fn #'all-playlists ::data-source/params {:user ^::data-source/dep [:user]} ::data-source/coll-of :playlist}}) ``` You can now fill this with an initial user (you could of course also get the user from yet another data source): ```clj (pharmacist.prescription/fill prescription {:params {:user {:playlists [{:id 1} {:id 2}]}}}) ``` The `:playlist` source will be fetched twice - once with `{:id 1}` as its parameters, and once with `{:id 2}`. The parameters returned from the selection function are merged into the map in the prescription, so in the above example, `{:id ^::data-source/dep [:playlist-id]}` isn't strictly necessary, but it is recommended, because it documents the expected parameters **and** it makes the source usable outside of the collection as well. In order to load a different selection of the collection, define another selection function, and make it a `::data-source/coll-of` the same item source. ## Data source keys | key | description | | -----------------------------|-------------| | `::data-source/id` | Keyword identifying this source. Can be inferred from `::fn` or `::async-fn`, see [[id]] | `::data-source/fn` | Function that fetches this source. Should return a [[pharmacist.result]] | `::data-source/async-fn` | Function that fetches this source asynchronously. Should return a core.async channel that emits a single [[pharmacist.result]] | `::data-source/params` | Parameter map to pass to the fetch function | `::data-source/retries` | The number of times `::result/retryable?` results from this source can be retried | `::data-source/timeout` | The maximum number of milliseconds to wait for this source to be fetched. | `::data-source/coll-of` | The type of source this source is a collection of | `::data-source/cache-params` | A collection of paths to extract as the cache key, see [[pharmacist.cache/cache-params]] | `::data-source/cache-deps` | The dependencies required to compute the cache key. Usually inferred from `::data-source/cache-params`, see [[pharmacist.cache/cache-deps]]
A prescription is a map of [path data-source]
that Pharmacist can fill.
From this fulfilled prescription you can select all, or parts of the described
data. You can consume/stream data as it becomes available, or combine
everything into a single map of [path result-data]
.
(require '[pharmacist.prescription :as p]
'[pharmacist.data-source :as data-source]
'[clojure.core.async :refer [<!!]])
(def prescription
{::auth {::data-source/fn #'spotify-auth
::data-source/params ^::data-source/dep [:config]}
::playlists {::data-source/fn #'spotify-playlists
::data-source/params {:token ^::data-source/dep [::auth :access_token]}}})
(-> prescription
(p/fill {:params {:config {:spotify-user "..."
:spotify-pass "..."}}})
(p/select [::playlists])
p/collect
<!!
:pharmacist.result/data)
A prescription is a map of `[path data-source]` that Pharmacist can fill. From this fulfilled prescription you can select all, or parts of the described data. You can consume/stream data as it becomes available, or combine everything into a single map of `[path result-data]`. ```clj (require '[pharmacist.prescription :as p] '[pharmacist.data-source :as data-source] '[clojure.core.async :refer [<!!]]) (def prescription {::auth {::data-source/fn #'spotify-auth ::data-source/params ^::data-source/dep [:config]} ::playlists {::data-source/fn #'spotify-playlists ::data-source/params {:token ^::data-source/dep [::auth :access_token]}}}) (-> prescription (p/fill {:params {:config {:spotify-user "..." :spotify-pass "..."}}}) (p/select [::playlists]) p/collect <!! :pharmacist.result/data) ```
Data structures and functions to work with the result
of pharmacist.data-source
fetch functions. A result is a map with the key
:pharmacist.result/success?
(a boolean) and any number of the below keys.
key | description |
---|---|
:pharmacist.result/success? | Boolean. Indicates successful retrieval |
:pharmacist.result/data | The resulting data from the fetch functions |
:pharmacist.result/retryable? | A boolean indicating whether this failure is worth retrying |
:pharmacist.result/refresh | Optional set of parameters that must be refreshed before retrying |
The following keys are set by pharmacist.prescription/fill
:
key | description |
---|---|
:pharmacist.result/partial? | true when the selection returned collection items, but the collection items are not yet available |
:pharmacist.result/attempts | The number of attempts made to fetch this source |
:pharmacist.result/retrying? | true if the result was a failure and Pharmacist intends to try it again |
:pharmacist.result/raw-data | When the source has a schema that transforms ::pharmacist.result/data , this key has the unprocessed result |
:pharmacist.result/timeout-after | The number of milliseconds at which this source was considered timed out |
:pharmacist.cache/cached-at | Timestamp indicating when this result was originally cached |
Data structures and functions to work with the result of [[pharmacist.data-source]] fetch functions. A result is a map with the key `:pharmacist.result/success?` (a boolean) and any number of the below keys. | key | description | | --------------------------------|-------------| | `:pharmacist.result/success?` | Boolean. Indicates successful retrieval | `:pharmacist.result/data` | The resulting data from the fetch functions | `:pharmacist.result/retryable?` | A boolean indicating whether this failure is worth retrying | `:pharmacist.result/refresh` | Optional set of parameters that must be refreshed before retrying The following keys are set by [[pharmacist.prescription/fill]]: | key | description | | -----------------------------------|-------------| | `:pharmacist.result/partial?` | `true` when the selection returned collection items, but the collection items are not yet available | `:pharmacist.result/attempts` | The number of attempts made to fetch this source | `:pharmacist.result/retrying?` | `true` if the result was a failure and Pharmacist intends to try it again | `:pharmacist.result/raw-data` | When the source has a schema that transforms `::pharmacist.result/data`, this key has the unprocessed result | `:pharmacist.result/timeout-after` | The number of milliseconds at which this source was considered timed out | `:pharmacist.cache/cached-at` | Timestamp indicating when this result was originally cached
Tools for mapping, coercing, and verifying data.
Tools for mapping, coercing, and verifying data.
Tools to validate a prescription before attempting to fill it. Pharmacist will make the best the situation, and in the case of cyclic dependencies and other forms of invalid states, it will just stop processing. The functions in this namespace can help you trigger those situations as errors instead.
Tools to validate a prescription before attempting to fill it. Pharmacist will make the best the situation, and in the case of cyclic dependencies and other forms of invalid states, it will just stop processing. The functions in this namespace can help you trigger those situations as errors instead.
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close