alexvollmer-httparty 0.1.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,349 @@
1
+ require 'time'
2
+
3
+ # Copyright (c) 2008 Sam Smoot.
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ class Object
25
+ # @return <TrueClass, FalseClass>
26
+ #
27
+ # @example [].blank? #=> true
28
+ # @example [1].blank? #=> false
29
+ # @example [nil].blank? #=> false
30
+ #
31
+ # Returns true if the object is nil or empty (if applicable)
32
+ def blank?
33
+ nil? || (respond_to?(:empty?) && empty?)
34
+ end
35
+ end # class Object
36
+
37
+ class Numeric
38
+ # @return <TrueClass, FalseClass>
39
+ #
40
+ # Numerics can't be blank
41
+ def blank?
42
+ false
43
+ end
44
+ end # class Numeric
45
+
46
+ class NilClass
47
+ # @return <TrueClass, FalseClass>
48
+ #
49
+ # Nils are always blank
50
+ def blank?
51
+ true
52
+ end
53
+ end # class NilClass
54
+
55
+ class TrueClass
56
+ # @return <TrueClass, FalseClass>
57
+ #
58
+ # True is not blank.
59
+ def blank?
60
+ false
61
+ end
62
+ end # class TrueClass
63
+
64
+ class FalseClass
65
+ # False is always blank.
66
+ def blank?
67
+ true
68
+ end
69
+ end # class FalseClass
70
+
71
+ class String
72
+ # @example "".blank? #=> true
73
+ # @example " ".blank? #=> true
74
+ # @example " hey ho ".blank? #=> false
75
+ #
76
+ # @return <TrueClass, FalseClass>
77
+ #
78
+ # Strips out whitespace then tests if the string is empty.
79
+ def blank?
80
+ strip.empty?
81
+ end
82
+ end # class String
83
+
84
+ require 'rexml/parsers/streamparser'
85
+ require 'rexml/parsers/baseparser'
86
+ require 'rexml/light/node'
87
+
88
+ # This is a slighly modified version of the XMLUtilityNode from
89
+ # http://merb.devjavu.com/projects/merb/ticket/95 (has.sox@gmail.com)
90
+ # It's mainly just adding vowels, as I ht cd wth n vwls :)
91
+ # This represents the hard part of the work, all I did was change the
92
+ # underlying parser.
93
+ class REXMLUtilityNode
94
+ attr_accessor :name, :attributes, :children, :type
95
+
96
+ def self.typecasts
97
+ @@typecasts
98
+ end
99
+
100
+ def self.typecasts=(obj)
101
+ @@typecasts = obj
102
+ end
103
+
104
+ def self.available_typecasts
105
+ @@available_typecasts
106
+ end
107
+
108
+ def self.available_typecasts=(obj)
109
+ @@available_typecasts = obj
110
+ end
111
+
112
+ self.typecasts = {}
113
+ self.typecasts["integer"] = lambda{|v| v.nil? ? nil : v.to_i}
114
+ self.typecasts["boolean"] = lambda{|v| v.nil? ? nil : (v.strip != "false")}
115
+ self.typecasts["datetime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
116
+ self.typecasts["date"] = lambda{|v| v.nil? ? nil : Date.parse(v)}
117
+ self.typecasts["dateTime"] = lambda{|v| v.nil? ? nil : Time.parse(v).utc}
118
+ self.typecasts["decimal"] = lambda{|v| BigDecimal(v)}
119
+ self.typecasts["double"] = lambda{|v| v.nil? ? nil : v.to_f}
120
+ self.typecasts["float"] = lambda{|v| v.nil? ? nil : v.to_f}
121
+ self.typecasts["symbol"] = lambda{|v| v.to_sym}
122
+ self.typecasts["string"] = lambda{|v| v.to_s}
123
+ self.typecasts["yaml"] = lambda{|v| v.nil? ? nil : YAML.load(v)}
124
+ self.typecasts["base64Binary"] = lambda{|v| v.unpack('m').first }
125
+
126
+ self.available_typecasts = self.typecasts.keys
127
+
128
+ def initialize(name, attributes = {})
129
+ @name = name.tr("-", "_")
130
+ # leave the type alone if we don't know what it is
131
+ @type = self.class.available_typecasts.include?(attributes["type"]) ? attributes.delete("type") : attributes["type"]
132
+
133
+ @nil_element = attributes.delete("nil") == "true"
134
+ @attributes = undasherize_keys(attributes)
135
+ @children = []
136
+ @text = false
137
+ end
138
+
139
+ def add_node(node)
140
+ @text = true if node.is_a? String
141
+ @children << node
142
+ end
143
+
144
+ def to_hash
145
+ if @type == "file"
146
+ f = StringIO.new((@children.first || '').unpack('m').first)
147
+ class << f
148
+ attr_accessor :original_filename, :content_type
149
+ end
150
+ f.original_filename = attributes['name'] || 'untitled'
151
+ f.content_type = attributes['content_type'] || 'application/octet-stream'
152
+ return {name => f}
153
+ end
154
+
155
+ if @text
156
+ return { name => typecast_value( translate_xml_entities( inner_html ) ) }
157
+ else
158
+ #change repeating groups into an array
159
+ groups = @children.inject({}) { |s,e| (s[e.name] ||= []) << e; s }
160
+
161
+ out = nil
162
+ if @type == "array"
163
+ out = []
164
+ groups.each do |k, v|
165
+ if v.size == 1
166
+ out << v.first.to_hash.entries.first.last
167
+ else
168
+ out << v.map{|e| e.to_hash[k]}
169
+ end
170
+ end
171
+ out = out.flatten
172
+
173
+ else # If Hash
174
+ out = {}
175
+ groups.each do |k,v|
176
+ if v.size == 1
177
+ out.merge!(v.first)
178
+ else
179
+ out.merge!( k => v.map{|e| e.to_hash[k]})
180
+ end
181
+ end
182
+ out.merge! attributes unless attributes.empty?
183
+ out = out.empty? ? nil : out
184
+ end
185
+
186
+ if @type && out.nil?
187
+ { name => typecast_value(out) }
188
+ else
189
+ { name => out }
190
+ end
191
+ end
192
+ end
193
+
194
+ # Typecasts a value based upon its type. For instance, if
195
+ # +node+ has #type == "integer",
196
+ # {{[node.typecast_value("12") #=> 12]}}
197
+ #
198
+ # @param value<String> The value that is being typecast.
199
+ #
200
+ # @details [:type options]
201
+ # "integer"::
202
+ # converts +value+ to an integer with #to_i
203
+ # "boolean"::
204
+ # checks whether +value+, after removing spaces, is the literal
205
+ # "true"
206
+ # "datetime"::
207
+ # Parses +value+ using Time.parse, and returns a UTC Time
208
+ # "date"::
209
+ # Parses +value+ using Date.parse
210
+ #
211
+ # @return <Integer, TrueClass, FalseClass, Time, Date, Object>
212
+ # The result of typecasting +value+.
213
+ #
214
+ # @note
215
+ # If +self+ does not have a "type" key, or if it's not one of the
216
+ # options specified above, the raw +value+ will be returned.
217
+ def typecast_value(value)
218
+ return value unless @type
219
+ proc = self.class.typecasts[@type]
220
+ proc.nil? ? value : proc.call(value)
221
+ end
222
+
223
+ # Convert basic XML entities into their literal values.
224
+ #
225
+ # @param value<#gsub> An XML fragment.
226
+ #
227
+ # @return <#gsub> The XML fragment after converting entities.
228
+ def translate_xml_entities(value)
229
+ value.gsub(/&lt;/, "<").
230
+ gsub(/&gt;/, ">").
231
+ gsub(/&quot;/, '"').
232
+ gsub(/&apos;/, "'").
233
+ gsub(/&amp;/, "&")
234
+ end
235
+
236
+ # Take keys of the form foo-bar and convert them to foo_bar
237
+ def undasherize_keys(params)
238
+ params.keys.each do |key, value|
239
+ params[key.tr("-", "_")] = params.delete(key)
240
+ end
241
+ params
242
+ end
243
+
244
+ # Get the inner_html of the REXML node.
245
+ def inner_html
246
+ @children.join
247
+ end
248
+
249
+ # Converts the node into a readable HTML node.
250
+ #
251
+ # @return <String> The HTML node in text form.
252
+ def to_html
253
+ attributes.merge!(:type => @type ) if @type
254
+ "<#{name}#{attributes.to_xml_attributes}>#{@nil_element ? '' : inner_html}</#{name}>"
255
+ end
256
+
257
+ # @alias #to_html #to_s
258
+ def to_s
259
+ to_html
260
+ end
261
+ end
262
+
263
+ class ToHashParser
264
+ def self.from_xml(xml)
265
+ stack = []
266
+ parser = REXML::Parsers::BaseParser.new(xml)
267
+
268
+ while true
269
+ event = parser.pull
270
+ case event[0]
271
+ when :end_document
272
+ break
273
+ when :end_doctype, :start_doctype
274
+ # do nothing
275
+ when :start_element
276
+ stack.push REXMLUtilityNode.new(event[1], event[2])
277
+ when :end_element
278
+ if stack.size > 1
279
+ temp = stack.pop
280
+ stack.last.add_node(temp)
281
+ end
282
+ when :text, :cdata
283
+ stack.last.add_node(event[1]) unless event[1].strip.length == 0
284
+ end
285
+ end
286
+ stack.pop.to_hash
287
+ end
288
+ end
289
+
290
+ class Hash
291
+ # @return <String> This hash as a query string
292
+ #
293
+ # @example
294
+ # { :name => "Bob",
295
+ # :address => {
296
+ # :street => '111 Ruby Ave.',
297
+ # :city => 'Ruby Central',
298
+ # :phones => ['111-111-1111', '222-222-2222']
299
+ # }
300
+ # }.to_params
301
+ # #=> "name=Bob&address[city]=Ruby Central&address[phones][]=111-111-1111&address[phones][]=222-222-2222&address[street]=111 Ruby Ave."
302
+ def to_params
303
+ params = self.map { |k,v| normalize_param(k,v) }.join
304
+ params.chop! # trailing &
305
+ params
306
+ end
307
+
308
+ # @param key<Object> The key for the param.
309
+ # @param value<Object> The value for the param.
310
+ #
311
+ # @return <String> This key value pair as a param
312
+ #
313
+ # @example normalize_param(:name, "Bob") #=> "name=Bob&"
314
+ def normalize_param(key, value)
315
+ param = ''
316
+ stack = []
317
+
318
+ if value.is_a?(Array)
319
+ param << value.map { |element| normalize_param("#{key}[]", element) }.join
320
+ elsif value.is_a?(Hash)
321
+ stack << [key,value]
322
+ else
323
+ param << "#{key}=#{URI.encode(value.to_s)}&"
324
+ end
325
+
326
+ stack.each do |parent, hash|
327
+ hash.each do |key, value|
328
+ if value.is_a?(Hash)
329
+ stack << ["#{parent}[#{key}]", value]
330
+ else
331
+ param << normalize_param("#{parent}[#{key}]", value)
332
+ end
333
+ end
334
+ end
335
+
336
+ param
337
+ end
338
+
339
+ # @return <String> The hash as attributes for an XML tag.
340
+ #
341
+ # @example
342
+ # { :one => 1, "two"=>"TWO" }.to_xml_attributes
343
+ # #=> 'one="1" two="TWO"'
344
+ def to_xml_attributes
345
+ map do |k,v|
346
+ %{#{k.to_s.snake_case.sub(/^(.{1,1})/) { |m| m.downcase }}="#{v}"}
347
+ end.join(' ')
348
+ end
349
+ end
@@ -0,0 +1,4 @@
1
+ module HTTParty
2
+ class UnsupportedFormat < StandardError; end
3
+ class RedirectionTooDeep < StandardError; end
4
+ end
@@ -1,28 +1,35 @@
1
+ require 'uri'
2
+
1
3
  module HTTParty
2
4
  class Request
3
5
  SupportedHTTPMethods = [Net::HTTP::Get, Net::HTTP::Post, Net::HTTP::Put, Net::HTTP::Delete]
4
6
 
5
7
  attr_accessor :http_method, :path, :options
6
-
7
- def initialize(http_method, path, options={})
8
+
9
+ def initialize(http_method, path, o={})
8
10
  self.http_method = http_method
9
11
  self.path = path
10
12
  self.options = {
11
- :limit => options.delete(:no_follow) ? 0 : 5,
13
+ :limit => o.delete(:no_follow) ? 0 : 5,
12
14
  :default_params => {},
13
- }.merge(options.dup)
15
+ :keep_body => false
16
+ }.merge(o)
14
17
  end
15
18
 
16
19
  def path=(uri)
17
20
  @path = URI.parse(uri)
18
21
  end
19
-
22
+
20
23
  def uri
21
24
  uri = path.relative? ? URI.parse("#{options[:base_uri]}#{path}") : path
22
25
  uri.query = query_string(uri)
23
26
  uri
24
27
  end
25
-
28
+
29
+ def format
30
+ options[:format]
31
+ end
32
+
26
33
  def perform
27
34
  validate!
28
35
  handle_response!(get_response(uri))
@@ -35,36 +42,48 @@ module HTTParty
35
42
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
36
43
  http
37
44
  end
38
-
45
+
39
46
  def get_response(uri) #:nodoc:
40
47
  request = http_method.new(uri.request_uri)
41
- request.body = options[:body].is_a?(Hash) ? options[:body].to_query : options[:body] unless options[:body].blank?
48
+
49
+ if post? && options[:query]
50
+ request.set_form_data(options[:query])
51
+ end
52
+
53
+ request.body = options[:body].is_a?(Hash) ? options[:body].to_params : options[:body] unless options[:body].blank?
42
54
  request.initialize_http_header options[:headers]
43
- request.basic_auth(options[:basic_auth][:username], options[:basic_auth][:password]) if options[:basic_auth]
55
+
56
+ if options[:basic_auth]
57
+ request.basic_auth(options[:basic_auth][:username], options[:basic_auth][:password])
58
+ end
59
+
44
60
  response = http(uri).request(request)
45
61
  options[:format] ||= format_from_mimetype(response['content-type'])
46
62
  response
47
63
  end
48
-
64
+
49
65
  def query_string(uri) #:nodoc:
50
66
  query_string_parts = []
51
67
  query_string_parts << uri.query unless uri.query.blank?
52
68
 
53
69
  if options[:query].is_a?(Hash)
54
- query_string_parts << options[:default_params].merge(options[:query]).to_query
70
+ query_string_parts << options[:default_params].merge(options[:query]).to_params
55
71
  else
56
- query_string_parts << options[:default_params].to_query unless options[:default_params].blank?
72
+ query_string_parts << options[:default_params].to_params unless options[:default_params].blank?
57
73
  query_string_parts << options[:query] unless options[:query].blank?
58
74
  end
59
-
75
+
60
76
  query_string_parts.size > 0 ? query_string_parts.join('&') : nil
61
77
  end
62
-
78
+
63
79
  # Raises exception Net::XXX (http error code) if an http error occured
64
80
  def handle_response!(response) #:nodoc:
65
81
  case response
66
82
  when Net::HTTPSuccess
67
- parse_response(response.body)
83
+ original_resp = parse_response(response.body)
84
+ final_resp = Response.new(response.body, nil, original_resp)
85
+ response.each_header { |n,v| final_resp.add_header(n, v) }
86
+ final_resp
68
87
  when Net::HTTPRedirection
69
88
  options[:limit] -= 1
70
89
  self.path = response['location']
@@ -75,30 +94,61 @@ module HTTParty
75
94
  response.error! # raises exception corresponding to http error Net::XXX
76
95
  end
77
96
  end
78
-
97
+
79
98
  def parse_response(body) #:nodoc:
80
99
  return nil if body.nil? or body.empty?
81
- case options[:format]
82
- when :xml
83
- Hash.from_xml(body)
84
- when :json
85
- ActiveSupport::JSON.decode(body)
86
- else
87
- body
88
- end
100
+ response = case format
101
+ when :xml
102
+ ToHashParser.from_xml(body)
103
+ when :json
104
+ JSON.parse(body)
105
+ else
106
+ body
107
+ end
108
+ response
89
109
  end
90
-
110
+
91
111
  # Uses the HTTP Content-Type header to determine the format of the response
92
112
  # It compares the MIME type returned to the types stored in the AllowedFormats hash
93
113
  def format_from_mimetype(mimetype) #:nodoc:
114
+ return nil if mimetype.nil?
94
115
  AllowedFormats.each { |k, v| return k if mimetype.include?(v) }
95
116
  end
96
-
117
+
97
118
  def validate! #:nodoc:
98
119
  raise HTTParty::RedirectionTooDeep, 'HTTP redirects too deep' if options[:limit].to_i <= 0
99
120
  raise ArgumentError, 'only get, post, put and delete methods are supported' unless SupportedHTTPMethods.include?(http_method)
100
121
  raise ArgumentError, ':headers must be a hash' if options[:headers] && !options[:headers].is_a?(Hash)
101
122
  raise ArgumentError, ':basic_auth must be a hash' if options[:basic_auth] && !options[:basic_auth].is_a?(Hash)
123
+ raise ArgumentError, ':query must be hash if using HTTP Post' if post? && !options[:query].nil? && !options[:query].is_a?(Hash)
102
124
  end
125
+
126
+ def post?
127
+ Net::HTTP::Post == http_method
128
+ end
129
+ end
130
+
131
+ class Response
132
+ attr_accessor :original_body, :format, :headers
133
+
134
+ def initialize(original_body, format, delegate)
135
+ self.original_body = original_body
136
+ self.format = format
137
+ self.headers = { }
138
+ @delegate = delegate
139
+ end
140
+
141
+ def add_header(name, value)
142
+ @headers[name] = value
143
+ end
144
+
145
+ def method_missing(sym, *args)
146
+ @delegate.send(sym, *args)
147
+ end
148
+
149
+ def pretty_print(pp)
150
+ @delegate.pretty_print(pp)
151
+ end
152
+
103
153
  end
104
154
  end
@@ -1,9 +1,3 @@
1
1
  module HTTParty
2
- module VERSION #:nodoc:
3
- MAJOR = 0
4
- MINOR = 1
5
- TINY = 5
6
-
7
- STRING = [MAJOR, MINOR, TINY].join('.')
8
- end
2
+ Version = '0.2.6'
9
3
  end
data/lib/httparty.rb CHANGED
@@ -1,37 +1,29 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
1
3
  require 'net/http'
2
4
  require 'net/https'
3
- require 'uri'
4
- require 'ostruct'
5
5
  require 'rubygems'
6
- require 'active_support'
7
-
8
- directory = File.dirname(__FILE__)
9
- $:.unshift(directory) unless $:.include?(directory) || $:.include?(File.expand_path(directory))
10
-
11
- require 'httparty/request'
6
+ gem 'json', '>= 1.1.3'
7
+ require 'json'
12
8
 
13
- module HTTParty
14
- class UnsupportedFormat < StandardError; end
15
- class RedirectionTooDeep < StandardError; end
9
+ require 'module_level_inheritable_attributes'
10
+ require 'core_extensions'
16
11
 
12
+ module HTTParty
17
13
  AllowedFormats = {:xml => 'text/xml', :json => 'application/json', :html => 'text/html'}
18
-
14
+
19
15
  def self.included(base)
20
16
  base.extend ClassMethods
17
+ base.send :include, ModuleLevelInheritableAttributes
18
+ base.send(:mattr_inheritable, :default_options)
19
+ base.instance_variable_set("@default_options", {})
21
20
  end
22
-
21
+
23
22
  module ClassMethods
24
23
  def default_options
25
- @@default_options ||= {}
24
+ @default_options
26
25
  end
27
26
 
28
- #
29
- # Set an http proxy
30
- #
31
- # class Twitter
32
- # include HTTParty
33
- # http_proxy 'http://myProxy', 1080
34
- # ....
35
27
  def http_proxy(addr=nil, port = nil)
36
28
  default_options[:http_proxyaddr] = addr
37
29
  default_options[:http_proxyport] = port
@@ -39,13 +31,13 @@ module HTTParty
39
31
 
40
32
  def base_uri(uri=nil)
41
33
  return default_options[:base_uri] unless uri
42
- default_options[:base_uri] = normalize_base_uri(uri)
34
+ default_options[:base_uri] = HTTParty.normalize_base_uri(uri)
43
35
  end
44
36
 
45
37
  def basic_auth(u, p)
46
38
  default_options[:basic_auth] = {:username => u, :password => p}
47
39
  end
48
-
40
+
49
41
  def default_params(h={})
50
42
  raise ArgumentError, 'Default params must be a hash' unless h.is_a?(Hash)
51
43
  default_options[:default_params] ||= {}
@@ -57,12 +49,12 @@ module HTTParty
57
49
  default_options[:headers] ||= {}
58
50
  default_options[:headers].merge!(h)
59
51
  end
60
-
52
+
61
53
  def format(f)
62
54
  raise UnsupportedFormat, "Must be one of: #{AllowedFormats.keys.join(', ')}" unless AllowedFormats.key?(f)
63
55
  default_options[:format] = f
64
56
  end
65
-
57
+
66
58
  def get(path, options={})
67
59
  perform_request Net::HTTP::Get, path, options
68
60
  end
@@ -81,27 +73,40 @@ module HTTParty
81
73
 
82
74
  private
83
75
  def perform_request(http_method, path, options) #:nodoc:
84
- Request.new(http_method, path, default_options.merge(options)).perform
76
+ Request.new(http_method, path, default_options.dup.merge(options)).perform
85
77
  end
78
+ end
86
79
 
87
- # Makes it so uri is sure to parse stuff like google.com without the http
88
- def normalize_base_uri(url) #:nodoc:
89
- use_ssl = (url =~ /^https/) || url.include?(':443')
90
- url.chop! if url.ends_with?('/')
91
- url.gsub!(/^https?:\/\//i, '')
92
- "http#{'s' if use_ssl}://#{url}"
93
- end
80
+ def self.normalize_base_uri(url) #:nodoc:
81
+ use_ssl = (url =~ /^https/) || url.include?(':443')
82
+ ends_with_slash = url =~ /\/$/
83
+
84
+ url.chop! if ends_with_slash
85
+ url.gsub!(/^https?:\/\//i, '')
86
+
87
+ "http#{'s' if use_ssl}://#{url}"
88
+ end
89
+
90
+ class Basement
91
+ include HTTParty
92
+ end
93
+
94
+ def self.get(*args)
95
+ Basement.get(*args)
96
+ end
97
+
98
+ def self.post(*args)
99
+ Basement.post(*args)
100
+ end
94
101
 
95
- # Figure out the Content-Type from the path extension
96
- def format_from_path(path) #:nodoc:
97
- case path
98
- when /\.xml$/
99
- 'xml'
100
- when /\.json$/
101
- 'json'
102
- else
103
- nil
104
- end
105
- end
102
+ def self.put(*args)
103
+ Basement.put(*args)
104
+ end
105
+
106
+ def self.delete(*args)
107
+ Basement.delete(*args)
106
108
  end
107
109
  end
110
+
111
+ require 'httparty/exceptions'
112
+ require 'httparty/request'
@@ -0,0 +1,25 @@
1
+ module ModuleLevelInheritableAttributes
2
+ def self.included(base)
3
+ base.extend(ClassMethods)
4
+ end
5
+
6
+ module ClassMethods
7
+ def mattr_inheritable(*args)
8
+ @mattr_inheritable_attrs ||= [:mattr_inheritable_attrs]
9
+ @mattr_inheritable_attrs += args
10
+ args.each do |arg|
11
+ module_eval %(
12
+ class << self; attr_accessor :#{arg} end
13
+ )
14
+ end
15
+ @mattr_inheritable_attrs
16
+ end
17
+
18
+ def inherited(subclass)
19
+ @mattr_inheritable_attrs.each do |inheritable_attribute|
20
+ instance_var = "@#{inheritable_attribute}"
21
+ subclass.instance_variable_set(instance_var, instance_variable_get(instance_var))
22
+ end
23
+ end
24
+ end
25
+ end