outside-in 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|