schema_linter 0.0.1

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,83 @@
1
+ class SchemaLinter
2
+ VERSION = Gem.loaded_specs['schema_linter'].version.to_s
3
+
4
+ class Railtie < ::Rails::Railtie
5
+ rake_tasks do
6
+ load File.join(File.dirname(__FILE__), "tasks/schema_linter.rake")
7
+ end
8
+ end
9
+
10
+ def initialize
11
+ @error_table_names = (config["error_table_names"] || []).map { |v| Regexp.new(v) }
12
+ @error_column_names = (config["error_column_names"] || []).map { |v| Regexp.new(v) }
13
+ @ignore_table_names = (config["ignore_table_names"] || []).map { |v| Regexp.new(v) }
14
+ @ignore_column_names = (config["ignore_column_names"] || []).map { |v| Regexp.new(v) }
15
+ @rails_reserved_table_names = load_keywords("#{root_dir}/lib/rails_avoid_tables.txt")
16
+ @rails_reserved_column_names = load_keywords("#{root_dir}/lib/rails_avoid_columns.txt")
17
+ @postgresql_reserved_keywords = load_keywords("#{root_dir}/lib/postgresql_keywords.txt")
18
+ @mysql_reserved_keywords = load_keywords("#{root_dir}/lib/mysql_keywords.txt")
19
+ end
20
+
21
+ def error_table_names
22
+ table_names = models.map(&:table_name)
23
+ table_names.filter do |table_name|
24
+ table_name_has_errors?(table_name)
25
+ end
26
+ end
27
+
28
+ def error_column_names
29
+ models.each.with_object([]) do |model, error_columns|
30
+ column_names = model.columns.map(&:name)
31
+
32
+ column_names.each do |column_name|
33
+ if column_name_has_errors?(column_name)
34
+ error_columns << "#{model.table_name}.#{column_name}"
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def root_dir
43
+ File.expand_path('..', File.dirname(__FILE__))
44
+ end
45
+
46
+ def config_filename
47
+ %w[.schema_linter.yaml .schema_linter.yml].detect { |f| File.exist?(f) }
48
+ end
49
+
50
+ def config
51
+ @config ||= config_filename ? YAML.load_file(config_filename) : {}
52
+ end
53
+
54
+ def models
55
+ return @models if @models
56
+
57
+ @models ||= ActiveRecord::Base.descendants.reject(&:abstract_class)
58
+ end
59
+
60
+ def table_name_has_errors?(table_name)
61
+ name = table_name.downcase
62
+ return false if @ignore_table_names.any? { |regexp| name.match?(regexp) }
63
+
64
+ @error_table_names.any? { |regexp| name.match?(regexp) } ||
65
+ @mysql_reserved_keywords.include?(name) ||
66
+ @postgresql_reserved_keywords.include?(name) ||
67
+ @rails_reserved_table_names.include?(name)
68
+ end
69
+
70
+ def column_name_has_errors?(column_name)
71
+ name = column_name.downcase
72
+ return false if @ignore_column_names.any? { |regexp| name.match?(regexp) }
73
+
74
+ @error_column_names.any? { |regexp| name.match?(regexp) } ||
75
+ @mysql_reserved_keywords.include?(name) ||
76
+ @postgresql_reserved_keywords.include?(name) ||
77
+ @rails_reserved_column_names.include?(name)
78
+ end
79
+
80
+ def load_keywords(file_path)
81
+ File.readlines(file_path).map(&:strip).map(&:downcase)
82
+ end
83
+ end
@@ -0,0 +1,20 @@
1
+ desc 'Perform schema linting and output error table and column names. Raise an error if FAIL_ON_ERROR is set and there are errors.'
2
+ task schema_linter: :environment do
3
+ schema_linter = SchemaLinter.new
4
+
5
+ error_table_names = schema_linter.error_table_names
6
+ if error_table_names.present?
7
+ puts "Error table names (#{error_table_names.size}):"
8
+ error_table_names.each { |table_name| puts " #{table_name}" }
9
+ end
10
+
11
+ error_column_names = schema_linter.error_column_names
12
+ if error_column_names.present?
13
+ puts "Error column names (#{error_column_names.size}):"
14
+ error_column_names.each { |column_name| puts " #{column_name}" }
15
+ end
16
+
17
+ if ENV['FAIL_ON_ERROR'] && (error_table_names.present? || error_column_names.present?)
18
+ raise 'Error detected.'
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ $:.push File.expand_path('lib', __dir__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'schema_linter'
5
+ s.version = '0.0.1'
6
+ s.platform = Gem::Platform::RUBY
7
+ s.authors = ['Akira Kusumoto']
8
+ s.email = ['akirakusumo10@gmail.com']
9
+ s.homepage = 'https://github.com/bluerabbit/schema_linter'
10
+ s.summary = 'A linter for database schema naming conventions'
11
+ s.description = "The SchemaLinter gem ensures your database schema naming conventions are adhered to by checking both table names and column names against custom-defined rules in a YAML configuration file."
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
16
+ s.require_paths = ['lib']
17
+
18
+ s.licenses = ['MIT']
19
+
20
+ s.add_dependency 'rails', ['>= 6.0.0']
21
+ s.add_development_dependency 'mysql2'
22
+ s.add_development_dependency 'pry-byebug'
23
+ s.add_development_dependency 'rspec', '~> 3.9'
24
+ end
data/spec/app.rb ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/all'
4
+ require 'action_controller/railtie'
5
+
6
+ require 'schema_linter'
7
+
8
+ module DummyApp
9
+ class Application < Rails::Application
10
+ config.root = File.expand_path(__dir__)
11
+ end
12
+ end
13
+
14
+ ActiveRecord::Base.establish_connection(
15
+ YAML.safe_load(ERB.new(File.read('spec/database.yml')).result, aliases: true)['test']
16
+ )
17
+
18
+ ActiveRecord::Schema.define version: 0 do
19
+ create_table :users, force: true do |t|
20
+ t.string :name
21
+ t.string :role # This column is a reserved word in MySQL.
22
+ end
23
+
24
+ # This table name conflicts with a Rails class.
25
+ create_table :configurations, force: true do |t|
26
+ t.string :service_name
27
+ end
28
+ end
29
+
30
+ class User < ActiveRecord::Base
31
+ end
32
+
33
+ class Configuration < ActiveRecord::Base
34
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,18 @@
1
+ default: &default
2
+ adapter: mysql2
3
+ encoding: utf8mb4
4
+ charset: utf8mb4
5
+ collation: utf8mb4_general_ci
6
+ pool: 5
7
+ username: <%= ENV.fetch("DB_USER") { 'root' } %>
8
+ password: <%= ENV.fetch("DB_PASS") { '' } %>
9
+ host: <%= ENV.fetch("DB_HOST") { '127.0.0.1' } %>
10
+ socket: /tmp/mysql.sock
11
+
12
+ development:
13
+ <<: *default
14
+ database: schema_linter_development
15
+
16
+ test:
17
+ <<: *default
18
+ database: schema_linter_test
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe SchemaLinter do
6
+ it 'has a version number' do
7
+ expect(SchemaLinter::VERSION).not_to be nil
8
+ end
9
+
10
+ let(:schema_linter) { SchemaLinter.new }
11
+
12
+ after { File.delete '.schema_linter.yml' if File.exist? '.schema_linter.yml' }
13
+
14
+ describe '#error_table_names' do
15
+ it do
16
+ expect(schema_linter.error_table_names).to match_array(["configurations"])
17
+ end
18
+
19
+ context 'When a configuration file is present' do
20
+ before do
21
+ File.open '.schema_linter.yml', 'w' do |file|
22
+ file.puts 'error_table_names:'
23
+ file.puts ' - ^users$' # Treat the users table as an error
24
+ file.puts 'ignore_table_names:'
25
+ file.puts ' - ^configurations$' # Ignore the configurations table
26
+ end
27
+ end
28
+
29
+ it 'finds error table names.' do
30
+ expect(schema_linter.error_table_names).to match_array(%w[users])
31
+ end
32
+ end
33
+ end
34
+
35
+ describe '#error_column_names' do
36
+ it 'Database columns that use reserved keywords will result in errors.' do
37
+ expect(schema_linter.error_column_names).to match_array(["users.role"])
38
+ end
39
+
40
+ context 'When a configuration file is present' do
41
+ before do
42
+ File.open '.schema_linter.yml', 'w' do |file|
43
+ file.puts 'error_column_names:'
44
+ file.puts ' - ^name$' # Treat the name column as an error
45
+ file.puts 'ignore_column_names:'
46
+ file.puts ' - ^role$' # Ignore the role column
47
+ end
48
+ end
49
+
50
+ it 'finds error column names.' do
51
+ expect(schema_linter.error_column_names).to match_array(%w[users.name])
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
2
+ require 'pry'
3
+ require 'rails'
4
+ require 'schema_linter'
5
+ require_relative 'app'
6
+
7
+ RSpec.configure do |config|
8
+ config.around do |example|
9
+ ActiveRecord::Base.transaction do
10
+ example.run
11
+
12
+ raise ActiveRecord::Rollback
13
+ end
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: schema_linter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Akira Kusumoto
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-11-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 6.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: mysql2
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry-byebug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.9'
69
+ description: The SchemaLinter gem ensures your database schema naming conventions
70
+ are adhered to by checking both table names and column names against custom-defined
71
+ rules in a YAML configuration file.
72
+ email:
73
+ - akirakusumo10@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".circleci/config.yml"
79
+ - ".gitignore"
80
+ - Gemfile
81
+ - LICENSE
82
+ - README.md
83
+ - Rakefile
84
+ - lib/mysql_keywords.txt
85
+ - lib/postgresql_keywords.txt
86
+ - lib/rails_avoid_columns.txt
87
+ - lib/rails_avoid_tables.txt
88
+ - lib/schema_linter.rb
89
+ - lib/tasks/schema_linter.rake
90
+ - schema_linter.gemspec
91
+ - spec/app.rb
92
+ - spec/database.yml
93
+ - spec/schema_linter_spec.rb
94
+ - spec/spec_helper.rb
95
+ homepage: https://github.com/bluerabbit/schema_linter
96
+ licenses:
97
+ - MIT
98
+ metadata: {}
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.4.22
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: A linter for database schema naming conventions
118
+ test_files:
119
+ - spec/app.rb
120
+ - spec/database.yml
121
+ - spec/schema_linter_spec.rb
122
+ - spec/spec_helper.rb