ap 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -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.
@@ -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
+
@@ -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
@@ -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
+
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
1
+ module AP
2
+ # Wrapper for the Associated Press API
3
+ class Client < API
4
+ require 'ap/client/category'
5
+
6
+ include AP::Client::Category
7
+ end
8
+ end
@@ -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
@@ -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
@@ -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