fakes3 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -2,3 +2,4 @@ pkg/*
2
2
  *.gem
3
3
  .bundle
4
4
  tmp
5
+ test_root
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- fakes3 (0.1.0)
4
+ fakes3 (0.1.1)
5
5
  builder
6
6
  thor
7
7
 
data/README.md CHANGED
@@ -12,9 +12,11 @@ FakeS3 doesn't support all of the S3 command set, but the basic ones like put, g
12
12
  list, copy, and make bucket are supported. More coming soon.
13
13
 
14
14
  ## Installation
15
+
15
16
  gem install fakes3
16
17
 
17
18
  ## Running
19
+
18
20
  To run a fakes3 server, you just specify a root and a port.
19
21
 
20
22
  fakes3 -r /mnt/fakes3_root -p 4567
@@ -25,6 +27,8 @@ Take a look at the test cases to see client example usage. For now, FakeS3 is
25
27
  mainly tested with s3cmd, aws-s3 gem, and right_aws. There are plenty more
26
28
  libraries out there, and please do mention if other clients work or not.
27
29
 
30
+ Here is a running list of [supported clients](https://github.com/jubos/fake-s3/wiki/Supported-Clients "Supported Clients")
31
+
28
32
  ## Running Tests
29
33
 
30
34
  Start the test server using
@@ -39,4 +43,4 @@ It is a TODO to get this to be just one command
39
43
 
40
44
  ## More Information
41
45
 
42
- Check out the wiki https://github.com/jubos/fake-s3/wiki
46
+ Check out the [wiki](https://github.com/jubos/fake-s3/wiki)
data/fakes3.gemspec CHANGED
@@ -18,9 +18,10 @@ Gem::Specification.new do |s|
18
18
  s.add_development_dependency "aws-s3"
19
19
  s.add_development_dependency "right_aws"
20
20
  #s.add_development_dependency "aws-sdk"
21
- #s.add_development_dependency "ruby-debug19"
21
+ s.add_development_dependency "ruby-debug19"
22
22
  s.add_dependency "thor"
23
23
  s.add_dependency "builder"
24
+ #s.add_dependency "algorithms"
24
25
 
25
26
  s.files = `git ls-files`.split("\n")
26
27
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
data/lib/fakes3/bucket.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'builder'
2
2
  require 'thread'
3
3
  require 'fakes3/s3_object'
4
- require 'fakes3/red_black_tree'
4
+ require 'fakes3/sorted_object_list'
5
5
 
6
6
  module FakeS3
7
7
  class Bucket
@@ -10,27 +10,32 @@ module FakeS3
10
10
  def initialize(name,creation_date,objects)
11
11
  @name = name
12
12
  @creation_date = creation_date
13
- @objects = SortedSet.new
14
- @objects = RedBlackTree.new
13
+ @objects = SortedObjectList.new
15
14
  objects.each do |obj|
16
15
  @objects.add(obj)
17
16
  end
18
17
  @mutex = Mutex.new
19
18
  end
20
19
 
21
- def <<(object)
22
- # Unfortunately have to synchronize here since the RB Tree is not thread
23
- # safe. Probably can get finer granularity if performance is important
20
+ def find(object_name)
24
21
  @mutex.synchronize do
25
- exists = @objects.search(object.name)
26
- if exists.nil?
27
- @objects.add(object.name,object)
28
- end
22
+ @objects.find(object_name)
29
23
  end
30
24
  end
31
25
 
32
- def delete(object_name)
33
- @objects.delete_via_key(object_name)
26
+ def add(object)
27
+ # Unfortunately have to synchronize here since the our SortedObjectList
28
+ # not thread safe. Probably can get finer granularity if performance is
29
+ # important
30
+ @mutex.synchronize do
31
+ @objects.add(object)
32
+ end
33
+ end
34
+
35
+ def remove(object)
36
+ @mutex.synchronize do
37
+ @objects.remove(object)
38
+ end
34
39
  end
35
40
 
36
41
  def query_for_range(options)
@@ -39,12 +44,9 @@ module FakeS3
39
44
  max_keys = options[:max_keys] || 1000
40
45
  delimiter = options[:delimiter]
41
46
 
42
- matches = []
43
- is_truncated = false
44
-
47
+ match_set = nil
45
48
  @mutex.synchronize do
46
- matches, is_truncated = @objects.search_for_range(
47
- marker,prefix,max_keys,delimiter)
49
+ match_set = @objects.list(options)
48
50
  end
49
51
 
50
52
  bq = BucketQuery.new
@@ -53,8 +55,8 @@ module FakeS3
53
55
  bq.prefix = prefix
54
56
  bq.max_keys = max_keys
55
57
  bq.delimiter = delimiter
56
- bq.matches = matches
57
- bq.is_truncated = is_truncated
58
+ bq.matches = match_set.matches
59
+ bq.is_truncated = match_set.is_truncated
58
60
  return bq
59
61
  end
60
62
 
data/lib/fakes3/cli.rb CHANGED
@@ -15,6 +15,7 @@ module FakeS3
15
15
  store = nil
16
16
  if options[:root]
17
17
  root = File.expand_path(options[:root])
18
+ # TODO Do some sanity checking here
18
19
  store = FileStore.new(root)
19
20
  end
20
21
 
@@ -0,0 +1,46 @@
1
+ module FakeS3
2
+ class FakeS3Exception < RuntimeError
3
+ attr_accessor :resource,:request_id
4
+
5
+ def self.metaclass; class << self; self; end; end
6
+
7
+ def self.traits(*arr)
8
+ return @traits if arr.empty?
9
+ attr_accessor *arr
10
+
11
+ arr.each do |a|
12
+ metaclass.instance_eval do
13
+ define_method( a ) do |val|
14
+ @traits ||= {}
15
+ @traits[a] = val
16
+ end
17
+ end
18
+ end
19
+
20
+ class_eval do
21
+ define_method( :initialize ) do
22
+ self.class.traits.each do |k,v|
23
+ instance_variable_set("@#{k}", v)
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ traits :message,:http_status
30
+
31
+ def code
32
+ self.class.to_s
33
+ end
34
+ end
35
+
36
+ class NoSuchBucket < FakeS3Exception
37
+ message "The bucket you tried to delete is not empty."
38
+ http_status "404"
39
+ end
40
+
41
+ class BucketNotEmpty < FakeS3Exception
42
+ message "The bucket you tried to delete is not empty."
43
+ http_status "409"
44
+ end
45
+
46
+ end
@@ -45,6 +45,10 @@ module FakeS3
45
45
  @buckets
46
46
  end
47
47
 
48
+ def get_bucket_folder(bucket)
49
+ File.join(@root,bucket.name)
50
+ end
51
+
48
52
  def get_bucket(bucket)
49
53
  @bucket_hash[bucket]
50
54
  end
@@ -58,12 +62,20 @@ module FakeS3
58
62
  end
59
63
  end
60
64
 
61
- def get_object(bucket,object, request)
65
+ def delete_bucket(bucket_name)
66
+ bucket = get_bucket(bucket_name)
67
+ raise NoSuchBucket if !bucket
68
+ raise BucketNotEmpty if bucket.objects.count > 0
69
+ FileUtils.rm_r(get_bucket_folder(bucket))
70
+ @bucket_hash.delete(bucket_name)
71
+ end
72
+
73
+ def get_object(bucket,object_name, request)
62
74
  begin
63
75
  real_obj = S3Object.new
64
- obj_root = File.join(@root,bucket,object,SHUCK_METADATA_DIR)
76
+ obj_root = File.join(@root,bucket,object_name,SHUCK_METADATA_DIR)
65
77
  metadata = YAML.parse(File.open(File.join(obj_root,"metadata"),'rb').read)
66
- real_obj.name = object
78
+ real_obj.name = object_name
67
79
  real_obj.md5 = metadata[:md5].value
68
80
  real_obj.content_type = metadata[:content_type] ? metadata[:content_type].value : "application/octet-stream"
69
81
  #real_obj.io = File.open(File.join(obj_root,"content"),'rb')
@@ -78,14 +90,13 @@ module FakeS3
78
90
  def object_metadata(bucket,object)
79
91
  end
80
92
 
81
- def copy_object(src_bucket,src_object,dst_bucket,dst_object)
82
- src_root = File.join(@root,src_bucket,src_object,SHUCK_METADATA_DIR)
83
- src_obj = S3Object.new
93
+ def copy_object(src_bucket_name,src_name,dst_bucket_name,dst_name)
94
+ src_root = File.join(@root,src_bucket_name,src_name,SHUCK_METADATA_DIR)
84
95
  src_metadata_filename = File.join(src_root,"metadata")
85
96
  src_metadata = YAML.parse(File.open(src_metadata_filename,'rb').read)
86
97
  src_content_filename = File.join(src_root,"content")
87
98
 
88
- dst_filename= File.join(@root,dst_bucket,dst_object)
99
+ dst_filename= File.join(@root,dst_bucket_name,dst_name)
89
100
  FileUtils.mkdir_p(dst_filename)
90
101
 
91
102
  metadata_dir = File.join(dst_filename,SHUCK_METADATA_DIR)
@@ -106,9 +117,17 @@ module FakeS3
106
117
  end
107
118
  end
108
119
 
120
+ src_bucket = self.get_bucket(src_bucket_name)
121
+ dst_bucket = self.get_bucket(dst_bucket_name)
122
+
109
123
  obj = S3Object.new
124
+ obj.name = dst_name
110
125
  obj.md5 = src_metadata[:md5]
111
126
  obj.content_type = src_metadata[:content_type]
127
+
128
+ src_obj = src_bucket.find(src_name)
129
+ dst_bucket.add(obj)
130
+ src_bucket.remove(src_obj)
112
131
  return obj
113
132
  end
114
133
 
@@ -146,8 +165,7 @@ module FakeS3
146
165
  obj.md5 = metadata_struct[:md5]
147
166
  obj.content_type = metadata_struct[:content_type]
148
167
 
149
- # TODO need a semaphore here since bucket is not probably not thread safe
150
- bucket << obj
168
+ bucket.add(obj)
151
169
  return obj
152
170
  rescue
153
171
  puts $!
@@ -160,7 +178,8 @@ module FakeS3
160
178
  begin
161
179
  filename = File.join(@root,bucket.name,object_name)
162
180
  FileUtils.rm_rf(filename)
163
- bucket.delete(object_name)
181
+ object = bucket.find(object_name)
182
+ bucket.remove(object)
164
183
  rescue
165
184
  puts $!
166
185
  $!.backtrace.each { |line| puts line }
@@ -3,6 +3,14 @@ module FakeS3
3
3
  include Comparable
4
4
  attr_accessor :name,:size,:creation_date,:md5,:io,:content_type
5
5
 
6
+ def hash
7
+ @name.hash
8
+ end
9
+
10
+ def eql?(object)
11
+ @name == object.name
12
+ end
13
+
6
14
  # Sort by the object's name
7
15
  def <=>(object)
8
16
  @name <=> object.name
data/lib/fakes3/server.rb CHANGED
@@ -3,6 +3,7 @@ require 'fakes3/file_store'
3
3
  require 'fakes3/xml_adapter'
4
4
  require 'fakes3/bucket_query'
5
5
  require 'fakes3/unsupported_operation'
6
+ require 'fakes3/errors'
6
7
 
7
8
  module FakeS3
8
9
  class Request
@@ -139,8 +140,8 @@ module FakeS3
139
140
  response['Content-Type'] = "text/xml"
140
141
  end
141
142
 
143
+ # Posts aren't supported yet
142
144
  def do_POST(request,response)
143
- p request
144
145
  end
145
146
 
146
147
  def do_DELETE(request,response)
@@ -150,6 +151,8 @@ module FakeS3
150
151
  when Request::DELETE_OBJECT
151
152
  bucket_obj = @store.get_bucket(s_req.bucket)
152
153
  @store.delete_object(bucket_obj,s_req.object,s_req.webrick_request)
154
+ when Request::DELETE_BUCKET
155
+ @store.delete_bucket(s_req.bucket)
153
156
  end
154
157
 
155
158
  response.status = 204
@@ -0,0 +1,100 @@
1
+ require 'set'
2
+ module FakeS3
3
+ class S3MatchSet
4
+ attr_accessor :matches,:is_truncated
5
+ def initialize
6
+ @matches = []
7
+ @is_truncated = false
8
+ end
9
+ end
10
+
11
+ # This class has some of the semantics necessary for how buckets can return
12
+ # their items
13
+ #
14
+ # It is currently implemented naively as a sorted set + hash If you are going
15
+ # to try to put massive lists inside buckets and ls them, you will be sorely
16
+ # disappointed about this performance.
17
+ class SortedObjectList
18
+
19
+ def initialize
20
+ @sorted_set = SortedSet.new
21
+ @object_map = {}
22
+ @mutex = Mutex.new
23
+ end
24
+
25
+ def count
26
+ @sorted_set.count
27
+ end
28
+
29
+ def find(object_name)
30
+ @object_map[object_name]
31
+ end
32
+
33
+ # Add an S3 object into the sorted list
34
+ def add(s3_object)
35
+ return if !s3_object
36
+
37
+ @object_map[s3_object.name] = s3_object
38
+ @sorted_set << s3_object
39
+ end
40
+
41
+ def remove(s3_object)
42
+ return if !s3_object
43
+
44
+ @object_map.delete(s3_object.name)
45
+ @sorted_set.delete(s3_object)
46
+ end
47
+
48
+ # Return back a set of matches based on the passed in options
49
+ #
50
+ # options:
51
+ #
52
+ # :marker : a string to start the lexographical search (it is not included
53
+ # in the result)
54
+ # :max_keys : a maximum number of results
55
+ # :prefix : a string to filter the results by
56
+ # :delimiter : not supported yet
57
+ def list(options)
58
+ marker = options[:marker]
59
+ prefix = options[:prefix]
60
+ max_keys = options[:max_keys] || 1000
61
+ delimiter = options[:delimiter]
62
+
63
+ ms = S3MatchSet.new
64
+
65
+ marker_found = true
66
+ pseudo = nil
67
+ if marker
68
+ marker_found = false
69
+ if !@object_map[marker]
70
+ pseudo = S3Object.new
71
+ pseudo.name = marker
72
+ @sorted_set << pseudo
73
+ end
74
+ end
75
+
76
+ count = 0
77
+ @sorted_set.each do |s3_object|
78
+ if marker_found && (!prefix or s3_object.name.index(prefix) == 0)
79
+ count += 1
80
+ if count <= max_keys
81
+ ms.matches << s3_object
82
+ else
83
+ is_truncated = true
84
+ break
85
+ end
86
+ end
87
+
88
+ if marker and marker == s3_object.name
89
+ marker_found = true
90
+ end
91
+ end
92
+
93
+ if pseudo
94
+ @sorted_set.delete(pseudo)
95
+ end
96
+
97
+ return ms
98
+ end
99
+ end
100
+ end
@@ -1,3 +1,3 @@
1
1
  module FakeS3
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -24,6 +24,19 @@ module FakeS3
24
24
  output
25
25
  end
26
26
 
27
+ def self.error(error)
28
+ output = ""
29
+ xml = Builder::XmlMarkup.new(:target => output)
30
+ xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
31
+ xml.Error { |err|
32
+ err.Code(error.code)
33
+ err.Message(error.message)
34
+ err.Resource(error.resource)
35
+ err.RequestId(1)
36
+ }
37
+ output
38
+ end
39
+
27
40
  # <?xml version="1.0" encoding="UTF-8"?>
28
41
  #<Error>
29
42
  # <Code>NoSuchKey</Code>
@@ -45,6 +58,19 @@ module FakeS3
45
58
  output
46
59
  end
47
60
 
61
+ def self.error_bucket_not_empty(name)
62
+ output = ""
63
+ xml = Builder::XmlMarkup.new(:target => output)
64
+ xml.instruct! :xml, :version=>"1.0", :encoding=>"UTF-8"
65
+ xml.Error { |err|
66
+ err.Code("BucketNotEmpty")
67
+ err.Message("The bucket you tried to delete is not empty.")
68
+ err.Resource(name)
69
+ err.RequestId(1)
70
+ }
71
+ output
72
+ end
73
+
48
74
  def self.error_no_such_key(name)
49
75
  output = ""
50
76
  xml = Builder::XmlMarkup.new(:target => output)
@@ -89,6 +115,12 @@ module FakeS3
89
115
  def self.append_objects_to_list_bucket_result(lbr,objects)
90
116
  return if objects.nil? or objects.size == 0
91
117
 
118
+ if objects.index(nil)
119
+ require 'ruby-debug'
120
+ Debugger.start
121
+ debugger
122
+ end
123
+
92
124
  objects.each do |s3_object|
93
125
  lbr.Contents { |contents|
94
126
  contents.Key(s3_object.name)
@@ -1,6 +1,6 @@
1
1
  require 'test/test_helper'
2
2
  require 'fileutils'
3
- require 'fakes3/server'
3
+ #require 'fakes3/server'
4
4
  require 'right_aws'
5
5
 
6
6
  class RightAWSCommandsTest < Test::Unit::TestCase
@@ -1,13 +1,16 @@
1
1
  require 'test/test_helper'
2
2
  require 'fileutils'
3
- require 'fakes3/server'
3
+ #require 'fakes3/server'
4
4
  require 'aws/s3'
5
5
 
6
6
  class S3CommandsTest < Test::Unit::TestCase
7
7
  include AWS::S3
8
8
 
9
9
  def setup
10
- AWS::S3::Base.establish_connection!(:access_key_id => "123", :secret_access_key => "abc", :server => "localhost", :port => "10453" )
10
+ AWS::S3::Base.establish_connection!(:access_key_id => "123",
11
+ :secret_access_key => "abc",
12
+ :server => "localhost",
13
+ :port => "10453" )
11
14
  end
12
15
 
13
16
  def teardown
@@ -17,6 +20,23 @@ class S3CommandsTest < Test::Unit::TestCase
17
20
  def test_create_bucket
18
21
  bucket = Bucket.create("ruby_aws_s3")
19
22
  assert_not_nil bucket
23
+
24
+ bucket_names = []
25
+ Service.buckets.each do |bucket|
26
+ bucket_names << bucket.name
27
+ end
28
+ assert(bucket_names.index("ruby_aws_s3") >= 0)
29
+ end
30
+
31
+ def test_destroy_bucket
32
+ Bucket.create("deletebucket")
33
+ Bucket.delete("deletebucket")
34
+
35
+ begin
36
+ bucket = Bucket.find("deletebucket")
37
+ assert_fail("Shouldn't succeed here")
38
+ rescue
39
+ end
20
40
  end
21
41
 
22
42
  def test_store
@@ -102,11 +122,29 @@ class S3CommandsTest < Test::Unit::TestCase
102
122
  S3Object.store("something_to_delete","asdf","ruby_aws_s3")
103
123
  something = S3Object.find("something_to_delete","ruby_aws_s3")
104
124
  S3Object.delete("something_to_delete","ruby_aws_s3")
125
+
126
+ assert_raise AWS::S3::NoSuchKey do
127
+ should_throw = S3Object.find("something_to_delete","find_bucket")
128
+ end
129
+ end
130
+
131
+ def test_rename
132
+ bucket = Bucket.create("ruby_aws_s3")
133
+ S3Object.store("something_to_rename","asdf","ruby_aws_s3")
134
+ S3Object.rename("something_to_rename","renamed","ruby_aws_s3")
135
+
136
+ renamed = S3Object.find("renamed","ruby_aws_s3")
137
+ assert_not_nil(renamed)
138
+ assert_equal(renamed.value,'asdf')
139
+
140
+ assert_raise AWS::S3::NoSuchKey do
141
+ should_throw = S3Object.find("something_to_rename","ruby_aws_s3")
142
+ end
105
143
  end
106
144
 
107
145
  def test_larger_lists
108
146
  Bucket.create("ruby_aws_s3_many")
109
- (0..100).each do |i|
147
+ (0..50).each do |i|
110
148
  ('a'..'z').each do |letter|
111
149
  name = "#{letter}#{i}"
112
150
  S3Object.store(name,"asdf","ruby_aws_s3_many")
@@ -114,10 +152,11 @@ class S3CommandsTest < Test::Unit::TestCase
114
152
  end
115
153
 
116
154
  bucket = Bucket.find("ruby_aws_s3_many")
117
- assert_equal(bucket.objects.first.key,"a0")
118
155
  assert_equal(bucket.size,1000)
156
+ assert_equal(bucket.objects.first.key,"a0")
119
157
  end
120
158
 
159
+
121
160
  # Copying an object
122
161
  #S3Object.copy 'headshot.jpg', 'headshot2.jpg', 'photos'
123
162
 
data/test/s3cmd_test.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  require 'test/test_helper'
2
2
  require 'fileutils'
3
- require 'fakes3/server'
4
3
 
5
4
  # You need to have s3cmd installed to use this
5
+ # Also, s3cmd doesn't support path style requests, so in order to properly test
6
+ # it you need to modify your dns by changing /etc/hosts or using dnsmasq
6
7
  class S3CmdTest < Test::Unit::TestCase
7
8
 
8
9
  def setup
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 1
9
- version: 0.1.1
8
+ - 2
9
+ version: 0.1.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - Curtis Spencer
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2012-04-16 00:00:00 -06:00
17
+ date: 2012-04-16 00:00:00 -07:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -59,7 +59,7 @@ dependencies:
59
59
  type: :development
60
60
  version_requirements: *id003
61
61
  - !ruby/object:Gem::Dependency
62
- name: thor
62
+ name: ruby-debug19
63
63
  prerelease: false
64
64
  requirement: &id004 !ruby/object:Gem::Requirement
65
65
  none: false
@@ -69,10 +69,10 @@ dependencies:
69
69
  segments:
70
70
  - 0
71
71
  version: "0"
72
- type: :runtime
72
+ type: :development
73
73
  version_requirements: *id004
74
74
  - !ruby/object:Gem::Dependency
75
- name: builder
75
+ name: thor
76
76
  prerelease: false
77
77
  requirement: &id005 !ruby/object:Gem::Requirement
78
78
  none: false
@@ -84,6 +84,19 @@ dependencies:
84
84
  version: "0"
85
85
  type: :runtime
86
86
  version_requirements: *id005
87
+ - !ruby/object:Gem::Dependency
88
+ name: builder
89
+ prerelease: false
90
+ requirement: &id006 !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ segments:
96
+ - 0
97
+ version: "0"
98
+ type: :runtime
99
+ version_requirements: *id006
87
100
  description: Use FakeS3 to test basic S3 functionality without actually connecting to S3
88
101
  email:
89
102
  - thorin@gmail.com
@@ -105,11 +118,12 @@ files:
105
118
  - lib/fakes3/bucket.rb
106
119
  - lib/fakes3/bucket_query.rb
107
120
  - lib/fakes3/cli.rb
121
+ - lib/fakes3/errors.rb
108
122
  - lib/fakes3/file_store.rb
109
123
  - lib/fakes3/rate_limitable_file.rb
110
- - lib/fakes3/red_black_tree.rb
111
124
  - lib/fakes3/s3_object.rb
112
125
  - lib/fakes3/server.rb
126
+ - lib/fakes3/sorted_object_list.rb
113
127
  - lib/fakes3/unsupported_operation.rb
114
128
  - lib/fakes3/version.rb
115
129
  - lib/fakes3/xml_adapter.rb
@@ -1,395 +0,0 @@
1
- # Algorithm based on "Introduction to Algorithms" by Cormen and others
2
- # Retrieved from
3
- # https://github.com/headius/redblack/blob/master/red_black_tree.rb
4
- #
5
- # Modified by Curtis Spencer
6
- class RedBlackTree
7
- class Node
8
- attr_accessor :color
9
- attr_accessor :key
10
- attr_accessor :left
11
- attr_accessor :right
12
- attr_accessor :parent
13
- attr_accessor :value
14
-
15
- RED = :red
16
- BLACK = :black
17
- COLORS = [RED, BLACK].freeze
18
-
19
- def initialize(key, value, color = RED)
20
- raise ArgumentError, "Bad value for color parameter" unless COLORS.include?(color)
21
- @color = color
22
- @key = key
23
- @value = value
24
- @left = @right = @parent = NilNode.instance
25
- end
26
-
27
- def black?
28
- return color == BLACK
29
- end
30
-
31
- def red?
32
- return color == RED
33
- end
34
- end
35
-
36
- class NilNode < Node
37
- class << self
38
- private :new
39
- @instance = nil
40
-
41
- # it's not thread safe
42
- def instance
43
- if @instance.nil?
44
- @instance = new
45
-
46
- def instance
47
- return @instance
48
- end
49
- end
50
-
51
- return @instance
52
- end
53
- end
54
-
55
- def initialize
56
- self.color = BLACK
57
- self.key = 0
58
- self.left = nil
59
- self.right = nil
60
- self.parent = nil
61
- end
62
-
63
- def nil?
64
- return true
65
- end
66
- end
67
-
68
- include Enumerable
69
-
70
- attr_accessor :root
71
- attr_accessor :size
72
-
73
- def initialize
74
- self.root = NilNode.instance
75
- self.size = 0
76
- end
77
-
78
- def add(key,value)
79
- insert(Node.new(key,value))
80
- end
81
-
82
- def insert(x)
83
- insert_helper(x)
84
-
85
- x.color = Node::RED
86
- while x != root && x.parent.color == Node::RED
87
- if x.parent == x.parent.parent.left
88
- y = x.parent.parent.right
89
- if !y.nil? && y.color == Node::RED
90
- x.parent.color = Node::BLACK
91
- y.color = Node::BLACK
92
- x.parent.parent.color = Node::RED
93
- x = x.parent.parent
94
- else
95
- if x == x.parent.right
96
- x = x.parent
97
- left_rotate(x)
98
- end
99
- x.parent.color = Node::BLACK
100
- x.parent.parent.color = Node::RED
101
- right_rotate(x.parent.parent)
102
- end
103
- else
104
- y = x.parent.parent.left
105
- if !y.nil? && y.color == Node::RED
106
- x.parent.color = Node::BLACK
107
- y.color = Node::BLACK
108
- x.parent.parent.color = Node::RED
109
- x = x.parent.parent
110
- else
111
- if x == x.parent.left
112
- x = x.parent
113
- right_rotate(x)
114
- end
115
- x.parent.color = Node::BLACK
116
- x.parent.parent.color = Node::RED
117
- left_rotate(x.parent.parent)
118
- end
119
- end
120
- end
121
- root.color = Node::BLACK
122
- end
123
-
124
- alias << insert
125
-
126
- def delete(z)
127
- y = (z.left.nil? || z.right.nil?) ? z : successor(z)
128
- x = y.left.nil? ? y.right : y.left
129
- x.parent = y.parent
130
-
131
- if y.parent.nil?
132
- self.root = x
133
- else
134
- if y == y.parent.left
135
- y.parent.left = x
136
- else
137
- y.parent.right = x
138
- end
139
- end
140
-
141
- z.key = y.key if y != z
142
-
143
- if y.color == Node::BLACK
144
- delete_fixup(x)
145
- end
146
-
147
- self.size -= 1
148
- return y
149
- end
150
-
151
- def minimum(x = root)
152
- while !x.left.nil?
153
- x = x.left
154
- end
155
- return x
156
- end
157
-
158
- def maximum(x = root)
159
- while !x.right.nil?
160
- x = x.right
161
- end
162
- return x
163
- end
164
-
165
- def successor(x)
166
- if !x.right.nil?
167
- return minimum(x.right)
168
- end
169
- y = x.parent
170
- while !y.nil? && x == y.right
171
- x = y
172
- y = y.parent
173
- end
174
- return y
175
- end
176
-
177
- def predecessor(x)
178
- if !x.left.nil?
179
- return maximum(x.left)
180
- end
181
- y = x.parent
182
- while !y.nil? && x == y.left
183
- x = y
184
- y = y.parent
185
- end
186
- return y
187
- end
188
-
189
- def inorder_walk(x = root)
190
- x = self.minimum
191
- while !x.nil?
192
- yield x.key
193
- x = successor(x)
194
- end
195
- end
196
-
197
- alias each inorder_walk
198
-
199
- def reverse_inorder_walk(x = root)
200
- x = self.maximum
201
- while !x.nil?
202
- yield x.key
203
- x = predecessor(x)
204
- end
205
- end
206
-
207
- alias reverse_each reverse_inorder_walk
208
-
209
- def search(key, x = root)
210
- while !x.nil? && x.key != key
211
- key < x.key ? x = x.left : x = x.right
212
- end
213
- return x
214
- end
215
-
216
- # Search for a successor even if the key doesn't exist
217
- def search_for_possible_successor(key, x = root)
218
- node = search(key)
219
- pseudo = false
220
- if node.class == NilNode
221
- insert(Node.new(key,nil))
222
- node = search(key)
223
- pseudo = true
224
- end
225
-
226
- succ = successor(node)
227
- delete(node) if pseudo
228
- return nil if !succ
229
- return succ
230
- end
231
-
232
- def delete_via_key(key)
233
- node = search(key)
234
- return if node.class == NilNode
235
- delete(node)
236
- end
237
-
238
- # Return array of matches and a boolean to denote whether truncation occurred
239
- def search_for_range(marker,prefix,max_keys,delimiter)
240
- succ = nil
241
- if marker
242
- succ = search_for_possible_successor(marker)
243
- else
244
- succ = self.minimum
245
- end
246
-
247
- return [],false if succ.class == NilNode
248
-
249
- keys_count = 0
250
- keys = []
251
- is_truncated = false
252
-
253
- loop do
254
- if !prefix or succ.key.index(prefix) == 0
255
- keys_count += 1
256
- if keys_count <= max_keys
257
- keys << succ.value
258
- else
259
- is_truncated = true
260
- break
261
- end
262
- end
263
- succ = successor(succ)
264
- if succ.class == NilNode
265
- break
266
- end
267
- end
268
-
269
- return keys,is_truncated
270
- end
271
-
272
- def empty?
273
- return self.root.nil?
274
- end
275
-
276
- def black_height(x = root)
277
- height = 0
278
- while !x.nil?
279
- x = x.left
280
- height +=1 if x.nil? || x.black?
281
- end
282
- return height
283
- end
284
-
285
- private
286
-
287
- def left_rotate(x)
288
- raise "x.right is nil!" if x.right.nil?
289
- y = x.right
290
- x.right = y.left
291
- y.left.parent = x if !y.left.nil?
292
- y.parent = x.parent
293
- if x.parent.nil?
294
- self.root = y
295
- else
296
- if x == x.parent.left
297
- x.parent.left = y
298
- else
299
- x.parent.right = y
300
- end
301
- end
302
- y.left = x
303
- x.parent = y
304
- end
305
-
306
- def right_rotate(x)
307
- raise "x.left is nil!" if x.left.nil?
308
- y = x.left
309
- x.left = y.right
310
- y.right.parent = x if !y.right.nil?
311
- y.parent = x.parent
312
- if x.parent.nil?
313
- self.root = y
314
- else
315
- if x == x.parent.left
316
- x.parent.left = y
317
- else
318
- x.parent.right = y
319
- end
320
- end
321
- y.right = x
322
- x.parent = y
323
- end
324
-
325
- def insert_helper(z)
326
- y = NilNode.instance
327
- x = root
328
- while !x.nil?
329
- y = x
330
- z.key < x.key ? x = x.left : x = x.right
331
- end
332
- z.parent = y
333
- if y.nil?
334
- self.root = z
335
- else
336
- z.key < y.key ? y.left = z : y.right = z
337
- end
338
- self.size += 1
339
- end
340
-
341
- def delete_fixup(x)
342
- while x != root && x.color == Node::BLACK
343
- if x == x.parent.left
344
- w = x.parent.right
345
- if w.color == Node::RED
346
- w.color = Node::BLACK
347
- x.parent.color = Node::RED
348
- left_rotate(x.parent)
349
- w = x.parent.right
350
- end
351
- if w.left.color == Node::BLACK && w.right.color == Node::BLACK
352
- w.color = Node::RED
353
- x = x.parent
354
- else
355
- if w.right.color == Node::BLACK
356
- w.left.color = Node::BLACK
357
- w.color = Node::RED
358
- right_rotate(w)
359
- w = x.parent.right
360
- end
361
- w.color = x.parent.color
362
- x.parent.color = Node::BLACK
363
- w.right.color = Node::BLACK
364
- left_rotate(x.parent)
365
- x = root
366
- end
367
- else
368
- w = x.parent.left
369
- if w.color == Node::RED
370
- w.color = Node::BLACK
371
- x.parent.color = Node::RED
372
- right_rotate(x.parent)
373
- w = x.parent.left
374
- end
375
- if w.right.color == Node::BLACK && w.left.color == Node::BLACK
376
- w.color = Node::RED
377
- x = x.parent
378
- else
379
- if w.left.color == Node::BLACK
380
- w.right.color = Node::BLACK
381
- w.color = Node::RED
382
- left_rotate(w)
383
- w = x.parent.left
384
- end
385
- w.color = x.parent.color
386
- x.parent.color = Node::BLACK
387
- w.left.color = Node::BLACK
388
- right_rotate(x.parent)
389
- x = root
390
- end
391
- end
392
- end
393
- x.color = Node::BLACK
394
- end
395
- end