tobacco 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ spec/test*
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm --create use 1.9.3@tobacco
data/.travis.yml ADDED
@@ -0,0 +1,2 @@
1
+ rvm:
2
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tobacco.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Craig Williams
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,319 @@
1
+ # Tobacco [![Build Status](https://secure.travis-ci.org/CraigWilliams/Tobacco.png)](http://travis-ci.org/CraigWilliams/Tobacco)
2
+
3
+
4
+ Tobacco is a convenience wrapper around fetching content from a url or using the content supplied to it, verifying that content was received, creating a directory structure where the file will live and finally writing the content to that file.
5
+
6
+ This procedure is mostly simple url reading and making directories and writing to a file. We deal with a system where many files are being written to a specific parent directory and urls are formed using a pre-determined host and structure. The implementation details are consistent and to avoid duplication in our code, we extract the things that don't change from the things that do.
7
+
8
+ ## Example
9
+
10
+ At Factory Code Labs, we work on a system for which we must deploy static HTML files. [Mike Pack](http://github.com/MikePack) has written a concurrency gem named [Pipes](http://github.com/MikePack/Pipes) that masterfully handles all the stages the publishing system must perform.
11
+
12
+ Tobacco is meant to complement the Writer classes that utilize Pipes. With a few configuration settings and two or three methods added to a writer class, Tobacco will handle the rest.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ gem 'tobacco'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install tobacco
27
+
28
+ ## Usage
29
+
30
+ Tobacco is very simple to use.
31
+
32
+
33
+ ##Configuration##
34
+
35
+ First add a configuration file. In Rails for example, this can live in 'config/initializers/tobacco.rb'
36
+
37
+ ```ruby
38
+ Tobacco.configure do |config|
39
+ config.published_host = Rails.env.development? ? 'http://localhost:3000' : 'http://localhost'
40
+ config.base_path = File.join(Rails.root, 'published_content', Rails.env)
41
+
42
+ config.content_method = :content
43
+ config.content_url_method = :content_url
44
+ config.output_filepath_method = :output_filepath
45
+ end
46
+ ```
47
+
48
+
49
+ **Default Values**
50
+
51
+ These are the setting that come default with Tobacco.
52
+
53
+ ```ruby
54
+ base_path = '/tmp/published_content'
55
+ published_host = 'http://localhost:3000'
56
+ content_method = :content
57
+ content_url_method = :content_url
58
+ output_filepath_method = :output_filepath
59
+ ```
60
+
61
+ **Configuration Options**
62
+
63
+ ```ruby
64
+ published_host
65
+ ```
66
+
67
+ published_host will be used to form the host part of the url before reading a web pages' content.
68
+
69
+ ```ruby
70
+ base_path
71
+ ```
72
+
73
+ base_path is the base folder where all other folders and files will live when publishing files.
74
+ All file writing paths generated will be appended to the base_path.
75
+
76
+
77
+ ### Methods in Writer Classes ###
78
+
79
+ **Optional**
80
+
81
+ If the Writer class will be providing its own content, say from manipulating data from a database, this is the method Tobacco will be calling to get that content.
82
+
83
+ It is not required otherwise.
84
+
85
+ Return a String
86
+
87
+ ```ruby
88
+ def content
89
+ # A string to be written to file
90
+ end
91
+ ```
92
+
93
+ **Required**
94
+
95
+ The url that will be read for content is created based on the published_host and the string returned from this method.
96
+
97
+ The following example will produce a url of "http://localhost:3000/entertainment/videos/1"
98
+
99
+ Return a String
100
+
101
+ ```ruby
102
+ def content_url
103
+ '/entertainment/videos/1'
104
+ end
105
+ ```
106
+
107
+
108
+ The ouput_filepath can return a string or an array of path options. All are joined with the base_path to create the full path the file location.
109
+
110
+ Return a String or Array
111
+
112
+ ```ruby
113
+ def output_filepath
114
+ [ 'public', 'videos', '1', 'index.html' ]
115
+
116
+ # or
117
+
118
+ 'public/videos/1/index.html'
119
+ end
120
+ ```
121
+
122
+
123
+ ## Hook Methods ##
124
+
125
+ There are four (4) hook methods you can tie into during the reading and writing process. Three are for handling errors and one is for manipulating the content before it is written.
126
+
127
+ Be default, there are no
128
+
129
+ ```ruby
130
+ def on_success(content)
131
+ # code to execute after content writen to file
132
+ end
133
+ ```
134
+
135
+ ```ruby
136
+ def on_read_error(error)
137
+ # handle
138
+ end
139
+ ```
140
+
141
+ ```ruby
142
+ def on_write_error(error)
143
+ # handle
144
+ end
145
+ ```
146
+
147
+ The error is a Tobacco::Error object with the following attributes:
148
+
149
+ ```ruby
150
+ msg - A short description of the error
151
+ content - The content or lack of content
152
+ filepath - The output path where the content was to be written
153
+ error - The error that was raised. An error object that responds to message.
154
+ ```
155
+
156
+
157
+ ```ruby
158
+ def before_write(content)
159
+ # manipulate content
160
+
161
+ return content #=> using "return" to emphasize that this method must return the content to Tobacco for writing
162
+ end
163
+ ```
164
+
165
+
166
+ ## Public API ##
167
+
168
+ The first thing to call is generate_file_paths so that the content_url and output_filepath is available to Tobacco for reading and writing.
169
+
170
+ ```ruby
171
+ generate_file_paths
172
+ ```
173
+
174
+ When the read method is called, it will do three things.
175
+
176
+ 1. Set the reader to either the calling class,
177
+ because it implemented the content method, or to an instance of Tobacco::Inhaler to prepare for reading.
178
+ 2. Read the content
179
+ 3. Verify content was read successfully.
180
+ If not, the callback :on_read_error will be called with an instance of Tobacco::Error as described above.
181
+
182
+ ```ruby
183
+ read
184
+ ```
185
+
186
+ Write can be called after these first two or you can skip the read method if the content is provided directly. In either case, if the content is written successfully the :on_success callback is called. If not, the :on_write_error callback is called.
187
+
188
+ ```ruby
189
+ write!
190
+ ```
191
+
192
+ Example using all three methods
193
+
194
+ ```ruby
195
+ writer = Tobacco::Smoker.new(self)
196
+ writer.generate_file_paths
197
+ writer.read
198
+ writer.write!
199
+ ```
200
+
201
+ Example when setting the content directly. This takes the content as a string and writes it to file.
202
+
203
+ ```ruby
204
+ writer = Tobacco::Smoker.new(self)
205
+ writer.generate_file_paths
206
+ writer.content = 'lorem ipsum'
207
+ writer.write!
208
+ ```
209
+
210
+ ###Usage###
211
+
212
+ There are only three methods to add to your class that Tobacco needs to do its work and
213
+ only two of those are required. The third method allows your class to provide its own
214
+ content to be written to file. In many cases, the content being written to file is taken from
215
+ a database or other source and formatted by the class itself. Tobacco will simple take the
216
+ provided content and write it to file for you.
217
+
218
+ Here is an example class using all three methods.
219
+
220
+ ```ruby
221
+ module Writers
222
+ class HTMLWriter
223
+ def write!
224
+ writer = Tobacco::Smoker.new(self)
225
+ writer.generate_file_paths
226
+ writer.read
227
+ writer.write!
228
+ end
229
+
230
+ # If this method is present, Tobacco will use it instead of the content_url method.
231
+ def content
232
+ 'Content to write to file'
233
+ end
234
+
235
+ def content_url
236
+ '/vehicles/1/index.html'
237
+ end
238
+
239
+ def output_filepath
240
+ [ 'vehicles', vehicle.model, vehicle.id, 'index.html']
241
+ end
242
+ end
243
+ end
244
+ ```
245
+
246
+
247
+ This example includes the callback methods
248
+
249
+ ```ruby
250
+ module Writers
251
+ class HTMLWriter
252
+ def write!
253
+ writer = Tobacco::Smoker.new(self)
254
+ writer.generate_file_paths
255
+ writer.read
256
+ writer.write!
257
+ end
258
+
259
+ # If this method is present, Tobacco will use it instead of the content_url method.
260
+ def content
261
+ 'Content to write to file'
262
+ end
263
+
264
+ def content_url
265
+ '/vehicles/1/index.html'
266
+ end
267
+
268
+ def output_filepath
269
+ [ 'vehicles', vehicle.model, vehicle.id, 'index.html']
270
+ end
271
+
272
+ #--------------------------------------
273
+ # Callbacks
274
+ #--------------------------------------
275
+ def on_success(content)
276
+ # Send email notfications
277
+ end
278
+
279
+ def on_read_error(error)
280
+ # code to handle missing content
281
+ end
282
+
283
+ def on_write_error(error)
284
+ # code to handle write error
285
+ end
286
+
287
+ def before_write(content)
288
+ # code to manipulate the content
289
+
290
+ return content #=> using "return" to emphasize that this method must return the content to Tobacco for writing
291
+ end
292
+ end
293
+ end
294
+ ```
295
+
296
+ ## Final Thoughts ##
297
+
298
+ To avoid duplication, we wrap the callbacks and write! method in a helper module that is included in all the Writer classes. This makes the individual Writers very small and easy to maintain.
299
+
300
+
301
+ ## Contributing
302
+
303
+ 1. Fork it
304
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
305
+ 3. Write tests
306
+ 4. Commit your changes (`git commit -am 'Add some feature'`)
307
+ 5. Push to the branch (`git push origin my-new-feature`)
308
+ 6. Create new Pull Request
309
+ ## Credits
310
+
311
+
312
+ ![Factory Code Labs](http://i.imgur.com/yV4u1.png)
313
+
314
+ Tobacco is maintained by [Factory Code Labs](http://www.factorycodelabs.com).
315
+
316
+ ## License
317
+
318
+ Tobacco is Copyright © 2012 Factory Code Labs. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.
319
+
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ desc 'Run RSpec code examples'
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.verbose = false
6
+ end
7
+
8
+ task default: :spec
9
+
@@ -0,0 +1,28 @@
1
+ module Tobacco
2
+ module Burnout
3
+
4
+ class MaximumAttemptsExceeded < Exception; end
5
+
6
+ def self.try(max_attempts, &block)
7
+ attempts = 1
8
+
9
+ while(attempts <= max_attempts)
10
+ success, return_value = attempt &block
11
+ return return_value if success
12
+
13
+ attempts += 1
14
+ end
15
+
16
+ raise MaximumAttemptsExceeded.new("Execution timed out more than #{max_attempts} time(s). I give up.")
17
+ end
18
+
19
+ def self.attempt(&block)
20
+ begin
21
+ [ true, yield ]
22
+ rescue
23
+ false
24
+ end
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ # Passed to callbacks when error occurs
2
+ #
3
+ # msg #=> Context where the error occurred
4
+ # filepath #=> Filepath to where the file was to be written
5
+ # content #=> Content to be written after it was modified using the callback before_write
6
+ # object #=> The error object that raised the error
7
+ #
8
+ module Tobacco
9
+ class Error
10
+ attr_accessor :msg, :filepath, :content, :object
11
+
12
+ def initialize(options = {})
13
+ self.msg = options[:msg]
14
+ self.filepath = options[:filepath]
15
+ self.content = options[:content]
16
+ self.object = options[:object]
17
+ end
18
+
19
+ # Allow destructure of the error into variable names
20
+ #
21
+ # eg.
22
+ #
23
+ # msg, filepath, content, object = *error
24
+ # [ 'Error writing file', '/path/to/file', '<h1>Title</h1>', #<Errno::EACCES: Permission denied - /users/index.txt> ]
25
+ #
26
+ # You can access the attributes normally as well
27
+ #
28
+ # error.msg #=> 'Error writing file'
29
+ # error.filepath #=> '/path/to/file'
30
+ # error.object #=> #<Errno::EACCES: Permission denied - /users/index.txt>
31
+ #
32
+ def to_a
33
+ [ msg, filepath, content, object ]
34
+ end
35
+ alias_method :to_ary, :to_a
36
+
37
+ end
38
+ end
@@ -0,0 +1,26 @@
1
+ module Tobacco
2
+ class Exhaler
3
+ attr_accessor :content, :filepath
4
+
5
+ def initialize(content = '', filepath = '')
6
+ self.content = content
7
+ self.filepath = filepath
8
+ end
9
+
10
+ def write!
11
+ create_directory
12
+ write_content_to_file
13
+ end
14
+
15
+ def create_directory
16
+ FileUtils.mkdir_p File.dirname(filepath)
17
+ end
18
+
19
+ def write_content_to_file
20
+ File.open(filepath, 'w') do |f|
21
+ f.write content
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ module Tobacco
2
+ class Inhaler
3
+ attr_accessor :url
4
+
5
+ def initialize(url = '')
6
+ self.url = url
7
+ end
8
+
9
+ def read
10
+ @content ||= Tobacco::Burnout.try(3) { URI.parse(url).read }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,38 @@
1
+ module Tobacco
2
+ class Roller
3
+
4
+ def initialize(smoker)
5
+ @smoker = smoker
6
+ end
7
+
8
+ def output_filepath
9
+ @output_filepath ||= File.join(base_path, *filepath_options)
10
+ end
11
+
12
+ def content_url
13
+ separator = url_path =~ /^\// ? '' : '/'
14
+
15
+ @content_url ||= "#{host}#{separator}#{url_path}"
16
+ end
17
+
18
+
19
+ #---------------------------------------------------------
20
+ private
21
+
22
+ def url_path
23
+ @url_path ||= @smoker.send(Tobacco.content_url_method)
24
+ end
25
+
26
+ def filepath_options
27
+ @smoker.send(Tobacco.output_filepath_method)
28
+ end
29
+
30
+ def host
31
+ Tobacco.published_host
32
+ end
33
+
34
+ def base_path
35
+ Tobacco.base_path
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,141 @@
1
+ module Tobacco
2
+ class MissingContentError < RuntimeError
3
+ def message
4
+ "No error encountered but content is empty"
5
+ end
6
+ end
7
+
8
+ class Smoker
9
+
10
+ attr_accessor :smoker,
11
+ :file_path_generator,
12
+ :reader,
13
+ :writer,
14
+ :content
15
+
16
+ def initialize(smoker, content = '')
17
+ self.smoker = smoker
18
+ self.content = content
19
+ end
20
+
21
+ def generate_file_paths
22
+ self.file_path_generator = Roller.new(smoker)
23
+ end
24
+
25
+ def read
26
+ choose_reader
27
+ read_content
28
+ verify_content
29
+ end
30
+
31
+ def write!
32
+ return unless content_present?
33
+
34
+ begin
35
+ filepath = file_path_generator.output_filepath
36
+ modified_content = modify_content_before_writing
37
+ content_writer = Tobacco::Exhaler.new(modified_content, filepath)
38
+
39
+ content_writer.write!
40
+
41
+ callback(:on_success, modified_content)
42
+
43
+ rescue => e
44
+
45
+ error = error_object('Error Writing', modified_content, e)
46
+ callback(:on_write_error, error)
47
+ end
48
+ end
49
+
50
+
51
+ #---------------------------------------------------------
52
+ # End of Public API
53
+ #---------------------------------------------------------
54
+
55
+
56
+ #---------------------------------------------------------
57
+ # Write helper methods
58
+ #---------------------------------------------------------
59
+ def modify_content_before_writing
60
+ callback(:before_write, content)
61
+ end
62
+
63
+ #---------------------------------------------------------
64
+ # Read helper methods
65
+ #---------------------------------------------------------
66
+ def read_content
67
+ self.content = reader.send(Tobacco.content_method)
68
+ end
69
+
70
+ def verify_content
71
+ unless content_present?
72
+
73
+ # At this point, the content might be an error object
74
+ # but if not, we create one
75
+ #
76
+ object = missing_content_error(content)
77
+ error = error_object('Error Reading', '', object)
78
+
79
+ callback(:on_read_error, error)
80
+ end
81
+ end
82
+
83
+ def missing_content_error(content)
84
+ if content.respond_to? :message
85
+ content
86
+ else
87
+ Tobacco::MissingContentError.new
88
+ end
89
+ end
90
+
91
+ def content_present?
92
+ @content_present ||= content?
93
+ end
94
+
95
+ def content?
96
+ return false if content.nil? || content.empty?
97
+
98
+ Array(content).last !~ /404 Not Found|The page you were looking for doesn't exist/
99
+ end
100
+
101
+ def choose_reader
102
+ # The reader will either be the calling class (smoker)
103
+ # if it provides the content method or a new Inhaler
104
+ # object that will be used to read the content from a url
105
+ #
106
+ self.reader = \
107
+ if smoker.respond_to? Tobacco.content_method
108
+ smoker
109
+ else
110
+ Inhaler.new(file_path_generator.content_url).tap do |inhaler|
111
+
112
+ # Add an alias for the user configured content_method
113
+ # so that when it is called it calls :read
114
+ # on the Inhaler instance
115
+ #
116
+ inhaler.instance_eval %{
117
+ alias :"#{Tobacco.content_method}" :read
118
+ }
119
+ end
120
+ end
121
+ end
122
+
123
+ #---------------------------------------------------------
124
+ # private
125
+
126
+ def error_object(msg, modified_content, e)
127
+ Tobacco::Error.new(
128
+ msg: msg,
129
+ filepath: file_path_generator.filepath,
130
+ content: modified_content,
131
+ object: e
132
+ )
133
+ end
134
+
135
+ def callback(name, error)
136
+ if smoker.respond_to? name
137
+ smoker.send(name, error)
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,3 @@
1
+ module Tobacco
2
+ VERSION = "0.0.1"
3
+ end