relaxo 0.4.7 → 1.0.0

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