tyler_koala 1.2.0beta
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.
- 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
|