rdropbox 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,109 @@
1
+ # Defines the Dropbox::Event class.
2
+
3
+ nil # doc fix
4
+
5
+ module Dropbox
6
+
7
+ # The Dropbox::Event class stores information about which entries were
8
+ # modified during a pingback event. You initialize this class from the JSON
9
+ # string given to you by Dropbox during a pingback:
10
+ #
11
+ # event = Dropbox::Event.new(params[:target_events])
12
+ #
13
+ # Once this is complete, the Dropbox::Event instance contains references for
14
+ # each of the entries, with the basic information included in the pingback:
15
+ #
16
+ # event.user_ids #=> [ 1, 2, 3 ]
17
+ # event.entries(1).first #=> #<Dropbox::Revision 1:10:100>
18
+ #
19
+ # For any of these entries, you can load its content and metadata:
20
+ #
21
+ # event.entries(1).first.load(dropbox_session)
22
+ # event.entries(1).first.content #=> "Content of file..."
23
+ # event.entries(1).first.size #=> 2245
24
+ #
25
+ # You can also load only the metadata for all of a user's entries:
26
+ #
27
+ # event.load_metadata(first_users_dropbox_session)
28
+ # event.entries(1).first.size #=> 154365
29
+
30
+ class Event
31
+ def initialize(json_pingback) # :nodoc:
32
+ @json_pingback = json_pingback
33
+ begin
34
+ @metadata = JSON.parse(json_pingback).stringify_keys_recursively
35
+ rescue JSON::ParserError
36
+ raise Dropbox::ParseError, "Invalid pingback event data"
37
+ end
38
+
39
+ process_pingback
40
+ end
41
+
42
+ # Returns an array of Dropbox user ID's involved in this pingback.
43
+
44
+ def user_ids
45
+ @entries_by_user_id.keys
46
+ end
47
+
48
+ # When given no arguments, returns an array of all entries (as
49
+ # Dropbox::Revision instances). When given a user ID, filters the list
50
+ # to only entries belonging to that Dropbox user.
51
+
52
+ def entries(user_id=nil)
53
+ user_id ? (@entries_by_user_id[user_id.to_i] || []).dup : @entries.dup
54
+ end
55
+
56
+ # Loads the metadata for all entries belonging to a given Dropbox session.
57
+ # Does not load data for files that do not belong to the user owning the
58
+ # given Dropbox session.
59
+ #
60
+ # Future calls to this method will result in additional network requests,
61
+ # though the Dropbox::Revision instances do cache their metadata values.
62
+ #
63
+ # Options:
64
+ #
65
+ # +mode+:: Temporarily changes the API mode. See the Dropbox::API::MODES
66
+ # array.
67
+
68
+ def load_metadata(session, options={})
69
+ process_metadata session.event_metadata(@json_pingback, options).stringify_keys_recursively
70
+ end
71
+
72
+ def inspect # :nodoc:
73
+ "#<#{self.class.to_s} (#{@entries.size} entries)>"
74
+ end
75
+
76
+ private
77
+
78
+ def process_pingback
79
+ @entries = Array.new
80
+ @entries_by_user_id = Hash.new
81
+ @entries_hashed = Hash.new
82
+
83
+ @metadata.each do |user_id, namespaces|
84
+ @entries_hashed[user_id.to_i] = Hash.new
85
+ @entries_by_user_id[user_id.to_i] = Array.new
86
+ namespaces.each do |namespace_id, journals|
87
+ @entries_hashed[user_id.to_i][namespace_id.to_i] = Hash.new
88
+ journals.each do |journal_id|
89
+ entry = Dropbox::Revision.new(user_id.to_i, namespace_id.to_i, journal_id.to_i)
90
+ @entries << entry
91
+ @entries_by_user_id[user_id.to_i] << entry
92
+ @entries_hashed[user_id.to_i][namespace_id.to_i][journal_id.to_i] = entry
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def process_metadata(metadata)
99
+ p metadata
100
+ metadata.each do |user_id, namespaces|
101
+ namespaces.each do |namespace_id, journals|
102
+ journals.each do |journal_id, attributes|
103
+ @entries_hashed[user_id.to_i][namespace_id.to_i][journal_id.to_i].process_metadata(attributes.symbolize_keys)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,98 @@
1
+ # Defines the Dropbox::Memoization module.
2
+
3
+ nil # doc fix
4
+
5
+ module Dropbox
6
+
7
+ # Adds methods to the Dropbox::Session class to support the temporary local
8
+ # storage of API results to reduce the number of network calls and simplify
9
+ # code.
10
+ #
11
+ # Memoization is <b>opt-in</b>; you must explicitly indicate that you want
12
+ # this functionality by calling the enable_memoization method on your
13
+ # Scribd::Session instance. Once memoization is enabled, subsequent calls to
14
+ # memoized methods will hit an in-memory cache as opposed to making identical
15
+ # network calls.
16
+ #
17
+ # If you would like to use your own caching strategy (for instance, your own
18
+ # memcache instance), set the +cache_proc+ and +cache_clear_proc+ attributes.
19
+ #
20
+ # Enabling memoization makes removes an instance's thread-safety.
21
+ #
22
+ # Example:
23
+ #
24
+ # session.metadata('file1') # network
25
+ #
26
+ # session.enable_memoization
27
+ # session.metadata('file1') # network
28
+ # session.metadata('file1') # cache
29
+ #
30
+ # session.metadata('file2') # network
31
+ # session.metadata('file2') # cache
32
+ #
33
+ # session.disable_memoization
34
+ # session.metadata('file2') # network
35
+
36
+ module Memoization
37
+ def self.included(base) # :nodoc:
38
+ base.extend ClassMethods
39
+ end
40
+
41
+ # The cache_proc is a proc with two arguments, the cache identifier and the
42
+ # proc to call and store in the event of a cache miss:
43
+ #
44
+ # instance.cache_proc = Proc.new do |identifier, calculate_proc|
45
+ # Rails.cache.fetch(identifier) { calculate_proc.call }
46
+ # end
47
+ #
48
+ # The Cache identifier will always be 64 lowercase hexadecimal characters.
49
+ # The second argument is a curried proc including all arguments to the
50
+ # original method call.
51
+
52
+ def cache_proc=(prc)
53
+ @_memo_cache_proc = prc
54
+ end
55
+
56
+ # The cache_clear_proc takes an identifier and should invalidate it from the
57
+ # cache:
58
+ #
59
+ # instance.cache_clear_proc = Proc.new { |identifier| Rails.cache.delete identifier }
60
+
61
+ def cache_clear_proc=(prc)
62
+ @_memo_cache_clear_proc = prc
63
+ end
64
+
65
+ # Begins memoizing the results of API calls. Memoization is off by default
66
+ # for new instances.
67
+
68
+ def enable_memoization
69
+ @_memoize = true
70
+ @_memo_identifiers ||= Set.new
71
+ end
72
+
73
+ # Halts memoization of API calls and clears the memoization cache.
74
+
75
+ def disable_memoization
76
+ @_memoize = false
77
+ @_memo_identifiers.each { |identifier| (@_memo_cache_clear_proc || Proc.new { |ident| eval "@_memo_#{ident} = nil" }).call(identifier) }
78
+ @_memo_identifiers.clear
79
+ end
80
+
81
+ module ClassMethods # :nodoc:
82
+ def memoize(*method_names) # :nodoc:
83
+ method_names.each do |meth|
84
+ define_method :"#{meth}_with_memo" do |*args|
85
+ if @_memoize then
86
+ identifier = Digest::SHA1.hexdigest(meth.to_s + ":" + args.to_yaml)
87
+ @_memo_identifiers << identifier
88
+ (@_memo_cache_proc || Proc.new { |ident, calculate_proc| eval "@_memo_#{ident} ||= calculate_proc.call" }).call identifier, Proc.new { send :"#{meth}_without_memo", *args }
89
+ else
90
+ send :"#{meth}_without_memo", *args
91
+ end
92
+ end
93
+ alias_method_chain meth, :memo
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,197 @@
1
+ # Defines the Dropbox::Revision class.
2
+
3
+ nil # doc fix
4
+
5
+ module Dropbox
6
+
7
+ # A file or folder at a point in time as referenced by a Dropbox::Event
8
+ # pingback event. Instances start out as "shells" only storing enough
9
+ # information to uniquely identify a file/folder belonging to a user at a
10
+ # certain revision.
11
+ #
12
+ # Instances of this class only appear in a Dropbox::Event object. To load the
13
+ # metadata for a revision, use the Dropbox::Event#load_metadata method.
14
+ # To load the content of the file at this revision, use the load_content
15
+ # method on this class.
16
+ #
17
+ # Once the metadata has been loaded, you can access it directly:
18
+ #
19
+ # revision.size #=> 2962
20
+ #
21
+ # The +mtime+ attribute will be a Time instance. The +mtime+ and +size+
22
+ # attributes will be +nil+ (not -1) if the file was deleted. All other
23
+ # attributes are as defined in
24
+ # http://developers.getdropbox.com/base.html#event-metadata
25
+ #
26
+ # If the metadata could not be read for whatever reason, the HTTP error code
27
+ # will be stored in the +error+ attribute.
28
+
29
+ class Revision
30
+ # The ID of the Dropbox user that owns this file.
31
+ attr_reader :user_id
32
+ # The namespace ID of the file (Dropbox internal).
33
+ attr_reader :namespace_id
34
+ # The journal ID of the file (Dropbox internal).
35
+ attr_reader :journal_id
36
+ # The HTTP error code received when trying to load metadata, or +nil+ if no
37
+ # error has yet been received.
38
+ attr_reader :error
39
+
40
+ def initialize(uid, nid, jid) # :nodoc:
41
+ @user_id = uid
42
+ @namespace_id = nid
43
+ @journal_id = jid
44
+ end
45
+
46
+ # The unique identifier string used by some Dropbox event API methods.
47
+
48
+ def identifier
49
+ "#{user_id}:#{namespace_id}:#{journal_id}"
50
+ end
51
+
52
+ # Uses the given Dropbox::Session to load the content and metadata for a
53
+ # file at a specific revision.
54
+ #
55
+ # Options:
56
+ #
57
+ # +mode+:: Temporarily changes the API mode. See the Dropbox::API::MODES
58
+ # array.
59
+
60
+ def load(session, options={})
61
+ @content, @metadata = session.event_content(identifier, options)
62
+ @metadata.symbolize_keys!
63
+
64
+ postprocess_metadata
65
+ end
66
+
67
+ # Returns true if the content for this revision has been previously loaded
68
+ # and is cached in this object.
69
+
70
+ def content_loaded?
71
+ @content.to_bool
72
+ end
73
+
74
+ # Returns true if the metadata for this revision has been previously loaded
75
+ # and is cached in this object.
76
+
77
+ def metadata_loaded?
78
+ @metadata.to_bool
79
+ end
80
+
81
+ # Sugar for the +latest+ attribute.
82
+
83
+ def latest?
84
+ raise NotLoadedError.new(:metadata) unless metadata_loaded?
85
+ self.latest
86
+ end
87
+
88
+ # Sugar for the +is_dir+ attribute.
89
+
90
+ def directory?
91
+ raise NotLoadedError.new(:metadata) unless metadata_loaded?
92
+ self.is_dir
93
+ end
94
+
95
+ # Synonym for the +mtime+ attribute, for "duck" compatibility with the
96
+ # Dropbox +metadata+ API.
97
+
98
+ def modified
99
+ raise NotLoadedError.new(:metadata) unless metadata_loaded?
100
+ self.mtime
101
+ end
102
+
103
+ # Returns true if an error occurred when trying to load metadata.
104
+
105
+ def error?
106
+ error.to_bool
107
+ end
108
+
109
+ # Returns true if this change represents the file being deleted.
110
+
111
+ def deleted?
112
+ raise NotLoadedError.new(:metadata) unless metadata_loaded?
113
+ self.mtime.nil? and self.size.nil?
114
+ end
115
+
116
+ # Returns the contents of the file as a string. Returns nil for directories.
117
+ # You must call load first to retrieve the content from the network.
118
+
119
+ def content
120
+ raise NotLoadedError.new(:content) unless content_loaded?
121
+ @content
122
+ end
123
+
124
+ def inspect # :nodoc:
125
+ "#<#{self.class.to_s} #{identifier}>"
126
+ end
127
+
128
+ # Allows you to access metadata attributes directly:
129
+ #
130
+ # revision.size #=> 10526
131
+ #
132
+ # A NoMethodError will be raised if the metadata has not yet been loaded for
133
+ # this revision, so be sure to call metadata_loaded? beforehand.
134
+
135
+ def method_missing(meth, *args)
136
+ if args.empty? then
137
+ if @metadata and @metadata.include?(meth) then
138
+ return @metadata[meth]
139
+ else
140
+ super
141
+ end
142
+ else
143
+ super
144
+ end
145
+ end
146
+
147
+ # Loads the metadata for the latest revision of the entry and returns it as
148
+ # as <tt>Struct</tt> object. Uses the given session and calls
149
+ # Dropbox::API.metadata.
150
+ #
151
+ # If the metadata for this object has not yet been loaded, raises an error.
152
+ # Options are passed to Dropbox::API.metadata.
153
+
154
+ def metadata_for_latest_revision(session, options={})
155
+ raise NotLoadedError.new(:metadata) unless metadata_loaded?
156
+ session.metadata self.path, options
157
+ end
158
+
159
+ def process_metadata(metadata) # :nodoc:
160
+ if metadata[:error] then
161
+ @error = metadata[:error] unless @metadata
162
+ return
163
+ end
164
+
165
+ @error = nil
166
+ @metadata = Hash.new
167
+ metadata.each { |key, value| @metadata[key.to_sym] = value }
168
+
169
+ postprocess_metadata
170
+ end
171
+
172
+ private
173
+
174
+ def postprocess_metadata
175
+ @metadata[:size] = nil if @metadata[:size] == -1
176
+ @metadata[:mtime] = (@metadata[:mtime] == -1 ? nil : Time.at(@metadata[:mtime])) if @metadata[:mtime]
177
+ @metadata[:ts] = Time.parse(@metadata[:ts]) if @metadata[:ts]
178
+ end
179
+ end
180
+
181
+ # Raised when trying to access content metadata before it has been loaded.
182
+
183
+ class NotLoadedError < StandardError
184
+
185
+ # What data did you attempt to access before it was loaded? Either
186
+ # <tt>:content</tt> or <tt>:metadata</tt>.
187
+ attr_reader :data
188
+
189
+ def initialize(data) # :nodoc:
190
+ @data = data
191
+ end
192
+
193
+ def to_s # :nodoc:
194
+ "#{data.capitalize} not yet loaded -- call #load on the Dropbox::Revision instance beforehand"
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,160 @@
1
+ # Defines the Dropbox::Session class.
2
+
3
+ require 'oauth'
4
+
5
+ module Dropbox
6
+
7
+ # This class is a portal to the Dropbox API and a façade over the Ruby OAuth
8
+ # gem allowing developers to authenticate their user's Dropbox accounts.
9
+ #
10
+ # == Authenticating a user
11
+ #
12
+ # You start by creating a new instance and providing your OAuth consumer key
13
+ # and secret. You then call the authorize_url method on your new instance to
14
+ # receive the authorization URL.
15
+ #
16
+ # Once your user visits the URL, it will complete the authorization process on
17
+ # the server side. You should call the authorize method:
18
+ #
19
+ # session = Dropbox::Session.new(my_key, my_secret)
20
+ # puts "Now visit #{session.authorize_url}. Hit enter when you have completed authorization."
21
+ # gets
22
+ # session.authorize
23
+ #
24
+ # The authorize method must be called on the same instance of Dropbox::Session
25
+ # that gave you the URL. If this is unfeasible (for instance, you are doing
26
+ # this in a stateless Rails application), you can serialize the Session for
27
+ # storage (e.g., in your Rails session):
28
+ #
29
+ # def authorize
30
+ # if params[:oauth_token] then
31
+ # dropbox_session = Dropbox::Session.deserialize(session[:dropbox_session])
32
+ # dropbox_session.authorize(params)
33
+ # session[:dropbox_session] = dropbox_session.serialize # re-serialize the authenticated session
34
+ #
35
+ # redirect_to :action => 'upload'
36
+ # else
37
+ # dropbox_session = Dropbox::Session.new('your_consumer_key', 'your_consumer_secret')
38
+ # session[:dropbox_session] = dropbox_session.serialize
39
+ # redirect_to dropbox_session.authorize_url(:oauth_callback => root_url)
40
+ # end
41
+ # end
42
+ #
43
+ # == Working with the API
44
+ #
45
+ # This class includes the methods of the Dropbox::API module. See that module
46
+ # to learn how to continue using the API.
47
+
48
+ class Session
49
+ include API
50
+
51
+ # Begins the authorization process. Provide the OAuth key and secret of your
52
+ # API account, assigned by Dropbox. This is the first step in the
53
+ # authorization process.
54
+ #
55
+ # Options:
56
+ #
57
+ # +ssl+:: If true, uses SSL to connect to the Dropbox API server.
58
+
59
+ def initialize(oauth_key, oauth_secret, options={})
60
+ @ssl = options[:ssl].to_bool
61
+ @consumer = OAuth::Consumer.new(oauth_key, oauth_secret,
62
+ :site => (@ssl ? Dropbox::SSL_HOST : Dropbox::HOST),
63
+ :request_token_path => "/#{Dropbox::VERSION}/oauth/request_token",
64
+ :authorize_path => "/#{Dropbox::VERSION}/oauth/authorize",
65
+ :access_token_path => "/#{Dropbox::VERSION}/oauth/access_token")
66
+ @request_token = @consumer.get_request_token
67
+ end
68
+
69
+ # Returns a URL that is used to complete the authorization process. Visiting
70
+ # this URL is the second step in the authorization process, after creating
71
+ # the Session instance.
72
+
73
+ def authorize_url(*args)
74
+ if authorized? then
75
+ raise AlreadyAuthorizedError, "You have already been authorized; no need to get an authorization URL."
76
+ else
77
+ return @request_token.authorize_url(*args)
78
+ end
79
+ end
80
+
81
+ # Authorizes a user from the information returned by Dropbox. This is the
82
+ # third step in the authorization process, after sending the user to the
83
+ # authorize_url.
84
+ #
85
+ # You can pass to this method a hash containing the keys and values of the
86
+ # OAuth parameters returned by Dropbox. An example in Rails:
87
+ #
88
+ # session.authorize :oauth_verifier => params[:oauth_verifier]
89
+ #
90
+ # Returns a boolean indicating if authentication was successful.
91
+
92
+ def authorize(options={})
93
+ @access_token = @request_token.get_access_token(options)
94
+ @request_token = nil if @access_token
95
+ return @access_token.to_bool
96
+ end
97
+
98
+ # Returns true if this session has been authorized.
99
+
100
+ def authorized?
101
+ @access_token.to_bool
102
+ end
103
+
104
+ # Serializes this object into a string that can then be recreated with the
105
+ # Dropbox::Session.deserialize method.
106
+
107
+ def serialize
108
+ if authorized? then
109
+ [ @consumer.key, @consumer.secret, authorized?, @access_token.token, @access_token.secret ].to_yaml
110
+ else
111
+ [ @consumer.key, @consumer.secret, authorized?, @request_token.token, @request_token.secret ].to_yaml
112
+ end
113
+ end
114
+
115
+ # Deserializes an instance from a string created from the serialize method.
116
+ # Returns the recreated instance.
117
+
118
+ def self.deserialize(data)
119
+ consumer_key, consumer_secret, authorized, token, token_secret = YAML.load(StringIO.new(data))
120
+ raise ArgumentError, "Must provide a properly serialized #{self.to_s} instance" unless [ consumer_key, consumer_secret, token, token_secret ].all? and authorized == true or authorized == false
121
+
122
+ session = self.new(consumer_key, consumer_secret)
123
+ if authorized then
124
+ session.instance_variable_set :@access_token, OAuth::AccessToken.new(session.instance_variable_get(:@consumer), token, token_secret)
125
+ else
126
+ session.instance_variable_set :@request_token, OAuth::RequestToken.new(session.instance_variable_get(:@consumer), token, token_secret)
127
+ end
128
+
129
+ return session
130
+ end
131
+
132
+ def inspect # :nodoc:
133
+ "#<#{self.class.to_s} #{@consumer.key} (#{'un' unless authorized?}authorized)>"
134
+ end
135
+
136
+ private
137
+
138
+ def access_token
139
+ @access_token || raise(UnauthorizedError, "You need to authorize the Dropbox user before you can call API methods")
140
+ end
141
+
142
+ def clone_with_host(host)
143
+ session = dup
144
+ consumer = OAuth::Consumer.new(@consumer.key, @consumer.secret, :site => host)
145
+ session.instance_variable_set :@consumer, consumer
146
+ session.instance_variable_set :@access_token, OAuth::AccessToken.new(consumer, @access_token.token, @access_token.secret)
147
+ return session
148
+ end
149
+ end
150
+
151
+ # Raised when trying to call Dropbox API methods without yet having completed
152
+ # the OAuth process.
153
+
154
+ class UnauthorizedError < StandardError; end
155
+
156
+ # Raised when trying to call Dropbox::Session#authorize_url on an already
157
+ # authorized session.
158
+
159
+ class AlreadyAuthorizedError < StandardError; end
160
+ end
data/lib/dropbox.rb ADDED
@@ -0,0 +1,43 @@
1
+ # Defines the Dropbox module.
2
+
3
+ require 'cgi'
4
+ require 'yaml'
5
+ require 'digest/sha1'
6
+ require 'thread'
7
+ require 'set'
8
+ require 'time'
9
+ require 'tempfile'
10
+
11
+ Dir.glob("#{File.expand_path File.dirname(__FILE__)}/extensions/*.rb") { |file| require file }
12
+ Dir.glob("#{File.expand_path File.dirname(__FILE__)}/dropbox/*.rb") { |file| require file }
13
+
14
+ # Container module for the all Dropbox API classes.
15
+
16
+ module Dropbox
17
+ # The API version this client works with.
18
+ VERSION = "0"
19
+ # The host serving API requests.
20
+ HOST = "http://api.dropbox.com"
21
+ # The SSL host serving API requests.
22
+ SSL_HOST = "https://api.dropbox.com"
23
+ # Alternate hosts for other API requests.
24
+ ALTERNATE_HOSTS = { 'event_content' => 'http://api-content.dropbox.com', 'files' => "http://api-content.dropbox.com" }
25
+ # Alternate SSL hosts for other API requests.
26
+ ALTERNATE_SSL_HOSTS = { 'event_content' => 'https://api-content.dropbox.com', 'files' => "https://api-content.dropbox.com" }
27
+
28
+ def self.api_url(*paths_and_options) # :nodoc:
29
+ params = paths_and_options.extract_options!
30
+ ssl = params.delete(:ssl)
31
+ host = (ssl ? ALTERNATE_SSL_HOSTS[paths_and_options.first] : ALTERNATE_HOSTS[paths_and_options.first]) || (ssl ? SSL_HOST : HOST)
32
+ url = "#{host}/#{VERSION}/#{paths_and_options.map { |path_elem| CGI.escape path_elem.to_s }.join('/')}"
33
+ url.gsub! '+', '%20' # dropbox doesn't really like plusses
34
+ url << "?#{params.map { |k,v| CGI.escape(k.to_s) + "=" + CGI.escape(v.to_s) }.join('&')}" unless params.empty?
35
+ return url
36
+ end
37
+
38
+ def self.check_path(path) # :nodoc:
39
+ raise ArgumentError, "Backslashes are not allowed in Dropbox paths" if path.include?('\\')
40
+ raise ArgumentError, "Dropbox paths are limited to 256 characters in length" if path.size > 256
41
+ return path
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ class Array # :nodoc:
2
+ def extract_options! # :nodoc:
3
+ last.is_a?(::Hash) ? pop : {}
4
+ end unless method_defined?(:extract_options!)
5
+
6
+ def to_hash # :nodoc:
7
+ inject({}) { |hsh, (k,v)| hsh[k] = v ; hsh }
8
+ end unless method_defined?(:to_hash)
9
+ end
@@ -0,0 +1,61 @@
1
+ class Hash # :nodoc:
2
+ def slice(*keys) #:nodoc:
3
+ keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
4
+ hash = self.class.new
5
+ keys.each { |k| hash[k] = self[k] if has_key?(k) }
6
+ hash
7
+ end unless method_defined?(:slice)
8
+
9
+ def symbolize_keys # :nodoc:
10
+ inject({}) do |options, (key, value)|
11
+ options[(key.to_sym rescue key) || key] = value
12
+ options
13
+ end
14
+ end unless method_defined?(:symbolize_keys)
15
+
16
+ def symbolize_keys! # :nodoc:
17
+ self.replace(self.symbolize_keys)
18
+ end unless method_defined?(:symbolize_keys!)
19
+
20
+ def symbolize_keys_recursively # :nodoc:
21
+ hsh = symbolize_keys
22
+ hsh.each { |k, v| hsh[k] = v.symbolize_keys_recursively if v.kind_of?(Hash) }
23
+ hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.symbolize_keys_recursively : i } if v.kind_of?(Array) }
24
+ return hsh
25
+ end
26
+
27
+ def stringify_keys # :nodoc:
28
+ inject({}) do |options, (key, value)|
29
+ options[(key.to_s rescue key) || key] = value
30
+ options
31
+ end
32
+ end unless method_defined?(:stringify_keys)
33
+
34
+ def stringify_keys! # :nodoc:
35
+ self.replace(self.stringify_keys)
36
+ end unless method_defined?(:stringify_keys!)
37
+
38
+ def stringify_keys_recursively # :nodoc:
39
+ hsh = stringify_keys
40
+ hsh.each { |k, v| hsh[k] = v.stringify_keys_recursively if v.kind_of?(Hash) }
41
+ hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.stringify_keys_recursively : i } if v.kind_of?(Array) }
42
+ return hsh
43
+ end
44
+
45
+ def to_struct # :nodoc:
46
+ struct = Struct.new(*keys).new(*values)
47
+ # attach methods for any predicate keys, since Struct.new doesn't seem to do that
48
+ pred_keys = slice(*(keys.select { |key| key.to_s.ends_with?('?') }))
49
+ pred_keys.each do |key, val|
50
+ struct.eigenclass.send(:define_method, key.to_sym) { return val }
51
+ end
52
+ return struct
53
+ end
54
+
55
+ def to_struct_recursively # :nodoc:
56
+ hsh = dup
57
+ hsh.each { |k, v| hsh[k] = v.to_struct_recursively if v.kind_of?(Hash) }
58
+ hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.to_struct_recursively : i } if v.kind_of?(Array) }
59
+ return hsh.to_struct
60
+ end
61
+ end