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 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,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 0
4
+ :patch: 0
@@ -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
@@ -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,11 @@
1
+ module CurlyMustache
2
+ module Attributes
3
+ class Definitions < Hash
4
+ def [](key)
5
+ key = key.to_sym
6
+ raise AttributeNotDefinedError, "unexpected attribute: #{key}" unless has_key?(key)
7
+ super(key)
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -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