active_schema 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.autotest +1 -0
  2. data/.document +11 -0
  3. data/.gitignore +48 -0
  4. data/.rspec +3 -0
  5. data/Gemfile +21 -0
  6. data/Gemfile.lock +63 -0
  7. data/LICENSE +20 -0
  8. data/README.textile +80 -0
  9. data/Rakefile +59 -0
  10. data/VERSION +1 -0
  11. data/autotest/discover.rb +1 -0
  12. data/lib/active_schema.rb +30 -0
  13. data/lib/active_schema/active_record/base.rb +46 -0
  14. data/lib/active_schema/associations/by_foreign_key.rb +50 -0
  15. data/lib/active_schema/associations/generator.rb +25 -0
  16. data/lib/active_schema/configuration.rb +20 -0
  17. data/lib/active_schema/feeder.rb +45 -0
  18. data/lib/active_schema/in_advance_feeder.rb +10 -0
  19. data/lib/active_schema/on_the_fly_feeder.rb +26 -0
  20. data/lib/active_schema/schema_feeder.rb +41 -0
  21. data/lib/active_schema/table.rb +30 -0
  22. data/lib/active_schema/table_hub.rb +42 -0
  23. data/lib/active_schema/validations/by_column.rb +41 -0
  24. data/lib/active_schema/validations/by_index.rb +5 -0
  25. data/lib/active_schema/validations/generator.rb +45 -0
  26. data/nbproject/project.properties +7 -0
  27. data/nbproject/project.xml +15 -0
  28. data/spec/.rspec +1 -0
  29. data/spec/active_schema/active_record/base_spec.rb +118 -0
  30. data/spec/active_schema/associations/by_foreign_key_spec.rb +73 -0
  31. data/spec/active_schema/associations/generator_spec.rb +5 -0
  32. data/spec/active_schema/in_advance_feeder_spec.rb +25 -0
  33. data/spec/active_schema/on_the_fly_feeder_spec.rb +34 -0
  34. data/spec/active_schema/schema_feeder_spec.rb +111 -0
  35. data/spec/active_schema/table_hub_spec.rb +70 -0
  36. data/spec/active_schema/table_spec.rb +13 -0
  37. data/spec/active_schema/validations/by_column_spec.rb +47 -0
  38. data/spec/active_schema/validations/by_index_spec.rb +15 -0
  39. data/spec/active_schema/validations/generator_spec.rb +23 -0
  40. data/spec/active_schema_spec.rb +14 -0
  41. data/spec/spec_helper.rb +31 -0
  42. data/spec/support/establish_connection.rb +8 -0
  43. data/spec/support/model_macros.rb +31 -0
  44. data/spec/support/test_models.rb +12 -0
  45. 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,10 @@
1
+ require 'active_schema/feeder'
2
+
3
+ module ActiveSchema
4
+ class InAdvanceFeeder < Feeder
5
+ def model_loaded(model)
6
+ add_model(model)
7
+ dispatch_attachments(model)
8
+ end
9
+ end
10
+ 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,5 @@
1
+ module ActiveSchema::Validations
2
+ class ByIndex < ValueGenerator
3
+
4
+ end
5
+ 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,7 @@
1
+ file.reference.active_schema-lib=lib
2
+ file.reference.active_schema-spec=spec
3
+ main.file=
4
+ platform.active=Ruby_0
5
+ source.encoding=UTF-8
6
+ src.dir=${file.reference.active_schema-lib}
7
+ test.src.dir=${file.reference.active_schema-spec}
@@ -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>
@@ -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