fakes3 0.1.1 → 0.1.2

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/.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