simplemapper 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. data/LICENSE +16 -0
  2. data/README +0 -0
  3. data/Rakefile +78 -0
  4. data/doc/classes/Array.html +139 -0
  5. data/doc/classes/Array.src/M000001.html +34 -0
  6. data/doc/classes/Callbacks.html +161 -0
  7. data/doc/classes/Callbacks.src/M000037.html +18 -0
  8. data/doc/classes/Callbacks.src/M000038.html +18 -0
  9. data/doc/classes/Callbacks.src/M000039.html +19 -0
  10. data/doc/classes/Enumerable.html +131 -0
  11. data/doc/classes/Enumerable.src/M000040.html +21 -0
  12. data/doc/classes/Hash.html +154 -0
  13. data/doc/classes/Hash.src/M000002.html +18 -0
  14. data/doc/classes/Hash.src/M000003.html +34 -0
  15. data/doc/classes/Inflector/Inflections.html +323 -0
  16. data/doc/classes/Inflector/Inflections.src/M000031.html +18 -0
  17. data/doc/classes/Inflector/Inflections.src/M000032.html +18 -0
  18. data/doc/classes/Inflector/Inflections.src/M000033.html +18 -0
  19. data/doc/classes/Inflector/Inflections.src/M000034.html +19 -0
  20. data/doc/classes/Inflector/Inflections.src/M000035.html +18 -0
  21. data/doc/classes/Inflector/Inflections.src/M000036.html +23 -0
  22. data/doc/classes/Inflector.html +516 -0
  23. data/doc/classes/Inflector.src/M000017.html +22 -0
  24. data/doc/classes/Inflector.src/M000018.html +25 -0
  25. data/doc/classes/Inflector.src/M000019.html +25 -0
  26. data/doc/classes/Inflector.src/M000020.html +22 -0
  27. data/doc/classes/Inflector.src/M000021.html +18 -0
  28. data/doc/classes/Inflector.src/M000022.html +22 -0
  29. data/doc/classes/Inflector.src/M000023.html +18 -0
  30. data/doc/classes/Inflector.src/M000024.html +18 -0
  31. data/doc/classes/Inflector.src/M000025.html +18 -0
  32. data/doc/classes/Inflector.src/M000026.html +18 -0
  33. data/doc/classes/Inflector.src/M000027.html +19 -0
  34. data/doc/classes/Inflector.src/M000028.html +18 -0
  35. data/doc/classes/Inflector.src/M000029.html +22 -0
  36. data/doc/classes/Inflector.src/M000030.html +27 -0
  37. data/doc/classes/Merb/Request.html +139 -0
  38. data/doc/classes/Merb/Request.src/M000041.html +22 -0
  39. data/doc/classes/Merb.html +111 -0
  40. data/doc/classes/OAuth/RequestProxy/Base.html +139 -0
  41. data/doc/classes/OAuth/RequestProxy/Base.src/M000012.html +18 -0
  42. data/doc/classes/OAuth/RequestProxy.html +111 -0
  43. data/doc/classes/OAuth/Signature/Base.html +139 -0
  44. data/doc/classes/OAuth/Signature/Base.src/M000011.html +25 -0
  45. data/doc/classes/OAuth/Signature.html +111 -0
  46. data/doc/classes/OAuth.html +112 -0
  47. data/doc/classes/OAuthController.html +243 -0
  48. data/doc/classes/OAuthController.src/M000005.html +22 -0
  49. data/doc/classes/OAuthController.src/M000006.html +18 -0
  50. data/doc/classes/OAuthController.src/M000007.html +18 -0
  51. data/doc/classes/OAuthController.src/M000008.html +25 -0
  52. data/doc/classes/OAuthController.src/M000009.html +19 -0
  53. data/doc/classes/Object.html +142 -0
  54. data/doc/classes/Object.src/M000010.html +19 -0
  55. data/doc/classes/Proc.html +143 -0
  56. data/doc/classes/Proc.src/M000004.html +18 -0
  57. data/doc/classes/Serialize.html +189 -0
  58. data/doc/classes/Serialize.src/M000013.html +21 -0
  59. data/doc/classes/Serialize.src/M000014.html +39 -0
  60. data/doc/classes/Serialize.src/M000015.html +16 -0
  61. data/doc/classes/Serialize.src/M000016.html +46 -0
  62. data/doc/classes/SimpleMapper/Base.html +528 -0
  63. data/doc/classes/SimpleMapper/Base.src/M000064.html +16 -0
  64. data/doc/classes/SimpleMapper/Base.src/M000065.html +16 -0
  65. data/doc/classes/SimpleMapper/Base.src/M000066.html +22 -0
  66. data/doc/classes/SimpleMapper/Base.src/M000067.html +21 -0
  67. data/doc/classes/SimpleMapper/Base.src/M000068.html +26 -0
  68. data/doc/classes/SimpleMapper/Base.src/M000069.html +18 -0
  69. data/doc/classes/SimpleMapper/Base.src/M000070.html +18 -0
  70. data/doc/classes/SimpleMapper/Base.src/M000071.html +18 -0
  71. data/doc/classes/SimpleMapper/Base.src/M000072.html +19 -0
  72. data/doc/classes/SimpleMapper/Base.src/M000073.html +23 -0
  73. data/doc/classes/SimpleMapper/Base.src/M000074.html +18 -0
  74. data/doc/classes/SimpleMapper/Base.src/M000075.html +20 -0
  75. data/doc/classes/SimpleMapper/Base.src/M000076.html +18 -0
  76. data/doc/classes/SimpleMapper/Base.src/M000077.html +18 -0
  77. data/doc/classes/SimpleMapper/Base.src/M000078.html +19 -0
  78. data/doc/classes/SimpleMapper/Base.src/M000079.html +18 -0
  79. data/doc/classes/SimpleMapper/Base.src/M000080.html +18 -0
  80. data/doc/classes/SimpleMapper/Base.src/M000081.html +19 -0
  81. data/doc/classes/SimpleMapper/Base.src/M000082.html +20 -0
  82. data/doc/classes/SimpleMapper/Base.src/M000083.html +24 -0
  83. data/doc/classes/SimpleMapper/Base.src/M000084.html +18 -0
  84. data/doc/classes/SimpleMapper/HttpAdapter.html +280 -0
  85. data/doc/classes/SimpleMapper/HttpAdapter.src/M000056.html +18 -0
  86. data/doc/classes/SimpleMapper/HttpAdapter.src/M000057.html +18 -0
  87. data/doc/classes/SimpleMapper/HttpAdapter.src/M000058.html +19 -0
  88. data/doc/classes/SimpleMapper/HttpAdapter.src/M000060.html +18 -0
  89. data/doc/classes/SimpleMapper/HttpAdapter.src/M000061.html +18 -0
  90. data/doc/classes/SimpleMapper/HttpAdapter.src/M000062.html +18 -0
  91. data/doc/classes/SimpleMapper/HttpAdapter.src/M000063.html +18 -0
  92. data/doc/classes/SimpleMapper/HttpOAuthExtension.html +188 -0
  93. data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000048.html +49 -0
  94. data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000049.html +23 -0
  95. data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000050.html +18 -0
  96. data/doc/classes/SimpleMapper/HttpOAuthExtension.src/M000051.html +24 -0
  97. data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.html +146 -0
  98. data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.src/M000046.html +19 -0
  99. data/doc/classes/SimpleMapper/SimpleModel/ClassMethods.src/M000047.html +19 -0
  100. data/doc/classes/SimpleMapper/SimpleModel.html +184 -0
  101. data/doc/classes/SimpleMapper/SimpleModel.src/M000042.html +18 -0
  102. data/doc/classes/SimpleMapper/SimpleModel.src/M000043.html +18 -0
  103. data/doc/classes/SimpleMapper/SimpleModel.src/M000044.html +18 -0
  104. data/doc/classes/SimpleMapper/SimpleModel.src/M000045.html +24 -0
  105. data/doc/classes/SimpleMapper/XmlFormat/ClassMethods.html +152 -0
  106. data/doc/classes/SimpleMapper/XmlFormat/ClassMethods.src/M000055.html +30 -0
  107. data/doc/classes/SimpleMapper/XmlFormat.html +169 -0
  108. data/doc/classes/SimpleMapper/XmlFormat.src/M000052.html +18 -0
  109. data/doc/classes/SimpleMapper/XmlFormat.src/M000053.html +18 -0
  110. data/doc/classes/SimpleMapper/XmlFormat.src/M000054.html +18 -0
  111. data/doc/classes/SimpleMapper.html +146 -0
  112. data/doc/classes/String.html +120 -0
  113. data/doc/created.rid +1 -0
  114. data/doc/files/lib/simple_mapper/adapters/http_adapter_rb.html +110 -0
  115. data/doc/files/lib/simple_mapper/base_rb.html +108 -0
  116. data/doc/files/lib/simple_mapper/default_plugins/callbacks_rb.html +101 -0
  117. data/doc/files/lib/simple_mapper/default_plugins/oauth_rb.html +116 -0
  118. data/doc/files/lib/simple_mapper/default_plugins/simple_model_rb.html +101 -0
  119. data/doc/files/lib/simple_mapper/formats/xml_format_rb.html +108 -0
  120. data/doc/files/lib/simple_mapper/support/bliss_serializer_rb.html +109 -0
  121. data/doc/files/lib/simple_mapper/support/core_ext_rb.html +108 -0
  122. data/doc/files/lib/simple_mapper/support/inflections_rb.html +101 -0
  123. data/doc/files/lib/simple_mapper/support/inflector_rb.html +108 -0
  124. data/doc/files/lib/simple_mapper/support_rb.html +109 -0
  125. data/doc/files/lib/simple_mapper_rb.html +137 -0
  126. data/doc/fr_class_index.html +52 -0
  127. data/doc/fr_file_index.html +38 -0
  128. data/doc/fr_method_index.html +110 -0
  129. data/doc/index.html +24 -0
  130. data/doc/rdoc-style.css +208 -0
  131. data/lib/simple_mapper/adapters/http_adapter.rb +64 -0
  132. data/lib/simple_mapper/base.rb +138 -0
  133. data/lib/simple_mapper/default_plugins/callbacks.rb +12 -0
  134. data/lib/simple_mapper/default_plugins/oauth.rb +167 -0
  135. data/lib/simple_mapper/default_plugins/simple_model.rb +36 -0
  136. data/lib/simple_mapper/formats/xml_format.rb +48 -0
  137. data/lib/simple_mapper/support/bliss_serializer.rb +168 -0
  138. data/lib/simple_mapper/support/core_ext.rb +73 -0
  139. data/lib/simple_mapper/support/inflections.rb +112 -0
  140. data/lib/simple_mapper/support/inflector.rb +275 -0
  141. data/lib/simple_mapper/support.rb +2 -0
  142. data/lib/simple_mapper.rb +16 -0
  143. metadata +236 -0
@@ -0,0 +1,138 @@
1
+ require 'simple_mapper/support'
2
+
3
+ module SimpleMapper
4
+ class Base
5
+ class << self
6
+ attr_reader :format
7
+ def debug?; @debug end
8
+ def debug!; @debug = true end
9
+
10
+ def connection_adapter=(adapter,debug=nil,&block)
11
+ # Should complain if the adapter doesn't exist.
12
+ @connection_adapter = adapter
13
+ require "#{File.dirname(__FILE__)}/adapters/#{@connection_adapter}_adapter"
14
+ @connection_init = block if block_given?
15
+ @connection_debug = debug
16
+ end
17
+ alias :set_connection_adapter :connection_adapter=
18
+
19
+ # set_format :xml
20
+ # self.format = :json
21
+ def format=(format)
22
+ @format_name = format.to_s
23
+ require "#{File.dirname(__FILE__)}/formats/#{@format_name}_format"
24
+ @format = Object.module_eval("::SimpleMapper::#{@format_name.camelize}Format", __FILE__, __LINE__)
25
+ include @format
26
+ end
27
+ alias :set_format :format=
28
+ attr_reader :format_name
29
+
30
+ def connection(refresh=false)
31
+ @connection = begin
32
+ # Initialize the connection with the connection adapter.
33
+ adapter = Object.module_eval("::SimpleMapper::#{@connection_adapter.to_s.camelize}Adapter", __FILE__, __LINE__).new
34
+ @connection_init.in_context(adapter).call if @connection_init.is_a?(Proc)
35
+ adapter.set_headers format.mime_type_headers
36
+ adapter.debug! if @connection_debug
37
+ adapter
38
+ end if !@connection || refresh
39
+ @connection
40
+ end
41
+
42
+ # get
43
+ def get(*args)
44
+ extract_from(connection.get(*args))
45
+ end
46
+
47
+ # new.save
48
+ def create(*args)
49
+ new(*args).save
50
+ end
51
+
52
+ def persistent?
53
+ true
54
+ end
55
+
56
+ def extract_from(formatted_data)
57
+ objs = send(:"from_#{format_name}", formatted_data)
58
+ objs.is_a?(Array) ? objs.collect {|e| e.extended {@persisted = true}} : objs.extended {@persisted = true}
59
+ end
60
+
61
+ def extract_one(formatted_data, identifier=nil)
62
+ objs = extract_from(formatted_data)
63
+ if objs.is_a?(Array)
64
+ identifier.nil? ? objs.first : objs.reject {|e| e.identifier != identifier}
65
+ else
66
+ identifier.nil? ? objs : (objs.identifier == identifier ? objs : nil)
67
+ end
68
+ end
69
+ end
70
+
71
+ def initialize(data=nil)
72
+ self.data = data unless data.nil?
73
+ end
74
+ attr_reader :identifier
75
+
76
+ def original_data=(data)
77
+ @original_data = data.freeze
78
+ @original_attributes = data.keys
79
+ instantiate(@original_data)
80
+ end
81
+ attr_reader :original_data, :original_attributes
82
+
83
+ def data=(data)
84
+ instantiate(data)
85
+ end
86
+ def data
87
+ to_hash
88
+ end
89
+
90
+ # Sets the data into the object. This is provided as a default method, but your model can overwrite it any
91
+ # way you want. For example, you could set the data to some other object type, or to a Marshalled storage.
92
+ # The type of data you receive will depend on the format and parser you use. Of course you could make up
93
+ # your own spin-off of one of those, too.
94
+ def instantiate(data)
95
+ raise TypeError, "data must be a hash" unless data.is_a?(Hash)
96
+ data.each {|k,v| instance_variable_set(:"@#{k}", v)}
97
+ end
98
+
99
+ # Reads the data from the object for saving back to the persisted store. This is provided as a default
100
+ # method, but you can overwrite it in your model.
101
+ def formatted_data
102
+ send(:"to_#{self.class.format_name}")
103
+ end
104
+
105
+ # persisted? ? put : post
106
+ def save
107
+ persisted? ? put : post
108
+ end
109
+
110
+ # sends a put request with self.data
111
+ def put
112
+ self.data = self.class.extract_one(self.class.connection.put(identifier, formatted_data), identifier).to_hash
113
+ self
114
+ end
115
+
116
+ # sends a post request with self.data
117
+ def post
118
+ self.data = self.class.extract_one(self.class.connection.post(formatted_data)).to_hash
119
+ @persisted = true
120
+ self
121
+ end
122
+
123
+ # delete
124
+ def delete
125
+ if self.class.connection.delete(identifier)
126
+ @persisted = false
127
+ instance_variable_set('@'+self.class.identifier, nil)
128
+ true
129
+ else
130
+ false
131
+ end
132
+ end
133
+
134
+ def persisted?
135
+ !!@persisted
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,12 @@
1
+ module Callbacks
2
+ def callbacks
3
+ @callbacks ||= Hash.new {|h,k| h[k] = []}
4
+ end
5
+ def add_callback(name,&block)
6
+ callbacks[name] << block
7
+ end
8
+ def run_callback(name, *args)
9
+ args = args.first if args.length == 1
10
+ callbacks[name].inject(args) {|args,cb| cb.call(*args)}
11
+ end
12
+ end
@@ -0,0 +1,167 @@
1
+ # gem 'oauth', '=0.2.2'
2
+ $:.unshift('gems/gems/oauth-0.2.2/lib')
3
+ require 'oauth'
4
+ require 'oauth/consumer'
5
+ require 'oauth/client/net_http'
6
+ module OAuth
7
+ module Signature
8
+ class Base
9
+ def initialize(request, options = {}, &block)
10
+ raise TypeError unless request.kind_of?(OAuth::RequestProxy::Base)
11
+ @request = request
12
+ if block_given?
13
+ @token_secret, @consumer_secret = yield block.arity == 1 ? token : [token, consumer_key,nonce,request.timestamp]
14
+ else
15
+ @consumer_secret = options[:consumer].respond_to?(:secret) ? options[:consumer].secret : options[:consumer]
16
+ @token_secret = options[:token].respond_to?(:secret) ? options[:token].secret : (options[:token] || '')
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ module RequestProxy
23
+ class Base
24
+ def inspect
25
+ "#<OAuth::RequestProxy::MerbRequest:#{object_id}\n\tconsumer_key: #{consumer_key}\n\ttoken: #{token}\n\tparameters: #{parameters.inspect}\n>"
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ module SimpleMapper
32
+ module HttpOAuthExtension
33
+ def requires_oauth(consumer_key, consumer_secret, options={})
34
+ @consumer_key = consumer_key
35
+ @consumer_secret = consumer_secret
36
+ @oauth_options = options
37
+
38
+ # Ingeniousity here... ;)
39
+ # Duplicates the class to give it a temporary session-attached oauth scope, sets oauth to the Model-Controller-OAuth class,
40
+ # then makes the class use the original class for all of its instantiation.
41
+ # NOTE: This only really makes the class methods use OAuth. Object methods, like associations, won't play the trick as well.
42
+ def self.with_oauth(controller)
43
+ self.element_name = self.element_name
44
+ self.collection_name = self.collection_name
45
+ duped = self.dup
46
+ duped.set_oauth(controller)
47
+ yield if block_given?
48
+ duped
49
+ end
50
+
51
+ def oauth
52
+ @oauth
53
+ end
54
+
55
+ def set_oauth(controller)
56
+ @oauth = OAuthController.new(controller, self, @consumer_key, @consumer_secret, @oauth_options)
57
+ connection.add_callback('initialize_request') do |request|
58
+ @oauth.authenticate! if !@oauth.authorized? && @oauth.scriptable?
59
+ raise RuntimeError, "Must authorize OAuth before attempting to get data from the provider." unless @oauth.authorized?
60
+ @oauth.request_signed!(request)
61
+ end
62
+ @oauth
63
+ end
64
+
65
+ true
66
+ end
67
+ end
68
+ end
69
+
70
+ # We'll have an instance of these for each controller-model pair.
71
+ class OAuthController
72
+ DEFAULT_OPTIONS = {
73
+ # Signature method used by server. Defaults to HMAC-SHA1
74
+ :signature_method=>'HMAC-SHA1',
75
+
76
+ # default paths on site. These are the same as the defaults set up by the generators
77
+ :request_token_path=>'/oauth/request_token',
78
+ :authorize_path=>'/oauth/authorize',
79
+ :access_token_path=>'/oauth/access_token',
80
+
81
+ # How do we send the oauth values to the server see
82
+ # http://oauth.googlecode.com/svn/spec/branches/1.0/drafts/6/spec.html#consumer_req_param for more info
83
+ #
84
+ # Possible values:
85
+ #
86
+ # :authorize - via the Authorize header (Default) ( option 1. in spec)
87
+ # :post - url form encoded in body of POST request ( option 2. in spec)
88
+ # :query - via the query part of the url ( option 3. in spec)
89
+ :auth_method=>:authorize,
90
+
91
+ # Default http method used for OAuth Token Requests (defaults to :post)
92
+ :http_method=>:post,
93
+
94
+ :version=>"1.0",
95
+
96
+ # Default authorization method: have the controller redirect to the authorize_url.
97
+ :authorization_method => lambda {|model| redirect(model.oauth.consumer.authorize_url)},
98
+
99
+ # Default session: grab session from the controller's session method -- session['Person_oauth'] for the Person ActiveResource model.
100
+ :session => lambda {|model| session[model.name.to_s + '_oauth'] ||= {} }
101
+ }
102
+ attr_accessor :options, :consumer
103
+
104
+ def initialize(controller, model, consumer_key, consumer_secret, options={})
105
+ @controller = controller
106
+ @model = model
107
+ @options = DEFAULT_OPTIONS.merge(options)
108
+ @model = @options.delete(:model)
109
+ @consumer = OAuth::Consumer.new(consumer_key, consumer_secret, options)
110
+ end
111
+
112
+ def authorized?
113
+ !!session[:access_token]
114
+ end
115
+
116
+ def scriptable?
117
+ @options[:authorization_method] == :scriptable
118
+ end
119
+
120
+ # The session is what holds which models are authenticated with what tokens.
121
+ # We just need the controller to retreive the session and to send back redirects when necessary.
122
+ def authenticate!
123
+ # 1) If we have no tokens, get a request_token and run the authorization method.
124
+ # 2) If we have a request_token, assume the user has already answered the question, go ahead and try to get an access_token.
125
+ if access_token
126
+ return true
127
+ elsif request_token
128
+ return @controller.begin_pathway(@options[:authorization_method].in_context(controller).call(@model)) if @options[:authorization_method].is_a?(Proc)
129
+ return true if access_token # For scriptables
130
+ end
131
+ end
132
+
133
+ def request_signed!(request)
134
+ @consumer.sign!(request, current_token)
135
+ request
136
+ end
137
+
138
+ private
139
+ # If none exist, go ahead and get one.
140
+ def request_token
141
+ session[:request_token] || begin
142
+ token = @consumer.get_request_token
143
+ session[:request_token] = token if token.token && token.secret
144
+ session[:request_token]
145
+ end
146
+ end
147
+
148
+ # If none exist but request_token exists, go ahead and request one.
149
+ def access_token
150
+ return nil if session[:request_token].nil?
151
+ session[:access_token] || begin
152
+ token = session[:request_token].get_access_token
153
+ session[:access_token] = token if token.token && token.secret
154
+ session[:access_token]
155
+ end
156
+ end
157
+
158
+ def current_token
159
+ access_token || request_token
160
+ end
161
+
162
+ def session
163
+ @session || begin
164
+ @session = @options[:session].in_context(@controller).call
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,36 @@
1
+ module SimpleMapper
2
+ module SimpleModel
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ def to_hash(options={})
8
+ self.class.properties.inject({}) {|h,k| h[k] = instance_variable_get("@#{k}"); h}
9
+ end
10
+
11
+ def identifier
12
+ instance_variable_get('@'+self.class.identifier)
13
+ end
14
+
15
+ def method_missing(method, *args)
16
+ if self.class.properties.include?(method.to_s)
17
+ instance_variable_get('@'+method.to_s)
18
+ elsif method.to_s =~ /=$/ && self.class.properties.include?(method.to_s.gsub(/=/, ''))
19
+ instance_variable_set('@'+method.to_s.gsub(/=/, ''), *args)
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+ def properties(*properties)
27
+ @properties = properties.collect {|e| e.to_s} if properties.length > 0
28
+ @properties
29
+ end
30
+ def identifier(id=nil)
31
+ @identifier = id.to_s unless id.nil?
32
+ @identifier
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ require 'simple_mapper/support/bliss_serializer'
2
+
3
+ module SimpleMapper
4
+ module XmlFormat
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ def self.mime_type_headers
10
+ {'Accept' => 'application/xml', 'Content-type' => 'application/xml'}
11
+ end
12
+
13
+ def to_xml
14
+ Serialize.object_to_xml(self, :key_name => 'self').to_s
15
+ end
16
+
17
+ module ClassMethods
18
+ # This assumes a standard xml format:
19
+ # <person attribute="">
20
+ # <another_att>value</another_att>
21
+ # </person>
22
+ # And for a collection of objects:
23
+ # <people>
24
+ # <person attribute="">
25
+ # <another_att>value 1</another_att>
26
+ # </person>
27
+ # <person attribute="">
28
+ # <another_att>value 2</another_att>
29
+ # </person>
30
+ # </people>
31
+ def from_xml(xml)
32
+ doc = Serialize.hash_from_xml(xml)
33
+ # doc could include a single 'model' element, or a 'models' wrapper around several.
34
+ puts "Top-level XML key(s): #{doc.keys.inspect}" if @debug
35
+ key = doc.keys.first
36
+ if doc[key].keys.uniq == [key.singularize] && doc[key][key.singularize].is_a?(Array)
37
+ puts "Several objects returned under key '#{key}'/'#{key.singularize}':" if @debug
38
+ doc[key][key.singularize].collect do |e|
39
+ puts "Obj: #{e.inspect}" if @debug
40
+ Object.module_eval("::#{key.singularize.camelize}", __FILE__, __LINE__).new(e)
41
+ end
42
+ else # top-level must be single object
43
+ Object.module_eval("::#{key.singularize.camelize}", __FILE__, __LINE__).new(Serialize.hash_from_xml(xml)[self.name.underscore])
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,168 @@
1
+ require 'rexml/light/node'
2
+ require 'rexml/document'
3
+
4
+ # This is a slighly modified version of the XMLUtilityNode from
5
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
6
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
7
+ # This represents the hard part of the work, all I did was change the underlying
8
+ # parser
9
+ class REXMLUtilityNode # :nodoc:
10
+ attr_accessor :name, :attributes, :children
11
+
12
+ def initialize(name, attributes = {})
13
+ @name = name.tr("-", "_")
14
+ @attributes = undasherize_keys(attributes)
15
+ @children = []
16
+ @text = false
17
+ end
18
+
19
+ def add_node(node)
20
+ @text = true if node.is_a? String
21
+ @children << node
22
+ end
23
+
24
+ def to_hash
25
+ if @text
26
+ return { name => typecast_value( translate_xml_entities( inner_html ) ) }
27
+ else
28
+ #change repeating groups into an array
29
+ # group by the first key of each element of the array to find repeating groups
30
+ groups = @children.group_by{ |c| c.name }
31
+
32
+ hash = {}
33
+ groups.each do |key, values|
34
+ if values.size == 1
35
+ hash.merge! values.first
36
+ else
37
+ hash.merge! key => values.map { |element| element.to_hash[key] }
38
+ end
39
+ end
40
+
41
+ # merge the arrays, including attributes
42
+ hash.merge! attributes unless attributes.empty?
43
+
44
+ { name => hash }
45
+ end
46
+ end
47
+
48
+ def typecast_value(value)
49
+ return value unless attributes["type"]
50
+
51
+ case attributes["type"]
52
+ when "integer" then value.to_i
53
+ when "boolean" then value.strip == "true"
54
+ when "datetime" then ::Time.parse(value).utc
55
+ when "date" then ::Date.parse(value)
56
+ else value
57
+ end
58
+ end
59
+
60
+ def translate_xml_entities(value)
61
+ value.gsub(/&lt;/, "<").
62
+ gsub(/&gt;/, ">").
63
+ gsub(/&quot;/, '"').
64
+ gsub(/&apos;/, "'").
65
+ gsub(/&amp;/, "&")
66
+ end
67
+
68
+ def undasherize_keys(params)
69
+ params.keys.each do |key, vvalue|
70
+ params[key.tr("-", "_")] = params.delete(key)
71
+ end
72
+ params
73
+ end
74
+
75
+ def inner_html
76
+ @children.join
77
+ end
78
+
79
+ def to_html
80
+ "<#{name}#{attributes.to_xml_attributes}>#{inner_html}</#{name}>"
81
+ end
82
+
83
+ def to_s
84
+ to_html
85
+ end
86
+ end
87
+
88
+ module Serialize
89
+ XML_OPTIONS = {
90
+ :include_key => :attribute, # Can be false, :element, or :attribute
91
+ :report_nil => true, # Sets an attribute nil="true" on elements that are nil, so that the reader doesn't read as an empty string
92
+ :key_name => 'id', # Default key name
93
+ }
94
+ def self.object_to_xml(obj, options={})
95
+ # Automatically set the key_name for DataMapper objects
96
+ options.merge!(:key_name => obj.class.table.key.name) if obj.class.respond_to?(:table) && obj.class.respond_to?(:persistent?) && obj.class.persistent?
97
+ # Should do the above also for ActiveRecord...
98
+ to_xml(obj.class.name.underscore, obj.to_hash(options), options)
99
+ end
100
+ def self.to_xml(root, attributes={}, options={})
101
+ options = XML_OPTIONS.merge(options)
102
+ attributes = attributes.dup.stringify_keys!
103
+
104
+ doc = REXML::Document.new
105
+ root_element = doc.add_element(root)
106
+
107
+ case options[:include_key]
108
+ when :attribute
109
+ root_element.add_attribute(options[:key_name], attributes.delete(options[:key_name].to_s).to_s).extended do
110
+ def self.to_string; %Q[#@expanded_name="#{to_s().gsub(/"/, '&quot;')}"] end
111
+ end
112
+ when :element
113
+ root_element.add_element(options[:key_name]) << REXML::Text.new(attributes.delete(options[:key_name].to_s).to_s)
114
+ end
115
+
116
+ attributes.each do |key,value|
117
+ node = root_element.add_element(key)
118
+ node << REXML::Text.new(value.to_s) unless value.nil?
119
+ node.add_attribute('nil', 'true') if value.nil? && options[:report_nil]
120
+ end
121
+
122
+ doc
123
+ end
124
+
125
+ def self.hash_from_xml(xml,options={})
126
+ options = XML_OPTIONS.merge(options)
127
+
128
+ stack = []
129
+ parser = REXML::Parsers::BaseParser.new(xml)
130
+
131
+ while true
132
+ event = parser.pull
133
+ case event[0]
134
+ when :end_document
135
+ break
136
+ when :end_doctype, :start_doctype
137
+ # do nothing
138
+ when :start_element
139
+ stack.push REXMLUtilityNode.new(event[1], event[2])
140
+ when :end_element
141
+ if stack.size > 1
142
+ temp = stack.pop
143
+ stack.last.add_node(temp)
144
+ end
145
+ when :text, :cdata
146
+ stack.last.add_node(event[1]) unless event[1].strip.length == 0
147
+ end
148
+ end
149
+ data = stack.pop.to_hash
150
+
151
+ # Turn any {"nil" => "true"} into just nil
152
+ data.crawl {|h,k,v| h[k] = nil if v == {}; v} unless options[:report_nil]
153
+ data.crawl {|h,k,v| h[k] = nil if v == {} || v == {'nil' => 'true'}; v} if options[:report_nil]
154
+ data
155
+ end
156
+ end
157
+
158
+ module Merb
159
+ class Request
160
+ def xml_params
161
+ @xml_params ||= begin
162
+ if Merb::Const::XML_MIME_TYPE_REGEXP.match(content_type)
163
+ Serialize.hash_from_xml(raw_post) rescue Mash.new
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end