active_schema 0.1.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/.autotest +1 -0
- data/.document +11 -0
- data/.gitignore +48 -0
- data/.rspec +3 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +63 -0
- data/LICENSE +20 -0
- data/README.textile +80 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/autotest/discover.rb +1 -0
- data/lib/active_schema.rb +30 -0
- data/lib/active_schema/active_record/base.rb +46 -0
- data/lib/active_schema/associations/by_foreign_key.rb +50 -0
- data/lib/active_schema/associations/generator.rb +25 -0
- data/lib/active_schema/configuration.rb +20 -0
- data/lib/active_schema/feeder.rb +45 -0
- data/lib/active_schema/in_advance_feeder.rb +10 -0
- data/lib/active_schema/on_the_fly_feeder.rb +26 -0
- data/lib/active_schema/schema_feeder.rb +41 -0
- data/lib/active_schema/table.rb +30 -0
- data/lib/active_schema/table_hub.rb +42 -0
- data/lib/active_schema/validations/by_column.rb +41 -0
- data/lib/active_schema/validations/by_index.rb +5 -0
- data/lib/active_schema/validations/generator.rb +45 -0
- data/nbproject/project.properties +7 -0
- data/nbproject/project.xml +15 -0
- data/spec/.rspec +1 -0
- data/spec/active_schema/active_record/base_spec.rb +118 -0
- data/spec/active_schema/associations/by_foreign_key_spec.rb +73 -0
- data/spec/active_schema/associations/generator_spec.rb +5 -0
- data/spec/active_schema/in_advance_feeder_spec.rb +25 -0
- data/spec/active_schema/on_the_fly_feeder_spec.rb +34 -0
- data/spec/active_schema/schema_feeder_spec.rb +111 -0
- data/spec/active_schema/table_hub_spec.rb +70 -0
- data/spec/active_schema/table_spec.rb +13 -0
- data/spec/active_schema/validations/by_column_spec.rb +47 -0
- data/spec/active_schema/validations/by_index_spec.rb +15 -0
- data/spec/active_schema/validations/generator_spec.rb +23 -0
- data/spec/active_schema_spec.rb +14 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/establish_connection.rb +8 -0
- data/spec/support/model_macros.rb +31 -0
- data/spec/support/test_models.rb +12 -0
- metadata +366 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
# To change this template, choose Tools | Templates
|
2
|
+
# and open the template in the editor.
|
3
|
+
|
4
|
+
module ActiveSchema
|
5
|
+
module Associations
|
6
|
+
class Generator
|
7
|
+
def initialize(from_table, to_table, column_name)
|
8
|
+
@from_table = from_table
|
9
|
+
@to_table = to_table
|
10
|
+
@column_name = column_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
[ByForwardForeignKey, ByReverseForeignKey].each do |generators|
|
15
|
+
generators.new(@from_table.model,
|
16
|
+
@to_table.model,
|
17
|
+
@column_name,
|
18
|
+
@from_table.unique_index_on?(@column_name)).generate
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveSchema
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :feeder,
|
4
|
+
:skip_validation_for_column,
|
5
|
+
:skip_model
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
self.feeder = OnTheFlyFeeder.new(self)
|
9
|
+
self.skip_validation_for_column = proc {|column|
|
10
|
+
column.name =~ /^(((created|updated)_(at|on))|position)$/
|
11
|
+
}
|
12
|
+
self.skip_model = proc {|model|
|
13
|
+
model == ::ActiveRecord::Base ||
|
14
|
+
(model.respond_to?(:abstract_class?) && model.abstract_class?)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ActiveSchema
|
2
|
+
class Feeder
|
3
|
+
attr_reader :associations_generator, :validations_generator, :table_hub
|
4
|
+
def initialize(configuration = ActiveSchema.configuration)
|
5
|
+
@table_hub = TableHub.new
|
6
|
+
@associations_generator = Associations::Generator
|
7
|
+
@validations_generator = Validations::Generator
|
8
|
+
@configuration = configuration
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_model(model)
|
12
|
+
table_hub.add_model(model)
|
13
|
+
end
|
14
|
+
|
15
|
+
def dispatch_attachments(model)
|
16
|
+
table = table_hub.tables[model.table_name]
|
17
|
+
generate_validations(table)
|
18
|
+
generate_assocations(table)
|
19
|
+
end
|
20
|
+
|
21
|
+
def generate_assocations(table)
|
22
|
+
table_hub.relations[table.model.table_name].select(&:model).each do |linked_table|
|
23
|
+
generate_associations_between(table, linked_table)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate_associations_between(table1, table2)
|
28
|
+
generate_directed_associations_between(table1, table2)
|
29
|
+
generate_directed_associations_between(table2, table1)
|
30
|
+
end
|
31
|
+
|
32
|
+
def generate_validations(table)
|
33
|
+
validations_generator.new(table, @configuration.skip_validation_for_column).generate
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_directed_associations_between(table1, table2)
|
37
|
+
table1.foreign_keys.each do |column, ref_table|
|
38
|
+
if ref_table == table2
|
39
|
+
associations_generator.new(table1, ref_table, column).generate
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'active_schema/feeder'
|
2
|
+
|
3
|
+
module ActiveSchema
|
4
|
+
class OnTheFlyFeeder < Feeder
|
5
|
+
def model_loaded(model)
|
6
|
+
add_model(model)
|
7
|
+
add_indexes(model)
|
8
|
+
add_foreign_keys(model)
|
9
|
+
dispatch_attachments(model)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def add_indexes(model)
|
14
|
+
model.connection.indexes(model.table_name).each do |index|
|
15
|
+
table_hub.add_index(model.table_name, index)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_foreign_keys(model)
|
20
|
+
model.connection.foreign_keys(model.table_name).each do |fk|
|
21
|
+
table_hub.add_foreign_key(fk.from_table, fk.to_table, fk.options[:column])
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'active_schema/in_advance_feeder'
|
2
|
+
|
3
|
+
module ActiveSchema
|
4
|
+
class FilteredSchemaReader
|
5
|
+
def initialize(schema_feeder, schema)
|
6
|
+
@schema_feeder = schema_feeder
|
7
|
+
@schema = schema
|
8
|
+
end
|
9
|
+
|
10
|
+
def filtered_lines
|
11
|
+
@schema.split.grep /^\s*(add_index|add_foreign_key)/
|
12
|
+
end
|
13
|
+
|
14
|
+
def evaluate
|
15
|
+
@schema_feeder.instance_eval(filtered_lines*"\n")
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
class SchemaFeeder < InAdvanceFeeder
|
21
|
+
def add_index(table_name, column_name, options = {})
|
22
|
+
index = ::ActiveRecord::ConnectionAdapters::IndexDefinition\
|
23
|
+
.new(table_name, options[:name], options[:unique], Array(column_name), options[:lengths])
|
24
|
+
table_hub.add_index(table_name, index)
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_foreign_key(from_table, to_table, options = {})
|
28
|
+
column = options[:column] ? options[:column] : "#{to_table.singularize}_id"
|
29
|
+
table_hub.add_foreign_key(from_table, to_table, column)
|
30
|
+
end
|
31
|
+
|
32
|
+
def read(schema)
|
33
|
+
FilteredSchemaReader.new(self, schema).evaluate
|
34
|
+
end
|
35
|
+
|
36
|
+
def feed
|
37
|
+
yield self
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# To change this template, choose Tools | Templates
|
2
|
+
# and open the template in the editor.
|
3
|
+
module ActiveSchema
|
4
|
+
class Table
|
5
|
+
attr_accessor :name, :model, :foreign_keys, :indexes
|
6
|
+
def initialize(name, model = nil)
|
7
|
+
@name = name
|
8
|
+
@model = model
|
9
|
+
@foreign_keys = {}
|
10
|
+
@indexes = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_foreign_key(column, dst_table)
|
14
|
+
@foreign_keys[column] = dst_table
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_index(index)
|
18
|
+
@indexes << index
|
19
|
+
end
|
20
|
+
|
21
|
+
def unique_index_on?(column)
|
22
|
+
@indexes.any? do |idx|
|
23
|
+
idx.columns.size == 1 &&
|
24
|
+
idx.columns.first == column &&
|
25
|
+
idx.unique
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module ActiveSchema
|
4
|
+
class TableHub
|
5
|
+
attr_accessor :tables, :relations
|
6
|
+
def initialize
|
7
|
+
@tables = {}
|
8
|
+
@relations = Hash.new {|h,k| h[k] = Set.new }
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_foreign_key(from_table_name, to_table_name, column_name)
|
12
|
+
from_table = find_or_create_table(from_table_name)
|
13
|
+
to_table = find_or_create_table(to_table_name)
|
14
|
+
|
15
|
+
add_relation(from_table, to_table)
|
16
|
+
from_table.add_foreign_key(column_name, to_table)
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_index(table_name, index_def)
|
20
|
+
find_or_create_table(table_name).add_index(index_def)
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_model(model)
|
24
|
+
find_or_create_table(model.table_name).model = model
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def add_relation(from_table, to_table)
|
29
|
+
@relations[from_table.name] << to_table
|
30
|
+
@relations[to_table.name] << from_table
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_or_create_table(table_name)
|
34
|
+
if @tables.has_key?(table_name)
|
35
|
+
@tables[table_name]
|
36
|
+
else
|
37
|
+
@tables[table_name] = Table.new(table_name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ActiveSchema::Validations
|
2
|
+
class ByColumn < ValueGenerator
|
3
|
+
def initialize(model, column)
|
4
|
+
super(model)
|
5
|
+
@column = column
|
6
|
+
end
|
7
|
+
|
8
|
+
def validation(name, opts = {})
|
9
|
+
super(name, @column.name.to_sym, opts)
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
class ByDataType < ByColumn
|
15
|
+
def generate
|
16
|
+
if @column.type == :integer
|
17
|
+
validation :validates_numericality_of, {:allow_nil => true, :only_integer => true}
|
18
|
+
elsif @column.number?
|
19
|
+
validation :validates_numericality_of, {:allow_nil => true}
|
20
|
+
elsif @column.text? && @column.limit
|
21
|
+
validation :validates_length_of, {:allow_nil => true, :maximum => @column.limit}
|
22
|
+
elsif @column.type == :enum
|
23
|
+
# Support MySQL ENUM type as provided by the enum_column plugin
|
24
|
+
validation :validates_inclusion_of, :in => @column.limit
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class ByNullability < ByColumn
|
30
|
+
def generate
|
31
|
+
if not @column.null
|
32
|
+
if @column.type == :boolean
|
33
|
+
validation :validates_inclusion_of, :in => [true, false]
|
34
|
+
else
|
35
|
+
validation :validates_presence_of
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module ActiveSchema::Validations
|
2
|
+
class ValueGenerator
|
3
|
+
def initialize(model)
|
4
|
+
@model = model
|
5
|
+
end
|
6
|
+
|
7
|
+
def validation(name, column, opts = {})
|
8
|
+
@model.send(name, column, opts)
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'active_schema/validations/by_column'
|
15
|
+
require 'active_schema/validations/by_index'
|
16
|
+
|
17
|
+
module ActiveSchema::Validations
|
18
|
+
class Generator
|
19
|
+
def initialize(table, skip_validation_for_column)
|
20
|
+
@table = table
|
21
|
+
@model = table.model
|
22
|
+
@skip_validation_for_column = skip_validation_for_column
|
23
|
+
end
|
24
|
+
|
25
|
+
def generate
|
26
|
+
generate_for_columns
|
27
|
+
generate_for_indexes
|
28
|
+
end
|
29
|
+
|
30
|
+
def generate_for_columns
|
31
|
+
@model.columns.each do |column|
|
32
|
+
next if @skip_validation_for_column.call(column)
|
33
|
+
ByDataType.new(@model, column).generate
|
34
|
+
ByNullability.new(@model, column).generate
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def generate_for_indexes
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<project xmlns="http://www.netbeans.org/ns/project/1">
|
3
|
+
<type>org.netbeans.modules.ruby.rubyproject</type>
|
4
|
+
<configuration>
|
5
|
+
<data xmlns="http://www.netbeans.org/ns/ruby-project/1">
|
6
|
+
<name>active_schema</name>
|
7
|
+
<source-roots>
|
8
|
+
<root id="src.dir"/>
|
9
|
+
</source-roots>
|
10
|
+
<test-roots>
|
11
|
+
<root id="test.src.dir"/>
|
12
|
+
</test-roots>
|
13
|
+
</data>
|
14
|
+
</configuration>
|
15
|
+
</project>
|
data/spec/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
require 'active_model/observing'
|
4
|
+
|
5
|
+
describe "Integration" do
|
6
|
+
before do
|
7
|
+
@base_class = Class.new()
|
8
|
+
@base_class.send(:include, ActiveModel::Observing)
|
9
|
+
@base_class.extend(ActiveSchema::ActiveRecord::ClassMethods)
|
10
|
+
@base_class.send(:include, ActiveSchema::ActiveRecord::InstanceMethods)
|
11
|
+
@feeder = mock("Feeder").as_null_object
|
12
|
+
@base_class.active_schema_configuration.stub!(:feeder).and_return(@feeder)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "responds to the active_schema method" do
|
16
|
+
@base_class.should respond_to(:active_schema)
|
17
|
+
end
|
18
|
+
|
19
|
+
context "activation" do
|
20
|
+
it "has a nil-valued class attribute active_schema_activated" do
|
21
|
+
@base_class.should respond_to(:active_schema_activated)
|
22
|
+
@base_class.active_schema_activated.should be_nil
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should not bubble up the activation" do
|
26
|
+
sub_class = Class.new(@base_class)
|
27
|
+
sub_class.active_schema
|
28
|
+
@base_class.active_schema_activated.should be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it "inherits activation in subclasses" do
|
32
|
+
sub_class = Class.new(@base_class)
|
33
|
+
sub_class.active_schema
|
34
|
+
below_sub_class = Class.new(sub_class)
|
35
|
+
below_sub_class.active_schema_activated.should be_true
|
36
|
+
end
|
37
|
+
|
38
|
+
it "does not overwrite activation or deactivation in subclasses" do
|
39
|
+
sub_class = Class.new(@base_class)
|
40
|
+
below_sub_class = Class.new(sub_class)
|
41
|
+
below_sub_class.active_schema_activated = false
|
42
|
+
sub_class.active_schema
|
43
|
+
below_sub_class.active_schema_activated.should be_false
|
44
|
+
end
|
45
|
+
|
46
|
+
it "allows independent subclass forks" do
|
47
|
+
sub_class1 = Class.new(@base_class)
|
48
|
+
sub_class2 = Class.new(@base_class)
|
49
|
+
sub_class1.active_schema
|
50
|
+
sub_class1.active_schema_activated.should be_true
|
51
|
+
sub_class2.active_schema_activated.should be_false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context "model loading" do
|
56
|
+
before do
|
57
|
+
@base_class.add_observer(ActiveRecord::ModelLoadedObserver.new)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "loads model in directly activated class" do
|
61
|
+
@feeder.should_receive(:model_loaded).with(instance_of(Class))
|
62
|
+
sub_class = Class.new(@base_class)
|
63
|
+
sub_class.active_schema
|
64
|
+
end
|
65
|
+
|
66
|
+
it "loads model in superclass activated class" do
|
67
|
+
@feeder.should_receive(:model_loaded).with(instance_of(Class)).twice
|
68
|
+
@base_class.active_schema
|
69
|
+
sub_class = Class.new(@base_class)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "observers" do
|
74
|
+
it "allows observers to be added" do
|
75
|
+
@base_class.should respond_to(:add_observer)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should call observers when subclassed" do
|
79
|
+
observer = mock("Observer")
|
80
|
+
observer.should_receive(:update).with(:observed_class_inherited, instance_of(Class))
|
81
|
+
@base_class.add_observer(observer)
|
82
|
+
Class.new(@base_class)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
describe ActiveRecord::Base do
|
89
|
+
before do
|
90
|
+
@prisoner_model = prisoner_model
|
91
|
+
@facility_model = facility_model
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should have the active_schema method" do
|
95
|
+
described_class.should respond_to(:active_schema)
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should have a correct Configuration object attached" do
|
99
|
+
@prisoner_model.active_schema_configuration.should be_instance_of(ActiveSchema::Configuration)
|
100
|
+
@prisoner_model.active_schema_configuration.feeder.should be_instance_of(ActiveSchema::OnTheFlyFeeder)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should activate model" do
|
104
|
+
@prisoner_model.active_schema
|
105
|
+
@prisoner_model.active_schema_activated.should be_true
|
106
|
+
end
|
107
|
+
|
108
|
+
it "adds (some) validations" do
|
109
|
+
@prisoner_model.active_schema
|
110
|
+
@prisoner_model.validators_on(:name).should_not be_empty
|
111
|
+
end
|
112
|
+
|
113
|
+
it "adds (some) assocations" do
|
114
|
+
@prisoner_model.active_schema
|
115
|
+
@facility_model.active_schema
|
116
|
+
@prisoner_model.new.should respond_to(:facility)
|
117
|
+
end
|
118
|
+
end
|