norman 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 +104 -0
- data/Rakefile +39 -0
- data/extras/bench.rb +107 -0
- data/extras/cookie_demo.rb +111 -0
- data/extras/countries.rb +70 -0
- data/lib/generators/norman_generator.rb +22 -0
- data/lib/norman.rb +54 -0
- data/lib/norman/abstract_key_set.rb +106 -0
- data/lib/norman/active_model.rb +122 -0
- data/lib/norman/adapter.rb +53 -0
- data/lib/norman/adapters/cookie.rb +55 -0
- data/lib/norman/adapters/file.rb +38 -0
- data/lib/norman/adapters/yaml.rb +17 -0
- data/lib/norman/hash_proxy.rb +55 -0
- data/lib/norman/mapper.rb +66 -0
- data/lib/norman/model.rb +164 -0
- data/lib/norman/version.rb +9 -0
- data/lib/rack/norman.rb +21 -0
- data/norman.gemspec +25 -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 +136 -0
data/lib/rack/norman.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "rack/contrib"
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Rack::Norman is a middleware that allows you to store a Norman datbase
|
5
|
+
# in a cookie.
|
6
|
+
# @see Norman::Adapters::Cookie
|
7
|
+
class Norman
|
8
|
+
def initialize(app, options = {})
|
9
|
+
@app = app
|
10
|
+
@norman = ::Norman::Adapters::Cookie.new(options.merge(:sync => true))
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
@norman.data = env["rack.cookies"]["norman_data"]
|
15
|
+
@norman.load_database
|
16
|
+
status, headers, body = @app.call(env)
|
17
|
+
env["rack.cookies"]["norman_data"] = @norman.export_data
|
18
|
+
[status, headers, body]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/norman.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path("../lib/norman/version", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.authors = "Norman Clarke"
|
5
|
+
s.email = "norman@njclarke.com"
|
6
|
+
s.files = `git ls-files`.split("\n").reject {|f| f =~ /^\./}
|
7
|
+
s.has_rdoc = true
|
8
|
+
s.homepage = "http://github.com/norman/norman"
|
9
|
+
s.name = "norman"
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
s.rubyforge_project = "[none]"
|
12
|
+
s.summary = "An ActiveModel-compatible ORM-like library for storing model instances in an in-memory Hash."
|
13
|
+
s.test_files = Dir.glob "test/**/*_test.rb"
|
14
|
+
s.version = Norman::Version::STRING
|
15
|
+
s.description = <<-EOD
|
16
|
+
Norman is not an ORM, man! It's a database and ORM replacement for (mostly)
|
17
|
+
static models and small datasets. It provides ActiveModel compatibility, and
|
18
|
+
flexible searching and storage.
|
19
|
+
EOD
|
20
|
+
s.add_development_dependency "ffaker"
|
21
|
+
s.add_development_dependency "minitest", "~> 2.2.2"
|
22
|
+
s.add_development_dependency "mocha"
|
23
|
+
s.add_development_dependency "activesupport", "~> 3.0"
|
24
|
+
s.add_development_dependency "activemodel", "~> 3.0"
|
25
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.expand_path("../spec_helper", __FILE__)
|
2
|
+
require "norman/active_model"
|
3
|
+
|
4
|
+
class Book
|
5
|
+
extend Norman::Model
|
6
|
+
extend Norman::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
|
+
Norman.adapters.clear
|
29
|
+
Norman::Adapter.new :name => :main
|
30
|
+
Book.use :main
|
31
|
+
@model = Book.create! valid_book
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe Norman::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 Norman::NormanError 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 Norman::Adapter do
|
4
|
+
|
5
|
+
before { Norman.adapters.clear }
|
6
|
+
after { Norman.adapters.clear }
|
7
|
+
|
8
|
+
describe "#initialize" do
|
9
|
+
|
10
|
+
it "should register itself" do
|
11
|
+
Norman::Adapter.new :name => :an_adapter
|
12
|
+
assert_equal :an_adapter, Norman.adapters.keys.first
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should use a default name if none given" do
|
16
|
+
assert_equal Norman.default_adapter_name, Norman::Adapter.new.name
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should raise error if a duplicate name is used" do
|
20
|
+
assert_raises Norman::NormanError do
|
21
|
+
2.times {Norman::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, Norman::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 = Norman.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 = Norman::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 "norman/adapters/cookie"
|
3
|
+
|
4
|
+
class User
|
5
|
+
extend Norman::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
|
+
Norman.adapters.clear
|
27
|
+
@adapter = Norman::Adapters::Cookie.new \
|
28
|
+
:name => :cookie,
|
29
|
+
:secret => secret
|
30
|
+
User.use :cookie, :sync => true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe Norman::Adapters::Cookie do
|
35
|
+
|
36
|
+
include CookieAdapterSpecHelpers
|
37
|
+
|
38
|
+
before { load_fixtures }
|
39
|
+
after { Norman.adapters.clear }
|
40
|
+
|
41
|
+
describe Norman::Adapters::Cookie do
|
42
|
+
|
43
|
+
describe "#initialize" do
|
44
|
+
it "should decode signed data if given" do
|
45
|
+
adapter = Norman::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 = Norman::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 NormanError if signed data exceeds max data length" do
|
74
|
+
Norman::Adapters::Cookie.stubs(:max_data_length).returns(1)
|
75
|
+
assert_raises Norman::NormanError 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 = [Norman::Adapters::File, Norman::Adapters::YAML]
|
5
|
+
|
6
|
+
classes.each do |klass|
|
7
|
+
|
8
|
+
describe klass.to_s do
|
9
|
+
|
10
|
+
before do
|
11
|
+
Norman.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
|
+
Norman.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 Norman::AbstractKeySet do
|
4
|
+
|
5
|
+
before { load_fixtures }
|
6
|
+
after { Norman.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 Norman::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 Norman::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
|