ambry 0.1.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.
- data/Changelog.md +5 -0
- data/Gemfile +2 -0
- data/Guide.md +320 -0
- data/MIT-LICENSE +18 -0
- data/README.md +97 -0
- data/Rakefile +39 -0
- data/ambry.gemspec +25 -0
- data/extras/bench.rb +107 -0
- data/extras/cookie_demo.rb +111 -0
- data/extras/countries.rb +70 -0
- data/lib/ambry.rb +54 -0
- data/lib/ambry/abstract_key_set.rb +106 -0
- data/lib/ambry/active_model.rb +122 -0
- data/lib/ambry/adapter.rb +53 -0
- data/lib/ambry/adapters/cookie.rb +55 -0
- data/lib/ambry/adapters/file.rb +38 -0
- data/lib/ambry/adapters/yaml.rb +17 -0
- data/lib/ambry/hash_proxy.rb +55 -0
- data/lib/ambry/mapper.rb +66 -0
- data/lib/ambry/model.rb +164 -0
- data/lib/ambry/version.rb +9 -0
- data/lib/generators/norman_generator.rb +22 -0
- data/lib/rack/norman.rb +21 -0
- data/spec/active_model_spec.rb +115 -0
- data/spec/adapter_spec.rb +48 -0
- data/spec/cookie_adapter_spec.rb +81 -0
- data/spec/file_adapter_spec.rb +48 -0
- data/spec/fixtures.yml +18 -0
- data/spec/key_set_spec.rb +104 -0
- data/spec/mapper_spec.rb +97 -0
- data/spec/model_spec.rb +162 -0
- data/spec/spec_helper.rb +38 -0
- metadata +147 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/actions'
|
3
|
+
|
4
|
+
# This generator adds an initializer and default empty database to your Rails
|
5
|
+
# application. It can be invoked on the command line like:
|
6
|
+
#
|
7
|
+
# rails generate ambry
|
8
|
+
#
|
9
|
+
class AmbryGenerator < Rails::Generators::Base
|
10
|
+
|
11
|
+
# Create the initializer and empty database.
|
12
|
+
def create_files
|
13
|
+
initializer("ambry.rb") do
|
14
|
+
<<-EOI
|
15
|
+
require "ambry/adapters/yaml"
|
16
|
+
require "ambry/active_model"
|
17
|
+
Ambry::Adapters::YAML.new :file => Rails.root.join('db', 'ambry.yml')
|
18
|
+
EOI
|
19
|
+
end
|
20
|
+
create_file("db/ambry.yml", '')
|
21
|
+
end
|
22
|
+
end
|
data/lib/rack/norman.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "rack/contrib"
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Rack::Ambry is a middleware that allows you to store a Ambry datbase
|
5
|
+
# in a cookie.
|
6
|
+
# @see Ambry::Adapters::Cookie
|
7
|
+
class Ambry
|
8
|
+
def initialize(app, options = {})
|
9
|
+
@app = app
|
10
|
+
@ambry = ::Ambry::Adapters::Cookie.new(options.merge(:sync => true))
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
@ambry.data = env["rack.cookies"]["ambry_data"]
|
15
|
+
@ambry.load_database
|
16
|
+
status, headers, body = @app.call(env)
|
17
|
+
env["rack.cookies"]["ambry_data"] = @ambry.export_data
|
18
|
+
[status, headers, body]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.expand_path("../spec_helper", __FILE__)
|
2
|
+
require "ambry/active_model"
|
3
|
+
|
4
|
+
class Book
|
5
|
+
extend Ambry::Model
|
6
|
+
extend Ambry::ActiveModel
|
7
|
+
field :slug, :title, :author
|
8
|
+
validates_presence_of :slug
|
9
|
+
validates_uniqueness_of :slug, :title
|
10
|
+
before_save :save_callback_fired
|
11
|
+
before_destroy :destroy_callback_fired
|
12
|
+
|
13
|
+
def save_callback_fired
|
14
|
+
@save_callback_fired = true
|
15
|
+
end
|
16
|
+
|
17
|
+
def destroy_callback_fired
|
18
|
+
@destroy_callback_fired = true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module ActiveModuleSupportSpecHelper
|
23
|
+
def valid_book
|
24
|
+
{:slug => "war-and-peace", :title => "War and Peace", :author => "Leo Tolstoy"}
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_fixtures
|
28
|
+
Ambry.adapters.clear
|
29
|
+
Ambry::Adapter.new :name => :main
|
30
|
+
Book.use :main
|
31
|
+
@model = Book.create! valid_book
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe Ambry::ActiveModel do
|
36
|
+
|
37
|
+
before { load_fixtures }
|
38
|
+
|
39
|
+
include ActiveModuleSupportSpecHelper
|
40
|
+
include ActiveModel::Lint::Tests
|
41
|
+
|
42
|
+
describe ".model_name" do
|
43
|
+
it "should return an ActiveModel::Name" do
|
44
|
+
assert_kind_of ::ActiveModel::Name, Book.model_name
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#keys" do
|
49
|
+
it "should return an array of attribute names" do
|
50
|
+
assert @model.keys.include?(:slug), "@model.keys should include :slug"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#save!" do
|
55
|
+
it "should raise an exception if the model is not valid" do
|
56
|
+
assert_raises Ambry::AmbryError do
|
57
|
+
Book.new.save!
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#to_json" do
|
63
|
+
it "should serialize" do
|
64
|
+
json = @model.to_json
|
65
|
+
refute_nil @model.to_json
|
66
|
+
assert_match /"author":"Leo Tolstoy"/, json
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#to_xml" do
|
71
|
+
it "should serialize to XML" do
|
72
|
+
xml = @model.to_xml
|
73
|
+
refute_nil xml
|
74
|
+
assert_match /<author>Leo Tolstoy<\/author>/, xml
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#valid?" do
|
79
|
+
it "should do validation" do
|
80
|
+
book = Book.new
|
81
|
+
refute book.valid?
|
82
|
+
book.slug = "hello-world"
|
83
|
+
assert book.valid?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "callbacks" do
|
88
|
+
it "should fire save callbacks" do
|
89
|
+
book = Book.new valid_book
|
90
|
+
book.save
|
91
|
+
assert book.instance_variable_defined? :@save_callback_fired
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should fire destroy callbacks" do
|
95
|
+
@model.destroy
|
96
|
+
assert @model.instance_variable_defined? :@destroy_callback_fired
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe ".validates_uniqueness_of" do
|
101
|
+
it "should validate on id attribute" do
|
102
|
+
@book = Book.new valid_book.merge(:title => "War and Peace II")
|
103
|
+
refute @book.valid?
|
104
|
+
@book.slug = "war-and-peace-2"
|
105
|
+
assert @book.valid?
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should validate on non-id attribute" do
|
109
|
+
@book = Book.new valid_book.merge(:slug => "war-and-peace-2")
|
110
|
+
refute @book.valid?
|
111
|
+
@book.title = "War and Peace II"
|
112
|
+
assert @book.valid?
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.expand_path("../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
describe Ambry::Adapter do
|
4
|
+
|
5
|
+
before { Ambry.adapters.clear }
|
6
|
+
after { Ambry.adapters.clear }
|
7
|
+
|
8
|
+
describe "#initialize" do
|
9
|
+
|
10
|
+
it "should register itself" do
|
11
|
+
Ambry::Adapter.new :name => :an_adapter
|
12
|
+
assert_equal :an_adapter, Ambry.adapters.keys.first
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should use a default name if none given" do
|
16
|
+
assert_equal Ambry.default_adapter_name, Ambry::Adapter.new.name
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should raise error if a duplicate name is used" do
|
20
|
+
assert_raises Ambry::AmbryError do
|
21
|
+
2.times {Ambry::Adapter.new(:name => :test_adapter)}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should set an empty hash as the db" do
|
26
|
+
assert_equal Hash.new, Ambry::Adapter.new.db
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "#db_for" do
|
31
|
+
|
32
|
+
before { load_fixtures }
|
33
|
+
|
34
|
+
it "should return a instance of Hash" do
|
35
|
+
adapter = Ambry.adapters[:main]
|
36
|
+
assert_kind_of Hash, adapter.db_for(Person)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "stubbed io operations" do
|
41
|
+
it "should return true" do
|
42
|
+
adapter = Ambry::Adapter.new
|
43
|
+
[:export_data, :import_data, :save_database].each do |method|
|
44
|
+
assert adapter.send method
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require File.expand_path("../spec_helper", __FILE__)
|
2
|
+
require "ambry/adapters/cookie"
|
3
|
+
|
4
|
+
class User
|
5
|
+
extend Ambry::Model
|
6
|
+
field :email, :name
|
7
|
+
end
|
8
|
+
|
9
|
+
module CookieAdapterSpecHelpers
|
10
|
+
def secret
|
11
|
+
"ssssshh... this is a secret!"
|
12
|
+
end
|
13
|
+
|
14
|
+
def sample_data
|
15
|
+
# hash = {"User" => {valid_user[:email] => valid_user}}
|
16
|
+
# p ActiveSupport::MessageVerifier.new(secret).generate(Zlib::Deflate.deflate(Marshal.dump(hash)))
|
17
|
+
"BAgiTnicY+GoZvNU4gwtTi1is2JzDQHxhLPyUx2KkzNy81P1kvNz2awZQqrZrTjzEnNTPZX4" +
|
18
|
+
"vfJTFYLBkiAJK67U3MTMHKyaAJGaGlk=--08913fe1c677e4bb0dd34ef90fb22f9027e587f4"
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid_user
|
22
|
+
@valid_user ||= {:name => "Joe Schmoe", :email => "joe@schmoe.com"}
|
23
|
+
end
|
24
|
+
|
25
|
+
def load_fixtures
|
26
|
+
Ambry.adapters.clear
|
27
|
+
@adapter = Ambry::Adapters::Cookie.new \
|
28
|
+
:name => :cookie,
|
29
|
+
:secret => secret
|
30
|
+
User.use :cookie, :sync => true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe Ambry::Adapters::Cookie do
|
35
|
+
|
36
|
+
include CookieAdapterSpecHelpers
|
37
|
+
|
38
|
+
before { load_fixtures }
|
39
|
+
after { Ambry.adapters.clear }
|
40
|
+
|
41
|
+
describe Ambry::Adapters::Cookie do
|
42
|
+
|
43
|
+
describe "#initialize" do
|
44
|
+
it "should decode signed data if given" do
|
45
|
+
adapter = Ambry::Adapters::Cookie.new \
|
46
|
+
:secret => secret,
|
47
|
+
:data => sample_data
|
48
|
+
assert_kind_of Hash, adapter.db["User"]
|
49
|
+
assert_equal "joe@schmoe.com", adapter.db["User"].keys.first
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should load properly with nil or blank data" do
|
53
|
+
[nil, ""].each_with_index do |arg, index|
|
54
|
+
adapter = Ambry::Adapters::Cookie.new \
|
55
|
+
:secret => secret,
|
56
|
+
:data => arg,
|
57
|
+
:name => :"main_#{index}"
|
58
|
+
assert_instance_of Hash, adapter.db
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#export_data" do
|
64
|
+
it "should encode and sign the database" do
|
65
|
+
User.create \
|
66
|
+
:name => Faker::Name.name,
|
67
|
+
:email => Faker::Internet.email
|
68
|
+
refute_nil @adapter.export_data
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#save_database" do
|
73
|
+
it "should raise a AmbryError if signed data exceeds max data length" do
|
74
|
+
Ambry::Adapters::Cookie.stubs(:max_data_length).returns(1)
|
75
|
+
assert_raises Ambry::AmbryError do
|
76
|
+
User.create valid_user
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.expand_path("../spec_helper", __FILE__)
|
3
|
+
|
4
|
+
classes = [Ambry::Adapters::File, Ambry::Adapters::YAML]
|
5
|
+
|
6
|
+
classes.each do |klass|
|
7
|
+
|
8
|
+
describe klass.to_s do
|
9
|
+
|
10
|
+
before do
|
11
|
+
Ambry.adapters.clear
|
12
|
+
@path = File.expand_path("../file_adapter_test", __FILE__)
|
13
|
+
@adapter = klass.new(:file => @path)
|
14
|
+
@adapter.instance_variable_set :@db, {
|
15
|
+
"Class" => {
|
16
|
+
:a => :b,
|
17
|
+
:unicode => "ü"
|
18
|
+
}
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
after do
|
23
|
+
Ambry.adapters.clear
|
24
|
+
FileUtils.rm_f @path
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#export_data" do
|
28
|
+
it "should be a string" do
|
29
|
+
assert_kind_of String, @adapter.export_data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#save_database" do
|
34
|
+
it "should write the data to disk" do
|
35
|
+
assert @adapter.save_database
|
36
|
+
assert File.exists? @path
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#load_database" do
|
41
|
+
it "should load the data from the filesystem" do
|
42
|
+
@adapter.save_database
|
43
|
+
a2 = @adapter.class.new(:name => "a2", :file => @path)
|
44
|
+
assert_equal @adapter.db["Class"][:unicode].bytes.entries, a2.db["Class"][:unicode].bytes.entries
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/spec/fixtures.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
---
|
2
|
+
Person:
|
3
|
+
moe@3stooges.com:
|
4
|
+
:name: Moe Howard
|
5
|
+
:email: moe@3stooges.com
|
6
|
+
shemp@3stooges.com:
|
7
|
+
:name: Shemp Howard
|
8
|
+
:email: shemp@3stooges.com
|
9
|
+
curly@3stooges.com:
|
10
|
+
:name: Curly Howard
|
11
|
+
:email: curly@3stooges.com
|
12
|
+
larry@3stooges.com:
|
13
|
+
:name: Larry Fine
|
14
|
+
:email: larry@3stooges.com
|
15
|
+
"MyModule::Animal":
|
16
|
+
Canis Familaris:
|
17
|
+
:species: Canis Familaris
|
18
|
+
:common_name: Dog
|
@@ -0,0 +1,104 @@
|
|
1
|
+
require File.expand_path("../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
describe Ambry::AbstractKeySet do
|
4
|
+
|
5
|
+
before { load_fixtures }
|
6
|
+
after { Ambry.adapters.clear }
|
7
|
+
|
8
|
+
describe "#+" do
|
9
|
+
it "should add two key sets" do
|
10
|
+
key_set = Person.find {|p| p.name =~ /Curly/} + Person.find {|p| p.name =~ /Larry/}
|
11
|
+
assert_equal 2, key_set.length
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should not duplicate entries" do
|
15
|
+
key_set = Person.find {|p| p.name =~ /Curly/} + Person.find {|p| p.name =~ /Curly/}
|
16
|
+
assert_equal 1, key_set.length
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "#-" do
|
21
|
+
it "should subtract a key set" do
|
22
|
+
a = Person.find
|
23
|
+
b = Person.find {|p| p.name =~ /Larry|Ted/}
|
24
|
+
key_set = a - b
|
25
|
+
assert_equal 3, key_set.length
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#&" do
|
30
|
+
it "should get set intersection" do
|
31
|
+
key_set = Person.find & Person.find {|p| p.name =~ /Larry/}
|
32
|
+
assert_equal 1, key_set.length
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#first" do
|
37
|
+
it "should return the first matching instance when called with a block" do
|
38
|
+
assert_equal "Curly Howard", Person.first {|p| p.name =~ /Curly/}.name
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should return the first instance when not called with a block" do
|
42
|
+
assert_kind_of Person, Person.first
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#count" do
|
47
|
+
it "should count matching instances when called with a block" do
|
48
|
+
assert_equal 3, Person.count {|p| p.name =~ /Howard/}
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should count all keys when called without a block" do
|
52
|
+
assert_equal 4, Person.count
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#find" do
|
57
|
+
it "should return a KeySet of matching keys when called with a block" do
|
58
|
+
key_set = Person.find {|p| p.name =~ /Larry/}
|
59
|
+
assert_kind_of Ambry::AbstractKeySet, key_set
|
60
|
+
assert_equal 1, key_set.size
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should return a KeySet of all keys when called with no block" do
|
64
|
+
assert_equal 4, Person.find.size
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should yield an instance of HashProxy to the block" do
|
68
|
+
Person.find {|x| assert_kind_of Ambry::HashProxy, x}
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should be chainable" do
|
72
|
+
assert_equal "Larry Fine", Person.stooges.non_howards.first.name
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should raise error when trying to chain nonexistant method" do
|
76
|
+
assert_raises NoMethodError do
|
77
|
+
Person.stooges.foobar
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#find_by_key" do
|
83
|
+
it "should yield a key to the block" do
|
84
|
+
Person.find_by_key {|x| assert_kind_of String, x}
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should return a KeySet of all keys when called with no block" do
|
88
|
+
assert_equal 4, Person.find_by_key.size
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#sort" do
|
93
|
+
it "should sort" do
|
94
|
+
assert_equal "Curly Howard", Person.find.sort {|a, b| a.name <=> b.name}.first.name
|
95
|
+
assert_equal "Shemp Howard", Person.find.sort {|b, a| a.name <=> b.name}.first.name
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "#limit" do
|
100
|
+
it "should limit" do
|
101
|
+
assert_equal 2, Person.find.limit(2).count
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|