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,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