ratis 2.5.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +157 -0
- data/lib/ratis.rb +49 -0
- data/lib/ratis/closest_stop.rb +43 -0
- data/lib/ratis/config.rb +21 -0
- data/lib/ratis/core_ext.rb +35 -0
- data/lib/ratis/errors.rb +70 -0
- data/lib/ratis/itinerary.rb +55 -0
- data/lib/ratis/landmark.rb +29 -0
- data/lib/ratis/landmark_category.rb +22 -0
- data/lib/ratis/location.rb +71 -0
- data/lib/ratis/next_bus.rb +93 -0
- data/lib/ratis/point_2_point.rb +112 -0
- data/lib/ratis/request.rb +39 -0
- data/lib/ratis/route.rb +31 -0
- data/lib/ratis/route_stops.rb +40 -0
- data/lib/ratis/schedule.rb +7 -0
- data/lib/ratis/schedule_group.rb +7 -0
- data/lib/ratis/schedule_nearby.rb +79 -0
- data/lib/ratis/schedule_trip.rb +7 -0
- data/lib/ratis/service.rb +7 -0
- data/lib/ratis/stop.rb +12 -0
- data/lib/ratis/timetable.rb +36 -0
- data/lib/ratis/version.rb +33 -0
- data/lib/ratis/walk.rb +37 -0
- metadata +217 -0
data/README.md
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# Ratis
|
2
|
+
A Ruby wrapper for Trapeze Group's ATIS SOAP server.
|
3
|
+
|
4
|
+
Goals:
|
5
|
+
|
6
|
+
- Wrap SOAP methods
|
7
|
+
- Provide an ActiveRecord like interface to queries
|
8
|
+
- Return object representations of SOAP responses
|
9
|
+
- Not encapulate any state (other than initial configuration)
|
10
|
+
- Try to catch erroneous queries before making a SOAP request
|
11
|
+
- Handle SOAP errors when they do occur
|
12
|
+
- Handle SOAP method versions changing
|
13
|
+
|
14
|
+
Presently based on:
|
15
|
+
|
16
|
+
**ATIS - SOAP Interface Specification Version 2.5.1, February 2012**
|
17
|
+
|
18
|
+
Currently Supports Ruby `1.8.7` and `1.9.3`
|
19
|
+
|
20
|
+
Gem installation
|
21
|
+
-------------------
|
22
|
+
1. Ensure that an SSH identity with permission for the *authoritylabs* organisation on github is available to Bundler.
|
23
|
+
1. Include the gem in your Gemfile thus:
|
24
|
+
|
25
|
+
gem 'ratis', :git => 'git@github.com:authoritylabs/ratis.git'
|
26
|
+
|
27
|
+
1. Add the following (Valley Metro specific) configuration block.
|
28
|
+
|
29
|
+
This must happen before Ratis is `require`d (before `Rails::Initializer.run` in a Rails app).
|
30
|
+
|
31
|
+
require 'ratis/config'
|
32
|
+
Ratis.configure do |config|
|
33
|
+
config.endpoint = 'http://soap.valleymetro.org/cgi-bin-soap-web-new/soap.cgi'
|
34
|
+
config.namespace = 'PX_WEB'
|
35
|
+
config.proxy = 'http://localhost:8080'
|
36
|
+
config.timeout = 5
|
37
|
+
end
|
38
|
+
|
39
|
+
If Ratis is `require`d prior to this config being set you will get a `RuntimeError` informing you so.
|
40
|
+
If the provided `endpoint` is invalid an `ArgumentError: Invalid URL: bad URI` will be thrown, but only when a request is made.
|
41
|
+
|
42
|
+
Gem usage
|
43
|
+
-------------------
|
44
|
+
|
45
|
+
### Classes
|
46
|
+
All the classes are prefixed with `Atis`, such as:
|
47
|
+
|
48
|
+
AtisError, AtisItinerary, AtisLandmark, AtisModel, AtisNextBus, AtisRoute,...
|
49
|
+
|
50
|
+
All `Atis` classes, with the exception of `AtisModel` and `AtisError` represent data structures returned by SOAP actions.
|
51
|
+
|
52
|
+
If you know the SOAP action you want to use, and wish to see if it is implemented in Ratis, you can open a console and ask `AtisModel` to tell you `who_implements_soap_action`:
|
53
|
+
|
54
|
+
> AtisModel.who_implements_soap_action 'Getlandmarks'
|
55
|
+
=> [AtisLandmark]
|
56
|
+
|
57
|
+
### Queries
|
58
|
+
By convention most provide either an `all` or `where` class method (following [Active Record's hash conditions syntax](http://guides.rubyonrails.org/active_record_querying.html#hash-conditions)), which will return an array of objects which wrap the response, e.g:
|
59
|
+
|
60
|
+
>> all_landmarks = AtisLandmark.where :type => :all
|
61
|
+
>> all_landmarks.count
|
62
|
+
=> 1510
|
63
|
+
>> all_landmarks.first
|
64
|
+
=> #<AtisLandmark:0x10d263190 @locality="N", @type="AIRPT", @location="4800 E. FALCON DR.", @verbose="FALCON FIELD AIRPORT">
|
65
|
+
|
66
|
+
### Errors
|
67
|
+
The `where` methods will try to sanity check your conditions before making a call to the SOAP server:
|
68
|
+
|
69
|
+
>> AtisLandmark.where({})
|
70
|
+
ArgumentError: You must provide a type
|
71
|
+
|
72
|
+
>> AtisLandmark.where :type => :all, :foo => 1
|
73
|
+
ArgumentError: Conditions not used by this class: [:foo]
|
74
|
+
|
75
|
+
When something goes wrong with the SOAP transaction an `AtisError` will be raised:
|
76
|
+
|
77
|
+
>> AtisNextBus.where :stop_id => 123456
|
78
|
+
#<AtisError: #10222--Unknown stop>
|
79
|
+
|
80
|
+
|
81
|
+
Development
|
82
|
+
-------------------
|
83
|
+
|
84
|
+
### Installation
|
85
|
+
1. Clone the repo
|
86
|
+
1. `bundle install`
|
87
|
+
|
88
|
+
### Usage
|
89
|
+
|
90
|
+
1. For development Ratis is hard coded to use a local proxy server on port 8080:
|
91
|
+
|
92
|
+
ssh -i ~/.ssh/authoritylabs.pem -L 8080:localhost:3128 ubuntu@codingsanctum.com
|
93
|
+
|
94
|
+
1. Run the test suite with `rake`
|
95
|
+
1. Test it out with `irb -r config/valley_metro.rb -I lib/ -r rubygems -r ratis`
|
96
|
+
|
97
|
+
### Extending
|
98
|
+
|
99
|
+
The `AtisLandmark` class is a simple one to look at for reference, and will be referred to below:
|
100
|
+
|
101
|
+
#### Testing
|
102
|
+
|
103
|
+
You can see the spec for it in `spec/ratis/atis_landmark_spec.rb`, it uses helper methods defined in `spec/spec_helper.rb`:
|
104
|
+
|
105
|
+
1. `stub_atis_request` tells `Webmock` to stub a `POST` to the ATIS SOAP server, any request which hasn't been explicitly allowed will trigger an exception.
|
106
|
+
|
107
|
+
1. `atis_response` returns a string of the form returned in the body of a response from the ATIS SOAP server. It takes the SOAP action and version the response appears to be for, and a method specific response code and body.
|
108
|
+
|
109
|
+
Because the method specific response bodies are quite long, it is convenient to wrap them in a heredoc, thus:
|
110
|
+
|
111
|
+
atis_response 'Getlandmarks', '1.4', '0', <<-BODY
|
112
|
+
<Landmarks>
|
113
|
+
[snip]
|
114
|
+
</Landmarks>
|
115
|
+
BODY
|
116
|
+
|
117
|
+
|
118
|
+
1. `an_atis_request` returns a `Webmock` `a_request` object for a `POST` to the ATIS SOAP, which can be used like this:
|
119
|
+
|
120
|
+
an_atis_request.should have_been_made.times 1
|
121
|
+
|
122
|
+
1. `an_atis_request_for` is an `a_request` object for a specific SOAP action with specific parameters passed, which can be used like this:
|
123
|
+
|
124
|
+
an_atis_request_for('Getlandmarks', 'Type' => 'ALL').should have_been_made
|
125
|
+
|
126
|
+
#### Implementing
|
127
|
+
|
128
|
+
To ease interaction with the ATIS SOAP server the `AtisModel` module is available. Any class which makes requests to the server should:
|
129
|
+
|
130
|
+
require 'ratis/atis_model'
|
131
|
+
extend AtisModel
|
132
|
+
|
133
|
+
You get the following:
|
134
|
+
|
135
|
+
1. `atis_request` should be used to make request to the ATIS SOAP server. It ensures the request is built in a way which the ATIS SOAP server expects, provides a version check against responses and returns a `Savon::Response`:
|
136
|
+
|
137
|
+
atis_request 'Getlandmarks', {'Type' => type}
|
138
|
+
|
139
|
+
The method and parameter names should be given as described by the ATIS SOAP Interface reference, with the first character uppercase and all others lowercase.
|
140
|
+
|
141
|
+
Now when a request for `Getlandmarks` is made the response's method version will be checked, and an `AtisError` will be thrown if it has not been declared. This ensures that a change on the SOAP server will not result in invalid response parsing by Ratis.
|
142
|
+
|
143
|
+
1. `all_conditions_used?` will raise an `ArgumentError` if the given hash is not empty.
|
144
|
+
|
145
|
+
Convention in Ratis is to provide a `self.where(conditions)` method (following [Active Record's hash conditions syntax](http://guides.rubyonrails.org/active_record_querying.html#hash-conditions)). As each key in `conditions` is used it can be `delete`d from `conditions`, then `all_conditions_used? conditions` can be called to ensure nothing unimplemented was passed to `where`.
|
146
|
+
|
147
|
+
It is also wise to raise an `ArgumentError` if an argument which is required isn't present in `conditions`.
|
148
|
+
|
149
|
+
Putting these steps together you get the following pattern:
|
150
|
+
|
151
|
+
type = conditions.delete(:type).to_s.upcase
|
152
|
+
raise ArgumentError.new('You must provide a type') if type.blank?
|
153
|
+
all_conditions_used? conditions
|
154
|
+
|
155
|
+
Following this pattern will provide a good deal of safety for someone using `where`, and eliminate potentially confusing SOAP errors.
|
156
|
+
|
157
|
+
1. `valid_latitude?` and `valid_longitude?` do range checks.
|
data/lib/ratis.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'savon'
|
2
|
+
|
3
|
+
require 'ratis/config'
|
4
|
+
require 'ratis/core_ext'
|
5
|
+
require 'ratis/closest_stop'
|
6
|
+
require 'ratis/errors'
|
7
|
+
require 'ratis/itinerary'
|
8
|
+
require 'ratis/landmark'
|
9
|
+
require 'ratis/landmark_category'
|
10
|
+
require 'ratis/location'
|
11
|
+
require 'ratis/next_bus'
|
12
|
+
require 'ratis/point_2_point'
|
13
|
+
require 'ratis/request'
|
14
|
+
require 'ratis/route'
|
15
|
+
require 'ratis/route_stops'
|
16
|
+
require 'ratis/schedule'
|
17
|
+
require 'ratis/schedule_group'
|
18
|
+
require 'ratis/schedule_nearby'
|
19
|
+
require 'ratis/schedule_trip'
|
20
|
+
require 'ratis/service'
|
21
|
+
require 'ratis/stop'
|
22
|
+
require 'ratis/timetable'
|
23
|
+
require 'ratis/walk'
|
24
|
+
|
25
|
+
module Ratis
|
26
|
+
|
27
|
+
extend self
|
28
|
+
|
29
|
+
def configure
|
30
|
+
yield config
|
31
|
+
end
|
32
|
+
|
33
|
+
def config
|
34
|
+
@config ||= Config.new
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid_latitude?(lat)
|
38
|
+
-90.0 <= lat.to_f and lat.to_f <= 90.0
|
39
|
+
end
|
40
|
+
|
41
|
+
def valid_longitude?(lon)
|
42
|
+
-180.0 <= lon.to_f and lon.to_f <= 180.0
|
43
|
+
end
|
44
|
+
|
45
|
+
def all_conditions_used?(conditions)
|
46
|
+
raise ArgumentError.new("Conditions not used by this class: #{conditions.keys.inspect}") unless conditions.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class ClosestStop
|
4
|
+
|
5
|
+
def self.where(conditions)
|
6
|
+
latitude = conditions.delete :latitude
|
7
|
+
longitude = conditions.delete :longitude
|
8
|
+
location_text = conditions.delete :location_text
|
9
|
+
num_stops = conditions.delete :num_stops
|
10
|
+
|
11
|
+
raise ArgumentError.new('You must provide a longitude') unless longitude
|
12
|
+
raise ArgumentError.new('You must provide a latitude') unless latitude
|
13
|
+
|
14
|
+
Ratis.all_conditions_used? conditions
|
15
|
+
|
16
|
+
response = Request.get 'Closeststop',
|
17
|
+
{'Locationlat' => latitude, 'Locationlong' => longitude, 'Locationtext' => location_text, 'Numstops' => num_stops}
|
18
|
+
|
19
|
+
return [] unless response.success?
|
20
|
+
|
21
|
+
stops = response.to_hash[:closeststop_response][:stops][:stop].map do |s|
|
22
|
+
next if s[:description].blank?
|
23
|
+
|
24
|
+
stop = Stop.new
|
25
|
+
stop.walk_dist = s[:walkdist]
|
26
|
+
stop.description = s[:description]
|
27
|
+
stop.stop_id = s[:stopid]
|
28
|
+
stop.atis_stop_id = s[:atisstopid]
|
29
|
+
stop.latitude = s[:lat]
|
30
|
+
stop.longitude = s[:long]
|
31
|
+
stop.walk_dir = s[:walkdir]
|
32
|
+
stop.side = s[:side]
|
33
|
+
stop.heading = s[:heading]
|
34
|
+
stop.stop_position = s[:stopposition]
|
35
|
+
stop.route_dir = s[:routedirs][:routedir]
|
36
|
+
stop
|
37
|
+
end
|
38
|
+
stops.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
data/lib/ratis/config.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class Config
|
4
|
+
|
5
|
+
attr_accessor :endpoint, :namespace, :proxy, :timeout
|
6
|
+
|
7
|
+
def valid?
|
8
|
+
return false if endpoint.nil? or namespace.nil?
|
9
|
+
return false if endpoint.empty? or namespace.empty?
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@timeout = 5
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
# Traverses the Hash for a given +path+ of Hash keys and returns
|
4
|
+
# the value as an Array. Defaults to return an empty Array in case the path does not
|
5
|
+
# exist or returns nil.
|
6
|
+
# Copied from Savon::SOAP::Response#to_array
|
7
|
+
def to_array(*path)
|
8
|
+
result = path.inject(self) do |memo, key|
|
9
|
+
return [] unless memo[key]
|
10
|
+
memo[key]
|
11
|
+
end
|
12
|
+
|
13
|
+
[result].compact.flatten(1)
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
class TrueClass
|
19
|
+
def y_or_n
|
20
|
+
'y'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class FalseClass
|
25
|
+
def y_or_n
|
26
|
+
'n'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class String
|
31
|
+
def y_or_n
|
32
|
+
raise ArgumentError.new 'Expecting y or n' unless ['y', 'n'].include? self.downcase
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
data/lib/ratis/errors.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class Error < StandardError
|
4
|
+
|
5
|
+
attr_accessor :fault_code, :fault_string
|
6
|
+
|
7
|
+
def initialize(savon_soap_fault = nil)
|
8
|
+
return if savon_soap_fault.nil? or savon_soap_fault.blank?
|
9
|
+
fault = savon_soap_fault.to_hash[:fault]
|
10
|
+
code = fault[:faultcode].scan(/\d+/).first
|
11
|
+
self.fault_code = code.to_i if code
|
12
|
+
self.fault_string = fault[:faultstring]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.version_mismatch(method, version)
|
16
|
+
error = Errors.new
|
17
|
+
error.fault_string = "Unimplemented SOAP method #{ method } #{ version }"
|
18
|
+
error
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_s
|
22
|
+
fault_string
|
23
|
+
end
|
24
|
+
|
25
|
+
def verbose_fault_string
|
26
|
+
case fault_string
|
27
|
+
when /10222|invalid Stopid/i
|
28
|
+
'Invalid STOP ID number. Please enter a valid five digit stop ID number'
|
29
|
+
when /20003|20046/
|
30
|
+
'No stops were found within the walking distance of the origin you specified'
|
31
|
+
when /20004|20047/
|
32
|
+
'No stops were found within the walking distance of the destination you specified'
|
33
|
+
when /20048/
|
34
|
+
'No stops were found within the walking distance of the destination or origin you specified'
|
35
|
+
when /20005/
|
36
|
+
'There is no service at this stop on the date and time specified'
|
37
|
+
when /20006/
|
38
|
+
'No services run at the date or time specified for your destination'
|
39
|
+
when /20007/
|
40
|
+
'No trips were found matching the criteria you specified'
|
41
|
+
when /20008/
|
42
|
+
'No services run at the date or time specified'
|
43
|
+
when /11085/
|
44
|
+
'Origin is within trivial distance of the destination'
|
45
|
+
when /15034/
|
46
|
+
'No runs available for the stop and times provided'
|
47
|
+
when /1007|no runs available/i
|
48
|
+
'There is no service at this stop on the date and time specified'
|
49
|
+
when /15035/
|
50
|
+
'The route you specified does not serve this stop at the date and time specified'
|
51
|
+
when /invalid Window|15030/i
|
52
|
+
'The minimum time range is one hour. Please adjust the start and/or end time and try again.'
|
53
|
+
when /invalid Location|20024/i
|
54
|
+
'Either the origin or destination could not be recognized by the server'
|
55
|
+
when /out of range/i
|
56
|
+
'The date you entered was out of range - please choose a valid date'
|
57
|
+
else
|
58
|
+
'The server could not handle your request at this time. Please try again later'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module Errors
|
64
|
+
|
65
|
+
class ConfigError < Error; end
|
66
|
+
class SoapError < Error; end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class Itinerary
|
4
|
+
|
5
|
+
attr_accessor :co2_auto, :co2_transit
|
6
|
+
attr_accessor :final_walk_dir, :legs
|
7
|
+
attr_accessor :reduced_fare, :regular_fare
|
8
|
+
attr_accessor :transit_time
|
9
|
+
|
10
|
+
def self.where(conditions)
|
11
|
+
date = conditions.delete :date
|
12
|
+
time = conditions.delete :time
|
13
|
+
minimize = conditions.delete(:minimize).to_s.upcase
|
14
|
+
|
15
|
+
origin_lat = conditions.delete(:origin_lat).to_f
|
16
|
+
origin_long = conditions.delete(:origin_long).to_f
|
17
|
+
destination_lat = conditions.delete(:destination_lat).to_f
|
18
|
+
destination_long = conditions.delete(:destination_long).to_f
|
19
|
+
|
20
|
+
raise ArgumentError.new('You must provide a date DD/MM/YYYY') unless DateTime.strptime(date, '%d/%m/%Y') rescue false
|
21
|
+
raise ArgumentError.new('You must provide a time as 24-hour HHMM') unless DateTime.strptime(time, '%H%M') rescue false
|
22
|
+
raise ArgumentError.new('You must provide a conditions of T|X|W to minimize') unless ['T', 'X', 'W'].include? minimize
|
23
|
+
|
24
|
+
raise ArgumentError.new('You must provide an origin latitude') unless Ratis.valid_latitude? origin_lat
|
25
|
+
raise ArgumentError.new('You must provide an origin longitude') unless Ratis.valid_longitude? origin_long
|
26
|
+
raise ArgumentError.new('You must provide an destination latitude') unless Ratis.valid_latitude? destination_lat
|
27
|
+
raise ArgumentError.new('You must provide an destination longitude') unless Ratis.valid_longitude? destination_long
|
28
|
+
|
29
|
+
Ratis.all_conditions_used? conditions
|
30
|
+
|
31
|
+
response = Request.get 'Plantrip',
|
32
|
+
'Date' => date, 'Time' => time, 'Minimize' => minimize,
|
33
|
+
'Originlat' => origin_lat, 'Originlong' => origin_long,
|
34
|
+
'Destinationlat' => destination_lat, 'Destinationlong' => destination_long
|
35
|
+
|
36
|
+
return [] unless response.success?
|
37
|
+
|
38
|
+
response.to_array(:plantrip_response, :itin).map do |itinerary|
|
39
|
+
atis_itinerary = Itinerary.new
|
40
|
+
atis_itinerary.co2_auto = itinerary[:co2auto].to_f
|
41
|
+
atis_itinerary.co2_transit = itinerary[:co2transit].to_f
|
42
|
+
atis_itinerary.final_walk_dir = itinerary[:finalwalkdir]
|
43
|
+
atis_itinerary.reduced_fare = itinerary[:reducedfare].to_f
|
44
|
+
atis_itinerary.regular_fare = itinerary[:regularfare].to_f
|
45
|
+
atis_itinerary.transit_time = itinerary[:transittime].to_i
|
46
|
+
atis_itinerary.legs = itinerary.to_array :legs, :leg
|
47
|
+
|
48
|
+
atis_itinerary
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ratis
|
2
|
+
|
3
|
+
class Landmark
|
4
|
+
|
5
|
+
attr_accessor :type, :verbose, :location, :locality
|
6
|
+
|
7
|
+
def self.where(conditions)
|
8
|
+
|
9
|
+
type = conditions.delete(:type).to_s.upcase
|
10
|
+
raise ArgumentError.new('You must provide a type') if type.blank?
|
11
|
+
Ratis.all_conditions_used? conditions
|
12
|
+
|
13
|
+
response = Request.get 'Getlandmarks', {'Type' => type}
|
14
|
+
return [] unless response.success?
|
15
|
+
|
16
|
+
response.to_array(:getlandmarks_response, :landmarks, :landmark).map do |landmark|
|
17
|
+
atis_landmark = Landmark.new
|
18
|
+
atis_landmark.type = landmark[:type]
|
19
|
+
atis_landmark.verbose = landmark[:verbose]
|
20
|
+
atis_landmark.location = landmark[:location]
|
21
|
+
atis_landmark.locality = landmark[:locality]
|
22
|
+
atis_landmark
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|