authorize 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|