{"id":13788392,"url":"https://github.com/plumatic/plumbing","last_synced_at":"2025-12-12T01:17:36.544Z","repository":{"id":6568632,"uuid":"7810562","full_name":"plumatic/plumbing","owner":"plumatic","description":"Prismatic's Clojure(Script) utility belt","archived":false,"fork":false,"pushed_at":"2025-07-01T01:21:46.000Z","size":606,"stargazers_count":1494,"open_issues_count":12,"forks_count":106,"subscribers_count":89,"default_branch":"master","last_synced_at":"2025-09-22T14:37:39.205Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/plumatic.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2013-01-25T00:39:58.000Z","updated_at":"2025-09-21T08:09:02.000Z","dependencies_parsed_at":"2023-02-17T01:46:44.322Z","dependency_job_id":null,"html_url":"https://github.com/plumatic/plumbing","commit_stats":null,"previous_names":["prismatic/plumbing"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/plumatic/plumbing","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plumatic%2Fplumbing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plumatic%2Fplumbing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plumatic%2Fplumbing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plumatic%2Fplumbing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/plumatic","download_url":"https://codeload.github.com/plumatic/plumbing/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/plumatic%2Fplumbing/sbom","scorecard":{"id":738295,"data":{"date":"2025-08-11","repo":{"name":"github.com/plumatic/plumbing","commit":"26b4bbdd3ee8c06d0e86136ca9acf5525d93ca93"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.5,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/test.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":2,"reason":"Found 3/14 approved changesets -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Maintained","score":2,"reason":"3 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 2","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/plumatic/plumbing/test.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:25: update your workflow using https://app.stepsecurity.io/secureworkflow/plumatic/plumbing/test.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:32: update your workflow using https://app.stepsecurity.io/secureworkflow/plumatic/plumbing/test.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/test.yml:36: update your workflow using https://app.stepsecurity.io/secureworkflow/plumatic/plumbing/test.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/test.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/plumatic/plumbing/test.yml/master?enable=pin","Info:   0 out of   4 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 20 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-22T16:34:24.173Z","repository_id":6568632,"created_at":"2025-08-22T16:34:24.174Z","updated_at":"2025-08-22T16:34:24.174Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27673723,"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","status":"online","status_checked_at":"2025-12-11T02:00:11.302Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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-08-03T21:00:44.834Z","updated_at":"2025-12-12T01:17:36.506Z","avatar_url":"https://github.com/plumatic.png","language":"Clojure","readme":"# Plumbing and Graph: the Clojure utility belt\n\n\u003cimg src=\"https://raw.github.com/wiki/plumatic/plumbing/images/prismatic-swiss-army-knife.png\" alt=\"prismatic/plumbing logo\" title=\"prismatic/plumbing logo\" align=\"right\" width=\"250\" /\u003e\n\nThis first release includes our '[Graph](http://plumatic.github.io/prismatics-graph-at-strange-loop)' library, our `plumbing.core` library of very commonly used functions (the only namespace we `:use` across our codebase), and a few other supporting namespaces.\n\n*New in 0.3.0: support for ClojureScript*\n\n*New in 0.2.0: support for schema.core/defn-style schemas on fnks and Graphs.  See `(doc fnk)` for details.*\n\nLeiningen dependency (Clojars): \n\n[![Clojars Project](http://clojars.org/prismatic/plumbing/latest-version.svg)](http://clojars.org/prismatic/plumbing) \n\n[Latest API docs](http://plumatic.github.io/plumbing).\n\n**This is an alpha release.  We  are using it internally in production, but the API and organizational structure are subject to change.  Comments and suggestions are much appreciated.**\n\nCheck back often, because we'll keep adding more useful namespaces and functions as we work through cleaning up and open-sourcing our stack of Clojure libraries.\n\n## Graph: the Functional Swiss-Army Knife\n\nGraph is a simple and *declarative* way to specify a structured computation, which is easy to analyze, change, compose, and monitor. Here's a simple example of an ordinary function definition, and its Graph equivalent:\n\n```clojure\n(require '[plumbing.core :refer (sum)])\n(defn stats\n  \"Take a map {:xs xs} and return a map of simple statistics on xs\"\n  [{:keys [xs] :as m}]\n  (assert (contains? m :xs))\n  (let [n  (count xs)\n        m  (/ (sum identity xs) n)\n        m2 (/ (sum #(* % %) xs) n)\n        v  (- m2 (* m m))]\n    {:n n   ; count\n     :m m   ; mean\n     :m2 m2 ; mean-square\n     :v v   ; variance\n     }))\n\n(require '[plumbing.core :refer (fnk sum)])\n(def stats-graph\n  \"A graph specifying the same computation as 'stats'\"\n  {:n  (fnk [xs]   (count xs))\n   :m  (fnk [xs n] (/ (sum identity xs) n))\n   :m2 (fnk [xs n] (/ (sum #(* % %) xs) n))\n   :v  (fnk [m m2] (- m2 (* m m)))})\n```\n\nA Graph is just a map from keywords to keyword functions ([learn more](#fnk)).  In this case, `stats-graph` represents the steps in taking a sequence of numbers (`xs`) and producing univariate statistics on those numbers (i.e., the mean `m` and the variance `v`).  The names of arguments to each `fnk` can refer to other steps that must happen before the step executes. For instance, in the above, to execute `:v`, you must first execute the `:m` and `:m2` steps (mean and mean-square respectively).\n\nWe can \"compile\" this Graph to produce a single function (equivalent to `stats`), which also checks that the map represents a valid Graph:\n\n```clojure\n(require '[plumbing.graph :as graph] '[schema.core :as s])\n(def stats-eager (graph/compile stats-graph))\n\n(= {:n 4\n    :m 3\n    :m2 (/ 25 2)\n    :v (/ 7 2)}\n   (into {} (stats-eager {:xs [1 2 3 6]})))\n\n;; Missing :xs key exception\n(thrown? Throwable (stats-eager {:ys [1 2 3]}))\n```\n\nMoreover, as of the 0.1.0 release, `stats-eager` is *fast* -- only about 30% slower than the hand-coded `stats` if `xs` has a single element, and within 5% of `stats` if `xs` has ten elements.\n\nUnlike the opaque `stats` fn, however, we can modify and extend `stats-graph` using ordinary operations on maps:\n\n```clojure\n(def extended-stats\n  (graph/compile\n    (assoc stats-graph\n      :sd (fnk [^double v] (Math/sqrt v)))))\n\n(= {:n 4\n    :m 3\n    :m2 (/ 25 2)\n    :v (/ 7 2)\n    :sd (Math/sqrt 3.5)}\n   (into {} (extended-stats {:xs [1 2 3 6]})))\n```\n\nA Graph encodes the structure of a computation, but not how it happens, allowing for many execution strategies. For example, we can compile a Graph lazily so that step values are computed as needed.  Or, we can parallel-compile the Graph so that independent step functions are run in separate threads:\n\n```clojure\n(def lazy-stats (graph/lazy-compile stats-graph))\n\n(def output (lazy-stats {:xs [1 2 3 6]}))\n;; Nothing has actually been computed yet\n(= (/ 25 2) (:m2 output))\n;; Now :n and :m2 have been computed, but :v and :m are still behind a delay\n\n\n(def par-stats (graph/par-compile stats-graph))\n\n(def output (par-stats {:xs [1 2 3 6]}))\n;; Nodes are being computed in futures, with :m and :m2 going in parallel after :n\n(= (/ 7 2) (:v output))\n```\n\nWe can also ask a Graph for information about its inputs and outputs (automatically computed from its definition):\n\n```clojure\n(require '[plumbing.fnk.pfnk :as pfnk])\n\n;; stats-graph takes a map with one required key, :xs\n(= {:xs s/Any}\n   (pfnk/input-schema stats-graph))\n\n;; stats-graph outputs a map with four keys, :n, :m, :m2, and :v\n(= {:n s/Any :m s/Any :m2 s/Any :v s/Any}\n   (pfnk/output-schema stats-graph))\n```\n\nIf schemas are provided on the inputs and outputs of the node functions, these propagate through into the Graph schema as expected.\n\nWe can also have higher-order functions on Graphs to wrap the behavior on each step. For instance, we can automatically profile each sub-function in 'stats' to see how long it takes to execute:\n\n```clojure\n(def profiled-stats (graph/compile (graph/profiled ::profile-data stats-graph)))\n\n;;; times in milliseconds for each step:\n(= {:n 1.001, :m 0.728, :m2 0.996, :v 0.069}\n   @(::profile-data (profiled-stats {:xs (range 10000)})))\n```\n\n… and so on.  For more examples and details about Graph, check out the [graph examples test](https://github.com/plumatic/plumbing/blob/master/test/plumbing/graph_examples_test.cljc).\n\n\u003ca name=\"fnk\"/\u003e\n\n## Bring on (de)fnk\n\nMany of the functions we write (in Graph and elsewhere) take a single (nested) map argument with keyword keys and have expectations about which keys must be present and which are optional. We developed a new style of binding ([read more here](https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk)) to make this a lot easier and to check that input data has the right 'shape'. We call these 'keyword functions' (defined by `defnk`) and here's what one looks like:\n\n```clojure\n(use 'plumbing.core)\n(defnk simple-fnk [a b c]\n  (+ a b c))\n\n(= 6 (simple-fnk {:a 1 :b 2 :c 3}))\n;; Below throws: Key :c not found in (:a :b)\n(thrown? Throwable (simple-fnk {:a 1 :b 2}))\n```\n\nYou can declare a key as optional and provide a default:\n```clojure\n(defnk simple-opt-fnk [a b {c 1}]\n  (+ a b c))\n\n(= 4 (simple-opt-fnk {:a 1 :b 2}))\n```\n\nYou can do nested map bindings:\n```clojure\n(defnk simple-nested-fnk [a [:b b1] c]\n  (+ a b1 c))\n\n(= 6 (simple-nested-fnk {:a 1 :b {:b1 2} :c 3}))\n;; Below throws: Expected a map at key-path [:b], got type class java.lang.Long\n(thrown? Throwable (simple-nested-fnk {:a 1 :b 1 :c 3}))\n```\n\nOf course, you can bind multiple variables from an inner map and do multiple levels of nesting:\n```clojure\n(defnk simple-nested-fnk2 [a [:b b1 [:c {d 3}]]]\n  (+ a b1 d))\n\n(= 4 (simple-nested-fnk2 {:a 1 :b {:b1 2 :c {:d 1}}}))\n(= 5 (simple-nested-fnk2 {:a 1 :b {:b1 1 :c {}}}))\n```\n\nYou can also use this binding style in a `let` statement using `letk`\nor within an anonymous function by using `fnk`.\n\n\n## More good stuff\n\nThere are a bunch of functions in `plumbing.core` that we can't live without. Here are a few of our favorites.\n\nWhen we build maps, we often use `for-map`, which works like `for` but for maps:\n\n```clojure\n(use 'plumbing.core)\n(= (for-map [i (range 3)\n             j (range 3)\n\t         :let [s (+ i j)]\n\t\t\t :when (\u003c s 3)]\n\t  [i j]\n\t  s)\n   {[0 0] 0, [0 1] 1, [0 2] 2, [1 0] 1, [1 1] 2, [2 0] 2})\n```\n\n`safe-get` is like `get` but throws when the key doesn't exist:\n\n```clojure\n;; IllegalArgumentException Key :c not found in {:a 1, :b 2}\n(thrown? Exception (safe-get {:a 1 :b 2} :c))\n```\n\nAnother frequently used map function is `map-vals`:\n\n```clojure\n;; return k -\u003e (f v) for [k, v] in map\n(= (map-vals inc {:a 0 :b 0})\n   {:a 1 :b 1})\n```\n\nEver wanted to conditionally do steps in a `-\u003e\u003e` or `-\u003e`? Now you can with our\n'penguin' operators. Here's a few examples:\n\n```clojure\n(use 'plumbing.core)\n(= (let [add-b? false]\n     (-\u003e {:a 1}\n         (merge {:c 2})\n         (?\u003e add-b? (assoc :b 2))))\n   {:a 1 :c 2})\n\n(= (let [inc-all? true]\n     (-\u003e\u003e (range 10)\n          (filter even?)\n          (?\u003e\u003e inc-all? (map inc))))\n\t[1 3 5 7 9])\n```\n\nCheck out [`plumbing.core`](https://github.com/plumatic/plumbing/blob/master/src/plumbing/core.cljc) for many other useful functions.\n\n## ClojureScript\n\nAs of 0.3.0, plumbing is available in ClojureScript! The vast majority of the\nlibrary supports ClojureScript, with the only exceptions that are JVM-specific\noptimizations.\n\nHere's an example usage of `for-map`:\n\n```clojure\n(ns plumbing.readme\n  (:require [plumbing.core :refer-macros [for-map]]))\n\n(defn js-obj-\u003emap\n  \"Recursively converts a JavaScript object into a map with keyword keys\"\n  [obj]\n  (for-map [k (js-keys obj)\n            :let [v (aget obj k)]]\n    (keyword k) (if (object? v) (js-obj-\u003emap v) v)))\n\n(is (= {:a 1 :b {:x \"x\" :y \"y\"}}\n       (js-obj-\u003emap\n        (js-obj \"a\" 1\n                \"b\" (js-obj \"x\" \"x\"\n                            \"y\" \"y\")))))\n\n;; Note: this is a contrived example; you would normally use `cljs.core/clj-\u003ejs`\n```\n\n## Community\n\nPlumbing now has a [mailing list](https://groups.google.com/forum/#!forum/prismatic-plumbing).  Please feel free to join and ask questions or discuss how you're using Plumbing and Graph.\n\n## Supported Clojure versions\n\nPlumbing is currently supported on Clojure 1.8 or later, and the latest ClojureScript version.\n\n## License\n\nDistributed under the Eclipse Public License, the same as Clojure.\n","funding_links":[],"categories":["functions","\u003ca name=\"Clojure\"\u003e\u003c/a\u003eClojure"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplumatic%2Fplumbing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fplumatic%2Fplumbing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fplumatic%2Fplumbing/lists"}