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