portero 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.simplecov +3 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +67 -0
- data/Rakefile +2 -0
- data/lib/portero/portero.rb +33 -0
- data/lib/portero/providers/foursquare.rb +39 -0
- data/lib/portero/providers/google_places.rb +41 -0
- data/lib/portero/search_provider.rb +52 -0
- data/lib/portero/search_result.rb +5 -0
- data/lib/portero/version.rb +3 -0
- data/lib/portero.rb +2 -0
- data/portero.gemspec +25 -0
- data/spec/data/foursquare.json +437 -0
- data/spec/data/google_places.json +343 -0
- data/spec/portero_spec.rb +24 -0
- data/spec/providers/foursquare_spec.rb +72 -0
- data/spec/providers/google_places_spec.rb +77 -0
- data/spec/search_provider_spec.rb +46 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/fakeweb.rb +6 -0
- metadata +173 -0
data/.gitignore
ADDED
data/.simplecov
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Robert Rouse
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Portero
|
2
|
+
|
3
|
+
Portero is another word for someone that may act as a concierge. Portero will help you find venues around a location using as many services known.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'portero'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install portero
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Configure Portero with providers, either in a Rails initializer or elsewhere:
|
22
|
+
|
23
|
+
Portero.providers = [Portero::SearchProvider::Foursquare.new(client_id: YOUR_ID, client_secret: YOUR_SECRET)]
|
24
|
+
Portero.init!
|
25
|
+
|
26
|
+
If more than one provider is given, it will search both providers and return combined results.
|
27
|
+
|
28
|
+
To search
|
29
|
+
|
30
|
+
results = Portero.search("pizza", latitude, longitude, options)
|
31
|
+
results #=> Array of Portero::SearchResult
|
32
|
+
|
33
|
+
Options can be anything the provider allows. All providers may not utilize all options. It's provider implementation dependant
|
34
|
+
|
35
|
+
SearchResult objects contain the following fields
|
36
|
+
|
37
|
+
name
|
38
|
+
address
|
39
|
+
latitude
|
40
|
+
longitude
|
41
|
+
city
|
42
|
+
state
|
43
|
+
postal_code
|
44
|
+
country
|
45
|
+
category
|
46
|
+
icon
|
47
|
+
extra
|
48
|
+
|
49
|
+
A provider may put whatever extraneous data that might be important in extra. It is also not forced to provide usable data in all fields.
|
50
|
+
|
51
|
+
## Writing a provider
|
52
|
+
|
53
|
+
A new provider must include Porter::SearchProvider and implement (at minimum) the search method that returns the SearchResult objects. A provider my require specific options that need to be passed to the constructor with requires_option. If those options are not given, an exception is raised. See one of the bundled providers in lib/portero/providers for examples.
|
54
|
+
|
55
|
+
## TODO
|
56
|
+
|
57
|
+
1. When using multiple providers, parallelize the requests.
|
58
|
+
2. When using multiple providers, analyze the result set and remove duplication as much as possible.
|
59
|
+
3. Always make it better
|
60
|
+
|
61
|
+
## Contributing
|
62
|
+
|
63
|
+
1. Fork it
|
64
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
65
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
66
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
67
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'portero/search_provider'
|
2
|
+
require 'portero/search_result'
|
3
|
+
require 'faraday'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Portero
|
7
|
+
module SearchProvider
|
8
|
+
autoload :Foursquare, 'portero/providers/foursquare'
|
9
|
+
autoload :GooglePlaces, 'portero/providers/google_places'
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.init!
|
13
|
+
@conn = Faraday.new do |builder|
|
14
|
+
builder.adapter Faraday.default_adapter
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.search(query, latitude, longitude, options = {})
|
19
|
+
results = []
|
20
|
+
@providers.each do |provider|
|
21
|
+
results += provider.search(@conn, query, latitude, longitude, options)
|
22
|
+
end
|
23
|
+
results
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.providers=(providers)
|
27
|
+
@providers = providers
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.providers
|
31
|
+
@providers
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Portero
|
2
|
+
module SearchProvider
|
3
|
+
class Foursquare
|
4
|
+
include SearchProvider
|
5
|
+
|
6
|
+
requires_option :client_id
|
7
|
+
requires_option :client_secret
|
8
|
+
|
9
|
+
def search(conn, query, latitude, longitude, options = {})
|
10
|
+
results = conn.get("https://api.foursquare.com/v2/venues/search", {query: query, client_id: @provider_options[:client_id], client_secret: @provider_options[:client_secret], v: "20120709",
|
11
|
+
limit: options[:limit], radius: options[:radius], ll: [latitude, longitude].join(",")})
|
12
|
+
parse_results(results)
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_results(results)
|
16
|
+
json = JSON.parse(results.body)
|
17
|
+
venues = json["response"]["venues"]
|
18
|
+
results = []
|
19
|
+
venues.each do |found_venue|
|
20
|
+
venue = Portero::SearchResult.new
|
21
|
+
venue.name = found_venue["name"]
|
22
|
+
venue.address = found_venue["location"]["address"]
|
23
|
+
venue.latitude = found_venue["location"]["lat"]
|
24
|
+
venue.longitude = found_venue["location"]["lng"]
|
25
|
+
venue.city = found_venue["location"]["city"]
|
26
|
+
venue.state = found_venue["location"]["state"]
|
27
|
+
venue.postal_code = found_venue["location"]["postalCode"]
|
28
|
+
venue.country = found_venue["location"]["country"]
|
29
|
+
venue.category = found_venue["categories"].first["name"]
|
30
|
+
venue.icon = found_venue["categories"].first["icon"]["prefix"] + "64" + found_venue["categories"].first["icon"]["suffix"]
|
31
|
+
venue.extra = {categories: found_venue["categories"], url: found_venue["url"]}
|
32
|
+
|
33
|
+
results << venue
|
34
|
+
end
|
35
|
+
results
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Portero
|
2
|
+
module SearchProvider
|
3
|
+
|
4
|
+
class GooglePlaces
|
5
|
+
include SearchProvider
|
6
|
+
|
7
|
+
requires_option :key
|
8
|
+
|
9
|
+
def search(conn, query, latitude, longitude, options = {})
|
10
|
+
results = conn.get("https://maps.googleapis.com/maps/api/place/textsearch/json", {query: query, key: @provider_options[:key],
|
11
|
+
radius: options[:radius], location: [latitude, longitude].join(","), sensor: false})
|
12
|
+
parse_results(results)
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_results(results)
|
16
|
+
json = JSON.parse(results.body)
|
17
|
+
venues = json["results"]
|
18
|
+
results = []
|
19
|
+
venues.each do |found_venue|
|
20
|
+
venue = Portero::SearchResult.new
|
21
|
+
venue.name = found_venue["name"]
|
22
|
+
venue.address = found_venue["formatted_address"]
|
23
|
+
venue.latitude = found_venue["geometry"]["location"]["lat"]
|
24
|
+
venue.longitude = found_venue["geometry"]["location"]["lng"]
|
25
|
+
venue.city = ""
|
26
|
+
venue.state = ""
|
27
|
+
venue.postal_code = ""
|
28
|
+
venue.country = ""
|
29
|
+
venue.category = found_venue["types"].first
|
30
|
+
venue.icon = found_venue["icon"]
|
31
|
+
venue.extra = {types: found_venue["types"]}
|
32
|
+
|
33
|
+
results << venue
|
34
|
+
end
|
35
|
+
results
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
|
3
|
+
module Portero
|
4
|
+
module SearchProvider
|
5
|
+
module ClassMethods
|
6
|
+
|
7
|
+
def api_not_implemented(klass)
|
8
|
+
caller.first.match(/in \`(.+)\'/)
|
9
|
+
method_name = $1
|
10
|
+
raise InterfaceNotImplementedError.new("#{klass.class.name} needs to implement '#{method_name}'!")
|
11
|
+
end
|
12
|
+
|
13
|
+
def requires_option(key)
|
14
|
+
required_options << key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
|
20
|
+
def initialize(options = {})
|
21
|
+
@provider_options = options
|
22
|
+
validate_options
|
23
|
+
end
|
24
|
+
|
25
|
+
def search(connection, query, latitude, longitude, options = {})
|
26
|
+
self.class.api_not_implemented(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def validate_options
|
30
|
+
self.class.required_options.each do |key|
|
31
|
+
raise MissingRequirementError.new("#{self.class.name} needs an options hash key of '#{key}' to function") unless @provider_options.has_key?(key)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.included(receiver)
|
38
|
+
receiver.extend ClassMethods
|
39
|
+
receiver.send :include, InstanceMethods
|
40
|
+
class << receiver
|
41
|
+
class_attribute(:required_options)
|
42
|
+
self.required_options = []
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class InterfaceNotImplementedError < NoMethodError
|
47
|
+
end
|
48
|
+
|
49
|
+
class MissingRequirementError < Exception
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/portero.rb
ADDED
data/portero.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/portero/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Robert Rouse"]
|
6
|
+
gem.email = ["robert@theymaybecoders.com"]
|
7
|
+
gem.description = %q{Portero will help you find venues around a location using as many services known}
|
8
|
+
gem.summary = %q{Portero will help you find venues around a location using as many services known}
|
9
|
+
gem.homepage = "http://github.com/theymaybecoders"
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "portero"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Portero::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency "rspec", '~>2.0'
|
19
|
+
gem.add_development_dependency "shoulda-matchers", '~>1.2'
|
20
|
+
gem.add_development_dependency "simplecov"
|
21
|
+
gem.add_development_dependency "fakeweb"
|
22
|
+
|
23
|
+
gem.add_dependency "faraday", '~>0.8'
|
24
|
+
gem.add_dependency "activesupport", '~> 3'
|
25
|
+
end
|