├── VERSION ├── lib ├── super_model.rb ├── supermodel │ ├── ext │ │ └── array.rb │ ├── random_id.rb │ ├── dirty.rb │ ├── observing.rb │ ├── callbacks.rb │ ├── validations.rb │ ├── timestamp.rb │ ├── validations │ │ └── uniqueness.rb │ ├── marshal.rb │ ├── association.rb │ ├── redis.rb │ └── base.rb └── supermodel.rb ├── .gitignore ├── Rakefile ├── MIT-LICENSE ├── README └── supermodel.gemspec /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.7 2 | -------------------------------------------------------------------------------- /lib/super_model.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), "supermodel") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dump.db 2 | test.rb 3 | redis_test.rb 4 | pkg 5 | lib/supermodel/cassandra.rb 6 | *.gem 7 | tags 8 | -------------------------------------------------------------------------------- /lib/supermodel/ext/array.rb: -------------------------------------------------------------------------------- 1 | class Array 2 | unless defined?(deep_dup) 3 | def deep_dup 4 | Marshal.load(Marshal.dump(self)) 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /lib/supermodel/random_id.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 2 | module RandomID 3 | protected 4 | def generate_id 5 | ActiveSupport::SecureRandom.hex(13) 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'jeweler' 3 | Jeweler::Tasks.new do |gemspec| 4 | gemspec.name = "supermodel" 5 | gemspec.summary = "In memory DB using ActiveModel" 6 | gemspec.email = "info@eribium.org" 7 | gemspec.homepage = "http://github.com/maccman/supermodel" 8 | gemspec.description = "In memory DB using ActiveModel" 9 | gemspec.authors = ["Alex MacCaw"] 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 15 | -------------------------------------------------------------------------------- /lib/supermodel/dirty.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 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 25 | -------------------------------------------------------------------------------- /lib/supermodel/observing.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 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 22 | -------------------------------------------------------------------------------- /lib/supermodel/callbacks.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 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 | %w( create save update destroy ).each do |method| 12 | class_eval(<<-EOS, __FILE__, __LINE__ + 1) 13 | def #{method}_with_callbacks(*args, &block) 14 | run_callbacks :#{method} do 15 | #{method}_without_callbacks(*args, &block) 16 | end 17 | end 18 | EOS 19 | alias_method_chain(method, :callbacks) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/supermodel/validations.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 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 "supermodel/validations/uniqueness" -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /lib/supermodel/timestamp.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 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 -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | Simple in-memory database using ActiveModel. 3 | 4 | Primarily developed for Bowline applications. 5 | http://github.com/maccman/bowline 6 | 7 | Supports: 8 | * Serialisation 9 | * Validations 10 | * Callbacks 11 | * Observers 12 | * Dirty (Changes) 13 | * Ruby Marshalling to disk 14 | * Redis 15 | 16 | Examples: 17 | 18 | require "supermodel" 19 | 20 | class Test < SuperModel::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 < SuperModel::Base 35 | include SuperModel::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 < SuperModel::Base 44 | include SuperModel::Marshal::Model 45 | end 46 | 47 | SuperModel::Marshal.path = "dump.db" 48 | SuperModel::Marshal.load 49 | 50 | at_exit { 51 | SuperModel::Marshal.dump 52 | } 53 | 54 | You can use Redis, you need the Redis gem installed: 55 | 56 | require "redis" 57 | class Test < SuperModel::Base 58 | include SuperModel::Redis::Model 59 | 60 | attributes :name 61 | indexes :name 62 | end 63 | 64 | Test.find_or_create_by_name("foo") -------------------------------------------------------------------------------- /lib/supermodel.rb: -------------------------------------------------------------------------------- 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 SuperModel 18 | class SuperModelError < StandardError; end 19 | class UnknownRecord < SuperModelError; end 20 | class InvalidRecord < SuperModelError; end 21 | end 22 | 23 | $:.unshift(File.dirname(__FILE__)) 24 | require "supermodel/ext/array" 25 | 26 | module SuperModel 27 | autoload :Association, "supermodel/association" 28 | autoload :Callbacks, "supermodel/callbacks" 29 | autoload :Observing, "supermodel/observing" 30 | autoload :Marshal, "supermodel/marshal" 31 | autoload :RandomID, "supermodel/random_id" 32 | autoload :Timestamp, "supermodel/timestamp" 33 | autoload :Validations, "supermodel/validations" 34 | autoload :Dirty, "supermodel/dirty" 35 | autoload :Redis, "supermodel/redis" 36 | autoload :Base, "supermodel/base" 37 | end -------------------------------------------------------------------------------- /supermodel.gemspec: -------------------------------------------------------------------------------- 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{supermodel} 8 | s.version = "0.1.7" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Alex MacCaw"] 12 | s.date = %q{2010-08-30} 13 | s.description = %q{In memory DB using ActiveModel} 14 | s.email = %q{info@eribium.org} 15 | s.extra_rdoc_files = [ 16 | "README" 17 | ] 18 | s.files = [ 19 | ".gitignore", 20 | "MIT-LICENSE", 21 | "README", 22 | "Rakefile", 23 | "VERSION", 24 | "lib/super_model.rb", 25 | "lib/supermodel.rb", 26 | "lib/supermodel/association.rb", 27 | "lib/supermodel/base.rb", 28 | "lib/supermodel/callbacks.rb", 29 | "lib/supermodel/dirty.rb", 30 | "lib/supermodel/ext/array.rb", 31 | "lib/supermodel/marshal.rb", 32 | "lib/supermodel/observing.rb", 33 | "lib/supermodel/random_id.rb", 34 | "lib/supermodel/redis.rb", 35 | "lib/supermodel/timestamp.rb", 36 | "lib/supermodel/validations.rb", 37 | "lib/supermodel/validations/uniqueness.rb", 38 | "supermodel.gemspec" 39 | ] 40 | s.homepage = %q{http://github.com/maccman/supermodel} 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, [">= 3.0.0"]) 52 | else 53 | s.add_dependency(%q, [">= 3.0.0"]) 54 | end 55 | else 56 | s.add_dependency(%q, [">= 3.0.0"]) 57 | end 58 | end 59 | 60 | -------------------------------------------------------------------------------- /lib/supermodel/validations/uniqueness.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 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 | # * :allow_nil - Attribute may be +nil+; skip validation. 26 | # * :allow_blank - Attribute may be blank; skip validation. 27 | # * :message - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message. 28 | # * :on - Specifies when this validation is active (default is :save, other options :create, :update). 29 | # * :if - Specifies a method, proc or string to call to determine if the validation should 30 | # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The 31 | # method, proc or string should return or evaluate to a true or false value. 32 | # * :unless - Specifies a method, proc or string to call to determine if the validation should 33 | # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). 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 -------------------------------------------------------------------------------- /lib/supermodel/marshal.rb: -------------------------------------------------------------------------------- 1 | require "tempfile" 2 | require "fileutils" 3 | 4 | module SuperModel 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 -------------------------------------------------------------------------------- /lib/supermodel/association.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/string/inflections.rb" 2 | 3 | module SuperModel 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 | 14 | class_eval(<<-EOS, __FILE__, __LINE__ + 1) 15 | def #{to_model} # def user 16 | #{foreign_key} && #{class_name}.find(#{foreign_key}) # user_id && User.find(user_id) 17 | end # end 18 | # 19 | def #{to_model}? # def user? 20 | #{foreign_key} && #{class_name}.exists?(#{foreign_key}) # user_id && User.exists?(user_id) 21 | end # end 22 | # 23 | def #{to_model}=(object) # def user=(model) 24 | self.#{foreign_key} = (object && object.#{primary_key}) # self.user_id = (model && model.id) 25 | end # end 26 | EOS 27 | end 28 | 29 | def has_many(to_model, options = {}) 30 | to_model = to_model.to_s 31 | class_name = options[:class_name] || to_model.classify 32 | foreign_key = options[:foreign_key] || "#{model_name.singular}_id" 33 | primary_key = options[:primary_key] || "id" 34 | class_eval(<<-EOS, __FILE__, __LINE__ + 1) 35 | def #{to_model} # def user 36 | #{class_name}.find_all_by_attribute( # User.find_all_by_attribute( 37 | :#{foreign_key}, # :task_id, 38 | #{primary_key} # id 39 | ) # ) 40 | end # end 41 | EOS 42 | end 43 | end 44 | 45 | module Model 46 | def self.included(base) 47 | base.extend(ClassMethods) 48 | end 49 | end 50 | end 51 | end -------------------------------------------------------------------------------- /lib/supermodel/redis.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 2 | module Redis 3 | module ClassMethods 4 | def self.extended(base) 5 | base.class_eval do 6 | class_inheritable_array :indexed_attributes 7 | self.indexed_attributes = [] 8 | 9 | class_inheritable_hash :redis_options 10 | self.redis_options = {} 11 | end 12 | end 13 | 14 | def namespace 15 | @namespace ||= self.name.downcase 16 | end 17 | 18 | def namespace=(namespace) 19 | @namespace = namespace 20 | end 21 | 22 | def redis 23 | @redis ||= ::Redis.connect(redis_options) 24 | end 25 | 26 | def indexes(*indexes) 27 | self.indexed_attributes += indexes.map(&:to_s) 28 | end 29 | 30 | def redis_key(*args) 31 | args.unshift(self.namespace) 32 | args.join(":") 33 | end 34 | 35 | def find(id) 36 | if redis.sismember(redis_key, id.to_s) 37 | existing(:id => id) 38 | else 39 | raise UnknownRecord, "Couldn't find #{self.name} with ID=#{id}" 40 | end 41 | end 42 | 43 | def first 44 | item_ids = redis.sort(redis_key, :order => "ASC", :limit => [0, 1]) 45 | item_id = item_ids.first 46 | item_id && existing(:id => item_id) 47 | end 48 | 49 | def last 50 | item_ids = redis.sort(redis_key, :order => "DESC", :limit => [0, 1]) 51 | item_id = item_ids.first 52 | item_id && existing(:id => item_id) 53 | end 54 | 55 | def exists?(id) 56 | redis.sismember(redis_key, id.to_s) 57 | end 58 | 59 | def count 60 | redis.scard(redis_key) 61 | end 62 | 63 | def all 64 | from_ids(redis.sort(redis_key)) 65 | end 66 | 67 | def select 68 | raise "Not implemented" 69 | end 70 | 71 | def delete_all 72 | raise "Not implemented" 73 | end 74 | 75 | def find_by_attribute(key, value) 76 | item_ids = redis.sort(redis_key(key, value.to_s)) 77 | item_id = item_ids.first 78 | item_id && existing(:id => item_id) 79 | end 80 | 81 | def find_all_by_attribute(key, value) 82 | from_ids(redis.sort(redis_key(key, value.to_s))) 83 | end 84 | 85 | protected 86 | def from_ids(ids) 87 | ids.map {|id| existing(:id => id) } 88 | end 89 | 90 | def existing(atts = {}) 91 | item = self.new(atts) 92 | item.new_record = false 93 | item.redis_get 94 | item 95 | end 96 | end 97 | 98 | module InstanceMethods 99 | protected 100 | def raw_destroy 101 | return if new? 102 | 103 | destroy_indexes 104 | redis.srem(self.class.redis_key, self.id) 105 | redis.del(redis_key) 106 | end 107 | 108 | def destroy_indexes 109 | indexed_attributes.each do |index| 110 | old_attribute = changes[index].try(:first) || send(index) 111 | redis.srem(self.class.redis_key(index, old_attribute), id) 112 | end 113 | end 114 | 115 | def create_indexes 116 | indexed_attributes.each do |index| 117 | new_attribute = send(index) 118 | redis.sadd(self.class.redis_key(index, new_attribute), id) 119 | end 120 | end 121 | 122 | def generate_id 123 | redis.incr(self.class.redis_key(:uid)) 124 | end 125 | 126 | def indexed_attributes 127 | attributes.keys & self.class.indexed_attributes 128 | end 129 | 130 | def redis 131 | self.class.redis 132 | end 133 | 134 | def redis_key(*args) 135 | self.class.redis_key(id, *args) 136 | end 137 | 138 | def redis_set 139 | redis.set(redis_key, serializable_hash.to_json) 140 | end 141 | 142 | def redis_get 143 | load(ActiveSupport::JSON.decode(redis.get(redis_key))) 144 | end 145 | public :redis_get 146 | 147 | def raw_create 148 | redis_set 149 | create_indexes 150 | redis.sadd(self.class.redis_key, self.id) 151 | end 152 | 153 | def raw_update 154 | destroy_indexes 155 | redis_set 156 | create_indexes 157 | end 158 | end 159 | 160 | module Model 161 | def self.included(base) 162 | base.send :include, InstanceMethods 163 | base.send :extend, ClassMethods 164 | end 165 | end 166 | end 167 | end -------------------------------------------------------------------------------- /lib/supermodel/base.rb: -------------------------------------------------------------------------------- 1 | module SuperModel 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 primary_key 10 | @primary_key ||= 'id' 11 | end 12 | 13 | def collection(&block) 14 | @collection ||= Class.new(Array) 15 | @collection.class_eval(&block) if block_given? 16 | @collection 17 | end 18 | 19 | def attributes(*attributes) 20 | self.known_attributes |= attributes.map(&:to_s) 21 | end 22 | 23 | def records 24 | @records ||= {} 25 | end 26 | 27 | def find_by_attribute(name, value) #:nodoc: 28 | item = records.values.find {|r| r.send(name) == value } 29 | item && item.dup 30 | end 31 | 32 | def find_all_by_attribute(name, value) #:nodoc: 33 | items = records.values.select {|r| r.send(name) == value } 34 | collection.new(items.deep_dup) 35 | end 36 | 37 | def raw_find(id) #:nodoc: 38 | records[id] || raise(UnknownRecord, "Couldn't find #{self.name} with ID=#{id}") 39 | end 40 | 41 | # Find record by ID, or raise. 42 | def find(id) 43 | item = raw_find(id) 44 | item && item.dup 45 | end 46 | alias :[] :find 47 | 48 | def first 49 | item = records.values[0] 50 | item && item.dup 51 | end 52 | 53 | def last 54 | item = records.values[-1] 55 | item && item.dup 56 | end 57 | 58 | def where(options) 59 | items = records.values.select do |r| 60 | options.all? do |k, v| 61 | if v.is_a?(Enumerable) 62 | v.include?(r.send(k)) 63 | else 64 | r.send(k) == v 65 | end 66 | end 67 | end 68 | collection.new(items.deep_dup) 69 | end 70 | 71 | def exists?(id) 72 | records.has_key?(id) 73 | end 74 | 75 | def count 76 | records.length 77 | end 78 | 79 | def all 80 | collection.new(records.values.deep_dup) 81 | end 82 | 83 | def select(&block) 84 | collection.new(records.values.select(&block).deep_dup) 85 | end 86 | 87 | def update(id, atts) 88 | find(id).update_attributes(atts) 89 | end 90 | 91 | def destroy(id) 92 | find(id).destroy 93 | end 94 | 95 | # Removes all records and executes 96 | # destroy callbacks. 97 | def destroy_all 98 | all.each {|r| r.destroy } 99 | end 100 | 101 | # Removes all records without executing 102 | # destroy callbacks. 103 | def delete_all 104 | records.clear 105 | end 106 | 107 | # Create a new record. 108 | # Example: 109 | # create(:name => "foo", :id => 1) 110 | def create(atts = {}) 111 | rec = self.new(atts) 112 | rec.save && rec 113 | end 114 | 115 | def create!(*args) 116 | create(*args) || raise(InvalidRecord) 117 | end 118 | 119 | def method_missing(method_symbol, *args) #:nodoc: 120 | method_name = method_symbol.to_s 121 | 122 | if method_name =~ /^find_by_(\w+)!/ 123 | send("find_by_#{$1}", *args) || raise(UnknownRecord) 124 | elsif method_name =~ /^find_by_(\w+)/ 125 | find_by_attribute($1, args.first) 126 | elsif method_name =~ /^find_or_create_by_(\w+)/ 127 | send("find_by_#{$1}", *args) || create($1 => args.first) 128 | elsif method_name =~ /^find_all_by_(\w+)/ 129 | find_all_by_attribute($1, args.first) 130 | else 131 | super 132 | end 133 | end 134 | end 135 | 136 | attr_accessor :attributes 137 | attr_writer :new_record 138 | 139 | def known_attributes 140 | self.class.known_attributes | self.attributes.keys.map(&:to_s) 141 | end 142 | 143 | def initialize(attributes = {}) 144 | @new_record = true 145 | @attributes = {}.with_indifferent_access 146 | @attributes.merge!(known_attributes.inject({}) {|h, n| h[n] = nil; h }) 147 | @changed_attributes = {} 148 | load(attributes) 149 | end 150 | 151 | def clone 152 | cloned = attributes.reject {|k,v| k == self.class.primary_key } 153 | cloned = cloned.inject({}) do |attrs, (k, v)| 154 | attrs[k] = v.clone 155 | attrs 156 | end 157 | self.class.new(cloned) 158 | end 159 | 160 | def new? 161 | @new_record || false 162 | end 163 | alias :new_record? :new? 164 | 165 | # Gets the \id attribute of the item. 166 | def id 167 | attributes[self.class.primary_key] 168 | end 169 | 170 | # Sets the \id attribute of the item. 171 | def id=(id) 172 | attributes[self.class.primary_key] = id 173 | end 174 | 175 | def ==(other) 176 | other.equal?(self) || (other.instance_of?(self.class) && other.id == id) 177 | end 178 | 179 | # Tests for equality (delegates to ==). 180 | def eql?(other) 181 | self == other 182 | end 183 | 184 | def hash 185 | id.hash 186 | end 187 | 188 | def dup 189 | self.class.new.tap do |base| 190 | base.attributes = attributes 191 | base.new_record = new_record? 192 | end 193 | end 194 | 195 | def save 196 | new? ? create : update 197 | end 198 | 199 | def save! 200 | save || raise(InvalidRecord) 201 | end 202 | 203 | def exists? 204 | !new? 205 | end 206 | alias_method :persisted?, :exists? 207 | 208 | def load(attributes) #:nodoc: 209 | return unless attributes 210 | attributes.each do |(name, value)| 211 | self.send("#{name}=".to_sym, value) 212 | end 213 | end 214 | 215 | def reload 216 | return self if new? 217 | item = self.class.find(id) 218 | load(item.attributes) 219 | return self 220 | end 221 | 222 | def update_attribute(name, value) 223 | self.send("#{name}=".to_sym, value) 224 | self.save 225 | end 226 | 227 | def update_attributes(attributes) 228 | load(attributes) && save 229 | end 230 | 231 | def update_attributes!(attributes) 232 | update_attributes(attributes) || raise(InvalidRecord) 233 | end 234 | 235 | def has_attribute?(name) 236 | @attributes.has_key?(name) 237 | end 238 | 239 | alias_method :respond_to_without_attributes?, :respond_to? 240 | 241 | def respond_to?(method, include_priv = false) 242 | method_name = method.to_s 243 | if attributes.nil? 244 | super 245 | elsif known_attributes.include?(method_name) 246 | true 247 | elsif method_name =~ /(?:=|\?)$/ && attributes.include?($`) 248 | true 249 | else 250 | super 251 | end 252 | end 253 | 254 | def destroy 255 | raw_destroy 256 | self 257 | end 258 | 259 | protected 260 | def read_attribute(name) 261 | @attributes[name] 262 | end 263 | 264 | def write_attribute(name, value) 265 | @attributes[name] = value 266 | end 267 | 268 | def generate_id 269 | object_id 270 | end 271 | 272 | def raw_destroy 273 | self.class.records.delete(self.id) 274 | end 275 | 276 | def raw_create 277 | self.class.records[self.id] = self.dup 278 | end 279 | 280 | def create 281 | self.id ||= generate_id 282 | self.new_record = false 283 | raw_create 284 | self.id 285 | end 286 | 287 | def raw_update 288 | item = self.class.raw_find(id) 289 | item.load(attributes) 290 | end 291 | 292 | def update 293 | raw_update 294 | true 295 | end 296 | 297 | private 298 | 299 | def method_missing(method_symbol, *arguments) #:nodoc: 300 | method_name = method_symbol.to_s 301 | 302 | if method_name =~ /(=|\?)$/ 303 | case $1 304 | when "=" 305 | attribute_will_change!($`) 306 | attributes[$`] = arguments.first 307 | when "?" 308 | attributes[$`] 309 | end 310 | else 311 | return attributes[method_name] if attributes.include?(method_name) 312 | return nil if known_attributes.include?(method_name) 313 | super 314 | end 315 | end 316 | end 317 | 318 | class Base 319 | extend ActiveModel::Naming 320 | include ActiveModel::Conversion 321 | include ActiveModel::Serializers::JSON 322 | include ActiveModel::Serializers::Xml 323 | include Dirty, Observing, Callbacks, Validations 324 | include Association::Model 325 | end 326 | end 327 | --------------------------------------------------------------------------------