ns-api 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +40 -0
- data/README.md +82 -0
- data/Rakefile +1 -0
- data/lib/ns.rb +29 -0
- data/lib/ns/api/request/base.rb +58 -0
- data/lib/ns/api/request/disruption_collection.rb +30 -0
- data/lib/ns/api/request/travel_advice.rb +34 -0
- data/lib/ns/api/response/disruption_collection.rb +58 -0
- data/lib/ns/api/response/parser.rb +22 -0
- data/lib/ns/api/response/travel_advice.rb +46 -0
- data/lib/ns/configuration.rb +14 -0
- data/lib/ns/disruption.rb +26 -0
- data/lib/ns/disruption_collection.rb +45 -0
- data/lib/ns/model.rb +13 -0
- data/lib/ns/station.rb +8 -0
- data/lib/ns/travel_option.rb +28 -0
- data/lib/ns/trip.rb +80 -0
- data/lib/version.rb +3 -0
- data/ns.gemspec +24 -0
- data/spec/fixtures/ns_disruptions_no_results.xml +10 -0
- data/spec/fixtures/ns_disruptions_with_results.xml +23 -0
- data/spec/fixtures/ns_travel_advice_response.xml +2444 -0
- data/spec/ns/api/request/base_spec.rb +67 -0
- data/spec/ns/api/request/disruption_collection_spec.rb +34 -0
- data/spec/ns/api/request/travel_advice_spec.rb +53 -0
- data/spec/ns/api/response/disruption_collection_spec.rb +43 -0
- data/spec/ns/api/response/parser_spec.rb +17 -0
- data/spec/ns/api/response/travel_advice_spec.rb +61 -0
- data/spec/ns/disruption_collection_spec.rb +32 -0
- data/spec/ns/disruption_spec.rb +33 -0
- data/spec/ns/station_spec.rb +11 -0
- data/spec/ns/travel_option_spec.rb +41 -0
- data/spec/ns/trip_spec.rb +85 -0
- data/spec/ns_spec.rb +17 -0
- data/spec/spec_helper.rb +19 -0
- metadata +147 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
ns (0.0.2)
|
5
|
+
httpi
|
6
|
+
nokogiri
|
7
|
+
nori
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: http://rubygems.org/
|
11
|
+
specs:
|
12
|
+
diff-lcs (1.1.3)
|
13
|
+
fakeweb (1.3.0)
|
14
|
+
httpi (2.0.2)
|
15
|
+
rack
|
16
|
+
multi_json (1.6.0)
|
17
|
+
nokogiri (1.5.6)
|
18
|
+
nori (2.0.3)
|
19
|
+
rack (1.5.2)
|
20
|
+
rspec (2.12.0)
|
21
|
+
rspec-core (~> 2.12.0)
|
22
|
+
rspec-expectations (~> 2.12.0)
|
23
|
+
rspec-mocks (~> 2.12.0)
|
24
|
+
rspec-core (2.12.2)
|
25
|
+
rspec-expectations (2.12.1)
|
26
|
+
diff-lcs (~> 1.1.3)
|
27
|
+
rspec-mocks (2.12.2)
|
28
|
+
simplecov (0.7.1)
|
29
|
+
multi_json (~> 1.0)
|
30
|
+
simplecov-html (~> 0.7.1)
|
31
|
+
simplecov-html (0.7.1)
|
32
|
+
|
33
|
+
PLATFORMS
|
34
|
+
ruby
|
35
|
+
|
36
|
+
DEPENDENCIES
|
37
|
+
fakeweb
|
38
|
+
ns!
|
39
|
+
rspec
|
40
|
+
simplecov
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
Ruby implementation of the NS API
|
2
|
+
=================================
|
3
|
+
|
4
|
+
Ruby implementation of the NS (Dutch Railways) API
|
5
|
+
|
6
|
+
## Request an API username and password from NS
|
7
|
+
|
8
|
+
Before using this gem you should request a username and password from the
|
9
|
+
NS API pages. This allows you to authenticate your API requests and will
|
10
|
+
make sure you get actual data back.
|
11
|
+
|
12
|
+
Head over to http://www.ns.nl/api/api to request your credentials.
|
13
|
+
|
14
|
+
## Configuring the gem
|
15
|
+
|
16
|
+
To configure the gem, place an <tt>Ns.configure</tt> block in your code:
|
17
|
+
|
18
|
+
```
|
19
|
+
Ns.configure do |config|
|
20
|
+
config.username = 'john@doe.com'
|
21
|
+
config.password = 'your-secret-password'
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
## Using the gem
|
26
|
+
|
27
|
+
Currently the gem implements the following API calls:
|
28
|
+
|
29
|
+
- Requesting a travel advice from one station to another
|
30
|
+
|
31
|
+
Each of these calls is explained below.
|
32
|
+
|
33
|
+
### Requesting a travel advice
|
34
|
+
|
35
|
+
To request a travel advice you need at least the names of the departure and
|
36
|
+
the arrival station. You may use the full string name of the station, e.g.
|
37
|
+
"Utrecht Centraal" or the abbreviated station code, e.g. "ut".
|
38
|
+
|
39
|
+
Requesting travel advice is done by creating a new <tt>Ns::Trip</tt>. A
|
40
|
+
trip has a <tt>travel_options</tt> method that returns a collection of
|
41
|
+
<tt>Ns::TravelOption</tt> objects. See the code for the methods these objects
|
42
|
+
expose.
|
43
|
+
|
44
|
+
```
|
45
|
+
trip = Ns::Trip.new(from: 'Amsterdam Centraal', to: 'Ede Centrum')
|
46
|
+
trip.travel_options # <= returns Ns::TravelOption objects
|
47
|
+
```
|
48
|
+
|
49
|
+
Optionally you may supply a desired departure or arrival time to the trip:
|
50
|
+
|
51
|
+
```
|
52
|
+
trip = Ns::Trip.new(from: 'Amsterdam Centraal', to: 'Ede Centrum', arrival: Time.now)
|
53
|
+
trip_2 = Ns::Trip.new(from: 'Amsterdam Centraal', to: 'Ede Centrum', departure: Time.now)
|
54
|
+
```
|
55
|
+
|
56
|
+
You may also specify a <tt>via</tt> station:
|
57
|
+
|
58
|
+
```
|
59
|
+
trip = Ns::Trip.new(from: 'Amsterdam Centraal', to: 'Ede Centrum', via: 'Utrecht Centraal', arrival: Time.now)
|
60
|
+
```
|
61
|
+
|
62
|
+
The returned <tt>Ns::TravelOption</tt> objects have a <tt>optimal</tt> attribute that tells you wether or not the
|
63
|
+
travel option is regarded as the optimal option for your trip by the NS.
|
64
|
+
|
65
|
+
**Note**: the times returned by an instance of <tt>Ns::TravelOption</tt> (e.g.
|
66
|
+
trip durations, delays) are all in seconds.
|
67
|
+
|
68
|
+
### Requesting a list of interruptions
|
69
|
+
|
70
|
+
Requesting disruptions requires a station. Disruptions that affect the given station will be returned:
|
71
|
+
|
72
|
+
```
|
73
|
+
disruption_collection = Ns::DisruptionCollection.new(station: 'Amsterdam Centraal')
|
74
|
+
```
|
75
|
+
|
76
|
+
An instance of <tt>Ns::DisruptionCollection</tt> has a <tt>planned_disruptions</tt> method and a <tt>unplanned_disruptions</tt>
|
77
|
+
method. The results of these methods are <tt>Ns::Disruption</tt> objects.
|
78
|
+
|
79
|
+
## Development
|
80
|
+
|
81
|
+
Pull requests are welcome! To add your feature: create a fork, implement the
|
82
|
+
feature on a topic branch, add specs and create a pull request here on Github.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/ns.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'httpi'
|
3
|
+
require 'nori'
|
4
|
+
|
5
|
+
this = Pathname.new(__FILE__).realpath
|
6
|
+
lib_path = File.expand_path("..", this)
|
7
|
+
$:.unshift(lib_path)
|
8
|
+
|
9
|
+
$ROOT = File.expand_path("../", lib_path)
|
10
|
+
|
11
|
+
require 'ns/model'
|
12
|
+
|
13
|
+
Dir.glob(File.join(lib_path, '/**/*.rb')).each do |file|
|
14
|
+
require file
|
15
|
+
end
|
16
|
+
|
17
|
+
module Ns
|
18
|
+
class << self
|
19
|
+
|
20
|
+
def configure
|
21
|
+
yield configuration
|
22
|
+
end
|
23
|
+
|
24
|
+
def configuration
|
25
|
+
@configuration ||= Ns::Configuration.new
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Ns
|
2
|
+
module Api
|
3
|
+
module Request
|
4
|
+
class Base
|
5
|
+
|
6
|
+
attr_reader :response, :parsed_response
|
7
|
+
|
8
|
+
def initialize(attributes = {})
|
9
|
+
HTTPI.log = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def response
|
13
|
+
self.class.response_class.new(parsed_response: parsed_response)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def url_for_request
|
19
|
+
http_request_with_query_and_authentication
|
20
|
+
end
|
21
|
+
|
22
|
+
def http_request_with_query_and_authentication
|
23
|
+
http_request.query = query
|
24
|
+
http_request.auth.basic(Ns.configuration.username, Ns.configuration.password)
|
25
|
+
|
26
|
+
http_request
|
27
|
+
end
|
28
|
+
|
29
|
+
def http_request
|
30
|
+
@http_request ||= HTTPI::Request.new(self.class.base_uri)
|
31
|
+
end
|
32
|
+
|
33
|
+
def parsed_response
|
34
|
+
@parsed_response ||= response_parser.parsed_response
|
35
|
+
end
|
36
|
+
|
37
|
+
def response_parser
|
38
|
+
@response_parser ||= Ns::Api::Response::Parser.new(response_text: response_body)
|
39
|
+
end
|
40
|
+
|
41
|
+
def response_body
|
42
|
+
@response_body ||= plain_text_response.body
|
43
|
+
#@response_body ||= File.read(File.join($ROOT, 'spec/fixtures/ns_travel_advice_response.xml'))
|
44
|
+
#@response_body ||= File.read(File.join($ROOT, 'spec/fixtures/ns_disruptions_with_results.xml'))
|
45
|
+
end
|
46
|
+
|
47
|
+
def plain_text_response
|
48
|
+
@plain_text_response ||= HTTPI.get(url_for_request)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.base_uri; raise "Implement me in a subclass"; end
|
52
|
+
def self.response_class; raise "Implement me in a subclass"; end
|
53
|
+
def query; raise "Implement me in a subclass"; end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ns
|
2
|
+
module Api
|
3
|
+
module Request
|
4
|
+
class DisruptionCollection < Ns::Api::Request::Base
|
5
|
+
include Ns::Model
|
6
|
+
|
7
|
+
attr_accessor :disruption_collection
|
8
|
+
|
9
|
+
def self.base_uri
|
10
|
+
'http://webservices.ns.nl/ns-api-storingen'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.response_class
|
14
|
+
Ns::Api::Response::DisruptionCollection
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def query
|
20
|
+
{
|
21
|
+
station: disruption_collection.station,
|
22
|
+
actual: disruption_collection.actual,
|
23
|
+
unplanned: disruption_collection.include_planned
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Ns
|
2
|
+
module Api
|
3
|
+
module Request
|
4
|
+
class TravelAdvice < Ns::Api::Request::Base
|
5
|
+
include Ns::Model
|
6
|
+
|
7
|
+
attr_accessor :trip
|
8
|
+
|
9
|
+
def self.base_uri
|
10
|
+
'http://webservices.ns.nl/ns-api-treinplanner'
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.response_class
|
14
|
+
Ns::Api::Response::TravelAdvice
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def query
|
20
|
+
{
|
21
|
+
fromStation: trip.from.name,
|
22
|
+
toStation: trip.to.name,
|
23
|
+
viaStation: trip.via.name,
|
24
|
+
dateTime: trip.formatted_time,
|
25
|
+
departure: trip.departure?,
|
26
|
+
hslAllowed: trip.allow_hsl,
|
27
|
+
yearCard: trip.year_card
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Ns
|
2
|
+
module Api
|
3
|
+
module Response
|
4
|
+
class DisruptionCollection
|
5
|
+
include Ns::Model
|
6
|
+
|
7
|
+
attr_accessor :parsed_response
|
8
|
+
|
9
|
+
def planned_disruptions
|
10
|
+
( raw_disruptions['Gepland'] || [] ).map do |raw_disruption|
|
11
|
+
new_disruption(raw_disruption)
|
12
|
+
end.flatten.compact
|
13
|
+
end
|
14
|
+
|
15
|
+
def unplanned_disruptions
|
16
|
+
( raw_disruptions['Ongepland'] || [] ).map do |raw_disruption|
|
17
|
+
new_disruption(raw_disruption)
|
18
|
+
end.flatten.compact
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
#
|
24
|
+
# Each disruption looks like this:
|
25
|
+
#
|
26
|
+
# [ 'Storing', { id: '12345', 'Traject': ... } ]
|
27
|
+
#
|
28
|
+
# which is why we do a check for size, shift the array, and return
|
29
|
+
# the first element in the array which is the actual disruption
|
30
|
+
# Hash
|
31
|
+
#
|
32
|
+
def new_disruption(raw_disruption)
|
33
|
+
if raw_disruption.size >= 2
|
34
|
+
raw_disruption = raw_disruption[1]
|
35
|
+
raw_disruption = raw_disruption[0] if raw_disruption.is_a?(Array)
|
36
|
+
|
37
|
+
Ns::Disruption.new(
|
38
|
+
route: raw_disruption['Traject'],
|
39
|
+
reason: raw_disruption['Reason'],
|
40
|
+
message: raw_disruption['Bericht'],
|
41
|
+
advice: raw_disruption['Advies'],
|
42
|
+
period: raw_disruption['Periode']
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def raw_disruptions
|
48
|
+
if parsed_response.has_key?('Storingen') && ( parsed_response['Storingen'].has_key?('Gepland') || parsed_response['Storingen'].has_key?('Ongepland') )
|
49
|
+
parsed_response['Storingen']
|
50
|
+
else
|
51
|
+
{}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Ns
|
2
|
+
module Api
|
3
|
+
module Response
|
4
|
+
class Parser
|
5
|
+
include Ns::Model
|
6
|
+
|
7
|
+
attr_accessor :response_text
|
8
|
+
|
9
|
+
def parsed_response
|
10
|
+
@parsed_response ||= parser.parse(response_text)
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def parser
|
16
|
+
@parser ||= Nori.new
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Ns
|
2
|
+
module Api
|
3
|
+
module Response
|
4
|
+
class TravelAdvice
|
5
|
+
include Ns::Model
|
6
|
+
|
7
|
+
attr_accessor :parsed_response
|
8
|
+
|
9
|
+
def travel_options
|
10
|
+
raw_travel_options.map do |travel_option|
|
11
|
+
Ns::TravelOption.new(
|
12
|
+
planned_departure: travel_option['GeplandeVertrekTijd'],
|
13
|
+
actual_departure: travel_option['ActueleVertrekTijd'],
|
14
|
+
planned_arrival: travel_option['GeplandeAankomstTijd'],
|
15
|
+
actual_arrival: travel_option['ActueleAankomstTijd'],
|
16
|
+
changes: travel_option['AantalOverstappen'],
|
17
|
+
platform: platform(travel_option),
|
18
|
+
optimal: travel_option['Optimaal']
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def raw_travel_options
|
26
|
+
if parsed_response.has_key?('ReisMogelijkheden') && parsed_response['ReisMogelijkheden'].has_key?('ReisMogelijkheid')
|
27
|
+
parsed_response['ReisMogelijkheden']['ReisMogelijkheid']
|
28
|
+
else
|
29
|
+
[]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def platform(travel_option)
|
34
|
+
travel_part = travel_option['ReisDeel']
|
35
|
+
|
36
|
+
if travel_part.is_a?(Array)
|
37
|
+
travel_part.first['ReisStop'].first['Spoor']
|
38
|
+
else
|
39
|
+
travel_part['ReisStop'].first['Spoor']
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|