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.
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