norman 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,39 @@
1
+ require "rake"
2
+ require "rake/testtask"
3
+ require "rake/clean"
4
+ require "rubygems/package_task"
5
+
6
+ task :default => :spec
7
+ task :test => :spec
8
+
9
+ CLEAN << %w[pkg doc coverage .yardoc]
10
+
11
+ begin
12
+ desc "Run SimpleCov"
13
+ task :coverage do
14
+ ENV["coverage"] = "true"
15
+ Rake::Task["spec"].execute
16
+ end
17
+ rescue LoadError
18
+ end
19
+
20
+ gemspec = File.expand_path("../norman.gemspec", __FILE__)
21
+ if File.exist? gemspec
22
+ Gem::PackageTask.new(eval(File.read(gemspec))) { |pkg| }
23
+ end
24
+
25
+ Rake::TestTask.new(:spec) { |t| t.pattern = "spec/**/*_spec.rb" }
26
+
27
+ begin
28
+ require "yard"
29
+ YARD::Rake::YardocTask.new do |t|
30
+ t.options = ["--output-dir=doc"]
31
+ t.options << "--files" << ["Guide.md", "Changelog.md"].join(",")
32
+ end
33
+ rescue LoadError
34
+ end
35
+
36
+ desc "Run benchmarks"
37
+ task :bench do
38
+ require File.expand_path("../extras/bench", __FILE__)
39
+ end
@@ -0,0 +1,107 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require "benchmark"
4
+ require "ffaker"
5
+ require "norman"
6
+
7
+ N = 500
8
+
9
+ Norman::Adapter.new
10
+
11
+ class Person
12
+ extend Norman::Model
13
+ field :email, :name, :age
14
+
15
+ def self.younger_than(age)
16
+ with_index("younger_than_#{age}") do
17
+ find {|person| person[:age] < age}
18
+ end
19
+ end
20
+
21
+ filters do
22
+ def older_than(age)
23
+ find {|person| person[:age] > age}
24
+ end
25
+
26
+ def email_matches(regexp)
27
+ find {|person| person[:email] =~ regexp}
28
+ end
29
+
30
+ end
31
+ end
32
+
33
+ until Person.count == 1000 do
34
+ Person.create \
35
+ :name => Faker::Name.name,
36
+ :email => Faker::Internet.email,
37
+ :age => rand(100)
38
+ end
39
+
40
+ keys = Person.all.keys.sort do |a, b|
41
+ rand(100) <=> rand(100)
42
+ end[0,10]
43
+
44
+ Benchmark.bmbm do |x|
45
+
46
+ puts "Benchmarking #{N} times:\n\n"
47
+
48
+ x.report("Count records") do
49
+ N.times do
50
+ Person.count {|p| p[:email] =~ /\.com/}
51
+ end
52
+ end
53
+
54
+ x.report("Count scoped records") do
55
+ N.times do
56
+ Person.older_than(50).count
57
+ end
58
+ end
59
+
60
+ x.report("Get 10 random keys") do
61
+ N.times do
62
+ keys.each {|k| Person.get(k)}
63
+ end
64
+ end
65
+
66
+ x.report("Find records iterating on values") do
67
+ N.times do
68
+ Person.find {|p| p[:email] =~ /\.com/}
69
+ end
70
+ end
71
+
72
+ x.report("Find records using proxy method") do
73
+ N.times do
74
+ Person.find {|p| p.email =~ /\.com/}
75
+ end
76
+ end
77
+
78
+ x.report("Find records iterating on keys") do
79
+ N.times do
80
+ Person.find_by_key {|k| k =~ /\.com/}
81
+ end
82
+ end
83
+
84
+ x.report("Find scoped people without index") do
85
+ N.times do
86
+ Person.find {|p| p[:age] < 50 && p[:email] =~ /\.com/}
87
+ end
88
+ end
89
+
90
+ x.report("Find scoped people with index") do
91
+ N.times do
92
+ Person.younger_than(50).find {|p| p[:email] =~ /\.com/}
93
+ end
94
+ end
95
+
96
+ x.report("Find with chained filters") do
97
+ N.times do
98
+ Person.older_than(50).email_matches(/\.com/)
99
+ end
100
+ end
101
+
102
+ x.report("Find records iterating on keys and using scope") do
103
+ N.times do
104
+ Person.find_by_key {|k| k =~ /\.com/}.older_than(50)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,111 @@
1
+ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
2
+ $LOAD_PATH.uniq!
3
+
4
+ require "rubygems"
5
+ require "sinatra"
6
+ require "haml"
7
+ require "babosa"
8
+ require "date"
9
+ require "norman"
10
+ require "norman/adapters/cookie"
11
+ require "rack/norman"
12
+
13
+ set :session, false
14
+
15
+ use Rack::Cookies
16
+ use Rack::Norman, :name => :cookie, :secret => "Sssshhhh! This is a secret."
17
+
18
+ class Book
19
+ extend Norman::Model
20
+ field :slug, :title, :author
21
+ use :cookie
22
+
23
+ def title=(value)
24
+ @slug = value.to_slug.normalize.to_s
25
+ @title = value
26
+ end
27
+ end
28
+
29
+ get "/" do
30
+ @header = "Books"
31
+ @books = Book.all
32
+ haml :index
33
+ end
34
+
35
+ get "/books/new" do
36
+ @header = "Add a Book"
37
+ @action = "/books"
38
+ haml :new
39
+ end
40
+
41
+ get "/books/:slug/edit" do |slug|
42
+ @book = Book.get(slug)
43
+ @action = "/books"
44
+ @header = @book.title
45
+ params[:title] = @book.title
46
+ params[:author] = @book.author
47
+ haml :edit
48
+ end
49
+
50
+ get "/books/:slug" do |slug|
51
+ @book = Book.get(slug)
52
+ @header = @book.title
53
+ haml :book
54
+ end
55
+
56
+ post "/books" do
57
+ Book.delete params[:slug] unless params[:slug].blank?
58
+ @book = Book.create params unless params[:title].blank?
59
+ redirect "/"
60
+ end
61
+
62
+ __END__
63
+ @@layout
64
+ !!! 5
65
+ %html
66
+ %head
67
+ %meta(http-equiv="Content-Type" content="text/html; charset=utf-8")
68
+ %title Norman Cookie Adapter Demo
69
+ %body
70
+ %h2= @header
71
+ = yield
72
+
73
+ @@edit
74
+ = haml(:form, :layout => false)
75
+
76
+ @@index
77
+ %ul
78
+ - @books.each do |book|
79
+ %li= '<a href="/books/%s">%s</a>' % [book.slug, book.title, book.author]
80
+ %p.controls
81
+ <a href="/books/new">New book</a>
82
+
83
+ @@new
84
+ = haml(:form, :layout => false)
85
+ %p.controls
86
+ <a href="/">Books</a>
87
+
88
+ @@book
89
+ by #{@book.author}
90
+ %p.controls
91
+ <a href="/">Books</a>
92
+ <a href="/books/#{@book.slug}/edit">Edit</a>
93
+
94
+ @@form
95
+ %form(method="post" enctype="utf-8" action=@action)
96
+ %p
97
+ - if @book
98
+ %input#slug{:type => "hidden", :value => @book.slug, :name => "slug"}
99
+ %label(for="title") Title:
100
+ %br
101
+ %input#title{:type => "text", :value => params[:title], :name => "title"}
102
+ %p
103
+ %label(for="author") Author:
104
+ %br
105
+ %input#author{:type => "text", :value => params[:author], :name => "author"}
106
+ %p
107
+ %input(type="submit" value="save it")
108
+ - if @book
109
+ %form{:method => "post", :enctype => "utf-8", :action => "/books"}
110
+ %input#slug{:type => "hidden", :value => @book.slug, :name => "slug"}
111
+ %input(type="submit" value="or delete it")
@@ -0,0 +1,70 @@
1
+ # A demo of Norman's filters
2
+
3
+ require "bundler/setup"
4
+ require "norman"
5
+
6
+ Norman::Adapter.new
7
+
8
+ class Country
9
+ extend Norman::Model
10
+ field :tld, :name, :population, :region
11
+
12
+ filters do
13
+ def african
14
+ find {|p| p.region == :africa}
15
+ end
16
+
17
+ def european
18
+ find {|p| p.region == :europe}
19
+ end
20
+
21
+ def population(op, num)
22
+ find {|p| p.population.send(op, num)}.sort {|a, b| b.population <=> a.population}
23
+ end
24
+ end
25
+ end
26
+
27
+ # Population data from: http://en.wikipedia.org/wiki/List_of_countries_by_population
28
+ [
29
+ {:tld => "br", :name => "Brazil", :population => 190_732_694, :region => :america},
30
+ {:tld => "bw", :name => "Botswana", :population => 1_839_833, :region => :africa},
31
+ {:tld => "cn", :name => "China", :population => 1_342_740_000, :region => :asia},
32
+ {:tld => "dz", :name => "Algeria", :population => 33_333_216, :region => :africa},
33
+ {:tld => "eg", :name => "Egypt", :population => 80_335_036, :region => :africa},
34
+ {:tld => "et", :name => "Ethiopia", :population => 85_237_338, :region => :africa},
35
+ {:tld => "fr", :name => "France", :population => 65_821_885, :region => :europe},
36
+ {:tld => "ma", :name => "Morocco", :population => 33_757_175, :region => :africa},
37
+ {:tld => "mc", :name => "Monaco", :population => 33_000, :region => :europe},
38
+ {:tld => "mz", :name => "Mozambique", :population => 20_366_795, :region => :africa},
39
+ {:tld => "ng", :name => "Nigeria", :population => 154_729_000, :region => :africa},
40
+ {:tld => "sc", :name => "Seychelles", :population => 80_654, :region => :africa}
41
+ ].each {|c| Country.create(c)}
42
+
43
+ @african_countries = Country.african
44
+ @bigger_countries = Country.population(:>=, 50_000_000)
45
+ @smaller_countries = Country.population(:<=, 5_000_000)
46
+ @european_countries = Country.european
47
+ @bigger_african_countries = Country.african.population(:>=, 50_000_000)
48
+ @bigger_non_african_countries = @bigger_countries - @african_countries
49
+ @bigger_or_european_countries = @bigger_countries + @european_countries
50
+ @smaller_or_european_countries = @smaller_countries + @european_countries
51
+ @smaller_european_countries = @smaller_countries & @european_countries
52
+
53
+ instance_variables.each do |name|
54
+ puts "%s: %s" % [
55
+ name.to_s.gsub("_", " ").gsub("@", ""),
56
+ instance_variable_get(name).all.map(&:name).join(", ")
57
+ ]
58
+ end
59
+
60
+ # Output:
61
+ #
62
+ # african countries: Algeria, Botswana, Egypt, Ethiopia, Nigeria, Seychelles, Mozambique, Morocco
63
+ # bigger countries: China, Brazil, Nigeria, Ethiopia, Egypt, France
64
+ # smaller countries: Botswana, Seychelles, Monaco
65
+ # european countries: France, Monaco
66
+ # bigger african countries: Egypt, Ethiopia, Nigeria
67
+ # bigger non african countries: China, Brazil, France
68
+ # bigger or european countries: China, Brazil, Nigeria, Ethiopia, Egypt, France, Monaco
69
+ # smaller and european countries: Botswana, Seychelles, Monaco, France
70
+ # smaller european countries: Monaco
@@ -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 norman
8
+ #
9
+ class NormanGenerator < Rails::Generators::Base
10
+
11
+ # Create the initializer and empty database.
12
+ def create_files
13
+ initializer("norman.rb") do
14
+ <<-EOI
15
+ require "norman/adapters/yaml"
16
+ require "norman/active_model"
17
+ Norman::Adapters::YAML.new :file => Rails.root.join('db', 'norman.yml')
18
+ EOI
19
+ end
20
+ create_file("db/norman.yml", '')
21
+ end
22
+ end
@@ -0,0 +1,54 @@
1
+ require "forwardable"
2
+ require "thread"
3
+ require "norman/adapter"
4
+ require "norman/abstract_key_set"
5
+ require "norman/mapper"
6
+ require "norman/model"
7
+ require "norman/hash_proxy"
8
+ require "norman/adapters/file"
9
+
10
+ # Norman is a database and ORM replacement for small, mostly static models.
11
+ #
12
+ # Norman is free software released under the terms of the MIT License.
13
+ # @author Norman Clarke
14
+ module Norman
15
+ extend self
16
+
17
+ @lock = Mutex.new
18
+
19
+ # The default adapter name.
20
+ attr_reader :default_adapter_name
21
+ @default_adapter_name = :main
22
+
23
+ # A hash of all instantiated Norman adapters.
24
+ attr_reader :adapters
25
+ @adapters = {}
26
+
27
+ # Registers an adapter with Norman. This facilitates allowing models to
28
+ # specify an adapter by name rather than class or instance.
29
+ #
30
+ # @param [Symbol] adapter The adapter name.
31
+ # @see Norman::Model::ClassMethods#use
32
+ def register_adapter(adapter)
33
+ name = adapter.name.to_sym
34
+ if adapters[name]
35
+ raise NormanError, "Adapter #{name.inspect} already registered"
36
+ end
37
+ @lock.synchronize do
38
+ adapters[name] = adapter
39
+ end
40
+ end
41
+
42
+ # Base error for Norman.
43
+ class NormanError < StandardError ; end
44
+
45
+ # Raised when a single instance is expected but could not be found.
46
+ class NotFoundError < NormanError
47
+
48
+ # @param [String] klass The class from which the error originated.
49
+ # @param [String] key The key whose lookup trigged the error.
50
+ def initialize(*args)
51
+ super('Could not find %s with key "%s"' % args)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,106 @@
1
+ module Norman
2
+
3
+ # @abstract
4
+ class AbstractKeySet
5
+ extend Forwardable
6
+ include Enumerable
7
+
8
+ attr_accessor :keys, :mapper
9
+ def_delegators :keys, :empty?, :length, :size
10
+ def_delegators :to_enum, :each
11
+
12
+ # Create a new KeySet from an array of keys and a mapper.
13
+ def initialize(keys = nil, mapper = nil)
14
+ @keys = keys || [].freeze
15
+ # Assume that if a frozen array is passed in, it's already been compacted
16
+ # and uniqued in order to improve performance.
17
+ unless @keys.frozen?
18
+ @keys.uniq!
19
+ @keys.compact!
20
+ @keys.freeze
21
+ end
22
+ @mapper = mapper
23
+ end
24
+
25
+ def +(key_set)
26
+ self.class.new(keys + key_set.keys, mapper)
27
+ end
28
+ alias | +
29
+
30
+ def -(key_set)
31
+ self.class.new((keys - key_set.keys).freeze, mapper)
32
+ end
33
+
34
+ def &(key_set)
35
+ self.class.new((keys & key_set.keys).compact.freeze, mapper)
36
+ end
37
+
38
+ # With no block, returns an instance for the first key. If a block is given,
39
+ # it returns the first instance yielding a true value.
40
+ def first(&block)
41
+ block_given? ? all.detect(&block) : all.first
42
+ end
43
+
44
+ # With no block, returns the number of keys. If a block is given, counts the
45
+ # number of elements yielding a true value.
46
+ def count(&block)
47
+ return keys.count unless block_given?
48
+ proxy = HashProxy.new
49
+ keys.inject(0) do |count, key|
50
+ proxy.with(mapper[key], &block) ? count.succ : count
51
+ end
52
+ end
53
+
54
+ def find(id = nil, &block)
55
+ return mapper.get(id) if id
56
+ return self unless block_given?
57
+ proxy = HashProxy.new
58
+ self.class.new(keys.inject([]) do |found, key|
59
+ found << key if proxy.with(mapper[key], &block)
60
+ found
61
+ end, mapper)
62
+ end
63
+
64
+ def to_enum
65
+ KeyIterator.new(keys) {|k| @mapper.get(k)}
66
+ end
67
+ alias all to_enum
68
+
69
+ def find_by_key(&block)
70
+ return self unless block_given?
71
+ self.class.new(keys.inject([]) do |set, key|
72
+ set << key if yield(key); set
73
+ end, mapper)
74
+ end
75
+
76
+ def sort(&block)
77
+ proxies = HashProxySet.new
78
+ self.class.new(@keys.sort do |a, b|
79
+ begin
80
+ yield(*proxies.using(mapper[a], mapper[b]))
81
+ ensure
82
+ proxies.clear
83
+ end
84
+ end, mapper)
85
+ end
86
+
87
+ def limit(length)
88
+ self.class.new(@keys.first(length).freeze, mapper)
89
+ end
90
+ end
91
+
92
+ class KeyIterator
93
+ include Enumerable
94
+
95
+ attr_reader :keys, :callable
96
+
97
+ def initialize(keys, &callable)
98
+ @keys = keys
99
+ @callable = callable
100
+ end
101
+
102
+ def each(&block)
103
+ block_given? ? keys.each {|k| yield callable.call(k)} : to_enum
104
+ end
105
+ end
106
+ end