ambry 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog.md +5 -0
- data/Gemfile +2 -0
- data/Guide.md +320 -0
- data/MIT-LICENSE +18 -0
- data/README.md +97 -0
- data/Rakefile +39 -0
- data/ambry.gemspec +25 -0
- data/extras/bench.rb +107 -0
- data/extras/cookie_demo.rb +111 -0
- data/extras/countries.rb +70 -0
- data/lib/ambry.rb +54 -0
- data/lib/ambry/abstract_key_set.rb +106 -0
- data/lib/ambry/active_model.rb +122 -0
- data/lib/ambry/adapter.rb +53 -0
- data/lib/ambry/adapters/cookie.rb +55 -0
- data/lib/ambry/adapters/file.rb +38 -0
- data/lib/ambry/adapters/yaml.rb +17 -0
- data/lib/ambry/hash_proxy.rb +55 -0
- data/lib/ambry/mapper.rb +66 -0
- data/lib/ambry/model.rb +164 -0
- data/lib/ambry/version.rb +9 -0
- data/lib/generators/norman_generator.rb +22 -0
- data/lib/rack/norman.rb +21 -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 +147 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
require "active_model"
|
2
|
+
|
3
|
+
module Ambry
|
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 Ambry::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 Ambry::AmbryError, 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 Ambry
|
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 {#Ambry.default_adapter_name}.
|
14
|
+
def initialize(options = {})
|
15
|
+
@name = options[:name] || Ambry.default_adapter_name
|
16
|
+
load_database
|
17
|
+
Ambry.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 Ambry
|
6
|
+
module Adapters
|
7
|
+
|
8
|
+
# Ambry's cookie adapter allows you to store a Ambry 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(AmbryError, "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 Ambry
|
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 Ambry
|
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 Ambry
|
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
|
data/lib/ambry/mapper.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Ambry
|
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 || Ambry.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
|
+
Ambry.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/ambry/model.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
module Ambry
|
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(Ambry::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 Ambry, 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 || Ambry.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
|
+
# Ambry 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
|
+
# Ambry 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
|