ntl-orchestra 0.9.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.
- 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
|