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