tyler_koala 1.2.0beta
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +12 -0
- data/.gitignore +5 -0
- data/.travis.yml +9 -0
- data/CHANGELOG +185 -0
- data/Gemfile +11 -0
- data/LICENSE +22 -0
- data/Manifest +39 -0
- data/Rakefile +16 -0
- data/autotest/discover.rb +1 -0
- data/koala.gemspec +50 -0
- data/lib/koala.rb +119 -0
- data/lib/koala/batch_operation.rb +74 -0
- data/lib/koala/graph_api.rb +281 -0
- data/lib/koala/graph_batch_api.rb +87 -0
- data/lib/koala/graph_collection.rb +54 -0
- data/lib/koala/http_service.rb +161 -0
- data/lib/koala/oauth.rb +181 -0
- data/lib/koala/realtime_updates.rb +89 -0
- data/lib/koala/rest_api.rb +95 -0
- data/lib/koala/test_users.rb +102 -0
- data/lib/koala/uploadable_io.rb +180 -0
- data/lib/koala/utils.rb +7 -0
- data/readme.md +160 -0
- data/spec/cases/api_base_spec.rb +101 -0
- data/spec/cases/error_spec.rb +30 -0
- data/spec/cases/graph_and_rest_api_spec.rb +48 -0
- data/spec/cases/graph_api_batch_spec.rb +600 -0
- data/spec/cases/graph_api_spec.rb +42 -0
- data/spec/cases/http_service_spec.rb +420 -0
- data/spec/cases/koala_spec.rb +21 -0
- data/spec/cases/oauth_spec.rb +428 -0
- data/spec/cases/realtime_updates_spec.rb +198 -0
- data/spec/cases/rest_api_spec.rb +41 -0
- data/spec/cases/test_users_spec.rb +281 -0
- data/spec/cases/uploadable_io_spec.rb +206 -0
- data/spec/cases/utils_spec.rb +8 -0
- data/spec/fixtures/beach.jpg +0 -0
- data/spec/fixtures/cat.m4v +0 -0
- data/spec/fixtures/facebook_data.yml +61 -0
- data/spec/fixtures/mock_facebook_responses.yml +439 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/graph_api_shared_examples.rb +502 -0
- data/spec/support/json_testing_fix.rb +42 -0
- data/spec/support/koala_test.rb +163 -0
- data/spec/support/mock_http_service.rb +98 -0
- data/spec/support/ordered_hash.rb +205 -0
- data/spec/support/rest_api_shared_examples.rb +285 -0
- data/spec/support/uploadable_io_shared_examples.rb +70 -0
- metadata +221 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
# when testing across Ruby versions, we found that JSON string creation inconsistently ordered keys
|
2
|
+
# which is a problem because our mock testing service ultimately matches strings to see if requests are mocked
|
3
|
+
# this fix solves that problem by ensuring all hashes are created with a consistent key order every time
|
4
|
+
module MultiJson
|
5
|
+
self.engine = :ok_json
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def encode_with_ordering(object)
|
9
|
+
# if it's a hash, recreate it with k/v pairs inserted in sorted-by-key order
|
10
|
+
# (for some reason, REE fails if we don't assign the ternary result as a local variable
|
11
|
+
# separately from calling encode_original)
|
12
|
+
encode_original(sort_object(object))
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :encode_original, :encode
|
16
|
+
alias_method :encode, :encode_with_ordering
|
17
|
+
|
18
|
+
def decode_with_ordering(string)
|
19
|
+
sort_object(decode_original(string))
|
20
|
+
end
|
21
|
+
|
22
|
+
alias_method :decode_original, :decode
|
23
|
+
alias_method :decode, :decode_with_ordering
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def sort_object(object)
|
28
|
+
if object.is_a?(Hash)
|
29
|
+
sort_hash(object)
|
30
|
+
elsif object.is_a?(Array)
|
31
|
+
object.collect {|item| item.is_a?(Hash) ? sort_hash(item) : item}
|
32
|
+
else
|
33
|
+
object
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def sort_hash(unsorted_hash)
|
38
|
+
sorted_hash = KoalaTest::OrderedHash.new(sorted_hash)
|
39
|
+
unsorted_hash.keys.sort {|a, b| a.to_s <=> b.to_s}.inject(sorted_hash) {|hash, k| hash[k] = unsorted_hash[k]; hash}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
# small helper method for live testing
|
2
|
+
module KoalaTest
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_accessor :oauth_token, :app_id, :secret, :app_access_token, :code, :session_key
|
6
|
+
attr_accessor :oauth_test_data, :subscription_test_data
|
7
|
+
end
|
8
|
+
|
9
|
+
# Test setup
|
10
|
+
|
11
|
+
def self.setup_test_environment!
|
12
|
+
setup_rspec
|
13
|
+
|
14
|
+
unless ENV['LIVE']
|
15
|
+
# By default the Koala specs are run using stubs for HTTP requests,
|
16
|
+
# so they won't fail due to Facebook-imposed rate limits or server timeouts.
|
17
|
+
#
|
18
|
+
# However as a result they are more brittle since
|
19
|
+
# we are not testing the latest responses from the Facebook servers.
|
20
|
+
# To be certain all specs pass with the current Facebook services,
|
21
|
+
# run LIVE=true bundle exec rake spec.
|
22
|
+
Koala.http_service = Koala::MockHTTPService
|
23
|
+
KoalaTest.setup_test_data(Koala::MockHTTPService::TEST_DATA)
|
24
|
+
else
|
25
|
+
# Runs Koala specs through the Facebook servers
|
26
|
+
# using data for a real app
|
27
|
+
live_data = YAML.load_file(File.join(File.dirname(__FILE__), '../fixtures/facebook_data.yml'))
|
28
|
+
KoalaTest.setup_test_data(live_data)
|
29
|
+
|
30
|
+
# allow live tests with different adapters
|
31
|
+
adapter = ENV['ADAPTER'] || "typhoeus"# use Typhoeus by default if available
|
32
|
+
begin
|
33
|
+
require adapter
|
34
|
+
Faraday.default_adapter = adapter.to_sym
|
35
|
+
rescue LoadError
|
36
|
+
puts "Unable to load adapter #{adapter}, using Net::HTTP."
|
37
|
+
end
|
38
|
+
|
39
|
+
# use a test user unless the developer wants to test against a real profile
|
40
|
+
unless token = KoalaTest.oauth_token
|
41
|
+
KoalaTest.setup_test_users
|
42
|
+
else
|
43
|
+
KoalaTest.validate_user_info(token)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.setup_rspec
|
49
|
+
# set up a global before block to set the token for tests
|
50
|
+
# set the token up for
|
51
|
+
RSpec.configure do |config|
|
52
|
+
config.before :each do
|
53
|
+
@token = KoalaTest.oauth_token
|
54
|
+
Koala::Utils.stub(:deprecate) # never fire deprecation warnings
|
55
|
+
end
|
56
|
+
|
57
|
+
config.after :each do
|
58
|
+
# clean up any objects posted to Facebook
|
59
|
+
if @temporary_object_id && !KoalaTest.mock_interface?
|
60
|
+
api = @api || (@test_users ? @test_users.graph_api : nil)
|
61
|
+
raise "Unable to locate API when passed temporary object to delete!" unless api
|
62
|
+
|
63
|
+
# wait 10ms to allow Facebook to propagate data so we can delete it
|
64
|
+
sleep(0.01)
|
65
|
+
|
66
|
+
# clean up any objects we've posted
|
67
|
+
result = (api.delete_object(@temporary_object_id) rescue false)
|
68
|
+
# if we errored out or Facebook returned false, track that
|
69
|
+
puts "Unable to delete #{@temporary_object_id}: #{result} (probably a photo or video, which can't be deleted through the API)" unless result
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.setup_test_data(data)
|
76
|
+
# make data accessible to all our tests
|
77
|
+
self.oauth_test_data = data["oauth_test_data"]
|
78
|
+
self.subscription_test_data = data["subscription_test_data"]
|
79
|
+
self.oauth_token = data["oauth_token"]
|
80
|
+
self.app_id = data["oauth_test_data"]["app_id"]
|
81
|
+
self.app_access_token = data["oauth_test_data"]["app_access_token"]
|
82
|
+
self.secret = data["oauth_test_data"]["secret"]
|
83
|
+
self.code = data["oauth_test_data"]["code"]
|
84
|
+
self.session_key = data["oauth_test_data"]["session_key"]
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.testing_permissions
|
88
|
+
"read_stream, publish_stream, user_photos, user_videos, read_insights"
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.setup_test_users
|
92
|
+
# note: we don't have to delete the two test users explicitly, since the test user specs do that for us
|
93
|
+
# technically, this is a point of brittleness and would break if the tests were run out of order
|
94
|
+
# however, for now we can live with it since it would slow tests way too much to constantly recreate our test users
|
95
|
+
print "Setting up test users..."
|
96
|
+
@test_user_api = Koala::Facebook::TestUsers.new(:app_id => self.app_id, :secret => self.secret)
|
97
|
+
|
98
|
+
# create two test users with specific names and befriend them
|
99
|
+
@live_testing_user = @test_user_api.create(true, testing_permissions, :name => user1_name)
|
100
|
+
@live_testing_friend = @test_user_api.create(true, testing_permissions, :name => user2_name)
|
101
|
+
@test_user_api.befriend(@live_testing_user, @live_testing_friend)
|
102
|
+
self.oauth_token = @live_testing_user["access_token"]
|
103
|
+
|
104
|
+
puts "done."
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.validate_user_info(token)
|
108
|
+
print "Validating permissions for live testing..."
|
109
|
+
# make sure we have the necessary permissions
|
110
|
+
api = Koala::Facebook::API.new(token)
|
111
|
+
perms = api.fql_query("select #{testing_permissions} from permissions where uid = me()")[0]
|
112
|
+
perms.each_pair do |perm, value|
|
113
|
+
if value == (perm == "read_insights" ? 1 : 0) # live testing depends on insights calls failing
|
114
|
+
puts "failed!\n" # put a new line after the print above
|
115
|
+
raise ArgumentError, "Your access token must have the read_stream, publish_stream, and user_photos permissions, and lack read_insights. You have: #{perms.inspect}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
puts "done!"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Info about the testing environment
|
122
|
+
def self.real_user?
|
123
|
+
!(mock_interface? || @test_user)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.test_user?
|
127
|
+
!!@test_user_api
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.mock_interface?
|
131
|
+
Koala.http_service == Koala::MockHTTPService
|
132
|
+
end
|
133
|
+
|
134
|
+
# Data for testing
|
135
|
+
def self.user1
|
136
|
+
test_user? ? @live_testing_user["id"] : "koppel"
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.user1_id
|
140
|
+
test_user? ? @live_testing_user["id"] : 2905623
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.user1_name
|
144
|
+
"Alex"
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.user2
|
148
|
+
test_user? ? @live_testing_friend["id"] : "lukeshepard"
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.user2_id
|
152
|
+
test_user? ? @live_testing_friend["id"] : 2901279
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.user2_name
|
156
|
+
"Luke"
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.page
|
160
|
+
"contextoptional"
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Koala
|
5
|
+
module MockHTTPService
|
6
|
+
include Koala::HTTPService
|
7
|
+
|
8
|
+
# fix our specs to use ok_json, so we always get the same results from to_json
|
9
|
+
MultiJson.engine = :ok_json
|
10
|
+
|
11
|
+
# Mocks all HTTP requests for with koala_spec_with_mocks.rb
|
12
|
+
# Mocked values to be included in TEST_DATA used in specs
|
13
|
+
ACCESS_TOKEN = '*'
|
14
|
+
APP_ACCESS_TOKEN = "**"
|
15
|
+
OAUTH_CODE = 'OAUTHCODE'
|
16
|
+
|
17
|
+
# Loads testing data
|
18
|
+
TEST_DATA = YAML.load_file(File.join(File.dirname(__FILE__), '..', 'fixtures', 'facebook_data.yml'))
|
19
|
+
TEST_DATA.merge!('oauth_token' => Koala::MockHTTPService::ACCESS_TOKEN)
|
20
|
+
TEST_DATA['oauth_test_data'].merge!('code' => Koala::MockHTTPService::OAUTH_CODE)
|
21
|
+
|
22
|
+
# Useful in mock_facebook_responses.yml
|
23
|
+
OAUTH_DATA = TEST_DATA['oauth_test_data']
|
24
|
+
OAUTH_DATA.merge!({
|
25
|
+
'app_access_token' => APP_ACCESS_TOKEN,
|
26
|
+
'session_key' => "session_key",
|
27
|
+
'multiple_session_keys' => ["session_key", "session_key_2"]
|
28
|
+
})
|
29
|
+
APP_ID = OAUTH_DATA['app_id']
|
30
|
+
SECRET = OAUTH_DATA['secret']
|
31
|
+
SUBSCRIPTION_DATA = TEST_DATA["subscription_test_data"]
|
32
|
+
|
33
|
+
# Loads the mock response data via ERB to substitue values for TEST_DATA (see oauth/access_token)
|
34
|
+
mock_response_file_path = File.join(File.dirname(__FILE__), '..', 'fixtures', 'mock_facebook_responses.yml')
|
35
|
+
RESPONSES = YAML.load(ERB.new(IO.read(mock_response_file_path)).result(binding))
|
36
|
+
|
37
|
+
def self.make_request(path, args, verb, options = {})
|
38
|
+
path = 'root' if path == '' || path == '/'
|
39
|
+
verb ||= 'get'
|
40
|
+
server = options[:rest_api] ? 'rest_api' : 'graph_api'
|
41
|
+
token = args.delete('access_token')
|
42
|
+
with_token = (token == ACCESS_TOKEN || token == APP_ACCESS_TOKEN) ? 'with_token' : 'no_token'
|
43
|
+
|
44
|
+
# Assume format is always JSON
|
45
|
+
args.delete('format')
|
46
|
+
|
47
|
+
# Create a hash key for the arguments
|
48
|
+
args = create_params_key(args)
|
49
|
+
|
50
|
+
begin
|
51
|
+
response = RESPONSES[server][path][args][verb][with_token]
|
52
|
+
|
53
|
+
# Raises an error of with_token/no_token key is missing
|
54
|
+
raise NoMethodError unless response
|
55
|
+
|
56
|
+
# create response class object
|
57
|
+
response_object = if response.is_a? String
|
58
|
+
Koala::Response.new(200, response, {})
|
59
|
+
else
|
60
|
+
Koala::Response.new(response["code"] || 200, response["body"] || "", response["headers"] || {})
|
61
|
+
end
|
62
|
+
|
63
|
+
rescue NoMethodError
|
64
|
+
# Raises an error message with the place in the data YML
|
65
|
+
# to place a mock as well as a URL to request from
|
66
|
+
# Facebook's servers for the actual data
|
67
|
+
# (Don't forget to replace ACCESS_TOKEN with a real access token)
|
68
|
+
data_trace = [server, path, args, verb, with_token] * ': '
|
69
|
+
|
70
|
+
args = args == 'no_args' ? '' : "#{args}&"
|
71
|
+
args += 'format=json'
|
72
|
+
args += "&access_token=#{ACCESS_TOKEN}" if with_token
|
73
|
+
|
74
|
+
raise "Missing a mock response for #{data_trace}\nAPI PATH: #{[path, args].join('?')}"
|
75
|
+
end
|
76
|
+
|
77
|
+
response_object
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.encode_params(*args)
|
81
|
+
# use HTTPService's encode_params
|
82
|
+
HTTPService.encode_params(*args)
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def self.create_params_key(params_hash)
|
88
|
+
if params_hash.empty?
|
89
|
+
'no_args'
|
90
|
+
else
|
91
|
+
params_hash.sort{ |a,b| a[0].to_s <=> b[0].to_s}.map do |arr|
|
92
|
+
arr[1] = '[FILE]' if arr[1].kind_of?(Koala::UploadableIO)
|
93
|
+
arr.join('=')
|
94
|
+
end.join('&')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
module KoalaTest
|
2
|
+
# directly taken from Rails 3.1's OrderedHash
|
3
|
+
# see https://github.com/rails/rails/blob/master/activesupport/lib/active_support/ordered_hash.rb
|
4
|
+
|
5
|
+
# The order of iteration over hashes in Ruby 1.8 is undefined. For example, you do not know the
|
6
|
+
# order in which +keys+ will return keys, or +each+ yield pairs. <tt>ActiveSupport::OrderedHash</tt>
|
7
|
+
# implements a hash that preserves insertion order, as in Ruby 1.9:
|
8
|
+
#
|
9
|
+
# oh = ActiveSupport::OrderedHash.new
|
10
|
+
# oh[:a] = 1
|
11
|
+
# oh[:b] = 2
|
12
|
+
# oh.keys # => [:a, :b], this order is guaranteed
|
13
|
+
#
|
14
|
+
# <tt>ActiveSupport::OrderedHash</tt> is namespaced to prevent conflicts with other implementations.
|
15
|
+
class OrderedHash < ::Hash #:nodoc:
|
16
|
+
def to_yaml_type
|
17
|
+
"!tag:yaml.org,2002:omap"
|
18
|
+
end
|
19
|
+
|
20
|
+
def encode_with(coder)
|
21
|
+
coder.represent_seq '!omap', map { |k,v| { k => v } }
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_yaml(opts = {})
|
25
|
+
if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck?
|
26
|
+
return super
|
27
|
+
end
|
28
|
+
|
29
|
+
YAML.quick_emit(self, opts) do |out|
|
30
|
+
out.seq(taguri) do |seq|
|
31
|
+
each do |k, v|
|
32
|
+
seq.add(k => v)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def nested_under_indifferent_access
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# Hash is ordered in Ruby 1.9!
|
43
|
+
if RUBY_VERSION < '1.9'
|
44
|
+
|
45
|
+
# In MRI the Hash class is core and written in C. In particular, methods are
|
46
|
+
# programmed with explicit C function calls and polymorphism is not honored.
|
47
|
+
#
|
48
|
+
# For example, []= is crucial in this implementation to maintain the @keys
|
49
|
+
# array but hash.c invokes rb_hash_aset() originally. This prevents method
|
50
|
+
# reuse through inheritance and forces us to reimplement stuff.
|
51
|
+
#
|
52
|
+
# For instance, we cannot use the inherited #merge! because albeit the algorithm
|
53
|
+
# itself would work, our []= is not being called at all by the C code.
|
54
|
+
|
55
|
+
def initialize(*args, &block)
|
56
|
+
super
|
57
|
+
@keys = []
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.[](*args)
|
61
|
+
ordered_hash = new
|
62
|
+
|
63
|
+
if (args.length == 1 && args.first.is_a?(Array))
|
64
|
+
args.first.each do |key_value_pair|
|
65
|
+
next unless (key_value_pair.is_a?(Array))
|
66
|
+
ordered_hash[key_value_pair[0]] = key_value_pair[1]
|
67
|
+
end
|
68
|
+
|
69
|
+
return ordered_hash
|
70
|
+
end
|
71
|
+
|
72
|
+
unless (args.size % 2 == 0)
|
73
|
+
raise ArgumentError.new("odd number of arguments for Hash")
|
74
|
+
end
|
75
|
+
|
76
|
+
args.each_with_index do |val, ind|
|
77
|
+
next if (ind % 2 != 0)
|
78
|
+
ordered_hash[val] = args[ind + 1]
|
79
|
+
end
|
80
|
+
|
81
|
+
ordered_hash
|
82
|
+
end
|
83
|
+
|
84
|
+
def initialize_copy(other)
|
85
|
+
super
|
86
|
+
# make a deep copy of keys
|
87
|
+
@keys = other.keys
|
88
|
+
end
|
89
|
+
|
90
|
+
def []=(key, value)
|
91
|
+
@keys << key unless has_key?(key)
|
92
|
+
super
|
93
|
+
end
|
94
|
+
|
95
|
+
def delete(key)
|
96
|
+
if has_key? key
|
97
|
+
index = @keys.index(key)
|
98
|
+
@keys.delete_at index
|
99
|
+
end
|
100
|
+
super
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete_if
|
104
|
+
super
|
105
|
+
sync_keys!
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
def reject!
|
110
|
+
super
|
111
|
+
sync_keys!
|
112
|
+
self
|
113
|
+
end
|
114
|
+
|
115
|
+
def reject(&block)
|
116
|
+
dup.reject!(&block)
|
117
|
+
end
|
118
|
+
|
119
|
+
def keys
|
120
|
+
@keys.dup
|
121
|
+
end
|
122
|
+
|
123
|
+
def values
|
124
|
+
@keys.collect { |key| self[key] }
|
125
|
+
end
|
126
|
+
|
127
|
+
def to_hash
|
128
|
+
self
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_a
|
132
|
+
@keys.map { |key| [ key, self[key] ] }
|
133
|
+
end
|
134
|
+
|
135
|
+
def each_key
|
136
|
+
return to_enum(:each_key) unless block_given?
|
137
|
+
@keys.each { |key| yield key }
|
138
|
+
self
|
139
|
+
end
|
140
|
+
|
141
|
+
def each_value
|
142
|
+
return to_enum(:each_value) unless block_given?
|
143
|
+
@keys.each { |key| yield self[key]}
|
144
|
+
self
|
145
|
+
end
|
146
|
+
|
147
|
+
def each
|
148
|
+
return to_enum(:each) unless block_given?
|
149
|
+
@keys.each {|key| yield [key, self[key]]}
|
150
|
+
self
|
151
|
+
end
|
152
|
+
|
153
|
+
alias_method :each_pair, :each
|
154
|
+
|
155
|
+
alias_method :select, :find_all
|
156
|
+
|
157
|
+
def clear
|
158
|
+
super
|
159
|
+
@keys.clear
|
160
|
+
self
|
161
|
+
end
|
162
|
+
|
163
|
+
def shift
|
164
|
+
k = @keys.first
|
165
|
+
v = delete(k)
|
166
|
+
[k, v]
|
167
|
+
end
|
168
|
+
|
169
|
+
def merge!(other_hash)
|
170
|
+
if block_given?
|
171
|
+
other_hash.each { |k, v| self[k] = key?(k) ? yield(k, self[k], v) : v }
|
172
|
+
else
|
173
|
+
other_hash.each { |k, v| self[k] = v }
|
174
|
+
end
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
alias_method :update, :merge!
|
179
|
+
|
180
|
+
def merge(other_hash, &block)
|
181
|
+
dup.merge!(other_hash, &block)
|
182
|
+
end
|
183
|
+
|
184
|
+
# When replacing with another hash, the initial order of our keys must come from the other hash -ordered or not.
|
185
|
+
def replace(other)
|
186
|
+
super
|
187
|
+
@keys = other.keys
|
188
|
+
self
|
189
|
+
end
|
190
|
+
|
191
|
+
def invert
|
192
|
+
OrderedHash[self.to_a.map!{|key_value_pair| key_value_pair.reverse}]
|
193
|
+
end
|
194
|
+
|
195
|
+
def inspect
|
196
|
+
"#<OrderedHash #{super}>"
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
def sync_keys!
|
201
|
+
@keys.delete_if {|k| !has_key?(k)}
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|