doeskeyvalue 0.2.2 → 0.9.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/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