topmodel 0.2

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6b8b321b0771de0cc8f8d29793630c8197e1170b
4
+ data.tar.gz: 0a5f7fcbea472f2063cc57d8c1977c573a355d5c
5
+ SHA512:
6
+ metadata.gz: 5cfeea8c618607147fbfa39a659e24dd4f2afa33d0d27918cf408806510bcc8f9a335ad6f2083f54b4eee18bbca357ab8800bb137e4da036b226912f0c1e31ed
7
+ data.tar.gz: d3454ce5cfb68900d8299fca12ee0fe78ef20003326198151bfadcb034c56e59b6e14f9ff0095e51420d348fc009e847793af5471a443ba65f72a9987100b05c
data/.gitignore ADDED
File without changes
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Alexander MacCaw (info@eribium.org)
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.md ADDED
@@ -0,0 +1,64 @@
1
+ #TopModel a Rails 4 compatible in-memory database using ActiveModel
2
+ **This gem is based on topmodel by Alex Maccaw (maccman). It includes some fixes and is updated to be compatible with Rails 4.**
3
+
4
+
5
+ **Primarily developed for Bowline applications.
6
+ http://github.com/maccman/bowline**
7
+
8
+ ##Supports:
9
+ * Serialisation
10
+ * Validations
11
+ * Callbacks
12
+ * Dirty (Changes)
13
+ * Ruby Marshalling to disk
14
+ * Redis
15
+
16
+ ##Examples:
17
+
18
+ require "topmodel"
19
+
20
+ class Test < TopModel::Base
21
+ end
22
+
23
+ t = Test.new
24
+ t.name = "foo"
25
+ t.save #=> true
26
+
27
+ Test.all
28
+ Test.first
29
+ Test.last
30
+ Test.find_by_name('foo)
31
+
32
+ You can use a random ID rather than the object ID:
33
+
34
+ class Test < TopModel::Base
35
+ include TopModel::RandomID
36
+ end
37
+
38
+ t = Test.create(:name => "test")
39
+ t.id #=> "7ee935377bb4aecc54ad4f9126"
40
+
41
+ You can marshal objects to disk on startup/shutdown
42
+
43
+ class Test < TopModel::Base
44
+ include TopModel::Marshal::Model
45
+ end
46
+
47
+ TopModel::Marshal.path = "dump.db"
48
+ TopModel::Marshal.load
49
+
50
+ at_exit {
51
+ TopModel::Marshal.dump
52
+ }
53
+
54
+ You can use Redis, you need the Redis gem installed:
55
+
56
+ require "redis"
57
+ class Test < TopModel::Base
58
+ include TopModel::Redis::Model
59
+
60
+ attributes :name
61
+ indexes :name
62
+ end
63
+
64
+ Test.find_or_create_by_name("foo")
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "topmodel"
5
+ gemspec.summary = "In memory DB using ActiveModel"
6
+ gemspec.email = "info@eribium.org"
7
+ gemspec.homepage = "http://github.com/maccman/topmodel"
8
+ gemspec.description = "In memory DB using ActiveModel"
9
+ gemspec.authors = ["Alex MacCaw", "Alex EH"]
10
+ gemspec.add_dependency("activemodel", "~> 3.0.0")
11
+ end
12
+ rescue LoadError
13
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
14
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2
data/lib/top_model.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), "topmodel")
data/lib/topmodel.rb ADDED
@@ -0,0 +1,37 @@
1
+ gem "activesupport"
2
+ gem "activemodel"
3
+
4
+ require "active_support/core_ext/class/attribute_accessors"
5
+ require "active_support/core_ext/hash/indifferent_access"
6
+ require "active_support/core_ext/kernel/reporting"
7
+ require "active_support/core_ext/module/delegation"
8
+ require "active_support/core_ext/module/aliasing"
9
+ require "active_support/core_ext/object/blank"
10
+ require "active_support/core_ext/object/try"
11
+ require "active_support/core_ext/object/to_query"
12
+ require "active_support/core_ext/class/attribute"
13
+ require "active_support/json"
14
+
15
+ require "active_model"
16
+
17
+ module TopModel
18
+ class TopModelError < StandardError; end
19
+ class UnknownRecord < TopModelError; end
20
+ class InvalidRecord < TopModelError; end
21
+ end
22
+
23
+ $:.unshift(File.dirname(__FILE__))
24
+ require "topmodel/ext/array"
25
+
26
+ module TopModel
27
+ autoload :Association, "topmodel/association"
28
+ autoload :Callbacks, "topmodel/callbacks"
29
+ #autoload :Observing, "topmodel/observing"
30
+ autoload :Marshal, "topmodel/marshal"
31
+ autoload :RandomID, "topmodel/random_id"
32
+ autoload :Timestamp, "topmodel/timestamp"
33
+ autoload :Validations, "topmodel/validations"
34
+ autoload :Dirty, "topmodel/dirty"
35
+ autoload :Redis, "topmodel/redis"
36
+ autoload :Base, "topmodel/base"
37
+ end
@@ -0,0 +1,54 @@
1
+ require "active_support/core_ext/string/inflections.rb"
2
+
3
+ module TopModel
4
+ module Association
5
+ module ClassMethods
6
+ def belongs_to(to_model, options = {})
7
+ to_model = to_model.to_s
8
+ class_name = options[:class_name] || to_model.classify
9
+ foreign_key = options[:foreign_key] || "#{to_model}_id"
10
+ primary_key = options[:primary_key] || "id"
11
+
12
+ attributes foreign_key
13
+ self.belongs_assoc[to_model] = {:class_name => class_name, :foreign_key => foreign_key, :primary_key => primary_key, :name => to_model}
14
+
15
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
16
+ def #{to_model} # def user
17
+ #{foreign_key} && #{class_name}.find(#{foreign_key}) # user_id && User.find(user_id)
18
+ end # end
19
+ #
20
+ def #{to_model}? # def user?
21
+ #{foreign_key} && #{class_name}.exists?(#{foreign_key}) # user_id && User.exists?(user_id)
22
+ end # end
23
+ #
24
+ def #{to_model}=(object) # def user=(model)
25
+ self.#{foreign_key} = (object && object.#{primary_key}) # self.user_id = (model && model.id)
26
+ end # end
27
+ EOS
28
+ end
29
+
30
+ def has_many(to_model, options = {})
31
+ to_model = to_model.to_s
32
+ class_name = options[:class_name] || to_model.classify
33
+ foreign_key = options[:foreign_key] || "#{model_name.singular}_id"
34
+ primary_key = options[:primary_key] || "id"
35
+ self.has_many_assoc[to_model] = {:class_name => class_name, :foreign_key => foreign_key, :primary_key => primary_key, :name => to_model}
36
+
37
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
38
+ def #{to_model} # def user
39
+ #{class_name}.find_all_by_attribute( # User.find_all_by_attribute(
40
+ :#{foreign_key}, # :task_id,
41
+ #{primary_key} # id
42
+ ) # )
43
+ end # end
44
+ EOS
45
+ end
46
+ end
47
+
48
+ module Model
49
+ def self.included(base)
50
+ base.extend(ClassMethods)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,336 @@
1
+ module TopModel
2
+ class Base
3
+ class_attribute :known_attributes
4
+ self.known_attributes = []
5
+
6
+ class << self
7
+ attr_accessor(:primary_key) #:nodoc:
8
+
9
+ def belongs_assoc
10
+ @belongs_assoc ||= {}
11
+ end
12
+
13
+ def has_many_assoc
14
+ @has_many_assoc ||= {}
15
+ end
16
+
17
+ def primary_key
18
+ @primary_key ||= 'id'
19
+ end
20
+
21
+ def collection(&block)
22
+ @collection ||= Class.new(Array)
23
+ @collection.class_eval(&block) if block_given?
24
+ @collection
25
+ end
26
+
27
+ def attributes(*attributes)
28
+ self.known_attributes |= attributes.map(&:to_s)
29
+ end
30
+
31
+ def records
32
+ @records ||= {}
33
+ end
34
+
35
+ def find_by_attribute(name, value) #:nodoc:
36
+ item = records.values.find {|r| r.send(name) == value }
37
+ item && item.dup
38
+ end
39
+
40
+ def find_all_by_attribute(name, value) #:nodoc:
41
+ items = records.values.select {|r| r.send(name) == value }
42
+ collection.new(items.deep_dup)
43
+ end
44
+
45
+ def raw_find(id) #:nodoc:
46
+ records[id] || raise(UnknownRecord, "Couldn't find #{self.name} with ID=#{id}")
47
+ end
48
+
49
+ # Find record by ID, or raise.
50
+ def find(id)
51
+ item = raw_find(id)
52
+ item && item.dup
53
+ end
54
+ alias :[] :find
55
+
56
+ def first
57
+ item = records.values[0]
58
+ item && item.dup
59
+ end
60
+
61
+ def last
62
+ item = records.values[-1]
63
+ item && item.dup
64
+ end
65
+
66
+ def where(options)
67
+ items = records.values.select do |r|
68
+ options.all? do |k, v|
69
+ if v.is_a?(Enumerable)
70
+ v.include?(r.send(k))
71
+ else
72
+ r.send(k) == v
73
+ end
74
+ end
75
+ end
76
+ collection.new(items.deep_dup)
77
+ end
78
+
79
+ def exists?(id)
80
+ records.has_key?(id)
81
+ end
82
+
83
+ def count
84
+ records.length
85
+ end
86
+
87
+ def all
88
+ collection.new(records.values.deep_dup)
89
+ end
90
+
91
+ def select(&block)
92
+ collection.new(records.values.select(&block).deep_dup)
93
+ end
94
+
95
+ def update(id, atts)
96
+ find(id).update_attributes(atts)
97
+ end
98
+
99
+ def destroy(id)
100
+ find(id).destroy
101
+ end
102
+
103
+ # Removes all records and executes
104
+ # destroy callbacks.
105
+ def destroy_all
106
+ all.each {|r| r.destroy }
107
+ end
108
+
109
+ # Removes all records without executing
110
+ # destroy callbacks.
111
+ def delete_all
112
+ records.clear
113
+ end
114
+
115
+ # Create a new record.
116
+ # Example:
117
+ # create(:name => "foo", :id => 1)
118
+ def create(atts = {})
119
+ rec = self.new(atts)
120
+ rec.save && rec
121
+ end
122
+
123
+ def create!(*args)
124
+ create(*args) || raise(InvalidRecord)
125
+ end
126
+
127
+ def method_missing(method_symbol, *args) #:nodoc:
128
+ method_name = method_symbol.to_s
129
+
130
+ if method_name =~ /^find_by_(\w+)!/
131
+ send("find_by_#{$1}", *args) || raise(UnknownRecord)
132
+ elsif method_name =~ /^find_by_(\w+)/
133
+ find_by_attribute($1, args.first)
134
+ elsif method_name =~ /^find_or_create_by_(\w+)/
135
+ send("find_by_#{$1}", *args) || create($1 => args.first)
136
+ elsif method_name =~ /^find_all_by_(\w+)/
137
+ find_all_by_attribute($1, args.first)
138
+ else
139
+ super
140
+ end
141
+ end
142
+ end
143
+
144
+ attr_accessor :attributes
145
+ attr_writer :new_record
146
+
147
+ def known_attributes
148
+ self.class.known_attributes | self.attributes.keys.map(&:to_s)
149
+ end
150
+
151
+ def initialize(attributes = {})
152
+ @new_record = true
153
+ @attributes = {}.with_indifferent_access
154
+ @attributes.merge!(known_attributes.inject({}) {|h, n| h[n] = nil; h })
155
+ @changed_attributes = {}
156
+ load(attributes)
157
+ end
158
+
159
+ def clone
160
+ cloned = attributes.reject {|k,v| k == self.class.primary_key }
161
+ cloned = cloned.inject({}) do |attrs, (k, v)|
162
+ attrs[k] = v.clone
163
+ attrs
164
+ end
165
+ self.class.new(cloned)
166
+ end
167
+
168
+ def new?
169
+ @new_record || false
170
+ end
171
+ alias :new_record? :new?
172
+
173
+ # Gets the <tt>\id</tt> attribute of the item.
174
+ def id
175
+ attributes[self.class.primary_key]
176
+ end
177
+
178
+ # Sets the <tt>\id</tt> attribute of the item.
179
+ def id=(id)
180
+ attributes[self.class.primary_key] = id
181
+ end
182
+
183
+ def ==(other)
184
+ other.equal?(self) || (other.instance_of?(self.class) && other.id == id)
185
+ end
186
+
187
+ # Tests for equality (delegates to ==).
188
+ def eql?(other)
189
+ self == other
190
+ end
191
+
192
+ def hash
193
+ id.hash
194
+ end
195
+
196
+ def dup
197
+ self.class.new.tap do |base|
198
+ base.attributes = attributes
199
+ base.new_record = new_record?
200
+ end
201
+ end
202
+
203
+ def save
204
+ new? ? create : update
205
+ end
206
+
207
+ def save!
208
+ save || raise(InvalidRecord)
209
+ end
210
+
211
+ def exists?
212
+ !new?
213
+ end
214
+ alias_method :persisted?, :exists?
215
+
216
+ def load(attributes) #:nodoc:
217
+ return unless attributes
218
+ attributes.each do |(name, value)|
219
+ self.send("#{name}=".to_sym, value)
220
+ end
221
+ end
222
+
223
+ def reload
224
+ return self if new?
225
+ item = self.class.find(id)
226
+ load(item.attributes)
227
+ return self
228
+ end
229
+
230
+ def update_attribute(name, value)
231
+ self.send("#{name}=".to_sym, value)
232
+ self.save
233
+ end
234
+
235
+ def update_attributes(attributes)
236
+ load(attributes) && save
237
+ end
238
+
239
+ def update_attributes!(attributes)
240
+ update_attributes(attributes) || raise(InvalidRecord)
241
+ end
242
+
243
+ def has_attribute?(name)
244
+ @attributes.has_key?(name)
245
+ end
246
+
247
+ alias_method :respond_to_without_attributes?, :respond_to?
248
+
249
+ def respond_to?(method, include_priv = false)
250
+ method_name = method.to_s
251
+ if attributes.nil?
252
+ super
253
+ elsif known_attributes.include?(method_name)
254
+ true
255
+ elsif method_name =~ /(?:=|\?)$/ && attributes.include?($`)
256
+ true
257
+ else
258
+ super
259
+ end
260
+ end
261
+
262
+ def destroy
263
+ raw_destroy
264
+ self
265
+ end
266
+
267
+ protected
268
+ def read_attribute(name)
269
+ @attributes[name]
270
+ end
271
+
272
+ def write_attribute(name, value)
273
+ @attributes[name] = value
274
+ end
275
+
276
+ def generate_id
277
+ object_id
278
+ end
279
+
280
+ def raw_destroy
281
+ self.class.records.delete(self.id)
282
+ end
283
+
284
+ def raw_create
285
+ self.class.records[self.id] = self.dup
286
+ end
287
+
288
+ def create
289
+ self.id ||= generate_id
290
+ self.new_record = false
291
+ raw_create
292
+ self.id
293
+ end
294
+
295
+ def raw_update
296
+ item = self.class.raw_find(id)
297
+ item.load(attributes)
298
+ end
299
+
300
+ def update
301
+ raw_update
302
+ true
303
+ end
304
+
305
+ private
306
+
307
+ def method_missing(method_symbol, *arguments) #:nodoc:
308
+ method_name = method_symbol.to_s
309
+
310
+ if method_name =~ /(=|\?)$/
311
+ case $1
312
+ when "="
313
+ attribute_will_change!($`)
314
+ attributes[$`] = arguments.first
315
+ when "?"
316
+ attributes[$`]
317
+ end
318
+ else
319
+ return attributes[method_name] if attributes.include?(method_name)
320
+ return nil if known_attributes.include?(method_name)
321
+ super
322
+ end
323
+ end
324
+ end
325
+
326
+ class Base
327
+ extend ActiveModel::Naming
328
+ include ActiveModel::Conversion
329
+ include ActiveModel::Serializers::JSON
330
+ include ActiveModel::Serializers::Xml
331
+ #include Dirty, Observing, Callbacks, Validations
332
+ include Dirty, Callbacks, Validations
333
+
334
+ include Association::Model
335
+ end
336
+ end
@@ -0,0 +1,48 @@
1
+ module TopModel
2
+ module Callbacks
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ instance_eval do
7
+ extend ActiveModel::Callbacks
8
+ define_model_callbacks :create, :save, :update, :destroy
9
+ end
10
+
11
+ # Callback Fixes https://raw.githubusercontent.com/locomotivapro/topmodel/33d952c5082245465f9f063a55a743b88df7558d/lib/topmodel/callbacks.rb
12
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
13
+ def create_with_callbacks(*args, &block)
14
+ run_callbacks :create do
15
+ # Your create action methods here
16
+ create_without_callbacks(*args, &block)
17
+ end
18
+ end
19
+ alias_method_chain(:create, :callbacks)
20
+
21
+ def save_with_callbacks(*args, &block)
22
+ run_callbacks :save do
23
+ # Your save action methods here
24
+ save_without_callbacks(*args, &block)
25
+ end
26
+ end
27
+ alias_method_chain(:save, :callbacks)
28
+
29
+ def update_with_callbacks(*args, &block)
30
+ run_callbacks :update do
31
+ # Your update action methods here
32
+ update_without_callbacks(*args, &block)
33
+ end
34
+ end
35
+ alias_method_chain(:update, :callbacks)
36
+
37
+ def destroy_with_callbacks(*args, &block)
38
+ run_callbacks :destroy do
39
+ # Your destroy action methods here
40
+ destroy_without_callbacks(*args, &block)
41
+ end
42
+ end
43
+ alias_method_chain(:destroy, :callbacks)
44
+ EOS
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ module TopModel
2
+ module Dirty
3
+ extend ActiveSupport::Concern
4
+ include ActiveModel::Dirty
5
+
6
+ included do
7
+ %w( create update ).each do |method|
8
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
9
+ def #{method}_with_dirty(*args, &block)
10
+ result = #{method}_without_dirty(*args, &block)
11
+ save_previous_changes
12
+ result
13
+ end
14
+ EOS
15
+ alias_method_chain(method, :dirty)
16
+ end
17
+ end
18
+
19
+ def save_previous_changes
20
+ @previously_changed = changes
21
+ @changed_attributes.clear
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ class Array
2
+ unless defined?(deep_dup)
3
+ def deep_dup
4
+ Marshal.load(Marshal.dump(self))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,91 @@
1
+ require "tempfile"
2
+ require "fileutils"
3
+
4
+ module TopModel
5
+ module Marshal
6
+ def path
7
+ @path || raise("Provide a path")
8
+ end
9
+
10
+ def path=(p)
11
+ @path = p
12
+ end
13
+
14
+ def klasses
15
+ @klasses ||= []
16
+ end
17
+
18
+ def load
19
+ return unless path
20
+ return unless File.exist?(path)
21
+ data = []
22
+ File.open(path, "rb") do |file|
23
+ begin
24
+ data = ::Marshal.load(file)
25
+ rescue => e
26
+ if defined?(Bowline)
27
+ Bowline::Logging.log_error(e)
28
+ end
29
+ # Lots of errors can occur during
30
+ # marshaling - such as EOF etc
31
+ return false
32
+ end
33
+ end
34
+ data.each do |klass, records|
35
+ klass.marshal_records = records
36
+ end
37
+ true
38
+ end
39
+
40
+ def dump
41
+ return unless path
42
+ tmp_file = Tempfile.new("rbdump")
43
+ tmp_file.binmode
44
+ data = klasses.inject({}) {|hash, klass|
45
+ hash[klass] = klass.marshal_records
46
+ hash
47
+ }
48
+ ::Marshal.dump(data, tmp_file)
49
+ tmp_file.close
50
+ # Atomic serialization - so we never corrupt the db
51
+ FileUtils.mv(tmp_file.path, path)
52
+ true
53
+ end
54
+
55
+ extend self
56
+
57
+ module Model
58
+ def self.included(base)
59
+ base.extend ClassMethods
60
+ Marshal.klasses << base
61
+ end
62
+
63
+ def marshal_dump
64
+ serializable_hash(self.class.marshal)
65
+ end
66
+
67
+ def marshal_load(atts)
68
+ # Can't call load, since class
69
+ # isn't setup properly
70
+ @attributes = atts.with_indifferent_access
71
+ @changed_attributes = {}
72
+ end
73
+
74
+ module ClassMethods
75
+ def marshal(options = nil)
76
+ @marshal = options if options
77
+ @marshal ||= {}
78
+ end
79
+ alias_method :marshal=, :marshal
80
+
81
+ def marshal_records=(records)
82
+ @records = records
83
+ end
84
+
85
+ def marshal_records
86
+ @records
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,21 @@
1
+ module TopModel
2
+ module Observing
3
+ # extend ActiveSupport::Concern
4
+ # include ActiveModel::Observing
5
+
6
+ # included do
7
+ # %w( create save update destroy ).each do |method|
8
+ # class_eval(<<-EOS, __FILE__, __LINE__ + 1)
9
+ # def #{method}_with_notifications(*args, &block)
10
+ # notify_observers(:before_#{method})
11
+ # if result = #{method}_without_notifications(*args, &block)
12
+ # notify_observers(:after_#{method})
13
+ # end
14
+ # result
15
+ # end
16
+ # EOS
17
+ # alias_method_chain(method, :notifications)
18
+ # end
19
+ # end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ module TopModel
2
+ module RandomID
3
+ protected
4
+ def generate_id
5
+ #ActiveSupport::SecureRandom.hex(13)
6
+ SecureRandom.hex(13)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,169 @@
1
+ module TopModel
2
+ module Redis
3
+ module ClassMethods
4
+ def self.extended(base)
5
+ base.class_eval do
6
+ #class_inheritable_array :indexed_attributes
7
+ class_attribute :indexed_attributes
8
+ self.indexed_attributes = []
9
+
10
+ #class_inheritable_hash :redis_options
11
+ class_attribute :redis_options
12
+ self.redis_options = {}
13
+ end
14
+ end
15
+
16
+ def namespace
17
+ @namespace ||= self.name.downcase
18
+ end
19
+
20
+ def namespace=(namespace)
21
+ @namespace = namespace
22
+ end
23
+
24
+ def redis
25
+ @redis ||= ::Redis.connect(redis_options)
26
+ end
27
+
28
+ def indexes(*indexes)
29
+ self.indexed_attributes += indexes.map(&:to_s)
30
+ end
31
+
32
+ def redis_key(*args)
33
+ args.unshift(self.namespace)
34
+ args.join(":")
35
+ end
36
+
37
+ def find(id)
38
+ if redis.sismember(redis_key, id.to_s)
39
+ existing(:id => id)
40
+ else
41
+ raise UnknownRecord, "Couldn't find #{self.name} with ID=#{id}"
42
+ end
43
+ end
44
+
45
+ def first
46
+ item_ids = redis.sort(redis_key, :order => "ASC", :limit => [0, 1])
47
+ item_id = item_ids.first
48
+ item_id && existing(:id => item_id)
49
+ end
50
+
51
+ def last
52
+ item_ids = redis.sort(redis_key, :order => "DESC", :limit => [0, 1])
53
+ item_id = item_ids.first
54
+ item_id && existing(:id => item_id)
55
+ end
56
+
57
+ def exists?(id)
58
+ redis.sismember(redis_key, id.to_s)
59
+ end
60
+
61
+ def count
62
+ redis.scard(redis_key)
63
+ end
64
+
65
+ def all
66
+ from_ids(redis.sort(redis_key))
67
+ end
68
+
69
+ def select
70
+ raise "Not implemented"
71
+ end
72
+
73
+ def delete_all
74
+ raise "Not implemented"
75
+ end
76
+
77
+ def find_by_attribute(key, value)
78
+ item_ids = redis.sort(redis_key(key, value.to_s))
79
+ item_id = item_ids.first
80
+ item_id && existing(:id => item_id)
81
+ end
82
+
83
+ def find_all_by_attribute(key, value)
84
+ from_ids(redis.sort(redis_key(key, value.to_s)))
85
+ end
86
+
87
+ protected
88
+ def from_ids(ids)
89
+ ids.map {|id| existing(:id => id) }
90
+ end
91
+
92
+ def existing(atts = {})
93
+ item = self.new(atts)
94
+ item.new_record = false
95
+ item.redis_get
96
+ item
97
+ end
98
+ end
99
+
100
+ module InstanceMethods
101
+ protected
102
+ def raw_destroy
103
+ return if new?
104
+
105
+ destroy_indexes
106
+ redis.srem(self.class.redis_key, self.id)
107
+ redis.del(redis_key)
108
+ end
109
+
110
+ def destroy_indexes
111
+ indexed_attributes.each do |index|
112
+ old_attribute = changes[index].try(:first) || send(index)
113
+ redis.srem(self.class.redis_key(index, old_attribute), id)
114
+ end
115
+ end
116
+
117
+ def create_indexes
118
+ indexed_attributes.each do |index|
119
+ new_attribute = send(index)
120
+ redis.sadd(self.class.redis_key(index, new_attribute), id)
121
+ end
122
+ end
123
+
124
+ def generate_id
125
+ redis.incr(self.class.redis_key(:uid))
126
+ end
127
+
128
+ def indexed_attributes
129
+ attributes.keys & self.class.indexed_attributes
130
+ end
131
+
132
+ def redis
133
+ self.class.redis
134
+ end
135
+
136
+ def redis_key(*args)
137
+ self.class.redis_key(id, *args)
138
+ end
139
+
140
+ def redis_set
141
+ redis.set(redis_key, serializable_hash.to_json)
142
+ end
143
+
144
+ def redis_get
145
+ load(ActiveSupport::JSON.decode(redis.get(redis_key)))
146
+ end
147
+ public :redis_get
148
+
149
+ def raw_create
150
+ redis_set
151
+ create_indexes
152
+ redis.sadd(self.class.redis_key, self.id)
153
+ end
154
+
155
+ def raw_update
156
+ destroy_indexes
157
+ redis_set
158
+ create_indexes
159
+ end
160
+ end
161
+
162
+ module Model
163
+ def self.included(base)
164
+ base.send :include, InstanceMethods
165
+ base.send :extend, ClassMethods
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,53 @@
1
+ module TopModel
2
+ module Timestamp
3
+ module Model
4
+ def self.included(base)
5
+ base.class_eval do
6
+ attributes :created_at, :updated_at
7
+
8
+ before_create :set_created_at
9
+ before_save :set_updated_at
10
+ end
11
+ end
12
+
13
+ def touch
14
+ set_updated_at
15
+ save!
16
+ end
17
+
18
+ def created_at=(time)
19
+ write_attribute(:created_at, parse_time(time))
20
+ end
21
+
22
+ def updated_at=(time)
23
+ write_attribute(:updated_at, parse_time(time))
24
+ end
25
+
26
+ private
27
+ def parse_time(time)
28
+ return time unless time.is_a?(String)
29
+ if Time.respond_to?(:zone) && Time.zone
30
+ Time.zone.parse(time)
31
+ else
32
+ Time.parse(time)
33
+ end
34
+ end
35
+
36
+ def current_time
37
+ if Time.respond_to?(:current)
38
+ Time.current
39
+ else
40
+ Time.now
41
+ end
42
+ end
43
+
44
+ def set_created_at
45
+ self.created_at = current_time
46
+ end
47
+
48
+ def set_updated_at
49
+ self.updated_at = current_time
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,32 @@
1
+ module TopModel
2
+ module Validations
3
+ extend ActiveSupport::Concern
4
+ include ActiveModel::Validations
5
+
6
+ included do
7
+ alias_method_chain :save, :validation
8
+ end
9
+
10
+ def save_with_validation(options = nil)
11
+ perform_validation = case options
12
+ when Hash
13
+ options[:validate] != false
14
+ when NilClass
15
+ true
16
+ else
17
+ options
18
+ end
19
+
20
+ if perform_validation && valid? || !perform_validation
21
+ save_without_validation
22
+ true
23
+ else
24
+ false
25
+ end
26
+ rescue InvalidRecord => error
27
+ false
28
+ end
29
+ end
30
+ end
31
+
32
+ require "topmodel/validations/uniqueness"
@@ -0,0 +1,40 @@
1
+ module TopModel
2
+ module Validations
3
+ class UniquenessValidator < ActiveModel::EachValidator
4
+ attr_reader :klass
5
+
6
+ def validate_each(record, attribute, value)
7
+ alternate = klass.find_by_attribute(attribute, value)
8
+ return unless alternate
9
+ record.errors.add(attribute, "must be unique", :default => options[:message])
10
+ end
11
+
12
+ def setup(klass)
13
+ @klass = klass
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ # Validates that the specified attribute is unique.
20
+ # class Person < ActiveRecord::Base
21
+ # validates_uniquness_of :essay
22
+ # end
23
+ #
24
+ # Configuration options:
25
+ # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
26
+ # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
27
+ # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>, <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
28
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
29
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
30
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
31
+ # method, proc or string should return or evaluate to a true or false value.
32
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
33
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
34
+ # method, proc or string should return or evaluate to a true or false value.
35
+ def validates_uniqueness_of(*attr_names)
36
+ validates_with UniquenessValidator, _merge_attributes(attr_names)
37
+ end
38
+ end
39
+ end
40
+ end
data/topmodel.gemspec ADDED
@@ -0,0 +1,59 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{topmodel}
8
+ s.version = "0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Alex MacCaw", "Alex Ebeling-Hoppe"]
12
+ s.date = %q{2014-04-03}
13
+ s.description = %q{In memory DB using ActiveModel}
14
+ s.email = %q{info@eribium.org}
15
+ s.extra_rdoc_files = [
16
+ "README.md"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "MIT-LICENSE",
21
+ "README.md",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "lib/top_model.rb",
25
+ "lib/topmodel.rb",
26
+ "lib/topmodel/association.rb",
27
+ "lib/topmodel/base.rb",
28
+ "lib/topmodel/callbacks.rb",
29
+ "lib/topmodel/dirty.rb",
30
+ "lib/topmodel/ext/array.rb",
31
+ "lib/topmodel/marshal.rb",
32
+ "lib/topmodel/observing.rb",
33
+ "lib/topmodel/random_id.rb",
34
+ "lib/topmodel/redis.rb",
35
+ "lib/topmodel/timestamp.rb",
36
+ "lib/topmodel/validations.rb",
37
+ "lib/topmodel/validations/uniqueness.rb",
38
+ "topmodel.gemspec"
39
+ ]
40
+ #s.homepage = %q{http://github.com/maccman/topmodel}
41
+ s.rdoc_options = ["--charset=UTF-8"]
42
+ s.require_paths = ["lib"]
43
+ s.rubygems_version = %q{1.3.7}
44
+ s.summary = %q{In memory DB using ActiveModel}
45
+
46
+ if s.respond_to? :specification_version then
47
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
48
+ s.specification_version = 3
49
+
50
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
51
+ s.add_runtime_dependency(%q<activemodel>, [">= 3.0.0", "<= 4.0.3"])
52
+ else
53
+ s.add_dependency(%q<activemodel>, [">= 3.0.0", "<= 4.0.3"])
54
+ end
55
+ else
56
+ s.add_dependency(%q<activemodel>, [">= 3.0.0", "<= 4.0.3"])
57
+ end
58
+ end
59
+
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: topmodel
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.2'
5
+ platform: ruby
6
+ authors:
7
+ - Alex MacCaw
8
+ - Alex Ebeling-Hoppe
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-04-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activemodel
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: 3.0.0
21
+ - - "<="
22
+ - !ruby/object:Gem::Version
23
+ version: 4.0.3
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: 3.0.0
31
+ - - "<="
32
+ - !ruby/object:Gem::Version
33
+ version: 4.0.3
34
+ description: In memory DB using ActiveModel
35
+ email: info@eribium.org
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files:
39
+ - README.md
40
+ files:
41
+ - ".gitignore"
42
+ - MIT-LICENSE
43
+ - README.md
44
+ - Rakefile
45
+ - VERSION
46
+ - lib/top_model.rb
47
+ - lib/topmodel.rb
48
+ - lib/topmodel/association.rb
49
+ - lib/topmodel/base.rb
50
+ - lib/topmodel/callbacks.rb
51
+ - lib/topmodel/dirty.rb
52
+ - lib/topmodel/ext/array.rb
53
+ - lib/topmodel/marshal.rb
54
+ - lib/topmodel/observing.rb
55
+ - lib/topmodel/random_id.rb
56
+ - lib/topmodel/redis.rb
57
+ - lib/topmodel/timestamp.rb
58
+ - lib/topmodel/validations.rb
59
+ - lib/topmodel/validations/uniqueness.rb
60
+ - topmodel.gemspec
61
+ homepage:
62
+ licenses: []
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options:
66
+ - "--charset=UTF-8"
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 2.2.2
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: In memory DB using ActiveModel
85
+ test_files: []