A => .gitignore +13 -0
@@ 1,13 @@
+/target
+/classes
+/checkouts
+profiles.clj
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+/.lein-*
+/.nrepl-port
+/.prepl-port
+.hgignore
+.hg/
A => LICENSE +20 -0
@@ 1,20 @@
+Copyright 2022 Joost Diepenmaat
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
A => README.md +54 -0
@@ 1,54 @@
+# Select Tree
+
+Select subtrees of collections.
+
+## Dependency coordinates
+
+[](https://clojars.org/nl.jomco/select-tree)
+
+## Documentation
+
+API documentation is available inline and at [cljdoc](https://cljdoc.org/d/nl.jomco/select-tree/CURRENT). [](https://cljdoc.org/jump/release/nl.jomco/select-tree)
+
+# USAGE
+
+```clojure
+(def ring-request
+ {:request-method :post
+ :uri "/foo"
+ :headers {"content-type" "text/html; charset=utf8"
+ "authorisation" "Bearer SOMECREDENTIALS"}
+ :body "<html><body>Hello, World</body></html>"
+ :params {:pageNumber 1
+ :pageSize 10
+ :other "thing"}})
+
+(select-tree ring-request
+ {:request-method nil
+ :uri nil
+ :headers #{"content-type"}
+ :params #{:pageNumber
+ :pageSize}})
+=> {:request-method :post
+ :uri "/foo"
+ :headers {"content-type" "text/html; charset=utf8"}
+ :params {:pageNumber 1
+ :pageSize 10}}
+```
+
+# Selectors specification
+
+`nil` means select whole tree
+
+A map selector recursively selects items in a map, with selectors as
+values.
+
+A set selector works as a map selector with `nil` values (select whole
+value for each key). Set selectors also work on sets.
+
+A sequential selector of size N recursively selects the first N, or
+less if the collection is smaller, items in the sequential collection,
+with selectors as values.
+
+An integer selector N selects the first N items in a sequential
+collection (like a sequential selector with N `nil` entries).
A => project.clj +20 -0
@@ 1,20 @@
+(defproject nl.jomco/select-tree "0.1.0-SNAPSHOT"
+ :description "Library to recursively select subtrees of collections"
+ :url "https://git.sr.ht/~jomco/select-tree"
+ :license {:name "MIT"
+ :url "https://opensource.org/licenses/MIT"
+ :distribution :repo}
+ :profiles {:provided {:dependencies [[org.clojure/clojure "1.11.1"]]}}
+ :repl-options {:init-ns nl.jomco.select-tree}
+ :release-tasks [["test-full"]
+ ["vcs" "assert-committed"]
+ ["change" "version" "leiningen.release/bump-version" "release"]
+ ["vcs" "commit"]
+ ["vcs" "tag" "v"]
+ ["deploy" "clojars"]
+ ["change" "version" "leiningen.release/bump-version"]
+ ["vcs" "commit"]
+ ["vcs" "push"]]
+ ;; link to git repo, also ensures that cljdoc.org finds additional markdown files
+ :scm {:name "git"
+ :url "https://git.sr.ht/~jomco/select-tree"})
A => src/nl/jomco/select_tree.clj +125 -0
@@ 1,125 @@
+(ns nl.jomco.select-tree
+ "Select subtrees of collections.
+
+ A straightforward way to describe and create subselections of nested
+ collections.
+
+ # Design Considerations
+
+ ## Clojury interface
+
+ Selectors should not look out of place in idiomatic clojure code;
+ selectors are plain data.
+
+ ## General applicability
+
+ - Any combination of standard clojure collections and leaf nodes can
+ be selected
+ - Vectors (x number of items from the head)
+ - Maps
+ - Sets
+ - Lists (x number of items from the head)
+
+ ## Selected structure should be a \"subtree\" of the original
+
+ - Selected elements in a collection remain on the same key or index;
+ paths to items in the collection remain valid in the selection.
+ - Selectors are unambiguous; the meaning of a selector does not depend
+ on the structure of the tree.
+ - Selection of a collection will return the same kind of collection.
+ - Selection does not add keys to collections or extend sequential
+ collections.
+ - Selection retains metadata.
+
+ ## Selectors should be unambigious
+
+ - Paths in a selection can only be specified once, to prevent
+ ambiguity."
+ (:require [clojure.set :as set]))
+
+(defn select-tree
+ "Return a selection from `coll` specified by `selector`.
+
+ Seletors are defined recursively:
+
+ A `nil` selector returns coll as is.
+
+ A map selector recursively selects items in a map coll, with selectors
+ as values.
+
+ A set selector works as a map selector with `nil` values (select whole
+ value for each key). Set selectors also work on sets.
+
+ A sequential selector of size N recursively selects the first N, or
+ less if the collection is smaller, items in the sequential collection,
+ with selectors as values.
+
+ An integer selector N selects the first N items in a sequential
+ collection (like a sequential selector with N `nil` entries).
+
+ Returns a selection of the same type as `coll`, with the same
+ metadata."
+ [coll selector]
+ (cond
+ (nil? selector)
+ coll
+
+ (nil? coll)
+ nil
+
+ (map? selector)
+ (if (map? coll)
+ (reduce (fn [m [k s]]
+ (if-let [[_ v] (find coll k)]
+ (assoc m k (select-tree v s))
+ m))
+ (empty coll)
+ selector)
+ (throw (ex-info "Map selector not valid for coll"
+ {:coll coll
+ :selector selector})))
+
+ (set? selector)
+ (cond
+ (map? coll)
+ (select-keys coll selector)
+
+ (set? coll)
+ (set/intersection coll selector)
+
+ :else
+ (throw (ex-info "Set selector not valid for coll"
+ {:coll coll
+ :selector selector})))
+
+ (sequential? selector)
+ (cond
+ (seq? coll)
+ (map select-tree coll selector)
+
+ (sequential? coll)
+ (into (empty coll)
+ (map select-tree coll selector))
+
+ :else
+ (throw (ex-info "Sequential selector not valid for coll"
+ {:coll coll
+ :selector selector})))
+
+ (pos-int? selector)
+ (cond
+ (seq? coll)
+ (take selector coll)
+
+ (sequential? coll)
+ (into (empty coll)
+ (take selector coll))
+
+ :else
+ (throw (ex-info "Numeric selector not valid for coll"
+ {:coll coll
+ :selector selector})))
+
+ :else
+ (throw (ex-info "Not a valid selector type" {:selector selector
+ :coll coll}))))
A => test/nl/jomco/select_tree_test.clj +118 -0
@@ 1,118 @@
+(ns nl.jomco.select-tree-test
+ (:require [clojure.test :refer :all]
+ [nl.jomco.select-tree :refer [select-tree]]))
+
+(def example
+ {:a {:a {:a {:a 1 :b 2}}
+ :b [1
+ [2 3]
+ [4 5]
+ {'a [6 7]
+ 2 [8 9]
+ nil #{:a :b :c}
+ "b" 'foo}]
+ :c #{:foo :bar}}
+ :b '(a b c d)})
+
+(deftest select-tree-test
+ (are [res selector] (= res (select-tree example selector))
+ {:a {:a {:a {:a 1 :b 2}}
+ :b [1
+ [2 3]
+ [4 5]
+ {'a [6 7]
+ 2 [8]
+ nil #{:a :b}}]
+ :c #{:foo :bar}}}
+ {:a {:a nil
+ :b [nil nil nil {'a nil
+ 2 [nil]
+ nil #{:a :b}}]
+ :c nil}}
+
+ {:a {:a {:a {:a 1 :b 2}}
+ :b [1
+ [2 3]
+ [4]
+ {'a [6 7]
+ 2 [8]
+ nil #{:a :b}}]
+ :c #{:foo :bar}}}
+ {:a {:a nil
+ :b [nil
+ nil
+ 1
+ {'a nil
+ 2 1
+ nil #{:a :b}}]
+ :c nil}}
+
+ {:a {:c #{:foo}}}
+ {:a {:c #{:foo :none}}
+ :f nil}
+
+ {:b '(a b c)}
+ {:b 3}
+
+ {:b '(a b c)}
+ {:b [nil nil nil]}
+
+ {:b '(a b c d)}
+ #{:b}))
+
+(deftest specification-examples
+ (testing "nil selector"
+ (are [coll] (= coll (select-tree coll nil))
+ {:foo :bar}
+ nil
+ #uuid "00c4eb6a-a4a8-4476-a476-38767df25eaa"))
+
+ (testing "map selector"
+ (is (= {:foo 1 :bar 2}
+ (select-tree {:foo 1 :bar 2 :baz 3}
+ {:foo nil :bar nil :whoo nil}))))
+
+ (testing "set selector"
+ (is (= {:foo 1 :bar 2}
+ (select-tree {:foo 1 :bar 2 :baz 3}
+ #{:foo :bar})))
+
+ (is (= #{:foo :bar}
+ (select-tree #{:foo :bar :baz}
+ #{:foo :bar :whoo}))))
+
+ (testing "sequential selector"
+ (is (= [1 2 3 4]
+ (select-tree [1 2 3 4 5] [nil nil nil nil])))
+
+ (is (= (list 1 2 3 4)
+ (select-tree (list 1 2 3 4 5) [nil nil nil nil]))))
+
+ (testing "numeric selector"
+ (is (= [1 2 3 4]
+ (select-tree [1 2 3 4 5] 4)))
+
+ (is (= (list 1 2 3 4)
+ (select-tree (list 1 2 3 4 5) 4)))))
+
+(def ring-request
+ {:request-method :post
+ :uri "/foo"
+ :headers {"content-type" "text/html; charset=utf8"
+ "authorisation" "Bearer SOMECREDENTIALS"}
+ :body "<html><body>Hello, World</body></html>"
+ :params {:pageNumber 1
+ :pageSize 10
+ :other "thing"}})
+
+(deftest ring-example
+ (is (= {:request-method :post
+ :uri "/foo"
+ :headers {"content-type" "text/html; charset=utf8"}
+ :params {:pageNumber 1
+ :pageSize 10}}
+ (select-tree ring-request
+ {:request-method nil
+ :uri nil
+ :headers #{"content-type"}
+ :params #{:pageNumber :pageSize}}))))