doeskeyvalue 0.2.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+