1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
|
module ActionWebService # :nodoc:
module API # :nodoc:
class CastingError < ActionWebService::ActionWebServiceError
end
# A web service API class specifies the methods that will be available for
# invocation for an API. It also contains metadata such as the method type
# signature hints.
#
# It is not intended to be instantiated.
#
# It is attached to web service implementation classes like
# ActionWebService::Base and ActionController::Base derivatives by using
# ClassMethods#web_service_api.
class Base
# Whether to transform the public API method names into camel-cased names
class_inheritable_option :inflect_names, true
# Whether to allow ActiveRecord::Base models in <tt>:expects</tt>.
# The default is +false+, you should be aware of the security implications
# of allowing this, and ensure that you don't allow remote callers to
# easily overwrite data they should not have access to.
class_inheritable_option :allow_active_record_expects, false
# If present, the name of a method to call when the remote caller
# tried to call a nonexistent method. Semantically equivalent to
# +method_missing+.
class_inheritable_option :default_api_method
# Disallow instantiation
private_class_method :new, :allocate
class << self
# API methods have a +name+, which must be the Ruby method name to use when
# performing the invocation on the web service object.
#
# The signatures for the method input parameters and return value can
# by specified in +options+.
#
# A signature is an array of one or more parameter specifiers.
# A parameter specifier can be one of the following:
#
# * A symbol or string of representing one of the Action Web Service base types.
# See ActionWebService::Signature for a canonical list of the base types.
# * The Class object of the parameter type
# * A single-element Array containing one of the two preceding items. This
# will cause Action Web Service to treat the parameter at that position
# as an array containing only values of the given type.
# * A Hash containing as key the name of the parameter, and as value
# one of the three preceding items
#
# If no method input parameter or method return value signatures are given,
# the method is assumed to take no parameters and/or return no values of
# interest, and any values that are received by the server will be
# discarded and ignored.
#
# Valid options:
# [<tt>:expects</tt>] Signature for the method input parameters
# [<tt>:returns</tt>] Signature for the method return value
# [<tt>:expects_and_returns</tt>] Signature for both input parameters and return value
def api_method(name, options={})
validate_options([:expects, :returns, :expects_and_returns], options.keys)
if options[:expects_and_returns]
expects = options[:expects_and_returns]
returns = options[:expects_and_returns]
else
expects = options[:expects]
returns = options[:returns]
end
expects = canonical_signature(expects)
returns = canonical_signature(returns)
if expects
expects.each do |param|
klass = WS::BaseTypes.canonical_param_type_class(param)
klass = klass[0] if klass.is_a?(Array)
if klass.ancestors.include?(ActiveRecord::Base) && !allow_active_record_expects
raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects")
end
end
end
name = name.to_sym
public_name = public_api_method_name(name)
method = Method.new(name, public_name, expects, returns)
write_inheritable_hash("api_methods", name => method)
write_inheritable_hash("api_public_method_names", public_name => name)
end
# Whether the given method name is a service method on this API
def has_api_method?(name)
api_methods.has_key?(name)
end
# Whether the given public method name has a corresponding service method
# on this API
def has_public_api_method?(public_name)
api_public_method_names.has_key?(public_name)
end
# The corresponding public method name for the given service method name
def public_api_method_name(name)
if inflect_names
name.to_s.camelize
else
name.to_s
end
end
# The corresponding service method name for the given public method name
def api_method_name(public_name)
api_public_method_names[public_name]
end
# A Hash containing all service methods on this API, and their
# associated metadata.
def api_methods
read_inheritable_attribute("api_methods") || {}
end
# The Method instance for the given public API method name, if any
def public_api_method_instance(public_method_name)
api_method_instance(api_method_name(public_method_name))
end
# The Method instance for the given API method name, if any
def api_method_instance(method_name)
api_methods[method_name]
end
# The Method instance for the default API method, if any
def default_api_method_instance
return nil unless name = default_api_method
instance = read_inheritable_attribute("default_api_method_instance")
if instance && instance.name == name
return instance
end
instance = Method.new(name, public_api_method_name(name), nil, nil)
write_inheritable_attribute("default_api_method_instance", instance)
instance
end
# Creates a dummy API Method instance for the given public method name
def dummy_public_api_method_instance(public_method_name)
Method.new(public_method_name.underscore.to_sym, public_method_name, nil, nil)
end
# Creates a dummy API Method instance for the given method name
def dummy_api_method_instance(method_name)
Method.new(method_name, public_api_method_name(method_name), nil, nil)
end
private
def api_public_method_names
read_inheritable_attribute("api_public_method_names") || {}
end
def validate_options(valid_option_keys, supplied_option_keys)
unknown_option_keys = supplied_option_keys - valid_option_keys
unless unknown_option_keys.empty?
raise(ActionWebServiceError, "Unknown options: #{unknown_option_keys}")
end
end
def canonical_signature(signature)
return nil if signature.nil?
signature.map{|spec| WS::BaseTypes.canonical_param_type_spec(spec)}
end
end
end
# Represents an API method and its associated metadata, and provides functionality
# to assist in commonly performed API method tasks.
class Method
attr :name
attr :public_name
attr :expects
attr :returns
def initialize(name, public_name, expects, returns)
@name = name
@public_name = public_name
@expects = expects
@returns = returns
end
# The list of parameter names for this method
def param_names
return [] unless @expects
i = 0
@expects.map{ |spec| param_name(spec, i += 1) }
end
# The name for the given parameter
def param_name(spec, i=1)
spec.is_a?(Hash) ? spec.keys.first.to_s : "p#{i}"
end
# The type of the parameter declared in +spec+. Is either
# the Class of the parameter, or its canonical name (if its a
# base type). Typed array specifications will return the type of
# their elements.
def param_type(spec)
spec = spec.values.first if spec.is_a?(Hash)
param_type = spec.is_a?(Array) ? spec[0] : spec
WS::BaseTypes::class_to_type_name(param_type) rescue param_type
end
# The Class of the parameter declared in +spec+.
def param_class(spec)
type = param_type(spec)
type.is_a?(Symbol) ? WS::BaseTypes.type_name_to_class(type) : type
end
# Registers all types known to this method with the given marshaler
def register_types(marshaler)
@expects.each{ |x| marshaler.register_type(x) } if @expects
@returns.each{ |x| marshaler.register_type(x) } if @returns
end
# Encodes an RPC call for this method. Casting is performed if
# the <tt>:strict</tt> option is given.
def encode_rpc_call(marshaler, encoder, params, options={})
name = options[:method_name] || @public_name
expects = @expects || []
returns = @returns || []
(expects + returns).each { |spec| marshaler.register_type spec }
(0..(params.length-1)).each do |i|
spec = expects[i] || params[i].class
type_binding = marshaler.lookup_type(spec)
param_info = WS::ParamInfo.create(spec, type_binding, i)
if options[:strict]
value = marshaler.cast_outbound_recursive(params[i], spec)
else
value = params[i]
end
param = WS::Param.new(value, param_info)
params[i] = marshaler.marshal(param)
end
encoder.encode_rpc_call(name, params)
end
# Encodes an RPC response for this method. Casting is performed if
# the <tt>:strict</tt> option is given.
def encode_rpc_response(marshaler, encoder, return_value, options={})
if !return_value.nil? && @returns
return_type = @returns[0]
type_binding = marshaler.register_type(return_type)
param_info = WS::ParamInfo.create(return_type, type_binding, 0)
if options[:strict]
return_value = marshaler.cast_inbound_recursive(return_value, return_type)
end
return_value = marshaler.marshal(WS::Param.new(return_value, param_info))
else
return_value = nil
end
encoder.encode_rpc_response(response_name(encoder), return_value)
end
# Casts a set of WS::Param values into the appropriate
# Ruby values
def cast_expects_ws2ruby(marshaler, params)
return [] if @expects.nil?
i = 0
@expects.map do |spec|
value = marshaler.cast_inbound_recursive(params[i].value, spec)
i += 1
value
end
end
# Casts a set of Ruby values into the expected Ruby values
def cast_expects(marshaler, params)
return [] if @expects.nil?
i = 0
@expects.map do |spec|
value = marshaler.cast_outbound_recursive(params[i], spec)
i += 1
value
end
end
# Cast a Ruby return value into the expected Ruby value
def cast_returns(marshaler, return_value)
return nil if @returns.nil?
marshaler.cast_inbound_recursive(return_value, @returns[0])
end
# String representation of this method
def to_s
fqn = ""
fqn << (@returns ? (friendly_param(@returns[0], nil) + " ") : "void ")
fqn << "#{@public_name}("
if @expects
i = 0
fqn << @expects.map{ |p| friendly_param(p, i+= 1) }.join(", ")
end
fqn << ")"
fqn
end
private
def response_name(encoder)
encoder.is_a?(WS::Encoding::SoapRpcEncoding) ? (@public_name + "Response") : @public_name
end
def friendly_param(spec, i)
name = param_name(spec, i)
type = param_type(spec)
spec = spec.values.first if spec.is_a?(Hash)
type = spec.is_a?(Array) ? (type.to_s + "[]") : type.to_s
i ? (type + " " + name) : type
end
end
end
end
|