ambry 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,25 @@
1
+ require File.expand_path("../lib/ambry/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.homepage = "http://github.com/norman/ambry"
8
+ s.name = "ambry"
9
+ s.platform = Gem::Platform::RUBY
10
+ s.rubyforge_project = "[none]"
11
+ s.summary = "An ActiveModel-compatible ORM-like library for storing model instances in an in-memory Hash."
12
+ s.test_files = Dir.glob "test/**/*_test.rb"
13
+ s.version = Ambry::Version::STRING
14
+ s.description = <<-EOD
15
+ Ambry is not an ORM, man! It's a database and ORM replacement for (mostly)
16
+ static models and small datasets. It provides ActiveModel compatibility, and
17
+ flexible searching and storage.
18
+ EOD
19
+ s.add_development_dependency "ffaker"
20
+ s.add_development_dependency "minitest", "~> 2.2.2"
21
+ s.add_development_dependency "mocha"
22
+ s.add_development_dependency "activesupport", "~> 3.0"
23
+ s.add_development_dependency "activemodel", "~> 3.0"
24
+ s.add_development_dependency "rake"
25
+ end
@@ -0,0 +1,107 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ require "benchmark"
4
+ require "ffaker"
5
+ require "ambry"
6
+
7
+ N = 500
8
+
9
+ Ambry::Adapter.new
10
+
11
+ class Person
12
+ extend Ambry::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 "ambry"
10
+ require "ambry/adapters/cookie"
11
+ require "rack/ambry"
12
+
13
+ set :session, false
14
+
15
+ use Rack::Cookies
16
+ use Rack::Ambry, :name => :cookie, :secret => "Sssshhhh! This is a secret."
17
+
18
+ class Book
19
+ extend Ambry::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 Ambry 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 Ambry's filters
2
+
3
+ require "bundler/setup"
4
+ require "ambry"
5
+
6
+ Ambry::Adapter.new
7
+
8
+ class Country
9
+ extend Ambry::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,54 @@
1
+ require "forwardable"
2
+ require "thread"
3
+ require "ambry/adapter"
4
+ require "ambry/abstract_key_set"
5
+ require "ambry/mapper"
6
+ require "ambry/model"
7
+ require "ambry/hash_proxy"
8
+ require "ambry/adapters/file"
9
+
10
+ # Ambry is a database and ORM replacement for small, mostly static models.
11
+ #
12
+ # Ambry is free software released under the terms of the MIT License.
13
+ # @author Norman Clarke
14
+ module Ambry
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 Ambry adapters.
24
+ attr_reader :adapters
25
+ @adapters = {}
26
+
27
+ # Registers an adapter with Ambry. 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 Ambry::Model::ClassMethods#use
32
+ def register_adapter(adapter)
33
+ name = adapter.name.to_sym
34
+ if adapters[name]
35
+ raise AmbryError, "Adapter #{name.inspect} already registered"
36
+ end
37
+ @lock.synchronize do
38
+ adapters[name] = adapter
39
+ end
40
+ end
41
+
42
+ # Base error for Ambry.
43
+ class AmbryError < StandardError ; end
44
+
45
+ # Raised when a single instance is expected but could not be found.
46
+ class NotFoundError < AmbryError
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 Ambry
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