rlocu 0.1.0 → 0.2.1
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/README.md +27 -26
- data/Rakefile +8 -7
- data/lib/rlocu.rb +21 -6
- data/lib/rlocu/geo_json.rb +22 -0
- data/lib/rlocu/menu.rb +113 -99
- data/lib/rlocu/query_builder.rb +55 -0
- data/lib/rlocu/utilities/lat_long_radius.rb +27 -0
- data/lib/rlocu/venue.rb +107 -24
- data/lib/rlocu/venue_query.rb +35 -0
- data/lib/rlocu/venue_search.rb +39 -21
- data/lib/rlocu/version.rb +1 -1
- data/rlocu.gemspec +13 -12
- data/spec/config_rlocu_spec_helper.rb +5 -0
- data/spec/geo_json_spec.rb +12 -0
- data/spec/menu/item_spec.rb +13 -0
- data/spec/menu/menu_item_spec.rb +13 -0
- data/spec/menu/option_group_spec.rb +10 -0
- data/spec/menu/option_spec.rb +9 -0
- data/spec/menu/section_spec.rb +9 -0
- data/spec/menu/section_text_spec.rb +9 -0
- data/spec/menu/subsection_spec.rb +9 -0
- data/spec/menu_spec.rb +9 -0
- data/spec/query/key_value_condition_spec.rb +37 -0
- data/spec/spec_helper.rb +99 -0
- data/spec/support/rspec_helpers.rb +53 -0
- data/spec/utilities/lat_long_radius_spec.rb +7 -0
- data/spec/venue_query_spec.rb +29 -0
- data/spec/venue_search_spec.rb +31 -0
- metadata +59 -25
- data/lib/rlocu/config.rb +0 -23
- data/lib/rlocu/venue_details.rb +0 -35
- data/lib/utilities.rb +0 -40
- data/test/test_truth.rb +0 -7
- data/test/test_utilities.rb +0 -21
- data/test/test_venue.rb +0 -15
- data/test/test_venue_details.rb +0 -11
- data/test/test_venue_search.rb +0 -26
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rlocu
|
2
|
+
module Utilities
|
3
|
+
class LatLongRadius
|
4
|
+
attr_reader :lat, :long, :radius
|
5
|
+
def initialize(lat:, long:, radius: 5000)
|
6
|
+
if !lat.is_a?(Numeric) || !long.is_a?(Numeric) || !radius.is_a?(Numeric)
|
7
|
+
raise ArgumentError.new "Parameters must be numeric (floats)"
|
8
|
+
end
|
9
|
+
@lat = lat
|
10
|
+
@long = long
|
11
|
+
@radius = radius
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.bounds(lat_lng1, lat_lng2)
|
15
|
+
"#{@lat_lng1.to_s}|#{@lat_lng2.to_s}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"#{lat},#{long}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_a
|
23
|
+
[lat, long, radius]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/rlocu/venue.rb
CHANGED
@@ -1,34 +1,117 @@
|
|
1
1
|
module Rlocu
|
2
2
|
class Venue
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
def initialize(
|
3
|
+
ExternalID = Struct.new(:id, :url, :mobile_url)
|
4
|
+
Category = Struct.new(:name, :str_id)
|
5
|
+
Contact = Struct.new(:phone, :fax, :email, :phones, :faxes, :emails, :business_owner)
|
6
|
+
# skipping Locu object for now
|
7
|
+
Delivery = Struct.new(:will_deliver, :hours, :minimum_order, :areas)
|
8
|
+
# skipping Extended object for now
|
9
|
+
Media = Struct.new(:cover_photo, :venue_photos, :menu_photos, :logos, :videos)
|
10
|
+
ATTRIBUTES = %i{locu_id name short_name description website_url menu_url menus menu_items open_hours external redirected_from categories location contact locu delivery extended media}
|
11
|
+
|
12
|
+
ATTRIBUTES.each { |prop| attr_accessor prop }
|
13
|
+
|
14
|
+
def initialize(venue)
|
15
|
+
@external = []
|
16
|
+
@categories = []
|
15
17
|
@menus = []
|
16
|
-
|
18
|
+
@not_supported_attributes = []
|
19
|
+
build_from_hash(venue)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_from_hash(venue)
|
23
|
+
venue.each do |k,v|
|
24
|
+
begin
|
25
|
+
self.send("#{k.to_s}=", v)
|
26
|
+
rescue NoMethodError
|
27
|
+
@not_supported_attributes << k
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def latitude
|
33
|
+
location.latitude
|
34
|
+
end
|
35
|
+
|
36
|
+
def longitude
|
37
|
+
location.longitude
|
17
38
|
end
|
18
|
-
|
19
|
-
def
|
20
|
-
|
39
|
+
|
40
|
+
def not_supported_attributes
|
41
|
+
@not_supported_attributes
|
42
|
+
end
|
43
|
+
|
44
|
+
def external=(externals_list)
|
45
|
+
@external = []
|
46
|
+
externals_list.each { |external_id| @external << ExternalID.new(id: external_id['id'], url: external_id['url'], mobile_url: external_id['mobile_url'])}
|
47
|
+
end
|
48
|
+
|
49
|
+
def categories=(categories_list)
|
50
|
+
@categories = []
|
51
|
+
categories_list.each do |category|
|
52
|
+
c = Category.new
|
53
|
+
category.each { |k,v| c.send("#{k.to_s}=", v) }
|
54
|
+
@categories << c
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def location=(location)
|
59
|
+
@location = Location.new(location)
|
21
60
|
end
|
22
61
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
62
|
+
def contact=(contact)
|
63
|
+
c = Contact.new
|
64
|
+
contact.each { |k,v| c.send("#{k.to_s}=", v) }
|
65
|
+
@location = c
|
66
|
+
end
|
67
|
+
|
68
|
+
def delivery=(delivery)
|
69
|
+
d = Delivery.new
|
70
|
+
delivery.each { |k,v| d.send("#{k.to_s}=", v) }
|
71
|
+
@location = l
|
72
|
+
end
|
73
|
+
|
74
|
+
def extended=(extended)
|
75
|
+
e = Extended.new
|
76
|
+
extended.each { |k,v| e.send("#{k.to_s}=", v) }
|
77
|
+
@extended = e
|
78
|
+
end
|
79
|
+
|
80
|
+
def media=(media)
|
81
|
+
m = Media.new
|
82
|
+
media.each { |k,v| m.send("#{k.to_s}=", v) }
|
83
|
+
@media = m
|
84
|
+
end
|
85
|
+
|
86
|
+
def menus=(menu_list)
|
87
|
+
@menus = []
|
88
|
+
menu_list.each do |menu|
|
89
|
+
@menus << Menu.new(menu)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class Location
|
94
|
+
attr_reader :address1, :address2, :address3, :locality, :region, :postal_code, :country, :geo
|
95
|
+
def initialize(location)
|
96
|
+
@address1 = location['address1']
|
97
|
+
@address2 = location['address2']
|
98
|
+
@address3 = location['address3']
|
99
|
+
@locality = location['locality']
|
100
|
+
@region = location['region']
|
101
|
+
@postal_code = location['postal_code']
|
102
|
+
@country = location['country']
|
103
|
+
@geo = GeoJSON.new(location['geo'])
|
104
|
+
end
|
105
|
+
|
106
|
+
def latitude
|
107
|
+
geo.latitude
|
108
|
+
end
|
109
|
+
|
110
|
+
def longitude
|
111
|
+
geo.longitude
|
30
112
|
end
|
31
113
|
end
|
32
114
|
|
115
|
+
# skipping menu_items since they can be accessed through the menus and only 15 are returned currently
|
33
116
|
end
|
34
|
-
end
|
117
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rest-client'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Rlocu
|
5
|
+
class VenueQuery
|
6
|
+
include Rlocu::QueryBuilder
|
7
|
+
VenueQueryError = Class.new(StandardError)
|
8
|
+
|
9
|
+
attr_reader :return_fields
|
10
|
+
|
11
|
+
def initialize(query_conditions:, return_fields:)
|
12
|
+
raise ArgumentError, 'Query Conditions Param must be an array of QueryConditions.' if !query_conditions.is_a?(Array) || !query_conditions.first.is_a?(QueryCondition)
|
13
|
+
|
14
|
+
raise ArgumentError, 'Return Fields Param must be an array of fields.' if !return_fields.is_a?(Array) || return_fields.empty?
|
15
|
+
@query_conditions = query_conditions
|
16
|
+
@return_fields = return_fields
|
17
|
+
end
|
18
|
+
|
19
|
+
def query_conditions
|
20
|
+
@query_conditions.map(&:to_h)
|
21
|
+
end
|
22
|
+
|
23
|
+
def form_data
|
24
|
+
{api_key: Rlocu.api_key, fields: return_fields, venue_queries: query_conditions }.to_json
|
25
|
+
end
|
26
|
+
|
27
|
+
def query
|
28
|
+
# TODO wrap this in a timeout
|
29
|
+
result = JSON.parse(RestClient.post(base_url, form_data))
|
30
|
+
status = result['status']
|
31
|
+
raise VenueQueryError.new("Query failed with status [#{status}] and http_status [#{result['http_status']}]") unless status == 'success'
|
32
|
+
result['venues'].each.reduce([]) { |accum, venue| accum << Rlocu::Venue.new(venue) }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rlocu/venue_search.rb
CHANGED
@@ -1,27 +1,45 @@
|
|
1
|
-
require 'open-uri'
|
2
|
-
require 'JSON'
|
3
|
-
|
4
1
|
module Rlocu
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
def self.query(params, &block)
|
10
|
-
unsupported = params.keys.reduce([]) {|memo,p| memo << p unless PARAMETERS.include? p; memo }
|
11
|
-
raise ArgumentError.new "Unsupported parameter to Rlocu::VenuSearch (#{unsupported})" unless unsupported.empty?
|
12
|
-
response = {}
|
13
|
-
open(url(params)) {|request| response = JSON.parse(request.read)}
|
14
|
-
venues = []
|
15
|
-
venues = response['objects'].reduce(venues) {|arr, o| arr << Rlocu::Venue.new(o) ; arr }
|
16
|
-
venues.each { |venue| yield venue } if block_given?
|
17
|
-
return venues
|
2
|
+
class VenueSearch
|
3
|
+
def initialize(return_fields: [])
|
4
|
+
@return_fields = return_fields.empty? ? %w{locu_id name location description menus} : return_fields
|
5
|
+
@key_value_conditions = []
|
18
6
|
end
|
19
7
|
|
20
|
-
|
8
|
+
def with_menus
|
9
|
+
@key_value_conditions << QueryBuilder::KeyValueCondition.new(key: 'menus', value: true, condition: '$present')
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def in_lat_long_radius(lat:, long:, radius:)
|
14
|
+
lat_long_radius = Utilities::LatLongRadius.new(lat: lat, long: long, radius: radius)
|
15
|
+
@key_value_conditions << QueryBuilder::KeyValueCondition.new(key: 'location', value: lat_long_radius, condition: '$in_lat_lng_radius')
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def locu_id(locu_id)
|
20
|
+
@key_value_conditions << QueryBuilder::KeyValueCondition.new(key: 'locu_id', value: locu_id)
|
21
|
+
self
|
22
|
+
end
|
21
23
|
|
22
|
-
def
|
23
|
-
|
24
|
+
def name(name)
|
25
|
+
@key_value_conditions << QueryBuilder::KeyValueCondition.new(key: 'name', value: name)
|
26
|
+
self
|
24
27
|
end
|
25
|
-
|
28
|
+
|
29
|
+
def in_categories(categories)
|
30
|
+
raise ArgumentError unless categories.is_a? Array
|
31
|
+
@key_value_conditions << QueryBuilder::KeyValueCondition.new(key: 'categories', value: categories, condition: '$contains_any')
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def search
|
36
|
+
conditions = [QueryBuilder::QueryCondition.new(key_value_conditions: key_value_conditions)]
|
37
|
+
venue_query = VenueQuery.new(query_conditions: conditions, return_fields: return_fields)
|
38
|
+
venue_query.query
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :return_fields, :key_value_conditions
|
26
44
|
end
|
27
|
-
end
|
45
|
+
end
|
data/lib/rlocu/version.rb
CHANGED
data/rlocu.gemspec
CHANGED
@@ -3,17 +3,18 @@ lib = File.expand_path('../lib', __FILE__)
|
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
4
|
require 'rlocu/version'
|
5
5
|
|
6
|
-
Gem::Specification.new do |
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "rlocu"
|
8
|
+
spec.version = Rlocu::VERSION
|
9
|
+
spec.authors = ["Stephen Philp"]
|
10
|
+
spec.email = ["stephen@stephenphilp.com"]
|
11
|
+
spec.description = %q{A simple ruby wrapper for the Locu API}
|
12
|
+
spec.summary = %q{Rlocu is a ruby wrapper for Locu's API. The Locu API gives you access to real-time local business data, from opening hours to price lists, such as restaurant menus.}
|
13
|
+
spec.homepage = "https://github.com/swelltrain/rlocu"
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
spec.add_development_dependency('rspec')
|
18
|
+
spec.add_runtime_dependency('rest-client')
|
19
|
+
spec.require_paths = ["lib"]
|
19
20
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
include Rlocu
|
3
|
+
|
4
|
+
RSpec.describe GeoJSON, '#new' do
|
5
|
+
it "should raise ArgumentError if geo['type'] does not exist" do
|
6
|
+
expect{ GeoJSON.new({}) }.to raise_error(ArgumentError)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should raise ArgumentError if geo['coordinates'] does not exist" do
|
10
|
+
expect{ GeoJSON.new({'type' => 'Point'}) }.to raise_error(ArgumentError)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
include Rlocu
|
2
|
+
|
3
|
+
RSpec.describe Menu::Item, '#new' do
|
4
|
+
it 'should set type, name, description, price and option groups' do
|
5
|
+
item = Menu::Item.new(item_hash)
|
6
|
+
|
7
|
+
expect(item.type).to eq(item_hash['type'])
|
8
|
+
expect(item.name).to eq(item_hash['name'])
|
9
|
+
expect(item.description).to eq(item_hash['description'])
|
10
|
+
expect(item.price).to eq(item_hash['price'])
|
11
|
+
expect(item.option_groups).not_to be_empty
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
include Rlocu
|
2
|
+
|
3
|
+
RSpec.describe Menu::MenuItem, '#new' do
|
4
|
+
it 'should set menu_name, section_name, subsection_name, section_text, type, currency_symbol and photos' do
|
5
|
+
menu_item = Menu::MenuItem.new(menu_menu_item_hash)
|
6
|
+
expect(menu_item.menu_name).to eq(menu_menu_item_hash['menu_name'])
|
7
|
+
expect(menu_item.section_name).to eq(menu_menu_item_hash['section_name'])
|
8
|
+
expect(menu_item.subsection_name).to eq(menu_menu_item_hash['subsection_name'])
|
9
|
+
expect(menu_item.section_text).to eq(menu_menu_item_hash['section_text'])
|
10
|
+
expect(menu_item.currency_symbol).to eq(menu_menu_item_hash['currency_symbol'])
|
11
|
+
expect(menu_item.photos).not_to be_empty
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
include Rlocu
|
2
|
+
|
3
|
+
RSpec.describe Menu::OptionGroup, '#new' do
|
4
|
+
it 'should set the name, text and options' do
|
5
|
+
option_group = Menu::OptionGroup.new(menu_option_group)
|
6
|
+
expect(option_group.type).to eq(menu_option_group['type'])
|
7
|
+
expect(option_group.text).to eq(menu_option_group['text'])
|
8
|
+
expect(option_group.options).not_to be_empty
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
include Rlocu
|
2
|
+
|
3
|
+
RSpec.describe Menu::Section, '#new' do
|
4
|
+
it 'should set section name and subsections' do
|
5
|
+
menu_section = Menu::Section.new(menu_section_hash)
|
6
|
+
expect(menu_section.section_name).to eq(menu_section_hash['section_name'])
|
7
|
+
expect(menu_section.subsections).not_to be_empty
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
include Rlocu
|
2
|
+
|
3
|
+
RSpec.describe Menu::SectionText, '#new' do
|
4
|
+
it 'should set type and text' do
|
5
|
+
menu_section_text = Menu::SectionText.new(menu_section_text_hash)
|
6
|
+
expect(menu_section_text.type).to eq(menu_section_text_hash['type'])
|
7
|
+
expect(menu_section_text.text).to eq(menu_section_text_hash['text'])
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
include Rlocu
|
2
|
+
|
3
|
+
RSpec.describe Menu::Subsection, '#new' do
|
4
|
+
it 'should set subsection_name and contents' do
|
5
|
+
menu_subsection = Menu::Subsection.new(menu_subsection_hash)
|
6
|
+
expect(menu_subsection.subsection_name).to eq(menu_subsection_hash['subsection_name'])
|
7
|
+
expect(menu_subsection.contents).not_to be_empty
|
8
|
+
end
|
9
|
+
end
|
data/spec/menu_spec.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
include Rlocu::QueryBuilder
|
2
|
+
include Rlocu::Utilities
|
3
|
+
|
4
|
+
RSpec.describe KeyValueCondition, '#new' do
|
5
|
+
context "when condition is not one of #{KeyValueCondition::ValidConditions}" do
|
6
|
+
it 'should raise ArgumentError' do
|
7
|
+
expect{ KeyValueCondition.new(key: 'spec', value: 'spec', condition: 'spec') }.to raise_error(ArgumentError)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'when condition is $in_lat_lng_radius' do
|
12
|
+
context 'when value is not a LatLongRadius' do
|
13
|
+
it 'should raise an ArgumentError' do
|
14
|
+
expect{ KeyValueCondition.new(key: 'location', value: 'spec', condition: '$in_lat_lng_radius') }.to raise_error(ArgumentError)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
RSpec.describe KeyValueCondition, '#to_h' do
|
21
|
+
let(:value) { LatLongRadius.new(lat: 1, long: 1) }
|
22
|
+
|
23
|
+
let!(:key_value_condition) {KeyValueCondition.new(key: 'location', value: value, condition: '$in_lat_lng_radius')}
|
24
|
+
|
25
|
+
context 'when condition is specified' do
|
26
|
+
context 'when value is a LatLongRadius' do
|
27
|
+
it 'should call to_a on the value' do
|
28
|
+
expect(value).to receive(:to_a)
|
29
|
+
key_value_condition.to_h
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should return a hash' do
|
35
|
+
expect(key_value_condition.to_h).to be_a_kind_of(Hash)
|
36
|
+
end
|
37
|
+
end
|