reduxco 1.0.0
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/LICENSE.txt +24 -0
- data/README.rdoc +126 -0
- data/Rakefile +19 -0
- data/lib/reduxco.rb +10 -0
- data/lib/reduxco/callable_ref.rb +142 -0
- data/lib/reduxco/context.rb +243 -0
- data/lib/reduxco/context/callable_table.rb +81 -0
- data/lib/reduxco/context/callstack.rb +74 -0
- data/lib/reduxco/reduxer.rb +41 -0
- data/lib/reduxco/version.rb +4 -0
- data/spec/callable_ref_spec.rb +273 -0
- data/spec/callable_table_spec.rb +174 -0
- data/spec/callstack_spec.rb +128 -0
- data/spec/context_spec.rb +619 -0
- data/spec/rdoc_examples_spec.rb +46 -0
- data/spec/reduxer_spec.rb +88 -0
- data/spec/spec_helper.rb +17 -0
- metadata +69 -0
data/LICENSE.txt
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Copyright (c) 2012, WhitePages, Inc.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
* Redistributions of source code must retain the above copyright
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
10
|
+
documentation and/or other materials provided with the distribution.
|
11
|
+
* Neither the name of the company nor the
|
12
|
+
names of its contributors may be used to endorse or promote products
|
13
|
+
derived from this software without specific prior written permission.
|
14
|
+
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL WHITEPAGES, INC. BE LIABLE FOR ANY
|
19
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
20
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
21
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
22
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
23
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
24
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
= Overview
|
2
|
+
|
3
|
+
Reduxco is a general purpose graph reduction calculation engine for those
|
4
|
+
non-linear dependency flows that normal pipelines and Rack Middleware-like
|
5
|
+
architectures can't do cleanly.
|
6
|
+
|
7
|
+
Conceptually, it is similar to using Rack Middleware with named keys to store
|
8
|
+
intermediate calculations that have to be reused later, but unlike Rack Middleware,
|
9
|
+
Reduxco is self organizing based on the dependencies used by each piece.
|
10
|
+
|
11
|
+
It's primary public facing class is Reduxco::Context.
|
12
|
+
|
13
|
+
= Examples
|
14
|
+
|
15
|
+
== Basic Context Use
|
16
|
+
|
17
|
+
In practice, one can build one ore more tables of callable objects (e.g. Procs or
|
18
|
+
custom class instances), and register them with a Reduxco::Context. Callables can then
|
19
|
+
used their Reduco::Context handle to refer to the callables they can depend on.
|
20
|
+
|
21
|
+
For example, the addition of two numbers could be done as follows:
|
22
|
+
|
23
|
+
map = {
|
24
|
+
sum: ->(c){ c[:x] + c[:y] },
|
25
|
+
x: ->(c){ 3 },
|
26
|
+
y: ->(c){ 5 }
|
27
|
+
}
|
28
|
+
|
29
|
+
sum = Reduxco::Reduxer.new(map).reduce(:sum)
|
30
|
+
sum.should == 8
|
31
|
+
|
32
|
+
Note that the symbol <code>:app</code> is the default root node of Reduxco::Context#reduce,
|
33
|
+
so if <code>:sum</code> were renamed to <code>:app</code> above, the last line could
|
34
|
+
be slightly simplified as:
|
35
|
+
|
36
|
+
sum = Reduxco::Context.new(map).reduce
|
37
|
+
|
38
|
+
Of course, any object responding to <code>call</code> can be used as the values in
|
39
|
+
the map, so one could just as easily define a class with an instance method of
|
40
|
+
<code>call</code> on it instead of using Proc objects.
|
41
|
+
|
42
|
+
== Overriding and Super
|
43
|
+
|
44
|
+
If multiple maps of callables are given, and the keys (referred to as names from
|
45
|
+
here on) are duplicated in the maps, the last map given wins, shadowing the previous map.
|
46
|
+
For example:
|
47
|
+
|
48
|
+
map1 = {
|
49
|
+
message: ->(c){ 'Hello From Map 1' }
|
50
|
+
}
|
51
|
+
|
52
|
+
map2 = {
|
53
|
+
message: ->(c){ 'Hello From Map 2' }
|
54
|
+
}
|
55
|
+
|
56
|
+
msg = Reduxco::Reduxer.new(map1, map2).reduce(:message)
|
57
|
+
msg.should == 'Hello From Map 2'
|
58
|
+
|
59
|
+
If one wishes to refer to previous (shadowed) callables, one can do that using
|
60
|
+
Context#super. For example:
|
61
|
+
|
62
|
+
map1 = {
|
63
|
+
message: ->(c){ 'Hello From Map 1' }
|
64
|
+
}
|
65
|
+
|
66
|
+
map2 = {
|
67
|
+
message: ->(c){ c.super + ' and Hello From Map 2' }
|
68
|
+
}
|
69
|
+
|
70
|
+
msg = Reduxco::Context.new(map1, map2).reduce(:message)
|
71
|
+
msg.should == 'Hello From Map 1 and Hello From Map 2'
|
72
|
+
|
73
|
+
== Introspection
|
74
|
+
|
75
|
+
There are several introspection methods for making assertions about the
|
76
|
+
Reduxco::Context. These are usually used by callables to inspect their
|
77
|
+
environment before proceeding.
|
78
|
+
|
79
|
+
[Reduxco::Context#include?] Allows you to inspect if the Reduxco::Context
|
80
|
+
can resolve a given refname if called.
|
81
|
+
[Reduxco::Context#completed?] Allows you to inspect if the callable associated
|
82
|
+
with a given block name has already been called;
|
83
|
+
useful for assertions about weak dependencies.
|
84
|
+
[Reduxco::Context#assert_completed] Like <code>computed?</code>, but it raises
|
85
|
+
an exception if it fails.
|
86
|
+
|
87
|
+
== Before, After, and Inside
|
88
|
+
|
89
|
+
= Contact
|
90
|
+
|
91
|
+
Jeff Reinecke <jreinecke@whitepages.com>
|
92
|
+
|
93
|
+
= Roadmap
|
94
|
+
|
95
|
+
TBD
|
96
|
+
|
97
|
+
= History
|
98
|
+
|
99
|
+
[1.0.0 - 2013-Apr-??] Initial Release.
|
100
|
+
|
101
|
+
= License
|
102
|
+
|
103
|
+
Copyright (c) 2012, WhitePages, Inc.
|
104
|
+
All rights reserved.
|
105
|
+
|
106
|
+
Redistribution and use in source and binary forms, with or without
|
107
|
+
modification, are permitted provided that the following conditions are met:
|
108
|
+
* Redistributions of source code must retain the above copyright
|
109
|
+
notice, this list of conditions and the following disclaimer.
|
110
|
+
* Redistributions in binary form must reproduce the above copyright
|
111
|
+
notice, this list of conditions and the following disclaimer in the
|
112
|
+
documentation and/or other materials provided with the distribution.
|
113
|
+
* Neither the name of the company nor the
|
114
|
+
names of its contributors may be used to endorse or promote products
|
115
|
+
derived from this software without specific prior written permission.
|
116
|
+
|
117
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
118
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
119
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
120
|
+
DISCLAIMED. IN NO EVENT SHALL WHITEPAGES, INC. BE LIABLE FOR ANY
|
121
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
122
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
123
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
124
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
125
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
126
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../Gemfile", Pathname.new(__FILE__).realpath)
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler/setup'
|
5
|
+
|
6
|
+
require "rspec/core/rake_task"
|
7
|
+
|
8
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
9
|
+
spec.rspec_opts = ['--backtrace']
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'rdoc/task'
|
13
|
+
|
14
|
+
RDoc::Task.new do |rdoc|
|
15
|
+
rdoc.rdoc_dir = "rdoc"
|
16
|
+
rdoc.rdoc_files.add "lib/**/*.rb", "README.rdoc"
|
17
|
+
rdoc.options << "--all"
|
18
|
+
#rdoc.options << "--coverage-report" # Useful for finding something undocumented, but won't generate output when this is selected!
|
19
|
+
end
|
data/lib/reduxco.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
module Reduxco
|
2
|
+
# An immutable class that represents a referrence to a callable in a
|
3
|
+
# CallableTable; this class is rarely used directly by clients.
|
4
|
+
class CallableRef
|
5
|
+
include Comparable
|
6
|
+
|
7
|
+
# The minimum depth number allowed.
|
8
|
+
MIN_DEPTH = 1
|
9
|
+
|
10
|
+
# For string representations (typically used in debugging), this is
|
11
|
+
# used as the separator between name and depth (if depth is given).
|
12
|
+
STR_SEPARATOR = ':'
|
13
|
+
|
14
|
+
# For string representations, what is the opening bracket string.
|
15
|
+
STR_LEFT_BRACKET = '<'
|
16
|
+
|
17
|
+
# For string representations, what is the opening bracket string.
|
18
|
+
STR_RIGHT_BRACKET = '>'
|
19
|
+
|
20
|
+
# [name] Typically the name is a symbol, but systems are free to use other
|
21
|
+
# objects as types are not coerced into other types at any point.
|
22
|
+
# If the name is a CallableRef, then this acts as a copy constructor.
|
23
|
+
#
|
24
|
+
# [depth] The depth is normally not given when used, can be specified for
|
25
|
+
# referencing specific shadowed callables when callables are flattend
|
26
|
+
# into a CallableTable; this is important for calls to super.
|
27
|
+
def initialize(name, depth=nil)
|
28
|
+
case name
|
29
|
+
when self.class
|
30
|
+
@name = name.name
|
31
|
+
@depth = (depth && depth.to_i) || name.depth
|
32
|
+
else
|
33
|
+
@name = name
|
34
|
+
@depth = depth && depth.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
raise IndexError, "Depth must be greater than zero", caller if depth && depth<MIN_DEPTH
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the name of the refernce.
|
41
|
+
attr_reader :name
|
42
|
+
|
43
|
+
# Returns the depth of the reference, or nil if the reference is dynamic.
|
44
|
+
attr_reader :depth
|
45
|
+
|
46
|
+
# Is true valued when the reference will dynamically bind to an entry
|
47
|
+
# in the CallableTable instead of to an entry at a specific depth.
|
48
|
+
def dynamic?
|
49
|
+
return depth.nil?
|
50
|
+
end
|
51
|
+
|
52
|
+
# Negation of dynamic?
|
53
|
+
def static?
|
54
|
+
return !dynamic?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns a CallableRef with the same name, but one depth deeper.
|
58
|
+
def succ
|
59
|
+
if( dynamic? )
|
60
|
+
raise RuntimeError, "Dynamic references cannot undergo relative movement."
|
61
|
+
else
|
62
|
+
self.class.new(name, depth.succ)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
alias_method :next, :succ
|
66
|
+
|
67
|
+
# Returns a CallableRef with the same name, but one depth higher.
|
68
|
+
def pred
|
69
|
+
if( dynamic? )
|
70
|
+
raise RuntimeError, "Dynamic references cannot undergo relative movement."
|
71
|
+
else
|
72
|
+
self.class.new(name, depth.pred)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns a unique hash value; useful resolving Hash entries.
|
77
|
+
def hash
|
78
|
+
@hash ||= self.to_a.hash
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns true if the passed ref is
|
82
|
+
#
|
83
|
+
# This method raises an exception when compared to anything that does not
|
84
|
+
# ducktype as a reference.
|
85
|
+
def include?(other)
|
86
|
+
other.name == self.name && (dynamic? ? true : other.depth == self.depth)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns true if the refs are equivalent.
|
90
|
+
def eql?(other)
|
91
|
+
if( other.kind_of?(CallableRef) || (other.respond_to?(:name) && other.respond_to?(:depth)) )
|
92
|
+
other.name == self.name && other.depth == self.depth
|
93
|
+
else
|
94
|
+
false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
alias_method :==, :eql?
|
98
|
+
alias_method :===, :==
|
99
|
+
|
100
|
+
# Returns the sort order of the reference. This is primarily useed
|
101
|
+
# for sorting references in CallableTable so that shadowed callables
|
102
|
+
# are called properly.
|
103
|
+
#
|
104
|
+
# Static references are sorted by the following rule: For all sets of static
|
105
|
+
# refs with equal names, sort by depth. For all sets of static refs with
|
106
|
+
# equal depths, only sort if the names are sortable. This means that
|
107
|
+
# there is no requirement for sort order to group by name or by depth, and
|
108
|
+
# so no software should be written around an assumption of which comes first.
|
109
|
+
#
|
110
|
+
# Refuses to sort dynamic references, as they are not ordered compared to
|
111
|
+
# static references.
|
112
|
+
def <=>(other)
|
113
|
+
if( dynamic? != other.dynamic? )
|
114
|
+
nil
|
115
|
+
else
|
116
|
+
depth_eql = depth <=> other.depth
|
117
|
+
(depth_eql==0 ? (name <=> other.name) : nil) || depth_eql
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns an array form of this CallableReference.
|
122
|
+
def to_a
|
123
|
+
@array ||= [name, depth]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns a hash form of this CallableReference.
|
127
|
+
def to_h
|
128
|
+
@hash ||= {name:name, depth:depth}
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns a human readable string form of this CallableReference.
|
132
|
+
def to_s
|
133
|
+
@string ||= STR_LEFT_BRACKET + self.to_a.compact.map {|prop| prop.to_s}.join(STR_SEPARATOR) + STR_RIGHT_BRACKET
|
134
|
+
end
|
135
|
+
|
136
|
+
# Returns a human readable string form of this CallableReference.
|
137
|
+
def inspect
|
138
|
+
to_s
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
require_relative 'callable_ref'
|
2
|
+
require_relative 'context/callable_table'
|
3
|
+
require_relative 'context/callstack'
|
4
|
+
|
5
|
+
module Reduxco
|
6
|
+
# Context is the client facing object for Reduxco.
|
7
|
+
#
|
8
|
+
# Typically, one instantiates a Context with one or more maps of callables
|
9
|
+
# by name, and then calls Contxt#reduce to calculate all dependent nodes and return
|
10
|
+
# a result.
|
11
|
+
#
|
12
|
+
# Maps may be any object that, when iterated with each, gives name/callable
|
13
|
+
# pairs. Names may be any object that can serve as a hash key. Callables can
|
14
|
+
# be any object that responds to the call method.
|
15
|
+
#
|
16
|
+
# == Overview
|
17
|
+
#
|
18
|
+
# Context orchestrates the reduction calculation. It is primarily used
|
19
|
+
# by callables invoked during computation to get access to their environment.
|
20
|
+
#
|
21
|
+
# Instantiators of a Context typically only use the Context#reduce method.
|
22
|
+
#
|
23
|
+
# Users of Reduxco should use Reduxco::Reduxer rather than directly consume
|
24
|
+
# Context directly.
|
25
|
+
#
|
26
|
+
# == Callable Helper Functions
|
27
|
+
#
|
28
|
+
# Callables (objects that respond to call) are the meat of the Context.
|
29
|
+
# When their call method is invoked, it is passed a reference to the Context.
|
30
|
+
# Callables can use this reference to access a range of methods, including
|
31
|
+
# the following:
|
32
|
+
#
|
33
|
+
# [Context#call] Given a refname, run the associated callable and returns
|
34
|
+
# its value. Usually invoked as Context#[]
|
35
|
+
# [Context#include?] Introspects if a refname is available.
|
36
|
+
# [Context#completed?] Instrospects if a callable has been called and returned.
|
37
|
+
# [Context#after] Given a refname and a block, runs the contents of the block
|
38
|
+
# after the given refname, but returns the value of the callable
|
39
|
+
# accociated with the refname.
|
40
|
+
# [Context#inside] Given a refname and a block, runs the callable associated
|
41
|
+
# with the refname, giving it access to running the block
|
42
|
+
# inside of it and getting its value.
|
43
|
+
class Context
|
44
|
+
|
45
|
+
# Special error type for halting due to a cyclic graph.
|
46
|
+
class CyclicalError < StandardError; end
|
47
|
+
|
48
|
+
# Special error type for when the callable provided has no call method.
|
49
|
+
class NotCallableError < NoMethodError; end
|
50
|
+
|
51
|
+
# A namespaced NameError for when the callref cannot be resolved.
|
52
|
+
class NameError < ::NameError; end
|
53
|
+
|
54
|
+
# Special error type when Context assert methods fail. See +assert_computed+.
|
55
|
+
class AssertError < StandardError; end
|
56
|
+
|
57
|
+
# A namespaced LocalJumpError for when no block is given but a yield is called.
|
58
|
+
class LocalJumpError < ::LocalJumpError; end
|
59
|
+
|
60
|
+
# Instantiate a Context with the one or more callalbe maps (e.g. hashes
|
61
|
+
# whose keys are names and values are callable) for calculations.
|
62
|
+
#
|
63
|
+
# The further to the right in the arguments that a map is, the higher
|
64
|
+
# the precedence of itsdefinition.
|
65
|
+
def initialize(*callable_maps)
|
66
|
+
@callable_maps = callable_maps
|
67
|
+
@calltable = CallableTable.new(@callable_maps)
|
68
|
+
|
69
|
+
@callstack = Callstack.new
|
70
|
+
@cache = {}
|
71
|
+
|
72
|
+
@block_association_cache = {}
|
73
|
+
end
|
74
|
+
|
75
|
+
# Given a refname, call it for this context and return the result.
|
76
|
+
#
|
77
|
+
# This can also take CallableRef instances directly, however if you find
|
78
|
+
# yourself passing in static references, this is likely because of design
|
79
|
+
# flaw in your callable map hierarchy.
|
80
|
+
#
|
81
|
+
# Call results are cached so that their values can be re-used. If callables
|
82
|
+
# have side-effects their side-effects are only invoked the first time
|
83
|
+
# they are run.
|
84
|
+
#
|
85
|
+
# Given a block, Context#yield may be used by the callable to invoke the
|
86
|
+
# block. Depending on the purpose of the block, Context#inside may be
|
87
|
+
# the preferrable alias.
|
88
|
+
def call(refname=:app, &block)
|
89
|
+
# First, we resolve the callref and invoke it.
|
90
|
+
frame, callable = @calltable.resolve( CallableRef.new(refname) )
|
91
|
+
|
92
|
+
# If the ref is nil then we couldn't resolve, otherwise invoke.
|
93
|
+
if( frame.nil? )
|
94
|
+
raise NameError, "No reference for name #{refname.inspect}", caller
|
95
|
+
else
|
96
|
+
invoke(frame, callable, &block)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
alias_method :reduce, :call
|
100
|
+
|
101
|
+
# Shorthand for call, [] is the most frequently used form of the call method
|
102
|
+
# due to its abbreviated form..
|
103
|
+
alias_method :[], :call
|
104
|
+
|
105
|
+
# Inside is preferred to call when call is given a block and the intent
|
106
|
+
# is to manage flow control. Compare to Context#before and Context#after.
|
107
|
+
alias_method :inside, :call
|
108
|
+
|
109
|
+
# When invoked, finds the next callable in the CallableTable up the chain
|
110
|
+
# from the current frame, calls it, and returns the result.
|
111
|
+
#
|
112
|
+
# This is primarily used to reference shadowed callables in their overrides.
|
113
|
+
#
|
114
|
+
# Like Context#call, it may take a block that is yielded to with
|
115
|
+
# Context#yield. If no block is given but the current scope has a block,
|
116
|
+
# its block will be automatically forwarded.
|
117
|
+
def super(&block)
|
118
|
+
# First, we resolve the super ref.
|
119
|
+
frame, callable = @calltable.resolve_super( current_frame )
|
120
|
+
|
121
|
+
# If the ref is nil then we couldn't resolve, otherwise invoke.
|
122
|
+
if( frame.nil? )
|
123
|
+
raise NameError, "No super found for #{current_frame}", caller
|
124
|
+
else
|
125
|
+
block = block_for_frame(current_frame) if block.nil?
|
126
|
+
invoke(frame, callable, &block)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Yields to the block given to a #Context.call
|
131
|
+
def yield(*args)
|
132
|
+
block = block_for_frame(current_frame)
|
133
|
+
if( block.nil? )
|
134
|
+
raise LocalJumpError, "No block given to yield to.", caller
|
135
|
+
else
|
136
|
+
block.yield(*args)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns a copy of the current callstack.
|
141
|
+
def callstack
|
142
|
+
@callstack.dup
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns the top frame of the callstack.
|
146
|
+
def current_frame
|
147
|
+
@callstack.top
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns a true value if the given refname is defined in this context.
|
151
|
+
#
|
152
|
+
# If given a CallableRef, it returns a true value if the reference is
|
153
|
+
# resolvable.
|
154
|
+
def include?(refname)
|
155
|
+
@calltable.resolve( CallableRef.new(refname) ) != CallableTable::RESOLUTION_FAILURE
|
156
|
+
end
|
157
|
+
|
158
|
+
# Returns a true value if the given refname has been computed.
|
159
|
+
#
|
160
|
+
# If the given CallableRef, it returns a true if the reference has already
|
161
|
+
# been computed.
|
162
|
+
def completed?(refname)
|
163
|
+
callref = CallableRef.new(refname)
|
164
|
+
key = callref.dynamic? ? @calltable.resolve(callref).first : callref
|
165
|
+
@cache.include?(key)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Raises an exception if +completed?+ is false. Useful for asserting weak
|
169
|
+
# dependencies (those which you do not need the return value of) have
|
170
|
+
# been met.
|
171
|
+
def assert_completed(refname)
|
172
|
+
raise AssertError, "Assertion that #{refname} has completed failed.", caller unless completed?(refname)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Runs the passed block before calling the passed refname. Returns the
|
176
|
+
# value of the call to refname.
|
177
|
+
def before(refname)
|
178
|
+
yield if block_given?
|
179
|
+
call(refname)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Runs the passed block after calling the passed refname. Returns the
|
183
|
+
# value of the call to refname.
|
184
|
+
def after(refname)
|
185
|
+
result = call(refname)
|
186
|
+
yield if block_given?
|
187
|
+
result
|
188
|
+
end
|
189
|
+
|
190
|
+
# Duplication of Contexts are dangerous because of all the deeply
|
191
|
+
# nested structures. That being said, it is very tempting to try
|
192
|
+
# to use a well-constructed Context rather than save and reuse the
|
193
|
+
# callable maps used for instantiation.
|
194
|
+
#
|
195
|
+
# To remedy this concern, dup acts as a copy constructor, making a new
|
196
|
+
# Context instance with the same callable maps, but is otherwise
|
197
|
+
# freshly constructed.
|
198
|
+
def dup
|
199
|
+
self.class.new(*@callable_maps)
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
# Invoke is the root method for all invocation of callables.
|
205
|
+
#
|
206
|
+
# It is given the frame to put on the stack (typically just a CallableRef),
|
207
|
+
# and the callable to invoke.
|
208
|
+
#
|
209
|
+
# It is up to the callers of this method to resolve the callable that must
|
210
|
+
# be called, or give a Reduxco::Context::NameError if it cannot be found.
|
211
|
+
def invoke(frame, callable, &block)
|
212
|
+
#Push the frame onto the callstack.
|
213
|
+
@callstack.push(frame)
|
214
|
+
|
215
|
+
# Once we've added the frame to the callstack, we MUST do all work in
|
216
|
+
# a begub/ensure so that exception handling callables get a consistent
|
217
|
+
# callstack!
|
218
|
+
begin
|
219
|
+
# If the ref is already in the stack, then we have a cyclical dependency.
|
220
|
+
if( @callstack.rest.include?(frame) )
|
221
|
+
raise CyclicalError, "Cyclical dependency on #{frame.inspect} in #{@callstack.rest.top.inspect}", callstack.to_caller(caller[1])
|
222
|
+
end
|
223
|
+
|
224
|
+
# Recall from cache, or build if necessary.
|
225
|
+
unless( @cache.include?(frame) )
|
226
|
+
@block_association_cache[frame] = block
|
227
|
+
@cache[frame] = callable.respond_to?(:call) ? callable.call(self) : raise(NotCallableError, "#{frame} does not resolve to a callable.", caller[1..-1])
|
228
|
+
end
|
229
|
+
@cache[frame]
|
230
|
+
ensure
|
231
|
+
# No matter what crashes happened, we must ensure we pop the frame off the stack.
|
232
|
+
popped = @callstack.pop
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Returns the block argument given to the Context#call for the given frame,
|
237
|
+
# or nil if no block was given.
|
238
|
+
def block_for_frame(frame)
|
239
|
+
@block_association_cache[frame]
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
end
|