norman 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.
- data/Changelog.md +5 -0
- data/Gemfile +2 -0
- data/Guide.md +320 -0
- data/MIT-LICENSE +18 -0
- data/README.md +104 -0
- data/Rakefile +39 -0
- data/extras/bench.rb +107 -0
- data/extras/cookie_demo.rb +111 -0
- data/extras/countries.rb +70 -0
- data/lib/generators/norman_generator.rb +22 -0
- data/lib/norman.rb +54 -0
- data/lib/norman/abstract_key_set.rb +106 -0
- data/lib/norman/active_model.rb +122 -0
- data/lib/norman/adapter.rb +53 -0
- data/lib/norman/adapters/cookie.rb +55 -0
- data/lib/norman/adapters/file.rb +38 -0
- data/lib/norman/adapters/yaml.rb +17 -0
- data/lib/norman/hash_proxy.rb +55 -0
- data/lib/norman/mapper.rb +66 -0
- data/lib/norman/model.rb +164 -0
- data/lib/norman/version.rb +9 -0
- data/lib/rack/norman.rb +21 -0
- data/norman.gemspec +25 -0
- data/spec/active_model_spec.rb +115 -0
- data/spec/adapter_spec.rb +48 -0
- data/spec/cookie_adapter_spec.rb +81 -0
- data/spec/file_adapter_spec.rb +48 -0
- data/spec/fixtures.yml +18 -0
- data/spec/key_set_spec.rb +104 -0
- data/spec/mapper_spec.rb +97 -0
- data/spec/model_spec.rb +162 -0
- data/spec/spec_helper.rb +38 -0
- metadata +136 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
require "active_model"
|
2
|
+
|
3
|
+
module Norman
|
4
|
+
# Extend this module if you want {Active Model}[http://github.com/rails/rails/tree/master/activemodel]
|
5
|
+
# support. Active Model is an API provided by Rails to make any Ruby object
|
6
|
+
# behave like an Active Record model instance. You can read an older writeup
|
7
|
+
# about it {here}[http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/].
|
8
|
+
module ActiveModel
|
9
|
+
def self.extended(base)
|
10
|
+
base.instance_eval do
|
11
|
+
extend ClassMethods
|
12
|
+
include InstanceMethods
|
13
|
+
extend ::ActiveModel::Naming
|
14
|
+
extend ::ActiveModel::Translation
|
15
|
+
include ::ActiveModel::Validations
|
16
|
+
include ::ActiveModel::Serializers::JSON
|
17
|
+
include ::ActiveModel::Serializers::Xml
|
18
|
+
extend ::ActiveModel::Callbacks
|
19
|
+
define_model_callbacks :save, :destroy
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Custom validations.
|
24
|
+
module Validations
|
25
|
+
# A uniqueness validator, similar to the one provided by Active Record.
|
26
|
+
class Uniqueness< ::ActiveModel::EachValidator
|
27
|
+
def validate_each(record, attribute, value)
|
28
|
+
return if record.persisted?
|
29
|
+
if attribute.to_sym == record.class.id_method
|
30
|
+
begin
|
31
|
+
if record.class.mapper[value]
|
32
|
+
record.errors[attribute] << "must be unique"
|
33
|
+
end
|
34
|
+
rescue Norman::NotFoundError
|
35
|
+
end
|
36
|
+
else
|
37
|
+
if record.class.all.detect {|x| x.send(attribute) == value}
|
38
|
+
record.errors[attribute] << "must be unique"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ClassMethods
|
46
|
+
# Create and save a model instance, raising an exception if any errors
|
47
|
+
# occur.
|
48
|
+
def create!(*args)
|
49
|
+
new(*args).save!
|
50
|
+
end
|
51
|
+
|
52
|
+
# Validate the uniqueness of a field's value in a model instance.
|
53
|
+
def validates_uniqueness_of(*attr_names)
|
54
|
+
validates_with Validations::Uniqueness, _merge_attributes(attr_names)
|
55
|
+
end
|
56
|
+
|
57
|
+
def model_name
|
58
|
+
@model_name ||= ::ActiveModel::Name.new(self)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module InstanceMethods
|
63
|
+
def initialize(*args)
|
64
|
+
@new_record = true
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
def attributes
|
69
|
+
hash = to_hash
|
70
|
+
hash.keys.each {|k| hash[k.to_s] = hash.delete(k)}
|
71
|
+
hash
|
72
|
+
end
|
73
|
+
|
74
|
+
def keys
|
75
|
+
self.class.attribute_names
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_model
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def new_record?
|
83
|
+
@new_record
|
84
|
+
end
|
85
|
+
|
86
|
+
def persisted?
|
87
|
+
!new_record?
|
88
|
+
end
|
89
|
+
|
90
|
+
def save
|
91
|
+
run_callbacks(:save) do
|
92
|
+
@new_record = false
|
93
|
+
super
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def save!
|
98
|
+
if !valid?
|
99
|
+
raise Norman::NormanError, errors.to_a.join(", ")
|
100
|
+
else
|
101
|
+
save
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def to_param
|
106
|
+
to_id if persisted?
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_key
|
110
|
+
[to_param] if persisted?
|
111
|
+
end
|
112
|
+
|
113
|
+
def destroy
|
114
|
+
run_callbacks(:destroy) { delete }
|
115
|
+
end
|
116
|
+
|
117
|
+
def update_attributes
|
118
|
+
run_callbacks(:save) { update }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Norman
|
2
|
+
|
3
|
+
# Adapters are responsible for persisting the database. This base adapter
|
4
|
+
# offers no persistence, all IO operations are just stubs. Adapters must also
|
5
|
+
# present the full database as a Hash to the mapper, and provide a `key`
|
6
|
+
# method that returns an array with all the keys for the specified model
|
7
|
+
# class.
|
8
|
+
class Adapter
|
9
|
+
|
10
|
+
attr_reader :name
|
11
|
+
attr_reader :db
|
12
|
+
|
13
|
+
# @option options [String] :name The adapter name. Defaults to {#Norman.default_adapter_name}.
|
14
|
+
def initialize(options = {})
|
15
|
+
@name = options[:name] || Norman.default_adapter_name
|
16
|
+
load_database
|
17
|
+
Norman.register_adapter(self)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Get a hash of all the data for the specified model class.
|
21
|
+
# @param klass [#to_s] The model class whose data to return.
|
22
|
+
def db_for(klass)
|
23
|
+
@db[klass.to_s] ||= {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Loads the database. For this adapter, that means simply creating a new
|
27
|
+
# hash.
|
28
|
+
def load_database
|
29
|
+
@db = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# These are all just noops for this adapter, which uses an in-memory hash
|
33
|
+
# and offers no persistence.
|
34
|
+
|
35
|
+
# Inheriting adapters can overload this method to export the data to a
|
36
|
+
# String.
|
37
|
+
def export_data
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Inheriting adapters can overload this method to load the data from some
|
42
|
+
# kind of storage.
|
43
|
+
def import_data
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Inheriting adapters can overload this method to persist the data to some
|
48
|
+
# kind of storage.
|
49
|
+
def save_database
|
50
|
+
true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_support/message_verifier"
|
3
|
+
require "zlib"
|
4
|
+
|
5
|
+
module Norman
|
6
|
+
module Adapters
|
7
|
+
|
8
|
+
# Norman's cookie adapter allows you to store a Norman database inside
|
9
|
+
# a zipped and signed string suitable for setting as a cookie. This can be
|
10
|
+
# useful for modelling things like basic shopping carts or form wizards.
|
11
|
+
# Keep in mind the data is signed, so it can't be tampered with. However,
|
12
|
+
# the data is not *encrypted*, so somebody that wanted to could unzip and
|
13
|
+
# load the cookie data to see what's inside. So don't send this data
|
14
|
+
# client-side if it's at all sensitive.
|
15
|
+
class Cookie < Adapter
|
16
|
+
|
17
|
+
attr :verifier
|
18
|
+
attr_accessor :data
|
19
|
+
|
20
|
+
MAX_DATA_LENGTH = 4096
|
21
|
+
|
22
|
+
def self.max_data_length
|
23
|
+
MAX_DATA_LENGTH
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(options)
|
27
|
+
@data = options[:data]
|
28
|
+
@verifier = ActiveSupport::MessageVerifier.new(options[:secret])
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def export_data
|
33
|
+
cookie = verifier.generate(Zlib::Deflate.deflate(Marshal.dump(db)))
|
34
|
+
length = cookie.bytesize
|
35
|
+
if length > Cookie.max_data_length
|
36
|
+
raise(NormanError, "Data is %s bytes, cannot exceed %s" % [length, Cookie.max_data_length])
|
37
|
+
end
|
38
|
+
cookie
|
39
|
+
end
|
40
|
+
|
41
|
+
def import_data
|
42
|
+
data.blank? ? {} : Marshal.load(Zlib::Inflate.inflate(verifier.verify(data)))
|
43
|
+
end
|
44
|
+
|
45
|
+
def load_database
|
46
|
+
@db = import_data
|
47
|
+
@db.map(&:freeze)
|
48
|
+
end
|
49
|
+
|
50
|
+
def save_database
|
51
|
+
@data = export_data
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Norman
|
2
|
+
module Adapters
|
3
|
+
# Loads and saves hash database from a Marshal.dump file.
|
4
|
+
class File < Adapter
|
5
|
+
|
6
|
+
attr_reader :file_path
|
7
|
+
attr :lock
|
8
|
+
|
9
|
+
def initialize(options)
|
10
|
+
@file_path = options[:file]
|
11
|
+
@lock = Mutex.new
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_database
|
16
|
+
@db = import_data
|
17
|
+
@db.blank? ? @db = {} : @db.map(&:freeze)
|
18
|
+
rescue Errno::ENOENT
|
19
|
+
# @TODO warn via logger when file doesn't exist
|
20
|
+
@db = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def export_data
|
24
|
+
Marshal.dump(db)
|
25
|
+
end
|
26
|
+
|
27
|
+
def import_data
|
28
|
+
Marshal.load(::File.read(file_path))
|
29
|
+
end
|
30
|
+
|
31
|
+
def save_database
|
32
|
+
@lock.synchronize do
|
33
|
+
::File.open(file_path, "w") {|f| f.write(export_data)}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
module Norman
|
4
|
+
module Adapters
|
5
|
+
# An Adapter that uses YAML for its storage.
|
6
|
+
class YAML < File
|
7
|
+
|
8
|
+
def import_data
|
9
|
+
data = ::YAML.load(::File.read(file_path))
|
10
|
+
end
|
11
|
+
|
12
|
+
def export_data
|
13
|
+
db.to_yaml
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Norman
|
2
|
+
# Wrapper around hash instances that allows values to be accessed as symbols,
|
3
|
+
# strings or method invocations. It behaves similary to OpenStruct, with the
|
4
|
+
# fundamental difference being that you instantiate *one* HashProxy instance
|
5
|
+
# and reassign its Hash during a loop in order to avoid creating garbage.
|
6
|
+
class HashProxy
|
7
|
+
attr :hash
|
8
|
+
|
9
|
+
# Allows accessing a hash attribute as a method.
|
10
|
+
def method_missing(symbol)
|
11
|
+
hash[symbol] or raise NoMethodError
|
12
|
+
end
|
13
|
+
|
14
|
+
# Allows accessing a hash attribute as hash key, either a string or symbol.
|
15
|
+
def [](value)
|
16
|
+
hash[value || value.to_sym || value.to_s]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Remove the hash.
|
20
|
+
def clear
|
21
|
+
@hash = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
# Assign the value to hash and return self.
|
25
|
+
def using(hash)
|
26
|
+
@hash = hash ; self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Set the hash to use while calling the block. When the block ends, the
|
30
|
+
# hash is unset.
|
31
|
+
def with(hash, &block)
|
32
|
+
yield using hash ensure clear
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Like HashProxy, but proxies access to two or more Hash instances.
|
37
|
+
class HashProxySet
|
38
|
+
|
39
|
+
attr :proxies
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
@proxies = []
|
43
|
+
end
|
44
|
+
|
45
|
+
def using(*args)
|
46
|
+
args.size.times { proxies.push HashProxy.new } if proxies.empty?
|
47
|
+
proxies.each_with_index {|proxy, index| proxy.using args[index] }
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear
|
51
|
+
proxies.map(&:clear)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Norman
|
2
|
+
|
3
|
+
# Mappers provide the middle ground between models and adapters. Mappers are
|
4
|
+
# responsible for performing finds and moving objects in and out of the
|
5
|
+
# hash.
|
6
|
+
class Mapper
|
7
|
+
extend Forwardable
|
8
|
+
attr :hash
|
9
|
+
attr_accessor :adapter_name, :klass, :indexes, :options
|
10
|
+
def_delegators :hash, :clear, :delete
|
11
|
+
def_delegators :key_set, :all, :count, :find, :find_by_key, :first, :keys
|
12
|
+
|
13
|
+
def initialize(klass, adapter_name = nil, options = {})
|
14
|
+
@klass = klass
|
15
|
+
@adapter_name = adapter_name || Norman.default_adapter_name
|
16
|
+
@indexes = {}
|
17
|
+
@lock = Mutex.new
|
18
|
+
@options = options
|
19
|
+
@hash = adapter.db_for(klass)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns a hash or model attributes corresponding to the provided key.
|
23
|
+
def [](key)
|
24
|
+
hash[key] or raise NotFoundError.new(klass, key)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets a hash by key.
|
28
|
+
def []=(key, value)
|
29
|
+
@lock.synchronize do
|
30
|
+
@indexes = {}
|
31
|
+
if value.id_changed?
|
32
|
+
hash.delete value.to_id(true)
|
33
|
+
end
|
34
|
+
saved = hash[key] = value.to_hash.freeze
|
35
|
+
adapter.save_database if @options[:sync]
|
36
|
+
saved
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Memoize the output of a find in a threadsafe manner.
|
41
|
+
def add_index(name, indexable)
|
42
|
+
@lock.synchronize do
|
43
|
+
@indexes[name] = indexable
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get the adapter.
|
48
|
+
def adapter
|
49
|
+
Norman.adapters[adapter_name]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get an instance by key
|
53
|
+
def get(key)
|
54
|
+
klass.send :from_hash, self[key]
|
55
|
+
end
|
56
|
+
|
57
|
+
def key_set
|
58
|
+
klass.key_class.new(hash.keys.freeze, self)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Sets an instance, invoking its to_id method
|
62
|
+
def put(instance)
|
63
|
+
self[instance.to_id] = instance
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/norman/model.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
module Norman
|
2
|
+
|
3
|
+
module Model
|
4
|
+
def self.extended(base)
|
5
|
+
base.instance_eval do
|
6
|
+
@lock = Mutex.new
|
7
|
+
@attribute_names = []
|
8
|
+
@key_class = Class.new(Norman::AbstractKeySet)
|
9
|
+
extend ClassMethods
|
10
|
+
include InstanceMethods
|
11
|
+
include Comparable
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
extend Forwardable
|
17
|
+
attr_accessor :attribute_names, :id_method, :mapper
|
18
|
+
attr_reader :key_class
|
19
|
+
def_delegators(*[:find, Enumerable.public_instance_methods(false)].flatten)
|
20
|
+
def_delegators(:mapper, :[], :all, :delete, :first, :get, :count, :find, :find_by_key, :keys)
|
21
|
+
alias id_field id_method=
|
22
|
+
|
23
|
+
def field(*names)
|
24
|
+
names.each do |name|
|
25
|
+
# First attribute added is the default id
|
26
|
+
id_field name if attribute_names.empty?
|
27
|
+
attribute_names << name.to_sym
|
28
|
+
class_eval(<<-EOM, __FILE__, __LINE__ + 1)
|
29
|
+
def #{name}
|
30
|
+
@#{name} or (@attributes[:#{name}] if @attributes)
|
31
|
+
end
|
32
|
+
|
33
|
+
def #{name}=(value)
|
34
|
+
@#{name} = value
|
35
|
+
end
|
36
|
+
EOM
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def use(adapter_name, options = {})
|
41
|
+
@mapper = nil
|
42
|
+
@adapter_name = adapter_name
|
43
|
+
@mapper_options = options
|
44
|
+
end
|
45
|
+
|
46
|
+
# Memoize the output of the method call invoked in the block.
|
47
|
+
# @param [#to_s] name If not given, the name of the method calling with_index will be used.
|
48
|
+
def with_index(name = nil, &block)
|
49
|
+
name ||= caller(1)[0].match(/in `(.*)'\z/)[1]
|
50
|
+
mapper.indexes[name.to_s] or begin
|
51
|
+
indexable = yield
|
52
|
+
mapper.add_index(name, indexable)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def create(hash)
|
57
|
+
new(hash).save
|
58
|
+
end
|
59
|
+
|
60
|
+
# The point of this method is to provide a fast way to get model instances
|
61
|
+
# based on the hash attributes managed by the mapper and adapter.
|
62
|
+
#
|
63
|
+
# The hash arg gets frozen, which can be a nasty side-effect, but helps
|
64
|
+
# avoid hard-to-track-down bugs if the hash is updated somewhere outside
|
65
|
+
# the model. This should only be used internally to Norman, which is why
|
66
|
+
# it's private.
|
67
|
+
def from_hash(hash)
|
68
|
+
instance = allocate
|
69
|
+
instance.instance_variable_set :@attributes, hash.freeze
|
70
|
+
instance
|
71
|
+
end
|
72
|
+
private :from_hash
|
73
|
+
|
74
|
+
def filters(&block)
|
75
|
+
key_class.class_eval(&block)
|
76
|
+
key_class.instance_methods(false).each do |name|
|
77
|
+
instance_eval(<<-EOM, __FILE__, __LINE__ + 1)
|
78
|
+
def #{name}(*args)
|
79
|
+
mapper.key_set.#{name}(*args)
|
80
|
+
end
|
81
|
+
EOM
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def mapper
|
86
|
+
@mapper or @lock.synchronize do
|
87
|
+
name = @adapter_name || Norman.default_adapter_name
|
88
|
+
options = @mapper_options || {}
|
89
|
+
@mapper ||= Mapper.new(self, name, options)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
module InstanceMethods
|
95
|
+
|
96
|
+
# Norman models can be instantiated with a hash of attribures, a block,
|
97
|
+
# or both. If both a hash and block are given, then the values set inside
|
98
|
+
# the block will take precedence over those set in the hash.
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# Person.new :name => "Joe"
|
102
|
+
# Person.new {|p| p.name = "Joe"}
|
103
|
+
# Person.new(params[:person]) {|p| p.age = 38}
|
104
|
+
#
|
105
|
+
def initialize(attributes = nil, &block)
|
106
|
+
@attributes = {}.freeze
|
107
|
+
return unless attributes || block_given?
|
108
|
+
if attributes
|
109
|
+
self.class.attribute_names.each do |name|
|
110
|
+
value = attributes[name] || attributes[name.to_s]
|
111
|
+
send("#{name}=", value) if value
|
112
|
+
end
|
113
|
+
end
|
114
|
+
yield self if block_given?
|
115
|
+
end
|
116
|
+
|
117
|
+
# Norman models implement the <=> method and mix in Comparable to provide
|
118
|
+
# sorting methods. This default implementation compares the result of
|
119
|
+
# #to_id. If the items being compared are not of the same kind.
|
120
|
+
def <=>(instance)
|
121
|
+
to_id <=> instance.to_id if instance.kind_of? self.class
|
122
|
+
end
|
123
|
+
|
124
|
+
# Get a hash of the instance's model attributes.
|
125
|
+
def to_hash
|
126
|
+
self.class.attribute_names.inject({}) do |hash, key|
|
127
|
+
hash[key] = self.send(key); hash
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Returns true is the model's id field has been updated.
|
132
|
+
def id_changed?
|
133
|
+
to_id != @attributes[self.class.id_method]
|
134
|
+
end
|
135
|
+
|
136
|
+
# Invoke the model's id method to return this instance's unique key. If
|
137
|
+
# true is passed, then the id will be read from the attributes hash rather
|
138
|
+
# than from an instance variable. This allows you to retrieve the old id,
|
139
|
+
# in the event that the id has been changed.
|
140
|
+
def to_id(use_old = false)
|
141
|
+
use_old ? @attributes[self.class.id_method] : send(self.class.id_method)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Tell the mapper to save the data for this model instance.
|
145
|
+
def save
|
146
|
+
self.class.mapper.put(self)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Update this instance's attributes and invoke #save.
|
150
|
+
def update(attributes)
|
151
|
+
self.class.attribute_names.each do |name|
|
152
|
+
value = attributes[name] || attributes[name.to_s]
|
153
|
+
send("#{name}=", value) if value
|
154
|
+
end
|
155
|
+
save
|
156
|
+
end
|
157
|
+
|
158
|
+
# Tell the mapper to delete the data for this instance.
|
159
|
+
def delete
|
160
|
+
self.class.delete(self.to_id)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|