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