diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..43ae203 --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--color +--require spec_helper +--format documentation diff --git a/lib/tsibog.rb b/lib/tsibog.rb new file mode 100644 index 0000000..8969981 --- /dev/null +++ b/lib/tsibog.rb @@ -0,0 +1,21 @@ +require 'foursquare2' + +module Tsibog + CLIENT = Foursquare2::Client.new(:client_id => 'LQSHZK4YX3P5DTIUPRET2EDJ53SZPQSSCXG5IZXDMMQHRR0L', :client_secret => '11FO21GAOBGCIS3S5JEJADXMRH2IIVTIW4DIZANVR4LAV0JE', api_version: 20140806) + FOOD = '4d4b7105d754a06374d81259' + + MAX_RADIUS = 100_000 # meters away + WALKING_DISTANCE = 80 # meters or 1 minute walk + TEN_MINUTES_AWAY = WALKING_DISTANCE * 10 # minutes + + def self.food_venues + Venues.new(CLIENT.method :search_venues).with_category(FOOD).within(TEN_MINUTES_AWAY) + end + + def self.[](id) + FoodPlace.new CLIENT.venue id + end + + require_relative 'tsibog/venues' + require_relative 'tsibog/food_place' +end diff --git a/lib/tsibog/food_place.rb b/lib/tsibog/food_place.rb new file mode 100644 index 0000000..cc17cd4 --- /dev/null +++ b/lib/tsibog/food_place.rb @@ -0,0 +1,47 @@ +module Tsibog + class FoodPlace + def initialize venue + @venue = venue + end + + def id + @venue.id + end + + def name + @venue.name + end + + def address + @venue.location.address + end + + def full_address + @venue.location.formattedAddress.join(', ') + end + + def operations + timeframes.map {|tf| StoreOperation.new(tf.days, tf.open) } + end + + private + + def timeframes + hours ? hours.timeframes : [] + end + + def hours + @venue.hours || @venue.popular + end + end + + class StoreOperation < Struct.new(:day, :open_hours) + def to_s + "#{day}: #{hours.join(', ')}" + end + + def hours + open_hours.map {|hour| hour['renderedTime'] } + end + end +end \ No newline at end of file diff --git a/lib/tsibog/venues.rb b/lib/tsibog/venues.rb new file mode 100644 index 0000000..3849617 --- /dev/null +++ b/lib/tsibog/venues.rb @@ -0,0 +1,67 @@ +require 'forwardable' + +module Tsibog + class Venues + def initialize(request) + raise ArgumentError, ":request must respond to []" unless request.respond_to?(:[]) + @request = request + end + + def options + @options ||= {} + end + + def categories + (options['categoryId'] || '').split(',') + end + + def with_category(category) + chain 'categoryId' => (categories << category).join(',') + end + + def near(latlng, accuracy_in_meters = nil) + chain empty_or('llAcc', accuracy_in_meters).merge(ll: latlng) + end + + def above(meters, accuracy_in_meters = nil) # altitude + chain empty_or('altAcc', accuracy_in_meters).merge(alt: meters) + end + + def within(meters) + chain radius: meters + end + + def top(quantity) + chain limit: quantity + end + + def search(term) + chain query: term + end + + def for(checkin_or_match_or_specials) + chain intent: checkin_or_match_or_specials + end + + include Enumerable + extend Forwardable + + def_delegators :request_venues, :each + def_delegators :to_a, :sample, :length, :size + + protected + + def request_venues + @request[options]['venues'] + end + + def chain(new_option) + (duplicate = dup).options.merge! new_option + duplicate + end + + def empty_or(key, value) + value.nil? ? {} : {key => value} + end + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..607474b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,89 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause this +# file to always be loaded, without a need to explicitly require it in any files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need it. +# +# The `.rspec` file also contains a few flags that are not defaults but that +# users commonly want. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + # Limits the available syntax to the non-monkey patched syntax that is recommended. + # For more details, see: + # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax + # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/spec/tsibog/venues_spec.rb b/spec/tsibog/venues_spec.rb new file mode 100644 index 0000000..9eb2c59 --- /dev/null +++ b/spec/tsibog/venues_spec.rb @@ -0,0 +1,111 @@ +require 'tsibog/venues' + +describe Tsibog::Venues do + subject { Tsibog::Venues.new(request) } + + let(:request) { Hash.new('venues' => fetched_data) } + let(:fetched_data) { [{id: 1, name: 'Jollibee'}, {id: 2, name: 'Chowking'}, {id: 3, name: 'Mang inasal'}] } + + FOOD = '4d4b7105d754a06374d81259' + HERE = '14.6371574,121.073077' + + describe "#initialize" do + context :options do + it { expect(subject.options).to be_empty } + end + + context :categories do + it { expect(subject.categories).to be_empty } + end + + context "when argument doesn't support []" do + let(:request) { nil } + + it { expect{subject}.to raise_error(ArgumentError) } + end + end + + describe "#with_category" do + let(:food_venues) { subject.with_category(FOOD) } + + context :categories do + it { expect(food_venues.categories).to include(FOOD) } + end + + context :options do + it { expect(food_venues.options).to eq('categoryId' => FOOD) } + end + end + + describe "#near" do + context :options do + it { expect(subject.near(HERE).options).to eq(ll: HERE) } + + describe "with accuracy in meters" do + it { expect(subject.near(HERE, 10).options).to eq(:ll => HERE, 'llAcc' => 10) } + end + end + end + + describe "#above" do + context :options do + it { expect(subject.above(100).options).to eq(alt: 100) } + + describe "with accuracy in meters" do + it { expect(subject.above(100, 1).options).to eq(alt: 100, 'altAcc' => 1) } + end + end + end + + describe "#within" do + context :options do + it { expect(subject.within(80).options).to eq(radius: 80) } + end + end + + describe "#top" do + context :options do + it { expect(subject.top(20).options).to eq(limit: 20) } + end + end + + describe "#search" do + context :options do + it { expect(subject.search('pizza').options).to eq(query: 'pizza') } + end + end + + describe "#for" do + context :options do + it { expect(subject.for('checkin').options).to eq(intent: 'checkin') } + end + end + + describe "when methods chained" do + context "returned output" do + let(:returned_output) { subject.with_category(FOOD).near(HERE).above(100).top(20).search('coffee').for('specials') } + + context :options do + it "retain inputs" do + expect(returned_output.options).to eq('categoryId' => FOOD, :ll => HERE, :alt => 100, :limit => 20, :query => 'coffee', :intent => 'specials') + end + end + + it { expect(returned_output).to be_instance_of(subject.class) } + end + end + + describe "when enumerated" do + let(:venues) { subject.within(800).near(HERE, 10).above(100, 1).top(20).search('restaurant').for('match') } + + context :request do + it "receive #options" do + expect(request).to receive(:[]).with(venues.options).and_return('venues' => ['somewhere']) + end + end + + after do + expect(venues.sample).to eq('somewhere') + end + end +end \ No newline at end of file diff --git a/tsibog.rb b/tsibog.rb index d8adf71..be532a0 100644 --- a/tsibog.rb +++ b/tsibog.rb @@ -1,79 +1,8 @@ -require 'foursquare2' +require_relative 'lib/tsibog' -module Tsibog - CLIENT = Foursquare2::Client.new(:client_id => 'LQSHZK4YX3P5DTIUPRET2EDJ53SZPQSSCXG5IZXDMMQHRR0L', :client_secret => '11FO21GAOBGCIS3S5JEJADXMRH2IIVTIW4DIZANVR4LAV0JE', api_version: 20140806) - FOOD = '4d4b7105d754a06374d81259' - - MAX_RADIUS = 100_000 # meters away - WALKING_DISTANCE = 80 # meters or 1 minute walk - TEN_MINUTES_AWAY = WALKING_DISTANCE * 10 # minutes - - def self.food_places_near(geodata, top = 20) - CLIENT.search_venues(:ll => geodata, :limit => top, 'categoryId' => FOOD, :radius => TEN_MINUTES_AWAY).venues - end - - def self.food_place id - FoodPlace.new CLIENT.venue id - end - - class FoodPlace - def initialize venue - @venue = venue - end - - def id - @venue.id - end - - def name - @venue.name - end - - def address - @venue.location.address - end - - def full_address - @venue.location.formattedAddress.join(', ') - end - - def operations - timeframes.map {|tf| StoreOperation.new(tf.days, tf.open) } - end - - private - - def timeframes - hours ? hours.timeframes : [] - end - - def hours - @venue.hours || @venue.popular - end - end - - class StoreOperation < Struct.new(:day, :open_hours) - def to_s - "#{day}: #{hours.join(', ')}" - end - - def hours - open_hours.map {|hour| hour['renderedTime'] } - end - end -end - -class Tsibog::Application +class Tsibog::CLI COORDINATES_OF = { '47East.ph' => '14.6371574,121.073077' } - def top(limit = 20) - Tsibog.food_places_near(COORDINATES_OF['47East.ph'], limit) - end - - def random_select - top(20).sample - end - def print_details restaurant puts restaurant.name puts restaurant.address @@ -82,10 +11,10 @@ def print_details restaurant end end - def initialize id = random_select.id - restaurant = Tsibog.food_place id + def initialize here = COORDINATES_OF['47East.ph'] + restaurant = Tsibog[ Tsibog.food_venues.near(here).top(20).sample.id ] print_details restaurant end end -app = Tsibog::Application.new +app = Tsibog::CLI.new