wash-out 0.10.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.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +35 -0
  5. data/Appraisals +25 -0
  6. data/CHANGELOG.md +102 -0
  7. data/Gemfile +16 -0
  8. data/Guardfile +12 -0
  9. data/LICENSE +22 -0
  10. data/README.md +246 -0
  11. data/Rakefile +13 -0
  12. data/app/helpers/wash_out_helper.rb +106 -0
  13. data/app/views/wash_out/document/error.builder +9 -0
  14. data/app/views/wash_out/document/response.builder +10 -0
  15. data/app/views/wash_out/document/wsdl.builder +68 -0
  16. data/app/views/wash_out/rpc/error.builder +10 -0
  17. data/app/views/wash_out/rpc/response.builder +11 -0
  18. data/app/views/wash_out/rpc/wsdl.builder +68 -0
  19. data/gemfiles/rails_3.1.3.gemfile +20 -0
  20. data/gemfiles/rails_3.2.12.gemfile +20 -0
  21. data/gemfiles/rails_4.0.0.gemfile +19 -0
  22. data/gemfiles/rails_4.1.0.gemfile +19 -0
  23. data/gemfiles/rails_4.2.0.gemfile +19 -0
  24. data/gemfiles/rails_5.0.0.beta2.gemfile +19 -0
  25. data/init.rb +1 -0
  26. data/lib/wash_out.rb +53 -0
  27. data/lib/wash_out/configurable.rb +41 -0
  28. data/lib/wash_out/dispatcher.rb +218 -0
  29. data/lib/wash_out/engine.rb +12 -0
  30. data/lib/wash_out/middleware.rb +41 -0
  31. data/lib/wash_out/model.rb +29 -0
  32. data/lib/wash_out/param.rb +200 -0
  33. data/lib/wash_out/router.rb +95 -0
  34. data/lib/wash_out/soap.rb +48 -0
  35. data/lib/wash_out/soap_config.rb +93 -0
  36. data/lib/wash_out/type.rb +29 -0
  37. data/lib/wash_out/version.rb +3 -0
  38. data/lib/wash_out/wsse.rb +101 -0
  39. data/spec/dummy/Rakefile +7 -0
  40. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  41. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  42. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  43. data/spec/dummy/config.ru +4 -0
  44. data/spec/dummy/config/application.rb +51 -0
  45. data/spec/dummy/config/boot.rb +10 -0
  46. data/spec/dummy/config/environment.rb +5 -0
  47. data/spec/dummy/config/environments/development.rb +23 -0
  48. data/spec/dummy/config/environments/test.rb +30 -0
  49. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  50. data/spec/dummy/config/initializers/inflections.rb +10 -0
  51. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  52. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  53. data/spec/dummy/config/initializers/session_store.rb +8 -0
  54. data/spec/dummy/config/locales/en.yml +5 -0
  55. data/spec/dummy/config/routes.rb +58 -0
  56. data/spec/dummy/public/404.html +26 -0
  57. data/spec/dummy/public/422.html +26 -0
  58. data/spec/dummy/public/500.html +26 -0
  59. data/spec/dummy/public/favicon.ico +0 -0
  60. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  61. data/spec/dummy/script/rails +6 -0
  62. data/spec/lib/wash_out/dispatcher_spec.rb +99 -0
  63. data/spec/lib/wash_out/middleware_spec.rb +33 -0
  64. data/spec/lib/wash_out/param_spec.rb +94 -0
  65. data/spec/lib/wash_out/router_spec.rb +22 -0
  66. data/spec/lib/wash_out/type_spec.rb +41 -0
  67. data/spec/lib/wash_out_spec.rb +754 -0
  68. data/spec/spec_helper.rb +82 -0
  69. data/wash_out.gemspec +21 -0
  70. metadata +128 -0
@@ -0,0 +1,41 @@
1
+ module WashOut
2
+ module Configurable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ cattr_reader :soap_config
7
+ class_variable_set :@@soap_config, WashOut::SoapConfig.new({})
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def soap_config=(obj)
13
+
14
+ unless obj.is_a?(Hash)
15
+ raise "Value needs to be a Hash."
16
+ end
17
+
18
+ if class_variable_defined?(:@@soap_config)
19
+ class_variable_get(:@@soap_config).configure obj
20
+ else
21
+ class_variable_set :@@soap_config, WashOut::SoapConfig.new(obj)
22
+ end
23
+ end
24
+ end
25
+
26
+ def soap_config=(obj)
27
+
28
+ unless obj.is_a?(Hash)
29
+ raise "Value needs to be a Hash."
30
+ end
31
+
32
+ class_eval do
33
+ if class_variable_defined?(:@@soap_config)
34
+ class_variable_get(:@@soap_config).configure obj
35
+ else
36
+ class_variable_set :@@soap_config, WashOut::SoapConfig.new(obj)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,218 @@
1
+ module WashOut
2
+ # The WashOut::Dispatcher module should be included in a controller acting
3
+ # as a SOAP endpoint. It includes actions for generating WSDL and handling
4
+ # SOAP requests.
5
+ module Dispatcher
6
+ # A SOAPError exception can be raised to return a correct SOAP error
7
+ # response.
8
+ class SOAPError < Exception
9
+ attr_accessor :code
10
+ def initialize(message, code=nil)
11
+ super(message)
12
+ @code = code
13
+ end
14
+ end
15
+
16
+ class ProgrammerError < Exception; end
17
+
18
+ def _authenticate_wsse
19
+
20
+ begin
21
+ xml_security = request.env['wash_out.soap_data'].values_at(:envelope, :Envelope).compact.first
22
+ xml_security = xml_security.values_at(:header, :Header).compact.first
23
+ xml_security = xml_security.values_at(:security, :Security).compact.first
24
+ username_token = xml_security.values_at(:username_token, :UsernameToken).compact.first
25
+ rescue
26
+ username_token = nil
27
+ end
28
+
29
+ WashOut::Wsse.authenticate soap_config, username_token
30
+
31
+ request.env['WSSE_TOKEN'] = username_token.with_indifferent_access unless username_token.blank?
32
+ end
33
+
34
+ def _map_soap_parameters
35
+ @_params = _load_params action_spec[:in],
36
+ _strip_empty_nodes(action_spec[:in], xml_data)
37
+ end
38
+
39
+ def _strip_empty_nodes(params, hash)
40
+ hash.keys.each do |key|
41
+ param = params.detect { |a| a.raw_name.to_s == key.to_s }
42
+ next if !(param && hash[key].is_a?(Hash))
43
+
44
+ value = hash[key].delete_if do |k, _|
45
+ k.to_s[0] == '@' && !param.map.detect { |a| a.raw_name.to_s == k.to_s }
46
+ end
47
+
48
+ if value.length > 0
49
+ hash[key] = _strip_empty_nodes param.map, value
50
+ else
51
+ hash[key] = nil
52
+ end
53
+ end
54
+
55
+ hash
56
+ end
57
+
58
+ # Creates the final parameter hash based on the request spec and xml_data from the request
59
+ def _load_params(spec, xml_data)
60
+ params = HashWithIndifferentAccess.new
61
+ spec.each do |param|
62
+ key = param.raw_name.to_sym
63
+ if xml_data.has_key? key
64
+ params[param.raw_name] = param.load(xml_data, key)
65
+ end
66
+ end
67
+ params
68
+ end
69
+
70
+ # This action generates the WSDL for defined SOAP methods.
71
+ def _generate_wsdl
72
+
73
+ @map = self.class.soap_actions
74
+ @namespace = soap_config.namespace
75
+ @name = controller_path.gsub('/', '_')
76
+
77
+ render :template => "wash_out/#{soap_config.wsdl_style}/wsdl", :layout => false,
78
+ :content_type => 'text/xml'
79
+ end
80
+
81
+ # Render a SOAP response.
82
+ def _render_soap(result, options)
83
+ @namespace = soap_config.namespace
84
+ @operation = soap_action = request.env['wash_out.soap_action']
85
+ @action_spec = self.class.soap_actions[soap_action]
86
+
87
+ result = { 'value' => result } unless result.is_a? Hash
88
+ result = HashWithIndifferentAccess.new(result)
89
+
90
+ inject = lambda {|data, map|
91
+ result_spec = []
92
+ return result_spec if data.nil?
93
+
94
+ map.each_with_index do |param, i|
95
+ result_spec[i] = param.flat_copy
96
+
97
+ unless data.is_a?(Hash)
98
+ raise ProgrammerError,
99
+ "SOAP response used #{data.inspect} (which is #{data.class.name}), " +
100
+ "in the context where a Hash with key of '#{param.raw_name}' " +
101
+ "was expected."
102
+ end
103
+
104
+ value = data[param.raw_name]
105
+
106
+ unless value.nil?
107
+ if param.multiplied && !value.is_a?(Array)
108
+ raise ProgrammerError,
109
+ "SOAP response tried to use '#{value.inspect}' " +
110
+ "(which is of type #{value.class.name}), as the value for " +
111
+ "'#{param.raw_name}' (which expects an Array)."
112
+ end
113
+
114
+ # Inline complex structure {:foo => {bar: ...}}
115
+ if param.struct? && !param.multiplied
116
+ result_spec[i].map = inject.call(value, param.map)
117
+
118
+ # Inline array of complex structures {:foo => [{bar: ...}]}
119
+ elsif param.struct? && param.multiplied
120
+ result_spec[i].map = value.map{|e| inject.call(e, param.map)}
121
+
122
+ # Inline scalar {:foo => :string}
123
+ else
124
+ result_spec[i].value = value
125
+ end
126
+ end
127
+ end
128
+
129
+ return result_spec
130
+ }
131
+
132
+ render :template => "wash_out/#{soap_config.wsdl_style}/response",
133
+ :layout => false,
134
+ :locals => { :result => inject.call(result, @action_spec[:out]) },
135
+ :content_type => 'text/xml'
136
+ end
137
+
138
+ # This action is a fallback for all undefined SOAP actions.
139
+ def _invalid_action
140
+ render_soap_error("Cannot find SOAP action mapping for #{request.env['wash_out.soap_action']}")
141
+ end
142
+
143
+ def _catch_soap_errors
144
+ yield
145
+ rescue SOAPError => error
146
+ render_soap_error(error.message, error.code)
147
+ end
148
+
149
+ # Render a SOAP error response.
150
+ #
151
+ # Rails do not support sequental rescue_from handling, that is, rescuing an
152
+ # exception from a rescue_from handler. Hence this function is a public API.
153
+ def render_soap_error(message, code=nil)
154
+ render :template => "wash_out/#{soap_config.wsdl_style}/error", :status => 500,
155
+ :layout => false,
156
+ :locals => { :error_message => message, :error_code => (code || 'Server') },
157
+ :content_type => 'text/xml'
158
+ end
159
+
160
+ def self.included(controller)
161
+ entity = if defined?(Rails::VERSION::MAJOR) && (Rails::VERSION::MAJOR >= 4)
162
+ 'action'
163
+ else
164
+ 'filter'
165
+ end
166
+
167
+ controller.send :"around_#{entity}", :_catch_soap_errors
168
+ controller.send :helper, :wash_out
169
+ controller.send :"before_#{entity}", :_authenticate_wsse, :except => [
170
+ :_generate_wsdl, :_invalid_action ]
171
+ controller.send :"before_#{entity}", :_map_soap_parameters, :except => [
172
+ :_generate_wsdl, :_invalid_action ]
173
+ controller.send :"skip_before_#{entity}", :verify_authenticity_token
174
+ end
175
+
176
+ def self.deep_select(hash, result=[], &blk)
177
+ result += Hash[hash.select(&blk)].values
178
+
179
+ hash.each do |key, value|
180
+ result = deep_select(value, result, &blk) if value.is_a? Hash
181
+ end
182
+
183
+ result
184
+ end
185
+
186
+ def self.deep_replace_href(hash, replace)
187
+ return replace[hash[:@href]] if hash.has_key?(:@href)
188
+
189
+ hash.keys.each do |key, value|
190
+ hash[key] = deep_replace_href(hash[key], replace) if hash[key].is_a?(Hash)
191
+ end
192
+
193
+ hash
194
+ end
195
+
196
+ private
197
+
198
+ def action_spec
199
+ self.class.soap_actions[soap_action]
200
+ end
201
+
202
+ def request_input_tag
203
+ action_spec[:request_tag]
204
+ end
205
+
206
+ def soap_action
207
+ request.env['wash_out.soap_action']
208
+ end
209
+
210
+ def xml_data
211
+ xml_data = request.env['wash_out.soap_data'].values_at(:envelope, :Envelope).compact.first
212
+ xml_data = xml_data.values_at(:body, :Body).compact.first || {}
213
+ return xml_data if soap_config.wsdl_style == "document"
214
+ xml_data = xml_data.values_at(soap_action.underscore.to_sym, soap_action.to_sym, request_input_tag.to_sym).compact.first || {}
215
+ end
216
+
217
+ end
218
+ end
@@ -0,0 +1,12 @@
1
+
2
+ module WashOut
3
+ class Engine < ::Rails::Engine
4
+ config.wash_out = ActiveSupport::OrderedOptions.new
5
+ initializer "wash_out.configuration" do |app|
6
+ if app.config.wash_out[:catch_xml_errors]
7
+ app.config.middleware.insert_after 'ActionDispatch::ShowExceptions', WashOut::Middleware
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ class WashOut::Middleware
2
+ def initialize app
3
+ @app = app
4
+ end
5
+
6
+ def call env
7
+ begin
8
+ @app.call env
9
+ rescue REXML::ParseException => e
10
+ self.class.raise_or_render_rexml_parse_error e, env
11
+ end
12
+ end
13
+
14
+ def self.raise_or_render_rexml_parse_error e, env
15
+ raise e unless env.has_key? 'HTTP_SOAPACTION'
16
+
17
+ # Normally input would be a StringIO, but Passenger has a different API:
18
+ input = env['rack.input']
19
+ req = if input.respond_to? :string then input.string else input.read end
20
+
21
+ env['rack.errors'].puts <<-EOERR
22
+ WashOut::Exception: #{e.continued_exception} for:
23
+ #{req}
24
+ EOERR
25
+ [400, {'Content-Type' => 'text/xml'},
26
+ [render_client_soap_fault("Error parsing SOAP Request XML")]]
27
+ end
28
+
29
+ def self.render_client_soap_fault msg
30
+ xml = Builder::XmlMarkup.new
31
+ xml.tag! 'soap:Envelope', 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/',
32
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' do
33
+ xml.tag! 'soap:Body' do
34
+ xml.tag! 'soap:Fault', :encodingStyle => 'http://schemas.xmlsoap.org/soap/encoding/' do
35
+ xml.faultcode 'Client'
36
+ xml.faultstring msg
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ module WashOut
2
+ module Model
3
+ def wash_out_columns
4
+ columns_hash
5
+ end
6
+
7
+ def wash_out_param_map
8
+ types = {
9
+ :text => :string,
10
+ :float => :double,
11
+ :decimal => :double,
12
+ :timestamp => :string
13
+ }
14
+ map = {}
15
+
16
+ wash_out_columns.each do |key, column|
17
+ type = column.type
18
+ type = types[type] if types.has_key?(type)
19
+ map[key] = type
20
+ end
21
+
22
+ map
23
+ end
24
+
25
+ def wash_out_param_name(*args)
26
+ return name.underscore
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,200 @@
1
+ module WashOut
2
+ class Param
3
+ attr_accessor :raw_name
4
+ attr_accessor :name
5
+ attr_accessor :map
6
+ attr_accessor :type
7
+ attr_accessor :multiplied
8
+ attr_accessor :value
9
+ attr_accessor :source_class
10
+ attr_accessor :soap_config
11
+
12
+ # Defines a WSDL parameter with name +name+ and type specifier +type+.
13
+ # The type specifier format is described in #parse_def.
14
+ def initialize(soap_config, name, type, multiplied = false)
15
+ type ||= {}
16
+ @soap_config = soap_config
17
+ @name = name.to_s
18
+ @raw_name = name.to_s
19
+ @map = {}
20
+ @multiplied = multiplied
21
+
22
+ if soap_config.camelize_wsdl.to_s == 'lower'
23
+ @name = @name.camelize(:lower)
24
+ elsif soap_config.camelize_wsdl
25
+ @name = @name.camelize
26
+ end
27
+
28
+ if type.is_a?(Symbol)
29
+ @type = type.to_s
30
+ elsif type.is_a?(Class)
31
+ @type = 'struct'
32
+ @map = self.class.parse_def(soap_config, type.wash_out_param_map)
33
+ @source_class = type
34
+ else
35
+ @type = 'struct'
36
+ @map = self.class.parse_def(soap_config, type)
37
+ end
38
+ end
39
+
40
+ # Converts a generic externally derived Ruby value, such as String or
41
+ # Hash, to a native Ruby object according to the definition of this type.
42
+ def load(data, key)
43
+ if !data.has_key? key
44
+ raise WashOut::Dispatcher::SOAPError, "Required SOAP parameter '#{key}' is missing"
45
+ end
46
+
47
+ data = data[key]
48
+ data = [data] if @multiplied && !data.is_a?(Array)
49
+
50
+ if struct?
51
+ data ||= {}
52
+ if @multiplied
53
+ data.map do |x|
54
+ map_struct x do |param, dat, elem|
55
+ param.load(dat, elem)
56
+ end
57
+ end
58
+ else
59
+ map_struct data do |param, dat, elem|
60
+ param.load(dat, elem)
61
+ end
62
+ end
63
+ else
64
+ operation = case type
65
+ when 'string'; :to_s
66
+ when 'integer'; :to_i
67
+ when 'long'; :to_i
68
+ when 'double'; :to_f
69
+ when 'boolean'; lambda{|dat| dat === "0" ? false : !!dat}
70
+ when 'date'; :to_date
71
+ when 'datetime'; :to_datetime
72
+ when 'time'; :to_time
73
+ when 'base64Binary'; lambda{|dat| Base64.decode64(dat)}
74
+ else raise RuntimeError, "Invalid WashOut simple type: #{type}"
75
+ end
76
+
77
+ begin
78
+ if data.nil?
79
+ data
80
+ elsif @multiplied
81
+ return data.map{|x| x.send(operation)} if operation.is_a?(Symbol)
82
+ return data.map{|x| operation.call(x)} if operation.is_a?(Proc)
83
+ elsif operation.is_a? Symbol
84
+ data.send(operation)
85
+ else
86
+ operation.call(data)
87
+ end
88
+ rescue
89
+ raise WashOut::Dispatcher::SOAPError, "Invalid SOAP parameter '#{key}' format"
90
+ end
91
+ end
92
+ end
93
+
94
+ # Checks if this Param defines a complex type.
95
+ def struct?
96
+ type == 'struct'
97
+ end
98
+
99
+ def classified?
100
+ !source_class.nil?
101
+ end
102
+
103
+ def basic_type
104
+ return name unless classified?
105
+ return source_class.wash_out_param_name(@soap_config)
106
+ end
107
+
108
+ def xsd_type
109
+ return 'int' if type.to_s == 'integer'
110
+ return 'dateTime' if type.to_s == 'datetime'
111
+ return type
112
+ end
113
+
114
+ # Returns a WSDL namespaced identifier for this type.
115
+ def namespaced_type
116
+ struct? ? "tns:#{basic_type}" : "xsd:#{xsd_type}"
117
+ end
118
+
119
+ # Parses a +definition+. The format of the definition is best described
120
+ # by the following BNF-like grammar.
121
+ #
122
+ # simple_type := :string | :integer | :double | :boolean
123
+ # nested_type := type_hash | simple_type | WashOut::Param instance
124
+ # type_hash := { :parameter_name => nested_type, ... }
125
+ # definition := [ WashOut::Param, ... ] |
126
+ # type_hash |
127
+ # simple_type
128
+ #
129
+ # If a simple type is passed as the +definition+, a single Param is returned
130
+ # with the +name+ set to "value".
131
+ # If a WashOut::Param instance is passed as a +nested_type+, the corresponding
132
+ # +:parameter_name+ is ignored.
133
+ #
134
+ # This function returns an array of WashOut::Param objects.
135
+ def self.parse_def(soap_config, definition)
136
+ raise RuntimeError, "[] should not be used in your params. Use nil if you want to mark empty set." if definition == []
137
+ return [] if definition == nil
138
+
139
+ if definition.is_a?(Class) && definition.ancestors.include?(WashOut::Type)
140
+ definition = definition.wash_out_param_map
141
+ end
142
+
143
+ if [Array, Symbol].include?(definition.class)
144
+ definition = { :value => definition }
145
+ end
146
+
147
+ if definition.is_a? Hash
148
+ definition.map do |name, opt|
149
+ if opt.is_a? WashOut::Param
150
+ opt
151
+ elsif opt.is_a? Array
152
+ WashOut::Param.new(soap_config, name, opt[0], true)
153
+ else
154
+ WashOut::Param.new(soap_config, name, opt)
155
+ end
156
+ end
157
+ else
158
+ raise RuntimeError, "Wrong definition: #{definition.inspect}"
159
+ end
160
+ end
161
+
162
+ def flat_copy
163
+ copy = self.class.new(@soap_config, @name, @type.to_sym, @multiplied)
164
+ copy.raw_name = raw_name
165
+ copy.source_class = source_class
166
+ copy
167
+ end
168
+
169
+ def attribute?
170
+ name[0] == "@"
171
+ end
172
+
173
+ def attr_name
174
+ raise 'Not attribute' unless attribute?
175
+ name[1..-1]
176
+ end
177
+
178
+ private
179
+
180
+ # Used to load an entire structure.
181
+ def map_struct(data)
182
+ unless data.is_a?(Hash)
183
+ raise WashOut::Dispatcher::SOAPError, "SOAP message structure is broken"
184
+ end
185
+
186
+ data = data.with_indifferent_access
187
+ struct = {}.with_indifferent_access
188
+
189
+ # RUBY18 Enumerable#each_with_object is better, but 1.9 only.
190
+ @map.map do |param|
191
+ if data.has_key? param.raw_name
192
+ param_name = param.attribute? ? param.attr_name : param.raw_name
193
+ struct[param_name] = yield param, data, param.raw_name
194
+ end
195
+ end
196
+
197
+ struct
198
+ end
199
+ end
200
+ end