code0-zero_track 0.0.0 → 0.0.2

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +0 -0
  3. data/README.md +48 -0
  4. data/Rakefile +0 -0
  5. data/lib/code0/zero_track/context.rb +133 -0
  6. data/lib/code0/zero_track/database/column_methods.rb +35 -0
  7. data/lib/code0/zero_track/database/migration.rb +26 -0
  8. data/lib/code0/zero_track/database/migration_helpers/add_column_enhancements.rb +41 -0
  9. data/lib/code0/zero_track/database/migration_helpers/constraint_helpers.rb +22 -0
  10. data/lib/code0/zero_track/database/migration_helpers/index_helpers.rb +18 -0
  11. data/lib/code0/zero_track/database/migration_helpers/table_enhancements.rb +78 -0
  12. data/lib/code0/zero_track/database/postgresql_adapter/dump_schema_versions_mixin.rb +27 -0
  13. data/lib/code0/zero_track/database/postgresql_database_tasks/load_schema_versions_mixin.rb +26 -0
  14. data/lib/code0/zero_track/database/schema_cleaner.rb +51 -0
  15. data/lib/code0/zero_track/database/schema_migrations/context.rb +48 -0
  16. data/lib/code0/zero_track/database/schema_migrations/migrations.rb +62 -0
  17. data/lib/code0/zero_track/database/schema_migrations.rb +32 -0
  18. data/lib/code0/zero_track/injectors/active_record_schema_migrations.rb +20 -0
  19. data/lib/code0/zero_track/injectors/active_record_timestamps.rb +21 -0
  20. data/lib/code0/zero_track/loggable.rb +48 -0
  21. data/lib/code0/zero_track/logs/json_formatter.rb +42 -0
  22. data/lib/code0/zero_track/memoize.rb +50 -0
  23. data/lib/code0/zero_track/railtie.rb +15 -0
  24. data/lib/code0/zero_track/version.rb +1 -1
  25. data/lib/code0/zero_track.rb +10 -2
  26. data/lib/rubocop/code0/zero_track/file_helpers.rb +25 -0
  27. data/lib/rubocop/cop/code0/zero_track/logs/rails_logger.rb +31 -0
  28. data/lib/rubocop/cop/code0/zero_track/migration/create_table_with_timestamps.rb +72 -0
  29. data/lib/rubocop/cop/code0/zero_track/migration/datetime.rb +53 -0
  30. data/lib/rubocop/cop/code0/zero_track/migration/timestamps.rb +38 -0
  31. data/lib/rubocop/cop/code0/zero_track/migration/versioned_class.rb +93 -0
  32. data/lib/rubocop/zero_track.rb +5 -0
  33. data/lib/tasks/code0/zero_track_tasks.rake +33 -4
  34. metadata +60 -7
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Injectors
6
+ class ActiveRecordSchemaMigrations
7
+ def self.inject!
8
+ # Patch to write version information as empty files under the db/schema_migrations directory
9
+ # This is intended to reduce potential for merge conflicts in db/structure.sql
10
+ ActiveSupport.on_load(:active_record_postgresqladapter) do
11
+ prepend Database::PostgresqlAdapter::DumpSchemaVersionsMixin
12
+ end
13
+ # Patch to load version information from empty files under the db/schema_migrations directory
14
+ ActiveRecord::Tasks::PostgreSQLDatabaseTasks
15
+ .prepend Database::PostgresqlDatabaseTasks::LoadSchemaVersionsMixin
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Injectors
6
+ class ActiveRecordTimestamps
7
+ def self.inject!
8
+ ActiveSupport.on_load(:active_record_postgresqladapter) do
9
+ self::NATIVE_DATABASE_TYPES[:datetime_with_timezone] = { name: 'timestamptz' }
10
+ end
11
+
12
+ ActiveSupport.on_load(:active_record) do
13
+ ActiveRecord::Base.time_zone_aware_types += [:datetime_with_timezone]
14
+ end
15
+
16
+ ActiveRecord::ConnectionAdapters::ColumnMethods.include ZeroTrack::Database::ColumnMethods::Timestamps
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Loggable
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def logger
10
+ Logger.new(Rails.logger, name || '<Anonymous>')
11
+ end
12
+ end
13
+
14
+ def logger
15
+ Logger.new(Rails.logger, self.class.name || '<Anonymous>')
16
+ end
17
+
18
+ class Logger
19
+ def initialize(log, clazz)
20
+ @log = log
21
+ @clazz = clazz
22
+ end
23
+
24
+ delegate :debug?, :info?, :warn?, :error?, :fatal?, :formatter, :level, to: :@log
25
+
26
+ def with_context(&block)
27
+ Code0::ZeroTrack::Context.with_context(class: @clazz, &block)
28
+ end
29
+
30
+ def debug(message)
31
+ with_context { @log.debug(message) }
32
+ end
33
+
34
+ def error(message)
35
+ with_context { @log.error(message) }
36
+ end
37
+
38
+ def warn(message)
39
+ with_context { @log.warn(message) }
40
+ end
41
+
42
+ def info(message)
43
+ with_context { @log.info(message) }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Logs
6
+ class JsonFormatter < ::Logger::Formatter
7
+ def call(severity, datetime, _progname, message)
8
+ JSON.generate(data(severity, datetime, message)) << "\n"
9
+ end
10
+
11
+ def data(severity, datetime, message)
12
+ data = {}
13
+ data[:severity] = severity
14
+ data[:time] = datetime.utc.iso8601(3)
15
+
16
+ case message
17
+ when String
18
+ data[:message] = chomp message
19
+ when Hash
20
+ data.merge!(message)
21
+ end
22
+
23
+ data.merge!(Code0::ZeroTrack::Context.current.to_h)
24
+ end
25
+
26
+ def chomp(message)
27
+ message.chomp! until message.chomp == message
28
+
29
+ message.strip
30
+ end
31
+
32
+ class Tagged < JsonFormatter
33
+ include ActiveSupport::TaggedLogging::Formatter
34
+
35
+ def tagged(*_args)
36
+ yield self # Ignore tags, they break the json layout as they are prepended to the log line
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Code0
4
+ module ZeroTrack
5
+ module Memoize
6
+ def memoize(name, reset_on_change: nil)
7
+ unless reset_on_change.nil?
8
+ reset_trigger = reset_on_change.call
9
+ reset_memoize = memoize("#{name}_reset_on_change") { reset_trigger }
10
+
11
+ if reset_trigger != reset_memoize
12
+ clear_memoize(name)
13
+ clear_memoize("#{name}_reset_on_change")
14
+ end
15
+ end
16
+
17
+ if memoized?(name)
18
+ instance_variable_get(ivar(name))
19
+ else
20
+ instance_variable_set(ivar(name), yield)
21
+ end
22
+ end
23
+
24
+ def memoized?(name)
25
+ instance_variable_defined?(ivar(name))
26
+ end
27
+
28
+ def clear_memoize(name)
29
+ clear_memoize!(name) if memoized?(name)
30
+ end
31
+
32
+ def clear_memoize!(name)
33
+ remove_instance_variable(ivar(name))
34
+ end
35
+
36
+ private
37
+
38
+ def ivar(name)
39
+ case name
40
+ when Symbol
41
+ name.to_s.prepend('@').to_sym
42
+ when String
43
+ :"@#{name}"
44
+ else
45
+ raise ArgumentError, "Invalid type of '#{name}'"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -3,6 +3,21 @@
3
3
  module Code0
4
4
  module ZeroTrack
5
5
  class Railtie < ::Rails::Railtie
6
+ config.zero_track = ActiveSupport::OrderedOptions.new
7
+ config.zero_track.active_record = ActiveSupport::OrderedOptions.new
8
+ config.zero_track.active_record.timestamps = false
9
+ config.zero_track.active_record.schema_migrations = false
10
+ config.zero_track.active_record.schema_cleaner = false
11
+
12
+ rake_tasks do
13
+ path = File.expand_path(__dir__)
14
+ Dir.glob("#{path}/../../tasks/**/*.rake").each { |f| load f }
15
+ end
16
+
17
+ config.after_initialize do
18
+ Injectors::ActiveRecordTimestamps.inject! if config.zero_track.active_record.timestamps
19
+ Injectors::ActiveRecordSchemaMigrations.inject! if config.zero_track.active_record.schema_migrations
20
+ end
6
21
  end
7
22
  end
8
23
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Code0
4
4
  module ZeroTrack
5
- VERSION = '0.0.0'
5
+ VERSION = '0.0.2'
6
6
  end
7
7
  end
@@ -1,10 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'code0/zero_track/version'
4
- require 'code0/zero_track/railtie'
3
+ require 'rails/railtie'
4
+
5
+ require 'zeitwerk'
6
+ loader = Zeitwerk::Loader.new
7
+ loader.tag = File.basename(__FILE__, '.rb')
8
+ loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
9
+ loader.push_dir(File.expand_path(File.join(__dir__, '..')))
10
+ loader.setup
5
11
 
6
12
  module Code0
7
13
  module ZeroTrack
8
14
  # Your code goes here...
9
15
  end
10
16
  end
17
+
18
+ Code0::ZeroTrack::Railtie # eager load the railtie
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Code0
5
+ module ZeroTrack
6
+ module FileHelpers
7
+ def dirname(node)
8
+ File.dirname(filepath(node))
9
+ end
10
+
11
+ def basename(node)
12
+ File.basename(filepath(node))
13
+ end
14
+
15
+ def filepath(node)
16
+ node.location.expression.source_buffer.name
17
+ end
18
+
19
+ def in_migration?(node)
20
+ dirname(node).end_with?('db/migrate')
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Code0
6
+ module ZeroTrack
7
+ module Logs
8
+ # Cop that checks if 'timestamps' method is called with timezone information.
9
+ class RailsLogger < RuboCop::Cop::Base
10
+ MSG = 'Do not use `Rails.logger` directly, include `Code0::ZeroTrack::Loggable` instead'
11
+ LOG_METHODS = %i[debug error fatal info warn].freeze
12
+ LOG_METHODS_PATTERN = LOG_METHODS.map(&:inspect).join(' ').freeze
13
+
14
+ def_node_matcher :rails_logger_log?, <<~PATTERN
15
+ (send
16
+ (send (const nil? :Rails) :logger)
17
+ {#{LOG_METHODS_PATTERN}} ...
18
+ )
19
+ PATTERN
20
+
21
+ def on_send(node)
22
+ return unless rails_logger_log?(node)
23
+
24
+ add_offense(node)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../code0/zero_track/file_helpers'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Code0
8
+ module ZeroTrack
9
+ module Migration
10
+ class CreateTableWithTimestamps < RuboCop::Cop::Base
11
+ include RuboCop::Code0::ZeroTrack::FileHelpers
12
+
13
+ MSG = 'Add timestamps when creating a new table.'
14
+ RESTRICT_ON_SEND = %i[create_table].freeze
15
+
16
+ def_node_matcher :create_table_with_timestamps_proc?, <<~PATTERN
17
+ (send nil? :create_table (sym _) ... (block-pass (sym :timestamps_with_timezone)))
18
+ PATTERN
19
+
20
+ def_node_search :timestamps_included?, <<~PATTERN
21
+ (send _var :timestamps_with_timezone ...)
22
+ PATTERN
23
+
24
+ def_node_search :created_at_included?, <<~PATTERN
25
+ (send _var :datetime_with_timezone
26
+ {(sym :created_at)(str "created_at")}
27
+ ...)
28
+ PATTERN
29
+
30
+ def_node_search :updated_at_included?, <<~PATTERN
31
+ (send _var :datetime_with_timezone
32
+ {(sym :updated_at)(str "updated_at")}
33
+ ...)
34
+ PATTERN
35
+
36
+ def_node_matcher :create_table_with_block?, <<~PATTERN
37
+ (block
38
+ (send nil? :create_table ...)
39
+ (args (arg _var)+)
40
+ _)
41
+ PATTERN
42
+
43
+ def on_send(node)
44
+ return unless in_migration?(node)
45
+ return unless node.command?(:create_table)
46
+
47
+ parent = node.parent
48
+
49
+ if create_table_with_block?(parent)
50
+ add_offense(parent) if parent.body.nil? || !time_columns_included?(parent.body)
51
+ elsif create_table_with_timestamps_proc?(node)
52
+ # nothing to do
53
+ else
54
+ add_offense(node)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def time_columns_included?(node)
61
+ timestamps_included?(node) || created_at_and_updated_at_included?(node)
62
+ end
63
+
64
+ def created_at_and_updated_at_included?(node)
65
+ created_at_included?(node) && updated_at_included?(node)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../code0/zero_track/file_helpers'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Code0
8
+ module ZeroTrack
9
+ module Migration
10
+ # Cop that checks if datetime data type is added with timezone information.
11
+ class Datetime < RuboCop::Cop::Base
12
+ include RuboCop::Code0::ZeroTrack::FileHelpers
13
+ extend AutoCorrector
14
+
15
+ MSG = 'Do not use the `%s` data type, use `datetime_with_timezone` instead'
16
+
17
+ # Check methods in table creation.
18
+ def on_def(node)
19
+ return unless in_migration?(node)
20
+
21
+ node.each_descendant(:send) do |send_node|
22
+ method_name = send_node.children[1]
23
+
24
+ next unless %i[datetime timestamp].include?(method_name)
25
+
26
+ add_offense(send_node.loc.selector, message: format(MSG, method_name)) do |corrector|
27
+ corrector.replace(send_node.loc.selector, 'datetime_with_timezone')
28
+ end
29
+ end
30
+ end
31
+
32
+ # Check methods.
33
+ def on_send(node)
34
+ return unless in_migration?(node)
35
+
36
+ node.each_descendant do |descendant|
37
+ next unless descendant.type == :sym
38
+
39
+ last_argument = descendant.children.last
40
+
41
+ next unless %i[datetime timestamp].include?(last_argument)
42
+
43
+ add_offense(descendant, message: format(MSG, last_argument)) do |corrector|
44
+ corrector.replace(descendant, ':datetime_with_timezone')
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../code0/zero_track/file_helpers'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Code0
8
+ module ZeroTrack
9
+ module Migration
10
+ # Cop that checks if 'timestamps' method is called with timezone information.
11
+ class Timestamps < RuboCop::Cop::Base
12
+ include RuboCop::Code0::ZeroTrack::FileHelpers
13
+ extend AutoCorrector
14
+
15
+ MSG = 'Do not use `timestamps`, use `timestamps_with_timezone` instead'
16
+
17
+ # Check methods in table creation.
18
+ def on_def(node)
19
+ return unless in_migration?(node)
20
+
21
+ node.each_descendant(:send) do |send_node|
22
+ next unless method_name(send_node) == :timestamps
23
+
24
+ add_offense(send_node.loc.selector) do |corrector|
25
+ corrector.replace(send_node.loc.selector, 'timestamps_with_timezone')
26
+ end
27
+ end
28
+ end
29
+
30
+ def method_name(node)
31
+ node.children[1]
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../code0/zero_track/file_helpers'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Code0
8
+ module ZeroTrack
9
+ module Migration
10
+ class VersionedClass < RuboCop::Cop::Base
11
+ include RuboCop::Code0::ZeroTrack::FileHelpers
12
+ extend AutoCorrector
13
+
14
+ MIGRATION_CLASS = 'Code0::ZeroTrack::Database::Migration'
15
+
16
+ # rubocop:disable Layout/LineLength
17
+ MSG_WRONG_BASE_CLASS = "Don't use `%<base_class>s`. Use `#{MIGRATION_CLASS}` instead.".freeze
18
+ MSG_WRONG_VERSION = "Don't use version `%<current_version>s` of `#{MIGRATION_CLASS}`. Use version `%<allowed_version>s` instead.".freeze
19
+ # rubocop:enable Layout/LineLength
20
+
21
+ def on_class(node)
22
+ return unless in_migration?(node)
23
+
24
+ return on_zerotrack_migration(node) if zerotrack_migration?(node)
25
+
26
+ add_offense(
27
+ node.parent_class,
28
+ message: format(MSG_WRONG_BASE_CLASS, base_class: superclass(node))
29
+ ) do |corrector|
30
+ corrector.replace(node.parent_class, "#{MIGRATION_CLASS}[#{find_allowed_versions(node).last}]")
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def on_zerotrack_migration(node)
37
+ return if cop_config['AllowedVersions'].nil? # allow all versions if nothing configured
38
+ return if correct_migration_version?(node)
39
+
40
+ current_version = get_migration_version(node)
41
+ allowed_version = find_allowed_versions(node).last
42
+
43
+ version_node = get_migration_version_node(node)
44
+
45
+ add_offense(
46
+ version_node,
47
+ message: format(MSG_WRONG_VERSION, current_version: current_version, allowed_version: allowed_version)
48
+ ) do |corrector|
49
+ corrector.replace(version_node, find_allowed_versions(node).last.to_s)
50
+ end
51
+ end
52
+
53
+ def zerotrack_migration?(node)
54
+ superclass(node) == MIGRATION_CLASS
55
+ end
56
+
57
+ def superclass(class_node)
58
+ _, *others = class_node.descendants
59
+
60
+ others.find { |node| node.const_type? && node.const_name != 'Types' }&.const_name
61
+ end
62
+
63
+ def correct_migration_version?(node)
64
+ find_allowed_versions(node).include?(get_migration_version(node))
65
+ end
66
+
67
+ def get_migration_version_node(node)
68
+ node.parent_class.arguments[0]
69
+ end
70
+
71
+ def get_migration_version(node)
72
+ get_migration_version_node(node).value
73
+ end
74
+
75
+ def find_allowed_versions(node)
76
+ migration_version = basename(node).split('_').first.to_i
77
+ allowed_versions.select do |range, _|
78
+ range.include?(migration_version)
79
+ end.values
80
+ end
81
+
82
+ def allowed_versions
83
+ cop_config['AllowedVersions'].transform_keys do |range|
84
+ range_ints = range.split('..').map(&:to_i)
85
+ range_ints[0]..range_ints[1]
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Lint/RedundantDirGlobSort
4
+ Dir[File.join(__dir__, 'cop', '**', '*.rb')].sort.each { |file| require file }
5
+ # rubocop:enable Lint/RedundantDirGlobSort
@@ -1,6 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # desc "Explaining what the task does"
4
- # task :code0_zero_track do
5
- # # Task goes here
6
- # end
3
+ namespace :code0 do
4
+ namespace :zero_track do
5
+ namespace :db do
6
+ desc 'This adjusts and cleans db/structure.sql - it runs after db:schema:dump'
7
+ task clean_structure_sql: :environment do |task_name|
8
+ # Allow this task to be called multiple times, as happens when running db:migrate:redo
9
+ Rake::Task[task_name].reenable
10
+
11
+ next unless Rails.application.config.zero_track.active_record.schema_cleaner
12
+
13
+ ActiveRecord::Base.configurations
14
+ .configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env)
15
+ .each do |db_config|
16
+ structure_file = ActiveRecord::Tasks::DatabaseTasks.schema_dump_path(db_config)
17
+
18
+ schema = File.read(structure_file)
19
+
20
+ File.open(structure_file, 'wb+') do |io|
21
+ Code0::ZeroTrack::Database::SchemaCleaner.new(schema).clean(io)
22
+ end
23
+ end
24
+ end
25
+
26
+ # Inform Rake that custom tasks should be run every time rake db:schema:dump is run
27
+ Rake::Task['db:schema:dump'].enhance do
28
+ Rake::Task['code0:zero_track:db:clean_structure_sql'].invoke
29
+ end
30
+ Rake::Task['db:prepare'].enhance do
31
+ Rake::Task['code0:zero_track:db:clean_structure_sql'].invoke
32
+ end
33
+ end
34
+ end
35
+ end