cjbottaro-curly_mustache 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +47 -0
- data/Rakefile +56 -0
- data/VERSION.yml +4 -0
- data/lib/adapters/abstract.rb +32 -0
- data/lib/adapters/memcache.rb +37 -0
- data/lib/adapters/redis.rb +34 -0
- data/lib/adapters/tokyo_tyrant.rb +44 -0
- data/lib/association_collection.rb +57 -0
- data/lib/association_manager.rb +88 -0
- data/lib/associations.rb +59 -0
- data/lib/attributes/definer.rb +26 -0
- data/lib/attributes/definitions.rb +11 -0
- data/lib/attributes/typecaster.rb +71 -0
- data/lib/attributes/types.rb +39 -0
- data/lib/base.rb +93 -0
- data/lib/crud.rb +122 -0
- data/lib/curly_mustache.rb +15 -0
- data/lib/errors.rb +8 -0
- data/lib/helpers.rb +16 -0
- data/test/associations_test.rb +79 -0
- data/test/crud_test.rb +165 -0
- data/test/test_helper.rb +13 -0
- metadata +78 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 cjbottaro
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
= CurlyMustache
|
2
|
+
|
3
|
+
Like ActiveRecord, but uses key-value stores (Memcached, Redis, Tokyo Cabinet, etc) instead of relational databases.
|
4
|
+
|
5
|
+
This is experimental and not used in any production environments yet.
|
6
|
+
|
7
|
+
http://github.com/cjbottaro/curly_mustache
|
8
|
+
|
9
|
+
== Installation
|
10
|
+
|
11
|
+
sudo gem install --source http://gems.github.com cjbottaro-curly_mustache
|
12
|
+
|
13
|
+
== Features
|
14
|
+
|
15
|
+
* Basic ActiveRecord-like CRUD operations (create, destroy, find, save).
|
16
|
+
* Simple associations.
|
17
|
+
* Validations (TODO)
|
18
|
+
* Callbacks (TODO)
|
19
|
+
|
20
|
+
== Connecting
|
21
|
+
|
22
|
+
CurlyMustache::Base.establish_connection :adapter => :memcache,
|
23
|
+
:servers => %w[one.example.com:11211 two.example.com:11211],
|
24
|
+
|
25
|
+
The +memcache+ adapter uses memcache-client[http://github.com/mperham/memcache-client]. Whatever other options you pass to +establish_connection+ will be passed to the constructor for it.
|
26
|
+
|
27
|
+
== Usage
|
28
|
+
|
29
|
+
class User < CurlyMustache::Base
|
30
|
+
attribute :name, :string
|
31
|
+
attribute :birthday_on, :date
|
32
|
+
attribute :login_count, :integer
|
33
|
+
attribute :last_login_at, :time
|
34
|
+
end
|
35
|
+
|
36
|
+
user = User.create :name => "chris"
|
37
|
+
user.id # => "676cef021584904876af7c4b3e42afb5"
|
38
|
+
user.name # => "chris"
|
39
|
+
user.birthday_on = "03/11/1980"
|
40
|
+
user.save
|
41
|
+
|
42
|
+
user = User.find(user.id)
|
43
|
+
puts user.birthday_on # => "Tue, 11 Mar 1980"
|
44
|
+
user.birthday_on.class # => Date
|
45
|
+
|
46
|
+
user.destroy
|
47
|
+
User.find(user.id) # => exception CurlyMustache::RecordNotFound
|
data/Rakefile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "curly_mustache"
|
8
|
+
gem.summary = %Q{Like ActiveRecord, but uses key-value stores (Tokyo Cabinet, Redis, MemcacheDB, etc) instead of relational databases.}
|
9
|
+
gem.email = "cjbottaro@alumni.cs.utexas.edu"
|
10
|
+
gem.homepage = "http://github.com/cjbottaro/curly_mustache"
|
11
|
+
gem.authors = ["Christopher J Bottaro"]
|
12
|
+
|
13
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
14
|
+
end
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
17
|
+
end
|
18
|
+
|
19
|
+
require 'rake/testtask'
|
20
|
+
Rake::TestTask.new(:test) do |test|
|
21
|
+
test.libs << 'lib' << 'test'
|
22
|
+
test.pattern = 'test/**/*_test.rb'
|
23
|
+
test.verbose = true
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
require 'rcov/rcovtask'
|
28
|
+
Rcov::RcovTask.new do |test|
|
29
|
+
test.libs << 'test'
|
30
|
+
test.pattern = 'test/**/*_test.rb'
|
31
|
+
test.verbose = true
|
32
|
+
end
|
33
|
+
rescue LoadError
|
34
|
+
task :rcov do
|
35
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
task :default => :test
|
41
|
+
|
42
|
+
require 'rake/rdoctask'
|
43
|
+
Rake::RDocTask.new do |rdoc|
|
44
|
+
if File.exist?('VERSION.yml')
|
45
|
+
config = YAML.load(File.read('VERSION.yml'))
|
46
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
47
|
+
else
|
48
|
+
version = ""
|
49
|
+
end
|
50
|
+
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "curly_mustache #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
56
|
+
|
data/VERSION.yml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
module Adapters
|
3
|
+
class Abstract
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
raise RuntimeError, "Not implemented!"
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(key)
|
10
|
+
raise RuntimeError, "Not implemented!"
|
11
|
+
end
|
12
|
+
|
13
|
+
def mget(keys)
|
14
|
+
keys.collect{ |key| get(key) }.compact
|
15
|
+
end
|
16
|
+
|
17
|
+
def put(key, value)
|
18
|
+
raise RuntimeError, "Not implemented!"
|
19
|
+
end
|
20
|
+
|
21
|
+
def delete(key)
|
22
|
+
raise RuntimeError, "Not implemented!"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Needed for tests.
|
26
|
+
def flush_db
|
27
|
+
raise RuntimeError, "Not implemented!"
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'memcache'
|
2
|
+
|
3
|
+
module CurlyMustache
|
4
|
+
module Adapters
|
5
|
+
class Memcache < Abstract
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
config = config.reverse_merge :servers => "localhost:11211"
|
9
|
+
@cache = MemCache.new(config[:servers], config)
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(key)
|
13
|
+
(data = @cache.get(key)) and Marshal.load(data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def mget(keys)
|
17
|
+
keys = keys.collect(&:to_s)
|
18
|
+
results = @cache.get_multi(*keys)
|
19
|
+
results = results.collect{ |k, v| [k, Marshal.load(v)] }
|
20
|
+
results.sort.collect{ |result| result[1] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def put(key, value)
|
24
|
+
@cache.set(key, Marshal.dump(value))
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(key)
|
28
|
+
@cache.delete(key)
|
29
|
+
end
|
30
|
+
|
31
|
+
def flush_db
|
32
|
+
@cache.flush_all
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'redis'
|
2
|
+
|
3
|
+
module CurlyMustache
|
4
|
+
module Adapters
|
5
|
+
class Redis < Abstract
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@redis = ::Redis.new(config)
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(key)
|
12
|
+
(data = @redis.get(key)) and Marshal.load(data)
|
13
|
+
end
|
14
|
+
|
15
|
+
def mget(keys)
|
16
|
+
keys = keys.collect(&:to_s)
|
17
|
+
@redis.mget(*keys).compact.collect{ |value| Marshal.load(value) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def put(key, value)
|
21
|
+
@redis.set(key, Marshal.dump(value))
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(key)
|
25
|
+
@redis.delete(key)
|
26
|
+
end
|
27
|
+
|
28
|
+
def raw
|
29
|
+
@redis
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rufus/tokyo/tyrant'
|
2
|
+
|
3
|
+
module CurlyMustache
|
4
|
+
module Adapters
|
5
|
+
class TokyoTyrant < Abstract
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
config = config.reverse_merge :server => "localhost", :port => 1978
|
9
|
+
@db = Rufus::Tokyo::Tyrant.new(config[:server], config[:port])
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(key)
|
13
|
+
(data = @db[key]) and Marshal.load(data)
|
14
|
+
end
|
15
|
+
|
16
|
+
def mget(keys)
|
17
|
+
keys = keys.collect(&:to_s)
|
18
|
+
results = @db.lget(keys)
|
19
|
+
keys.inject([]) do |memo, key|
|
20
|
+
data = results[key]
|
21
|
+
memo << Marshal.load(data) if data
|
22
|
+
memo
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def put(key, value)
|
27
|
+
@db[key] = Marshal.dump(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete(key)
|
31
|
+
@db.delete(key) ? true : false
|
32
|
+
end
|
33
|
+
|
34
|
+
def flush_db
|
35
|
+
@db.clear
|
36
|
+
end
|
37
|
+
|
38
|
+
def raw
|
39
|
+
@db
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
class AssociationCollection
|
3
|
+
|
4
|
+
delegate :[], :inspect, :length, :to => :implementation
|
5
|
+
|
6
|
+
def initialize(owner, reflection)
|
7
|
+
@owner, @reflection = owner, reflection
|
8
|
+
reload
|
9
|
+
end
|
10
|
+
|
11
|
+
def reload
|
12
|
+
klass = @reflection[:class_name].constantize
|
13
|
+
@array = klass.find(get_ids)
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
@array.dup
|
18
|
+
end
|
19
|
+
|
20
|
+
def <<(value)
|
21
|
+
raise InvalidAssociation unless value.kind_of?(CurlyMustache::Base)
|
22
|
+
@array << value
|
23
|
+
ids = get_ids
|
24
|
+
ids << value.id
|
25
|
+
set_ids(ids)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def implementation
|
31
|
+
@array
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_ids
|
35
|
+
ids = @owner.send(foreign_key)
|
36
|
+
if fkey_type == :string
|
37
|
+
ids.split(',')
|
38
|
+
else
|
39
|
+
ids
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_ids(ids)
|
44
|
+
ids = ids.join(',') if fkey_type == :string
|
45
|
+
@owner.send("#{foreign_key}=", ids)
|
46
|
+
end
|
47
|
+
|
48
|
+
def foreign_key
|
49
|
+
@reflection[:foreign_key]
|
50
|
+
end
|
51
|
+
|
52
|
+
def fkey_type
|
53
|
+
@owner.send(:attribute_type, foreign_key)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
class AssociationManager
|
3
|
+
|
4
|
+
def initialize(owner)
|
5
|
+
@owner = owner
|
6
|
+
@cache = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def get(name, reload = false)
|
10
|
+
return cache_get(name) if cache_hit?(name) and !reload
|
11
|
+
reflection = reflection(name)
|
12
|
+
if reflection.arity == :one
|
13
|
+
get_one(reflection)
|
14
|
+
else
|
15
|
+
get_many(reflection)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_one(reflection)
|
20
|
+
klass = reflection.class_name.constantize
|
21
|
+
fkey = @owner.send(reflection.foreign_key)
|
22
|
+
object = klass.find(fkey)
|
23
|
+
cache_set(reflection.name, object)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_many(reflection)
|
27
|
+
object = AssociationCollection.new(@owner, reflection)
|
28
|
+
cache_set(reflection.name, object)
|
29
|
+
end
|
30
|
+
|
31
|
+
def set(name, object)
|
32
|
+
reflection = reflection(name)
|
33
|
+
if reflection.arity == :one
|
34
|
+
set_one(reflection, object)
|
35
|
+
else
|
36
|
+
set_many(reflection, object)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def set_one(reflection, object)
|
41
|
+
if object.nil?
|
42
|
+
object_id = nil
|
43
|
+
elsif object.kind_of?(CurlyMustache::Base)
|
44
|
+
object_id = object.id
|
45
|
+
else
|
46
|
+
raise InvalidAssociation
|
47
|
+
end
|
48
|
+
@owner.send("#{reflection.foreign_key}=", object_id)
|
49
|
+
cache_set(reflection.name, object)
|
50
|
+
end
|
51
|
+
|
52
|
+
def set_many(reflection, objects)
|
53
|
+
objects = [] if objects.blank?
|
54
|
+
raise InvalidAssociation unless objects.all?{ |object| object.kind_of?(CurlyMustache::Base) }
|
55
|
+
if objects.blank?
|
56
|
+
object_ids = []
|
57
|
+
else
|
58
|
+
object_ids = objects.collect{ |object| object.id }
|
59
|
+
end
|
60
|
+
object_ids = object_ids.join(',') if attribute_type(reflection.foreign_key) == :string
|
61
|
+
@owner.send("#{reflection.foreign_key}=", object_ids)
|
62
|
+
cache_set(reflection.name, objects)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def cache_hit?(name)
|
68
|
+
@cache.has_key?(name.to_s)
|
69
|
+
end
|
70
|
+
|
71
|
+
def cache_get(name)
|
72
|
+
@cache[name.to_s]
|
73
|
+
end
|
74
|
+
|
75
|
+
def cache_set(name, object)
|
76
|
+
@cache[name.to_s] = object
|
77
|
+
end
|
78
|
+
|
79
|
+
def reflection(name)
|
80
|
+
@owner.class.associations[name.to_sym]
|
81
|
+
end
|
82
|
+
|
83
|
+
def attribute_type(name)
|
84
|
+
@owner.send(:attribute_type, name)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
data/lib/associations.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'association_manager'
|
2
|
+
|
3
|
+
module CurlyMustache
|
4
|
+
module Associations
|
5
|
+
|
6
|
+
def self.included(mod)
|
7
|
+
mod.class_eval{ class_inheritable_accessor :associations }
|
8
|
+
mod.associations = {}
|
9
|
+
mod.send(:extend, ClassMethods)
|
10
|
+
mod.send(:include, InstanceMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
def association(name, options = {})
|
16
|
+
# Sanitize arguments.
|
17
|
+
name = name.to_s
|
18
|
+
|
19
|
+
# Default options.
|
20
|
+
options = options.reverse_merge :arity => :many,
|
21
|
+
:name => name
|
22
|
+
|
23
|
+
# More default options (based off previous default options).
|
24
|
+
if options[:arity] == :many
|
25
|
+
options = options.reverse_merge :class_name => name.singularize.camelize,
|
26
|
+
:foreign_key => name.singularize.foreign_key.pluralize
|
27
|
+
else
|
28
|
+
options = options.reverse_merge :class_name => name.camelize,
|
29
|
+
:foreign_key => name.foreign_key
|
30
|
+
end
|
31
|
+
|
32
|
+
# Save the association reflection info.
|
33
|
+
self.associations[name.to_sym] = options.to_struct
|
34
|
+
|
35
|
+
# Create associations accessors.
|
36
|
+
class_eval <<-END
|
37
|
+
def #{name}(reload = false)
|
38
|
+
association_manager.get(:#{name}, reload)
|
39
|
+
end
|
40
|
+
def #{name}=(value)
|
41
|
+
association_manager.set(:#{name}, value)
|
42
|
+
end
|
43
|
+
END
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
module InstanceMethods
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def association_manager
|
53
|
+
@associations_manager ||= AssociationManager.new(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end # module Crud
|
59
|
+
end # module CurlyMustache
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
module Attributes
|
3
|
+
class Definer
|
4
|
+
|
5
|
+
def initialize(klass)
|
6
|
+
@class = klass
|
7
|
+
end
|
8
|
+
|
9
|
+
def define(name, type, options = {})
|
10
|
+
@class.check_attribute_type(type)
|
11
|
+
definition = { :type => type }
|
12
|
+
@class.attribute_definitions[name.to_sym] = definition
|
13
|
+
@class.class_eval <<-END
|
14
|
+
def #{name}
|
15
|
+
read_attribute('#{name}')
|
16
|
+
end
|
17
|
+
|
18
|
+
def #{name}=(value)
|
19
|
+
write_attribute('#{name}', value)
|
20
|
+
end
|
21
|
+
END
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
module Attributes
|
3
|
+
class Typecaster
|
4
|
+
|
5
|
+
SCOPES = %w[read write load store]
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
end
|
9
|
+
|
10
|
+
def cast(scope, type, value)
|
11
|
+
method_name = "to_#{scope}_#{type}"
|
12
|
+
return nil if value.nil?
|
13
|
+
respond_to?(method_name) ? send(method_name, value) : value
|
14
|
+
end
|
15
|
+
|
16
|
+
SCOPES.each do |scope|
|
17
|
+
class_eval <<-END
|
18
|
+
def cast_for_#{scope}(type, value)
|
19
|
+
cast('#{scope}', type, value)
|
20
|
+
end
|
21
|
+
END
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_write_array(value)
|
25
|
+
value.to_a
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_write_integer(value)
|
29
|
+
value.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_write_string(value)
|
33
|
+
value.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_write_date(value)
|
37
|
+
value.to_date
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_write_time(value)
|
41
|
+
value.to_time.utc
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_write_datetime(value)
|
45
|
+
value.to_datetime.utc
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_write_boolean(value)
|
49
|
+
case value
|
50
|
+
when String
|
51
|
+
%w[t true 1 y yes].include?(value.downcase)
|
52
|
+
else
|
53
|
+
[true, 1].include?(value)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_write_float(value)
|
58
|
+
value.to_f
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_read_time(value)
|
62
|
+
value.time_in_zone
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_read_datetime(value)
|
66
|
+
value.time_in_zone
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
module Attributes
|
3
|
+
module Types
|
4
|
+
|
5
|
+
DEFAULT_ATTRIBUTE_TYPES = %w[array integer string date time datetime boolean float].freeze
|
6
|
+
|
7
|
+
def self.included(mod)
|
8
|
+
mod.class_eval{ class_inheritable_accessor :attribute_types }
|
9
|
+
mod.attribute_types = DEFAULT_ATTRIBUTE_TYPES.dup
|
10
|
+
mod.extend(ClassMethods)
|
11
|
+
mod.send(:include, InstanceMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
module InstanceMethods
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def attribute_type(name)
|
19
|
+
self.class.attribute_definitions[name.to_sym][:type]
|
20
|
+
end
|
21
|
+
|
22
|
+
end # end module InstanceMethods
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
|
26
|
+
def valid_attribute_type?(type)
|
27
|
+
self.attribute_types.include?(type.to_s)
|
28
|
+
end
|
29
|
+
|
30
|
+
def check_attribute_type(type)
|
31
|
+
raise InvaildAttributeType, "unexpected attribute type: #{type}" unless valid_attribute_type?(type)
|
32
|
+
end
|
33
|
+
|
34
|
+
end # end module ClassMethods
|
35
|
+
|
36
|
+
end # end module Types
|
37
|
+
end # end module Attributes
|
38
|
+
end # end module CurlyMustache
|
39
|
+
|
data/lib/base.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
class Base
|
3
|
+
include Attributes::Types
|
4
|
+
include Crud
|
5
|
+
include Associations
|
6
|
+
|
7
|
+
class_inheritable_accessor :connection
|
8
|
+
class_inheritable_accessor :connection_config
|
9
|
+
class_inheritable_accessor :attribute_definitions
|
10
|
+
class_inheritable_accessor :typecaster
|
11
|
+
|
12
|
+
self.attribute_definitions = Attributes::Definitions.new
|
13
|
+
Attributes::Definer.new(self).define(:id, :integer)
|
14
|
+
|
15
|
+
self.typecaster = Attributes::Typecaster.new
|
16
|
+
|
17
|
+
def self.establish_connection(config)
|
18
|
+
adapter_name = config[:adapter]
|
19
|
+
require "adapters/#{adapter_name}"
|
20
|
+
self.connection_config = config
|
21
|
+
self.connection = "CurlyMustache::Adapters::#{adapter_name.camelize}".constantize.new(config)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.define_attributes(&block)
|
25
|
+
yield(Attributes::Definer.new(self))
|
26
|
+
end
|
27
|
+
|
28
|
+
def id
|
29
|
+
@attributes[:id]
|
30
|
+
end
|
31
|
+
|
32
|
+
def new_record?
|
33
|
+
!!@new_record
|
34
|
+
end
|
35
|
+
|
36
|
+
def attributes
|
37
|
+
@attributes
|
38
|
+
end
|
39
|
+
|
40
|
+
def read_attribute(name)
|
41
|
+
self.class.check_attribute_defined(name)
|
42
|
+
@attributes[name.to_sym]
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_attribute(name, value)
|
46
|
+
self.class.check_attribute_defined(name)
|
47
|
+
type = self.class.attribute_definitions[name][:type]
|
48
|
+
@attributes[name.to_sym] = typecast(:write, type, value)
|
49
|
+
end
|
50
|
+
|
51
|
+
def ==(other)
|
52
|
+
self.attributes == other.attributes and
|
53
|
+
self.new_record? == other.new_record?
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def self.id_to_key(id)
|
59
|
+
"#{self.name.underscore}_#{id}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.ids_to_keys(ids)
|
63
|
+
[ids].flatten.collect{ |id| id_to_key(id) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.key_to_id(key)
|
67
|
+
key.split("_").last
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.keys_to_ids(keys)
|
71
|
+
keys.collect{ |key| key_to_id(key) }
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.attribute_defined?(name)
|
75
|
+
self.attribute_definitions.has_key?(name.to_sym)
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.check_attribute_defined(name)
|
79
|
+
raise AttributeNotDefinedError, "unexpected attribute: #{name}" unless attribute_defined?(name)
|
80
|
+
end
|
81
|
+
|
82
|
+
def key
|
83
|
+
raise NoKeyError unless id
|
84
|
+
self.class.id_to_key(id)
|
85
|
+
end
|
86
|
+
|
87
|
+
def typecast(scope, type, value)
|
88
|
+
self.class.typecaster.cast(scope, type, value)
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
data/lib/crud.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
|
3
|
+
module Crud
|
4
|
+
|
5
|
+
def self.included(mod)
|
6
|
+
mod.send(:extend, ClassMethods)
|
7
|
+
mod.send(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def generate_id
|
13
|
+
Digest::MD5.hexdigest(rand.to_s + Time.now.to_s)
|
14
|
+
end
|
15
|
+
|
16
|
+
def create(attributes = {})
|
17
|
+
returning(new(attributes)){ |record| record.save }
|
18
|
+
end
|
19
|
+
|
20
|
+
def create!(attributes = {})
|
21
|
+
new(attributes).save!
|
22
|
+
end
|
23
|
+
|
24
|
+
def find(*ids)
|
25
|
+
ids = [ids].flatten
|
26
|
+
if ids.length == 1
|
27
|
+
find_one(ids.first)
|
28
|
+
else
|
29
|
+
find_many(ids)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def delete_all(*ids)
|
34
|
+
ids_to_keys(ids).each{ |key| connection.delete(key) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def destroy_all(*ids)
|
38
|
+
find(ids).each{ |record| record.destroy }
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def find_one(id)
|
44
|
+
raise RecordNotFound, "Couldn't find #{self} without an ID" if id.blank?
|
45
|
+
raise RecordNotFound, "Couldn't find #{self} with ID=#{id}" if (data = connection.get(id_to_key(id))).blank?
|
46
|
+
returning(new){ |record| record.send(:load, data) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def find_many(ids)
|
50
|
+
keys = ids_to_keys(ids)
|
51
|
+
datas = connection.mget(keys)
|
52
|
+
if keys.length != datas.length
|
53
|
+
raise RecordNotFound, "Couldn't find all #{self.name} with IDs (#{ids.join(',')}) (found #{datas.length} results, but was looking for #{ids.length})"
|
54
|
+
else
|
55
|
+
datas.collect{ |data| returning(new){ |record| record.send(:load, data) } }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
module InstanceMethods
|
62
|
+
|
63
|
+
def initialize(attributes = {})
|
64
|
+
@attributes = {}
|
65
|
+
@new_record = true
|
66
|
+
attributes.each{ |k, v| write_attribute(k, v) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def reload
|
70
|
+
returning(self){ @attributes = self.class.find(id).attributes }
|
71
|
+
end
|
72
|
+
|
73
|
+
def save
|
74
|
+
save!
|
75
|
+
true
|
76
|
+
rescue ValidationError => e
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
def save!
|
81
|
+
new_record? ? create : update
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def destroy
|
86
|
+
self.class.delete_all(id)
|
87
|
+
self.freeze
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def set_id
|
93
|
+
if id.blank?
|
94
|
+
@attributes[:id] = self.class.generate_id
|
95
|
+
else
|
96
|
+
true
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def create
|
101
|
+
set_id and store
|
102
|
+
end
|
103
|
+
|
104
|
+
def update
|
105
|
+
store
|
106
|
+
end
|
107
|
+
|
108
|
+
def load(data)
|
109
|
+
@attributes = data
|
110
|
+
@new_record = false
|
111
|
+
end
|
112
|
+
|
113
|
+
def store
|
114
|
+
connection.put(key, @attributes)
|
115
|
+
@new_record = false
|
116
|
+
end
|
117
|
+
|
118
|
+
end # end module InstanceMethods
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'activesupport'
|
3
|
+
require 'digest'
|
4
|
+
|
5
|
+
require 'adapters/abstract'
|
6
|
+
require 'errors'
|
7
|
+
require 'helpers'
|
8
|
+
require 'attributes/definer'
|
9
|
+
require 'attributes/definitions'
|
10
|
+
require 'attributes/types'
|
11
|
+
require 'attributes/typecaster'
|
12
|
+
require 'crud'
|
13
|
+
require 'associations'
|
14
|
+
require 'association_collection'
|
15
|
+
require 'base'
|
data/lib/errors.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
module CurlyMustache
|
2
|
+
class NoKeyError < RuntimeError; end
|
3
|
+
class AttributeNotDefinedError < RuntimeError; end
|
4
|
+
class InvaildAttributeType < ArgumentError; end
|
5
|
+
class InvalidAssociation < RuntimeError; end
|
6
|
+
class RecordNotFound < RuntimeError; end
|
7
|
+
class ValidationError < RuntimeError; end
|
8
|
+
end
|
data/lib/helpers.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Hash
|
2
|
+
def to_struct(name = nil)
|
3
|
+
name = "Hash" if name.blank?
|
4
|
+
struct = Struct.new(name.to_s, *(keys.collect{ |key| key.to_sym }))
|
5
|
+
returning(struct.new){ |struct| each{ |k, v| struct.send("#{k}=", v) }}
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class Object
|
10
|
+
def meta_eval(&block)
|
11
|
+
(class << self; self; end).instance_eval(&block)
|
12
|
+
end
|
13
|
+
def full?
|
14
|
+
!blank?
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class Account < CurlyMustache::Base
|
4
|
+
define_attributes do |attribute|
|
5
|
+
attribute.define :name, :string
|
6
|
+
attribute.define :user_ids, :array
|
7
|
+
end
|
8
|
+
|
9
|
+
association :users
|
10
|
+
end
|
11
|
+
|
12
|
+
class User < CurlyMustache::Base
|
13
|
+
define_attributes do |attribute|
|
14
|
+
attribute.define :account_id, :integer
|
15
|
+
attribute.define :name, :string
|
16
|
+
end
|
17
|
+
|
18
|
+
association :account, :arity => :one
|
19
|
+
end
|
20
|
+
|
21
|
+
class AssociationsTest < Test::Unit::TestCase
|
22
|
+
|
23
|
+
def test_one
|
24
|
+
# set up some records
|
25
|
+
Account.create! :id => 1, :name => "account"
|
26
|
+
user = User.create! :id => 1, :account_id => 1, :name => "user"
|
27
|
+
|
28
|
+
# test that the association loads
|
29
|
+
assert(user.account)
|
30
|
+
assert_equal("account", user.account.name)
|
31
|
+
|
32
|
+
# test that we can set the association's attributes
|
33
|
+
user.account.name = "tnuocca"
|
34
|
+
assert_equal("tnuocca", user.account.name)
|
35
|
+
|
36
|
+
# reload association
|
37
|
+
assert_equal("account", user.account(true).name)
|
38
|
+
|
39
|
+
# Assigning to the association should change our "foreign key" attribute.
|
40
|
+
account2 = Account.create :id => 2, :name => "account2"
|
41
|
+
user.account = account2
|
42
|
+
assert_equal(account2, user.account)
|
43
|
+
assert_equal(account2.id, user.account_id)
|
44
|
+
|
45
|
+
# Assigning nil to the association should blank out our "foreign key" attribute.
|
46
|
+
user.account = nil
|
47
|
+
assert_equal(nil, user.account)
|
48
|
+
assert_equal(nil, user.account_id)
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_many
|
52
|
+
# Set up some records.
|
53
|
+
1.upto(5){ |i| User.create! :id => i }
|
54
|
+
account = Account.create! :id => 1, :user_ids => [1, 2, 3]
|
55
|
+
|
56
|
+
# Load the association, make sure it looks ok.
|
57
|
+
assert(account.users.instance_of?(CurlyMustache::AssociationCollection))
|
58
|
+
assert_equal 3, account.users.length
|
59
|
+
assert_equal User.find(1, 2, 3), account.users.to_a
|
60
|
+
|
61
|
+
# Add an item to the association.
|
62
|
+
account.users << User.find(4)
|
63
|
+
assert_equal 4, account.users.length
|
64
|
+
assert_equal User.find(1, 2, 3, 4), account.users.to_a
|
65
|
+
assert_equal [1, 2, 3, 4], account.user_ids
|
66
|
+
|
67
|
+
# Make sure reloading works.
|
68
|
+
old_name = account.users[0].name
|
69
|
+
account.users[0].name = old_name.to_s + "blah"
|
70
|
+
assert_equal(old_name, account.users(true)[0].name)
|
71
|
+
|
72
|
+
# Set the association.
|
73
|
+
account.users = User.find(1, 2, 3, 4)
|
74
|
+
assert_equal 4, account.users.length
|
75
|
+
assert_equal User.find(1, 2, 3, 4), account.users
|
76
|
+
assert_equal [1, 2, 3, 4], account.user_ids
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/test/crud_test.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class User < CurlyMustache::Base
|
4
|
+
define_attributes do |attribute|
|
5
|
+
attribute.define :name, :string
|
6
|
+
attribute.define :phone_number, :integer
|
7
|
+
attribute.define :balance, :float
|
8
|
+
attribute.define :is_admin, :boolean
|
9
|
+
attribute.define :created_at, :datetime
|
10
|
+
attribute.define :updated_at, :time
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class CrudTest < Test::Unit::TestCase
|
15
|
+
|
16
|
+
def setup
|
17
|
+
CurlyMustache::Base.connection.flush_db
|
18
|
+
end
|
19
|
+
|
20
|
+
def attributes_hash
|
21
|
+
{ :id => 123,
|
22
|
+
:name => "chris",
|
23
|
+
:phone_number => 5128258325,
|
24
|
+
:balance => 1.01,
|
25
|
+
:is_admin => true,
|
26
|
+
:created_at => DateTime.parse("2009-04-23 22:09:50.936751 -05:00"),
|
27
|
+
:updated_at => Time.parse("2009-04-23 22:09:50.936751 -05:00") }.dup
|
28
|
+
end
|
29
|
+
|
30
|
+
def assert_attributes(record, attributes = nil)
|
31
|
+
(attributes or attributes_hash).each do |k, v|
|
32
|
+
assert_equal v, record.send(k), "for attribute '#{k}'"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_create
|
37
|
+
user = User.create(attributes_hash)
|
38
|
+
assert(user)
|
39
|
+
assert(!user.new_record?)
|
40
|
+
user = User.find(attributes_hash[:id])
|
41
|
+
assert_attributes(user)
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_create!
|
45
|
+
user = User.create!(attributes_hash)
|
46
|
+
assert(user)
|
47
|
+
assert(!user.new_record?)
|
48
|
+
user = User.find(attributes_hash[:id])
|
49
|
+
assert_attributes(user)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_create_autogen_id
|
53
|
+
id = "1341adb122d8190872c2928dbcc08b9d"
|
54
|
+
User.expects(:generate_id).returns(id)
|
55
|
+
user = User.create :name => "christopher"
|
56
|
+
assert_equal id, user.id
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_find
|
60
|
+
attributes1 = attributes_hash
|
61
|
+
attributes1[:id] = 1
|
62
|
+
User.create(attributes1)
|
63
|
+
|
64
|
+
attributes2 = attributes_hash
|
65
|
+
attributes2[:id] = 2
|
66
|
+
User.create(attributes2)
|
67
|
+
|
68
|
+
assert_equal User.find(1), User.find(1)
|
69
|
+
|
70
|
+
user1 = User.find(1)
|
71
|
+
assert_attributes(user1, attributes1)
|
72
|
+
|
73
|
+
user2 = User.find(2)
|
74
|
+
assert_attributes(user2, attributes2)
|
75
|
+
|
76
|
+
assert_equal [user1, user2], User.find(1, 2)
|
77
|
+
assert_raise(CurlyMustache::RecordNotFound){ User.find(3) }
|
78
|
+
assert_raise(CurlyMustache::RecordNotFound){ User.find(1, 2, 3) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_delete_all
|
82
|
+
1.upto(3) do |i|
|
83
|
+
attributes = attributes_hash
|
84
|
+
attributes[:id] = i
|
85
|
+
User.create(attributes)
|
86
|
+
end
|
87
|
+
|
88
|
+
User.delete_all(1, 2)
|
89
|
+
assert_raise(CurlyMustache::RecordNotFound){ User.find(1) }
|
90
|
+
assert_raise(CurlyMustache::RecordNotFound){ User.find(2) }
|
91
|
+
assert(User.find(3))
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_destroy_all
|
95
|
+
1.upto(3) do |i|
|
96
|
+
attributes = attributes_hash
|
97
|
+
attributes[:id] = i
|
98
|
+
User.create(attributes)
|
99
|
+
end
|
100
|
+
|
101
|
+
User.destroy_all(1, 2)
|
102
|
+
assert_raise(CurlyMustache::RecordNotFound){ User.find(1) }
|
103
|
+
assert_raise(CurlyMustache::RecordNotFound){ User.find(2) }
|
104
|
+
assert(User.find(3))
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_new
|
108
|
+
user = User.new(attributes_hash)
|
109
|
+
assert_attributes(user)
|
110
|
+
assert(user.new_record?)
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_reload
|
114
|
+
user = User.new(attributes_hash)
|
115
|
+
assert_raise(CurlyMustache::RecordNotFound){ user.reload }
|
116
|
+
|
117
|
+
assert(user = User.create(attributes_hash))
|
118
|
+
User.delete_all(user.id)
|
119
|
+
assert_raise(CurlyMustache::RecordNotFound){ user.reload }
|
120
|
+
|
121
|
+
assert(user = User.create(attributes_hash))
|
122
|
+
user.name = "callie"
|
123
|
+
assert_equal(user.read_attribute(:name), "callie")
|
124
|
+
user.reload
|
125
|
+
assert_equal(user.read_attribute(:name), attributes_hash[:name])
|
126
|
+
end
|
127
|
+
|
128
|
+
def test_save_by_expectations
|
129
|
+
user = User.new(attributes_hash)
|
130
|
+
user.expects(:create).once
|
131
|
+
user.save
|
132
|
+
|
133
|
+
user = User.create!(attributes_hash)
|
134
|
+
user.expects(:update).once
|
135
|
+
user.save
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_save
|
139
|
+
do_test_save(:save)
|
140
|
+
end
|
141
|
+
|
142
|
+
def test_save!
|
143
|
+
do_test_save(:save!)
|
144
|
+
end
|
145
|
+
|
146
|
+
def test_destroy
|
147
|
+
user = User.create!(attributes_hash)
|
148
|
+
user.destroy
|
149
|
+
assert(user.frozen?)
|
150
|
+
assert_raise(CurlyMustache::RecordNotFound){ user.reload }
|
151
|
+
end
|
152
|
+
|
153
|
+
def do_test_save(method_name)
|
154
|
+
user = User.new(attributes_hash)
|
155
|
+
user.send(method_name)
|
156
|
+
assert_attributes(user.reload)
|
157
|
+
|
158
|
+
user = User.create!(attributes_hash)
|
159
|
+
user.name = "callie"
|
160
|
+
user.send(method_name)
|
161
|
+
user.reload
|
162
|
+
assert_equal("callie", user.name)
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'mocha'
|
4
|
+
|
5
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
6
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
7
|
+
require 'curly_mustache'
|
8
|
+
|
9
|
+
class Test::Unit::TestCase
|
10
|
+
end
|
11
|
+
|
12
|
+
adapter = ENV["ADAPTER"] || :memcache
|
13
|
+
CurlyMustache::Base.establish_connection(:adapter => adapter)
|
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cjbottaro-curly_mustache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christopher J Bottaro
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-12 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: cjbottaro@alumni.cs.utexas.edu
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README.rdoc
|
28
|
+
- Rakefile
|
29
|
+
- VERSION.yml
|
30
|
+
- lib/adapters/abstract.rb
|
31
|
+
- lib/adapters/memcache.rb
|
32
|
+
- lib/adapters/redis.rb
|
33
|
+
- lib/adapters/tokyo_tyrant.rb
|
34
|
+
- lib/association_collection.rb
|
35
|
+
- lib/association_manager.rb
|
36
|
+
- lib/associations.rb
|
37
|
+
- lib/attributes/definer.rb
|
38
|
+
- lib/attributes/definitions.rb
|
39
|
+
- lib/attributes/typecaster.rb
|
40
|
+
- lib/attributes/types.rb
|
41
|
+
- lib/base.rb
|
42
|
+
- lib/crud.rb
|
43
|
+
- lib/curly_mustache.rb
|
44
|
+
- lib/errors.rb
|
45
|
+
- lib/helpers.rb
|
46
|
+
- test/associations_test.rb
|
47
|
+
- test/crud_test.rb
|
48
|
+
- test/test_helper.rb
|
49
|
+
has_rdoc: true
|
50
|
+
homepage: http://github.com/cjbottaro/curly_mustache
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options:
|
53
|
+
- --charset=UTF-8
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.2.0
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Like ActiveRecord, but uses key-value stores (Tokyo Cabinet, Redis, MemcacheDB, etc) instead of relational databases.
|
75
|
+
test_files:
|
76
|
+
- test/associations_test.rb
|
77
|
+
- test/crud_test.rb
|
78
|
+
- test/test_helper.rb
|