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 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