sanction 0.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +9 -17
- data/.travis.yml +6 -0
- data/LICENSE.txt +1 -1
- data/README.md +105 -5
- data/Rakefile +5 -0
- data/lib/sanction/attached_list.rb +57 -0
- data/lib/sanction/blacklist/list.rb +34 -0
- data/lib/sanction/blacklist/node.rb +42 -0
- data/lib/sanction/blacklist/null_list.rb +21 -0
- data/lib/sanction/blacklist/null_node.rb +37 -0
- data/lib/sanction/node.rb +135 -0
- data/lib/sanction/permission.rb +38 -0
- data/lib/sanction/tree.rb +124 -0
- data/lib/sanction/version.rb +1 -1
- data/lib/sanction/whitelist/list.rb +34 -0
- data/lib/sanction/whitelist/node.rb +43 -0
- data/lib/sanction/whitelist/null_list.rb +21 -0
- data/lib/sanction/whitelist/null_node.rb +37 -0
- data/lib/sanction.rb +32 -1
- data/sanction.gemspec +11 -6
- data/spec/application_spec.rb +91 -0
- data/spec/node_spec.rb +49 -0
- data/spec/permission_spec.rb +218 -0
- data/spec/resources_spec.rb +158 -0
- data/spec/spec_helper.rb +91 -0
- data/spec/wildcard_spec.rb +95 -0
- metadata +91 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0d9b7c911e6b858db458af6c2a1a1a2576831c6c
|
4
|
+
data.tar.gz: dfc5c0aa9ea471972fb4897649e195195b8d4002
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 627ca53b8f5c5d2d162b4035e792f94af8560e6f66cb6ab4030db8a646ccb909a9078a28c3e7d16c6f59254a3ef03cffd61c2fd9d38f48db93ea4b2ee550e7ed
|
7
|
+
data.tar.gz: 31e827b77feb6d04d471d54e08f260b833bf845d930fbab5cf401ecd4f2ea18d0936455d78f7c94c17fda1e043c673a30af6c6c094ac9653bf904165627b32b5
|
data/.gitignore
CHANGED
@@ -1,20 +1,12 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
.
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
doc/
|
11
|
-
lib/bundler/man
|
12
|
-
pkg
|
13
|
-
rdoc
|
14
|
-
spec/reports
|
15
|
-
test/tmp
|
16
|
-
test/version_tmp
|
17
|
-
tmp
|
1
|
+
/.bundle/
|
2
|
+
/.yardoc
|
3
|
+
/Gemfile.lock
|
4
|
+
/_yardoc/
|
5
|
+
/coverage/
|
6
|
+
/doc/
|
7
|
+
/pkg/
|
8
|
+
/spec/reports/
|
9
|
+
/tmp/
|
18
10
|
*.bundle
|
19
11
|
*.so
|
20
12
|
*.o
|
data/.travis.yml
ADDED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
# Sanction
|
2
2
|
|
3
|
-
|
3
|
+
Sanction is a permissions manager specifically for managing nested permission sets, with varying scopes or roles. Having found nothing that fit our specific problem domain. The idea is that object relationships are stored as a Hash, and persisted as JSON, and Sanction can then read that permission graph, and return you a true or false for your resource, or scope for that resource.
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
7
7
|
Add this line to your application's Gemfile:
|
8
8
|
|
9
|
-
|
9
|
+
```ruby
|
10
|
+
gem 'sanction'
|
11
|
+
```
|
10
12
|
|
11
13
|
And then execute:
|
12
14
|
|
@@ -16,13 +18,111 @@ Or install it yourself as:
|
|
16
18
|
|
17
19
|
$ gem install sanction
|
18
20
|
|
19
|
-
##
|
21
|
+
## What can it give me?
|
20
22
|
|
21
|
-
|
23
|
+
Sanction is designed to be as flexible as possible, allowing various scopes to be applied to specific points in the resource graph, along with specific grant or deny to global resource types at specific levels. With either whitelisting or blacklisting, plus wildcarding for whitelists.
|
24
|
+
|
25
|
+
### Object Structure
|
26
|
+
```ruby
|
27
|
+
{
|
28
|
+
id: 1
|
29
|
+
type: 'bookcase'
|
30
|
+
mode: 'whitelist',
|
31
|
+
scope: ['manage', 'read'],
|
32
|
+
resources: ['shelf'],
|
33
|
+
subjects: [ ... ]
|
34
|
+
}
|
35
|
+
```
|
36
|
+
* __ID__: The ID of the object that this permission applies to, is optional, root nodes will have a nil ID, in a whitelist node, this can be a wildcard("*") character to allow access to all
|
37
|
+
* __Type__: The Type of object in this graph, again it is optional, root nodes will have a nil type.
|
38
|
+
* __Mode__: The mode of operation for the subjects and resources arrays:
|
39
|
+
* Whitelist:
|
40
|
+
* Objects that exist in the subjects array are allowed, others are denied
|
41
|
+
* A blank array is an implicit *DENY ALL* subjects
|
42
|
+
* Blacklist:
|
43
|
+
* Objects that exist in the subjects array are denied, others are allowed
|
44
|
+
* A blank array is an implicit *ALLOW ALL* subjects
|
45
|
+
* __Scope__: An array of 'actions' that can be performed on the object, this can be anything, 'read', 'write', 'testing' etc.
|
46
|
+
* __Resources__: Resources are subject to the mode, blacklist is to exclude these resources from being accessed, whitelist being the opposite.
|
47
|
+
* Resources allows/denies will always take precedence over objects in the subjects array
|
48
|
+
* __Subjects__: An array that contains more of these objects, subject to the whitelist/blacklist mode of operation
|
49
|
+
|
50
|
+
### Interface
|
51
|
+
|
52
|
+
To generate an interactive object graph from storage:
|
53
|
+
```ruby
|
54
|
+
perms = Sanction.build(hash)
|
55
|
+
```
|
56
|
+
|
57
|
+
This will return you the root node of the graph, and you can navigate it by using hash accessors, and then call interrogation methods on the returned objects
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
perms[:bookcase][1] # => Node in the graph
|
61
|
+
perms[:bookcase][1].permitted? # => true/false
|
62
|
+
perms[:bookcase][1].has_scope?(:read) # => true/false
|
63
|
+
perms[:bookcase][1].whitelist? # => true/false
|
64
|
+
perms[:bookcase][1].blacklist? # => true/false
|
65
|
+
```
|
66
|
+
|
67
|
+
You can also get the state of the collection of objects too.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
perms[:bookcase].allowed_ids # => Array of allowed ids
|
71
|
+
perms[:bookcase].denied_ids # => Array of allowed ids
|
72
|
+
perms[:bookcase].whitelist? # => true/false
|
73
|
+
perms[:bookcase].blacklist? # => true/false
|
74
|
+
perms[:bookcase].permitted? # => true/false
|
75
|
+
perms[:bookcase].has_scope?(:read) # => true/false
|
76
|
+
```
|
77
|
+
|
78
|
+
And mutate the state of the graph
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
perms.whitelist? # => true
|
82
|
+
perms[:bookcase][1].allow!
|
83
|
+
perms[:bookcase][1].permitted? # => true
|
84
|
+
|
85
|
+
perms[:bookcase][1].deny!
|
86
|
+
perms[:bookcase][1].permitted? # => false
|
87
|
+
|
88
|
+
perms[:bookcase][1].scope << :testing
|
89
|
+
perms[:bookcase][1].has_scope? :testing # => true
|
90
|
+
|
91
|
+
perms = perms.change_type! :blacklist # => Returns a new root graph, blacklisted at root
|
92
|
+
perms[:bookcase].whitelist? # => false
|
93
|
+
|
94
|
+
perms[:bookcase][1].change_type! :blacklist # => Changes this to blacklist mode, applies to children
|
95
|
+
perms[:bookcase][1][:shelf].whitelist? # => false
|
96
|
+
```
|
97
|
+
|
98
|
+
And of course add/remove objects.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
perms.add_subject({
|
102
|
+
id: 1
|
103
|
+
type: 'user'
|
104
|
+
})
|
105
|
+
|
106
|
+
perms.whitelist? # => true
|
107
|
+
perms[:user][1].permitted? # => true
|
108
|
+
|
109
|
+
|
110
|
+
perms[:user][1].unlink
|
111
|
+
perms[:user][1].permitted? # => false
|
112
|
+
```
|
113
|
+
|
114
|
+
One caveat is that for nodes that have a deny on a specific resource type, even if you add it in the graph it will still return false, ensure you either remove/add the resource type to the resources array, or ensure that a whitelisted node has a wildcard("*").
|
115
|
+
|
116
|
+
The best bit is that the objects don't have to exist in the graph to be queried against, it just relies on the last fragment it could find and applies the rule that was set there.
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
perms.whitelist? # => true
|
120
|
+
perms[:bookcase][1][:shelf][12][:book][3].permitted? # => false
|
121
|
+
```
|
22
122
|
|
23
123
|
## Contributing
|
24
124
|
|
25
|
-
1. Fork it ( https://github.com/
|
125
|
+
1. Fork it ( https://github.com/boardiq/sanction/fork )
|
26
126
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
127
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
128
|
4. Push to the branch (`git push origin my-new-feature`)
|
data/Rakefile
CHANGED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Sanction
|
2
|
+
class AttachedList < SimpleDelegator
|
3
|
+
|
4
|
+
attr_accessor :key, :parent
|
5
|
+
|
6
|
+
def initialize(array = [])
|
7
|
+
super(array)
|
8
|
+
end
|
9
|
+
|
10
|
+
def [](index)
|
11
|
+
detect {|x| x.id == index} || wildcard_member || null_node_class.new({id: index, type: key, scope: []}, @parent)
|
12
|
+
end
|
13
|
+
|
14
|
+
def type
|
15
|
+
key
|
16
|
+
end
|
17
|
+
|
18
|
+
def ids_blank?
|
19
|
+
denied_ids.blank? && allowed_ids.blank?
|
20
|
+
end
|
21
|
+
|
22
|
+
def denied_ids
|
23
|
+
[]
|
24
|
+
end
|
25
|
+
|
26
|
+
def allowed_ids
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_scope? scope
|
31
|
+
@parent.has_scope? scope
|
32
|
+
end
|
33
|
+
|
34
|
+
def wildcard_member
|
35
|
+
detect {|x| x.wildcarded? }
|
36
|
+
end
|
37
|
+
|
38
|
+
def wildcarded?
|
39
|
+
!!wildcard_member
|
40
|
+
end
|
41
|
+
|
42
|
+
def wildcard_resource?
|
43
|
+
resources.include?(:*)
|
44
|
+
end
|
45
|
+
|
46
|
+
def resources
|
47
|
+
@parent.resources
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def null_node_class
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Sanction
|
2
|
+
module Blacklist
|
3
|
+
class List < Sanction::AttachedList
|
4
|
+
|
5
|
+
def allowed_ids
|
6
|
+
[]
|
7
|
+
end
|
8
|
+
|
9
|
+
def permitted?
|
10
|
+
return false if wildcard_resource?
|
11
|
+
return false if resources.include?(@key)
|
12
|
+
return true if ids_blank?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def blacklist?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def whitelist?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def denied_ids
|
25
|
+
entries.map {|x| x.id}
|
26
|
+
end
|
27
|
+
|
28
|
+
def null_node_class
|
29
|
+
Sanction::Blacklist::NullNode
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Sanction
|
2
|
+
module Blacklist
|
3
|
+
class Node < Sanction::Node
|
4
|
+
|
5
|
+
def permitted?
|
6
|
+
super
|
7
|
+
root? ? true : (@parent[type].permitted? && @parent[type].allowed_ids.include?(id))
|
8
|
+
end
|
9
|
+
|
10
|
+
def allow!
|
11
|
+
@parent.resources.reject! {|x| x == type } unless @parent[type].count > 1
|
12
|
+
unlink
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def deny!
|
17
|
+
false
|
18
|
+
end
|
19
|
+
|
20
|
+
def whitelist?
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
def blacklist?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
def mode
|
29
|
+
'blacklist'
|
30
|
+
end
|
31
|
+
|
32
|
+
def array_class
|
33
|
+
Sanction::Blacklist::List
|
34
|
+
end
|
35
|
+
|
36
|
+
def null_array_class
|
37
|
+
Sanction::Blacklist::NullList
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Sanction
|
2
|
+
module Blacklist
|
3
|
+
class NullList < Sanction::Blacklist::List
|
4
|
+
|
5
|
+
def permitted?
|
6
|
+
return false if wildcard_resource?
|
7
|
+
return false if resources.include?(@key) && ids_blank?
|
8
|
+
return true if ids_blank?
|
9
|
+
end
|
10
|
+
|
11
|
+
def persisted?
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def null_node_class
|
16
|
+
Sanction::Blacklist::NullNode
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Sanction
|
2
|
+
module Blacklist
|
3
|
+
class NullNode < Sanction::Blacklist::Node
|
4
|
+
|
5
|
+
def permitted?
|
6
|
+
a = ancestors.reject(&:root?).map(&:permitted?)
|
7
|
+
a << true
|
8
|
+
a.all?
|
9
|
+
end
|
10
|
+
|
11
|
+
def allow!
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def deny!
|
16
|
+
ancestors.reject(&:persisted?).each(&:deny!)
|
17
|
+
@parent.resources << type
|
18
|
+
@parent.resources.uniq!
|
19
|
+
@parent.add_subject({
|
20
|
+
id: id,
|
21
|
+
type: type
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
def persisted?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def array_class
|
30
|
+
Sanction::Blacklist::NullList
|
31
|
+
end
|
32
|
+
|
33
|
+
alias :null_array_class :array_class
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Sanction
|
2
|
+
class Node
|
3
|
+
include Tree
|
4
|
+
|
5
|
+
attr_reader :id, :type
|
6
|
+
|
7
|
+
def initialize(hash, parent=nil)
|
8
|
+
@parent = parent
|
9
|
+
process_hash(hash)
|
10
|
+
end
|
11
|
+
|
12
|
+
def permitted?
|
13
|
+
return false if wildcarded? && @parent.blacklist?
|
14
|
+
return true if wildcarded? && @parent.whitelist?
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
{
|
19
|
+
id: @id,
|
20
|
+
type: @type,
|
21
|
+
mode: mode,
|
22
|
+
scope: @scope,
|
23
|
+
subjects: subjects.map {|x| x.to_hash},
|
24
|
+
resources: @resources
|
25
|
+
}.reject { |k, v| v.blank? }
|
26
|
+
end
|
27
|
+
|
28
|
+
def persisted?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the new graph with the switched mode
|
33
|
+
def change_type! type
|
34
|
+
hash = to_hash
|
35
|
+
klass = "sanction/#{type}/node".classify.constantize
|
36
|
+
if root?
|
37
|
+
klass.new(hash)
|
38
|
+
else
|
39
|
+
node = klass.new(hash, parent)
|
40
|
+
parent.children << node
|
41
|
+
unlink
|
42
|
+
node.root
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_subject(hash)
|
47
|
+
mode_class = hash[:mode] || mode
|
48
|
+
children << "sanction/#{mode_class}/node".classify.constantize.new(hash, self)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Virtual
|
52
|
+
def array_class
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
def null_array_class
|
57
|
+
raise NotImplementedError
|
58
|
+
end
|
59
|
+
|
60
|
+
def [](key)
|
61
|
+
klass = subjects.select {|x| x.type?(key) }.any? ? array_class : null_array_class
|
62
|
+
klass.new(subjects.select {|x| x.type?(key) }).tap do |x|
|
63
|
+
x.key = key
|
64
|
+
x.parent = self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def find(type, id)
|
69
|
+
out = root
|
70
|
+
walk do |child|
|
71
|
+
out = child if (child.type?(type) && child.id?(id))
|
72
|
+
end
|
73
|
+
out
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_scope? scope_symbol
|
77
|
+
scope.include? scope_symbol.to_sym
|
78
|
+
end
|
79
|
+
|
80
|
+
def type?(type)
|
81
|
+
@type == type.to_sym
|
82
|
+
end
|
83
|
+
|
84
|
+
def id?(id)
|
85
|
+
@id == id
|
86
|
+
end
|
87
|
+
|
88
|
+
def scope
|
89
|
+
@scope.blank? ? parent.scope : @scope
|
90
|
+
end
|
91
|
+
|
92
|
+
def scope=(scope)
|
93
|
+
@scope = [scope].flatten.compact.map(&:to_sym)
|
94
|
+
end
|
95
|
+
|
96
|
+
def resources
|
97
|
+
return [] if (@resources.blank? && root?)
|
98
|
+
@resources.blank? ? parent.resources : @resources
|
99
|
+
end
|
100
|
+
|
101
|
+
def resources=(resource)
|
102
|
+
@resources = [resource].flatten.compact.map(&:to_sym)
|
103
|
+
end
|
104
|
+
|
105
|
+
def mode
|
106
|
+
raise NotImplementedError
|
107
|
+
end
|
108
|
+
|
109
|
+
def children?
|
110
|
+
children.any?
|
111
|
+
end
|
112
|
+
|
113
|
+
def wildcarded?
|
114
|
+
@id == '*'
|
115
|
+
end
|
116
|
+
|
117
|
+
def children
|
118
|
+
@children ||= array_class.new
|
119
|
+
end
|
120
|
+
|
121
|
+
alias :subjects :children
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def process_hash(hash)
|
126
|
+
@id = hash[:id]
|
127
|
+
@scope = hash[:scope].map(&:to_sym) unless hash[:scope].blank?
|
128
|
+
@type = hash[:type].to_sym if hash[:type]
|
129
|
+
@resources = []
|
130
|
+
@resources += hash[:resources].map(&:to_sym) unless hash[:resources].blank?
|
131
|
+
hash[:subjects].each { |subject| add_subject(subject) } unless hash[:subjects].blank?
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Sanction
|
2
|
+
class Permission
|
3
|
+
|
4
|
+
attr_reader :predicates
|
5
|
+
|
6
|
+
def initialize(permission_graph, *predicates)
|
7
|
+
@graph = permission_graph
|
8
|
+
@predicates = predicates
|
9
|
+
end
|
10
|
+
|
11
|
+
def path
|
12
|
+
@path ||= begin
|
13
|
+
path = @graph.root
|
14
|
+
@predicates.each do |predicate|
|
15
|
+
if predicate.is_a?(Class)
|
16
|
+
path = path[predicate.to_s.demodulize.underscore.to_sym]
|
17
|
+
else
|
18
|
+
path = path[predicate.class.to_s.demodulize.underscore.to_sym][predicate.id]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
path
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def persisted?
|
26
|
+
path.persisted?
|
27
|
+
end
|
28
|
+
|
29
|
+
def permitted?
|
30
|
+
path.permitted?
|
31
|
+
end
|
32
|
+
|
33
|
+
def permitted_with_scope?(scope)
|
34
|
+
permitted? && path.has_scope?(scope)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Sanction
|
2
|
+
module Tree
|
3
|
+
|
4
|
+
# This node's parent.
|
5
|
+
def parent
|
6
|
+
@parent
|
7
|
+
end
|
8
|
+
|
9
|
+
# Is this node the root of the tree?
|
10
|
+
def root?
|
11
|
+
parent.nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
# Is this node a leaf node? Is this node childless?
|
15
|
+
def leaf?
|
16
|
+
children.nil? || children.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get the root node in this tree.
|
20
|
+
def root
|
21
|
+
if root?
|
22
|
+
self
|
23
|
+
else
|
24
|
+
parent.root
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Get the children of this node.
|
29
|
+
def children
|
30
|
+
@children ||= []
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get the siblings of this node. The other children belonging to this node's parent.
|
34
|
+
def siblings
|
35
|
+
if parent
|
36
|
+
parent.children.reject {|child| child == self }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Is this node the only child of its parent. Does it have any siblings?
|
41
|
+
def only_child?
|
42
|
+
if parent
|
43
|
+
siblings.empty?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Does this node have children? Is it not a leaf node?
|
48
|
+
def has_children?
|
49
|
+
!leaf?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get all of the ancestors for this node.
|
53
|
+
def ancestors
|
54
|
+
ancestors = []
|
55
|
+
if parent
|
56
|
+
ancestors << parent
|
57
|
+
parent.ancestors.each {|ancestor| ancestors << ancestor }
|
58
|
+
end
|
59
|
+
ancestors
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get all of the descendants of this node.
|
63
|
+
# All of its children, and its childrens' children, and its childrens' childrens' children...
|
64
|
+
def descendants
|
65
|
+
descendants = []
|
66
|
+
if !children.empty?
|
67
|
+
(descendants << children).flatten!
|
68
|
+
children.each {|descendant| descendants << descendant.descendants }
|
69
|
+
descendants.flatten!
|
70
|
+
end
|
71
|
+
descendants
|
72
|
+
end
|
73
|
+
|
74
|
+
# An integer representation of how deep in the tree this node is.
|
75
|
+
# The root node has a depth of 1, its children have a depth of 2, etc.
|
76
|
+
def depth
|
77
|
+
ancestors.size + 1
|
78
|
+
end
|
79
|
+
|
80
|
+
# From Wikipedia: The height of a node is the length
|
81
|
+
# of the longest downward path to a leaf from that node.
|
82
|
+
# In other words, the length of this node to its furthest descendant.
|
83
|
+
def height
|
84
|
+
if !leaf?
|
85
|
+
descendants.collect {|child| child.depth }.uniq.size + 1
|
86
|
+
else
|
87
|
+
1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Orphan this node. Remove it from its parent node.
|
92
|
+
def unlink
|
93
|
+
if parent
|
94
|
+
parent.children.delete(self)
|
95
|
+
self.instance_variable_set(:@parent, nil)
|
96
|
+
return self
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Abandon all of this node's children.
|
101
|
+
def prune
|
102
|
+
if children
|
103
|
+
children.each {|child| child.unlink }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Append a node to this node's children, and return the node.
|
108
|
+
def graft(node)
|
109
|
+
node.instance_variable_set(:@parent, self)
|
110
|
+
children << node
|
111
|
+
node
|
112
|
+
end
|
113
|
+
|
114
|
+
# Recursively yield every node in the tree.
|
115
|
+
def walk(&block)
|
116
|
+
if block_given?
|
117
|
+
yield self
|
118
|
+
children.each {|child| child.walk(&block) }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
data/lib/sanction/version.rb
CHANGED