curly_mustache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ require 'cassandra'
2
+
3
+ module CurlyMustache
4
+ module Adapters
5
+ class Cassandra < Abstract
6
+
7
+ def initialize(options)
8
+ @client = ::Cassandra.new(options[:keyspace], options[:servers])
9
+ @column_family = options[:column_family]
10
+ end
11
+
12
+ def column_family
13
+ @column_family || model_class.name.pluralize.to_sym
14
+ end
15
+
16
+ def put(key, value)
17
+ @client.insert(column_family, key, value)
18
+ end
19
+
20
+ def get(key)
21
+ result = @client.get(column_family, key)
22
+ result.empty? ? nil : result
23
+ end
24
+
25
+ def delete(key)
26
+ @client.remove(column_family, key)
27
+ end
28
+
29
+ def flush_db
30
+ @client.clear_keyspace!
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,56 @@
1
+ require 'memcache'
2
+
3
+ module CurlyMustache
4
+ module Adapters
5
+ # You can use this adapter with any data store that speaks Memcached. The adapter uses
6
+ # {memcache-client}[http://github.com/mperham/memcache-client]. The <tt>:servers</tt> key in
7
+ # the hash passed to CurlyMustache::Base#establish_connection will be the first argument to
8
+ # <tt>MemCache.new</tt> and entire hash will be passed as the second argument.
9
+ class Memcached < Abstract
10
+
11
+ # <tt>config[:servers]</tt> will be passed as the first argument to <tt>MemCache.new</tt> and
12
+ # <tt>config</tt> itself will be passed as the second argument.
13
+ def initialize(config)
14
+ config = config.reverse_merge :servers => "localhost:11211"
15
+ @cache = MemCache.new(config[:servers], config)
16
+ end
17
+
18
+ def get(key)
19
+ @cache.get(key)
20
+ end
21
+
22
+ def mget(keys)
23
+ keys = keys.collect(&:to_s)
24
+ results = @cache.get_multi(*keys)
25
+ results = results.collect{ |k, v| [k, v] }
26
+ results.sort.collect{ |result| result[1] }
27
+ end
28
+
29
+ def put(key, value)
30
+ @cache.set(key, value)
31
+ end
32
+
33
+ def delete(key)
34
+ @cache.delete(key)
35
+ end
36
+
37
+ def flush_db
38
+ @cache.flush_all
39
+ end
40
+
41
+ def lock(key, options = {})
42
+ expires_in = options[:expires_in] || 0
43
+ @cache.add(key, Time.now.to_s(:number), expires_in) == "STORED\r\n"
44
+ end
45
+
46
+ def unlock(key)
47
+ delete(key) == "DELETED\r\n"
48
+ end
49
+
50
+ def locked?(key)
51
+ !!@cache.get(key)
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,85 @@
1
+ require "curly_mustache/attributes/manager"
2
+
3
+ module CurlyMustache
4
+
5
+ # It looks like typecasting happens at assignment for ActiveRecord, so we're just going to follow that.
6
+ # user.account_id = "test"
7
+ # user.account_id
8
+ # => 0
9
+ module Attributes
10
+
11
+ def self.included(mod)
12
+ mod.class_eval do
13
+ class_inheritable_accessor :attribute_manager
14
+ class_inheritable_accessor :allow_settable_id
15
+ end
16
+ mod.attribute_manager = Manager.new
17
+ mod.send(:extend, ClassMethods)
18
+ mod.send(:include, InstanceMethods)
19
+ end
20
+
21
+ module ClassMethods
22
+
23
+ def attribute(name, type, options = {})
24
+ attribute_manager.define(self, name, type, options)
25
+ end
26
+
27
+ def attributes
28
+ attribute_manager.definitions
29
+ end
30
+
31
+ def attribute_type(name)
32
+ attribute_manager[name].type
33
+ end
34
+
35
+ def allow_settable_id!(settable = true)
36
+ self.allow_settable_id = settable
37
+ end
38
+
39
+ end
40
+
41
+ module InstanceMethods
42
+
43
+ def attributes=(hash)
44
+ hash.stringify_keys.each{ |k, v| write_attribute(k, v) }
45
+ end
46
+
47
+ def attributes
48
+ @attributes.dup
49
+ end
50
+
51
+ def read_attribute(name)
52
+ @attributes[name.to_s]
53
+ end
54
+
55
+ def write_attribute(name, value)
56
+ send("#{name}_will_change!") # ActiveModel::Dirty
57
+ @attributes[name.to_s] = value
58
+ end
59
+
60
+ def write_attribute_with_typecast(name, value)
61
+ casted_value = attribute_manager[name].cast(value)
62
+ write_attribute_without_typecast(name, casted_value)
63
+ end
64
+
65
+ alias_method_chain :write_attribute, :typecast
66
+
67
+ def write_attribute_with_id_guard(name, value)
68
+ raise IdNotSettableError, "not allowed to set id" if name.to_s == "id" and !allow_settable_id
69
+ write_attribute_without_id_guard(name, value)
70
+ end
71
+
72
+ alias_method_chain :write_attribute, :id_guard
73
+
74
+ private
75
+
76
+ # This is like #attributes= but allows for setting the id. It's intended to be used
77
+ # internally by methods like #read.
78
+ def set_attributes(hash)
79
+ hash.stringify_keys.each{ |k, v| write_attribute_without_id_guard(k, v) }
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,24 @@
1
+ module CurlyMustache
2
+ module Attributes
3
+ class Definition
4
+ attr_reader :name, :type
5
+
6
+ def initialize(name, type, options = {})
7
+ @options = options.symbolize_keys.reverse_merge :default => nil,
8
+ :allow_nil => true
9
+ @name = name.to_sym
10
+ @type = type.to_sym
11
+ @caster = Types[type].caster
12
+ end
13
+
14
+ def cast(value)
15
+ if value.nil? and @options[:allow_nil]
16
+ nil
17
+ else
18
+ @caster.call(value)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ require "curly_mustache/attributes/types"
2
+ require "curly_mustache/attributes/definition"
3
+
4
+ module CurlyMustache
5
+ module Attributes
6
+ class Manager
7
+ attr_reader :definitions
8
+
9
+ def initialize
10
+ @definitions = {}
11
+ end
12
+
13
+ def define(klass, name, type, options = {})
14
+ @definitions[name.to_s] = Definition.new(name, type, options)
15
+
16
+ klass.class_eval <<-eval
17
+ def #{name}; read_attribute(:#{name}); end
18
+ def #{name}=(value); write_attribute(:#{name}, value); end
19
+ eval
20
+
21
+ # This is so ghetto, but these are the hoops we have to jump through
22
+ # to get ActiveModel::Dirty working with inheritance.
23
+ klass.undefine_attribute_methods
24
+ klass.define_attribute_methods(@definitions.keys.collect(&:to_sym))
25
+ end
26
+
27
+ def [](name)
28
+ name = name.to_s
29
+ raise AttributeNotDefinedError, "#{name} is not defined" unless @definitions.has_key?(name)
30
+ @definitions[name]
31
+ end
32
+
33
+ def dup
34
+ returning(self.class.new) do |new_manager|
35
+ new_manager.instance_variable_set("@definitions", @definitions.dup)
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,49 @@
1
+ module CurlyMustache
2
+ module Attributes
3
+ # <tt>CurlyMustache</tt> comes with 5 types predefined: string, integer, float, time, boolean.
4
+ # You can redefine any of them or add new type defintions. To define a type is simply to define
5
+ # how a value gets typecasted.
6
+ # CurlyMustache::Attributes::Types.define(:capitalized_string) do |value|
7
+ # value.capitalize
8
+ # end
9
+ # Now if you have a user class...
10
+ # class User < CurlyMustache::Base
11
+ # attribute :name, :string
12
+ # attribute :title, :capitalized_string
13
+ # end
14
+ # And you can see the new type in action...
15
+ # user = User.new
16
+ # user.name = "chris"
17
+ # user.title = "mr"
18
+ # user.name # => "chris"
19
+ # user.title # => "Mr"
20
+ # user.title = 123 # NoMethodError: undefined method `capitalize' for 123:Fixnum
21
+ module Types
22
+
23
+ # Gets a hash of all type defintions. The keys will be the type names and they will always be strings.
24
+ def self.definitions
25
+ @definitions ||= {}
26
+ end
27
+
28
+ # Clear all type defintions (including the defaults).
29
+ def self.clear
30
+ @definitions = {}
31
+ end
32
+
33
+ # Define a type. The block takes a single argument which is the raw value and should return
34
+ # the typecasted value.
35
+ def self.define(name, &block)
36
+ definitions[name.to_s] = OpenStruct.new(:name => name, :caster => block)
37
+ end
38
+
39
+ # Similar to <tt>CurlyMustache::Attributes::Types.defintions[name]</tt> but is indifferent to
40
+ # whether +name+ is a string or symbol and will raise an exception if +name+ is not a defined.
41
+ def self.[](name)
42
+ name = name.to_s
43
+ raise TypeError, "type #{name} is not defined" unless definitions.has_key?(name)
44
+ definitions[name]
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,36 @@
1
+ require "curly_mustache/default_types"
2
+
3
+ module CurlyMustache
4
+ class Base
5
+
6
+ include Connection
7
+ include Attributes
8
+ include Crud
9
+
10
+ extend ActiveModel::Callbacks
11
+ include ActiveModel::Validations
12
+ include ActiveModel::Dirty
13
+
14
+ define_model_callbacks :create, :destroy, :save, :update, :validation, :validation_on_create, :validation_on_update, :only => [:before, :after]
15
+ define_model_callbacks :find, :only => :after
16
+
17
+ # Set this to true if you want to set your own ids as opposed to having CurlyMustache
18
+ # automatically generate them for you. Ex:
19
+ # class User
20
+ # self.allow_settable_id = true
21
+ # attribute :name, :string
22
+ # end
23
+ # User.create(:id => 123, :name => "blah")
24
+ # User.find(123)
25
+ allow_settable_id!(false)
26
+
27
+ attribute :id, :string
28
+
29
+ def ==(other)
30
+ self.attributes == other.attributes and
31
+ self.new_record? == other.new_record?
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,65 @@
1
+ module CurlyMustache
2
+ # NOTE: The way this is implemented makes CurlyMustache not thread safe!
3
+ #
4
+ # You are probably looking for {establish_connection}[link:/classes/CurlyMustache/Connection/ClassMethods.html#M000084].
5
+ module Connection
6
+
7
+ def self.included(mod) # :nodoc:
8
+ mod.class_eval do
9
+ class_inheritable_accessor :_connection
10
+ end
11
+ mod.send(:extend, ClassMethods)
12
+ mod.send(:include, InstanceMethods)
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ # Establishes a connection using the adapter specified in <tt>config[:adapter]</tt>.
18
+ # If you call +establish_connection+ on CurlyMustache::Base, then all models will
19
+ # use that connection unless +establish_connection+ is called directly on a model class.
20
+ # Note that +config+ itself is passed to the adapter's constructor.
21
+ #
22
+ # Ex:
23
+ # CurlyMustache::Base.establish_connection(:adapter => :memcached, :servers => "localhost:11211")
24
+ def establish_connection(config)
25
+ config = config.symbolize_keys
26
+ self._connection = Adapters.get(config[:adapter]).new(config)
27
+ end
28
+
29
+ def connection # :nodoc:
30
+ _connection.model_class = self
31
+ _connection
32
+ end
33
+
34
+ end
35
+
36
+ module InstanceMethods
37
+
38
+ # Override this method if you want to massage the data that is sent to the adapter.
39
+ #
40
+ # +attributes+ is the same as <tt>self.attributes</tt>.
41
+ #
42
+ # Return value will be sent to the adapter's +put+ method.
43
+ def send_attributes(attributes)
44
+ attributes
45
+ end
46
+
47
+ # Override this method if you want to massage the data that is received from the adapter.
48
+ #
49
+ # +attributes+ is what is returned from the adapter's +get+ method.
50
+ #
51
+ # Return value will be assigned to <tt>self.attributes</tt>.
52
+ def recv_attributes(attributes)
53
+ attributes
54
+ end
55
+
56
+ private
57
+
58
+ def connection # :nodoc:
59
+ self.class.connection
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,244 @@
1
+ module CurlyMustache
2
+
3
+ module Crud
4
+
5
+ def self.included(mod) # :nodoc:
6
+ mod.send(:extend, ClassMethods)
7
+ mod.send(:include, InstanceMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ # Create a record and save it to the data store. Returns a record with errors if validation fails.
13
+ def create(attributes = {})
14
+ returning(new) do |record|
15
+ record.attributes = attributes
16
+ record.save
17
+ end
18
+ end
19
+
20
+ # Create a record and save it to the data store. Raises RecordInvalid if validation fails.
21
+ def create!(attributes = {})
22
+ returning(create(attributes)) do |record|
23
+ record.errors.count > 0 and raise(RecordInvalid, "Validation failed: #{record.errors.full_messages.join(', ')}")
24
+ end
25
+ end
26
+
27
+ # Find by id. Can take multiple ids. Raise RecordNotFound if not all ids are found.
28
+ def find(*ids)
29
+ ids = [ids].flatten
30
+ if ids.length == 1
31
+ find_one(ids.first)
32
+ else
33
+ find_many(ids, :raise)
34
+ end
35
+ end
36
+
37
+ # Find multiple records by ids. May return an array with less records
38
+ # than ids asked for or an empty array.
39
+ def find_all_by_id(*ids)
40
+ ids = [ids].flatten
41
+ find_many(ids)
42
+ end
43
+
44
+ # Find a single record by id. Returns nil if record is not found.
45
+ def find_by_id(id)
46
+ find_one(id)
47
+ rescue RecordNotFound
48
+ nil
49
+ end
50
+
51
+ # Deletes records by ids without instantiating them first, thus the
52
+ # *_destroy callbacks won't be invoked.
53
+ def delete_all(*ids)
54
+ ids_to_keys(ids).each{ |key| connection.delete(key) }
55
+ end
56
+
57
+ # Instantiate records then calls destroy on them.
58
+ def destroy_all(*ids)
59
+ find(ids).each{ |record| record.destroy }
60
+ end
61
+
62
+ private
63
+
64
+ def id_to_key(id)
65
+ raise NoKeyError if id.blank?
66
+ "#{self}:#{id}"
67
+ end
68
+
69
+ def ids_to_keys(ids)
70
+ [ids].flatten.collect{ |id| id_to_key(id) }
71
+ end
72
+
73
+ def find_one(id)
74
+ raise RecordNotFound, "Couldn't find #{name} without an ID" if id.blank?
75
+ new.send(:read, :id => id)
76
+ end
77
+
78
+ def find_many(ids, should_raise = false)
79
+ hashes = connection.mget(ids_to_keys(ids))
80
+ if should_raise and ids.length != hashes.length
81
+ raise RecordNotFound, find_many_error_message(ids, hashes)
82
+ else
83
+ ids.zip(hashes).collect do |id, attributes|
84
+ record = new
85
+ record.send(:read, :attributes => record.send(:recv_attributes, attributes))
86
+ record
87
+ end
88
+ end
89
+ end
90
+
91
+ def find_many_error_message(ids, hashes)
92
+ ids_string = ids.join(",")
93
+ models_name = name.pluralize
94
+ found, wanted = hashes.length, ids.length
95
+ "Couldn't find all #{models_name} with IDs (#{ids_string}) (found #{found} results, but was looking for #{wanted})"
96
+ end
97
+
98
+ end
99
+
100
+ module InstanceMethods
101
+
102
+ # Make a new record in memory with supplied attributes.
103
+ def initialize(attributes = {})
104
+ @attributes = {}
105
+ @new_record = true
106
+ self.attributes = attributes
107
+ end
108
+
109
+ # Returns true if the record has been saved yet.
110
+ def new_record?
111
+ !!@new_record
112
+ end
113
+
114
+ # Reload the record from the data store, overwriting any attribute changes.
115
+ def reload
116
+ returning(self){ read }
117
+ end
118
+
119
+ # Save the record to the data store. Returns false if validation fails.
120
+ def save
121
+ new_record? ? create : update
122
+ (errors.count > 0) ? false : self
123
+ end
124
+
125
+ # Save the record to the data store. Raises RecordInvalid if validation fails.
126
+ def save!
127
+ returning(save) do
128
+ errors.count > 0 and raise(RecordInvalid, "Validation failed: #{errors.full_messages.join(', ')}")
129
+ end
130
+ end
131
+
132
+ # Delete a record from the data store, invoking the *_destroy callbacks.
133
+ def destroy
134
+ delete
135
+ end
136
+
137
+ private
138
+
139
+ def generate_id
140
+ Digest::MD5.hexdigest(rand.to_s + Time.now.to_s)
141
+ end
142
+
143
+ def id_to_key(id)
144
+ self.class.send(:id_to_key, id)
145
+ end
146
+
147
+ def key
148
+ id_to_key(id)
149
+ end
150
+
151
+ def create
152
+ @attributes["id"] = generate_id if id.blank?
153
+ update_without_callbacks
154
+ end
155
+
156
+ def create_with_callbacks
157
+ _run_validation_on_create_callbacks do
158
+ _run_validation_callbacks do
159
+ valid? or return
160
+ end
161
+ end
162
+
163
+ _run_create_callbacks do
164
+ _run_save_callbacks do
165
+ create_without_callbacks
166
+ end
167
+ end
168
+ end
169
+
170
+ alias_method_chain :create, :callbacks
171
+
172
+ def read(options = {})
173
+ options = options.reverse_merge :id => nil,
174
+ :attributes => nil,
175
+ :keep_new => false
176
+
177
+ if options[:attributes]
178
+ set_attributes(options[:attributes])
179
+ else
180
+ if options[:id]
181
+ _id, _key = options[:id], id_to_key(options[:id])
182
+ else
183
+ _id, _key = id, key
184
+ end
185
+ attributes = recv_attributes(connection.get(_key)) || raise(RecordNotFound, "Couldn't find #{self.class.name} with ID=#{_id}")
186
+ set_attributes(attributes)
187
+ end
188
+
189
+ @new_record = options[:keep_new]
190
+
191
+ self
192
+ end
193
+
194
+ def read_with_callbacks(*args)
195
+ _run_find_callbacks do
196
+ read_without_callbacks(*args)
197
+ end
198
+ end
199
+
200
+ alias_method_chain :read, :callbacks
201
+
202
+ def update
203
+ connection.put(key, send_attributes(attributes))
204
+ @new_record = false
205
+
206
+ # ActiveModel::Dirty
207
+ previously_changed_attributes.replace(changes)
208
+ changed_attributes.clear
209
+ end
210
+
211
+ def update_with_callbacks
212
+ _run_validation_on_update_callbacks do
213
+ _run_validation_callbacks do
214
+ valid? or return
215
+ end
216
+ end
217
+
218
+ _run_update_callbacks do
219
+ _run_save_callbacks do
220
+ update_without_callbacks
221
+ end
222
+ end
223
+ end
224
+
225
+ alias_method_chain :update, :callbacks
226
+
227
+ def delete
228
+ connection.delete(key)
229
+ freeze
230
+ end
231
+
232
+ def delete_with_callbacks
233
+ _run_destroy_callbacks do
234
+ delete_without_callbacks
235
+ end
236
+ end
237
+
238
+ alias_method_chain :delete, :callbacks
239
+
240
+ end # end module InstanceMethods
241
+
242
+ end
243
+
244
+ end