pants 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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