pax_global_header00006660000000000000000000000064131175354140014516gustar00rootroot0000000000000052 comment=891a067bbd2b9b88097a36f21b52d44f8876b6f1 specter-1.0.2/000077500000000000000000000000001311753541400131635ustar00rootroot00000000000000specter-1.0.2/.gitignore000066400000000000000000000002031311753541400151460ustar00rootroot00000000000000pom.xml pom.xml.asc *jar /lib/ /classes/ /target/ /checkouts/ .lein-deps-sum .lein-repl-history .lein-plugins/ .lein-failures out/ specter-1.0.2/.travis.yml000066400000000000000000000001721311753541400152740ustar00rootroot00000000000000language: clojure lein: 2.7.1 cache: directories: - $HOME/.m2 script: - lein test - lein doo node test-build once specter-1.0.2/CHANGES.md000066400000000000000000000425101311753541400145570ustar00rootroot00000000000000## 1.0.2 * Added `pred=`, `pred<`, `pred>`, `pred<=`, `pred>=` for filtering using common comparisons * Add `map-key` navigator * Add `set-elem` navigator * Add `ALL-WITH-META` navigator * `walker` and `codewalker` can now be used with `NONE` to remove elements * Improve `walker` performance by 70% by replacing clojure.walk implementation with custom recursive path * Extend `ALL` to work on records (navigate to key/value pairs) * Add ability to declare a function for end index of `srange-dynamic` that takes in the result of the start index fn. Use `end-fn` macro to declare this function (takes in 2 args of [collection, start-index]). Functions defined with normal mechanisms (e.g. `fn`) will still only take in the collection as an argument. * Workaround for ClojureScript bug that emits warnings for vars named the same as a private var in cljs.core (in this case `NONE`, added as private var to cljs.core with 1.9.562) * For ALL transforms on maps, interpret transformed key/value pair of size < 2 as removal * Bug fix: Fix incorrect inline compilation when a dynamic function invocation is nested in a data structure within a parameter to a navigator builder ## 1.0.1 * `subselect`/`filterer` can remove entries in source by transforming to a smaller sequence * Add `satisfies-protpath?` * Inline cache vars are marked private so as not to interfere with tooling * Improve performance of `ALL` transform on lists by 20% * Bug fix: Using `pred` no longer inserts unnecessary `coerce-nav` call at callsite * Bug fix: Dynamic navs in argument position to another nav now properly expanded and compiled * Bug fix: Dynamic parameters nested inside data structures as arguments are now compiled correctly by inline compiler ## 1.0.0 * Transform to `com.rpl.specter/NONE` to remove elements from data structures. Works with `keypath` (for both sequences and maps), `must`, `nthpath`, `ALL`, `MAP-VALS`, `FIRST`, and `LAST` * Add `nthpath` navigator * Add `with-fresh-collected` higher order navigator * Added `traverse-all` which returns a transducer that traverses over all elements matching the given path. * `select-first` and `select-any` now avoid traversal beyond the first value matched by the path (like when using `ALL`), so they are faster now for those use cases. * Add `MAP-KEYS` navigator that's more efficient than `[ALL FIRST]` * Add `NAME` and `NAMESPACE` navigators * Extend `srange`, `BEGINNING`, `END` to work on strings. Navigates to a substring. * Extend `FIRST` and `LAST` to work on strings. Navigates to a character. * Add `BEFORE-ELEM` and `AFTER-ELEM` for prepending or appending a single element to a sequence * Add `NONE-ELEM` to efficiently add a single element to a set * Improved `ALL` performance for PersistentHashSet * Dynamic navs automatically compile sequence returns if completely static * Eliminate reflection warnings for clj (thanks @mpenet) * Bug fix: Collected vals now properly passed to subpaths for `if-path`, `selected?`, and `not-selected?` * Bug fix: `LAST`, `FIRST`, `BEGINNING`, and `END` properly transform subvector types to a vector type ## 0.13.2 * Bug fix: Fix race condition relating to retrieving path from cache and AOT compilation * Bug fix: LAST no longer converts lists to vectors * Bug fix: Workaround issue with aot + uberjar ## 0.13.1 * Remove any? in com.rpl.specter.impl to avoid conflict with Clojure 1.9 * Enhanced dynamic navigators to continue expanding if any other dynamic navs are returned * Added `eachnav` to turn any 1-argument navigator into a navigator that accepts any number of arguments, navigating by each argument in order * `keypath` and `must` enhanced to take in multiple arguments for concisely specifying multiple steps * Added `traversed` * Bug fix: Fix regression from 0.13.0 where [ALL FIRST] on a PersistentArrayMap that created duplicate keys would create an invalid PersistentArrayMap * Bug fix: Fix problems with multi-path and if-path in latest versions of ClojureScript * Bug fix: Inline compiler no longer flattens and changes the type of sequential params ## 0.13.0 * BREAKING CHANGE: `com.rpl.specter.macros` namespace removed and all macros moved into core `com.rpl.specter` namespace * BREAKING CHANGE: Core protocol `Navigator` changed to `RichNavigator` and functions now have an extra argument. * BREAKING CHANGE: All navigators must be defined with `defnav` and its variations from `com.rpl.specter`. The core protocols may no longer be extended. Existing types can be turned into navigators with the new `IndirectNav` protocol. * BREAKING CHANGE: Removed `fixed-pathed-nav` and `variable-pathed-nav` and replaced with much more generic `late-bound-nav`. `late-bound-nav` can have normal values be late-bound parameterized (not just paths). Use `late-path` function to indicate which parameters are paths. If all bindings given to `late-bound-nav` are static, the navigator will be resolved and cached immediately. See `transformed` and `selected?` for examples. * BREAKING CHANGE: Paths can no longer be compiled without their parameters. Instead, use the `path` macro to handle the parameterization. * BREAKING CHANGE: Parameterized protocol paths now work differently since paths cannot be specified without their parameters. Instead, use the parameter names from the declaration in the extension to specify where the parameters should go. For example: ```clojure (defprotocolpath MyProtPath [a]) (extend-protocolpath MyProtPath clojure.lang.PersistentArrayMap (must a)) ``` * BREAKING CHANGE: Removed `defpathedfn` and replaced with much more generic `defdynamicnav`. `defdynamicnav` works similar to a macro and takes as input the parameters seen during inline caching. Use `dynamic-param?` to distinguish which parameters are statically specified and which are dynamic. `defdynamicnav` is typically used in conjunction with `late-bound-nav` – see implementation of `selected?` for an example. * Inline caching now works with locals, dynamic vars, and special forms used in the nav position. When resolved at runtime, those values will be coerced to a navigator if a vector or implicit nav (e.g. keyword). Can hint with ^:direct-nav metadata to remove this coercion if know for sure those values will be implementations of `RichNavigator` interface. * Redesigned internals so navigators use interface dispatch rather than storing transform/selection functions as separate fields. * Added `local-declarepath` to assist in making local recursive or mutually recursive paths. Use with `providepath`. * Added `recursive-path` to assist in making recursive paths, both parameterized and unparameterized. Example: ```clojure (let [tree-walker (recursive-path [] p (if-path vector? [ALL p] STAY))] (select tree-walker [1 [2 [3 4] 5] [[6]]])) ``` * Significantly improved performance of paths that use dynamic parameters. * Inline factoring now parameterizes navigators immediately when all parameters are constants (rather than factoring it to use late-bound parameterization). This creates leaner, faster code. * Added `IndirectNav` protocol for turning a value type into a navigator. * Removed `must-cache-paths!`. No longer relevant since all paths can now be compiled and cached. * Added `with-inline-debug` macro that prints information about the code being analyzed and produced by the inline compiler / cacher. * Switched codebase from cljx to cljc * Improved performance of ALL and MAP-VALS on PersistentArrayMap by about 2x * `defnav` now generates helper functions for every method. For example, `keypath` now has helpers `keypath-select*` and `keypath-transform*`. These functions take parameters `[key structure next-fn]` * Bug fix: ALL and MAP-VALS transforms on PersistentArrayMap above the threshold now output PersistentArrayMap instead of PersistentHashMap ## 0.12.0 * BREAKING CHANGE: Changed semantics of `Navigator` protocol `select*` in order to enable very large performance improvements to `select`, `select-one`, `select-first`, and `select-one!`. Custom navigators will need to be updated to conform to the new required semantics. Codebases that do not use custom navigators do not require any changes. See the docstring on the protocol for the details. * Added `select-any` operation which selects a single element navigated to by the path. Which element returned is undefined. If no elements are navigated to, returns `com.rpl.specter/NONE`. This is the fastest selection operation. * Added `selected-any?` operation that returns true if any element is navigated to. * Added `traverse` operation which returns a reducible object of all the elements navigated to by the path. Very efficient. * Added `multi-transform` operation which can be used to perform multiple transformations in a single traversal. Much more efficient than doing the transformations with `transform` one after another when the transformations share a lot of navigation. `multi-transform` is used in conjunction with `terminal` and `terminal-val` – see the docstring for details. * Huge performance improvements to `select`, `select-one`, `select-first`, and `select-one!` * Huge performance improvement to `multi-path` * Added META navigator (thanks @aengelberg) * Added DISPENSE navigator to drop all collected values for subsequent navigation * Added `collected?` macro to create a filter function which operates on the collected values. * Error now thrown if a pathedfn (like filterer) is used without being parameterized * Performance improvement for ALL and MAP-VALS on small maps for Clojure by leveraging IMapIterable interface * Added low-level `richnav` macro for creating navigators with full flexibility * Bug fix: multi-path and if-path now work properly with value collection * Bug fix: END, BEGINNING, FIRST, LAST, and MAP-VALS now work properly on nil * Bug fix: ALL and MAP-VALS now maintain the comparator of sorted maps * Bug fix: Using value collection along with `setval` no longer throws exception * Bug fix: Fix error when trying to use Specter along with AOT compilation ## 0.11.2 * Renamed com.rpl.specter.transient namespace to com.rpl.specter.transients to eliminate ClojureScript compiler warning about reserved keyword * Eliminated compiler warnings for ClojureScript version ## 0.11.1 * More efficient inline caching for Clojure version, benchmarks show inline caching within 5% of manually precompiled code for all cases * Added navigators for transients in com.rpl.specter.transient namespace (thanks @aengelberg) * Huge performance improvement for ALL transform on maps and vectors * Significant performance improvements for FIRST/LAST for vectors * Huge performance improvements for `if-path`, `cond-path`, `selected?`, and `not-selected?`, especially for condition path containing only static functions * Huge performance improvement for `END` on vectors * Added specialized MAP-VALS navigator that is twice as fast as using [ALL LAST] * Dropped support for Clojurescript below v1.7.10 * Added :notpath metadata to signify pathedfn arguments that should be treated as regular arguments during inline factoring. If one of these arguments is not a static var reference or non-collection value, the path will not factor. * Bug fix: `transformed` transform-fn no longer factors into `pred` when an anonymous function during inline factoring * Bug fix: Fixed nil->val to not replace the val on `false` * Bug fix: Eliminate reflection when using primitive parameters in an inline cached path ## 0.11.0 * New `path` macro does intelligent inline caching of the provided path. The path is factored into a static portion and into params which may change on each usage of the path (e.g. local parameters). The static part is factored and compiled on the first run-through, and then re-used for all subsequent invocations. As an example, `[ALL (keypath k)]` is factored into `[ALL keypath]`, which is compiled and cached, and `[k]`, which is provided on each execution. If it is not possible to precompile the path (e.g. [ALL some-local-variable]), nothing is cached and the path will be compiled on each run-through. * BREAKING CHANGE: all `select/transform/setval/replace-in` functions changed to macros and moved to com.rpl.specter.macros namespace. The new macros now automatically wrap the provided path in `path` to enable inline caching. Expect up to a 100x performance improvement without using explicit precompilation, and to be within 2% to 15% of the performance of explicitly precompiled usage. * Added `select*/transform*/setval*/replace-in*/etc.` functions that have the same functionality as the old `select/transform/setval/replace-in` functions. * Added `must-cache-paths!` function to throw an error if it is not possible to factor a path into a static portion and dynamic parameters. * BREAKING CHANGE: `defpath` renamed to `defnav` * BREAKING CHANGE: `path` renamed to `nav` * BREAKING CHANGE: `fixed-pathed-path` and `variable-pathed-path` renamed to `fixed-pathed-nav` and `variabled-pathed-nav` * Added `must` navigator to navigate to a key if and only if it exists in the structure * Added `continuous-subseqs` navigator * Added `ATOM` navigator (thanks @rakeshp) * Added "navigator constructors" that can be defined via `defnavconstructor`. These allow defining a flexible function to parameterize a defnav, and the function integrates with inline caching for high performance. ## 0.10.0 * Make codebase bootstrap cljs compatible * Remove usage of reducers in cljs version in favor of transducers (thanks @StephenRudolph) * ALL now maintains type of queues (thanks @StephenRudolph) * Added `parser` path (thanks @thomasathorne) * Added `submap` path (thanks @bfabry) * Added `subselect` path (thanks @aengelberg) * Fix filterer to maintain the type of the input sequence in transforms * Integrated zipper navigation into com.rpl.specter.zipper namespace ## 0.9.3 * Change clojure/clojurescript to provided dependencies * ALL on maps auto-coerces MapEntry to vector, enabling smoother transformation of map keys * declarepath can now be parameterized * Added params-reset which calls its path with the params index walked back by the number of params needed by its path. This enables recursive parameterized paths * Added convenience syntax for defprotocolpath with no params, e.g. (defprotocolpath foo) * Rename VOID to STOP ## 0.9.2 * Added VOID selector which navigates nowhere * Better syntax checking for defpath * Fixed bug in protocol paths (#48) * Protocol paths now error when extension has invalid number of needed parameters * Fix replace-in to work with value collection * Added STAY selector * Added stay-then-continue and continue-then-stay selectors which enable pre-order/post-order traversals * Added declarepath and providepath, which enable arbitrary recursive or mutually recursive paths * Renamed paramspath to path ## 0.9.1 * Fixed reflection in protocol path code * Optimized late-bound parameterization for JVM implementation by directly creating the object array rather than use object-array * Incorrectly specified function names in defpath will now throw error ## 0.9.0 * Fixed bug where comp-paths wouldn't work on lazy seqs in cljs * Renamed defparamspath and defparamscollector to defpath and defcollector * For Clojure version only, implemented protocol paths (see #38) ## 0.8.0 * Now compatible with Clojure 1.6.0 and 1.5.1 by switching build to cljx (thanks @MerelyAPseudonym) * Added subset selector (like srange but for sets) * Added nil->val, NIL->SET, NIL->LIST, and NIL->VECTOR selectors to make it easier to manipulate maps (e.g. (setval [:akey NIL->VECTOR END] [:a :b] amap) to append that vector into that value for the map, even if nothing was at that value at the start) ## 0.7.1 * view can now be late-bound parameterized * Added a late-bound parameterized version of using a function as a selector called "pred" * Added paramsfn helper macro for defining filter functions that take late-bound parameters * walker and codewalker can now be late-bound parameterized ## 0.7.0 * Added late-bound parameterization feature: allows selectors that require params to be precompiled without the parameters, and the parameters are supplied later in bulk. This effectively enables Specter to be used in any situation with very high performance. * Converted Specter built-in selectors to use late-bound parameterization when appropriate * ALL, FIRST, and LAST are now precompiled ## 0.6.2 * Added not-selected? selector * Added transformed selector * Sped up CLJS implementation for comp-paths by replacing obj-extends? call with satisfies? * Fixed CLJS implementation to extend core types appropriately * Used not-native hint to enable direct method invocation to speed up CLJS implementation ## 0.6.1 * Huge speedup to ClojureScript implementation by optimizing field access ## 0.6.0 * Added ClojureScript compatibility ## 0.5.7 * Fix bug in select-one! which wouldn't allow nil result ## 0.5.6 * Add multi-path implementation * change FIRST/LAST to select nothing on an empty sequence * Allow sets to be used directly as selectors (acts as filter) ## 0.5.5 * Change filterer to accept a selector (that acts like selected? to determine whether or not to select value) ## 0.5.4 * Change cond-path and if-path to take in a selector for conditionals (same idea as selected?) ## 0.5.3 * Added cond-path and if-path selectors for choosing paths depending on value of structure at that location ## 0.5.2 * Fix error for selectors with one element defined using comp-paths, e.g. [:a (comp-paths :b)] ## 0.5.1 * Added putval for adding external values to collected values list * nil is now interpreted as identity selector * empty selector is now interpreted as identity selector instead of producing error specter-1.0.2/DEVELOPER.md000066400000000000000000000002011311753541400150230ustar00rootroot00000000000000# Running Clojure tests ``` lein cleantest ``` # Running ClojureScript tests ``` lein javac lein doo node test-build once ``` specter-1.0.2/LICENSE000066400000000000000000000260751311753541400142020ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. specter-1.0.2/README.md000066400000000000000000000306201311753541400144430ustar00rootroot00000000000000# Specter [![Build Status](https://secure.travis-ci.org/nathanmarz/specter.png?branch=master)](http://travis-ci.org/nathanmarz/specter) Clojure has fantastic facilities for doing immutable programming, with a rich library of persistent data structures and efficient mechanisms for manipulating and traversing them. However, Clojure's story is incomplete. Once you nest data structures – which is extremely common – Clojure becomes cumbersome and complex. Clojure even lacks a facility for a basic task like transforming every value in a generic sequence without changing the type or order of that sequence. Specter, available for both Clojure and ClojureScript, provides a high performance abstraction called navigators which complete the story around immutable programming and make it easy to transform and query nested data structures. It allows you to concisely specify what you want to change within a data structure, and get a new data structure back with only your changes applied – everything else is reconstructed and the types of data structures throughout don't unexpectedly change. Consider these examples: **Example 1: Increment every even number nested within map of vector of maps** ```clojure (def data {:a [{:aa 1 :bb 2} {:cc 3}] :b [{:dd 4}]}) ;; Manual Clojure (defn map-vals [m afn] (->> m (map (fn [[k v]] [k (afn v)])) (into {}))) (map-vals data (fn [v] (mapv (fn [m] (map-vals m (fn [v] (if (even? v) (inc v) v)))) v))) ;; Specter (transform [MAP-VALS ALL MAP-VALS even?] inc data) ``` **Example 2: Append a sequence of elements to a nested vector** ```clojure (def data {:a [1 2 3]}) ;; Manual Clojure (update data :a (fn [v] (into (if v v []) [4 5]))) ;; Specter (setval [:a END] [4 5] data) ``` **Example 3: Increment the last odd number in a sequence** ```clojure (def data [1 2 3 4 5 6 7 8]) ;; Manual Clojure (let [idx (reduce-kv (fn [res i v] (if (odd? v) i res)) nil data)] (if idx (update data idx inc) data)) ;; Specter (transform [(filterer odd?) LAST] inc data) ``` **Example 4: Map a function over a sequence without changing the type or order of the sequence** ```clojure ;; Manual Clojure (map inc data) ;; doesn't work, becomes a lazy sequence (into (empty data) (map inc data)) ;; doesn't work, reverses the order of lists ;; Specter (transform ALL inc data) ;; works for all Clojure datatypes with near-optimal efficiency ``` Specter has performance rivaling hand-optimized code (see [this benchmark](https://gist.github.com/nathanmarz/b7c612b417647db80b9eaab618ff8d83)). Clojure's only comparable built-in operations are `get-in` and `update-in`, and the Specter equivalents are 30% and 85% faster respectively (while being just as concise). Under the hood, Specter uses [advanced dynamic techniques](https://github.com/nathanmarz/specter/wiki/Specter's-inline-caching-implementation) to strip away the overhead of composition. Additionally, the built-in navigators use the most efficient means possible of accessing data structures. For example, `ALL` uses `mapv` on vectors, the `IMapIterable` interface on small maps, and `reduce-kv` in conjunction with transients on larger maps. The most important aspect of Specter is its composability. Specter navigators can be composed with any other navigators, so the supported use cases grow combinatorially. And because Specter is completely extensible, it can be used to navigate any data structure or object you have. # Latest Version The latest release version of Specter is hosted on [Clojars](https://clojars.org): [![Current Version](https://clojars.org/com.rpl/specter/latest-version.svg)](https://clojars.org/com.rpl/specter) # Learn Specter - Introductory blog post: [Clojure's missing piece](http://nathanmarz.com/blog/clojures-missing-piece.html) - Presentation about Specter: [Specter: Powerful and Simple Data Structure Manipulation](https://www.youtube.com/watch?v=VTCy_DkAJGk) - Note that this presentation was given before Specter's inline compilation/caching system was developed. You no longer need to do anything special to get near-optimal performance. - List of navigators with examples: [This wiki page](https://github.com/nathanmarz/specter/wiki/List-of-Navigators) provides a more comprehensive overview than the API docs about the behavior of specific navigators and includes many examples. - Core operations and defining new navigators: [This wiki page](https://github.com/nathanmarz/specter/wiki/List-of-Macros) provides a more comprehensive overview than the API docs of the core select/transform/etc. operations and the operations for defining new navigators. - [API docs](http://nathanmarz.github.io/specter/) - Performance guide: [This post](https://github.com/nathanmarz/specter/wiki/Specter's-inline-caching-implementation) provides an overview of how Specter achieves its performance. Specter's API is contained in these files: - [specter.cljc](https://github.com/nathanmarz/specter/blob/master/src/clj/com/rpl/specter.cljc): This contains the built-in navigators and the definition of the core operations. - [transients.cljc](https://github.com/nathanmarz/specter/blob/master/src/clj/com/rpl/specter/transients.cljc): This contains navigators for transient collections. - [zipper.cljc](https://github.com/nathanmarz/specter/blob/master/src/clj/com/rpl/specter/zipper.cljc): This integrates zipper-based navigation into Specter. # Questions? You can ask questions about Specter by [opening an issue](https://github.com/nathanmarz/specter/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3Aquestion+) on Github. You can also find help in the #specter channel on [Clojurians](http://clojurians.net/). # Examples Increment all the values in maps of maps: ```clojure user> (use 'com.rpl.specter) user> (transform [MAP-VALS MAP-VALS] inc {:a {:aa 1} :b {:ba -1 :bb 2}}) {:a {:aa 2}, :b {:ba 0, :bb 3}} ``` Increment all the even values for :a keys in a sequence of maps: ```clojure user> (transform [ALL :a even?] inc [{:a 1} {:a 2} {:a 4} {:a 3}]) [{:a 1} {:a 3} {:a 5} {:a 3}] ``` Retrieve every number divisible by 3 out of a sequence of sequences: ```clojure user> (select [ALL ALL #(= 0 (mod % 3))] [[1 2 3 4] [] [5 3 2 18] [2 4 6] [12]]) [3 3 18 6 12] ``` Increment the last odd number in a sequence: ```clojure user> (transform [(filterer odd?) LAST] inc [2 1 3 6 9 4 8]) [2 1 3 6 10 4 8] ``` Remove nils from a nested sequence: ```clojure user> (setval [:a ALL nil?] NONE {:a [1 2 nil 3 nil]}) {:a [1 2 3]} ``` Increment all the odd numbers between indices 1 (inclusive) and 4 (exclusive): ```clojure user> (transform [(srange 1 4) ALL odd?] inc [0 1 2 3 4 5 6 7]) [0 2 2 4 4 5 6 7] ``` Replace the subsequence from indices 2 to 4 with [:a :b :c :d :e]: ```clojure user> (setval (srange 2 4) [:a :b :c :d :e] [0 1 2 3 4 5 6 7 8 9]) [0 1 :a :b :c :d :e 4 5 6 7 8 9] ``` Concatenate the sequence [:a :b] to every nested sequence of a sequence: ```clojure user> (setval [ALL END] [:a :b] [[1] '(1 2) [:c]]) [[1 :a :b] (1 2 :a :b) [:c :a :b]] ``` Get all the numbers out of a data structure, no matter how they're nested: ```clojure user> (select (walker number?) {2 [1 2 [6 7]] :a 4 :c {:a 1 :d [2 nil]}}) [2 1 2 1 2 6 7 4] ``` Navigate via non-keyword keys: ```clojure user> (select (keypath "a" "b") {"a" {"b" 10}}) [10] ``` Reverse the positions of all even numbers between indices 4 and 11: ```clojure user> (transform [(srange 4 11) (filterer even?)] reverse [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15]) [0 1 2 3 10 5 8 7 6 9 4 11 12 13 14 15] ``` Append [:c :d] to every subsequence that has at least two even numbers: ```clojure user> (setval [ALL (selected? (filterer even?) (view count) (pred>= 2)) END] [:c :d] [[1 2 3 4 5 6] [7 0 -1] [8 8] []]) [[1 2 3 4 5 6 :c :d] [7 0 -1] [8 8 :c :d] []] ``` When doing more involved transformations, you often find you lose context when navigating deep within a data structure and need information "up" the data structure to perform the transformation. Specter solves this problem by allowing you to collect values during navigation to use in the transform function. Here's an example which transforms a sequence of maps by adding the value of the :b key to the value of the :a key, but only if the :a key is even: ```clojure user> (transform [ALL (collect-one :b) :a even?] + [{:a 1 :b 3} {:a 2 :b -10} {:a 4 :b 10} {:a 3}]) [{:b 3, :a 1} {:b -10, :a -8} {:b 10, :a 14} {:a 3}] ``` The transform function receives as arguments all the collected values followed by the navigated to value. So in this case `+` receives the value of the :b key followed by the value of the :a key, and the transform is performed to :a's value. The four built-in ways for collecting values are `VAL`, `collect`, `collect-one`, and `putval`. `VAL` just adds whatever element it's currently on to the value list, while `collect` and `collect-one` take in a selector to navigate to the desired value. `collect` works just like `select` by finding a sequence of values, while `collect-one` expects to only navigate to a single value. Finally, `putval` adds an external value into the collected values list. Increment the value for :a key by 10: ```clojure user> (transform [:a (putval 10)] + {:a 1 :b 3}) {:b 3 :a 11} ``` For every map in a sequence, increment every number in :c's value if :a is even or increment :d if :a is odd: ```clojure user> (transform [ALL (if-path [:a even?] [:c ALL] :d)] inc [{:a 2 :c [1 2] :d 4} {:a 4 :c [0 10 -1]} {:a -1 :c [1 1 1] :d 1}]) [{:c [2 3], :d 4, :a 2} {:c [1 11 0], :a 4} {:c [1 1 1], :d 2, :a -1}] ``` "Protocol paths" can be used to navigate on polymorphic data. For example, if you have two ways of storing "account" information: ```clojure (defrecord Account [funds]) (defrecord User [account]) (defrecord Family [accounts-list]) ``` You can make an "AccountPath" that dynamically chooses its path based on the type of element it is currently navigated to: ```clojure (defprotocolpath AccountPath []) (extend-protocolpath AccountPath User :account Family [:accounts-list ALL]) ``` Then, here is how to select all the funds out of a list of `User` and `Family`: ```clojure user> (select [ALL AccountPath :funds] [(->User (->Account 50)) (->User (->Account 51)) (->Family [(->Account 1) (->Account 2)]) ]) [50 51 1 2] ``` The next examples demonstrate recursive navigation. Here's one way to double all the even numbers in a tree: ```clojure (defprotocolpath TreeWalker []) (extend-protocolpath TreeWalker Object nil clojure.lang.PersistentVector [ALL TreeWalker]) (transform [TreeWalker number? even?] #(* 2 %) [:a 1 [2 [[[3]]] :e] [4 5 [6 7]]]) ;; => [:a 1 [4 [[[3]]] :e] [8 5 [12 7]]] ``` Here's how to reverse the positions of all even numbers in a tree (with order based on a depth first search). This example uses conditional navigation instead of protocol paths to do the walk: ```clojure (def TreeValues (recursive-path [] p (if-path vector? [ALL p] STAY ))) (transform (subselect TreeValues even?) reverse [1 2 [3 [[4]] 5] [6 [7 8] 9 [[10]]]] ) ;; => [1 10 [3 [[8]] 5] [6 [7 4] 9 [[2]]]] ``` # ClojureScript Specter supports ClojureScript! However, some of the differences between Clojure and ClojureScript affect how you use Specter in ClojureScript, in particular with the namespace declarations. In Clojure, you might `(use 'com.rpl.specter)` or say `(:require [com.rpl.specter :refer :all])` in your namespace declaration. But in ClojureScript, these options [aren't allowed](https://groups.google.com/d/msg/clojurescript/SzYK08Oduxo/MxLUjg50gQwJ). Instead, consider using one of these options: ```clojure (:require [com.rpl.specter :as s]) (:require [com.rpl.specter :as s :refer-macros [select transform]]) ;; add in the Specter macros that you need ``` # Future work - Integrate Specter with other kinds of data structures, such as graphs. Desired navigations include: reduction in topological order, navigate to outgoing/incoming nodes, to a subgraph (with metadata indicating how to attach external edges on transformation), to node attributes, to node values, to specific nodes. - Make it possible to parallelize selects/transforms # License Copyright 2015-2017 Red Planet Labs, Inc. Specter is licensed under Apache License v2.0. specter-1.0.2/VERSION000066400000000000000000000000061311753541400142270ustar00rootroot000000000000001.0.2 specter-1.0.2/project.clj000066400000000000000000000033561311753541400153320ustar00rootroot00000000000000(def VERSION (.trim (slurp "VERSION"))) (defproject com.rpl/specter VERSION :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] ; this prevents JVM from doing optimizations which can remove stack traces from NPE and other exceptions ;"-agentpath:/Applications/YourKit_Java_Profiler_2015_build_15056.app/Contents/Resources/bin/mac/libyjpagent.jnilib"] :source-paths ["src/clj"] :java-source-paths ["src/java"] :test-paths ["test", "target/test-classes"] :auto-clean false :dependencies [[riddley "0.1.12"]] :plugins [[lein-codox "0.9.5"] [lein-doo "0.1.7"]] :codox {:source-paths ["target/classes" "src/clj"] :namespaces [com.rpl.specter com.rpl.specter.zipper com.rpl.specter.protocols com.rpl.specter.transients] :source-uri {#"target/classes" "https://github.com/nathanmarz/specter/tree/{version}/src/clj/{classpath}x#L{line}" #".*" "https://github.com/nathanmarz/specter/tree/{version}/src/clj/{classpath}#L{line}"}} :cljsbuild {:builds [{:id "test-build" :source-paths ["src/clj" "target/classes" "test"] :compiler {:output-to "out/testable.js" :main 'com.rpl.specter.cljs-test-runner :target :nodejs :optimizations :none}}]} :profiles {:dev {:dependencies [[org.clojure/test.check "0.9.0"] [org.clojure/clojure "1.8.0"] [org.clojure/clojurescript "1.9.229"]]} :test {:dependencies [[org.clojure/clojure "1.7.0"]]}} :aliases {"deploy" ["do" "clean," "deploy" "clojars"]}) specter-1.0.2/repl.clj000066400000000000000000000001341311753541400146150ustar00rootroot00000000000000(require '[cljs.repl :as repl] '[cljs.repl.node :as node]) (repl/repl (node/repl-env)) specter-1.0.2/scripts/000077500000000000000000000000001311753541400146525ustar00rootroot00000000000000specter-1.0.2/scripts/benchmarks.clj000066400000000000000000000262121311753541400174640ustar00rootroot00000000000000(ns com.rpl.specter.benchmarks (:use [com.rpl.specter] [com.rpl.specter.transients]) (:require [clojure.walk :as walk] [com.rpl.specter.impl :as i])) ;; run via `lein repl` with `(load-file "scripts/benchmarks.clj")` (defn pretty-float5 [anum] (format "%.5g" anum)) (defn pretty-float3 [anum] (format "%.3g" anum)) (defn time-ms [amt afn] (let [start (System/nanoTime) _ (dotimes [_ amt] (afn)) end (System/nanoTime)] (/ (- end start) 1000000.0))) (defn avg [numbers] (/ (reduce + numbers) (count numbers) 1.0)) (defn average-time-ms [iters amt-per-iter afn] (avg ;; treat 1st run as warmup (next (for [i (range (inc iters))] (time-ms amt-per-iter afn))))) (defn compare-benchmark [amt-per-iter afn-map] (System/runFinalization) (System/gc) (let [results (transform MAP-VALS (fn [afn] (average-time-ms 8 amt-per-iter afn)) afn-map) [[_ best-time] & _ :as sorted] (sort-by last results)] (println "\nAvg(ms)\t\tvs best\t\tCode") (doseq [[k t] sorted] (println (pretty-float5 t) "\t\t" (pretty-float3 (/ t best-time 1.0)) "\t\t" k)))) (defmacro run-benchmark [name amt-per-iter & exprs] (let [afn-map (->> exprs shuffle (map (fn [e] [`(quote ~e) `(fn [] ~e)])) (into {}))] `(do (println "Benchmark:" ~name (str "(" ~amt-per-iter " iterations)")) (compare-benchmark ~amt-per-iter ~afn-map) (println "\n********************************\n")))) (defn specter-dynamic-nested-get [data a b c] (select-any (keypath a b c) data)) (defn get-k [k] (fn [m next] (next (get m k)))) (def get-a-b-c (reduce (fn [curr afn] (fn [structure] (afn structure curr))) [identity (get-k :c) (get-k :b) (get-k :a)])) (let [data {:a {:b {:c 1}}} p (comp-paths :a :b :c)] (run-benchmark "get value in nested map" 2500000 (select-any [:a :b :c] data) (select-any (keypath :a :b :c) data) (select-one [:a :b :c] data) (select-first [:a :b :c] data) (select-one! [:a :b :c] data) (compiled-select-any p data) (specter-dynamic-nested-get data :a :b :c) (get-in data [:a :b :c]) (get-a-b-c data) (-> data :a :b :c identity) (-> data (get :a) (get :b) (get :c)) (-> data :a :b :c) (select-any [(keypath :a) (keypath :b) (keypath :c)] data))) (let [data {:a {:b {:c 1}}}] (run-benchmark "set value in nested map" 2500000 (assoc-in data [:a :b :c] 1) (setval [:a :b :c] 1 data))) ;; because below 1.7 there is no update function (defn- my-update [m k afn] (assoc m k (afn (get m k)))) (defn manual-transform [m afn] (my-update m :a (fn [m2] (my-update m2 :b (fn [m3] (my-update m3 :c afn)))))) (let [data {:a {:b {:c 1}}}] (run-benchmark "update value in nested map" 500000 (update-in data [:a :b :c] inc) (transform [:a :b :c] inc data) (manual-transform data inc))) (defn map-vals-map-iterable [^clojure.lang.IMapIterable m afn] (let [k-it (.keyIterator m) v-it (.valIterator m)] (loop [ret {}] (if (.hasNext k-it) (let [k (.next k-it) v (.next v-it)] (recur (assoc ret k (afn v)))) ret)))) (defn map-vals-map-iterable-transient [^clojure.lang.IMapIterable m afn] (persistent! (let [k-it (.keyIterator m) v-it (.valIterator m)] (loop [ret (transient {})] (if (.hasNext k-it) (let [k (.next k-it) v (.next v-it)] (recur (assoc! ret k (afn v)))) ret))))) (let [data '(1 2 3 4 5)] (run-benchmark "transform values of a list" 500000 (transform ALL inc data) (doall (sequence (map inc) data)) (reverse (into '() (map inc) data)) )) (let [data {:a 1 :b 2 :c 3 :d 4}] (run-benchmark "transform values of a small map" 500000 (into {} (for [[k v] data] [k (inc v)])) (reduce-kv (fn [m k v] (assoc m k (inc v))) {} data) (persistent! (reduce-kv (fn [m k v] (assoc! m k (inc v))) (transient {}) data)) (reduce-kv (fn [m k v] (assoc m k (inc v))) (empty data) data) (transform [ALL LAST] inc data) (transform MAP-VALS inc data) (zipmap (keys data) (map inc (vals data))) (into {} (map (fn [e] [(key e) (inc (val e))]) data)) (into {} (map (fn [e] [(key e) (inc (val e))])) data) (map-vals-map-iterable data inc) (map-vals-map-iterable-transient data inc) )) (let [data (->> (for [i (range 1000)] [i i]) (into {}))] (run-benchmark "transform values of large map" 600 (into {} (for [[k v] data] [k (inc v)])) (reduce-kv (fn [m k v] (assoc m k (inc v))) {} data) (persistent! (reduce-kv (fn [m k v] (assoc! m k (inc v))) (transient {}) data)) (persistent! (reduce-kv (fn [m k v] (assoc! m k (inc v))) (transient clojure.lang.PersistentHashMap/EMPTY) data)) (reduce-kv (fn [m k v] (assoc m k (inc v))) (empty data) data) (transform [ALL LAST] inc data) (transform MAP-VALS inc data) (zipmap (keys data) (map inc (vals data))) (into {} (map (fn [e] [(key e) (inc (val e))]) data)) (into {} (map (fn [e] [(key e) (inc (val e))])) data) (map-vals-map-iterable data inc) (map-vals-map-iterable-transient data inc))) (let [data [1 2 3 4 5 6 7 8 9 10]] (run-benchmark "first value of a size 10 vector" 10000000 (first data) (select-any ALL data) (select-any FIRST data) (select-first ALL data) )) (let [data [1 2 3 4 5]] (run-benchmark "map a function over a vector" 1000000 (vec (map inc data)) (mapv inc data) (transform ALL inc data) (into [] (map inc) data))) (let [data [1 2 3 4 5 6 7 8 9 10]] (run-benchmark "filter a sequence" 500000 (doall (filter even? data)) (filterv even? data) (select [ALL even?] data) (select-any (filterer even?) data) (into [] (filter even?) data))) (let [data [{:a 2 :b 2} {:a 1} {:a 4} {:a 6}] xf (comp (map :a) (filter even?))] (run-benchmark "even :a values from sequence of maps" 500000 (select [ALL :a even?] data) (->> data (mapv :a) (filter even?) doall) (into [] (comp (map :a) (filter even?)) data) (into [] xf data))) (let [v (vec (range 1000))] (run-benchmark "Append to a large vector" 2000000 (setval END [1] v) (setval AFTER-ELEM 1 v) (reduce conj v [1]) (conj v 1))) (let [data [1 2 3 4 5 6 7 8 9 10]] (run-benchmark "prepend to a vector" 1000000 (vec (cons 0 data)) (setval BEFORE-ELEM 0 data) (into [0] data) )) (declarepath TreeValues) (providepath TreeValues (if-path vector? [ALL TreeValues] STAY)) (defprotocolpath TreeValuesProt) (extend-protocolpath TreeValuesProt clojure.lang.PersistentVector [ALL TreeValuesProt] Object STAY) (defn tree-value-transform [afn atree] (if (vector? atree) (mapv #(tree-value-transform afn %) atree) (afn atree))) (let [data [1 2 [[3]] [4 6 [7 [8]] 10]]] (run-benchmark "update every value in a tree (represented with vectors)" 50000 (walk/postwalk (fn [e] (if (and (number? e) (even? e)) (inc e) e)) data) (transform [(walker number?) even?] inc data) (transform [TreeValues even?] inc data) (transform [TreeValuesProt even?] inc data) (tree-value-transform (fn [e] (if (even? e) (inc e) e)) data))) (let [toappend (range 1000)] (run-benchmark "transient comparison: building up vectors" 8000 (reduce (fn [v i] (conj v i)) [] toappend) (reduce (fn [v i] (conj! v i)) (transient []) toappend) (setval END toappend []) (setval END! toappend (transient [])))) (let [toappend (range 1000)] (run-benchmark "transient comparison: building up vectors one at a time" 7000 (reduce (fn [v i] (conj v i)) [] toappend) (reduce (fn [v i] (conj! v i)) (transient []) toappend) (reduce (fn [v i] (setval END [i] v)) [] toappend) (reduce (fn [v i] (setval END! [i] v)) (transient []) toappend))) (let [data (vec (range 1000)) tdata (transient data) tdata2 (transient data)] (run-benchmark "transient comparison: assoc'ing in vectors" 2500000 (assoc data 600 0) (assoc! tdata 600 0) (setval (keypath 600) 0 data) (setval (keypath! 600) 0 tdata2))) (let [data (into {} (for [k (range 1000)] [k (rand)])) tdata (transient data) tdata2 (transient data)] (run-benchmark "transient comparison: assoc'ing in maps" 1500000 (assoc data 600 0) (assoc! tdata 600 0) (setval (keypath 600) 0 data) (setval (keypath! 600) 0 tdata2))) (defn modify-submap [m] (assoc m 0 1 458 89)) (let [data (into {} (for [k (range 1000)] [k (rand)])) tdata (transient data)] (run-benchmark "transient comparison: submap" 150000 (transform (submap [600 700]) modify-submap data) (transform (submap! [600 700]) modify-submap tdata))) (let [data {:x 1} meta-map {:my :metadata}] (run-benchmark "set metadata" 1500000 (with-meta data meta-map) (setval META meta-map data))) (let [data (with-meta {:x 1} {:my :metadata})] (run-benchmark "get metadata" 15000000 (meta data) (select-any META data))) (let [data (with-meta {:x 1} {:my :metadata})] (run-benchmark "vary metadata" 800000 (vary-meta data assoc :y 2) (setval [META :y] 2 data))) (let [data (range 1000)] (run-benchmark "Traverse into a set" 5000 (set data) (set (select ALL data)) (into #{} (traverse ALL data)) (persistent! (reduce conj! (transient #{}) (traverse ALL data))) (reduce conj #{} (traverse ALL data)))) (defn mult-10 [v] (* 10 v)) (let [data [1 2 3 4 5 6 7 8 9]] (run-benchmark "multi-transform vs. consecutive transforms, one shared nav" 300000 (->> data (transform [ALL even?] mult-10) (transform [ALL odd?] dec)) (multi-transform [ALL (multi-path [even? (terminal mult-10)] [odd? (terminal dec)])] data))) (let [data [[1 2 3 4 :a] [5] [6 7 :b 8 9] [10 11 12 13]]] (run-benchmark "multi-transform vs. consecutive transforms, three shared navs" 150000 (->> data (transform [ALL ALL number? even?] mult-10) (transform [ALL ALL number? odd?] dec)) (multi-transform [ALL ALL number? (multi-path [even? (terminal mult-10)] [odd? (terminal dec)])] data))) (let [data {:a 1 :b 2 :c 3 :d 4}] (run-benchmark "namespace qualify keys of a small map" 1000000 (into {} (map (fn [[k v]] [(keyword (str *ns*) (name k)) v])) data) (reduce-kv (fn [m k v] (assoc m (keyword (str *ns*) (name k)) v)) {} data) (setval [MAP-KEYS NAMESPACE] (str *ns*) data) )) (let [data (->> (for [i (range 1000)] [(keyword (str i)) i]) (into {}))] (run-benchmark "namespace qualify keys of a large map" 1200 (into {} (map (fn [[k v]] [(keyword (str *ns*) (name k)) v])) data) (reduce-kv (fn [m k v] (assoc m (keyword (str *ns*) (name k)) v)) {} data) (setval [MAP-KEYS NAMESPACE] (str *ns*) data) )) (defnav walker-old [afn] (select* [this structure next-fn] (i/walk-select afn next-fn structure)) (transform* [this structure next-fn] (i/walk-until afn next-fn structure))) (let [data {:a [1 2 {:c '(3 4) :d {:e [1 2 3] 7 8 9 10}}]}] (run-benchmark "walker vs. clojure.walk version" 150000 (transform (walker number?) inc data) (transform (walker-old number?) inc data) )) specter-1.0.2/scripts/cljs-repl.sh000077500000000000000000000001051311753541400171000ustar00rootroot00000000000000#!/bin/bash rlwrap java -cp `lein classpath` clojure.main repl.clj specter-1.0.2/scripts/run-benchmarks000077500000000000000000000002141311753541400175140ustar00rootroot00000000000000#!/bin/bash `lein javac` java -server -XX:MaxPermSize=128m -XX:MaxInlineSize=100 -cp `lein classpath` clojure.main scripts/benchmarks.clj specter-1.0.2/src/000077500000000000000000000000001311753541400137525ustar00rootroot00000000000000specter-1.0.2/src/clj/000077500000000000000000000000001311753541400145225ustar00rootroot00000000000000specter-1.0.2/src/clj/com/000077500000000000000000000000001311753541400153005ustar00rootroot00000000000000specter-1.0.2/src/clj/com/rpl/000077500000000000000000000000001311753541400160755ustar00rootroot00000000000000specter-1.0.2/src/clj/com/rpl/specter.cljc000066400000000000000000001324061311753541400204050ustar00rootroot00000000000000(ns com.rpl.specter #?(:cljs (:require-macros [com.rpl.specter :refer [late-bound-nav late-bound-richnav late-bound-collector defcollector defnav defdynamicnav dynamicnav richnav defrichnav recursive-path]] [com.rpl.specter.util-macros :refer [doseqres]])) ;; workaround for cljs bug that emits warnings for vars named the same as a ;; private var in cljs.core (in this case `NONE`, added as private var to ;; cljs.core with 1.9.562) #?(:cljs (:refer-clojure :exclude [NONE])) (:use [com.rpl.specter.protocols :only [ImplicitNav RichNavigator]] #?(:clj [com.rpl.specter.util-macros :only [doseqres]])) (:require [com.rpl.specter.impl :as i] [com.rpl.specter.navs :as n] #?(:clj [clojure.walk :as cljwalk]) #?(:clj [com.rpl.specter.macros :as macros]) [clojure.set :as set])) (defn- static-path? [path] (if (sequential? path) (every? static-path? path) (-> path i/dynamic-param? not) )) (defn wrap-dynamic-nav [f] (fn [& args] (let [ret (apply f args)] (cond (and (sequential? ret) (static-path? ret)) (i/comp-paths* ret) (and (sequential? ret) (= 1 (count ret))) (first ret) :else ret )))) #?(:clj (do (defmacro defmacroalias [name target] `(do (def ~name (var ~target)) (alter-meta! (var ~name) merge {:macro true}))) (defmacroalias richnav macros/richnav) (defmacroalias nav macros/nav) (defmacroalias defnav macros/defnav) (defmacroalias defrichnav macros/defrichnav) (defmacro collector [params [_ [_ structure-sym] & body]] `(richnav ~params (~'select* [this# vals# ~structure-sym next-fn#] (next-fn# (conj vals# (do ~@body)) ~structure-sym)) (~'transform* [this# vals# ~structure-sym next-fn#] (next-fn# (conj vals# (do ~@body)) ~structure-sym)))) (defmacro defcollector [name & body] `(def ~name (collector ~@body))) (defn- late-bound-operation [bindings builder-op impls] (let [bindings (partition 2 bindings) params (map first bindings) curr-params (map second bindings)] `(let [builder# (~builder-op [~@params] ~@impls) curr-params# [~@curr-params]] (if (every? (complement i/dynamic-param?) curr-params#) (apply builder# curr-params#) (com.rpl.specter.impl/->DynamicFunction builder# curr-params# nil))))) (defmacro late-bound-nav [bindings & impls] (late-bound-operation bindings `nav impls)) (defmacro late-bound-collector [bindings impl] (late-bound-operation bindings `collector [impl])) (defmacro late-bound-richnav [bindings & impls] (late-bound-operation bindings `richnav impls)) (defmacro with-inline-debug [& body] `(binding [i/*DEBUG-INLINE-CACHING* true] ~@body)) (defmacro declarepath [name] `(def ~name (i/local-declarepath))) (defmacro providepath [name apath] `(i/providepath* ~name (path ~apath))) (defmacro recursive-path [params self-sym path] (if (empty? params) `(let [~self-sym (i/local-declarepath)] (providepath ~self-sym ~path) ~self-sym) `(i/direct-nav-obj (fn ~params (let [~self-sym (i/local-declarepath)] (providepath ~self-sym ~path) ~self-sym))))) ;; copied from tools.macro to avoid the dependency (defn- name-with-attributes "To be used in macro definitions. Handles optional docstrings and attribute maps for a name to be defined in a list of macro arguments. If the first macro argument is a string, it is added as a docstring to name and removed from the macro argument list. If afterwards the first macro argument is a map, its entries are added to the name's metadata map and the map is removed from the macro argument list. The return value is a vector containing the name with its extended metadata map and the list of unprocessed macro arguments." [name macro-args] (let [[docstring macro-args] (if (string? (first macro-args)) [(first macro-args) (next macro-args)] [nil macro-args]) [attr macro-args] (if (map? (first macro-args)) [(first macro-args) (next macro-args)] [{} macro-args]) attr (if docstring (assoc attr :doc docstring) attr) attr (if (meta name) (conj (meta name) attr) attr)] [(with-meta name attr) macro-args])) (defmacro dynamicnav [& args] `(vary-meta (wrap-dynamic-nav (fn ~@args)) assoc :dynamicnav true)) (defmacro defdynamicnav "Defines a function that can choose what navigator to use at runtime based on the dynamic context. The arguments will either be static values or objects satisfying `dynamic-param?`. Use `late-bound-nav` to produce a runtime navigator that uses the values of the dynamic params. See `selected?` for an illustrative example of dynamic navs." [name & args] (let [[name args] (name-with-attributes name args)] `(def ~name (dynamicnav ~@args)))) (defn- ic-prepare-path [locals-set path] (cond (vector? path) (mapv #(ic-prepare-path locals-set %) path) (symbol? path) (if (contains? locals-set path) (let [s (get locals-set path) embed (i/maybe-direct-nav path (-> s meta :direct-nav))] `(com.rpl.specter.impl/->LocalSym ~path (quote ~embed))) ;; var-get doesn't work in cljs, so capture the val in the macro instead `(com.rpl.specter.impl/->VarUse ~path (var ~path) (quote ~path))) (i/fn-invocation? path) (let [[op & params] path] ;; need special case for 'fn since macroexpand does NOT ;; expand fn when run on cljs code, but it's also not considered a special symbol (if (or (= 'fn op) (special-symbol? op)) `(com.rpl.specter.impl/->SpecialFormUse ~path (quote ~path)) `(com.rpl.specter.impl/->FnInvocation ~(ic-prepare-path locals-set op) ~(mapv #(ic-prepare-path locals-set %) params) (quote ~path)))) :else (if (empty? (i/used-locals locals-set path)) path `(com.rpl.specter.impl/->DynamicVal (quote ~path))))) (defn- ic-possible-params [path] (do (mapcat (fn [e] (cond (or (set? e) (map? e) (symbol? e) (and (i/fn-invocation? e) (or (contains? #{'fn* 'fn} (first e)) (special-symbol? (first e))))) [e] (sequential? e) (concat (if (vector? e) [e]) (ic-possible-params e)))) path))) (defn- cljs-macroexpand [env form] (let [expand-fn (i/cljs-analyzer-macroexpand-1) mform (expand-fn env form)] (cond (identical? form mform) mform (and (seq? mform) (#{'js*} (first mform))) form :else (cljs-macroexpand env mform)))) (defn- cljs-macroexpand-all* [env form] (if (and (seq? form) (#{'fn 'fn* 'cljs.core/fn} (first form))) form (let [expanded (if (seq? form) (cljs-macroexpand env form) form)] (cljwalk/walk #(cljs-macroexpand-all* env %) identity expanded)))) (defn- cljs-macroexpand-all [env form] (let [ret (cljs-macroexpand-all* env form)] ret)) (defmacro path "Same as calling comp-paths, except it caches the composition of the static parts of the path for later re-use (when possible). For almost all idiomatic uses of Specter provides huge speedup. This macro is automatically used by the select/transform/setval/replace-in/etc. macros." [& path] (let [;;this is a hack, but the composition of &env is considered stable for cljs platform (if (contains? &env :locals) :cljs :clj) local-syms (if (= platform :cljs) (-> &env :locals keys set) ;cljs (-> &env keys set)) ;clj used-locals (i/used-locals local-syms path) ;; note: very important to use riddley's macroexpand-all here, so that ;; &env is preserved in any potential nested calls to select (like via ;; a view function) expanded (if (= platform :clj) (i/clj-macroexpand-all (vec path)) (cljs-macroexpand-all &env (vec path))) prepared-path (ic-prepare-path local-syms expanded) possible-params (vec (ic-possible-params expanded)) cache-sym (vary-meta (gensym "pathcache") merge {:cljs.analyzer/no-resolve true :no-doc true :private true}) info-sym (gensym "info") get-cache-code (if (= platform :clj) `(try (i/get-cell ~cache-sym) (catch ClassCastException e# ;; With AOT compilation it's possible for: ;; Thread 1: unbound, so throw exception ;; Thread 2: unbound, so throw exception ;; Thread 1: do alter-var-root ;; Thread 2: it's bound, so retrieve the current value (if (bound? (var ~cache-sym)) (i/get-cell ~cache-sym) (do (alter-var-root (var ~cache-sym) (fn [_#] (i/mutable-cell))) nil)))) cache-sym) add-cache-code (if (= platform :clj) `(i/set-cell! ~cache-sym ~info-sym) `(def ~cache-sym ~info-sym)) precompiled-sym (gensym "precompiled") handle-params-code (if (= platform :clj) `(~precompiled-sym ~@used-locals) `(~precompiled-sym ~possible-params))] (if (= platform :clj) (i/intern* *ns* cache-sym (i/mutable-cell))) `(let [info# ~get-cache-code info# (if (nil? info#) (let [~info-sym (i/magic-precompilation ~prepared-path ~(str *ns*) (quote ~used-locals) (quote ~possible-params))] ~add-cache-code ~info-sym) info#) ~precompiled-sym (i/cached-path-info-precompiled info#) dynamic?# (i/cached-path-info-dynamic? info#)] (if dynamic?# ~handle-params-code ~precompiled-sym)))) (defmacro select "Navigates to and returns a sequence of all the elements specified by the path. This macro will do inline caching of the path." [apath structure] `(i/compiled-select* (path ~apath) ~structure)) (defmacro select-one! "Returns exactly one element, throws exception if zero or multiple elements found. This macro will do inline caching of the path." [apath structure] `(i/compiled-select-one!* (path ~apath) ~structure)) (defmacro select-one "Like select, but returns either one element or nil. Throws exception if multiple elements found. This macro will do inline caching of the path." [apath structure] `(i/compiled-select-one* (path ~apath) ~structure)) (defmacro select-first "Returns first element found. This macro will do inline caching of the path." [apath structure] `(i/compiled-select-first* (path ~apath) ~structure)) (defmacro select-any "Returns any element found or [[NONE]] if nothing selected. This is the most efficient of the various selection operations. This macro will do inline caching of the path." [apath structure] `(i/compiled-select-any* (path ~apath) ~structure)) (defmacro selected-any? "Returns true if any element was selected, false otherwise. This macro will do inline caching of the path." [apath structure] `(i/compiled-selected-any?* (path ~apath) ~structure)) (defmacro transform "Navigates to each value specified by the path and replaces it by the result of running the transform-fn on it. This macro will do inline caching of the path." [apath transform-fn structure] `(i/compiled-transform* (path ~apath) ~transform-fn ~structure)) (defmacro multi-transform "Just like `transform` but expects transform functions to be specified inline in the path using `terminal`. Error is thrown if navigation finishes at a non-`terminal` navigator. `terminal-val` is a wrapper around `terminal` and is the `multi-transform` equivalent of `setval`. This macro will do inline caching of the path." [apath structure] `(i/compiled-multi-transform* (path ~apath) ~structure)) (defmacro setval "Navigates to each value specified by the path and replaces it by `aval`. This macro will do inline caching of the path." [apath aval structure] `(i/compiled-setval* (path ~apath) ~aval ~structure)) (defmacro traverse "Return a reducible object that traverses over `structure` to every element specified by the path. This macro will do inline caching of the path." [apath structure] `(i/do-compiled-traverse (path ~apath) ~structure)) (defmacro traverse-all "Returns a transducer that traverses over each element with the given path." [apath] `(i/compiled-traverse-all* (path ~apath))) (defmacro replace-in "Similar to transform, except returns a pair of [transformed-structure sequence-of-user-ret]. The transform-fn in this case is expected to return [ret user-ret]. ret is what's used to transform the data structure, while user-ret will be added to the user-ret sequence in the final return. replace-in is useful for situations where you need to know the specific values of what was transformed in the data structure. This macro will do inline caching of the path." [apath transform-fn structure & args] `(i/compiled-replace-in* (path ~apath) ~transform-fn ~structure ~@args)) (defmacro collected? "Creates a filter function navigator that takes in all the collected values as input. For arguments, can use `(collected? [a b] ...)` syntax to look at each collected value as individual arguments, or `(collected? v ...)` syntax to capture all the collected values as a single vector." [params & body] `(i/collected?* (~'fn [~params] ~@body))) (defn- protpath-sym [name] (-> name (str "-prot") symbol)) (defn- protpath-meth-sym [name] (-> name (str "-retrieve") symbol)) (defmacro defprotocolpath "Defines a navigator that chooses the path to take based on the type of the value at the current point. May be specified with parameters to specify that all extensions must require that number of parameters. Currently not available for ClojureScript. Example of usage: (defrecord SingleAccount [funds]) (defrecord FamilyAccount [single-accounts]) (defprotocolpath FundsPath) (extend-protocolpath FundsPath SingleAccount :funds FamilyAccount [ALL FundsPath] ) " ([name] `(defprotocolpath ~name [])) ([name params] (let [prot-name (protpath-sym name) m (protpath-meth-sym name) num-params (count params) ssym (gensym "structure") rargs [(gensym "vals") ssym (gensym "next-fn")] retrieve `(~m ~ssym ~@params)] `(do (defprotocol ~prot-name (~m [structure# ~@params])) (defrichnav ~name ~params (~'select* [this# ~@rargs] (let [inav# ~retrieve] (i/exec-select* inav# ~@rargs))) (~'transform* [this# ~@rargs] (let [inav# ~retrieve] (i/exec-transform* inav# ~@rargs)))))))) (defmacro satisfies-protpath? [protpath o] `(satisfies? ~(protpath-sym protpath) ~o)) (defn extend-protocolpath* [protpath-prot extensions] (let [m (-> protpath-prot :sigs keys first) params (-> protpath-prot :sigs first last :arglists first)] (doseq [[atype path-code] extensions] (extend atype protpath-prot {m (eval `(fn ~params (path ~path-code)))})))) (defmacro extend-protocolpath "Used in conjunction with `defprotocolpath`. See [[defprotocolpath]]." [protpath & extensions] (let [extensions (partition 2 extensions) embed (vec (for [[t p] extensions] [t `(quote ~p)]))] `(extend-protocolpath* ~(protpath-sym protpath) ~embed))) (defmacro end-fn [& args] `(n/->SrangeEndFunction (fn ~@args))) )) (defn comp-paths "Returns a compiled version of the given path for use with compiled-{select/transform/setval/etc.} functions." [& apath] (i/comp-paths* (vec apath))) ;; Selection functions (def ^{:doc "Version of select that takes in a path precompiled with comp-paths"} compiled-select i/compiled-select*) (defn select* "Navigates to and returns a sequence of all the elements specified by the path." [path structure] (compiled-select (i/comp-paths* path) structure)) (def ^{:doc "Version of select-one that takes in a path precompiled with comp-paths"} compiled-select-one i/compiled-select-one*) (defn select-one* "Like select, but returns either one element or nil. Throws exception if multiple elements found" [path structure] (compiled-select-one (i/comp-paths* path) structure)) (def ^{:doc "Version of select-one! that takes in a path precompiled with comp-paths"} compiled-select-one! i/compiled-select-one!*) (defn select-one!* "Returns exactly one element, throws exception if zero or multiple elements found" [path structure] (compiled-select-one! (i/comp-paths* path) structure)) (def ^{:doc "Version of select-first that takes in a path precompiled with comp-paths"} compiled-select-first i/compiled-select-first*) (defn select-first* "Returns first element found." [path structure] (compiled-select-first (i/comp-paths* path) structure)) (def ^{:doc "Version of select-any that takes in a path precompiled with comp-paths"} compiled-select-any i/compiled-select-any*) (def ^{:doc "Global value used to indicate no elements selected during [[select-any]]."} NONE i/NONE) (defn select-any* "Returns any element found or [[NONE]] if nothing selected. This is the most efficient of the various selection operations." [path structure] (compiled-select-any (i/comp-paths* path) structure)) (def ^{:doc "Version of selected-any? that takes in a path precompiled with comp-paths"} compiled-selected-any? i/compiled-selected-any?*) (defn selected-any?* "Returns true if any element was selected, false otherwise." [path structure] (compiled-selected-any? (i/comp-paths* path) structure)) ;; Reducible traverse functions (def ^{:doc "Version of traverse that takes in a path precompiled with comp-paths"} compiled-traverse i/do-compiled-traverse) (defn traverse* "Return a reducible object that traverses over `structure` to every element specified by the path" [apath structure] (compiled-traverse (i/comp-paths* apath) structure)) (def ^{:doc "Version of traverse-all that takes in a path precompiled with comp-paths"} compiled-traverse-all i/compiled-traverse-all*) (defn traverse-all* "Returns a transducer that traverses over each element with the given path." [apath] (compiled-traverse-all (i/comp-paths* apath))) ;; Transformation functions (def ^{:doc "Version of transform that takes in a path precompiled with comp-paths"} compiled-transform i/compiled-transform*) (defn transform* "Navigates to each value specified by the path and replaces it by the result of running the transform-fn on it" [path transform-fn structure] (compiled-transform (i/comp-paths* path) transform-fn structure)) (def ^{:doc "Version of `multi-transform` that takes in a path precompiled with `comp-paths`"} compiled-multi-transform i/compiled-multi-transform*) (defn multi-transform* "Just like `transform` but expects transform functions to be specified inline in the path using `terminal`. Error is thrown if navigation finishes at a non-`terminal` navigator. `terminal-val` is a wrapper around `terminal` and is the `multi-transform` equivalent of `setval`." [path structure] (compiled-multi-transform (i/comp-paths* path) structure)) (def ^{:doc "Version of setval that takes in a path precompiled with comp-paths"} compiled-setval i/compiled-setval*) (defn setval* "Navigates to each value specified by the path and replaces it by val" [path val structure] (compiled-setval (i/comp-paths* path) val structure)) (def ^{:doc "Version of replace-in that takes in a path precompiled with comp-paths"} compiled-replace-in i/compiled-replace-in*) (defn replace-in* "Similar to transform, except returns a pair of [transformed-structure sequence-of-user-ret]. The transform-fn in this case is expected to return [ret user-ret]. ret is what's used to transform the data structure, while user-ret will be added to the user-ret sequence in the final return. replace-in is useful for situations where you need to know the specific values of what was transformed in the data structure." [path transform-fn structure & {:keys [merge-fn] :or {merge-fn concat}}] (compiled-replace-in (i/comp-paths* path) transform-fn structure :merge-fn merge-fn)) ;; Helper for making late-bound navs (def late-path i/late-path) (def dynamic-param? i/dynamic-param?) (def late-resolved-fn i/late-resolved-fn) (defdynamicnav ^{:doc "Turns a navigator that takes one argument into a navigator that takes many arguments and uses the same navigator with each argument. There is no performance cost to using this. See implementation of `keypath`"} eachnav [navfn] (let [latenavfn (late-resolved-fn navfn)] (dynamicnav [& args] (map latenavfn args)))) ;; Helpers for making recursive or mutually recursive navs (def local-declarepath i/local-declarepath) ;; Built-in pathing and context operations (defnav ^{:doc "Stops navigation at this point. For selection returns nothing and for transformation returns the structure unchanged"} STOP [] (select* [this structure next-fn] NONE) (transform* [this structure next-fn] structure)) (def ^{:doc "Stays navigated at the current point. Essentially a no-op navigator."} STAY i/STAY*) (def ^{:doc "For usage with `multi-transform`, defines an endpoint in the navigation that will have the parameterized transform function run. The transform function works just like it does in `transform`, with collected values given as the first arguments"} terminal (richnav [afn] (select* [this vals structure next-fn] (i/throw-illegal "'terminal' should only be used in multi-transform")) (transform* [this vals structure next-fn] (i/terminal* afn vals structure)))) (defn ^:direct-nav terminal-val "Like `terminal` but specifies a val to set at the location regardless of the collected values or the value at the location." [v] (terminal (i/fast-constantly v))) (defnav ^{:doc "Navigate to every element of the collection. For maps navigates to a vector of `[key value]`."} ALL [] (select* [this structure next-fn] (n/all-select structure next-fn)) (transform* [this structure next-fn] (n/all-transform structure next-fn))) (defnav ^{:doc "Same as ALL, except maintains metadata on the structure."} ALL-WITH-META [] (select* [this structure next-fn] (n/all-select structure next-fn)) (transform* [this structure next-fn] (let [m (meta structure) res (n/all-transform structure next-fn)] (if (some? res) (with-meta res m) )))) (defnav ^{:doc "Navigate to each value of the map. This is more efficient than navigating via [ALL LAST]"} MAP-VALS [] (select* [this structure next-fn] (doseqres NONE [v (vals structure)] (next-fn v))) (transform* [this structure next-fn] (n/map-vals-transform structure next-fn))) (defnav ^{:doc "Navigate to each key of the map. This is more efficient than navigating via [ALL FIRST]"} MAP-KEYS [] (select* [this structure next-fn] (doseqres NONE [k (keys structure)] (next-fn k))) (transform* [this structure next-fn] (n/map-keys-transform structure next-fn))) (defcollector VAL [] (collect-val [this structure] structure)) (def ^{:doc "Navigate to the last element of the collection. If the collection is empty navigation is stopped at this point."} LAST (n/PosNavigator n/get-last n/update-last)) (def ^{:doc "Navigate to the first element of the collection. If the collection is empty navigation is stopped at this point."} FIRST (n/PosNavigator n/get-first n/update-first)) (defnav ^{:doc "Uses start-index-fn and end-index-fn to determine the bounds of the subsequence to select when navigating. `start-index-fn` takes in the structure as input. `end-index-fn` can be one of two forms. If a regular function (e.g. defined with `fn`), it takes in only the structure as input. If a function defined using special `end-fn` macro, it takes in the structure and the result of `start-index-fn`."} srange-dynamic [start-index-fn end-index-fn] (select* [this structure next-fn] (let [s (start-index-fn structure)] (n/srange-select structure s (n/invoke-end-fn end-index-fn structure s) next-fn))) (transform* [this structure next-fn] (let [s (start-index-fn structure)] (n/srange-transform structure s (n/invoke-end-fn end-index-fn structure s) next-fn)))) (defnav ^{:doc "Navigates to the subsequence bound by the indexes start (inclusive) and end (exclusive)"} srange [start end] (select* [this structure next-fn] (n/srange-select structure start end next-fn)) (transform* [this structure next-fn] (n/srange-transform structure start end next-fn))) (defnav ^{:doc "Navigates to every continuous subsequence of elements matching `pred`"} continuous-subseqs [pred] (select* [this structure next-fn] (doseqres NONE [[s e] (i/matching-ranges structure pred)] (n/srange-select structure s e next-fn))) (transform* [this structure next-fn] (i/continuous-subseqs-transform* pred structure next-fn))) (defnav ^{:doc "Navigate to the empty subsequence before the first element of the collection."} BEGINNING [] (select* [this structure next-fn] (next-fn (if (string? structure) "" []))) (transform* [this structure next-fn] (if (string? structure) (str (next-fn "") structure) (let [to-prepend (next-fn [])] (n/prepend-all structure to-prepend))))) (defnav ^{:doc "Navigate to the empty subsequence after the last element of the collection."} END [] (select* [this structure next-fn] (next-fn (if (string? structure) "" []))) (transform* [this structure next-fn] (if (string? structure) (str structure (next-fn "")) (let [to-append (next-fn [])] (n/append-all structure to-append))))) (defnav ^{:doc "Navigate to 'void' elem in the set. For transformations - if result is not `NONE`, then add that value to the set."} NONE-ELEM [] (select* [this structure next-fn] (next-fn NONE)) (transform* [this structure next-fn] (let [newe (next-fn NONE)] (if (identical? NONE newe) structure (if (nil? structure) #{newe} (conj structure newe) ))))) (defnav ^{:doc "Navigate to 'void' element before the sequence. For transformations – if result is not `NONE`, then prepend that value."} BEFORE-ELEM [] (select* [this structure next-fn] (next-fn NONE)) (transform* [this structure next-fn] (let [newe (next-fn NONE)] (if (identical? NONE newe) structure (n/prepend-one structure newe) )))) (defnav ^{:doc "Navigate to 'void' element after the sequence. For transformations – if result is not `NONE`, then append that value."} AFTER-ELEM [] (select* [this structure next-fn] (next-fn NONE)) (transform* [this structure next-fn] (let [newe (next-fn NONE)] (if (identical? NONE newe) structure (n/append-one structure newe) )))) (defnav ^{:doc "Navigates to the specified subset (by taking an intersection). In a transform, that subset in the original set is changed to the new value of the subset."} subset [aset] (select* [this structure next-fn] (next-fn (set/intersection structure aset))) (transform* [this structure next-fn] (let [subset (set/intersection structure aset) newset (next-fn subset)] (-> structure (set/difference subset) (set/union newset))))) (defnav ^{:doc "Navigates to the specified submap (using select-keys). In a transform, that submap in the original map is changed to the new value of the submap."} submap [m-keys] (select* [this structure next-fn] (next-fn (select-keys structure m-keys))) (transform* [this structure next-fn] (let [submap (select-keys structure m-keys) newmap (next-fn submap)] (merge (reduce dissoc structure m-keys) newmap)))) (defdynamicnav subselect "Navigates to a sequence that contains the results of (select ...), but is a view to the original structure that can be transformed. Requires that the input navigators will walk the structure's children in the same order when executed on \"select\" and then \"transform\". If transformed sequence is smaller than input sequence, missing entries will be filled in with NONE, triggering removal if supported by that navigator." [& path] (late-bound-nav [late (late-path path)] (select* [this structure next-fn] (next-fn (compiled-select late structure))) (transform* [this structure next-fn] (let [select-result (compiled-select late structure) transformed (next-fn select-result) values-to-insert (i/mutable-cell transformed)] (compiled-transform late (fn [_] (let [vs (i/get-cell values-to-insert)] (if vs (do (i/update-cell! values-to-insert next) (first vs)) NONE ))) structure))))) (defrichnav ^{:doc "Navigates to the given key in the map (not to the value). Navigates only if the key currently exists in the map. Can transform to NONE to remove the key/value pair from the map."} map-key [key] (select* [this vals structure next-fn] (if (contains? structure key) (next-fn vals key) NONE )) (transform* [this vals structure next-fn] (if (contains? structure key) (let [newkey (next-fn vals key) dissoced (dissoc structure key)] (if (identical? NONE newkey) dissoced (assoc dissoced newkey (get structure key)) )) structure ))) (defrichnav ^{:doc "Navigates to the given element in the set only if it exists in the set. Can transform to NONE to remove the element from the set."} set-elem [elem] (select* [this vals structure next-fn] (if (contains? structure elem) (next-fn vals elem) NONE )) (transform* [this vals structure next-fn] (if (contains? structure elem) (let [newelem (next-fn vals elem) removed (disj structure elem)] (if (identical? NONE newelem) removed (conj removed newelem) )) structure ))) (def ^{:doc "Navigate to the specified keys one after another. If navigate to NONE, that element is removed from the map or vector."} keypath (eachnav n/keypath*)) (def ^{:doc "Navigate to the specified keys one after another, only if they exist in the data structure. If navigate to NONE, that element is removed from the map or vector."} must (eachnav n/must*)) (def ^{:doc "Navigate to the specified indices one after another.If navigate to NONE, that element is removed from the sequence."} nthpath (eachnav n/nthpath*)) (defrichnav ^{:doc "Navigates to result of running `afn` on the currently navigated value."} view [afn] (select* [this vals structure next-fn] (next-fn vals (afn structure))) (transform* [this vals structure next-fn] (next-fn vals (afn structure)))) (defnav ^{:doc "Navigate to the result of running `parse-fn` on the value. For transforms, the transformed value then has `unparse-fn` run on it to get the final value at this point."} parser [parse-fn unparse-fn] (select* [this structure next-fn] (next-fn (parse-fn structure))) (transform* [this structure next-fn] (unparse-fn (next-fn (parse-fn structure))))) (defnav ^{:doc "Navigates to atom value."} ATOM [] (select* [this structure next-fn] (next-fn @structure)) (transform* [this structure next-fn] (do (swap! structure next-fn) structure))) (defdynamicnav selected? "Filters the current value based on whether a path finds anything. e.g. (selected? :vals ALL even?) keeps the current element only if an even number exists for the :vals key." [& path] (if-let [afn (n/extract-basic-filter-fn path)] afn (late-bound-richnav [late (late-path path)] (select* [this vals structure next-fn] (i/filter-select #(n/selected?* late vals %) vals structure next-fn)) (transform* [this vals structure next-fn] (i/filter-transform #(n/selected?* late vals %) vals structure next-fn))))) (defdynamicnav not-selected? [& path] (if-let [afn (n/extract-basic-filter-fn path)] (fn [s] (not (afn s))) (late-bound-richnav [late (late-path path)] (select* [this vals structure next-fn] (i/filter-select #(n/not-selected?* late vals %) vals structure next-fn)) (transform* [this vals structure next-fn] (i/filter-transform #(n/not-selected?* late vals %) vals structure next-fn))))) (defdynamicnav filterer "Navigates to a view of the current sequence that only contains elements that match the given path. An element matches the selector path if calling select on that element with the path yields anything other than an empty sequence. For transformation: `NONE` entries in the result sequence cause corresponding entries in input to be removed. A result sequence smaller than the input sequence is equivalent to padding the result sequence with `NONE` at the end until the same size as the input." [& path] (subselect ALL (selected? path))) (defdynamicnav transformed "Navigates to a view of the current value by transforming it with the specified path and update-fn." [path update-fn] (late-bound-nav [late (late-path path) late-fn update-fn] (select* [this structure next-fn] (next-fn (compiled-transform late late-fn structure))) (transform* [this structure next-fn] (next-fn (compiled-transform late late-fn structure))))) (defdynamicnav traversed "Navigates to a view of the current value by transforming with a reduction over the specified traversal." [path reduce-fn] (late-bound-nav [late (late-path path) late-fn reduce-fn] (select* [this structure next-fn] (next-fn (reduce late-fn (compiled-traverse late structure)))) (transform* [this structure next-fn] (next-fn (reduce late-fn (compiled-traverse late structure))) ))) (def ^{:doc "Keeps the element only if it matches the supplied predicate. This is the late-bound parameterized version of using a function directly in a path." :direct-nav true} pred i/pred*) (defn ^:direct-nav pred= [v] (pred #(= % v))) (defn ^:direct-nav pred< [v] (pred #(< % v))) (defn ^:direct-nav pred> [v] (pred #(> % v))) (defn ^:direct-nav pred<= [v] (pred #(<= % v))) (defn ^:direct-nav pred>= [v] (pred #(>= % v))) (extend-type nil ImplicitNav (implicit-nav [this] STAY)) (extend-type #?(:clj clojure.lang.Keyword :cljs cljs.core/Keyword) ImplicitNav (implicit-nav [this] (n/keypath* this))) (extend-type #?(:clj clojure.lang.AFn :cljs function) ImplicitNav (implicit-nav [this] (pred this))) (extend-type #?(:clj clojure.lang.PersistentHashSet :cljs cljs.core/PersistentHashSet) ImplicitNav (implicit-nav [this] (pred this))) (defnav ^{:doc "Navigates to the provided val if the structure is nil. Otherwise it stays navigated at the structure."} nil->val [v] (select* [this structure next-fn] (next-fn (if (nil? structure) v structure))) (transform* [this structure next-fn] (next-fn (if (nil? structure) v structure)))) (def ^{:doc "Navigates to #{} if the value is nil. Otherwise it stays navigated at the current value."} NIL->SET (nil->val #{})) (def ^{:doc "Navigates to '() if the value is nil. Otherwise it stays navigated at the current value."} NIL->LIST (nil->val '())) (def ^{:doc "Navigates to [] if the value is nil. Otherwise it stays navigated at the current value."} NIL->VECTOR (nil->val [])) (defnav ^{:doc "Navigates to the metadata of the structure, or nil if the structure has no metadata or may not contain metadata."} META [] (select* [this structure next-fn] (next-fn (meta structure))) (transform* [this structure next-fn] (with-meta structure (next-fn (meta structure))))) (defnav ^{:doc "Navigates to the name portion of the keyword or symbol"} NAME [] (select* [this structure next-fn] (next-fn (name structure))) (transform* [this structure next-fn] (let [new-name (next-fn (name structure)) ns (namespace structure)] (cond (keyword? structure) (keyword ns new-name) (symbol? structure) (symbol ns new-name) :else (i/throw-illegal "NAME can only be used on symbols or keywords - " structure) )))) (defnav ^{:doc "Navigates to the namespace portion of the keyword or symbol"} NAMESPACE [] (select* [this structure next-fn] (next-fn (namespace structure))) (transform* [this structure next-fn] (let [name (name structure) new-ns (next-fn (namespace structure))] (cond (keyword? structure) (keyword new-ns name) (symbol? structure) (symbol new-ns name) :else (i/throw-illegal "NAMESPACE can only be used on symbols or keywords - " structure) )))) (defdynamicnav ^{:doc "Adds the result of running select with the given path on the current value to the collected vals."} collect [& path] (late-bound-collector [late (late-path path)] (collect-val [this structure] (compiled-select late structure)))) (defdynamicnav ^{:doc "Adds the result of running select-one with the given path on the current value to the collected vals."} collect-one [& path] (late-bound-collector [late (late-path path)] (collect-val [this structure] (compiled-select-one late structure)))) (defcollector ^{:doc "Adds an external value to the collected vals. Useful when additional arguments are required to the transform function that would otherwise require partial application or a wrapper function. e.g., incrementing val at path [:a :b] by 3: (transform [:a :b (putval 3)] + some-map)"} putval [val] (collect-val [this structure] val)) (defdynamicnav ^{:doc "Continues navigating on the given path with the collected vals reset to []. Once navigation leaves the scope of with-fresh-collected, the collected vals revert to what they were before."} with-fresh-collected [& path] (late-bound-richnav [late (late-path path)] (select* [this vals structure next-fn] (i/exec-select* late [] structure (fn [_ structure] (next-fn vals structure))) ) (transform* [this vals structure next-fn] (i/exec-transform* late [] structure (fn [_ structure] (next-fn vals structure)))) )) (defrichnav ^{:doc "Drops all collected values for subsequent navigation."} DISPENSE [] (select* [this vals structure next-fn] (next-fn [] structure)) (transform* [this vals structure next-fn] (next-fn [] structure))) (defdynamicnav if-path "Like cond-path, but with if semantics." ([cond-p then-path] (if-path cond-p then-path STOP)) ([cond-p then-path else-path] (if-let [afn (n/extract-basic-filter-fn cond-p)] (late-bound-richnav [late-then (late-path then-path) late-else (late-path else-path)] (select* [this vals structure next-fn] (n/if-select vals structure next-fn afn late-then late-else)) (transform* [this vals structure next-fn] (n/if-transform vals structure next-fn afn late-then late-else))) (late-bound-richnav [late-cond (late-path cond-p) late-then (late-path then-path) late-else (late-path else-path)] (select* [this vals structure next-fn] (n/if-select vals structure next-fn #(n/selected?* late-cond vals %) late-then late-else)) (transform* [this vals structure next-fn] (n/if-transform vals structure next-fn #(n/selected?* late-cond vals %) late-then late-else)))))) (defdynamicnav cond-path "Takes in alternating cond-path path cond-path path... Tests the structure if selecting with cond-path returns anything. If so, it uses the following path for this portion of the navigation. Otherwise, it tries the next cond-path. If nothing matches, then the structure is not selected. The input paths may be parameterized, in which case the result of cond-path will be parameterized in the order of which the parameterized navigators were declared." [& conds] (let [pairs (reverse (partition 2 conds))] (reduce (fn [p [tester apath]] (if-path tester apath p)) STOP pairs))) (defdynamicnav multi-path "A path that branches on multiple paths. For updates, applies updates to the paths in order." ([] STAY) ([path] path) ([path1 path2] (late-bound-richnav [late1 (late-path path1) late2 (late-path path2)] (select* [this vals structure next-fn] (let [res1 (i/exec-select* late1 vals structure next-fn)] (if (reduced? res1) res1 (let [res2 (i/exec-select* late2 vals structure next-fn)] (if (identical? NONE res1) res2 res1 ))))) (transform* [this vals structure next-fn] (let [s1 (i/exec-transform* late1 vals structure next-fn)] (i/exec-transform* late2 vals s1 next-fn))))) ([path1 path2 & paths] (reduce multi-path (multi-path path1 path2) paths))) (defdynamicnav stay-then-continue "Navigates to the current element and then navigates via the provided path. This can be used to implement pre-order traversal." [& path] (multi-path STAY path)) (defdynamicnav continue-then-stay "Navigates to the provided path and then to the current element. This can be used to implement post-order traversal." [& path] (multi-path path STAY)) (def ^{:doc "Navigate the data structure until reaching a value for which `afn` returns truthy. Has same semantics as clojure.walk."} walker (recursive-path [afn] p (cond-path (pred afn) STAY coll? [ALL p] ))) (def ^{:doc "Like `walker` but maintains metadata of any forms traversed."} codewalker (recursive-path [afn] p (cond-path (pred afn) STAY coll? [ALL-WITH-META p] ))) specter-1.0.2/src/clj/com/rpl/specter/000077500000000000000000000000001311753541400175425ustar00rootroot00000000000000specter-1.0.2/src/clj/com/rpl/specter/impl.cljc000066400000000000000000000651111311753541400213440ustar00rootroot00000000000000(ns com.rpl.specter.impl #?(:cljs (:require-macros [com.rpl.specter.util-macros :refer [doseqres mk-comp-navs mk-late-fn mk-late-fn-records]])) ;; workaround for cljs bug that emits warnings for vars named the same as a ;; private var in cljs.core (in this case `NONE`, added as private var to ;; cljs.core with 1.9.562) #?(:cljs (:refer-clojure :exclude [NONE])) (:use [com.rpl.specter.protocols :only [select* transform* collect-val RichNavigator]] #?(:clj [com.rpl.specter.util-macros :only [doseqres mk-comp-navs]])) (:require [com.rpl.specter.protocols :as p] #?(:clj [clojure.pprint :as pp] :cljs [cljs.pprint :as pp]) [clojure.string :as s] [clojure.walk :as walk] #?(:clj [riddley.walk :as riddley])) #?(:clj (:import [com.rpl.specter Util MutableCell]))) (def NONE ::NONE) (defn spy [e] (println "SPY:") (println (pr-str e)) e) (defn- smart-str* [o] (if (coll? o) (pr-str o) (str o))) (defn ^String smart-str [& elems] (apply str (map smart-str* elems))) (defn fast-constantly [v] (fn ([] v) ([a1] v) ([a1 a2] v) ([a1 a2 a3] v) ([a1 a2 a3 a4] v) ([a1 a2 a3 a4 a5] v) ([a1 a2 a3 a4 a5 a6] v) ([a1 a2 a3 a4 a5 a6 a7] v) ([a1 a2 a3 a4 a5 a6 a7 a8] v) ([a1 a2 a3 a4 a5 a6 a7 a8 a9] v) ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10] v) ([a1 a2 a3 a4 a5 a6 a7 a8 a9 a10 & r] v))) #?(:clj (defmacro throw* [etype & args] `(throw (new ~etype (smart-str ~@args))))) #?( :clj (defmacro throw-illegal [& args] `(throw* IllegalArgumentException ~@args)) :cljs (defn throw-illegal [& args] (throw (js/Error. (apply str args))))) ;; need to get the expansion function like this so that ;; this code compiles in a clojure environment where cljs.analyzer ;; namespace does not exist #?( :clj (defn cljs-analyzer-macroexpand-1 [] (eval 'cljs.analyzer/macroexpand-1)) ;; this version is for bootstrap cljs :cljs (defn cljs-analyzer-macroexpand-1 [] ^:cljs.analyzer/no-resolve cljs.analyzer/macroexpand-1)) #?( :clj (defn clj-macroexpand-all [form] (riddley/macroexpand-all form)) :cljs (defn clj-macroexpand-all [form] (throw-illegal "not implemented"))) #?( :clj (defn intern* [ns name val] (intern ns name val)) :cljs (defn intern* [ns name val] (throw-illegal "intern not supported in ClojureScript"))) #?( :clj (defmacro fast-object-array [i] `(com.rpl.specter.Util/makeObjectArray ~i))) (defn benchmark [iters afn] (time (dotimes [_ iters] (afn)))) #?( :clj (defmacro exec-select* [this & args] (let [platform (if (contains? &env :locals) :cljs :clj) hinted (with-meta (gensym) {:tag 'com.rpl.specter.protocols.RichNavigator})] (if (= platform :cljs) `(p/select* ~this ~@args) `(let [~hinted ~this] (.select* ~hinted ~@args))))) :cljs (defn exec-select* [this vals structure next-fn] (p/select* ^not-native this vals structure next-fn))) #?( :clj (defmacro exec-transform* [this & args] (let [platform (if (contains? &env :locals) :cljs :clj) hinted (with-meta (gensym) {:tag 'com.rpl.specter.protocols.RichNavigator})] (if (= platform :cljs) `(p/transform* ~this ~@args) `(let [~hinted ~this] (.transform* ~hinted ~@args))))) :cljs (defn exec-transform* [this vals structure next-fn] (p/transform* ^not-native this vals structure next-fn))) (defprotocol PathComposer (do-comp-paths [paths])) (defn rich-nav? [n] #?(:clj (instance? com.rpl.specter.protocols.RichNavigator n) :cljs (satisfies? RichNavigator n))) (defn comp-paths* [p] (if (rich-nav? p) p (do-comp-paths p))) (defn- coerce-object [this] (cond (rich-nav? this) this (satisfies? p/ImplicitNav this) (p/implicit-nav this) :else (throw-illegal "Not a navigator: " this " " (pr-str (type this))))) (defprotocol CoercePath (coerce-path [this])) (extend-protocol CoercePath nil ; needs its own coercer because it doesn't count as an Object (coerce-path [this] (coerce-object this)) #?(:clj java.util.List :cljs cljs.core/PersistentVector) (coerce-path [this] (do-comp-paths this)) #?(:cljs cljs.core/IndexedSeq) #?(:cljs (coerce-path [this] (coerce-path (vec this)))) #?(:cljs cljs.core/EmptyList) #?(:cljs (coerce-path [this] (coerce-path (vec this)))) #?(:cljs cljs.core/List) #?(:cljs (coerce-path [this] (coerce-path (vec this)))) #?(:cljs cljs.core/LazySeq) #?(:cljs (coerce-path [this] (coerce-path (vec this)))) #?(:cljs cljs.core/Subvec) #?(:cljs (coerce-path [this] (coerce-path (vec this)))) #?(:clj Object :cljs default) (coerce-path [this] (coerce-object this))) (def STAY* (reify RichNavigator (select* [this vals structure next-fn] (next-fn vals structure)) (transform* [this vals structure next-fn] (next-fn vals structure)))) (defn combine-two-navs [nav1 nav2] (reify RichNavigator (select* [this vals structure next-fn] (exec-select* nav1 vals structure (fn [vals-next structure-next] (exec-select* nav2 vals-next structure-next next-fn)))) (transform* [this vals structure next-fn] (exec-transform* nav1 vals structure (fn [vals-next structure-next] (exec-transform* nav2 vals-next structure-next next-fn)))))) (extend-protocol PathComposer nil (do-comp-paths [o] (coerce-path o)) #?(:clj Object :cljs default) (do-comp-paths [o] (coerce-path o)) #?(:clj java.util.List :cljs cljs.core/PersistentVector) (do-comp-paths [navigators] (let [coerced (map coerce-path navigators)] (cond (empty? coerced) STAY* (= 1 (count coerced)) (first coerced) :else (reduce combine-two-navs coerced))))) ;; cell implementation idea taken from prismatic schema library #?(:cljs (defprotocol PMutableCell (set_cell [cell x]))) #?(:cljs (deftype MutableCell [^:volatile-mutable q] PMutableCell (set_cell [this x] (set! q x)))) #?( :clj (defn mutable-cell ([] (mutable-cell nil)) ([v] (MutableCell. v))) :cljs (defn mutable-cell ([] (mutable-cell nil)) ([init] (MutableCell. init)))) #?( :clj (defn set-cell! [^MutableCell c v] (.set c v)) :cljs (defn set-cell! [cell val] (set_cell cell val))) #?( :clj (defn get-cell [^MutableCell c] (.get c)) :cljs (defn get-cell [cell] (.-q cell))) (defn update-cell! [cell afn] (let [ret (afn (get-cell cell))] (set-cell! cell ret) ret)) #?( :clj (defmacro compiled-traverse-with-vals* [path result-fn vals structure] `(exec-select* ~path ~vals ~structure (fn [vals# structure#] (if (identical? vals# []) (~result-fn structure#) (~result-fn (conj vals# structure#)))))) :cljs (defn compiled-traverse-with-vals* [path result-fn vals structure] (exec-select* path vals structure (fn [vals structure] (if (identical? vals []) (result-fn structure) (result-fn (conj vals structure))))))) (defn compiled-traverse* [path result-fn structure] (compiled-traverse-with-vals* path result-fn [] structure)) (defn do-compiled-traverse* [apath structure] (reify #?(:clj clojure.lang.IReduce :cljs cljs.core/IReduce) (#?(:clj reduce :cljs -reduce) [this afn] (#?(:clj .reduce :cljs -reduce) this afn (afn))) (#?(:clj reduce :cljs -reduce) [this afn start] (let [cell (mutable-cell start)] (compiled-traverse* apath (fn [elem] (let [curr (get-cell cell) newv (afn curr elem)] (set-cell! cell newv) newv ; to support reduced handling during traverse )) structure) (get-cell cell) )))) (defn do-compiled-traverse [apath structure] (let [traverser (do-compiled-traverse* apath structure)] (reify #?(:clj clojure.lang.IReduce :cljs cljs.core/IReduce) (#?(:clj reduce :cljs -reduce) [this afn] (#?(:clj .reduce :cljs -reduce) this afn (afn))) (#?(:clj reduce :cljs -reduce) [this afn start] (let [res (#?(:clj .reduce :cljs -reduce) traverser afn start)] (unreduced res) ))))) (defn compiled-traverse-all* [path] (fn [xf] (fn ([] (xf)) ([result] (xf result)) ([result input] (reduce (fn [r i] (xf r i)) result ;; use this one to make sure reduced vals are propagated back (do-compiled-traverse* path input) ) )))) (defn compiled-select* [path structure] (let [res (mutable-cell (transient [])) result-fn (fn [structure] (let [curr (get-cell res)] (set-cell! res (conj! curr structure))))] (compiled-traverse* path result-fn structure) (persistent! (get-cell res)))) (defn compiled-select-one* [path structure] (let [res (mutable-cell NONE) result-fn (fn [structure] (let [curr (get-cell res)] (if (identical? curr NONE) (set-cell! res structure) (throw-illegal "More than one element found in structure: " structure))))] (compiled-traverse* path result-fn structure) (let [ret (get-cell res)] (if (identical? ret NONE) nil ret)))) (defn compiled-select-one!* [path structure] (let [res (mutable-cell NONE) result-fn (fn [structure] (let [curr (get-cell res)] (if (identical? curr NONE) (set-cell! res structure) (throw-illegal "More than one element found in structure: " structure))))] (compiled-traverse* path result-fn structure) (let [ret (get-cell res)] (if (identical? NONE ret) (throw-illegal "Found no elements for select-one! on " structure)) ret))) (defn compiled-select-any* ([path structure] (compiled-select-any* path [] structure)) ([path vals structure] (unreduced (compiled-traverse-with-vals* path reduced vals structure)))) (defn compiled-select-first* [path structure] (let [ret (compiled-select-any* path structure)] (if (identical? ret NONE) nil ret ))) (defn compiled-selected-any?* [path structure] (not (identical? NONE (compiled-select-any* path structure)))) (defn terminal* [afn vals structure] (if (identical? vals []) (afn structure) (apply afn (conj vals structure)))) ;;TODO: could inline cache the transform-fn, or even have a different one ;;if know there are no vals at the end (defn compiled-transform* [nav transform-fn structure] (exec-transform* nav [] structure (fn [vals structure] (terminal* transform-fn vals structure)))) (defn fn-invocation? [f] (or #?(:clj (instance? clojure.lang.Cons f)) #?(:clj (instance? clojure.lang.LazySeq f)) #?(:cljs (instance? cljs.core.LazySeq f)) (list? f))) (defrecord LocalSym [val sym]) (defrecord VarUse [val var sym]) (defrecord SpecialFormUse [val code]) (defrecord FnInvocation ;; op and params elems can be any of the above [op params code]) (defrecord DynamicVal [code]) (defrecord DynamicPath [path]) (defrecord DynamicFunction [op params code]) (defn dynamic-param? [o] (contains? #{DynamicPath DynamicVal DynamicFunction} (type o))) (defn static-path? [path] (if (sequential? path) (every? static-path? path) (-> path dynamic-param? not))) (defn late-path [path] (if (static-path? path) (comp-paths* path) (com.rpl.specter.impl/->DynamicPath path))) (defrecord CachedPathInfo [dynamic? precompiled]) ;; these are defined to avoid having to type-hint the CachedPathInfo ;; in com.rpl.specter/path, which causes problems during aot/uberjar ;; (clojure seems to be defining CachedPathInfo multiple times) (defn cached-path-info-precompiled [^CachedPathInfo c] (.-precompiled c)) (defn cached-path-info-dynamic? [^CachedPathInfo c] (.-dynamic? c)) (defn filter-select [afn vals structure next-fn] (if (afn structure) (next-fn vals structure) NONE)) (defn filter-transform [afn vals structure next-fn] (if (afn structure) (next-fn vals structure) structure)) (defn ^:direct-nav pred* [afn] (reify RichNavigator (select* [this vals structure next-fn] (if (afn structure) (next-fn vals structure) NONE)) (transform* [this vals structure next-fn] (if (afn structure) (next-fn vals structure) structure)))) (defn ^:direct-nav collected?* [afn] (reify RichNavigator (select* [this vals structure next-fn] (if (afn vals) (next-fn vals structure) NONE)) (transform* [this vals structure next-fn] (if (afn vals) (next-fn vals structure) structure)))) (defn ^:direct-nav cell-nav [cell] (reify RichNavigator (select* [this vals structure next-fn] (exec-select* (get-cell cell) vals structure next-fn)) (transform* [this vals structure next-fn] (exec-transform* (get-cell cell) vals structure next-fn)))) (defn local-declarepath [] (let [cell (mutable-cell nil)] (vary-meta (cell-nav cell) assoc ::cell cell))) (defn providepath* [declared compiled-path] (let [cell (-> declared meta ::cell)] (set-cell! cell compiled-path))) (defn- gensyms [amt] (vec (repeatedly amt gensym))) (mk-comp-navs) (defn srange-transform* [structure start end next-fn] (if (string? structure) (let [newss (next-fn (subs structure start end))] (str (subs structure 0 start) newss (subs structure end (count structure)) )) (let [structurev (vec structure) newpart (next-fn (-> structurev (subvec start end))) res (concat (subvec structurev 0 start) newpart (subvec structurev end (count structure)))] (if (vector? structure) (vec res) res )))) (defn- matching-indices [aseq p] (keep-indexed (fn [i e] (if (p e) i)) aseq)) (defn matching-ranges [aseq p] (first (reduce (fn [[ranges curr-start curr-last :as curr] i] (cond (nil? curr-start) [ranges i i] (= i (inc curr-last)) [ranges curr-start i] :else [(conj ranges [curr-start (inc curr-last)]) i i])) [[] nil nil] (concat (matching-indices aseq p) [-1])))) (defn continuous-subseqs-transform* [pred structure next-fn] (reduce (fn [structure [s e]] (srange-transform* structure s e next-fn)) structure (reverse (matching-ranges structure pred)))) (defn codewalk-until [pred on-match-fn structure] (if (pred structure) (on-match-fn structure) (let [ret (walk/walk (partial codewalk-until pred on-match-fn) identity structure)] (if (and (fn-invocation? structure) (fn-invocation? ret)) (with-meta ret (meta structure)) ret)))) (defn walk-select [pred continue-fn structure] (let [ret (mutable-cell NONE) walker (fn this [structure] (if (pred structure) (let [r (continue-fn structure)] (if-not (identical? r NONE) (set-cell! ret r)) r) (walk/walk this identity structure)))] (walker structure) (get-cell ret))) (defn walk-until [pred on-match-fn structure] (if (pred structure) (on-match-fn structure) (walk/walk (partial walk-until pred on-match-fn) identity structure))) #?(:clj (do (def ^:dynamic *tmp-closure*) (defn closed-code [closure body] (let [lv (mapcat #(vector % `(*tmp-closure* '~%)) (keys closure))] (binding [*tmp-closure* closure] (eval `(let [~@lv] ~body))))) (let [embeddable? (some-fn number? symbol? keyword? string? char? list? vector? set? #(and (map? %) (not (record? %))) nil? #(instance? clojure.lang.Cons %) #(instance? clojure.lang.LazySeq %))] (defn eval+ "Automatically extracts non-evalable stuff into a closure and then evals" [form] (let [replacements (mutable-cell {}) new-form (codewalk-until #(-> % embeddable? not) (fn [o] (let [s (gensym)] (update-cell! replacements #(assoc % s o)) s)) form) closure (get-cell replacements)] (closed-code closure new-form)))))) (defn coerce-nav [o] (cond #?(:clj (instance? com.rpl.specter.protocols.RichNavigator o) :cljs (satisfies? RichNavigator o)) o (sequential? o) (comp-paths* o) :else (p/implicit-nav o))) (defn dynamic-var? [v] (-> v meta :dynamic)) ;; original-obj stuff is done to avoid using functions with metadata on them ;; clojure's implementation of function metadata causes the function to do an ;; apply for every invocation (defn direct-nav-obj [o] (vary-meta o merge {:direct-nav true :original-obj o})) (defn maybe-direct-nav [obj direct-nav?] (if direct-nav? (direct-nav-obj obj) obj)) (defn original-obj [o] (let [orig (-> o meta :original-obj)] (if orig (recur orig) o))) (defn direct-nav? [o] (-> o meta :direct-nav)) (defn all-static? [params] (identical? NONE (walk-select dynamic-param? identity params))) (defn late-resolved-fn [afn] (fn [& args] (if (all-static? args) (apply afn args) (->DynamicFunction afn args nil) ))) (defn preserve-map [afn o] (if (or (list? o) (seq? o)) (map afn o) (into (empty o) (map afn o)))) (defn- magic-precompilation* [o] (cond (sequential? o) (preserve-map magic-precompilation* o) (instance? VarUse o) (if (dynamic-var? (:var o)) (->DynamicVal (maybe-direct-nav (:sym o) (or (-> o :var direct-nav?) (-> o :sym direct-nav?)))) (maybe-direct-nav (:val o) (or (-> o :var direct-nav?) (-> o :sym direct-nav?) (-> o :val direct-nav?)))) (instance? LocalSym o) (->DynamicVal (:sym o)) (instance? SpecialFormUse o) (->DynamicVal (:code o)) (instance? FnInvocation o) (let [op (magic-precompilation* (:op o)) params (doall (map magic-precompilation* (:params o)))] (if (or (-> op meta :dynamicnav) (all-static? (conj params op))) (magic-precompilation* (apply op params)) (->DynamicFunction op params (:code o)))) :else ;; this handles dynamicval as well o)) (defn static-combine ([o] (static-combine o true)) ([o nav-pos?] (cond (sequential? o) (if nav-pos? (let [res (continuous-subseqs-transform* rich-nav? (doall (map static-combine (flatten o))) (fn [s] [(comp-paths* s)]))] (if (= 1 (count res)) (first res) res)) (preserve-map #(static-combine % false) o)) (instance? DynamicFunction o) (->DynamicFunction (static-combine (:op o) false) (doall (map #(static-combine % false) (:params o))) (:code o)) (instance? DynamicPath o) (->DynamicPath (static-combine (:path o))) (instance? DynamicVal o) o :else (if nav-pos? (coerce-nav o) o)))) #?(:cljs (do (defprotocol LateResolve (late-resolve [this dynamic-params])) ;; one of the "possible params" (defrecord LocalParam [idx] LateResolve (late-resolve [this dynamic-params] (nth dynamic-params idx))) ;; statically precomputed (defrecord StaticParam [val] LateResolve (late-resolve [this dynamic-params] val)) (mk-late-fn-records) (mk-late-fn))) #?(:clj (defn static-fn-code [afn args] `(~afn ~@args)) :cljs (defn static-fn-code [afn args] (late-fn (->StaticParam afn) args))) #?(:clj (defn dynamic-fn-code [afn args] `(~afn ~@args)) :cljs (defn dynamic-fn-code [afn args] (late-fn afn args))) #?(:clj (defn dynamic-val-code [code possible-params] code) :cljs (defn dynamic-val-code [code possible-params] (let [[i] (keep-indexed (fn [i v] (if (= v code) i)) possible-params)] (if (nil? i) (throw-illegal "Could not find " code " in possible params " possible-params)) (maybe-direct-nav (->LocalParam i) (direct-nav? code))))) #?(:clj (defn static-val-code [o] o) :cljs (defn static-val-code [o] (maybe-direct-nav (->StaticParam o) (direct-nav? o)))) (declare resolve-nav-code) (defn dynamic->code [o] ;; works because both DynamicVal and DynamicFunction have :code field (walk-until dynamic-param? :code o)) (defn resolve-arg-code [o possible-params] (cond (instance? DynamicFunction o) (let [op (resolve-arg-code (:op o) possible-params) params (map #(resolve-arg-code % possible-params) (:params o))] (maybe-direct-nav (dynamic-fn-code (original-obj op) params) (direct-nav? (:op o)))) (instance? DynamicVal o) (dynamic-val-code (:code o) possible-params) (instance? DynamicPath o) (resolve-nav-code o possible-params) :else ;; handle dynamic params nested inside data structures ;; e.g. (terminal-val [v]) (if (identical? NONE (walk-select dynamic-param? identity o)) (static-val-code o) ;; done this way so it's compatible with cljs as well (since this dynamic val will be ;; a possible param) (resolve-arg-code (->DynamicVal (dynamic->code o)) possible-params) ))) (defn resolve-nav-code [o possible-params] (cond (instance? DynamicPath o) (let [path (:path o)] (if (sequential? path) (let [resolved (vec (map #(resolve-nav-code % possible-params) path))] (cond (empty? resolved) (static-val-code STAY*) (= 1 (count resolved)) (first resolved) :else (static-fn-code comp-navs resolved))) (resolve-nav-code path possible-params))) (instance? DynamicVal o) (let [code (:code o) d (dynamic-val-code code possible-params)] (cond (direct-nav? code) d (or (set? code) (and (fn-invocation? code) (= 'fn* (first code)))) (static-fn-code pred* [d]) :else (static-fn-code coerce-nav [d]))) (instance? DynamicFunction o) (let [res (resolve-arg-code o possible-params)] (if (direct-nav? res) res (static-fn-code coerce-nav [res]))) :else (static-val-code (coerce-nav o)))) (defn used-locals [locals-set form] (let [used-locals-cell (mutable-cell [])] (walk/postwalk (fn [e] (if (contains? locals-set e) (update-cell! used-locals-cell #(conj % e)) e)) form) (get-cell used-locals-cell))) (def ^:dynamic *DEBUG-INLINE-CACHING* false) #?(:cljs (defn mk-fn-name-strs [o] (walk/postwalk (fn [e] (if (fn? e) (re-find #" .*" (pr-str e)) e)) o))) #?(:clj (defn mk-dynamic-path-maker [resolved-code ns-str used-locals-list possible-param] (let [code `(fn [~@used-locals-list] ~resolved-code) ns (find-ns (symbol ns-str))] (when *DEBUG-INLINE-CACHING* (println "Produced code:") (pp/pprint code) (println)) (binding [*ns* ns] (eval+ code)))) :cljs (defn mk-dynamic-path-maker [resolved-code ns-str used-locals-list possible-params] (when *DEBUG-INLINE-CACHING* (println "Possible params:") (println possible-params) (println "\nProduced dynamic object:") (pp/pprint (mk-fn-name-strs resolved-code)) (println)) (fn [dynamic-params] (late-resolve resolved-code dynamic-params)))) ;; TODO: could have a global flag about whether or not to compile and cache static ;; portions, or whether to compile everything together on each invocation (so that ;; it can be redefined in repl ;; could have three levels: ;; 1. NO-COERCION (never allow coerce-nav at runtime) ;; 2. REGULAR (allow coerce-nav at runtime, cache static parts together) ;; 3. REDEFINABLE-VARS (don't cache static parts together) (defn magic-precompilation [path ns-str used-locals-list possible-params] (let [magic-path (-> path magic-precompilation* static-combine)] (when *DEBUG-INLINE-CACHING* (println "Inline caching debug information") (println "--------------------------------") (println "Input path:" path "\n") (println "Processed path:" magic-path "\n")) (if (rich-nav? magic-path) (do (when *DEBUG-INLINE-CACHING* (println "Static result:" magic-path)) (->CachedPathInfo false magic-path)) (let [maker (mk-dynamic-path-maker (resolve-nav-code (->DynamicPath magic-path) possible-params) ns-str used-locals-list possible-params)] (->CachedPathInfo true maker))))) (defn compiled-setval* [path val structure] (compiled-transform* path (fast-constantly val) structure)) (defn compiled-replace-in* [path transform-fn structure & {:keys [merge-fn] :or {merge-fn concat}}] (let [state (mutable-cell nil)] [(compiled-transform* path (fn [& args] (let [res (apply transform-fn args)] (if res (let [[ret user-ret] res] (->> user-ret (merge-fn (get-cell state)) (set-cell! state)) ret) (last args)))) structure) (get-cell state)])) (defn- multi-transform-error-fn [& nav] (throw-illegal "All navigation in multi-transform must end in 'terminal' " "navigators. Instead navigated to: " nav)) (defn compiled-multi-transform* [path structure] (compiled-transform* path multi-transform-error-fn structure)) specter-1.0.2/src/clj/com/rpl/specter/macros.clj000066400000000000000000000032451311753541400215240ustar00rootroot00000000000000(ns com.rpl.specter.macros (:use [com.rpl.specter.protocols :only [RichNavigator]]) (:require [com.rpl.specter.impl :as i])) (defn- determine-params-impls [impls] (let [grouped (->> impls (map (fn [[n & body]] [n body])) (into {}))] (if-not (= #{'select* 'transform*} (-> grouped keys set)) (i/throw-illegal "defnav must implement select* and transform*, instead got " (keys grouped))) grouped)) (defmacro richnav [params & impls] (if (empty? params) `(reify RichNavigator ~@impls) `(i/direct-nav-obj (fn ~params (reify RichNavigator ~@impls))))) (defmacro nav [params & impls] (let [{[[_ s-structure-sym s-next-fn-sym] & s-body] 'select* [[_ t-structure-sym t-next-fn-sym] & t-body] 'transform*} (determine-params-impls impls)] `(richnav ~params (~'select* [this# vals# ~s-structure-sym next-fn#] (let [~s-next-fn-sym (fn [s#] (next-fn# vals# s#))] ~@s-body)) (~'transform* [this# vals# ~t-structure-sym next-fn#] (let [~t-next-fn-sym (fn [s#] (next-fn# vals# s#))] ~@t-body))))) (defn- helper-name [name method-name] (with-meta (symbol (str name "-" method-name)) {:no-doc true})) (defmacro defnav [name params & impls] ;; remove the "this" param for the helper (let [helpers (for [[mname [_ & mparams] & mbody] impls] `(defn ~(helper-name name mname) [~@params ~@mparams] ~@mbody)) decls (for [[mname & _] impls] `(declare ~(helper-name name mname)))] `(do ~@decls ~@helpers (def ~name (nav ~params ~@impls))))) (defmacro defrichnav [name params & impls] `(def ~name (richnav ~params ~@impls))) specter-1.0.2/src/clj/com/rpl/specter/navs.cljc000066400000000000000000000442211311753541400213510ustar00rootroot00000000000000(ns com.rpl.specter.navs #?(:cljs (:require-macros [com.rpl.specter :refer [defnav defrichnav]] [com.rpl.specter.util-macros :refer [doseqres]])) (:use #?(:clj [com.rpl.specter.macros :only [defnav defrichnav]]) #?(:clj [com.rpl.specter.util-macros :only [doseqres]])) (:require [com.rpl.specter.impl :as i] #?(:clj [clojure.core.reducers :as r]))) (defn not-selected?* [compiled-path vals structure] (->> structure (i/compiled-select-any* compiled-path vals) (identical? i/NONE))) (defn selected?* [compiled-path vals structure] (not (not-selected?* compiled-path vals structure))) (defn all-select [structure next-fn] (doseqres i/NONE [e structure] (next-fn e))) #?( :clj (defn queue? [coll] (instance? clojure.lang.PersistentQueue coll)) :cljs (defn queue? [coll] (= (type coll) (type #queue [])))) (defprotocol AllTransformProtocol (all-transform [structure next-fn])) (defn void-transformed-kv-pair? [newkv] (or (identical? newkv i/NONE) (< (count newkv) 2))) (defn- non-transient-map-all-transform [structure next-fn empty-map] (reduce-kv (fn [m k v] (let [newkv (next-fn [k v])] (if (void-transformed-kv-pair? newkv) m (assoc m (nth newkv 0) (nth newkv 1))))) empty-map structure)) (defn not-NONE? [v] (-> v (identical? i/NONE) not)) (defn- all-transform-list [structure next-fn] (doall (sequence (comp (map next-fn) (filter not-NONE?)) structure))) (defn- all-transform-record [structure next-fn] (reduce (fn [res kv] (conj res (next-fn kv))) structure structure )) (extend-protocol AllTransformProtocol nil (all-transform [structure next-fn] nil) ;; in cljs they're PersistentVector so don't need a special case #?(:clj clojure.lang.MapEntry) #?(:clj (all-transform [structure next-fn] (let [newk (next-fn (key structure)) newv (next-fn (val structure))] (clojure.lang.MapEntry. newk newv)))) #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) (all-transform [structure next-fn] (into [] (comp (map next-fn) (filter not-NONE?)) structure)) #?(:clj clojure.lang.PersistentHashSet :cljs cljs.core/PersistentHashSet) (all-transform [structure next-fn] (into #{} (comp (map next-fn) (filter not-NONE?)) structure)) #?(:clj clojure.lang.PersistentArrayMap) #?(:clj (all-transform [structure next-fn] (let [k-it (.keyIterator structure) v-it (.valIterator structure) none-cell (i/mutable-cell 0) len (.count structure) array (i/fast-object-array (* 2 len))] (loop [i 0 j 0] (if (.hasNext k-it) (let [k (.next k-it) v (.next v-it) newkv (next-fn [k v])] (if (void-transformed-kv-pair? newkv) (do (i/update-cell! none-cell inc) (recur (+ i 2) j)) (do (aset array j (nth newkv 0)) (aset array (inc j) (nth newkv 1)) (recur (+ i 2) (+ j 2))))))) (let [none-count (i/get-cell none-cell) array (if (not= 0 none-count) (java.util.Arrays/copyOf array (int (* 2 (- len none-count)))) array )] (clojure.lang.PersistentArrayMap/createAsIfByAssoc array))))) #?(:cljs cljs.core/PersistentArrayMap) #?(:cljs (all-transform [structure next-fn] (non-transient-map-all-transform structure next-fn {}))) #?(:clj clojure.lang.PersistentTreeMap :cljs cljs.core/PersistentTreeMap) (all-transform [structure next-fn] (non-transient-map-all-transform structure next-fn (empty structure))) #?(:clj clojure.lang.IRecord) #?(:clj (all-transform [structure next-fn] (all-transform-record structure next-fn))) #?(:clj clojure.lang.PersistentHashMap :cljs cljs.core/PersistentHashMap) (all-transform [structure next-fn] (persistent! (reduce-kv (fn [m k v] (let [newkv (next-fn [k v])] (if (void-transformed-kv-pair? newkv) m (assoc! m (nth newkv 0) (nth newkv 1))))) (transient #?(:clj clojure.lang.PersistentHashMap/EMPTY :cljs cljs.core.PersistentHashMap.EMPTY)) structure))) #?(:clj Object) #?(:clj (all-transform [structure next-fn] (let [empty-structure (empty structure)] (cond (and (list? empty-structure) (not (queue? empty-structure))) (all-transform-list structure next-fn) (map? structure) ;; reduce-kv is much faster than doing r/map through call to (into ...) (reduce-kv (fn [m k v] (let [newkv (next-fn [k v])] (if (void-transformed-kv-pair? newkv) m (assoc m (nth newkv 0) (nth newkv 1))))) empty-structure structure) :else (->> structure (r/map next-fn) (r/filter not-NONE?) (into empty-structure)))))) #?(:cljs default) #?(:cljs (all-transform [structure next-fn] (if (record? structure) ;; this case is solely for cljs since extending to IRecord doesn't work for cljs (all-transform-record structure next-fn) (let [empty-structure (empty structure)] (cond (and (list? empty-structure) (not (queue? empty-structure))) (all-transform-list structure next-fn) (map? structure) (reduce-kv (fn [m k v] (let [newkv (next-fn [k v])] (if (void-transformed-kv-pair? newkv) m (assoc m (nth newkv 0) (nth newkv 1))))) empty-structure structure) :else (into empty-structure (comp (map next-fn) (filter not-NONE?)) structure))))))) (defprotocol MapTransformProtocol (map-vals-transform [structure next-fn]) (map-keys-transform [structure next-fn]) ) (defn map-vals-non-transient-transform [structure empty-map next-fn] (reduce-kv (fn [m k v] (let [newv (next-fn v)] (if (identical? newv i/NONE) m (assoc m k newv)))) empty-map structure)) (defn map-keys-non-transient-transform [structure empty-map next-fn] (reduce-kv (fn [m k v] (let [newk (next-fn k)] (if (identical? newk i/NONE) m (assoc m newk v)))) empty-map structure)) (extend-protocol MapTransformProtocol nil (map-vals-transform [structure next-fn] nil) (map-keys-transform [structure next-fn] nil) #?(:clj clojure.lang.PersistentArrayMap) #?(:clj (map-vals-transform [structure next-fn] (let [k-it (.keyIterator structure) v-it (.valIterator structure) none-cell (i/mutable-cell 0) len (.count structure) array (i/fast-object-array (* 2 len))] (loop [i 0 j 0] (if (.hasNext k-it) (let [k (.next k-it) v (.next v-it) newv (next-fn v)] (if (identical? newv i/NONE) (do (i/update-cell! none-cell inc) (recur (+ i 2) j)) (do (aset array j k) (aset array (inc j) newv) (recur (+ i 2) (+ j 2))))))) (let [none-count (i/get-cell none-cell) array (if (not= 0 none-count) (java.util.Arrays/copyOf array (int (* 2 (- len none-count)))) array )] (clojure.lang.PersistentArrayMap. array))))) #?(:clj (map-keys-transform [structure next-fn] (let [k-it (.keyIterator structure) v-it (.valIterator structure) none-cell (i/mutable-cell 0) len (.count structure) array (i/fast-object-array (* 2 len))] (loop [i 0 j 0] (if (.hasNext k-it) (let [k (.next k-it) v (.next v-it) newk (next-fn k)] (if (identical? newk i/NONE) (do (i/update-cell! none-cell inc) (recur (+ i 2) j)) (do (aset array j newk) (aset array (inc j) v) (recur (+ i 2) (+ j 2))))))) (let [none-count (i/get-cell none-cell) array (if (not= 0 none-count) (java.util.Arrays/copyOf array (int (* 2 (- len none-count)))) array )] (clojure.lang.PersistentArrayMap/createAsIfByAssoc array))))) #?(:cljs cljs.core/PersistentArrayMap) #?(:cljs (map-vals-transform [structure next-fn] (map-vals-non-transient-transform structure {} next-fn))) #?(:cljs (map-keys-transform [structure next-fn] (map-keys-non-transient-transform structure {} next-fn))) #?(:clj clojure.lang.PersistentTreeMap :cljs cljs.core/PersistentTreeMap) (map-vals-transform [structure next-fn] (map-vals-non-transient-transform structure (empty structure) next-fn)) (map-keys-transform [structure next-fn] (map-keys-non-transient-transform structure (empty structure) next-fn)) #?(:clj clojure.lang.PersistentHashMap :cljs cljs.core/PersistentHashMap) (map-vals-transform [structure next-fn] (persistent! (reduce-kv (fn [m k v] (let [newv (next-fn v)] (if (identical? newv i/NONE) m (assoc! m k newv)))) (transient #?(:clj clojure.lang.PersistentHashMap/EMPTY :cljs cljs.core.PersistentHashMap.EMPTY)) structure))) (map-keys-transform [structure next-fn] (persistent! (reduce-kv (fn [m k v] (let [newk (next-fn k)] (if (identical? newk i/NONE) m (assoc! m newk v)))) (transient #?(:clj clojure.lang.PersistentHashMap/EMPTY :cljs cljs.core.PersistentHashMap.EMPTY)) structure))) #?(:clj Object :cljs default) (map-vals-transform [structure next-fn] (reduce-kv (fn [m k v] (let [newv (next-fn v)] (if (identical? newv i/NONE) m (assoc m k newv)))) (empty structure) structure)) (map-keys-transform [structure next-fn] (reduce-kv (fn [m k v] (let [newk (next-fn k)] (if (identical? newk i/NONE) m (assoc m newk v)))) (empty structure) structure))) (defn srange-select [structure start end next-fn] (next-fn (if (string? structure) (subs structure start end) (-> structure vec (subvec start end)) ))) (def srange-transform i/srange-transform*) (defn extract-basic-filter-fn [path] (cond (fn? path) path (and (coll? path) (every? fn? path)) (reduce (fn [combined afn] (fn [structure] (and (combined structure) (afn structure)))) path))) (defn if-select [vals structure next-fn then-tester then-nav else-nav] (i/exec-select* (if (then-tester structure) then-nav else-nav) vals structure next-fn)) (defn if-transform [vals structure next-fn then-tester then-nav else-nav] (i/exec-transform* (if (then-tester structure) then-nav else-nav) vals structure next-fn)) (defprotocol AddExtremes (append-all [structure elements]) (prepend-all [structure elements]) (append-one [structure elem]) (prepend-one [structure elem]) ) (extend-protocol AddExtremes nil (append-all [_ elements] elements) (prepend-all [_ elements] elements) (append-one [_ elem] (list elem)) (prepend-one [_ elem] (list elem)) #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) (append-all [structure elements] (reduce conj structure elements)) (prepend-all [structure elements] (let [ret (transient [])] (as-> ret <> (reduce conj! <> elements) (reduce conj! <> structure) (persistent! <>)))) (append-one [structure elem] (conj structure elem)) (prepend-one [structure elem] (into [elem] structure)) #?(:clj Object :cljs default) (append-all [structure elements] (concat structure elements)) (prepend-all [structure elements] (concat elements structure)) (append-one [structure elem] (concat structure [elem])) (prepend-one [structure elem] (cons elem structure)) ) (defprotocol UpdateExtremes (update-first [s afn]) (update-last [s afn])) (defprotocol GetExtremes (get-first [s]) (get-last [s])) (defprotocol FastEmpty (fast-empty? [s])) (defnav PosNavigator [getter updater] (select* [this structure next-fn] (if-not (fast-empty? structure) (next-fn (getter structure)) i/NONE)) (transform* [this structure next-fn] (if (fast-empty? structure) structure (updater structure next-fn)))) (defn- update-first-list [l afn] (let [newf (afn (first l)) restl (rest l)] (if (identical? i/NONE newf) restl (cons newf restl)))) (defn- update-last-list [l afn] (let [lastl (afn (last l)) bl (butlast l)] (if (identical? i/NONE lastl) (if (nil? bl) '() bl) (concat bl [lastl])))) #?( :clj (defn vec-count [^clojure.lang.IPersistentVector v] (.length v)) :cljs (defn vec-count [v] (count v))) #?( :clj (defn transient-vec-count [^clojure.lang.ITransientVector v] (.count v)) :cljs (defn transient-vec-count [v] (count v))) (extend-protocol UpdateExtremes #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) (update-first [v afn] (let [val (nth v 0) newv (afn val)] (if (identical? i/NONE newv) (subvec v 1) (assoc v 0 newv) ))) (update-last [v afn] ;; type-hinting vec-count to ^int caused weird errors with case (let [c (int (vec-count v))] (case c 1 (let [[e] v newe (afn e)] (if (identical? i/NONE newe) [] [newe])) 2 (let [[e1 e2] v newe (afn e2)] (if (identical? i/NONE newe) [e1] [e1 newe])) (let [i (dec c) newe (afn (nth v i))] (if (identical? i/NONE newe) (pop v) (assoc v i newe)))))) #?(:clj String :cljs string) (update-first [s afn] (let [rests (subs s 1 (count s)) newb (afn (nth s 0))] (if (identical? i/NONE newb) rests (str newb rests)))) (update-last [s afn] (let [last-idx (-> s count dec) newl (afn (nth s last-idx)) begins (subs s 0 last-idx)] (if (identical? i/NONE newl) begins (str begins newl) ))) #?(:clj Object :cljs default) (update-first [l val] (update-first-list l val)) (update-last [l val] (update-last-list l val))) (extend-protocol GetExtremes #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) (get-first [v] (nth v 0)) (get-last [v] (peek v)) #?(:clj Object :cljs default) (get-first [s] (first s)) (get-last [s] (last s)) #?(:clj String :cljs string) (get-first [s] (nth s 0)) (get-last [s] (nth s (-> s count dec)) )) (extend-protocol FastEmpty nil (fast-empty? [_] true) #?(:clj clojure.lang.IPersistentVector :cljs cljs.core/PersistentVector) (fast-empty? [v] (= 0 (vec-count v))) #?(:clj clojure.lang.ITransientVector :cljs cljs.core/TransientVector) (fast-empty? [v] (= 0 (transient-vec-count v))) #?(:clj Object :cljs default) (fast-empty? [s] (empty? s))) (defn- do-keypath-transform [vals structure key next-fn] (let [newv (next-fn vals (get structure key))] (if (identical? newv i/NONE) (if (sequential? structure) (i/srange-transform* structure key (inc key) (fn [_] [])) (dissoc structure key)) (assoc structure key newv)))) (defrichnav ^{:doc "Navigates to the specified key, navigating to nil if it does not exist. Setting the value to NONE will remove it from the collection."} keypath* [key] (select* [this vals structure next-fn] (next-fn vals (get structure key))) (transform* [this vals structure next-fn] (do-keypath-transform vals structure key next-fn) )) (defrichnav ^{:doc "Navigates to the key only if it exists in the map. Setting the value to NONE will remove it from the collection."} must* [k] (select* [this vals structure next-fn] (if (contains? structure k) (next-fn vals (get structure k)) i/NONE)) (transform* [this vals structure next-fn] (if (contains? structure k) (do-keypath-transform vals structure k next-fn) structure))) (defrichnav nthpath* ^{:doc "Navigates to the given position in the sequence. Setting the value to NONE will remove it from the sequence. Works for all sequence types."} [i] (select* [this vals structure next-fn] (next-fn vals (nth structure i))) (transform* [this vals structure next-fn] (if (vector? structure) (let [newv (next-fn vals (nth structure i))] (if (identical? newv i/NONE) (i/srange-transform* structure i (inc i) (fn [_] [])) (assoc structure i newv))) (i/srange-transform* ; can make this much more efficient with alternate impl structure i (inc i) (fn [[e]] (let [v (next-fn vals e)] (if (identical? v i/NONE) [] [v]) )))))) (defrecord SrangeEndFunction [end-fn]) ;; done this way to maintain backwards compatibility (defn invoke-end-fn [end-fn structure start] (if (instance? SrangeEndFunction end-fn) ((:end-fn end-fn) structure start) (end-fn structure) )) specter-1.0.2/src/clj/com/rpl/specter/protocols.cljc000066400000000000000000000020101311753541400224140ustar00rootroot00000000000000(ns com.rpl.specter.protocols) (defprotocol RichNavigator "Do not use this protocol directly. All navigators must be created using macros in com.rpl.specter namespace." (select* [this vals structure next-fn] "An implementation of `select*` must call `next-fn` on each subvalue of `structure`. The result of `select*` is specified as follows: 1. `NONE` if `next-fn` never called 2. `NONE` if all calls to `next-fn` return `NONE` 3. Otherwise, any non-`NONE` return value from calling `next-fn` ") (transform* [this vals structure next-fn] "An implementation of `transform*` must use `next-fn` to transform any subvalues of `structure` and then merge those transformed values back into `structure`. Everything else in `structure` must be unchanged.")) (defprotocol Collector "Do not use this protocol directly. All navigators must be created using macros in com.rpl.specter namespace." (collect-val [this structure])) (defprotocol ImplicitNav (implicit-nav [obj])) specter-1.0.2/src/clj/com/rpl/specter/transients.cljc000066400000000000000000000052531311753541400225760ustar00rootroot00000000000000(ns com.rpl.specter.transients #?(:cljs (:require-macros [com.rpl.specter :refer [defnav]])) (:use #?(:clj [com.rpl.specter :only [defnav]])) (:require [com.rpl.specter.navs :as n] [com.rpl.specter :refer [subselect selected?]])) (defnav ^{:doc "Navigates to the specified key of a transient collection, navigating to nil if it doesn't exist."} keypath! [key] (select* [this structure next-fn] (next-fn (get structure key))) (transform* [this structure next-fn] (assoc! structure key (next-fn (get structure key))))) (defnav ^{:doc "Navigates to an empty (persistent) vector at the end of a transient vector."} END! [] (select* [this structure next-fn] (next-fn [])) (transform* [this structure next-fn] (let [res (next-fn [])] (reduce conj! structure res)))) (defn- t-get-first [tv] (nth tv 0)) (defn- t-get-last [tv] (nth tv (dec (n/transient-vec-count tv)))) (defn- t-update-first [tv next-fn] (assoc! tv 0 (next-fn (nth tv 0)))) (defn- t-update-last [tv next-fn] (let [i (dec (n/transient-vec-count tv))] (assoc! tv i (next-fn (nth tv i))))) (def FIRST! "Navigates to the first element of a transient vector." (n/PosNavigator t-get-first t-update-first)) (def LAST! "Navigates to the last element of a transient vector." (n/PosNavigator t-get-last t-update-last)) #?( :clj (defn- select-keys-from-transient-map "Selects keys from transient map, because built-in select-keys uses `find` which is unsupported." [m m-keys] (loop [result {} m-keys m-keys] (if-not (seq m-keys) result (let [k (first m-keys) ;; support Clojure 1.6 where contains? is broken on transients item (get m k ::not-found)] (recur (if-not (identical? item ::not-found) (assoc result k item) result) (rest m-keys)))))) :cljs (defn- select-keys-from-transient-map "Uses select-keys on a transient map." [m m-keys] (select-keys m m-keys))) (defnav ^{:doc "Navigates to the specified persistent submap of a transient map."} submap! [m-keys] (select* [this structure next-fn] (next-fn (select-keys-from-transient-map structure m-keys))) (transform* [this structure next-fn] (let [selected (select-keys-from-transient-map structure m-keys) res (next-fn selected)] (as-> structure % (reduce (fn [m k] (dissoc! m k)) % m-keys) (reduce-kv (fn [m k v] (assoc! m k v)) % res))))) specter-1.0.2/src/clj/com/rpl/specter/util_macros.clj000066400000000000000000000037761311753541400225720ustar00rootroot00000000000000(ns com.rpl.specter.util-macros) (defmacro doseqres [backup-res [n aseq] & body] `(reduce (fn [curr# ~n] (let [ret# (do ~@body)] (if (identical? ret# ~backup-res) curr# (if (reduced? ret#) (reduced ret#) ret#)))) ~backup-res ~aseq)) (defn- gensyms [amt] (vec (repeatedly amt gensym))) (defmacro mk-comp-navs [] (let [impls (for [i (range 3 20)] (let [[fsym & rsyms :as syms] (gensyms i)] `([~@syms] (~'comp-navs ~fsym (~'comp-navs ~@rsyms))))) last-syms (gensyms 19)] `(defn ~'comp-navs ([] ~'com.rpl.specter.impl/STAY*) ([nav1#] nav1#) ([nav1# nav2#] (~'com.rpl.specter.impl/combine-two-navs nav1# nav2#)) ~@impls ([~@last-syms ~'& rest#] (~'comp-navs (~'comp-navs ~@last-syms) (reduce ~'comp-navs rest#)))))) ;;TODO: move these definitions somewhere else (defn late-fn-record-name [i] (symbol (str "LateFn" i))) (defn late-fn-record-constructor-name [i] (symbol (str "->LateFn" i))) (defn- mk-late-fn-record [i] (let [fields (concat ['fn] (for [j (range i)] (symbol (str "arg" j)))) dparams (gensym "dynamic-params") resolvers (for [f fields] `(~'late-resolve ~f ~dparams))] `(defrecord ~(late-fn-record-name i) [~@fields] ~'LateResolve (~'late-resolve [this# ~dparams] (~@resolvers))))) (defmacro mk-late-fn-records [] (let [impls (for [i (range 20)] (mk-late-fn-record i))] `(do ~@impls))) (defmacro mk-late-fn [] (let [f (gensym "afn") args (gensym "args") cases (for [i (range 19)] [i (let [gets (for [j (range i)] `(nth ~args ~j))] `(~(late-fn-record-constructor-name i) ~f ~@gets))])] `(defn ~'late-fn [~f ~args] (case (count ~args) ~@(apply concat cases) (com.rpl.specter.impl/throw-illegal "Cannot have late function with more than 18 args"))))) specter-1.0.2/src/clj/com/rpl/specter/zipper.cljc000066400000000000000000000077531311753541400217240ustar00rootroot00000000000000(ns com.rpl.specter.zipper #?(:cljs (:require-macros [com.rpl.specter :refer [defnav nav declarepath providepath recursive-path]])) #?(:clj (:use [com.rpl.specter :only [defnav nav declarepath providepath recursive-path]])) (:require [com.rpl.specter :as s] [clojure.zip :as zip])) (defnav zipper [constructor] (select* [this structure next-fn] (next-fn (constructor structure))) (transform* [this structure next-fn] (zip/root (next-fn (constructor structure))))) (def VECTOR-ZIP (zipper zip/vector-zip)) (def SEQ-ZIP (zipper zip/seq-zip)) (def XML-ZIP (zipper zip/xml-zip)) (def ^{:doc "Navigate to the next element in the structure. If no next element, works like STOP."} NEXT (s/comp-paths (s/view zip/next) (s/if-path zip/end? s/STOP s/STAY))) (defn- mk-zip-nav [znav] (nav [] (select* [this structure next-fn] (let [ret (znav structure)] (if ret (next-fn ret)))) (transform* [this structure next-fn] (let [ret (znav structure)] (if ret (next-fn ret) structure))))) ;; (multi-path RIGHT LEFT) will not navigate to the right and left ;; of the currently navigated element because locations aren't stable ;; like they are for maps/graphs. The path following RIGHT could ;; insert lots of elements all over the sequence, and there's no ;; way to determine how to get "back". (def ^{:doc "Navigate to the element to the right. If no element there, works like STOP."} RIGHT (mk-zip-nav zip/right)) (def ^{:doc "Navigate to the element to the left. If no element there, works like STOP."} LEFT (mk-zip-nav zip/left)) (def DOWN (mk-zip-nav zip/down)) (def UP (mk-zip-nav zip/up)) (def ^{:doc "Navigate to the previous element. If this is the first element, works like STOP."} PREV (mk-zip-nav zip/prev)) (def RIGHTMOST (s/view zip/rightmost)) (def LEFTMOST (s/view zip/leftmost)) (defn- inner-insert [structure next-fn inserter mover backer] (let [to-insert (next-fn []) inserts (reduce (fn [z e] (-> z (inserter e) mover)) structure to-insert)] (if backer (reduce (fn [z _] (backer z)) inserts to-insert) inserts))) (defnav ^{:doc "Navigate to the empty subsequence directly to the right of this element."} INNER-RIGHT [] (select* [this structure next-fn] (next-fn [])) (transform* [this structure next-fn] (inner-insert structure next-fn zip/insert-right zip/right zip/left))) (defnav ^{:doc "Navigate to the empty subsequence directly to the left of this element."} INNER-LEFT [] (select* [this structure next-fn] (next-fn [])) (transform* [this structure next-fn] (inner-insert structure next-fn zip/insert-left identity nil))) (defnav NODE [] (select* [this structure next-fn] (next-fn (zip/node structure))) (transform* [this structure next-fn] (zip/edit structure next-fn))) (defnav ^{:doc "Navigate to the subsequence containing only the node currently pointed to. This works just like srange and can be used to remove elements from the structure"} NODE-SEQ [] (select* [this structure next-fn] (next-fn [(zip/node structure)])) (transform* [this structure next-fn] (let [to-insert (next-fn [(zip/node structure)]) inserted (reduce zip/insert-left structure to-insert)] (zip/remove inserted)))) (def ^{:doc "Navigate the zipper to the first element in the structure matching predfn. A linear scan is done using NEXT to find the element."} find-first (recursive-path [predfn] p (s/if-path [NODE (s/pred predfn)] s/STAY [NEXT p]))) (declarepath ^{:doc "Navigate to every element reachable using calls to NEXT"} NEXT-WALK) (providepath NEXT-WALK (s/stay-then-continue NEXT NEXT-WALK)) specter-1.0.2/src/java/000077500000000000000000000000001311753541400146735ustar00rootroot00000000000000specter-1.0.2/src/java/com/000077500000000000000000000000001311753541400154515ustar00rootroot00000000000000specter-1.0.2/src/java/com/rpl/000077500000000000000000000000001311753541400162465ustar00rootroot00000000000000specter-1.0.2/src/java/com/rpl/specter/000077500000000000000000000000001311753541400177135ustar00rootroot00000000000000specter-1.0.2/src/java/com/rpl/specter/MutableCell.java000066400000000000000000000003371311753541400227520ustar00rootroot00000000000000package com.rpl.specter; public class MutableCell { private Object o; public MutableCell(Object o) { this.o = o; } public Object get() { return o; } public void set(Object o) { this.o = o; } } specter-1.0.2/src/java/com/rpl/specter/Util.java000066400000000000000000000002061311753541400214710ustar00rootroot00000000000000package com.rpl.specter; public class Util { public static Object[] makeObjectArray(int size) { return new Object[size]; } } specter-1.0.2/test/000077500000000000000000000000001311753541400141425ustar00rootroot00000000000000specter-1.0.2/test/com/000077500000000000000000000000001311753541400147205ustar00rootroot00000000000000specter-1.0.2/test/com/rpl/000077500000000000000000000000001311753541400155155ustar00rootroot00000000000000specter-1.0.2/test/com/rpl/specter/000077500000000000000000000000001311753541400171625ustar00rootroot00000000000000specter-1.0.2/test/com/rpl/specter/cljs_test_helpers.clj000066400000000000000000000010411311753541400233640ustar00rootroot00000000000000(ns com.rpl.specter.cljs-test-helpers) ;; it seems like gen/bind and gen/return are a monad (hence the names) (defmacro for-all+ [bindings & body] (let [parts (partition 2 bindings) vars (vec (map first parts)) genned (reduce (fn [curr [v code]] `(clojure.test.check.generators/bind ~code (fn [~v] ~curr))) `(clojure.test.check.generators/return ~vars) (reverse parts))] `(clojure.test.check.properties/for-all [~vars ~genned] ~@body))) specter-1.0.2/test/com/rpl/specter/cljs_test_runner.cljs000066400000000000000000000003741311753541400234260ustar00rootroot00000000000000(ns com.rpl.specter.cljs-test-runner (:require [doo.runner :refer-macros [doo-tests]] [com.rpl.specter.core-test] [com.rpl.specter.zipper-test])) (doo-tests 'com.rpl.specter.core-test 'com.rpl.specter.zipper-test) specter-1.0.2/test/com/rpl/specter/core_test.cljc000066400000000000000000001413611311753541400220140ustar00rootroot00000000000000(ns com.rpl.specter.core-test #?(:cljs (:require-macros [cljs.test :refer [is deftest]] [clojure.test.check.clojure-test :refer [defspec]] [com.rpl.specter.cljs-test-helpers :refer [for-all+]] [com.rpl.specter.test-helpers :refer [ic-test]] [com.rpl.specter :refer [defprotocolpath defnav extend-protocolpath nav declarepath providepath select select-one select-one! select-first transform setval replace-in select-any selected-any? collected? traverse multi-transform path dynamicnav recursive-path defdynamicnav traverse-all satisfies-protpath? end-fn]])) (:use #?(:clj [clojure.test :only [deftest is]]) #?(:clj [clojure.test.check.clojure-test :only [defspec]]) #?(:clj [com.rpl.specter.test-helpers :only [for-all+ ic-test]]) #?(:clj [com.rpl.specter :only [defprotocolpath defnav extend-protocolpath nav declarepath providepath select select-one select-one! select-first transform setval replace-in select-any selected-any? collected? traverse multi-transform path dynamicnav recursive-path defdynamicnav traverse-all satisfies-protpath? end-fn]])) (:require #?(:clj [clojure.test.check.generators :as gen]) #?(:clj [clojure.test.check.properties :as prop]) #?(:cljs [clojure.test.check :as tc]) #?(:cljs [clojure.test.check.generators :as gen]) #?(:cljs [clojure.test.check.properties :as prop :include-macros true]) [com.rpl.specter :as s] [com.rpl.specter.transients :as t] [clojure.set :as set])) ;;TODO: ;; test walk, codewalk (defn limit-size [n {gen :gen}] (gen/->Generator (fn [rnd _size] (gen rnd (if (< _size n) _size n))))) (defn gen-map-with-keys [key-gen val-gen & keys] (gen/bind (gen/map key-gen val-gen) (fn [m] (gen/bind (apply gen/hash-map (mapcat (fn [k] [k val-gen]) keys)) (fn [m2] (gen/return (merge m m2))))))) (defspec select-all-keyword-filter (for-all+ [kw gen/keyword v (gen/vector (limit-size 5 (gen-map-with-keys gen/keyword gen/int kw))) pred (gen/elements [odd? even?])] (= (select [s/ALL kw pred] v) (->> v (map kw) (filter pred))))) (defspec select-pos-extreme-pred (for-all+ [v (gen/vector gen/int) pred (gen/elements [odd? even?]) pos (gen/elements [[s/FIRST first] [s/LAST last]])] (= (select-one [(s/filterer pred) (first pos)] v) (->> v (filter pred) ((last pos)))))) (defspec select-all-on-map (for-all+ [m (limit-size 5 (gen/map gen/keyword gen/int)) p (gen/elements [s/MAP-VALS [s/ALL s/LAST]])] (= (select p m) (for [[k v] m] v)))) (deftest select-one-test (is (thrown? #?(:clj Exception :cljs js/Error) (select-one [s/ALL even?] [1 2 3 4]))) (is (= 1 (select-one [s/ALL odd?] [2 4 1 6])))) (deftest select-first-test (is (= 7 (select-first [(s/filterer odd?) s/ALL #(> % 4)] [3 4 2 3 7 5 9 8]))) (is (nil? (select-first [s/ALL even?] [1 3 5 9])))) (defspec transform-all-on-map (for-all+ [m (limit-size 5 (gen/map gen/keyword gen/int)) p (gen/elements [s/MAP-VALS [s/ALL s/LAST]])] (= (transform p inc m) (into {} (for [[k v] m] [k (inc v)]))))) (defspec transform-all (for-all+ [v (gen/vector gen/int)] (let [v2 (transform [s/ALL] inc v)] (and (vector? v2) (= v2 (map inc v)))))) (defspec transform-all-list (for-all+ [v (gen/list gen/int)] (let [v2 (transform [s/ALL] inc v)] (and (seq? v2) (= v2 (map inc v)))))) (defspec transform-all-filter (for-all+ [v (gen/vector gen/int) pred (gen/elements [odd? even?]) action (gen/elements [inc dec])] (let [v2 (transform [s/ALL pred] action v)] (= v2 (map (fn [v] (if (pred v) (action v) v)) v))))) (defspec transform-last (for-all+ [v (gen/not-empty (gen/vector gen/int)) pred (gen/elements [inc dec])] (let [v2 (transform [s/LAST] pred v)] (= v2 (concat (butlast v) [(pred (last v))]))))) (defspec transform-first (for-all+ [v (gen/not-empty (gen/vector gen/int)) pred (gen/elements [inc dec])] (let [v2 (transform [s/FIRST] pred v)] (= v2 (concat [(pred (first v))] (rest v)))))) (defspec transform-filterer-all-equivalency (prop/for-all [s (gen/vector gen/int) target-type (gen/elements ['() []]) pred (gen/elements [even? odd?]) updater (gen/elements [inc dec])] (let [v (into target-type s) v2 (transform [(s/filterer pred) s/ALL] updater v) v3 (transform [s/ALL pred] updater v)] (and (= v2 v3) (= (type v2) (type v3)))))) (defspec transform-with-context (for-all+ [kw1 gen/keyword kw2 gen/keyword m (limit-size 10 (gen-map-with-keys gen/keyword gen/int kw1 kw2)) pred (gen/elements [odd? even?])] (= (transform [(s/collect-one kw2) kw1 pred] + m) (if (pred (kw1 m)) (assoc m kw1 (+ (kw1 m) (kw2 m))) m)))) (defn differing-elements [v1 v2] (->> (map vector v1 v2) (map-indexed (fn [i [e1 e2]] (if (not= e1 e2) i))) (filter identity))) (defspec transform-last-compound (for-all+ [pred (gen/elements [odd? even?]) v (gen/such-that #(some pred %) (gen/vector gen/int))] (let [v2 (transform [(s/filterer pred) s/LAST] inc v) differing-elems (differing-elements v v2)] (and (= (count v2) (count v)) (= (count differing-elems) 1) (every? (complement pred) (drop (first differing-elems) v2)))))) ;; max sizes prevent too much data from being generated and keeps test from taking forever (defspec transform-keyword (for-all+ [k1 (limit-size 3 gen/keyword) k2 (limit-size 3 gen/keyword) m1 (limit-size 5 (gen-map-with-keys gen/keyword (gen-map-with-keys gen/keyword gen/int k2) k1)) pred (gen/elements [inc dec])] (let [m2 (transform [k1 k2] pred m1)] (and (= (assoc-in m1 [k1 k2] nil) (assoc-in m2 [k1 k2] nil)) (= (pred (get-in m1 [k1 k2])) (get-in m2 [k1 k2])))))) (defspec replace-in-test (for-all+ [v (gen/vector gen/int)] (let [res (->> v (map (fn [v] (if (even? v) (inc v) v)))) user-ret (->> v (filter even?) (map (fn [v] [v v])) (apply concat)) user-ret (if (empty? user-ret) nil user-ret)] (= (replace-in [s/ALL even?] (fn [v] [(inc v) [v v]]) v) [res user-ret])))) (defspec replace-in-custom-merge (for-all+ [v (gen/vector gen/int)] (let [res (->> v (map (fn [v] (if (even? v) (inc v) v)))) last-even (->> v (filter even?) last) user-ret (if last-even {:a last-even})] (= (replace-in [s/ALL even?] (fn [v] [(inc v) v]) v :merge-fn (fn [curr new] (assoc curr :a new))) [res user-ret])))) (defspec srange-extremes-test (for-all+ [v (gen/vector gen/int) v2 (gen/vector gen/int)] (let [b (setval s/BEGINNING v2 v) e (setval s/END v2 v)] (and (= b (concat v2 v)) (= e (concat v v2)))))) (defspec srange-test (for-all+ [v (gen/vector gen/int) b (gen/elements (-> v count inc range)) e (gen/elements (range b (-> v count inc)))] (let [sv (subvec v b e) predcount (fn [pred v] (->> v (filter pred) count)) even-count (partial predcount even?) odd-count (partial predcount odd?) b (transform (s/srange b e) (fn [r] (filter odd? r)) v)] (and (= (odd-count v) (odd-count b)) (= (+ (even-count b) (even-count sv)) (even-count v)))))) (deftest structure-path-directly-test (is (= 3 (select-one :b {:a 1 :b 3}))) (is (= 5 (select-one (s/comp-paths :a :b) {:a {:b 5}})))) (deftest atom-test (let [v (transform s/ATOM inc (atom 1))] (is (instance? #?(:clj clojure.lang.Atom :cljs cljs.core/Atom) v)) (is (= 2 (select-one s/ATOM v) @v)))) (defspec view-test (for-all+ [i gen/int afn (gen/elements [inc dec])] (= (first (select (s/view afn) i)) (afn i) (transform (s/view afn) identity i)))) (defspec must-test (for-all+ [k1 gen/int k2 (gen/such-that #(not= k1 %) gen/int) m (gen-map-with-keys gen/int gen/int k1) op (gen/elements [inc dec])] (let [m (dissoc m k2)] (and (= (transform (s/must k1) op m) (transform (s/keypath k1) op m)) (= (transform (s/must k2) op m) m) (= (select (s/must k1) m) (select (s/keypath k1) m)) (empty? (select (s/must k2) m)))))) (defspec parser-test (for-all+ [i gen/int afn (gen/elements [inc dec #(* % 2)]) bfn (gen/elements [inc dec #(* % 2)]) cfn (gen/elements [inc dec #(* % 2)])] (and (= (select-one! (s/parser afn bfn) i) (afn i)) (= (transform (s/parser afn bfn) cfn i) (-> i afn cfn bfn))))) (deftest selected?-test (is (= [[1 3 5] [2 :a] [7 11 4 2 :a] [10 1 :a] []] (setval [s/ALL (s/selected? s/ALL even?) s/END] [:a] [[1 3 5] [2] [7 11 4 2] [10 1] []]))) (is (= [2 4] (select [s/ALL (s/selected? even?)] [1 2 3 4]))) (is (= [1 3] (select [s/ALL (s/not-selected? even?)] [1 2 3 4])))) (defspec identity-test (for-all+ [i gen/int afn (gen/elements [inc dec])] (and (= [i] (select nil i)) (= (afn i) (transform nil afn i))))) (deftest nil-comp-test (is (= [5] (select (com.rpl.specter.impl/comp-paths* nil) 5)))) (defspec putval-test (for-all+ [kw gen/keyword m (limit-size 10 (gen-map-with-keys gen/keyword gen/int kw)) c gen/int] (= (transform [(s/putval c) kw] + m) (transform [kw (s/putval c)] + m) (assoc m kw (+ c (get m kw)))))) (defspec empty-selector-test (for-all+ [v (gen/vector gen/int)] (= [v] (select [] v) (select nil v) (select (s/comp-paths) v) (select (s/comp-paths nil) v) (select [nil nil nil] v)))) (defspec empty-selector-transform-test (for-all+ [kw gen/keyword m (limit-size 10 (gen-map-with-keys gen/keyword gen/int kw))] (and (= m (transform nil identity m) (transform [] identity m) (transform (s/comp-paths []) identity m) (transform (s/comp-paths nil nil) identity m)) (= (transform kw inc m) (transform [nil kw] inc m) (transform (s/comp-paths kw nil) inc m) (transform (s/comp-paths nil kw nil) inc m))))) (deftest compose-empty-comp-path-test (let [m {:a 1}] (is (= [1] (select [:a (s/comp-paths)] m) (select [(s/comp-paths) :a] m))))) (defspec mixed-selector-test (for-all+ [k1 (limit-size 3 gen/keyword) k2 (limit-size 3 gen/keyword) m (limit-size 5 (gen-map-with-keys gen/keyword (gen-map-with-keys gen/keyword gen/int k2) k1))] (= [(-> m k1 k2)] (select [k1 (s/comp-paths k2)] m) (select [(s/comp-paths k1) k2] m) (select [(s/comp-paths k1 k2) nil] m) (select [(s/comp-paths) k1 k2] m) (select [k1 (s/comp-paths) k2] m)))) (deftest cond-path-test (is (= [4 2 6 8 10] (select [s/ALL (s/cond-path even? [(s/view inc) (s/view inc)] #(= 3 %) (s/view dec))] [1 2 3 4 5 6 7 8]))) (is (empty? (select (s/if-path odd? (s/view inc)) 2))) (is (= [6 2 10 6 14] (transform [(s/putval 2) s/ALL (s/if-path odd? [(s/view inc) (s/view inc)] (s/view dec))] * [1 2 3 4 5]))) (is (= 2 (transform [(s/putval 2) (s/if-path odd? (s/view inc))] * 2)))) (defspec cond-path-selector-test (for-all+ [k1 (limit-size 3 gen/keyword) k2 (limit-size 3 gen/keyword) k3 (limit-size 3 gen/keyword) m (limit-size 5 (gen-map-with-keys gen/keyword gen/int k1 k2 k3)) pred (gen/elements [odd? even?])] (let [v1 (get m k1) k (if (pred v1) k2 k3)] (and (= (transform (s/if-path [k1 pred] k2 k3) inc m) (transform k inc m)) (= (select (s/if-path [k1 pred] k2 k3) m) (select k m)))))) (deftest optimized-if-path-test (is (= [-4 -2] (select [s/ALL (s/if-path [even? neg?] s/STAY)] [1 2 -3 -4 0 -2]))) (is (= [1 2 -3 4 0 2] (transform [s/ALL (s/if-path [even? neg?] s/STAY)] - [1 2 -3 -4 0 -2])))) (defspec multi-path-test (for-all+ [k1 (limit-size 3 gen/keyword) k2 (limit-size 3 gen/keyword) m (limit-size 5 (gen-map-with-keys gen/keyword gen/int k1 k2))] (= (transform (s/multi-path k1 k2) inc m) (->> m (transform k1 inc) (transform k2 inc))))) (deftest empty-pos-transform (is (empty? (select s/FIRST []))) (is (empty? (select s/LAST []))) (is (= [] (transform s/FIRST inc []))) (is (= [] (transform s/LAST inc [])))) (defspec set-filter-test (for-all+ [k1 gen/keyword k2 (gen/such-that #(not= k1 %) gen/keyword) k3 (gen/such-that (complement #{k1 k2}) gen/keyword) v (gen/vector (gen/elements [k1 k2 k3]))] (= (filter #{k1 k2} v) (select [s/ALL #{k1 k2}] v)))) (deftest nil-select-one-test (is (= nil (select-one! s/ALL [nil]))) (is (thrown? #?(:clj Exception :cljs js/Error) (select-one! s/ALL [])))) (defspec transformed-test (for-all+ [v (gen/vector gen/int) pred (gen/elements [even? odd?]) op (gen/elements [inc dec])] (= (select-one (s/transformed [s/ALL pred] op) v) (transform [s/ALL pred] op v)))) (defspec basic-parameterized-composition-test (for-all+ [k1 (limit-size 3 gen/keyword) k2 (limit-size 3 gen/keyword) m1 (limit-size 5 (gen-map-with-keys gen/keyword (gen-map-with-keys gen/keyword gen/int k2) k1)) pred (gen/elements [inc dec])] (let [p (dynamicnav [a b] (path (s/keypath a) (s/keypath b)))] (and (= (s/compiled-select (p k1 k2) m1) (select [k1 k2] m1)) (= (s/compiled-transform (p k1 k2) pred m1) (transform [k1 k2] pred m1)))))) (defspec filterer-param-test (for-all+ [k gen/keyword k2 gen/keyword v (gen/vector (limit-size 5 (gen-map-with-keys gen/keyword gen/int k k2))) pred (gen/elements [odd? even?]) updater (gen/elements [inc dec])] (and (= (select (s/filterer (s/keypath k) pred) v) (select (s/filterer k pred) v)) (= (transform [(s/filterer (s/keypath k) pred) s/ALL k2] updater v) (transform [(s/filterer k pred) s/ALL k2] updater v))))) (deftest nested-param-paths (let [p (fn [a b c] (path (s/filterer (s/keypath a) (s/selected? s/ALL (s/keypath b) (s/filterer (s/keypath c) even?) s/ALL)))) p2 (p :a :b :c) p3 (s/filterer :a (s/selected? s/ALL :b (s/filterer :c even?) s/ALL)) data [{:a [{:b [{:c 4 :d 5}]}]} {:a [{:c 3}]} {:a [{:b [{:c 7}] :e [1]}]}]] (is (= (select p2 data) (select p3 data) [[{:a [{:b [{:c 4 :d 5}]}]}]])))) (defspec subselect-nested-vectors (for-all+ [v1 (gen/vector (gen/vector gen/int))] (let [path (s/comp-paths (s/subselect s/ALL s/ALL)) v2 (s/compiled-transform path reverse v1)] (and (= (s/compiled-select path v1) [(flatten v1)]) (= (flatten v1) (reverse (flatten v2))) (= (map count v1) (map count v2)))))) (defspec subselect-param-test (for-all+ [k gen/keyword v (gen/vector (limit-size 5 (gen-map-with-keys gen/keyword gen/int k)))] (and (= (s/compiled-select (s/subselect s/ALL (s/keypath k)) v) [(map k v)]) (let [v2 (s/compiled-transform (s/comp-paths (s/subselect s/ALL (s/keypath k))) reverse v)] (and (= (map k v) (reverse (map k v2))) (= (map #(dissoc % k) v) (map #(dissoc % k) v2))))))) ; only key k was touched in any of the maps (defspec param-multi-path-test (for-all+ [k1 gen/keyword k2 gen/keyword k3 gen/keyword m (limit-size 5 (gen-map-with-keys gen/keyword gen/int k1 k2 k3)) pred1 (gen/elements [odd? even?]) pred2 (gen/elements [odd? even?]) updater (gen/elements [inc dec])] (let [paths [(path (s/multi-path [(s/keypath k1) pred1] [(s/keypath k2) pred2] k3)) (path (s/multi-path [k1 pred1] [(s/keypath k2) pred2] (s/keypath k3))) (path (s/multi-path [(s/keypath k1) pred1] [(s/keypath k2) pred2] (s/keypath k3))) (s/multi-path [k1 pred1] [k2 pred2] k3) (path (s/multi-path [k1 pred1] [(s/keypath k2) pred2] k3))]] (and (apply = (for [p paths] (select p m))) (apply = (for [p paths] (transform p updater m))))))) (defspec subset-test (for-all+ [s1 (gen/vector (limit-size 5 gen/keyword)) s2 (gen/vector (limit-size 5 gen/keyword)) s3 (gen/vector (limit-size 5 gen/int)) s4 (gen/vector (limit-size 5 gen/keyword))] (let [s1 (set s1) s2 (set s1) s3 (set s1) s4 (set s1) combined (set/union s1 s2) ss (set/union s2 s3)] (and (= (transform (s/subset s3) identity combined) combined) (= (setval (s/subset s3) #{} combined) (set/difference combined s2)) (= (setval (s/subset s3) s4 combined) (-> combined (set/difference s2) (set/union s4))))))) (deftest submap-test (is (= [{:foo 1}] (select [(s/submap [:foo :baz])] {:foo 1 :bar 2}))) (is (= {:foo 1, :barry 1} (setval [(s/submap [:bar])] {:barry 1} {:foo 1 :bar 2}))) (is (= {:bar 1, :foo 2} (transform [(s/submap [:foo :baz]) s/ALL s/LAST] inc {:foo 1 :bar 1}))) (is (= {:a {:new 1} :c {:new 1 :old 1}} (setval [s/ALL s/LAST (s/submap [])] {:new 1} {:a nil, :c {:old 1}})))) (deftest nil->val-test (is (= {:a #{:b}} (setval [:a s/NIL->SET (s/subset #{})] #{:b} nil))) (is (= {:a #{:b :c :d}} (setval [:a s/NIL->SET (s/subset #{})] #{:b} {:a #{:c :d}}))) (is (= {:a [:b]} (setval [:a s/NIL->VECTOR s/END] [:b] nil)))) (defspec void-test (for-all+ [s1 (gen/vector (limit-size 5 gen/int))] (and (empty? (select s/STOP s1)) (empty? (select [s/STOP s/ALL s/ALL s/ALL s/ALL] s1)) (= s1 (transform s/STOP inc s1)) (= s1 (transform [s/ALL s/STOP s/ALL] inc s1)) (= (transform [s/ALL (s/cond-path even? nil odd? s/STOP)] inc s1) (transform [s/ALL even?] inc s1))))) (deftest stay-continue-tests (is (= [[1 2 [:a :b]] [3 [:a :b]] [:a :b [:a :b]]] (setval [(s/stay-then-continue s/ALL) s/END] [[:a :b]] [[1 2] [3]]))) (is (= [[1 2 [:a :b]] [3 [:a :b]] [:a :b]] (setval [(s/continue-then-stay s/ALL) s/END] [[:a :b]] [[1 2] [3]]))) (is (= [[1 2 3] 1 3] (select (s/stay-then-continue s/ALL odd?) [1 2 3]))) (is (= [1 3 [1 2 3]] (select (s/continue-then-stay s/ALL odd?) [1 2 3])))) (declarepath MyWalker) (providepath MyWalker (s/if-path vector? (s/if-path [s/FIRST #(= :abc %)] (s/continue-then-stay s/ALL MyWalker) [s/ALL MyWalker]))) (deftest recursive-path-test (is (= [9 1 10 3 1] (select [MyWalker s/ALL number?] [:bb [:aa 34 [:abc 10 [:ccc 9 8 [:abc 9 1]]]] [:abc 1 [:abc 3]]]))) (is (= [:bb [:aa 34 [:abc 11 [:ccc 9 8 [:abc 10 2]]]] [:abc 2 [:abc 4]]] (transform [MyWalker s/ALL number?] inc [:bb [:aa 34 [:abc 10 [:ccc 9 8 [:abc 9 1]]]] [:abc 1 [:abc 3]]])))) (def map-key-walker (recursive-path [akey] p [s/ALL (s/if-path [s/FIRST #(= % akey)] s/LAST [s/LAST p])])) (deftest recursive-params-path-test (is (= #{1 2 3} (set (select (map-key-walker :aaa) {:a {:aaa 3 :b {:c {:aaa 2} :aaa 1}}})))) (is (= {:a {:aaa 4 :b {:c {:aaa 3} :aaa 2}}} (transform (map-key-walker :aaa) inc {:a {:aaa 3 :b {:c {:aaa 2} :aaa 1}}}))) (is (= {:a {:c {:b "X"}}} (setval (map-key-walker :b) "X" {:a {:c {:b {:d 1}}}})))) (deftest recursive-params-composable-path-test (let [p (fn [k k2] (path (s/keypath k) (map-key-walker k2)))] (is (= [1] (select (p 1 :a) [{:a 3} {:a 1} {:a 2}]))))) (deftest all-map-test (is (= {3 3} (transform [s/ALL s/FIRST] inc {2 3}))) (is (= {3 21 4 31} (transform [s/ALL s/ALL] inc {2 20 3 30})))) (def NestedHigherOrderWalker (recursive-path [k] p (s/if-path vector? (s/if-path [s/FIRST #(= % k)] (s/continue-then-stay s/ALL p) [s/ALL p])))) (deftest nested-higher-order-walker-test (is (= [:q [:abc :I 3] [:ccc [:abc :I] [:abc :I "a" [:abc :I [:abc :I [:d]]]]]] (setval [(NestedHigherOrderWalker :abc) (s/srange 1 1)] [:I] [:q [:abc 3] [:ccc [:abc] [:abc "a" [:abc [:abc [:d]]]]]])))) #?(:clj (deftest large-params-test (let [path (apply com.rpl.specter.impl/comp-navs (for [i (range 25)] (s/keypath i))) m (reduce (fn [m k] {k m}) :a (reverse (range 25)))] (is (= :a (select-one path m)))))) ;;TODO: there's a bug in clojurescript that won't allow ;; non function implementations of IFn to have more than 20 arguments #?(:clj (do (defprotocolpath AccountPath []) (defrecord Account [funds]) (defrecord User [account]) (defrecord Family [accounts]) (extend-protocolpath AccountPath User :account Family [:accounts s/ALL]))) #?(:clj (deftest protocolpath-basic-test (let [data [(->User (->Account 30)) (->User (->Account 50)) (->Family [(->Account 51) (->Account 52)])]] (is (= [30 50 51 52] (select [s/ALL AccountPath :funds] data))) (is (= [(->User (->Account 31)) (->User (->Account 51)) (->Family [(->Account 52) (->Account 53)])] (transform [s/ALL AccountPath :funds] inc data)))))) #?(:clj (do (defprotocolpath LabeledAccountPath [label]) (defrecord LabeledUser [account]) (defrecord LabeledFamily [accounts]) (extend-protocolpath LabeledAccountPath LabeledUser [:account (s/keypath label)] LabeledFamily [:accounts (s/keypath label) s/ALL]))) #?(:clj (deftest protocolpath-params-test (let [data [(->LabeledUser {:a (->Account 30)}) (->LabeledUser {:a (->Account 50)}) (->LabeledFamily {:a [(->Account 51) (->Account 52)]})]] (is (= [30 50 51 52] (select [s/ALL (LabeledAccountPath :a) :funds] data))) (is (= [(->LabeledUser {:a (->Account 31)}) (->LabeledUser {:a (->Account 51)}) (->LabeledFamily {:a [(->Account 52) (->Account 53)]})] (transform [s/ALL (LabeledAccountPath :a) :funds] inc data)))))) #?(:clj (do (defprotocolpath CustomWalker []) (extend-protocolpath CustomWalker Object nil clojure.lang.PersistentHashMap [(s/keypath :a) CustomWalker] clojure.lang.PersistentArrayMap [(s/keypath :a) CustomWalker] clojure.lang.PersistentVector [s/ALL CustomWalker]))) #?(:clj (deftest mixed-rich-regular-protocolpath (is (= [1 2 3 11 21 22 25] (select [CustomWalker number?] [{:a [1 2 :c [3]]} [[[[[[11]]] 21 [22 :c 25]]]]]))) (is (= [2 3 [[[4]] :b 0] {:a 4 :b 10}] (transform [CustomWalker number?] inc [1 2 [[[3]] :b -1] {:a 3 :b 10}]))))) #?( :clj (defn make-queue [coll] (reduce #(conj %1 %2) clojure.lang.PersistentQueue/EMPTY coll)) :cljs (defn make-queue [coll] (reduce #(conj %1 %2) #queue [] coll))) (defspec transform-idempotency 50 (for-all+ [v1 (gen/vector gen/int) l1 (gen/list gen/int) m1 (gen/map gen/keyword gen/int)] (let [s1 (set v1) q1 (make-queue v1) v2 (transform s/ALL identity v1) m2 (transform s/ALL identity m1) s2 (transform s/ALL identity s1) l2 (transform s/ALL identity l1) q2 (transform s/ALL identity q1)] (and (= v1 v2) (= (type v1) (type v2)) (= m1 m2) (= (type m1) (type m2)) (= s1 s2) (= (type s1) (type s2)) (= l1 l2) (seq? l2) ; Transformed lists are only guaranteed to impelment ISeq (= q1 q2) (= (type q1) (type q2)))))) (defn ^:direct-nav double-str-keypath [s1 s2] (path (s/keypath (str s1 s2)))) (defn ^:direct-nav some-keypath ([] (s/keypath "a")) ([k1] (s/keypath (str k1 "!"))) ([k & args] (s/keypath "bbb"))) (deftest nav-constructor-test ;; this also tests that the eval done by clj platform during inline ;; caching rebinds to the correct namespace since this is run ;; by clojure.test in a different namespace (is (= 1 (select-one! (double-str-keypath "a" "b") {"ab" 1 "c" 2}))) (is (= 1 (select-one! (some-keypath) {"a" 1 "a!" 2 "bbb" 3 "d" 4}))) (is (= 2 (select-one! (some-keypath "a") {"a" 1 "a!" 2 "bbb" 3 "d" 4}))) (is (= 3 (select-one! (some-keypath 1 2 3 4 5) {"a" 1 "a!" 2 "bbb" 3 "d" 4})))) (def ^:dynamic *APATH* s/keypath) (deftest inline-caching-test (ic-test [k] [s/ALL (s/must k)] inc [{:a 1} {:b 2 :c 3} {:a 7 :d -1}] [[:a] [:b] [:c] [:d] [:e]]) (ic-test [] [s/ALL #{4 5 11} #(> % 2) (fn [e] (< e 7))] inc (range 20) []) (ic-test [v] (if v :a :b) inc {:a 1 :b 2} [[true] [false]]) (ic-test [v] [s/ALL (double-str-keypath v (inc v))] str [{"12" :a "1011" :b} {"1011" :c}] [[1] [10]]) (ic-test [k] (*APATH* k) str {:a 1 :b 2} [[:a] [:b] [:c]]) (binding [*APATH* s/must] (ic-test [k] (*APATH* k) inc {:a 1 :b 2} [[:a] [:b] [:c]])) (ic-test [k k2] [s/ALL (s/selected? (s/must k) #(> % 2)) (s/must k2)] dec [{:a 1 :b 2} {:a 10 :b 6} {:c 7 :b 8} {:c 1 :d 9} {:c 3 :d -1}] [[:a :b] [:b :a] [:c :d] [:b :c]]) (ic-test [] [(s/transformed s/STAY inc)] inc 10 []) ;; verifying that these don't throw errors (is (= 1 (select-any (if true :a :b) {:a 1}))) (is (= 3 (select-any (*APATH* :a) {:a 3}))) (is (= 2 (select-any [:a (identity even?)] {:a 2}))) (is (= [10 11] (select-one! [(s/putval 10) (s/transformed s/STAY #(inc %))] 10))) (is (= 2 (let [p :a] (select-one! [p even?] {:a 2})))) (is (= [{:a 2}] (let [p :a] (select [s/ALL (s/selected? p even?)] [{:a 2}]))))) (deftest nested-inline-caching-test (is (= [[1]] (let [a :b] (select (s/view (fn [v] (select [(s/keypath v) (s/keypath a)] {:a {:b 1}}))) :a))))) (defspec continuous-subseqs-filter-equivalence (for-all+ [aseq (gen/vector (gen/elements [1 2 3 :a :b :c 4 5 :d :e])) pred (gen/elements [keyword? number?])] (= (setval (s/continuous-subseqs pred) nil aseq) (filter (complement pred) aseq)))) (deftest continuous-subseqs-test (is (= [1 "ab" 2 3 "c" 4 "def"] (transform (s/continuous-subseqs string?) (fn [s] [(apply str s)]) [1 "a" "b" 2 3 "c" 4 "d" "e" "f"]))) (is (= [[] [2] [4 6]] (select [(s/continuous-subseqs number?) (s/filterer even?)] [1 "a" "b" 2 3 "c" 4 5 6 "d" "e" "f"])))) ;; verifies that late binding of dynamic parameters works correctly (deftest transformed-inline-caching (dotimes [i 10] (is (= [(inc i)] (select (s/transformed s/STAY #(+ % i)) 1))))) ;; test for issue #103 (deftest nil->val-regression-test (is (= false (transform (s/nil->val true) identity false))) (is (= false (select-one! (s/nil->val true) false)))) #?(:clj (deftest all-map-entry (let [e (transform s/ALL inc (first {1 3}))] (is (instance? clojure.lang.MapEntry e)) (is (= 2 (key e))) (is (= 4 (val e)))))) (deftest select-on-empty-vector (is (= s/NONE (select-any s/ALL []))) (is (nil? (select-first s/ALL []))) (is (nil? (select-one s/ALL []))) (is (= s/NONE (select-any s/FIRST []))) (is (= s/NONE (select-any s/LAST []))) (is (nil? (select-first s/FIRST []))) (is (nil? (select-one s/FIRST []))) (is (nil? (select-first s/LAST []))) (is (nil? (select-one s/LAST [])))) (defspec select-first-one-any-equivalency (for-all+ [aval gen/int apred (gen/elements [even? odd?])] (let [data [aval] r1 (select-any [s/ALL (s/pred apred)] data) r2 (select-first [s/ALL (s/pred apred)] data) r3 (select-one [s/ALL (s/pred apred)] data) r4 (first (select [s/ALL (s/pred apred)] data)) r5 (select-any [s/FIRST (s/pred apred)] data) r6 (select-any [s/LAST (s/pred apred)] data)] (or (and (= r1 s/NONE) (nil? r2) (nil? r3) (nil? r4) (= r5 s/NONE) (= r6 s/NONE)) (and (not= r1 s/NONE) (some? r1) (= r1 r2 r3 r4 r5 r6)))))) (deftest select-any-static-fn (is (= 2 (select-any even? 2))) (is (= s/NONE (select-any odd? 2)))) (deftest select-any-keywords (is (= s/NONE (select-any [:a even?] {:a 1}))) (is (= 2 (select-any [:a even?] {:a 2}))) (is (= s/NONE (select-any [(s/keypath "a") even?] {"a" 1}))) (is (= 2 (select-any [(s/keypath "a") even?] {"a" 2}))) (is (= s/NONE (select-any (s/must :b) {:a 1 :c 3}))) (is (= 2 (select-any (s/must :b) {:a 1 :b 2 :c 3}))) (is (= s/NONE (select-any [(s/must :b) odd?] {:a 1 :b 2 :c 3})))) (defspec select-any-ALL (for-all+ [v (gen/vector gen/int) pred (gen/elements [even? odd?])] (let [r1 (select [s/ALL pred] v) r2 (select-any [s/ALL pred] v)] (or (and (empty? r1) (= s/NONE r2)) (contains? (set r1) r2))))) (deftest select-any-beginning-end (is (= [] (select-any s/BEGINNING [1 2 3]) (select-any s/END [1]))) (is (= s/NONE (select-any [s/BEGINNING s/STOP] [1 2 3]) (select-any [s/END s/STOP] [2 3])))) (deftest select-any-walker (let [data [1 [2 3 4] [[6]]]] (is (= s/NONE (select-any (s/walker keyword?) data))) (is (= s/NONE (select-any [(s/walker number?) neg?] data))) (is (#{1 3} (select-any [(s/walker number?) odd?] data))) (is (#{2 4 6} (select-any [(s/walker number?) even?] data))))) (defspec selected-any?-select-equivalence (for-all+ [v (gen/vector gen/int) pred (gen/elements [even? odd?])] (let [r1 (not (empty? (select [s/ALL pred] v))) r2 (selected-any? [s/ALL pred] v)] (= r1 r2)))) (defn div-by-3? [v] (= 0 (mod v 3))) (defspec selected?-filter-equivalence (for-all+ [v (gen/vector gen/int) pred (gen/elements [even? odd?])] (and (= (select-any [s/ALL pred] v) (select-any [s/ALL (s/selected? pred)] v)) (= (select-any [s/ALL pred div-by-3?] v) (select-any [s/ALL (s/selected? pred) div-by-3?] v))))) (deftest multi-path-select-any-test (is (= s/NONE (select-any (s/multi-path s/STOP s/STOP) 1))) (is (= 1 (select-any (s/multi-path s/STAY s/STOP) 1) (select-any (s/multi-path s/STOP s/STAY) 1) (select-any (s/multi-path s/STOP s/STAY s/STOP) 1))) (is (= s/NONE (select-any [(s/multi-path s/STOP s/STAY) even?] 1)))) (deftest if-path-select-any-test (is (= s/NONE (select-any (s/if-path even? s/STAY) 1))) (is (= 2 (select-any (s/if-path even? s/STAY s/STAY) 2))) (is (= s/NONE (select-any [(s/if-path even? s/STAY s/STAY) odd?] 2))) (is (= 2 (select-any (s/if-path odd? s/STOP s/STAY) 2))) (is (= s/NONE (select-any [(s/if-path odd? s/STOP s/STAY) odd?] 2)))) (defspec transient-vector-test (for-all+ [v (gen/vector (limit-size 5 gen/int))] (every? identity (for [[path transient-path f] [[s/FIRST t/FIRST! (fnil inc 0)] ;; fnil in case vector is empty [s/LAST t/LAST! (fnil inc 0)] [(s/keypath 0) (t/keypath! 0) (fnil inc 0)] [s/END t/END! #(conj % 1 2 3)]]] (and (= (s/transform* path f v) (persistent! (s/transform* transient-path f (transient v)))) (= (s/select* path v) (s/select* transient-path (transient v)))))))) (defspec transient-map-test (for-all+ [m (limit-size 5 (gen/not-empty (gen/map gen/keyword gen/int))) new-key gen/keyword] (let [existing-key (first (keys m))] (every? identity (for [[path transient-path f] [[(s/keypath existing-key) (t/keypath! existing-key) inc] [(s/keypath new-key) (t/keypath! new-key) (constantly 3)] [(s/submap [existing-key new-key]) (t/submap! [existing-key new-key]) (constantly {new-key 1234})]]] (and (= (s/transform* path f m) (persistent! (s/transform* transient-path f (transient m)))) (= (s/select* path m) (s/select* transient-path (transient m))))))))) (defspec meta-test (for-all+ [v (gen/vector gen/int) meta-map (limit-size 5 (gen/map gen/keyword gen/int))] (= meta-map (meta (setval s/META meta-map v)) (first (select s/META (with-meta v meta-map))) (first (select s/META (setval s/META meta-map v)))))) (deftest beginning-end-all-first-last-on-nil (is (= [2 3] (setval s/END [2 3] nil) (setval s/BEGINNING [2 3] nil))) (is (nil? (setval s/FIRST :a nil))) (is (nil? (setval s/LAST :a nil))) (is (nil? (transform s/ALL inc nil))) (is (empty? (select s/FIRST nil))) (is (empty? (select s/LAST nil))) (is (empty? (select s/ALL nil)))) (deftest map-vals-nil (is (= nil (transform s/MAP-VALS inc nil))) (is (empty? (select s/MAP-VALS nil)))) (defspec dispense-test (for-all+ [k1 gen/int k2 gen/int k3 gen/int m (gen-map-with-keys gen/int gen/int k1 k2 k3)] (= (select [(s/collect-one (s/keypath k1)) (s/collect-one (s/keypath k2)) s/DISPENSE (s/collect-one (s/keypath k2)) (s/keypath k3)] m) (select [(s/collect-one (s/keypath k2)) (s/keypath k3)] m)))) (deftest collected?-test (let [data {:active-id 1 :items [{:id 1 :name "a"} {:id 2 :name "b"}]}] (is (= {:id 1 :name "a"} (select-any [(s/collect-one :active-id) :items s/ALL (s/collect-one :id) (collected? [a i] (= a i)) s/DISPENSE] data) (select-any [(s/collect-one :active-id) :items s/ALL (s/collect-one :id) (collected? v (apply = v)) s/DISPENSE] data)))) (let [data {:active 3 :items [{:id 1 :val 0} {:id 3 :val 11}]}] (is (= (transform [:items s/ALL (s/selected? :id #(= % 3)) :val] inc data) (transform [(s/collect-one :active) :items s/ALL (s/collect-one :id) (collected? [a i] (= a i)) s/DISPENSE :val] inc data))))) (defspec traverse-test (for-all+ [v (gen/vector gen/int) p (gen/elements [odd? even?]) i gen/int] (and (= (reduce + (traverse [s/ALL p] v)) (reduce + (filter p v))) (= (reduce + i (traverse [s/ALL p] v)) (reduce + i (filter p v)))))) (def KeyAccumWalker (recursive-path [k] p (s/if-path (s/must k) s/STAY [s/ALL (s/collect-one s/FIRST) s/LAST p]))) (deftest recursive-if-path-select-vals-test (let [data {"e1" {"e2" {"e1" {:template 1} "e2" {:template 2}}}}] (is (= [["e1" "e2" "e1" {:template 1}] ["e1" "e2" "e2" {:template 2}]] (select (KeyAccumWalker :template) data))) (is (= {"e1" {"e2" {"e1" "e1e2e1" "e2" "e1e2e2"}}} (transform (KeyAccumWalker :template) (fn [& all] (apply str (butlast all))) data))))) (deftest multi-path-vals-test (is (= {:a 1 :b 6 :c 3} (transform [(s/multi-path (s/collect-one :a) (s/collect-one :c)) :b] + {:a 1 :b 2 :c 3}))) (is (= [[1 2] [3 2]] (select [(s/multi-path (s/collect-one :a) (s/collect-one :c)) :b] {:a 1 :b 2 :c 3})))) (deftest sorted-map-by-transform (let [amap (sorted-map-by > 1 10 2 20 3 30)] (is (= [3 2 1] (keys (transform s/MAP-VALS inc amap)))) (is (= [3 2 1] (keys (transform [s/ALL s/LAST] inc amap)))))) (deftest setval-vals-collection-test (is (= 2 (setval s/VAL 2 :a)))) (defspec multi-transform-test (for-all+ [kw1 gen/keyword kw2 gen/keyword m (limit-size 5 (gen-map-with-keys gen/keyword gen/int kw1 kw2))] (= (->> m (transform [(s/keypath kw1) s/VAL] +) (transform (s/keypath kw2) dec)) (multi-transform (s/multi-path [(s/keypath kw1) s/VAL (s/terminal +)] [(s/keypath kw2) (s/terminal dec)]) m)))) (deftest multi-transform-overrun-error (is (thrown? #?(:clj Exception :cljs js/Error) (multi-transform s/STAY 3)))) (deftest terminal-val-test (is (= 3 (multi-transform (s/terminal-val 3) 2))) (is (= 3 (multi-transform [s/VAL (s/terminal-val 3)] 2)))) (deftest multi-path-order-test (is (= 102 (multi-transform (s/multi-path [odd? (s/terminal #(* 2 %))] [even? (s/terminal-val 100)] [#(= 100 %) (s/terminal inc)] [#(= 101 %) (s/terminal inc)]) 1)))) (defdynamicnav ignorer [a] s/STAY) (deftest dynamic-nav-ignores-dynamic-arg (let [a 1] (is (= 1 (select-any (ignorer a) 1))) (is (= 1 (select-any (ignorer :a) 1))))) (deftest nested-dynamic-nav (let [data {:a {:a 1 :b 2} :b {:a 3 :b 4}} afn (fn [a b] (select-any (s/selected? (s/must a) (s/selected? (s/must b))) data))] (is (= data (afn :a :a))) (is (= s/NONE (afn :a :c))) (is (= data (afn :a :b))) (is (= s/NONE (afn :c :a))) (is (= data (afn :b :a))) (is (= data (afn :b :b))))) (deftest duplicate-map-keys-test (let [res (setval [s/ALL s/FIRST] "a" {:a 1 :b 2})] (is (= {"a" 2} res)) (is (= 1 (count res))))) (deftest inline-caching-vector-params-test (is (= [10 [11]] (multi-transform (s/terminal-val [10 [11]]) :a)))) (defn eachnav-fn-test [akey data] (select-any (s/keypath "a" akey) data)) (deftest eachnav-test (let [data {"a" {"b" 1 "c" 2}}] (is (= 1 (eachnav-fn-test "b" data))) (is (= 2 (eachnav-fn-test "c" data))) )) (deftest traversed-test (is (= 10 (select-any (s/traversed s/ALL +) [1 2 3 4])))) (defn- predand= [pred ret v] (and (pred ret) (= ret v))) (deftest nthpath-test (is (predand= vector? [1 2 -3 4] (transform (s/nthpath 2) - [1 2 3 4]))) (is (predand= vector? [1 2 4] (setval (s/nthpath 2) s/NONE [1 2 3 4]))) (is (predand= (complement vector?) '(1 -2 3 4) (transform (s/nthpath 1) - '(1 2 3 4)))) (is (predand= (complement vector?) '(1 2 4) (setval (s/nthpath 2) s/NONE '(1 2 3 4)))) (is (= [0 1 [2 4 4]] (transform (s/nthpath 2 1) inc [0 1 [2 3 4]]))) ) (deftest remove-with-NONE-test (is (predand= vector? [1 2 3] (setval [s/ALL nil?] s/NONE [1 2 nil 3 nil]))) (is (predand= list? '(1 2 3) (setval [s/ALL nil?] s/NONE '(1 2 nil 3 nil)))) (is (= {:b 2} (setval :a s/NONE {:a 1 :b 2}))) (is (= {:b 2} (setval (s/must :a) s/NONE {:a 1 :b 2}))) (is (predand= vector? [1 3] (setval (s/keypath 1) s/NONE [1 2 3]))) ;; test with PersistentArrayMap (is (= {:a 1 :c 3} (setval [s/MAP-VALS even?] s/NONE {:a 1 :b 2 :c 3 :d 4}))) (is (= {:a 1 :c 3} (setval [s/ALL (s/selected? s/LAST even?)] s/NONE {:a 1 :b 2 :c 3 :d 4}))) ;; test with PersistentHashMap (let [m (into {} (for [i (range 500)] [i i]))] (is (= (dissoc m 31) (setval [s/MAP-VALS #(= 31 %)] s/NONE m))) (is (= (dissoc m 31) (setval [s/ALL (s/selected? s/LAST #(= 31 %))] s/NONE m))) )) (deftest fresh-collected-test (let [data [{:a 1 :b 2} {:a 3 :b 3}]] (is (= [[{:a 1 :b 2} 2]] (select [s/ALL s/VAL (s/with-fresh-collected (s/collect-one :a) (s/collected? [a] (= a 1))) :b] data))) (is (= [{:a 1 :b 3} {:a 3 :b 3}] (transform [s/ALL s/VAL (s/with-fresh-collected (s/collect-one :a) (s/collected? [a] (= a 1))) :b] (fn [m v] (+ (:a m) v)) data ))) )) (deftest traverse-all-test (is (= 3 (transduce (comp (mapcat identity) (traverse-all :a)) (completing (fn [r i] (if (= i 4) (reduced r) (+ r i)))) 0 [[{:a 1}] [{:a 2}] [{:a 4}] [{:a 5}]]))) (is (= 6 (transduce (traverse-all [s/ALL :a]) + 0 [[{:a 1} {:a 2}] [{:a 3}]] ))) (is (= [1 2] (into [] (traverse-all :a) [{:a 1} {:a 2}]))) ) (deftest early-terminate-traverse-test (is (= 6 (reduce (completing (fn [r i] (if (> r 5) (reduced r) (+ r i)))) 0 (traverse [s/ALL s/ALL] [[1 2] [3 4] [5]]))))) (deftest select-any-vals-test (is (= [1 1] (select-any s/VAL 1)))) (deftest conditional-vals-test (is (= 2 (select-any (s/with-fresh-collected (s/collect-one (s/keypath 0)) (s/if-path (collected? [n] (even? n)) (s/keypath 1) (s/keypath 2))) [4 2 3]))) (is (= [4 2 3] (select-any (s/with-fresh-collected (s/collect-one (s/keypath 0)) (s/selected? (collected? [n] (even? n)))) [4 2 3]))) ) (deftest name-namespace-test (is (= :a (setval s/NAME "a" :e))) (is (= :a/b (setval s/NAME "b" :a/e))) (is (= 'a (setval s/NAME "a" 'e))) (is (= 'a/b (setval s/NAME "b" 'a/e))) (is (= :a/e (setval s/NAMESPACE "a" :e))) (is (= :a/e (setval s/NAMESPACE "a" :f/e))) (is (= 'a/e (setval s/NAMESPACE "a" 'e))) (is (= 'a/e (setval s/NAMESPACE "a" 'f/e))) ) (deftest string-navigation-test (is (= "ad" (setval (s/srange 1 3) "" "abcd"))) (is (= "abcxd" (setval [(s/srange 1 3) s/END] "x" "abcd"))) (is (= "bc" (select-any (s/srange 1 3) "abcd"))) (is (= "ab" (setval s/END "b" "a"))) (is (= "ba" (setval s/BEGINNING "b" "a"))) (is (= "" (select-any s/BEGINNING "abc"))) (is (= "" (select-any s/END "abc"))) (is (= \a (select-any s/FIRST "abc"))) (is (= \c (select-any s/LAST "abc"))) (is (= "qbc" (setval s/FIRST \q "abc"))) (is (= "abq" (setval s/LAST "q" "abc"))) ) (deftest single-value-none-navigators-test (is (predand= vector? [1 2 3] (setval s/AFTER-ELEM 3 [1 2]))) (is (predand= list? '(1 2 3) (setval s/AFTER-ELEM 3 '(1 2)))) (is (predand= list? '(1) (setval s/AFTER-ELEM 1 nil))) (is (predand= vector? [3 1 2] (setval s/BEFORE-ELEM 3 [1 2]))) (is (predand= list? '(3 1 2) (setval s/BEFORE-ELEM 3 '(1 2)))) (is (predand= list? '(1) (setval s/BEFORE-ELEM 1 nil))) (is (= #{1 2 3} (setval s/NONE-ELEM 3 #{1 2}))) (is (= #{1} (setval s/NONE-ELEM 1 nil))) ) (deftest subvec-test (let [v (subvec [1] 0)] (is (predand= vector? [2] (transform s/FIRST inc v))) (is (predand= vector? [2] (transform s/LAST inc v))) (is (predand= vector? [2] (transform s/ALL inc v))) (is (predand= vector? [0 1] (setval s/BEGINNING [0] v))) (is (predand= vector? [1 0] (setval s/END [0] v))) (is (predand= vector? [0 1] (setval s/BEFORE-ELEM 0 v))) (is (predand= vector? [1 0] (setval s/AFTER-ELEM 0 v))) (is (predand= vector? [1 0] (setval (s/srange 1 1) [0] v))) )) (defspec map-keys-all-first-equivalence-transform (for-all+ [m (limit-size 10 (gen/map gen/int gen/keyword))] (= (transform s/MAP-KEYS inc m) (transform [s/ALL s/FIRST] inc m ) ))) (defspec map-keys-all-first-equivalence-select (for-all+ [m (limit-size 10 (gen/map gen/int gen/keyword))] (= (select s/MAP-KEYS m) (select [s/ALL s/FIRST] m) ))) (defspec remove-first-vector (for-all+ [v (limit-size 10 (gen/not-empty (gen/vector gen/int)))] (let [newv (setval s/FIRST s/NONE v)] (and (= newv (vec (rest v))) (vector? newv) )))) (defspec remove-first-list (for-all+ [l (limit-size 10 (gen/not-empty (gen/list gen/int)))] (let [newl (setval s/FIRST s/NONE l)] (and (= newl (rest l)) (list? newl) )))) (defspec remove-last-vector (for-all+ [v (limit-size 10 (gen/not-empty (gen/vector gen/int)))] (let [newv (setval s/LAST s/NONE v)] (and (= newv (vec (butlast v))) (vector? newv) )))) (defspec remove-last-list (for-all+ [l (limit-size 10 (gen/not-empty (gen/list gen/int)))] (let [newl (setval s/LAST s/NONE l) bl (butlast l)] (and (or (= newl bl) (and (nil? bl) (= '() newl))) (seq? newl) )))) (deftest remove-extreme-string (is (= "b" (setval s/FIRST s/NONE "ab"))) (is (= "a" (setval s/LAST s/NONE "ab"))) ) (deftest nested-dynamic-arg-test (let [foo (fn [v] (multi-transform (s/terminal-val [v]) nil))] (is (= [1] (foo 1))) (is (= [10] (foo 10))) )) (deftest filterer-remove-test (is (= [1 :a 3 5] (setval (s/filterer even?) [:a] [1 2 3 4 5]))) ) (deftest helper-preds-test (let [data [1 2 2 3 4 0]] (is (= [2 2] (select [s/ALL (s/pred= 2)] data))) (is (= [1 2 2 0] (select [s/ALL (s/pred< 3)] data))) (is (= [1 2 2 3 0] (select [s/ALL (s/pred<= 3)] data))) (is (= [4] (select [s/ALL (s/pred> 3)] data))) (is (= [3 4] (select [s/ALL (s/pred>= 3)] data))) )) (deftest map-key-test (is (= {:c 3} (setval (s/map-key :a) :b {:c 3}))) (is (= {:b 2} (setval (s/map-key :a) :b {:a 2}))) (is (= {:b 2} (setval (s/map-key :a) :b {:a 2 :b 1}))) (is (= {:b 2} (setval (s/map-key :a) s/NONE {:a 1 :b 2}))) ) (deftest set-elem-test (is (= #{:b :d} (setval (s/set-elem :a) :x #{:b :d}))) (is (= #{:x :a} (setval (s/set-elem :b) :x #{:b :a}))) (is (= #{:a} (setval (s/set-elem :b) :a #{:b :a}))) (is (= #{:b} (setval (s/set-elem :a) s/NONE #{:a :b}))) ) ;; this function necessary to trigger the bug from happening (defn inc2 [v] (inc v)) (deftest dynamic-function-arg-test (is (= {[2] 4} (let [a 1] (transform (s/keypath [(inc2 a)]) inc {[2] 3})))) ) (defrecord FooW [a b]) (deftest walker-test (is (= [1 2 3 4 5 6] (select (s/walker number?) [{1 2 :b '(3 :c 4)} 5 #{6 :d}]))) (is (= [{:b '(:c)} #{:d}] (setval (s/walker number?) s/NONE [{:q 3 10 :l 1 2 :b '(3 :c 4)} 5 #{6 :d}]))) (is (= [{:q 4 11 :l 2 3 :b '(4 :c 5)} 6 #{7 :d}] (transform (s/walker number?) inc [{:q 3 10 :l 1 2 :b '(3 :c 4)} 5 #{6 :d}]))) (let [f (->FooW 1 2)] (is (= [[:a 1] [:b 2]] (select (s/walker (complement record?)) f))) (is (= (assoc f :a! 1 :b! 2) (setval [(s/walker (complement record?)) s/FIRST s/NAME s/END] "!" f))) (is (= (assoc f :b 1 :c 2) (transform [(s/walker (complement record?)) s/FIRST] (fn [k] (if (= :a k) :b :c)) f))) )) (def MIDDLE (s/comp-paths (s/srange-dynamic (fn [aseq] (long (/ (count aseq) 2))) (end-fn [aseq s] (if (empty? aseq) 0 (inc s)))) s/FIRST )) (deftest srange-dynamic-test (is (= 2 (select-any MIDDLE [1 2 3]))) (is (identical? s/NONE (select-any MIDDLE []))) (is (= 1 (select-any MIDDLE [1]))) (is (= 2 (select-any MIDDLE [1 2]))) (is (= [1 3 3] (transform MIDDLE inc [1 2 3]))) ) #?(:clj (do (defprotocolpath FooPP) (extend-protocolpath FooPP String s/STAY) (deftest satisfies-protpath-test (is (satisfies-protpath? FooPP "a")) (is (not (satisfies-protpath? FooPP 1))) ))) specter-1.0.2/test/com/rpl/specter/test_helpers.clj000066400000000000000000000027211311753541400223570ustar00rootroot00000000000000(ns com.rpl.specter.test-helpers (:require [clojure.test.check [generators :as gen] [properties :as prop]] [clojure.test]) (:use [com.rpl.specter :only [select transform]] [com.rpl.specter :only [select* transform*]])) ;; it seems like gen/bind and gen/return are a monad (hence the names) ;; this is only for clj (cljs version in different file) (defmacro for-all+ [bindings & body] (let [parts (partition 2 bindings) vars (vec (map first parts)) genned (reduce (fn [curr [v code]] `(gen/bind ~code (fn [~v] ~curr))) `(gen/return ~vars) (reverse parts))] `(prop/for-all [~vars ~genned] ~@body))) (defmacro ic-test [params-decl apath transform-fn data params] (let [platform (if (contains? &env :locals) :cljs :clj) is-sym (if (= platform :clj) 'clojure.test/is 'cljs.test/is)] `(let [icfnsel# (fn [~@params-decl] (select ~apath ~data)) icfntran# (fn [~@params-decl] (transform ~apath ~transform-fn ~data)) regfnsel# (fn [~@params-decl] (select* ~apath ~data)) regfntran# (fn [~@params-decl] (transform* ~apath ~transform-fn ~data)) params# (if (empty? ~params) [[]] ~params)] (dotimes [_# 3] (doseq [ps# params#] (~is-sym (= (apply icfnsel# ps#) (apply regfnsel# ps#))) (~is-sym (= (apply icfntran# ps#) (apply regfntran# ps#)))))))) specter-1.0.2/test/com/rpl/specter/zipper_test.cljc000066400000000000000000000070511311753541400223720ustar00rootroot00000000000000(ns com.rpl.specter.zipper-test #?(:cljs (:require-macros [cljs.test :refer [is deftest]] [clojure.test.check.clojure-test :refer [defspec]] [com.rpl.specter.cljs-test-helpers :refer [for-all+]] [com.rpl.specter :refer [declarepath providepath select select-one select-one! select-first transform setval replace-in]])) (:use #?(:clj [clojure.test :only [deftest is]]) #?(:clj [clojure.test.check.clojure-test :only [defspec]]) #?(:clj [com.rpl.specter.test-helpers :only [for-all+]]) #?(:clj [com.rpl.specter :only [declarepath providepath select select-one select-one! select-first transform setval replace-in]])) (:require #?(:clj [clojure.test.check.generators :as gen]) #?(:clj [clojure.test.check.properties :as prop]) #?(:cljs [clojure.test.check :as tc]) #?(:cljs [clojure.test.check.generators :as gen]) #?(:cljs [clojure.test.check.properties :as prop :include-macros true]) [com.rpl.specter :as s] [com.rpl.specter.zipper :as z])) (defspec zipper-end-equivalency-test (for-all+ [v (gen/not-empty (gen/vector gen/int)) i (gen/vector gen/int)] (= (setval s/END i v) (setval [z/VECTOR-ZIP z/DOWN z/RIGHTMOST z/INNER-RIGHT] i v)))) (deftest zipper-multi-insert-test (is (= [1 2 :a :b 3 :a :b 4] (setval [z/VECTOR-ZIP z/DOWN z/RIGHT z/RIGHT (s/multi-path z/INNER-RIGHT z/INNER-LEFT)] [:a :b] [1 2 3 4]) (setval [z/VECTOR-ZIP z/DOWN z/RIGHT z/RIGHT (s/multi-path z/INNER-LEFT z/INNER-RIGHT)] [:a :b] [1 2 3 4])))) (deftest zipper-down-up-test (is (= [1 [2 3 5] 6] (transform [z/VECTOR-ZIP z/DOWN z/RIGHT z/DOWN z/RIGHT z/RIGHT (s/multi-path s/STAY [z/UP z/RIGHT]) z/NODE] inc [1 [2 3 4] 5])))) (deftest next-terminate-test (is (= [2 [3 4 [5]] 6] (transform [z/VECTOR-ZIP z/NEXT-WALK z/NODE number?] inc [1 [2 3 [4]] 5]))) (is (= [1 [3 [[]] 5]] (setval [z/VECTOR-ZIP z/NEXT-WALK (s/selected? z/NODE number? even?) z/NODE-SEQ] [] [1 2 [3 [[4]] 5] 6])))) (deftest zipper-nav-stop-test (is (= [1] (transform [z/VECTOR-ZIP z/UP z/NODE] inc [1]))) (is (= [1] (transform [z/VECTOR-ZIP z/DOWN z/LEFT z/NODE] inc [1]))) (is (= [1] (transform [z/VECTOR-ZIP z/DOWN z/RIGHT z/NODE] inc [1]))) (is (= [] (transform [z/VECTOR-ZIP z/DOWN z/NODE] inc [])))) (deftest find-first-test (is (= [1 [3 [[4]] 5] 6] (setval [z/VECTOR-ZIP (z/find-first #(and (number? %) (even? %))) z/NODE-SEQ] [] [1 2 [3 [[4]] 5] 6])))) (deftest nodeseq-expand-test (is (= [2 [2] [[4 4 4]] 4 4 4 6] (transform [z/VECTOR-ZIP z/NEXT-WALK (s/selected? z/NODE number? odd?) (s/collect-one z/NODE) z/NODE-SEQ] (fn [v _] (repeat v (inc v))) [1 [2] [[3]] 3 6]))))