tweetwine 0.2.4

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