eav_hashes 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +136 -0
- data/Rakefile +30 -0
- data/init.rb +1 -0
- data/lib/eav_hashes.rb +8 -0
- data/lib/eav_hashes/activerecord_extension.rb +37 -0
- data/lib/eav_hashes/eav_entry.rb +129 -0
- data/lib/eav_hashes/eav_hash.rb +168 -0
- data/lib/eav_hashes/util.rb +122 -0
- data/lib/eav_hashes/version.rb +5 -0
- data/lib/generators/eav_migration/USAGE +26 -0
- data/lib/generators/eav_migration/eav_migration.rb +36 -0
- data/lib/generators/eav_migration/templates/eav_migration.erb +16 -0
- data/lib/tasks/eav_hashes_tasks.rake +4 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/custom_test_object.rb +7 -0
- data/spec/dummy/app/models/product.rb +4 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +68 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20121206133059_create_products.rb +9 -0
- data/spec/dummy/db/migrate/20121210055854_create_product_tech_specs.rb +16 -0
- data/spec/dummy/db/schema.rb +35 -0
- data/spec/dummy/db/seeds.rb +31 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +46 -0
- data/spec/dummy/log/test.log +3878 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/lib/eav_hashes/eav_hash_spec.rb +138 -0
- data/spec/lib/generators/eav_migration_spec.rb +61 -0
- data/spec/spec_helper.rb +25 -0
- metadata +178 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 Ilya Ostrovskiy
|
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,136 @@
|
|
1
|
+
eav_hashes
|
2
|
+
=========
|
3
|
+
|
4
|
+
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/200proof/eav_hashes) [![Build Status](https://travis-ci.org/200proof/eav_hashes.png?branch=master)](https://travis-ci.org/200proof/eav_hashes)
|
5
|
+
|
6
|
+
`eav_hashes` is a neato gem for implementing the EAV (entity-attribute-value)
|
7
|
+
database design pattern in your Rails models. All you need to do is add one
|
8
|
+
line to your model's code and that's it! Schema generation is automatically
|
9
|
+
handled for you.
|
10
|
+
|
11
|
+
Why would I need it?
|
12
|
+
-
|
13
|
+
Rails' ActiveRecord includes a helper function, `serialize`, to allow you to
|
14
|
+
save complex data types (like hashes) into your database. Unfortunately, it
|
15
|
+
isn't very useful. A lot of overhead is created from serialization and
|
16
|
+
deserialization, and you can't search by the contents of your hash. That's
|
17
|
+
where `eav_hashes` comes in.
|
18
|
+
|
19
|
+
How does it work?
|
20
|
+
-
|
21
|
+
Great question! Lets dive in with a simple code example:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Product < ActiveRecord::Base
|
25
|
+
eav_hash_for :tech_specs
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
Now run this generator to create a migration:
|
30
|
+
|
31
|
+
$ rails generate eav_migration Product tech_specs
|
32
|
+
|
33
|
+
And run the migration:
|
34
|
+
|
35
|
+
$ rake db:migrate
|
36
|
+
|
37
|
+
Now watch the magic the happen:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# Assuming this whole example is on a blank DB, of course
|
41
|
+
a_product = Product.new
|
42
|
+
a_product.tech_specs["Widget Power"] = "1.21 GW"
|
43
|
+
a_product.tech_specs["Battery Life (hours)"] = 12
|
44
|
+
a_product.tech_specs["Warranty (years)"] = 3.5
|
45
|
+
a_product.tech_specs["RoHS Compliant"] = true
|
46
|
+
a_product.save!
|
47
|
+
|
48
|
+
# Setting a value to nil deletes the entry
|
49
|
+
a_product.tech_specs["Warranty (years)"] = nil
|
50
|
+
a_product.save!
|
51
|
+
|
52
|
+
the_same_product = Product.first
|
53
|
+
puts the_same_product.tech_specs["nonexistant key"]
|
54
|
+
|
55
|
+
# magic alert: this actually gets the count of EVERY entry of every
|
56
|
+
# hash for this model, but for this example this works
|
57
|
+
puts "Entry Count: #{ProductTechSpecsEntry.count}"
|
58
|
+
the_same_product.tech_specs.each_pair do |key, value|
|
59
|
+
puts "#{key}: #{value.to_s}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Ruby's default types: Integer, Float, Complex, Rational, Symbol,
|
63
|
+
# TrueClass, and FalseClass are preserved between transactions like
|
64
|
+
# you would expect them to.
|
65
|
+
puts the_same_product.tech_specs["Battery Life (hours)"]+3
|
66
|
+
```
|
67
|
+
|
68
|
+
And the output, as you can expect, will be along the lines of:
|
69
|
+
|
70
|
+
nil
|
71
|
+
Entry Count: 3
|
72
|
+
Widget Power: 1.21 GW
|
73
|
+
Battery Life (hours): 12
|
74
|
+
RoHS Compliant: true
|
75
|
+
15
|
76
|
+
|
77
|
+
|
78
|
+
That looks incredibly simple, right? Good! It's supposed to be! All the magic
|
79
|
+
happens when you call `save!`.
|
80
|
+
|
81
|
+
Now you could start doing other cool stuff, like searching for products based
|
82
|
+
on their tech specs! You've already figured out how to do this, haven't you?
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
flux_capacitor = Product.find_by_tech_specs("Widget Power", "1.21 GW")
|
86
|
+
```
|
87
|
+
|
88
|
+
Nifty, right?
|
89
|
+
|
90
|
+
Can I store arrays/hashes/custom types inside my hashes?
|
91
|
+
--
|
92
|
+
Sure, but they'll be serialized into YAML (so you cant search by them like you
|
93
|
+
would an eav_hash). The `value` column is a TEXT type by default but if you
|
94
|
+
want to optimize your DB size you could change it to a VARCHAR in the migration
|
95
|
+
if you don't plan on storing large values.
|
96
|
+
|
97
|
+
|
98
|
+
What if I want to change the table name?
|
99
|
+
--
|
100
|
+
By default, `eav_hash` uses a table name derived from the following:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
"<ClassName>_<hash_name>".tableize
|
104
|
+
```
|
105
|
+
|
106
|
+
You can change this by passing a symbol to the `:table_name` argument:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class Widget < ActiveRecord::Base
|
110
|
+
eav_hash_for :foobar, table_name: :bar_foo
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
Just remember to edit the table name in the migration, or use the following
|
115
|
+
migration generator:
|
116
|
+
|
117
|
+
$ rails generate eav_migration Widget foobar bar_foo
|
118
|
+
|
119
|
+
|
120
|
+
What's the catch?
|
121
|
+
-
|
122
|
+
By using this software, you agree to write me into your will as your next of
|
123
|
+
kin, and to sacrifice the soul of your first born child to Beelzebub.
|
124
|
+
|
125
|
+
Just kidding, the code is released under the MIT license so you can use it for
|
126
|
+
whatever purposes you see fit. Just don't sue me if your application blows up
|
127
|
+
from the sheer awesomeness! Check out the LICENSE file for more information.
|
128
|
+
|
129
|
+
Special Thanks!
|
130
|
+
-
|
131
|
+
Thanks to Matt Kimmel (@mattkimmel) for adding support for models contained in namespaces.
|
132
|
+
|
133
|
+
I found a bug or want to contribute!
|
134
|
+
-
|
135
|
+
You're probably reading this from GitHub, so you know what to do. If not, the
|
136
|
+
Github project is at https://github.com/200proof/eav_hashes
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'rdoc/task'
|
10
|
+
rescue LoadError
|
11
|
+
require 'rdoc/rdoc'
|
12
|
+
require 'rake/rdoctask'
|
13
|
+
RDoc::Task = Rake::RDocTask
|
14
|
+
end
|
15
|
+
|
16
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'EavHashes'
|
19
|
+
rdoc.options << '--line-numbers'
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
Bundler::GemHelper.install_tasks
|
24
|
+
|
25
|
+
require "rspec/core"
|
26
|
+
require "rspec/core/rake_task"
|
27
|
+
|
28
|
+
RSpec::Core::RakeTask.new(:spec)
|
29
|
+
|
30
|
+
task :default => :spec
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'lib/eav_hashes'
|
data/lib/eav_hashes.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module EavHashes
|
3
|
+
def self.included (base)
|
4
|
+
base.extend ActiveRecord::EavHashes::ClassMethods
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def eav_hash_for (hash_name, options={})
|
9
|
+
# Fill in default options not otherwise specified
|
10
|
+
options[:hash_name] = hash_name
|
11
|
+
options[:parent_class_name] = self.name.to_sym
|
12
|
+
options = ActiveRecord::EavHashes::Util::fill_options_hash options
|
13
|
+
|
14
|
+
# Store the options hash in a class variable to create the EavHash object
|
15
|
+
# if and when it's actually used.
|
16
|
+
class_variable_set "@@#{hash_name}_hash_options".to_sym, options
|
17
|
+
|
18
|
+
# Create the association, the entry update hook, and a helper method to lazy-load the entries
|
19
|
+
class_eval <<-END_EVAL
|
20
|
+
has_many :#{options[:entry_assoc_name]}, class_name: #{options[:entry_class_name]}, foreign_key: "#{options[:parent_assoc_name]}_id"
|
21
|
+
after_save :save_#{hash_name}
|
22
|
+
def #{hash_name}
|
23
|
+
@#{hash_name} ||= ActiveRecord::EavHashes::EavHash.new(self, @@#{hash_name}_hash_options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def save_#{hash_name}
|
27
|
+
@#{hash_name}.save_entries if @#{hash_name}
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find_by_#{hash_name} (key, value=nil)
|
31
|
+
self.find (ActiveRecord::EavHashes::Util::run_find_expression(key, value, @@#{hash_name}_hash_options))
|
32
|
+
end
|
33
|
+
END_EVAL
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module EavHashes
|
3
|
+
# Used instead of nil when a nil value is assigned
|
4
|
+
# (otherwise, the value will try to deserialize itself and
|
5
|
+
# that would break everything horrifically)
|
6
|
+
class NilPlaceholder; end
|
7
|
+
|
8
|
+
# Represent an EAV row. This class should NOT be used directly, instead it should be inherited from
|
9
|
+
# by the class generated by eav_hash_for.
|
10
|
+
class EavEntry < ActiveRecord::Base
|
11
|
+
# prevent activerecord from thinking we're trying to do STI
|
12
|
+
self.abstract_class = true
|
13
|
+
|
14
|
+
# Tell ActiveRecord to convert the value to its DB storable format
|
15
|
+
before_save :serialize_value
|
16
|
+
|
17
|
+
# Let the key be assignable only once on creation
|
18
|
+
attr_readonly :entry_key
|
19
|
+
|
20
|
+
# Contains the values the value_type column should have based on the type of the value being stored
|
21
|
+
SUPPORTED_TYPES = {
|
22
|
+
:String => 0,
|
23
|
+
:Symbol => 1,
|
24
|
+
:Integer => 2,
|
25
|
+
:Fixnum => 2,
|
26
|
+
:Bignum => 2,
|
27
|
+
:Float => 3,
|
28
|
+
:Complex => 4,
|
29
|
+
:Rational => 5,
|
30
|
+
:Boolean => 6, # For code readability
|
31
|
+
:TrueClass => 6,
|
32
|
+
:FalseClass => 6,
|
33
|
+
:Object => 7 # anything else (including Hashes, Arrays) will be serialized to yaml and saved as Object
|
34
|
+
}
|
35
|
+
|
36
|
+
# Does some sanity checks.
|
37
|
+
def after_initialize
|
38
|
+
raise "key should be a string or symbol!" unless key.is_a? String or key.is_a? Symbol
|
39
|
+
raise "value should not be empty!" if @value.is_a? String and value.empty?
|
40
|
+
raise "value should not be nil!" if @value.nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
# Gets the EAV row's value
|
44
|
+
def value
|
45
|
+
return nil if @value.is_a? NilPlaceholder
|
46
|
+
@value.nil? ? deserialize_value : @value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Sets the EAV row's value
|
50
|
+
# @param [Object] val the value
|
51
|
+
def value= (val)
|
52
|
+
@value = (val.nil? ? NilPlaceholder.new : val)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def key
|
57
|
+
k = read_attribute :entry_key
|
58
|
+
(read_attribute :symbol_key) ? k.to_sym : k
|
59
|
+
end
|
60
|
+
|
61
|
+
# Raises an error if you try changing the key (unless no key is set)
|
62
|
+
def key= (val)
|
63
|
+
raise "Keys are immutable!" if read_attribute(:entry_key)
|
64
|
+
raise "Key must be a string!" unless val.is_a?(String) or val.is_a?(Symbol)
|
65
|
+
write_attribute :entry_key, val.to_s
|
66
|
+
write_attribute :symbol_key, (val.is_a? Symbol)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Gets the value_type column's value for the type of value passed
|
70
|
+
# @param [Object] val the object whose value_type to determine
|
71
|
+
def self.get_value_type (val)
|
72
|
+
return nil if val.nil?
|
73
|
+
ret = SUPPORTED_TYPES[val.class.name.to_sym]
|
74
|
+
if ret.nil?
|
75
|
+
ret = SUPPORTED_TYPES[:Object]
|
76
|
+
end
|
77
|
+
ret
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
# Sets the value_type column to the appropriate value based on the value's type
|
82
|
+
def update_value_type
|
83
|
+
write_attribute :value_type, EavEntry.get_value_type(@value)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Converts the value to its database-storable form and tells ActiveRecord that it's been changed (if it has)
|
87
|
+
def serialize_value
|
88
|
+
# Returning nil will prevent the row from being saved, to save some time since the EavHash that manages this
|
89
|
+
# entry will have marked it for deletion.
|
90
|
+
raise "Tried to save with a nil value!" if @value.nil? or @value.is_a? NilPlaceholder
|
91
|
+
|
92
|
+
update_value_type
|
93
|
+
if value_type == SUPPORTED_TYPES[:Object]
|
94
|
+
write_attribute :value, YAML::dump(@value)
|
95
|
+
else
|
96
|
+
write_attribute :value, @value.to_s
|
97
|
+
end
|
98
|
+
|
99
|
+
read_attribute :value
|
100
|
+
end
|
101
|
+
|
102
|
+
# Converts the value from it's database representation to the type specified in the value_type column.
|
103
|
+
def deserialize_value
|
104
|
+
if @value.nil?
|
105
|
+
@value = read_attribute :value
|
106
|
+
end
|
107
|
+
|
108
|
+
case value_type
|
109
|
+
when SUPPORTED_TYPES[:Object] # or Hash, Array, etc.
|
110
|
+
@value = YAML::load @value
|
111
|
+
when SUPPORTED_TYPES[:Symbol]
|
112
|
+
@value = @value.to_sym
|
113
|
+
when SUPPORTED_TYPES[:Integer] # or Fixnum, Bignum
|
114
|
+
@value = @value.to_i
|
115
|
+
when SUPPORTED_TYPES[:Float]
|
116
|
+
@value = @value.to_f
|
117
|
+
when SUPPORTED_TYPES[:Complex]
|
118
|
+
@value = Complex @value
|
119
|
+
when SUPPORTED_TYPES[:Rational]
|
120
|
+
@value = Rational @value
|
121
|
+
when SUPPORTED_TYPES[:Boolean]
|
122
|
+
@value = (@value == "true")
|
123
|
+
else
|
124
|
+
@value
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module EavHashes
|
3
|
+
# Wraps a bunch of EavEntries and lets you use them like you would a hash
|
4
|
+
# This class should not be used directly and you should instead let eav_hash_for create one for you
|
5
|
+
class EavHash
|
6
|
+
# Creates a new EavHash. You should really let eav_hash_for do this for you...
|
7
|
+
# @param [ActiveRecord::Base] owner the Model which will own this hash
|
8
|
+
# @param [Hash] options the options hash which eav_hash generated
|
9
|
+
def initialize(owner, options)
|
10
|
+
Util::sanity_check options
|
11
|
+
@owner = owner
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Saves any modified entries and deletes any which have been nil'd to save DB space
|
16
|
+
def save_entries
|
17
|
+
# The entries are lazy-loaded, so don't do anything if they haven't been accessed or modified
|
18
|
+
return unless (@entries and @changes_made)
|
19
|
+
|
20
|
+
@entries.values.each do |entry|
|
21
|
+
if entry.value.nil?
|
22
|
+
entry.delete
|
23
|
+
else
|
24
|
+
set_entry_owner(entry)
|
25
|
+
entry.save
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Gets the value of an EAV attribute
|
31
|
+
# @param [String, Symbol] key
|
32
|
+
def [](key)
|
33
|
+
raise "Key must be a string or a symbol!" unless key.is_a?(String) or key.is_a?(Symbol)
|
34
|
+
load_entries_if_needed
|
35
|
+
return @entries[key].value if @entries[key]
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Sets the value of the EAV attribute `key` to `value`
|
40
|
+
# @param [String, Symbol] key the attribute
|
41
|
+
# @param [Object] value the value
|
42
|
+
def []=(key, value)
|
43
|
+
update_or_create_entry key, value
|
44
|
+
end
|
45
|
+
|
46
|
+
# I don't know why Ruby hashes don't have a shovel operator, but I will make damn sure that I
|
47
|
+
# fight the power and stick it to the man by implementing it.
|
48
|
+
# @param [Hash, EavHash] dirt the dirt to shovel (ba dum, tss)
|
49
|
+
def <<(dirt)
|
50
|
+
if dirt.is_a? Hash
|
51
|
+
dirt.each do |key, value|
|
52
|
+
update_or_create_entry key, value
|
53
|
+
end
|
54
|
+
elsif dirt.is_a? EavHash
|
55
|
+
dirt.entries.each do |key, entry|
|
56
|
+
update_or_create_entry key, entry.value
|
57
|
+
end
|
58
|
+
else
|
59
|
+
raise "You can't shovel something that's not a Hash or EavHash here!"
|
60
|
+
end
|
61
|
+
|
62
|
+
self
|
63
|
+
end
|
64
|
+
|
65
|
+
# Gets the raw hash containing EavEntries by their keys
|
66
|
+
def entries
|
67
|
+
load_entries_if_needed
|
68
|
+
end
|
69
|
+
|
70
|
+
# Gets the actual values this EavHash contains
|
71
|
+
def values
|
72
|
+
load_entries_if_needed
|
73
|
+
|
74
|
+
ret = []
|
75
|
+
@entries.values.each do |value|
|
76
|
+
ret << value
|
77
|
+
end
|
78
|
+
|
79
|
+
ret
|
80
|
+
end
|
81
|
+
|
82
|
+
# Gets the keys this EavHash manages
|
83
|
+
def keys
|
84
|
+
load_entries_if_needed
|
85
|
+
@entries.keys
|
86
|
+
end
|
87
|
+
|
88
|
+
# Emulates Hash.each
|
89
|
+
def each (&block)
|
90
|
+
as_hash.each block
|
91
|
+
end
|
92
|
+
|
93
|
+
# Emulates Hash.each_pair (same as each)
|
94
|
+
def each_pair (&block)
|
95
|
+
each &block
|
96
|
+
end
|
97
|
+
|
98
|
+
# Empties the hash by setting all the values to nil
|
99
|
+
# (without committing them, of course)
|
100
|
+
def clear
|
101
|
+
load_entries_if_needed
|
102
|
+
@entries.each do |_, entry|
|
103
|
+
entry.value = nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns a hash with each entry key mapped to its actual value,
|
108
|
+
# not the internal EavEntry
|
109
|
+
def as_hash
|
110
|
+
load_entries_if_needed
|
111
|
+
hsh = {}
|
112
|
+
@entries.each do |k, entry|
|
113
|
+
hsh[k] = entry.value
|
114
|
+
end
|
115
|
+
|
116
|
+
hsh
|
117
|
+
end
|
118
|
+
|
119
|
+
# Take the crap out of #inspect calls
|
120
|
+
def inspect
|
121
|
+
as_hash
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
def update_or_create_entry(key, value)
|
126
|
+
raise "Key must be a string or a symbol!" unless key.is_a?(String) or key.is_a?(Symbol)
|
127
|
+
load_entries_if_needed
|
128
|
+
|
129
|
+
@changes_made = true
|
130
|
+
@owner.updated_at = Time.now
|
131
|
+
|
132
|
+
if @entries[key]
|
133
|
+
@entries[key].value = value
|
134
|
+
else
|
135
|
+
new_entry = @options[:entry_class].new
|
136
|
+
set_entry_owner(new_entry)
|
137
|
+
new_entry.key = key
|
138
|
+
new_entry.value = value
|
139
|
+
|
140
|
+
@entries[key] = new_entry
|
141
|
+
|
142
|
+
value
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Since entries are lazy-loaded, this is called just before an operation on an entry happens and
|
147
|
+
# loads the rows only once per EavHash lifetime.
|
148
|
+
def load_entries_if_needed
|
149
|
+
if @entries.nil?
|
150
|
+
@entries = {}
|
151
|
+
rows_from_model = @owner.send("#{@options[:entry_assoc_name]}")
|
152
|
+
rows_from_model.each do |row|
|
153
|
+
@entries[row.key] = row
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
@entries
|
158
|
+
end
|
159
|
+
|
160
|
+
# Sets an entry's owner ID. This is called when we save attributes for a model which has just been
|
161
|
+
# created and not committed to the DB prior to having its EAV hash(es) modified
|
162
|
+
# @param [EavEntry] entry the entry whose owner to change
|
163
|
+
def set_entry_owner(entry)
|
164
|
+
entry.send "#{@options[:parent_assoc_name]}_id=", @owner.id
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|