curly_mustache 0.1.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.
@@ -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