~jomco/select-tree

15b093282fb9b3e3ab8ce59eefaed597f6cb62fe — Joost Diepenmaat 1 year, 11 days ago
Initial commit
6 files changed, 350 insertions(+), 0 deletions(-)

A .gitignore
A LICENSE
A README.md
A project.clj
A src/nl/jomco/select_tree.clj
A test/nl/jomco/select_tree_test.clj
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

[![Clojars Project](https://img.shields.io/clojars/v/nl.jomco/select-tree.svg)](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/badge/nl.jomco/select-tree)](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}}))))