wadl 0.1.2 → 0.1.3.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/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