pants 0.1.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.
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ require 'thor'
5
+ require_relative '../lib/pants'
6
+ require_relative '../lib/pants/version'
7
+
8
+
9
+ class PantsRunner < Thor
10
+ desc "dup [URI]", "read on the URI and send to --destinations"
11
+ long_desc <<-LONGDESC
12
+ This will read in any data given by the URI and redirect it to all of the I/O\x5
13
+ objects given by --destinations. Specify all I/O objects as URIs in the form:\x5
14
+
15
+ udp://127.0.0.1:1234, file:///home/bob/BarrellRoll.avi
16
+
17
+ ...although files can be given without using the URI form.
18
+
19
+ Separate multiple destinations by a space:
20
+
21
+ $ pants udp://127.0.0.1:1234 --dest=udp://239.0.0.1:1234 udp://10.0.0.1:1234
22
+
23
+ LONGDESC
24
+ option :dest, type: :array, required: true
25
+ option :verbose, type: :boolean
26
+ def dup(read_uri)
27
+ Pants.log = options[:verbose] ? true : false
28
+
29
+ add_writers = lambda do |reader|
30
+ options[:dest].each do |write_uri|
31
+ reader.write_to(write_uri)
32
+ end
33
+ end
34
+
35
+ Pants.read(read_uri, &add_writers)
36
+ end
37
+
38
+ desc "version", "print the version of pants"
39
+ def version
40
+ puts <<-INFO
41
+ ---------------------------
42
+ | || O || | # => pants v#{Pants::VERSION}
43
+ --------------------------- # => https://github.com/turboladen/pants
44
+ | | | | # => #{RUBY_DESCRIPTION}
45
+ | | | |
46
+ | |_| |
47
+ | / \\ |
48
+ | | | |
49
+ | | | |
50
+ | | | |
51
+ | | | |
52
+ |__________| |__________|
53
+
54
+ INFO
55
+ end
56
+ end
57
+
58
+
59
+ PantsRunner.start
@@ -0,0 +1,84 @@
1
+ require_relative 'pants/core'
2
+ require_relative 'pants/version'
3
+ Dir[File.dirname(__FILE__) + "/readers/*.rb"].each { |f| require f }
4
+ Dir[File.dirname(__FILE__) + "/writers/*.rb"].each { |f| require f }
5
+ require_relative 'pants/seam'
6
+
7
+
8
+ # This base class provides some helpers for doing quick, non-complex reading
9
+ # and writing. Check docs on readers and writers for more information.
10
+ class Pants
11
+
12
+ DEFAULT_URI_TO_READER_MAP = [
13
+ { uri_scheme: nil, klass: Pants::Readers::FileReader, args: [:path] },
14
+ { uri_scheme: 'file', klass: Pants::Readers::FileReader, args: [:path] },
15
+ { uri_scheme: 'udp', klass: Pants::Readers::UDPReader, args: [:host, :port] }
16
+ ]
17
+
18
+ DEFAULT_URI_TO_WRITER_MAP = [
19
+ { uri_scheme: nil, klass: Pants::Writers::FileWriter, args: [:path] },
20
+ { uri_scheme: 'file', klass: Pants::Writers::FileWriter, args: [:path] },
21
+ { uri_scheme: 'udp', klass: Pants::Writers::UDPWriter, args: [:host, :port] }
22
+ ]
23
+
24
+ # The list of mappings of URIs to Reader class types. These mappings allow
25
+ # Pants to look up the URI scheme and find what type of object should be
26
+ # created when creating a Reader by giving it a URI. It also defines the
27
+ # arguments that the Reader class takes; these should Symbols that represent
28
+ # names of methods that can be called on objects of the URI type.
29
+ #
30
+ # You can register new mappings here by pushing new mappings to the list.
31
+ # Mappings should be in the form:
32
+ # { uri_scheme: 'my_scheme', klass: MyReaderClass, args: [:arg] }
33
+ #
34
+ # Note that if you're wanting to add to this list, and URI doesn't recognize
35
+ # the URI scheme that you're adding for, you'll need to define that within
36
+ # URI. An example is given here: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html
37
+ #
38
+ # If you want to use your own reader but don't want to go through all of this
39
+ # hassle, you can add your reader using a different method. See the docs for
40
+ # Pants::Core for more info.
41
+ #
42
+ # @return [Array<Hash>] The list of mappings.
43
+ def self.readers
44
+ @readers ||= DEFAULT_URI_TO_READER_MAP
45
+ end
46
+
47
+ # The list of mappings of URIs to Writer class types. See the docs for
48
+ # .readers for more info.
49
+ #
50
+ # @return [Array<Hash>] The list of mappings.
51
+ # @see Pants.readers
52
+ def self.writers
53
+ @writers ||= DEFAULT_URI_TO_WRITER_MAP
54
+ end
55
+
56
+ # Convenience method; doing something like:
57
+ #
58
+ # pants = Pants::Core.new
59
+ # reader = pants.read('udp://0.0.0.0:1234')
60
+ # reader.add_writer('udp://1.2.3.4:5999')
61
+ # reader.add_writer('udp_data.raw')
62
+ # pants.run
63
+ #
64
+ # ...becomes:
65
+ #
66
+ # Pants.read('udp://0.0.0.1234') do |seam|
67
+ # seam.add_writer('udp://1.2.3.4:5999')
68
+ # seam.add_writer('udp_data.raw')
69
+ # end
70
+ #
71
+ # @param [String] uri Takes a URI ('udp://...' or 'file://...') or the path
72
+ # to a file.
73
+ def self.read(uri, &block)
74
+ pants = Pants::Core.new(&block)
75
+
76
+ if uri.kind_of? Pants::Readers::BaseReader
77
+ pants.add_reader(uri)
78
+ else
79
+ pants.read(uri)
80
+ end
81
+
82
+ pants.run
83
+ end
84
+ end
@@ -0,0 +1,216 @@
1
+ require 'uri'
2
+ require 'eventmachine'
3
+ require_relative 'error'
4
+ require_relative 'logger'
5
+
6
+ Dir[File.dirname(__FILE__) + "/readers/*.rb"].each { |f| require f }
7
+ Dir[File.dirname(__FILE__) + "/writers/*.rb"].each { |f| require f }
8
+ require_relative 'seam'
9
+
10
+
11
+ class Pants
12
+
13
+ # A single Core object is necessary for Pants to run. Root-level readers are
14
+ # attached to the core, then writers are attached to those readers. It's
15
+ # main job, other than giving a home to readers, is to handle the order of
16
+ # starting up and shutting of the readers and writers so that no data is lost.
17
+ class Core
18
+ include LogSwitch::Mixin
19
+
20
+ # @return [Array] The list of readers that are reading data.
21
+ attr_reader :readers
22
+
23
+ def initialize(&block)
24
+ setup_signals
25
+ @readers = []
26
+
27
+ @convenience_block = block
28
+ end
29
+
30
+ # One method of adding a Reader to the Core. Use this method to make code
31
+ # reader nicer when reading something that's expressed as a URI.
32
+ #
33
+ # @example
34
+ # core = Pants::Core.new
35
+ # core.read 'udp://10.2.3.4:9000'
36
+ #
37
+ # @param [String,URI] uri The URI to the object to read. Can be a file:///,
38
+ # udp://, or a string with the path to a file.
39
+ #
40
+ # @return [Pants::Reader] The newly created reader.
41
+ def read(uri)
42
+ begin
43
+ uri = uri.is_a?(URI) ? uri : URI(uri)
44
+ rescue URI::InvalidURIError
45
+ @readers << new_reader_from_uri(nil, callback)
46
+ else
47
+ @readers << new_reader_from_uri(uri, callback)
48
+ end
49
+
50
+ @convenience_block.call(@readers.last) if @convenience_block
51
+
52
+ @readers.last
53
+ end
54
+
55
+ # One method of adding a Reader to the Core. Use this method to add an
56
+ # a) already instantiated Reader object, or b) a Reader from a class of
57
+ # Reader objects.
58
+ #
59
+ # @example Add using class and init variables
60
+ # core = Pants::Core.new
61
+ # core.add_reader(Pants::Readers::UDPReader, '10.2.3.4', 9000)
62
+ #
63
+ # @example Add using an already instantiated Reader object
64
+ # core = Pants::Core.new
65
+ # reader = Pants::Readers::UDPReader.new('10.2.3.4', 9000, core.callback)
66
+ # core.add_reader(reader)
67
+ #
68
+ # Notice how using the last method requires you to pass in the core's
69
+ # callback method--this is probably one reason for avoiding this method of
70
+ # adding a reader, yet remains available for flexibility.
71
+ #
72
+ # @param [Class,Pants::Reader] obj Either the class of a Reader to create,
73
+ # or an already created Reader object.
74
+ # @param [*] args Any arguments that need to be used for creating the
75
+ # Reader.
76
+ def add_reader(obj, *args)
77
+ if obj.is_a? Class
78
+ @readers << obj.new(*args, callback)
79
+ elsif obj.kind_of? Pants::Readers::BaseReader
80
+ @readers << obj
81
+ else
82
+ raise Pants::Error, "Don't know how to add a reader of type #{obj}"
83
+ end
84
+
85
+ @convenience_block.call(@readers.last) if @convenience_block
86
+
87
+ @readers.last
88
+ end
89
+
90
+ # Creates an EventMachine::Callback method that other Readers, Writers, and
91
+ # others can use for letting the Core know when it can shutdown. Those
92
+ # Readers, Writers, etc. should handle calling this callback when they're
93
+ # done doing what they need to do.
94
+ #
95
+ # @return [EventMachine::Callback]
96
+ def callback
97
+ EM.Callback do
98
+ if @readers.none?(&:running?)
99
+ EM.stop_event_loop
100
+ end
101
+ end
102
+ end
103
+
104
+ # Starts the EventMachine reactor, the reader and the writers.
105
+ def run
106
+ raise Pants::Error, "No readers added yet" if @readers.empty?
107
+
108
+ starter = proc do
109
+ puts "Pants v#{Pants::VERSION}"
110
+ puts ">> Reading from #{@readers.size} readers"
111
+
112
+ @readers.each_with_index do |reader, i|
113
+ puts ">> reader#{i}: Starting read on: #{reader.read_object}"
114
+ puts ">> reader#{i}: Writing to #{reader.writers.size} writers"
115
+
116
+ reader.writers.each_with_index do |writer, j|
117
+ puts ">> reader#{i}writer#{j}: #{writer.write_object}"
118
+ end
119
+ end
120
+
121
+ EM::Iterator.new(@readers).each do |reader, iter|
122
+ reader.start
123
+ iter.next
124
+ end
125
+ end
126
+
127
+ if EM.reactor_running?
128
+ log "Joining reactor..."
129
+ starter.call
130
+ else
131
+ log "Starting reactor..."
132
+ EM.run(&starter)
133
+ end
134
+ end
135
+
136
+ # Tells the reader to signal to its writers that it's time to finish.
137
+ def stop!
138
+ puts "Stop called. Closing readers and writers..."
139
+
140
+ if @readers.none?(&:running?)
141
+ puts "No readers are running; nothing to do."
142
+ else
143
+ puts "Stopping readers:"
144
+
145
+ @readers.each do |reader|
146
+ puts "\t#{reader}" if reader.running?
147
+ end
148
+
149
+ @readers.each(&:stop!)
150
+ end
151
+ end
152
+
153
+ # Stop, then run.
154
+ def restart
155
+ stop!
156
+ puts "Restarting..."
157
+ run
158
+ end
159
+
160
+ private
161
+
162
+ # Register signals:
163
+ # * TERM & QUIT calls +stop+ to shutdown gracefully.
164
+ # * INT calls <tt>stop!</tt> to force shutdown.
165
+ # * HUP calls <tt>restart</tt> to ... surprise, restart!
166
+ def setup_signals
167
+ @trapped_count ||= 0
168
+
169
+ stopper = proc do
170
+ @trapped_count += 1
171
+ stop!
172
+
173
+ # Reset count after 5 seconds
174
+ EM.add_timer(5) { @trapped_count = 0 }
175
+ end
176
+
177
+ trap('INT') do
178
+ stopper.call
179
+ abort "Multiple INT signals trapped; aborting!" if @trapped_count > 1
180
+ end
181
+
182
+ trap('TERM') { stopper.call }
183
+
184
+ unless !!RUBY_PLATFORM =~ /mswin|mingw/
185
+ trap('QUIT') { stop! }
186
+ trap('HUP') { restart }
187
+ end
188
+ end
189
+
190
+ # @param [URI] uri The URI the Reader is mapped to.
191
+ #
192
+ # @return [Pants::Reader] An object of the type that's defined by the URI
193
+ # scheme.
194
+ #
195
+ # @raise [Pants::Error] If no Reader is mapped to +scheme+.
196
+ def new_reader_from_uri(uri, callback)
197
+ reader_to_use = if uri.nil?
198
+ Pants.readers.find { |reader| reader[:uri_scheme].nil? }
199
+ else
200
+ Pants.readers.find { |reader| reader[:uri_scheme] == uri.scheme }
201
+ end
202
+
203
+ unless reader_to_use
204
+ raise ArgumentError, "No reader found with URI scheme: #{uri.scheme}"
205
+ end
206
+
207
+ args = if reader_to_use[:args]
208
+ reader_to_use[:args].map { |arg| uri.send(arg) }
209
+ else
210
+ []
211
+ end
212
+
213
+ reader_to_use[:klass].new(*args, callback)
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,5 @@
1
+ class Pants
2
+ class Error < StandardError
3
+
4
+ end
5
+ end
@@ -0,0 +1,8 @@
1
+ require 'log_switch'
2
+
3
+
4
+ class Pants
5
+ extend LogSwitch
6
+ end
7
+
8
+ Pants.log_class_name = true
@@ -0,0 +1,25 @@
1
+ require 'ipaddr'
2
+ require 'socket'
3
+
4
+
5
+ class Pants
6
+
7
+ # Methods that are shared by Readers and Writers that deal with networking.
8
+ module NetworkHelpers
9
+
10
+ private
11
+
12
+ # Sets Socket options to allow for multicasting.
13
+ #
14
+ # @param [String] ip The IP address to add to the multicast group.
15
+ def setup_multicast_socket(ip)
16
+ set_membership(::IPAddr.new(ip).hton + ::IPAddr.new('0.0.0.0').hton)
17
+ end
18
+
19
+ # @param [String] membership The network byte ordered String that represents
20
+ # the IP(s) that should join the membership group.
21
+ def set_membership(membership)
22
+ set_sock_opt(Socket::IPPROTO_IP, Socket::IP_ADD_MEMBERSHIP, membership)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,302 @@
1
+ require_relative '../logger'
2
+
3
+
4
+ class Pants
5
+ module Readers
6
+ class BaseReader
7
+ include LogSwitch::Mixin
8
+
9
+ # @return [Array] The list of Writers attached to the Reader.
10
+ attr_reader :writers
11
+
12
+ # @return [EventMachine::Channel] The channel that Writers should subscribe
13
+ # to.
14
+ attr_reader :write_to_channel
15
+
16
+ # @return [EventMachine::Callback] The callback from Core that should be
17
+ # called when the Reader is done reading.
18
+ attr_reader :core_stopper_callback
19
+
20
+ # @param [EventMachine::Callback] core_stopper_callback This gets called when all
21
+ # reading is done and the writers have written out all their data. It
22
+ # signals to the caller that the job of the reader is all done. For
23
+ # first level readers (readers that are not Tees), this lets Pants check
24
+ # all existing Readers to see if they're done, so it can know when to stop
25
+ # the reactor.
26
+ #
27
+ def initialize(core_stopper_callback)
28
+ @writers = []
29
+ @write_to_channel = EM::Channel.new
30
+ @core_stopper_callback = core_stopper_callback
31
+ @read_object ||= nil
32
+ @starter = nil
33
+ @stopper = nil
34
+ @running = false
35
+ end
36
+
37
+ # Starts all of the writers, then starts the reader. Child readers must
38
+ # call this to make sure the writers are all running and ready for data
39
+ # before the reader starts pushing data onto its Channel.
40
+ #
41
+ # @param [EventMachine::Callback] callback Once all writers are up and
42
+ # running, this gets called, letting the caller know all Writers are up
43
+ # and running. This should contain all code that the child Reader wants
44
+ # to execute on start.
45
+ def start(callback)
46
+ start_loop = EM.tick_loop do
47
+ if @writers.empty? || @writers.all?(&:running?)
48
+ :stop
49
+ end
50
+ end
51
+ start_loop.on_stop { callback.call }
52
+
53
+ log "Starting writers for reader #{self.__id__}..."
54
+ EM::Iterator.new(@writers).each do |writer, iter|
55
+ writer.start
56
+ iter.next
57
+ end
58
+ end
59
+
60
+ # Calls the reader's #stopper, thus forcing the reader to shutdown. For
61
+ # readers that intend to read a finite amount of data, the Reader should
62
+ # call the #stopper when it's done; for readers that read a non-stop stream
63
+ # (i.e. like an open socket), this gets called by OS signals (i.e. if you
64
+ # ctrl-c).
65
+ def stop!
66
+ stopper.call
67
+ end
68
+
69
+ # Allows for adding "about me" info, depending on the reader type. This
70
+ # info is printed out when Pants starts, so you know get confirmation of
71
+ # what you're about to do. If you don't define this in your reader, nothing
72
+ # will be printed out.
73
+ #
74
+ # @return [String] A String that identifies what the reader is reading
75
+ # from.
76
+ def read_object
77
+ if @read_object
78
+ @read_object
79
+ else
80
+ warn "No read_object info has been defined for this reader."
81
+ end
82
+ end
83
+
84
+ # @return [Boolean]
85
+ def running?
86
+ @running
87
+ end
88
+
89
+ # @param [String] uri The URI to the object to read. Can be of URI type
90
+ # that's defined in Pants.writers.
91
+ #
92
+ # @return [Pants::Writers::BaseWriter] The newly created writer.
93
+ def write_to(uri)
94
+ begin
95
+ uri = uri.is_a?(URI) ? uri : URI(uri)
96
+ rescue URI::InvalidURIError
97
+ @writers << new_writer_from_uri(nil, @write_to_channel)
98
+ else
99
+ @writers << new_writer_from_uri(uri, @write_to_channel)
100
+ end
101
+
102
+ @writers.last
103
+ end
104
+
105
+ # One method of adding a Writer to the Reader. Use this method to add an
106
+ # a) already instantiated Writer object, or b) a Writers from a class of
107
+ # Writer objects.
108
+ #
109
+ # @example Add using class and init variables
110
+ # core = Pants::Core.new
111
+ # core.read 'udp://10.2.3.4:9000'
112
+ # core.add_writer(Pants::Writers::UDPWriter, '10.5.6.7', 9000)
113
+ #
114
+ # @example Add using an already instantiated Writer object
115
+ # core = Pants::Core.new
116
+ # reader = core.read 'udp://10.2.3.4:9000'
117
+ # writer = Pants::Writers::UDPWriter.new('10.5.6.7', 9000, reader.write_to_channel)
118
+ # core.add_writer(writer)
119
+ #
120
+ # Notice how using the last method requires you to pass in the channel
121
+ # that the reader is pushing data to--this is probably one reason for
122
+ # avoiding this method of adding a writer, yet remains available for
123
+ # flexibility.
124
+ #
125
+ # @param [Class,Pants::Reader] obj Either the class of a Writer to create,
126
+ # or an already created Writer object.
127
+ #
128
+ # @param [*] args Any arguments that need to be used for creating the
129
+ # Writer.
130
+ def add_writer(obj, *args)
131
+ if obj.is_a? Class
132
+ @writers << obj.new(*args, @write_to_channel)
133
+ elsif obj.kind_of? Pants::Writers::BaseWriter
134
+ @writers << obj
135
+ else
136
+ raise Pants::Error, "Don't know how to add a writer of type #{obj}"
137
+ end
138
+
139
+ @writers.last
140
+ end
141
+
142
+ # Removes a writer object from the internal list of writers.
143
+ #
144
+ # @example Using URI
145
+ # reader.writers # => [<Pants::Writers::FileWriter @file_path='./testfile'...>]
146
+ # reader.remove_writer('./testfile')
147
+ # reader.writers # => []
148
+ #
149
+ # @example Using class and args as key/value pairs
150
+ # reader.writers # => [<Pants::Writers::FileWriter @file_path='./testfile'...>]
151
+ # reader.remove_writer(Pants::Writers::FileWriter, file_path: './testfile')
152
+ # reader.writers # => []
153
+ #
154
+ # @param [Class] obj Class of the writer to remove.
155
+ #
156
+ # @param [Hash] key_value_pairs Keys are methods to be called on each
157
+ # writer and will be checked to see if the return value from that method
158
+ # equals the given value.
159
+ def remove_writer(obj, key_value_pairs=nil)
160
+ if obj.is_a? Class
161
+ @writers.delete_if do |writer|
162
+ writer.is_a?(obj) &&
163
+ key_value_pairs.all? { |k, v| writer.send(k) == v }
164
+ end
165
+ elsif obj.is_a? String
166
+ writer = begin
167
+ uri = obj.is_a?(URI) ? obj : URI(obj)
168
+ rescue URI::InvalidURIError
169
+ find_writer_from_uri(nil)
170
+ else
171
+ find_writer_from_uri(uri)
172
+ end
173
+
174
+ unless writer
175
+ raise ArgumentError, "No writer found wth URI scheme: #{uri.scheme}"
176
+ end
177
+
178
+ key_value_pairs = if writer[:args]
179
+ writer[:args].inject({}) do |result, arg|
180
+ result[arg] = uri.send(arg)
181
+
182
+ result
183
+ end
184
+ else
185
+ {}
186
+ end
187
+
188
+ @writers.delete_if do |w|
189
+ w.is_a?(writer[:klass]) &&
190
+ key_value_pairs.all? { |k, v| w.send(k) == v }
191
+ end
192
+ end
193
+ end
194
+
195
+ # Allows for adding a Pants::Seam (or child) object to the reader's list
196
+ # of internal writers. For more info on Seams, see the docs for
197
+ # Pants::Seam.
198
+ #
199
+ # @param [Pants::Seam] klass The class of the Pants::Seam object to
200
+ # create.
201
+ #
202
+ # @return [Pants::Seam] The seam that was just created.
203
+ #
204
+ # @see Pants::Seam
205
+ def add_seam(klass, *args)
206
+ @writers << klass.new(@core_stopper_callback, @write_to_channel, *args)
207
+
208
+ @writers.last
209
+ end
210
+
211
+ #---------------------------------------------------------------------------
212
+ # Protecteds
213
+ #---------------------------------------------------------------------------
214
+ protected
215
+
216
+ # The block to be called when starting up. Writers should all have been
217
+ # added before calling this; if writers are started after this, they won't
218
+ # get the first bytes that are read (due to start-up time).
219
+ #
220
+ # This is used internally by child Readers to signal that they're up and
221
+ # running. If implementing your own Reader, make sure to call this.
222
+ def starter
223
+ @starter ||= EM.Callback { @running = true }
224
+ end
225
+
226
+ # The callback that gets called when the Reader is done reading. Tells all
227
+ # of the associated writers to finish up.
228
+ #
229
+ # @return [EventMachine::Callback] The Callback that should get
230
+ # called by any Reader when it's done reading.
231
+ def stopper
232
+ return @stopper if @stopper
233
+
234
+ @stopper = EM.Callback do
235
+ log "Got called back after finished reading. Starting shutdown..."
236
+
237
+ # remove this next_tick?
238
+ EM.next_tick do
239
+ start_loop = EM.tick_loop do
240
+ if @writers.empty? || @writers.none?(&:running?)
241
+ :stop
242
+ end
243
+ end
244
+
245
+ start_loop.on_stop do
246
+ @running = false
247
+ puts ">> All done reading on '#{@read_object}'."
248
+ @core_stopper_callback.call
249
+ end
250
+
251
+ log "Stopping writers for reader #{self.__id__}"
252
+ EM::Iterator.new(@writers).each do |writer, iter|
253
+ writer.stop
254
+ iter.next
255
+ end
256
+ end
257
+ end
258
+
259
+ @stopper
260
+ end
261
+
262
+ #---------------------------------------------------------------------------
263
+ # Privates
264
+ #---------------------------------------------------------------------------
265
+ private
266
+
267
+ # Creates a Writer based on the mapping defined in Pants.writers.
268
+ #
269
+ # @param [URI] uri The URI that defines the Writer.
270
+ # @param [EventMachine::Channel] read_from_channel The channel that the
271
+ # Writer will read from.
272
+ # @return [Pants::Writer] The newly created Writer.
273
+ # @raise [ArgumentError] If Pants.writers doesn't contain a mapping for the
274
+ # URI to a Writer class.
275
+ def new_writer_from_uri(uri, read_from_channel)
276
+ writer_to_use = find_writer_from_uri(uri)
277
+
278
+ unless writer_to_use
279
+ raise ArgumentError, "No writer found wth URI scheme: #{uri.scheme}"
280
+ end
281
+
282
+ args = if writer_to_use[:args]
283
+ writer_to_use[:args].map { |arg| uri.send(arg) }
284
+ else
285
+ []
286
+ end
287
+
288
+ writer_to_use[:klass].new(*args, read_from_channel)
289
+ end
290
+
291
+ # @param [URI] uri The URI that defines the Writer.
292
+ # @return [Hash] The Hash from Pants.writers that matches the URI.
293
+ def find_writer_from_uri(uri)
294
+ if uri.nil?
295
+ Pants.writers.find { |writer| writer[:uri_scheme].nil? }
296
+ else
297
+ Pants.writers.find { |writer| writer[:uri_scheme] == uri.scheme }
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end