authorize 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +20 -0
- data/README +155 -0
- data/Rakefile +25 -0
- data/TODO.txt +9 -0
- data/authorize.gemspec +25 -0
- data/generators/authorize/USAGE +8 -0
- data/generators/authorize/authorize_generator.rb +7 -0
- data/generators/authorize/templates/migrate/create_authorizations.rb +26 -0
- data/install.rb +1 -0
- data/lib/authorize.rb +2 -0
- data/lib/authorize/action_controller.rb +59 -0
- data/lib/authorize/action_view.rb +4 -0
- data/lib/authorize/active_record.rb +37 -0
- data/lib/authorize/bitmask.rb +84 -0
- data/lib/authorize/exceptions.rb +30 -0
- data/lib/authorize/graph.rb +4 -0
- data/lib/authorize/graph/directed_acyclic_graph.rb +10 -0
- data/lib/authorize/graph/directed_acyclic_graph_reverse_traverser.rb +27 -0
- data/lib/authorize/graph/directed_acyclic_graph_traverser.rb +30 -0
- data/lib/authorize/graph/directed_graph.rb +27 -0
- data/lib/authorize/graph/edge.rb +58 -0
- data/lib/authorize/graph/factory.rb +39 -0
- data/lib/authorize/graph/fixtures.rb +33 -0
- data/lib/authorize/graph/graph.rb +55 -0
- data/lib/authorize/graph/traverser.rb +89 -0
- data/lib/authorize/graph/undirected_graph.rb +14 -0
- data/lib/authorize/graph/vertex.rb +53 -0
- data/lib/authorize/permission.rb +97 -0
- data/lib/authorize/redis.rb +2 -0
- data/lib/authorize/redis/array.rb +36 -0
- data/lib/authorize/redis/base.rb +165 -0
- data/lib/authorize/redis/connection_manager.rb +88 -0
- data/lib/authorize/redis/connection_specification.rb +16 -0
- data/lib/authorize/redis/factory.rb +64 -0
- data/lib/authorize/redis/fixtures.rb +22 -0
- data/lib/authorize/redis/hash.rb +34 -0
- data/lib/authorize/redis/model_reference.rb +21 -0
- data/lib/authorize/redis/model_set.rb +19 -0
- data/lib/authorize/redis/set.rb +42 -0
- data/lib/authorize/redis/string.rb +17 -0
- data/lib/authorize/resource.rb +4 -0
- data/lib/authorize/resource_pool.rb +87 -0
- data/lib/authorize/role.rb +115 -0
- data/lib/authorize/test_helper.rb +42 -0
- data/lib/authorize/trustee.rb +4 -0
- data/lib/authorize/version.rb +3 -0
- data/rails/init.rb +5 -0
- data/tasks/authorize_tasks.rake +4 -0
- data/test/Rakefile +7 -0
- data/test/app/controllers/application_controller.rb +5 -0
- data/test/app/controllers/thingy_controller.rb +11 -0
- data/test/app/controllers/widgets_controller.rb +2 -0
- data/test/app/models/public.rb +14 -0
- data/test/app/models/user.rb +8 -0
- data/test/app/models/widget.rb +7 -0
- data/test/config/boot.rb +109 -0
- data/test/config/database.yml +25 -0
- data/test/config/environment.rb +28 -0
- data/test/config/environments/development.rb +4 -0
- data/test/config/environments/test.rb +0 -0
- data/test/config/initializers/mask.rb +1 -0
- data/test/config/initializers/redis.rb +8 -0
- data/test/config/routes.rb +5 -0
- data/test/db/.gitignore +1 -0
- data/test/db/schema.rb +26 -0
- data/test/log/.gitignore +2 -0
- data/test/public/javascripts/application.js +2 -0
- data/test/public/javascripts/controls.js +963 -0
- data/test/public/javascripts/dragdrop.js +972 -0
- data/test/public/javascripts/effects.js +1120 -0
- data/test/public/javascripts/prototype.js +4225 -0
- data/test/script/about +3 -0
- data/test/script/console +3 -0
- data/test/script/dbconsole +3 -0
- data/test/script/destroy +3 -0
- data/test/script/generate +3 -0
- data/test/script/performance/benchmarker +3 -0
- data/test/script/performance/profiler +3 -0
- data/test/script/performance/request +3 -0
- data/test/script/plugin +3 -0
- data/test/script/process/inspector +3 -0
- data/test/script/process/reaper +3 -0
- data/test/script/process/spawner +3 -0
- data/test/script/runner +3 -0
- data/test/script/server +3 -0
- data/test/test/fixtures/authorize/role_graph.yml +11 -0
- data/test/test/fixtures/permissions.yml +27 -0
- data/test/test/fixtures/redis/redis.yml +8 -0
- data/test/test/fixtures/redis/role_graph.yml +29 -0
- data/test/test/fixtures/roles.yml +28 -0
- data/test/test/fixtures/users.yml +12 -0
- data/test/test/fixtures/widgets.yml +12 -0
- data/test/test/functional/controller_class_test.rb +36 -0
- data/test/test/functional/controller_test.rb +46 -0
- data/test/test/test_helper.rb +35 -0
- data/test/test/unit/bitmask_test.rb +112 -0
- data/test/test/unit/fixture_test.rb +59 -0
- data/test/test/unit/graph_directed_acyclic_graph_reverse_traverser_test.rb +43 -0
- data/test/test/unit/graph_directed_acyclic_graph_traverser_test.rb +57 -0
- data/test/test/unit/graph_directed_graph_test.rb +66 -0
- data/test/test/unit/graph_edge_test.rb +53 -0
- data/test/test/unit/graph_graph_test.rb +50 -0
- data/test/test/unit/graph_traverser_test.rb +43 -0
- data/test/test/unit/graph_vertex_test.rb +57 -0
- data/test/test/unit/permission_test.rb +123 -0
- data/test/test/unit/redis_array_test.rb +60 -0
- data/test/test/unit/redis_connection_manager_test.rb +54 -0
- data/test/test/unit/redis_factory_test.rb +85 -0
- data/test/test/unit/redis_fixture_test.rb +18 -0
- data/test/test/unit/redis_hash_test.rb +43 -0
- data/test/test/unit/redis_model_reference_test.rb +39 -0
- data/test/test/unit/redis_set_test.rb +68 -0
- data/test/test/unit/redis_string_test.rb +25 -0
- data/test/test/unit/redis_test.rb +121 -0
- data/test/test/unit/resource_pool_test.rb +93 -0
- data/test/test/unit/resource_test.rb +33 -0
- data/test/test/unit/role_test.rb +143 -0
- data/test/test/unit/trustee_test.rb +35 -0
- data/test/tmp/.gitignore +2 -0
- data/uninstall.rb +1 -0
- metadata +319 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
class Authorize::Bitmask < Set
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
class << self
|
7
|
+
attr_reader :name_values
|
8
|
+
def new(fixnum_or_enum = Set.new)
|
9
|
+
enum = fixnum_or_enum.kind_of?(Fixnum) ? enum(fixnum_or_enum) : fixnum_or_enum
|
10
|
+
super(enum)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Record bit names and define dynamic methods
|
14
|
+
def name_values=(h)
|
15
|
+
h.each do |n, v|
|
16
|
+
define_method("_#{n}") {include?(n) ? true : false}
|
17
|
+
define_method("_#{n}=") {|v| v ? self.add(n) : self.delete(n)}
|
18
|
+
alias_method "_#{n}?", "_#{n}"
|
19
|
+
end
|
20
|
+
@name_values = h
|
21
|
+
end
|
22
|
+
|
23
|
+
# The maximum value this bitmask can hold (in which every named bit is set).
|
24
|
+
def max
|
25
|
+
name_values.values.inject{|memo, v| memo | v}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Enumerates all operations included in the given mask
|
29
|
+
def enum(mask)
|
30
|
+
raise RangeError, "Unnamed bits in mask (#{mask.to_s(2)})" unless (mask | max) == max
|
31
|
+
name_values.inject(Set[]){|s, (p, v)| s << p if (v == (mask & v)); s }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def add(el)
|
36
|
+
raise ArgumentError, "Unrecognized bit name (#{el})" unless self.class.name_values.keys.include?(el)
|
37
|
+
super
|
38
|
+
end
|
39
|
+
alias << add
|
40
|
+
|
41
|
+
# Calculate the integer value for the mask
|
42
|
+
def to_i
|
43
|
+
inject(0) {|memo, n| memo | self.class.name_values[n]}
|
44
|
+
end
|
45
|
+
alias to_int to_i
|
46
|
+
|
47
|
+
def valid?
|
48
|
+
inject(true) {|memo, n| memo && !!self.class.name_values[n]}
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return an equivalent Bitmask using only fundamental names, never aggregate names
|
52
|
+
def fundamental
|
53
|
+
complete.to_canonical_array.inject(self.class.new) do |memo, n|
|
54
|
+
memo << n unless (memo.to_i & self.class.name_values[n]) == self.class.name_values[n]
|
55
|
+
memo
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Return an equivalent Bitmask using aggregated names to replace fundamental names where possible
|
60
|
+
def minimal
|
61
|
+
complete.to_canonical_array.reverse.inject(self.class.new) do |memo, n|
|
62
|
+
memo << n unless (memo.to_i & self.class.name_values[n]) == self.class.name_values[n]
|
63
|
+
memo
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Return an equivalent Bitmask using all possible names (fundamental and aggregate)
|
68
|
+
def complete
|
69
|
+
self.class.new(to_int)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Comparability derives from integer representation
|
73
|
+
def <=>(other)
|
74
|
+
to_int <=> other.to_int
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
to_canonical_array.join("|")
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_canonical_array
|
82
|
+
sort_by{|name| self.class.name_values[name]}
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Authorize #:nodoc:
|
2
|
+
|
3
|
+
# Base error class for Authorization module
|
4
|
+
class AuthorizationError < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
# Raised when the authorization expression is invalid (cannot be parsed)
|
8
|
+
class AuthorizationExpressionInvalid < AuthorizationError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Raised when we can't find the current user
|
12
|
+
class CannotObtainTokens < AuthorizationError
|
13
|
+
end
|
14
|
+
|
15
|
+
# Raised when an authorization expression contains a model class that doesn't exist
|
16
|
+
class CannotObtainModelClass < AuthorizationError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Raised when an authorization expression contains a model reference that doesn't exist
|
20
|
+
class CannotObtainModelObject < AuthorizationError
|
21
|
+
end
|
22
|
+
|
23
|
+
# Raised when the obtained trustee object doesn't implement #has_role?
|
24
|
+
class TrusteeDoesntImplementRoles < AuthorizationError
|
25
|
+
end
|
26
|
+
|
27
|
+
# Raised when the obtained model doesn't implement #accepts_role?
|
28
|
+
class ModelDoesntImplementRoles < AuthorizationError
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module Authorize
|
4
|
+
module Graph
|
5
|
+
class DirectedAcyclicGraphReverseTraverser < DirectedAcyclicGraphTraverser
|
6
|
+
private
|
7
|
+
# Recursively traverse vertices breadth-wise, in reverse.
|
8
|
+
# Traversal is pruned if the block returns an untrue value.
|
9
|
+
def _traverse_breadth_first(start, depth, &block)
|
10
|
+
depth += 1
|
11
|
+
start.inbound_edges.select{|e| yield(e.from, e, depth)}.each do |e|
|
12
|
+
_traverse_breadth_first(e.from, depth, &block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Recursively traverse vertices depth-wise, in reverse.
|
17
|
+
# Traversal is pruned if the block returns an untrue value.
|
18
|
+
def _traverse_depth_first(start, depth, &block)
|
19
|
+
depth += 1
|
20
|
+
start.inbound_edges.each do |e|
|
21
|
+
_traverse_depth_first(e.from, depth, &block) if yield(e.from, e, depth)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
alias _traverse _traverse_depth_first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module Authorize
|
4
|
+
module Graph
|
5
|
+
class DirectedAcyclicGraphTraverser < Traverser
|
6
|
+
def traverse(check = false, &block)
|
7
|
+
super(&block) unless check
|
8
|
+
t = self.class.new(self, :traverse)
|
9
|
+
if check
|
10
|
+
t.cycle_detector.pruner.cost_collector
|
11
|
+
else
|
12
|
+
t.pruner.cost_collector
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Detect cycles in the graph by recording the path taken (effectively an array of visited vertices indexed by
|
17
|
+
# depth). When a cycle is detected (by finding the current vertex earlier in the path), raise an exception.
|
18
|
+
def cycle_detector(&block)
|
19
|
+
return self.class.new(self, :cycle_detector) unless block_given?
|
20
|
+
seen = ::Array.new
|
21
|
+
self.each do |vertex, edge, depth|
|
22
|
+
found = seen.index(vertex)
|
23
|
+
raise "Cycle detected at #{vertex} along #{edge} at depth #{found} and #{depth}" if found && (found < depth)
|
24
|
+
seen[depth] = vertex
|
25
|
+
yield vertex, edge, depth
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'authorize/redis'
|
2
|
+
module Authorize
|
3
|
+
module Graph
|
4
|
+
# A directed graph implementation. Every edge either connects "to" a vertex or "from" it.
|
5
|
+
# This implementation ensures that edges are not duplicated (which precludes multigraphs). In
|
6
|
+
# cases where a duplicate edge is requested, the given properties are merged with the properties
|
7
|
+
# of the existing edge and the existing edge is returned.
|
8
|
+
|
9
|
+
# Notes:
|
10
|
+
# Edges are created in the context of a graph in order to allow for graph-specific indexing
|
11
|
+
class DirectedGraph < Graph::Graph
|
12
|
+
# Find or create a directed edge joining the given vertices
|
13
|
+
def join(name, v0, v1, properties = {})
|
14
|
+
existing_edge = v0.edges.detect{|e| v1.eql?(e.to)}
|
15
|
+
existing_edge.try(:merge, properties)
|
16
|
+
existing_edge || edge(name, v0, v1, properties)
|
17
|
+
end
|
18
|
+
|
19
|
+
def disjoin(v0, v1)
|
20
|
+
return unless existing_edge = v0.edges.detect{|e| v1.eql?(e.to)}
|
21
|
+
existing_edge.tap do |edge|
|
22
|
+
edge.destroy
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Authorize
|
2
|
+
module Graph
|
3
|
+
# An edge connects two vertices. The order in which the vertices are supplied is preserved and can be
|
4
|
+
# used to imply direction.
|
5
|
+
# TODO: persist the connected vertices in an array.
|
6
|
+
# TODO: a hyperedge can be modeled with a set of vertices instead of explicit left and right vertices.
|
7
|
+
class Edge < Redis::Hash
|
8
|
+
include Redis::ModelReference
|
9
|
+
|
10
|
+
def self.exists?(id)
|
11
|
+
super(subordinate_key(id, 'l_id'))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.load_all(namespace = name)
|
15
|
+
redis_glob = subordinate_key(namespace, '*', 'l_id')
|
16
|
+
re = Regexp.new(subordinate_key(namespace, ".+(?=#{NAMESPACE_SEPARATOR})"))
|
17
|
+
keys = db.keys(redis_glob)
|
18
|
+
keys = keys.map{|m| m.slice(re)}
|
19
|
+
keys.map{|id| load(id)}
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(v0, v1, properties = {})
|
23
|
+
super()
|
24
|
+
set_reference(subordinate_key('l_id'), v0)
|
25
|
+
set_reference(subordinate_key('r_id'), v1)
|
26
|
+
v0.outbound_edges << self
|
27
|
+
v1.inbound_edges << self
|
28
|
+
merge(properties) if properties.any?
|
29
|
+
end
|
30
|
+
|
31
|
+
def from
|
32
|
+
load_reference(subordinate_key('l_id'), Vertex)
|
33
|
+
end
|
34
|
+
alias left from
|
35
|
+
|
36
|
+
def to
|
37
|
+
load_reference(subordinate_key('r_id'), Vertex)
|
38
|
+
end
|
39
|
+
alias right to
|
40
|
+
|
41
|
+
def vertices
|
42
|
+
[from, to]
|
43
|
+
end
|
44
|
+
|
45
|
+
def destroy
|
46
|
+
from && from.outbound_edges.delete(self)
|
47
|
+
to && to.inbound_edges.delete(self)
|
48
|
+
self.class.db.del(subordinate_key('l_id'))
|
49
|
+
self.class.db.del(subordinate_key('r_id'))
|
50
|
+
super
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid?
|
54
|
+
from && to && super
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'authorize/graph/vertex'
|
2
|
+
require 'authorize/graph/edge'
|
3
|
+
require 'authorize/redis/factory'
|
4
|
+
|
5
|
+
module Authorize
|
6
|
+
module Graph
|
7
|
+
class Factory < Redis::Factory
|
8
|
+
def directed_graph(name, value = Set[], options = {}, &block)
|
9
|
+
options = {:edge_ids => ::Set[]}.merge(options)
|
10
|
+
obj = set(name, value) do
|
11
|
+
set('edge_ids', options[:edge_ids])
|
12
|
+
yield if block_given?
|
13
|
+
end
|
14
|
+
DirectedGraph.load(obj.id)
|
15
|
+
end
|
16
|
+
|
17
|
+
def vertex(name, value = {}, options = {}, &block)
|
18
|
+
options = {:edge_ids => ::Set[], :inbound_edge_ids => ::Set[]}.merge(options)
|
19
|
+
obj = hash(name, value) do
|
20
|
+
string('_', nil)
|
21
|
+
set('edge_ids', options[:edge_ids])
|
22
|
+
set('inbound_edge_ids', options[:inbound_edge_ids])
|
23
|
+
yield if block_given?
|
24
|
+
end
|
25
|
+
Vertex.load(obj.id)
|
26
|
+
end
|
27
|
+
|
28
|
+
def edge(name, value = {}, options = {}, &block)
|
29
|
+
options = {:l_id => nil, :r_id => nil}.merge(options)
|
30
|
+
obj = hash(name, value) do
|
31
|
+
string(:l_id, options[:l_id])
|
32
|
+
string(:r_id, options[:r_id])
|
33
|
+
yield if block_given?
|
34
|
+
end
|
35
|
+
Edge.load(obj.id)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'active_support/test_case'
|
2
|
+
require 'active_record/fixtures'
|
3
|
+
|
4
|
+
module Authorize
|
5
|
+
module Graph
|
6
|
+
module Fixtures
|
7
|
+
YAML.add_domain_type("hapgoods.com,2010", 'graph') do |type, value|
|
8
|
+
process(Role.graph, value)
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_fixtures(db = Redis::Base.db, pathname = Pathname.new(ActiveSupport::TestCase.fixture_path).join('authorize', 'role_graph.yml'), flush = true)
|
12
|
+
db.flushdb if flush
|
13
|
+
YAML.load(ERB.new(pathname.read).result)
|
14
|
+
end
|
15
|
+
module_function :create_fixtures
|
16
|
+
|
17
|
+
def self.process(graph, nodes, parent = nil)
|
18
|
+
nodes.each do |node|
|
19
|
+
name = node.respond_to?(:keys) ? node.keys.first : node
|
20
|
+
children = node.respond_to?(:values) ? node.values.first : []
|
21
|
+
key = name_to_key(name)
|
22
|
+
vertex = graph.vertex(key)
|
23
|
+
graph.edge(nil, parent, vertex) if parent
|
24
|
+
process(graph, children, vertex) unless children.empty?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.name_to_key(name)
|
29
|
+
::Fixtures.identify(name).to_s
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'authorize/redis'
|
2
|
+
|
3
|
+
module Authorize
|
4
|
+
module Graph
|
5
|
+
# A binary property graph. Vertices and Edges have an arbitrary set of named properties.
|
6
|
+
# Reference: http://www.nist.gov/dads/HTML/graph.html
|
7
|
+
class Graph < Redis::Set
|
8
|
+
def self.exists?(id)
|
9
|
+
db.keys(subordinate_key(id, '*')).any?
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_writer :edge_namespace, :vertex_namespace
|
13
|
+
|
14
|
+
def edge_namespace
|
15
|
+
@edge_namespace ||= subordinate_key('_edges')
|
16
|
+
end
|
17
|
+
|
18
|
+
def vertex_namespace
|
19
|
+
@vertex_namespace ||= subordinate_key('_vertices')
|
20
|
+
end
|
21
|
+
|
22
|
+
def edges
|
23
|
+
Edge.load_all(edge_namespace)
|
24
|
+
end
|
25
|
+
|
26
|
+
def vertices
|
27
|
+
Vertex.load_all(vertex_namespace)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Create an vertex on this graph with the given name and additional properties.
|
31
|
+
def vertex(name, *args)
|
32
|
+
name ||= self.class.next_counter(vertex_namespace)
|
33
|
+
key = self.class.subordinate_key(vertex_namespace, name)
|
34
|
+
Vertex.new(key, *args)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Create an edge on this graph with the given name and additional properties.
|
38
|
+
def edge(name, *args)
|
39
|
+
name ||= self.class.next_counter(edge_namespace)
|
40
|
+
key = self.class.subordinate_key(edge_namespace, name)
|
41
|
+
Edge.new(key, *args)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Load the existing vertex in this graph with the given name.
|
45
|
+
def vertex_by_name(name)
|
46
|
+
key = self.class.subordinate_key(vertex_namespace, name)
|
47
|
+
Vertex.load(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def traverse(start = Vertex.load(sort_by{rand}.first))
|
51
|
+
Traverser.traverse(start)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module Authorize
|
4
|
+
module Graph
|
5
|
+
# Traverse the graph by enumerating the encountered vertices.
|
6
|
+
class Traverser < Enumerable::Enumerator
|
7
|
+
# Traverse the graph starting at the given vertex. A "bootstrap" enumerator is created from the starting
|
8
|
+
# vertex. The bootstrap enumerator is then passed through a recursive expander and finally, several filters.
|
9
|
+
# In ruby 1.9, bootstrapping could be simplified with an anonymous enumerator (Enumerator.new {})
|
10
|
+
def self.traverse(start, *args, &block)
|
11
|
+
self.new(start, :tap).traverse(*args).visit(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Prune the graph by accumulating a set of visited nodes. When a vertex has already been visited, interrupt the
|
15
|
+
# visit and signal the yielder.
|
16
|
+
def pruner(&block)
|
17
|
+
return self.class.new(self, :pruner) unless block_given?
|
18
|
+
seen = ::Set.new
|
19
|
+
self.each do |vertex, edge, depth|
|
20
|
+
next false if seen.include?(vertex)
|
21
|
+
seen << vertex
|
22
|
+
yield vertex, edge, depth
|
23
|
+
true # Don't let client block return value influence traversal
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Output the values yielded by the yielder as well as the return value of the supplied block.
|
28
|
+
def debugger(&block)
|
29
|
+
return self.class.new(self, :debugger) unless block_given?
|
30
|
+
count = 0 # A transit counter that
|
31
|
+
self.each do |*args|
|
32
|
+
count += 1
|
33
|
+
block.call(*args).tap do |result|
|
34
|
+
print "#{count}\t#{result}\t" + args.join("\t") + "\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the accumulated cost of traversing the graph. The default accumulator is a simple transit counter.
|
40
|
+
def cost_collector(cost = 0, f = lambda{|e| 1}, &block)
|
41
|
+
return self.class.new(self, :cost_collector) unless block_given?
|
42
|
+
inject(cost) do |total, (vertex, edge, depth)|
|
43
|
+
yield vertex, edge, depth
|
44
|
+
total + f.call(edge)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Invoke callback on vertex. Strip the edge to be more conventional.
|
49
|
+
# This is typically the last filter in the traverse chain.
|
50
|
+
def visit(&block)
|
51
|
+
return self.class.new(self, :visit) unless block_given?
|
52
|
+
self.each do |vertex, edge, depth|
|
53
|
+
vertex.visit(edge, &block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Visit the yielded start vertex and traverse to its adjacencies.
|
58
|
+
# This operation effectively "expands" the enumerator.
|
59
|
+
def traverse(&block)
|
60
|
+
return self.class.new(self, :traverse).pruner.cost_collector unless block_given?
|
61
|
+
each do |vertex, edge|
|
62
|
+
depth = 0
|
63
|
+
yield vertex, edge, depth
|
64
|
+
_traverse(vertex, depth, &block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
# Recursively traverse vertices breadth-wise
|
70
|
+
# Traversal is pruned if the block returns an untrue value.
|
71
|
+
def _traverse_breadth_first(start, depth, &block)
|
72
|
+
depth += 1
|
73
|
+
start.outbound_edges.select{|e| yield(e.to, e, depth)}.each do |e|
|
74
|
+
_traverse_breadth_first(e.to, depth, &block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Recursively traverse vertices depth-wise
|
79
|
+
# Traversal is pruned if the block returns an untrue value.
|
80
|
+
def _traverse_depth_first(start, depth, &block)
|
81
|
+
depth += 1
|
82
|
+
start.outbound_edges.each do |e|
|
83
|
+
_traverse_depth_first(e.to, depth, &block) if yield(e.to, e, depth)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
alias _traverse _traverse_depth_first
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|