doeskeyvalue 0.2.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/doeskeyvalue.rb CHANGED
@@ -1,100 +1,67 @@
1
1
  # AWEXOME LABS
2
2
  # DoesKeyValue
3
3
 
4
- require 'doeskeyvalue'
5
- require 'rails'
6
- require 'active_record'
7
- require 'hashie'
8
-
9
- require 'doeskeyvalue/key_manager'
10
- require 'doeskeyvalue/keys'
11
- require 'doeskeyvalue/indexes'
12
- require 'doeskeyvalue/util'
4
+ require "doeskeyvalue"
5
+ require "active_record"
6
+ require "active_support"
7
+ require "hashie"
13
8
 
9
+ require "doeskeyvalue/configuration"
10
+ require "doeskeyvalue/state"
11
+ require "doeskeyvalue/util"
12
+ require "doeskeyvalue/index"
13
+ require "doeskeyvalue/accessors"
14
+ # require "doeskeyvalue/column_storage" # <= Deprecating in favor of combined Accessors
15
+ # require "doeskeyvalue/table_storage" # <= Deprecating in favor of combined Accessors
14
16
 
15
17
  module DoesKeyValue
16
18
 
17
- # Create a Rails Engine
18
- class Engine < Rails::Engine
19
- end
20
-
21
19
  # Return the current working version from VERSION file:
22
20
  def self.version
23
- @@version ||= File.open(File.join(File.dirname(__FILE__), "..", "VERSION"), "r").read
24
- end
25
-
26
- # Exception Types for DoesKeyValue
27
- class NoColumnNameSpecified < Exception
28
- def initialize(msg="A class column name must be provided for storing key values in blob"); super(msg); end
29
- end
30
- class NoKeyNameSpecified < Exception
31
- def initialize(msg="A key name must be provided to build a DoesKeyValue key"); super(msg); end
21
+ Gem.loaded_specs["doeskeyvalue"].version.to_s
32
22
  end
33
- class NoKeyForThatIndex < Exception
34
- def initialize(msg="A key must exist before an index can be applied to it"); super(msg); end
35
- end
36
- class KeyAndIndexOptionsMustBeHash < Exception
37
- def initialize(msg="Options passed to declarations of keys and indexes must of class Hash"); super(msg); end
38
- end
39
- class KeyValueIndexTableDoesNotExist < Exception
40
- def initialize(msg="DoesKeyValue requires an index table be generated to use key indexes. Use generator to generate migration"); super(msg); end
23
+
24
+ # Log messages
25
+ def self.log(msg)
26
+ puts "DoesKeyValue: #{msg}" unless configuration.log_level == :silent
41
27
  end
42
-
28
+
43
29
  end # DoesKeyValue
44
30
 
45
31
 
46
32
  module ActiveRecord
47
33
  class Base
48
-
49
- # Call this method within your class to establish key-value behavior and prep
50
- # the internal structure that will hold the blob
51
- def self.doeskeyvalue(column, opts={})
34
+
35
+ # Call this "acts as" method within your ActiveRecord class to establish key-
36
+ # value behavior and prepare internal storage structures
37
+ def self.does_keys(opts={})
38
+ DoesKeyValue.log("Adding key-value support to class #{self.to_s}")
39
+ # TODO: Raise exception to improper opts passed
52
40
  self.instance_eval do
53
- extend DoesKeyValue::Keys
54
- extend DoesKeyValue::Indexes
55
-
56
- # Identify the AR text column holding our data and serialize it:
57
- @column_name = column.to_sym
58
- cattr_accessor :column_name
59
- serialize @column_name, Hashie::Mash
60
-
61
- # Add the column to the key and column manager so we can reference it later:
62
- DoesKeyValue::KeyManager.instance.declare_column(self, @column_name)
41
+ DoesKeyValue::State.instance.add_class(self, opts)
42
+ self.send(:serialize, opts[:column], Hash) if opts[:column]
43
+ self.send(:attr_accessor, :key_value_cache) if opts[:table]
44
+ extend DoesKeyValue::Accessors
63
45
  end
64
-
65
- Array.class_eval do
66
- include DoesKeyValue::Util::CondArray
67
- end
68
-
69
- instance_eval <<-EOS
70
- def #{@column_name}_key(key_name, opts={})
71
- column_name = :#{@column_name}
72
- key_name = key_name.to_sym
73
- declare_key(column_name, key_name, opts)
74
- end
75
-
76
- def #{@column_name}_index(key_name, opts={})
77
- column_name = :#{@column_name}
78
- key_name = key_name.to_sym
79
- declare_index(column_name, key_name, opts)
80
- end
81
-
82
- def key_value_columns
83
- return DoesKeyValue::KeyManager.instance.columns_for(self)
84
- end
85
-
86
- def #{@column_name}_keys
87
- column_name = :#{@column_name}
88
- return DoesKeyValue::KeyManager.instance.keys_for(self, column_name)
89
- end
90
-
91
- def #{@column_name}_indexes
92
- column_name = :#{@column_name}
93
- return DoesKeyValue::KeyManager.instance.indexes_for(self, column_name)
94
- end
95
- EOS
46
+
47
+
48
+ # if storage_column = opts[:column]
49
+ # DoesKeyValue.log("Adding key-value support via column #{storage_column} to class #{self.to_s}")
50
+ # self.instance_eval do
51
+ # DoesKeyValue::State.instance.add_class(self, :column=>storage_column)
52
+ # self.send(:serialize, storage_column, Hash)
53
+ # extend DoesKeyValue::ColumnStorage
54
+ # end
55
+
56
+ # elsif storage_table = opts[:table]
57
+ # DoesKeyValue.log("Adding key-value support via table #{storage_table} to class #{self.to_s}")
58
+ # self.instance_eval do
59
+ # DoesKeyValue::State.instance.add_class(self, :table=>storage_table)
60
+ # extend DoesKeyValue::TableStorage
61
+ # end
62
+ # end
63
+
96
64
  end
97
65
 
98
-
99
- end # ActiveRecord::Base
66
+ end # Base
100
67
  end # ActiveRecord
@@ -0,0 +1,136 @@
1
+ # AWEXOME LABS
2
+ # DoesKeyValue
3
+ #
4
+ # TableStorage -- Accessor and update methods for keys managed under the
5
+ # key-value store defined for given classes.
6
+
7
+ module DoesKeyValue
8
+
9
+ # Define our types for strongly-typed results storage:
10
+ SUPPORTED_DATA_TYPES = %w(string integer decimal boolean datetime)
11
+
12
+ module Accessors
13
+
14
+ # Define a supported key
15
+ def has_key(key_name, opts={})
16
+ opts = {
17
+ index: true,
18
+ type: "string",
19
+ default: nil
20
+ }.merge(opts)
21
+
22
+ # Table-based storage dictates an index:
23
+ opts[:index] = true if table_storage?
24
+
25
+ # Cache values of common options:
26
+ key_indexed = opts[:index]
27
+ key_type = opts[:type].to_sym
28
+ key_default_value = opts[:default]
29
+
30
+ # Ensure we are invoked with a supported data type:
31
+ raise Exception.new("Data type not supported: #{key_type}") unless DoesKeyValue::SUPPORTED_DATA_TYPES.include?(key_type.to_s)
32
+
33
+ # Save a representation of this key in the State:
34
+ define_key(key_name, opts)
35
+
36
+ # Accessor for new key with support for default value:
37
+ define_method(key_name) do
38
+ DoesKeyValue.log("Accessing key:#{key_name} for class:#{self.class}")
39
+ blob = if self.class.column_storage?
40
+ Hashie::Mash.new( self.send(:read_attribute, self.class.storage_column) || Hash.new )
41
+ elsif self.class.table_storage?
42
+ # TODO: Proper cache-through hash for Table-based storage
43
+ Hashie::Mash.new( {key_name => DoesKeyValue::Index.read_index(self, key_name)} )
44
+ end
45
+
46
+ value = blob.send(key_name)
47
+
48
+ return value if value
49
+ return key_default_value
50
+ end
51
+
52
+ # Manipulator for new key:
53
+ define_method("#{key_name}=") do |value|
54
+ DoesKeyValue.log("Modifying value for key:#{key_name} to value:#{value}")
55
+
56
+ if self.class.column_storage?
57
+ blob = Hashie::Mash.new( self.send(:read_attribute, self.class.storage_column) || Hash.new )
58
+ typed_value = DoesKeyValue::Util.to_type(value, key_type)
59
+ blob[key_name] = typed_value
60
+ self.send(:write_attribute, self.class.storage_column, blob.to_hash)
61
+
62
+ elsif self.class.table_storage?
63
+ # TODO: Proper cache-through hash for Table-based storage:
64
+ DoesKeyValue::Index.update_index(self, key_name, value) unless self.new_record?
65
+ end
66
+ end
67
+
68
+ # If key is indexed, add scopes, finders, and callbacks for index management:
69
+ if key_indexed
70
+
71
+ # With scope:
72
+ scope "with_#{key_name}", lambda {|value|
73
+ DoesKeyValue::Index.find_objects(self, key_name, value)
74
+ }
75
+ DoesKeyValue.log("Scope with_#{key_name} added for indexed key #{key_name}")
76
+
77
+ # Update the index after save:
78
+ if column_storage?
79
+ define_method("update_index_for_#{key_name}") do
80
+ DoesKeyValue::Index.update_index(self, key_name, self.send(key_name))
81
+ end
82
+ after_save "update_index_for_#{key_name}"
83
+ end
84
+
85
+ # Delete the index after destroy:
86
+ define_method("destroy_index_for_#{key_name}") do
87
+ DoesKeyValue::Index.delete_index(self, key_name)
88
+ end
89
+ after_destroy "destroy_index_for_#{key_name}"
90
+
91
+ end # if key_indexed
92
+
93
+
94
+ end
95
+
96
+
97
+ # Return true if this class uses column-based storage for key-value pairs:
98
+ def column_storage?
99
+ !storage_column.nil?
100
+ end
101
+
102
+ # Return the column used for storing key values for this class:
103
+ def storage_column
104
+ @storage_column ||= DoesKeyValue::State.instance.options_for_class(self)[:column]
105
+ end
106
+
107
+ # Return true if this class uses table-based storage for key-value pairs:
108
+ def table_storage?
109
+ !storage_table.nil?
110
+ end
111
+
112
+ # Return the table used for storing key values for this class:
113
+ def storage_table
114
+ @storage_table ||= DoesKeyValue::State.instance.options_for_class(self)[:table]
115
+ end
116
+
117
+ # Return a list of currently supported keys:
118
+ def keys
119
+ DoesKeyValue::State.instance.keys[self.to_s]
120
+ end
121
+
122
+ # Return the specific configuration for a given key:
123
+ def key_options(key_name)
124
+ DoesKeyValue::State.instance.options_for_key(self, key_name)
125
+ end
126
+
127
+
128
+ private
129
+
130
+ # Define a new key for a class:
131
+ def define_key(key_name, opts={})
132
+ DoesKeyValue::State.instance.add_key(self, key_name, opts)
133
+ end
134
+
135
+ end # Accessors
136
+ end # DoesKeyValue
@@ -0,0 +1,95 @@
1
+ # AWEXOME LABS
2
+ # DoesKeyValue
3
+ #
4
+ # ColumnStorage -- Support and methods for key-value pairs stored within TEXT
5
+ # or BLOB fields on the same table as the parent object.
6
+
7
+ module DoesKeyValue
8
+
9
+ SUPPORTED_DATA_TYPES = %w(string integer decimal boolean datetime)
10
+
11
+ module ColumnStorage
12
+
13
+ # Define a supported key
14
+ def has_key(key_name, opts={})
15
+ opts = {
16
+ index: true,
17
+ type: "string",
18
+ default: nil
19
+ }.merge(opts)
20
+
21
+ key_indexed = opts[:index]
22
+ key_type = opts[:type].to_sym
23
+ key_default_value = opts[:default]
24
+
25
+ raise Exception.new("Data type not supported: #{key_type}") unless DoesKeyValue::SUPPORTED_DATA_TYPES.include?(key_type.to_s)
26
+
27
+ DoesKeyValue::State.instance.add_key(self, key_name, opts)
28
+ storage_column = DoesKeyValue::State.instance.options_for_class(self)[:column]
29
+
30
+ # Accessor for new key with support for default value:
31
+ define_method(key_name) do
32
+ DoesKeyValue.log("Accessing key:#{key_name} for class:#{self.class}")
33
+ blob = self.send(:read_attribute, storage_column) || Hash.new
34
+ blob = Hashie::Mash.new(blob)
35
+ value = blob.send(key_name)
36
+
37
+ if value
38
+ return value
39
+ elsif default_value = self.class.key_options(key_name)[:default]
40
+ return default_value
41
+ end
42
+ end
43
+
44
+ # Manipulator for new key:
45
+ define_method("#{key_name}=") do |value|
46
+ DoesKeyValue.log("Modifying value for key:#{key_name} to value:#{value}")
47
+ blob = self.send(:read_attribute, storage_column) || Hash.new
48
+ blob = Hashie::Mash.new(blob)
49
+
50
+ typed_value = DoesKeyValue::Util.to_type(value, key_type)
51
+
52
+ blob[key_name] = typed_value
53
+ self.send(:write_attribute, storage_column, blob.to_hash)
54
+ end
55
+
56
+ # If key is indexed, add scopes, finders, and callbacks for index management:
57
+ if key_indexed
58
+
59
+ # With scope:
60
+ scope "with_#{key_name}", lambda {|value|
61
+ DoesKeyValue::Index.find_objects(self, key_name, value)
62
+ }
63
+ DoesKeyValue.log("Scope with_#{key_name} added for indexed key #{key_name}")
64
+
65
+ # Update the index after save:
66
+ define_method("update_index_for_#{key_name}") do
67
+ DoesKeyValue::Index.update_index(self, key_name, self.send(key_name))
68
+ end
69
+ after_save "update_index_for_#{key_name}"
70
+
71
+ # Delete the index after destroy:
72
+ define_method("destroy_index_for_#{key_name}") do
73
+ DoesKeyValue::Index.delete_index(self, key_name)
74
+ end
75
+ after_destroy "destroy_index_for_#{key_name}"
76
+
77
+ end # if key_indexed
78
+
79
+
80
+ end
81
+
82
+
83
+ # Return a list of currently supported keys:
84
+ def keys
85
+ DoesKeyValue::State.instance.keys[self.to_s]
86
+ end
87
+
88
+ # Return the specific configuration for a given key:
89
+ def key_options(key_name)
90
+ DoesKeyValue::State.instance.options_for_key(self, key_name)
91
+ end
92
+
93
+
94
+ end # ColumnStorage
95
+ end # DoesKeyValue
@@ -0,0 +1,32 @@
1
+ # AWEXOME LABS
2
+ # DoesKeyValue : Configuration
3
+
4
+ module DoesKeyValue
5
+
6
+ class Configuration
7
+
8
+ # Configurable options:
9
+ attr_accessor :log_level
10
+
11
+ # Declare defaults on load:
12
+ def initialize
13
+ @log_level = :silent
14
+ end
15
+
16
+ end # Configuration
17
+
18
+
19
+ # Provide an accessor to the gem configuration:
20
+ class << self
21
+ attr_accessor :configuration
22
+ end
23
+
24
+ # Yield the configuration to host:
25
+ def self.configure
26
+ self.configuration ||= Configuration.new
27
+ yield(configuration)
28
+ end
29
+
30
+ DoesKeyValue.configuration = Configuration.new
31
+
32
+ end # DoesKeyValue
@@ -0,0 +1,145 @@
1
+ # AWEXOME LABS
2
+ # DoesKeyValue
3
+ #
4
+ # DoesKeyValue::Index -- An AR model used for updating indexes
5
+
6
+ module DoesKeyValue
7
+
8
+ class Index < ActiveRecord::Base
9
+
10
+ # The default index table (overidden individually for custom tables):
11
+ self.table_name = "key_value_index"
12
+
13
+ # All attributes of an index row are mass-editable:
14
+ attr_accessible :obj_type, :obj_id, :key_name, :key_type,
15
+ :value_string, :value_integer, :value_decimal, :value_boolean, :value_datetime
16
+
17
+
18
+ # Search the database for objects matching the given query for the given key:
19
+ def self.find_objects(klass, key_name, value)
20
+ object_type = klass.to_s
21
+ key_type = klass.key_options(key_name)[:type]
22
+
23
+ condition_set = {obj_type: object_type, key_name: key_name, key_type: key_type, "value_#{key_type}"=>value}
24
+ DoesKeyValue.log("Condition Set for index find: #{condition_set.inspect}")
25
+ table_agnostic_exec(klass) do
26
+ index_rows = DoesKeyValue::Index.where(condition_set)
27
+ object_ids = index_rows.blank? ? [] : index_rows.collect {|i| i.obj_id }
28
+ klass.where(:id=>object_ids)
29
+ end
30
+ end
31
+
32
+
33
+ # Read the appropriate index values for the given object/key combination:
34
+ def self.read_index(object, key_name)
35
+ object_type = object.class.to_s
36
+ object_id = object.id
37
+ key_type = object.class.key_options(key_name)[:type]
38
+
39
+ # Prepare the query conditions:
40
+ condition_set = {obj_type: object_type, obj_id: object_id, key_name: key_name}
41
+
42
+ # Access the appropriate value column of the returned index:
43
+ table_agnostic_exec(object.class) do
44
+ DoesKeyValue::Index.where(condition_set).first().try(:send, "value_#{key_type}")
45
+ end
46
+ end
47
+
48
+
49
+ # Update the appropriate index with new/changed information for the given
50
+ # object/key/value combination:
51
+ def self.update_index(object, key_name, value)
52
+ # Log our index column values:
53
+ object_type = object.class.to_s
54
+ object_id = object.id
55
+ key_type = object.class.key_options(key_name)[:type]
56
+
57
+ # Prepare update conditions and manipulators:
58
+ update_set = {key_type: key_type}
59
+ condition_set = {obj_type: object_type, obj_id: object_id, key_name: key_name}
60
+ create_set = {obj_type: object_type, obj_id: object_id, key_name: key_name, key_type: key_type}
61
+
62
+ # Insert the new value of the correct type:
63
+ update_set["value_#{key_type}"] = value
64
+ create_set["value_#{key_type}"] = value
65
+
66
+ DoesKeyValue.log("Updating Index for class:#{object_type} key:#{key_name}:")
67
+ DoesKeyValue.log("update_set: #{update_set.inspect}")
68
+ DoesKeyValue.log("condition_set: #{condition_set.inspect}")
69
+ DoesKeyValue.log("create_set: #{create_set.inspect}")
70
+
71
+ # Update an index in a table-agnostic way to support table-based key-value storage in
72
+ # database tables other than the universal table:
73
+ table_agnostic_exec(object.class) do
74
+ updated_count = DoesKeyValue::Index.update_all( update_set, condition_set )
75
+ if !value.nil? && updated_count == 0
76
+ DoesKeyValue::Index.create( create_set )
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+
83
+ # Delete an index record for a given object/key combination:
84
+ def self.delete_index(object, key_name)
85
+ object_type = object.class.to_s
86
+ object_id = object.id
87
+
88
+ deleted_count = table_agnostic_exec(object.class) do
89
+ DoesKeyValue::Index.delete_all(
90
+ obj_type: object_type, obj_id: object_id, key_name: key_name
91
+ )
92
+ end
93
+ end
94
+
95
+
96
+
97
+ private
98
+
99
+ # Return true only if the storage method for the given class is table:
100
+ def self.table_storage_for_class?(klass)
101
+ storage_options = DoesKeyValue::State.instance.options_for_class(klass)
102
+ storage_options[:table].nil? ? false : true
103
+ end
104
+
105
+ # Perform a block action inside of table-altered query:
106
+ def self.table_agnostic_exec(klass)
107
+ begin
108
+ original_table_name = DoesKeyValue::Index.table_name
109
+ if DoesKeyValue::Index.table_storage_for_class?(klass)
110
+ class_storage_options = DoesKeyValue::State.instance.options_for_class(klass)
111
+ if class_storage_options[:table]
112
+ DoesKeyValue::Index.table_name = class_storage_options[:table]
113
+ DoesKeyValue.log("Storage table for index changed to '#{DoesKeyValue::Index.table_name}'")
114
+ end
115
+ end
116
+
117
+ exec_result = yield
118
+
119
+ if DoesKeyValue::Index.table_storage_for_class?(klass)
120
+ DoesKeyValue::Index.table_name = original_table_name
121
+ DoesKeyValue.log("Storage table for index changed back to original '#{DoesKeyValue::Index.table_name}")
122
+ end
123
+
124
+ return exec_result
125
+
126
+ rescue ActiveRecord::StatementInvalid => e
127
+ DoesKeyValue.log("Database query statement invalid: #{e.message}")
128
+ if (e.message =~ /doesn't exist/)
129
+ DoesKeyValue.log("It appears the index table `#{DoesKeyValue::Index.table_name}` expected has not been generated.")
130
+ DoesKeyValue.log("To generate the necessary table run: rails generate doeskeyvalue #{DoesKeyValue::Index.table_name}")
131
+ end
132
+ raise e
133
+
134
+ ensure
135
+ # Ensure that the table name is always restored
136
+ DoesKeyValue::Index.table_name = original_table_name
137
+ DoesKeyValue.log("After table-agnostic execution, table name restored to '#{DoesKeyValue::Index.table_name}'")
138
+ end
139
+ end
140
+
141
+
142
+
143
+ end # Index
144
+
145
+ end # DoesKeyValue