ratis 2.5.2.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/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
|