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.
@@ -0,0 +1,9 @@
1
+ module Norman
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+ BUILD = nil
7
+ STRING = [MAJOR, MINOR, TINY, BUILD].compact.join('.')
8
+ end
9
+ end
@@ -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
@@ -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
@@ -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