https://github.com/druids/rop
Yet another Railway Oriented Programming in Clojure
https://github.com/druids/rop
Last synced: 12 months ago
JSON representation
Yet another Railway Oriented Programming in Clojure
- Host: GitHub
- URL: https://github.com/druids/rop
- Owner: druids
- License: mit
- Created: 2017-12-19T16:59:21.000Z (over 8 years ago)
- Default Branch: master
- Last Pushed: 2018-11-12T14:23:07.000Z (over 7 years ago)
- Last Synced: 2024-11-28T19:39:36.039Z (over 1 year ago)
- Language: Clojure
- Size: 22.5 KB
- Stars: 34
- Watchers: 4
- Forks: 4
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
Railway Oriented Programming
============================
This is another Clojure implementation of [Railway Oriented
Programming](https://fsharpforfunandprofit.com/posts/recipe-part2/). It's based on
[this gist](https://gist.github.com/ah45/7518292c620679c460557a7038751d6d).
The reason for another implementation is to provide more pleasant usage for common cases. See example of usage below.
[](https://circleci.com/gh/druids/rop)
[](https://jarkeeper.com/druids/rop)
[](https://opensource.org/licenses/MIT)
Leiningen/Boot
--------------
```clojure
[rop "0.4.1"]
```
Documentation
-------------
```clojure
(require [rop.core :as rop])
```
Let's define "bussiness logic" functions like this
```clojure
(defn format-email
[input]
(update input :email lower-case))
(defn validate-email
[input]
(if (-> input :email blank?)
(rop/fail {:status 400, :body {:errors {:email ["Invalid format"]}}})
(rop/succeed input)))
(defn create-user
[input]
(assoc input :new-user {:email (:email input), :id 1})
(defn send-email!
[input]
;; send e-mail here
(println "Sending e-mail"))
```
A simple use case looks like this
```clojure
(rop/>>= {:email "FOO@BAR.COM", :new-user nil}
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
```
A result of the use case is
```clojure
{:email "foo@bar.com", :new-user {:email "foo@bar.com", :id 1}}
```
An input hash-map flows through functions defined in `>>=`, until any function returns `rop/fail`.
Otherwise the input hash-map is returned at the end. Internally `>>=` uses `funcool/cats` library, but the result is
extracted for you.
### succeed
It marks a result of a function as a success result. Thus `>>=` will call another function.
```clojure
(defn format-email
[input]
(rop/succeed (update input :email lower-case)))
```
### fail
It marks a result of a function as a fail result. It stops computation.
```clojure
(defn validate-email
[input]
(if (-> input :email blank?)
(rop/fail {:errors {:email ["Invalid format"]}})
(rop/succeed input)))
```
### switch
Makes a normal function to be tautological (always returns a success result). It's a shortcut for wrapping functions.
```clojure
(defn format-email
[input]
(update input :email lower-case)) ;; <-- see not marking a result as a success
(rop/>>= {:email "FOO@BAR.COM", :new-user nil}
(rop/switch format-email) ;; <-- I can wrap here
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
```
### dead
A wrapper for deadend functions. Any side effect can be done here and I don't need to care about returning `succeed`.
```clojure
(defn send-email!
[input]
;; send e-mail here
(println "Sending e-mail")) ;; <-- See no `succeed` here
(rop/>>= {:email "FOO@BAR.COM", :new-user nil}
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!))) ;; <-- I can wrap here
```
### >>=
An infix version of bind for piping two-track values into switch fns. Can be used to pipe two-track values
through a series of switch fns. First is an input hash-map it will be passed throgh switch fns.
Rest parameters as switch fns.
### >>=*
It's advanced `>>=` function. Returning Ring's response from `>>=` is a common use case and this function helps with it.
First parameter is a success key (it will be used as :body in result hash-map) or a tuple with success-key and
output-keys (at the end `select-keys` will be applied on a success result with these `output-keys`).
Second is an input hash-map it will be passed throgh switch fns. Rest parameters as switch fns.
Above use case can be improved via `>>=*`
```clojure
(rop/>>= {:email "FOO@BAR.COM", :new-user nil}
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
;; returns
{:email "foo@bar.com", :new-user {:email "foo@bar.com", :id 1}}
```
But it's common that we want to take just one key and return it as a Ring's response. We can do it with `>>=*` by
passing `:new-user` as a first argument.
```clojure
(rop/>>=* :new-user
{:email "FOO@BAR.COM", :new-user nil}
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
;; returns
{:body {:email "foo@bar.com", :id 1}, :status 200, :headers {}}
```
Also it's common that not all keys of hash-map can be exposed. Output keys can be limited like this
```clojure
(rop/>>=* [:new-user #{:id}]
{:email "FOO@BAR.COM", :new-user nil}
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
;; returns
{:body {:id 1}, :status 200, :headers {}}
```
Of course it also works with sequences.
```clojure
(rop/>>=* [:new-users #{:id}]
{:email "FOO@BAR.COM", :new-user nil, :new-users nil}
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/switch #(assoc % :new-users [(:new-user %)]))
(rop/dead send-email!))))))
;; returns
{:body [{:id 1}], :status 200, :headers {}} ;; <-- See :body
```
Sure HTTP headers and status code can be defined with `>>=*` too
```clojure
(defn create-user
[input]
(-> input
(assoc :new-user {:email (:email input), :id 1})
(assoc-in [:response :status] 201)
(assoc-in [:response :headers] {:content-type :application/json})))
(rop/>>=* [:new-user #{:id}]
{:email "FOO@BAR.COM", :new-user nil}
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
;; returns
{:body {:id 1}, :status 201, :headers {:content-type :application/json}}
```
Same when a failure needs to define status of headers
```clojure
(defn validate-email
[input]
(if (-> input :email blank?)
(rop/fail {:status 400, :body {:errors {:email ["Invalid format"]}}}) ;; <-- a whole Ring's response here
(rop/succeed input)))
(rop/>>=* [:new-user #{:id}]
{:email "", :new-user nil}
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
;; returns
{:status 400, :body {:errors {:email ["Invalid format"]}}}
```
### =validate-request=
A railway function that validates a request by a given scheme.
If data are valid it updates them in the request (with coerced data), otherwise returns Bad Requests within errors.
Parameters:
- `validate` a function that takes an input and a validation scheme, it should return a tuple of errors
and validated input
- `scheme` a validation scheme
- `default` default values as a `hash-map`, it will be merged into a validated input
- `request-key a key in a `request` that holds the input data`
- `input` a ROP input"
An example usage with Struct library:
```clojure
(require '[struct.core :as st])
(rop/>>=* [:new-user #{:id}]
{:email "", :new-user nil}, :request request)
(partial rop/=validate-request= st/validate {:id [st/number-str]} {} :params
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
```
Function `=validate-request=` will fail when `st/validate` returns any error it creates Ring Bad Request response:
```clojure
{:status 400, :body {:errors {:id "mut be a number"}}}))))
```
Or it continues in a flow and updates an input by a coerced input returned by Struct.
### =merge-params=
A railway function that merges a given `source` key into a `target` key in a request.
It's useful when route params and body params are validated together."
```clojure
(rop/>>=* [:new-user #{:id}]
{:email "", :new-user nil}, :request request
(partial rop/=merge-params= :body-params :params)
(partial rop/=validate-request= st/validate {:id [st/number-str]} {} :params)
(rop/switch format-email)
validate-email
(rop/switch create-user)
(rop/dead send-email!)))
```
Function `=merge-params=` updates `:params` within `:body-params` in a request. It's useful when combining query
parameter within body parameters before a validation.