├── .github └── workflows │ └── rspec.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── changelog.md ├── config └── initializers │ └── db_validator.rb ├── db_validator.gemspec ├── lib ├── db_validator.rb ├── db_validator │ ├── cli.rb │ ├── config_updater.rb │ ├── configuration.rb │ ├── formatters │ │ ├── json_formatter.rb │ │ └── message_formatter.rb │ ├── railtie.rb │ ├── reporter.rb │ ├── test_task.rb │ ├── validate_task.rb │ ├── validator.rb │ └── version.rb ├── generators │ └── db_validator │ │ ├── install_generator.rb │ │ └── templates │ │ └── initializer.rb └── tasks │ └── db_validator_tasks.rake ├── readme.md └── spec ├── db_validator ├── formatters │ └── json_formatter_spec.rb ├── reporter_spec.rb └── validator_spec.rb ├── generators └── db_validator │ └── install_generator_spec.rb ├── integration └── db_validator_spec.rb ├── spec_helper.rb ├── support └── custom_helpers.rb └── tmp └── config └── initializers └── db_validator.rb /.github/workflows/rspec.yml: -------------------------------------------------------------------------------- 1 | name: RSpec Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | ruby-version: ["3.2"] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Ruby ${{ matrix.ruby-version }} 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby-version }} 23 | bundler-cache: true 24 | 25 | - name: Install dependencies 26 | run: | 27 | bundle config set --local path 'vendor/bundle' 28 | bundle config set --local deployment 'true' 29 | bundle install --jobs 4 30 | 31 | - name: Run tests 32 | run: bundle exec rspec 33 | 34 | - name: Upload test results 35 | if: always() 36 | uses: actions/upload-artifact@v3 37 | with: 38 | name: rspec-results-${{ matrix.ruby-version }} 39 | path: | 40 | .rspec_status 41 | coverage/ 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ruby/Rails specific 2 | *.gem 3 | *.rbc 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /spec/reports/ 9 | /spec/log/ 10 | /db_validator_reports/ 11 | /spec/examples.txt 12 | /test/tmp/ 13 | /test/version_tmp/ 14 | /tmp/ 15 | .rspec_status 16 | 17 | # Environment 18 | .env 19 | .env.* 20 | !.env.example 21 | 22 | # Documentation cache and generated files 23 | /.yardoc/ 24 | /_yardoc/ 25 | /doc/ 26 | /rdoc/ 27 | 28 | # Dependencies 29 | /.bundle/ 30 | /vendor/bundle 31 | /lib/bundler/man/ 32 | 33 | # Logs 34 | /log/* 35 | !/log/.keep 36 | *.log 37 | 38 | # Temporary files 39 | .byebug_history 40 | .dat* 41 | .repl_history 42 | *.bridgesupport 43 | build-iPhoneOS/ 44 | build-iPhoneSimulator/ 45 | 46 | # IDE specific files 47 | .idea/ 48 | .vscode/ 49 | *.swp 50 | *.swo 51 | *~ 52 | 53 | # OS generated files 54 | .DS_Store 55 | .DS_Store? 56 | ._* 57 | .Spotlight-V100 58 | .Trashes 59 | ehthumbs.db 60 | Thumbs.db 61 | 62 | # Database 63 | *.sqlite3 64 | *.sqlite3-journal 65 | 66 | # Node (if using webpacker or other JS tools) 67 | node_modules/ 68 | /public/packs 69 | /public/packs-test 70 | /public/assets 71 | /yarn-error.log 72 | yarn-debug.log* 73 | .yarn-integrity 74 | 75 | # Ignore development logs 76 | spec/log/development.log 77 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --format documentation 3 | --color 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rails 3 | - rubocop-rspec 4 | 5 | AllCops: 6 | NewCops: enable 7 | TargetRubyVersion: 2.7 8 | Exclude: 9 | - "bin/**/*" 10 | - "vendor/**/*" 11 | - "tmp/**/*" 12 | - "db/**/*" 13 | - "node_modules/**/*" 14 | 15 | Style/Documentation: 16 | Enabled: false 17 | 18 | Style/StringLiterals: 19 | EnforcedStyle: double_quotes 20 | 21 | Metrics/BlockLength: 22 | Exclude: 23 | - "spec/**/*" 24 | - "*.gemspec" 25 | 26 | Metrics/MethodLength: 27 | Max: 20 28 | 29 | Layout/LineLength: 30 | Max: 120 31 | 32 | Style/FrozenStringLiteralComment: 33 | Enabled: true 34 | EnforcedStyle: always 35 | 36 | Rails: 37 | Enabled: true 38 | 39 | Rails/Output: 40 | Enabled: false 41 | 42 | RSpec: 43 | Enabled: true 44 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.3 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "ruby-progressbar", "~> 1.11" 6 | 7 | gemspec 8 | 9 | group :development do 10 | gem "rspec", "~> 3.0" 11 | gem "rubocop", "~> 1.0" 12 | gem "rubocop-rails", "~> 2.0" 13 | gem "rubocop-rspec", "~> 2.0" 14 | gem "simplecov", require: false, group: :test 15 | gem "sqlite3" 16 | end 17 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | db_validator (1.0.1) 5 | rails (>= 5.2) 6 | ruby-progressbar (~> 1.11) 7 | tty-box (~> 0.7.0) 8 | tty-prompt (~> 0.23.1) 9 | tty-spinner (~> 0.9.3) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | actioncable (7.2.2) 15 | actionpack (= 7.2.2) 16 | activesupport (= 7.2.2) 17 | nio4r (~> 2.0) 18 | websocket-driver (>= 0.6.1) 19 | zeitwerk (~> 2.6) 20 | actionmailbox (7.2.2) 21 | actionpack (= 7.2.2) 22 | activejob (= 7.2.2) 23 | activerecord (= 7.2.2) 24 | activestorage (= 7.2.2) 25 | activesupport (= 7.2.2) 26 | mail (>= 2.8.0) 27 | actionmailer (7.2.2) 28 | actionpack (= 7.2.2) 29 | actionview (= 7.2.2) 30 | activejob (= 7.2.2) 31 | activesupport (= 7.2.2) 32 | mail (>= 2.8.0) 33 | rails-dom-testing (~> 2.2) 34 | actionpack (7.2.2) 35 | actionview (= 7.2.2) 36 | activesupport (= 7.2.2) 37 | nokogiri (>= 1.8.5) 38 | racc 39 | rack (>= 2.2.4, < 3.2) 40 | rack-session (>= 1.0.1) 41 | rack-test (>= 0.6.3) 42 | rails-dom-testing (~> 2.2) 43 | rails-html-sanitizer (~> 1.6) 44 | useragent (~> 0.16) 45 | actiontext (7.2.2) 46 | actionpack (= 7.2.2) 47 | activerecord (= 7.2.2) 48 | activestorage (= 7.2.2) 49 | activesupport (= 7.2.2) 50 | globalid (>= 0.6.0) 51 | nokogiri (>= 1.8.5) 52 | actionview (7.2.2) 53 | activesupport (= 7.2.2) 54 | builder (~> 3.1) 55 | erubi (~> 1.11) 56 | rails-dom-testing (~> 2.2) 57 | rails-html-sanitizer (~> 1.6) 58 | activejob (7.2.2) 59 | activesupport (= 7.2.2) 60 | globalid (>= 0.3.6) 61 | activemodel (7.2.2) 62 | activesupport (= 7.2.2) 63 | activerecord (7.2.2) 64 | activemodel (= 7.2.2) 65 | activesupport (= 7.2.2) 66 | timeout (>= 0.4.0) 67 | activestorage (7.2.2) 68 | actionpack (= 7.2.2) 69 | activejob (= 7.2.2) 70 | activerecord (= 7.2.2) 71 | activesupport (= 7.2.2) 72 | marcel (~> 1.0) 73 | activesupport (7.2.2) 74 | base64 75 | benchmark (>= 0.3) 76 | bigdecimal 77 | concurrent-ruby (~> 1.0, >= 1.3.1) 78 | connection_pool (>= 2.2.5) 79 | drb 80 | i18n (>= 1.6, < 2) 81 | logger (>= 1.4.2) 82 | minitest (>= 5.1) 83 | securerandom (>= 0.3) 84 | tzinfo (~> 2.0, >= 2.0.5) 85 | ast (2.4.2) 86 | base64 (0.2.0) 87 | benchmark (0.3.0) 88 | bigdecimal (3.1.8) 89 | builder (3.3.0) 90 | concurrent-ruby (1.3.4) 91 | connection_pool (2.4.1) 92 | crass (1.0.6) 93 | database_cleaner-active_record (2.2.0) 94 | activerecord (>= 5.a) 95 | database_cleaner-core (~> 2.0.0) 96 | database_cleaner-core (2.0.1) 97 | date (3.4.0) 98 | diff-lcs (1.5.1) 99 | docile (1.4.1) 100 | drb (2.2.1) 101 | erubi (1.13.0) 102 | globalid (1.2.1) 103 | activesupport (>= 6.1) 104 | i18n (1.14.6) 105 | concurrent-ruby (~> 1.0) 106 | io-console (0.7.2) 107 | irb (1.14.1) 108 | rdoc (>= 4.0.0) 109 | reline (>= 0.4.2) 110 | json (2.8.1) 111 | language_server-protocol (3.17.0.3) 112 | logger (1.6.1) 113 | loofah (2.23.1) 114 | crass (~> 1.0.2) 115 | nokogiri (>= 1.12.0) 116 | mail (2.8.1) 117 | mini_mime (>= 0.1.1) 118 | net-imap 119 | net-pop 120 | net-smtp 121 | marcel (1.0.4) 122 | mini_mime (1.1.5) 123 | minitest (5.25.1) 124 | net-imap (0.5.0) 125 | date 126 | net-protocol 127 | net-pop (0.1.2) 128 | net-protocol 129 | net-protocol (0.2.2) 130 | timeout 131 | net-smtp (0.5.0) 132 | net-protocol 133 | nio4r (2.7.4) 134 | nokogiri (1.16.7-aarch64-linux) 135 | racc (~> 1.4) 136 | nokogiri (1.16.7-arm-linux) 137 | racc (~> 1.4) 138 | nokogiri (1.16.7-arm64-darwin) 139 | racc (~> 1.4) 140 | nokogiri (1.16.7-x86-linux) 141 | racc (~> 1.4) 142 | nokogiri (1.16.7-x86_64-darwin) 143 | racc (~> 1.4) 144 | nokogiri (1.16.7-x86_64-linux) 145 | racc (~> 1.4) 146 | parallel (1.26.3) 147 | parser (3.3.6.0) 148 | ast (~> 2.4.1) 149 | racc 150 | pastel (0.8.0) 151 | tty-color (~> 0.5) 152 | psych (5.2.0) 153 | stringio 154 | racc (1.8.1) 155 | rack (3.1.8) 156 | rack-session (2.0.0) 157 | rack (>= 3.0.0) 158 | rack-test (2.1.0) 159 | rack (>= 1.3) 160 | rackup (2.2.0) 161 | rack (>= 3) 162 | rails (7.2.2) 163 | actioncable (= 7.2.2) 164 | actionmailbox (= 7.2.2) 165 | actionmailer (= 7.2.2) 166 | actionpack (= 7.2.2) 167 | actiontext (= 7.2.2) 168 | actionview (= 7.2.2) 169 | activejob (= 7.2.2) 170 | activemodel (= 7.2.2) 171 | activerecord (= 7.2.2) 172 | activestorage (= 7.2.2) 173 | activesupport (= 7.2.2) 174 | bundler (>= 1.15.0) 175 | railties (= 7.2.2) 176 | rails-dom-testing (2.2.0) 177 | activesupport (>= 5.0.0) 178 | minitest 179 | nokogiri (>= 1.6) 180 | rails-html-sanitizer (1.6.0) 181 | loofah (~> 2.21) 182 | nokogiri (~> 1.14) 183 | railties (7.2.2) 184 | actionpack (= 7.2.2) 185 | activesupport (= 7.2.2) 186 | irb (~> 1.13) 187 | rackup (>= 1.0.0) 188 | rake (>= 12.2) 189 | thor (~> 1.0, >= 1.2.2) 190 | zeitwerk (~> 2.6) 191 | rainbow (3.1.1) 192 | rake (13.2.1) 193 | rdoc (6.7.0) 194 | psych (>= 4.0.0) 195 | regexp_parser (2.9.2) 196 | reline (0.5.11) 197 | io-console (~> 0.5) 198 | rspec (3.13.0) 199 | rspec-core (~> 3.13.0) 200 | rspec-expectations (~> 3.13.0) 201 | rspec-mocks (~> 3.13.0) 202 | rspec-core (3.13.2) 203 | rspec-support (~> 3.13.0) 204 | rspec-expectations (3.13.3) 205 | diff-lcs (>= 1.2.0, < 2.0) 206 | rspec-support (~> 3.13.0) 207 | rspec-mocks (3.13.2) 208 | diff-lcs (>= 1.2.0, < 2.0) 209 | rspec-support (~> 3.13.0) 210 | rspec-rails (6.1.5) 211 | actionpack (>= 6.1) 212 | activesupport (>= 6.1) 213 | railties (>= 6.1) 214 | rspec-core (~> 3.13) 215 | rspec-expectations (~> 3.13) 216 | rspec-mocks (~> 3.13) 217 | rspec-support (~> 3.13) 218 | rspec-support (3.13.1) 219 | rubocop (1.68.0) 220 | json (~> 2.3) 221 | language_server-protocol (>= 3.17.0) 222 | parallel (~> 1.10) 223 | parser (>= 3.3.0.2) 224 | rainbow (>= 2.2.2, < 4.0) 225 | regexp_parser (>= 2.4, < 3.0) 226 | rubocop-ast (>= 1.32.2, < 2.0) 227 | ruby-progressbar (~> 1.7) 228 | unicode-display_width (>= 2.4.0, < 3.0) 229 | rubocop-ast (1.34.0) 230 | parser (>= 3.3.1.0) 231 | rubocop-capybara (2.21.0) 232 | rubocop (~> 1.41) 233 | rubocop-factory_bot (2.26.1) 234 | rubocop (~> 1.61) 235 | rubocop-rails (2.27.0) 236 | activesupport (>= 4.2.0) 237 | rack (>= 1.1) 238 | rubocop (>= 1.52.0, < 2.0) 239 | rubocop-ast (>= 1.31.1, < 2.0) 240 | rubocop-rspec (2.31.0) 241 | rubocop (~> 1.40) 242 | rubocop-capybara (~> 2.17) 243 | rubocop-factory_bot (~> 2.22) 244 | rubocop-rspec_rails (~> 2.28) 245 | rubocop-rspec_rails (2.29.1) 246 | rubocop (~> 1.61) 247 | ruby-progressbar (1.13.0) 248 | securerandom (0.3.1) 249 | simplecov (0.22.0) 250 | docile (~> 1.1) 251 | simplecov-html (~> 0.11) 252 | simplecov_json_formatter (~> 0.1) 253 | simplecov-html (0.13.1) 254 | simplecov_json_formatter (0.1.4) 255 | sqlite3 (1.7.3-aarch64-linux) 256 | sqlite3 (1.7.3-arm-linux) 257 | sqlite3 (1.7.3-arm64-darwin) 258 | sqlite3 (1.7.3-x86-linux) 259 | sqlite3 (1.7.3-x86_64-darwin) 260 | sqlite3 (1.7.3-x86_64-linux) 261 | stringio (3.1.2) 262 | strings (0.2.1) 263 | strings-ansi (~> 0.2) 264 | unicode-display_width (>= 1.5, < 3.0) 265 | unicode_utils (~> 1.4) 266 | strings-ansi (0.2.0) 267 | thor (1.3.2) 268 | timeout (0.4.2) 269 | tty-box (0.7.0) 270 | pastel (~> 0.8) 271 | strings (~> 0.2.0) 272 | tty-cursor (~> 0.7) 273 | tty-color (0.6.0) 274 | tty-cursor (0.7.1) 275 | tty-prompt (0.23.1) 276 | pastel (~> 0.8) 277 | tty-reader (~> 0.8) 278 | tty-reader (0.9.0) 279 | tty-cursor (~> 0.7) 280 | tty-screen (~> 0.8) 281 | wisper (~> 2.0) 282 | tty-screen (0.8.2) 283 | tty-spinner (0.9.3) 284 | tty-cursor (~> 0.7) 285 | tzinfo (2.0.6) 286 | concurrent-ruby (~> 1.0) 287 | unicode-display_width (2.6.0) 288 | unicode_utils (1.4.0) 289 | useragent (0.16.10) 290 | websocket-driver (0.7.6) 291 | websocket-extensions (>= 0.1.0) 292 | websocket-extensions (0.1.5) 293 | wisper (2.0.1) 294 | zeitwerk (2.7.1) 295 | 296 | PLATFORMS 297 | aarch64-linux 298 | aarch64-linux-gnu 299 | aarch64-linux-musl 300 | arm-linux 301 | arm-linux-gnu 302 | arm-linux-musl 303 | arm64-darwin 304 | x86-linux 305 | x86-linux-gnu 306 | x86-linux-musl 307 | x86_64-darwin 308 | x86_64-linux 309 | x86_64-linux-gnu 310 | x86_64-linux-musl 311 | 312 | DEPENDENCIES 313 | database_cleaner-active_record (~> 2.1) 314 | db_validator! 315 | rspec (~> 3.0) 316 | rspec-rails (~> 6.0) 317 | rubocop (~> 1.0) 318 | rubocop-rails (~> 2.0) 319 | rubocop-rspec (~> 2.0) 320 | ruby-progressbar (~> 1.11) 321 | simplecov 322 | sqlite3 323 | 324 | BUNDLED WITH 325 | 2.5.18 326 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.1] - 2024-12-13 9 | 10 | ### Fixed 11 | - Fix incorrect documentation url 12 | 13 | ## [1.0.0] - 2024-12-13 14 | 15 | ### Added 16 | - Add error message for enum validation 17 | 18 | ## [0.3.0] - 2024-11-09 19 | 20 | ### Added 21 | - Add test mode - `rake db_validator:test` 22 | - Add error count and grouping in JSON reports 23 | - Save JSON report to a file in the `db_validator_reports` directory 24 | 25 | ### Fixed 26 | - Skip validation of HABTM (Has And Belongs To Many) join tables 27 | 28 | ### Changed 29 | - Restructure JSON output format to include error counts and grouped records 30 | 31 | ## [0.2.0] - 2024-11-07 32 | 33 | ### Added 34 | - Enhanced interactive mode with improved UI 35 | 36 | ## [0.1.0] - 2024-11-07 37 | 38 | ### Added 39 | - Initial release 40 | -------------------------------------------------------------------------------- /config/initializers/db_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | DbValidator.configure do |config| 4 | config.only_models = %w[Documents Users] 5 | config.batch_size = 100 6 | config.limit = 500 7 | config.model_limits = { 8 | "documents" => 500, 9 | "users" => 1000 10 | } 11 | config.report_format = :json 12 | end 13 | -------------------------------------------------------------------------------- /db_validator.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/db_validator/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "db_validator" 7 | spec.version = DbValidator::VERSION 8 | spec.authors = ["Krzysztof Duda"] 9 | spec.email = ["duda_krzysztof@outlook.com"] 10 | 11 | spec.summary = "DbValidator helps identify invalid records in your Rails application that don't meet model validation requirements" 12 | spec.description = "DbValidator helps identify invalid records in your Rails application that don't meet model validation requirements. It finds records that became invalid after validation rule changes, and validates imported or manually edited data. You can use it to audit records before deploying new validations and catch any data that bypassed validation checks." 13 | spec.homepage = "https://github.com/krzysztoff1/db-validator" 14 | spec.license = "MIT" 15 | 16 | spec.required_ruby_version = ">= 2.7.0" 17 | 18 | spec.metadata = { 19 | "source_code_uri" => "https://github.com/krzysztoff1/db-validator/", 20 | "documentation_uri" => "https://github.com/krzysztoff1/db-validator/blob/main/readme.md", 21 | "changelog_uri" => "https://github.com/krzysztoff1/db-validator/blob/main/changelog.md", 22 | "rubygems_mfa_required" => "true" 23 | } 24 | 25 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 26 | 27 | spec.add_dependency "rails", ">= 5.2" 28 | spec.add_development_dependency "database_cleaner-active_record", "~> 2.1" 29 | spec.add_development_dependency "rspec-rails", "~> 6.0" 30 | spec.add_development_dependency "sqlite3", "~> 1.4" 31 | spec.add_dependency "ruby-progressbar", "~> 1.11" 32 | spec.add_dependency "tty-box", "~> 0.7.0" 33 | spec.add_dependency "tty-prompt", "~> 0.23.1" 34 | spec.add_dependency "tty-spinner", "~> 0.9.3" 35 | end 36 | -------------------------------------------------------------------------------- /lib/db_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails" 4 | require "db_validator/version" 5 | require "db_validator/configuration" 6 | require "db_validator/validator" 7 | require "db_validator/reporter" 8 | require "db_validator/cli" 9 | require "db_validator/config_updater" 10 | require "db_validator/test_task" 11 | require "db_validator/validate_task" 12 | 13 | module DbValidator 14 | class Error < StandardError; end 15 | 16 | class << self 17 | def validate(options = {}) 18 | validator = Validator.new(options) 19 | validator.validate_all 20 | end 21 | end 22 | end 23 | 24 | require "db_validator/railtie" if defined?(Rails) 25 | -------------------------------------------------------------------------------- /lib/db_validator/cli.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tty-prompt" 4 | require "tty-box" 5 | require "tty-spinner" 6 | require "optparse" 7 | require "logger" 8 | 9 | module DbValidator 10 | class CLI 11 | def initialize 12 | @prompt = TTY::Prompt.new 13 | @options = {} 14 | end 15 | 16 | def start 17 | if ARGV.empty? 18 | interactive_mode 19 | else 20 | parse_command_line_args 21 | validate_with_options 22 | end 23 | end 24 | 25 | def display_progress(message) 26 | spinner = TTY::Spinner.new("[:spinner] #{message}", format: :dots) 27 | spinner.auto_spin 28 | yield if block_given? 29 | spinner.success 30 | end 31 | 32 | def select_models(available_models) 33 | system "clear" 34 | display_header 35 | 36 | if available_models.empty? 37 | @prompt.error("No models found in the application.") 38 | exit 39 | end 40 | 41 | choices = available_models.map { |model| { name: model, value: model } } 42 | choices.unshift({ name: "All Models", value: "all" }) 43 | 44 | @prompt.say("\n") 45 | selected = @prompt.multi_select( 46 | "Select models to validate:", 47 | choices, 48 | per_page: 10, 49 | echo: false, 50 | show_help: :always, 51 | filter: true, 52 | cycle: true 53 | ) 54 | 55 | if selected.include?("all") 56 | available_models 57 | else 58 | selected 59 | end 60 | end 61 | 62 | def parse_command_line_args # rubocop:disable Metrics/AbcSize 63 | args = ARGV.join(" ").split(/\s+/) 64 | args.each do |arg| 65 | key, value = arg.split("=") 66 | case key 67 | when "models" 68 | @options[:only_models] = value.split(",").map(&:strip).map(&:classify) 69 | when "limit" 70 | @options[:limit] = value.to_i 71 | when "format" 72 | @options[:report_format] = value.to_sym 73 | when "show_records" 74 | @options[:show_records] = value.to_sym 75 | end 76 | end 77 | end 78 | 79 | def validate_with_options 80 | load_rails 81 | configure_validator(@options[:only_models], @options) 82 | validator = DbValidator::Validator.new 83 | report = validator.validate_all 84 | Rails.logger.debug { "\n#{report}" } 85 | end 86 | 87 | def interactive_mode 88 | load_rails 89 | display_header 90 | 91 | display_progress("Loading models") do 92 | Rails.application.eager_load! 93 | end 94 | 95 | available_models = ActiveRecord::Base.descendants 96 | .reject(&:abstract_class?) 97 | .select(&:table_exists?) 98 | .map(&:name) 99 | .sort 100 | 101 | if available_models.empty? 102 | Rails.logger.debug "No models found in the application." 103 | raise "No models found in the application. Please run this command from your Rails application root." 104 | end 105 | 106 | selected_models = select_models(available_models) 107 | options = configure_options 108 | 109 | configure_validator(selected_models, options) 110 | validator = DbValidator::Validator.new 111 | report = validator.validate_all 112 | Rails.logger.debug { "\n#{report}" } 113 | end 114 | 115 | def load_rails 116 | require File.expand_path("config/environment", Dir.pwd) 117 | rescue LoadError 118 | Rails.logger.debug "Error: Rails application not found. Please run this command from your Rails application root." 119 | raise "Rails application not found. Please run this command from your Rails application root." 120 | end 121 | 122 | def configure_validator(models = nil, options = {}) 123 | config = DbValidator.configuration 124 | config.only_models = models if models 125 | config.limit = options[:limit] if options[:limit] 126 | config.batch_size = options[:batch_size] if options[:batch_size] 127 | config.report_format = options[:format] if options[:format] 128 | config.show_records = options[:show_records] if options[:show_records] 129 | end 130 | 131 | def configure_options 132 | options = {} 133 | 134 | @prompt.say("\n") 135 | limit_input = @prompt.ask("Enter record limit (leave blank for no limit):") do |q| 136 | q.validate(/^\d*$/, "Please enter a valid number") 137 | q.convert(:int, nil) 138 | end 139 | options[:limit] = limit_input if limit_input.present? 140 | 141 | options[:format] = @prompt.select("Select report format:", %w[text json], default: "text") 142 | 143 | options 144 | end 145 | 146 | def display_header 147 | title = TTY::Box.frame( 148 | "DB Validator", 149 | "Interactive Model Validation", 150 | padding: 1, 151 | align: :center, 152 | border: :thick, 153 | style: { 154 | border: { 155 | fg: :cyan 156 | } 157 | } 158 | ) 159 | Rails.logger.debug title 160 | end 161 | end 162 | end 163 | -------------------------------------------------------------------------------- /lib/db_validator/config_updater.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DbValidator 4 | class ConfigUpdater 5 | def self.update_from_env 6 | new.update_from_env 7 | end 8 | 9 | def self.update_from_options(options) 10 | new.update_from_options(options) 11 | end 12 | 13 | def update_from_env 14 | update_config( 15 | limit: ENV["limit"]&.to_i, 16 | report_format: ENV["format"]&.to_sym, 17 | show_records: ENV["show_records"] != "false" 18 | ) 19 | end 20 | 21 | def update_from_options(options) 22 | update_config( 23 | limit: options[:limit], 24 | batch_size: options[:batch_size], 25 | report_format: options[:format]&.to_sym, 26 | show_records: options[:show_records] 27 | ) 28 | end 29 | 30 | private 31 | 32 | def update_config(settings) 33 | settings.each do |key, value| 34 | next unless value 35 | 36 | DbValidator.configuration.public_send("#{key}=", value) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/db_validator/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DbValidator 4 | class Configuration 5 | attr_accessor :only_models, :ignored_models, :ignored_attributes, :batch_size, :report_format, :limit, :show_records 6 | 7 | def initialize 8 | @only_models = [] 9 | @ignored_models = [] 10 | @ignored_attributes = {} 11 | @batch_size = 1000 12 | @report_format = :text 13 | @limit = nil 14 | @show_records = true 15 | end 16 | 17 | def only_models=(models) 18 | @only_models = models.map(&:downcase) 19 | end 20 | 21 | def ignored_models 22 | @ignored_models ||= [] 23 | end 24 | 25 | def ignored_models=(models) 26 | @ignored_models = models.map(&:downcase) 27 | end 28 | end 29 | 30 | class << self 31 | def configuration 32 | @configuration ||= Configuration.new 33 | end 34 | 35 | def configure 36 | yield(configuration) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/db_validator/formatters/json_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "json" 4 | require "fileutils" 5 | 6 | module DbValidator 7 | module Formatters 8 | class JsonFormatter 9 | def initialize(invalid_records) 10 | @invalid_records = invalid_records 11 | end 12 | 13 | def format 14 | formatted_data = @invalid_records.group_by { |r| r[:model] }.transform_values do |records| 15 | { 16 | error_count: records.length, 17 | records: records.map { |r| format_record(r) } 18 | } 19 | end 20 | 21 | save_to_file(formatted_data) 22 | formatted_data.to_json 23 | end 24 | 25 | private 26 | 27 | def format_record(record) 28 | { 29 | id: record[:id], 30 | errors: record[:errors] 31 | } 32 | end 33 | 34 | def save_to_file(data) 35 | FileUtils.mkdir_p("db_validator_reports") 36 | timestamp = Time.zone.now.strftime("%Y%m%d_%H%M%S") 37 | filename = "db_validator_reports/validation_report_#{timestamp}.json" 38 | 39 | File.write(filename, JSON.pretty_generate(data)) 40 | Rails.logger.info "JSON report saved to #{filename}" 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/db_validator/formatters/message_formatter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DbValidator 4 | module Formatters 5 | class MessageFormatter 6 | def initialize(record) 7 | @record = record 8 | end 9 | 10 | def format_error_message(error, field_value, message) 11 | return enum_validation_message(error, field_value, message) if error.options[:in].present? 12 | return enum_field_message(error, field_value, message) if enum_field?(error) 13 | 14 | basic_validation_message(error, field_value, message) 15 | end 16 | 17 | private 18 | 19 | attr_reader :record 20 | 21 | def enum_validation_message(error, field_value, message) 22 | allowed = error.options[:in].join(", ") 23 | error_message = "#{error.attribute} #{message}" 24 | details = " (allowed values: #{allowed}, actual value: #{field_value.inspect})" 25 | 26 | "#{error_message} #{details}" 27 | end 28 | 29 | def enum_field_message(error, field_value, message) 30 | enum_values = record.class.defined_enums[error.attribute.to_s].keys 31 | error_message = "#{error.attribute} #{message}" 32 | details = " (allowed values: #{enum_values.join(', ')}, actual value: #{field_value.inspect})" 33 | 34 | "#{error_message} #{details}" 35 | end 36 | 37 | def enum_field?(error) 38 | record.class.defined_enums[error.attribute.to_s].present? 39 | end 40 | 41 | def basic_validation_message(error, field_value, message) 42 | "#{error.attribute} #{message} (actual value: #{format_value(field_value)})" 43 | end 44 | 45 | def format_value(value) 46 | case value 47 | when true, false, Symbol 48 | value.to_s 49 | when String 50 | "\"#{value}\"" 51 | when nil 52 | "nil" 53 | else 54 | value 55 | end 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/db_validator/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DbValidator 4 | class Railtie < Rails::Railtie 5 | railtie_name :db_validator 6 | 7 | rake_tasks do 8 | load "tasks/db_validator_tasks.rake" 9 | end 10 | 11 | generators do 12 | require "generators/db_validator/install_generator" 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/db_validator/reporter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tty-box" 4 | require "tty-spinner" 5 | require "db_validator/formatters/json_formatter" 6 | require "db_validator/formatters/message_formatter" 7 | 8 | module DbValidator 9 | class Reporter 10 | def initialize 11 | @invalid_records = [] 12 | end 13 | 14 | def add_invalid_record(record) 15 | formatter = Formatters::MessageFormatter.new(record) 16 | enhanced_errors = record.errors.map do |error| 17 | field_value = record.send(error.attribute) 18 | message = error.message 19 | formatter.format_error_message(error, field_value, message) 20 | end 21 | 22 | @invalid_records << { 23 | model: record.class.name, 24 | id: record.id, 25 | errors: enhanced_errors 26 | } 27 | end 28 | 29 | def generate_report_message(error, field_value, message) 30 | formatter = Formatters::MessageFormatter.new(record) 31 | formatter.format_error_message(error, field_value, message) 32 | end 33 | 34 | def generate_report 35 | case DbValidator.configuration.report_format 36 | when :json 37 | Formatters::JsonFormatter.new(@invalid_records).format 38 | else 39 | generate_text_report 40 | end 41 | end 42 | 43 | private 44 | 45 | def generate_text_report 46 | print_title 47 | 48 | report = StringIO.new 49 | 50 | if @invalid_records.empty? 51 | report.puts "No invalid records found." 52 | return report.string 53 | end 54 | 55 | report.puts print_summary 56 | report.puts 57 | 58 | @invalid_records.group_by { |r| r[:model] }.each do |model, records| 59 | report.puts generate_model_report(model, records) 60 | end 61 | 62 | report.string 63 | end 64 | 65 | def print_summary 66 | report = StringIO.new 67 | is_plural = @invalid_records.count > 1 68 | record_word = is_plural ? "records" : "record" 69 | model_word = is_plural ? "models" : "model" 70 | 71 | report.puts "Found #{@invalid_records.count} invalid #{record_word} across #{@invalid_records.group_by do |r| 72 | r[:model] 73 | end.keys.count} #{model_word}" 74 | 75 | report.string 76 | end 77 | 78 | def generate_model_report(model, records) 79 | report = StringIO.new 80 | report.puts 81 | report.puts "#{model}: #{records.count} invalid #{records.count == 1 ? 'record' : 'records'}" 82 | report.puts 83 | 84 | records.each_with_index do |record, index| 85 | report.puts generate_record_report(record, index) 86 | end 87 | 88 | report.string 89 | end 90 | 91 | def generate_record_report(record, index) 92 | report = StringIO.new 93 | record_obj = fetch_record_object(record) 94 | info = collect_record_info(record_obj, record, index) 95 | 96 | report.puts " #{info.join(', ')}" 97 | add_error_messages(report, record[:errors]) 98 | 99 | report.string 100 | end 101 | 102 | def fetch_record_object(record) 103 | record[:model].constantize.find_by(id: record[:id]) 104 | end 105 | 106 | def collect_record_info(record_obj, record, index) 107 | info = [] 108 | info << "Record ##{index + 1}" 109 | info << "ID: #{record[:id]}" 110 | 111 | add_timestamp_info(info, record_obj) 112 | add_identifying_fields(info, record_obj) 113 | 114 | info 115 | end 116 | 117 | def add_timestamp_info(info, record_obj) 118 | if record_obj.respond_to?(:created_at) 119 | info << "Created: #{record_obj.created_at.strftime('%b %d, %Y at %I:%M %p')}" 120 | end 121 | return unless record_obj.respond_to?(:updated_at) 122 | 123 | info << "Updated: #{record_obj.updated_at.strftime('%b %d, %Y at %I:%M %p')}" 124 | end 125 | 126 | def add_identifying_fields(info, record_obj) 127 | info << "Name: #{record_obj.name}" if record_obj.respond_to?(:name) && record_obj.name.present? 128 | info << "Title: #{record_obj.title}" if record_obj.respond_to?(:title) && record_obj.title.present? 129 | end 130 | 131 | def add_error_messages(report, errors) 132 | errors.each do |error| 133 | report.puts " \e[31m- #{error}\e[0m" 134 | end 135 | end 136 | 137 | def print_title 138 | title_box = TTY::Box.frame( 139 | width: 50, 140 | align: :center, 141 | padding: [1, 2], 142 | title: { top_left: "DbValidator" }, 143 | style: { 144 | fg: :cyan, 145 | border: { 146 | fg: :cyan 147 | } 148 | } 149 | ) do 150 | "Database Validation Report" 151 | end 152 | 153 | puts 154 | puts title_box 155 | puts 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/db_validator/test_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DbValidator 4 | class TestTask 5 | def initialize(model_name, validation_rule) 6 | @model_name = model_name 7 | @validation_rule = validation_rule 8 | end 9 | 10 | def execute 11 | validate_and_test_model 12 | rescue NameError 13 | puts "Model '#{@model_name}' not found" 14 | raise "Model '#{@model_name}' not found" 15 | rescue SyntaxError 16 | puts "Invalid validation rule syntax" 17 | raise "Invalid validation rule syntax" 18 | ensure 19 | cleanup_temporary_model 20 | end 21 | 22 | private 23 | 24 | def validate_and_test_model 25 | base_model = @model_name.constantize 26 | validate_attribute(base_model) 27 | 28 | temp_model = create_temporary_model(base_model) 29 | Object.const_set("Temporary#{@model_name}", temp_model) 30 | 31 | validator = DbValidator::Validator.new 32 | report = validator.validate_test_model("Temporary#{@model_name}") 33 | puts report 34 | end 35 | 36 | def validate_attribute(base_model) 37 | attribute_match = @validation_rule.match(/validates\s+:(\w+)/) 38 | return unless attribute_match 39 | 40 | attribute_name = attribute_match[1] 41 | return if base_model.column_names.include?(attribute_name) || base_model.method_defined?(attribute_name) 42 | 43 | puts "Attribute '#{attribute_name}' does not exist for model '#{@model_name}'" 44 | raise "Attribute '#{attribute_name}' does not exist for model '#{@model_name}'" 45 | end 46 | 47 | def create_temporary_model(base_model) 48 | Class.new(base_model) do 49 | self.table_name = base_model.table_name 50 | class_eval(@validation_rule) 51 | end 52 | end 53 | 54 | def cleanup_temporary_model 55 | temp_const_name = "Temporary#{@model_name}" 56 | Object.send(:remove_const, temp_const_name) if Object.const_defined?(temp_const_name) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/db_validator/validate_task.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DbValidator 4 | class ValidateTask 5 | def initialize(cli = DbValidator::CLI.new) 6 | @cli = cli 7 | end 8 | 9 | def execute 10 | configure_from_env_or_cli 11 | run_validation 12 | end 13 | 14 | private 15 | 16 | def configure_from_env_or_cli 17 | if env_args_present? 18 | configure_from_env 19 | else 20 | configure_from_cli 21 | end 22 | end 23 | 24 | def env_args_present? 25 | ENV["models"].present? || ENV["limit"].present? || 26 | ENV["format"].present? || ENV["show_records"].present? 27 | end 28 | 29 | def configure_from_env 30 | if ENV["models"].present? 31 | models = ENV["models"].split(",").map(&:strip).map(&:classify) 32 | DbValidator.configuration.only_models = models 33 | end 34 | 35 | ConfigUpdater.update_from_env 36 | end 37 | 38 | def configure_from_cli 39 | @cli.display_progress("Loading models") { Rails.application.eager_load! } 40 | 41 | available_models = ActiveRecord::Base.descendants 42 | .reject(&:abstract_class?) 43 | .select(&:table_exists?) 44 | .map(&:name) 45 | .sort 46 | 47 | selected_models = @cli.select_models(available_models) 48 | options = @cli.configure_options 49 | 50 | DbValidator.configuration.only_models = selected_models 51 | ConfigUpdater.update_from_options(options) 52 | end 53 | 54 | def run_validation 55 | validator = DbValidator::Validator.new 56 | report = validator.validate_all 57 | puts report 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/db_validator/validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "ruby-progressbar" 4 | 5 | module DbValidator 6 | class Validator 7 | attr_reader :reporter 8 | 9 | def initialize(options = {}) 10 | configure_from_options(options) 11 | @reporter = Reporter.new 12 | end 13 | 14 | def validate_all 15 | models = models_to_validate 16 | invalid_count = 0 17 | 18 | models.each do |model| 19 | model_count = validate_model(model) 20 | invalid_count += model_count if model_count 21 | end 22 | 23 | if invalid_count.zero? 24 | Rails.logger.debug "\nValidation passed! All records are valid." 25 | else 26 | total_records = models.sum(&:count) 27 | Rails.logger.debug get_summary(total_records, invalid_count) 28 | end 29 | 30 | @reporter.generate_report 31 | end 32 | 33 | def get_summary(records_count, invalid_count) 34 | is_plural = invalid_count > 1 35 | records_word = is_plural ? "records" : "record" 36 | first_part = "\nFound #{invalid_count} invalid #{records_word} out of #{records_count} total #{records_word}." 37 | second_part = "\nValidation failed! Some records are invalid." if invalid_count.positive? 38 | 39 | "#{first_part} #{second_part}" 40 | end 41 | 42 | def validate_test_model(model_name) 43 | model = model_name.constantize 44 | scope = model.all 45 | scope = scope.limit(DbValidator.configuration.limit) if DbValidator.configuration.limit 46 | 47 | total_count = scope.count 48 | progress_bar = create_progress_bar("Testing #{model.name}", total_count) 49 | invalid_count = 0 50 | 51 | begin 52 | scope.find_each(batch_size: DbValidator.configuration.batch_size) do |record| 53 | invalid_count += 1 unless validate_record(record) 54 | progress_bar.increment 55 | end 56 | rescue StandardError => e 57 | Rails.logger.debug { "Error validating #{model.name}: #{e.message}" } 58 | end 59 | 60 | if invalid_count.zero? 61 | Rails.logger.debug "\nValidation rule passed! All records would be valid." 62 | else 63 | Rails.logger.debug do 64 | "\nFound #{invalid_count} records that would become invalid out of #{total_count} total records." 65 | end 66 | end 67 | 68 | @reporter.generate_report 69 | end 70 | 71 | private 72 | 73 | def configure_from_options(options) 74 | return unless options.is_a?(Hash) 75 | 76 | DbValidator.configuration.only_models = Array(options[:only_models]) if options[:only_models] 77 | DbValidator.configuration.limit = options[:limit] if options[:limit] 78 | DbValidator.configuration.batch_size = options[:batch_size] if options[:batch_size] 79 | DbValidator.configuration.report_format = options[:report_format] if options[:report_format] 80 | DbValidator.configuration.show_records = options[:show_records] if options[:show_records] 81 | end 82 | 83 | def find_all_models 84 | ObjectSpace.each_object(Class).select do |klass| 85 | klass < ActiveRecord::Base 86 | end 87 | end 88 | 89 | def should_validate_model?(model) 90 | return false if model.abstract_class? 91 | return false unless model.table_exists? 92 | 93 | config = DbValidator.configuration 94 | model_name = model.name.downcase 95 | 96 | if config.only_models.any? 97 | return config.only_models.map(&:downcase).include?(model_name) || 98 | config.only_models.map(&:downcase).include?(model_name.singularize) || 99 | config.only_models.map(&:downcase).include?(model_name.pluralize) 100 | end 101 | 102 | config.ignored_models.map(&:downcase).exclude?(model_name) 103 | end 104 | 105 | def validate_model(model) 106 | scope = build_scope(model) 107 | total_count = scope.count 108 | return 0 if total_count.zero? 109 | 110 | process_records(scope, model, total_count) 111 | end 112 | 113 | def build_scope(model) 114 | scope = model.all 115 | scope = scope.limit(DbValidator.configuration.limit) if DbValidator.configuration.limit 116 | scope 117 | end 118 | 119 | def process_records(scope, model, total_count) 120 | progress_bar = create_progress_bar(model.name, total_count) 121 | process_batches(scope, progress_bar, model) 122 | end 123 | 124 | def process_batches(scope, progress_bar, model) 125 | invalid_count = 0 126 | batch_size = DbValidator.configuration.batch_size || 100 127 | 128 | begin 129 | scope.find_in_batches(batch_size: batch_size) do |batch| 130 | invalid_count += process_batch(batch, progress_bar) 131 | end 132 | rescue StandardError => e 133 | Rails.logger.debug { "Error validating #{model.name}: #{e.message}" } 134 | end 135 | 136 | invalid_count 137 | end 138 | 139 | def process_batch(batch, progress_bar) 140 | invalid_count = 0 141 | batch.each do |record| 142 | invalid_count += 1 unless validate_record(record) 143 | progress_bar.increment 144 | end 145 | invalid_count 146 | end 147 | 148 | def create_progress_bar(model_name, total) 149 | ProgressBar.create( 150 | title: "Validating #{model_name}", 151 | total: total, 152 | format: "%t: |%B| %p%% %e", 153 | output: $stderr 154 | ) 155 | end 156 | 157 | def validate_record(record) 158 | return true if record.valid? 159 | 160 | @reporter.add_invalid_record(record) 161 | false 162 | end 163 | 164 | def models_to_validate 165 | models = find_all_models 166 | models.select { |model| should_validate_model?(model) } 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /lib/db_validator/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DbValidator 4 | VERSION = "1.0.1" 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/db_validator/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/generators" 4 | require "rails/generators/base" 5 | 6 | module DbValidator 7 | module Generators 8 | class InstallGenerator < Rails::Generators::Base 9 | source_root File.expand_path("templates", __dir__) 10 | 11 | def create_initializer 12 | template "initializer.rb", "config/initializers/db_validator.rb" 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/generators/db_validator/templates/initializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | DbValidator.configure do |config| 4 | # Specify specific models to validate 5 | config.only_models = %w[User Post] 6 | 7 | # Ignore specific models from validation 8 | # config.ignored_models = ["AdminUser"] 9 | 10 | # Ignore specific attributes for specific models 11 | # config.ignored_attributes = { 12 | # "User" => ["encrypted_password", "reset_password_token"], 13 | # "Post" => ["cached_votes"] 14 | # } 15 | 16 | # Set the report format (:text or :json) 17 | # config.report_format = :text 18 | 19 | # Show detailed record information in reports 20 | # config.show_records = true 21 | end 22 | -------------------------------------------------------------------------------- /lib/tasks/db_validator_tasks.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :db_validator do 4 | desc "Validate records in the database" 5 | task validate: :environment do 6 | DbValidator::ValidateTask.new.execute 7 | end 8 | 9 | desc "Test validation rules on existing records" 10 | task test: :environment do 11 | unless ENV["model"] && ENV["rule"] 12 | puts "Usage: rake db_validator:test model=user rule='validates :field, presence: true' [show_records=false] [limit=1000] [format=json]" 13 | raise "No models found in the application. Please run this command from your Rails application root." 14 | end 15 | 16 | model_name = ENV.fetch("model").classify 17 | validation_rule = ENV.fetch("rule", nil) 18 | 19 | DbValidator.configuration.show_records = ENV["show_records"] != "false" if ENV["show_records"].present? 20 | DbValidator.configuration.limit = ENV["limit"].to_i if ENV["limit"].present? 21 | DbValidator.configuration.report_format = ENV["format"].to_sym if ENV["format"].present? 22 | 23 | DbValidator::TestTask.new(model_name, validation_rule).execute 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/db_validator.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/db_validator) 2 | [![RSpec Tests](https://github.com/krzysztoff1/db-validator/actions/workflows/rspec.yml/badge.svg)](https://github.com/krzysztoff1/db-validator/actions/workflows/rspec.yml) 3 | 4 | # DbValidator 5 | 6 | DbValidator helps identify invalid records in your Rails application that don't meet model validation requirements. It finds records that became invalid after validation rule changes, and validates imported or manually edited data. You can use it to audit records before deploying new validations and catch any data that bypassed validation checks. 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'db_validator' 14 | ``` 15 | 16 | Then execute: 17 | 18 | ```bash 19 | $ bundle install 20 | ``` 21 | 22 | ## Usage 23 | 24 | ### Rake Task 25 | 26 | The simplest way to run validation is using the provided rake task: 27 | 28 | #### Validate models in interactive mode 29 | 30 | Screenshot 2024-11-07 at 21 50 57 31 | 32 | ```bash 33 | $ rake db_validator:validate 34 | ``` 35 | 36 | This will start an interactive mode where you can select which models to validate and adjust other options. 37 | 38 | #### Validate specific models 39 | 40 | ```bash 41 | $ rake db_validator:validate models=user,post 42 | ``` 43 | 44 | #### Limit the number of records to validate 45 | 46 | ```bash 47 | $ rake db_validator:validate limit=1000 48 | ``` 49 | 50 | #### Generate JSON report 51 | 52 | ```bash 53 | $ rake db_validator:validate format=json 54 | ``` 55 | 56 | ### Test Mode 57 | 58 | You can test new validation rules before applying them to your models: 59 | 60 | ```bash 61 | $ rake db_validator:test model=User rule='validates :name, presence: true' 62 | ``` 63 | 64 | #### Testing Email Format Validation 65 | 66 | Here's an example of testing email format validation rules: 67 | 68 | ```bash 69 | # Testing invalid email format (without @) 70 | $ rake db_validator:test model=User rule='validates :email, format: { without: /@/, message: "must contain @" }' 71 | 72 | Found 100 records that would become invalid out of 100 total records. 73 | 74 | # Testing valid email format (with @) 75 | $ rake db_validator:test model=User rule='validates :email, format: { with: /@/, message: "must contain @" }' 76 | 77 | No invalid records found. 78 | ``` 79 | 80 | #### Error Handling 81 | 82 | Trying to test a validation rule for a non-existent attribute will return an error: 83 | 84 | ``` 85 | ❌ Error: Attribute 'i_dont_exist' does not exist for model 'User' 86 | Available columns: id, email, created_at, updated_at, name 87 | ``` 88 | 89 | ### Ruby Code 90 | 91 | You can also run validation from your Ruby code: 92 | 93 | #### Validate all models 94 | 95 | ```ruby 96 | report = DbValidator.validate 97 | ``` 98 | 99 | #### Validate with options 100 | 101 | ```ruby 102 | report = DbValidator.validate( 103 | only_models: ['User', 'Post'], 104 | limit: 1000, 105 | report_format: :json 106 | ) 107 | ``` 108 | 109 | ## Report Format 110 | 111 | ### Text Format (Default) 112 | 113 | ``` 114 | DbValidator Report 115 | ================== 116 | Found invalid records: 117 | 118 | User: 2 invalid records 119 | ID: 1 120 | email is invalid (actual value: "invalid-email") 121 | ID: 2 122 | name can't be blank (actual value: "") 123 | 124 | Post: 1 invalid record 125 | ID: 5 126 | title can't be blank (actual value: "") 127 | category is not included in the list (allowed values: news, blog, actual value: "invalid") 128 | ``` 129 | 130 | ### JSON Format 131 | 132 | The JSON report is saved to a file in the `db_validator_reports` directory. 133 | 134 | ```json 135 | { 136 | "User": { 137 | "error_count": 2, 138 | "records": [ 139 | { 140 | "id": 1, 141 | "errors": [ 142 | "email is invalid (actual value: \"invalid-email\")" 143 | ] 144 | }, 145 | { 146 | "id": 2, 147 | "errors": [ 148 | "name can't be blank (actual value: \"\")" 149 | ] 150 | } 151 | ] 152 | } 153 | } 154 | ``` 155 | -------------------------------------------------------------------------------- /spec/db_validator/formatters/json_formatter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe DbValidator::Formatters::JsonFormatter do 6 | describe "#format" do 7 | let(:invalid_records) do 8 | [ 9 | { model: "Skill", id: 1, errors: ["name can't be blank"] }, 10 | { model: "Skill", id: 2, errors: ["name can't be blank"] }, 11 | { model: "User", id: 1, errors: ["email is invalid"] } 12 | ] 13 | end 14 | 15 | before do 16 | FileUtils.rm_rf("db_validator_reports") 17 | end 18 | 19 | after do 20 | FileUtils.rm_rf("db_validator_reports") 21 | end 22 | 23 | it "formats invalid records into grouped JSON with error counts" do 24 | formatter = described_class.new(invalid_records) 25 | result = JSON.parse(formatter.format) 26 | 27 | expect(result["Skill"]["error_count"]).to eq(2) 28 | expect(result["Skill"]["records"].length).to eq(2) 29 | expect(result["User"]["error_count"]).to eq(1) 30 | expect(result["User"]["records"].length).to eq(1) 31 | end 32 | 33 | it "saves the report to a file in the working directory" do 34 | formatter = described_class.new(invalid_records) 35 | formatter.format 36 | 37 | expect(Dir.exist?("db_validator_reports")).to be true 38 | 39 | report_files = Dir["db_validator_reports/validation_report_*.json"] 40 | expect(report_files).not_to be_empty 41 | 42 | file_content = JSON.parse(File.read(report_files.first)) 43 | expect(file_content["Skill"]["error_count"]).to eq(2) 44 | expect(file_content["User"]["error_count"]).to eq(1) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/db_validator/reporter_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe DbValidator::Reporter do 6 | let(:reporter) { described_class.new } 7 | 8 | describe "#add_invalid_record" do 9 | let(:invalid_product) do 10 | product = Product.new(name: "", product_type: "unknown") 11 | product.save(validate: false) 12 | product.valid? 13 | product 14 | end 15 | 16 | before do 17 | setup_test_table(:products) do |t| 18 | t.string :name 19 | t.string :product_type 20 | t.timestamps 21 | end 22 | 23 | create_test_model(:Product) do 24 | validates :name, presence: true 25 | validates :product_type, inclusion: { in: %w[physical digital service] } 26 | end 27 | end 28 | 29 | after do 30 | Product.delete_all 31 | CustomHelpers.remove_test_model(:Product) 32 | ActiveRecord::Base.connection.drop_table(:products) 33 | end 34 | 35 | context "when adding an invalid product" do 36 | let(:report) do 37 | reporter.add_invalid_record(invalid_product) 38 | strip_color_codes(reporter.generate_report) 39 | end 40 | 41 | it "shows the model name and invalid record count" do 42 | expect(report).to include("Product: 1 invalid record") 43 | end 44 | 45 | it "shows the name validation error" do 46 | expect(report).to include("name can't be blank (actual value: \"\")") 47 | end 48 | 49 | it "shows the product_type inclusion error" do 50 | expect(report).to include("product_type is not included in the list (actual value: \"unknown\")") 51 | end 52 | end 53 | end 54 | 55 | describe "#generate_report" do 56 | before do 57 | setup_test_table(:users) do |t| 58 | t.string :name 59 | t.timestamps 60 | end 61 | 62 | create_test_model(:User) do 63 | validates :name, presence: true 64 | end 65 | 66 | invalid_user = User.new(name: "") 67 | invalid_user.save(validate: false) 68 | invalid_user.valid? 69 | 70 | reporter.add_invalid_record(invalid_user) 71 | end 72 | 73 | after do 74 | User.delete_all 75 | CustomHelpers.remove_test_model(:User) 76 | ActiveRecord::Base.connection.drop_table(:users) 77 | end 78 | 79 | context "with text format" do 80 | before do 81 | allow(DbValidator.configuration).to receive(:report_format).and_return(:text) 82 | end 83 | 84 | it "shows number of invalid records and models" do 85 | report = reporter.generate_report 86 | clean_report = strip_color_codes(report) 87 | 88 | expect(clean_report).to include("Found 1 invalid record across 1 model") 89 | end 90 | 91 | it "shows number of invalid records in a model" do 92 | report = reporter.generate_report 93 | clean_report = strip_color_codes(report) 94 | 95 | expect(clean_report).to include("User: 1 invalid record") 96 | end 97 | 98 | it "shows error details" do 99 | report = reporter.generate_report 100 | clean_report = strip_color_codes(report) 101 | 102 | expect(clean_report).to include("name can't be blank (actual value: \"\")") 103 | end 104 | end 105 | 106 | context "with json format" do 107 | let(:invalid_skills) do 108 | skills = [ 109 | Skill.new(name: ""), 110 | Skill.new(name: nil), 111 | Skill.new(name: " ") 112 | ] 113 | 114 | skills.each do |skill| 115 | skill.save(validate: false) 116 | skill.valid? 117 | reporter.add_invalid_record(skill) 118 | end 119 | 120 | skills 121 | end 122 | 123 | before do 124 | allow(DbValidator.configuration).to receive(:report_format).and_return(:json) 125 | 126 | setup_test_table(:skills) do |t| 127 | t.string :name 128 | t.timestamps 129 | end 130 | 131 | create_test_model(:Skill) do 132 | validates :name, presence: true 133 | end 134 | 135 | invalid_skills 136 | end 137 | 138 | after do 139 | Skill.delete_all 140 | CustomHelpers.remove_test_model(:Skill) 141 | ActiveRecord::Base.connection.drop_table(:skills) 142 | end 143 | 144 | it "generates a JSON report with the correct structure" do 145 | report = reporter.generate_report 146 | parsed_report = JSON.parse(report) 147 | 148 | expect(parsed_report["Skill"]).to be_a(Hash) 149 | end 150 | 151 | it "includes the correct error count" do 152 | report = reporter.generate_report 153 | parsed_report = JSON.parse(report) 154 | 155 | expect(parsed_report["Skill"]["error_count"]).to eq(3) 156 | end 157 | 158 | it "includes an array of records" do 159 | report = reporter.generate_report 160 | parsed_report = JSON.parse(report) 161 | 162 | expect(parsed_report["Skill"]["records"].length).to eq(3) 163 | end 164 | 165 | it "includes the correct record details" do 166 | report = JSON.parse(reporter.generate_report) 167 | expect(report["Skill"]["records"].first).to include("id" => kind_of(Integer), 168 | "errors" => include(match(/name can't be blank/))) 169 | end 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /spec/db_validator/validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe DbValidator::Validator do 4 | let(:validator) { described_class.new } 5 | 6 | describe "#validate_all" do 7 | before do 8 | setup_test_table(:users) do |t| 9 | t.string :name 10 | t.string :email 11 | t.timestamps 12 | end 13 | 14 | create_test_model(:User) do 15 | validates :name, presence: true 16 | validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } 17 | end 18 | 19 | # Create exactly one valid and one invalid record 20 | User.create!(name: "Valid User", email: "valid@example.com") 21 | 22 | # Create only one invalid record 23 | User.connection.execute( 24 | "INSERT INTO users (name, email, created_at, updated_at) VALUES ('', 'invalid-email', datetime('now'), datetime('now'))" 25 | ) 26 | end 27 | 28 | after do 29 | ActiveRecord::Base.connection.drop_table(:users) if ActiveRecord::Base.connection.table_exists?(:users) 30 | CustomHelpers.remove_test_model(:User) if defined?(User) 31 | end 32 | 33 | it "reports name validation errors" do 34 | validator.validate_all 35 | report = validator.reporter.generate_report 36 | clean_report = strip_color_codes(report) 37 | 38 | expect(clean_report).to include("name can't be blank") 39 | end 40 | 41 | it "reports email validation errors" do 42 | validator.validate_all 43 | report = validator.reporter.generate_report 44 | clean_report = strip_color_codes(report) 45 | 46 | expect(clean_report).to include("email is invalid") 47 | end 48 | end 49 | 50 | describe "#validate_test_model" do 51 | before do 52 | setup_test_table(:users) do |t| 53 | t.string :name 54 | t.string :email 55 | t.timestamps 56 | end 57 | end 58 | 59 | after do 60 | ActiveRecord::Base.connection.drop_table(:users) if ActiveRecord::Base.connection.table_exists?(:users) 61 | end 62 | 63 | before do 64 | create_test_model(:User) 65 | 66 | 5.times do |i| 67 | user = User.new(name: "User #{i}", email: "user#{i}@example.com") 68 | user.save(validate: false) 69 | end 70 | end 71 | 72 | after do 73 | User.delete_all if defined?(User) 74 | CustomHelpers.remove_test_model(:User) 75 | DbValidator.configuration.limit = nil 76 | end 77 | 78 | context "with limit" do 79 | it "respects the record limit" do 80 | DbValidator.configuration.limit = 2 81 | 82 | temp_model = Class.new(User) do 83 | validates :email, presence: true 84 | end 85 | Object.const_set("TemporaryUser", temp_model) 86 | 87 | User.delete_all 88 | 89 | 3.times do |i| 90 | user = User.new(name: "No Email User #{i}") 91 | user.save(validate: false) 92 | end 93 | 94 | validator.validate_test_model("TemporaryUser") 95 | report = validator.reporter.generate_report 96 | clean_report = strip_color_codes(report) 97 | 98 | expect(clean_report).to include("Found 2 invalid records across 1 models") 99 | expect(clean_report).to include("TemporaryUser: 2 invalid records") 100 | expect(clean_report).to include("email can't be blank") 101 | ensure 102 | Object.send(:remove_const, "TemporaryUser") if Object.const_defined?("TemporaryUser") 103 | end 104 | end 105 | 106 | context "with email validation" do 107 | it "identifies invalid email formats" do 108 | User.delete_all 109 | 110 | 3.times do |i| 111 | user = User.new(name: "User #{i}", email: "invalid-email") 112 | user.save(validate: false) 113 | end 114 | 115 | temp_model = Class.new(User) do 116 | validates :email, format: { with: /@/, message: "must contain @" } 117 | end 118 | Object.const_set("TemporaryUser", temp_model) 119 | 120 | validator.validate_test_model("TemporaryUser") 121 | report = validator.reporter.generate_report 122 | clean_report = strip_color_codes(report) 123 | 124 | expect(clean_report).to include("Found 3 invalid records across 1 models") 125 | expect(clean_report).to include("TemporaryUser: 3 invalid records") 126 | expect(clean_report).to include("email must contain @") 127 | ensure 128 | Object.send(:remove_const, "TemporaryUser") if Object.const_defined?("TemporaryUser") 129 | end 130 | end 131 | 132 | context "with presence validation" do 133 | it "identifies records with missing required fields" do 134 | User.delete_all 135 | 136 | user = User.new(email: "test@example.com") 137 | user.save(validate: false) 138 | 139 | temp_model = Class.new(User) do 140 | validates :name, presence: true 141 | end 142 | Object.const_set("TemporaryUser", temp_model) 143 | 144 | validator.validate_test_model("TemporaryUser") 145 | report = validator.reporter.generate_report 146 | clean_report = strip_color_codes(report) 147 | 148 | expect(clean_report).to include("Found 1 invalid record across 1 model") 149 | expect(clean_report).to include("TemporaryUser: 1 invalid record") 150 | expect(clean_report).to include("name can't be blank") 151 | ensure 152 | Object.send(:remove_const, "TemporaryUser") if Object.const_defined?("TemporaryUser") 153 | end 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /spec/generators/db_validator/install_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | require "generators/db_validator/install_generator" 5 | require "rails/generators/test_case" 6 | require "rails/generators/testing/behavior" 7 | require "rails/generators/testing/setup_and_teardown" 8 | require "rails/generators/testing/assertions" 9 | require "fileutils" 10 | 11 | RSpec.describe DbValidator::Generators::InstallGenerator do 12 | include Rails::Generators::Testing::Behavior 13 | include Rails::Generators::Testing::SetupAndTeardown 14 | include FileUtils 15 | 16 | tests described_class 17 | destination File.expand_path("../../tmp", __dir__) 18 | 19 | before(:all) do 20 | prepare_destination 21 | end 22 | 23 | it "creates initializer file" do 24 | run_generator 25 | expect(File).to exist("#{destination_root}/config/initializers/db_validator.rb") 26 | end 27 | 28 | it "creates properly formatted initializer" do 29 | run_generator 30 | content = File.read("#{destination_root}/config/initializers/db_validator.rb") 31 | expect(content).to match(/DbValidator\.configure do \|config\|/) 32 | expect(content).to match(/# config\.ignored_models/) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/integration/db_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe DbValidator do 6 | before(:all) do 7 | ActiveRecord::Schema.define do 8 | create_table :test_users do |t| 9 | t.string :name 10 | t.string :email 11 | t.timestamps 12 | end 13 | end 14 | 15 | TestUser = Class.new(ApplicationRecord) do 16 | self.table_name = "test_users" 17 | 18 | validates :name, presence: true 19 | validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } 20 | end 21 | end 22 | 23 | after(:all) do 24 | TestUser.delete_all if Object.const_defined?(:TestUser) 25 | ensure 26 | Object.send(:remove_const, :TestUser) if Object.const_defined?(:TestUser) 27 | ActiveRecord::Base.connection.drop_table(:test_users) if ActiveRecord::Base.connection.table_exists?(:test_users) 28 | end 29 | 30 | let(:validator) { DbValidator::Validator.new } 31 | 32 | describe "full validation cycle" do 33 | before do 34 | TestUser.create!(name: "Valid User", email: "valid@example.com") 35 | invalid_user = TestUser.new(name: "", email: "invalid-email") 36 | invalid_user.save(validate: false) 37 | end 38 | 39 | after do 40 | TestUser.delete_all if Object.const_defined?(:TestUser) 41 | end 42 | 43 | it "identifies and reports invalid records" do 44 | validator.validate_all 45 | report = validator.reporter.generate_report 46 | clean_report = strip_color_codes(report) 47 | 48 | expect(clean_report).to include("Found 1 invalid record across 1 model") 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "rails" 5 | require "active_record" 6 | require "database_cleaner-active_record" 7 | require "simplecov" 8 | 9 | SimpleCov.start 10 | 11 | # Create a minimal Rails application for testing 12 | class TestApp < Rails::Application 13 | config.root = File.dirname(__FILE__) 14 | config.eager_load = false 15 | end 16 | Rails.application.initialize! 17 | 18 | require "db_validator" 19 | 20 | # Define ApplicationRecord for testing 21 | class ApplicationRecord < ActiveRecord::Base 22 | self.abstract_class = true 23 | end 24 | 25 | # Set up a test database 26 | ActiveRecord::Base.establish_connection( 27 | adapter: "sqlite3", 28 | database: ":memory:" 29 | ) 30 | 31 | # Load support files 32 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f } 33 | 34 | RSpec.configure do |config| 35 | # Enable flags like --only-failures and --next-failure 36 | config.example_status_persistence_file_path = ".rspec_status" 37 | 38 | # Disable RSpec exposing methods globally on `Module` and `main` 39 | config.disable_monkey_patching! 40 | 41 | config.expect_with :rspec do |c| 42 | c.syntax = :expect 43 | end 44 | 45 | # Configure DatabaseCleaner 46 | config.before(:suite) do 47 | DatabaseCleaner.clean_with(:truncation) 48 | DatabaseCleaner.strategy = :transaction 49 | end 50 | 51 | config.before do 52 | DatabaseCleaner.start 53 | end 54 | 55 | config.after do 56 | DatabaseCleaner.clean 57 | end 58 | end 59 | 60 | def strip_color_codes(text) 61 | text.gsub(/\e\[\d+(;\d+)*m/, "") 62 | end 63 | -------------------------------------------------------------------------------- /spec/support/custom_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_support/core_ext/string/inflections" 4 | 5 | module CustomHelpers 6 | def self.remove_test_model(name) 7 | model_name = name.to_s.camelize 8 | return unless Object.const_defined?(model_name) 9 | 10 | model_class = Object.const_get(model_name) 11 | model_class.reset_column_information if model_class.respond_to?(:reset_column_information) 12 | Object.send(:remove_const, model_name) 13 | rescue NameError 14 | # Ignore if the constant is already removed 15 | end 16 | 17 | def create_test_model(name, &block) 18 | model_name = name.to_s.camelize 19 | 20 | # Remove existing constant if defined 21 | CustomHelpers.remove_test_model(name) 22 | 23 | model_class = Class.new(ApplicationRecord) do 24 | self.table_name = name.to_s.downcase.pluralize 25 | class_eval(&block) if block_given? 26 | end 27 | 28 | Object.const_set(model_name, model_class) 29 | model_class 30 | end 31 | 32 | def setup_test_table(name, &block) 33 | table_name = name.to_s.downcase.pluralize 34 | ActiveRecord::Base.connection.drop_table(table_name) if ActiveRecord::Base.connection.table_exists?(table_name) 35 | 36 | ActiveRecord::Schema.define do 37 | create_table table_name, force: true do |t| 38 | block.call(t) 39 | end 40 | end 41 | end 42 | end 43 | 44 | RSpec.configure do |config| 45 | config.include CustomHelpers 46 | 47 | config.after do 48 | # Clean up any test models after each example 49 | %i[User Product TestUser].each do |model_name| 50 | CustomHelpers.remove_test_model(model_name) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/tmp/config/initializers/db_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | DbValidator.configure do |config| 4 | # Specify specific models to validate 5 | config.only_models = %w[User Post] 6 | 7 | # Ignore specific models from validation 8 | # config.ignored_models = ["AdminUser"] 9 | 10 | # Ignore specific attributes for specific models 11 | # config.ignored_attributes = { 12 | # "User" => ["encrypted_password", "reset_password_token"], 13 | # "Post" => ["cached_votes"] 14 | # } 15 | 16 | # Set the report format (:text or :json) 17 | # config.report_format = :text 18 | 19 | # Show detailed record information in reports 20 | # config.show_records = true 21 | end 22 | --------------------------------------------------------------------------------