{"id":22196754,"url":"https://github.com/druids/rop","last_synced_at":"2025-07-27T01:30:40.979Z","repository":{"id":57713985,"uuid":"114790315","full_name":"druids/rop","owner":"druids","description":"Yet another Railway Oriented Programming in Clojure","archived":false,"fork":false,"pushed_at":"2018-11-12T14:23:07.000Z","size":23,"stargazers_count":34,"open_issues_count":1,"forks_count":4,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-11-28T19:39:36.039Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/druids.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-12-19T16:59:21.000Z","updated_at":"2024-11-25T07:06:54.000Z","dependencies_parsed_at":"2022-08-25T12:51:31.123Z","dependency_job_id":null,"html_url":"https://github.com/druids/rop","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Frop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Frop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Frop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/druids%2Frop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/druids","download_url":"https://codeload.github.com/druids/rop/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227740673,"owners_count":17812687,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-02T14:16:22.419Z","updated_at":"2024-12-02T14:16:23.051Z","avatar_url":"https://github.com/druids.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"Railway Oriented Programming\n============================\n\nThis is another Clojure implementation of [Railway Oriented\nProgramming](https://fsharpforfunandprofit.com/posts/recipe-part2/). It's based on\n[this gist](https://gist.github.com/ah45/7518292c620679c460557a7038751d6d).\n\nThe reason for another implementation is to provide more pleasant usage for common cases. See example of usage below.\n\n[![CircleCI](https://circleci.com/gh/druids/rop.svg?style=svg)](https://circleci.com/gh/druids/rop)\n[![Dependencies Status](https://jarkeeper.com/druids/rop/status.png)](https://jarkeeper.com/druids/rop)\n[![License](https://img.shields.io/badge/MIT-Clause-blue.svg)](https://opensource.org/licenses/MIT)\n\n\nLeiningen/Boot\n--------------\n\n```clojure\n[rop \"0.4.1\"]\n```\n\n\nDocumentation\n-------------\n\n```clojure\n(require [rop.core :as rop])\n\n```\n\nLet's define \"bussiness logic\" functions like this\n```clojure\n(defn format-email\n  [input]\n  (update input :email lower-case))\n\n\n(defn validate-email\n  [input]\n  (if (-\u003e input :email blank?)\n    (rop/fail {:status 400, :body {:errors {:email [\"Invalid format\"]}}})\n    (rop/succeed input)))\n\n\n(defn create-user\n  [input]\n  (assoc input :new-user {:email (:email input), :id 1})\n\n\n(defn send-email!\n  [input]\n  ;; send e-mail here\n  (println \"Sending e-mail\"))\n```\n\nA simple use case looks like this\n```clojure\n(rop/\u003e\u003e=  {:email \"FOO@BAR.COM\", :new-user nil}\n          (rop/switch format-email)\n          validate-email\n          (rop/switch create-user)\n          (rop/dead send-email!)))\n```\n\nA result of the use case is\n```clojure\n{:email \"foo@bar.com\", :new-user {:email \"foo@bar.com\", :id 1}}\n```\n\nAn input hash-map flows through functions defined in `\u003e\u003e=`, until any function returns `rop/fail`.\n Otherwise the input hash-map is returned at the end. Internally `\u003e\u003e=` uses `funcool/cats` library, but the result is\n extracted for you.\n\n\n### succeed\n\nIt marks a result of a function as a success result. Thus `\u003e\u003e=` will call another function.\n\n```clojure\n\n(defn format-email\n  [input]\n  (rop/succeed (update input :email lower-case)))\n```\n\n\n### fail\n\nIt marks a result of a function as a fail result. It stops computation.\n\n```clojure\n(defn validate-email\n  [input]\n  (if (-\u003e input :email blank?)\n    (rop/fail {:errors {:email [\"Invalid format\"]}})\n    (rop/succeed input)))\n```\n\n### switch\n\nMakes a normal function to be tautological (always returns a success result). It's a shortcut for wrapping functions.\n\n```clojure\n(defn format-email\n  [input]\n  (update input :email lower-case)) ;; \u003c-- see not marking a result as a success\n\n(rop/\u003e\u003e=  {:email \"FOO@BAR.COM\", :new-user nil}\n          (rop/switch format-email) ;; \u003c-- I can wrap here\n          validate-email\n          (rop/switch create-user)\n          (rop/dead send-email!)))\n```\n\n### dead\n\nA wrapper for deadend functions. Any side effect can be done here and I don't need to care about returning `succeed`.\n\n```clojure\n(defn send-email!\n  [input]\n  ;; send e-mail here\n  (println \"Sending e-mail\")) ;; \u003c-- See no `succeed` here\n\n\n(rop/\u003e\u003e=  {:email \"FOO@BAR.COM\", :new-user nil}\n          (rop/switch format-email)\n          validate-email\n          (rop/switch create-user)\n          (rop/dead send-email!))) ;; \u003c-- I can wrap here\n```\n\n\n### \u003e\u003e=\n\nAn infix version of bind for piping two-track values into switch fns. Can be used to pipe two-track values\n through a series of switch fns. First is an input hash-map it will be passed throgh switch fns.\n Rest parameters as switch fns.\n\n\n### \u003e\u003e=*\n\nIt's advanced `\u003e\u003e=` function. Returning Ring's response from `\u003e\u003e=` is a common use case and this function helps with it.\n First parameter is a success key (it will be used as :body in result hash-map) or a tuple with success-key and\n output-keys (at the end `select-keys` will be applied on a success result with these `output-keys`).\n Second is an input hash-map it will be passed throgh switch fns. Rest parameters as switch fns.\n\nAbove use case can be improved via `\u003e\u003e=*`\n```clojure\n(rop/\u003e\u003e=  {:email \"FOO@BAR.COM\", :new-user nil}\n          (rop/switch format-email)\n          validate-email\n          (rop/switch create-user)\n          (rop/dead send-email!)))\n;; returns\n{:email \"foo@bar.com\", :new-user {:email \"foo@bar.com\", :id 1}}\n```\n\nBut it's common that we want to take just one key and return it as a Ring's response. We can do it with `\u003e\u003e=*` by\n passing `:new-user` as a first argument.\n```clojure\n(rop/\u003e\u003e=*  :new-user\n           {:email \"FOO@BAR.COM\", :new-user nil}\n           (rop/switch format-email)\n           validate-email\n           (rop/switch create-user)\n           (rop/dead send-email!)))\n;; returns\n{:body {:email \"foo@bar.com\", :id 1}, :status 200, :headers {}}\n```\n\nAlso it's common that not all keys of hash-map can be exposed. Output keys can be limited like this\n```clojure\n(rop/\u003e\u003e=*  [:new-user #{:id}]\n           {:email \"FOO@BAR.COM\", :new-user nil}\n           (rop/switch format-email)\n           validate-email\n           (rop/switch create-user)\n           (rop/dead send-email!)))\n;; returns\n{:body {:id 1}, :status 200, :headers {}}\n```\n\nOf course it also works with sequences.\n```clojure\n(rop/\u003e\u003e=*  [:new-users #{:id}]\n           {:email \"FOO@BAR.COM\", :new-user nil, :new-users nil}\n           (rop/switch format-email)\n           validate-email\n           (rop/switch create-user)\n           (rop/switch #(assoc % :new-users [(:new-user %)]))\n           (rop/dead send-email!))))))\n;; returns\n{:body [{:id 1}], :status 200, :headers {}} ;; \u003c-- See :body\n```\n\nSure HTTP headers and status code can be defined with `\u003e\u003e=*` too\n```clojure\n(defn create-user\n  [input]\n  (-\u003e input\n      (assoc :new-user {:email (:email input), :id 1})\n      (assoc-in [:response :status] 201)\n      (assoc-in [:response :headers] {:content-type :application/json})))\n\n(rop/\u003e\u003e=*  [:new-user #{:id}]\n           {:email \"FOO@BAR.COM\", :new-user nil}\n           (rop/switch format-email)\n           validate-email\n           (rop/switch create-user)\n           (rop/dead send-email!)))\n;; returns\n{:body {:id 1}, :status 201, :headers {:content-type :application/json}}\n```\n\nSame when a failure needs to define status of headers\n```clojure\n(defn validate-email\n  [input]\n  (if (-\u003e input :email blank?)\n    (rop/fail {:status 400, :body {:errors {:email [\"Invalid format\"]}}}) ;; \u003c-- a whole Ring's response here\n    (rop/succeed input)))\n\n(rop/\u003e\u003e=*  [:new-user #{:id}]\n           {:email \"\", :new-user nil}\n           (rop/switch format-email)\n           validate-email\n           (rop/switch create-user)\n           (rop/dead send-email!)))\n;; returns\n{:status 400, :body {:errors {:email [\"Invalid format\"]}}}\n```\n\n### =validate-request=\nA railway function that validates a request by a given scheme.\n If data are valid it updates them in the request (with coerced data), otherwise returns Bad Requests within errors.\n\nParameters:\n  - `validate` a function that takes an input and a validation scheme, it should return a tuple of errors\n       and validated input\n  - `scheme` a validation scheme\n  - `default` default values as a `hash-map`, it will be merged into a validated input\n  - `request-key a key in a `request` that holds the input data`\n  - `input` a ROP input\"\n\nAn example usage with Struct library:\n\n```clojure\n(require '[struct.core :as st])\n\n(rop/\u003e\u003e=*  [:new-user #{:id}]\n           {:email \"\", :new-user nil}, :request request)\n           (partial rop/=validate-request= st/validate {:id [st/number-str]} {} :params\n           (rop/switch format-email)\n           validate-email\n           (rop/switch create-user)\n           (rop/dead send-email!)))\n```\n\nFunction `=validate-request=` will fail when `st/validate` returns any error it creates Ring Bad Request response:\n\n```clojure\n{:status 400, :body {:errors {:id \"mut be a number\"}}}))))\n```\n\nOr it continues in a flow and updates an input by a coerced input returned by Struct.\n\n\n### =merge-params=\nA railway function that merges a given `source` key into a `target` key in a request.\n It's useful when route params and body params are validated together.\"\n\n```clojure\n(rop/\u003e\u003e=*  [:new-user #{:id}]\n           {:email \"\", :new-user nil}, :request request\n           (partial rop/=merge-params= :body-params :params)\n           (partial rop/=validate-request= st/validate {:id [st/number-str]} {} :params)\n           (rop/switch format-email)\n           validate-email\n           (rop/switch create-user)\n           (rop/dead send-email!)))\n```\n\nFunction `=merge-params=` updates `:params` within `:body-params` in a request. It's useful when combining query\n parameter within body parameters before a validation.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdruids%2Frop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdruids%2Frop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdruids%2Frop/lists"}