cjbottaro-curly_mustache 0.0.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.
- 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
|