wrest 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/README.rdoc +31 -17
  2. data/Rakefile +147 -134
  3. data/VERSION.yml +1 -1
  4. data/bin/jwrest +3 -0
  5. data/bin/wrest +1 -20
  6. data/bin/wrest_shell.rb +21 -0
  7. data/lib/wrest/components.rb +4 -1
  8. data/lib/wrest/components/attributes_container.rb +65 -10
  9. data/lib/wrest/{translators/xml.rb → components/mutators.rb} +11 -13
  10. data/lib/wrest/components/mutators/base.rb +43 -0
  11. data/lib/wrest/components/mutators/camel_to_snake_case.rb +20 -0
  12. data/lib/wrest/components/mutators/xml_simple_type_caster.rb +35 -0
  13. data/lib/wrest/{translators.rb → components/translators.rb} +6 -7
  14. data/lib/wrest/{translators → components/translators}/content_types.rb +5 -5
  15. data/lib/wrest/components/translators/json.rb +22 -0
  16. data/lib/wrest/components/translators/xml.rb +26 -0
  17. data/lib/wrest/components/typecast_helpers.rb +41 -0
  18. data/lib/wrest/core_ext/hash.rb +5 -0
  19. data/lib/wrest/core_ext/hash/conversions.rb +44 -0
  20. data/lib/wrest/exceptions.rb +6 -1
  21. data/lib/wrest/exceptions/method_not_overridden_exception.rb +17 -0
  22. data/lib/wrest/exceptions/unsupported_content_type_exception.rb +11 -9
  23. data/lib/wrest/resource/base.rb +5 -1
  24. data/lib/wrest/response.rb +2 -2
  25. data/lib/wrest/uri.rb +3 -3
  26. data/lib/wrest/version.rb +1 -1
  27. data/spec/spec_helper.rb +2 -1
  28. data/spec/wrest/components/attributes_container_spec.rb +66 -11
  29. data/spec/wrest/components/mutators/base_spec.rb +38 -0
  30. data/spec/wrest/components/mutators/camel_to_snake_spec.rb +22 -0
  31. data/spec/wrest/components/mutators/xml_simple_type_caster_spec.rb +47 -0
  32. data/spec/wrest/{translators → components/translators}/xml_spec.rb +3 -3
  33. data/spec/wrest/components/translators_spec.rb +9 -0
  34. data/spec/wrest/core_ext/hash/conversions_spec.rb +22 -0
  35. data/{lib/wrest/translators/json.rb → spec/wrest/core_ext/string/conversions_spec.rb} +4 -9
  36. data/spec/wrest/resource/base_spec.rb +1 -1
  37. data/spec/wrest/response_spec.rb +3 -3
  38. data/spec/wrest/uri_spec.rb +2 -2
  39. metadata +34 -19
  40. data/lib/wrest/translators/typed_hash.rb +0 -4
  41. data/spec/wrest/core_ext/string_spec.rb +0 -7
  42. data/spec/wrest/translators/typed_hash_spec.rb +0 -9
  43. data/spec/wrest/translators_spec.rb +0 -9
@@ -0,0 +1,21 @@
1
+ puts "Ruby #{RUBY_VERSION}, #{RUBY_RELEASE_DATE}, #{RUBY_PLATFORM}"
2
+
3
+ entry_point = "#{File.dirname(__FILE__)}/../lib/wrest.rb"
4
+ version = "#{File.dirname(__FILE__)}/../lib/wrest/version"
5
+
6
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
7
+
8
+ require 'optparse'
9
+ options = { :irb => irb }
10
+ OptionParser.new do |opt|
11
+ opt.banner = "Usage: console [options]"
12
+ opt.on("--irb=[#{irb}]", 'Invoke a different irb.') { |v| options[:irb] = v }
13
+ opt.parse!(ARGV)
14
+ end
15
+
16
+ libs = " -r irb/completion"
17
+ libs << " -r #{entry_point}"
18
+
19
+ require version
20
+ puts "Loading Wrest #{Wrest::VERSION::STRING}"
21
+ exec "#{options[:irb]} #{libs} --simple-prompt"
@@ -15,4 +15,7 @@ module Wrest #:nodoc:
15
15
  end
16
16
  end
17
17
 
18
- require "#{WREST_ROOT}/wrest/components/attributes_container"
18
+ require "#{WREST_ROOT}/wrest/components/typecast_helpers"
19
+ require "#{WREST_ROOT}/wrest/components/attributes_container"
20
+ require "#{WREST_ROOT}/wrest/components/mutators"
21
+ require "#{WREST_ROOT}/wrest/components/translators"
@@ -17,26 +17,39 @@ module Wrest::Components #:nodoc:
17
17
  # <tt>respond_to?</tt> however will respond as though
18
18
  # they are all already present.
19
19
  # This means that two different instances of the same
20
- # <tt>AttributesContainer</tt> could well have
20
+ # AttributesContainer could well have
21
21
  # different attribute getters/setters/query methods.
22
- #
23
- # Note that this means the first call to a particular
22
+ #
23
+ # Note that the first call to a particular getter/setter/query
24
24
  # method will be slower because the method is defined
25
25
  # at that point; subsequent calls will be much faster.
26
26
  #
27
- # If you're implementing your own initialize method
28
- # remember to delegate to the default initialize
29
- # of AttributesContainer by invoking <tt>super(attributes)</tt>
30
27
  # Also keep in mind that attribute getter/setter/query methods
31
28
  # will _not_ override any existing methods on the class.
32
29
  #
33
30
  # In situations where this is a problem, such as a client consuming Rails
34
31
  # REST services where <tt>id</tt> is a common attribute and clashes with
35
- # Object#id it is recommended to create getter/setter/query methods
36
- # on the class (which affects all instances) using the <tt>has_attributes</tt> macro.
32
+ # Object#id, it is recommended to create getter/setter/query methods
33
+ # on the class (which affects all instances) using the +has_attributes+ macro.
34
+ #
35
+ # If you're implementing your own initialize method
36
+ # remember to delegate to the default initialize
37
+ # of AttributesContainer by invoking <tt>super(attributes)</tt>
38
+ #
39
+ # Example:
40
+ # class ShenCoin
41
+ # include Wrest::Components::AttributesContainer
42
+ #
43
+ # has_attributes :id
44
+ # typecast :id => as_integer
45
+ # end
46
+ # coin = ShenCoin.new(:id => '5', :chi_count => 500, :owner => 'Kai Wren')
47
+ # coin.id # => 5
48
+ # coin.owner # => 'Kai Wren'
37
49
  module AttributesContainer
38
50
  def self.included(klass) #:nodoc:
39
51
  klass.extend AttributesContainer::ClassMethods
52
+ klass.extend TypecastHelpers
40
53
  klass.class_eval{ include AttributesContainer::InstanceMethods }
41
54
  end
42
55
 
@@ -59,8 +72,7 @@ module Wrest::Components #:nodoc:
59
72
  # an example would be Rails REST services which frequently make use
60
73
  # an attribute named <tt>id</tt> which clashes with Object#id. Also,
61
74
  # this can be used as a performance optimisation if the incoming
62
- # attributes are known beforehand, as defining methods on the first
63
- # invocation is no longer necessary.
75
+ # attributes are known beforehand.
64
76
  def has_attributes(*attribute_names)
65
77
  attribute_names.each do |attribute_name|
66
78
  self.class_eval(
@@ -70,6 +82,45 @@ module Wrest::Components #:nodoc:
70
82
  )
71
83
  end
72
84
  end
85
+
86
+ # Accepts a set of attribute-name/lambda pairs which are used
87
+ # to typecast string values injected through the constructor.
88
+ # Typically needed when populating an +AttributesContainer+
89
+ # directly from request params. Typecasting kicks in for
90
+ # a given value _only_ if it is a string.
91
+ #
92
+ # Typcast information is inherited by subclasses; however be
93
+ # aware that explicitly invoking +typecast+ in a subclass will
94
+ # discard inherited typecast information leaving only the casts
95
+ # defined in the subclass.
96
+ #
97
+ # Common typecasts such as integer, float, datetime etc. are
98
+ # available through predefined helpers. See TypecastHelpers
99
+ # for a full list.
100
+ #
101
+ # Example:
102
+ #
103
+ # class Demon
104
+ # include Wrest::Components::AttributesContainer
105
+ # typecast :age => as_integer,
106
+ # :chi => lambda{|chi| Chi.new(chi)}
107
+ # end
108
+ # kai_wren = Demon.new('age' => '1500', 'chi' => '1024')
109
+ # kai_wren.age # => 1500
110
+ # kai_wren.chi # => #<Chi:0x113af8c @count="1024">
111
+ def typecast(cast_map)
112
+ @typecast_map = @typecast_map ? @typecast_map.merge(cast_map.symbolize_keys) : cast_map.symbolize_keys
113
+ end
114
+
115
+ def typecast_map #:nodoc:
116
+ if defined?(@typecast_map)
117
+ @typecast_map
118
+ elsif superclass != Object && superclass.respond_to?(:typecast_map)
119
+ superclass.typecast_map
120
+ else
121
+ {}
122
+ end
123
+ end
73
124
  end
74
125
 
75
126
  module InstanceMethods
@@ -81,6 +132,10 @@ module Wrest::Components #:nodoc:
81
132
  # own class.
82
133
  def initialize(attributes = {})
83
134
  @attributes = attributes.symbolize_keys
135
+ self.class.typecast_map.each do |key, typecaster|
136
+ value = @attributes[key]
137
+ @attributes[key] = typecaster.call(value) if value.is_a?(String)
138
+ end
84
139
  @interface = Module.new
85
140
  self.extend @interface
86
141
  end
@@ -7,18 +7,16 @@
7
7
  # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
8
  # See the License for the specific language governing permissions and limitations under the License.
9
9
 
10
- require 'xmlsimple'
11
-
12
- module Wrest
13
- module Translators
14
- # Knows how to deserialise xml.
15
- # Depends on the xmlsimple gem.
16
- Xml = lambda{|response|
17
- XmlSimple.xml_in(
18
- response.body,
19
- 'keeproot' => true
20
- )
21
- }
10
+ module Wrest #:nodoc:
11
+ module Components
12
+ # A mutator understands how to transform
13
+ # one tuple(key/value pair) from a hash
14
+ # into another
15
+ module Mutators
16
+ end
22
17
  end
23
18
  end
24
-
19
+
20
+ require "#{WREST_ROOT}/wrest/components/mutators/base"
21
+ require "#{WREST_ROOT}/wrest/components/mutators/xml_simple_type_caster"
22
+ require "#{WREST_ROOT}/wrest/components/mutators/camel_to_snake_case"
@@ -0,0 +1,43 @@
1
+ # Copyright 2009 Sidu Ponnappa
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License
7
+ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+
10
+ # This is a base implementation of a
11
+ # hash mutator that ensures that the <tt>mutate</tt> method
12
+ # will chain to the next mutator by using a
13
+ # template method.
14
+ class Wrest::Components::Mutators::Base
15
+ attr_reader :next_mutator
16
+
17
+ def initialize(next_mutator = nil)
18
+ @next_mutator = next_mutator
19
+ end
20
+
21
+ # This is a template method which operates on a tuple (well, pair)
22
+ # from a hash map and guarantees mutator chaining.
23
+ #
24
+ # Iterating over any hash using <tt>each</tt> injects
25
+ # each key/value pair from the hash in the
26
+ # form of an array.
27
+ # This method expects of this form as an argument, i.e.
28
+ # an array with the structure [:key, :value]
29
+ #
30
+ # The implementation of the mutation is achieved by
31
+ # overriding the <tt>do_mutate</tt> method in a subclass.
32
+ # Note that failing to do so will result in an exception
33
+ # at runtime.
34
+ def mutate(tuple)
35
+ out_tuple = do_mutate(tuple)
36
+ next_mutator ? next_mutator.mutate(out_tuple) : out_tuple
37
+ end
38
+
39
+ protected
40
+ def do_mutate(tuple)
41
+ raise Wrest::Exceptions::MethodNotOverriddenException
42
+ end
43
+ end
@@ -0,0 +1,20 @@
1
+ # Copyright 2009 Sidu Ponnappa
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License
7
+ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+
10
+ module Wrest::Components
11
+ class Mutators::CamelToSnakeCase < Mutators::Base
12
+ # Converts the key to snake case
13
+ #
14
+ # Example:
15
+ # Mutators::CamelToSnakeCase.new.mutate(['Spirit-Sword', 'true']) # => ['spirit_sword', 'true']
16
+ def do_mutate(tuple)
17
+ [tuple.first.underscore, tuple.last]
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # Copyright 2009 Sidu Ponnappa
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License
7
+ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+
10
+ # This mutator undertands how do type casting
11
+ # using the type data embedded in a hash
12
+ # created by deserialising an xml using
13
+ # xml-simple
14
+ module Wrest::Components
15
+ class Mutators::XmlSimpleTypeCaster < Mutators::Base
16
+ def do_mutate(tuple)
17
+ out_key = tuple.first
18
+ in_value = tuple.last[0]
19
+ out_value = in_value
20
+
21
+ case in_value
22
+ when Hash
23
+ if in_value['nil'] == 'true'
24
+ out_value = nil
25
+ elsif in_value.key?('type')
26
+ out_value = ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING[in_value['type']].call(in_value['content'])
27
+ else
28
+ out_value = in_value.mutate_using(self)
29
+ end
30
+ end
31
+
32
+ [out_key, out_value]
33
+ end
34
+ end
35
+ end
@@ -7,20 +7,19 @@
7
7
  # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
8
  # See the License for the specific language governing permissions and limitations under the License.
9
9
 
10
- module Wrest
10
+ module Wrest::Components #:nodoc:
11
11
  # Contains strategies/lambdas which know how to deserialise
12
12
  # different content types.
13
13
  module Translators
14
14
  # Loads the appropriate desirialisation strategy based on
15
15
  # the content type
16
- def self.load(content_type)
16
+ def self.lookup(content_type)
17
17
  translator = CONTENT_TYPES[content_type]
18
- translator || (raise UnsupportedContentTypeException.new("Unsupported content type #{content_type}"))
18
+ translator || (raise Wrest::Exceptions::UnsupportedContentTypeException.new("Unsupported content type #{content_type}"))
19
19
  end
20
20
  end
21
21
  end
22
22
 
23
- require "#{WREST_ROOT}/wrest/translators/xml"
24
- require "#{WREST_ROOT}/wrest/translators/json"
25
- require "#{WREST_ROOT}/wrest/translators/content_types"
26
- require "#{WREST_ROOT}/wrest/translators/typed_hash"
23
+ require "#{WREST_ROOT}/wrest/components/translators/xml"
24
+ require "#{WREST_ROOT}/wrest/components/translators/json"
25
+ require "#{WREST_ROOT}/wrest/components/translators/content_types"
@@ -7,14 +7,14 @@
7
7
  # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
8
  # See the License for the specific language governing permissions and limitations under the License.
9
9
 
10
- module Wrest
10
+ module Wrest::Components
11
11
  module Translators
12
12
  # Maps content types to deserialisers
13
13
  CONTENT_TYPES = {
14
- 'application/xml' => Wrest::Translators::Xml,
15
- 'text/xml' => Wrest::Translators::Xml,
16
- 'application/json' => Wrest::Translators::Json,
17
- 'text/javascript' => Wrest::Translators::Json
14
+ 'application/xml' => Wrest::Components::Translators::Xml,
15
+ 'text/xml' => Wrest::Components::Translators::Xml,
16
+ 'application/json' => Wrest::Components::Translators::Json,
17
+ 'text/javascript' => Wrest::Components::Translators::Json
18
18
  }
19
19
  end
20
20
  end
@@ -0,0 +1,22 @@
1
+ # Copyright 2009 Sidu Ponnappa
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License
7
+ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+
10
+ module Wrest::Components::Translators
11
+ module Json
12
+ extend self
13
+
14
+ def deserialise(response)
15
+ ActiveSupport::JSON.decode(response.body)
16
+ end
17
+
18
+ def serialise(hash)
19
+ ActiveSupport::JSON.encode(hash)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # Copyright 2009 Sidu Ponnappa
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License
7
+ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+
10
+ require 'xmlsimple'
11
+
12
+ module Wrest::Components::Translators
13
+ module Xml
14
+ extend self
15
+
16
+ def deserialise(response)
17
+ XmlSimple.xml_in(
18
+ response.body,
19
+ 'keeproot' => true
20
+ )
21
+ end
22
+
23
+ def serialise(hash)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ module Wrest::Components #:nodoc:
2
+ # Provides helper methods which build lambdas
3
+ # to cast strings to specific types.
4
+ module TypecastHelpers
5
+ def as_base64Binary
6
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['base64Binary']
7
+ end
8
+
9
+ def as_boolean
10
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['boolean']
11
+ end
12
+
13
+ def as_decimal
14
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['decimal']
15
+ end
16
+
17
+ def as_date
18
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['date']
19
+ end
20
+
21
+ def as_datetime
22
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['datetime']
23
+ end
24
+
25
+ def as_float
26
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['float']
27
+ end
28
+
29
+ def as_integer
30
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['integer']
31
+ end
32
+
33
+ def as_symbol
34
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['symbol']
35
+ end
36
+
37
+ def as_yaml
38
+ ActiveSupport::CoreExtensions::Hash::Conversions::XML_PARSING['yaml']
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ require "#{WREST_ROOT}/wrest/core_ext/hash/conversions"
2
+
3
+ class Hash #:nodoc:
4
+ include Wrest::CoreExt::Hash::Conversions
5
+ end
@@ -0,0 +1,44 @@
1
+ # Copyright 2009 Sidu Ponnappa
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ # Unless required by applicable law or agreed to in writing, software distributed under the License
7
+ # is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8
+ # See the License for the specific language governing permissions and limitations under the License.
9
+
10
+ module Wrest
11
+ module CoreExt #:nodoc:
12
+ module Hash #:nodoc:
13
+ # Makes it easier to build other objects from a Hash
14
+ module Conversions
15
+
16
+ # This method accepts a hash mutator (found in Wrest::Compononents)
17
+ # to build a new hash map by making changes to an existing one.
18
+ #
19
+ # No, this method does not mutate the state of the hash it is invoked on,
20
+ # but rather builds a new one.
21
+ #
22
+ # Yes, the name is misleading in that respect. However, one
23
+ # hopes the absence of an exclamation mark will increase clarity.
24
+ #
25
+ # Uses include mutating the hash produced by xml-simple
26
+ # by using the meta data in the hash to type cast values.
27
+ #
28
+ # Example:
29
+ # "http://search.yahooapis.com/NewsSearchService/V1/newsSearch".to_uri.get(
30
+ # :appid => 'YahooDemo',
31
+ # :output => 'xml',
32
+ # :query => 'India',
33
+ # :results=> '3',
34
+ # :start => '1'
35
+ # ).deserialise.mutate_using(XmlSimpleTypeCaster.new)
36
+ def mutate_using(mutator)
37
+ returning({})do |mutated_hash|
38
+ self.each{|tuple| mutated_hash.store(*mutator.mutate(tuple))}
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end