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.
@@ -0,0 +1,48 @@
1
+ # AWEXOME LABS
2
+ # DoesKeyValue
3
+ #
4
+ # State -- Singleton state class representing the state of supported objects,
5
+ # known keys, and powered indexes
6
+
7
+ require "singleton"
8
+
9
+ module DoesKeyValue
10
+ class State
11
+
12
+ # There can be only one!
13
+ include Singleton
14
+
15
+ attr_reader :classes, :keys
16
+
17
+ # Add support for a single class of objects:
18
+ def add_class(klass, opts={})
19
+ DoesKeyValue.log("State: Add support for class #{klass.to_s} with opts:#{opts.inspect}")
20
+ @classes ||= Hash.new
21
+ @classes[klass.to_s] = opts
22
+ end
23
+
24
+ # Add a key for a given class:
25
+ def add_key(klass, key_name, opts={})
26
+ DoesKeyValue.log("State: Add key #{key_name} to class #{klass.to_s} with opts:#{opts.inspect}")
27
+ @keys ||= Hash.new
28
+ @keys[klass.to_s] ||= Array.new
29
+ @keys[klass.to_s] << {name:key_name, options:opts}
30
+ end
31
+
32
+ # Return the configuration for a given klass:
33
+ def options_for_class(klass)
34
+ DoesKeyValue.log("State: Querying options_for_class for class:#{klass.to_s}")
35
+ @classes[klass.to_s] rescue {}
36
+ end
37
+
38
+ # Return the list of keys for a given klass:
39
+ def options_for_key(klass, key_name)
40
+ DoesKeyValue.log("State: Querying options_for_key for class:#{klass.to_s} and key:#{key_name}")
41
+ key_def = (@keys[klass.to_s] || Array.new).find{|x| x[:name]==key_name.to_sym} || {}
42
+ opts = key_def[:options]
43
+ end
44
+
45
+
46
+ end # State
47
+ end # DoesKeyValue
48
+
@@ -0,0 +1,79 @@
1
+ # AWEXOME LABS
2
+ # DoesKeyValue
3
+ #
4
+ # TableStorage -- Support and methods for key-value pairs stored in an altogether
5
+ # separate database table.
6
+
7
+ module DoesKeyValue
8
+ module TableStorage
9
+
10
+ # Define a supported key
11
+ def has_key(key_name, opts={})
12
+ opts = {
13
+ index: true,
14
+ type: "string",
15
+ default: nil
16
+ }.merge(opts)
17
+
18
+ key_indexed = opts[:index]
19
+ key_type = opts[:type].to_sym
20
+ key_default_value = opts[:default]
21
+
22
+ raise Exception.new("Data type not supported: #{key_type}") unless DoesKeyValue::SUPPORTED_DATA_TYPES.include?(key_type.to_s)
23
+
24
+ DoesKeyValue::State.instance.add_key(self, key_name, opts)
25
+ storage_table = DoesKeyValue::State.instance.options_for_class(self)[:table]
26
+
27
+ # Accessor for new key with support for default value:
28
+ define_method(key_name) do
29
+ DoesKeyValue.log("Accessing BY TABLE key:#{key_name} for class:#{self.class}")
30
+ if self.new_record?
31
+ DoesKeyValue.log("-- Object does not have a database ID. No resource to query against.")
32
+ return nil
33
+ end
34
+
35
+ value = DoesKeyValue::Index.read_index(self, key_name)
36
+ if !value.nil?
37
+ return value
38
+ elsif default_value = self.class.key_options(key_name)[:default]
39
+ return default_value
40
+ end
41
+ end
42
+
43
+ # Manipulator for new key:
44
+ define_method("#{key_name}=") do |value|
45
+ DoesKeyValue.log("Modifying BY TABLE value for key:#{key_name} to value:#{value}")
46
+ unless self.new_record?
47
+ DoesKeyValue::Index.update_index(self, key_name, value)
48
+ else
49
+ DoesKeyValue.log("-- Object does not have a database ID. Holding back table index update.")
50
+ end
51
+ end
52
+
53
+ # All table-based key-value stores have index finders and scopes:
54
+ scope "with_#{key_name}", lambda {|value|
55
+ DoesKeyValue::Index.find_objects(self, key_name, value)
56
+ }
57
+ DoesKeyValue.log("Scope with_#{key_name} added for table-storage key #{key_name}")
58
+
59
+ # Delete the index after destroy:
60
+ define_method("destroy_index_for_#{key_name}") do
61
+ DoesKeyValue::Index.delete_index(self, key_name)
62
+ end
63
+ after_destroy "destroy_index_for_#{key_name}"
64
+
65
+ end
66
+
67
+
68
+ # Return a list of currently supported keys:
69
+ def keys
70
+ DoesKeyValue::State.instance.keys[self.to_s]
71
+ end
72
+
73
+ # Return the specific configuration for a given key:
74
+ def key_options(key_name)
75
+ DoesKeyValue::State.instance.options_for_key(self, key_name)
76
+ end
77
+
78
+ end # TableStorage
79
+ end # DoesKeyValue
@@ -1,27 +1,34 @@
1
1
  # AWEXOME LABS
2
2
  # DoesKeyValue
3
+ #
4
+ # Util -- Utility methods and helpers for use in data access and manipulation
3
5
 
6
+ require "singleton"
4
7
 
5
8
  module DoesKeyValue
6
- module Util
7
-
8
- module CondArray
9
- def add_condition(cond, conj="AND")
10
- if cond.is_a?(Array)
11
- if self.empty?
12
- (self << cond).flatten!
13
- else
14
- self[0] += " #{conj} #{cond.shift}"
15
- (self << cond).flatten!
16
- end
17
- elsif cond.is_a?(String)
18
- self[0] += " #{conj} #{cond}"
9
+ class Util
10
+
11
+ # Convert a value to the given type:
12
+ def self.to_type(value, type)
13
+ DoesKeyValue.log("Converting type of value:#{value} to type:#{type}")
14
+ case type.to_sym
15
+ when :string
16
+ value.to_s
17
+ when :integer
18
+ value.to_i
19
+ when :boolean
20
+ converted = true if value == true || value =~ /(true|t|yes|y|1)$/i
21
+ converted = false if value == false || value =~ /(false|f|no|n|0)$/i
22
+ converted
23
+ when :decimal
24
+ value.to_f
25
+ when :datetime
26
+ value.to_datetime
19
27
  else
20
- raise "Condition must be an Array or String"
21
- end
22
- self
28
+ value
23
29
  end
24
30
  end
25
-
26
- end
31
+
32
+ end # Util
27
33
  end # DoesKeyValue
34
+
@@ -1,14 +1,15 @@
1
1
  # AWEXOME LABS
2
2
  # DoesKeyValue
3
3
  #
4
- # IndexTableGenerator -- generator for the Index database table
4
+ # Generator -- Migration generator for the column-backed index table and/or
5
+ # the table-backed storage tables
5
6
 
6
- require 'rails/generators'
7
- require 'rails/generators/migration'
7
+ require "rails/generators"
8
+ require "rails/generators/migration"
8
9
 
9
10
  class DoeskeyvalueGenerator < Rails::Generators::Base
10
11
  include Rails::Generators::Migration
11
- source_root File.expand_path('../templates', __FILE__)
12
+ source_root File.expand_path("../templates", __FILE__)
12
13
 
13
14
  def self.next_migration_number(path)
14
15
  if ActiveRecord::Base.timestamped_migrations
@@ -19,7 +20,10 @@ class DoeskeyvalueGenerator < Rails::Generators::Base
19
20
  end
20
21
 
21
22
  def create_migration_file
22
- migration_template 'create_key_value_index.rb', 'db/migrate/create_key_value_index.rb'
23
+ puts "Creating DoesKeyValue index table migration"
24
+ index_table_name = ARGV.first || "key_value_index"
25
+ puts "=> #{index_table_name} table to be created"
26
+ migration_template "create_key_value_index.rb", "db/migrate/create_#{index_table_name}.rb", table_name:index_table_name
23
27
  end
24
28
 
25
29
  end
@@ -3,22 +3,37 @@
3
3
  #
4
4
  # CreateKeyValueIndex -- generated migration template for key/value index table
5
5
 
6
- class CreateKeyValueIndex < ActiveRecord::Migration
6
+ <%
7
+ table_name = config[:table_name]
8
+ -%>
9
+
10
+ class Create<%=table_name.camelize-%> < ActiveRecord::Migration
7
11
  def self.up
8
- create_table :key_value_index do |t|
12
+ create_table :<%=table_name-%> do |t|
13
+ # The object is linked by class type and id:
9
14
  t.string :obj_type
10
- t.string :key_name
11
- t.string :value
12
15
  t.integer :obj_id
16
+
17
+ # The key is identified by name and data type:
18
+ t.string :key_name
19
+ t.string :key_type
13
20
 
21
+ # The value is stored in various possible formats:
22
+ t.string :value_string
23
+ t.integer :value_integer
24
+ t.decimal :value_decimal
25
+ t.boolean :value_boolean
26
+ t.datetime :value_datetime
27
+
28
+ # Traditional record-keeping for the index:
14
29
  t.timestamps
15
30
  end
16
31
 
17
32
  # Index is important here:
18
- add_index :key_value_index, [:obj_type, :key_name, :value]
33
+ add_index :<%=table_name-%>, [:obj_type, :key_name]
19
34
  end
20
35
 
21
36
  def self.down
22
- drop_table :key_value_index
37
+ drop_table :<%=table_name%>
23
38
  end
24
- end
39
+ end
@@ -0,0 +1,139 @@
1
+ # AWEXOME LABS
2
+ # DoesKeyValue Test Suite : Column Storage
3
+
4
+ # Get started:
5
+ require File.expand_path(File.dirname(__FILE__)+"/../spec_helper")
6
+
7
+ # Create schema for sample User model for testing column-based storage:
8
+ ActiveRecord::Base.connection.drop_table(:users) if ActiveRecord::Base.connection.table_exists?(:users)
9
+ ActiveRecord::Base.connection.create_table(:users) do |t|
10
+ t.string :name
11
+ t.string :email
12
+ t.text :settings
13
+ t.timestamps
14
+ end
15
+
16
+ # Build the generator-style key value index table:
17
+ ActiveRecord::Base.connection.drop_table(:key_value_index) if ActiveRecord::Base.connection.table_exists?(:key_value_index)
18
+ ActiveRecord::Base.connection.create_table(:key_value_index) do |t|
19
+ t.string :obj_type
20
+ t.integer :obj_id
21
+ t.string :key_name
22
+ t.string :key_type
23
+ t.string :value_string
24
+ t.integer :value_integer
25
+ t.decimal :value_decimal
26
+ t.boolean :value_boolean
27
+ t.datetime :value_datetime
28
+ t.timestamps
29
+ end
30
+
31
+
32
+ # Define the sample User model which will exhibit column-based storage:
33
+ class User < ActiveRecord::Base
34
+ attr_accessible :name, :email, :settings
35
+ has_many :posts
36
+
37
+ # Key-Value storage:
38
+ does_keys :column=>"settings"
39
+ has_key :string_key
40
+ has_key :integer_key, :type=>:integer
41
+ has_key :decimal_key, :type=>:decimal
42
+ has_key :bool_key, :type=>:boolean
43
+ has_key :date_key, :type=>:datetime
44
+ has_key :default_val_key, :default=>"The Default"
45
+ has_key :indexless_key, :index=>false
46
+ end
47
+
48
+
49
+ # Test the ColumnStorage Module against the sample User class
50
+ describe "column_storage" do
51
+
52
+ before(:each) do
53
+ ActiveRecord::Base.connection.increment_open_transactions
54
+ ActiveRecord::Base.connection.begin_db_transaction
55
+ @user = User.new
56
+ end
57
+
58
+ after(:each) do
59
+ ActiveRecord::Base.connection.rollback_db_transaction
60
+ ActiveRecord::Base.connection.decrement_open_transactions
61
+ end
62
+
63
+ it "defines key read accessors" do
64
+ %w(string_key integer_key decimal_key bool_key date_key default_val_key indexless_key).each do |key_name|
65
+ @user.respond_to?( key_name.to_sym )
66
+ @user.methods.include?( key_name.to_sym ).should be_true
67
+ end
68
+ end
69
+
70
+ it "defines key write accessors" do
71
+ %w(string_key integer_key decimal_key bool_key date_key default_val_key indexless_key).each do |key_name|
72
+ @user.respond_to?( "#{key_name}=".to_sym )
73
+ @user.methods.include?( "#{key_name}=".to_sym ).should be_true
74
+ end
75
+ end
76
+
77
+ it "returns default value if none defined" do
78
+ @user.default_val_key.should == "The Default"
79
+ end
80
+
81
+ it "saves and returns the same value for keys" do
82
+ @user.string_key = "Ron Swanson"
83
+ @user.save
84
+ @user.reload
85
+ @user.string_key.should == "Ron Swanson"
86
+ end
87
+
88
+ it "sets a string value when assigned" do
89
+ @user.string_key = "Hello"
90
+ @user.string_key.should == "Hello"
91
+ end
92
+
93
+ it "sets an integer value when assigned" do
94
+ @user.integer_key = 123
95
+ @user.integer_key.should == 123
96
+ @user.integer_key.class.should == Fixnum
97
+ end
98
+
99
+ it "sets a decimal value when assigned" do
100
+ @user.decimal_key = 12.21
101
+ @user.decimal_key.should == 12.21
102
+ @user.decimal_key.class.should == Float
103
+ end
104
+
105
+ it "sets a boolean value when assigned" do
106
+ @user.bool_key = true
107
+ @user.bool_key.should be_true
108
+ @user.bool_key = false
109
+ @user.bool_key.should be_false
110
+ end
111
+
112
+ it "sets a datetime value when assigned" do
113
+ d0 = DateTime.now
114
+ @user.date_key = d0
115
+ @user.date_key.should == d0
116
+ @user.date_key.class.should == DateTime
117
+ end
118
+
119
+ it "defines with_ scope for indexed keys" do
120
+ %w(string_key integer_key decimal_key bool_key date_key default_val_key).each do |key_name|
121
+ User.methods.include?("with_#{key_name}".to_sym).should be_true
122
+ end
123
+ end
124
+
125
+ it "does not define with_ scope for non-indexed keys" do
126
+ User.methods.include?(:with_indexless_key).should be_false
127
+ end
128
+
129
+ it "finds objects via the scope and index" do
130
+ @user.string_key = "Champion"
131
+ @user.save
132
+ find_results = User.with_string_key("Champion")
133
+ find_results.length.should == 1
134
+ find_results.first.should == @user
135
+ end
136
+
137
+ end # column_stage
138
+
139
+
@@ -0,0 +1,140 @@
1
+ # AWEXOME LABS
2
+ # DoesKeyValue Test Suite : Table Storage
3
+
4
+ # Get started:
5
+ #require File.expand_path(File.dirname(__FILE__)+"/../spec_helper")
6
+
7
+ # Create schema for sample User model for testing column-based storage:
8
+ ActiveRecord::Base.connection.drop_table(:posts) if ActiveRecord::Base.connection.table_exists?(:posts)
9
+ ActiveRecord::Base.connection.create_table(:posts) do |t|
10
+ t.integer :user_id
11
+ t.string :title
12
+ t.text :body
13
+ t.timestamps
14
+ end
15
+
16
+ # Build the generator-style key value index table:
17
+ ActiveRecord::Base.connection.drop_table(:post_preferences) if ActiveRecord::Base.connection.table_exists?(:post_preferences)
18
+ ActiveRecord::Base.connection.create_table(:post_preferences) do |t|
19
+ t.string :obj_type
20
+ t.integer :obj_id
21
+ t.string :key_name
22
+ t.string :key_type
23
+ t.string :value_string
24
+ t.integer :value_integer
25
+ t.decimal :value_decimal
26
+ t.boolean :value_boolean
27
+ t.datetime :value_datetime
28
+ t.timestamps
29
+ end
30
+
31
+
32
+ # Define the sample Post model which will exhibit table-based storage:
33
+ class Post < ActiveRecord::Base
34
+ attr_accessible :user_id, :title, :body
35
+ belongs_to :user
36
+
37
+ # Key-Value storage:
38
+ does_keys :table=>"post_preferences"
39
+ has_key :string_key
40
+ has_key :integer_key, :type=>:integer
41
+ has_key :decimal_key, :type=>:decimal
42
+ has_key :bool_key, :type=>:boolean
43
+ has_key :date_key, :type=>:datetime
44
+ has_key :default_val_key, :default=>"The Default"
45
+ has_key :indexless_key, :index=>false
46
+ end
47
+
48
+
49
+ # Test the ColumnStorage Module against the sample User class
50
+ describe "table_storage" do
51
+
52
+ before(:each) do
53
+ ActiveRecord::Base.connection.increment_open_transactions
54
+ ActiveRecord::Base.connection.begin_db_transaction
55
+ @post = Post.new
56
+ @post.save
57
+ end
58
+
59
+ after(:each) do
60
+ ActiveRecord::Base.connection.rollback_db_transaction
61
+ ActiveRecord::Base.connection.decrement_open_transactions
62
+ end
63
+
64
+ it "defines key read accessors" do
65
+ %w(string_key integer_key decimal_key bool_key date_key default_val_key indexless_key).each do |key_name|
66
+ @post.respond_to?( key_name.to_sym )
67
+ @post.methods.include?( key_name.to_sym ).should be_true
68
+ end
69
+ end
70
+
71
+ it "defines key write accessors" do
72
+ %w(string_key integer_key decimal_key bool_key date_key default_val_key indexless_key).each do |key_name|
73
+ @post.respond_to?( "#{key_name}=".to_sym )
74
+ @post.methods.include?( "#{key_name}=".to_sym ).should be_true
75
+ end
76
+ end
77
+
78
+ it "returns default value if none defined" do
79
+ @post.default_val_key.should == "The Default"
80
+ end
81
+
82
+ it "saves and returns the same value for keys" do
83
+ @post.string_key = "Ron Swanson"
84
+ @post.save
85
+ @post.reload
86
+ @post.string_key.should == "Ron Swanson"
87
+ end
88
+
89
+ it "sets a string value when assigned" do
90
+ @post.string_key = "Hello"
91
+ @post.string_key.should == "Hello"
92
+ end
93
+
94
+ it "sets an integer value when assigned" do
95
+ @post.integer_key = 123
96
+ @post.integer_key.should == 123
97
+ @post.integer_key.class.should == Fixnum
98
+ end
99
+
100
+ it "sets a decimal value when assigned" do
101
+ @post.decimal_key = 12.21
102
+ @post.decimal_key.should == 12.21
103
+ @post.decimal_key.class.should == Float
104
+ end
105
+
106
+ it "sets a boolean value when assigned" do
107
+ @post.bool_key = true
108
+ @post.bool_key.should be_true
109
+ @post.bool_key = false
110
+ @post.bool_key.should be_false
111
+ end
112
+
113
+ it "sets a datetime value when assigned" do
114
+ d0 = DateTime.now
115
+ @post.date_key = d0
116
+ @post.date_key.should == d0
117
+ @post.date_key.class.should == DateTime
118
+ end
119
+
120
+ it "defines with_ scope for indexed keys" do
121
+ %w(string_key integer_key decimal_key bool_key date_key default_val_key).each do |key_name|
122
+ User.methods.include?("with_#{key_name}".to_sym).should be_true
123
+ end
124
+ end
125
+
126
+ it "does not define with_ scope for non-indexed keys" do
127
+ User.methods.include?(:with_indexless_key).should be_false
128
+ end
129
+
130
+ it "finds objects via the scope and index" do
131
+ @post.string_key = "Champion"
132
+ @post.save
133
+ find_results = User.with_string_key("Champion")
134
+ find_results.length.should == 1
135
+ find_results.first.should == @post
136
+ end
137
+
138
+ end # column_stage
139
+
140
+