wrest 0.0.5 → 0.0.6

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