ambry 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|