ntl-orchestra 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/Gemfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +539 -0
- data/Rakefile +21 -0
- data/bin/rake +16 -0
- data/lib/orchestra/conductor.rb +119 -0
- data/lib/orchestra/configuration.rb +12 -0
- data/lib/orchestra/dsl/nodes.rb +72 -0
- data/lib/orchestra/dsl/object_adapter.rb +134 -0
- data/lib/orchestra/dsl/operations.rb +108 -0
- data/lib/orchestra/errors.rb +44 -0
- data/lib/orchestra/node/output.rb +61 -0
- data/lib/orchestra/node.rb +130 -0
- data/lib/orchestra/operation.rb +49 -0
- data/lib/orchestra/performance.rb +137 -0
- data/lib/orchestra/recording.rb +83 -0
- data/lib/orchestra/run_list.rb +171 -0
- data/lib/orchestra/thread_pool.rb +163 -0
- data/lib/orchestra/util.rb +98 -0
- data/lib/orchestra/version.rb +3 -0
- data/lib/orchestra.rb +35 -0
- data/orchestra.gemspec +26 -0
- data/test/examples/fizz_buzz.rb +32 -0
- data/test/examples/invitation_service.rb +118 -0
- data/test/integration/multithreading_test.rb +38 -0
- data/test/integration/recording_telemetry_test.rb +86 -0
- data/test/integration/replayable_operation_test.rb +53 -0
- data/test/lib/console.rb +103 -0
- data/test/lib/test_runner.rb +19 -0
- data/test/support/telemetry_recorder.rb +49 -0
- data/test/test_helper.rb +16 -0
- data/test/unit/conductor_test.rb +25 -0
- data/test/unit/dsl_test.rb +122 -0
- data/test/unit/node_test.rb +122 -0
- data/test/unit/object_adapter_test.rb +100 -0
- data/test/unit/operation_test.rb +224 -0
- data/test/unit/run_list_test.rb +131 -0
- data/test/unit/thread_pool_test.rb +105 -0
- data/test/unit/util_test.rb +20 -0
- data/tmp/.keep +0 -0
- metadata +159 -0
@@ -0,0 +1,49 @@
|
|
1
|
+
module Orchestra
|
2
|
+
class Operation < Module
|
3
|
+
extend Forwardable
|
4
|
+
|
5
|
+
def_delegators :@default_run_list, :node_names, :provisions, :dependencies,
|
6
|
+
:optional_dependencies, :required_dependencies
|
7
|
+
|
8
|
+
attr :registry, :result, :nodes
|
9
|
+
|
10
|
+
def initialize args = {}
|
11
|
+
@result, @command, @nodes = Util.extract_key_args args,
|
12
|
+
:result, :command => false, :nodes => {}
|
13
|
+
@default_run_list = RunList.build nodes, result, []
|
14
|
+
end
|
15
|
+
|
16
|
+
def process output
|
17
|
+
output.select do |key, _| key = result end
|
18
|
+
end
|
19
|
+
|
20
|
+
def start_performance *args
|
21
|
+
conductor, input = extract_args args
|
22
|
+
run_list = RunList.build nodes, result, input.keys
|
23
|
+
performance = Performance.new conductor, run_list, input
|
24
|
+
yield performance if block_given?
|
25
|
+
performance.publish :operation_entered, name, input
|
26
|
+
performance
|
27
|
+
end
|
28
|
+
|
29
|
+
def perform *args, &block
|
30
|
+
performance = start_performance *args, &block
|
31
|
+
performance.perform
|
32
|
+
output = performance.extract_result result
|
33
|
+
performance.publish :operation_exited, name, output
|
34
|
+
@command ? nil : output
|
35
|
+
end
|
36
|
+
|
37
|
+
def command?
|
38
|
+
@command ? true : false
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def extract_args args
|
44
|
+
conductor = args.size > 1 ? args.shift : Conductor.new
|
45
|
+
input = args.fetch 0 do {} end
|
46
|
+
[conductor, input]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Orchestra
|
2
|
+
class Performance
|
3
|
+
include Observable
|
4
|
+
extend Forwardable
|
5
|
+
|
6
|
+
def_delegators :@run_list, :node_names, :provisions, :dependencies,
|
7
|
+
:optional_dependencies, :required_dependencies
|
8
|
+
|
9
|
+
attr :conductor, :input, :state, :registry, :run_list
|
10
|
+
|
11
|
+
def initialize conductor, run_list, input
|
12
|
+
@conductor = conductor
|
13
|
+
@input = input.dup
|
14
|
+
@run_list = run_list
|
15
|
+
@registry = conductor.build_registry self
|
16
|
+
@state = registry.merge input
|
17
|
+
end
|
18
|
+
|
19
|
+
def perform
|
20
|
+
ensure_inputs_are_present!
|
21
|
+
run_list.each do |name, node| process name, node end
|
22
|
+
rescue => error
|
23
|
+
publish :error_raised, error
|
24
|
+
raise error
|
25
|
+
end
|
26
|
+
|
27
|
+
def process name, node
|
28
|
+
input = input_for node
|
29
|
+
publish :node_entered, name, input
|
30
|
+
output = perform_node node
|
31
|
+
publish :node_exited, name, output
|
32
|
+
state.merge! output
|
33
|
+
end
|
34
|
+
|
35
|
+
def perform_node node
|
36
|
+
Movement.perform node, self
|
37
|
+
end
|
38
|
+
|
39
|
+
def ensure_inputs_are_present!
|
40
|
+
has_dep = state.method :[]
|
41
|
+
missing_input = required_dependencies.reject &has_dep
|
42
|
+
raise MissingInputError.new missing_input unless missing_input.empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
def input_for node
|
46
|
+
state.reject do |key, val|
|
47
|
+
registry[key] == val or not node.dependencies.include? key
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def extract_result result
|
52
|
+
state.fetch result
|
53
|
+
end
|
54
|
+
|
55
|
+
def publish event, *payload
|
56
|
+
changed
|
57
|
+
notify_observers event, *payload
|
58
|
+
end
|
59
|
+
|
60
|
+
def thread_pool
|
61
|
+
conductor.thread_pool
|
62
|
+
end
|
63
|
+
|
64
|
+
class Movement
|
65
|
+
def self.perform node, *args
|
66
|
+
if node.is_a? Operation
|
67
|
+
klass = EmbeddedOperation
|
68
|
+
else
|
69
|
+
klass = node.collection ? CollectionMovement : self
|
70
|
+
end
|
71
|
+
instance = klass.new node, *args
|
72
|
+
node.process instance.perform
|
73
|
+
end
|
74
|
+
|
75
|
+
attr :context, :node, :performance
|
76
|
+
|
77
|
+
def initialize node, performance
|
78
|
+
@node = node
|
79
|
+
@performance = performance
|
80
|
+
@context = build_context performance
|
81
|
+
end
|
82
|
+
|
83
|
+
def perform
|
84
|
+
context.perform
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_context performance
|
88
|
+
node.build_context performance.state
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class CollectionMovement < Movement
|
93
|
+
def perform
|
94
|
+
batch, output = prepare_collection
|
95
|
+
jobs = enqueue_jobs batch do |result, index| output[index] = result end
|
96
|
+
jobs.each &:wait
|
97
|
+
output
|
98
|
+
end
|
99
|
+
|
100
|
+
def enqueue_jobs batch, &block
|
101
|
+
batch.map.with_index do |element, index|
|
102
|
+
enqueue_job element, index, &block
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def enqueue_job element, index
|
107
|
+
performance.thread_pool.enqueue do
|
108
|
+
result = context.perform element
|
109
|
+
yield [result, index]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def prepare_collection
|
114
|
+
batch = context.fetch_collection
|
115
|
+
output = [nil] * batch.size
|
116
|
+
[batch, output]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class EmbeddedOperation < Movement
|
121
|
+
def perform
|
122
|
+
super
|
123
|
+
context.state.select do |k,_| k == node.result end
|
124
|
+
end
|
125
|
+
|
126
|
+
def build_context performance
|
127
|
+
conductor = performance.registry[:conductor]
|
128
|
+
copy_observers = conductor.method :copy_observers
|
129
|
+
node.start_performance conductor, input, ©_observers
|
130
|
+
end
|
131
|
+
|
132
|
+
def input
|
133
|
+
performance.state
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Orchestra
|
2
|
+
class Recording
|
3
|
+
attr :input, :output, :services
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@services = Hash.new do |hsh, service_name| hsh[service_name] = [] end
|
7
|
+
end
|
8
|
+
|
9
|
+
def update event_name, *args
|
10
|
+
case event_name
|
11
|
+
when :service_accessed then
|
12
|
+
service_name, recording = args
|
13
|
+
@services[service_name] << recording
|
14
|
+
when :operation_entered then
|
15
|
+
_, @input = args
|
16
|
+
when :operation_exited then
|
17
|
+
_, @output = args
|
18
|
+
else
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
:input => input,
|
25
|
+
:output => output,
|
26
|
+
:service_recordings => services,
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.replay operation, input, service_recordings
|
31
|
+
replayed_services = {}
|
32
|
+
service_recordings.each do |svc, service_recording|
|
33
|
+
replayed_services[svc] = Playback.build service_recording
|
34
|
+
end
|
35
|
+
conductor = Conductor.new replayed_services
|
36
|
+
conductor.perform operation, input
|
37
|
+
end
|
38
|
+
|
39
|
+
class Playback < BasicObject
|
40
|
+
attr :mocks
|
41
|
+
|
42
|
+
def initialize mocks
|
43
|
+
@mocks = mocks
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to? meth
|
47
|
+
mocks.has_key? meth
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.build service_recording
|
51
|
+
factory = Factory.new
|
52
|
+
factory.build service_recording
|
53
|
+
end
|
54
|
+
|
55
|
+
class Factory
|
56
|
+
attr :klass, :mocks
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
@klass = Class.new Playback
|
60
|
+
@mocks = Hash.new do |hsh, meth| hsh[meth] = {} end
|
61
|
+
end
|
62
|
+
|
63
|
+
def build service_recording
|
64
|
+
record = method :<<
|
65
|
+
service_recording.each &record
|
66
|
+
klass.new mocks
|
67
|
+
end
|
68
|
+
|
69
|
+
def << record
|
70
|
+
method = record[:method].to_sym
|
71
|
+
unless klass.instance_methods.include? method
|
72
|
+
klass.send :define_method, method do |*args| mocks[method][args] end
|
73
|
+
end
|
74
|
+
mocks[method][record[:input]] = record[:output]
|
75
|
+
end
|
76
|
+
|
77
|
+
def singleton
|
78
|
+
singleton = class << instance ; self end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module Orchestra
|
2
|
+
class RunList
|
3
|
+
def self.build nodes, result, input_names
|
4
|
+
builder = Builder.new result, input_names
|
5
|
+
builder.merge! nodes
|
6
|
+
builder.build
|
7
|
+
end
|
8
|
+
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def initialize nodes
|
12
|
+
@nodes = nodes
|
13
|
+
@nodes.freeze
|
14
|
+
freeze
|
15
|
+
end
|
16
|
+
|
17
|
+
def each &block
|
18
|
+
return to_enum :each unless block_given?
|
19
|
+
@nodes.each &block
|
20
|
+
end
|
21
|
+
|
22
|
+
def node_names
|
23
|
+
@nodes.keys
|
24
|
+
end
|
25
|
+
|
26
|
+
def dependencies
|
27
|
+
optional = collect_from_nodes :optional_dependencies
|
28
|
+
required = collect_from_nodes :required_dependencies
|
29
|
+
(optional + required).uniq
|
30
|
+
end
|
31
|
+
|
32
|
+
def optional_dependencies
|
33
|
+
collect_from_nodes :optional_dependencies
|
34
|
+
end
|
35
|
+
|
36
|
+
def provisions
|
37
|
+
collect_from_nodes :provisions
|
38
|
+
end
|
39
|
+
|
40
|
+
def required_dependencies
|
41
|
+
required_deps = collect_from_nodes :required_dependencies
|
42
|
+
required_deps - optional_dependencies - provisions
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def collect_from_nodes method_name
|
48
|
+
set = @nodes.each_with_object Set.new do |(_, node), set|
|
49
|
+
deps = node.public_send method_name
|
50
|
+
deps.each &set.method(:<<)
|
51
|
+
end
|
52
|
+
set.to_a.tap &:sort!
|
53
|
+
end
|
54
|
+
|
55
|
+
class Builder
|
56
|
+
attr :input_names, :result
|
57
|
+
|
58
|
+
def initialize result, input_names = []
|
59
|
+
@input_names = input_names
|
60
|
+
@nodes_hash = {}
|
61
|
+
@required = [result]
|
62
|
+
@result = result
|
63
|
+
freeze
|
64
|
+
end
|
65
|
+
|
66
|
+
def merge! nodes
|
67
|
+
nodes.each do |name, node|
|
68
|
+
self[name] = node
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def []= name, node
|
73
|
+
@nodes_hash[name] = node
|
74
|
+
end
|
75
|
+
|
76
|
+
def node_names
|
77
|
+
@nodes_hash.keys
|
78
|
+
end
|
79
|
+
|
80
|
+
def nodes
|
81
|
+
@nodes_hash.values
|
82
|
+
end
|
83
|
+
|
84
|
+
def build
|
85
|
+
sort!
|
86
|
+
prune!
|
87
|
+
RunList.new @nodes_hash
|
88
|
+
end
|
89
|
+
|
90
|
+
def sort!
|
91
|
+
sorter = Sorter.new @nodes_hash
|
92
|
+
sorter.sort!
|
93
|
+
end
|
94
|
+
|
95
|
+
def prune!
|
96
|
+
nodes.reverse_each.with_object [] do |node, removed|
|
97
|
+
removed.<< remove node and next unless required? node
|
98
|
+
require node
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def remove node
|
103
|
+
@nodes_hash.reject! do |_, n| n == node end
|
104
|
+
node
|
105
|
+
end
|
106
|
+
|
107
|
+
def require node
|
108
|
+
supplied_by_input = input_names.method :include?
|
109
|
+
deps = node.required_dependencies.reject &supplied_by_input
|
110
|
+
@required.concat deps
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
def required? node
|
115
|
+
required = @required.method :include?
|
116
|
+
node.provisions.any? &required
|
117
|
+
end
|
118
|
+
|
119
|
+
class Sorter
|
120
|
+
include TSort
|
121
|
+
|
122
|
+
def initialize nodes_hash
|
123
|
+
@nodes = nodes_hash
|
124
|
+
end
|
125
|
+
|
126
|
+
def sort!
|
127
|
+
build_dependency_tree
|
128
|
+
tsort.each do |name|
|
129
|
+
@nodes[name] = @nodes.delete name
|
130
|
+
end
|
131
|
+
rescue TSort::Cyclic
|
132
|
+
raise CircularDependencyError.new
|
133
|
+
end
|
134
|
+
|
135
|
+
def build_dependency_tree
|
136
|
+
@hsh = @nodes.each_with_object Hash.new do |(name, node), hsh|
|
137
|
+
hsh[name] = build_dependencies_for node
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def build_dependencies_for node
|
142
|
+
node.required_dependencies.each_with_object Set.new do |dep, set|
|
143
|
+
provider = provider_for dep
|
144
|
+
set << provider if provider
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def tsort_each_node(&block)
|
149
|
+
@hsh.each_key &block
|
150
|
+
end
|
151
|
+
|
152
|
+
def tsort_each_child(name, &block)
|
153
|
+
deps = @hsh.fetch name
|
154
|
+
deps.each &block
|
155
|
+
end
|
156
|
+
|
157
|
+
def provider_for dep
|
158
|
+
@nodes.each do |name, node|
|
159
|
+
provisions = effective_provisions_for node, dep
|
160
|
+
return name if provisions.include? dep
|
161
|
+
end
|
162
|
+
nil
|
163
|
+
end
|
164
|
+
|
165
|
+
def effective_provisions_for node, dep
|
166
|
+
node.optional_dependencies | node.provisions
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
module Orchestra
|
2
|
+
class ThreadPool
|
3
|
+
def self.build count
|
4
|
+
instance = new
|
5
|
+
instance.count = count
|
6
|
+
instance
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.default
|
10
|
+
build 1
|
11
|
+
end
|
12
|
+
|
13
|
+
attr :queue, :timeout
|
14
|
+
|
15
|
+
def initialize args = {}
|
16
|
+
@timeout, _ = Util.extract_key_args args, :timeout_ms => 1000
|
17
|
+
@threads = Set.new
|
18
|
+
@dead_pool = Set.new
|
19
|
+
@pool_lock = Mutex.new
|
20
|
+
@queue = Queue.new
|
21
|
+
@jobs = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def enqueue &work
|
25
|
+
job = Job.new work
|
26
|
+
job.add_observer self
|
27
|
+
while_locked do queue << job end
|
28
|
+
job
|
29
|
+
end
|
30
|
+
|
31
|
+
def perform &work
|
32
|
+
job = enqueue &work
|
33
|
+
job.wait
|
34
|
+
end
|
35
|
+
|
36
|
+
def count
|
37
|
+
threads.size
|
38
|
+
end
|
39
|
+
|
40
|
+
def count= new_count
|
41
|
+
while_locked do
|
42
|
+
loop do
|
43
|
+
case @threads.size <=> new_count
|
44
|
+
when 0 then return
|
45
|
+
when -1 then add_thread!
|
46
|
+
when 1 then remove_thread!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def add_thread
|
53
|
+
while_locked do add_thread! end
|
54
|
+
end
|
55
|
+
|
56
|
+
def remove_thread
|
57
|
+
while_locked do remove_thread! end
|
58
|
+
end
|
59
|
+
|
60
|
+
def shutdown
|
61
|
+
self.count = 0
|
62
|
+
end
|
63
|
+
|
64
|
+
def status
|
65
|
+
while_locked do @threads.map &:status end
|
66
|
+
end
|
67
|
+
|
68
|
+
def threads
|
69
|
+
while_locked do @threads end
|
70
|
+
end
|
71
|
+
|
72
|
+
def update event, *;
|
73
|
+
reap_dead_pool if event == :failed
|
74
|
+
end
|
75
|
+
|
76
|
+
def with_timeout &block
|
77
|
+
Timeout.timeout Rational(timeout, 1000), &block
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def add_thread!
|
83
|
+
wait_for_thread_count_to_change do
|
84
|
+
thr = Thread.new &method(:thread_loop)
|
85
|
+
@threads << thr
|
86
|
+
end
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
def reap_dead_pool
|
91
|
+
@dead_pool.each &:join
|
92
|
+
@dead_pool.clear
|
93
|
+
end
|
94
|
+
|
95
|
+
def remove_thread!
|
96
|
+
wait_for_thread_count_to_change do queue << :terminate end
|
97
|
+
sleep Rational(1, 10000) # FIXME
|
98
|
+
true
|
99
|
+
end
|
100
|
+
|
101
|
+
class Job
|
102
|
+
include Observable
|
103
|
+
|
104
|
+
attr :block, :error
|
105
|
+
|
106
|
+
def initialize block
|
107
|
+
@block = block
|
108
|
+
@output_queue = Queue.new
|
109
|
+
end
|
110
|
+
|
111
|
+
def done?
|
112
|
+
not @output_queue.empty?
|
113
|
+
end
|
114
|
+
|
115
|
+
def perform
|
116
|
+
@output_queue.push block.call
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_error error
|
120
|
+
@error = error
|
121
|
+
@output_queue.push Failed
|
122
|
+
end
|
123
|
+
|
124
|
+
def wait
|
125
|
+
result = @output_queue.pop
|
126
|
+
changed
|
127
|
+
if result == Failed
|
128
|
+
notify_observers :failed, error
|
129
|
+
raise error
|
130
|
+
else
|
131
|
+
notify_observers :finished, result
|
132
|
+
end
|
133
|
+
result
|
134
|
+
end
|
135
|
+
|
136
|
+
Failed = Module.new
|
137
|
+
end
|
138
|
+
|
139
|
+
def thread_loop
|
140
|
+
Thread.current.abort_on_exception = false
|
141
|
+
until (job = queue.pop) == :terminate
|
142
|
+
job.perform
|
143
|
+
end
|
144
|
+
rescue => error
|
145
|
+
add_thread!
|
146
|
+
job.set_error error
|
147
|
+
ensure
|
148
|
+
@threads.delete Thread.current
|
149
|
+
@dead_pool << Thread.current
|
150
|
+
end
|
151
|
+
|
152
|
+
def wait_for_thread_count_to_change
|
153
|
+
old_count = queue.num_waiting
|
154
|
+
yield
|
155
|
+
ensure
|
156
|
+
Thread.pass while queue.num_waiting == old_count
|
157
|
+
end
|
158
|
+
|
159
|
+
def while_locked &block
|
160
|
+
@pool_lock.synchronize do with_timeout &block end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Orchestra
|
2
|
+
module Util
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def extract_key_args hsh, *args
|
6
|
+
defaults, args = extract_hash args
|
7
|
+
unknown_args = hsh.keys - (args + defaults.keys)
|
8
|
+
missing_args = args - hsh.keys
|
9
|
+
unless unknown_args.empty? and missing_args.empty?
|
10
|
+
raise ArgumentError, key_arg_error(unknown_args, missing_args)
|
11
|
+
end
|
12
|
+
(args + defaults.keys).map do |arg|
|
13
|
+
hsh.fetch arg do defaults.fetch arg end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def extract_hash ary
|
18
|
+
if ary.last.is_a? Hash
|
19
|
+
hsh = ary.pop
|
20
|
+
else
|
21
|
+
hsh = {}
|
22
|
+
end
|
23
|
+
[hsh, ary]
|
24
|
+
end
|
25
|
+
|
26
|
+
def recursively_symbolize obj
|
27
|
+
case obj
|
28
|
+
when Array
|
29
|
+
obj.map &method(:recursively_symbolize)
|
30
|
+
when Hash then
|
31
|
+
obj.each_with_object Hash.new do |(k, v), out_hsh|
|
32
|
+
out_hsh[k.to_sym] = recursively_symbolize v
|
33
|
+
end
|
34
|
+
else obj
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_lazy_thunk obj
|
39
|
+
if obj.respond_to? :to_proc and not obj.is_a? Symbol
|
40
|
+
obj
|
41
|
+
else
|
42
|
+
Proc.new do obj end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_camel_case str
|
47
|
+
str = "_#{str}"
|
48
|
+
str.gsub!(%r{_[a-z]}) { |snake| snake.slice(1).upcase }
|
49
|
+
str.gsub!('/', '::')
|
50
|
+
str
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_snake_case str
|
54
|
+
str = str.gsub '::', '/'
|
55
|
+
# Convert FOOBar => FooBar
|
56
|
+
str.gsub! %r{[[:upper:]]{2,}} do |uppercase|
|
57
|
+
bit = uppercase[0]
|
58
|
+
bit << uppercase[1...-1].downcase
|
59
|
+
bit << uppercase[-1]
|
60
|
+
bit
|
61
|
+
end
|
62
|
+
# Convert FooBar => foo_bar
|
63
|
+
str.gsub! %r{[[:lower:]][[:upper:]]+[[:lower:]]} do |camel|
|
64
|
+
bit = camel[0]
|
65
|
+
bit << '_'
|
66
|
+
bit << camel[1..-1].downcase
|
67
|
+
end
|
68
|
+
str.downcase!
|
69
|
+
str
|
70
|
+
end
|
71
|
+
|
72
|
+
def demodulize str
|
73
|
+
split_namespaces(str).last
|
74
|
+
end
|
75
|
+
|
76
|
+
def deconstantize str
|
77
|
+
split_namespaces(str).first
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def split_namespaces name
|
83
|
+
name.split '::'
|
84
|
+
end
|
85
|
+
|
86
|
+
def key_arg_error unknown, missing
|
87
|
+
str = "bad arguments. "
|
88
|
+
if unknown.any?
|
89
|
+
str.concat " unknown: #{unknown.join ', '}"
|
90
|
+
str.concat "; " if missing.any?
|
91
|
+
end
|
92
|
+
if missing.any?
|
93
|
+
str.concat " missing: #{missing.join ', '}"
|
94
|
+
end
|
95
|
+
str
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|