tweetwine 0.2.4

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/CHANGELOG.rdoc ADDED
@@ -0,0 +1,84 @@
1
+ === 0.2.4 released 2009-09-16
2
+
3
+ * Retry connection upon connection reset, trying maximum of three times.
4
+ * Display proper version info on Ruby 1.8 when using option "-v".
5
+ * Minor code cleanups.
6
+ * Release 0.2.3 is skipped due to my error in tagging the wrong commit.
7
+
8
+ === 0.2.2 released 2009-09-03
9
+
10
+ * Highlight hashtags in statuses.
11
+ * URL shortening step shortens only unique URLs.
12
+ * URL shortening step is skipped if
13
+ - there is a connection error to the URL shortening service, or
14
+ - required libraries are not installed (nokogiri).
15
+ * Fixed a colorization bug for duplicate URLs.
16
+ * Removed dependencies to Rubygems.
17
+
18
+ === 0.2.1 released 2009-08-17
19
+
20
+ * Command line option "-v" show version information.
21
+ * Slight implementation code cleaning.
22
+
23
+ === 0.2.0 released 2009-08-17
24
+
25
+ * URL shortening by using an external web service.
26
+ * Show a preview before sending status update.
27
+ * Avoid stack trace upon erroneous connection.
28
+
29
+ === 0.1.11 released 2009-08-10
30
+
31
+ * Fixed highlighting multiple nicks in statuses.
32
+
33
+ === 0.1.10 released 2009-08-09
34
+
35
+ * Improved URL highlight support.
36
+
37
+ === 0.1.9 released 2009-07-15
38
+
39
+ * Added commands "friends" and "followers"
40
+ * Removed dependency to the "json" gem. It is included in Ruby 1.9.
41
+
42
+ === 0.1.8 released 2009-07-01
43
+
44
+ * SIGINT (Ctrl+c) is trapper earlier, resulting in clean abort while Ruby
45
+ loads the program's required libraries.
46
+
47
+ === 0.1.7 released 2009-06-07
48
+
49
+ * Small compatibility fix with Ruby 1.9.
50
+
51
+ === 0.1.6 released 2009-06-07
52
+
53
+ * Improved URL highlighting.
54
+
55
+ === 0.1.5 released 2009-06-06
56
+
57
+ * URLs of http(s) scheme are highlighted. Changed colors.
58
+ * Friendly abort message when interrupting the program with Ctrl+C.
59
+
60
+ === 0.1.4 released 2009-05-12
61
+
62
+ * Command line option "--page N" fetches a specific status page
63
+
64
+ === 0.1.3 released 2009-05-05
65
+
66
+ * Empty status update indicates cancellation of the command
67
+ * Other minor improvements
68
+
69
+ === 0.1.2 released 2009-05-04
70
+
71
+ * Renamed command "friends" to "home"
72
+ * Added command "mentions"
73
+ * When showing a status, indicate if it is a reply
74
+ * Improved command line argument and configuration file parsing
75
+
76
+ === 0.1.1 released 2009-04-23
77
+
78
+ * Renamed command "msg" to "update"
79
+ * If status update if longer than 140 characters, warn about it
80
+
81
+ === 0.1.0 released 2009-04-22
82
+
83
+ * Initial release with minimal functionality
84
+ * Usable for quickly checking friends' statuses and sending status updates
data/MIT-LICENSE.txt ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009 Tuomas Kareinen.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,87 @@
1
+ = Tweetwine
2
+
3
+ A simple but tasty Twitter agent for command line use, made for fun.
4
+
5
+ == Installation
6
+
7
+ Install Tweetwine as a RubyGem from GitHub:
8
+
9
+ $ sudo gem install tuomas-tweetwine --source http://gems.github.com
10
+
11
+ The program is compatible with Ruby 1.9.1, and it requires <b>rest_client</b>
12
+ gem to be installed. In addition, <b>json</b> gem is needed for Ruby 1.8.6.
13
+
14
+ == Usage
15
+
16
+ In the command line, run the program by entering
17
+
18
+ $ tweetwine [options...] [command]
19
+
20
+ The program needs the user's username and password for authentication. This
21
+ information can be supplied either via a configuration file or as an option
22
+ (<tt>-a USERNAME:PASSWORD</tt>) to the program. It is recommended to use the
23
+ former method over the latter.
24
+
25
+ The configuration file, in <tt>~/.tweetwine</tt>, is in YAML syntax. The
26
+ program recognizes the following basic settings:
27
+
28
+ username: <your_username>
29
+ password: <your_password>
30
+ colorize: true|false
31
+
32
+ When invoking the program, the supported commands are
33
+
34
+ <tt>home</tt>:: Fetch friends' latest statuses (the contents of the authenticated user's home).
35
+ <tt>mentions</tt>:: Fetch latest mentions for the authenticated user.
36
+ <tt>user [name]</tt>:: Fetch a specific user's latest statuses, identified by the argument; if given no argument, fetch the statuses of the authenticated user.
37
+ <tt>update [status]</tt>:: Send a status update, but confirm the action first before actually sending. The status update can either be given as an argument or via STDIN if no argument is given.
38
+ <tt>friends</tt>:: Fetch friends and their latest statuses.
39
+ <tt>followers</tt>:: Fetch followers and their latest statuses.
40
+
41
+ If no <tt>[command]</tt> is given, the default action is <tt>home</tt>.
42
+
43
+ For all the options, see:
44
+
45
+ $ tweetwine -h
46
+
47
+ === URL shortening for status update
48
+
49
+ Before actually sending a status update, it is possible for the software to
50
+ shorten the URLs in the update by using an external web service. This can be
51
+ enabled via the <tt>shorten_urls</tt> key in configuration file; for
52
+ example:
53
+
54
+ username: spoonman
55
+ password: withyourhands
56
+ colorize: true
57
+ shorten_urls:
58
+ enable: true
59
+ service_url: http://is.gd/create.php
60
+ method: post
61
+ url_param_name: URL
62
+ xpath_selector: //input[@id='short_url']/@value
63
+
64
+ The supported methods are <tt>get</tt> and <tt>post</tt>. The method chosen
65
+ affects whether parameters are passed as URL query parameters or as payload
66
+ in the HTTP request, respectively. Extra parameters can be given via
67
+ <tt>extra_params</tt> key, as a hash.
68
+
69
+ The <tt>xpath_selector</tt> is needed to extract the shortened URL from the
70
+ result.
71
+
72
+ The feature can be disabled by
73
+
74
+ * not defining <tt>shorten_urls</tt> key in the configuration file,
75
+ * setting key <tt>enable</tt> to <tt>false</tt>, or
76
+ * using the command line option <tt>--no-url-shorten</tt>.
77
+
78
+ The use of the feature requires <b>nokogiri</b> gem to be installed.
79
+
80
+ == Contacting
81
+
82
+ Please send feedback by email to Tuomas Kareinen < tkareine (at) gmail (dot)
83
+ com >.
84
+
85
+ == Legal notes
86
+
87
+ Copyright (c) 2009 Tuomas Kareinen. See MIT-LICENSE.txt in this directory.
data/Rakefile ADDED
@@ -0,0 +1,86 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))
2
+
3
+ require "rubygems"
4
+
5
+ name = "tweetwine"
6
+ require "#{name}"
7
+ version = Tweetwine::VERSION
8
+
9
+ require "rake/clean"
10
+
11
+ require "rake/gempackagetask"
12
+ spec = Gem::Specification.new do |s|
13
+ s.name = name
14
+ s.version = version
15
+ s.homepage = "http://github.com/tuomas/tweetwine"
16
+ s.summary = "A simple Twitter agent for command line use"
17
+ s.description = "A simple but tasty Twitter agent for command line use, made for fun."
18
+
19
+ s.author = "Tuomas Kareinen"
20
+ s.email = "tkareine@gmail.com"
21
+
22
+ s.platform = Gem::Platform::RUBY
23
+ s.files = FileList["Rakefile", "MIT-LICENSE.txt", "*.rdoc", "bin/**/*", "lib/**/*", "test/**/*"].to_a
24
+ s.executables = ["tweetwine"]
25
+
26
+ s.add_dependency("rest-client", ">= 1.0.0")
27
+
28
+ s.has_rdoc = true
29
+ s.extra_rdoc_files = FileList["MIT-LICENSE.txt", "*.rdoc"].to_a
30
+ s.rdoc_options << "--title" << "#{name} #{version}" \
31
+ << "--main" << "README.rdoc" \
32
+ << "--exclude" << "test" \
33
+ << "--line-numbers"
34
+ end
35
+
36
+ Rake::GemPackageTask.new(spec) do |pkg|
37
+ pkg.need_zip = false
38
+ pkg.need_tar = true
39
+ end
40
+
41
+ desc "Generate a gemspec file"
42
+ task :gemspec do
43
+ File.open("#{spec.name}.gemspec", "w") do |f|
44
+ f.write spec.to_ruby
45
+ end
46
+ end
47
+
48
+ task :install => [:package] do
49
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
50
+ end
51
+
52
+ task :uninstall => [:clean] do
53
+ sh %{sudo gem uninstall #{name}}
54
+ end
55
+
56
+ require "rake/rdoctask"
57
+ desc "Create documentation"
58
+ Rake::RDocTask.new(:rdoc) do |rd|
59
+ rd.rdoc_dir = "rdoc"
60
+ rd.title = "#{name} #{version}"
61
+ rd.main = "README.rdoc"
62
+ rd.rdoc_files.include("MIT-LICENSE.txt", "*.rdoc", "lib/**/*.rb")
63
+ rd.options << "--line-numbers"
64
+ end
65
+
66
+ require "rake/testtask"
67
+ desc "Run tests"
68
+ Rake::TestTask.new(:test) do |t|
69
+ t.test_files = FileList["test/**/*_test.rb"]
70
+ t.verbose = true
71
+ t.warning = true
72
+ t.ruby_opts << "-rrubygems"
73
+ t.libs << "test"
74
+ end
75
+
76
+ desc "Find code smells"
77
+ task :roodi do
78
+ sh %{roodi "**/*.rb"}
79
+ end
80
+
81
+ desc "Search unfinished parts of source code"
82
+ task :todo do
83
+ FileList["**/*.rb", "**/*.rdoc", "**/*.txt"].egrep /(TODO|FIXME)/
84
+ end
85
+
86
+ task :default => :test
data/bin/tweetwine ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ EXIT_HELP = 1
4
+ EXIT_VERSION = 2
5
+ EXIT_SIGINT = 6
6
+ EXIT_ERROR = 64
7
+
8
+ trap("INT") do
9
+ puts "\nAbort"
10
+ exit(EXIT_SIGINT)
11
+ end
12
+
13
+ require "optparse"
14
+ require "tweetwine"
15
+
16
+ include Tweetwine
17
+
18
+ cmd_parser = lambda do |args|
19
+ options = {}
20
+ begin
21
+ OptionParser.new do |opt|
22
+ opt.banner =<<-END
23
+ Usage: tweetwine [options...] [command]
24
+
25
+ Commands: #{Client::COMMANDS.join(", ")}
26
+
27
+ Options:
28
+
29
+ END
30
+ opt.on("-a", "--auth USERNAME:PASSWORD", "Authentication") do |arg|
31
+ options[:username], options[:password] = arg.split(":", 2)
32
+ end
33
+ opt.on("-c", "--colorize", "Colorize output with ANSI escape codes") do
34
+ options[:colorize] = true
35
+ end
36
+ opt.on("-n", "--num N", Integer, "The number of statuses to fetch, defaults to #{Client::DEFAULT_NUM_STATUSES}") do |arg|
37
+ options[:num_statuses] = arg
38
+ end
39
+ opt.on("--no-url-shorten", "Do not shorten URLs for status update") do
40
+ options[:shorten_urls] = { :enable => false }
41
+ end
42
+ opt.on("-p", "--page N", Integer, "The page number of the statuses to fetch, defaults to #{Client::DEFAULT_PAGE_NUM}") do |arg|
43
+ options[:page_num] = arg
44
+ end
45
+ opt.on("-v", "--version", "Show version information and exit") do
46
+ puts "#{File.basename($0)} #{Tweetwine::VERSION}"
47
+ exit(EXIT_VERSION)
48
+ end
49
+ opt.on_tail("-h", "--help", "Show this help message and exit") do
50
+ puts opt
51
+ exit(EXIT_HELP)
52
+ end
53
+ end.parse!(args)
54
+ rescue OptionParser::ParseError => e
55
+ raise ArgumentError, e.message
56
+ end
57
+ options
58
+ end
59
+
60
+ def create_dependencies(options)
61
+ io = Tweetwine::IO.new(options)
62
+ rest_client = RestClientWrapper.new(io)
63
+ url_shortener = lambda { |opts| UrlShortener.new(rest_client, opts) }
64
+ {
65
+ :io => io,
66
+ :rest_client => rest_client,
67
+ :url_shortener => url_shortener
68
+ }
69
+ end
70
+
71
+ begin
72
+ config = StartupConfig.new(Client::COMMANDS)
73
+ config.parse(ARGV, ENV["HOME"] + "/.tweetwine", &cmd_parser)
74
+ client = Client.new(create_dependencies(config.options), config.options)
75
+ client.send(config.command, *config.args)
76
+ rescue ArgumentError, ClientError => e
77
+ puts "Error: #{e.message}"
78
+ exit(EXIT_ERROR)
79
+ end
@@ -0,0 +1,174 @@
1
+ require "json"
2
+ require "uri"
3
+
4
+ module Tweetwine
5
+ class Client
6
+ attr_reader :num_statuses, :page_num
7
+
8
+ COMMANDS = [:home, :mentions, :user, :update, :friends, :followers]
9
+
10
+ DEFAULT_NUM_STATUSES = 20
11
+ DEFAULT_PAGE_NUM = 1
12
+ MAX_STATUS_LENGTH = 140
13
+
14
+ def initialize(dependencies, options)
15
+ @io = dependencies[:io]
16
+ @rest_client = dependencies[:rest_client]
17
+ @username = options[:username].to_s
18
+ raise ArgumentError, "No authentication data given" if @username.empty?
19
+ @base_url = "https://#{@username}:#{options[:password]}@twitter.com/"
20
+ @num_statuses = Util.parse_int_gt(options[:num_statuses], DEFAULT_NUM_STATUSES, 1, "number of statuses_to_show")
21
+ @page_num = Util.parse_int_gt(options[:page_num], DEFAULT_PAGE_NUM, 1, "page number")
22
+ @url_shortener = if options[:shorten_urls] && options[:shorten_urls][:enable]
23
+ dependencies[:url_shortener].call(options[:shorten_urls])
24
+ else
25
+ nil
26
+ end
27
+ @status_update_factory = StatusUpdateFactory.new(@io, @url_shortener)
28
+ end
29
+
30
+ def home
31
+ show_statuses(get_response_as_json("statuses/friends_timeline", :num_statuses, :page))
32
+ end
33
+
34
+ def mentions
35
+ show_statuses(get_response_as_json("statuses/mentions", :num_statuses, :page))
36
+ end
37
+
38
+ def user(user = @username)
39
+ show_statuses(get_response_as_json("statuses/user_timeline/#{user}", :num_statuses, :page))
40
+ end
41
+
42
+ def update(new_status = nil)
43
+ new_status = @status_update_factory.prepare(new_status)
44
+ completed = false
45
+ unless new_status.empty?
46
+ @io.show_status_preview(new_status)
47
+ if @io.confirm("Really send?")
48
+ status = JSON.parse(post("statuses/update.json", {:status => new_status.to_s}))
49
+ @io.info "Sent status update.\n\n"
50
+ show_statuses([status])
51
+ completed = true
52
+ end
53
+ end
54
+ @io.info "Cancelled." unless completed
55
+ end
56
+
57
+ def friends
58
+ show_users(get_response_as_json("statuses/friends/#{@username}", :page))
59
+ end
60
+
61
+ def followers
62
+ show_users(get_response_as_json("statuses/followers/#{@username}", :page))
63
+ end
64
+
65
+ private
66
+
67
+ def get_response_as_json(url_body, *query_opts)
68
+ url = url_body + ".json?#{parse_query_options(query_opts)}"
69
+ JSON.parse(get(url))
70
+ end
71
+
72
+ def parse_query_options(query_opts)
73
+ str = []
74
+ query_opts.each do |opt|
75
+ case opt
76
+ when :page
77
+ str << "page=#{@page_num}"
78
+ when :num_statuses
79
+ str << "count=#{@num_statuses}"
80
+ # do nothing on else
81
+ end
82
+ end
83
+ str.join("&")
84
+ end
85
+
86
+ def show_statuses(data)
87
+ show_responses(data) { |entry| [entry["user"], entry] }
88
+ end
89
+
90
+ def show_users(data)
91
+ show_responses(data) { |entry| [entry, entry["status"]] }
92
+ end
93
+
94
+ def show_responses(data)
95
+ data.each do |entry|
96
+ user_data, status_data = yield entry
97
+ @io.show_record(parse_response(user_data, status_data))
98
+ end
99
+ end
100
+
101
+ def parse_response(user_data, status_data)
102
+ record = { :user => user_data["screen_name"] }
103
+ if status_data
104
+ record[:status] = {
105
+ :created_at => status_data["created_at"],
106
+ :in_reply_to => status_data["in_reply_to_screen_name"],
107
+ :text => status_data["text"]
108
+ }
109
+ end
110
+ record
111
+ end
112
+
113
+ def get(body_url)
114
+ @rest_client.get @base_url + body_url
115
+ end
116
+
117
+ def post(body_url, body)
118
+ @rest_client.post @base_url + body_url, body
119
+ end
120
+
121
+ class StatusUpdateFactory
122
+ def initialize(io, url_shortener)
123
+ @io = io
124
+ @url_shortener = url_shortener
125
+ end
126
+
127
+ def prepare(status)
128
+ StatusUpdate.new(status, @io, @url_shortener).to_s
129
+ end
130
+ end
131
+
132
+ class StatusUpdate
133
+ def initialize(status, io, url_shortener)
134
+ @io = io
135
+ @url_shortener = url_shortener
136
+ @text = prepare(status)
137
+ end
138
+
139
+ def to_s
140
+ @text.to_s
141
+ end
142
+
143
+ private
144
+
145
+ def prepare(status)
146
+ status = unless status
147
+ @io.prompt("Status update")
148
+ else
149
+ status.dup
150
+ end
151
+ status.strip!
152
+ shorten_urls!(status) if @url_shortener
153
+ truncate!(status) if status.length > MAX_STATUS_LENGTH
154
+ status
155
+ end
156
+
157
+ def truncate!(status)
158
+ status.replace status[0...MAX_STATUS_LENGTH]
159
+ @io.warn("Status will be truncated.")
160
+ end
161
+
162
+ def shorten_urls!(status)
163
+ url_pairs = URI.extract(status, ["http", "https"]).uniq.map do |url_to_be_shortened|
164
+ [url_to_be_shortened, @url_shortener.shorten(url_to_be_shortened)]
165
+ end
166
+ url_pairs.reject { |pair| pair.last.nil? || pair.last.empty? }.each do |url_pair|
167
+ status.gsub!(url_pair.first, url_pair.last)
168
+ end
169
+ rescue ClientError, LoadError => e
170
+ @io.warn "#{e}. Skipping URL shortening..."
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,117 @@
1
+ require "uri"
2
+
3
+ module Tweetwine
4
+ class IO
5
+ COLOR_CODES = {
6
+ :cyan => 36,
7
+ :green => 32,
8
+ :magenta => 35,
9
+ :yellow => 33
10
+ }
11
+
12
+ HASHTAG_REGEX = /#[\w-]+/
13
+ USERNAME_REGEX = /@\w+/
14
+
15
+ def initialize(options)
16
+ @input = options[:input] || $stdin
17
+ @output = options[:output] || $stdout
18
+ @colorize = options[:colorize] || false
19
+ end
20
+
21
+ def prompt(prompt)
22
+ @output.print "#{prompt}: "
23
+ @input.gets.strip!
24
+ end
25
+
26
+ def info(msg)
27
+ @output.puts(msg)
28
+ end
29
+
30
+ def warn(msg)
31
+ @output.puts "Warning: #{msg}"
32
+ end
33
+
34
+ def confirm(msg)
35
+ @output.print "#{msg} [yN] "
36
+ confirmation = @input.gets.strip
37
+ confirmation.downcase[0,1] == "y"
38
+ end
39
+
40
+ def show_status_preview(status)
41
+ @output.puts <<-END
42
+
43
+ #{format_status(status)}
44
+
45
+ END
46
+ end
47
+
48
+ def show_record(record)
49
+ if record[:status]
50
+ show_record_as_user_with_status(record)
51
+ else
52
+ show_record_as_user(record)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def show_record_as_user(record)
59
+ @output.puts <<-END
60
+ #{format_user(record[:user])}
61
+
62
+ END
63
+ end
64
+
65
+ def show_record_as_user_with_status(record)
66
+ @output.puts <<-END
67
+ #{format_record_header(record)}
68
+ #{format_status(record[:status][:text])}
69
+
70
+ END
71
+ end
72
+
73
+ def format_user(user)
74
+ user = user.dup
75
+ colorize!(:green, user) if @colorize
76
+ user
77
+ end
78
+
79
+ def format_status(status)
80
+ status = status.dup
81
+ if @colorize
82
+ colorize_all!(:yellow, status, USERNAME_REGEX)
83
+ colorize_all!(:magenta, status, HASHTAG_REGEX)
84
+ URI.extract(status, ["http", "https"]).uniq.each do |url|
85
+ colorize_all!(:cyan, status, url)
86
+ end
87
+ end
88
+ status
89
+ end
90
+
91
+ def format_record_header(record)
92
+ time_diff_value, time_diff_unit = Util.humanize_time_diff(record[:status][:created_at], Time.now)
93
+ from_user = record[:user].dup
94
+ colorize!(:green, from_user) if @colorize
95
+ in_reply_to = record[:status][:in_reply_to]
96
+ in_reply_to = if in_reply_to && !in_reply_to.empty?
97
+ in_reply_to = colorize!(:green, in_reply_to.dup) if @colorize
98
+ "in reply to #{in_reply_to}, "
99
+ else
100
+ ""
101
+ end
102
+ "#{from_user}, #{in_reply_to}#{time_diff_value} #{time_diff_unit} ago:"
103
+ end
104
+
105
+ def colorize_all!(color, str, pattern)
106
+ str.gsub!(pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
107
+ end
108
+
109
+ def colorize!(color, str)
110
+ str.replace colorize_str(COLOR_CODES[color.to_sym], str)
111
+ end
112
+
113
+ def colorize_str(color_code, str)
114
+ "\033[#{color_code}m#{str}\033[0m"
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,3 @@
1
+ module Tweetwine
2
+ VERSION = "0.2.4"
3
+ end
@@ -0,0 +1,22 @@
1
+ module Tweetwine
2
+ class Options
3
+ def initialize(options, source = nil)
4
+ @hash = options.to_hash
5
+ @source = source
6
+ end
7
+
8
+ def [](key)
9
+ @hash[key]
10
+ end
11
+
12
+ def require(key)
13
+ value = @hash[key]
14
+ if value.nil?
15
+ msg = "Option #{key} is required"
16
+ msg << " for #{@source}" if @source
17
+ raise ArgumentError, msg
18
+ end
19
+ value
20
+ end
21
+ end
22
+ end