wadl 0.1.2 → 0.1.3.1

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