citibike 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,104 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'citibike/api'
4
+ require 'citibike/response'
5
+
6
+ module Citibike
7
+
8
+ # Client for interacting with the Citibike NYC
9
+ # unofficial API
10
+ class Client
11
+
12
+ attr_reader :options, :connection
13
+
14
+ VALID_KEYS = [:unwrapped]
15
+
16
+ def initialize(opts = {})
17
+ @options = {}
18
+ # Parse out the valid keys for this class
19
+ VALID_KEYS.each do |vk|
20
+ @options[vk] = opts[vk]
21
+ end
22
+
23
+ # Raw forces unwrapped to be true
24
+ if opts[:raw]
25
+ @options[:unwrapped] = true
26
+ end
27
+
28
+ @connection = Citibike::Connection.new(opts)
29
+ end
30
+
31
+ #
32
+ # Wrapper around a call to list all stations
33
+ #
34
+ # @return [Response] [A response object unless]
35
+ def stations
36
+ resp = self.connection.request(
37
+ :get,
38
+ Citibike::Station.path
39
+ )
40
+
41
+ return resp if @options[:unwrapped]
42
+
43
+ Citibike::Responses::Station.new(resp)
44
+ end
45
+
46
+ def helmets
47
+ resp = self.connection.request(
48
+ :get,
49
+ Citibike::Station.path
50
+ )
51
+
52
+ return resp if @options[:unwrapped]
53
+
54
+ Citibike::Responses::Helmet.new(resp)
55
+ end
56
+
57
+ def branches
58
+ resp = self.connection.request(
59
+ :get,
60
+ Citibike::Station.path
61
+ )
62
+
63
+ return resp if @options[:unwrapped]
64
+
65
+ Citibike::Responses::Branch.new(resp)
66
+ end
67
+
68
+ def self.stations
69
+ Citibike::Responses::Station.new(
70
+ # create a new connection in case
71
+ self.connection.request(
72
+ :get,
73
+ Citibike::Station.path
74
+ )
75
+ )
76
+ end
77
+
78
+ def self.helmets
79
+ Citibike::Responses::Helmet.new(
80
+ self.connection.request(
81
+ :get,
82
+ Citibike::Helmet.path
83
+ )
84
+ )
85
+ end
86
+
87
+ def self.branches
88
+ Citibike::Responses::Branch.new(
89
+ self.connection.request(
90
+ :get,
91
+ Citibike::Branch.path
92
+ )
93
+ )
94
+ end
95
+
96
+ private
97
+
98
+ def self.connection
99
+ @connection ||= Citibike::Connection.new
100
+ end
101
+
102
+ end
103
+
104
+ end
@@ -0,0 +1,85 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'faraday'
4
+ require 'faraday_middleware'
5
+ require 'yajl'
6
+
7
+ module Citibike
8
+ # Class representing a Faraday instance used to connect to the
9
+ # actual Citibike API, takes a variety of options
10
+ class Connection
11
+
12
+ def initialize(opts = {})
13
+ @options = {
14
+ adapter: Faraday.default_adapter,
15
+ headers: {
16
+ 'Accept' => 'application/json; charset=utf-8',
17
+ 'UserAgent' => 'Citibike Gem'
18
+ },
19
+ proxy: nil,
20
+ ssl: {
21
+ verify: false
22
+ },
23
+ debug: false,
24
+ test: false,
25
+ stubs: nil,
26
+ raw: false,
27
+ format_path: true,
28
+ format: :json,
29
+ url: 'http://appservices.citibikenyc.com/'
30
+ }
31
+ # Reject any keys that aren't already in @options
32
+ @options.keys.each do |k|
33
+ @options[k] = opts[k] if opts.key?(k)
34
+ end
35
+ end
36
+
37
+ def http
38
+ @http ||= Faraday.new(@options) do |connection|
39
+ connection.use Faraday::Request::UrlEncoded
40
+ connection.use Faraday::Response::ParseJson unless @options[:raw]
41
+ connection.use Faraday::Response::Logger if @options[:debug]
42
+ if @options[:test]
43
+ connection.adapter(@options[:adapter], @options[:stubs])
44
+ else
45
+ connection.adapter(@options[:adapter])
46
+ end
47
+ end
48
+ end
49
+
50
+ #
51
+ # Makes a request to the API through the set up interface
52
+ # @param method [Symbol] [:get, :post, :put, :delete]
53
+ # @param path [String] [The path to request from the server]
54
+ # @param options = {} [Optional Hash] [Args to include in the request]
55
+ #
56
+ # @return [String or Hash] [The result of the request, generally a hash]
57
+ def request(method, path, options = {})
58
+ if @options[:format_path]
59
+ path = format_path(path, @options[:format])
60
+ end
61
+ response = self.http.send(method) do |request|
62
+ case method
63
+ when :get, :delete
64
+ request.url(path, options)
65
+ when :post, :put
66
+ request.path = path
67
+ request.body = options unless options.empty?
68
+ end
69
+ end
70
+ response.body
71
+ end
72
+
73
+ private
74
+
75
+ def format_path(path, format)
76
+ if path =~ /\./
77
+ path
78
+ else
79
+ [path, format].map(&:to_s).compact.join('.')
80
+ end
81
+ end
82
+
83
+ end
84
+
85
+ end
@@ -0,0 +1,119 @@
1
+ # encoding: UTF-8
2
+
3
+ module Citibike
4
+
5
+ # Base class that defines the shared behavior for
6
+ # all the different response types
7
+ class Response
8
+
9
+ include Enumerable
10
+
11
+ Dir[File.expand_path('../responses/*.rb', __FILE__)].each { |f| require f }
12
+
13
+ # allow direct access to the data stored in this object
14
+ attr_reader :data, :success, :last_update
15
+
16
+ # undefine methods from this class so they get proxied to data
17
+ undef_method :to_s, :inspect
18
+
19
+ # Initializes a Response object
20
+ # @param data [Hash] [Response data from an api request]
21
+ #
22
+ # @return [Nil] [This return value is ignored]
23
+ def initialize(data)
24
+ @data = data['results']
25
+ @success = data['ok']
26
+ @last_update = Time.at(data['last_updated'].to_i)
27
+
28
+ # build the id_hash
29
+ @id_hash ||= {}
30
+ @data.each do |d|
31
+ @id_hash[d.id] = d
32
+ end
33
+ end
34
+
35
+ #
36
+ # Returns true if this holds a successful response
37
+ #
38
+ # @return [Boolean] [Whether the request succeeded]
39
+ def success?
40
+ @success == true
41
+ end
42
+
43
+ # each provided to include enumberable
44
+ # @param &block [Block] [For enumerable]
45
+ #
46
+ # @return [Enumerable] [Another enumerable]
47
+ def each(&block)
48
+ self.data.each(&block)
49
+ end
50
+
51
+ #
52
+ # Clones this object and populates
53
+ # @param data [Array] [Array of API objects]
54
+ #
55
+ # @return [Response] [A clone of self wrapping data]
56
+ def clone_with(data)
57
+ clone = self.clone
58
+ clone.instance_variable_set(:@data, data)
59
+ clone
60
+ end
61
+
62
+ #
63
+ # Finds the given object by id using a hash for a shortcut
64
+ # @param id [Integer] [The id to lookup]
65
+ #
66
+ # @return [Citibike::Api] [Some type of API object e.g Helmet or Station]
67
+ def find_by_id(id)
68
+ id = id.to_i
69
+
70
+ @id_hash[id]
71
+ end
72
+
73
+ def find_by_ids(*ids)
74
+ ids.map { |i| self.find_by_id(i) }.compact
75
+ end
76
+
77
+ #
78
+ # Returns every object within dist (miles)
79
+ # of lat/long
80
+ #
81
+ # @param lat [Float] [A latitude position]
82
+ # @param long [Float] [A longitude position]
83
+ # @param dist [Float] [A radius in miles]
84
+ #
85
+ # @return [Array] [Array of objects within dist of lat/long]
86
+ def all_within(lat, long, dist)
87
+ @data.select { |d| d.distance_from(lat, long) < dist }
88
+ end
89
+
90
+ #
91
+ # Returns every object within dist miles
92
+ # of obj
93
+ #
94
+ # @param obj [Api] [Api Object]
95
+ # @param dist [Float] [Distance to consider]
96
+ #
97
+ # @return [type] [description]
98
+ def all_near(obj, dist)
99
+ @data.select do |d|
100
+ if d.id == obj.id
101
+ false
102
+ else
103
+ d.distance_from(obj.latitude, obj.longitude) < dist
104
+ end
105
+ end
106
+ end
107
+
108
+ # Delegates any undefined methods to the underlying data
109
+ def method_missing(sym, *args, &block)
110
+ if self.data.respond_to?(sym)
111
+ return self.data.send(sym, *args, &block)
112
+ end
113
+
114
+ super
115
+ end
116
+
117
+ end
118
+
119
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: UTF-8
2
+
3
+ module Citibike
4
+ # Namespace for different response objects
5
+ # representing a list of api objects
6
+ module Responses
7
+ # Represents a list of Branch API objects
8
+ class Branch < Citibike::Response
9
+
10
+ #
11
+ # Transforms part of the input hash to the proper
12
+ # type of object. Expects the keys:
13
+ # {
14
+ # :data => [{...}, {...}],
15
+ # :ok => true/false,
16
+ # :last_updated => Time
17
+ # }
18
+ #
19
+ # @param data [Hash] [A hash from the Citibike API]
20
+ #
21
+ # @return [nil] [Not used]
22
+ def initialize(data = {})
23
+ data['results'].map! { |r| Citibike::Branch.new(r) }
24
+ super
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+
3
+ module Citibike
4
+
5
+ module Responses
6
+ # Represents a list of Helmet API objects
7
+ class Helmet < Citibike::Response
8
+
9
+ def initialize(data)
10
+ data['results'].map! { |r| Citibike::Helmet.new(r) }
11
+ super
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: UTF-8
2
+
3
+ module Citibike
4
+
5
+ module Responses
6
+ # Represents a list of Station api objects
7
+ class Station < Citibike::Response
8
+
9
+ def initialize(data)
10
+ data['results'].map! { |r| Citibike::Station.new(r) }
11
+ super
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+
3
+ module Citibike
4
+ VERSION = '0.0.1'
5
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe Citibike::Station do
4
+
5
+ let(:stations) do
6
+ Citibike.stations
7
+ end
8
+
9
+ let(:stat) do
10
+ stations.first
11
+ end
12
+
13
+ around(:each) do |example|
14
+ VCR.use_cassette(:station, record: :new_episodes) do
15
+ example.run
16
+ end
17
+ end
18
+
19
+ it "should provide consistent accessor methods" do
20
+ stat.nearby_stations.should be_a(Array)
21
+ # this field is nil for some reason
22
+ stat.should respond_to(:station_address)
23
+ stat.available_docks.should be_a(Integer)
24
+ stat.available_bikes.should be_a(Integer)
25
+
26
+ stat.should be_active
27
+ end
28
+
29
+ it "should provide a helper for nearby station ids" do
30
+ stat.nearby_station_ids.should be_a(Array)
31
+ stat.nearby_station_ids.first.should be_a(Integer)
32
+ end
33
+
34
+ context ".distance_to_nearby" do
35
+ it "should provide a helper to get the distance to a nearby station" do
36
+ stat.distance_to_nearby(
37
+ stat.nearby_station_ids.first
38
+ ).should be_a(Float)
39
+ end
40
+
41
+ it "should raise an error if the provided id is not nearby" do
42
+ lambda{
43
+ stat.distance_to_nearby(stat.nearby_station_ids.sum)
44
+ }.should raise_error
45
+ end
46
+ end
47
+
48
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Citibike do
4
+
5
+ it "should proxy methods to Citibike::Client" do
6
+ Citibike::Client.expects(:stations)
7
+ Citibike::Client.expects(:helmets)
8
+ Citibike::Client.expects(:branches)
9
+
10
+ Citibike.stations
11
+ Citibike.helmets
12
+ Citibike.branches
13
+ end
14
+
15
+ end