wukong 3.0.1 → 4.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/.gitignore +1 -0
- data/Gemfile +1 -1
- data/README.md +253 -45
- data/bin/wu +34 -0
- data/bin/wu-source +5 -0
- data/examples/Gemfile +0 -1
- data/examples/deploy_pack/Gemfile +0 -1
- data/examples/improver/tweet_summary.rb +73 -0
- data/examples/ruby_project/Gemfile +0 -1
- data/examples/splitter.rb +94 -0
- data/examples/twitter.rb +5 -0
- data/lib/hanuman.rb +1 -1
- data/lib/hanuman/graph.rb +39 -22
- data/lib/hanuman/stage.rb +46 -13
- data/lib/hanuman/tree.rb +67 -0
- data/lib/wukong.rb +6 -1
- data/lib/wukong/dataflow.rb +19 -48
- data/lib/wukong/driver.rb +176 -65
- data/lib/wukong/{local → driver}/event_machine_driver.rb +1 -13
- data/lib/wukong/driver/wiring.rb +68 -0
- data/lib/wukong/local.rb +6 -4
- data/lib/wukong/local/runner.rb +14 -16
- data/lib/wukong/local/stdio_driver.rb +72 -12
- data/lib/wukong/processor.rb +1 -30
- data/lib/wukong/runner.rb +2 -0
- data/lib/wukong/runner/command_runner.rb +44 -0
- data/lib/wukong/source.rb +33 -0
- data/lib/wukong/source/source_driver.rb +74 -0
- data/lib/wukong/source/source_runner.rb +38 -0
- data/lib/wukong/spec_helpers/shared_examples.rb +0 -1
- data/lib/wukong/spec_helpers/unit_tests.rb +6 -5
- data/lib/wukong/spec_helpers/unit_tests/unit_test_driver.rb +4 -14
- data/lib/wukong/spec_helpers/unit_tests/unit_test_runner.rb +7 -8
- data/lib/wukong/version.rb +1 -1
- data/lib/wukong/widget/echo.rb +55 -0
- data/lib/wukong/widget/{processors.rb → extract.rb} +0 -106
- data/lib/wukong/widget/filters.rb +15 -0
- data/lib/wukong/widget/logger.rb +56 -0
- data/lib/wukong/widget/operators.rb +82 -0
- data/lib/wukong/widget/reducers.rb +2 -0
- data/lib/wukong/widget/reducers/improver.rb +71 -0
- data/lib/wukong/widget/reducers/join_xml.rb +37 -0
- data/lib/wukong/widget/serializers.rb +21 -6
- data/lib/wukong/widgets.rb +6 -3
- data/spec/hanuman/graph_spec.rb +73 -10
- data/spec/hanuman/stage_spec.rb +15 -0
- data/spec/hanuman/tree_spec.rb +119 -0
- data/spec/spec_helper.rb +13 -1
- data/spec/support/example_test_helpers.rb +0 -1
- data/spec/support/model_test_helpers.rb +1 -1
- data/spec/support/shared_context_for_graphs.rb +57 -0
- data/spec/support/shared_examples_for_builders.rb +8 -15
- data/spec/wukong/driver_spec.rb +152 -0
- data/spec/wukong/local/runner_spec.rb +1 -12
- data/spec/wukong/local/stdio_driver_spec.rb +73 -0
- data/spec/wukong/processor_spec.rb +0 -1
- data/spec/wukong/runner_spec.rb +2 -2
- data/spec/wukong/source_spec.rb +6 -0
- data/spec/wukong/widget/extract_spec.rb +101 -0
- data/spec/wukong/widget/logger_spec.rb +23 -0
- data/spec/wukong/widget/operators_spec.rb +25 -0
- data/spec/wukong/widget/reducers/join_xml_spec.rb +25 -0
- data/spec/wukong/wu-source_spec.rb +32 -0
- data/spec/wukong/wu_spec.rb +14 -0
- data/wukong.gemspec +1 -2
- metadata +45 -28
- data/lib/wukong/local/tcp_driver.rb +0 -47
- data/spec/wu/geo/geolocated_spec.rb +0 -247
- data/spec/wukong/widget/processors_spec.rb +0 -125
data/lib/wukong/local.rb
CHANGED
@@ -16,13 +16,15 @@ module Wukong
|
|
16
16
|
def self.configure settings, program
|
17
17
|
case program
|
18
18
|
when 'wu-local'
|
19
|
-
settings.define :run,
|
20
|
-
settings.define :tcp_port, description: "Consume TCP requests on the given port instead of lines over STDIN", type: Integer, flag: 't'
|
19
|
+
settings.define :run, description: "Name of the processor or dataflow to use. Defaults to basename of first argument", flag: 'r'
|
21
20
|
|
22
21
|
settings.define :from, description: "Parse input from given data format (json, tsv, &c.) before processing"
|
23
22
|
settings.define :to, description: "Convert input to given data format (json, tsv, &c.) before emitting"
|
24
|
-
|
25
|
-
|
23
|
+
settings.define :as, description: "Call Class.receive on each input (will run after --from)", type: Class
|
24
|
+
when 'wu-source'
|
25
|
+
settings.define :per_sec, description: "Number of events produced per second", type: Float
|
26
|
+
settings.define :period, description: "Number of seconds between events (overrides --per_sec)", type: Float
|
27
|
+
settings.define :batch_size, description: "Trigger a finalize across the dataflow each time this many records are processed", type: Integer
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
data/lib/wukong/local/runner.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
require_relative 'stdio_driver'
|
2
|
-
require_relative 'tcp_driver'
|
3
2
|
|
4
3
|
module Wukong
|
5
4
|
module Local
|
@@ -44,10 +43,10 @@ module Wukong
|
|
44
43
|
clever
|
45
44
|
EOF
|
46
45
|
|
47
|
-
# Returns the name of the
|
46
|
+
# Returns the name of the dataflow we're going to run.
|
48
47
|
#
|
49
48
|
# @return [String]
|
50
|
-
def
|
49
|
+
def dataflow
|
51
50
|
arg = args.first
|
52
51
|
basename = File.basename(arg.to_s, '.rb')
|
53
52
|
|
@@ -57,14 +56,15 @@ module Wukong
|
|
57
56
|
else arg
|
58
57
|
end
|
59
58
|
end
|
59
|
+
alias_method :processor, :dataflow
|
60
60
|
|
61
61
|
# Validates the chosen processor.
|
62
62
|
#
|
63
63
|
# @raise [Wukong::Error] if it finds a problem
|
64
64
|
# @return [true]
|
65
65
|
def validate
|
66
|
-
raise Error.new("Must provide a processor or dataflow to run, via either the --run option or as the first argument") if
|
67
|
-
raise Error.new("No such processor or dataflow <#{
|
66
|
+
raise Error.new("Must provide a processor or dataflow to run, via either the --run option or as the first argument") if dataflow.nil? || dataflow.empty?
|
67
|
+
raise Error.new("No such processor or dataflow <#{dataflow}>") unless registered?(dataflow)
|
68
68
|
true
|
69
69
|
end
|
70
70
|
|
@@ -72,25 +72,23 @@ module Wukong
|
|
72
72
|
# # itself.
|
73
73
|
def setup
|
74
74
|
super()
|
75
|
-
dataflow_class_for(
|
75
|
+
dataflow_class_for(dataflow).configure(settings) if registered?(dataflow)
|
76
76
|
end
|
77
77
|
|
78
|
-
#
|
79
|
-
#
|
78
|
+
# Starts up the driver with the right dataflow and settings.
|
79
|
+
#
|
80
|
+
# Starts the EventMachine reactor before starting the driver.
|
80
81
|
def run
|
81
|
-
EM.run do
|
82
|
-
driver.start(
|
82
|
+
EM.run do
|
83
|
+
driver.start(dataflow, settings)
|
83
84
|
end
|
84
85
|
end
|
85
86
|
|
86
|
-
# The
|
87
|
-
#
|
88
|
-
# Defaults to the Wukong::Local::StdioDriver, but will use the
|
89
|
-
# TcpDriver if it has a :port setting defined.
|
87
|
+
# The class used
|
90
88
|
#
|
91
|
-
# @return [
|
89
|
+
# @return [Class, #start]
|
92
90
|
def driver
|
93
|
-
|
91
|
+
StdioDriver
|
94
92
|
end
|
95
93
|
|
96
94
|
end
|
@@ -1,33 +1,74 @@
|
|
1
|
-
require_relative('event_machine_driver')
|
2
1
|
module Wukong
|
3
2
|
module Local
|
4
3
|
|
5
4
|
# A class for driving processors over the STDIN/STDOUT protocol.
|
5
|
+
#
|
6
|
+
# Relies on EventMachine's [LineAndTextProtocol](http://eventmachine.rubyforge.org/EventMachine/Protocols/LineText2.html).
|
6
7
|
class StdioDriver < EM::P::LineAndTextProtocol
|
7
|
-
include EventMachineDriver
|
8
|
-
include Processor::StdoutProcessor
|
9
|
-
include Logging
|
10
8
|
|
9
|
+
include DriverMethods
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
#
|
13
|
+
# == Startup ==
|
14
|
+
#
|
15
|
+
|
16
|
+
# Start a new StdioDriver.
|
17
|
+
#
|
18
|
+
# @param [Symbol] the name of the processor or dataflow to drive
|
19
|
+
# @param [Configliere::Param] settings the settings to use
|
11
20
|
def self.start(label, settings = {})
|
12
21
|
EM.attach($stdin, self, label, settings)
|
13
22
|
end
|
14
23
|
|
24
|
+
# :nodoc:
|
25
|
+
def initialize(label, settings)
|
26
|
+
super
|
27
|
+
construct_dataflow(label, settings)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Ensures that $stdout is synced.
|
31
|
+
def setup()
|
32
|
+
$stdout.sync
|
33
|
+
end
|
34
|
+
|
35
|
+
# Adds signal traps for SIGINT and SIGTERM to Ensure we capture
|
36
|
+
# C-c and friends, stop the EventMachine reactor, &c.
|
37
|
+
def self.add_signal_traps
|
38
|
+
Signal.trap('INT') { log.info 'Received SIGINT. Stopping.' ; EM.stop }
|
39
|
+
Signal.trap('TERM') { log.info 'Received SIGTERM. Stopping.' ; EM.stop }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Called by EventMachine framework after successfully attaching
|
43
|
+
# to $stdin.
|
44
|
+
#
|
45
|
+
# Adds signal handlers and calls the #setup_dataflow method.
|
15
46
|
def post_init
|
16
47
|
self.class.add_signal_traps
|
17
48
|
setup_dataflow
|
18
49
|
end
|
19
|
-
|
50
|
+
|
51
|
+
#
|
52
|
+
# == Reading Input ==
|
53
|
+
#
|
54
|
+
|
55
|
+
# Called by EventMachine framework after successfully reading a
|
56
|
+
# line from $stdin.
|
57
|
+
#
|
58
|
+
# @param [String] line
|
20
59
|
def receive_line line
|
21
|
-
|
60
|
+
send_through_dataflow(line)
|
22
61
|
rescue => e
|
23
62
|
error = Wukong::Error.new(e)
|
24
|
-
EM.stop
|
63
|
+
# EM.stop
|
25
64
|
|
26
|
-
# We'd to *raise* `error` here and have it be handled by
|
27
|
-
# Wukong::Runner.run but we are fighting with EventMachine.
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
65
|
+
# We'd like to *raise* `error` here and have it be handled by
|
66
|
+
# Wukong::Runner.run but we are fighting with EventMachine.run
|
67
|
+
# which executes in the middle.
|
68
|
+
#
|
69
|
+
# It seems no matter what we do, EventMachine.run will swallow
|
70
|
+
# any Exception raised here (including SystemExit) and exit
|
71
|
+
# the Ruby process with a return code of 0.
|
31
72
|
#
|
32
73
|
# Instead we just log the message that *would* have gotten
|
33
74
|
# logged by Wukong::Runner.run and leave it to EventMachine to
|
@@ -35,6 +76,25 @@ module Wukong
|
|
35
76
|
log.error(error.message)
|
36
77
|
end
|
37
78
|
|
79
|
+
#
|
80
|
+
# == Handling Output ==
|
81
|
+
#
|
82
|
+
|
83
|
+
# Writes a record to $stdout.
|
84
|
+
#
|
85
|
+
# @param [#to_s] record
|
86
|
+
def process(record)
|
87
|
+
$stdout.puts record
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# == Shutdown ==
|
92
|
+
#
|
93
|
+
|
94
|
+
# Called by EventMachine framework after EOF from $stdin.
|
95
|
+
#
|
96
|
+
# Calls #finalize_and_stop_dataflow method and stops the
|
97
|
+
# EventMachine reactor.
|
38
98
|
def unbind
|
39
99
|
finalize_and_stop_dataflow
|
40
100
|
EM.stop
|
data/lib/wukong/processor.rb
CHANGED
@@ -13,7 +13,6 @@ module Wukong
|
|
13
13
|
# local machine. You can glue processors together
|
14
14
|
class Processor < Hanuman::Stage
|
15
15
|
include Logging
|
16
|
-
include Vayacondios::Notifications
|
17
16
|
|
18
17
|
field :action, Whatever, :doc => false
|
19
18
|
|
@@ -23,32 +22,12 @@ module Wukong
|
|
23
22
|
@description = desc if desc
|
24
23
|
@description
|
25
24
|
end
|
26
|
-
|
27
|
-
def consumes(*args)
|
28
|
-
options = args.extract_options!
|
29
|
-
@consumes = options[:as]
|
30
|
-
validate_and_set_serialization(:from, args.first)
|
31
|
-
end
|
32
|
-
|
33
|
-
def produces(*args)
|
34
|
-
options = args.extract_options!
|
35
|
-
@produces = options[:as]
|
36
|
-
validate_and_set_serialization(:to, args.first)
|
37
|
-
end
|
38
25
|
|
39
|
-
def valid_serializer? label
|
40
|
-
label
|
41
|
-
end
|
42
|
-
|
43
|
-
def validate_and_set_serialization(direction, label)
|
44
|
-
instance_variable_set("@serialization_#{direction}", label) if %w[ tsv json xml ].include?(label.to_s)
|
45
|
-
end
|
46
|
-
|
47
26
|
def configure(settings)
|
48
27
|
settings.description = description if description
|
49
28
|
fields.each_pair do |name, field|
|
50
29
|
next if field.doc == false || field.doc.to_s == 'false'
|
51
|
-
next if [:log
|
30
|
+
next if [:log].include?(name)
|
52
31
|
field_props = {}.tap do |props|
|
53
32
|
props[:description] = field.doc unless field.doc == "#{name} field"
|
54
33
|
field_type = (field.type.respond_to?(:product) ? field.type.product : field.type)
|
@@ -69,14 +48,6 @@ module Wukong
|
|
69
48
|
|
70
49
|
end
|
71
50
|
|
72
|
-
def expected_record_type(type)
|
73
|
-
self.class.instance_variable_get("@#{type}")
|
74
|
-
end
|
75
|
-
|
76
|
-
def expected_serialization(direction)
|
77
|
-
self.class.instance_variable_get("@serialization_#{direction.to_s}")
|
78
|
-
end
|
79
|
-
|
80
51
|
# When instantiated with a block, the block will replace this
|
81
52
|
# method.
|
82
53
|
#
|
data/lib/wukong/runner.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require_relative("runner/code_loader")
|
2
2
|
require_relative("runner/deploy_pack_loader")
|
3
3
|
require_relative("runner/boot_sequence")
|
4
|
+
require_relative("runner/command_runner")
|
4
5
|
|
5
6
|
module Wukong
|
6
7
|
|
@@ -18,6 +19,7 @@ module Wukong
|
|
18
19
|
include CodeLoader
|
19
20
|
include DeployPackLoader
|
20
21
|
include BootSequence
|
22
|
+
include CommandRunner
|
21
23
|
|
22
24
|
# The settings object that will be configured and booted from.
|
23
25
|
# All plugins will configure this object.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Wukong
|
2
|
+
class Runner
|
3
|
+
|
4
|
+
# Provides methods for executing commandlines.
|
5
|
+
module CommandRunner
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
# Execute a command composed of the given parts.
|
10
|
+
#
|
11
|
+
# Will print the command instead if the <tt>--dry_run</tt>
|
12
|
+
# option was given.
|
13
|
+
#
|
14
|
+
# Will *not* raise an error if the command fails.
|
15
|
+
#
|
16
|
+
# @param [Array<String>] argv
|
17
|
+
def execute_command(*argv)
|
18
|
+
command = argv.flatten.reject(&:blank?).join(" \\\n ")
|
19
|
+
if settings[:dry_run]
|
20
|
+
log.info("Dry run:")
|
21
|
+
puts command
|
22
|
+
else
|
23
|
+
output = `#{command}`
|
24
|
+
puts output unless output.empty?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Execute a command composed of the given parts.
|
29
|
+
#
|
30
|
+
# Will print the command instead if the <tt>--dry_run</tt>
|
31
|
+
# option was given.
|
32
|
+
#
|
33
|
+
# *Will* raise an error if the command fails.
|
34
|
+
#
|
35
|
+
# @param [Array<String>] argv
|
36
|
+
def execute_command!(*argv)
|
37
|
+
execute_command(argv)
|
38
|
+
raise Error.new("Command failed!") unless $?.success?
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Wukong
|
2
|
+
|
3
|
+
# Provides a runner for periodically triggering a dataflow or
|
4
|
+
# processor.
|
5
|
+
module Source
|
6
|
+
include Plugin
|
7
|
+
|
8
|
+
# Configures the given +settings+ object with all settings
|
9
|
+
# specific to Wukong::Source for the given program +name+.
|
10
|
+
#
|
11
|
+
# @param [Configliere::Param] settings the settings to configure
|
12
|
+
# @param [String] program the name of the currently executing program
|
13
|
+
def self.configure settings, program
|
14
|
+
case program
|
15
|
+
when 'wu-source'
|
16
|
+
settings.define :per_sec, description: "Number of events produced per second", type: Float
|
17
|
+
settings.define :period, description: "Number of seconds between events (overrides --per_sec)", type: Float
|
18
|
+
settings.define :batch_size, description: "Trigger a finalize across the dataflow each time this many records are processed", type: Integer
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Boots Wukong::Source using the given +settings+ at the given
|
23
|
+
# +root.
|
24
|
+
#
|
25
|
+
# @param [Configliere::Param] settings the settings to use to boot
|
26
|
+
# @param [String] root the root directory to boot in
|
27
|
+
def self.boot(settings, root)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require_relative('source/source_runner')
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Wukong
|
2
|
+
module Source
|
3
|
+
|
4
|
+
# A driver which works just like the `Wukong::Local::StdioDriver`
|
5
|
+
# except it ignores input from `STDIN` and instead generates its
|
6
|
+
# own input records according to some periodic schedule. Each
|
7
|
+
# consecutive record produced will be an incrementing positive
|
8
|
+
# integer (as a string), starting with '1'.
|
9
|
+
class SourceDriver < Wukong::Local::StdioDriver
|
10
|
+
|
11
|
+
include Logging
|
12
|
+
|
13
|
+
# The index of the record.
|
14
|
+
attr_accessor :index
|
15
|
+
|
16
|
+
# The number of records after which a `Processor#finalize` will
|
17
|
+
# be called.
|
18
|
+
attr_accessor :batch_size
|
19
|
+
|
20
|
+
# Sets the initial value of `index` to 1 and sets the batch size
|
21
|
+
# (only if it's positive).
|
22
|
+
def post_init
|
23
|
+
super()
|
24
|
+
self.index = 1
|
25
|
+
self.batch_size = settings[:batch_size].to_i if settings[:batch_size] && settings[:batch_size].to_i > 0
|
26
|
+
end
|
27
|
+
|
28
|
+
# Starts periodically feeding the processor or dataflow given by
|
29
|
+
# `label` using the given `settings`.
|
30
|
+
#
|
31
|
+
# @param [String, Symbol] label
|
32
|
+
# @param [Configliere::Param, Hash] settings
|
33
|
+
def self.start(label, settings={})
|
34
|
+
driver = new(:foobar, label, settings) # i don't think the 1st argument matters here...
|
35
|
+
driver.post_init
|
36
|
+
|
37
|
+
period = case
|
38
|
+
when settings[:period] then settings[:period]
|
39
|
+
when settings[:per_sec] then (1.0 / settings[:per_sec]) rescue 1.0
|
40
|
+
else 1.0
|
41
|
+
end
|
42
|
+
driver.create_event
|
43
|
+
EventMachine::PeriodicTimer.new(period) { driver.create_event }
|
44
|
+
end
|
45
|
+
|
46
|
+
# Creates a new event using the following steps:
|
47
|
+
#
|
48
|
+
# 1. Feeds a record with the existing `index` to the dataflow.
|
49
|
+
# 2. Increments the `index`.
|
50
|
+
# 3. Finalizes the dataflow if the number of records is a
|
51
|
+
# multiple of the `batch_size`.
|
52
|
+
#
|
53
|
+
# @see DriverMethods
|
54
|
+
def create_event
|
55
|
+
receive_line(index.to_s)
|
56
|
+
self.index += 1
|
57
|
+
finalize_dataflow if self.batch_size && (self.index % self.batch_size) == 0
|
58
|
+
end
|
59
|
+
|
60
|
+
# Outputs a `record` from the dataflow or processor to `STDOUT`.
|
61
|
+
#
|
62
|
+
# `STDOUT` will automatically be flushed to force output to
|
63
|
+
# prevent the feeling of "no output" when the looping period is
|
64
|
+
# long.
|
65
|
+
#
|
66
|
+
# @param [Object] record the record yielded by the processor or the terminal node(s) of the dataflow
|
67
|
+
def process record
|
68
|
+
$stdout.puts record
|
69
|
+
$stdout.flush
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require_relative('source_driver')
|
2
|
+
module Wukong
|
3
|
+
module Source
|
4
|
+
|
5
|
+
# Implements the `wu-source` command.
|
6
|
+
class SourceRunner < Wukong::Local::LocalRunner
|
7
|
+
|
8
|
+
usage "PROCESSOR|DATAFLOW"
|
9
|
+
|
10
|
+
description <<-EOF.gsub(/^ {8}/,'')
|
11
|
+
wu-source is a tool for using Wukong processors as sources of
|
12
|
+
data in streams.
|
13
|
+
|
14
|
+
Run any Wukong processor as a source for data:
|
15
|
+
|
16
|
+
$ wu-source fake_log_data
|
17
|
+
205.4.75.208 - 3918471017 [27/Nov/2012:05:06:57 -0600] "GET /products/eget HTTP/1.0" 200 25600
|
18
|
+
63.181.105.15 - 3650805763 [27/Nov/2012:05:06:57 -0600] "GET /products/lacinia-nulla-vitae HTTP/1.0" 200 3790
|
19
|
+
227.190.78.101 - 39543891 [27/Nov/2012:05:06:58 -0600] "GET /products/odio-nulla-nulla-ipsum HTTP/1.0" 200 31718
|
20
|
+
...
|
21
|
+
|
22
|
+
The fake_log_data processor will receive an event once every
|
23
|
+
second. Each event will consist of a single string giving a
|
24
|
+
consecutive integer starting with '1' as the first event.
|
25
|
+
EOF
|
26
|
+
|
27
|
+
include Logging
|
28
|
+
|
29
|
+
# The driver class used by `wu-source`.
|
30
|
+
#
|
31
|
+
# @return [Class]
|
32
|
+
def driver
|
33
|
+
SourceDriver
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|