relaxo 0.4.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -18,16 +18,43 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- # You can define `Relaxo::JSON` before loading Relaxo, and it will use this implementation without trying to load the default JSON implementation.
21
+ require 'rugged'
22
22
 
23
- module Relaxo
24
- end
23
+ require_relative 'directory'
25
24
 
26
- unless defined? Relaxo::JSON
27
- unless defined? JSON
28
- # Try to load a JSON implementation if it doesn't already exist:
29
- require 'json'
25
+ module Relaxo
26
+ class Dataset
27
+ def initialize(repository, tree)
28
+ @repository = repository
29
+ @tree = tree
30
+
31
+ @directories = {}
32
+ end
33
+
34
+ def read(path)
35
+ if entry = @tree.path(path) and entry[:type] == :blob and oid = entry[:oid]
36
+ @repository.read(oid)
37
+ end
38
+ rescue Rugged::TreeError
39
+ return nil
40
+ end
41
+
42
+ alias [] read
43
+
44
+ def exist?(path)
45
+ read(path) != nil
46
+ end
47
+
48
+ def each(path = nil, &block)
49
+ return to_enum(:each, path) unless block_given?
50
+
51
+ directory(path).each(&block)
52
+ end
53
+
54
+ protected
55
+
56
+ def directory(path = nil)
57
+ @directories[path] ||= Directory.new(@repository, @tree, path)
58
+ end
30
59
  end
31
-
32
- Relaxo::JSON = JSON
33
60
  end
@@ -0,0 +1,97 @@
1
+ # Copyright, 2017, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'rugged'
22
+
23
+ module Relaxo
24
+ class Directory
25
+ def initialize(repository, tree, path)
26
+ @repository = repository
27
+ @tree = tree
28
+ @path = path
29
+
30
+ @entries = nil
31
+ @changes = {}
32
+ end
33
+
34
+ def freeze
35
+ @changes.freeze
36
+
37
+ super
38
+ end
39
+
40
+ def entries
41
+ @entries ||= load_entries!
42
+ end
43
+
44
+ def each(&block)
45
+ return to_enum(:each) unless block_given?
46
+
47
+ entries.each do |entry|
48
+ entry[:object] ||= @repository.read(entry[:oid])
49
+
50
+ yield entry[:name], entry[:object]
51
+ end
52
+ end
53
+
54
+ def insert(entry)
55
+ _, _, name = entry[:name].rpartition('/')
56
+
57
+ @changes[name] = entry
58
+
59
+ # Blow away the cache:
60
+ @entries = nil
61
+ end
62
+
63
+ def delete(entry)
64
+ _, _, name = entry[:name].rpartition('/')
65
+
66
+ @changes[name] = nil
67
+
68
+ # Blow away the cache:
69
+ @entries = nil
70
+ end
71
+
72
+ private
73
+
74
+ def fetch_tree(path = @path)
75
+ entry = @tree.path(path)
76
+
77
+ Rugged::Tree.new(@repository, entry[:oid])
78
+ rescue Rugged::TreeError
79
+ return nil
80
+ end
81
+
82
+ # Load the entries from the tree, applying any changes.
83
+ def load_entries!
84
+ entries = @changes.dup
85
+
86
+ if tree = fetch_tree
87
+ tree.each_blob do |entry|
88
+ unless entries.key? entry[:name]
89
+ entries[entry[:name]] = entry
90
+ end
91
+ end
92
+ end
93
+
94
+ return entries.values.compact.sort_by{|entry| entry[:name]}
95
+ end
96
+ end
97
+ end
@@ -19,5 +19,5 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  module Relaxo
22
- VERSION = "0.4.7"
22
+ VERSION = "1.0.0"
23
23
  end
@@ -22,8 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_dependency "json", "~> 1.8"
26
- spec.add_dependency "rest-client"
25
+ spec.add_dependency "rugged"
27
26
 
28
27
  spec.add_development_dependency "rspec", "~> 3.4.0"
29
28
  spec.add_development_dependency "bundler", "~> 1.3"
@@ -0,0 +1,39 @@
1
+
2
+ require_relative 'test_records'
3
+
4
+ RSpec.describe Relaxo::Changeset do
5
+ include_context "test records"
6
+
7
+ it "should enumerate all documents including writes" do
8
+ records = []
9
+
10
+ database.commit(message: "Testing Enumeration") do |dataset|
11
+ 5.times do |i|
12
+ object = dataset.append("extra-#{i}")
13
+ dataset.write("#{prefix}/extra-#{i}", object)
14
+ end
15
+
16
+ expect(dataset.exist?("#{prefix}/extra-0")).to be_truthy
17
+
18
+ records = dataset.each(prefix).to_a
19
+ end
20
+
21
+ expect(records.count).to be 25
22
+ end
23
+
24
+ it "should enumerate all documents excluding deletes" do
25
+ records = []
26
+
27
+ database.commit(message: "Testing Enumeration") do |dataset|
28
+ 5.times do |i|
29
+ dataset.delete("#{prefix}/#{i}")
30
+ end
31
+
32
+ expect(dataset.exist?("#{prefix}/0")).to be_falsey
33
+
34
+ records = dataset.each(prefix).to_a
35
+ end
36
+
37
+ expect(records.count).to be 15
38
+ end
39
+ end
@@ -0,0 +1,39 @@
1
+
2
+ require_relative 'test_records'
3
+
4
+ RSpec.describe Relaxo::Changeset do
5
+ include_context "test records"
6
+
7
+ it "should detect conflicts" do
8
+ events = []
9
+
10
+ alice = Fiber.new do
11
+ database.commit(message: "Alice Data") do |changeset|
12
+ events << :alice
13
+
14
+ object = changeset.append("sample-data-1")
15
+ changeset.write("conflict-path", object)
16
+
17
+ Fiber.yield
18
+ end
19
+ end
20
+
21
+ bob = Fiber.new do
22
+ database.commit(message: "Bob Data") do |changeset|
23
+ events << :bob
24
+
25
+ object = changeset.append("sample-data-1")
26
+ changeset.write("conflict-path", object)
27
+
28
+ Fiber.yield
29
+ end
30
+ end
31
+
32
+ alice.resume
33
+ bob.resume
34
+ alice.resume
35
+ bob.resume
36
+
37
+ expect(events).to be == [:alice, :bob, :bob]
38
+ end
39
+ end
@@ -1,52 +1,87 @@
1
- #!/usr/bin/env ruby
2
1
 
3
2
  require 'relaxo'
4
- require 'relaxo/attachments'
5
-
6
- require_relative 'spec_helper'
7
3
 
8
4
  RSpec.describe Relaxo::Database do
9
- before :all do
10
- @connection = Relaxo::Connection.new(TEST_DATABASE_HOST)
11
- @database = Relaxo::Database.new(@connection, TEST_DATABASE_NAME)
5
+ let(:database_path) {File.join(__dir__, 'test')}
6
+
7
+ let(:database) {Relaxo.connect(database_path, test_key: "test_value")}
8
+
9
+ let(:document_path) {'test/document.json'}
10
+ let(:sample_json) {'[1, 2, 3]'}
11
+
12
+ before(:each) {FileUtils.rm_rf(database_path)}
13
+
14
+ it "should be initially empty" do
15
+ expect(database).to be_empty
16
+ end
17
+
18
+ it "should not be empty with one document" do
19
+ database.commit(message: "Create test document") do |dataset|
20
+ oid = dataset.append(sample_json)
21
+ dataset.write(document_path, oid)
22
+ end
12
23
 
13
- if @database.exist?
14
- @database.delete!
24
+ expect(database).to_not be_empty
25
+ end
26
+
27
+ it "should have metadata" do
28
+ expect(database[:test_key]).to be == "test_value"
29
+ end
30
+
31
+ it "should create a document" do
32
+ database.commit(message: "Create test document") do |dataset|
33
+ oid = dataset.append(sample_json)
34
+ dataset.write(document_path, oid)
15
35
  end
16
36
 
17
- @database.create!
37
+ database.current do |dataset|
38
+ expect(dataset[document_path].data).to be == sample_json
39
+ end
18
40
  end
19
41
 
20
- it "should connect and add a document" do
21
- expect(@database.id?('foobar')).to be false
42
+ it "should erase a document" do
43
+ database.commit(message: "Create test document") do |dataset|
44
+ oid = dataset.append(sample_json)
45
+ dataset.write(document_path, oid)
46
+ end
22
47
 
23
- document = {'animal' => 'Cat', 'name' => 'Seifa'}
48
+ database.commit(message: "Delete test document") do |dataset|
49
+ dataset.delete(document_path)
50
+ end
24
51
 
25
- @database.save(document)
52
+ database.current do |dataset|
53
+ expect(dataset[document_path]).to be nil
54
+ end
55
+ end
56
+
57
+ it "should create multiple documents" do
58
+ database.commit(message: "Create first document") do |dataset|
59
+ oid = dataset.append(sample_json)
60
+ dataset.write(document_path, oid)
61
+ end
26
62
 
27
- id = document[Relaxo::ID]
28
- expect(@database.id?(id)).to be true
63
+ database.commit(message: "Create second document") do |dataset|
64
+ oid = dataset.append(sample_json)
65
+ dataset.write(document_path + '2', oid)
66
+ end
29
67
 
30
- copy = @database.get(id)
31
- document.each do |key, value|
32
- expect(copy[key]).to be == value
68
+ database.current do |dataset|
69
+ expect(dataset[document_path].data).to be == sample_json
70
+ expect(dataset[document_path + '2'].data).to be == sample_json
33
71
  end
34
72
  end
35
73
 
36
- it "should save an attachment" do
37
- document = {
38
- Relaxo::ATTACHMENTS => {
39
- "foo.txt" => {
40
- "content_type" => "text\/plain",
41
- "data" => "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
42
- }
43
- }
44
- }
45
-
46
- result = @database.save(document)
47
- expect(result['ok']).to be true
74
+ it "can enumerate documents" do
75
+ database.commit(message: "Create first document") do |dataset|
76
+ oid = dataset.append(sample_json)
77
+
78
+ 10.times do |id|
79
+ dataset.write(document_path + "-#{id}", oid)
80
+ end
81
+ end
48
82
 
49
- document = @database.get(document[Relaxo::ID])
50
- expect(document[Relaxo::ATTACHMENTS].size).to be == 1
83
+ database.current do |dataset|
84
+ expect(dataset.each('test').count).to be == 10
85
+ end
51
86
  end
52
87
  end
@@ -0,0 +1,30 @@
1
+
2
+ require_relative 'test_records'
3
+
4
+ RSpec.describe Relaxo::Dataset do
5
+ include_context "test records"
6
+
7
+ it "should enumerate all documents" do
8
+ records = []
9
+
10
+ database.current do |dataset|
11
+ records = dataset.each(prefix).to_a
12
+ end
13
+
14
+ expect(records.count).to be 20
15
+ end
16
+ end
17
+
18
+ RSpec.describe Relaxo::Changeset do
19
+ include_context "test records"
20
+
21
+ it "should enumerate all documents" do
22
+ records = []
23
+
24
+ database.commit(message: "Testing Enumeration") do |dataset|
25
+ records = dataset.each(prefix).to_a
26
+ end
27
+
28
+ expect(records.count).to be 20
29
+ end
30
+ end
@@ -0,0 +1,78 @@
1
+
2
+ require 'benchmark/ips' if ENV['BENCHMARK']
3
+ require 'ruby-prof' if ENV['PROFILE']
4
+ require 'flamegraph' if ENV['FLAMEGRAPH']
5
+
6
+ RSpec.describe "Relaxo Performance" do
7
+ let(:database_path) {File.join(__dir__, 'test')}
8
+ let(:database) {Relaxo.connect(database_path)}
9
+
10
+ if defined? Benchmark
11
+ def benchmark(name = nil)
12
+ Benchmark.ips do |benchmark|
13
+ # Collect more data for benchmark:
14
+ benchmark.time = 20
15
+ benchmark.warmup = 10
16
+
17
+ benchmark.report(name) do |i|
18
+ yield i
19
+ end
20
+
21
+ benchmark.compare!
22
+ end
23
+ end
24
+ elsif defined? RubyProf
25
+ def benchmark(name)
26
+ result = RubyProf.profile do
27
+ yield 1000
28
+ end
29
+
30
+ #result.eliminate_methods!([/^((?!Utopia).)*$/])
31
+ printer = RubyProf::FlatPrinter.new(result)
32
+ printer.print($stderr, min_percent: 1.0)
33
+
34
+ printer = RubyProf::GraphHtmlPrinter.new(result)
35
+ filename = name.gsub('/', '_') + '.html'
36
+ File.open(filename, "w") do |file|
37
+ printer.print(file)
38
+ end
39
+ end
40
+ elsif defined? Flamegraph
41
+ def benchmark(name)
42
+ filename = name.gsub('/', '_') + '.html'
43
+ Flamegraph.generate(filename) do
44
+ yield 1
45
+ end
46
+ end
47
+ else
48
+ def benchmark(name)
49
+ yield 1
50
+ end
51
+ end
52
+
53
+ before(:each) do
54
+ FileUtils.rm_rf(database_path)
55
+ end
56
+
57
+ it "single transaction should be fast" do
58
+ benchmark("single") do |iterations|
59
+ database.commit(message: "Some Documents") do |dataset|
60
+ iterations.times do |i|
61
+ object = dataset.append("good-#{i}")
62
+ dataset.write("#{i%100}/#{i}", object)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ it "multiple transactions should be fast" do
69
+ benchmark("multiple") do |iterations|
70
+ iterations.times do |i|
71
+ database.commit(message: "Some Documents") do |dataset|
72
+ object = dataset.append("good-#{i}")
73
+ dataset.write("#{i%100}/#{i}", object)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end