├── lib
├── ruby-hackernews
│ ├── version.rb
│ ├── services
│ │ ├── text_service.rb
│ │ ├── not_authenticated_error.rb
│ │ ├── voting_service.rb
│ │ ├── parsers
│ │ │ ├── text_parser.rb
│ │ │ ├── link_info_parser.rb
│ │ │ ├── user_info_parser.rb
│ │ │ ├── time_info_parser.rb
│ │ │ ├── voting_info_parser.rb
│ │ │ ├── comments_info_parser.rb
│ │ │ ├── entry_page_parser.rb
│ │ │ └── entry_parser.rb
│ │ ├── page_fetcher.rb
│ │ ├── signup_service.rb
│ │ ├── mechanize_context.rb
│ │ ├── user_info_service.rb
│ │ ├── login_service.rb
│ │ ├── configuration_service.rb
│ │ ├── entry_service.rb
│ │ └── comment_service.rb
│ └── domain
│ │ ├── entry
│ │ ├── user_info.rb
│ │ ├── link_info.rb
│ │ ├── voting_info.rb
│ │ ├── comments_info.rb
│ │ ├── time_info.rb
│ │ └── entry.rb
│ │ ├── user.rb
│ │ └── comment
│ │ └── comment.rb
└── ruby-hackernews.rb
├── Rakefile
├── spec
├── spec_helper.rb
└── HNAPI
│ ├── services
│ ├── entries
│ │ ├── parsers
│ │ │ ├── user_info_parser_spec.rb
│ │ │ ├── time_info_parser_spec.rb
│ │ │ ├── entry_page_parser_spec.rb
│ │ │ ├── comments_info_parser_spec.rb
│ │ │ ├── link_info_parser_spec.rb
│ │ │ ├── voting_info_parser_spec.rb
│ │ │ ├── entry_parser_spec.rb
│ │ │ ├── parser_helper.rb
│ │ │ └── hn_test_page.html
│ │ └── entry_service_spec.rb
│ └── mechanize_context_spec.rb
│ └── domain
│ └── time_info_spec.rb
├── .gitignore
├── Gemfile
├── ruby-hackernews.gemspec
└── README.rdoc
/lib/ruby-hackernews/version.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 | VERSION = "1.4.0"
3 | end
4 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | require "bundler/gem_tasks"
3 |
4 | require 'rspec/core/rake_task'
5 |
6 | RSpec::Core::RakeTask.new('spec')
7 |
8 | task :default => :spec
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $:.unshift File.join(File.dirname(__FILE__),'..','lib')
2 |
3 | require 'rubygems'
4 | require 'bundler/setup'
5 |
6 | require 'ruby-hackernews.rb'
7 |
8 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/text_service.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class TextService
4 | include MechanizeContext
5 |
6 | def get_text(page)
7 | return TextParser.new(page.search("table")[2]).parse
8 | end
9 |
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
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 | test/tmp
16 | test/version_tmp
17 | tmp
18 |
19 | nbproject
20 | *~
21 | *.save*
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/not_authenticated_error.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class NotAuthenticatedError < StandardError
4 |
5 | def message
6 | return "You need to authenticate before making this operation"
7 | end
8 |
9 | end
10 |
11 | end
12 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/voting_service.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class VotingService
4 | include MechanizeContext
5 |
6 | def vote(url)
7 | require_authentication
8 | agent.get(url)
9 | return true
10 | end
11 |
12 | end
13 |
14 | end
15 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/domain/entry/user_info.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class UserInfo
4 |
5 | attr_reader :name
6 | attr_reader :page
7 |
8 | def initialize(name, page)
9 | @name = name
10 | @page = page
11 | end
12 |
13 | end
14 |
15 | end
16 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews.rb:
--------------------------------------------------------------------------------
1 |
2 | require "ruby-hackernews/version"
3 |
4 | require 'rubygems'
5 | require 'mechanize'
6 |
7 | require 'require_all'
8 |
9 | require_all File.join(File.dirname(__FILE__), 'ruby-hackernews', 'domain')
10 | require_all File.join(File.dirname(__FILE__), 'ruby-hackernews', 'services')
11 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | group :development do
4 | # rspec is here and not in gemspec, because :require is not supported there
5 | # put every other gem in gemspec
6 | gem 'rspec', :require => "spec"
7 | end
8 |
9 | # Specify your gem's dependencies in ruby-hackernews.gemspec
10 | gemspec
11 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/parsers/text_parser.rb:
--------------------------------------------------------------------------------
1 |
2 | module RubyHackernews
3 |
4 | class TextParser
5 |
6 | def initialize(table)
7 | @target_line = table.search("tr")[3]
8 | end
9 |
10 | def parse
11 | return @target_line.search("td")[1].inner_text
12 | end
13 |
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/domain/entry/link_info.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class LinkInfo
4 |
5 | attr_reader :title
6 | attr_reader :href
7 | attr_reader :site
8 |
9 | def initialize(title, href, site)
10 | @title = title
11 | @href = href
12 | @site = site
13 | end
14 |
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/domain/entry/voting_info.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class VotingInfo
4 |
5 | attr_reader :score
6 | attr_reader :upvote
7 | attr_reader :downvote
8 |
9 | def initialize(score, upvote, downvote)
10 | @score = score
11 | @upvote = upvote
12 | @downvote = downvote
13 | end
14 |
15 | end
16 |
17 | end
18 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/page_fetcher.rb:
--------------------------------------------------------------------------------
1 |
2 | module RubyHackernews
3 |
4 | #acts as a cache
5 | class PageFetcher
6 | include MechanizeContext
7 |
8 | def initialize(page_url)
9 | @url = page_url
10 | end
11 |
12 | def page
13 | unless @page
14 | @page = agent.get(@url)
15 | end
16 | return @page
17 | end
18 |
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/domain/entry/comments_info.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class CommentsInfo
4 |
5 | attr_accessor :count
6 | attr_reader :page
7 |
8 | def initialize(count, page)
9 | @count = count
10 | @page = page
11 | end
12 |
13 | def id
14 | return page[/\d+/]
15 | end
16 |
17 | def url
18 | return page
19 | end
20 |
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/parsers/link_info_parser.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class LinkInfoParser
4 |
5 | def initialize(link_element)
6 | @element = link_element
7 | end
8 |
9 | def parse
10 | link = @element.search("a")[0]['href']
11 | title = @element.search("a")[0].inner_html
12 | site_element = @element.search("span")
13 | site = site_element.inner_html.sub("(","").sub(")","").strip if site_element.any?
14 | return LinkInfo.new(title, link, site)
15 | end
16 |
17 | end
18 |
19 | end
20 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/parsers/user_info_parser.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class UserInfoParser
4 |
5 | def initialize(second_line)
6 | @second_line = second_line
7 | end
8 |
9 | def parse
10 | return unless user_link
11 |
12 | user_name = user_link.inner_html
13 | user_page = user_link['href']
14 |
15 | UserInfo.new(user_name, user_page)
16 | end
17 |
18 | private
19 |
20 | def user_link
21 | links.find { |link| link['href'] =~ /user\?id=/ }
22 | end
23 |
24 | def links
25 | @second_line.css('a')
26 | end
27 |
28 | end
29 |
30 | end
31 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/parsers/time_info_parser.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class TimeInfoParser
4 |
5 | def initialize(second_line)
6 | @second_line = second_line
7 | end
8 |
9 | def parse
10 | return unless time_link
11 |
12 | value = time_link.text.strip.split[0].to_i
13 | unit_of_measure = time_link.text.strip.split[1]
14 |
15 | TimeInfo.new(value, unit_of_measure)
16 | end
17 |
18 | private
19 |
20 | def time_link
21 | links.find { |link| link.text =~ /\sago/ }
22 | end
23 |
24 | def links
25 | @second_line.css('a')
26 | end
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/signup_service.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class SignupService
4 | include MechanizeContext
5 |
6 | def signup(username, password)
7 | raise "You are logged in already - logout first." if authenticated?
8 | page = agent.get(ConfigurationService.base_url)
9 | login_url = page.search(".pagetop/a").last['href'].sub("/","")
10 | login_page = agent.get(ConfigurationService.base_url + login_url)
11 | form = login_page.forms[1]
12 | form.acct = username
13 | form.pw = password
14 | page = form.submit
15 | return page.title != nil
16 | end
17 |
18 | end
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/domain/entry/time_info.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class TimeInfo
4 |
5 | SECOND = 1
6 | MINUTE = 60 * SECOND
7 | HOUR = 60 * MINUTE
8 | DAY = 24 * HOUR
9 |
10 | def time
11 | return Time.now - @unit_of_measure * @value
12 | end
13 |
14 | def initialize(value, unit_of_measure)
15 | @value = value
16 | if(unit_of_measure)
17 | descriptor = unit_of_measure
18 | descriptor = unit_of_measure[0..-2] if unit_of_measure[-1].chr == 's'
19 | @unit_of_measure = self.class.const_get(descriptor.upcase)
20 | end
21 | end
22 |
23 |
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/parsers/voting_info_parser.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class VotingInfoParser
4 |
5 | def initialize(voting_element, score_element)
6 | @voting_element = voting_element
7 | @score_element = score_element
8 | end
9 |
10 | def parse
11 | upvote = @voting_element[0]['href'] if @voting_element[0]
12 | downvote = @voting_element[1]['href'] if @voting_element[1]
13 | points_element = @score_element.search("span")[0]
14 | score = points_element.inner_html.split[0].to_i if points_element
15 | return VotingInfo.new(score, upvote, downvote)
16 | end
17 |
18 | end
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/parsers/comments_info_parser.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class CommentsInfoParser
4 |
5 | def initialize(second_line)
6 | @second_line = second_line
7 | end
8 |
9 | def parse
10 | return unless comments_link
11 |
12 | comments = comments_link.text.split[0].to_i
13 | comments_page = comments_link['href']
14 |
15 | CommentsInfo.new(comments, comments_page)
16 | end
17 |
18 | private
19 |
20 | def comments_link
21 | links.find { |link| link.text =~ /comment|discuss/ }
22 | end
23 |
24 | def links
25 | @second_line.css('a')
26 | end
27 |
28 | end
29 |
30 | end
31 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/mechanize_context.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | module MechanizeContext
4 |
5 | @@contexts = {}
6 |
7 | def self.agent=(key)
8 | @@default = key
9 | end
10 |
11 | def agent
12 | @@default ||= :default
13 | @@contexts[@@default] = Mechanize.new unless @@contexts[@@default]
14 | return @@contexts[@@default]
15 | end
16 |
17 | def [](key)
18 | return @@contexts[key]
19 | end
20 |
21 | def require_authentication
22 | raise NotAuthenticatedError unless authenticated?
23 | end
24 |
25 | def authenticated?(key = :default)
26 | return @@contexts[key] && @@contexts[key].cookie_jar.jar.any?
27 | end
28 |
29 | end
30 |
31 | end
32 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/parsers/entry_page_parser.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class EntryPageParser
4 |
5 | def initialize(page)
6 | @page = page
7 | end
8 |
9 | def get_lines
10 | lines = @page.search("//table")[2].search("tr").select do |tr|
11 | tr['style'] !~ /height/
12 | end
13 | more_link = lines.last.search("a").first
14 | lines.pop if more_link && more_link.inner_html == "More"
15 | return lines
16 | end
17 |
18 | def get_next_url
19 | more_link = @page.search("//table")[2].search("tr/td/a").select { |node| node.inner_html == "More"}.first
20 | return more_link['href'] if more_link
21 | return nil
22 | end
23 |
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/parsers/user_info_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper')
3 |
4 | module RubyHackernews
5 | describe UserInfoParser do
6 |
7 | before :each do
8 | @parser = UserInfoParser.new(ParserHelper.second_line)
9 | end
10 |
11 | describe :parse do
12 |
13 | it "should pass the right user name" do
14 | UserInfo.should_receive(:new).with("johnthedebs", anything)
15 | @parser.parse
16 | end
17 |
18 | it "should pass the right user page" do
19 | UserInfo.should_receive(:new).with(anything, "user?id=johnthedebs")
20 | @parser.parse
21 | end
22 |
23 | end
24 | end
25 | end
26 |
27 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/parsers/time_info_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require File.join(File.dirname(__FILE__), 'parser_helper')
3 |
4 | module RubyHackernews
5 | describe TimeInfoParser do
6 |
7 | before :each do
8 | @parser = TimeInfoParser.new(ParserHelper.second_line)
9 | end
10 |
11 | describe :parse do
12 |
13 | it "should pass the right value as the first parameter" do
14 | TimeInfo.should_receive(:new).with(4, anything)
15 | @parser.parse
16 | end
17 |
18 | it "should pass the right UoM descriptor as the second parameter" do
19 | TimeInfo.should_receive(:new).with(anything, "hours")
20 | @parser.parse
21 | end
22 |
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/parsers/entry_page_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper')
3 |
4 | module RubyHackernews
5 | describe EntryPageParser do
6 |
7 | before :each do
8 | @page = ParserHelper.full_page
9 | end
10 |
11 | describe :get_lines do
12 | it "should always return 60 elements" do
13 | lines = EntryPageParser.new(@page).get_lines
14 | lines.length.should == 60
15 | end
16 | end
17 |
18 | describe :get_next_url do
19 | it "should get the url in 'More' element" do
20 | url = EntryPageParser.new(@page).get_next_url
21 | url.should == "/x?fnid=9LO1ba1swy"
22 | end
23 | end
24 |
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/domain/user.rb:
--------------------------------------------------------------------------------
1 |
2 | module RubyHackernews
3 |
4 | class User
5 |
6 | attr_reader :name
7 |
8 | def initialize(name)
9 | @name = name
10 | end
11 |
12 | def login(password)
13 | return LoginService.new.login(@name, password)
14 | end
15 |
16 | def signup(password)
17 | return SignupService.new.signup(@name, password)
18 | end
19 |
20 | def submissions(pages = 1)
21 | return UserInfoService.new.submissions(@name, pages)
22 | end
23 |
24 | def saved(pages = 1)
25 | return UserInfoService.new.saved(@name, pages)
26 | end
27 |
28 | def logout
29 | return LoginService.new.logout
30 | end
31 |
32 | def comments(pages = 1)
33 | return UserInfoService.new.comments(@name, pages)
34 | end
35 |
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/user_info_service.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class UserInfoService
4 | include MechanizeContext
5 |
6 | def submissions(username, pages = 1)
7 | page_url = ConfigurationService.base_url + 'submitted?id=' + username
8 | return EntryService.new.get_entries(pages, page_url)
9 | end
10 |
11 | def saved(username, pages = 1)
12 | require_authentication
13 | page_url = ConfigurationService.base_url + 'saved?id=' + username
14 | return EntryService.new.get_entries(pages, page_url)
15 | end
16 |
17 | ##This doesn't work, need a new commentService to handle the user comment page
18 | def comments(username, pages = 1)
19 | page_url = ConfigurationService.base_url + 'threads?id=' + username
20 | return CommentService.new.get_new_comments(pages,page_url)
21 | end
22 |
23 | end
24 |
25 | end
26 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/login_service.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class LoginService
4 | include MechanizeContext
5 |
6 | def login(username, password)
7 | page = agent.get(ConfigurationService.base_url)
8 | login_url = page.search(".pagetop/a").last['href'].sub("/","")
9 | login_page = agent.get(ConfigurationService.base_url + login_url)
10 | form = login_page.forms.first
11 | form.acct = username
12 | form.pw = password
13 | page = form.submit
14 | return page.title != nil
15 | end
16 |
17 | def logout
18 | require_authentication
19 | page = agent.get(ConfigurationService.base_url)
20 | login_url = page.search(".pagetop/a").last['href'].sub("/","")
21 | logout_page = agent.get(ConfigurationService.base_url + login_url)
22 | agent.cookie_jar.jar.clear
23 | return logout_page.search(".pagetop/a").last.inner_html == "login"
24 | end
25 |
26 | end
27 |
28 | end
29 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/configuration_service.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class ConfigurationService
4 |
5 | def self.base_url=(url)
6 | @base_url = url
7 | end
8 |
9 | def self.base_url
10 | return @base_url || "http://news.ycombinator.com/"
11 | end
12 |
13 | def self.new_url
14 | return File.join(self.base_url, "newest")
15 | end
16 |
17 | def self.ask_url
18 | return File.join(self.base_url, "ask")
19 | end
20 |
21 | def self.jobs_url
22 | return File.join(self.base_url, "jobs")
23 | end
24 |
25 | def self.show_url
26 | return File.join(self.base_url, "show")
27 | end
28 |
29 | def self.new_shows_url
30 | return File.join(self.base_url, "shownew")
31 | end
32 |
33 | def self.comments_url
34 | return File.join(self.base_url, "newcomments")
35 | end
36 |
37 | def self.submit_url
38 | return File.join(self.base_url, "submit")
39 | end
40 |
41 | end
42 |
43 | end
44 |
--------------------------------------------------------------------------------
/ruby-hackernews.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'ruby-hackernews/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "ruby-hackernews"
8 | spec.version = RubyHackernews::VERSION
9 | spec.authors = ["Andrea Dallera"]
10 | spec.email = ["andrea@andreadallera.com"]
11 | spec.description = %q{An interface to Hacker News}
12 | spec.summary = %q{An interface to Hacker News}
13 | spec.homepage = "http://github.com/bolthar/ruby-hackernews"
14 |
15 | spec.files = `git ls-files`.split($/)
16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18 | spec.require_paths = ["lib"]
19 |
20 | spec.has_rdoc = false
21 |
22 | spec.add_development_dependency "bundler", "~> 1.3"
23 | spec.add_development_dependency "rake"
24 |
25 | spec.add_dependency('require_all', '>= 1.1.0')
26 | spec.add_dependency('mechanize', '>= 1.0.0')
27 | end
28 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/parsers/entry_parser.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class EntryParser
4 |
5 | def initialize(first_line, second_line)
6 | @first_line = first_line
7 | @second_line = second_line
8 | end
9 |
10 | def parse
11 | first_line_title = @first_line.search("[@class='title']")
12 | number_segment = nil
13 | link_segment = nil
14 | if(first_line_title.length > 1)
15 | number_segment = first_line_title[0]
16 | link_segment = first_line_title[1]
17 | else
18 | link_segment = first_line_title[0]
19 | end
20 | number = number_segment.inner_html.sub(".","").to_i if number_segment
21 | link = LinkInfoParser.new(link_segment).parse
22 | voting = VotingInfoParser.new(@first_line.search("td/center/a"), @second_line.search("[@class='subtext']")[0]).parse
23 | user = UserInfoParser.new(@second_line).parse
24 | comments = CommentsInfoParser.new(@second_line).parse
25 | time = TimeInfoParser.new(@second_line).parse
26 | return Entry.new(number, link, voting, user, comments, time)
27 | end
28 |
29 | end
30 |
31 | end
32 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/parsers/comments_info_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper')
3 |
4 | module RubyHackernews
5 | describe CommentsInfoParser do
6 |
7 | before(:each) do
8 | @line = ParserHelper.second_line
9 | @comment_link = "item?id=1645686"
10 | end
11 |
12 | describe :parse do
13 |
14 | it "should not call CommentsInfo.new if comment tag is missing" do
15 | parser = CommentsInfoParser.new(ParserHelper.second_line_comments_tag_missing)
16 | CommentsInfo.should_not_receive(:new)
17 | parser.parse
18 | end
19 |
20 | it "should call CommentsInfo.new with comments count if not 'discuss'" do
21 | parser = CommentsInfoParser.new(@line)
22 | CommentsInfo.should_receive(:new).with(2, anything)
23 | parser.parse
24 | end
25 |
26 | it "should call CommentsInfo.new with 0 if is 'discuss'" do
27 | parser = CommentsInfoParser.new(ParserHelper.second_line_no_comments_yet)
28 | CommentsInfo.should_receive(:new).with(0, anything)
29 | parser.parse
30 | end
31 |
32 | it "should call CommentsInfo.new with comment link" do
33 | parser = CommentsInfoParser.new(@line)
34 | CommentsInfo.should_receive(:new).with(anything, @comment_link)
35 | parser.parse
36 | end
37 |
38 | end
39 |
40 | end
41 | end
42 |
43 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/mechanize_context_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module RubyHackernews
4 | describe MechanizeContext do
5 |
6 | before :each do
7 | MechanizeContext.send(:class_variable_set, :@@contexts, nil)
8 | MechanizeContext.send(:class_variable_set, :@@default, nil)
9 | end
10 |
11 | describe "agent=" do
12 |
13 | it "should set @@default as passed key" do
14 | MechanizeContext.agent = :test_agent
15 | MechanizeContext.send(:class_variable_get, :@@default).should == :test_agent
16 | end
17 |
18 | end
19 |
20 | describe "agent" do
21 |
22 | it "should return the @@default agent" do
23 | MechanizeContext.send(:class_variable_set, :@@default, :test_value)
24 | MechanizeContext.send(:class_variable_set, :@@contexts, {:test_value => :target})
25 | klass = Class.new
26 | klass.instance_eval do
27 | include MechanizeContext
28 | end
29 | klass.new.agent.should == :target
30 | end
31 |
32 | end
33 |
34 | describe "[]" do
35 |
36 | it "should return the [key] agent" do
37 | MechanizeContext.send(:class_variable_set, :@@contexts, {:test_value => :target})
38 | klass = Class.new
39 | klass.instance_eval do
40 | include MechanizeContext
41 | end
42 | klass.new[:test_value].should == :target
43 | end
44 |
45 | end
46 | end
47 | end
48 |
49 |
--------------------------------------------------------------------------------
/spec/HNAPI/domain/time_info_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | module RubyHackernews
4 | describe TimeInfo do
5 |
6 | before :all do
7 | class RubyHackernews::TimeInfo
8 | DUMMY = 10
9 | end
10 | end
11 |
12 | describe :initialize do
13 |
14 | it "should set unit_of_measure as uppercase descriptor const if singular" do
15 | timeinfo = TimeInfo.new(5, "dummy")
16 | timeinfo.instance_variable_get(:@unit_of_measure).should == 10
17 | end
18 |
19 | it "should set unit_of_measure as uppercase minus last s descriptor const if plural" do
20 | timeinfo = TimeInfo.new(5, "dummys")
21 | timeinfo.instance_variable_get(:@unit_of_measure).should == 10
22 | end
23 |
24 | end
25 |
26 | describe :time do
27 |
28 | it "should return right amount when passed seconds" do
29 | TimeInfo.new(15, "seconds").time.round.should == (Time.now - 15).round
30 | end
31 |
32 | it "should return right amount when passed minutes" do
33 | TimeInfo.new(24, "minutes").time.round.should == (Time.now - 24*60).round
34 | end
35 |
36 | it "should return right amount when passed hours" do
37 | TimeInfo.new(3, "hours").time.round.should == (Time.now - 3*3600).round
38 | end
39 |
40 | it "should return right amount when passed days" do
41 | TimeInfo.new(16, "days").time.round.should == (Time.now - 16*86400).round
42 | end
43 |
44 | end
45 | end
46 | end
47 |
48 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/parsers/link_info_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper')
3 |
4 | module RubyHackernews
5 | describe LinkInfoParser do
6 |
7 | before :each do
8 | @line = ParserHelper.link_line
9 | @correct_title = "Flow chart: How to find out which things to throw out"
10 | @correct_link = "http://mortenjust.com/2010/08/30/finding-out-which-things-to-throw-out/"
11 | @correct_site = "mortenjust.com"
12 | end
13 |
14 | describe :parse do
15 |
16 | it "should call LinkInfo.new with correct title" do
17 | parser = LinkInfoParser.new(@line)
18 | LinkInfo.should_receive(:new).with(@correct_title, anything, anything)
19 | parser.parse
20 | end
21 |
22 | it "should call LinkInfo.new with correct link" do
23 | parser = LinkInfoParser.new(@line)
24 | LinkInfo.should_receive(:new).with(anything, @correct_link, anything)
25 | parser.parse
26 | end
27 |
28 | it "should call LinkInfo.new with correct site if site is present" do
29 | parser = LinkInfoParser.new(@line)
30 | LinkInfo.should_receive(:new).with(anything, anything, @correct_site)
31 | parser.parse
32 | end
33 |
34 | it "should call LinkInfo.new with nil if site is not present" do
35 | parser = LinkInfoParser.new(ParserHelper.link_line_no_site)
36 | LinkInfo.should_receive(:new).with(anything, anything, nil)
37 | parser.parse
38 | end
39 |
40 | end
41 |
42 | end
43 | end
44 |
45 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/parsers/voting_info_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper')
3 |
4 | module RubyHackernews
5 | describe VotingInfoParser do
6 |
7 | before :each do
8 | @voting_element = ParserHelper.voting_node
9 | @score_element = ParserHelper.second_line
10 | @correct_vote_link = "vote?for=1645686&dir=up&whence=%6e%65%77%73"
11 | end
12 |
13 | describe :parse do
14 |
15 | it "should assign link.href to upvote if element not nil" do
16 | parser = VotingInfoParser.new(@voting_element, @score_element)
17 | VotingInfo.should_receive(:new).with(anything, @correct_vote_link, anything)
18 | parser.parse
19 | end
20 |
21 | it "should assing nil to upvote if element nil" do
22 | parser = VotingInfoParser.new(ParserHelper.voting_node_no_upvote, @score_element)
23 | VotingInfo.should_receive(:new).with(anything, nil, anything)
24 | parser.parse
25 | end
26 |
27 | it "should assing link.href to downvote if element not nil" do
28 | parser = VotingInfoParser.new(@voting_element, @score_element)
29 | VotingInfo.should_receive(:new).with(anything, anything, @correct_vote_link)
30 | parser.parse
31 | end
32 |
33 | it "should assing nil to downvote if element nil" do
34 | parser = VotingInfoParser.new(ParserHelper.voting_node_no_downvote, @score_element)
35 | VotingInfo.should_receive(:new).with(anything, anything, nil)
36 | parser.parse
37 | end
38 |
39 | it "should assing correct score to score" do
40 | parser = VotingInfoParser.new(@voting_element, @score_element)
41 | VotingInfo.should_receive(:new).with(17, anything, anything)
42 | parser.parse
43 | end
44 | end
45 |
46 | end
47 | end
48 |
49 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/entry_service_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | #useless purely integration tests... remove?
4 | module RubyHackernews
5 | describe EntryService do
6 |
7 | describe :get_entries do
8 |
9 | it "should always call EntryPageParser with current agent" do
10 | parser = Object.new
11 | allow(parser).to receive_messages(:get_lines => [], :get_next_url => nil)
12 | agent = Object.new
13 | allow(agent).to receive_messages(:get => :page)
14 | MechanizeContext.send(:class_variable_set, :@@contexts, {:default => agent})
15 | service = EntryService.new
16 | expect(EntryPageParser).to receive(:new).at_least(1).with(:page).and_return(parser)
17 | service.get_entries
18 | end
19 |
20 | it "should always call get_lines 'pages' number of times" do
21 | pages = 5
22 | parser = Object.new
23 | expect(parser).to receive(:get_lines).and_return([])
24 | allow(parser).to receive(:get_next_url).and_return(nil)
25 | agent = Object.new
26 | allow(agent).to receive_messages(:get => :page)
27 | MechanizeContext.send(:class_variable_set, :@@contexts, {:default => agent})
28 | service = EntryService.new
29 | allow(EntryPageParser).to receive(:new).with(:page).and_return(parser)
30 | service.get_entries
31 | end
32 |
33 | it "should always call get_next_url 'pages' number of times" do
34 | parser = Object.new
35 | allow(parser).to receive(:get_lines).and_return([])
36 | expect(parser).to receive(:get_next_url).and_return(nil)
37 | agent = Object.new
38 | allow(agent).to receive_messages(:get => :page)
39 | MechanizeContext.send(:class_variable_set, :@@contexts, {:default => agent})
40 | service = EntryService.new
41 | allow(EntryPageParser).to receive(:new).with(:page).and_return(parser)
42 | service.get_entries
43 | end
44 |
45 | end
46 | end
47 | end
48 |
49 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/parsers/entry_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require File.expand_path(File.dirname(__FILE__) + '/parser_helper')
3 |
4 | module RubyHackernews
5 | describe EntryParser do
6 |
7 | before(:each) do
8 | @first_line = ParserHelper.first_line_full
9 | @second_line = ParserHelper.second_line_full
10 | end
11 |
12 | describe :parse do
13 |
14 | it "should call Entry.new with the right number" do
15 | parser = EntryParser.new(@first_line, @second_line)
16 | Entry.should_receive(:new).with(2, anything, anything, anything, anything, anything)
17 | parser.parse
18 | end
19 |
20 | it "should return an entry with correct link" do
21 | parser = EntryParser.new(@first_line, @second_line)
22 | entry = parser.parse
23 | entry.link.title.should == "The Tragic Death of Practically Everything"
24 | entry.link.href.should == "http://technologizer.com/2010/08/18/the-tragic-death-of-practically-everything/"
25 | entry.link.site.should == "technologizer.com"
26 | end
27 |
28 | it "should return an entry with correct voting" do
29 | parser = EntryParser.new(@first_line, @second_line)
30 | entry = parser.parse
31 | entry.voting.upvote.should == "vote?for=1645745&dir=up&whence=%6e%65%77%73"
32 | entry.voting.downvote.should be_nil
33 | entry.voting.score.should == 59
34 | end
35 |
36 | it "should return an entry with correct user" do
37 | parser = EntryParser.new(@first_line, @second_line)
38 | entry = parser.parse
39 | entry.user.name.should == "dhotson"
40 | entry.user.page.should == "user?id=dhotson"
41 | end
42 |
43 | it "should return an entry with correct time" do
44 | parser = EntryParser.new(@first_line, @second_line)
45 | entry = parser.parse
46 | entry.time.round.should == (Time.now - 3600*4).round
47 | end
48 |
49 | end
50 |
51 | end
52 | end
53 |
54 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/domain/comment/comment.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class Comment
4 | include Enumerable
5 |
6 | attr_reader :text_html
7 | attr_reader :voting
8 | attr_reader :user
9 |
10 | attr_accessor :parent
11 |
12 | def initialize(text_html, voting, user_info, reply_link, absolute_link, parent_link)
13 | @text_html = text_html
14 | @voting = voting
15 | @user = user_info
16 | @reply_link = reply_link
17 | @absolute_link = absolute_link
18 | @parent_link = parent_link
19 | @children = []
20 | end
21 |
22 | def id
23 | return @absolute_link.split("=")[1].to_i
24 | end
25 |
26 | def parent_id
27 | if parent
28 | parent.id
29 | elsif @parent_link
30 | @parent_link[/\d+/].to_i
31 | end
32 | end
33 |
34 | def text
35 | @text ||= text_html.gsub(/<.{1,2}>/, "")
36 | end
37 |
38 | def <<(comment)
39 | comment.parent = self
40 | @children << comment
41 | end
42 |
43 | def each(&block)
44 | @children.each(&block)
45 | end
46 |
47 | def <=>(other_comment)
48 | return other_comment.voting.score <=> @voting.score
49 | end
50 |
51 | def method_missing(method, *args, &block)
52 | @children.send(method, *args, &block)
53 | end
54 |
55 | def self.newest(pages = 1)
56 | return CommentService.new.get_new_comments(pages)
57 | end
58 |
59 | def self.newest_with_extra(pages = 1, url = nil)
60 | args = [pages]
61 | args << url unless url.nil?
62 | return CommentService.new.get_new_comments_with_extra *args
63 | end
64 |
65 | def self.find(id)
66 | return CommentService.new.find_by_id(id)
67 | end
68 |
69 | def reply(text)
70 | return false unless @reply_link
71 | CommentService.new.write_comment(@reply_link, text)
72 | return true
73 | end
74 |
75 | def upvote
76 | VotingService.new.vote(@voting.upvote)
77 | end
78 |
79 | def downvote
80 | VotingService.new.vote(@voting.downvote)
81 | end
82 |
83 | end
84 |
85 | end
86 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/entry_service.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class EntryService
4 | include MechanizeContext
5 |
6 | def get_entries(pages = 1, url = ConfigurationService.base_url)
7 | get_entries_with_extra(pages, url)[:entries]
8 | end
9 |
10 | def get_entries_with_extra(pages = 1, url = ConfigurationService.base_url)
11 | parser = EntryPageParser.new(agent.get(url))
12 | entry_infos = []
13 | next_url = nil
14 | pages.times do
15 | lines = parser.get_lines
16 | while lines.first["class"] != "athing"
17 | lines.shift
18 | end
19 | (lines.length / 2).times do
20 | entry_infos << EntryParser.new(lines.shift, lines.shift).parse
21 | end
22 | next_url = parser.get_next_url || break
23 | parser = EntryPageParser.new(agent.get(next_url))
24 | end
25 | return {:entries => entry_infos, :next_url => next_url}
26 | end
27 |
28 | def find_by_id(id)
29 | page = agent.get(ConfigurationService.base_url + "item?id=#{id}")
30 | lines = page.search("table")[2].search("tr")
31 | return EntryParser.new(lines[0], lines[1]).parse
32 | end
33 |
34 | def get_new_entries(pages = 1)
35 | return get_entries(pages, ConfigurationService.new_url)
36 | end
37 |
38 | def get_questions(pages = 1)
39 | return get_entries(pages, ConfigurationService.ask_url)
40 | end
41 |
42 | def get_jobs(pages = 1)
43 | return get_entries(pages, ConfigurationService.jobs_url)
44 | end
45 |
46 | def get_shows(pages = 1)
47 | return get_entries(pages, ConfigurationService.show_url)
48 | end
49 |
50 | def get_new_shows(pages = 1)
51 | return get_entries(pages, ConfigurationService.new_shows_url)
52 | end
53 |
54 | def submit(title, url)
55 | require_authentication
56 | form = agent.get(ConfigurationService.submit_url).forms.first
57 | submit_link(form, title, url)
58 | return true
59 | end
60 |
61 | def ask(title, text)
62 | require_authentication
63 | form = agent.get(ConfigurationService.submit_url).forms.first
64 | submit_question(form, title, text)
65 | return true
66 | end
67 |
68 | private
69 | def submit_link(form, title, url)
70 | form.t = title
71 | form.u = url
72 | form.submit
73 | end
74 |
75 | def submit_question(form, title, text)
76 | form.t = title
77 | form.x = text
78 | form.submit
79 | end
80 |
81 | end
82 |
83 | end
84 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/domain/entry/entry.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class Entry
4 |
5 | attr_reader :number
6 | attr_reader :link
7 | attr_reader :voting
8 | attr_reader :user
9 |
10 | def initialize(number, link, voting, user, comments, time)
11 | @number = number
12 | @link = link
13 | @voting = voting
14 | @user = user
15 | @time = time
16 | @comments_info = comments
17 | if @comments_info
18 | @cache = PageFetcher.new(@comments_info.page)
19 | else
20 | @cache = PageFetcher.new(@link.href)
21 | end
22 | end
23 |
24 | def text
25 | unless @text
26 | @text = TextService.new.get_text(@cache.page)
27 | end
28 | return @text
29 | end
30 |
31 | def comments
32 | unless @comments
33 | @comments = CommentService.new.get_comments(@cache.page)
34 | end
35 | return @comments
36 | end
37 |
38 | def self.all(pages = 1)
39 | return EntryService.new.get_entries(pages)
40 | end
41 |
42 | def self.newest(pages = 1)
43 | return EntryService.new.get_new_entries(pages)
44 | end
45 |
46 | def self.newest_with_extra(pages = 1, url = nil)
47 | args = [pages]
48 | args << url unless url.nil?
49 | return EntryService.new.get_entries_with_extra *args
50 | end
51 |
52 | def self.questions(pages = 1)
53 | return EntryService.new.get_questions(pages)
54 | end
55 |
56 | def self.jobs(pages = 1)
57 | return EntryService.new.get_jobs(pages)
58 | end
59 |
60 | def self.shows(pages = 1)
61 | return EntryService.new.get_shows(pages)
62 | end
63 |
64 | def self.new_shows(pages = 1)
65 | return EntryService.new.get_new_shows(pages)
66 | end
67 |
68 | def self.find(id)
69 | return EntryService.new.find_by_id(id)
70 | end
71 |
72 | def time
73 | return @time.time
74 | end
75 |
76 | def id
77 | return @comments_info ? @comments_info.id : nil
78 | end
79 |
80 | def comments_url
81 | return @comments_info ? ConfigurationService.base_url + @comments_info.url : nil
82 | end
83 |
84 | def comments_count
85 | return @comments_info.count unless @comments_info.nil?
86 | end
87 |
88 | def write_comment(text)
89 | return CommentService.new.write_comment(@comments_info.page, text)
90 | end
91 |
92 | def self.submit(title, url)
93 | return EntryService.new.submit(title, url)
94 | end
95 |
96 | def self.ask(title, text)
97 | return EntryService.new.ask(title, text)
98 | end
99 |
100 | def upvote
101 | return VotingService.new.vote(@voting.upvote)
102 | end
103 |
104 | end
105 |
106 | end
107 |
--------------------------------------------------------------------------------
/lib/ruby-hackernews/services/comment_service.rb:
--------------------------------------------------------------------------------
1 | module RubyHackernews
2 |
3 | class CommentService
4 | include MechanizeContext
5 |
6 | def get_comments(page)
7 | table = page.search("table")[3]
8 | return get_comments_entities(table)
9 | end
10 |
11 | def get_user_comments(user)
12 | page = agent.get(ConfigurationService.base_url + "threads?id=#{user.name}")
13 | table = page.search("table")[2]
14 | return get_comments_entities(table)
15 | end
16 |
17 | def find_by_id(id)
18 | page = agent.get(ConfigurationService.base_url + "item?id=#{id}")
19 | comment = parse_comment(page.search("table")[2].search("tr").first)
20 | comment.instance_variable_set(:@absolute_link, "item?id=#{id}")
21 | get_comments_entities(page.search("table")[3]).each do |c|
22 | comment << c
23 | end
24 | return comment
25 | end
26 |
27 | def get_comments_entities(table)
28 | comments = []
29 | target = comments
30 | current_level = 0
31 | trs = table.search("table/tr").select do |tr|
32 | tr.search("span.comment").inner_html != "[deleted]"
33 | end
34 | trs.each do |tr|
35 | comment = parse_comment(tr)
36 | level = tr.search("img[@src='s.gif']").first['width'].to_i / 40
37 | difference = current_level - level
38 | (difference + 1).times do
39 | target = target.kind_of?(Comment) && target.parent ? target.parent : comments
40 | end
41 | current_level = level
42 | target << comment
43 | target = comment
44 | end
45 | return comments
46 | end
47 |
48 | def get_new_comments(pages = 1, url = ConfigurationService.comments_url)
49 | get_new_comments_with_extra(pages,url)[:comments]
50 | end
51 |
52 | def get_new_comments_with_extra(pages = 1, url = ConfigurationService.comments_url)
53 | parser = EntryPageParser.new(agent.get(url))
54 | comments = []
55 | next_url = nil
56 | pages.times do
57 | lines = parser.get_lines
58 | lines.each do |line|
59 | comments << parse_comment(line)
60 | end
61 | next_url = parser.get_next_url || break
62 | parser = EntryPageParser.new(agent.get(next_url))
63 | end
64 | return {:comments => comments, :next_url => next_url}
65 | end
66 |
67 | def parse_comment(element)
68 | text_html = element.search("span.comment").first.search("font").children.map { |x| x.inner_text }.join("\n")
69 | header = element.search("span.comhead").first
70 | voting = VotingInfoParser.new(element.search("td/center/a"), header).parse
71 | user_info = UserInfoParser.new(header).parse
72 | reply_link = element.search("td[@class='default']/p//u//a").first
73 | reply_url = reply_link['href'] if reply_link
74 | absolute_link_group = header.search("a")
75 | absolute_url = absolute_link_group.count == 2 ? absolute_link_group[1]['href'] : nil
76 | parent_link = header.search("a[text()*='parent']").first
77 | parent_url = parent_link['href'] if parent_link
78 | return Comment.new(text_html, voting, user_info, reply_url, absolute_url, parent_url)
79 | end
80 |
81 | def write_comment(page_url, comment)
82 | require_authentication
83 | form = agent.get(page_url).forms.first
84 | form.text = comment
85 | form.submit
86 | return true
87 | end
88 |
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/spec/HNAPI/services/entries/parsers/parser_helper.rb:
--------------------------------------------------------------------------------
1 |
2 | class ParserHelper
3 |
4 | #upvote: vote?for=1645686&dir=up&whence=%6e%65%77%73
5 | #downvote: nil
6 | def self.voting_node_no_downvote
7 | return Nokogiri::HTML.fragment('
'
8 | ).children
9 | end
10 |
11 | #upvote: nil
12 | #downvote: vote?for=1645686&dir=up&whence=%6e%65%77%73
13 | def self.voting_node_no_upvote
14 | return Nokogiri::HTML.fragment('
'
15 | ).children
16 | end
17 |
18 | #upvote: vote?for=1645686&dir=up&whence=%6e%65%77%73
19 | #downvote: vote?for=1645686&dir=up&whence=%6e%65%77%73
20 | def self.voting_node
21 | return Nokogiri::HTML.fragment('
'
22 | ).children
23 | end
24 |
25 | #username : johnthedebs
26 | #user page : user?id=johnthedebs
27 | #score : 17
28 | #comments : 2
29 | #comment_page : item?id=1645686
30 | def self.second_line
31 | return Nokogiri::HTML.fragment('
