citibike 0.0.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.
- data/.gitignore +19 -0
- data/.rspec +4 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +59 -0
- data/LICENSE +20 -0
- data/LICENSE.txt +22 -0
- data/README.md +121 -0
- data/Rakefile +6 -0
- data/citibike.gemspec +31 -0
- data/lib/citibike.rb +20 -0
- data/lib/citibike/api.rb +82 -0
- data/lib/citibike/apis/branch.rb +11 -0
- data/lib/citibike/apis/helmet.rb +11 -0
- data/lib/citibike/apis/station.rb +83 -0
- data/lib/citibike/client.rb +104 -0
- data/lib/citibike/connection.rb +85 -0
- data/lib/citibike/response.rb +119 -0
- data/lib/citibike/responses/branch.rb +31 -0
- data/lib/citibike/responses/helmet.rb +18 -0
- data/lib/citibike/responses/station.rb +18 -0
- data/lib/citibike/version.rb +5 -0
- data/spec/lib/apis/station_spec.rb +48 -0
- data/spec/lib/citibike_spec.rb +15 -0
- data/spec/lib/client_spec.rb +106 -0
- data/spec/lib/connection_spec.rb +68 -0
- data/spec/lib/response_spec.rb +90 -0
- data/spec/spec_helper.rb +20 -0
- metadata +232 -0
@@ -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,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
|