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.
Files changed (38) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +9 -0
  3. data/Gemfile.lock +40 -0
  4. data/README.md +82 -0
  5. data/Rakefile +1 -0
  6. data/lib/ns.rb +29 -0
  7. data/lib/ns/api/request/base.rb +58 -0
  8. data/lib/ns/api/request/disruption_collection.rb +30 -0
  9. data/lib/ns/api/request/travel_advice.rb +34 -0
  10. data/lib/ns/api/response/disruption_collection.rb +58 -0
  11. data/lib/ns/api/response/parser.rb +22 -0
  12. data/lib/ns/api/response/travel_advice.rb +46 -0
  13. data/lib/ns/configuration.rb +14 -0
  14. data/lib/ns/disruption.rb +26 -0
  15. data/lib/ns/disruption_collection.rb +45 -0
  16. data/lib/ns/model.rb +13 -0
  17. data/lib/ns/station.rb +8 -0
  18. data/lib/ns/travel_option.rb +28 -0
  19. data/lib/ns/trip.rb +80 -0
  20. data/lib/version.rb +3 -0
  21. data/ns.gemspec +24 -0
  22. data/spec/fixtures/ns_disruptions_no_results.xml +10 -0
  23. data/spec/fixtures/ns_disruptions_with_results.xml +23 -0
  24. data/spec/fixtures/ns_travel_advice_response.xml +2444 -0
  25. data/spec/ns/api/request/base_spec.rb +67 -0
  26. data/spec/ns/api/request/disruption_collection_spec.rb +34 -0
  27. data/spec/ns/api/request/travel_advice_spec.rb +53 -0
  28. data/spec/ns/api/response/disruption_collection_spec.rb +43 -0
  29. data/spec/ns/api/response/parser_spec.rb +17 -0
  30. data/spec/ns/api/response/travel_advice_spec.rb +61 -0
  31. data/spec/ns/disruption_collection_spec.rb +32 -0
  32. data/spec/ns/disruption_spec.rb +33 -0
  33. data/spec/ns/station_spec.rb +11 -0
  34. data/spec/ns/travel_option_spec.rb +41 -0
  35. data/spec/ns/trip_spec.rb +85 -0
  36. data/spec/ns_spec.rb +17 -0
  37. data/spec/spec_helper.rb +19 -0
  38. metadata +147 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ coverage
6
+ InstalledFiles
7
+ lib/bundler/man
8
+ pkg
9
+ rdoc
10
+ spec/reports
11
+ test/tmp
12
+ test/version_tmp
13
+ tmp
14
+
15
+ # YARD artifacts
16
+ .yardoc
17
+ _yardoc
18
+ doc/
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'rspec'
7
+ gem 'simplecov'
8
+ gem 'fakeweb'
9
+ end
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