ns-api 0.0.2
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 +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
|