ap 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +53 -0
- data/Rakefile +40 -0
- data/VERSION +1 -0
- data/ap.gemspec +73 -0
- data/lib/ap.rb +31 -0
- data/lib/ap/api.rb +19 -0
- data/lib/ap/article.rb +40 -0
- data/lib/ap/category.rb +28 -0
- data/lib/ap/client.rb +8 -0
- data/lib/ap/client/category.rb +15 -0
- data/lib/ap/configuration.rb +47 -0
- data/lib/ap/parser.rb +19 -0
- data/lib/ap/search.rb +275 -0
- data/lib/ap/version.rb +3 -0
- data/spec/ap/api_spec.rb +22 -0
- data/spec/ap/article_spec.rb +83 -0
- data/spec/ap/category_spec.rb +50 -0
- data/spec/ap/client/category_spec.rb +74 -0
- data/spec/ap/client_spec.rb +15 -0
- data/spec/ap/parser_spec.rb +14 -0
- data/spec/ap/search_spec.rb +453 -0
- data/spec/ap_spec.rb +8 -0
- data/spec/fixtures/categories-31990.xml +529 -0
- data/spec/fixtures/categories.xml +1 -0
- data/spec/fixtures/search-obama.xml +405 -0
- data/spec/spec_helper.rb +30 -0
- metadata +169 -0
data/.document
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Alex Coomans
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
= AP - Associated Press API Gem http://travis-ci.org/drcapulet/ap.png
|
2
|
+
|
3
|
+
This is a Ruby API wrapper for the Assosciated Press API v2. This code was based on version 1.1 of the documentation, which was last revised 06/15/2010
|
4
|
+
|
5
|
+
== Usage
|
6
|
+
|
7
|
+
require 'ap'
|
8
|
+
AP.configure do |config|
|
9
|
+
config.api_key = "your_api_key"
|
10
|
+
end
|
11
|
+
# Categories & Articles
|
12
|
+
categories = AP.categories
|
13
|
+
=> [#<AP::Category:0x10226e118 @content="AP Online Top General Short Headlines", @id="category id", @title="AP Online Top General Short Headlines", @updated=Sat Apr 30 01:25:10 UTC 2011>, ....]
|
14
|
+
category = categories.first
|
15
|
+
=> #<AP::Category:0x10226e118 @content="AP Online Top General Short Headlines", @id="category id", @title="AP Online Top General Short Headlines", @updated=Sat Apr 30 01:25:10 UTC 2011>
|
16
|
+
articles = category.articles
|
17
|
+
=> [#<AP::Article:0x101e15440 @content="...html content..." @id="...AP Article ID....", @tags=["Property damage", "Tornados", "Humanitarian crises", ..., "North America"], @link="...link to AP hosted version...", @title="...article title...", @updated=Sat Apr 30 00:07:01 UTC 2011, @authors=["Author 1", "Author 2"]>
|
18
|
+
articles.first.similar
|
19
|
+
=> #<AP::Search:0x1018aa618 @query={:count=>20, :searchTerms=>"...article id...", :startPage=>1}, @search_type="similar", @total_results=0>
|
20
|
+
# Searching
|
21
|
+
search = AP::Search.new
|
22
|
+
=> #<AP::Search:0x1018bb760 @query={:count=>20, :searchTerms=>[], :startPage=>1}, @search_type="request", @total_results=0>
|
23
|
+
search.contains("Obama").and.contains("Iraq")
|
24
|
+
=> #<AP::Search:0x101891870 @query={:count=>20, :searchTerms=>["Obama", "AND", "Iraq"], :startPage=>1}, @search_type="request", @total_results=0>
|
25
|
+
search.fetch
|
26
|
+
=> An array of AP::Articles
|
27
|
+
search.next_page?
|
28
|
+
=> true
|
29
|
+
search.next_page
|
30
|
+
=> An array of AP::Articles
|
31
|
+
search.clear
|
32
|
+
search.scoped do |s|
|
33
|
+
s.contains("Obama").or.contains("Iraq")
|
34
|
+
end.and.contains("Iran")
|
35
|
+
search.to_s
|
36
|
+
=> "( Obama OR Iraq ) AND Iran"
|
37
|
+
|
38
|
+
Why is all the inofrmation missing? I'm not sure exactly what I can share, so I'm being careful. Either way it gets the information across.
|
39
|
+
|
40
|
+
== Contributing to ap
|
41
|
+
|
42
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
43
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
44
|
+
* Fork the project
|
45
|
+
* Start a feature/bugfix branch
|
46
|
+
* Commit and push until you are happy with your contribution
|
47
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
48
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
49
|
+
|
50
|
+
== Copyright
|
51
|
+
|
52
|
+
Copyright (c) 2011 Alex Coomans. See LICENSE.txt for further details.
|
53
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
Bundler::GemHelper.install_tasks
|
13
|
+
|
14
|
+
|
15
|
+
begin
|
16
|
+
require 'rspec/core/rake_task'
|
17
|
+
RSpec::Core::RakeTask.new(:spec)
|
18
|
+
|
19
|
+
task :test => :spec
|
20
|
+
task :default => :spec
|
21
|
+
|
22
|
+
# require 'rcov/rcovtask'
|
23
|
+
# Rcov::RcovTask.new do |test|
|
24
|
+
# test.libs << 'test'
|
25
|
+
# test.pattern = 'test/**/test_*.rb'
|
26
|
+
# test.verbose = true
|
27
|
+
# end
|
28
|
+
|
29
|
+
require 'rake/rdoctask'
|
30
|
+
Rake::RDocTask.new do |rdoc|
|
31
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
32
|
+
|
33
|
+
rdoc.rdoc_dir = 'rdoc'
|
34
|
+
rdoc.title = "ap #{version}"
|
35
|
+
rdoc.rdoc_files.include('README*')
|
36
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
37
|
+
end
|
38
|
+
rescue LoadError => e
|
39
|
+
puts e
|
40
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
data/ap.gemspec
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{ap}
|
8
|
+
s.version = "0.1.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Alex Coomans"]
|
12
|
+
s.date = %q{2011-05-10}
|
13
|
+
s.description = %q{Ruby gem for interfacing with the Associated Press Breaking News API}
|
14
|
+
s.email = %q{alex@alexcoomans.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
"Gemfile",
|
22
|
+
"LICENSE.txt",
|
23
|
+
"README.rdoc",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"ap.gemspec",
|
27
|
+
"lib/ap.rb",
|
28
|
+
"lib/ap/api.rb",
|
29
|
+
"lib/ap/article.rb",
|
30
|
+
"lib/ap/category.rb",
|
31
|
+
"lib/ap/client.rb",
|
32
|
+
"lib/ap/client/category.rb",
|
33
|
+
"lib/ap/configuration.rb",
|
34
|
+
"lib/ap/parser.rb",
|
35
|
+
"lib/ap/search.rb",
|
36
|
+
"lib/ap/version.rb",
|
37
|
+
"spec/ap/api_spec.rb",
|
38
|
+
"spec/ap/article_spec.rb",
|
39
|
+
"spec/ap/category_spec.rb",
|
40
|
+
"spec/ap/client/category_spec.rb",
|
41
|
+
"spec/ap/client_spec.rb",
|
42
|
+
"spec/ap/parser_spec.rb",
|
43
|
+
"spec/ap/search_spec.rb",
|
44
|
+
"spec/ap_spec.rb",
|
45
|
+
"spec/fixtures/categories-31990.xml",
|
46
|
+
"spec/fixtures/categories.xml",
|
47
|
+
"spec/fixtures/search-obama.xml",
|
48
|
+
"spec/spec_helper.rb"
|
49
|
+
]
|
50
|
+
s.homepage = %q{http://github.com/drcapulet/ap}
|
51
|
+
s.licenses = ["MIT"]
|
52
|
+
s.require_paths = ["lib"]
|
53
|
+
s.rubygems_version = %q{1.6.1}
|
54
|
+
s.summary = %q{Ruby Associated Press API Gem}
|
55
|
+
s.test_files = [
|
56
|
+
"spec/ap/api_spec.rb",
|
57
|
+
"spec/ap/article_spec.rb",
|
58
|
+
"spec/ap/category_spec.rb",
|
59
|
+
"spec/ap/client/category_spec.rb",
|
60
|
+
"spec/ap/client_spec.rb",
|
61
|
+
"spec/ap/parser_spec.rb",
|
62
|
+
"spec/ap/search_spec.rb",
|
63
|
+
"spec/ap_spec.rb",
|
64
|
+
"spec/spec_helper.rb"
|
65
|
+
]
|
66
|
+
|
67
|
+
s.required_rubygems_version = ">= 1.3.6"
|
68
|
+
s.add_runtime_dependency(%q<httparty>, [">= 0.7.7"])
|
69
|
+
s.add_development_dependency(%q<rspec>, [">= 2.5.0"])
|
70
|
+
s.add_development_dependency(%q<webmock>, [">= 1.6.2"])
|
71
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
72
|
+
end
|
73
|
+
|
data/lib/ap.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'crack/xml'
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
require 'ap/version'
|
6
|
+
require 'ap/parser'
|
7
|
+
require 'ap/category'
|
8
|
+
require 'ap/article'
|
9
|
+
require 'ap/api'
|
10
|
+
require 'ap/client'
|
11
|
+
require 'ap/configuration'
|
12
|
+
require 'ap/search'
|
13
|
+
|
14
|
+
|
15
|
+
module AP
|
16
|
+
extend Configuration
|
17
|
+
|
18
|
+
# Alias for AP::Client.new
|
19
|
+
def self.client(options = {})
|
20
|
+
AP::Client.new(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.method_missing(method, *args, &block)
|
24
|
+
return super unless client.respond_to?(method)
|
25
|
+
client.send(method, *args, &block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.respond_to?(method, include_private = false)
|
29
|
+
client.respond_to?(method, include_private) || super(method, include_private)
|
30
|
+
end
|
31
|
+
end
|
data/lib/ap/api.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module AP
|
2
|
+
class API
|
3
|
+
include HTTParty
|
4
|
+
format :ap_xml
|
5
|
+
base_uri 'developerapi.ap.org'
|
6
|
+
|
7
|
+
class MissingAPIKeyError < StandardError; def to_s; "You didn't provide an API key"; end; end
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
options = AP.options.merge(options)
|
11
|
+
self.class.default_params :apiKey => options[:api_key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.get(*args)
|
15
|
+
raise MissingAPIKeyError if default_params.nil? || default_params[:apiKey].nil? || default_params[:apiKey].empty?
|
16
|
+
super
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/ap/article.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
module AP
|
2
|
+
class Article
|
3
|
+
attr_accessor :id, :title, :authors, :tags, :link, :content, :updated
|
4
|
+
|
5
|
+
# Creates a new AP::Article object given the following attributes
|
6
|
+
# - id: the article id as reported by the AP
|
7
|
+
# - title: the title of the article
|
8
|
+
# - authors: an Array of the name(s) of the author(s) of this article
|
9
|
+
# - tags: and array of tags or categories that have been attached to this article
|
10
|
+
# - link: string with the hosted version on the AP site
|
11
|
+
# - content: the article content
|
12
|
+
# - updated: Time object of when the article was last updated
|
13
|
+
def initialize(opts = {})
|
14
|
+
@id = opts[:id]
|
15
|
+
@title = opts[:title]
|
16
|
+
@tags = opts[:tags] || []
|
17
|
+
@authors = opts[:authors] || []
|
18
|
+
@link = opts[:link]
|
19
|
+
@content = opts[:content]
|
20
|
+
@updated = opts[:updated]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Creates a new object from data returned by the API
|
24
|
+
def self.new_from_api_data(data)
|
25
|
+
if data["author"].is_a?(Hash)
|
26
|
+
authors = [ data["author"]["name"] ]
|
27
|
+
elsif data["author"].is_a?(Array)
|
28
|
+
authors = data["author"].collect{ |x| x["name"] }
|
29
|
+
end
|
30
|
+
categories = data["category"] ? data["category"].collect { |x| x["label"] } : []
|
31
|
+
return new(:id => data["id"].split(":").last, :title => data["title"], :authors => authors, :tags => categories, :link => data["link"]["href"], :content => data["content"], :updated => Time.parse(data["updated"]))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns a search object that when fetched, will find articles
|
35
|
+
# similar to this one. Refer to AP::Search for more information
|
36
|
+
def similar
|
37
|
+
return AP::Search.similar(@id)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/ap/category.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module AP
|
2
|
+
class Category
|
3
|
+
attr_accessor :id, :title, :content, :updated
|
4
|
+
|
5
|
+
|
6
|
+
# Creates a new AP::Category object given the following attributes
|
7
|
+
# - id: the category id as reported by the AP
|
8
|
+
# - title: the title/name of the category
|
9
|
+
# - content: the category content. most often is the same as the title
|
10
|
+
# - updated: Time object of when the article was last updated
|
11
|
+
def initialize(opts = {})
|
12
|
+
@id = opts[:id]
|
13
|
+
@title = opts[:title]
|
14
|
+
@content = opts[:content]
|
15
|
+
@updated = opts[:updated]
|
16
|
+
end
|
17
|
+
|
18
|
+
# Creates a new object from data returned by the API
|
19
|
+
def self.new_from_api_data(data)
|
20
|
+
return new(:id => data["id"].split(":").last, :title => data["title"], :content => data["content"], :updated => Time.parse(data["updated"]))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns an array of AP::Article objects that represent recent news in this category
|
24
|
+
def articles
|
25
|
+
return AP.category(@id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/ap/client.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module AP
|
2
|
+
class Client
|
3
|
+
module Category
|
4
|
+
# Returns an array of AP::Category objects representing the news categories
|
5
|
+
def categories
|
6
|
+
self.class.get("/v2/categories.svc/")["feed"]["entry"].collect { |e| AP::Category.new_from_api_data(e) }
|
7
|
+
end
|
8
|
+
|
9
|
+
# Returns an array of AP::Articles objects representing recent news in a category
|
10
|
+
def category(id)
|
11
|
+
self.class.get("/v2/categories.svc/#{id}")["feed"]["entry"].collect { |e| AP::Article.new_from_api_data(e) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module AP
|
2
|
+
# thanks to jnunemaker's twitter gem for this
|
3
|
+
# Defines constants and methods related to configuration
|
4
|
+
module Configuration
|
5
|
+
# An array of valid keys in the options hash
|
6
|
+
VALID_OPTIONS_KEYS = [
|
7
|
+
:api_key,
|
8
|
+
:user_agent,
|
9
|
+
:search_query_defaults].freeze
|
10
|
+
|
11
|
+
# @private
|
12
|
+
attr_accessor *VALID_OPTIONS_KEYS
|
13
|
+
|
14
|
+
# When this module is extended, set all configuration options to their default values
|
15
|
+
def self.extended(base)
|
16
|
+
base.reset
|
17
|
+
end
|
18
|
+
|
19
|
+
# Convenience method to allow configuration options to be set in a block
|
20
|
+
def configure
|
21
|
+
yield self
|
22
|
+
end
|
23
|
+
|
24
|
+
# Create a hash of options and their values
|
25
|
+
def options
|
26
|
+
options = {}
|
27
|
+
VALID_OPTIONS_KEYS.each{|k| options[k] = send(k) }
|
28
|
+
options
|
29
|
+
end
|
30
|
+
|
31
|
+
DEFAULT_API_KEY = nil
|
32
|
+
|
33
|
+
DEFAULT_USER_AGENT = "Ruby AP Gem #{::AP::VERSION}".freeze
|
34
|
+
|
35
|
+
DEFAULT_SEARCH_SETTINGS = {
|
36
|
+
:count => 20
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
# Reset all configuration options to defaults
|
40
|
+
def reset
|
41
|
+
self.api_key = DEFAULT_API_KEY
|
42
|
+
self.user_agent = DEFAULT_USER_AGENT
|
43
|
+
self.search_query_defaults = DEFAULT_SEARCH_SETTINGS
|
44
|
+
self
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/ap/parser.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# This is a monkey-patch to HTTParty because the AP API doesn't return the HTML in the <content>
|
2
|
+
# tag in CDATA tags, so we need to gsub the response to add them before being parsed
|
3
|
+
class HTTParty::Parser
|
4
|
+
SupportedFormats.merge!(
|
5
|
+
{
|
6
|
+
'text/xml' => :ap_xml,
|
7
|
+
'application/xml' => :ap_xml
|
8
|
+
}
|
9
|
+
)
|
10
|
+
|
11
|
+
# Fixes and parses the XML returned by the AP
|
12
|
+
# Why is it broken? The HTML content doesn't include CDATA tags
|
13
|
+
def ap_xml
|
14
|
+
# other gsub could be negaitve /<content?([A-Za-z "=]+)>(?!<\!\[CDATA\[)/
|
15
|
+
# but CS theory says that isn't a good idea, and so does running time tests
|
16
|
+
Crack::XML.parse(body.gsub(/<content?([A-Za-z "=]+)><\!\[CDATA\[/, '<content>').gsub(/\]\]><\/content>/, "</content>").gsub(/<content?([A-Za-z "=]+)>/, "<content><![CDATA[").gsub(/<\/content>/, "]]></content>"))
|
17
|
+
# Crack::XML.parse(body.gsub(/<content?([A-Za-z "=]+)>(?!<\!\[CDATA\[)/, "<content><![CDATA[").gsub(/<\/content>/, "]]></content>"))
|
18
|
+
end
|
19
|
+
end
|
data/lib/ap/search.rb
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
module AP
|
2
|
+
class Search < API
|
3
|
+
attr_reader :query, :search_type
|
4
|
+
|
5
|
+
# Error class for when unsupported methords are called on searches that
|
6
|
+
# don't have the seach_type to "request" (that is the default). This error
|
7
|
+
# is railed on objects obtained by the similar(id) method
|
8
|
+
class UnsupportedSearchMethod < StandardError; def to_s; "This method isn't supported for this search type"; end; end
|
9
|
+
class InvalidGeocodinates < StandardError; def to_s; "Latitude must be between -90 and 90 and longitude must be between -180 and 180"; end; end
|
10
|
+
|
11
|
+
# Returns a new Search object
|
12
|
+
def initialize
|
13
|
+
clear
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a new Search object that will search for articled
|
18
|
+
# similar to the one provided by the id parameter
|
19
|
+
# The id parameter will the same as the id returned by an AP::Article
|
20
|
+
# object. Supports limited functionality compared to a standard object:
|
21
|
+
# - clear
|
22
|
+
# - geocode
|
23
|
+
# - location
|
24
|
+
# - sort_by_locaation
|
25
|
+
# - to_s
|
26
|
+
# - per_page
|
27
|
+
# - page
|
28
|
+
# - next_page?
|
29
|
+
# - next_page
|
30
|
+
# - fetch
|
31
|
+
def self.similar(id)
|
32
|
+
obj = self.new
|
33
|
+
obj.instance_variable_set(:@search_type, "similar")
|
34
|
+
obj.instance_variable_set(:@query, obj.query.merge!(AP.search_query_defaults).merge!({ :searchTerms => id }))
|
35
|
+
return obj
|
36
|
+
end
|
37
|
+
|
38
|
+
# Resets every parameter of a Search object
|
39
|
+
# When called upon a Search object with a search type of similar
|
40
|
+
# it will reset it a request search
|
41
|
+
def clear
|
42
|
+
@query = {}
|
43
|
+
@query[:searchTerms] = []
|
44
|
+
@query[:startPage] = 1
|
45
|
+
@query[:count] = 20
|
46
|
+
@total_results = 0
|
47
|
+
@search_type = "request"
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# Basic Keyword Search
|
52
|
+
# A basic query contains one or more words and no operators.
|
53
|
+
# Sample Query Returned Results
|
54
|
+
# Iraq Returns all documents containing the word “Iraq” and related word variations, such as “Iraqi”, but not “Iran”.
|
55
|
+
# iraq Returns the same results as Iraq (case is ignored).
|
56
|
+
# Obama Iraq Obama Returns the same results as Obama Iraq (repeated words are ignored).
|
57
|
+
# Example Usage:
|
58
|
+
# search.containing("obama")
|
59
|
+
# search.containing("iraq")
|
60
|
+
# search.contains("iraq")
|
61
|
+
# search.q("iraq")
|
62
|
+
# Aliased as contains and q
|
63
|
+
def containing(query)
|
64
|
+
raise UnsupportedSearchMethod unless @search_type == "request"
|
65
|
+
@query[:searchTerms] << query
|
66
|
+
return self
|
67
|
+
end
|
68
|
+
alias :contains :containing
|
69
|
+
alias :q :containing
|
70
|
+
|
71
|
+
# Exact Keyword Search (quotation marks)
|
72
|
+
# Sample Query Returned Results
|
73
|
+
# "Iraq" Returns all documents containing the word “Iraq”. Since stemming is not applied to the words in quotation marks, the query will match “Iraq” but not “Iraqi”.
|
74
|
+
# "iraq" Returns the same results as Iraq (case is still ignored in quoted text).
|
75
|
+
# "Barack Obama" Iraq Returns all documents containing “Barack Obama” and “Iraq”. Stemming is applied to “Iraq”, so the query will match “Barack Obama announces Iraqi elections”, but will not match “President Obama visits Iraq”.
|
76
|
+
# "The Who" Performed Stop words are not ignored in the quoted text. This query will match “The Who performed at MSG”, but will not match “Who performed at MSG?”
|
77
|
+
# Example Usage:
|
78
|
+
# For the query "Iraq":
|
79
|
+
# search.exact("Iraq")
|
80
|
+
# For the query "Barack Obama" Iraq:
|
81
|
+
# search.exact("Brack Obama")
|
82
|
+
# search.contains("Iraq")
|
83
|
+
def exact(query)
|
84
|
+
raise UnsupportedSearchMethod unless @search_type == "request"
|
85
|
+
@query[:searchTerms] << "\"#{query}\""
|
86
|
+
return self
|
87
|
+
end
|
88
|
+
|
89
|
+
# Wildcard search for one character
|
90
|
+
# Sample Query Returned Results
|
91
|
+
# Ira? This query matches any four-letter word beginning with “ira.” It matches “Iraq” and “Iran,” but does not match “Iris,” “IRA,” “miracle,” “IRAAM” or “Aardvark.”
|
92
|
+
# Obama AND ira? This search returns any document containing “Obama” and any four-letter word beginning with “ira.” This query will match “Obama visits Iraq” or “Obama visits Iran.” It will not match “Will Obama meet the IRA?”
|
93
|
+
# Obama AND "ira?" Wildcards are considered even when the term is enclosed in quotation marks. This query is equivalent to Obama AND ira?
|
94
|
+
# Example usage:
|
95
|
+
# For the query Ira?
|
96
|
+
# search.matches("Ira")
|
97
|
+
def matches(prefix)
|
98
|
+
raise UnsupportedSearchMethod unless @search_type == "request"
|
99
|
+
@query[:searchTerms] << prefix.to_s + "?"
|
100
|
+
return self
|
101
|
+
end
|
102
|
+
|
103
|
+
# Matches words beginning with passed string
|
104
|
+
# Sample Query Returned Results
|
105
|
+
# ira* This query matches any word beginning with “ira.” It matches “Iraq,” “Iran,” “IRA” and “IRAAM.” It does not match “Iris,” “miracle” or “aardvark.”
|
106
|
+
# Example usage:
|
107
|
+
# search.loose_match("ira")
|
108
|
+
def loose_match(str) # loose match
|
109
|
+
raise UnsupportedSearchMethod unless @search_type == "request"
|
110
|
+
@query[:searchTerms] << str.to_s + "*"
|
111
|
+
return self
|
112
|
+
end
|
113
|
+
|
114
|
+
# Filter search results to latitude & longitude
|
115
|
+
# within a specific radius
|
116
|
+
# Parameters:
|
117
|
+
# - latitude: The latitude of the location. The range of possible values is -90 to 90.
|
118
|
+
# - longitude: The longitude of the location. The range of possible values is -180 to 180. (Note: If both latitude and longitude are specified, they wil take priority over all other location parameters - for example the location method)
|
119
|
+
# - radius: The distance in miles from the specified location. The default is 50
|
120
|
+
# Example:
|
121
|
+
# search.geocode(37.760401, -122.416534)
|
122
|
+
# The example above would limit results to the San Francisco bay area, shown by this map[http://www.freemaptools.com/radius-around-point.htm?clat=37.760401&clng=-122.41653400000001&r=80.47&lc=FFFFFF&lw=1&fc=00FF00]
|
123
|
+
def geocode(latitude, longitude, radius = 50)
|
124
|
+
raise InvalidGeocodinates unless (-90 <= latitude && latitude <= 90 && -180 <= longitude && longitude <= 180)
|
125
|
+
@query[:latitude] = latitude
|
126
|
+
@query[:longitude] = longitude
|
127
|
+
@query[:radius] = radius
|
128
|
+
return self
|
129
|
+
end
|
130
|
+
|
131
|
+
# Filter a search around a City/State/Zip Code
|
132
|
+
# Valid combinations:
|
133
|
+
# - US zip code
|
134
|
+
# - City, State
|
135
|
+
# - City, State, Zip
|
136
|
+
# Note: If zip code is specified, it will take priority over city and state.
|
137
|
+
# The options hash takes three parameters:
|
138
|
+
# - :city
|
139
|
+
# - :state should be in two letter form; e.g. TX for Texas, AZ for Arizona
|
140
|
+
# - :zip_code
|
141
|
+
# Examples:
|
142
|
+
# search.location(:city => "Fremont", :state => "CA", :zip_code => "94536")
|
143
|
+
# search.location(:city => "Los Angeles", :state => "CA")
|
144
|
+
# search.location(:zip_code => "99652")
|
145
|
+
def location(opts = {})
|
146
|
+
if opts[:city] && opts[:state] && opts[:zip_code]
|
147
|
+
@query[:location] = opts[:city] + ", " + opts[:state] + ", " + opts[:zip_code].to_s
|
148
|
+
elsif opts[:zip_code]
|
149
|
+
@query[:location] = opts[:zip_code].to_s
|
150
|
+
elsif opts[:city] && opts[:state]
|
151
|
+
@query[:location] = opts[:city] + ", " + opts[:state]
|
152
|
+
end
|
153
|
+
return self
|
154
|
+
end
|
155
|
+
|
156
|
+
# Orders results by proximity to the specified location
|
157
|
+
# Default parameter is true
|
158
|
+
# Examples:
|
159
|
+
# search.sort_by_location # will sort by location
|
160
|
+
# search.sort_by_location(true) # same as above
|
161
|
+
# search.sort_by_location(false) # will not sort by proximity
|
162
|
+
def sort_by_location(sort = true)
|
163
|
+
@query[:sortByLocation] = sort
|
164
|
+
return self
|
165
|
+
end
|
166
|
+
|
167
|
+
# Scopes all of the following commands in parentheses to specify order.
|
168
|
+
# It yields itself during the block, so it's the exact same object you
|
169
|
+
# have been working with
|
170
|
+
# Examples:
|
171
|
+
# search.scoped do |s|
|
172
|
+
# s.contains("Obama")
|
173
|
+
# s.or()
|
174
|
+
# s.contains("Iraq")
|
175
|
+
# end
|
176
|
+
# search.and()
|
177
|
+
# search.contains("Iran")
|
178
|
+
# Will produce the query: (Obama OR Iraq) AND Iran
|
179
|
+
def scoped(&block)
|
180
|
+
raise UnsupportedSearchMethod unless @search_type == "request"
|
181
|
+
@query[:searchTerms] << "("
|
182
|
+
yield self
|
183
|
+
@query[:searchTerms] << ")"
|
184
|
+
return self
|
185
|
+
end
|
186
|
+
|
187
|
+
# Returns the query represented in string form
|
188
|
+
# the way it will be submitted to the api
|
189
|
+
def to_s
|
190
|
+
return @query[:searchTerms].join(" ")
|
191
|
+
end
|
192
|
+
|
193
|
+
# Represents the AND boolean operator in the query
|
194
|
+
# Sample Query Returned Results
|
195
|
+
# Obama AND Iraq AND election Returns all documents containing all of the words “Obama,” “Iraq,” and “election.” This is equivalent to Obama Iraq Election.
|
196
|
+
# Example:
|
197
|
+
# search.contains("Obama")
|
198
|
+
# search.and()
|
199
|
+
# search.contains("Iraq")
|
200
|
+
# Produces: Obama AND Iraw
|
201
|
+
def and
|
202
|
+
raise UnsupportedSearchMethod unless @search_type == "request"
|
203
|
+
@query[:searchTerms] << "AND" unless(@query[:searchTerms].last == "(" || @query[:searchTerms].last == nil || @query[:searchTerms].last == "OR" || @query[:searchTerms].last == "AND" || @query[:searchTerms].last == "AND NOT")
|
204
|
+
return self
|
205
|
+
end
|
206
|
+
|
207
|
+
# Represents the OR boolean operator in the query
|
208
|
+
# Sample Query Returned Results
|
209
|
+
# Obama OR Iraq Returns all documents containing either “Obama” or “Iraq.” The query will match both “Barack Obama” and “Iraqi elections.”
|
210
|
+
# Example:
|
211
|
+
# search.contains("Obama")
|
212
|
+
# search.or()
|
213
|
+
# search.contains("Iraq")
|
214
|
+
# Produces: Obama OR Iraq
|
215
|
+
def or
|
216
|
+
raise UnsupportedSearchMethod unless @search_type == "request"
|
217
|
+
@query[:searchTerms] << "OR" unless(@query[:searchTerms].last == "(" || @query[:searchTerms].last == nil || @query[:searchTerms].last == "OR" || @query[:searchTerms].last == "AND" || @query[:searchTerms].last == "AND NOT")
|
218
|
+
return self
|
219
|
+
end
|
220
|
+
|
221
|
+
# Represents the AND NOT boolean operator in the query
|
222
|
+
# Sample Query Returned Results
|
223
|
+
# Obama AND Iraq AND NOT Iran Returns all documents that contain both “Obama” and “Iraq,” but not “Iran.”
|
224
|
+
# Example:
|
225
|
+
# search.contains("Obama")
|
226
|
+
# search.and()
|
227
|
+
# search.contains("Iraq")
|
228
|
+
# search.and_not()
|
229
|
+
# search.contains("Iran")
|
230
|
+
# Produces: Obama AND Iraq AND NOT Iran
|
231
|
+
def and_not
|
232
|
+
raise UnsupportedSearchMethod unless @search_type == "request"
|
233
|
+
@query[:searchTerms] << "AND NOT" unless(@query[:searchTerms].last == "(" || @query[:searchTerms].last == nil || @query[:searchTerms].last == "OR" || @query[:searchTerms].last == "AND" || @query[:searchTerms].last == "AND NOT")
|
234
|
+
return self
|
235
|
+
end
|
236
|
+
|
237
|
+
# Sets the number of results to return per page
|
238
|
+
# Defaults to 20
|
239
|
+
def per_page(pp = 20)
|
240
|
+
@query[:count] = pp
|
241
|
+
return self
|
242
|
+
end
|
243
|
+
|
244
|
+
# Sets the page to the parameter so you can fetch it
|
245
|
+
def page(p = 1)
|
246
|
+
@query[:startPage] = p
|
247
|
+
return self
|
248
|
+
end
|
249
|
+
|
250
|
+
# Returns whether or not there is a next page
|
251
|
+
def next_page?
|
252
|
+
return (@query[:startPage] * (@query[:count] + 1)) < @total_results
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns the next page if next_page? is true
|
256
|
+
def next_page
|
257
|
+
if next_page?
|
258
|
+
@query[:startPage] += 1
|
259
|
+
fetch
|
260
|
+
end
|
261
|
+
end
|
262
|
+
alias :fetch_next_page :next_page
|
263
|
+
|
264
|
+
# Fetches and parses the search response. An array of AP::Article objects
|
265
|
+
# are returned
|
266
|
+
# Example:
|
267
|
+
# search.contains("Obama").and.contains("Iraq").fetch
|
268
|
+
def fetch
|
269
|
+
data = self.class.get("/v2/search.svc/#{@search_type}/", :query => @query.merge({ :searchTerms => CGI.escape(@query[:searchTerms].join(" ")) }))
|
270
|
+
r = data["feed"]["entry"].collect { |e| AP::Article.new_from_api_data(e) }
|
271
|
+
@total_results = data["feed"]["opensearch:totalResults"].to_i
|
272
|
+
return r
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|