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