outside-in 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -1
- data/Gemfile +11 -0
- data/Gemfile.lock +24 -0
- data/HISTORY +2 -0
- data/README.md +101 -0
- data/Rakefile +13 -6
- data/VERSION +1 -1
- data/config/oi.sample.yml +2 -0
- data/lib/outside_in/base.rb +79 -0
- data/lib/outside_in/category.rb +22 -0
- data/lib/outside_in/location.rb +61 -0
- data/lib/outside_in/resource/base.rb +91 -0
- data/lib/outside_in/resource/location_finder.rb +29 -0
- data/lib/outside_in/resource/query_params.rb +60 -0
- data/lib/outside_in/resource/story_finder.rb +31 -0
- data/lib/outside_in/story.rb +108 -26
- data/lib/outside_in/tag.rb +16 -5
- data/lib/outside_in.rb +76 -13
- data/outside-in.gemspec +21 -15
- data/tasks/oi.thor +173 -0
- metadata +22 -15
- data/LICENSE +0 -21
- data/README +0 -9
- data/lib/outside_in/place.rb +0 -21
- data/lib/outside_in/radar.rb +0 -48
- data/lib/outside_in/tweet.rb +0 -26
data/.gitignore
CHANGED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activesupport (3.0.0)
|
5
|
+
crack (0.1.8)
|
6
|
+
httparty (0.6.1)
|
7
|
+
crack (= 0.1.8)
|
8
|
+
json (1.4.6)
|
9
|
+
simple_uuid (0.1.1)
|
10
|
+
text-hyphen (1.0.0)
|
11
|
+
text-reform (0.2.0)
|
12
|
+
text-hyphen (~> 1.0.0)
|
13
|
+
thor (0.14.0)
|
14
|
+
|
15
|
+
PLATFORMS
|
16
|
+
ruby
|
17
|
+
|
18
|
+
DEPENDENCIES
|
19
|
+
activesupport (= 3.0.0)
|
20
|
+
httparty
|
21
|
+
json
|
22
|
+
simple_uuid
|
23
|
+
text-reform
|
24
|
+
thor
|
data/HISTORY
ADDED
data/README.md
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
Ruby SDK for the [Outside.in API](http://developers.outside.in/)
|
2
|
+
|
3
|
+
## Prerequisites
|
4
|
+
|
5
|
+
### Developer account
|
6
|
+
|
7
|
+
To make API requests, register for an account at [developers.outside.in](http://developers.outside.in/) to receive a developer key and the shared secret you'll use with the key to sign requests.
|
8
|
+
|
9
|
+
### Dependencies
|
10
|
+
|
11
|
+
Gem dependencies are managed with [Bundler](http://gembundler.com/). Install them like so:
|
12
|
+
|
13
|
+
$ bundle install
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
require 'outside_in'
|
18
|
+
|
19
|
+
OutsideIn.key = 'faffledweomercraft'
|
20
|
+
OutsideIn.secret = 'deadbeef'
|
21
|
+
OutsideIn.logger.level = Logger::DEBUG # defaults to WARN
|
22
|
+
|
23
|
+
# find locations by name
|
24
|
+
# returns a hash:
|
25
|
+
# * total - the total number of matched stories
|
26
|
+
# * locations - the array of matched locations up to the specified limit (default 10)
|
27
|
+
data = OutsideIn::Location.named("Brooklyn")
|
28
|
+
puts "Total matches: #{data[:total]}"
|
29
|
+
data[:locations].each {|loc| puts " #{loc.display_name}"}
|
30
|
+
|
31
|
+
# displays:
|
32
|
+
# Total matches: 624
|
33
|
+
# Brooklyn, NY
|
34
|
+
# Brooklyn, IL
|
35
|
+
# Brooklyn, IN
|
36
|
+
# etc.
|
37
|
+
|
38
|
+
# all story finders return a hash:
|
39
|
+
# * total - the total number of matched stories
|
40
|
+
# * stories - the array of matched stories up to the specified limit (default 10)
|
41
|
+
# * location - the identified location -OR-
|
42
|
+
# * locations - the identified locations (when finding stories for multiple location UUIDs)
|
43
|
+
|
44
|
+
# city, state and neighborhood names are case-insensitive.
|
45
|
+
# states can be identified by name or postal abbreviation.
|
46
|
+
|
47
|
+
# find stories for a zip code
|
48
|
+
data = OutsideIn::Story.for_zip_code("11211")
|
49
|
+
puts "Total stories for #{data[:location].display_name}: #{data[:total]}"
|
50
|
+
data[:stories].each {|story| puts " #{story.title} - #{story.feed_title}"}
|
51
|
+
|
52
|
+
# displays:
|
53
|
+
# Total stories for 11211: 438
|
54
|
+
# Fashionistas to Go Higher-End with 11K-Plus Feet at 550 Seventh - The New York Observer Real Estate
|
55
|
+
# Carlos C.'s Review of East River State Park - Brooklyn (3/5) on Yelp - Yelp Reviews New York
|
56
|
+
# What's going on Tuesday? - Brooklyn Vegan
|
57
|
+
# etc.
|
58
|
+
|
59
|
+
# find by state
|
60
|
+
data = OutsideIn::Story.for_state("New York")
|
61
|
+
|
62
|
+
# find by city
|
63
|
+
data = OutsideIn::Story.for_city("NY", "New York")
|
64
|
+
|
65
|
+
# find by neighborhood
|
66
|
+
data = OutsideIn::Story.for_nabe("ny", "new york", "williamsburg")
|
67
|
+
|
68
|
+
# find by location UUID
|
69
|
+
data = OutsideIn::Story.for_uuids([
|
70
|
+
"a02aa3e4-2aaa-41d7-b9d7-45642eb1c557", # Brooklyn, NY
|
71
|
+
"98653b8d-fa8f-4d50-93b2-f3977a81f40c", # Brooklyn, Jacksonville, FL
|
72
|
+
])
|
73
|
+
|
74
|
+
See [the class docs](http://rdoc.info/github/outsidein/api-rb/master/frames) for more information.
|
75
|
+
|
76
|
+
## CLI
|
77
|
+
|
78
|
+
A set of Thor tasks is provided so that you can call API methods from the command line (read more about Thor at [http://github.com/wycats/thor](http://github.com/wycats/thor)).
|
79
|
+
|
80
|
+
The following examples assume you have Thor installed system-wide. If it's local to your bundle, then replace `thor` with `bundle exec thor`.
|
81
|
+
|
82
|
+
### Configuration
|
83
|
+
|
84
|
+
Copy `config/oi.sample.yml` to `config/oi.yml` and replace the placeholder values with your key and secret.
|
85
|
+
|
86
|
+
### Tasks
|
87
|
+
|
88
|
+
You can see all available tasks with this command:
|
89
|
+
|
90
|
+
$ thor list oi
|
91
|
+
|
92
|
+
See which options are defined for a particular task with this command (replacing the task name as necessary):
|
93
|
+
|
94
|
+
$ thor help oi:locations:named
|
95
|
+
|
96
|
+
## Help
|
97
|
+
|
98
|
+
* Source code: [http://github.com/outsidein/api-rb](http://github.com/outsidein/api-rb)
|
99
|
+
* Class docs: [http://rdoc.info/github/outsidein/api-rb/master/frames](http://rdoc.info/github/outsidein/api-rb/master/frames)
|
100
|
+
* General API docs: [http://developers.outside.in/docs](http://developers.outside.in/docs)
|
101
|
+
* Post questions in the help forum: [http://developers.outside.in/forum](http://developers.outside.in/forum)
|
data/Rakefile
CHANGED
@@ -2,13 +2,20 @@ begin
|
|
2
2
|
require 'jeweler'
|
3
3
|
Jeweler::Tasks.new do |gemspec|
|
4
4
|
gemspec.name = "outside-in"
|
5
|
-
gemspec.summary = "Ruby
|
6
|
-
gemspec.
|
7
|
-
gemspec.
|
8
|
-
gemspec.
|
9
|
-
gemspec.authors = ["Doug Petkanics"]
|
5
|
+
gemspec.summary = "Ruby SDK for the Outside.in API"
|
6
|
+
gemspec.email = "brian@outside.in"
|
7
|
+
gemspec.homepage = "http://github.com/outsidein/api-rb"
|
8
|
+
gemspec.authors = ["Brian Moseley"]
|
10
9
|
end
|
11
|
-
Jeweler::GemcutterTasks.new
|
12
10
|
rescue LoadError
|
13
11
|
puts "Jeweler not available. Install it with: gem install jeweler"
|
14
12
|
end
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'yard'
|
16
|
+
YARD::Rake::YardocTask.new do |t|
|
17
|
+
t.options = ['--hide-void-return', '--title', 'Outside.in Ruby SDK']
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
puts "YARD is not available. Install it with: gem install yard"
|
21
|
+
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
1.0.0
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module OutsideIn
|
2
|
+
# The base class for API models.
|
3
|
+
#
|
4
|
+
# Models interact with the remote service through the low level {#call_remote} method. Each model class
|
5
|
+
# defines its own high-level finder methods that encapsulate the remote service call. For example:
|
6
|
+
#
|
7
|
+
# module OutsideIn
|
8
|
+
# class Thing < Base
|
9
|
+
# def self.by_name(name)
|
10
|
+
# new(call_remote("/things/named/#{URI.escape(name)}"))
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# Model attributes are declared using {#api_attr}. Only attributes declared this way are recognized by the
|
16
|
+
# initializer when setting the model's initial state.
|
17
|
+
#
|
18
|
+
# @abstract Subclass and declare attributes with {#api_attr} to implement a custom model class.
|
19
|
+
# @since 1.0
|
20
|
+
class Base
|
21
|
+
class << self
|
22
|
+
# Returns the map of defined attributes for this model class. The keys are attribute symbols and the values are
|
23
|
+
# the attribute classes (or +nil+, indicating that the attribute is of a primitive type).
|
24
|
+
#
|
25
|
+
# @return [Hash<Symbol, Class>]
|
26
|
+
# @since 1.0
|
27
|
+
def api_attrs
|
28
|
+
@api_attrs
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds one or more defined attributes for this model class.
|
33
|
+
#
|
34
|
+
# If the first argument is a +Hash+, then its entries are added directly to the defined attributes map.
|
35
|
+
# Otherwise, each argument is taken to be the name of a primitive-typed attribute. In either case,
|
36
|
+
# {::Module#attr_accessor} is called for each attribute.
|
37
|
+
#
|
38
|
+
# @return [void]
|
39
|
+
# @since 1.0
|
40
|
+
def self.api_attr(*names)
|
41
|
+
@api_attrs ||= {}
|
42
|
+
if ! names.empty? && names.first.is_a?(Hash)
|
43
|
+
names.first.each_pair do |name, clazz|
|
44
|
+
@api_attrs[name.to_sym] = clazz
|
45
|
+
attr_accessor(name.to_sym)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
names.each do |name|
|
49
|
+
@api_attrs[name.to_sym] = nil
|
50
|
+
attr_accessor(name.to_sym)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns a new instance.
|
56
|
+
#
|
57
|
+
# Each entry of +attrs+ whose key identifies a defined model attribute is used to set the value of that
|
58
|
+
# attribute. If the attribute's type is a +Class+, then an instance of that class is created with the raw
|
59
|
+
# value passed to its initializer. Otherwise, the raw value is used directly.
|
60
|
+
#
|
61
|
+
# @param [Hash<Symbol, Object>] attrs the data used to initialize the model's attributes
|
62
|
+
# @return [OutsideIn::Base]
|
63
|
+
# @since 1.0
|
64
|
+
def initialize(attrs = {})
|
65
|
+
self.class.api_attrs.each_pair do |name, clazz|
|
66
|
+
str = name.to_s
|
67
|
+
if attrs.include?(str)
|
68
|
+
v = attrs[str]
|
69
|
+
val = if v.is_a?(Array)
|
70
|
+
v.map {|it| clazz.nil?? it : clazz.new(it)}
|
71
|
+
else
|
72
|
+
clazz.nil?? v : clazz.new(v)
|
73
|
+
end
|
74
|
+
instance_variable_set("@#{str}".to_sym, val)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module OutsideIn
|
2
|
+
# Category model class. Each location has a category that describes its places in the location hierachy, e.g
|
3
|
+
# state, city, neighborhood, zip code, etc.
|
4
|
+
#
|
5
|
+
# Categories have the following attributes:
|
6
|
+
#
|
7
|
+
# * display_name
|
8
|
+
# * name
|
9
|
+
#
|
10
|
+
# @since 1.0
|
11
|
+
class Category < Base
|
12
|
+
api_attr :name, :display_name
|
13
|
+
|
14
|
+
# Returns the category's display name.
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
# @since 1.0
|
18
|
+
def to_s
|
19
|
+
display_name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'simple_uuid'
|
2
|
+
|
3
|
+
module OutsideIn
|
4
|
+
# Location model class.
|
5
|
+
#
|
6
|
+
# Locations have the following attributes:
|
7
|
+
#
|
8
|
+
# * category ({OutsideIn::Category})
|
9
|
+
# * city
|
10
|
+
# * display_name
|
11
|
+
# * lat ({::Float})
|
12
|
+
# * lng ({::Float})
|
13
|
+
# * state
|
14
|
+
# * state_abbrev
|
15
|
+
# * url
|
16
|
+
# * url_name
|
17
|
+
# * uuid ({SimpleUUID::UUID})
|
18
|
+
#
|
19
|
+
# Location finders accept query parameter options as described by {OutsideIn::Location#parameterize_url}. They
|
20
|
+
# return data structures as described by {OutsideIn::Location#query_result}.
|
21
|
+
#
|
22
|
+
# @see http://developers.outside.in/docs/locations_query_resource General API documentation for locations
|
23
|
+
# @since 1.0
|
24
|
+
class Location < Base
|
25
|
+
api_attr :city, :display_name, :lat, :lng, :state, :state_abbrev, :url, :url_name
|
26
|
+
api_attr :category => Category
|
27
|
+
api_attr :uuid => SimpleUUID::UUID
|
28
|
+
|
29
|
+
# Returns the locations matching +name+. See the API docs for specifics regarding matching rules.
|
30
|
+
#
|
31
|
+
# @param [String] name the name to match
|
32
|
+
# @param [Hash<String, Object>] inputs the data inputs
|
33
|
+
# @return [Hash<Symbol, Object>] the query result
|
34
|
+
# @since 1.0
|
35
|
+
def self.named(name, inputs)
|
36
|
+
query_result(OutsideIn::Resource::LocationFinder.new("/locations/named/#{URI.escape(name)}").GET(inputs))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the location's display name and uuid.
|
40
|
+
#
|
41
|
+
# @return [String]
|
42
|
+
# @since 1.0
|
43
|
+
def to_s
|
44
|
+
"#{display_name} (#{uuid.to_guid})"
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns a hash encapsulating the data returned from a successful finder query.
|
48
|
+
#
|
49
|
+
# The hash contains the following data:
|
50
|
+
#
|
51
|
+
# * +:total+ - the total number of matching locations (may be greater than the number of returned stories)
|
52
|
+
# * +:locations+ - the array of best matching {OutsideIn::Location} as per the specified or implied limit
|
53
|
+
#
|
54
|
+
# @param [Hash<String, Object>] data the raw query result
|
55
|
+
# @return [Hash<Symbol, Object>]
|
56
|
+
# @since 1.0
|
57
|
+
def self.query_result(data)
|
58
|
+
{:total => data['total'], :locations => data['locations'].map {|l| new(l)}}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
require 'md5'
|
4
|
+
|
5
|
+
module OutsideIn
|
6
|
+
module Resource
|
7
|
+
# The base class for API resources.
|
8
|
+
#
|
9
|
+
# Resources are exposed by the API service at particular endpoints identified by URLs. Consumers can interact
|
10
|
+
# with resources via the HTTP uniform interface.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# resource = new MyResource("/an/endpoint")
|
14
|
+
# data = resource.GET({'publication-id' => 1234, 'limit' => 5})
|
15
|
+
#
|
16
|
+
# @abstract Subclass and override {#scope} and {#params} to implement a custom resource class.
|
17
|
+
# @since 1.0
|
18
|
+
class Base
|
19
|
+
# Returns a version of +url+ that includes publication scoping when +inputs+ contains a non-nil
|
20
|
+
# +publication-id+ entry.
|
21
|
+
#
|
22
|
+
# @param [String] url the URL
|
23
|
+
# @param [Hash<String, Object>] inputs the data inputs
|
24
|
+
# @return [String] the potentially scoped URL
|
25
|
+
# @since 1.0
|
26
|
+
def self.scope(url, inputs)
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a version of +url+ with parameters in the query string corresponding to +inputs+.
|
31
|
+
#
|
32
|
+
# @param [String] url the URL
|
33
|
+
# @param [Hash<String, Object>] inputs the data inputs
|
34
|
+
# @return [String] the URL including query parameters
|
35
|
+
# @since 1.0
|
36
|
+
def self.parameterize(url, inputs)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the signed form of +url+. Signing adds the +dev_key+ and +sig+ query parameters to the query string.
|
41
|
+
#
|
42
|
+
# @param [String] url a URL to be signed
|
43
|
+
# @return [String] the signed URL
|
44
|
+
# @raise [OutsideIn::SignatureException] if the key or secret are not set
|
45
|
+
# @since 1.0
|
46
|
+
def self.sign(url)
|
47
|
+
raise SignatureException, "Key not set" unless OutsideIn.key
|
48
|
+
raise SignatureException, "Secret not set" unless OutsideIn.secret
|
49
|
+
sig_params = "dev_key=#{OutsideIn.key}&sig=#{MD5.new(OutsideIn.key + OutsideIn.secret +
|
50
|
+
Time.now.to_i.to_s).hexdigest}"
|
51
|
+
url =~ /\?/ ? "#{url}&#{sig_params}" : "#{url}?#{sig_params}"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a new instance. Stores the absolutized, signed URL.
|
55
|
+
#
|
56
|
+
# @param [String] relative_url a URL relative to the version component of the base service URL
|
57
|
+
# @return [OutsideIn::Resource::Base]
|
58
|
+
def initialize(relative_url)
|
59
|
+
@url = "http://#{HOST}/v#{VERSION}#{relative_url}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Calls +GET+ on the remote API service and returns the data encapsulated in the response. The URL that is
|
63
|
+
# called is created by scoping and parameterizing the canonical resource URL based on +inputs+.
|
64
|
+
#
|
65
|
+
# @param [Hash<String, Object>] inputs the data inputs
|
66
|
+
# @return [Object] the returned data structure as defined by the API specification (as parsed from the JSON
|
67
|
+
# envelope)
|
68
|
+
# @raise [OutsideIn::ForbiddenException] for a +403+ response
|
69
|
+
# @raise [OutsideIn::NotFoundException] for a +404+ response
|
70
|
+
# @raise [OutsideIn::ServiceException] for any error response that indicates a service fault of some type
|
71
|
+
# @raise [OutsideIn::QueryException] for any error response that indicates an invalid request or other client
|
72
|
+
# problem
|
73
|
+
# @since 1.0
|
74
|
+
def GET(inputs)
|
75
|
+
url = self.class.sign(self.class.parameterize(self.class.scope(@url, inputs), inputs))
|
76
|
+
OutsideIn.logger.debug("Requesting #{url}") if OutsideIn.logger
|
77
|
+
response = HTTParty.get(url)
|
78
|
+
unless response.code < 300
|
79
|
+
raise ForbiddenException if response.code == 403
|
80
|
+
raise NotFoundException if response.code == 404
|
81
|
+
if response.headers.include?('x-mashery-error-code')
|
82
|
+
raise ServiceException, response.headers['x-mashery-error-code']
|
83
|
+
else
|
84
|
+
raise QueryException.new(JSON[response.body])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
JSON[response.body]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module OutsideIn
|
2
|
+
module Resource
|
3
|
+
# A resource that performs queries for locations.
|
4
|
+
#
|
5
|
+
# @since 1.0
|
6
|
+
class LocationFinder < OutsideIn::Resource::Base
|
7
|
+
QP = QueryParams.new({:limit => :limit}, {:category => :category})
|
8
|
+
|
9
|
+
# Returns a version of +url+ that includes publication scoping when +inputs+ contains a non-nil
|
10
|
+
# +publication-id+ entry.
|
11
|
+
#
|
12
|
+
# @param (see Resource#scope)
|
13
|
+
# @return [String] the potentially scoped URL
|
14
|
+
# @since 1.0
|
15
|
+
def self.scope(url, inputs)
|
16
|
+
inputs['publication-id'].nil?? url : "#{url}/publications/#{inputs['publication-id']}"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns a version of +url+ with parameters in the query string corresponding to +inputs+.
|
20
|
+
#
|
21
|
+
# @param (see Resource#scope)
|
22
|
+
# @return [String] the URL including query parameters
|
23
|
+
# @since 1.0
|
24
|
+
def self.parameterize(url, inputs)
|
25
|
+
QP.parameterize(url, inputs)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module OutsideIn
|
2
|
+
module Resource
|
3
|
+
# A helper class for computing query parameters. These helpers know how to convert input values into query
|
4
|
+
# parameters and to add them to query URLs.
|
5
|
+
#
|
6
|
+
# @since 1.0
|
7
|
+
class QueryParams
|
8
|
+
|
9
|
+
# Creates and returns an instance based on various parameter names.
|
10
|
+
#
|
11
|
+
# For each simple parameter name, a parameter with that name is added when the inputs hash contains a value for
|
12
|
+
# that key.
|
13
|
+
#
|
14
|
+
# Negatable parameters are those which have "include" and "exclude" variants (e.g. category for locations,
|
15
|
+
# keyword for stories). An "include" parameter is added when the inputs hash contains a value for the negatable
|
16
|
+
# parameter's name, just like for a simple parameter. An exclude parameter prefixed with "no-" is added when the
|
17
|
+
# inputs hash contains a value for the parameter's name prefixed with "no-" or the name prefixed with "wo-" (the
|
18
|
+
# form used by the Thor tasks, which reserve the "no-" prefix for a different purpose).
|
19
|
+
#
|
20
|
+
# @param [Hash<Symbol, Symbol>] simple the names of simple input parameters mapped to API parameter names
|
21
|
+
# @param [Hash<Symbol, Symbol>] negatable the names of negatable input parameters mapped to API parameter names
|
22
|
+
# @return [OutsideIn::QueryParams]
|
23
|
+
# @since 1.0
|
24
|
+
def initialize(simple = {}, negatable = {})
|
25
|
+
@simple = simple
|
26
|
+
@negatable = negatable
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the provided URL with parameters attached to the query string.
|
30
|
+
#
|
31
|
+
# @param [Hash<String, Object>] inputs the query parameter inputs
|
32
|
+
# @param [String] url the base query resource URL
|
33
|
+
# @return [String] the URL including query parameters
|
34
|
+
# @since 1.0
|
35
|
+
def parameterize(url, inputs)
|
36
|
+
params = []
|
37
|
+
|
38
|
+
@simple.each_pair do |input, api|
|
39
|
+
nk = input.to_s
|
40
|
+
params << "#{api}=#{inputs[nk]}" unless inputs[nk].nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
@negatable.each_pair do |input, api|
|
44
|
+
nk = input.to_s
|
45
|
+
params.concat(inputs[nk].map {|s| "#{api}=#{URI.escape(s)}"}) unless inputs[nk].nil?
|
46
|
+
["wo-#{input}", "no-#{input}"].each do |nk|
|
47
|
+
params.concat(inputs[nk].map {|s| "no-#{api}=#{URI.escape(s)}"}) unless inputs[nk].nil?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
if params.empty?
|
52
|
+
url
|
53
|
+
else
|
54
|
+
sep = url =~ /\?/ ? '&' : '?'
|
55
|
+
"#{url}#{sep}#{params.join('&')}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module OutsideIn
|
2
|
+
module Resource
|
3
|
+
# A resource that performs queries for stories.
|
4
|
+
#
|
5
|
+
# @since 1.0
|
6
|
+
class StoryFinder < OutsideIn::Resource::Base
|
7
|
+
QP = QueryParams.new({:limit => :limit, :'max-age' => :max_age}, {:keyword => :keyword, :vertical => :vertical,
|
8
|
+
:format => :format, :'author-type' => :'author-type'})
|
9
|
+
|
10
|
+
# Returns a version of +url+ that includes publication scoping when +inputs+ contains a non-nil
|
11
|
+
# +publication-id+ entry.
|
12
|
+
#
|
13
|
+
# @param (see Resource#scope)
|
14
|
+
# @return [String] the potentially scoped URL
|
15
|
+
# @since 1.0
|
16
|
+
def self.scope(url, inputs)
|
17
|
+
inputs['publication-id'].nil?? url : url.gsub(/\/stories$/,
|
18
|
+
"/publications/#{inputs['publication-id']}/stories")
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns a version of +url+ with parameters in the query string corresponding to +inputs+.
|
22
|
+
#
|
23
|
+
# @param (see Resource#scope)
|
24
|
+
# @return [String] the URL including query parameters
|
25
|
+
# @since 1.0
|
26
|
+
def self.parameterize(url, inputs)
|
27
|
+
QP.parameterize(url, inputs)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/outside_in/story.rb
CHANGED
@@ -1,34 +1,116 @@
|
|
1
|
+
require 'simple_uuid'
|
2
|
+
|
1
3
|
module OutsideIn
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
4
|
+
# Story model class.
|
5
|
+
#
|
6
|
+
# Stories have the following attributes:
|
7
|
+
#
|
8
|
+
# * feed_title
|
9
|
+
# * feed_url
|
10
|
+
# * story_url
|
11
|
+
# * summary
|
12
|
+
# * tags - (+Array+ of {OutsideIn::Tag})
|
13
|
+
# * title
|
14
|
+
# * uuid ({SimpleUUID::UUID})
|
15
|
+
#
|
16
|
+
# Story finders accept query parameter inputs as described by {OutsideIn::Story#parameterize_url}. They return data
|
17
|
+
# structures as described by {OutsideIn::Story#query_result}.
|
18
|
+
#
|
19
|
+
# @see http://developers.outside.in/docs/stories_query_resource General API documentation for stories
|
20
|
+
# @since 1.0
|
21
|
+
class Story < Base
|
22
|
+
api_attr :feed_title, :feed_url, :story_url, :summary, :title, :uuid
|
23
|
+
api_attr :tags => Tag
|
24
|
+
|
25
|
+
# Returns the stories attached to +state+.
|
26
|
+
#
|
27
|
+
# @param [String] state the state name or postal abbreviation
|
28
|
+
# @param [Hash<String, Object>] inputs the query parameter inputs
|
29
|
+
# @return [Hash<Symbol, Object>] the query result
|
30
|
+
# @since 1.0
|
31
|
+
def self.for_state(state, inputs = {})
|
32
|
+
url = "/states/#{URI.escape(state)}/stories"
|
33
|
+
query_result(OutsideIn::Resource::StoryFinder.new(url).GET(inputs))
|
18
34
|
end
|
19
35
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
36
|
+
# Returns the stories attached to +city+ in +state+.
|
37
|
+
#
|
38
|
+
# @param [String] state the state name or postal abbreviation
|
39
|
+
# @param [String] city the city name
|
40
|
+
# @param [Hash<String, Object>] inputs the query parameter inputs
|
41
|
+
# @return [Hash<Symbol, Object>] the query result
|
42
|
+
# @since 1.0
|
43
|
+
def self.for_city(state, city, inputs = {})
|
44
|
+
url = "/states/#{URI.escape(state)}/cities/#{URI.escape(city)}/stories"
|
45
|
+
query_result(OutsideIn::Resource::StoryFinder.new(url).GET(inputs))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the stories attached to +nabe+ in +city+ in +state+.
|
49
|
+
#
|
50
|
+
# @param [String] state the state name or postal abbreviation
|
51
|
+
# @param [String] city the city name
|
52
|
+
# @param [String] nabe the neighborhood name
|
53
|
+
# @param [Hash<String, Object>] inputs the query parameter inputs
|
54
|
+
# @return [Hash<Symbol, Object>] the query result
|
55
|
+
# @since 1.0
|
56
|
+
def self.for_nabe(state, city, nabe, inputs = {})
|
57
|
+
url = "/states/#{URI.escape(state)}/cities/#{URI.escape(city)}/nabes/#{URI.escape(nabe)}/stories"
|
58
|
+
query_result(OutsideIn::Resource::StoryFinder.new(url).GET(inputs))
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the stories attached to +zip+.
|
62
|
+
#
|
63
|
+
# @param [String] zip the zip code
|
64
|
+
# @param [Hash<String, Object>] inputs the query parameter inputs
|
65
|
+
# @return [Hash<Symbol, Object>] the query result
|
66
|
+
# @since 1.0
|
67
|
+
def self.for_zip_code(zip, inputs = {})
|
68
|
+
url = "/zipcodes/#{URI.escape(zip)}/stories"
|
69
|
+
query_result(OutsideIn::Resource::StoryFinder.new(url).GET(inputs))
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the stories attached to the locations identified by +uuids+.
|
73
|
+
#
|
74
|
+
# @param [Array<SimpleUUID::UUID>] uuids the location uuids
|
75
|
+
# @param [Hash<String, Object>] inputs the query parameter inputs
|
76
|
+
# @return [Hash<Symbol, Object>] the query result
|
77
|
+
# @since 1.0
|
78
|
+
def self.for_uuids(uuids, inputs = {})
|
79
|
+
url = "/locations/#{uuids.map{|u| URI.escape(u.to_guid)}.join(",")}/stories"
|
80
|
+
query_result(OutsideIn::Resource::StoryFinder.new(url).GET(inputs))
|
25
81
|
end
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
82
|
+
|
83
|
+
# Returns the story's title and uuid.
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
# @since 1.0
|
87
|
+
def to_s
|
88
|
+
"#{title} (#{uuid.to_guid})"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns a hash encapsulating the data returned from a successful finder query.
|
92
|
+
#
|
93
|
+
# The hash contains the following data:
|
94
|
+
#
|
95
|
+
# * +:total+ - the total number of matching stories (may be greater than the number of returned stories)
|
96
|
+
# * +:stories+ - the array of most recent matching {OutsideIn::Story} in reverse chronological order as per the
|
97
|
+
# specified or implied limit
|
98
|
+
# * +:location+ - the {OutsideIn::Location} to which the finder was scoped (present for all non-UUID finders)
|
99
|
+
# * +:locations+ - the array of {OutsideIn::Location} to which the finder was scoped (present only for the UUID
|
100
|
+
# finder)
|
101
|
+
#
|
102
|
+
# @param [Hash<String, Object>] data the raw query result
|
103
|
+
# @return [Hash<Symbol, Object>]
|
104
|
+
# @since 1.0
|
105
|
+
def self.query_result(data)
|
106
|
+
rv = {:total => data['total'], :stories => []}
|
107
|
+
if data.include?('locations')
|
108
|
+
rv[:locations] = data['locations'].map {|l| Location.new(l)}
|
109
|
+
else
|
110
|
+
rv[:location] = Location.new(data['location'])
|
30
111
|
end
|
112
|
+
rv[:stories] = data['stories'].map {|s| new(s)}
|
113
|
+
rv
|
31
114
|
end
|
32
|
-
|
33
115
|
end
|
34
116
|
end
|
data/lib/outside_in/tag.rb
CHANGED
@@ -1,10 +1,21 @@
|
|
1
1
|
module OutsideIn
|
2
|
-
class
|
3
|
-
|
2
|
+
# Topic tag model class. Tags are attached to stories to provide hints for content and relevance. Keyword queries
|
3
|
+
# match against tags as well as story title and summary.
|
4
|
+
#
|
5
|
+
# Tags have the following attributes:
|
6
|
+
#
|
7
|
+
# * name
|
8
|
+
#
|
9
|
+
# @since 1.0
|
10
|
+
class Tag < Base
|
11
|
+
api_attr :name
|
4
12
|
|
5
|
-
|
6
|
-
|
7
|
-
|
13
|
+
# Returns the tag's name.
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
# @since 1.0
|
17
|
+
def to_s
|
18
|
+
name
|
8
19
|
end
|
9
20
|
end
|
10
21
|
end
|
data/lib/outside_in.rb
CHANGED
@@ -1,19 +1,82 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
|
1
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
require 'outside_in/base'
|
5
|
+
require 'outside_in/category'
|
6
|
+
require 'outside_in/tag'
|
7
|
+
require 'outside_in/location'
|
8
|
+
require 'outside_in/story'
|
9
|
+
require 'outside_in/resource/query_params'
|
10
|
+
require 'outside_in/resource/base'
|
11
|
+
require 'outside_in/resource/location_finder'
|
12
|
+
require 'outside_in/resource/story_finder'
|
4
13
|
|
5
14
|
module OutsideIn
|
6
|
-
|
7
|
-
|
8
|
-
|
15
|
+
# The API service host
|
16
|
+
# @since 1.0
|
17
|
+
HOST = 'hyperlocal-api.outside.in'
|
9
18
|
|
10
|
-
|
19
|
+
# The current version of the API (not the version of the SDK!)
|
20
|
+
# @since 1.0
|
21
|
+
VERSION = '1.1'
|
22
|
+
|
23
|
+
mattr_accessor :logger, :instance_writer => false
|
24
|
+
# That which logs. Use +OutsideIn.logger+ and +OutsideIn.logger=+ to access. Defaults to +WARN+.
|
25
|
+
# @since 1.0
|
26
|
+
@@logger = Logger.new(STDOUT)
|
27
|
+
@@logger.level = Logger::WARN
|
28
|
+
|
29
|
+
mattr_accessor :key, :instance_writer => false
|
30
|
+
# The developer key used to identify who is making the API request. Use +OutsideIn.key+ and +OutsideIn.key=+ to
|
31
|
+
# access.
|
32
|
+
# @since 1.0
|
33
|
+
@@key = nil
|
34
|
+
|
35
|
+
mattr_accessor :secret, :instance_writer => false
|
36
|
+
# The shared secret used to sign the API request. Use +OutsideIn.secret+ and +OutsideIn.secret=+ to access.
|
37
|
+
# @since 1.0
|
38
|
+
@@secret = nil
|
11
39
|
|
12
|
-
|
40
|
+
# The base class for API exceptions.
|
41
|
+
# @since 1.0
|
42
|
+
class ApiException < Exception; end
|
13
43
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
require File.join(directory, 'outside_in', 'story')
|
44
|
+
# Indicates that access was denied to the requested API resource. This may mean that the developer key is invalid
|
45
|
+
# or does not provide access to a curated resource.
|
46
|
+
# @since 1.0
|
47
|
+
class ForbiddenException < ApiException; end
|
19
48
|
|
49
|
+
# Indicates that the requested API resource was not found. Usually this means that a data item was not found for
|
50
|
+
# a given UUID or other identifier, but in the case of an SDK bug, it may mean the requested URL was incorrect
|
51
|
+
# somehow.
|
52
|
+
# @since 1.0
|
53
|
+
class NotFoundException < ApiException; end
|
54
|
+
|
55
|
+
# Usually means that something has gone wrong on the service side of the communication.
|
56
|
+
# @since 1.0
|
57
|
+
class ServiceException < ApiException; end
|
58
|
+
|
59
|
+
# Indicates that a request could not be signed for some reason.
|
60
|
+
# @since 1.0
|
61
|
+
class SignatureException < ApiException; end
|
62
|
+
|
63
|
+
# Indicates that a query request returned an error response.
|
64
|
+
# @since 1.0
|
65
|
+
class QueryException < Exception
|
66
|
+
|
67
|
+
# Returns a new instance.
|
68
|
+
#
|
69
|
+
# @param [Hash<String, Object>] data the error hash returned in the query response body
|
70
|
+
# @return [OutsideIn::QueryException]
|
71
|
+
# @since 1.0
|
72
|
+
def initialize(data)
|
73
|
+
if data.include?('error')
|
74
|
+
super(data['error'])
|
75
|
+
elsif data.include?('errors')
|
76
|
+
super(data['errors'].join('; '))
|
77
|
+
else
|
78
|
+
super('unknown query error')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/outside-in.gemspec
CHANGED
@@ -5,36 +5,42 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{outside-in}
|
8
|
-
s.version = "
|
8
|
+
s.version = "1.0.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
-
s.authors = ["
|
12
|
-
s.date = %q{2010-
|
13
|
-
s.
|
14
|
-
s.email = %q{petkanics@gmail.com}
|
11
|
+
s.authors = ["Brian Moseley"]
|
12
|
+
s.date = %q{2010-09-20}
|
13
|
+
s.email = %q{brian@outside.in}
|
15
14
|
s.extra_rdoc_files = [
|
16
|
-
"
|
17
|
-
"README"
|
15
|
+
"README.md"
|
18
16
|
]
|
19
17
|
s.files = [
|
20
18
|
".gitignore",
|
21
|
-
"
|
22
|
-
"
|
19
|
+
"Gemfile",
|
20
|
+
"Gemfile.lock",
|
21
|
+
"HISTORY",
|
22
|
+
"README.md",
|
23
23
|
"Rakefile",
|
24
24
|
"VERSION",
|
25
|
+
"config/oi.sample.yml",
|
25
26
|
"lib/outside_in.rb",
|
26
|
-
"lib/outside_in/
|
27
|
-
"lib/outside_in/
|
27
|
+
"lib/outside_in/base.rb",
|
28
|
+
"lib/outside_in/category.rb",
|
29
|
+
"lib/outside_in/location.rb",
|
30
|
+
"lib/outside_in/resource/base.rb",
|
31
|
+
"lib/outside_in/resource/location_finder.rb",
|
32
|
+
"lib/outside_in/resource/query_params.rb",
|
33
|
+
"lib/outside_in/resource/story_finder.rb",
|
28
34
|
"lib/outside_in/story.rb",
|
29
35
|
"lib/outside_in/tag.rb",
|
30
|
-
"
|
31
|
-
"
|
36
|
+
"outside-in.gemspec",
|
37
|
+
"tasks/oi.thor"
|
32
38
|
]
|
33
|
-
s.homepage = %q{http://github.com/
|
39
|
+
s.homepage = %q{http://github.com/outsidein/api-rb}
|
34
40
|
s.rdoc_options = ["--charset=UTF-8"]
|
35
41
|
s.require_paths = ["lib"]
|
36
42
|
s.rubygems_version = %q{1.3.6}
|
37
|
-
s.summary = %q{Ruby
|
43
|
+
s.summary = %q{Ruby SDK for the Outside.in API}
|
38
44
|
|
39
45
|
if s.respond_to? :specification_version then
|
40
46
|
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
data/tasks/oi.thor
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
Bundler.setup
|
4
|
+
|
5
|
+
# XXX: only do this when the task has not been installed
|
6
|
+
$: << 'lib'
|
7
|
+
|
8
|
+
require 'outside_in'
|
9
|
+
require 'simple_uuid'
|
10
|
+
require 'text/reform'
|
11
|
+
require 'yaml'
|
12
|
+
|
13
|
+
module OutsideIn
|
14
|
+
class CLI < Thor
|
15
|
+
namespace :oi
|
16
|
+
|
17
|
+
protected
|
18
|
+
def run(&block)
|
19
|
+
configure
|
20
|
+
if ::OutsideIn.key && ::OutsideIn.secret
|
21
|
+
begin
|
22
|
+
yield
|
23
|
+
rescue ::OutsideIn::ForbiddenException => e
|
24
|
+
error("Access denied - doublecheck key and secret in #{cfg_file}")
|
25
|
+
rescue ::OutsideIn::QueryException => e
|
26
|
+
error("Invalid query: #{e.message}")
|
27
|
+
rescue ::OutsideIn::ApiException => e
|
28
|
+
error("API error: #{e.message}")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def cfg_file
|
34
|
+
File.join('config', 'oi.yml')
|
35
|
+
end
|
36
|
+
|
37
|
+
def configure
|
38
|
+
cfg = YAML.load_file(cfg_file)
|
39
|
+
::OutsideIn.key = cfg['key'] or error("You must specify your key in #{cfg_file}")
|
40
|
+
::OutsideIn.secret = cfg['secret'] or error("You must specify your secret in #{cfg_file}")
|
41
|
+
::OutsideIn.logger.level = Logger::DEBUG
|
42
|
+
end
|
43
|
+
|
44
|
+
def warn(msg)
|
45
|
+
say_status :WARN, msg, :yellow
|
46
|
+
end
|
47
|
+
|
48
|
+
def ok(msg)
|
49
|
+
say_status :OK, msg
|
50
|
+
end
|
51
|
+
|
52
|
+
def error(msg)
|
53
|
+
say_status :ERROR, msg, :red
|
54
|
+
end
|
55
|
+
|
56
|
+
def debug(msg)
|
57
|
+
say_status :DEBUG, msg, :cyan
|
58
|
+
end
|
59
|
+
|
60
|
+
def show_locations(data)
|
61
|
+
if data[:locations].empty?
|
62
|
+
warn("No matching locations found.")
|
63
|
+
else
|
64
|
+
names = data[:locations].map do |l|
|
65
|
+
l.display_name.length > 32 ? "#{l.display_name[0, 27]} ..." : l.display_name
|
66
|
+
end
|
67
|
+
uuids = data[:locations].map {|l| l.uuid.to_guid}
|
68
|
+
r = Text::Reform.new
|
69
|
+
say(r.format(
|
70
|
+
"",
|
71
|
+
"Name UUID",
|
72
|
+
"======================================================================",
|
73
|
+
"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[",
|
74
|
+
names, uuids,
|
75
|
+
"",
|
76
|
+
"Best #{data[:locations].size} of #{data[:total]} matching locations"
|
77
|
+
))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def show_stories(data)
|
82
|
+
if data[:stories].empty?
|
83
|
+
warn("No stories found.")
|
84
|
+
else
|
85
|
+
titles = data[:stories].map {|s| s.title.length > 48 ? "#{s.title[0, 43]} ..." : s.title}
|
86
|
+
feeds = data[:stories].map {|s| s.feed_title.length > 26 ? "#{s.feed_title[0, 21]} ..." : s.feed_title}
|
87
|
+
r = Text::Reform.new
|
88
|
+
say(r.format(
|
89
|
+
"",
|
90
|
+
"Title Feed",
|
91
|
+
"============================================================================",
|
92
|
+
"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[ [[[[[[[[[[[[[[[[[[[[[[[[[[",
|
93
|
+
titles, feeds,
|
94
|
+
"",
|
95
|
+
"#{data[:stories].size} most recent of #{data[:total]} stories"
|
96
|
+
))
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Locations < CLI
|
102
|
+
namespace 'oi:locations'
|
103
|
+
|
104
|
+
desc 'named NAME', 'Find locations matching a name'
|
105
|
+
method_options :limit => :numeric, :category => :array, :'wo-category' => :array, :'publication-id' => :numeric
|
106
|
+
def named(name)
|
107
|
+
run do
|
108
|
+
show_locations(::OutsideIn::Location.named(name, options))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Stories < CLI
|
114
|
+
namespace 'oi:stories'
|
115
|
+
|
116
|
+
def self.param_method_options
|
117
|
+
method_options :limit => :numeric, :'max-age' => :string, :keyword => :array, :'wo-keyword' => :array,
|
118
|
+
:vertical => :array, :'wo-vertical' => :array, :format => :array, :'wo-format' => :array,
|
119
|
+
:'author-type' => :array, :'wo-author-type' => :array, :'publication-id' => :numeric
|
120
|
+
end
|
121
|
+
|
122
|
+
desc 'state STATE', 'Find stories for a state'
|
123
|
+
param_method_options
|
124
|
+
def state(state)
|
125
|
+
run do
|
126
|
+
show_stories(::OutsideIn::Story.for_state(state, options))
|
127
|
+
end
|
128
|
+
rescue ::OutsideIn::NotFoundException => e
|
129
|
+
error("State not found")
|
130
|
+
end
|
131
|
+
|
132
|
+
desc 'city STATE CITY', 'Find stories for a city'
|
133
|
+
param_method_options
|
134
|
+
def city(state, city)
|
135
|
+
run do
|
136
|
+
show_stories(::OutsideIn::Story.for_city(state, city, options))
|
137
|
+
end
|
138
|
+
rescue ::OutsideIn::NotFoundException => e
|
139
|
+
error("City not found")
|
140
|
+
end
|
141
|
+
|
142
|
+
desc 'nabe STATE CITY NABE', 'Find stories for a neighborhood'
|
143
|
+
param_method_options
|
144
|
+
def nabe(state, city, nabe)
|
145
|
+
run do
|
146
|
+
show_stories(::OutsideIn::Story.for_nabe(state, city, nabe, options))
|
147
|
+
end
|
148
|
+
rescue ::OutsideIn::NotFoundException => e
|
149
|
+
error("Neighborhood not found")
|
150
|
+
end
|
151
|
+
|
152
|
+
desc 'zip ZIP', 'Find stories for a zip code'
|
153
|
+
param_method_options
|
154
|
+
def zip(zip)
|
155
|
+
run do
|
156
|
+
show_stories(::OutsideIn::Story.for_zip_code(zip, options))
|
157
|
+
end
|
158
|
+
rescue ::OutsideIn::NotFoundException => e
|
159
|
+
error("Zip code not found")
|
160
|
+
end
|
161
|
+
|
162
|
+
desc 'uuid UUID[,UUID[...]]', 'Find stories for one to five location UUIDs'
|
163
|
+
param_method_options
|
164
|
+
def uuid(uuids)
|
165
|
+
run do
|
166
|
+
show_stories(::OutsideIn::Story.for_uuids(uuids.split(',').map {|s| SimpleUUID::UUID.new(s)}, options))
|
167
|
+
end
|
168
|
+
rescue ::OutsideIn::NotFoundException => e
|
169
|
+
error("UUID not found")
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|
metadata
CHANGED
@@ -3,45 +3,52 @@ name: outside-in
|
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
|
-
- 0
|
7
6
|
- 1
|
8
7
|
- 0
|
9
|
-
|
8
|
+
- 0
|
9
|
+
version: 1.0.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
|
-
-
|
12
|
+
- Brian Moseley
|
13
13
|
autorequire:
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-09-20 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
21
|
-
description:
|
22
|
-
email:
|
21
|
+
description:
|
22
|
+
email: brian@outside.in
|
23
23
|
executables: []
|
24
24
|
|
25
25
|
extensions: []
|
26
26
|
|
27
27
|
extra_rdoc_files:
|
28
|
-
-
|
29
|
-
- README
|
28
|
+
- README.md
|
30
29
|
files:
|
31
30
|
- .gitignore
|
32
|
-
-
|
33
|
-
-
|
31
|
+
- Gemfile
|
32
|
+
- Gemfile.lock
|
33
|
+
- HISTORY
|
34
|
+
- README.md
|
34
35
|
- Rakefile
|
35
36
|
- VERSION
|
37
|
+
- config/oi.sample.yml
|
36
38
|
- lib/outside_in.rb
|
37
|
-
- lib/outside_in/
|
38
|
-
- lib/outside_in/
|
39
|
+
- lib/outside_in/base.rb
|
40
|
+
- lib/outside_in/category.rb
|
41
|
+
- lib/outside_in/location.rb
|
42
|
+
- lib/outside_in/resource/base.rb
|
43
|
+
- lib/outside_in/resource/location_finder.rb
|
44
|
+
- lib/outside_in/resource/query_params.rb
|
45
|
+
- lib/outside_in/resource/story_finder.rb
|
39
46
|
- lib/outside_in/story.rb
|
40
47
|
- lib/outside_in/tag.rb
|
41
|
-
- lib/outside_in/tweet.rb
|
42
48
|
- outside-in.gemspec
|
49
|
+
- tasks/oi.thor
|
43
50
|
has_rdoc: true
|
44
|
-
homepage: http://github.com/
|
51
|
+
homepage: http://github.com/outsidein/api-rb
|
45
52
|
licenses: []
|
46
53
|
|
47
54
|
post_install_message:
|
@@ -69,6 +76,6 @@ rubyforge_project:
|
|
69
76
|
rubygems_version: 1.3.6
|
70
77
|
signing_key:
|
71
78
|
specification_version: 3
|
72
|
-
summary: Ruby
|
79
|
+
summary: Ruby SDK for the Outside.in API
|
73
80
|
test_files: []
|
74
81
|
|
data/LICENSE
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
The MIT License
|
2
|
-
|
3
|
-
Copyright (c) 2010 Doug Petkanics
|
4
|
-
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
7
|
-
in the Software without restriction, including without limitation the rights
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
10
|
-
furnished to do so, subject to the following conditions:
|
11
|
-
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
13
|
-
all copies or substantial portions of the Software.
|
14
|
-
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
-
THE SOFTWARE.
|
data/README
DELETED
data/lib/outside_in/place.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
module OutsideIn
|
2
|
-
|
3
|
-
class Point
|
4
|
-
attr_reader :lat, :lng
|
5
|
-
|
6
|
-
def initialize(point_string)
|
7
|
-
@lat, @lng = point_string.split(' ')
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
class Place
|
12
|
-
attr_reader :id, :name, :url, :point
|
13
|
-
|
14
|
-
def initialize(place_hash)
|
15
|
-
@id = place_hash['id']
|
16
|
-
@name = place_hash['name']
|
17
|
-
@url = place_hash['url']
|
18
|
-
@point = Point.new(place_hash['georss:point'])
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
data/lib/outside_in/radar.rb
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
module OutsideIn
|
2
|
-
|
3
|
-
class Radar
|
4
|
-
attr_accessor :stories, :tweets
|
5
|
-
|
6
|
-
def initialize(lat, lng, radius=nil, only=nil, except=nil)
|
7
|
-
@lat = lat
|
8
|
-
@lng = lng
|
9
|
-
@radius = radius
|
10
|
-
@only = only
|
11
|
-
@except = except
|
12
|
-
@stories = []
|
13
|
-
@tweets = []
|
14
|
-
hit_api
|
15
|
-
end
|
16
|
-
|
17
|
-
def hit_api
|
18
|
-
res = JSON.parse(request)
|
19
|
-
populate_stories_and_tweets(res)
|
20
|
-
end
|
21
|
-
|
22
|
-
def populate_stories_and_tweets(radars)
|
23
|
-
radars.each do |radar|
|
24
|
-
if radar["type"] == "Story"
|
25
|
-
@stories << OutsideIn::Story.new(radar)
|
26
|
-
elsif radar["type"] == "Tweet"
|
27
|
-
@tweets << OutsideIn::Tweet.new(radar)
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def construct_url
|
33
|
-
url = "#{OutsideIn::JSON_ENDPOINT}?lat=#{@lat}&lng=#{@lng}"
|
34
|
-
url += "&radius=#{@radius}" if @radius
|
35
|
-
url += "&only=#{@only}" if @only
|
36
|
-
url += "&except=#{@except}" if @except
|
37
|
-
url
|
38
|
-
end
|
39
|
-
|
40
|
-
def request
|
41
|
-
url = URI.parse(construct_url)
|
42
|
-
res = Net::HTTP.get(url)
|
43
|
-
end
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
end
|
48
|
-
|
data/lib/outside_in/tweet.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
module OutsideIn
|
2
|
-
class Tweet
|
3
|
-
attr_reader :item_id, :icon_path, :author, :author_url, :published_at, :body, :url, :image_url, :places
|
4
|
-
|
5
|
-
def initialize(tweet_hash)
|
6
|
-
@item_id = tweet_hash['item_id']
|
7
|
-
@icon_path = tweet_hash['icon_path']
|
8
|
-
@author = tweet_hash['author']
|
9
|
-
@author_url = tweet_hash['author_url']
|
10
|
-
@published_at = tweet_hash['published_at']
|
11
|
-
@body = tweet_hash['body']
|
12
|
-
@url = tweet_hash['url']
|
13
|
-
@image_url = tweet_hash['image_url']
|
14
|
-
@places = []
|
15
|
-
populate_places(tweet_hash)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
def populate_places(tweet_hash)
|
20
|
-
tweet_hash["places"].each do |place|
|
21
|
-
@places << OutsideIn::Place.new(place)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
end
|
26
|
-
end
|