├── .editorconfig ├── .gitignore ├── .rspec ├── .travis.yml ├── .yardopts ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CREDITS ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── VERSION ├── bin ├── console └── setup ├── lib ├── sycamore.rb └── sycamore │ ├── absence.rb │ ├── exceptions.rb │ ├── extension.rb │ ├── extension │ ├── nothing.rb │ ├── path.rb │ └── tree.rb │ ├── nothing.rb │ ├── path.rb │ ├── path_root.rb │ ├── stree.rb │ ├── tree.rb │ └── version.rb ├── spec ├── spec_helper.rb ├── support │ └── matchers │ │ ├── be_path_of.rb │ │ └── include_tree_part.rb ├── sycamore_spec.rb └── unit │ └── sycamore │ ├── absence_spec.rb │ ├── nothing_spec.rb │ ├── path_root_spec.rb │ ├── path_spec.rb │ ├── tree │ ├── access_spec.rb │ ├── addition_spec.rb │ ├── comparison_spec.rb │ ├── conversion_spec.rb │ ├── deletion_spec.rb │ └── enumeration_spec.rb │ └── tree_spec.rb ├── support ├── doctest_helper.rb └── travis.sh └── sycamore.gemspec /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.rb] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.gemspec] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.yml] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [*.md] 22 | indent_style = space 23 | indent_size = 2 24 | trim_trailing_whitespace = false 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /.ruby-version 5 | /_yardoc/ 6 | /coverage/ 7 | /doc 8 | /pkg/ 9 | /spec/reports/ 10 | /spec/examples.txt 11 | /tmp/ 12 | 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | #--format documentation 3 | #--format progress 4 | #--format Fuubar 5 | #--fail-fast 6 | --require spec_helper 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | bundler_args: --without debug 3 | rvm: 4 | - 2.2.4 5 | - 2.3.1 6 | - 2.4.1 7 | - 2.5.1 8 | - ruby-head 9 | - jruby-head 10 | - rbx-3 11 | cache: bundler 12 | before_install: 13 | - gem update bundler 14 | before_script: 15 | - support/travis.sh 16 | dist: trusty 17 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title "Sycamore" 2 | --output-dir doc/yard 3 | --protected 4 | --private 5 | --no-private 6 | --readme README.md 7 | - 8 | AUTHORS 9 | CREDITS 10 | LICENSE.txt 11 | CHANGELOG.md 12 | CONTRIBUTING.md 13 | VERSION 14 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | - Marcel Otto 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/) and 5 | [Keep a CHANGELOG](http://keepachangelog.com). 6 | 7 | 8 | ## 0.3.1 - 2016-05-07 9 | 10 | ### Added 11 | 12 | - array-access operator and `fetch` on `Path` for random access 13 | 14 | ### Changed 15 | 16 | - Lazy initialization of the internal hash. This improves extensibility, eg. by 17 | not requiring a `super` call in the constructors of `Tree` subclasses. 18 | 19 | 20 | [Compare v0.3.0...v0.3.1](https://github.com/marcelotto/sycamore/compare/v0.3.0...v0.3.1) 21 | 22 | 23 | 24 | ## 0.3.0 - 2016-04-23 25 | 26 | ### Added 27 | 28 | - support `Path` objects as input on the following `Tree` methods: 29 | - the `Tree.[]` population constructor 30 | - `fetch` 31 | - `add` 32 | - `delete` 33 | - `replace` 34 | - `[]=` 35 | - `include_node?` 36 | - `leaf?` 37 | - `strict_leaf?` 38 | - `strict_leaves?` 39 | - `internal?` 40 | - `external?` 41 | - `Tree#fetch_path` for fetching a child by path 42 | 43 | ### Fixed 44 | 45 | - `Tree#add` or `Tree#delete` now fail without making any changes, when given 46 | invalid input. Previously these command methods performed their operations 47 | until the invalid input elements were encountered. 48 | - `Tree#delete` deleted paths, when they matched a given input path partially, 49 | e.g. `Tree[a: 1] >> a: {1 => 2}` deleted successfully. 50 | 51 | 52 | [Compare v0.2.1...v0.3.0](https://github.com/marcelotto/sycamore/compare/v0.2.1...v0.3.0) 53 | 54 | 55 | 56 | ## 0.2.1 - 2016-04-07 57 | 58 | ### Added 59 | 60 | - assigning `nil` via `Tree#[]=` removes a child tree, similar to the assignment 61 | of `Sycamore::Nothing` 62 | 63 | ### Fixed 64 | 65 | - [#2](https://github.com/marcelotto/sycamore/issues/2): Rubinius support 66 | 67 | 68 | [Compare v0.2.0...v0.2.1](https://github.com/marcelotto/sycamore/compare/v0.2.0...v0.2.1) 69 | 70 | 71 | 72 | ## 0.2.0 - 2016-04-05 73 | 74 | ### Added 75 | 76 | - assigning `Sycamore::Nothing` via `Tree#[]=` removes a child tree 77 | - `Tree#search` for searching the tree for one or multiple nodes or a tree 78 | - `Tree#node!` as a more strict variant of `Tree#node`, which raises an error 79 | when no node present 80 | 81 | 82 | [Compare v0.1.0...v0.2.0](https://github.com/marcelotto/sycamore/compare/v0.1.0...v0.2.0) 83 | 84 | 85 | 86 | ## 0.1.0 - 2016-03-28 87 | 88 | Initial release 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 1. Fork it ( ) 2 | 2. Create your feature branch (`git checkout -b my-new-feature`) 3 | 3. Commit your changes (`git commit -am 'Add some feature'`) 4 | 4. Push to the branch (`git push origin my-new-feature`) 5 | 5. Create a new Pull Request 6 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcelotto/sycamore/cb712faf7df12fde9b5f1cb7dfd9173aae9c449c/CREDITS -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :debug do 6 | gem 'guard-rspec' 7 | gem 'pry' 8 | end 9 | 10 | group :test do 11 | gem 'coveralls', require: false, platform: :mri 12 | end 13 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # -*- ruby -*- 2 | 3 | guard 'rspec', cmd: 'bundle exec rspec', all_after_pass: false do 4 | watch(%r{^spec/.+_spec\.rb$}) 5 | watch('spec/spec_helper.rb') { 'spec' } 6 | watch(/spec\/support\/(.+)\.rb/) { 'spec' } 7 | 8 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" } 9 | watch(%r{^lib/sycamore/(.+)\.rb$}) { |m| "spec/unit/sycamore/#{m[1]}/" } 10 | 11 | end 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2016 Marcel Otto 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Sycamore 3 | 4 | > _"The Egyptians' Holy Sycamore also stood on the threshold of life and death, connecting the two worlds."_ 5 | > -- [Wikipedia: Tree of Life](http://en.wikipedia.org/wiki/Tree_of_life) 6 | 7 | [![Gem Version](https://badge.fury.io/rb/sycamore.svg)](http://badge.fury.io/rb/sycamore) 8 | [![Travis CI Build Status](https://secure.travis-ci.org/marcelotto/sycamore.png)](https://travis-ci.org/marcelotto/sycamore?branch=master) 9 | [![Coverage Status](https://coveralls.io/repos/marcelotto/sycamore/badge.png)](https://coveralls.io/r/marcelotto/sycamore) 10 | [![Inline docs](http://inch-ci.org/github/marcelotto/sycamore.png)](http://inch-ci.org/github/marcelotto/sycamore) 11 | [![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://rubydoc.org/gems/sycamore/frames) 12 | [![Gitter Chat](http://img.shields.io/badge/chat-gitter.im-orange.svg)](https://gitter.im/marcelotto/sycamore) 13 | [![License](http://img.shields.io/license/MIT.png?color=green)](http://opensource.org/licenses/MIT) 14 | 15 | **Sycamore is an implementation of an unordered tree data structure.** 16 | 17 | Features: 18 | 19 | - easy, hassle-free access to arbitrarily deep nested elements 20 | - grows automatically when needed 21 | - familiar Hash interface 22 | - no more `nil`-induced errors 23 | 24 | Imagine a Sycamore tree as a recursively nested set. The elements of this set, called nodes, are associated with a child tree of additional nodes and so on. This might be different to your usual understanding of a tree, which has to have one single root node, but this notion is much more general. The usual tree is just a special case with just one node at the first level. But I prefer to think of the root to be implicit. Effectively every object is a tree in this sense. You can assume `self` to be the implicit root. 25 | 26 | Restrictions: 27 | 28 | - Only values you would use as keys of a hash should be used as nodes of a Sycamore tree. Although Ruby's official Hash documentation says *a Hash allows you to use any object type*, one is well advised [to use immutable objects only](http://jafrog.com/2012/10/07/mutable-objects-as-hash-keys-in-ruby.html). Enumerables as nodes are explicitly excluded by Sycamore. 29 | - The nodes are unordered and can't contain duplicates. 30 | - A Sycamore tree is uni-directional, i.e. has no relationship to its parent. 31 | 32 | ## Why 33 | 34 | Trees in the sense of recursively nested sets are omnipresent today. But why then are there so few implementations of tree data structures? The answer is simple: because of Ruby's powerful built-in hashes. The problem is that while Ruby's Hash, as an implementation of the [Hash map data structure](https://en.wikipedia.org/wiki/Hash_table), might be perfectly fine for flat dictionary like structures, it is not very well-suited for storing tree structures. Ruby's hash literals, which allow it to easily nest multiple hashes, belie this fact. But it catches up when you want to build up a tree with hashes dynamically and have to manage the hash nesting manually. 35 | 36 | In contrast to the few existing implementations of tree data structures in Ruby, Sycamores is based on Ruby's very efficient hashes and contains the values directly without any additional overhead. It only wraps the hashes itself. This wrapper object is very thin, containing nothing more than the hash itself. This comes at the price of the aforementioned restrictions, prohibiting it to be a general applicable tree implementation. 37 | 38 | Another compelling reason for the use of Sycamore is its handling of `nil`. Much has [been](https://www.youtube.com/watch?v=OMPfEXIlTVE) [said](http://programmers.stackexchange.com/questions/12777/are-null-references-really-a-bad-thing) about the problem of `nil` (or equivalent null-values in other languages), including: ["It was my Billion-dollar mistake"](http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare) from its founder, Tony Hoare. Every developer has experienced it in the form of errors such as 39 | 40 | ``` 41 | NoMethodError: undefined method '[]' for nil:NilClass 42 | ``` 43 | 44 | With Sycamore this is a thing of the past. 45 | 46 | 47 | ## Supported Ruby versions 48 | 49 | - MRI >= 2.1 50 | - JRuby 51 | - Rubinius 52 | 53 | 54 | ## Dependencies 55 | 56 | - none 57 | 58 | ## Installation 59 | 60 | The recommended installation method is via [RubyGems](http://rubygems.org/). 61 | 62 | $ gem install sycamore 63 | 64 | 65 | ## Usage 66 | 67 | I will introduce Sycamore's Tree API by comparing it with [Ruby's Hash API](http://ruby-doc.org/core-2.2.3/Hash.html). 68 | 69 | In the following I'll always write `Tree` for the Sycamore tree class, instead of the fully qualified `Sycamore::Tree`. By default, this global `Tree` constant is not available. If you want this, you'll have to 70 | 71 | ```ruby 72 | require 'sycamore/extension' 73 | ``` 74 | 75 | When you can't or don't to want to have the `Tree` alias constant in the global namespace, but still want a short alternative name, you can alternatively 76 | 77 | ```ruby 78 | require 'sycamore/stree' 79 | ``` 80 | 81 | to get an alias constant `STree` with less potential for conflicts. 82 | 83 | I recommend trying the following code yourself in a Ruby REPL like [Pry](http://pryrepl.org). 84 | 85 | 86 | ### Creating trees 87 | 88 | A `Sycamore::Tree` can be created similar to Hashes with the standard constructor or the class-level `[]` operator. 89 | 90 | `Tree.new` creates an empty `Sycamore::Tree`. 91 | 92 | ```ruby 93 | tree = Tree.new 94 | tree.empty? # => true 95 | ``` 96 | 97 | No additional arguments are supported at the time. As you'll see, for a `Sycamore::Tree` the functionality of the Hash constructor to specify the default value behaviour is of too little value to justify its use in the default constructor, so I'd like to reserve them for something more useful. 98 | 99 | The `[]` operator creates a new `Tree` and adds the arguments as its initial input. It can handle a single node value, a collection of nodes or a complete tree. 100 | 101 | ```ruby 102 | Tree[1] # => #n/a}> 103 | Tree[1, 2, 3] # => #n/a, 2=>n/a, 3=>n/a}> 104 | Tree[1, 2, 2, 3] # => #n/a, 2=>n/a, 3=>n/a}> 105 | Tree[x: 1, y: 2] # => #1, :y=>2}> 106 | ``` 107 | 108 | As you can see in line 3 nodes are stored as a set, i.e. with duplicates removed. 109 | 110 | Note that multiple arguments are not interpreted as an associative array as `Hash[]` does, but rather as a set of leaves, i.e. nodes without children. 111 | 112 | ```ruby 113 | Hash[1, 2, 3, 4] # => {1=>2, 3=>4} 114 | Hash[1, 2, 3] # => ArgumentError: odd number of arguments for Hash 115 | ``` 116 | 117 | You can also see that children of leaves, i.e. nodes without children, are signified with `n/a`. When providing input data with Hashes, you can use `nil` as the child value of a leaf. 118 | 119 | ```ruby 120 | Tree[x: 1, y: 2, z: nil] 121 | # => #1, :y=>2, :z=>n/a}> 122 | ``` 123 | 124 | In general the `nil` child value for leaves in Hash literals is mandatory, but on the first level it can be ommitted, by providing the leaves as an argument before the non-leaf nodes. 125 | 126 | ```ruby 127 | Tree[:a, :b, c: {d: 1, e: nil}] 128 | # => #n/a, :b=>n/a, :c=>{:d=>1, :e=>n/a}}> 129 | ``` 130 | 131 | If you really want to have a node with `nil` as a child, you'll have to put the `nil` in an array. 132 | 133 | ```ruby 134 | Tree[x: 1, y: 2, z: [nil]] 135 | # => #1, :y=>2, :z=>nil}> 136 | ``` 137 | 138 | 139 | ### Accessing trees 140 | 141 | Access to elements of a `Sycamore::Tree` is mostly API-compatible to that of Rubys Hash class. But there is one major difference in the return type of most of the access methods: Since we are dealing with a recursively defined tree structure, the returned children are always trees as well. 142 | 143 | The main method for accessing a tree is the `[]` operator. 144 | 145 | ```ruby 146 | tree = Tree[x: 1, y: {2 => "a"}] 147 | 148 | tree[:x] # => #n/a}> 149 | tree[:y] # => #"a"}> 150 | tree[:y][2] # => #n/a}> 151 | ``` 152 | 153 | The actual nodes of a tree can be retrieved with the method `nodes`. 154 | 155 | ```ruby 156 | tree.nodes # => [:x, :y] 157 | tree[:x].nodes # => [1] 158 | tree[:y].nodes # => [2] 159 | tree[:y][2].nodes # => ["a"] 160 | ``` 161 | 162 | If it's certain that a tree has at most one element, you can also use `node` to get that node directly. 163 | 164 | ```ruby 165 | tree[:y].node # => 2 166 | tree[:y][2].node # => "a" 167 | tree[:x][1].node # => nil 168 | tree.node # Sycamore::NonUniqueNodeSet: multiple nodes present: [:x, :y] 169 | ``` 170 | 171 | The bang variant `node!` raises an error when the node set is empty, instead of returning `nil`. 172 | 173 | ```ruby 174 | tree[:y][2].node! # => "a" 175 | tree[:x][1].node! # => # Sycamore::EmptyNodeSet: no node present 176 | ``` 177 | 178 | As opposed to Hash, the `[]` operator of `Sycamore::Tree` also supports multiple arguments which get interpreted as a path. 179 | 180 | ```ruby 181 | tree[:y, 2].node # => "a" 182 | ``` 183 | 184 | For compatibility with Ruby 2.3 Hashes, this can also be done with the `dig` method. 185 | 186 | ```ruby 187 | tree.dig(:y, 2).node # => "a" 188 | ``` 189 | 190 | `fetch`, as a more controlled way to access the elements, is also supported. 191 | 192 | ```ruby 193 | tree.fetch(:x) # => #n/a}> 194 | tree.fetch(:z) # => KeyError: key not found: :z 195 | tree.fetch(:z, :default) # => :default 196 | tree.fetch(:z) { :default } # => :default 197 | ``` 198 | 199 | Fetching the child of a leaf behaves almost the same as fetching the child of a non-existing node, i.e. the default value is returned or a `KeyError` gets raised. In order to differentiate these cases, a `Sycamore::ChildError` as a subclass of `KeyError` is raised when accessing the child of a leaf. 200 | 201 | `fetch_path` allows a `dig` similar access with `fetch` semantics, except it requires the path of nodes to be given as an Enumerable. 202 | 203 | ```ruby 204 | tree.fetch_path([:y, 2]).node # => "a" 205 | tree.fetch_path([:y, 3]) # => KeyError: key not found: 3 206 | tree.fetch_path([:y, 3], :default) # => :default 207 | tree.fetch_path([:y, 3]) { :default } # => :default 208 | ``` 209 | 210 | The number of nodes of a tree can be determined with `size`. This will only count direct nodes. 211 | 212 | ```ruby 213 | tree.size # => 2 214 | ``` 215 | 216 | `total_size` or its short alias `tsize` returns the total number of nodes of a tree, including the nodes of children. 217 | 218 | ```ruby 219 | tree.total_size # => 5 220 | tree[:y].tsize # => 2 221 | ``` 222 | 223 | The height of a tree, i.e. the length of its longest path can be computed with the method `height`. 224 | 225 | ```ruby 226 | tree.height # => 3 227 | ``` 228 | 229 | `empty?` checks if a tree is empty. 230 | 231 | ```ruby 232 | tree.empty? # => false 233 | tree[:x, 1].empty? # => true 234 | ``` 235 | 236 | `leaf?` checks if a node is a leaf. 237 | 238 | ```ruby 239 | tree.leaf? :x # => false 240 | tree[:x].leaf? 1 # => true 241 | ``` 242 | 243 | `leaves?` (or one of its aliases `external?` and `flat?`) can be used to determine this for more nodes at once. 244 | 245 | ```ruby 246 | Tree[1, 2, 3].leaves?(1, 2) # => true 247 | ``` 248 | 249 | Without any arguments `leaves?` returns whether all nodes of a tree are leaves. 250 | 251 | ```ruby 252 | Tree[1, 2].leaves? # => true 253 | ``` 254 | 255 | `include?` checks whether one or more nodes are in the set of nodes of this tree. 256 | 257 | ```ruby 258 | tree.include? :x # => true 259 | tree.include? [:x, :y] # => true 260 | ``` 261 | 262 | `include?` can also check whether a tree structure (incl. a hash) is a sub tree of a `Sycamore::Tree`. 263 | 264 | ```ruby 265 | tree.include?(x: 1, y: 2) # => true 266 | ``` 267 | 268 | `to_h` returns the tree as a Hash. 269 | 270 | ```ruby 271 | tree.to_h # => {:x=>1, :y=>{2=>"a"}} 272 | ``` 273 | 274 | ### Accessing absent trees 275 | 276 | There is another major difference in the access method behaviour of a Scyamore tree in comparison to hashes: The child access methods even return a tree when it does not exist. When you ask a hash for a non-existent element with the `[]` operator, you'll get a `nil`, which is an incarnation of the null-problem and the cause of many bug tracking sessions. 277 | 278 | ```ruby 279 | hash = {x: 1, y: {2 => "a"}} 280 | hash[:z] # => nil 281 | hash[:z][3] # => NoMethodError: undefined method `[]' for nil:NilClass 282 | ``` 283 | 284 | Sycamore on the other side returns a special tree, the `Nothing` tree: 285 | 286 | ```ruby 287 | tree = Tree[x: 1, y: {2 => "a"}] 288 | tree[:z] # => # 289 | tree[:z][3] # => # 290 | ``` 291 | 292 | `Sycamore::Nothing` is a singleton `Tree` implementing a [null object](https://en.wikipedia.org/wiki/Null_Object_pattern). It behaves on every query method call like an empty tree. 293 | 294 | ```ruby 295 | Sycamore::Nothing.empty? # => true 296 | Sycamore::Nothing.size # => 0 297 | Sycamore::Nothing[42] # => # 298 | ``` 299 | 300 | Sycamore adheres to a strict [command-query-separation (CQS)](https://en.wikipedia.org/wiki/Command%E2%80%93query_separation). A method is either a command changing the state of the tree and returning `self` or a query method, which only computes and returns the results of the query, but leaves the state unchanged. The only exception to this strict separation is made, when it is necessary in order to preserve Hash compatibility. All query methods are supported by the `Sycamore::Nothing` tree with empty tree semantics. 301 | 302 | Among the command methods are two subclasses: additive command methods, which add elements and destructive command methods, which remove elements. These are further refined into pure additive and pure destructive command methods, which either support additions or deletions only, not both operations at once. The `Sycamore::Tree` extends Ruby's reflection API with class methods to retrieve the respective methods: `query_methods`, `command_methods`, `additive_command_methods`, `destructive_command_methods`, `pure_additive_command_methods`, `pure_destructive_command_methods`. 303 | 304 | ```ruby 305 | Tree.command_methods 306 | # => [:add, :<<, :replace, :create_child, :[]=, :delete, :>>, :clear, :compact, :replace, :[]=, :freeze] 307 | Tree.additive_command_methods 308 | # => [:add, :<<, :replace, :create_child, :[]=] 309 | Tree.pure_additive_command_methods 310 | # => [:add, :<<, :create_child] 311 | Tree.pure_destructive_command_methods 312 | # => [:delete, :>>, :clear, :compact] 313 | ``` 314 | 315 | Pure destructive command methods on `Sycamore::Nothing` are no-ops. All other command methods raise an exception. 316 | 317 | ```ruby 318 | Sycamore::Nothing.clear # => # 319 | Sycamore::Nothing[:foo] = :bar 320 | # => Sycamore::NothingMutation: attempt to change the Nothing tree 321 | ``` 322 | 323 | But inspecting the `Nothing` tree returned by `Tree#[]` further shows, that this isn't the end of the story. 324 | 325 | ```ruby 326 | tree[:z].inspect 327 | # => absent child of node :z in #1, :y=>{2=>"a"}}> 328 | tree[:z][3].inspect 329 | # => absent child of node 3 in absent child of node :z in #1, :y=>{2=>"a"}}> 330 | ``` 331 | 332 | We'll actually get an `Absence` object, a [proxy object](https://en.wikipedia.org/wiki/Proxy_pattern) for the requested not yet existing tree. As long as we don't try to change it, this `Absence` object delegates all method calls to `Sycamore::Nothing`. But as soon as we call a non-pure-destructive command method, the missing tree will be created, added to the parent tree and the method call gets delegated to the now existing tree. 333 | 334 | ```ruby 335 | tree[:z] = 3 336 | tree.to_h # => {:x=>1, :y=>{2=>"a"}, :z=>3} 337 | ``` 338 | 339 | So a `Sycamore::Tree` is a tree, on which the nodes grow automatically, but only when needed. And this works recursively on arbitrarily deep nested absent trees. 340 | 341 | ```ruby 342 | tree[:some][:very][:deep] = :node 343 | tree.to_h # => {:x=>1, :y=>{2=>"a"}, :z=>3, :some=>{:very=>{:deep=>:node}}} 344 | ``` 345 | 346 | In order to determine whether a node has no children, you can simply use `empty?`. 347 | 348 | ```ruby 349 | tree = Tree[a: 1] 350 | tree[:a].empty? # => false 351 | tree[:b].empty? # => true 352 | ``` 353 | 354 | But how can you distinguish an empty from a missing tree? 355 | 356 | ```ruby 357 | user = Tree[name: 'Adam', shopping_cart_items: []] 358 | 359 | user[:shopping_cart_items].empty? # => true 360 | user[:foo].empty? # => true 361 | ``` 362 | 363 | One way is the use of the `absent?` method, which only returns `true` on an `Absence` object. 364 | 365 | ```ruby 366 | user[:shopping_cart_items].absent? # => false 367 | user[:foo].absent? # => true 368 | ``` 369 | 370 | Another possibility, without the need to create the `Absence` in the first place is the `leaf?` method, since it also checks for the presence of a node. 371 | 372 | ```ruby 373 | user.leaf? :shopping_cart_items # => true 374 | user.leaf? :foo # => false 375 | ``` 376 | 377 | But the `leaf?` method has as similar problem in this respect: it doesn't differentiate between absent and empty children. 378 | 379 | ```ruby 380 | tree = Tree[foo: nil, bar: []] 381 | tree.leaf? :foo # => true 382 | tree.leaf? :bar # => true 383 | ``` 384 | 385 | `strict_leaf?` and `strict_leaves?` (or their short aliases `sleaf?` and `sleaves?`) are more strict in this regard: when a node has an empty child tree it is considered a leaf, but not a strict leaf. 386 | 387 | ```ruby 388 | tree.strict_leaf? :foo # => true 389 | tree.strict_leaf? :bar # => false 390 | ``` 391 | 392 | Besides `absent?`, the congeneric methods `blank?` (as an alias of `empty?`) and its negation `present?` are ActiveSupport compatible available. Unfortunately, the natural expectation of `Tree#present?` and `Tree#absent?` to be mutually opposed leads astray. 393 | 394 | ```ruby 395 | user[:shopping_cart_items].absent? # => false 396 | user[:shopping_cart_items].present? # => false 397 | ``` 398 | 399 | The risks rising from an ActiveSupport incompatible `present?` is probably greater then this inconsistence. So, if you want check if a tree is not absent, use `existent?` as the negation of `absent?`. 400 | 401 | Beside these options, `fetch` is also a method to handle this situation in a nuanced way. 402 | 403 | ```ruby 404 | user.fetch(:shopping_cart_items) # => # 405 | user.fetch(:foo) 406 | # => KeyError: key not found: :foo 407 | user.fetch(:foo, :default) # => :default 408 | ``` 409 | 410 | Empty child trees also play a role when determining equality. The `eql?` and `==` equivalence differ exactly in their handling of this question: `==` treats empty child trees as absent trees, while `eql?` doesn't. 411 | 412 | ```ruby 413 | Tree[:foo].eql? Tree[foo: []] # => false 414 | Tree[:foo] == Tree[foo: []] # => true 415 | ``` 416 | 417 | All empty child trees can be removed with `compact`. 418 | 419 | ```ruby 420 | Tree[:foo].eql? Tree[foo: []].compact # => true 421 | ``` 422 | 423 | An arbitrary structure can be compared with a `Sycamore::Tree` for equality with `===`. 424 | 425 | ```ruby 426 | Tree[:foo, :bar] === [:foo, :bar] # => true 427 | Tree[:foo, :bar] === Set[:foo, :bar] # => true 428 | Tree[:foo => :bar] === {:foo => :bar} # => true 429 | ``` 430 | 431 | 432 | ### Changing trees 433 | 434 | Let's examine the command methods to change the contents of a tree. The `add` method or the `<<` operator as its alias allows the addition of one, multiple or a tree structure of nodes. 435 | 436 | ```ruby 437 | tree = Tree.new 438 | tree << 1 439 | tree << [2, 3] 440 | tree << {3 => :a, 4 => :b} 441 | puts tree 442 | > Tree[1=>nil, 2=>nil, 3=>:a, 4=>:b] 443 | ``` 444 | 445 | The `[]=` operator is Hash-compatible supported. 446 | 447 | ```ruby 448 | tree[5] = :c 449 | puts tree 450 | > Tree[1=>nil, 2=>nil, 3=>:a, 4=>:b, 5=>:c] 451 | ``` 452 | 453 | Note that this is just an `add` with a previous call of `clear`, which deletes all elements of the tree. This means, you can safely assign another tree without having to think about object identity. 454 | 455 | If you want to explicitly state, that a node doesn't have any children, you can specify it in the following equivalent ways. 456 | 457 | ```ruby 458 | tree[:foo] = [] 459 | tree[:foo] = {} 460 | ``` 461 | 462 | To remove a child tree entirely, you can assign `Nothing` or `nil` to the parent node. 463 | 464 | ```ruby 465 | tree[:foo] = Nothing 466 | tree[:foo] = nil 467 | ``` 468 | 469 | If you really want to overwrite the current child nodes with a single `nil` node, you have to do it in the following way. 470 | 471 | ```ruby 472 | tree[:foo] = [nil] 473 | ``` 474 | 475 | Note that all of these values are interpreted consistently inside input tree structures on creation, addition, deletion etc., i.e. empty Enumerables become empty child trees, `Nothing` or `nil` are used as place holders for the explicit negation of a child and `[nil]` is used for a child trees with a single `nil` node. 476 | 477 | ```ruby 478 | puts Tree[ a: { b: nil }, c: { d: []}, d: [nil] ] 479 | >Tree[:a=>:b, :c=>{:d=>[]}, :d=>[nil]] 480 | ``` 481 | 482 | Beside the deletion of all elements with the already mentioned `clear` method, single or multiple nodes and entire tree structures can be removed with `delete` or the `>>` operator. 483 | 484 | ```ruby 485 | tree >> 1 486 | tree >> [2, 3] 487 | tree >> {4 => :b} 488 | puts tree 489 | > Tree[5=>:c, :foo=>[]] 490 | ``` 491 | 492 | When removing a tree structure, only child trees with no more existing nodes get deleted. 493 | 494 | ```ruby 495 | tree = Tree[a: [1,2]] 496 | tree >> {a: 1} 497 | puts tree 498 | > Tree[:a=>2] 499 | 500 | tree = Tree[a: 1, b: 2] 501 | tree >> {a: 1} 502 | puts tree 503 | > Tree[:b=>2] 504 | ``` 505 | 506 | 507 | ### Iterating trees 508 | 509 | The fundamental `each` and with that all Enumerable methods behave Hash-compatible. 510 | 511 | ```ruby 512 | tree = Tree[ 1 => {a: 'foo'}, 2 => :b, 3 => nil ] 513 | tree.each { |node, child| puts "#{node} => #{child}" } 514 | 515 | > 1 => Tree[:a=>"foo"] 516 | > 2 => Tree[:b] 517 | > 3 => Tree[] 518 | ``` 519 | 520 | `each_path` iterates over all paths to leafs of a tree. 521 | 522 | ```ruby 523 | tree.each_path { |path| puts path } 524 | 525 | > # 526 | > # 527 | > # 528 | ``` 529 | 530 | The paths are represented by `Sycamore::Path` objects and are basically an Enumerable of the nodes on the path, specifically optimized for the enumeration of the set of paths of a tree. It does this, by sharing nodes between the different path objects. This means in the set of all paths, every node is contained exactly once, even the internal nodes being part of multiple paths. 531 | 532 | ```ruby 533 | Tree['some possibly very big data chunk' => [1, 2]].each_path.to_a 534 | # => [#, 535 | # #] 536 | ``` 537 | 538 | 539 | ### Searching in trees 540 | 541 | `search` returns the set of all paths to child trees containing a node or tree. 542 | 543 | ```ruby 544 | tree = Tree[ 1 => {a: 'foo'}, 2 => :b, 3 => [:a, :b, :c] ] 545 | tree.search :a # => [#, #] 546 | tree.search a: 'foo' # => [#] 547 | ``` 548 | 549 | If you search for multiple nodes, only the paths to child trees containing all of the given nodes are returned. 550 | 551 | ```ruby 552 | tree.search [:b, :c] # => [#] 553 | ``` 554 | 555 | All `Tree` methods for which it makes sense accept path objects as input instead or in combination with nodes or tree structures. This allows it to apply the search results to any of these methods. 556 | 557 | 558 | ## Getting help 559 | 560 | - [RDoc](http://www.rubydoc.info/gems/sycamore/) 561 | - [Gitter](https://gitter.im/marcelotto/sycamore) 562 | 563 | 564 | ## Contributing 565 | 566 | see [CONTRIBUTING](CONTRIBUTING.md) for details. 567 | 568 | 569 | ## License and Copyright 570 | 571 | (c) 2015-2016 Marcel Otto. MIT Licensed, see [LICENSE](LICENSE.txt) for details. 572 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | begin 4 | require 'rspec/core/rake_task' 5 | RSpec::Core::RakeTask.new(:spec) 6 | rescue LoadError 7 | puts "Couldn't find RSpec core Rake task" 8 | end 9 | 10 | begin 11 | require 'yard' 12 | YARD::Rake::YardocTask.new do |t| 13 | t.options = ['--verbose'] 14 | t.files = ['lib/**/*.rb', 'doc/**/*.md'] 15 | t.stats_options = ['--list-undoc'] 16 | end 17 | rescue LoadError 18 | puts "Couldn't find YARD" 19 | end 20 | 21 | begin 22 | require 'yard-doctest' 23 | YARD::Doctest::RakeTask.new do |task| 24 | task.doctest_opts = %w[] 25 | task.pattern = 'lib/**/*.rb' 26 | end 27 | rescue LoadError 28 | puts "Couldn't find yard-doctest" 29 | end 30 | 31 | task :default => 32 | if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby' 33 | [:spec] 34 | else 35 | [:spec, 'yard:doctest'] 36 | end 37 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.3.1 2 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'sycamore/extension' 5 | 6 | require 'pry' 7 | Pry.start 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | 9 | -------------------------------------------------------------------------------- /lib/sycamore.rb: -------------------------------------------------------------------------------- 1 | require 'sycamore/version' 2 | require 'sycamore/exceptions' 3 | require 'sycamore/path' 4 | require 'sycamore/path_root' 5 | require 'sycamore/tree' 6 | require 'sycamore/nothing' 7 | require 'sycamore/absence' 8 | 9 | ## 10 | # see README.md 11 | # 12 | module Sycamore 13 | end 14 | -------------------------------------------------------------------------------- /lib/sycamore/absence.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module Sycamore 4 | 5 | ## 6 | # An Absence object represents the absence of a specific child {Sycamore::Tree}. 7 | # 8 | # +Absence+ instances get created when accessing non-existent children of a 9 | # Tree with {Tree#child_of} or {Tree#child_at}. 10 | # It is not intended to be instantiated by the user. 11 | # 12 | # An +Absence+ object can be used like a normal {Sycamore::Tree}. 13 | # {Tree::QUERY_METHODS Query} and {Tree::DESTRUCTIVE_COMMAND_METHODS pure destructive command method} 14 | # calls get delegated to {Sycamore::Nothing}, i.e. will behave like an empty Tree. 15 | # On every other {Tree::COMMAND_METHODS command} calls, the +Absence+ object 16 | # gets resolved, which means the missing tree will be created, added to the 17 | # parent tree and the method call gets delegated to the now existing tree. 18 | # After the +Absence+ object is resolved all subsequent method calls are 19 | # delegated to the created tree. 20 | # The type of tree eventually created is determined by the {Tree#new_child} 21 | # implementation of the parent tree and the parent node. 22 | # 23 | class Absence < Delegator 24 | 25 | ## 26 | # @api private 27 | # 28 | def initialize(parent_tree, parent_node) 29 | @parent_tree, @parent_node = parent_tree, parent_node 30 | end 31 | 32 | class << self 33 | alias at new 34 | end 35 | 36 | ######################################################################## 37 | # presence creation 38 | ######################################################################## 39 | 40 | ## 41 | # The tree object to which all method calls are delegated. 42 | # 43 | # @api private 44 | # 45 | def presence 46 | @tree or Nothing 47 | end 48 | 49 | alias __getobj__ presence 50 | 51 | ## 52 | # @api private 53 | # 54 | def create 55 | @parent_tree = @parent_tree.add_node_with_empty_child(@parent_node) 56 | @tree = @parent_tree[@parent_node] 57 | end 58 | 59 | ######################################################################## 60 | # Absence and Nothing predicates 61 | ######################################################################## 62 | 63 | ## 64 | # (see Tree#absent?) 65 | # 66 | def absent? 67 | @tree.nil? 68 | end 69 | 70 | ## 71 | # (see Tree#nothing?) 72 | # 73 | def nothing? 74 | false 75 | end 76 | 77 | ######################################################################## 78 | # Element access 79 | ######################################################################## 80 | 81 | ##################### 82 | # query methods # 83 | ##################### 84 | 85 | def child_of(node) 86 | if absent? 87 | raise InvalidNode, "#{node} is not a valid tree node" if node.is_a? Enumerable 88 | 89 | Absence.at(self, node) 90 | else 91 | presence.child_of(node) 92 | end 93 | end 94 | 95 | def child_at(*path) 96 | if absent? 97 | # TODO: This is duplication of Tree#child_at! How can we remove it, without introducing a module for this single method or inherit from Tree? 98 | case path.length 99 | when 0 then raise ArgumentError, 'wrong number of arguments (given 0, expected 1+)' 100 | when 1 then child_of(*path) 101 | else child_of(path[0]).child_at(*path[1..-1]) 102 | end 103 | else 104 | presence.child_at(*path) 105 | end 106 | end 107 | 108 | alias [] child_at 109 | alias dig child_at 110 | 111 | ## 112 | # A developer-friendly string representation of the absent tree. 113 | # 114 | # @return [String] 115 | # 116 | def inspect 117 | "#{absent? ? 'absent' : 'present'} child of node #{@parent_node.inspect} in #{@parent_tree.inspect}" 118 | end 119 | 120 | ## 121 | # Duplicates the resolved tree or raises an error, when unresolved. 122 | # 123 | # @return [Tree] 124 | # 125 | # @raise [TypeError] when this {Absence} is not resolved yet 126 | # 127 | def dup 128 | presence.dup 129 | end 130 | 131 | ## 132 | # Clones the resolved tree or raises an error, when unresolved. 133 | # 134 | # @return [Tree] 135 | # 136 | # @raise [TypeError] when this {Absence} is not resolved yet 137 | # 138 | def clone 139 | presence.clone 140 | end 141 | 142 | ## 143 | # Checks if the absent tree is frozen. 144 | # 145 | # @return [Boolean] 146 | # 147 | def frozen? 148 | if absent? 149 | false 150 | else 151 | presence.frozen? 152 | end 153 | end 154 | 155 | ##################### 156 | # command methods # 157 | ##################### 158 | 159 | # TODO: YARD should be informed about this method definitions. 160 | Tree.command_methods.each do |command_method| 161 | if Tree.pure_destructive_command_methods.include?(command_method) 162 | define_method command_method do |*args| 163 | if absent? 164 | self 165 | else 166 | presence.send(command_method, *args) # TODO: How can we hand over a possible block? With eval etc.? 167 | end 168 | end 169 | else 170 | # TODO: This method should be atomic. 171 | define_method command_method do |*args| 172 | create if absent? 173 | presence.send(command_method, *args) # TODO: How can we hand over a possible block? With eval etc.? 174 | end 175 | end 176 | end 177 | 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /lib/sycamore/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Sycamore 2 | # raised when a value is not a valid node 3 | class InvalidNode < ArgumentError ; end 4 | 5 | # raised when trying to call a additive command method of the {Nothing} tree 6 | class NothingMutation < StandardError ; end 7 | 8 | # raised when calling {Tree#node} or {Tree#node!} on a Tree with multiple nodes 9 | class NonUniqueNodeSet < StandardError ; end 10 | 11 | # raised when calling {Tree#node!} on a Tree without nodes 12 | class EmptyNodeSet < StandardError ; end 13 | 14 | # raised when trying to fetch the child of a leaf 15 | class ChildError < KeyError ; end 16 | end 17 | -------------------------------------------------------------------------------- /lib/sycamore/extension.rb: -------------------------------------------------------------------------------- 1 | require 'sycamore/extension/tree' 2 | require 'sycamore/extension/nothing' 3 | -------------------------------------------------------------------------------- /lib/sycamore/extension/nothing.rb: -------------------------------------------------------------------------------- 1 | require 'sycamore/nothing' 2 | 3 | # optional global shortcut constant for {Sycamore::Nothing} 4 | Nothing = Sycamore::Nothing unless defined? Nothing 5 | -------------------------------------------------------------------------------- /lib/sycamore/extension/path.rb: -------------------------------------------------------------------------------- 1 | require 'sycamore' 2 | 3 | # optional global shortcut constant for Sycamore::Path 4 | Path = Sycamore::Path 5 | 6 | # optional global shortcut constant for Sycamore::Path 7 | TreePath = Sycamore::Path 8 | -------------------------------------------------------------------------------- /lib/sycamore/extension/tree.rb: -------------------------------------------------------------------------------- 1 | require 'sycamore' 2 | 3 | # optional global shortcut constant for Sycamore::Tree 4 | Tree = Sycamore::Tree 5 | -------------------------------------------------------------------------------- /lib/sycamore/nothing.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Sycamore 4 | 5 | ## 6 | # The Nothing Tree singleton class. 7 | # 8 | # The Nothing Tree is an empty Sycamore Tree, and means "there are no nodes". 9 | # 10 | # It is immutable: 11 | # - {Tree::QUERY_METHODS Query method} calls will behave like a normal, empty Tree. 12 | # - {Tree::DESTRUCTIVE_COMMAND_METHODS Pure destructive command} calls, will be ignored, i.e. being no-ops. 13 | # - But all other {Tree::COMMAND_METHODS command} calls, will raise a {NothingMutation}. 14 | # 15 | # It is the only Tree object that will return +true+ on a {#nothing?} call. 16 | # But like {Absence}, it will return +true+ on {#absent?} and +false+ on {#existent?}. 17 | # 18 | class NothingTree < Tree 19 | include Singleton 20 | 21 | ######################################################################## 22 | # Absence and Nothing predicates 23 | ######################################################################## 24 | 25 | ## 26 | # (see Tree#nothing?) 27 | # 28 | def nothing? 29 | true 30 | end 31 | 32 | ## 33 | # (see Tree#absent?) 34 | # 35 | def absent? 36 | true 37 | end 38 | 39 | ######################################################################## 40 | # CQS element access 41 | ######################################################################## 42 | 43 | # TODO: YARD should be informed about this method definitions. 44 | command_methods.each do |command_method| 45 | define_method command_method do |*args| 46 | raise NothingMutation, 'attempt to change the Nothing tree' 47 | end 48 | end 49 | 50 | # TODO: YARD should be informed about this method definitions. 51 | pure_destructive_command_methods.each do |command_method| 52 | define_method(command_method) { |*args| self } 53 | end 54 | 55 | def child_of(node) 56 | self 57 | end 58 | 59 | def to_native_object(sleaf_child_as: nil, **args) 60 | sleaf_child_as 61 | end 62 | 63 | ## 64 | # A string representation of the Nothing tree. 65 | # 66 | # @return [String] 67 | # 68 | def to_s 69 | 'Tree[Nothing]' 70 | end 71 | 72 | ## 73 | # A developer-friendly string representation of the Nothing tree. 74 | # 75 | # @return [String] 76 | # 77 | def inspect 78 | '#' 79 | end 80 | 81 | def freeze 82 | super 83 | end 84 | 85 | ######################################################################## 86 | # Equality 87 | ######################################################################## 88 | 89 | ## 90 | # Checks if the given object is an empty tree. 91 | # 92 | # @return [Boolean] 93 | # 94 | def ==(other) 95 | (other.is_a?(Tree) or other.is_a?(Absence)) and other.empty? 96 | end 97 | 98 | 99 | ######################################################################## 100 | # Falsiness 101 | # 102 | # Sadly, in Ruby we can't do that match to reach this goal. 103 | # 104 | # see http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/ 105 | ######################################################################## 106 | 107 | ## 108 | # Try to emulate a falsey value, by negating to +true+. 109 | # 110 | # @return [Boolean] +true+ 111 | # 112 | # @see http://devblog.avdi.org/2011/05/30/null-objects-and-falsiness/ 113 | # 114 | def ! 115 | true 116 | end 117 | 118 | # def nil? 119 | # true 120 | # end 121 | 122 | 123 | ######################################################################## 124 | # Some helpers 125 | # 126 | # Ideally these would be implemented with Refinements, but since they 127 | # aren't available anywhere (I'm looking at you, JRuby), we have to be 128 | # content with this. 129 | # 130 | ######################################################################## 131 | 132 | def like?(object) 133 | object.nil? or object.equal? self 134 | end 135 | 136 | ## 137 | # @api private 138 | class NestedStringPresentation 139 | include Singleton 140 | 141 | def inspect 142 | 'n/a' 143 | end 144 | end 145 | 146 | ## 147 | # @api private 148 | NestedString = NestedStringPresentation.instance.freeze 149 | 150 | end 151 | 152 | ############################################################################ 153 | # The Nothing Tree Singleton object 154 | # 155 | Nothing = NothingTree.instance.freeze 156 | 157 | end 158 | -------------------------------------------------------------------------------- /lib/sycamore/path.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Sycamore 4 | 5 | ## 6 | # A compact, immutable representation of Tree paths, i.e. node sequences. 7 | # 8 | # This class is optimized for its usage in {Tree#each_path}, where it 9 | # can efficiently represent the whole tree as a set of paths by sharing the 10 | # parent paths. 11 | # It is not intended to be instantiated by the user. 12 | # 13 | # @example 14 | # tree = Tree[foo: [:bar, :baz]] 15 | # path1, path2 = tree.paths.to_a 16 | # path1 == Sycamore::Path[:foo, :bar] # => true 17 | # path2 == Sycamore::Path[:foo, :baz] # => true 18 | # path1.parent.equal? path2.parent # => true 19 | # 20 | # @todo Measure the performance and memory consumption in comparison with a 21 | # pure Array-based implementation (where tree nodes are duplicated), esp. in 22 | # the most common use case of property-value structures. 23 | # 24 | class Path 25 | include Enumerable 26 | extend Forwardable 27 | 28 | attr_reader :node, :parent 29 | 30 | ######################################################################## 31 | # @group Construction 32 | ######################################################################## 33 | 34 | ## 35 | # @private 36 | # 37 | def initialize(parent, node) 38 | @parent, @node = parent, node 39 | end 40 | 41 | ## 42 | # @return the root of all Paths 43 | # 44 | def self.root 45 | ROOT 46 | end 47 | 48 | ## 49 | # Creates a new path. 50 | # 51 | # Depending on whether the first argument is a {Path}, the new Path is 52 | # {#branch}ed from this path or the {root}. 53 | # 54 | # @overload of(path, nodes) 55 | # @param path [Path] the path from which should be {#branch}ed 56 | # @param nodes [nodes] 57 | # @return [Path] the {#branch}ed path from the given path, with the given nodes expanded 58 | # 59 | # @overload of(nodes) 60 | # @param nodes [nodes] 61 | # @return [Path] the {#branch}ed path from the {root}, with the given nodes 62 | # 63 | def self.of(*args) 64 | if (parent = args.first).is_a? Path 65 | parent.branch(*args[1..-1]) 66 | else 67 | root.branch(*args) 68 | end 69 | end 70 | 71 | class << self 72 | private :new # disable Path.new 73 | 74 | alias [] of 75 | end 76 | 77 | ######################################################################## 78 | # @group Elements 79 | ######################################################################## 80 | 81 | def_delegators :to_a, :[], :fetch 82 | 83 | ## 84 | # Returns a new path based on this path, but with the given nodes extended. 85 | # 86 | # @param nodes [nodes] an arbitrary number of nodes 87 | # @return [Path] 88 | # 89 | # @raise [InvalidNode] if one or more of the given nodes is an Enumerable 90 | # 91 | # @example 92 | # path = Sycamore::Path[:foo, :bar] 93 | # path.branch(:baz, :qux) == 94 | # Sycamore::Path[:foo, :bar, :baz, :qux] # => true 95 | # path / :baz / :qux == 96 | # Sycamore::Path[:foo, :bar, :baz, :qux] # => true 97 | # 98 | def branch(*nodes) 99 | return branch(*nodes.first) if nodes.size == 1 and nodes.first.is_a? Enumerable 100 | 101 | parent = self 102 | nodes.each do |node| 103 | raise InvalidNode, "#{node} in Path #{nodes.inspect} is not a valid tree node" if 104 | node.is_a? Enumerable 105 | parent = Path.__send__(:new, parent, node) 106 | end 107 | 108 | parent 109 | end 110 | 111 | alias + branch 112 | alias / branch 113 | 114 | ## 115 | # @return [Path] the n-th last parent path 116 | # @param distance [Integer] the number of nodes to go up 117 | # 118 | # @example 119 | # path = Sycamore::Path[:foo, :bar, :baz] 120 | # path.up # => Sycamore::Path[:foo, :bar] 121 | # path.up(2) # => Sycamore::Path[:foo] 122 | # path.up(3) # => Sycamore::Path[] 123 | # 124 | def up(distance = 1) 125 | raise TypeError, "expected an integer, but got #{distance.inspect}" unless 126 | distance.is_a? Integer 127 | 128 | case distance 129 | when 1 then @parent 130 | when 0 then self 131 | else parent.up(distance - 1) 132 | end 133 | end 134 | 135 | ## 136 | # @return [Boolean] if this is the root path 137 | # 138 | def root? 139 | false 140 | end 141 | 142 | ## 143 | # @return [Integer] the number of nodes on this path 144 | # 145 | def length 146 | i, parent = 1, self 147 | i += 1 until (parent = parent.parent).root? 148 | i 149 | end 150 | 151 | alias size length 152 | 153 | ## 154 | # Iterates over all nodes on this path. 155 | # 156 | # @overload each_node 157 | # @yield [node] each node 158 | # 159 | # @overload each_node 160 | # @return [Enumerator] 161 | # 162 | def each_node(&block) 163 | return enum_for(__callee__) unless block_given? 164 | 165 | if @parent 166 | @parent.each_node(&block) 167 | yield @node 168 | end 169 | end 170 | 171 | alias each each_node 172 | 173 | ## 174 | # If a given structure contains this path. 175 | # 176 | # @param struct [Object] 177 | # @return [Boolean] if the given structure contains the nodes on this path 178 | # 179 | # @example 180 | # hash = {foo: {bar: :baz}} 181 | # Sycamore::Path[:foo, :bar].present_in? hash # => true 182 | # Sycamore::Path[:foo, :bar].present_in? Tree[hash] # => true 183 | # 184 | def present_in?(struct) 185 | each do |node| 186 | case 187 | when struct.is_a?(Enumerable) 188 | return false unless struct.include? node 189 | struct = (Tree.like?(struct) ? struct[node] : Nothing ) 190 | else 191 | return false unless struct.eql? node 192 | struct = Nothing 193 | end 194 | end 195 | true 196 | end 197 | 198 | alias in? present_in? 199 | 200 | ######################################################################## 201 | # @group Equality 202 | ######################################################################## 203 | 204 | ## 205 | # @return [Fixnum] hash code for this path 206 | # 207 | def hash 208 | to_a.hash ^ self.class.hash 209 | end 210 | 211 | ## 212 | # @return [Boolean] if the other is a Path with the same nodes in the same order 213 | # @param other [Object] 214 | # 215 | def eql?(other) 216 | other.is_a?(self.class) and 217 | self.length == other.length and begin 218 | i = other.each ; all? { |node| node.eql? i.next } 219 | end 220 | end 221 | 222 | ## 223 | # @return [Boolean] if the other is an Enumerable with the same nodes in the same order 224 | # @param other [Object] 225 | # 226 | def ==(other) 227 | other.is_a?(Enumerable) and self.length == other.length and begin 228 | i = other.each ; all? { |node| node == i.next } 229 | end 230 | end 231 | 232 | ######################################################################## 233 | # @group Conversion 234 | ######################################################################## 235 | 236 | ## 237 | # @return [String] a string created by converting each node on this path to a string, separated by the given separator 238 | # @param separator [String] 239 | # 240 | # @note Since the root path with no node is at the beginning of each path, 241 | # the returned string always begins with the given separator. 242 | # 243 | # @example 244 | # Sycamore::Path[1,2,3].join # => '/1/2/3' 245 | # Sycamore::Path[1,2,3].join('|') # => '|1|2|3' 246 | # 247 | def join(separator = '/') 248 | @parent.join(separator) + separator + node.to_s 249 | end 250 | 251 | ## 252 | # @return [String] a compact string representation of this path 253 | # 254 | def to_s 255 | "#" 256 | end 257 | 258 | ## 259 | # @return [String] a more verbose string representation of this path 260 | # 261 | def inspect 262 | "#" 263 | end 264 | end 265 | 266 | end 267 | -------------------------------------------------------------------------------- /lib/sycamore/path_root.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | module Sycamore 4 | class Path 5 | ## 6 | # @api private 7 | # 8 | class Root < Path 9 | include Singleton 10 | 11 | def initialize 12 | @parent, @node = nil, nil 13 | end 14 | 15 | def up(distance = 1) 16 | super unless distance.is_a? Integer 17 | self 18 | end 19 | 20 | def root? 21 | true 22 | end 23 | 24 | def length 25 | 0 26 | end 27 | 28 | def join(delimiter = '/') 29 | '' 30 | end 31 | 32 | def to_s 33 | '#' 34 | end 35 | 36 | def inspect 37 | '#' 38 | end 39 | end 40 | 41 | ROOT = Root.instance # @api private 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/sycamore/stree.rb: -------------------------------------------------------------------------------- 1 | require 'sycamore' 2 | 3 | # optional global shortcut constant for Sycamore::Tree 4 | STree = Sycamore::Tree 5 | -------------------------------------------------------------------------------- /lib/sycamore/version.rb: -------------------------------------------------------------------------------- 1 | module Sycamore 2 | # version representation 3 | module VERSION 4 | # the file containing the project version number 5 | FILE = File.expand_path('../../../VERSION', __FILE__) 6 | MAJOR, MINOR, TINY, EXTRA = File.read(FILE).chomp.split('.') 7 | # the normalized version string 8 | STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.').freeze 9 | 10 | ## 11 | # @return [String] 12 | def self.to_s() STRING end 13 | 14 | ## 15 | # @return [String] 16 | def self.to_str() STRING end 17 | 18 | ## 19 | # @return [Array(Integer, Integer, Integer)] 20 | def self.to_a() [MAJOR, MINOR, TINY] end 21 | 22 | ## 23 | # @return [Boolean] 24 | def self.==(other) 25 | other == self.to_s 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | begin 3 | require 'simplecov' 4 | require 'coveralls' 5 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 6 | SimpleCov::Formatter::HTMLFormatter, 7 | Coveralls::SimpleCov::Formatter 8 | ]) 9 | SimpleCov.start do 10 | add_filter "/spec/" 11 | end 12 | rescue LoadError => e 13 | STDERR.puts "Coverage Skipped: #{e.message}" 14 | end 15 | 16 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 17 | require 'sycamore' 18 | 19 | SPEC_DIR = File.dirname(__FILE__) 20 | Dir[File.join(SPEC_DIR, 'support/**/*.rb')].each {|f| require f } 21 | 22 | RSpec::Matchers.define_negated_matcher :be_different_to, :be 23 | 24 | RSpec.configure do |config| 25 | config.raise_errors_for_deprecations! 26 | 27 | config.example_status_persistence_file_path = './spec/examples.txt' 28 | end 29 | -------------------------------------------------------------------------------- /spec/support/matchers/be_path_of.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :be_path_of do |*nodes| 2 | match do |this_path| 3 | expect(this_path).to be_a Sycamore::Path 4 | current = this_path 5 | nodes.reverse_each do |node| 6 | expect(current.node ).to eql node 7 | expect(current.parent).to be_a Sycamore::Path 8 | current = current.parent 9 | end 10 | expect(current).to be_root 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/matchers/include_tree_part.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :include_tree_part do |part| 2 | match do |this_tree| 3 | expect(this_tree).to be_a(Sycamore::Tree).or be_a(Sycamore::Absence) 4 | expect(this_tree).to include part 5 | end 6 | end 7 | 8 | RSpec::Matchers.define :include_node do |node| 9 | match do |this_tree| 10 | expect(this_tree).to include_tree_part node 11 | end 12 | end 13 | 14 | RSpec::Matchers.define :include_nodes do |*nodes| 15 | match do |this_tree| 16 | nodes = nodes.first if nodes.count == 1 17 | expect(this_tree).to include_tree_part nodes 18 | end 19 | end 20 | 21 | RSpec::Matchers.define :include_tree do |tree| 22 | match do |this_tree| 23 | expect(this_tree).to include_tree_part tree 24 | end 25 | end 26 | 27 | RSpec::Matchers.define :include_path do |path| 28 | match do |this_tree| 29 | tree = this_tree 30 | path.each do |node| 31 | expect(tree).to include_node node 32 | tree = tree[node] 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/sycamore_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Sycamore do 4 | it 'has a version number' do 5 | expect(Sycamore::VERSION).not_to be_nil 6 | expect(Sycamore::VERSION).to match /\d\.\d.\d/ 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /spec/unit/sycamore/absence_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Absence do 2 | 3 | let(:parent_node) { :missing } 4 | let(:parent_tree) { Sycamore::Tree.new } 5 | let(:absent_tree) { Sycamore::Absence.new(parent_tree, parent_node) } 6 | 7 | ############################################################################ 8 | 9 | describe '#presence' do 10 | context 'when the absent tree has not been created' do 11 | it 'does return the Nothing tree' do 12 | expect( absent_tree.presence ).to be Sycamore::Nothing 13 | end 14 | end 15 | 16 | context 'when the absent tree has been created' do 17 | before(:each) { absent_tree.add :something } 18 | 19 | it 'does return the created tree' do 20 | expect( absent_tree.presence ).to be_a Sycamore::Tree 21 | expect( absent_tree.presence ).not_to be Sycamore::Nothing 22 | expect( absent_tree.presence ).not_to be_a Sycamore::Absence 23 | end 24 | end 25 | end 26 | 27 | ############################################################################ 28 | # Absence and Nothing predicates 29 | ############################################################################ 30 | 31 | describe '#nothing?' do 32 | context 'when the absent tree has not been created' do 33 | specify { expect( absent_tree.nothing? ).to be false } 34 | end 35 | 36 | context 'when the absent tree has been created' do 37 | before(:each) { absent_tree.add :something } 38 | 39 | specify { expect( absent_tree.nothing? ).to be false } 40 | end 41 | end 42 | 43 | ############################################################################ 44 | 45 | describe '#absent?' do 46 | context 'when the absent tree has not been created' do 47 | specify { expect( absent_tree.absent? ).to be true } 48 | end 49 | 50 | context 'when the absent tree has been created' do 51 | context 'when it is blank' do 52 | before(:each) { absent_tree.add(1).delete(1) } 53 | 54 | specify { expect( absent_tree.absent? ).to be false } 55 | end 56 | 57 | context 'when it contains data' do 58 | before(:each) { absent_tree.add :something } 59 | 60 | specify { expect( absent_tree.absent? ).to be false } 61 | end 62 | end 63 | end 64 | 65 | ############################################################################ 66 | 67 | describe '#existent?' do 68 | context 'when the absent tree has not been created' do 69 | specify { expect( absent_tree.existent? ).to be false } 70 | end 71 | 72 | context 'when the absent tree has been created' do 73 | context 'when it is blank' do 74 | before(:each) { absent_tree.add(1).delete(1) } 75 | 76 | specify { expect( absent_tree.existent? ).to be true } 77 | end 78 | 79 | context 'when it contains data' do 80 | before(:each) { absent_tree.add :something } 81 | 82 | specify { expect( absent_tree.existent? ).to be true } 83 | end 84 | end 85 | end 86 | 87 | ############################################################################ 88 | 89 | describe '#present?' do 90 | context 'when the absent tree has not been created' do 91 | specify { expect( absent_tree.present? ).to be false } 92 | end 93 | 94 | context 'when the absent tree has been created' do 95 | context 'when it is blank' do 96 | before(:each) { absent_tree.add Sycamore::Nothing } 97 | specify { expect( absent_tree.present? ).to be false } 98 | end 99 | context 'when it contains data' do 100 | before(:each) { absent_tree.add :something } 101 | specify { expect( absent_tree.present? ).to be true } 102 | end 103 | end 104 | end 105 | 106 | ############################################################################ 107 | # command methods 108 | ############################################################################ 109 | 110 | COMMAND_METHODS_RETURN_SPECIAL_CASES = [:[]=] 111 | 112 | shared_examples_for 'with and without the parent node' do 113 | context 'when the node is present, but the child tree absent' do 114 | before(:each) { present_root_tree << parent_node } 115 | 116 | include_examples 'direct and nested absence' 117 | end 118 | 119 | context 'when the node and child tree are absent' do 120 | include_examples 'direct and nested absence' 121 | end 122 | end 123 | 124 | shared_examples_for 'direct and nested absence' do 125 | context 'when the parent is present' do 126 | let(:parent_tree) { present_root_tree } 127 | 128 | it_behaves_like 'every command method call' 129 | end 130 | 131 | context 'when the parent itself is absent' do 132 | let(:parent_tree) { Sycamore::Absence.new(present_root_tree, root_node) } 133 | let(:parent_node) { :other_missing } 134 | 135 | it_behaves_like 'every command method call' 136 | 137 | it 'does add the absent parent to the root tree' do 138 | method_call.call 139 | 140 | expect( present_root_tree ) 141 | .to include_tree( root_node => { parent_node => absent_tree} ) 142 | end 143 | end 144 | end 145 | 146 | shared_examples_for 'every command method call' do 147 | it 'does create and return a real tree' do 148 | unless COMMAND_METHODS_RETURN_SPECIAL_CASES.include? command_method_name 149 | created_tree = method_call.call 150 | 151 | expect( created_tree ).to be_a Sycamore::Tree 152 | expect( created_tree ).not_to be absent_tree 153 | expect( created_tree ).not_to be_a Sycamore::Absence 154 | expect( created_tree ).to be absent_tree.presence 155 | end 156 | end 157 | 158 | it 'does add the created tree to the parent tree' do 159 | method_call.call 160 | 161 | expect( parent_tree ) 162 | .to include_tree( parent_node => absent_tree ) 163 | end 164 | 165 | # it 'does delegate the command method call to the created tree' do 166 | # skip 'Can we specify this in general?' 167 | # end 168 | end 169 | 170 | shared_examples_for 'command method calls under different circumstances' do |command_method, *args| 171 | let(:command_method_name) { command_method } 172 | let(:method_call) do 173 | proc { absent_tree.send(command_method, *args) } 174 | end 175 | 176 | let(:present_root_tree) { Sycamore::Tree.new } 177 | let(:root_node) { :missing } 178 | let(:parent_node) { root_node } 179 | let(:absent_tree) { Sycamore::Absence.new(parent_tree, parent_node) } 180 | 181 | include_examples 'with and without the parent node' 182 | end 183 | 184 | ############################################################################ 185 | 186 | describe '#add' do 187 | include_examples 'command method calls under different circumstances', :add, :foo 188 | include_examples 'command method calls under different circumstances', :add, nil 189 | include_examples 'command method calls under different circumstances', :add, Sycamore::Nothing 190 | 191 | it 'does execute the #add on the created tree' do 192 | expect( absent_tree.add(:foo) ).to include_node :foo 193 | expect( absent_tree[:nested].add(:foo) ).to include_node :foo 194 | end 195 | end 196 | 197 | ############################################################################ 198 | 199 | describe '#replace' do 200 | include_examples 'command method calls under different circumstances', :replace, :foo 201 | include_examples 'command method calls under different circumstances', :replace, nil 202 | include_examples 'command method calls under different circumstances', :replace, Sycamore::Nothing 203 | 204 | it 'does execute the #replace on the created tree' do 205 | expect( absent_tree.replace(:foo) ).to include_node :foo 206 | end 207 | end 208 | 209 | ############################################################################ 210 | 211 | describe '#[]=' do 212 | include_examples 'command method calls under different circumstances', :[]=, :foo, :bar 213 | include_examples 'command method calls under different circumstances', :[]=, :foo, nil 214 | include_examples 'command method calls under different circumstances', :[]=, :foo, Sycamore::Nothing 215 | 216 | it 'does execute the #[]= on the created tree' do 217 | absent_tree[:foo] = :bar 218 | 219 | expect( absent_tree.presence ).to include_tree foo: :bar 220 | end 221 | end 222 | 223 | ############################################################################ 224 | 225 | describe '#freeze' do 226 | include_examples 'command method calls under different circumstances', :freeze 227 | 228 | it 'does freeze the Absence object and the created tree' do 229 | absent_tree.freeze 230 | 231 | expect( absent_tree.presence.freeze ).to be_frozen 232 | expect( absent_tree ).to be_frozen 233 | end 234 | end 235 | 236 | ############################################################################ 237 | 238 | describe '#clear' do 239 | context 'when the absent tree has not been created' do 240 | it 'does nothing' do 241 | expect { absent_tree.clear }.not_to change { absent_tree } 242 | end 243 | 244 | it 'does return the absent tree' do 245 | expect( absent_tree.clear ).to be absent_tree 246 | end 247 | end 248 | 249 | context 'when the absent tree has been created' do 250 | before(:each) { absent_tree.add :something } 251 | 252 | it 'does delegate to the created tree' do 253 | expect(absent_tree.presence).to receive(:clear) 254 | absent_tree.clear 255 | end 256 | end 257 | end 258 | 259 | ############################################################################ 260 | 261 | describe '#delete' do 262 | context 'when the absent tree has not been created' do 263 | it 'does nothing' do 264 | expect { absent_tree.delete 42 }.not_to change { absent_tree } 265 | end 266 | 267 | it 'does return the absent tree' do 268 | expect( absent_tree.delete 42 ).to be absent_tree 269 | end 270 | end 271 | 272 | context 'when the absent tree has been created' do 273 | before(:each) { absent_tree.add :something } 274 | 275 | it 'does delegate to the created tree' do 276 | expect(absent_tree.presence).to receive(:delete) 277 | absent_tree.delete 42 278 | end 279 | end 280 | end 281 | 282 | ############################################################################ 283 | # query methods 284 | ############################################################################ 285 | 286 | let(:nothing) { spy('nothing') } 287 | 288 | UNSUPPORTED_TEST_DOUBLE_METHODS = %i[hash to_s] 289 | EXCLUDE_QUERY_METHODS = %i[== === eql? dup clone] + UNSUPPORTED_TEST_DOUBLE_METHODS + 290 | Sycamore::Absence.instance_methods(false) 291 | 292 | (Sycamore::Tree.query_methods - EXCLUDE_QUERY_METHODS).each do |query_method| 293 | describe "##{query_method}" do 294 | context 'when the absent tree has not been created' do 295 | it 'does delegate to Nothing' do 296 | absent_tree.instance_variable_set(:@tree, nothing) 297 | absent_tree.send(query_method) 298 | expect(nothing).to have_received(query_method) 299 | end 300 | end 301 | 302 | context 'when the absent tree has been created' do 303 | before(:each) { absent_tree.add :something } 304 | 305 | let(:created_tree) { absent_tree.presence } 306 | 307 | it 'does delegate to the created tree' do 308 | expect(created_tree).to be_present 309 | expect(created_tree).to receive(query_method) 310 | absent_tree.send(query_method) 311 | end 312 | end 313 | end 314 | end 315 | 316 | ############################################################################ 317 | 318 | describe '#child_of' do 319 | context 'when the absent tree has not been created' do 320 | it 'does return another absent tree' do 321 | expect( absent_tree.child_of(:another) ) 322 | .to be_a(Sycamore::Absence) 323 | .and be_absent 324 | .and be_different_to absent_tree 325 | end 326 | 327 | context 'edge cases' do 328 | it 'does treat nil like any other value' do 329 | expect( absent_tree.child_of(nil) ) 330 | .to be_a(Sycamore::Absence) 331 | .and be_absent 332 | .and be_different_to absent_tree 333 | end 334 | 335 | it 'does raise an error, when given the Nothing tree' do 336 | expect { absent_tree.child_of(Sycamore::Nothing) }.to raise_error Sycamore::InvalidNode 337 | end 338 | 339 | it 'does raise an error, when given an Enumerable' do 340 | expect { absent_tree.child_of([1]) }.to raise_error Sycamore::InvalidNode 341 | expect { absent_tree.child_of([1, 2]) }.to raise_error Sycamore::InvalidNode 342 | expect { absent_tree.child_of(foo: :bar) }.to raise_error Sycamore::InvalidNode 343 | expect { absent_tree.child_of(Sycamore::Tree[1]) }.to raise_error Sycamore::InvalidNode 344 | end 345 | end 346 | end 347 | 348 | context 'when the absent tree has been created' do 349 | before(:each) { absent_tree.add :something } 350 | let(:created_tree) { absent_tree.presence } 351 | 352 | it 'does delegate to the created tree' do 353 | expect(created_tree).to receive(:child_of) 354 | absent_tree.child_of(:something) 355 | end 356 | end 357 | end 358 | 359 | ############################################################################ 360 | 361 | describe '#child_at' do 362 | context 'when the absent tree has not been created' do 363 | it 'does return another absent tree' do 364 | expect( absent_tree[1,2,3] ) 365 | .to be_a(Sycamore::Absence) 366 | .and be_absent 367 | .and be_different_to absent_tree 368 | end 369 | end 370 | 371 | context 'when the absent tree has been created' do 372 | before(:each) { absent_tree.add :something } 373 | let(:created_tree) { absent_tree.presence } 374 | 375 | it 'does delegate to the created tree' do 376 | expect(created_tree).to receive(:child_at) 377 | absent_tree.child_at(:something) 378 | end 379 | end 380 | 381 | it 'does raise an ArgumentError, when given no arguments' do 382 | expect { absent_tree.child_at() }.to raise_error ArgumentError 383 | end 384 | end 385 | 386 | ############################################################################ 387 | # Equality 388 | ############################################################################ 389 | 390 | describe '#===' do 391 | context 'when the absent tree has not been created' do 392 | it 'does delegate to Nothing' do 393 | expect( absent_tree === Sycamore::Nothing ).to be true 394 | expect( Sycamore::Nothing === absent_tree ).to be true 395 | end 396 | end 397 | 398 | context 'when the absent tree has been created' do 399 | before(:each) { absent_tree.add :something } 400 | 401 | it 'does delegate to the created tree' do 402 | expect( absent_tree === Sycamore::Tree[:something] ).to be true 403 | expect( Sycamore::Tree[:something] === absent_tree ).to be true 404 | end 405 | end 406 | end 407 | 408 | ############################################################################ 409 | 410 | describe '#==' do 411 | context 'when the absent tree has not been created' do 412 | it 'does delegate to Nothing' do 413 | expect( absent_tree == Sycamore::Nothing ).to be true 414 | expect( Sycamore::Nothing == absent_tree ).to be true 415 | end 416 | end 417 | 418 | context 'when the absent tree has been created' do 419 | before(:each) { absent_tree.add :something } 420 | 421 | it 'does delegate to the created tree' do 422 | expect( absent_tree == Sycamore::Tree[:something] ).to be true 423 | expect( Sycamore::Tree[:something] == absent_tree ).to be true 424 | end 425 | end 426 | end 427 | 428 | ############################################################################ 429 | 430 | describe '#eql?' do 431 | context 'when the absent tree has not been created' do 432 | it 'does delegate to Nothing' do 433 | expect( absent_tree.eql? Sycamore::Nothing ).to be true 434 | expect( Sycamore::Nothing.eql? absent_tree ).to be true 435 | end 436 | end 437 | 438 | context 'when the absent tree has been created' do 439 | before(:each) { absent_tree.add :something } 440 | 441 | it 'does delegate to the created tree' do 442 | expect( absent_tree.eql? Sycamore::Tree[:something] ).to be true 443 | expect( Sycamore::Tree[:something].eql? absent_tree ).to be true 444 | end 445 | end 446 | end 447 | 448 | ############################################################################ 449 | 450 | describe '#hash' do 451 | context 'when the absent tree has not been created' do 452 | it 'does delegate to Nothing' do 453 | expect( absent_tree.hash ).to be Sycamore::Nothing.hash 454 | end 455 | end 456 | 457 | context 'when the absent tree has been created' do 458 | before(:each) { absent_tree.add :something } 459 | 460 | it 'does delegate to the created tree' do 461 | expect( absent_tree.hash ).to be Sycamore::Tree[:something].hash 462 | end 463 | end 464 | end 465 | 466 | ############################################################################ 467 | # Conversion 468 | ############################################################################ 469 | 470 | describe '#to_s' do 471 | context 'when the absent tree has not been created' do 472 | it 'does delegate to Nothing' do 473 | expect( absent_tree.to_s ).to eql Sycamore::Nothing.to_s 474 | end 475 | end 476 | 477 | context 'when the absent tree has been created' do 478 | before(:each) { absent_tree.add :something } 479 | 480 | it 'does delegate to the created tree' do 481 | expect( absent_tree.to_s ).to eql Sycamore::Tree[:something].to_s 482 | end 483 | end 484 | end 485 | 486 | ############################################################################ 487 | 488 | describe '#inspect' do 489 | shared_examples_for 'every inspect string' do |absent_tree| 490 | context 'when the absent tree has not been created' do 491 | it 'contains the word "absent"' do 492 | expect( absent_tree.inspect ).to include 'absent child of' 493 | end 494 | end 495 | 496 | context 'when the absent tree has been created' do 497 | before(:each) { absent_tree.add :something } 498 | 499 | it 'contains the word "present"' do 500 | expect( absent_tree.inspect ).to include 'present child of' 501 | end 502 | end 503 | 504 | it 'contains the inspect representation of the parent tree' do 505 | expect( absent_tree.inspect ).to include absent_tree.instance_variable_get(:@parent_tree).inspect 506 | end 507 | 508 | it 'contains the inspect representation of the parent node' do 509 | expect( absent_tree.inspect ).to include absent_tree.instance_variable_get(:@parent_node).inspect 510 | end 511 | end 512 | 513 | include_examples 'every inspect string', Sycamore::Absence.new(Sycamore::Tree.new, :missing) 514 | include_examples 'every inspect string', Sycamore::Absence.new(Sycamore::Tree[1,2,3], :missing) 515 | end 516 | 517 | ############################################################################ 518 | # Standard Ruby methods 519 | ############################################################################ 520 | 521 | describe '#dup' do 522 | context 'when the absent tree has not been created' do 523 | it 'does raise an error' do 524 | expect { absent_tree.dup }.to raise_error TypeError 525 | end 526 | end 527 | 528 | context 'when the absent tree has been created' do 529 | before(:each) { absent_tree.add :something } 530 | let(:created_tree) { absent_tree.presence } 531 | 532 | it 'does return a duplicate of the present tree, not the Absence' do 533 | duplicate = absent_tree.dup 534 | expect(duplicate).not_to be_a Sycamore::Absence 535 | expect(duplicate).to be_a Sycamore::Tree 536 | end 537 | 538 | it 'does delegate to the created tree' do 539 | expect(created_tree).to be_present 540 | expect(created_tree).to receive(:dup) 541 | absent_tree.dup 542 | end 543 | end 544 | end 545 | 546 | ############################################################################ 547 | 548 | describe '#clone' do 549 | context 'when the absent tree has not been created' do 550 | it 'does raise an error' do 551 | expect { absent_tree.clone }.to raise_error TypeError 552 | end 553 | end 554 | 555 | context 'when the absent tree has been created' do 556 | before(:each) { absent_tree.add :something } 557 | let(:created_tree) { absent_tree.presence } 558 | 559 | it 'does return a duplicate of the present tree, not the Absence' do 560 | klone = absent_tree.clone 561 | expect(klone).not_to be_a Sycamore::Absence 562 | expect(klone).to be_a Sycamore::Tree 563 | end 564 | 565 | it 'does delegate to the created tree' do 566 | expect(created_tree).to be_present 567 | expect(created_tree).to receive(:clone) 568 | absent_tree.clone 569 | end 570 | end 571 | end 572 | 573 | ############################################################################ 574 | 575 | describe '#frozen?' do 576 | context 'when the absent tree has not been created' do 577 | it 'does return false' do 578 | expect( absent_tree ).not_to be_frozen 579 | end 580 | end 581 | 582 | context 'when the absent tree has been created' do 583 | before(:each) { absent_tree.add :something } 584 | 585 | it 'does delegate to the created tree' do 586 | expect( absent_tree ).not_to be_frozen 587 | end 588 | end 589 | end 590 | 591 | end 592 | -------------------------------------------------------------------------------- /spec/unit/sycamore/nothing_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Nothing do 2 | 3 | it { is_expected.to be_a Singleton } 4 | it { is_expected.to be_a Sycamore::Tree } 5 | 6 | it { is_expected.to be_falsey } 7 | 8 | describe 'query methods' do 9 | describe 'children' do 10 | specify { expect( Sycamore::Nothing.child_of(1 ) ).to be Sycamore::Nothing } 11 | specify { expect( Sycamore::Nothing.child_of(nil ) ).to be Sycamore::Nothing } 12 | specify { expect( Sycamore::Nothing.child_at(1, 2, 3) ).to be Sycamore::Nothing } 13 | specify { expect( Sycamore::Nothing.child_at(1,nil,3) ).to be Sycamore::Nothing } 14 | specify { expect( Sycamore::Nothing[:foo, :bar] ).to be Sycamore::Nothing } 15 | end 16 | 17 | describe '#nothing?' do 18 | specify { expect( Sycamore::Nothing.nothing? ).to be true } 19 | end 20 | 21 | describe '#absent?' do 22 | specify { expect( Sycamore::Nothing.absent? ).to be true } 23 | end 24 | 25 | describe '#existent?' do 26 | specify { expect( Sycamore::Nothing.existent? ).to be false } 27 | end 28 | 29 | describe '#present?' do 30 | specify { expect( Sycamore::Nothing.present? ).to be false } 31 | end 32 | 33 | describe '#empty?' do 34 | specify { expect( Sycamore::Nothing.empty? ).to be true } 35 | end 36 | 37 | describe '#size' do 38 | specify { expect( Sycamore::Nothing.size ).to be 0 } 39 | end 40 | 41 | describe '#to_s' do 42 | specify { expect( Sycamore::Nothing.to_s ).to eql 'Tree[Nothing]' } 43 | end 44 | 45 | describe '#inspect' do 46 | specify { expect( Sycamore::Nothing.inspect ).to eql '#' } 47 | end 48 | end 49 | 50 | describe 'additive command methods' do 51 | it 'does raise an exception on all command methods' do 52 | expect_failing { Sycamore::Nothing << 'Bye' } 53 | expect_failing { Sycamore::Nothing.add 42 } 54 | expect_failing { Sycamore::Nothing.add :foo, :bar } 55 | end 56 | 57 | def expect_failing(&block) 58 | expect(&block).to raise_error Sycamore::NothingMutation 59 | end 60 | end 61 | 62 | describe 'purely destructive command methods' do 63 | describe '#clear' do 64 | specify { expect( Sycamore::Nothing.clear ).to be Sycamore::Nothing } 65 | end 66 | 67 | describe '#delete' do 68 | specify { expect( Sycamore::Nothing >> :foo ).to be Sycamore::Nothing } 69 | specify { expect( Sycamore::Nothing.delete(42) ).to be Sycamore::Nothing } 70 | end 71 | end 72 | 73 | describe '#eql?' do 74 | it 'does return false when compared with any empty tree' do 75 | expect( Sycamore::Nothing.eql? Sycamore::Tree.new ).to be false 76 | expect( Sycamore::Nothing.eql? Class.new(Sycamore::Tree).new ).to be false 77 | end 78 | 79 | it 'does return true when compared with any absent tree' do 80 | expect( Sycamore::Nothing.eql? Sycamore::Tree.new.child_of(42) ).to be true 81 | end 82 | end 83 | 84 | describe '#==' do 85 | it 'does return true when compared with an empty tree' do 86 | expect( Sycamore::Nothing == Sycamore::Tree.new ).to be true 87 | expect( Sycamore::Nothing == Class.new(Sycamore::Tree).new ).to be true 88 | end 89 | 90 | it 'does return true when compared with any absent tree' do 91 | expect( Sycamore::Nothing == Sycamore::Tree.new.child_of(42) ).to be true 92 | end 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /spec/unit/sycamore/path_root_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Path::ROOT do 2 | subject(:root) { Sycamore::Path::ROOT } 3 | 4 | it { is_expected.to be_a Sycamore::Path } 5 | 6 | ############################################################################ 7 | 8 | describe '#node' do 9 | specify { expect( root.node ).to be_nil } 10 | end 11 | 12 | ############################################################################ 13 | 14 | describe '#parent' do 15 | specify { expect( root.parent ).to be_nil } 16 | end 17 | 18 | ############################################################################ 19 | 20 | describe '#up' do 21 | it 'does return the root tree again, for any given distance' do 22 | expect( root.up ).to be root 23 | expect( root.up(3) ).to be root 24 | end 25 | 26 | it 'does raise an error, if not given an integer' do 27 | expect { root.up(nil) }.to raise_error TypeError 28 | expect { root.up('1') }.to raise_error TypeError 29 | expect { root.up(1.1) }.to raise_error TypeError 30 | end 31 | end 32 | 33 | ############################################################################ 34 | 35 | describe '#root?' do 36 | specify { expect( root.root? ).to be true } 37 | end 38 | 39 | ############################################################################ 40 | 41 | describe '#length' do 42 | specify { expect( root.length ).to be 0 } 43 | end 44 | 45 | ############################################################################ 46 | 47 | describe '#present_in?' do 48 | it 'does always return true' do 49 | expect( root.in? 1 ).to be true 50 | expect( root.in? nil ).to be true 51 | end 52 | end 53 | 54 | ############################################################################ 55 | 56 | describe '#to_s' do 57 | specify { expect( root.to_s ).to eq "#" } 58 | end 59 | 60 | ############################################################################ 61 | 62 | describe '#inspect' do 63 | specify { expect( root.inspect ).to eq "#" } 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /spec/unit/sycamore/path_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Path do 2 | 3 | let(:example_path) { Sycamore::Path[:foo, :bar] } 4 | 5 | ############################################################################ 6 | # Construction 7 | ############################################################################ 8 | 9 | describe '.new' do 10 | it 'is not available' do 11 | expect { Sycamore::Path.new(:foo, :bar) }.to raise_error NoMethodError 12 | end 13 | end 14 | 15 | ############################################################################ 16 | 17 | describe '.root' do 18 | it 'does return the Path::ROOT singleton' do 19 | expect( Sycamore::Path.root ).to be Sycamore::Path::ROOT 20 | end 21 | end 22 | 23 | ############################################################################ 24 | 25 | describe '.of' do 26 | context 'when the first argument is a path' do 27 | it 'does delegate to the branch method of the given path with the rest of the arguments' do 28 | path = Sycamore::Path[:foo] 29 | expect( path ).to receive(:branch).with(:bar) 30 | Sycamore::Path.of(path, :bar) 31 | end 32 | end 33 | 34 | context 'when the first argument is not a path' do 35 | it 'does delegate to the branch method of the root path with the given arguments' do 36 | expect( Sycamore::Path.root ).to receive(:branch).with(1, 2) 37 | Sycamore::Path.of(1, 2) 38 | end 39 | end 40 | 41 | it 'does return the root path, when no arguments given' do 42 | expect( Sycamore::Path[] ).to be Sycamore::Path.root 43 | end 44 | 45 | context 'edge cases' do 46 | it 'does raise an error, when given a nested collection' do 47 | expect { Sycamore::Path[1, [2], 3] }.to raise_error Sycamore::InvalidNode 48 | expect { Sycamore::Path[:foo, {bar: :baz}] }.to raise_error Sycamore::InvalidNode 49 | end 50 | end 51 | end 52 | 53 | ############################################################################ 54 | # Element access 55 | ############################################################################ 56 | 57 | describe '#node' do 58 | it 'does return the last node of a path' do 59 | expect( Sycamore::Path[1].node ).to be 1 60 | expect( Sycamore::Path[:foo, :bar].node ).to be :bar 61 | end 62 | end 63 | 64 | ############################################################################ 65 | 66 | describe '#parent' do 67 | it 'does return the path without the last node' do 68 | expect( Sycamore::Path[1].parent ).to be Sycamore::Path.root 69 | expect( Sycamore::Path[:foo, :bar].parent ).to eq Sycamore::Path[:foo] 70 | end 71 | end 72 | 73 | ############################################################################ 74 | 75 | describe '#branch' do 76 | context 'when no arguments given' do 77 | it 'does return the path itself' do 78 | path = Sycamore::Path[] 79 | expect( path.branch() ).to be path 80 | 81 | path = Sycamore::Path[1, 2, 3] 82 | expect( path.branch() ).to be path 83 | end 84 | end 85 | 86 | context 'when given a single node' do 87 | it 'does return a path with given node appended' do 88 | expect( Sycamore::Path[ ].branch(1)).to be_path_of 1 89 | expect( Sycamore::Path[1].branch(2)).to be_path_of 1, 2 90 | end 91 | end 92 | 93 | context 'when given multiple arguments' do 94 | it 'does return a path with the given nodes appended' do 95 | expect( Sycamore::Path[ ].branch(:foo, :bar)).to be_path_of :foo, :bar 96 | expect( Sycamore::Path[1].branch(2, 3)).to be_path_of 1, 2, 3 97 | end 98 | end 99 | 100 | context 'when given a collection of nodes' do 101 | it 'does return a path with the given nodes appended' do 102 | expect( Sycamore::Path[ ].branch([:foo, :bar])).to be_path_of :foo, :bar 103 | expect( Sycamore::Path[1].branch([2, 3])).to be_path_of 1, 2, 3 104 | end 105 | end 106 | 107 | context 'when given another Path' do 108 | it 'does return a path with the nodes of the given path appended' do 109 | another_path = Sycamore::Path[2, 3] 110 | expect( Sycamore::Path[ ].branch(another_path)).to be_path_of 2, 3 111 | expect( Sycamore::Path[1].branch(another_path)).to be_path_of 1, 2, 3 112 | end 113 | end 114 | 115 | context 'edge cases' do 116 | it 'does treat nil like any other value' do 117 | expect( Sycamore::Path[1].branch(nil) ).to be_path_of 1, nil 118 | expect( Sycamore::Path[1].branch(nil, 2) ).to be_path_of 1, nil, 2 119 | expect( Sycamore::Path[1].branch(2, nil) ).to be_path_of 1, 2, nil 120 | end 121 | 122 | it 'does raise an error, when given multiple collections of nodes' do 123 | expect { Sycamore::Path[1].branch([:foo, :bar], [:baz]) }.to raise_error Sycamore::InvalidNode 124 | end 125 | 126 | it 'does raise an error, when given a nested collection' do 127 | expect { Sycamore::Path[1].branch([1, [2], 3]) }.to raise_error Sycamore::InvalidNode 128 | expect { Sycamore::Path[1].branch([:foo, {bar: :baz}]) }.to raise_error Sycamore::InvalidNode 129 | end 130 | 131 | it 'does raise an error, when given a nested collection' do 132 | expect { Sycamore::Path[1].branch([1, [2], 3]) }.to raise_error Sycamore::InvalidNode 133 | expect { Sycamore::Path[1].branch([:foo, {bar: :baz}]) }.to raise_error Sycamore::InvalidNode 134 | expect { Sycamore::Path[1].branch(Sycamore::Path[2, 3], 4) }.to raise_error Sycamore::InvalidNode 135 | expect { Sycamore::Path[1].branch(2, Sycamore::Path[3, 4]) }.to raise_error Sycamore::InvalidNode 136 | expect { Sycamore::Path[1].branch(Sycamore::Path[2, 3], Sycamore::Path[4, 5]) }.to raise_error Sycamore::InvalidNode 137 | end 138 | end 139 | end 140 | 141 | ############################################################################ 142 | 143 | describe '#up' do 144 | it 'does return the path itself, when the given distance is 0' do 145 | path = Sycamore::Path[1, 2, 3] 146 | expect( path.up(0) ).to be path 147 | end 148 | 149 | it 'does return the parent, when the given distance is 1 or not specified ' do 150 | path = Sycamore::Path[1, 2, 3] 151 | expect( path.up ).to be path.parent 152 | end 153 | 154 | it 'does return the path above the given distance' do 155 | path = Sycamore::Path[1, 2, 3] 156 | expect( path.up(1) ).to be path.parent 157 | expect( path.up(2) ).to be path.parent.parent 158 | expect( path.up(3) ).to be path.parent.parent.parent 159 | end 160 | 161 | it 'does raise an error, if not given an integer' do 162 | expect { Sycamore::Path[1,2,3].up(nil) }.to raise_error TypeError 163 | expect { Sycamore::Path[1,2,3].up('1') }.to raise_error TypeError 164 | expect { Sycamore::Path[1,2,3].up(1.1) }.to raise_error TypeError 165 | end 166 | end 167 | 168 | ############################################################################ 169 | 170 | describe '#[]' do 171 | it 'does return the node at the given index position if present' do 172 | expect( example_path[0] ).to be :foo 173 | expect( example_path[1] ).to be :bar 174 | end 175 | 176 | it 'does return nil if the given index is out of range' do 177 | expect( example_path[2] ).to be_nil 178 | end 179 | end 180 | 181 | ############################################################################ 182 | 183 | describe '#fetch' do 184 | it 'does return the node at the given index position if present' do 185 | expect( example_path.fetch(0) ).to be :foo 186 | expect( example_path.fetch(1) ).to be :bar 187 | end 188 | 189 | it 'does raise an error if the given index is out of range' do 190 | expect { example_path.fetch(2) }.to raise_error IndexError 191 | end 192 | end 193 | 194 | ############################################################################ 195 | 196 | describe '#root?' do 197 | specify { expect( example_path.root? ).to be false } 198 | end 199 | 200 | ############################################################################ 201 | 202 | describe '#length' do 203 | it 'does return the number of nodes on this path' do 204 | expect( Sycamore::Path[ ].length).to be 0 205 | expect( Sycamore::Path[42 ].length).to be 1 206 | expect( Sycamore::Path[1,2,3].length).to be 3 207 | end 208 | end 209 | 210 | ############################################################################ 211 | 212 | describe '#each_node' do 213 | it 'does yield the block with each node of the path as an argument' do 214 | expect { |b| Sycamore::Path[1,2,3].each(&b) }.to yield_successive_args 1,2,3 215 | end 216 | 217 | context 'when no block given' do 218 | it 'does return an enumerator' do 219 | expect( Sycamore::Path[1,2,3].each_node ).to be_a Enumerator 220 | end 221 | 222 | it 'does return an enumerator over the nodes' do 223 | expect( Sycamore::Path[1,2,3].each_node.to_a ).to eq [1,2,3] 224 | end 225 | end 226 | end 227 | 228 | ############################################################################ 229 | 230 | describe '#present_in?' do 231 | PRESENT_IN_EXAMPLES = [ 232 | # Path , Structure 233 | [ [1] , 1 ], 234 | [ [1] , [1] ], 235 | [ [1, 2] , {1 => 2} ], 236 | [ [1] , {1 => {2 => 3, 4 => 5}} ], 237 | [ [1,2] , {1 => {2 => 3, 4 => 5}} ], 238 | [ [1,2,3] , {1 => {2 => 3, 4 => 5}} ], 239 | [ [1,2] , {1 => [2, 3]} ], 240 | [ [1,2,3] , {1 => {2 => [3], 4 => 5}} ], 241 | ] 242 | NOT_PRESENT_IN_EXAMPLES = [ 243 | [ [1] , {} ], 244 | [ [1] , [] ], 245 | [ [2] , {1 => 2} ], 246 | [ [1,2,3] , {1 => 2} ], 247 | ] 248 | 249 | context 'when given a structure' do 250 | it 'does return true, if the given structure includes this path' do 251 | PRESENT_IN_EXAMPLES.each do |path_nodes, struct| 252 | path = Sycamore::Path[*path_nodes] 253 | expect( path.in?(struct) ).to be(true), 254 | "expected #{struct.inspect} to include path #{path.inspect}" 255 | end 256 | end 257 | 258 | it 'does return false, if the given structure does not include this path' do 259 | NOT_PRESENT_IN_EXAMPLES.each do |path_nodes, struct| 260 | path = Sycamore::Path[*path_nodes] 261 | expect( path.in?(struct) ).to be(false), 262 | "expected #{struct.inspect} not to include path #{path.inspect}" 263 | end 264 | end 265 | end 266 | 267 | context 'when given a Tree' do 268 | it 'does return true, if the given tree includes this path' do 269 | PRESENT_IN_EXAMPLES.each do |path_nodes, struct| 270 | path, tree = Sycamore::Path[*path_nodes], Sycamore::Tree[struct] 271 | expect( path.in?(tree) ).to be(true), 272 | "expected #{tree.inspect} to include path #{path.inspect}" 273 | end 274 | end 275 | 276 | it 'does return false, if the given tree does not include this path' do 277 | NOT_PRESENT_IN_EXAMPLES.each do |path_nodes, struct| 278 | path, tree = Sycamore::Path[*path_nodes], Sycamore::Tree[struct] 279 | expect( path.in?(tree) ).to be(false), 280 | "expected #{tree.inspect} not to include path #{path.inspect}" 281 | end 282 | end 283 | end 284 | 285 | context 'edge cases' do 286 | it 'does treat nil like any other value' do 287 | expect( Sycamore::Path[nil].in? nil ).to be true 288 | expect( Sycamore::Path[nil].in? [nil, :foo] ).to be true 289 | expect( Sycamore::Path[nil, 1].in? nil => 1 ).to be true 290 | expect( Sycamore::Path[nil, nil].in? nil => [nil] ).to be true 291 | end 292 | end 293 | end 294 | 295 | ############################################################################ 296 | # Equality 297 | ############################################################################ 298 | 299 | PATH_EQL = [ 300 | [ Sycamore::Path[ ] , Sycamore::Path[] ], 301 | [ Sycamore::Path[1 ] , Sycamore::Path[1] ], 302 | [ Sycamore::Path[nil ] , Sycamore::Path[nil] ], 303 | [ Sycamore::Path[1,2,3] , Sycamore::Path[1,2,3] ], 304 | [ Sycamore::Path[nil, nil] , Sycamore::Path[nil, nil] ], 305 | ] 306 | 307 | PATH_EQ = PATH_EQL + [ 308 | [ Sycamore::Path[] , [] ], 309 | [ Sycamore::Path[1, 2, 3] , [1, 2, 3] ], 310 | [ Sycamore::Path[1, 2.0] , [1.0, 2] ], 311 | ] 312 | 313 | PATH_NOT_EQ_BY_CONTENT = [ 314 | [ Sycamore::Path[1] , Sycamore::Path[2] ], 315 | [ Sycamore::Path[/a/] , Sycamore::Path['a'] ], 316 | [ Sycamore::Path[1,2,3] , Sycamore::Path[1,2] ], 317 | [ Sycamore::Path[1,2] , Sycamore::Path[1,2,3] ], 318 | ] 319 | 320 | PATH_NOT_EQL_BY_CONTENT = PATH_NOT_EQ_BY_CONTENT + [ 321 | [ Sycamore::Path[1] , Sycamore::Path[1.0] ], 322 | ] 323 | 324 | PATH_NOT_EQL_BY_ORDER = [ 325 | [ Sycamore::Path[1,2,3] , Sycamore::Path[3,2,1] ], 326 | ] 327 | 328 | PATH_NOT_EQL_BY_TYPE = [ 329 | [ Sycamore::Path[] , [] ], 330 | [ Sycamore::Path[1,2,3] , [1,2,3] ], 331 | ] 332 | 333 | PATH_NOT_EQL = PATH_NOT_EQL_BY_TYPE + PATH_NOT_EQL_BY_CONTENT + PATH_NOT_EQL_BY_ORDER 334 | 335 | ############################################################################ 336 | 337 | describe '#eql?' do 338 | it 'does return true, when the other is of the same type and has eql content' do 339 | PATH_EQL.each do |path, other| 340 | expect(path).to eql(other) 341 | end 342 | end 343 | 344 | it 'does return false, when the content is not eql' do 345 | PATH_NOT_EQL_BY_CONTENT.each do |path, other| 346 | expect(path).not_to eql(other) 347 | end 348 | end 349 | 350 | it 'does return false, when the order is not equal' do 351 | PATH_NOT_EQL_BY_ORDER.each do |path, other| 352 | expect(path).not_to eql(other) 353 | end 354 | end 355 | 356 | it 'does return false, when the type is not equal' do 357 | PATH_NOT_EQL_BY_TYPE.each do |path, other| 358 | expect(path).not_to eql(other) 359 | end 360 | end 361 | end 362 | 363 | ############################################################################ 364 | 365 | describe '#hash' do 366 | it 'does produce equal values, when the path is eql' do 367 | PATH_EQL.each do |path, other| 368 | expect( path.hash ).to be(other.hash), 369 | "expected the hash of #{path.inspect} to be also the hash of #{other.inspect}" 370 | end 371 | end 372 | 373 | it 'does produce different values, when the path is not eql' do 374 | PATH_NOT_EQL.each do |path, other| 375 | expect(path.hash).not_to eq(other.hash), 376 | "expected the hash of #{path.inspect} not to equal the hash of #{other.inspect}" 377 | end 378 | end 379 | end 380 | 381 | ############################################################################ 382 | 383 | describe '#==' do 384 | it 'does return true, when the other is an enumerable, has == nodes in the same order' do 385 | PATH_EQ.each do |path, other| 386 | expect(path).to eq(other) 387 | end 388 | end 389 | 390 | it 'does return false, when the content is not ==' do 391 | PATH_NOT_EQ_BY_CONTENT.each do |path, other| 392 | expect(path).not_to eq(other) 393 | end 394 | end 395 | 396 | it 'does return false, when the order is not equal' do 397 | PATH_NOT_EQL_BY_ORDER.each do |path, other| 398 | expect(path).not_to eq(other) 399 | end 400 | end 401 | 402 | it 'does return false, when the other is not an enumerable' do 403 | expect( Sycamore::Path[1] ).not_to eq 1 404 | end 405 | end 406 | 407 | ############################################################################ 408 | # Conversion 409 | ############################################################################ 410 | 411 | describe '#to_a' do 412 | it 'does return an array representation of the path' do 413 | expect( Sycamore::Path[ 42 ].to_a ).to eq [42] 414 | expect( Sycamore::Path[ nil ].to_a ).to eq [nil] 415 | expect( Sycamore::Path[ 1, 2, 3 ].to_a ).to eq [1,2,3] 416 | expect( Sycamore::Path['foo' ].to_a ).to eq ["foo"] 417 | expect( Sycamore::Path['foo', 'bar' ].to_a ).to eq %w[foo bar] 418 | expect( Sycamore::Path['foo', 'bar', 42].to_a ).to eq ['foo', 'bar', 42] 419 | expect( Sycamore::Path['foo', nil, 42 ].to_a ).to eq ['foo', nil, 42] 420 | end 421 | end 422 | 423 | ############################################################################ 424 | 425 | describe '#join' do 426 | shared_examples_for 'every join string' do |path, delimiter = '/'| 427 | it 'contains the to_s representation of the nodes' do 428 | path.each_node do |node| 429 | expect( path.join(delimiter) ).to include node.to_s 430 | end 431 | end 432 | it 'delimits the nodes with the specified delimiter' do 433 | expect( path.join(delimiter).count(delimiter) ).to be path.length 434 | end 435 | end 436 | 437 | include_examples 'every join string', Sycamore::Path[1] 438 | include_examples 'every join string', Sycamore::Path[:foo, 'bar', 42] 439 | include_examples 'every join string', Sycamore::Path[:foo, 'bar', 42], '\\' 440 | include_examples 'every join string', Sycamore::Path[:foo, nil, 42], '\\' 441 | end 442 | 443 | ############################################################################ 444 | 445 | describe '#to_s' do 446 | shared_examples_for 'every to_s string' do |path| 447 | it 'starts with a Path-specific prefix' do 448 | expect( path.to_s ).to match /^# [2,3]].nodes ).to eql [1] 22 | end 23 | end 24 | end 25 | 26 | context 'when containing multiple nodes' do 27 | context 'without children' do 28 | it 'does return an array with the nodes' do 29 | expect( Sycamore::Tree[:foo, :bar, :baz].nodes.to_set ) 30 | .to eql %i[foo bar baz].to_set 31 | end 32 | end 33 | 34 | context 'with children' do 35 | it 'does return an array with the nodes only' do 36 | expect( Sycamore::Tree[foo: 1, bar: 2, baz: nil].nodes.to_set ) 37 | .to eql %i[foo bar baz].to_set 38 | end 39 | end 40 | end 41 | end 42 | 43 | ############################################################################ 44 | 45 | describe '#node' do 46 | context 'when empty' do 47 | it 'does return nil' do 48 | expect( Sycamore::Tree.new.node ).to eql nil 49 | end 50 | end 51 | 52 | context 'when containing a single node' do 53 | context 'without children' do 54 | it 'does return the node' do 55 | expect( Sycamore::Tree[1].node ).to eql 1 56 | end 57 | end 58 | 59 | context 'with children' do 60 | it 'does return the node' do 61 | expect( Sycamore::Tree[1 => [2,3]].node ).to eql 1 62 | end 63 | end 64 | end 65 | 66 | context 'when containing multiple nodes' do 67 | it 'does raise an error' do 68 | expect { Sycamore::Tree[:foo, :bar].node }.to raise_error Sycamore::NonUniqueNodeSet 69 | expect { Sycamore::Tree[foo: 1, bar: 2, baz: nil].node }.to raise_error Sycamore::NonUniqueNodeSet 70 | end 71 | end 72 | end 73 | 74 | ############################################################################ 75 | 76 | describe '#node!' do 77 | context 'when empty' do 78 | it 'does raise an error' do 79 | expect { Sycamore::Tree.new.node! }.to raise_error Sycamore::EmptyNodeSet 80 | end 81 | end 82 | 83 | context 'when containing a single node' do 84 | context 'without children' do 85 | it 'does return the node' do 86 | expect( Sycamore::Tree[1].node! ).to eql 1 87 | end 88 | end 89 | 90 | context 'with children' do 91 | it 'does return the node' do 92 | expect( Sycamore::Tree[1 => [2,3]].node! ).to eql 1 93 | end 94 | end 95 | end 96 | 97 | context 'when containing multiple nodes' do 98 | it 'does raise a TypeError' do 99 | expect { Sycamore::Tree[:foo, :bar].node! }.to raise_error Sycamore::NonUniqueNodeSet 100 | expect { Sycamore::Tree[foo: 1, bar: 2, baz: nil].node! }.to raise_error Sycamore::NonUniqueNodeSet 101 | end 102 | end 103 | end 104 | 105 | ############################################################################ 106 | 107 | describe '#child_of' do 108 | it 'does return the child tree of the given node, when the given the node is present' do 109 | expect( Sycamore::Tree[property: :value].child_of(:property).node ).to be :value 110 | end 111 | 112 | it 'does return an absent tree, when the given node is a leaf' do 113 | expect( Sycamore::Tree[42].child_of(42) ).to be_a Sycamore::Absence 114 | end 115 | 116 | it 'does return an absent tree, when the given the node is not present' do 117 | expect( Sycamore::Tree.new.child_of(:missing) ).to be_a Sycamore::Absence 118 | end 119 | 120 | context 'edge cases' do 121 | it 'does treat nil like any other value' do 122 | expect( Sycamore::Tree[nil => 42].child_of(nil).node ).to be 42 123 | expect( Sycamore::Tree[4 => {nil => 2}].child_of(4) ).to eql Sycamore::Tree[nil => 2] 124 | expect( Sycamore::Tree[nil => 42].child_of(nil).node ).to be 42 125 | expect( Sycamore::Tree[nil].child_of(nil) ).to be_a Sycamore::Absence 126 | expect( Sycamore::Tree.new.child_of(nil) ).to be_a Sycamore::Absence 127 | end 128 | 129 | it 'does treat false like any other value' do 130 | expect( Sycamore::Tree[false => 42].child_of(false).node ).to be 42 131 | expect( Sycamore::Tree[4 => {false => 2}].child_of(4) ).to eql Sycamore::Tree[false => 2] 132 | expect( Sycamore::Tree[false => 42].child_of(false).node ).to be 42 133 | expect( Sycamore::Tree[false].child_of(false) ).to be_a Sycamore::Absence 134 | expect( Sycamore::Tree.new.child_of(false ) ).to be_a Sycamore::Absence 135 | end 136 | 137 | it 'does raise an error, when given the Nothing tree' do 138 | expect { Sycamore::Tree.new.child_of(Sycamore::Nothing) }.to raise_error Sycamore::InvalidNode 139 | end 140 | 141 | it 'does raise an error, when given an Enumerable' do 142 | expect { Sycamore::Tree.new.child_of([1]) }.to raise_error Sycamore::InvalidNode 143 | expect { Sycamore::Tree.new.child_of([1, 2]) }.to raise_error Sycamore::InvalidNode 144 | expect { Sycamore::Tree.new.child_of(foo: :bar) }.to raise_error Sycamore::InvalidNode 145 | expect { Sycamore::Tree.new.child_of(Sycamore::Tree[1]) }.to raise_error Sycamore::InvalidNode 146 | end 147 | end 148 | end 149 | 150 | ############################################################################ 151 | 152 | describe '#child_at' do 153 | context 'when given a path as a sequence of nodes' do 154 | it 'does return the child tree of the given node, when the node at the given path is present' do 155 | expect( Sycamore::Tree[property: :value].child_at(:property).node ).to be :value 156 | 157 | expect( Sycamore::Tree[1 => {2 => 3}].child_at(1, 2).node ).to be 3 158 | expect( Sycamore::Tree[1 => {2 => 3}].child_at([1, 2]).node ).to be 3 159 | end 160 | 161 | it 'does return an absent tree, when the node at the given path is a leaf' do 162 | expect( Sycamore::Tree[42 ].child_at(42 ) ).to be_a Sycamore::Absence 163 | expect( Sycamore::Tree[1=>{2=>3}].child_at(1, 2, 3 ) ).to be_a Sycamore::Absence 164 | expect( Sycamore::Tree[1=>{2=>3}].child_at([1, 2, 3]) ).to be_a Sycamore::Absence 165 | end 166 | 167 | context 'when the node at the given path is not present' do 168 | it 'does return an absent tree' do 169 | expect( Sycamore::Tree.new.child_at(:missing ) ).to be_a Sycamore::Absence 170 | expect( Sycamore::Tree.new.child_at( 1, 2, 3 ) ).to be_a Sycamore::Absence 171 | expect( Sycamore::Tree.new.child_at([1, 2, 3]) ).to be_a Sycamore::Absence 172 | end 173 | 174 | it 'does return a correctly configured absent tree' do 175 | tree = Sycamore::Tree.new 176 | absent_tree = tree.child_at(1, 2, 3) 177 | absent_tree << 4 178 | expect(tree).to eql Sycamore::Tree[1=>{2=>{3=>4}}] 179 | end 180 | end 181 | end 182 | 183 | context 'when given a path as a Sycamore::Path object' do 184 | it 'does return the child tree of the given node, when the node at the given path is present' do 185 | expect( Sycamore::Tree[property: :value].child_at(Sycamore::Path[:property]).node ).to be :value 186 | expect( Sycamore::Tree[1 => {2 => 3}].child_at(Sycamore::Path[1, 2]).node ).to be 3 187 | end 188 | 189 | it 'does return an absent tree, when the node at the given path is a leaf' do 190 | expect( Sycamore::Tree[42 ].child_at(Sycamore::Path[42] ) ).to be_a Sycamore::Absence 191 | expect( Sycamore::Tree[1,2,3].child_at(Sycamore::Path[1, 2, 3]) ).to be_a Sycamore::Absence 192 | end 193 | 194 | context 'when the node at the given path is not present' do 195 | it 'does return an absent tree' do 196 | expect( Sycamore::Tree.new.child_at(Sycamore::Path[:missing]) ).to be_a Sycamore::Absence 197 | expect( Sycamore::Tree.new.child_at(Sycamore::Path[1, 2, 3 ]) ).to be_a Sycamore::Absence 198 | end 199 | 200 | it 'does return a correctly configured absent tree' do 201 | tree = Sycamore::Tree.new 202 | absent_tree = tree.child_at(Sycamore::Path[1, 2, 3]) 203 | absent_tree << 4 204 | expect(tree).to eql Sycamore::Tree[1=>{2=>{3=>4}}] 205 | end 206 | end 207 | end 208 | 209 | context 'edge cases' do 210 | it 'does raise an ArgumentError, when given no arguments' do 211 | expect { Sycamore::Tree.new.child_at() }.to raise_error ArgumentError 212 | end 213 | 214 | it 'does raise an ArgumentError, when given an empty enumerable' do 215 | expect { Sycamore::Tree.new.child_at([]) }.to raise_error ArgumentError 216 | expect { Sycamore::Tree.new.child_at(Sycamore::Path[]) }.to raise_error ArgumentError 217 | end 218 | 219 | it 'does treat nil like any other value' do 220 | expect( Sycamore::Tree[nil => 42 ].child_at(nil).node ).to be 42 221 | expect( Sycamore::Tree[4 => {nil => 2} ].child_at(4) ).to eq Sycamore::Tree[nil => 2] 222 | expect( Sycamore::Tree[1 => {nil => 3}].child_at(1, nil).node ).to be 3 223 | expect( Sycamore::Tree[nil => {nil => nil}].child_at(nil, nil).node ).to be nil 224 | expect( Sycamore::Tree[nil => nil].child_at(nil, nil) ).to be_a Sycamore::Absence 225 | expect( Sycamore::Tree.new.child_at(nil, nil) ).to be_a Sycamore::Absence 226 | 227 | tree = Sycamore::Tree.new 228 | absent_tree = tree.child_at(nil, nil, nil) 229 | absent_tree << nil 230 | expect(tree).to eql Sycamore::Tree[nil => {nil => {nil => [nil]}}] 231 | 232 | expect( Sycamore::Tree[nil => 42 ].child_at(Sycamore::Path[nil]).node ).to be 42 233 | expect( Sycamore::Tree[4 => {nil => 2} ].child_at(Sycamore::Path[4]) ).to eq Sycamore::Tree[nil => 2] 234 | end 235 | 236 | it 'does treat boolean values like any other value' do 237 | expect( Sycamore::Tree[false => 42 ].child_at(false).node ).to be 42 238 | expect( Sycamore::Tree[4 => {false => 2}].child_at(4) ).to eq Sycamore::Tree[false => 2] 239 | expect( Sycamore::Tree[1 => {false => 3}].child_at(1, false).node ).to be 3 240 | expect( Sycamore::Tree[false => {false => false}].child_at(false, false).node ).to be false 241 | expect( Sycamore::Tree[false => false].child_at(false, false) ).to be_a Sycamore::Absence 242 | expect( Sycamore::Tree.new.child_at(false, false) ).to be_a Sycamore::Absence 243 | 244 | tree = Sycamore::Tree.new 245 | absent_tree = tree.child_at(false, false, false) 246 | absent_tree << false 247 | expect(tree).to eql Sycamore::Tree[false => {false => {false => false}}] 248 | 249 | expect( Sycamore::Tree[false => 42 ].child_at(Sycamore::Path[false]).node ).to be 42 250 | expect( Sycamore::Tree[4 => {false => 2} ].child_at(Sycamore::Path[4]) ).to eq Sycamore::Tree[false => 2] 251 | end 252 | end 253 | end 254 | 255 | ############################################################################ 256 | 257 | describe '#fetch' do 258 | let(:example_tree) { Sycamore::Tree[foo: {bar: :baz}] } 259 | 260 | context 'when given no default value or block' do 261 | it 'does return the child tree of the given node, when the given the node is present' do 262 | expect( Sycamore::Tree[property: :value].fetch(:property).node ).to be :value 263 | expect( Sycamore::Tree[false => 42 ].fetch(false).node ).to be 42 264 | expect( Sycamore::Tree[true => 42 ].fetch(true).node ).to be 42 265 | expect( example_tree.fetch(:foo) ).to be example_tree.child_of(:foo) 266 | end 267 | 268 | it 'does raise an error, when the given node has no child tree' do 269 | expect { Sycamore::Tree[42 ].fetch(42 ) }.to raise_error KeyError 270 | expect { Sycamore::Tree[false].fetch(false) }.to raise_error KeyError 271 | expect { Sycamore::Tree[true ].fetch(true) }.to raise_error KeyError 272 | end 273 | 274 | it 'does raise an error, when the given the node is not present' do 275 | expect { Sycamore::Tree.new.fetch(:missing) }.to raise_error KeyError 276 | expect { Sycamore::Tree[false].fetch(true) }.to raise_error KeyError 277 | expect { Sycamore::Tree[true ].fetch(false) }.to raise_error KeyError 278 | end 279 | end 280 | 281 | context 'when given a default value' do 282 | it 'does return the child tree of the given node, when the given the node is present' do 283 | expect( Sycamore::Tree[property: :value].fetch(:property, :default).node ).to be :value 284 | expect( Sycamore::Tree[false => 42 ].fetch(false , true).node ).to be 42 285 | expect( Sycamore::Tree[true => 42 ].fetch(true , false).node ).to be 42 286 | expect( example_tree.fetch(:foo, :default) ).to be example_tree.child_of(:foo) 287 | end 288 | 289 | it 'does return the given default value, when the given node has no child tree' do 290 | expect( Sycamore::Tree[:miss].fetch(:miss, :default) ).to be :default 291 | expect( Sycamore::Tree[false].fetch(false, :default) ).to be :default 292 | expect( Sycamore::Tree[true ].fetch(true , :default) ).to be :default 293 | end 294 | 295 | it 'does return the given default value, when the given the node is not present' do 296 | expect( Sycamore::Tree.new.fetch(:miss , :default) ).to be :default 297 | expect( Sycamore::Tree[false].fetch(true , :default) ).to be :default 298 | expect( Sycamore::Tree[true ].fetch(false, :default) ).to be :default 299 | end 300 | end 301 | 302 | context 'when given a default block' do 303 | it 'does return the child tree of the given node, when the given the node is present' do 304 | expect( Sycamore::Tree[property: :value].fetch(:property) { :default }.node ).to be :value 305 | expect( Sycamore::Tree[false => 42 ].fetch(false ) { true }.node ).to be 42 306 | expect( Sycamore::Tree[true => 42 ].fetch(true ) { false }.node ).to be 42 307 | expect( example_tree.fetch(:foo, :default) ).to be example_tree.child_of(:foo) 308 | end 309 | 310 | it 'does return the evaluation result of given block, when the given node has no child tree' do 311 | expect( Sycamore::Tree[:miss].fetch(:miss) { :default } ).to be :default 312 | expect( Sycamore::Tree[false].fetch(false) { :default } ).to be :default 313 | expect( Sycamore::Tree[true ].fetch(true ) { :default } ).to be :default 314 | end 315 | 316 | it 'does return the evaluation result of given block, when the given the node is not present' do 317 | expect( Sycamore::Tree.new.fetch(:miss ) { :default } ).to be :default 318 | expect( Sycamore::Tree[false].fetch(true ) { :default } ).to be :default 319 | expect( Sycamore::Tree[true ].fetch(false) { :default } ).to be :default 320 | end 321 | end 322 | 323 | context 'when given a path object' do 324 | it 'does return the child tree of the node at the given given path, when present' do 325 | expect( example_tree.fetch Sycamore::Path[:foo] ).to eql Sycamore::Tree[bar: :baz] 326 | expect( example_tree.fetch Sycamore::Path[:foo, :bar] ).to eql Sycamore::Tree[:baz] 327 | end 328 | 329 | it 'does behave like fetch, when no child tree at the given path present' do 330 | expect { example_tree.fetch Sycamore::Path[:missing_node, :foo] }.to raise_error KeyError, /missing_node/ 331 | expect { example_tree.fetch Sycamore::Path[:foo, :missing_node, :baz] }.to raise_error KeyError, /missing_node/ 332 | expect { example_tree.fetch Sycamore::Path[:foo, :bar, :missing_node] }.to raise_error KeyError, /missing_node/ 333 | expect { example_tree.fetch Sycamore::Path[:foo, :bar, :baz] }.to raise_error Sycamore::ChildError, 'node :baz has no child tree' 334 | 335 | expect( example_tree.fetch(Sycamore::Path[:missing, :foo ], :default) ).to be :default 336 | expect( example_tree.fetch(Sycamore::Path[:foo, :missing, :baz]) { :default } ).to be :default 337 | expect( example_tree.fetch(Sycamore::Path[:foo, :bar, :missing], :default) ).to be :default 338 | expect( example_tree.fetch(Sycamore::Path[:foo, :bar, :baz]) { :default } ).to be :default 339 | end 340 | end 341 | 342 | context 'edge cases' do 343 | it 'does treat nil like any other value' do 344 | expect { Sycamore::Tree.new.fetch(nil) }.to raise_error KeyError 345 | expect( Sycamore::Tree.new.fetch(nil, :default) ).to be :default 346 | expect( Sycamore::Tree.new.fetch(nil) { :default } ).to be :default 347 | 348 | expect { Sycamore::Tree[nil].fetch(nil) }.to raise_error KeyError 349 | expect( Sycamore::Tree[nil].fetch(nil, :default) ).to be :default 350 | expect( Sycamore::Tree[nil].fetch(nil) { :default } ).to be :default 351 | 352 | expect( Sycamore::Tree[nil => :foo].fetch(nil).node ).to be :foo 353 | expect( Sycamore::Tree[nil => :foo].fetch(nil, :default).node ).to be :foo 354 | expect( Sycamore::Tree[nil => :foo].fetch(nil) { :default }.node ).to be :foo 355 | end 356 | 357 | it 'does raise an error, when given the Nothing tree' do 358 | expect { Sycamore::Tree.new.fetch(Sycamore::Nothing) }.to raise_error Sycamore::InvalidNode 359 | expect { Sycamore::Tree.new.fetch(Sycamore::Nothing, :default) }.to raise_error Sycamore::InvalidNode 360 | expect { Sycamore::Tree.new.fetch(Sycamore::Nothing) { 42 } }.to raise_error Sycamore::InvalidNode 361 | end 362 | 363 | it 'does raise an error, when given an Enumerable' do 364 | expect { Sycamore::Tree.new.fetch([1]) }.to raise_error Sycamore::InvalidNode 365 | expect { Sycamore::Tree.new.fetch(foo: :bar) }.to raise_error Sycamore::InvalidNode 366 | expect { Sycamore::Tree.new.fetch(Sycamore::Tree[1]) }.to raise_error Sycamore::InvalidNode 367 | end 368 | end 369 | end 370 | 371 | ############################################################################ 372 | 373 | describe '#fetch_path' do 374 | let(:example_tree) { Sycamore::Tree[foo: {bar: :baz}] } 375 | 376 | 377 | context 'when given a nodes path as an enumerable of nodes' do 378 | it 'does return the child tree of the node at the given given path, when present' do 379 | expect( example_tree.fetch_path [:foo] ).to eql Sycamore::Tree[bar: :baz] 380 | expect( example_tree.fetch_path [:foo, :bar] ).to eql Sycamore::Tree[:baz] 381 | end 382 | 383 | it 'does behave like fetch, when no child tree at the given path present' do 384 | expect { example_tree.fetch_path [:missing_node, :foo] }.to raise_error KeyError, /missing_node/ 385 | expect { example_tree.fetch_path [:foo, :missing_node, :baz] }.to raise_error KeyError, /missing_node/ 386 | expect { example_tree.fetch_path [:foo, :bar, :missing_node] }.to raise_error KeyError, /missing_node/ 387 | expect { example_tree.fetch_path [:foo, :bar, :baz] }.to raise_error Sycamore::ChildError, 'node :baz has no child tree' 388 | 389 | expect( example_tree.fetch_path([:missing, :foo ], :default) ).to be :default 390 | expect( example_tree.fetch_path([:foo, :missing, :baz]) { :default } ).to be :default 391 | expect( example_tree.fetch_path([:foo, :bar, :missing], :default) ).to be :default 392 | expect( example_tree.fetch_path([:foo, :bar, :baz]) { :default } ).to be :default 393 | end 394 | end 395 | 396 | context 'when given a nodes path as a Sycamore::Path' do 397 | it 'does return the child tree of the node at the given given path, when present' do 398 | expect( example_tree.fetch_path Sycamore::Path[:foo] ).to eql Sycamore::Tree[bar: :baz] 399 | expect( example_tree.fetch_path Sycamore::Path[:foo, :bar] ).to eql Sycamore::Tree[:baz] 400 | end 401 | 402 | it 'does behave like fetch, when no child tree at the given path present' do 403 | expect { example_tree.fetch_path Sycamore::Path[:missing_node, :foo] }.to raise_error KeyError, /missing_node/ 404 | expect { example_tree.fetch_path Sycamore::Path[:foo, :missing_node, :baz] }.to raise_error KeyError, /missing_node/ 405 | expect { example_tree.fetch_path Sycamore::Path[:foo, :bar, :missing_node] }.to raise_error KeyError, /missing_node/ 406 | expect { example_tree.fetch_path Sycamore::Path[:foo, :bar, :baz] }.to raise_error Sycamore::ChildError, 'node :baz has no child tree' 407 | 408 | expect( example_tree.fetch_path(Sycamore::Path[:missing, :foo ], :default) ).to be :default 409 | expect( example_tree.fetch_path(Sycamore::Path[:foo, :missing, :baz]) { :default } ).to be :default 410 | expect( example_tree.fetch_path(Sycamore::Path[:foo, :bar, :missing], :default) ).to be :default 411 | expect( example_tree.fetch_path(Sycamore::Path[:foo, :bar, :baz]) { :default } ).to be :default 412 | end 413 | end 414 | 415 | context 'edge cases' do 416 | it 'raises an error, when given no arguments' do 417 | expect { example_tree.fetch_path }.to raise_error ArgumentError 418 | end 419 | 420 | it 'does return the tree itself, when the given path is empty' do 421 | expect( example_tree.fetch_path [] ).to be example_tree 422 | expect( example_tree.fetch_path Sycamore::Path[] ).to be example_tree 423 | end 424 | end 425 | end 426 | 427 | ############################################################################ 428 | 429 | describe '#search' do 430 | context 'when given a single node' do 431 | it 'does return an empty array, when the given node is not present in the tree or any of its child trees' do 432 | expect( Sycamore::Tree.new.search(1) ).to be_an(Array).and be_empty 433 | expect( Sycamore::Tree[foo: '1'].search(1) ).to be_an(Array).and be_empty 434 | end 435 | 436 | it 'does return the root path, when the given node is present in the node set directly' do 437 | expect( Sycamore::Tree[1,2,3].search(2) ).to contain_exactly Sycamore::Path::ROOT 438 | end 439 | 440 | it 'does return all paths to the child trees, where the given node is in the node set' do 441 | expect( Sycamore::Tree[foo: :bar].search(:bar) ).to contain_exactly Sycamore::Path[:foo] 442 | expect( Sycamore::Tree[1 => {2 => [3, 42], 42 => 4}].search(42) ) 443 | .to contain_exactly Sycamore::Path[1, 2], Sycamore::Path[1] 444 | end 445 | 446 | it 'does return every path to the child trees, even when the given node is multiple times present on the path' do 447 | expect( Sycamore::Tree[foo: {foo: :foo}].search(:foo) ) 448 | .to contain_exactly Sycamore::Path[], Sycamore::Path[:foo], Sycamore::Path[:foo, :foo] 449 | end 450 | end 451 | 452 | context 'when given an enumerable collection of nodes' do 453 | it 'does return an empty array, when all of the given nodes are not present in the tree or any of its child trees' do 454 | expect( Sycamore::Tree[1].search([1,2]) ).to be_an(Array).and be_empty 455 | expect( Sycamore::Tree[foo: :bar].search([:foo, :bar]) ).to be_an(Array).and be_empty 456 | end 457 | 458 | it 'does return the root path, when the given nodes are present in the node set directly' do 459 | expect( Sycamore::Tree[1,2,3].search([1, 2]) ).to contain_exactly Sycamore::Path::ROOT 460 | end 461 | 462 | it 'does return all paths to the child trees, where the given nodes are in the node set' do 463 | expect( Sycamore::Tree[foo: [:bar, :baz]].search([:bar, :baz]) ).to contain_exactly Sycamore::Path[:foo] 464 | expect( Sycamore::Tree[1 => {2 => [3, :foo, :bar], 4 => [:foo, :bar]}].search([:foo, :bar]) ) 465 | .to contain_exactly Sycamore::Path[1, 2], Sycamore::Path[1, 4] 466 | end 467 | 468 | it 'does return every path to the child trees, even when the given nodes are multiple times present on the path' do 469 | expect( Sycamore::Tree[foo: {foo: [:foo, :bar]}, bar: nil].search([:foo, :bar]) ) 470 | .to contain_exactly Sycamore::Path[], Sycamore::Path[:foo, :foo] 471 | end 472 | end 473 | 474 | context 'when given a tree-like structure' do 475 | it 'does return an empty array, when all the given tree structure is not present in the tree or any of its child trees' do 476 | expect( Sycamore::Tree[1].search(foo: :bar) ).to be_an(Array).and be_empty 477 | expect( Sycamore::Tree[foo: :bar].search(foo: :baz) ).to be_an(Array).and be_empty 478 | end 479 | 480 | it 'does return the root path, when the given tree structure is present directly' do 481 | expect( Sycamore::Tree[foo: :bar].search(foo: :bar) ).to contain_exactly Sycamore::Path::ROOT 482 | end 483 | 484 | it 'does return all paths to the child trees, where the given tree structure is present' do 485 | expect( Sycamore::Tree[1 => {foo: [:bar, :baz]}].search(foo: :bar) ).to contain_exactly Sycamore::Path[1] 486 | expect( Sycamore::Tree[1 => {2 => {3 => nil, foo: :bar}, 4 => {foo: :bar}}].search(foo: :bar) ) 487 | .to contain_exactly Sycamore::Path[1, 2], Sycamore::Path[1, 4] 488 | end 489 | 490 | it 'does return every path to the child trees, even when the given tree structure is multiple times present on the path' do 491 | expect( Sycamore::Tree[foo: {foo: {foo: :bar}, bar: nil}].search(foo: :bar) ) 492 | .to contain_exactly Sycamore::Path[], Sycamore::Path[:foo, :foo] 493 | end 494 | end 495 | end 496 | 497 | end 498 | -------------------------------------------------------------------------------- /spec/unit/sycamore/tree/addition_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Tree do 2 | 3 | subject(:tree) { Sycamore::Tree.new } 4 | 5 | ############################################################################ 6 | 7 | describe '#add_node_with_empty_child' do 8 | it 'does add the given node' do 9 | expect( tree.add_node_with_empty_child(:foo) ).to include_node :foo 10 | end 11 | 12 | it 'does create the new child with #new_child' do 13 | expect( tree ).to receive(:new_child) 14 | tree.add_node_with_empty_child(:foo) 15 | end 16 | 17 | it 'does add an empty tree as a child of the given node' do 18 | expect( tree.add_node_with_empty_child(:foo).child_of(:foo) ).to be_a Sycamore::Tree 19 | expect( tree.add_node_with_empty_child(:foo).child_of(:foo) ).not_to be Sycamore::Nothing 20 | expect( tree.add_node_with_empty_child(:foo).child_of(:foo) ).not_to be_absent 21 | expect( tree.add_node_with_empty_child(:foo).child_of(:foo) ).to be_empty 22 | 23 | expect( Sycamore::Tree[:foo].add_node_with_empty_child(:foo).child_of(:foo) ).to be_a Sycamore::Tree 24 | expect( Sycamore::Tree[:foo].add_node_with_empty_child(:foo).child_of(:foo) ).not_to be Sycamore::Nothing 25 | expect( Sycamore::Tree[:foo].add_node_with_empty_child(:foo).child_of(:foo) ).not_to be_absent 26 | expect( Sycamore::Tree[:foo].add_node_with_empty_child(:foo).child_of(:foo) ).to be_empty 27 | end 28 | 29 | it 'does nothing, when the given node is already present' do 30 | tree = Sycamore::Tree[foo: :bar] 31 | expect { tree.add_node_with_empty_child(:foo) }.not_to change { tree.child_of(:foo) } 32 | end 33 | 34 | context 'edge cases' do 35 | it 'does treat nil like any other value' do 36 | expect( tree.add_node_with_empty_child(nil) ).to include_node nil 37 | end 38 | end 39 | end 40 | 41 | ############################################################################ 42 | 43 | describe '#add' do 44 | context 'when given a single node' do 45 | it 'does add the value to the set of nodes' do 46 | expect( Sycamore::Tree.new.add 1 ).to include_node 1 47 | end 48 | 49 | context 'when the given value is already present' do 50 | it 'does nothing' do 51 | expect( Sycamore::Tree[1].add(1).size ).to be 1 52 | end 53 | 54 | it 'does not overwrite the existing children' do 55 | expect( Sycamore::Tree[a: 1].add(:a) ).to include_tree(a: 1) 56 | end 57 | end 58 | 59 | context 'edge cases' do 60 | it 'does nothing, when given the Nothing tree' do 61 | expect( Sycamore::Tree.new.add Sycamore::Nothing ).to be_empty 62 | end 63 | 64 | it 'does treat nil like any other value' do 65 | expect( Sycamore::Tree.new.add nil).to include_node nil 66 | end 67 | 68 | it 'does treat false like any other value' do 69 | expect( Sycamore::Tree.new.add false).to include_node false 70 | end 71 | end 72 | end 73 | 74 | context 'when given an array' do 75 | it 'does add all values to the set of nodes' do 76 | expect( Sycamore::Tree.new.add [1,2] ).to include_nodes 1, 2 77 | end 78 | 79 | it 'does merge the values with the existing nodes' do 80 | expect( Sycamore::Tree[1,2].add([2,3]).nodes.to_set ).to eql Set[1,2,3] 81 | end 82 | 83 | it 'does ignore duplicates' do 84 | expect( Sycamore::Tree.new.add [1,2,2,3,3,3] ).to include_nodes 1, 2, 3 85 | expect( Sycamore::Tree.new.add(['foo', 'bar', 'baz', 'foo', 'bar']).nodes.to_set).to eql %w[baz foo bar].to_set 86 | end 87 | 88 | context 'when the array is nested' do 89 | it 'does treat hashes as trees' do 90 | expect( Sycamore::Tree.new.add [:a, b: 1] ).to include_tree({a: nil, b: 1}) 91 | expect( Sycamore::Tree.new.add [:b, a: 1, c: 2 ] ).to include_tree({a: 1, b: nil, c: 2}) 92 | expect( Sycamore::Tree.new.add [:b, {a: 1, c: 2}] ).to include_tree({a: 1, b: nil, c: 2}) 93 | expect( Sycamore::Tree.new.add [:a, b: {c: 2} ] ).to include_tree({a: nil, b: {c: 2}}) 94 | end 95 | 96 | it 'does merge the children of duplicate nodes' do 97 | expect( Sycamore::Tree.new.add [1,{1=>2}] ).to include_tree({1=>2}) 98 | expect( Sycamore::Tree.new.add [1,{1=>2}, {1=>3}] ).to include_tree({1=>[2,3]}) 99 | expect( Sycamore::Tree.new.add [1,{1=>{2=>3}}, {1=>{2=>4}}] ).to include_tree({1=>{2=>[3,4]}}) 100 | end 101 | end 102 | 103 | context 'when the array contains a nested enumerable that is not Tree-like' do 104 | it 'raises an error' do 105 | expect { Sycamore::Tree.new.add([1, [2, 3]]) }.to raise_error Sycamore::InvalidNode 106 | end 107 | 108 | it 'does not change the tree' do 109 | expect { tree.add([1, [2, 3]]) }.to raise_error Sycamore::InvalidNode 110 | expect( tree ).to be_empty 111 | end 112 | end 113 | 114 | context 'edge cases' do 115 | it 'does treat nil like any other value' do 116 | expect( Sycamore::Tree.new.add([1, nil]).nodes.to_set ).to eq [1, nil].to_set 117 | expect( Sycamore::Tree.new.add([nil, :foo, nil, :bar]).nodes.to_set).to eql [:foo, :bar, nil].to_set 118 | end 119 | 120 | it 'does nothing, when given an empty array' do 121 | expect( Sycamore::Tree.new.add [] ).to be_empty 122 | end 123 | end 124 | end 125 | 126 | ADD_TREE_EXAMPLES = [ 127 | { foo: :bar }, 128 | { foo: [:bar, :baz] }, 129 | { a: 1, b: 2 }, 130 | { a: 1, b: [2,3] }, 131 | { a: [1, 'foo'], b: {2 => 3} }, 132 | { foo: {bar: :baz} }, 133 | ] 134 | 135 | MERGE_TREE_EXAMPLES = [ 136 | { before: {foo: [1, 2]}, add: {foo: [2, 3]}, after: {foo: [1, 2, 3]} }, 137 | { before: {foo: {1=>2}}, add: {foo: {1=>3}}, after: {foo: {1=>[2, 3]}} }, 138 | { before: {noah: { shem: :elam }}, 139 | add: {noah: { shem: :asshur, japeth: :gomer}}, 140 | after: {noah: { shem: [:elam, :asshur], japeth: :gomer}} }, 141 | ] 142 | 143 | context 'when given a hash' do 144 | it 'does add the given tree structure' do 145 | ADD_TREE_EXAMPLES.each do |example| 146 | expect( Sycamore::Tree.new.add(example) ).to include_tree example 147 | end 148 | end 149 | 150 | it 'does merge the given hash with the existing tree structure' do 151 | MERGE_TREE_EXAMPLES.each do |example| 152 | expect( Sycamore::Tree[example[:before]].add(example[:add]) ) 153 | .to eql Sycamore::Tree[example[:after]] 154 | end 155 | end 156 | 157 | context 'when given a tree with an enumerable key' do 158 | it 'raises an error' do 159 | expect { Sycamore::Tree.new.add([1,2] => 3) }.to raise_error Sycamore::InvalidNode 160 | expect { Sycamore::Tree.new.add({1 => 2} => 3) }.to raise_error Sycamore::InvalidNode 161 | expect { Sycamore::Tree.new.add(Sycamore::Tree[1] => 42) }.to raise_error Sycamore::InvalidNode 162 | expect { Sycamore::Tree.new.add(Sycamore::Nothing => 42) }.to raise_error Sycamore::InvalidNode 163 | end 164 | 165 | it 'does not change the tree' do 166 | expect { tree.add([foo: :bar, [1,2] => 3]) }.to raise_error Sycamore::InvalidNode 167 | expect( tree ).to be_empty 168 | end 169 | end 170 | 171 | context 'edge cases' do 172 | it 'does nothing, when given an empty hash' do 173 | expect( Sycamore::Tree.new << {} ).to be_empty 174 | end 175 | 176 | it 'does treat false as a key like any other value' do 177 | expect( Sycamore::Tree.new.add(false => 1) ).to include_tree({false => 1}) 178 | end 179 | 180 | it 'does treat nil as a key like any other value' do 181 | expect( Sycamore::Tree.new.add(nil => 1) ).to include_tree({nil => 1}) 182 | end 183 | 184 | it 'does ignore Nothing-like values as children' do 185 | expect(Sycamore::Tree.new.add({1 => Sycamore::Nothing}).child_of(1)).to be_absent 186 | expect(Sycamore::Tree.new.add({1 => nil, 2 => nil}).child_of(1)).to be_absent 187 | end 188 | 189 | it 'does add empty child enumerables as empty trees' do 190 | expect(Sycamore::Tree.new.add(1 => []).child_of(1)).not_to be_absent 191 | expect(Sycamore::Tree.new.add({1 => {}, 2 => {}}).child_of(1)).not_to be_absent 192 | end 193 | 194 | it 'does add a child with a nil node, when given an Array with nil as a child' do 195 | expect(Sycamore::Tree.new.add({1 => [nil]}).child_of(1)).not_to be_absent 196 | expect(Sycamore::Tree.new.add({1 => [nil], 2 => [nil]}).child_of(1)).to include_node nil 197 | end 198 | end 199 | end 200 | 201 | context 'when given a tree' do 202 | it 'does add the tree structure' do 203 | ADD_TREE_EXAMPLES.each do |example| 204 | expect( Sycamore::Tree.new.add(example) ).to include_tree example 205 | end 206 | end 207 | 208 | it 'does merge the tree with the existing tree structure' do 209 | MERGE_TREE_EXAMPLES.each do |example| 210 | expect( Sycamore::Tree[example[:before]] .add(Sycamore::Tree[example[:add]]) ) 211 | .to eql Sycamore::Tree[example[:after]] 212 | end 213 | end 214 | 215 | context 'edge cases' do 216 | it 'does nothing, when given an empty tree' do 217 | expect( Sycamore::Tree.new << Sycamore::Tree.new ).to be_empty 218 | end 219 | 220 | it 'does nothing, when given the Nothing tree' do 221 | expect( Sycamore::Tree.new << Sycamore::Nothing ).to be_empty 222 | end 223 | 224 | context 'when given an Absence' do 225 | let(:absent_tree) { Sycamore::Tree.new.child_of(:missing) } 226 | 227 | it 'does ignore it, when it is absent' do 228 | expect( Sycamore::Tree.new.add absent_tree ).to be_empty 229 | expect( Sycamore::Tree.new.add(1 => absent_tree).leaf?(1) ).to be true 230 | end 231 | 232 | it 'does treat it like a normal tree, when it was created' do 233 | absent_tree << 42 234 | 235 | expect( Sycamore::Tree.new.add absent_tree ).to eql Sycamore::Tree[42] 236 | expect( Sycamore::Tree.new.add 1 => absent_tree ).to eql Sycamore::Tree[1 => 42] 237 | end 238 | end 239 | end 240 | end 241 | 242 | context 'when given a single Path object' do 243 | let(:path) { Sycamore::Path[:foo, :bar, :baz] } 244 | 245 | it 'does add all nodes, when the path does not exist' do 246 | expect( Sycamore::Tree.new.add(path) ).to include_path(path) 247 | end 248 | 249 | it 'does add the missing nodes, when the path exists partially' do 250 | expect( Sycamore::Tree[foo: :bar].add(path) ) 251 | .to eql Sycamore::Tree[foo: {bar: :baz}] 252 | expect( Sycamore::Tree[foo: :other].add(path) ) 253 | .to eql Sycamore::Tree[foo: {bar: :baz, other: nil}] 254 | end 255 | 256 | it 'does nothing, when the path already exists' do 257 | expect( Sycamore::Tree[foo: {bar: :baz}].add(path) ) 258 | .to eql Sycamore::Tree[foo: {bar: :baz}] 259 | expect( Sycamore::Tree[foo: {bar: {baz: :more}}].add(path) ) 260 | .to eql Sycamore::Tree[foo: {bar: {baz: :more}}] 261 | end 262 | 263 | it 'does not add an empty child at the path' do 264 | expect( Sycamore::Tree.new.add(path).child_at(path) ).to be_absent 265 | expect( Sycamore::Tree[foo: :bar].add(path).child_at(path) ).to be_absent 266 | expect( Sycamore::Tree[foo: {bar: :baz}].add(path).child_at(path) ).to be_absent 267 | end 268 | 269 | context 'edge cases' do 270 | it 'does nothing, when given an empty path' do 271 | expect( Sycamore::Tree[foo: :bar].add(Sycamore::Path[]) ) 272 | .to eql Sycamore::Tree[foo: :bar] 273 | end 274 | end 275 | end 276 | 277 | context 'when given an Enumerable of Path objects' do 278 | it 'does add all paths' do 279 | expect( Sycamore::Tree.new.add( 280 | [ Sycamore::Path[:foo, :bar, :baz], Sycamore::Path[1,2,3] ]) ) 281 | .to eql Sycamore::Tree[foo: {bar: :baz}, 1 => {2 => 3}] 282 | end 283 | end 284 | 285 | context 'when given an Enumerable of mixed objects' do 286 | it 'does add the elements appropriately' do 287 | expect( Sycamore::Tree.new.add( 288 | [ :foo, :bar, Sycamore::Path[:foo, :bar, :baz], {1=>2}, 289 | Sycamore::Tree[1=>{2=>3}]]) ) 290 | .to eql Sycamore::Tree[foo: {bar: :baz}, bar: nil, 1 => {2 => 3}] 291 | end 292 | end 293 | end 294 | 295 | ############################################################################ 296 | 297 | describe '#replace' do 298 | it 'does clear the tree before adding the arguments' do 299 | expect( Sycamore::Tree[:foo].replace(:bar).nodes ).to eql [:bar] 300 | expect( Sycamore::Tree[:foo].replace([:bar, :baz]).nodes ).to eql %i[bar baz] 301 | expect( Sycamore::Tree[a: 1].replace(a: 2) ).to include_tree(a: 2) 302 | expect( Sycamore::Tree[a: 1].replace(a: 2) ).not_to include_tree(a: 1) 303 | expect( Sycamore::Tree[a: 1].replace(Sycamore::Path[:foo, :bar]) ) 304 | .to eql Sycamore::Tree[foo: :bar] 305 | end 306 | 307 | context 'edge cases' do 308 | specify { expect( Sycamore::Tree[:foo].replace(nil).nodes ).to eql [nil] } 309 | specify { expect( Sycamore::Tree[:foo].replace([]).nodes ).to be_empty } 310 | specify { expect( Sycamore::Tree[:foo].replace({}).nodes ).to be_empty } 311 | specify { expect( Sycamore::Tree[:foo].replace(Sycamore::Nothing).nodes ).to be_empty } 312 | end 313 | end 314 | 315 | ############################################################################ 316 | 317 | describe '#[]=' do 318 | context 'when the node at the given path is present' do 319 | it 'does clear a child tree before adding the arguments to it' do 320 | tree = Sycamore::Tree[a: 1] 321 | expect { tree[:a] = 2 }.to change { tree[:a].node }.from( 1 ).to(2) 322 | expect { tree[:a] = [3, 4] }.to change { tree[:a].nodes }.from([2]).to([3,4]) 323 | 324 | tree = Sycamore::Tree[a: {b: 1}] 325 | expect { tree[:a] = :b }.to change { tree[:a, :b].nodes }.from([1]).to([]) 326 | 327 | tree = Sycamore::Tree[a: {b: 2}] 328 | expect { tree[:a, :b] = [3, 4] }.to change { tree[:a, :b].nodes }.from([2]).to([3,4]) 329 | 330 | tree = Sycamore::Tree[a: {b: 2}] 331 | expect { tree[:a, :b] = Sycamore::Path[3, 4] } 332 | .to change { tree }.to Sycamore::Tree[a: {b: {3=>4}}] 333 | end 334 | end 335 | 336 | context 'when the node at the given path is not present' do 337 | it 'does create the tree and add the arguments' do 338 | expect { tree[:a] = 1 }.to change { tree[:a].nodes }.from([]).to([1]) 339 | expect { tree[:b, :c] = 1 }.to change { tree[:b, :c].nodes }.from([]).to([1]) 340 | end 341 | end 342 | 343 | context 'when assigning Nothing' do 344 | context 'when the node at the given path is present' do 345 | it 'does remove a child' do 346 | tree = Sycamore::Tree[a: 1] 347 | expect { tree[:a] = Sycamore::Nothing } 348 | .to change { tree[:a].class }.from(Sycamore::Tree).to(Sycamore::Absence) 349 | expect( tree ).to eql Sycamore::Tree[:a] 350 | 351 | tree = Sycamore::Tree[a: {b: 1}] 352 | expect { tree[:a, :b] = Sycamore::Nothing } 353 | .to change { tree[:a, :b].class }.from(Sycamore::Tree).to(Sycamore::Absence) 354 | expect( tree ).to eql Sycamore::Tree[a: :b] 355 | end 356 | end 357 | 358 | context 'when the node at the given path is not present' do 359 | it 'does create the tree' do 360 | tree = Sycamore::Tree.new 361 | expect { tree[:a] = Sycamore::Nothing }.to change { tree }.from(Sycamore::Tree[]).to(Sycamore::Tree[:a]) 362 | tree = Sycamore::Tree.new 363 | expect { tree[:b, :c] = Sycamore::Nothing }.to change { tree }.from(Sycamore::Tree[]).to(Sycamore::Tree[b: :c]) 364 | end 365 | end 366 | end 367 | 368 | context 'when assigning nil' do 369 | context 'when the node at the given path is present' do 370 | it 'does remove a child' do 371 | tree = Sycamore::Tree[a: 1] 372 | expect { tree[:a] = nil } 373 | .to change { tree[:a].class }.from(Sycamore::Tree).to(Sycamore::Absence) 374 | expect( tree ).to eql Sycamore::Tree[:a] 375 | 376 | tree = Sycamore::Tree[a: {b: 1}] 377 | expect { tree[:a, :b] = nil } 378 | .to change { tree[:a, :b].class }.from(Sycamore::Tree).to(Sycamore::Absence) 379 | expect( tree ).to eql Sycamore::Tree[a: :b] 380 | end 381 | end 382 | 383 | context 'when the node at the given path is not present' do 384 | it 'does create the tree' do 385 | tree = Sycamore::Tree.new 386 | expect { tree[:a] = nil }.to change { tree }.from(Sycamore::Tree[]).to(Sycamore::Tree[:a]) 387 | tree = Sycamore::Tree.new 388 | expect { tree[:b, :c] = nil }.to change { tree }.from(Sycamore::Tree[]).to(Sycamore::Tree[b: :c]) 389 | end 390 | end 391 | 392 | it 'does assign a nil node, when assigning nil in an array' do 393 | tree = Sycamore::Tree[foo: :bar] 394 | tree[:foo] = [nil] 395 | expect( tree[:foo].nodes ).to contain_exactly nil 396 | end 397 | end 398 | 399 | context 'edge cases' do 400 | it 'does raise an error, when the given path is empty' do 401 | expect { tree[] = 42 }.to raise_error ArgumentError 402 | end 403 | 404 | it 'does treat nil as part of the path like any other value' do 405 | expect { tree[nil] = 1 }.to change { tree[nil].nodes }.from([]).to([1]) 406 | expect { tree[nil, nil] = 1 }.to change { tree[nil, nil].nodes }.from([]).to([1]) 407 | 408 | tree = Sycamore::Tree[nil => 1] 409 | expect { tree[nil] = 2 }.to change { tree[nil].node }.from(1).to(2) 410 | 411 | tree = Sycamore::Tree[nil => {nil => 2}] 412 | expect { tree[nil, nil] = [3, 4] }.to change { tree[nil, nil].nodes }.from([2]).to([3,4]) 413 | end 414 | end 415 | end 416 | 417 | end 418 | -------------------------------------------------------------------------------- /spec/unit/sycamore/tree/comparison_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Tree do 2 | 3 | ############################################################################ 4 | # Equality 5 | ############################################################################ 6 | 7 | MyTree = Class.new(Sycamore::Tree) 8 | 9 | TREE_EQL = [ 10 | [ Sycamore::Tree.new , Sycamore::Tree.new ], 11 | [ MyTree.new , MyTree.new ], 12 | [ Sycamore::Tree[1] , Sycamore::Tree[1] ], 13 | [ Sycamore::Tree[nil] , Sycamore::Tree[nil] ], 14 | [ Sycamore::Tree[1] , Sycamore::Tree[1 => Sycamore::Nothing] ], 15 | [ Sycamore::Tree[1] , Sycamore::Tree[1 => nil] ], 16 | [ Sycamore::Tree[1, 2] , Sycamore::Tree[1, 2] ], 17 | [ Sycamore::Tree[1, 2] , Sycamore::Tree[2, 1] ], 18 | [ Sycamore::Tree[a: 1] , Sycamore::Tree[a: 1] ], 19 | [ Sycamore::Tree[a: 1] , Sycamore::Tree[a: {1 => nil}] ], 20 | [ Sycamore::Tree[foo: 'foo', bar: %w[bar baz]], 21 | Sycamore::Tree[foo: 'foo', bar: %w[bar baz]] ], 22 | ] 23 | TREE_NOT_EQL_BY_CONTENT = [ 24 | [ Sycamore::Tree[1] , Sycamore::Tree[2] ], 25 | [ Sycamore::Tree[nil] , Sycamore::Tree[] ], 26 | [ Sycamore::Tree[1] , Sycamore::Tree[1.0] ], 27 | [ Sycamore::Tree[:foo, :bar] , Sycamore::Tree['foo', 'bar'] ], 28 | [ Sycamore::Tree[a: 1] , Sycamore::Tree[:a] ], 29 | [ Sycamore::Tree[a: 1] , Sycamore::Tree[a: 2] ], 30 | [ Sycamore::Tree[a: [nil]] , Sycamore::Tree[:a] ], 31 | [ Sycamore::Tree[a: [nil]] , Sycamore::Tree[a: 2] ], 32 | [ Sycamore::Tree[1=>{2=>{3=>4}}] , Sycamore::Tree[1=>{2=>{3=>1}}] ], 33 | ] 34 | TREE_NOT_EQL_BY_TYPE = [ 35 | [ Sycamore::Tree[a: 1] , Hash[a: 1] ], 36 | [ Sycamore::Tree.new , MyTree.new ], 37 | [ MyTree.new , Sycamore::Tree.new ], 38 | ] 39 | TREE_NOT_EQL = TREE_NOT_EQL_BY_CONTENT + TREE_NOT_EQL_BY_TYPE 40 | 41 | TREE_EQ_BUT_NOT_EQL = [ 42 | [ Sycamore::Tree.new , Sycamore::Nothing ], 43 | [ MyTree.new , Sycamore::Nothing ], 44 | [ Sycamore::Tree[1] , Sycamore::Tree[1 => []] ], 45 | [ Sycamore::Tree[1=>[]] , Sycamore::Tree[1] ], 46 | ] 47 | 48 | ############################################################################ 49 | 50 | describe '#hash' do 51 | it 'does produce equal values, when the tree is eql' do 52 | TREE_EQL.each do |tree, other| 53 | expect( tree.hash ).to be(other.hash), 54 | "expected the hash of #{tree.inspect} to be also the hash of #{other.inspect}" 55 | end 56 | end 57 | 58 | it 'does produce different values, when the tree is not eql' do 59 | TREE_NOT_EQL.each do |tree, other| 60 | expect(tree.hash).not_to eq(other.hash), 61 | "expected the hash of #{tree.inspect} not to equal the hash of #{other.inspect}" 62 | end 63 | end 64 | end 65 | 66 | ############################################################################ 67 | 68 | describe '#eql?' do 69 | it 'does return true, when the given value is of the same type and has eql content' do 70 | TREE_EQL.each do |tree, other| 71 | expect(tree).to eql(other) 72 | end 73 | end 74 | 75 | it 'does return false, when the content of the value tree is not eql' do 76 | TREE_NOT_EQL_BY_CONTENT.each do |tree, other| 77 | expect(tree).not_to eql(other) 78 | end 79 | end 80 | 81 | it 'does return false, when the given value is not an instance of the same class' do 82 | TREE_NOT_EQL_BY_TYPE.each do |tree, other| 83 | expect(tree).not_to eql(other) 84 | end 85 | end 86 | 87 | it 'does consider empty child trees' do 88 | TREE_EQ_BUT_NOT_EQL.each do |tree, other| 89 | expect(tree).not_to eql(other) 90 | end 91 | end 92 | end 93 | 94 | ############################################################################ 95 | 96 | describe '#==' do 97 | it 'does return true, when the given value is of the same type and has eql content' do 98 | TREE_EQL.each do |tree, other| 99 | expect(tree).to eq(other) 100 | end 101 | end 102 | 103 | it 'does return false, when the content of the value tree is not eql' do 104 | TREE_NOT_EQL_BY_CONTENT.each do |tree, other| 105 | expect(tree).not_to eq(other) 106 | end 107 | end 108 | 109 | it 'does return false, when the given value is not an instance of the same class' do 110 | TREE_NOT_EQL_BY_TYPE.each do |tree, other| 111 | expect(tree).not_to eq(other) 112 | end 113 | end 114 | 115 | it 'does ignore empty child trees' do 116 | TREE_EQ_BUT_NOT_EQL.each do |tree, other| 117 | expect(tree).to eq(other) 118 | end 119 | end 120 | end 121 | 122 | ############################################################################ 123 | 124 | TREE_MATCH = TREE_EQL + TREE_EQ_BUT_NOT_EQL + [ 125 | [ Sycamore::Tree.new , Sycamore::Nothing ], 126 | [ Sycamore::Nothing , Sycamore::Tree.new ], 127 | [ Sycamore::Tree.new , MyTree.new ], 128 | [ MyTree.new , Sycamore::Tree.new ], 129 | [ Sycamore::Tree.new , Hash.new ], 130 | [ Sycamore::Tree.new , Array.new ], 131 | [ Sycamore::Tree.new , Set.new ], 132 | [ Sycamore::Tree[:a ] , :a ], 133 | [ Sycamore::Tree['a'] , 'a' ], 134 | [ Sycamore::Tree[ 1 ] , 1 ], 135 | [ Sycamore::Tree[:a] , Hash[a: nil] ], 136 | [ Sycamore::Tree[:a] , Hash[a: []] ], 137 | [ Sycamore::Tree[:a] , Hash[a: {}] ], 138 | [ Sycamore::Tree[1,2,3] , [1,2,3] ], 139 | [ Sycamore::Tree[1,2,3] , Set[1,2,3] ], 140 | [ Sycamore::Tree[:a,:b] , [:b, :a] ], 141 | [ Sycamore::Tree[a: 1] , Hash[a: 1] ], 142 | [ Sycamore::Tree[foo: 'foo', bar: %w[bar baz]], 143 | Hash[foo: 'foo', bar: %w[bar baz]] ], 144 | ] 145 | TREE_MATCH_BY_COERCION = [ 146 | [ Sycamore::Tree[ 1 ] , 1.0 ], 147 | [ Sycamore::Tree[ 1 ] , [1.0] ], 148 | [ Sycamore::Tree[ 1 ] , Sycamore::Tree[1.0] ], 149 | ] 150 | TREE_NO_MATCH = TREE_NOT_EQL_BY_CONTENT + [ 151 | [ Sycamore::Tree.new , nil ], 152 | [ Sycamore::Tree[ 1 ] , 2 ], 153 | [ Sycamore::Tree[ 1 ] , '1' ], 154 | [ Sycamore::Tree['a'] , :a ], 155 | [ Sycamore::Tree[:a] , {a: 1} ], 156 | [ Sycamore::Tree[:foo, :bar] , {foo: 1, bar: 2} ], 157 | [ Sycamore::Tree[:foo, :bar] , ['foo', 'bar'] ], 158 | [ Sycamore::Tree[1,2,3] , [1,2] ], 159 | [ Sycamore::Tree[1,2] , [1,2,3] ], 160 | [ Sycamore::Tree[1,2,3] , [1,2,[3]] ], 161 | [ Sycamore::Tree[a: 1] , :a ], 162 | [ Sycamore::Tree[a: 1, b: 2] , [:a, :b] ], 163 | [ Sycamore::Tree[a: 1] , {a: 2} ], 164 | [ Sycamore::Tree[a: 1] , {a: {1=>2}} ], 165 | [ Sycamore::Tree[1=>{2=>{3=>4}}] , {1=>{2=>{3=>1}}} ], 166 | [ Sycamore::Tree.new , { nil => nil } ], 167 | [ Sycamore::Tree[:foo] , { foo: :bar } ], 168 | [ Sycamore::Tree[foo: :bar] , [:foo] ], 169 | [ Sycamore::Tree[1=>[2,3] ] , {1=>{2=>3}} ], 170 | [ Sycamore::Tree[1=>{2=>3}] , {1=>[2,3]} ], 171 | ] 172 | 173 | ############################################################################ 174 | 175 | describe '#===' do 176 | it 'does return true, when the given value is structurally equivalent and has equal content' do 177 | TREE_MATCH.each do |tree, other| 178 | expect( tree === other ).to be(true), 179 | "expected #{tree.inspect} === #{other.inspect}" 180 | end 181 | end 182 | 183 | # see comment on Tree#matches? 184 | # it 'does return true, when the given value is structurally equivalent and has equal content' do 185 | # TREE_MATCH_BY_COERCION.each do |tree, other| 186 | # pending 'matching by coercion' 187 | # expect( tree === other ).to be(true), 188 | # "expected #{tree.inspect} === #{other.inspect}" 189 | # end 190 | # end 191 | 192 | it 'does return false, when the given value is structurally different and has different content in terms of ==' do 193 | TREE_NO_MATCH.each do |tree, other| 194 | expect( tree === other ).to be(false), 195 | "expected not #{tree.inspect} === #{other.inspect}" 196 | end 197 | end 198 | 199 | it 'does ignore empty child trees' do 200 | tree = Sycamore::Tree[foo: :bar] 201 | tree[:foo].clear 202 | 203 | expect( tree === Sycamore::Tree[:foo] ).to be true 204 | end 205 | end 206 | 207 | ############################################################################ 208 | # comparison 209 | ############################################################################ 210 | 211 | TREE_INCLUDES_NODE = [ 212 | [ [1, 2 ], 1 ], 213 | [ [1, 2 ], 2 ], 214 | [ [42, 'text'], 42 ], 215 | [ [foo: :bar ], :foo ], 216 | ] 217 | TREE_NOT_INCLUDES_NODE =[ 218 | [ [ ], 1 ], 219 | [ [1 ], 2 ], 220 | [ [foo: :bar], :bar ], 221 | ] 222 | TREE_INCLUDES_ENUMERABLE = [ 223 | [ [1, 2 ], [1 ] ], 224 | [ [1, 2, 3 ], [1, 2 ] ], 225 | [ [:a, :b, :c], [:c, :a] ], 226 | ] 227 | TREE_NOT_INCLUDES_ENUMERABLE = [ 228 | [ [ ] , [1 ] ], 229 | [ [1, 2 ] , [3 ] ], 230 | [ [1, 2 ] , [1, 3 ] ], 231 | [ [:a, :b, :c ] , [:a, :b, 1] ], 232 | [ [a: :b, c: :d] , [:a, :d ] ], 233 | ] 234 | TREE_INCLUDES_TREE = [ 235 | [ [1 => 2 ], {1 => nil } ], 236 | [ [1 => [2, 3] ], {1 => 2 } ], 237 | [ [1 => 2, 3 => 1 ], {1 => 2 } ], 238 | [ [1 => [2, 3], 3 => 1], {1 => 2, 3 => 1 } ], 239 | [ [1 => [2, 3], 3 => 1], {1 => 2, 3 => nil} ], 240 | ] 241 | TREE_NOT_INCLUDES_TREE = [ 242 | [ [ ] , {1 => 2 } ], 243 | [ [1 ] , {1 => 2 } ], 244 | [ [42 => 2] , {1 => 2 } ], 245 | [ [1 => 2 ] , {1 => [2, 3] } ], 246 | [ [1 => 2 ] , {1 => 2, 3 => 1 } ], 247 | [ [2 => 1 ] , {1 => 2 } ], 248 | ] 249 | TREE_INCLUDES = TREE_INCLUDES_NODE + TREE_INCLUDES_ENUMERABLE + TREE_INCLUDES_TREE 250 | 251 | def to_tree(tree_or_data) 252 | return tree_or_data if tree_or_data.is_a? Sycamore::Tree 253 | tree_or_data = [tree_or_data] unless tree_or_data.is_a? Array 254 | Sycamore::Tree[*tree_or_data] 255 | end 256 | 257 | ############################################################################ 258 | 259 | describe '#include?' do 260 | it 'does return true, when the given value matches the tree in terms of ===' do 261 | TREE_MATCH.each do |tree, other| 262 | expect( tree.include?(other) ).to be(true), 263 | "expected #{tree.inspect} to include #{other.inspect}" 264 | end 265 | end 266 | 267 | context 'when given a single atomic value' do 268 | it 'does return true, when the value is in the set of nodes' do 269 | TREE_INCLUDES_NODE.each do |data, other| 270 | tree = to_tree(data) 271 | expect( tree.include?(other) ).to be(true), 272 | "expected #{tree.inspect} to include #{other.inspect}" 273 | end 274 | end 275 | 276 | it 'does return false, when the value is not in the set of nodes' do 277 | TREE_NOT_INCLUDES_NODE.each do |data, other| 278 | tree = to_tree(data) 279 | expect( tree.include?(other) ).to be(false), 280 | "expected #{tree.inspect} not to include #{other.inspect}" 281 | end 282 | end 283 | end 284 | 285 | context 'when given a single enumerable' do 286 | it 'does return true, when all elements are in the set of nodes' do 287 | TREE_INCLUDES_ENUMERABLE.each do |data, other| 288 | tree = to_tree(data) 289 | expect( tree.include?(other) ).to be(true), 290 | "expected #{tree.inspect} to include #{other.inspect}" 291 | expect( tree.include?(Set[*other]) ).to be(true), 292 | "expected #{tree.inspect} to include Set[#{other}]" 293 | end 294 | end 295 | 296 | it 'does return false, when some elements are not in the set of nodes' do 297 | TREE_NOT_INCLUDES_ENUMERABLE.each do |data, other| 298 | tree = to_tree(data) 299 | expect( tree.include?(other) ).to be(false), 300 | "expected #{tree.inspect} not to include #{other.inspect}" 301 | expect( tree.include?(Set[*other]) ).to be(false), 302 | "expected #{tree.inspect} not to include Set[#{other.inspect}]" 303 | end 304 | end 305 | end 306 | 307 | context 'when given a single hash' do 308 | it 'does return true, when all of its elements are part of the tree and nested equally' do 309 | TREE_INCLUDES_TREE.each do |data, other| 310 | tree = to_tree(data) 311 | expect( tree.include?(other) ).to be(true), 312 | "expected #{tree.inspect} to include #{other.inspect}" 313 | end 314 | end 315 | 316 | it 'does return false, when some of its elements are not part of the tree' do 317 | TREE_NOT_INCLUDES_TREE.each do |data, other| 318 | tree = to_tree(data) 319 | expect( tree.include?(other) ).to be(false), 320 | "expected #{tree.inspect} not to include #{other.inspect}" 321 | end 322 | end 323 | end 324 | 325 | context 'when given another Tree' do 326 | it 'does return true, when all of its elements are part of the tree and nested equally' do 327 | TREE_INCLUDES_TREE.each do |data, other| 328 | tree, other_tree = to_tree(data), to_tree(other) 329 | expect( tree.include?(other_tree) ).to be(true), 330 | "expected #{tree.inspect} to include #{other_tree.inspect}" 331 | end 332 | end 333 | 334 | it 'does return false, when some of its elements are not part of the tree' do 335 | TREE_NOT_INCLUDES_TREE.each do |data, other| 336 | tree, other_tree = to_tree(data), to_tree(other) 337 | expect( tree.include?(other_tree) ).to be(false), 338 | "expected #{tree.inspect} not to include #{other_tree.inspect}" 339 | end 340 | end 341 | end 342 | 343 | context 'when given a Path' do 344 | it 'does delegate to include_path?' do 345 | tree = Sycamore::Tree[foo: :bar] 346 | path = Sycamore::Path[:foo, :bar] 347 | expect( tree ).to receive(:include_path?).with(path) 348 | tree.include?(path) 349 | end 350 | end 351 | 352 | context 'when given multiple arguments' do 353 | context 'when all arguments are nodes' do 354 | it 'does return true, when each arguments is included' do 355 | TREE_INCLUDES_ENUMERABLE.each do |data, other| 356 | tree = to_tree(data) 357 | expect( tree.include?(*other) ).to be(true), 358 | "expected #{tree.inspect} to include #{other.inspect} as an arguments splash" 359 | end 360 | end 361 | 362 | it 'does return false, when some arguments are not included' do 363 | TREE_NOT_INCLUDES_ENUMERABLE.each do |data, other| 364 | tree = to_tree(data) 365 | expect( tree.include?(*other) ).to be(false), 366 | "expected #{tree.inspect} not to include #{other.inspect} as an arguments splash" 367 | end 368 | end 369 | end 370 | 371 | context 'when nodes and tree-like structures mixed' do 372 | it 'does return true, when each arguments is included' do 373 | TREE_INCLUDES_TREE.each do |data, other| 374 | tree = to_tree(data) << :foo 375 | expect( tree.include?(:foo, other) ).to be(true), 376 | "expected #{tree.inspect} to include :foo and #{other.inspect} as an arguments splash" 377 | end 378 | end 379 | 380 | it 'does return false, when some arguments are not included' do 381 | TREE_NOT_INCLUDES_TREE.each do |data, other| 382 | tree = to_tree(data) << :foo 383 | expect( tree.include?(:foo, other) ).to be(false), 384 | "expected #{tree.inspect} not to include #{other.inspect} as an arguments splash" 385 | end 386 | end 387 | end 388 | end 389 | 390 | 391 | context 'edge cases' do 392 | it 'raise an error, when no arguments given' do 393 | expect { Sycamore::Tree.new.include? }.to raise_error ArgumentError 394 | end 395 | 396 | context 'when given a single value' do 397 | specify { expect( Sycamore::Tree[false].include? false).to be true } 398 | specify { expect( Sycamore::Tree[nil ].include? nil ).to be true } 399 | specify { expect( Sycamore::Tree[0 ].include? 0 ).to be true } 400 | end 401 | end 402 | end 403 | 404 | ############################################################################ 405 | 406 | describe '#>=' do 407 | it 'does return true, when the given value is a tree and this tree includes it' do 408 | TREE_INCLUDES.each do |data, other| 409 | tree, other_tree = to_tree(data), to_tree(other) 410 | expect( tree >= other_tree ).to be(true), 411 | "expected #{tree.inspect} >= #{other_tree.inspect}" 412 | end 413 | end 414 | 415 | it 'considers Absence to be a tree' do 416 | absence = Sycamore::Absence.at(Sycamore::Tree.new, :missing) 417 | absence << :a 418 | expect( Sycamore::Tree[a: :b] >= absence ).to be true 419 | end 420 | 421 | it 'does return true, when the given tree is equal' do 422 | TREE_EQL.each do |tree, other_tree| 423 | expect( tree >= other_tree ).to be(true), 424 | "expected #{tree.inspect} >= #{other_tree.inspect}" 425 | end 426 | end 427 | 428 | it 'does return false, when the given value is not a tree' do 429 | expect( Sycamore::Tree[a: :b] >= [:a] ).to be false 430 | end 431 | end 432 | 433 | ############################################################################ 434 | 435 | describe '#>' do 436 | it 'does return true, when the given value is a tree and this tree includes it' do 437 | TREE_INCLUDES.each do |data, other| 438 | tree, other_tree = to_tree(data), to_tree(other) 439 | expect( tree > other_tree ).to be(true), 440 | "expected #{tree.inspect} > #{other_tree.inspect}" 441 | end 442 | end 443 | 444 | it 'considers Absence to be a tree' do 445 | absence = Sycamore::Absence.at(Sycamore::Tree.new, :missing) 446 | absence << :a 447 | expect( Sycamore::Tree[a: :b] > absence ).to be true 448 | end 449 | 450 | it 'does return false, when the given tree is equal' do 451 | TREE_EQL.each do |tree, other_tree| 452 | expect( tree > other_tree ).to be(false), 453 | "expected #{tree.inspect} > #{other_tree.inspect} to be false" 454 | end 455 | end 456 | 457 | it 'does return false, when the given value is not a tree' do 458 | expect( Sycamore::Tree[1, 2] > [1] ).to be false 459 | end 460 | end 461 | 462 | ############################################################################ 463 | 464 | describe '#<' do 465 | it 'does return true, when the given value is a tree and includes this tree' do 466 | TREE_INCLUDES.each do |data, other| 467 | tree, other_tree = to_tree(data), to_tree(other) 468 | expect( other_tree < tree).to be(true), 469 | "expected #{other_tree.inspect} < #{tree.inspect}" 470 | end 471 | end 472 | 473 | it 'considers Absence to be a tree' do 474 | absence = Sycamore::Absence.at(Sycamore::Tree.new, :missing) 475 | absence << {a: :b} 476 | expect( Sycamore::Tree[:a] < absence ).to be true 477 | end 478 | 479 | it 'does return false, when the given tree is equal' do 480 | TREE_EQL.each do |tree, other_tree| 481 | expect( other_tree < tree ).to be(false), 482 | "expected #{other_tree.inspect} < #{tree.inspect} to be false" 483 | end 484 | end 485 | 486 | it 'does return false, when the given value is not a tree' do 487 | expect( Sycamore::Tree[1] < {1 => 2} ).to be false 488 | end 489 | end 490 | 491 | ############################################################################ 492 | 493 | describe '#<=' do 494 | it 'does return true, when the given value is a tree and includes this tree' do 495 | TREE_INCLUDES.each do |data, other| 496 | tree, other_tree = to_tree(data), to_tree(other) 497 | expect( other_tree <= tree).to be(true), 498 | "expected #{other_tree.inspect} <= #{tree.inspect}" 499 | end 500 | end 501 | 502 | it 'considers Absence to be a tree' do 503 | absence = Sycamore::Absence.at(Sycamore::Tree.new, :missing) 504 | absence << {a: :b} 505 | expect( Sycamore::Tree[:a] <= absence ).to be true 506 | end 507 | 508 | it 'does return true, when the given tree is equal' do 509 | TREE_EQL.each do |tree, other_tree| 510 | expect( tree <= other_tree ).to be(true), 511 | "expected #{tree.inspect} <= #{other_tree.inspect}" 512 | end 513 | end 514 | 515 | it 'does return false, when the given value is not a tree' do 516 | expect( Sycamore::Tree[1] <= {1 => 2} ).to be false 517 | end 518 | end 519 | 520 | ############################################################################ 521 | 522 | describe '#include_node?' do 523 | context 'when given a node' do 524 | it 'does return true when the given node is a node of this tree' do 525 | expect( Sycamore::Tree[1,2,3].include_node?(1) ).to be true 526 | end 527 | 528 | it 'does return false when the given node is not a node of this tree' do 529 | expect( Sycamore::Tree[].include_node?(1) ).to be false 530 | expect( Sycamore::Tree[foo: :bar].include_node?(:bar) ).to be false 531 | end 532 | end 533 | 534 | context 'when given a Path object' do 535 | it 'does return true when the node at the given path exists in this tree' do 536 | expect( Sycamore::Tree[1,2,3].include_node?(Sycamore::Path[1]) ).to be true 537 | expect( Sycamore::Tree[foo: {bar: :baz}] 538 | .include_node?(Sycamore::Path[:foo, :bar, :baz]) ).to be true 539 | end 540 | 541 | it 'does return false when the node at the given path not exists in this tree' do 542 | expect( Sycamore::Tree[] 543 | .include_node?(Sycamore::Path[:foo, :bar, :baz]) ).to be false 544 | expect( Sycamore::Tree[foo: {bar: :baz}] 545 | .include_node?(Sycamore::Path[:foo, :bar, :qux]) ).to be false 546 | end 547 | end 548 | end 549 | 550 | ############################################################################ 551 | 552 | describe '#include_path?' do 553 | HAS_PATH_EXAMPLES = [ 554 | # Path , Tree 555 | [ [1] , [1] ], 556 | [ [nil] , [nil] ], 557 | [ [1, 2] , {1 => 2} ], 558 | [ [1] , {1 => {2 => 3, 4 => 5}} ], 559 | [ [1,2] , {1 => {2 => 3, 4 => 5}} ], 560 | [ [1,2,3] , {1 => {2 => 3, 4 => 5}} ], 561 | [ [1,2] , {1 => [2, 3]} ], 562 | [ [nil,2,3] , {nil => {2 => 3, 4 => 5}} ], 563 | [ [1,2,nil] , {1 => {2 => [nil], 4 => 5}} ], 564 | [ [nil,nil,nil] , {nil => {nil => [nil]}} ], 565 | ] 566 | 567 | NOT_HAS_PATH_EXAMPLES = [ 568 | # Path , Tree 569 | [ [1] , [] ], 570 | [ [nil] , [] ], 571 | [ [2] , {1 => 2} ], 572 | [ [1,2,3] , {1 => 2} ], 573 | ] 574 | 575 | context 'when given a nodes path as one or more node arguments' do 576 | it 'does return true, if the given nodes path is present' do 577 | HAS_PATH_EXAMPLES.each do |path_nodes, struct| 578 | tree = Sycamore::Tree[struct] 579 | expect( tree.include_path?(*path_nodes) ).to be(true), 580 | "expected #{tree.inspect} to include path #{path_nodes.inspect}" 581 | end 582 | end 583 | 584 | it 'does return false, if the given nodes path is not present' do 585 | NOT_HAS_PATH_EXAMPLES.each do |path_nodes, struct| 586 | tree = Sycamore::Tree[struct] 587 | expect( tree.include_path?(*path_nodes) ).to be(false), 588 | "expected #{tree.inspect} to not include path #{path_nodes.inspect}" 589 | end 590 | end 591 | end 592 | 593 | context 'when given a nodes path as an enumerable of nodes' do 594 | it 'does return true, if the given nodes path is present' do 595 | HAS_PATH_EXAMPLES.each do |path_nodes, struct| 596 | tree = Sycamore::Tree[struct] 597 | expect( tree.include_path?(path_nodes) ).to be(true), 598 | "expected #{tree.inspect} to include path #{path_nodes.inspect}" 599 | end 600 | end 601 | 602 | it 'does return false, if the given nodes path is not present' do 603 | NOT_HAS_PATH_EXAMPLES.each do |path_nodes, struct| 604 | tree = Sycamore::Tree[struct] 605 | expect( tree.include_path?(path_nodes) ).to be(false), 606 | "expected #{tree.inspect} to not include path #{path_nodes.inspect}" 607 | end 608 | end 609 | end 610 | 611 | context 'when given a nodes path as a Sycamore::Path' do 612 | it 'does return true, if the given nodes path is present' do 613 | HAS_PATH_EXAMPLES.each do |path_nodes, struct| 614 | tree = Sycamore::Tree[struct] 615 | expect( tree.include_path?(Sycamore::Path[path_nodes]) ).to be(true), 616 | "expected #{tree.inspect} to include path #{path_nodes.inspect}" 617 | end 618 | end 619 | 620 | it 'does return false, if the given nodes path is not present' do 621 | NOT_HAS_PATH_EXAMPLES.each do |path_nodes, struct| 622 | tree = Sycamore::Tree[struct] 623 | expect( tree.include_path?(Sycamore::Path[path_nodes]) ).to be(false), 624 | "expected #{tree.inspect} to not include path #{path_nodes.inspect}" 625 | end 626 | end 627 | end 628 | 629 | context 'edge cases' do 630 | it 'raises an error, when given no arguments' do 631 | expect { Sycamore::Tree.new.path? }.to raise_error ArgumentError 632 | end 633 | 634 | it 'raises an error, when given multiple collections' do 635 | expect { Sycamore::Tree.new.path?([1,2], [3,4]) }.to raise_error Sycamore::InvalidNode 636 | end 637 | end 638 | end 639 | 640 | end 641 | -------------------------------------------------------------------------------- /spec/unit/sycamore/tree/conversion_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Tree do 2 | 3 | describe '#to_native_object' do 4 | it 'does return an empty array, when empty' do 5 | expect( Sycamore::Tree.new.to_native_object ).to eql [] 6 | end 7 | 8 | it 'does return a hash, with leaves having an empty array as a child' do 9 | expect( Sycamore::Tree[1=>[], 2=>{}].to_native_object ).to eql( {1=>[], 2=>[]} ) 10 | end 11 | 12 | it 'does return a hash, with strict leaves having no child' do 13 | expect( Sycamore::Tree[1 ].to_native_object ).to eql 1 14 | expect( Sycamore::Tree[1, 2].to_native_object ).to eql [1, 2] 15 | end 16 | 17 | context 'edge cases' do 18 | specify { expect( Sycamore::Tree[nil ].to_native_object ).to eql nil } 19 | specify { expect( Sycamore::Tree[false].to_native_object ).to eql false } 20 | specify { expect( Sycamore::Tree[true ].to_native_object ).to eql true } 21 | end 22 | end 23 | 24 | ############################################################################ 25 | 26 | describe '#to_h' do 27 | 28 | it 'does return a hash, where the first level is unflattened and the rest flattened' do 29 | expect( Sycamore::Tree[a: 1 ].to_h ).to eql( {a: 1} ) 30 | expect( Sycamore::Tree[:a, b: 1 ].to_h ).to eql( {a: nil, b: 1} ) 31 | expect( Sycamore::Tree[a: 1, b: [2, 3] ].to_h ).to eql( {a: 1, b: [2, 3]} ) 32 | expect( Sycamore::Tree[a: {b: nil, c: { }}].to_h ).to eql( {a: {b: nil, c: []}} ) 33 | expect( Sycamore::Tree[a: {b: nil, c: [1]}].to_h ).to eql( {a: {b: nil, c: 1}} ) 34 | end 35 | 36 | it 'does return an empty hash, when empty' do 37 | expect( Sycamore::Tree.new.to_h ).to eql Hash.new 38 | end 39 | 40 | it 'does return a hash, with leaves having an empty array as a child' do 41 | expect( Sycamore::Tree[1=>[], 2=>[]].to_h ).to eql( {1=>[], 2=>[]} ) 42 | end 43 | 44 | context 'when no sleaf_child_as defined' do 45 | it 'does return a hash, with strict leaves having nil as a child' do 46 | expect( Sycamore::Tree[1 ].to_h ).to eql( {1 => nil} ) 47 | expect( Sycamore::Tree[1, 2].to_h ).to eql( {1 => nil, 2 => nil} ) 48 | end 49 | end 50 | 51 | context 'when sleaf_child_as defined' do 52 | let(:sleaf_child_as) { Sycamore::Nothing } 53 | it 'does return a hash, with strict leaves having nil as a child' do 54 | expect( Sycamore::Tree[1].to_h(sleaf_child_as: sleaf_child_as) ) 55 | .to eql( {1 => sleaf_child_as} ) 56 | expect( Sycamore::Tree[1, 2].to_h(sleaf_child_as: sleaf_child_as) ) 57 | .to eql( {1 => sleaf_child_as, 2 => sleaf_child_as} ) 58 | end 59 | end 60 | 61 | context 'edge cases' do 62 | it 'does treat nil like any other value' do 63 | expect( Sycamore::Tree[nil => 1].to_h ).to eql( {nil => 1} ) 64 | expect( Sycamore::Tree[nil => [], 2 => []].to_h ).to eql( {nil => [], 2 => []} ) 65 | expect( Sycamore::Tree[nil].to_h ).to eql( {nil => nil} ) 66 | expect( Sycamore::Tree[nil => [nil]].to_h ).to eql( {nil => nil} ) 67 | expect( Sycamore::Tree[nil => {nil => nil}].to_h ).to eql( {nil => nil} ) 68 | end 69 | end 70 | end 71 | 72 | ############################################################################ 73 | 74 | describe '#to_s' do 75 | module ContextWithTreeConstant 76 | Tree = Sycamore::Tree 77 | end 78 | 79 | shared_examples_for 'every to_s string' do |tree| 80 | it 'starts with a Tree-specific prefix' do 81 | expect( tree.to_s ).to match /^Tree\[/ 82 | end 83 | 84 | it 'evaluates to the original Tree, when all nodes contained have this property' do 85 | expect( ContextWithTreeConstant.module_eval(tree.to_s) ).to eql tree 86 | end 87 | end 88 | 89 | shared_examples_for 'every to_s string (single leaf)' do |tree| 90 | include_examples 'every to_s string', tree 91 | it 'contains the nodes inspect representation' do 92 | expect( tree.to_s ).to include tree.node.inspect 93 | end 94 | end 95 | 96 | shared_examples_for 'every to_s string (non-single leaf)' do |tree| 97 | include_examples 'every to_s string', tree 98 | it 'contains the to_s representation of the hash representation without brackets' do 99 | expect( tree.to_s ).to include tree.to_native_object(special_nil: true).to_s[1..-2] 100 | end 101 | end 102 | 103 | include_examples 'every to_s string (single leaf)', Sycamore::Tree['foo'] 104 | include_examples 'every to_s string (single leaf)', Sycamore::Tree[nil] 105 | include_examples 'every to_s string (non-single leaf)', Sycamore::Tree.new 106 | include_examples 'every to_s string (non-single leaf)', Sycamore::Tree[1.0, 2, 3] 107 | include_examples 'every to_s string (non-single leaf)', Sycamore::Tree[foo: [1,2]] 108 | include_examples 'every to_s string (non-single leaf)', Sycamore::Tree[:foo, bar: [2,3]] 109 | include_examples 'every to_s string (non-single leaf)', Sycamore::Tree[foo: 1, bar: [], baz: nil, qux: {}] 110 | include_examples 'every to_s string (non-single leaf)', Sycamore::Tree[nil, foo: {nil => [nil]}] 111 | end 112 | 113 | ############################################################################ 114 | 115 | describe '#inspect' do 116 | shared_examples_for 'every inspect string' do |tree| 117 | it 'is in the usual Ruby inspect style' do 118 | expect( tree.inspect ).to match /^# [nil]}] 137 | end 138 | 139 | end 140 | -------------------------------------------------------------------------------- /spec/unit/sycamore/tree/deletion_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Tree do 2 | 3 | describe '#delete' do 4 | 5 | context 'when given a single node' do 6 | it 'does delete the value from the set of nodes' do 7 | expect( Sycamore::Tree[1] >> 1 ).to be_empty 8 | expect( Sycamore::Tree[1,2,3].delete(2).nodes.to_set ).to eql Set[1,3] 9 | expect( Sycamore::Tree[:foo, :bar].delete(:foo).size ).to be 1 10 | end 11 | 12 | it 'does nothing, when the given value is not present' do 13 | expect( Sycamore::Tree[1 ].delete(2 ) ).to include_node 1 14 | expect( Sycamore::Tree[:foo].delete('foo') ).to include_node :foo 15 | end 16 | 17 | context 'edge cases' do 18 | it 'does nothing, when given the Nothing tree' do 19 | expect( Sycamore::Tree[1].delete(Sycamore::Nothing) ).to include_node 1 20 | end 21 | 22 | it 'does treat nil like any other value' do 23 | expect( Sycamore::Tree[nil].delete(nil)).to be_empty 24 | end 25 | 26 | it 'does treat false like any other value' do 27 | expect( Sycamore::Tree[false].delete(false)).to be_empty 28 | end 29 | end 30 | end 31 | 32 | context 'when given an array' do 33 | it 'does delete the values from the set of nodes that are present' do 34 | expect( Sycamore::Tree[1,2,3] >> [1,2,3] ).to be_empty 35 | expect( Sycamore::Tree[1,2,3] >> [2,3 ] ).to include 1 36 | expect( Sycamore::Tree[1,2,3].delete([2,3]).size ).to be 1 37 | end 38 | 39 | it 'does ignore the values that are not present' do 40 | expect( Sycamore::Tree.new >> [1,2] ).to be_empty 41 | expect( Sycamore::Tree[1,2] >> [2,3] ).to include 1 42 | expect( Sycamore::Tree[1,2].delete([2,3]).size ).to be 1 43 | end 44 | 45 | context 'when the array is nested' do 46 | it 'does treat hashes as nodes with children' do 47 | expect( Sycamore::Tree[a: 1, b: 2 ].delete([:a, b: 2]) ).to be_empty 48 | expect( Sycamore::Tree[a: 1, b: [2, 3]].delete([:a, b: 2]) === {b: 3} ).to be true 49 | end 50 | end 51 | 52 | context 'when the array contains a nested enumerable that is not Tree-like' do 53 | it 'raises an error' do 54 | expect { Sycamore::Tree.new.delete([1, [2, 3]]) }.to raise_error Sycamore::InvalidNode 55 | end 56 | 57 | it 'does not change the tree' do 58 | tree = Sycamore::Tree[1,2] 59 | expect { tree.delete([1, [2, 3]]) }.to raise_error Sycamore::InvalidNode 60 | expect( tree.nodes ).to contain_exactly 1, 2 61 | end 62 | end 63 | 64 | context 'edge cases' do 65 | it 'does nothing, when given an empty array' do 66 | expect( Sycamore::Tree[1,2,3].delete([]).nodes.to_set ).to eql Set[1,2,3] 67 | end 68 | 69 | it 'does treat nil like any other value' do 70 | expect( Sycamore::Tree[1, 2, nil].delete([nil, 1]).nodes ).to eql [2] 71 | end 72 | end 73 | end 74 | 75 | DELETE_TREE_EXAMPLES = [ 76 | { before: {a: 1} , delete: {a: 1} , after: {} }, 77 | { before: {a: [1, 2]} , delete: {a: 2} , after: {a: 1} }, 78 | { before: {a: [1, 2]} , delete: {a: [2]} , after: {a: 1} }, 79 | { before: {a: 1, b: [2,3]} , delete: {a:1, b:2} , after: {b: 3} }, 80 | { before: {a: 1} , delete: {a: Sycamore::Tree[1]} , after: {} }, 81 | { before: {a: [1, 2]} , delete: {a: Sycamore::Tree[2]} , after: {a: 1} }, 82 | ] 83 | 84 | NOT_DELETE_TREE_EXAMPLES = [ 85 | { before: {a: 1} , delete: {a: 2} }, 86 | { before: {a: [1, 2]} , delete: {a: 3} }, 87 | { before: {a: [1, 2]} , delete: {a: [3]} }, 88 | { before: {a: 1, b: [2,3]} , delete: {a:2, b:4} }, 89 | { before: {a: [1, 2]} , delete: {a: Sycamore::Tree[3]} }, 90 | { before: {a: 1} , delete: {a: {1 => 2}} }, 91 | ] 92 | 93 | PARTIAL_DELETE_TREE_EXAMPLES = [ 94 | { before: {a: [1, 2]} , delete: {a: [2, 3]} , after: {a: 1} }, 95 | { before: {a: 1, b: [2,3]} , delete: {c:1, b:2} , after: {a:1, b:3} }, 96 | ] 97 | 98 | context 'when given a hash' do 99 | it 'does delete the given tree structure' do 100 | DELETE_TREE_EXAMPLES.each do |example| 101 | expect( Sycamore::Tree[example[:before]].delete(example[:delete]) ) 102 | .to eql Sycamore::Tree[example[:after]] 103 | end 104 | end 105 | 106 | it 'does nothing, when given something not part of the tree' do 107 | NOT_DELETE_TREE_EXAMPLES.each do |example| 108 | expect( Sycamore::Tree[example[:before]].delete(example[:delete]) ) 109 | .to eql Sycamore::Tree[example[:before]] 110 | end 111 | end 112 | 113 | it 'does delete the existing paths and ignore the not existing paths of given input data' do 114 | PARTIAL_DELETE_TREE_EXAMPLES.each do |example| 115 | expect( Sycamore::Tree[example[:before]].delete(example[:delete]) ) 116 | .to eql Sycamore::Tree[example[:after]] 117 | end 118 | end 119 | 120 | context 'when given a tree with an enumerable key' do 121 | it 'raises an error' do 122 | expect { Sycamore::Tree.new.delete([1,2] => 3) }.to raise_error Sycamore::InvalidNode 123 | expect { Sycamore::Tree.new.delete({1 => 2} => 3) }.to raise_error Sycamore::InvalidNode 124 | expect { Sycamore::Tree.new.delete(Sycamore::Tree[1] => 42) }.to raise_error Sycamore::InvalidNode 125 | expect { Sycamore::Tree.new.delete(Sycamore::Nothing => 42) }.to raise_error Sycamore::InvalidNode 126 | end 127 | 128 | it 'does not change the tree' do 129 | tree = Sycamore::Tree[:foo, 1] 130 | expect { tree.delete([foo: :bar, [1,2] => 3]) }.to raise_error Sycamore::InvalidNode 131 | expect( tree.nodes ).to contain_exactly :foo, 1 132 | end 133 | end 134 | 135 | context 'edge cases' do 136 | it 'does nothing, when given an empty hash' do 137 | expect( Sycamore::Tree.new >> {} ).to be_empty 138 | end 139 | 140 | it 'does treat false as a key like any other value' do 141 | expect( Sycamore::Tree[false => :foo].delete(false => :foo) ).to be_empty 142 | end 143 | 144 | it 'does treat nil as a key like any other value' do 145 | expect( Sycamore::Tree[nil => :foo].delete(nil => :foo) ).to be_empty 146 | end 147 | 148 | it 'does treat nil as an element of the child tree like any other value' do 149 | expect( Sycamore::Tree[1 => [2, nil]].delete(1 => [nil]) ).to eql Sycamore::Tree[1=>2] 150 | expect( Sycamore::Tree[1 => [2, nil]].delete(1 => [nil, 2]) ).to be_empty 151 | expect( Sycamore::Tree[1 => {nil => 2}].delete(1 => {nil => 2}) ).to be_empty 152 | end 153 | 154 | it 'does ignore null values as children' do 155 | expect(Sycamore::Tree[1 => 2].delete({1 => {}})).to be_empty 156 | expect(Sycamore::Tree[1 ].delete({1 => []})).to be_empty 157 | expect(Sycamore::Tree[1 => 2].delete({1 => Sycamore::Nothing})).to be_empty 158 | expect(Sycamore::Tree[1 => 2].delete({1 => nil})).to be_empty 159 | expect(Sycamore::Tree[1 => nil].delete({1 => nil})).to be_empty 160 | end 161 | end 162 | end 163 | 164 | context 'when given a tree' do 165 | it 'does delete the given tree structure' do 166 | DELETE_TREE_EXAMPLES.each do |example| 167 | expect( Sycamore::Tree[example[:before]] 168 | .delete(Sycamore::Tree[example[:delete]]) ) 169 | .to eql Sycamore::Tree[example[:after]] 170 | end 171 | end 172 | 173 | context 'edge cases' do 174 | it 'does nothing, when given an empty tree' do 175 | expect( Sycamore::Tree[42] >> Sycamore::Tree.new ).to eql Sycamore::Tree[42] 176 | end 177 | 178 | context 'when given an Absence' do 179 | let(:absent_tree) { Sycamore::Tree.new.child_of(:something) } 180 | 181 | it 'does ignore it, when it is absent' do 182 | expect( Sycamore::Tree[:something].delete absent_tree ).to include :something 183 | expect( Sycamore::Tree[foo: :something].delete(foo: absent_tree)).to be_empty 184 | end 185 | 186 | it 'does treat it like a normal tree, when it was created' do 187 | absent_tree << 42 188 | 189 | expect( Sycamore::Tree[42].delete absent_tree ).to be_empty 190 | expect( Sycamore::Tree[foo: 42].delete(foo: absent_tree)).to be_empty 191 | expect( Sycamore::Tree[foo: [42, 3.14]].delete(foo: absent_tree)).to eql Sycamore::Tree[foo: 3.14] 192 | end 193 | end 194 | 195 | it 'does ignore null values as children' do 196 | expect(Sycamore::Tree[1 => 2].delete(Sycamore::Tree[1 => Sycamore::Nothing])).to be_empty 197 | expect(Sycamore::Tree[1 => 2].delete(Sycamore::Tree[1 => nil])).to be_empty 198 | expect(Sycamore::Tree[1 => 2].delete(Sycamore::Tree[1 => {}])).to be_empty 199 | expect(Sycamore::Tree[1 ].delete(Sycamore::Tree[1 => []])).to be_empty 200 | end 201 | end 202 | end 203 | 204 | context 'when given a single Path object' do 205 | let(:path) { Sycamore::Path[:foo, :bar, :baz] } 206 | 207 | it 'does delete the node at the given path and all other nodes on the path unless there are remaining children' do 208 | expect( Sycamore::Tree[foo: { bar: [:baz, 42], qux: nil}].delete(path) ) 209 | .to eql Sycamore::Tree[foo: { bar: 42, qux: nil}] 210 | expect( Sycamore::Tree[foo: { bar: :baz, qux: nil}].delete(path) ) 211 | .to eql Sycamore::Tree[foo: { qux: nil }] 212 | expect( Sycamore::Tree[foo: { bar: :baz}].delete(path) ).to be_empty 213 | end 214 | 215 | it 'does nothing when the path does not exist on the tree' do 216 | tree = Sycamore::Tree[1 => {2 => 3}] 217 | expect( tree.delete(path) ).to eql tree 218 | end 219 | 220 | it 'does nothing, when given an empty path' do 221 | tree = Sycamore::Tree[1 => {2 => 3}] 222 | expect( tree.delete(Sycamore::Path[]) ).to eql tree 223 | end 224 | end 225 | 226 | context 'when given multiple path objects' do 227 | it 'does delete the nodes at all given paths and all other nodes on the paths unless there are remaining children' do 228 | expect( Sycamore::Tree[foo: { bar: [:baz, 42], qux: nil}] 229 | .delete([Sycamore::Path[:foo, :bar, :baz], 230 | Sycamore::Path[:foo, :qux], 231 | Sycamore::Path[:missing]]) ) 232 | .to eql Sycamore::Tree[foo: { bar: 42}] 233 | end 234 | end 235 | 236 | context 'when given an Enumerable of mixed objects' do 237 | it 'does delete the elements appropriately' do 238 | expect( Sycamore::Tree[foo: { bar: [:baz, 42], qux: [1,2]}, more: nil] 239 | .delete([:more, Sycamore::Path[:foo, :bar, 42], 240 | {foo: {qux: 1}}, Sycamore::Tree[foo: {qux: 2}] ]) ) 241 | .to eql Sycamore::Tree[foo: { bar: :baz}] 242 | end 243 | end 244 | end 245 | 246 | ############################################################################ 247 | 248 | describe '#clear' do 249 | it 'does nothing when empty' do 250 | expect( Sycamore::Tree.new.clear.size ).to be 0 251 | expect( Sycamore::Tree.new.clear.nodes ).to eql [] 252 | end 253 | 254 | it 'does delete all nodes and their children' do 255 | expect( Sycamore::Tree[1, 2 ].clear.size ).to be 0 256 | expect( Sycamore::Tree[:foo, :bar].clear.nodes ).to eql [] 257 | end 258 | end 259 | 260 | ############################################################################ 261 | 262 | describe '#compact' do 263 | it 'does not change a tree without empty child trees' do 264 | tree = Sycamore::Tree[1, foo: :bar] 265 | org_tree = tree.dup 266 | expect(tree.compact).to eql org_tree 267 | end 268 | 269 | it 'does delete all empty child trees' do 270 | expect( Sycamore::Tree[1=>[]].compact.child_of(1)).to be_absent 271 | expect( Sycamore::Tree[{1=>{},2=>{3=>{}}}].compact[1]).to be_absent 272 | expect( Sycamore::Tree[{1=>{},2=>{3=>{}}}].compact[2,3]).to be_absent 273 | end 274 | end 275 | end 276 | -------------------------------------------------------------------------------- /spec/unit/sycamore/tree/enumeration_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Tree do 2 | 3 | let(:tree) { Sycamore::Tree.new } 4 | 5 | ############################################################################ 6 | 7 | describe '#each_node' do 8 | context 'when a block given' do 9 | it 'does return the tree' do 10 | expect( tree.each_node { 'foo' } ).to be tree 11 | end 12 | 13 | it 'does yield the block with each node as an argument' do 14 | expect { |b| Sycamore::Tree[1, 2, 3 ].each_node(&b) }.to yield_successive_args(1, 2, 3) 15 | expect { |b| Sycamore::Tree[1, nil, 3].each_node(&b) }.to yield_successive_args(1, nil, 3) 16 | expect { |b| Sycamore::Tree[foo: :bar].each_node(&b) }.to yield_successive_args(:foo) 17 | end 18 | end 19 | 20 | context 'when no block given' do 21 | it 'does return an enumerator' do 22 | expect( Sycamore::Tree[1].each_node ).to be_a Enumerator 23 | end 24 | 25 | it 'does return an enumerator over the nodes' do 26 | expect( Sycamore::Tree[1, 2=>3].each_node.to_a ).to eql [1, 2] 27 | end 28 | end 29 | end 30 | 31 | ############################################################################ 32 | 33 | describe '#each_pair' do 34 | context 'when a block given' do 35 | it 'does return the tree' do 36 | expect( tree.each_pair { 'foo' } ).to be tree 37 | end 38 | 39 | it 'does yield the block with each node-child-pairs as an argument' do 40 | expect { |b| Sycamore::Tree[foo: :bar].each(&b) } 41 | .to yield_successive_args([:foo, Sycamore::Tree[:bar]]) 42 | expect { |b| Sycamore::Tree[1=>2, 3=>[4, 5]].each(&b) } 43 | .to yield_successive_args([1, Sycamore::Tree[2]], [3, Sycamore::Tree[4, 5]]) 44 | expect { |b| Sycamore::Tree[nil => {nil => 2}].each(&b) } 45 | .to yield_successive_args([nil, Sycamore::Tree[nil => 2]]) 46 | end 47 | 48 | it 'does yield the Nothing tree as the child of leaves' do 49 | expect { |b| Sycamore::Tree[1, 2, 3].each(&b) } 50 | .to yield_successive_args([1, Sycamore::Nothing], [2, Sycamore::Nothing], [3, Sycamore::Nothing]) 51 | expect { |b| Sycamore::Tree[1, 4 => 5].each(&b) } 52 | .to yield_successive_args([1, Sycamore::Nothing], [4, Sycamore::Tree[5]]) 53 | expect { |b| Sycamore::Tree[nil, 4 => 5].each(&b) } 54 | .to yield_successive_args([nil, Sycamore::Nothing], [4, Sycamore::Tree[5]]) 55 | expect { |b| Sycamore::Tree[1, nil => 5].each(&b) } 56 | .to yield_successive_args([1, Sycamore::Nothing], [nil, Sycamore::Tree[5]]) 57 | end 58 | end 59 | 60 | context 'when no block given' do 61 | it 'does return an enumerator' do 62 | expect( Sycamore::Tree[1].each ).to be_a Enumerator 63 | end 64 | 65 | it 'does return an enumerator over the node-child-pairs' do 66 | expect( Sycamore::Tree[1, 2=>3].each.to_a ).to eql [[1, Sycamore::Nothing], [2, Sycamore::Tree[3]]] 67 | end 68 | end 69 | end 70 | 71 | ############################################################################ 72 | 73 | describe '#each_path' do 74 | context 'when a block given' do 75 | it 'does return the tree' do 76 | expect( tree.each_path { 'foo' } ).to be tree 77 | end 78 | 79 | it 'does yield the block with the paths to each leaf of the complete tree' do 80 | expect{ |b| Sycamore::Tree[42 ].each_path(&b) }.to yield_successive_args Sycamore::Path[42] 81 | expect{ |b| Sycamore::Tree[1, 2 ].each_path(&b) }.to yield_successive_args Sycamore::Path[1], Sycamore::Path[2] 82 | expect{ |b| Sycamore::Tree[1,nil,3].each_path(&b) }.to yield_successive_args Sycamore::Path[1], Sycamore::Path[nil], Sycamore::Path[3] 83 | expect{ |b| Sycamore::Tree[1 => 2 ].each_path(&b) }.to yield_successive_args Sycamore::Path[1, 2] 84 | expect{ |b| Sycamore::Tree[1 => { 2 => [3, 4] }].each_path(&b) } 85 | .to yield_successive_args Sycamore::Path[1, 2, 3], Sycamore::Path[1, 2, 4] 86 | expect{ |b| Sycamore::Tree[1 => { nil => [nil, 3] }, nil => 4].each_path(&b) } 87 | .to yield_successive_args Sycamore::Path[1, nil, nil], 88 | Sycamore::Path[1, nil, 3], 89 | Sycamore::Path[nil, 4] 90 | end 91 | end 92 | 93 | context 'when no block given' do 94 | it 'does return an enumerator' do 95 | expect( Sycamore::Tree[1].each_path ).to be_a Enumerator 96 | end 97 | 98 | it 'does return an enumerator with the node-child-pairs' do 99 | expect(Sycamore::Tree[1 ].paths.to_a ).to eql [Sycamore::Path[1]] 100 | expect(Sycamore::Tree[1, 2 ].paths.to_a ).to eql [Sycamore::Path[1], Sycamore::Path[2]] 101 | expect(Sycamore::Tree[1 => 2].paths.to_a ).to eql [Sycamore::Path[1, 2]] 102 | expect(Sycamore::Tree[1 => { 2 => [3, 4] }].paths.to_a ) 103 | .to eql [Sycamore::Path[1, 2, 3], Sycamore::Path[1, 2, 4]] 104 | end 105 | end 106 | 107 | context 'edge cases' do 108 | it 'does ignore empty child trees' do 109 | expect(Sycamore::Tree[1 => []].paths.to_a ).to eql [Sycamore::Path[1]] 110 | end 111 | end 112 | end 113 | 114 | end 115 | -------------------------------------------------------------------------------- /spec/unit/sycamore/tree_spec.rb: -------------------------------------------------------------------------------- 1 | describe Sycamore::Tree do 2 | 3 | it { is_expected.to be_a Enumerable } 4 | 5 | let(:subclass) { Class.new(Sycamore::Tree) } 6 | 7 | 8 | specify { expect { Sycamore::Tree[].data }.to raise_error NoMethodError } 9 | 10 | describe 'CQS reflection class methods' do 11 | specify 'all Tree methods are separated into command and query methods' do 12 | # TODO: Should we also separate the inherited methods into commands and queries? At least the command methods are required for proper Absence and Nothing behaviour. 13 | tree_methods = 14 | Sycamore::Tree.public_instance_methods(false).to_set.to_a.sort 15 | command_query_methods = 16 | (Sycamore::Tree.command_methods + Sycamore::Tree.query_methods).to_set.to_a.sort 17 | expect( tree_methods ).to eq command_query_methods 18 | end 19 | end 20 | 21 | 22 | ############################################################################ 23 | # construction 24 | ############################################################################ 25 | 26 | describe '.new' do 27 | context 'when given no arguments and no block' do 28 | specify { expect( Sycamore::Tree.new ).to be_a Sycamore::Tree } 29 | specify { expect( Sycamore::Tree.new ).to be_empty } 30 | end 31 | end 32 | 33 | describe '.with' do 34 | context 'when given no arguments and no block' do 35 | subject { Sycamore::Tree[] } 36 | it { is_expected.to be_a Sycamore::Tree } 37 | it { is_expected.to be_empty } 38 | end 39 | 40 | context 'when given a single atomic value' do 41 | it 'does return a new tree' do 42 | expect( Sycamore::Tree[1] ).to be_a Sycamore::Tree 43 | end 44 | 45 | it 'does initialize the new tree with the given value' do 46 | expect( Sycamore::Tree[1] ).to include_node 1 47 | end 48 | end 49 | 50 | context 'when given a single array' do 51 | it 'does initialize the new tree with the elements of the array' do 52 | expect( Sycamore::Tree[[1, 2]] ).to include_nodes 1, 2 53 | expect( Sycamore::Tree[Set[1, 2, 2]] ).to include_nodes 1, 2 54 | expect( Sycamore::Tree[[1, 2, 2]] ).to include_nodes 1, 2 55 | expect( Sycamore::Tree[[1, 2]].size ).to be 2 56 | expect( Sycamore::Tree[[1, 2, :foo]] ).to include_nodes 1, 2, :foo 57 | end 58 | end 59 | 60 | context 'when given a single hash' do 61 | it 'does initialize the new tree with the elements of the hash' do 62 | tree = Sycamore::Tree[a: 1, b: 2] 63 | expect(tree).to include :a 64 | expect(tree).to include :b 65 | expect(tree[:a]).to include 1 66 | expect(tree[:b]).to include 2 67 | end 68 | end 69 | 70 | context 'when given multiple arguments' do 71 | context 'when all arguments are atomic' do 72 | it 'does initialize the new tree with the given values' do 73 | expect( Sycamore::Tree[1, 2] ).to include_nodes 1, 2 74 | expect( Sycamore::Tree[1, 2, 2] ).to include_nodes 1, 2 75 | expect( Sycamore::Tree[1, 2].size ).to be 2 76 | expect( Sycamore::Tree[1, 2, :foo] ).to include_nodes 1, 2, :foo 77 | end 78 | end 79 | 80 | context 'when all arguments are atomic or tree-like' do 81 | it 'does initialize the new tree with the given values' do 82 | expect( Sycamore::Tree[1, {2 => 3}] ).to include_node 1 83 | expect( Sycamore::Tree[1, {2 => 3}] ).to include_tree({2 => 3}) 84 | end 85 | end 86 | 87 | context 'when some arguments are non-tree-like enumerables' do 88 | it 'does raise an error' do 89 | expect { Sycamore::Tree[1, [2]] }.to raise_error Sycamore::InvalidNode 90 | expect { Sycamore::Tree[[1, 2], [3, 4]] }.to raise_error Sycamore::InvalidNode 91 | end 92 | end 93 | end 94 | 95 | context 'when given a single Path object' do 96 | it 'does initialize the new tree with the given path' do 97 | tree = Sycamore::Tree[Sycamore::Path[:foo, :bar]] 98 | expect(tree).to eql Sycamore::Tree[foo: :bar] 99 | end 100 | end 101 | 102 | context 'when given a multiple Path objects' do 103 | it 'does initialize the new tree with the given paths' do 104 | tree = Sycamore::Tree[Sycamore::Path[:foo, :bar], Sycamore::Path[1,2,3]] 105 | expect(tree).to eql Sycamore::Tree[foo: :bar, 1=>{2=>3}] 106 | end 107 | end 108 | 109 | context 'when given a mix of objects' do 110 | it 'does add the elements appropriately' do 111 | expect( Sycamore::Tree[:foo, :bar, 112 | Sycamore::Path[:foo, :bar, :baz], {1=>2}, Sycamore::Tree[1=>{2=>3}]] ) 113 | .to eql Sycamore::Tree[foo: {bar: :baz}, bar: nil, 1 => {2 => 3}] 114 | end 115 | end 116 | 117 | end 118 | 119 | describe '#new_child' do 120 | it 'does create a tree of the same type as the parent' do 121 | expect( Sycamore::Tree.new.new_child(1) ).to be_instance_of Sycamore::Tree 122 | expect( subclass.new.new_child(1) ).to be_instance_of subclass 123 | end 124 | end 125 | 126 | 127 | ############################################################################ 128 | # Absence and Nothing predicates 129 | ############################################################################ 130 | 131 | describe '#nothing?' do 132 | specify { expect( Sycamore::Tree.new.nothing? ).to be false } 133 | end 134 | 135 | describe '#absent?' do 136 | specify { expect( Sycamore::Tree.new.absent? ).to be false } 137 | end 138 | 139 | describe '#existent?' do 140 | specify { expect( Sycamore::Tree.new.existent? ).to be true } 141 | end 142 | 143 | describe '#present?' do 144 | specify { expect( Sycamore::Tree.new.present? ).to be false } 145 | specify { expect( Sycamore::Tree[0 ].present? ).to be true } 146 | specify { expect( Sycamore::Tree[''].present? ).to be true } 147 | specify { expect( Sycamore::Tree[false].present? ).to be true } 148 | specify { expect( Sycamore::Tree[nil].present? ).to be true } 149 | end 150 | 151 | 152 | ############################################################################ 153 | # Various 154 | ############################################################################ 155 | 156 | describe '#empty?' do 157 | it 'does return true, when the Tree has no nodes' do 158 | expect( Sycamore::Tree.new.empty? ).to be true 159 | expect( Sycamore::Tree[Sycamore::Nothing].empty?).to be true 160 | end 161 | 162 | it 'does return false, when the Tree has nodes' do 163 | expect( Sycamore::Tree[42 ].empty? ).to be false 164 | expect( Sycamore::Tree[[42] ].empty? ).to be false 165 | expect( Sycamore::Tree[property: :value].empty? ).to be false 166 | end 167 | 168 | it 'does treat nil like any other value' do 169 | expect( Sycamore::Tree[nil].empty?).to be false 170 | end 171 | end 172 | 173 | ############################################################################ 174 | 175 | describe '#size' do 176 | it 'does return 0, when empty' do 177 | expect( Sycamore::Tree.new.size ).to be 0 178 | expect( Sycamore::Tree.new.add(:foo).delete(:foo).size ).to be 0 179 | end 180 | 181 | it 'does return the number of nodes' do 182 | expect( Sycamore::Tree[1 ].size ).to be 1 183 | expect( Sycamore::Tree[nil ].size ).to be 1 184 | expect( Sycamore::Tree[:foo, 2, 'bar'].size ).to be 3 185 | expect( Sycamore::Tree[1,2,2,3,3,3 ].size ).to be 3 186 | end 187 | 188 | it 'does return the number of nodes, not counting the nodes of the children' do 189 | expect( Sycamore::Tree[a: [1,2,3] ].size ).to be 1 190 | expect( Sycamore::Tree[a: 1, b: nil].size ).to be 2 191 | end 192 | end 193 | 194 | ############################################################################ 195 | 196 | describe '#total_size' do 197 | it 'does return 0, when empty' do 198 | expect( Sycamore::Tree.new.total_size ).to be 0 199 | expect( Sycamore::Tree.new.add(:foo).delete(:foo).total_size ).to be 0 200 | end 201 | 202 | it 'does return the number of nodes' do 203 | expect( Sycamore::Tree[1 ].total_size ).to be 1 204 | expect( Sycamore::Tree[nil ].total_size ).to be 1 205 | expect( Sycamore::Tree[:foo, 2, 'bar'].total_size ).to be 3 206 | expect( Sycamore::Tree[1,2,2,3,3,3 ].total_size ).to be 3 207 | end 208 | 209 | it 'does return the number of nodes including the nodes of children' do 210 | expect( Sycamore::Tree[a: [1,2,3] ].total_size ).to be 4 211 | expect( Sycamore::Tree[a: 1, b: nil].total_size ).to be 3 212 | expect( Sycamore::Tree[a: 1, b: [nil]].total_size ).to be 4 213 | end 214 | 215 | it 'does return the number of nodes including the nodes of children recursively' do 216 | expect( Sycamore::Tree[x: 1, y: {2 => "a"}].total_size ).to be 5 217 | expect( Sycamore::Tree[a: {b: {c: :d}}, e: []].total_size ).to be 5 218 | end 219 | end 220 | 221 | ############################################################################ 222 | 223 | describe '#height' do 224 | it 'does return 0, when empty' do 225 | expect( Sycamore::Tree.new.height ).to be 0 226 | expect( Sycamore::Tree.new.add(:foo).delete(:foo).height ).to be 0 227 | end 228 | 229 | it 'does return the length of the longest path' do 230 | expect( Sycamore::Tree[42 ].height ).to be 1 231 | expect( Sycamore::Tree[nil ].height ).to be 1 232 | expect( Sycamore::Tree[1,2,3 ].height ).to be 1 233 | expect( Sycamore::Tree[a: [1,2,3]].height ).to be 2 234 | expect( Sycamore::Tree[:a, b: 1 ].height ).to be 2 235 | end 236 | 237 | it 'does ignore empty child trees' do 238 | expect( Sycamore::Tree[:a, b: {1=>[]}].height ).to be 2 239 | end 240 | end 241 | 242 | ############################################################################ 243 | 244 | describe '#leaf?' do 245 | context 'when given a node' do 246 | it 'does return true, when the given node is present and has no child tree' do 247 | expect( Sycamore::Tree[1 ].leaf?(1) ).to be true 248 | expect( Sycamore::Tree[1 => nil ].leaf?(1) ).to be true 249 | expect( Sycamore::Tree[1 => Sycamore::Nothing].leaf?(1) ).to be true 250 | expect( Sycamore::Tree[1 => :foo, 2 => nil ].leaf?(2) ).to be true 251 | end 252 | 253 | it 'does return true, when the given node is present and has an empty child tree' do 254 | expect(Sycamore::Tree.new.add_node_with_empty_child(1).leaf?(1)).to be true 255 | end 256 | 257 | it 'does return false, when the given node is not present' do 258 | expect( Sycamore::Tree.new.leaf?(42) ).to be false 259 | expect( Sycamore::Tree[43].leaf?(42) ).to be false 260 | end 261 | 262 | it 'does return false, when the given node has a child' do 263 | expect( Sycamore::Tree[1 => :foo ].leaf?(1) ).to be false 264 | expect( Sycamore::Tree[1 => :foo, 2 => nil].leaf?(1) ).to be false 265 | end 266 | 267 | context 'edge cases' do 268 | it 'does treat nil like any other value' do 269 | expect( Sycamore::Tree.new.leaf?(nil) ).to be false 270 | expect( Sycamore::Tree[nil].leaf?(nil) ).to be true 271 | expect( Sycamore::Tree[nil => 1].leaf?(nil) ).to be false 272 | end 273 | end 274 | end 275 | 276 | context 'when given a Path object' do 277 | it 'does return true, when the node at the given path is present and has no child tree' do 278 | expect( Sycamore::Tree[foo: 1 ].leaf?(Sycamore::Path[:foo, 1]) ).to be true 279 | expect( Sycamore::Tree[foo: {1 => nil} ].leaf?(Sycamore::Path[:foo, 1]) ).to be true 280 | expect( Sycamore::Tree[foo: {1 => Sycamore::Nothing}].leaf?(Sycamore::Path[:foo, 1]) ).to be true 281 | expect( Sycamore::Tree[foo: {1 => :foo, 2 => nil} ].leaf?(Sycamore::Path[:foo, 2]) ).to be true 282 | end 283 | 284 | it 'does return true, when the node at the given path is present and has an empty child tree' do 285 | expect( Sycamore::Tree[foo: {bar: []}].leaf?(Sycamore::Path[:foo, :bar]) ).to be true 286 | end 287 | 288 | it 'does return false, when the node at the given path is not present' do 289 | expect( Sycamore::Tree.new.leaf?(Sycamore::Path[:foo, :bar]) ).to be false 290 | expect( Sycamore::Tree[foo: :bar].leaf?(Sycamore::Path[:foo, :baz]) ).to be false 291 | end 292 | 293 | it 'does return false, when the node at the given path has a child' do 294 | expect( Sycamore::Tree[foo: {bar: :baz}].leaf?(Sycamore::Path[:foo, :bar]) ).to be false 295 | end 296 | end 297 | end 298 | 299 | ############################################################################ 300 | 301 | describe '#strict_leaf?' do 302 | context 'when given a node' do 303 | it 'does return true, when the given node is present and has no child tree' do 304 | expect( Sycamore::Tree[1 ].strict_leaf?(1) ).to be true 305 | expect( Sycamore::Tree[1 => nil ].strict_leaf?(1) ).to be true 306 | expect( Sycamore::Tree[1 => Sycamore::Nothing].strict_leaf?(1) ).to be true 307 | expect( Sycamore::Tree[1 => :foo, 2 => nil ].strict_leaf?(2) ).to be true 308 | end 309 | 310 | it 'does return false, when the given node is present and has an empty child tree' do 311 | expect(Sycamore::Tree.new.add_node_with_empty_child(1).strict_leaf?(1)).to be false 312 | end 313 | 314 | it 'does return false, when the given node is not present' do 315 | expect( Sycamore::Tree.new.strict_leaf?(42) ).to be false 316 | expect( Sycamore::Tree[43].strict_leaf?(42) ).to be false 317 | end 318 | 319 | it 'does return false, when the given node has a child' do 320 | expect( Sycamore::Tree[1 => :foo ].strict_leaf?(1) ).to be false 321 | expect( Sycamore::Tree[1 => :foo, 2 => nil].strict_leaf?(1) ).to be false 322 | end 323 | 324 | context 'edge cases' do 325 | it 'does treat nil like any other value' do 326 | expect( Sycamore::Tree.new.strict_leaf?(nil) ).to be false 327 | expect( Sycamore::Tree[nil].strict_leaf?(nil) ).to be true 328 | expect( Sycamore::Tree[nil => 1].strict_leaf?(nil) ).to be false 329 | end 330 | end 331 | end 332 | 333 | context 'when given a Path object' do 334 | it 'does return true, when the node at the given path is present and has no child tree' do 335 | expect( Sycamore::Tree[foo: 1 ].strict_leaf?(Sycamore::Path[:foo, 1]) ).to be true 336 | expect( Sycamore::Tree[foo: {1 => nil} ].strict_leaf?(Sycamore::Path[:foo, 1]) ).to be true 337 | expect( Sycamore::Tree[foo: {1 => Sycamore::Nothing}].strict_leaf?(Sycamore::Path[:foo, 1]) ).to be true 338 | expect( Sycamore::Tree[foo: {1 => :foo, 2 => nil} ].strict_leaf?(Sycamore::Path[:foo, 2]) ).to be true 339 | end 340 | 341 | it 'does return false, when the node at the given path is present and has an empty child tree' do 342 | expect( Sycamore::Tree[foo: {bar: []}].strict_leaf?(Sycamore::Path[:foo, :bar]) ).to be false 343 | end 344 | 345 | it 'does return false, when the node at the given path is not present' do 346 | expect( Sycamore::Tree.new.strict_leaf?(Sycamore::Path[:foo, :bar]) ).to be false 347 | expect( Sycamore::Tree[foo: :bar].strict_leaf?(Sycamore::Path[:foo, :baz]) ).to be false 348 | end 349 | 350 | it 'does return false, when the node at the given path has a child' do 351 | expect( Sycamore::Tree[foo: {bar: :baz}].strict_leaf?(Sycamore::Path[:foo, :bar]) ).to be false 352 | end 353 | end 354 | 355 | end 356 | 357 | ############################################################################ 358 | 359 | describe '#strict_leaves?' do 360 | context 'when given a single atomic value' do 361 | # see #strict_leaf? 362 | end 363 | 364 | context 'without arguments' do 365 | it 'does return true, when none of the nodes has children' do 366 | expect( Sycamore::Tree[].strict_leaves? ).to be true 367 | expect( Sycamore::Tree[1].strict_leaves? ).to be true 368 | expect( Sycamore::Tree[1 => nil].strict_leaves? ).to be true 369 | expect( Sycamore::Tree[1 => Sycamore::Nothing].strict_leaves? ).to be true 370 | expect( Sycamore::Tree[1, 2, 3].strict_leaves? ).to be true 371 | end 372 | 373 | it 'does return false, when some of the nodes have children' do 374 | expect( Sycamore::Tree[1 => 2 ].strict_leaves? ).to be false 375 | expect( Sycamore::Tree[1 => :a, 2 => nil, 3 => nil ].strict_leaves? ).to be false 376 | expect( Sycamore::Tree[1 => :a, 2 => Sycamore::Nothing].strict_leaves? ).to be false 377 | end 378 | 379 | it 'does return false, when some of the nodes have an empty child tree' do 380 | expect( Sycamore::Tree[1 => [] ].strict_leaves? ).to be false 381 | expect( Sycamore::Tree[1 => [], 2 => nil, 3 => nil ].strict_leaves? ).to be false 382 | expect( Sycamore::Tree[1 => [], 2 => Sycamore::Nothing].strict_leaves? ).to be false 383 | end 384 | end 385 | 386 | context 'when given arguments' do 387 | it 'does return true, if all given nodes are present and have no child tree' do 388 | expect( Sycamore::Tree[1, 2, 3 ].strict_leaves?(1,2,3) ).to be true 389 | expect( Sycamore::Tree[1 => nil, 2 => nil].strict_leaves?(1,2) ).to be true 390 | end 391 | 392 | it 'does return false, if some of the given nodes are not present' do 393 | expect( Sycamore::Tree[1,2].strict_leaves?(1,2,3) ).to be false 394 | expect( Sycamore::Tree[].strict_leaves?(1,2,3) ).to be false 395 | end 396 | 397 | it 'does return false, if some of the given nodes have a child' do 398 | expect( Sycamore::Tree[1 => :a, 2 => nil].strict_leaves?(1,2) ).to be false 399 | end 400 | 401 | it 'does return false, if some of the given nodes have an empty child tree' do 402 | expect( Sycamore::Tree[1 => [], 2 => nil].strict_leaves?(1,2) ).to be false 403 | end 404 | end 405 | 406 | context 'edge cases' do 407 | it 'does treat nil like any other value' do 408 | expect( Sycamore::Tree[1 ].strict_leaves?(nil, 1) ).to be false 409 | expect( Sycamore::Tree[1, nil].strict_leaves?(nil, 1) ).to be true 410 | end 411 | end 412 | end 413 | 414 | ############################################################################ 415 | 416 | describe '#external?' do 417 | context 'when given a single atomic value' do 418 | # see #leaf? 419 | end 420 | 421 | context 'without arguments' do 422 | it 'does return true, when none of the nodes has children' do 423 | expect( Sycamore::Tree[].external? ).to be true 424 | expect( Sycamore::Tree[1].external? ).to be true 425 | expect( Sycamore::Tree[1 => nil].external? ).to be true 426 | expect( Sycamore::Tree[1 => Sycamore::Nothing].external? ).to be true 427 | expect( Sycamore::Tree[1, 2, 3].external? ).to be true 428 | end 429 | 430 | it 'does return false, when some of the nodes have children' do 431 | expect( Sycamore::Tree[1 => 2 ].external? ).to be false 432 | expect( Sycamore::Tree[1 => :a, 2 => nil, 3 => nil ].external? ).to be false 433 | expect( Sycamore::Tree[1 => :a, 2 => Sycamore::Nothing].external? ).to be false 434 | end 435 | 436 | it 'does return false, when some of the nodes have an empty child tree' do 437 | expect( Sycamore::Tree[1 => [] ].external? ).to be true 438 | expect( Sycamore::Tree[1 => [], 2 => nil, 3 => nil ].external? ).to be true 439 | expect( Sycamore::Tree[1 => [], 2 => Sycamore::Nothing].external? ).to be true 440 | end 441 | end 442 | 443 | context 'when given arguments' do 444 | it 'does return true, if all given nodes are present and have no children' do 445 | expect( Sycamore::Tree[1, 2, 3 ].external?(1,2,3) ).to be true 446 | expect( Sycamore::Tree[1 => nil, 2 => nil].external?(1,2) ).to be true 447 | end 448 | 449 | it 'does return true, if some of the given nodes have an empty child tree' do 450 | expect( Sycamore::Tree[1 => [], 2 => nil].external?(1,2) ).to be true 451 | end 452 | 453 | it 'does return false, if some of the given nodes are not present' do 454 | expect( Sycamore::Tree[1,2].external?(1,2,3) ).to be false 455 | expect( Sycamore::Tree[].external?(1,2,3) ).to be false 456 | end 457 | 458 | it 'does return false, if some of the given nodes have a child' do 459 | expect( Sycamore::Tree[1 => :a, 2 => nil].external?(1,2) ).to be false 460 | end 461 | end 462 | 463 | context 'edge cases' do 464 | it 'does treat nil like any other value' do 465 | expect( Sycamore::Tree[1, ].external?(1, nil) ).to be false 466 | expect( Sycamore::Tree[1, nil].external?(1, nil) ).to be true 467 | end 468 | end 469 | end 470 | 471 | ############################################################################ 472 | 473 | describe '#internal?' do 474 | context 'when given no arguments' do 475 | it 'does return true, when all nodes have children' do 476 | expect( Sycamore::Tree[1 => 2].internal? ).to be true 477 | end 478 | 479 | it 'does return false, when some of the nodes are leaves' do 480 | expect( Sycamore::Tree[].internal? ).to be false 481 | expect( Sycamore::Tree[1].internal? ).to be false 482 | expect( Sycamore::Tree[1 => nil].internal? ).to be false 483 | expect( Sycamore::Tree[1 => Sycamore::Nothing].internal? ).to be false 484 | expect( Sycamore::Tree[1, 2, 3].internal? ).to be false 485 | expect( Sycamore::Tree[1 => :a, 2 => nil].internal? ).to be false 486 | end 487 | end 488 | 489 | context 'when given arguments' do 490 | it 'does return true, when all of the given nodes are present and have children' do 491 | expect( Sycamore::Tree[1 => 2 ].internal?(1) ).to be true 492 | expect( Sycamore::Tree[1 => :a, 2 => nil].internal?(1) ).to be true 493 | end 494 | 495 | it 'does return false, if some of the given nodes are present, but have no children' do 496 | expect( Sycamore::Tree[1, 2, 3 ].internal?(1,2,3) ).to be false 497 | expect( Sycamore::Tree[1 => nil, 2 => nil].internal?(1,2) ).to be false 498 | expect( Sycamore::Tree[1 => :a, 2 => nil ].internal?(1,2) ).to be false 499 | end 500 | 501 | it 'does return false, if some of the given nodes are not present' do 502 | expect( Sycamore::Tree.new.internal?(42) ).to be false 503 | expect( Sycamore::Tree[1, 2 ].internal?(1,2,3) ).to be false 504 | expect( Sycamore::Tree[1 => 2].internal?(2) ).to be false 505 | end 506 | end 507 | 508 | context 'edge cases' do 509 | it 'does treat nil like any other value' do 510 | expect( Sycamore::Tree[nil => 1].internal?(nil) ).to be true 511 | end 512 | end 513 | end 514 | 515 | ############################################################################ 516 | 517 | describe '#dup' do 518 | it 'does returns a different but equal Tree' do 519 | tree = Sycamore::Tree[foo: :bar] 520 | duplicate = tree.dup 521 | 522 | expect( duplicate ).not_to be tree 523 | expect( duplicate ).to eql tree 524 | expect( tree[:foo] ).not_to be duplicate[:foo] 525 | end 526 | 527 | it 'does return an independent Tree' do 528 | tree = Sycamore::Tree[foo: {bar: :baz}] 529 | duplicate = tree.dup 530 | tree.add :more 531 | 532 | expect( duplicate ).not_to eql tree 533 | 534 | duplicate = tree.dup 535 | tree[:foo] << :more 536 | 537 | expect( duplicate ).not_to eql tree 538 | end 539 | 540 | it 'returns an unfrozen tree, even if the original was frozen' do 541 | tree = Sycamore::Tree.new 542 | tree.freeze 543 | duplicate = tree.dup 544 | 545 | expect( duplicate ).not_to be_frozen 546 | end 547 | 548 | it 'returns a tainted tree, if the original was tainted' do 549 | tree = Sycamore::Tree.new 550 | tree.taint 551 | duplicate = tree.dup 552 | 553 | expect( duplicate ).to be_tainted 554 | end 555 | end 556 | 557 | ############################################################################ 558 | 559 | describe '#clone' do 560 | it 'does returns a different but equal Tree' do 561 | tree = Sycamore::Tree[foo: :bar] 562 | klone = tree.clone 563 | 564 | expect( klone ).not_to be tree 565 | expect( klone ).to eql tree 566 | expect( klone[:foo] ).not_to be tree[:foo] 567 | end 568 | 569 | it 'does return an independent Tree' do 570 | tree = Sycamore::Tree[foo: {bar: :baz}] 571 | klone = tree.clone 572 | tree.add :more 573 | 574 | expect( klone ).not_to eql tree 575 | 576 | klone = tree.clone 577 | tree[:foo] << :more 578 | 579 | expect( klone ).not_to eql tree 580 | end 581 | 582 | it 'returns a frozen tree, if the original was frozen' do 583 | tree = Sycamore::Tree.new 584 | tree.freeze 585 | klone = tree.clone 586 | 587 | expect( klone ).to be_frozen 588 | end 589 | 590 | it 'returns a tainted tree, if the original was tainted' do 591 | tree = Sycamore::Tree.new 592 | tree.taint 593 | klone = tree.clone 594 | 595 | expect( klone ).to be_tainted 596 | end 597 | 598 | it 'does copy singleton methods' do 599 | tree = Sycamore::Tree.new 600 | def tree.some_method ; end 601 | 602 | klone = tree.clone 603 | expect(klone).to respond_to :some_method 604 | end 605 | 606 | end 607 | 608 | ############################################################################ 609 | 610 | describe '#freeze' do 611 | it 'behaves Object#freeze conform' do 612 | # stolen from Ruby's tests of set.rb (test_freeze) adapted to RSpec and with Trees 613 | # see https://www.omniref.com/ruby/2.2.0/files/test/test_set.rb 614 | orig = tree = Sycamore::Tree[1, 2, 3] 615 | expect(tree).not_to be_frozen 616 | tree << 4 617 | expect(tree.freeze).to be orig 618 | expect(tree).to be_frozen 619 | expect { tree << 5 }.to raise_error RuntimeError 620 | expect(tree.size).to be 4 621 | end 622 | 623 | it 'does freeze all children' do 624 | frozen_tree = Sycamore::Tree[foo: :bar].freeze 625 | expect( frozen_tree[:foo] ).to be_frozen 626 | end 627 | 628 | it 'does freeze all children recursively' do 629 | frozen_tree = Sycamore::Tree[foo: {bar: :baz}].freeze 630 | expect( frozen_tree[:foo, :bar] ).to be_frozen 631 | end 632 | end 633 | 634 | end 635 | -------------------------------------------------------------------------------- /support/doctest_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH << 'lib' 2 | require 'sycamore/extension' 3 | -------------------------------------------------------------------------------- /support/travis.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | set -e 3 | set -x 4 | 5 | mkdir ~/.yard 6 | bundle exec yard config -a autoload_plugins yard-doctest 7 | -------------------------------------------------------------------------------- /sycamore.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'sycamore/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'sycamore' 8 | spec.version = Sycamore::VERSION 9 | spec.authors = ['Marcel Otto'] 10 | spec.email = ['marcelotto@gmx.de'] 11 | 12 | spec.summary = %q{An unordered tree data structure for Ruby.} 13 | spec.description = %q{Sycamore is an unordered tree data structure.} 14 | spec.homepage = 'https://github.com/marcelotto/sycamore' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = 'exe' 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.required_ruby_version = '>= 2.1' 23 | 24 | spec.add_development_dependency 'bundler', '~> 1.11' 25 | spec.add_development_dependency 'rake', '~> 11.0' 26 | spec.add_development_dependency 'rspec', '~> 3.4' 27 | spec.add_development_dependency 'yard', '~> 0.9' 28 | spec.add_development_dependency 'yard-doctest', '0.1.7' 29 | end 30 | --------------------------------------------------------------------------------