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 CHANGED
@@ -1 +1,4 @@
1
- pkg/
1
+ .yardoc
2
+ *.gem
3
+ config/oi.yml
4
+ doc
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'activesupport', '3.0.0'
4
+ gem 'httparty'
5
+ gem 'json'
6
+ gem 'simple_uuid'
7
+
8
+ group 'cli' do
9
+ gem 'text-reform'
10
+ gem 'thor'
11
+ end
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
@@ -0,0 +1,2 @@
1
+ 1.0.0
2
+ - Initial release
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 wrapper on the Outside.In API"
6
- gemspec.description = "The Outside.IN Radar API provides hyperlocal news, tweets, and commentary based around a given latitude and longitude. This is a ruby wrapper around that API."
7
- gemspec.email = "petkanics@gmail.com"
8
- gemspec.homepage = "http://github.com/dob/outsidein"
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
- 0.1.0
1
+ 1.0.0
@@ -0,0 +1,2 @@
1
+ key: your-developer-key
2
+ secret: your-shared-secret
@@ -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
@@ -1,34 +1,116 @@
1
+ require 'simple_uuid'
2
+
1
3
  module OutsideIn
2
- class Story
3
- attr_reader :item_id, :icon_path, :author, :author_url, :published_at, :title, :body, :url, :tags, :places
4
-
5
- def initialize(story_hash)
6
- @item_id = story_hash['item_id']
7
- @icon_path = story_hash['icon_path']
8
- @author = story_hash['author']
9
- @author_url = story_hash['author_url']
10
- @published_at = story_hash['published_at']
11
- @title = story_hash['title']
12
- @body = story_hash['body']
13
- @url = story_hash['url']
14
- @tags = []
15
- @places = []
16
- populate_tags(story_hash)
17
- populate_places(story_hash)
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
- private
21
- def populate_tags(story_hash)
22
- story_hash["tags"].each do |tag|
23
- @tags << OutsideIn::Tag.new(tag)
24
- end
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
- def populate_places(story_hash)
28
- story_hash["places"].each do |place|
29
- @places << OutsideIn::Place.new(place)
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
@@ -1,10 +1,21 @@
1
1
  module OutsideIn
2
- class Tag
3
- attr_reader :name, :url
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
- def initialize(tag_hash)
6
- @name = tag_hash['name']
7
- @url = tag_hash['url']
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 'rubygems'
2
- require 'json'
3
- require 'net/http'
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
- ENDPOINT = "http://api.outside.in/radar"
8
- JSON_ENDPOINT = "#{ENDPOINT}.json"
15
+ # The API service host
16
+ # @since 1.0
17
+ HOST = 'hyperlocal-api.outside.in'
9
18
 
10
- end
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
- directory = File.expand_path(File.dirname(__FILE__))
40
+ # The base class for API exceptions.
41
+ # @since 1.0
42
+ class ApiException < Exception; end
13
43
 
14
- require File.join(directory, 'outside_in', 'radar')
15
- require File.join(directory, 'outside_in', 'tweet')
16
- require File.join(directory, 'outside_in', 'place')
17
- require File.join(directory, 'outside_in', 'tag')
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 = "0.1.0"
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 = ["Doug Petkanics"]
12
- s.date = %q{2010-06-23}
13
- s.description = %q{The Outside.IN Radar API provides hyperlocal news, tweets, and commentary based around a given latitude and longitude. This is a ruby wrapper around that API.}
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
- "LICENSE",
17
- "README"
15
+ "README.md"
18
16
  ]
19
17
  s.files = [
20
18
  ".gitignore",
21
- "LICENSE",
22
- "README",
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/place.rb",
27
- "lib/outside_in/radar.rb",
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
- "lib/outside_in/tweet.rb",
31
- "outside-in.gemspec"
36
+ "outside-in.gemspec",
37
+ "tasks/oi.thor"
32
38
  ]
33
- s.homepage = %q{http://github.com/dob/outsidein}
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 wrapper on the Outside.In API}
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
- version: 0.1.0
8
+ - 0
9
+ version: 1.0.0
10
10
  platform: ruby
11
11
  authors:
12
- - Doug Petkanics
12
+ - Brian Moseley
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-06-23 00:00:00 -04:00
17
+ date: 2010-09-20 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
21
- description: The Outside.IN Radar API provides hyperlocal news, tweets, and commentary based around a given latitude and longitude. This is a ruby wrapper around that API.
22
- email: petkanics@gmail.com
21
+ description:
22
+ email: brian@outside.in
23
23
  executables: []
24
24
 
25
25
  extensions: []
26
26
 
27
27
  extra_rdoc_files:
28
- - LICENSE
29
- - README
28
+ - README.md
30
29
  files:
31
30
  - .gitignore
32
- - LICENSE
33
- - README
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/place.rb
38
- - lib/outside_in/radar.rb
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/dob/outsidein
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 wrapper on the Outside.In API
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
@@ -1,9 +0,0 @@
1
- Wrapper for the Outside.in API
2
-
3
- Usage:
4
- require 'outside_in'
5
-
6
- radars = OutsideIn::Radar.new(lat, lng)
7
- radars.stories.each do |story|
8
- puts "#{story.title} by #{story.author}"
9
- end
@@ -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
@@ -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
-
@@ -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