wadl 0.1.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/COPYING +676 -0
- data/ChangeLog +5 -0
- data/README +75 -0
- data/Rakefile +23 -0
- data/examples/YahooSearch.rb +7 -0
- data/examples/YahooSearch.wadl +88 -0
- data/examples/crummy.rb +17 -0
- data/examples/crummy.wadl +34 -0
- data/examples/delicious.rb +24 -0
- data/examples/delicious.wadl +348 -0
- data/examples/yahoo.rb +14 -0
- data/examples/yahoo.wadl +37 -0
- data/lib/wadl/version.rb +27 -0
- data/lib/wadl.rb +1263 -0
- data/test/test_wadl.rb +303 -0
- metadata +112 -0
data/lib/wadl.rb
ADDED
@@ -0,0 +1,1263 @@
|
|
1
|
+
#--
|
2
|
+
###############################################################################
|
3
|
+
# #
|
4
|
+
# wadl -- Super cheap Ruby WADL client #
|
5
|
+
# #
|
6
|
+
# Copyright (C) 2006-2008 Leonard Richardson #
|
7
|
+
# Copyright (C) 2010 Jens Wille #
|
8
|
+
# #
|
9
|
+
# Authors: #
|
10
|
+
# Leonard Richardson <leonardr@segfault.org> (Original author) #
|
11
|
+
# Jens Wille <jens.wille@uni-koeln.de> #
|
12
|
+
# #
|
13
|
+
# wadl is free software; you can redistribute it and/or modify it under the #
|
14
|
+
# terms of the GNU General Public License as published by the Free Software #
|
15
|
+
# Foundation; either version 3 of the License, or (at your option) any later #
|
16
|
+
# version. #
|
17
|
+
# #
|
18
|
+
# wadl is distributed in the hope that it will be useful, but WITHOUT ANY #
|
19
|
+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS #
|
20
|
+
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more #
|
21
|
+
# details. #
|
22
|
+
# #
|
23
|
+
# You should have received a copy of the GNU General Public License along #
|
24
|
+
# with wadl. If not, see <http://www.gnu.org/licenses/>. #
|
25
|
+
# #
|
26
|
+
###############################################################################
|
27
|
+
#++
|
28
|
+
|
29
|
+
%w[delegate rexml/document set cgi yaml rubygems rest-open-uri].each { |lib|
|
30
|
+
require lib
|
31
|
+
}
|
32
|
+
|
33
|
+
begin
|
34
|
+
require 'mime/types'
|
35
|
+
rescue LoadError
|
36
|
+
end
|
37
|
+
|
38
|
+
module WADL
|
39
|
+
|
40
|
+
OAUTH_HEADER = 'Authorization'
|
41
|
+
OAUTH_PREFIX = 'OAuth:'
|
42
|
+
|
43
|
+
# A container for application-specific faults
|
44
|
+
module Faults
|
45
|
+
end
|
46
|
+
|
47
|
+
#########################################################################
|
48
|
+
#
|
49
|
+
# A cheap way of defining an XML schema as Ruby classes and then parsing
|
50
|
+
# documents into instances of those classes.
|
51
|
+
class CheapSchema
|
52
|
+
|
53
|
+
@may_be_reference = false
|
54
|
+
@contents_are_mixed_data = false
|
55
|
+
|
56
|
+
ATTRIBUTES = %w[names members collections required_attributes attributes]
|
57
|
+
|
58
|
+
class << self
|
59
|
+
|
60
|
+
attr_reader(*ATTRIBUTES)
|
61
|
+
|
62
|
+
def init
|
63
|
+
@names, @members, @collections = {}, {}, {}
|
64
|
+
@required_attributes, @attributes = [], []
|
65
|
+
end
|
66
|
+
|
67
|
+
def inherit(from)
|
68
|
+
init
|
69
|
+
|
70
|
+
ATTRIBUTES.each { |attr|
|
71
|
+
value = from.send(attr)
|
72
|
+
instance_variable_set("@#{attr}", value.dup) if value
|
73
|
+
}
|
74
|
+
|
75
|
+
%w[may_be_reference contents_are_mixed_data].each { |attr|
|
76
|
+
instance_variable_set("@#{attr}", from.instance_variable_get("@#{attr}"))
|
77
|
+
}
|
78
|
+
end
|
79
|
+
|
80
|
+
def inherited(klass)
|
81
|
+
klass.inherit(self)
|
82
|
+
end
|
83
|
+
|
84
|
+
def may_be_reference?
|
85
|
+
@may_be_reference
|
86
|
+
end
|
87
|
+
|
88
|
+
def in_document(element_name)
|
89
|
+
@names[:element] = element_name
|
90
|
+
@names[:member] = element_name
|
91
|
+
@names[:collection] = element_name + 's'
|
92
|
+
end
|
93
|
+
|
94
|
+
def as_collection(collection_name)
|
95
|
+
@names[:collection] = collection_name
|
96
|
+
end
|
97
|
+
|
98
|
+
def as_member(member_name)
|
99
|
+
@names[:member] = member_name
|
100
|
+
end
|
101
|
+
|
102
|
+
def contents_are_mixed_data
|
103
|
+
@contents_are_mixed_data = true
|
104
|
+
end
|
105
|
+
|
106
|
+
def has_one(*classes)
|
107
|
+
classes.each { |klass|
|
108
|
+
@members[klass.names[:element]] = klass
|
109
|
+
dereferencing_instance_accessor(klass.names[:member])
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def has_many(*classes)
|
114
|
+
classes.each { |klass|
|
115
|
+
@collections[klass.names[:element]] = klass
|
116
|
+
|
117
|
+
collection_name = klass.names[:collection]
|
118
|
+
dereferencing_instance_accessor(collection_name)
|
119
|
+
|
120
|
+
# Define a method for finding a specific element of this
|
121
|
+
# collection.
|
122
|
+
class_eval <<-EOT, __FILE__, __LINE__ + 1
|
123
|
+
def find_#{klass.names[:element]}(*args, &block)
|
124
|
+
block ||= begin
|
125
|
+
name = args.shift.to_s
|
126
|
+
lambda { |match| match.matches?(name) }
|
127
|
+
end
|
128
|
+
|
129
|
+
auto_dereference = args.shift
|
130
|
+
auto_dereference = true if auto_dereference.nil?
|
131
|
+
|
132
|
+
match = #{collection_name}.find { |match|
|
133
|
+
block[match] || (
|
134
|
+
#{klass}.may_be_reference? &&
|
135
|
+
auto_dereference &&
|
136
|
+
block[match.dereference]
|
137
|
+
)
|
138
|
+
}
|
139
|
+
|
140
|
+
match && auto_dereference ? match.dereference : match
|
141
|
+
end
|
142
|
+
EOT
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
def dereferencing_instance_accessor(*symbols)
|
147
|
+
define_dereferencing_accessors(symbols,
|
148
|
+
'd, v = dereference, :@%s; ' <<
|
149
|
+
'd.instance_variable_get(v) if d.instance_variable_defined?(v)',
|
150
|
+
'dereference.instance_variable_set(:@%s, value)'
|
151
|
+
)
|
152
|
+
end
|
153
|
+
|
154
|
+
def dereferencing_attr_accessor(*symbols)
|
155
|
+
define_dereferencing_accessors(symbols,
|
156
|
+
'dereference.attributes["%s"]',
|
157
|
+
'dereference.attributes["%s"] = value'
|
158
|
+
)
|
159
|
+
end
|
160
|
+
|
161
|
+
def has_attributes(*names)
|
162
|
+
has_required_or_attributes(names, @attributes)
|
163
|
+
end
|
164
|
+
|
165
|
+
def has_required(*names)
|
166
|
+
has_required_or_attributes(names, @required_attributes)
|
167
|
+
end
|
168
|
+
|
169
|
+
def may_be_reference
|
170
|
+
@may_be_reference = true
|
171
|
+
|
172
|
+
find_method_name = "find_#{names[:element]}"
|
173
|
+
|
174
|
+
class_eval <<-EOT, __FILE__, __LINE__ + 1
|
175
|
+
def dereference
|
176
|
+
return self unless href = attributes['href']
|
177
|
+
|
178
|
+
unless @referenced
|
179
|
+
p = self
|
180
|
+
|
181
|
+
until @referenced || !p
|
182
|
+
begin
|
183
|
+
p = p.parent
|
184
|
+
end until !p || p.respond_to?(:#{find_method_name})
|
185
|
+
|
186
|
+
@referenced = p.#{find_method_name}(href, false) if p
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
dereference_with_context(@referenced) if @referenced
|
191
|
+
end
|
192
|
+
EOT
|
193
|
+
end
|
194
|
+
|
195
|
+
# Turn an XML element into an instance of this class.
|
196
|
+
def from_element(parent, element, need_finalization)
|
197
|
+
attributes = element.attributes
|
198
|
+
|
199
|
+
me = new
|
200
|
+
me.parent = parent
|
201
|
+
|
202
|
+
@collections.each { |name, klass|
|
203
|
+
me.instance_variable_set("@#{klass.names[:collection]}", [])
|
204
|
+
}
|
205
|
+
|
206
|
+
if may_be_reference? and href = attributes['href']
|
207
|
+
# Handle objects that are just references to other objects
|
208
|
+
# somewhere above this one in the hierarchy
|
209
|
+
href = href.dup
|
210
|
+
href.sub!(/\A#/, '') or warn "Warning: HREF #{href} should be ##{href}"
|
211
|
+
|
212
|
+
me.attributes['href'] = href
|
213
|
+
else
|
214
|
+
# Handle this element's attributes
|
215
|
+
@required_attributes.each { |name|
|
216
|
+
name = name.to_s
|
217
|
+
|
218
|
+
raise ArgumentError, %Q{Missing required attribute "#{name}" in element: #{element}} unless attributes[name]
|
219
|
+
|
220
|
+
me.attributes[name] = attributes[name]
|
221
|
+
me.index_key = attributes[name] if name == @index_attribute
|
222
|
+
}
|
223
|
+
|
224
|
+
@attributes.each { |name|
|
225
|
+
name = name.to_s
|
226
|
+
|
227
|
+
me.attributes[name] = attributes[name]
|
228
|
+
me.index_key = attributes[name] if name == @index_attribute
|
229
|
+
}
|
230
|
+
end
|
231
|
+
|
232
|
+
# Handle this element's children.
|
233
|
+
if @contents_are_mixed_data
|
234
|
+
me.instance_variable_set(:@contents, element.children)
|
235
|
+
else
|
236
|
+
element.each_element { |child|
|
237
|
+
if klass = @members[child.name] || @collections[child.name]
|
238
|
+
object = klass.from_element(me, child, need_finalization)
|
239
|
+
|
240
|
+
if klass == @members[child.name]
|
241
|
+
instance_variable_name = "@#{klass.names[:member]}"
|
242
|
+
|
243
|
+
if me.instance_variable_defined?(instance_variable_name)
|
244
|
+
raise "#{name} can only have one #{klass.name}, but several were specified in element: #{element}"
|
245
|
+
end
|
246
|
+
|
247
|
+
me.instance_variable_set(instance_variable_name, object)
|
248
|
+
else
|
249
|
+
me.instance_variable_get("@#{klass.names[:collection]}") << object
|
250
|
+
end
|
251
|
+
end
|
252
|
+
}
|
253
|
+
end
|
254
|
+
|
255
|
+
need_finalization << me if me.respond_to?(:finalize_creation)
|
256
|
+
|
257
|
+
me
|
258
|
+
end
|
259
|
+
|
260
|
+
private
|
261
|
+
|
262
|
+
def define_dereferencing_accessors(symbols, getter, setter)
|
263
|
+
symbols.each { |name|
|
264
|
+
name = name.to_s
|
265
|
+
|
266
|
+
class_eval <<-EOT, __FILE__, __LINE__ + 1 unless name =~ /\W/
|
267
|
+
def #{name}; #{getter % name}; end
|
268
|
+
def #{name}=(value); #{setter % name}; end
|
269
|
+
EOT
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
def has_required_or_attributes(names, var)
|
274
|
+
names.each { |name|
|
275
|
+
var << name
|
276
|
+
@index_attribute ||= name.to_s
|
277
|
+
name == :href ? attr_accessor(name) : dereferencing_attr_accessor(name)
|
278
|
+
}
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
# Common instance methods
|
284
|
+
|
285
|
+
attr_accessor :index_key, :href, :parent
|
286
|
+
attr_reader :attributes
|
287
|
+
|
288
|
+
def initialize
|
289
|
+
@attributes, @contents, @referenced = {}, nil, nil
|
290
|
+
end
|
291
|
+
|
292
|
+
# This object is a reference to another object. This method returns
|
293
|
+
# an object that acts like the other object, but also contains any
|
294
|
+
# neccessary context about this object. See the ResourceAndAddress
|
295
|
+
# implementation, in which a dereferenced resource contains
|
296
|
+
# information about the parent of the resource that referenced it
|
297
|
+
# (otherwise, there's no way to build the URI).
|
298
|
+
def dereference_with_context(referent)
|
299
|
+
referent
|
300
|
+
end
|
301
|
+
|
302
|
+
# A null implementation so that foo.dereference will always return the
|
303
|
+
# "real" object.
|
304
|
+
def dereference
|
305
|
+
self
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns whether or not the given name matches this object.
|
309
|
+
# By default, checks the index key for this class.
|
310
|
+
def matches?(name)
|
311
|
+
index_key == name
|
312
|
+
end
|
313
|
+
|
314
|
+
def to_s(indent = 0)
|
315
|
+
klass = self.class
|
316
|
+
|
317
|
+
i = ' ' * indent
|
318
|
+
s = "#{i}#{klass.name}\n"
|
319
|
+
|
320
|
+
if klass.may_be_reference? and href = attributes['href']
|
321
|
+
s << "#{i} href=#{href}\n"
|
322
|
+
else
|
323
|
+
[klass.required_attributes, klass.attributes].each { |list|
|
324
|
+
list.each { |attr|
|
325
|
+
val = attributes[attr.to_s]
|
326
|
+
s << "#{i} #{attr}=#{val}\n" if val
|
327
|
+
}
|
328
|
+
}
|
329
|
+
|
330
|
+
klass.members.each_value { |member_class|
|
331
|
+
o = send(member_class.names[:member])
|
332
|
+
s << o.to_s(indent + 1) if o
|
333
|
+
}
|
334
|
+
|
335
|
+
klass.collections.each_value { |collection_class|
|
336
|
+
c = send(collection_class.names[:collection])
|
337
|
+
|
338
|
+
if c && !c.empty?
|
339
|
+
s << "#{i} Collection of #{c.size} #{collection_class.name}(s)\n"
|
340
|
+
c.each { |o| s << o.to_s(indent + 2) }
|
341
|
+
end
|
342
|
+
}
|
343
|
+
|
344
|
+
if @contents && !@contents.empty?
|
345
|
+
sep = '-' * 80
|
346
|
+
s << "#{sep}\n#{@contents.join(' ')}\n#{sep}\n"
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
s
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
#########################################################################
|
356
|
+
# Classes to keep track of the logical structure of a URI.
|
357
|
+
class URIParts < Struct.new(:uri, :query, :headers)
|
358
|
+
|
359
|
+
def to_s
|
360
|
+
qs = "#{uri.include?('?') ? '&' : '?'}#{query_string}" unless query.empty?
|
361
|
+
"#{uri}#{qs}"
|
362
|
+
end
|
363
|
+
|
364
|
+
alias_method :to_str, :to_s
|
365
|
+
|
366
|
+
def inspect
|
367
|
+
hs = " Plus headers: #{headers.inspect}" if headers
|
368
|
+
"#{to_s}#{hs}"
|
369
|
+
end
|
370
|
+
|
371
|
+
def query_string
|
372
|
+
query.join('&')
|
373
|
+
end
|
374
|
+
|
375
|
+
def hash(x)
|
376
|
+
to_str.hash
|
377
|
+
end
|
378
|
+
|
379
|
+
def ==(x)
|
380
|
+
x.respond_to?(:to_str) ? to_str == x : super
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
|
385
|
+
# The Address class keeps track of the user's path through a resource
|
386
|
+
# graph. Values for WADL parameters may be specified at any time using
|
387
|
+
# the bind method. An Address cannot be turned into a URI and header
|
388
|
+
# set until all required parameters have been bound to values.
|
389
|
+
#
|
390
|
+
# An Address object is built up through calls to Resource#address
|
391
|
+
class Address
|
392
|
+
|
393
|
+
attr_reader :path_fragments, :query_vars, :headers,
|
394
|
+
:path_params, :query_params, :header_params
|
395
|
+
|
396
|
+
def self.embedded_param_names(fragment)
|
397
|
+
fragment.scan(/\{(.+?)\}/).flatten
|
398
|
+
end
|
399
|
+
|
400
|
+
def initialize(path_fragments = [], query_vars = [], headers = {},
|
401
|
+
path_params = {}, query_params = {}, header_params = {})
|
402
|
+
@path_fragments, @query_vars, @headers = path_fragments, query_vars, headers
|
403
|
+
@path_params, @query_params, @header_params = path_params, query_params, header_params
|
404
|
+
end
|
405
|
+
|
406
|
+
def _deep_copy_hash(h)
|
407
|
+
h.inject({}) { |h, (k, v)| h[k] = v && v.dup; h }
|
408
|
+
end
|
409
|
+
|
410
|
+
def _deep_copy_array(a)
|
411
|
+
a.inject([]) { |a, e| a << (e && e.dup) }
|
412
|
+
end
|
413
|
+
|
414
|
+
# Perform a deep copy.
|
415
|
+
def deep_copy
|
416
|
+
Address.new(
|
417
|
+
_deep_copy_array(@path_fragments),
|
418
|
+
_deep_copy_array(@query_vars),
|
419
|
+
_deep_copy_hash(@headers),
|
420
|
+
@path_params.dup,
|
421
|
+
@query_params.dup,
|
422
|
+
@header_params.dup
|
423
|
+
)
|
424
|
+
end
|
425
|
+
|
426
|
+
def to_s
|
427
|
+
"Address:\n" <<
|
428
|
+
" Path fragments: #{@path_fragments.inspect}\n" <<
|
429
|
+
" Query variables: #{@query_vars.inspect}\n" <<
|
430
|
+
" Header variables: #{@headers.inspect}\n" <<
|
431
|
+
" Unbound path parameters: #{@path_params.inspect}\n" <<
|
432
|
+
" Unbound query parameters: #{@query_params.inspect}\n" <<
|
433
|
+
" Unbound header parameters: #{@header_params.inspect}\n"
|
434
|
+
end
|
435
|
+
|
436
|
+
alias_method :inspect, :to_s
|
437
|
+
|
438
|
+
# Binds some or all of the unbound variables in this address to values.
|
439
|
+
def bind!(args = {})
|
440
|
+
path_var_values = args[:path] || {}
|
441
|
+
query_var_values = args[:query] || {}
|
442
|
+
header_var_values = args[:headers] || {}
|
443
|
+
|
444
|
+
# Bind variables found in the path fragments.
|
445
|
+
path_params_to_delete = []
|
446
|
+
|
447
|
+
path_fragments.each { |fragment|
|
448
|
+
if fragment.respond_to?(:to_str)
|
449
|
+
# This fragment is a string which might contain {} substitutions.
|
450
|
+
# Make any substitutions available to the provided path variables.
|
451
|
+
self.class.embedded_param_names(fragment).each { |param_name|
|
452
|
+
value = path_var_values[param_name] || path_var_values[param_name.to_sym]
|
453
|
+
|
454
|
+
value = if param = path_params[param_name]
|
455
|
+
path_params_to_delete << param
|
456
|
+
param % value
|
457
|
+
else
|
458
|
+
Param.default.format(value, param_name)
|
459
|
+
end
|
460
|
+
|
461
|
+
fragment.gsub!("{#{param_name}}", value)
|
462
|
+
}
|
463
|
+
else
|
464
|
+
# This fragment is an array of Param objects (style 'matrix'
|
465
|
+
# or 'plain') which may be bound to strings. As substitutions
|
466
|
+
# happen, the array will become a mixed array of Param objects
|
467
|
+
# and strings.
|
468
|
+
fragment.each_with_index { |param, i|
|
469
|
+
if param.respond_to?(:name)
|
470
|
+
name = param.name
|
471
|
+
|
472
|
+
value = path_var_values[name] || path_var_values[name.to_sym]
|
473
|
+
value = param % value
|
474
|
+
fragment[i] = value if value
|
475
|
+
|
476
|
+
path_params_to_delete << param
|
477
|
+
end
|
478
|
+
}
|
479
|
+
end
|
480
|
+
}
|
481
|
+
|
482
|
+
# Delete any embedded path parameters that are now bound from
|
483
|
+
# our list of unbound parameters.
|
484
|
+
path_params_to_delete.each { |p| path_params.delete(p.name) }
|
485
|
+
|
486
|
+
# Bind query variable values to query parameters
|
487
|
+
query_var_values.each { |name, value|
|
488
|
+
param = query_params.delete(name.to_s)
|
489
|
+
query_vars << param % value if param
|
490
|
+
}
|
491
|
+
|
492
|
+
# Bind header variables to header parameters
|
493
|
+
header_var_values.each { |name, value|
|
494
|
+
param = header_params.delete(name.to_s)
|
495
|
+
headers[name] = param % value if param
|
496
|
+
}
|
497
|
+
|
498
|
+
self
|
499
|
+
end
|
500
|
+
|
501
|
+
def uri(args = {})
|
502
|
+
obj, uri = deep_copy.bind!(args), ''
|
503
|
+
|
504
|
+
# Build the path
|
505
|
+
obj.path_fragments.flatten.each { |fragment|
|
506
|
+
if fragment.respond_to?(:to_str)
|
507
|
+
embedded_param_names = self.class.embedded_param_names(fragment)
|
508
|
+
|
509
|
+
unless embedded_param_names.empty?
|
510
|
+
raise ArgumentError, %Q{Missing a value for required path parameter "#{embedded_param_names[0]}"!}
|
511
|
+
end
|
512
|
+
|
513
|
+
unless fragment.empty?
|
514
|
+
uri << '/' unless uri.empty? || uri =~ /\/\z/
|
515
|
+
uri << fragment
|
516
|
+
end
|
517
|
+
elsif fragment.required?
|
518
|
+
# This is a required Param that was never bound to a value.
|
519
|
+
raise ArgumentError, %Q{Missing a value for required path parameter "#{fragment.name}"!}
|
520
|
+
end
|
521
|
+
}
|
522
|
+
|
523
|
+
# Hunt for required unbound query parameters.
|
524
|
+
obj.query_params.each { |name, value|
|
525
|
+
if value.required?
|
526
|
+
raise ArgumentError, %Q{Missing a value for required query parameter "#{value.name}"!}
|
527
|
+
end
|
528
|
+
}
|
529
|
+
|
530
|
+
# Hunt for required unbound header parameters.
|
531
|
+
obj.header_params.each { |name, value|
|
532
|
+
if value.required?
|
533
|
+
raise ArgumentError, %Q{Missing a value for required header parameter "#{value.name}"!}
|
534
|
+
end
|
535
|
+
}
|
536
|
+
|
537
|
+
URIParts.new(uri, obj.query_vars, obj.headers)
|
538
|
+
end
|
539
|
+
|
540
|
+
end
|
541
|
+
|
542
|
+
#########################################################################
|
543
|
+
#
|
544
|
+
# Now we use Ruby classes to define the structure of a WADL document
|
545
|
+
class Documentation < CheapSchema
|
546
|
+
|
547
|
+
in_document 'doc'
|
548
|
+
has_attributes 'xml:lang', :title
|
549
|
+
contents_are_mixed_data
|
550
|
+
|
551
|
+
end
|
552
|
+
|
553
|
+
class HasDocs < CheapSchema
|
554
|
+
|
555
|
+
has_many Documentation
|
556
|
+
|
557
|
+
# Convenience method to define a no-argument singleton method on
|
558
|
+
# this object.
|
559
|
+
def define_singleton(r, sym, method)
|
560
|
+
name = r.send(sym)
|
561
|
+
|
562
|
+
if name && name !~ /\W/ && !r.respond_to?(name) && !respond_to?(name)
|
563
|
+
instance_eval(%Q{def #{name}\n#{method}('#{name}')\nend})
|
564
|
+
end
|
565
|
+
end
|
566
|
+
|
567
|
+
end
|
568
|
+
|
569
|
+
class Option < HasDocs
|
570
|
+
|
571
|
+
in_document 'option'
|
572
|
+
has_required :value
|
573
|
+
|
574
|
+
end
|
575
|
+
|
576
|
+
class Link < HasDocs
|
577
|
+
|
578
|
+
in_document 'link'
|
579
|
+
has_attributes :href, :rel, :rev
|
580
|
+
|
581
|
+
end
|
582
|
+
|
583
|
+
class Param < HasDocs
|
584
|
+
|
585
|
+
in_document 'param'
|
586
|
+
has_required :name
|
587
|
+
has_attributes :type, :default, :style, :path, :required, :repeating, :fixed
|
588
|
+
has_many Option, Link
|
589
|
+
may_be_reference
|
590
|
+
|
591
|
+
# cf. <http://www.w3.org/TR/xmlschema-2/#boolean>
|
592
|
+
BOOLEAN_RE = %r{\A(?:true|1)\z}
|
593
|
+
|
594
|
+
# A default Param object to use for a path parameter that is
|
595
|
+
# only specified as a name in the path of a resource.
|
596
|
+
def self.default
|
597
|
+
@default ||= begin
|
598
|
+
default = Param.new
|
599
|
+
|
600
|
+
default.required = 'true'
|
601
|
+
default.style = 'plain'
|
602
|
+
default.type = 'xsd:string'
|
603
|
+
|
604
|
+
default
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
def required?
|
609
|
+
required =~ BOOLEAN_RE
|
610
|
+
end
|
611
|
+
|
612
|
+
def repeating?
|
613
|
+
repeating =~ BOOLEAN_RE
|
614
|
+
end
|
615
|
+
|
616
|
+
def inspect
|
617
|
+
%Q{Param "#{name}"}
|
618
|
+
end
|
619
|
+
|
620
|
+
# Validates and formats a proposed value for this parameter. Returns
|
621
|
+
# the formatted value. Raises an ArgumentError if the value
|
622
|
+
# is invalid.
|
623
|
+
#
|
624
|
+
# The 'name' and 'style' arguments are used in conjunction with the
|
625
|
+
# default Param object.
|
626
|
+
def format(value, name = nil, style = nil)
|
627
|
+
name ||= self.name
|
628
|
+
style ||= self.style
|
629
|
+
|
630
|
+
value = fixed if fixed
|
631
|
+
value ||= default if default
|
632
|
+
|
633
|
+
unless value
|
634
|
+
if required?
|
635
|
+
raise ArgumentError, %Q{No value provided for required param "#{name}"!}
|
636
|
+
else
|
637
|
+
return '' # No value provided and none required.
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
if value.respond_to?(:each) && !value.respond_to?(:to_str)
|
642
|
+
if repeating?
|
643
|
+
values = value
|
644
|
+
else
|
645
|
+
raise ArgumentError, %Q{Multiple values provided for single-value param "#{name}"}
|
646
|
+
end
|
647
|
+
else
|
648
|
+
values = [value]
|
649
|
+
end
|
650
|
+
|
651
|
+
# If the param lists acceptable values in option tags, make sure that
|
652
|
+
# all values are found in those tags.
|
653
|
+
if options && !options.empty?
|
654
|
+
values.each { |value|
|
655
|
+
unless find_option(value)
|
656
|
+
acceptable = options.map { |o| o.value }.join('", "')
|
657
|
+
raise ArgumentError, %Q{"#{value}" is not among the acceptable parameter values ("#{acceptable}")}
|
658
|
+
end
|
659
|
+
}
|
660
|
+
end
|
661
|
+
|
662
|
+
if style == 'query' || parent.is_a?(RequestFormat) || (
|
663
|
+
parent.respond_to?(:is_form_representation?) && parent.is_form_representation?
|
664
|
+
)
|
665
|
+
values.map { |v| "#{URI.escape(name)}=#{URI.escape(v.to_s)}" }.join('&')
|
666
|
+
elsif style == 'matrix'
|
667
|
+
if type == 'xsd:boolean'
|
668
|
+
values.map { |v| ";#{name}" if v =~ BOOLEAN_RE }.compact.join
|
669
|
+
else
|
670
|
+
values.map { |v| ";#{URI.escape(name)}=#{URI.escape(v.to_s)}" if v }.compact.join
|
671
|
+
end
|
672
|
+
elsif style == 'header'
|
673
|
+
values.join(',')
|
674
|
+
else
|
675
|
+
# All other cases: plain text representation.
|
676
|
+
values.map { |v| URI.escape(v.to_s) }.join(',')
|
677
|
+
end
|
678
|
+
end
|
679
|
+
|
680
|
+
alias_method :%, :format
|
681
|
+
|
682
|
+
end
|
683
|
+
|
684
|
+
# A mixin for objects that contain representations
|
685
|
+
module RepresentationContainer
|
686
|
+
|
687
|
+
def find_representation_by_media_type(type)
|
688
|
+
representations.find { |r| r.mediaType == type }
|
689
|
+
end
|
690
|
+
|
691
|
+
def find_form
|
692
|
+
representations.find { |r| r.is_form_representation? }
|
693
|
+
end
|
694
|
+
|
695
|
+
end
|
696
|
+
|
697
|
+
class RepresentationFormat < HasDocs
|
698
|
+
|
699
|
+
in_document 'representation'
|
700
|
+
has_attributes :id, :mediaType, :element
|
701
|
+
has_many Param
|
702
|
+
may_be_reference
|
703
|
+
|
704
|
+
def is_form_representation?
|
705
|
+
mediaType == 'application/x-www-form-encoded' || mediaType == 'multipart/form-data'
|
706
|
+
end
|
707
|
+
|
708
|
+
# Creates a representation by plugging a set of parameters
|
709
|
+
# into a representation format.
|
710
|
+
def %(values)
|
711
|
+
unless mediaType == 'application/x-www-form-encoded'
|
712
|
+
raise "wadl.rb can't instantiate a representation of type #{mediaType}"
|
713
|
+
end
|
714
|
+
|
715
|
+
representation = []
|
716
|
+
|
717
|
+
params.each { |param|
|
718
|
+
name = param.name
|
719
|
+
|
720
|
+
if param.fixed
|
721
|
+
p_values = [param.fixed]
|
722
|
+
elsif p_values = values[name] || values[name.to_sym]
|
723
|
+
p_values = [p_values] if !param.repeating? || !p_values.respond_to?(:each) || p_values.respond_to?(:to_str)
|
724
|
+
else
|
725
|
+
raise ArgumentError, "Your proposed representation is missing a value for #{param.name}" if param.required?
|
726
|
+
end
|
727
|
+
|
728
|
+
p_values.each { |v| representation << "#{CGI::escape(name)}=#{CGI::escape(v.to_s)}" } if p_values
|
729
|
+
}
|
730
|
+
|
731
|
+
representation.join('&')
|
732
|
+
end
|
733
|
+
|
734
|
+
end
|
735
|
+
|
736
|
+
class FaultFormat < RepresentationFormat
|
737
|
+
|
738
|
+
in_document 'fault'
|
739
|
+
has_attributes :id, :mediaType, :element, :status
|
740
|
+
has_many Param
|
741
|
+
may_be_reference
|
742
|
+
|
743
|
+
attr_writer :subclass
|
744
|
+
|
745
|
+
def subclass
|
746
|
+
attributes['href'] ? dereference.subclass : @subclass
|
747
|
+
end
|
748
|
+
|
749
|
+
# Define a custom subclass for this fault, so that the programmer
|
750
|
+
# can rescue this particular fault.
|
751
|
+
def self.from_element(*args)
|
752
|
+
me = super
|
753
|
+
|
754
|
+
me.subclass = if name = me.attributes['id']
|
755
|
+
begin
|
756
|
+
WADL::Faults.const_defined?(name) ?
|
757
|
+
WADL::Faults.const_get(name) :
|
758
|
+
WADL::Faults.const_set(name, Class.new(Fault))
|
759
|
+
rescue NameError
|
760
|
+
# This fault format's ID can't be a class name. Use the
|
761
|
+
# generic subclass of Fault.
|
762
|
+
end
|
763
|
+
end || Fault unless me.attributes['href']
|
764
|
+
|
765
|
+
me
|
766
|
+
end
|
767
|
+
|
768
|
+
end
|
769
|
+
|
770
|
+
class RequestFormat < HasDocs
|
771
|
+
|
772
|
+
include RepresentationContainer
|
773
|
+
|
774
|
+
in_document 'request'
|
775
|
+
has_many RepresentationFormat, Param
|
776
|
+
|
777
|
+
# Returns a URI and a set of HTTP headers for this request.
|
778
|
+
def uri(resource, args = {})
|
779
|
+
uri = resource.uri(args)
|
780
|
+
|
781
|
+
query_values = args[:query] || {}
|
782
|
+
header_values = args[:headers] || {}
|
783
|
+
|
784
|
+
params.each { |param|
|
785
|
+
name = param.name
|
786
|
+
|
787
|
+
if param.style == 'header'
|
788
|
+
value = header_values[name] || header_values[name.to_sym]
|
789
|
+
value = param % value
|
790
|
+
|
791
|
+
uri.headers[name] = value if value
|
792
|
+
else
|
793
|
+
value = query_values[name] || query_values[name.to_sym]
|
794
|
+
value = param.format(value, nil, 'query')
|
795
|
+
|
796
|
+
uri.query << value if value
|
797
|
+
end
|
798
|
+
}
|
799
|
+
|
800
|
+
uri
|
801
|
+
end
|
802
|
+
|
803
|
+
end
|
804
|
+
|
805
|
+
class ResponseFormat < HasDocs
|
806
|
+
|
807
|
+
include RepresentationContainer
|
808
|
+
|
809
|
+
in_document 'response'
|
810
|
+
has_many RepresentationFormat, FaultFormat
|
811
|
+
|
812
|
+
# Builds a service response object out of an HTTPResponse object.
|
813
|
+
def build(http_response)
|
814
|
+
# Figure out which fault or representation to use.
|
815
|
+
|
816
|
+
status = http_response.status[0]
|
817
|
+
|
818
|
+
unless response_format = faults.find { |f| f.dereference.status == status }
|
819
|
+
# Try to match the response to a response format using a media
|
820
|
+
# type.
|
821
|
+
response_media_type = http_response.content_type
|
822
|
+
response_format = representations.find { |f|
|
823
|
+
t = f.dereference.mediaType and response_media_type.index(t) == 0
|
824
|
+
}
|
825
|
+
|
826
|
+
# If an exact media type match fails, use the mime-types gem to
|
827
|
+
# match the response to a response format using the underlying
|
828
|
+
# subtype. This will match "application/xml" with "text/xml".
|
829
|
+
response_format ||= begin
|
830
|
+
mime_type = MIME::Types[response_media_type]
|
831
|
+
raw_sub_type = mime_type[0].raw_sub_type if mime_type && !mime_type.empty?
|
832
|
+
|
833
|
+
representations.find { |f|
|
834
|
+
if t = f.dereference.mediaType
|
835
|
+
response_mime_type = MIME::Types[t]
|
836
|
+
response_raw_sub_type = response_mime_type[0].raw_sub_type if response_mime_type && !response_mime_type.empty?
|
837
|
+
response_raw_sub_type == raw_sub_type
|
838
|
+
end
|
839
|
+
}
|
840
|
+
end if defined?(MIME::Types)
|
841
|
+
|
842
|
+
# If all else fails, try to find a response that specifies no
|
843
|
+
# media type. TODO: check if this would be valid WADL.
|
844
|
+
response_format ||= representations.find { |f| !f.dereference.mediaType }
|
845
|
+
end
|
846
|
+
|
847
|
+
body = http_response.read
|
848
|
+
|
849
|
+
if response_format && response_format.mediaType =~ /xml/
|
850
|
+
begin
|
851
|
+
body = REXML::Document.new(body)
|
852
|
+
|
853
|
+
# Find the appropriate element of the document
|
854
|
+
if response_format.element
|
855
|
+
# TODO: don't strip the damn namespace. I'm not very good at
|
856
|
+
# namespaces and I don't see how to deal with them here.
|
857
|
+
element = response_format.element.sub(/.*:/, '')
|
858
|
+
body = REXML::XPath.first(body, "//#{element}")
|
859
|
+
end
|
860
|
+
rescue REXML::ParseException
|
861
|
+
end
|
862
|
+
|
863
|
+
body.extend(XMLRepresentation)
|
864
|
+
body.representation_of(response_format)
|
865
|
+
end
|
866
|
+
|
867
|
+
klass = response_format.is_a?(FaultFormat) ? response_format.subclass : Response
|
868
|
+
obj = klass.new(http_response.status, http_response, body, response_format)
|
869
|
+
|
870
|
+
obj.is_a?(Exception) ? raise(obj) : obj
|
871
|
+
end
|
872
|
+
|
873
|
+
end
|
874
|
+
|
875
|
+
class HTTPMethod < HasDocs
|
876
|
+
|
877
|
+
in_document 'method'
|
878
|
+
as_collection 'http_methods'
|
879
|
+
has_required :id, :name
|
880
|
+
has_one RequestFormat, ResponseFormat
|
881
|
+
may_be_reference
|
882
|
+
|
883
|
+
# Args:
|
884
|
+
# :path - Values for path parameters
|
885
|
+
# :query - Values for query parameters
|
886
|
+
# :headers - Values for header parameters
|
887
|
+
# :send_representation
|
888
|
+
# :expect_representation
|
889
|
+
def call(resource, args = {})
|
890
|
+
unless parent.respond_to?(:uri)
|
891
|
+
raise "You can't call a method that's not attached to a resource! (You may have dereferenced a method when you shouldn't have)"
|
892
|
+
end
|
893
|
+
|
894
|
+
resource ||= parent
|
895
|
+
method = dereference
|
896
|
+
|
897
|
+
uri = method.request ? method.request.uri(resource, args) : resource.uri(args)
|
898
|
+
headers = uri.headers.dup
|
899
|
+
|
900
|
+
headers['Accept'] = expect_representation.mediaType if args[:expect_representation]
|
901
|
+
headers['User-Agent'] = 'Ruby WADL client' unless headers['User-Agent']
|
902
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
903
|
+
headers[:method] = name.downcase.to_sym
|
904
|
+
headers[:body] = args[:send_representation]
|
905
|
+
|
906
|
+
set_oauth_header(headers, uri)
|
907
|
+
|
908
|
+
response = begin
|
909
|
+
open(uri, headers)
|
910
|
+
rescue OpenURI::HTTPError => err
|
911
|
+
err.io
|
912
|
+
end
|
913
|
+
|
914
|
+
method.response.build(response)
|
915
|
+
end
|
916
|
+
|
917
|
+
def set_oauth_header(headers, uri)
|
918
|
+
args = headers[OAUTH_HEADER] or return
|
919
|
+
|
920
|
+
yaml = args.dup
|
921
|
+
yaml.sub!(/\A#{OAUTH_PREFIX}/, '') or return
|
922
|
+
|
923
|
+
consumer_key, consumer_secret, access_token, token_secret = YAML.load(yaml)
|
924
|
+
|
925
|
+
require 'oauth/client/helper'
|
926
|
+
|
927
|
+
request = OpenURI::Methods[headers[:method]].new(uri.to_s)
|
928
|
+
|
929
|
+
consumer = OAuth::Consumer.new(consumer_key, consumer_secret)
|
930
|
+
token = OAuth::AccessToken.new(consumer, access_token, token_secret)
|
931
|
+
|
932
|
+
helper = OAuth::Client::Helper.new(request,
|
933
|
+
:request_uri => request.path,
|
934
|
+
:consumer => consumer,
|
935
|
+
:token => token,
|
936
|
+
:scheme => 'header',
|
937
|
+
:signature_method => 'HMAC-SHA1'
|
938
|
+
)
|
939
|
+
|
940
|
+
headers[OAUTH_HEADER] = helper.header
|
941
|
+
end
|
942
|
+
|
943
|
+
end
|
944
|
+
|
945
|
+
# A mixin for objects that contain resources. If you include this, be
|
946
|
+
# sure to alias :find_resource to :find_resource_autogenerated
|
947
|
+
# beforehand.
|
948
|
+
module ResourceContainer
|
949
|
+
|
950
|
+
def resource(name_or_id)
|
951
|
+
name_or_id = name_or_id.to_s
|
952
|
+
find_resource { |r| r.id == name_or_id || r.path == name_or_id }
|
953
|
+
end
|
954
|
+
|
955
|
+
def find_resource_by_path(path, auto_dereference = nil)
|
956
|
+
path = path.to_s
|
957
|
+
find_resource(auto_dereference) { |r| r.path == path }
|
958
|
+
end
|
959
|
+
|
960
|
+
def finalize_creation
|
961
|
+
resources.each { |r|
|
962
|
+
define_singleton(r, :id, :find_resource)
|
963
|
+
define_singleton(r, :path, :find_resource_by_path)
|
964
|
+
} if resources
|
965
|
+
end
|
966
|
+
|
967
|
+
end
|
968
|
+
|
969
|
+
# A type of resource. Basically a mixin of methods and params for actual
|
970
|
+
# resources.
|
971
|
+
class ResourceType < HasDocs
|
972
|
+
|
973
|
+
in_document 'resource_type'
|
974
|
+
has_attributes :id
|
975
|
+
has_many HTTPMethod, Param
|
976
|
+
|
977
|
+
end
|
978
|
+
|
979
|
+
class Resource < HasDocs
|
980
|
+
|
981
|
+
include ResourceContainer
|
982
|
+
|
983
|
+
in_document 'resource'
|
984
|
+
has_attributes :id, :path
|
985
|
+
has_many Resource, HTTPMethod, Param, ResourceType
|
986
|
+
may_be_reference # not conforming to spec (20090831), but tests make use of it
|
987
|
+
|
988
|
+
def initialize(*args)
|
989
|
+
super
|
990
|
+
end
|
991
|
+
|
992
|
+
def dereference_with_context(child)
|
993
|
+
ResourceAndAddress.new(child, parent.address)
|
994
|
+
end
|
995
|
+
|
996
|
+
# Returns a ResourceAndAddress object bound to this resource
|
997
|
+
# and the given query variables.
|
998
|
+
def bind(args = {})
|
999
|
+
ResourceAndAddress.new(self).bind!(args)
|
1000
|
+
end
|
1001
|
+
|
1002
|
+
# Sets basic auth parameters
|
1003
|
+
def with_basic_auth(user, pass, param_name = 'Authorization')
|
1004
|
+
bind(:headers => { param_name => "Basic #{["#{user}:#{pass}"].pack('m')}" })
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
# Sets OAuth parameters
|
1008
|
+
#
|
1009
|
+
# Args:
|
1010
|
+
# :consumer_key
|
1011
|
+
# :consumer_secret
|
1012
|
+
# :access_token
|
1013
|
+
# :token_secret
|
1014
|
+
def with_oauth(*args)
|
1015
|
+
bind(:headers => { OAUTH_HEADER => "#{OAUTH_PREFIX}#{args.to_yaml}" })
|
1016
|
+
end
|
1017
|
+
|
1018
|
+
def uri(args = {}, working_address = nil)
|
1019
|
+
address(working_address && working_address.deep_copy).uri(args)
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
# Returns an Address object refering to this resource
|
1023
|
+
def address(working_address = nil)
|
1024
|
+
working_address &&= working_address.deep_copy
|
1025
|
+
working_address ||= if parent.respond_to?(:base)
|
1026
|
+
address = Address.new
|
1027
|
+
address.path_fragments << parent.base
|
1028
|
+
address
|
1029
|
+
else
|
1030
|
+
parent.address.deep_copy
|
1031
|
+
end
|
1032
|
+
|
1033
|
+
working_address.path_fragments << path.dup
|
1034
|
+
|
1035
|
+
# Install path, query, and header parameters in the Address. These
|
1036
|
+
# may override existing parameters with the same names, but if
|
1037
|
+
# you've got a WADL application that works that way, you should
|
1038
|
+
# have bound parameters to values earlier.
|
1039
|
+
new_path_fragments = []
|
1040
|
+
embedded_param_names = Set.new(Address.embedded_param_names(path))
|
1041
|
+
|
1042
|
+
params.each { |param|
|
1043
|
+
name = param.name
|
1044
|
+
|
1045
|
+
if embedded_param_names.include?(name)
|
1046
|
+
working_address.path_params[name] = param
|
1047
|
+
else
|
1048
|
+
if param.style == 'query'
|
1049
|
+
working_address.query_params[name] = param
|
1050
|
+
elsif param.style == 'header'
|
1051
|
+
working_address.header_params[name] = param
|
1052
|
+
else
|
1053
|
+
new_path_fragments << param
|
1054
|
+
working_address.path_params[name] = param
|
1055
|
+
end
|
1056
|
+
end
|
1057
|
+
}
|
1058
|
+
|
1059
|
+
working_address.path_fragments << new_path_fragments unless new_path_fragments.empty?
|
1060
|
+
|
1061
|
+
working_address
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
def representation_for(http_method, request = true, all = false)
|
1065
|
+
method = find_method_by_http_method(http_method)
|
1066
|
+
representations = (request ? method.request : method.response).representations
|
1067
|
+
|
1068
|
+
all ? representations : representations[0]
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
def find_by_id(id)
|
1072
|
+
id = id.to_s
|
1073
|
+
resources.find { |r| r.dereference.id == id }
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
# Find HTTP methods in this resource and in the mixed-in types
|
1077
|
+
def each_http_method
|
1078
|
+
[self, *resource_types].each { |t| t.http_methods.each { |m| yield m } }
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
def find_method_by_id(id)
|
1082
|
+
id = id.to_s
|
1083
|
+
each_http_method { |m| return m if m.dereference.id == id }
|
1084
|
+
end
|
1085
|
+
|
1086
|
+
def find_method_by_http_method(action)
|
1087
|
+
action = action.to_s.downcase
|
1088
|
+
each_http_method { |m| return m if m.dereference.name.downcase == action }
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
# Methods for reading or writing this resource
|
1092
|
+
%w[get post put delete].each { |method|
|
1093
|
+
class_eval <<-EOT, __FILE__, __LINE__ + 1
|
1094
|
+
def #{method}(*args, &block)
|
1095
|
+
find_method_by_http_method(:#{method}).call(self, *args, &block)
|
1096
|
+
end
|
1097
|
+
EOT
|
1098
|
+
}
|
1099
|
+
|
1100
|
+
end
|
1101
|
+
|
1102
|
+
# A resource bound beneath a certain address. Used to keep track of a
|
1103
|
+
# path through a twisting resource hierarchy that includes references.
|
1104
|
+
class ResourceAndAddress < DelegateClass(Resource)
|
1105
|
+
|
1106
|
+
def initialize(resource, address = nil, combine_address_with_resource = true)
|
1107
|
+
@resource = resource
|
1108
|
+
@address = combine_address_with_resource ? resource.address(address) : address
|
1109
|
+
|
1110
|
+
super(resource)
|
1111
|
+
end
|
1112
|
+
|
1113
|
+
# The id method is not delegated, because it's the name of a
|
1114
|
+
# (deprecated) built-in Ruby method. We wnat to delegate it.
|
1115
|
+
def id
|
1116
|
+
@resource.id
|
1117
|
+
end
|
1118
|
+
|
1119
|
+
def to_s
|
1120
|
+
inspect
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
def inspect
|
1124
|
+
"ResourceAndAddress\n Resource: #{@resource}\n #{@address.inspect}"
|
1125
|
+
end
|
1126
|
+
|
1127
|
+
def address
|
1128
|
+
@address
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
def bind(*args)
|
1132
|
+
ResourceAndAddress.new(@resource, @address.deep_copy, false).bind!(*args)
|
1133
|
+
end
|
1134
|
+
|
1135
|
+
def bind!(args = {})
|
1136
|
+
@address.bind!(args)
|
1137
|
+
self
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
def uri(args = {})
|
1141
|
+
@address.deep_copy.bind!(args).uri
|
1142
|
+
end
|
1143
|
+
|
1144
|
+
# method_missing is to catch generated methods that don't get delegated.
|
1145
|
+
def method_missing(name, *args, &block)
|
1146
|
+
if @resource.respond_to?(name)
|
1147
|
+
result = @resource.send(name, *args, &block)
|
1148
|
+
result.is_a?(Resource) ? ResourceAndAddress.new(result, @address.dup) : result
|
1149
|
+
else
|
1150
|
+
super
|
1151
|
+
end
|
1152
|
+
end
|
1153
|
+
|
1154
|
+
# method_missing won't catch these guys because they were defined in
|
1155
|
+
# the delegation operation.
|
1156
|
+
def resource(*args, &block)
|
1157
|
+
resource = @resource.resource(*args, &block)
|
1158
|
+
resource && ResourceAndAddress.new(resource, @address)
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
def find_resource(*args, &block)
|
1162
|
+
resource = @resource.find_resource(*args, &block)
|
1163
|
+
resource && ResourceAndAddress.new(resource, @address)
|
1164
|
+
end
|
1165
|
+
|
1166
|
+
def find_resource_by_path(*args, &block)
|
1167
|
+
resource = @resource.find_resource_by_path(*args, &block)
|
1168
|
+
resource && ResourceAndAddress.new(resource, @address)
|
1169
|
+
end
|
1170
|
+
|
1171
|
+
end
|
1172
|
+
|
1173
|
+
class Resources < HasDocs
|
1174
|
+
|
1175
|
+
include ResourceContainer
|
1176
|
+
|
1177
|
+
in_document 'resources'
|
1178
|
+
as_member 'resource_list'
|
1179
|
+
has_attributes :base
|
1180
|
+
has_many Resource
|
1181
|
+
|
1182
|
+
end
|
1183
|
+
|
1184
|
+
class Application < HasDocs
|
1185
|
+
|
1186
|
+
in_document 'application'
|
1187
|
+
has_one Resources
|
1188
|
+
has_many HTTPMethod, RepresentationFormat, FaultFormat
|
1189
|
+
|
1190
|
+
def self.from_wadl(wadl)
|
1191
|
+
wadl = wadl.read if wadl.respond_to?(:read)
|
1192
|
+
doc = REXML::Document.new(wadl)
|
1193
|
+
|
1194
|
+
application = from_element(nil, doc.root, need_finalization = [])
|
1195
|
+
need_finalization.each { |x| x.finalize_creation }
|
1196
|
+
|
1197
|
+
application
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
def find_resource(symbol, *args, &block)
|
1201
|
+
resource_list.find_resource(symbol, *args, &block)
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
def resource(symbol)
|
1205
|
+
resource_list.resource(symbol)
|
1206
|
+
end
|
1207
|
+
|
1208
|
+
def find_resource_by_path(symbol, *args, &block)
|
1209
|
+
resource_list.find_resource_by_path(symbol, *args, &block)
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
def finalize_creation
|
1213
|
+
resource_list.resources.each { |r|
|
1214
|
+
define_singleton(r, :id, 'resource_list.find_resource')
|
1215
|
+
define_singleton(r, :path, 'resource_list.find_resource_by_path')
|
1216
|
+
} if resource_list
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
end
|
1220
|
+
|
1221
|
+
# A module mixed in to REXML documents to make them representations in the
|
1222
|
+
# WADL sense.
|
1223
|
+
module XMLRepresentation
|
1224
|
+
|
1225
|
+
def representation_of(format)
|
1226
|
+
@params = format.params
|
1227
|
+
end
|
1228
|
+
|
1229
|
+
def lookup_param(name)
|
1230
|
+
param = @params.find { |p| p.name == name }
|
1231
|
+
|
1232
|
+
raise ArgumentError, "No such param #{name}" unless param
|
1233
|
+
raise ArgumentError, "Param #{name} has no path!" unless param.path
|
1234
|
+
|
1235
|
+
param
|
1236
|
+
end
|
1237
|
+
|
1238
|
+
# Yields up each XML element for the given Param object.
|
1239
|
+
def each_by_param(param_name)
|
1240
|
+
REXML::XPath.each(self, lookup_param(param_name).path) { |e| yield e }
|
1241
|
+
end
|
1242
|
+
|
1243
|
+
# Returns an XML element for the given Param object.
|
1244
|
+
def get_by_param(param_name)
|
1245
|
+
REXML::XPath.first(self, lookup_param(param_name).path)
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
end
|
1249
|
+
|
1250
|
+
class Response < Struct.new(:code, :headers, :representation, :format)
|
1251
|
+
end
|
1252
|
+
|
1253
|
+
class Fault < Exception
|
1254
|
+
|
1255
|
+
attr_accessor :code, :headers, :representation, :format
|
1256
|
+
|
1257
|
+
def initialize(code, headers, representation, format)
|
1258
|
+
@code, @headers, @representation, @format = code, headers, representation, format
|
1259
|
+
end
|
1260
|
+
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
end
|