tweetwine 0.2.5 → 0.2.7

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.
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