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 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
@@ -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
@@ -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