tweetwine 0.2.5 → 0.2.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/CHANGELOG.rdoc +30 -17
  2. data/README.rdoc +16 -17
  3. data/Rakefile +2 -0
  4. data/bin/tweetwine +3 -68
  5. data/example/example_helper.rb +31 -41
  6. data/example/fixtures/{statuses.json → home.json} +0 -0
  7. data/example/fixtures/mentions.json +1 -0
  8. data/example/fixtures/search.json +1 -0
  9. data/example/fixtures/update.json +1 -0
  10. data/example/fixtures/user.json +1 -0
  11. data/example/fixtures/users.json +1 -0
  12. data/example/search_statuses_example.rb +36 -0
  13. data/example/show_followers_example.rb +23 -0
  14. data/example/show_friends_example.rb +23 -0
  15. data/example/show_home_example.rb +51 -0
  16. data/example/show_mentions_example.rb +23 -0
  17. data/example/show_metadata_example.rb +54 -7
  18. data/example/show_user_example.rb +37 -0
  19. data/example/update_status_example.rb +65 -0
  20. data/lib/tweetwine/cli.rb +241 -0
  21. data/lib/tweetwine/client.rb +94 -57
  22. data/lib/tweetwine/io.rb +39 -28
  23. data/lib/tweetwine/meta.rb +1 -1
  24. data/lib/tweetwine/retrying_http.rb +93 -0
  25. data/lib/tweetwine/startup_config.rb +14 -15
  26. data/lib/tweetwine/url_shortener.rb +13 -8
  27. data/lib/tweetwine/util.rb +14 -0
  28. data/lib/tweetwine.rb +2 -1
  29. data/test/cli_test.rb +16 -0
  30. data/test/client_test.rb +275 -205
  31. data/test/fixtures/test_config.yaml +2 -1
  32. data/test/io_test.rb +89 -62
  33. data/test/retrying_http_test.rb +127 -0
  34. data/test/startup_config_test.rb +52 -27
  35. data/test/test_helper.rb +32 -17
  36. data/test/url_shortener_test.rb +18 -18
  37. data/test/util_test.rb +145 -47
  38. metadata +20 -7
  39. data/example/show_latest_statuses_example.rb +0 -45
  40. data/lib/tweetwine/rest_client_wrapper.rb +0 -37
  41. data/test/rest_client_wrapper_test.rb +0 -68
data/lib/tweetwine/io.rb CHANGED
@@ -15,7 +15,7 @@ module Tweetwine
15
15
  def initialize(options)
16
16
  @input = options[:input] || $stdin
17
17
  @output = options[:output] || $stdout
18
- @colorize = options[:colorize] || false
18
+ @colors = options[:colors] || false
19
19
  end
20
20
 
21
21
  def prompt(prompt)
@@ -34,7 +34,7 @@ module Tweetwine
34
34
  def confirm(msg)
35
35
  @output.print "#{msg} [yN] "
36
36
  confirmation = @input.gets.strip
37
- confirmation.downcase[0,1] == "y"
37
+ confirmation.downcase[0, 1] == "y"
38
38
  end
39
39
 
40
40
  def show_status_preview(status)
@@ -46,6 +46,7 @@ module Tweetwine
46
46
  end
47
47
 
48
48
  def show_record(record)
49
+ clean_record!(record)
49
50
  if record[:status]
50
51
  show_record_as_user_with_status(record)
51
52
  else
@@ -55,63 +56,73 @@ module Tweetwine
55
56
 
56
57
  private
57
58
 
59
+ def clean_record!(record)
60
+ record.each_pair do |key, value|
61
+ if value.is_a? Hash
62
+ clean_record!(value)
63
+ else
64
+ unless value.nil?
65
+ value = value.to_s
66
+ record[key] = value.empty? ? nil : value
67
+ end
68
+ end
69
+ end
70
+ end
71
+
58
72
  def show_record_as_user(record)
59
73
  @output.puts <<-END
60
- #{format_user(record[:user])}
74
+ #{format_user(record[:from_user])}
61
75
 
62
76
  END
63
77
  end
64
78
 
65
79
  def show_record_as_user_with_status(record)
66
80
  @output.puts <<-END
67
- #{format_record_header(record)}
68
- #{format_status(record[:status][:text])}
81
+ #{format_record_header(record[:from_user], record[:to_user], record[:created_at])}
82
+ #{format_status(record[:status])}
69
83
 
70
84
  END
71
85
  end
72
86
 
73
87
  def format_user(user)
74
- user = user.dup
75
- colorize!(:green, user) if @colorize
88
+ user = colorize(:green, user) if @colors
76
89
  user
77
90
  end
78
91
 
79
92
  def format_status(status)
80
- status = status.dup
81
- if @colorize
82
- colorize_all_by_group!(:yellow, status, USERNAME_REGEX)
83
- colorize_all_by_group!(:magenta, status, HASHTAG_REGEX)
93
+ if @colors
94
+ status = colorize_all_by_group(:yellow, status, USERNAME_REGEX)
95
+ status = colorize_all_by_group(:magenta, status, HASHTAG_REGEX)
84
96
  URI.extract(status, ["http", "https"]).uniq.each do |url|
85
- colorize_all!(:cyan, status, url)
97
+ status = colorize_all(:cyan, status, url)
86
98
  end
87
99
  end
88
100
  status
89
101
  end
90
102
 
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}, "
103
+ def format_record_header(from_user, to_user, created_at)
104
+ time_diff_value, time_diff_unit = Util.humanize_time_diff(created_at, Time.now)
105
+ if @colors
106
+ from_user = colorize(:green, from_user)
107
+ to_user = colorize(:green, to_user) if to_user
108
+ end
109
+ if to_user
110
+ "#{from_user}, in reply to #{to_user}, #{time_diff_value} #{time_diff_unit} ago:"
99
111
  else
100
- ""
112
+ "#{from_user}, #{time_diff_value} #{time_diff_unit} ago:"
101
113
  end
102
- "#{from_user}, #{in_reply_to}#{time_diff_value} #{time_diff_unit} ago:"
103
114
  end
104
115
 
105
- def colorize_all!(color, str, pattern)
106
- str.gsub!(pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
116
+ def colorize_all(color, str, pattern)
117
+ str.gsub(pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
107
118
  end
108
119
 
109
- def colorize_all_by_group!(color, str, pattern)
110
- str.replace Util.str_gsub_by_group(str, pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
120
+ def colorize_all_by_group(color, str, pattern)
121
+ Util.str_gsub_by_group(str, pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
111
122
  end
112
123
 
113
- def colorize!(color, str)
114
- str.replace colorize_str(COLOR_CODES[color.to_sym], str)
124
+ def colorize(color, str)
125
+ colorize_str(COLOR_CODES[color.to_sym], str)
115
126
  end
116
127
 
117
128
  def colorize_str(color_code, str)
@@ -1,3 +1,3 @@
1
1
  module Tweetwine
2
- VERSION = "0.2.5"
2
+ VERSION = "0.2.7"
3
3
  end
@@ -0,0 +1,93 @@
1
+ require "rest_client"
2
+
3
+ module Tweetwine
4
+ class HttpError < RuntimeError; end
5
+
6
+ module RetryingHttp
7
+ class Base
8
+ MAX_RETRIES = 3
9
+ RETRY_BASE_WAIT_TIMEOUT = 4
10
+
11
+ def self.use_retries_with(*methods)
12
+ methods.each do |method_name|
13
+ module_eval do
14
+ non_retrying_method_name = "original_#{method_name}".to_sym
15
+ alias_method non_retrying_method_name, method_name
16
+ define_method(method_name) do |*args|
17
+ do_with_retries { send(non_retrying_method_name, *args) }
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def do_with_retries
26
+ tries = 0
27
+ begin
28
+ tries += 1
29
+ yield
30
+ rescue Errno::ECONNRESET => e
31
+ if tries < MAX_RETRIES
32
+ timeout = RETRY_BASE_WAIT_TIMEOUT**tries
33
+ @io.warn("Could not connect -- retrying in #{timeout} seconds") if @io
34
+ sleep timeout
35
+ retry
36
+ else
37
+ raise HttpError, e
38
+ end
39
+ rescue RestClient::Exception, SocketError, SystemCallError => e
40
+ raise HttpError, e
41
+ end
42
+ end
43
+ end
44
+
45
+ class Client < Base
46
+ attr_accessor :io
47
+
48
+ def initialize(io)
49
+ @io = io
50
+ end
51
+
52
+ def get(*args)
53
+ RestClient.get(*args)
54
+ end
55
+
56
+ def post(*args)
57
+ RestClient.post(*args)
58
+ end
59
+
60
+ def as_resource(url, options = {})
61
+ resource = Resource.new(RestClient::Resource.new(url, options))
62
+ resource.io = @io
63
+ resource
64
+ end
65
+
66
+ use_retries_with :get, :post
67
+ end
68
+
69
+ class Resource < Base
70
+ attr_accessor :io
71
+
72
+ def initialize(wrapped_resource)
73
+ @wrapped = wrapped_resource
74
+ end
75
+
76
+ def [](suburl)
77
+ instance = self.class.new(@wrapped[suburl])
78
+ instance.io = @io
79
+ instance
80
+ end
81
+
82
+ def get(*args)
83
+ @wrapped.get(*args)
84
+ end
85
+
86
+ def post(*args)
87
+ @wrapped.post(*args)
88
+ end
89
+
90
+ use_retries_with :get, :post
91
+ end
92
+ end
93
+ end
@@ -2,34 +2,33 @@ require "yaml"
2
2
 
3
3
  module Tweetwine
4
4
  class StartupConfig
5
- attr_reader :options, :command, :args, :supported_commands
5
+ attr_reader :options, :command
6
6
 
7
- def initialize(supported_commands)
8
- @supported_commands = supported_commands.to_a
9
- raise ArgumentError, "Must give at least one supported command" if @supported_commands.empty?
10
- @options = {}
11
- @command = @supported_commands.first
12
- @args = []
7
+ def initialize(supported_commands, default_command, default_opts = {})
8
+ raise ArgumentError, "Must give at least one supported command" if supported_commands.empty?
9
+ raise ArgumentError, "Default command is not a supported command" unless supported_commands.include? default_command
10
+ @supported_commands, @default_command = supported_commands, default_command
11
+ @options, @command = default_opts, nil
13
12
  end
14
13
 
15
- def parse(args = [], config_file = nil, &cmd_parser)
16
- options = parse_options(args, config_file, &cmd_parser)
17
- command = if args.empty? then @supported_commands.first else args.shift.to_sym end
14
+ def parse(args = [], config_file = nil, &cmd_option_parser)
15
+ options = @options.merge(parse_options(args, config_file, &cmd_option_parser))
16
+ command = if args.empty? then @default_command else args.shift.to_sym end
18
17
  raise ArgumentError, "Unknown command" unless @supported_commands.include? command
19
- @options, @command, @args = options, command, args
18
+ @options, @command = options, command
20
19
  self
21
20
  end
22
21
 
23
22
  private
24
23
 
25
- def parse_options(args, config_file, &cmd_parser)
26
- cmd_options = if cmd_parser then parse_cmdline_args(args, &cmd_parser) else {} end
24
+ def parse_options(args, config_file, &cmd_option_parser)
25
+ cmd_options = if cmd_option_parser then parse_cmdline_args(args, &cmd_option_parser) else {} end
27
26
  config_options = if config_file && File.exists?(config_file) then parse_config_file(config_file) else {} end
28
27
  config_options.merge(cmd_options)
29
28
  end
30
29
 
31
- def parse_cmdline_args(args, &cmd_parser)
32
- cmd_parser.call(args)
30
+ def parse_cmdline_args(args, &cmd_option_parser)
31
+ cmd_option_parser.call(args)
33
32
  end
34
33
 
35
34
  def parse_config_file(config_file)
@@ -1,7 +1,7 @@
1
1
  module Tweetwine
2
2
  class UrlShortener
3
- def initialize(rest_client, options)
4
- @rest_client = rest_client
3
+ def initialize(http_client, options)
4
+ @http_client = http_client
5
5
  options = Options.new(options, "URL shortening")
6
6
  @method = (options[:method] || :get).to_sym
7
7
  @service_url = options.require :service_url
@@ -17,22 +17,27 @@ module Tweetwine
17
17
 
18
18
  def shorten(url)
19
19
  require "nokogiri"
20
- rest = case @method
20
+ response = @http_client.send(@method, *get_service_url_and_params(url))
21
+ doc = Nokogiri::HTML(response)
22
+ doc.xpath(@xpath_selector).first.to_s
23
+ end
24
+
25
+ private
26
+
27
+ def get_service_url_and_params(url_to_shorten)
28
+ case @method
21
29
  when :get
22
30
  tmp = @extra_params.dup
23
- tmp << "#{@url_param_name}=#{url}"
31
+ tmp << "#{@url_param_name}=#{url_to_shorten}"
24
32
  service_url = "#{@service_url}?#{tmp.join('&')}"
25
33
  [service_url]
26
34
  when :post
27
35
  service_url = @service_url
28
- params = @extra_params.merge({ @url_param_name.to_sym => url })
36
+ params = @extra_params.merge({ @url_param_name.to_sym => url_to_shorten })
29
37
  [service_url, params]
30
38
  else
31
39
  raise "Unrecognized HTTP request method"
32
40
  end
33
- response = @rest_client.send(@method, *rest)
34
- doc = Nokogiri::HTML(response)
35
- doc.xpath(@xpath_selector).first.to_s
36
41
  end
37
42
  end
38
43
  end
@@ -1,4 +1,5 @@
1
1
  require "time"
2
+ require "uri"
2
3
 
3
4
  module Tweetwine
4
5
  module Util
@@ -59,6 +60,19 @@ module Tweetwine
59
60
  dup_str
60
61
  end
61
62
 
63
+ def self.percent_encode(str)
64
+ URI.escape(str.to_s, /[^#{URI::PATTERN::UNRESERVED}]/)
65
+ end
66
+
67
+ def self.find_hash_path(hash, path)
68
+ return nil if hash.nil?
69
+ path = [path] if !path.is_a? Array
70
+ path.inject(hash) do |result, key|
71
+ return hash.default if key.nil? || result.nil?
72
+ result[key]
73
+ end
74
+ end
75
+
62
76
  private
63
77
 
64
78
  def self.pluralize_unit(value, unit)
data/lib/tweetwine.rb CHANGED
@@ -4,9 +4,10 @@
4
4
  options
5
5
  startup_config
6
6
  io
7
- rest_client_wrapper
7
+ retrying_http
8
8
  url_shortener
9
9
  client
10
+ cli
10
11
  }.each do |f|
11
12
  require "tweetwine/#{f}"
12
13
  end
data/test/cli_test.rb ADDED
@@ -0,0 +1,16 @@
1
+ require "test_helper"
2
+
3
+ module Tweetwine
4
+
5
+ class CLITest < Test::Unit::TestCase
6
+ context "A CLI, upon initialization" do
7
+ should "disallow using #new to create a new instance" do
8
+ assert_raise(NoMethodError) { CLI.new("-v", "test", "") {} }
9
+ end
10
+ end
11
+
12
+ # Other unit tests are meaningless. See /example directory for tests
13
+ # about the functionality of the application.
14
+ end
15
+
16
+ end