├── .dockerignore ├── .document ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── .rspec ├── .standard.yml ├── .standard_todo.yml ├── .yardopts ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── MIGRATION_GUIDE.md ├── README.md ├── Rakefile ├── VERSION ├── annotaterb.gemspec ├── bin ├── console └── setup ├── exe └── annotaterb ├── lib ├── annotate_rb.rb ├── annotate_rb │ ├── commands.rb │ ├── commands │ │ ├── annotate_models.rb │ │ ├── annotate_routes.rb │ │ ├── print_help.rb │ │ └── print_version.rb │ ├── config_finder.rb │ ├── config_generator.rb │ ├── config_loader.rb │ ├── core.rb │ ├── eager_loader.rb │ ├── helper.rb │ ├── model_annotator.rb │ ├── model_annotator │ │ ├── annotated_file.rb │ │ ├── annotated_file │ │ │ ├── generator.rb │ │ │ └── updater.rb │ │ ├── annotation.rb │ │ ├── annotation │ │ │ ├── annotation_builder.rb │ │ │ ├── main_header.rb │ │ │ ├── markdown_header.rb │ │ │ ├── schema_footer.rb │ │ │ └── schema_header.rb │ │ ├── annotation_decider.rb │ │ ├── annotation_diff.rb │ │ ├── annotation_diff_generator.rb │ │ ├── annotator.rb │ │ ├── bad_model_file_error.rb │ │ ├── check_constraint_annotation.rb │ │ ├── check_constraint_annotation │ │ │ ├── annotation.rb │ │ │ ├── annotation_builder.rb │ │ │ └── check_constraint_component.rb │ │ ├── column_annotation.rb │ │ ├── column_annotation │ │ │ ├── annotation_builder.rb │ │ │ ├── attributes_builder.rb │ │ │ ├── column_component.rb │ │ │ ├── column_wrapper.rb │ │ │ ├── default_value_builder.rb │ │ │ └── type_builder.rb │ │ ├── components.rb │ │ ├── file_name_resolver.rb │ │ ├── file_parser.rb │ │ ├── file_parser │ │ │ ├── annotation_finder.rb │ │ │ ├── custom_parser.rb │ │ │ ├── parsed_file.rb │ │ │ ├── parsed_file_result.rb │ │ │ └── yml_parser.rb │ │ ├── file_to_parser_mapper.rb │ │ ├── foreign_key_annotation.rb │ │ ├── foreign_key_annotation │ │ │ ├── annotation.rb │ │ │ ├── annotation_builder.rb │ │ │ ├── foreign_key_component.rb │ │ │ └── foreign_key_component_builder.rb │ │ ├── index_annotation.rb │ │ ├── index_annotation │ │ │ ├── annotation.rb │ │ │ ├── annotation_builder.rb │ │ │ └── index_component.rb │ │ ├── model_class_getter.rb │ │ ├── model_files_getter.rb │ │ ├── model_wrapper.rb │ │ ├── pattern_getter.rb │ │ ├── project_annotation_remover.rb │ │ ├── project_annotator.rb │ │ ├── related_files_list_builder.rb │ │ ├── single_file_annotation_remover.rb │ │ ├── single_file_annotator.rb │ │ ├── single_file_annotator_instruction.rb │ │ ├── single_file_remove_annotation_instruction.rb │ │ └── zeitwerk_class_getter.rb │ ├── options.rb │ ├── parser.rb │ ├── rake_bootstrapper.rb │ ├── route_annotator.rb │ ├── route_annotator │ │ ├── annotation_processor.rb │ │ ├── annotator.rb │ │ ├── base_processor.rb │ │ ├── header_generator.rb │ │ ├── helper.rb │ │ └── removal_processor.rb │ ├── runner.rb │ └── tasks │ │ └── annotate_models_migrate.rake ├── annotaterb.rb └── generators │ └── annotate_rb │ ├── config │ ├── USAGE │ └── config_generator.rb │ ├── hook │ ├── USAGE │ ├── hook_generator.rb │ └── templates │ │ └── annotate_rb.rake │ ├── install │ ├── USAGE │ └── install_generator.rb │ └── update_config │ ├── USAGE │ └── update_config_generator.rb ├── potato.md └── spec ├── dummyapp ├── .rbenv-gemsets ├── Gemfile ├── README.md ├── Rakefile ├── app │ ├── assets │ │ └── stylesheets │ │ │ └── application.css │ ├── controllers │ │ └── application_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── models │ │ ├── abstract_model.rb │ │ ├── application_record.rb │ │ ├── collapsed │ │ │ └── example │ │ │ │ └── test_model.rb │ │ ├── secondary │ │ │ └── test_default.rb │ │ ├── secondary_record.rb │ │ ├── test_child_default.rb │ │ ├── test_default.rb │ │ ├── test_null_false.rb │ │ ├── test_parent.rb │ │ ├── test_sibling_default.rb │ │ └── test_true_sti.rb │ └── views │ │ └── layouts │ │ └── application.html.erb ├── bin │ ├── bundle │ ├── rails │ ├── rake │ └── setup ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── credentials.yml.enc │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── content_security_policy.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── new_framework_defaults_7_1.rb │ │ ├── permissions_policy.rb │ │ └── test_zeitwork_collapsed.rb │ ├── locales │ │ └── en.yml │ ├── master.key │ ├── puma.rb │ └── routes.rb ├── db │ ├── migrate │ │ ├── 20230630123456_create_test_tables.rb │ │ ├── 20240210100506_create_collapsed_test_models.rb │ │ ├── 20240628051901_add_index_to_test_null_false.rb │ │ ├── 20240916190235_create_test_child_defaults.rb │ │ └── 20250825003411_create_test_parents.rb │ ├── secondary_migrate │ │ └── 20250324112726_create_test_defaults.rb │ ├── secondary_schema.rb │ └── seeds.rb ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ └── robots.txt ├── secondary_annotaterb_development └── test │ ├── fixtures │ ├── secondary │ │ └── test_defaults.yml │ └── test_defaults.yml │ └── test_helper.rb ├── integration ├── annotate_after_migration_spec.rb ├── annotate_collapsed_models_nested_position_spec.rb ├── annotate_collapsed_models_spec.rb ├── annotate_file_with_existing_annotations_spec.rb ├── annotate_model_with_foreign_key_spec.rb ├── annotate_models_in_multi_db_spec.rb ├── annotate_routes_spec.rb ├── annotate_single_file_spec.rb ├── annotate_sti_model_spec.rb ├── annotate_with_show_migration_option_spec.rb ├── cli_spec.rb ├── rails_generator_install_spec.rb └── rails_generator_update_config_spec.rb ├── integration_spec_helper.rb ├── lib ├── annotate_rb │ ├── annotate_models │ │ ├── annotate_models_annotating_a_file_frozen_spec.rb │ │ └── annotate_models_annotating_a_file_spec.rb │ ├── config_generator_spec.rb │ ├── config_loader_spec.rb │ ├── core_spec.rb │ ├── model_annotator │ │ ├── annotated_file │ │ │ ├── generator_spec.rb │ │ │ └── updater_spec.rb │ │ ├── annotating_a_file_with_comments_spec.rb │ │ ├── annotation │ │ │ ├── annotation_builder_spec.rb │ │ │ ├── main_header_spec.rb │ │ │ ├── markdown_header_spec.rb │ │ │ └── schema_header_spec.rb │ │ ├── annotation_decider_spec.rb │ │ ├── annotation_diff_generator_spec.rb │ │ ├── annotation_diff_spec.rb │ │ ├── check_constraint_annotation │ │ │ └── annotation_builder_spec.rb │ │ ├── column_annotation │ │ │ ├── annotation_builder_spec.rb │ │ │ ├── attributes_builder_spec.rb │ │ │ ├── column_wrapper_spec.rb │ │ │ ├── default_value_builder_spec.rb │ │ │ └── type_builder_spec.rb │ │ ├── file_name_resolver_spec.rb │ │ ├── file_parser │ │ │ ├── annotation_finder_spec.rb │ │ │ ├── custom_parser_spec.rb │ │ │ └── yml_parser_spec.rb │ │ ├── file_to_parser_mapper_spec.rb │ │ ├── foreign_key_annotation │ │ │ └── annotation_builder_spec.rb │ │ ├── index_annotation │ │ │ └── annotation_builder_spec.rb │ │ ├── model_class_getter_spec.rb │ │ ├── model_files_getter_spec.rb │ │ ├── model_wrapper_spec.rb │ │ ├── pattern_getter_spec.rb │ │ ├── related_files_list_builder_spec.rb │ │ ├── single_file_annotation_remover_spec.rb │ │ └── single_file_annotator_spec.rb │ ├── options_spec.rb │ ├── parser_spec.rb │ ├── route_annotator │ │ └── annotator_spec.rb │ └── runner_spec.rb └── tasks │ └── annotate_models_migrate_spec.rb ├── spec_helper.rb ├── support ├── annotate_test_constants.rb ├── annotate_test_helpers.rb ├── aruba.rb ├── custom_matchers.rb ├── database_contexts.rb └── shared_contexts.rb └── templates ├── migrations └── 20231013230731_add_int_field_to_test_defaults.rb ├── mysql2 ├── collapsed_test_model.rb ├── nested_position_collapsed_test_model.rb ├── test_child_default.rb ├── test_default.rb ├── test_default_updated.rb ├── test_default_with_bottom_annotations.rb ├── test_null_false.rb ├── test_parent.rb ├── test_sibling_default.rb └── test_true_sti.rb ├── pg ├── collapsed_test_model.rb ├── nested_position_collapsed_test_model.rb ├── test_child_default.rb ├── test_default.rb ├── test_default_updated.rb ├── test_default_with_bottom_annotations.rb ├── test_null_false.rb ├── test_parent.rb ├── test_sibling_default.rb └── test_true_sti.rb ├── routes.rb └── sqlite3 ├── collapsed_test_model.rb ├── nested_position_collapsed_test_model.rb ├── test_child_default.rb ├── test_default.rb ├── test_default_updated.rb ├── test_default_with_bottom_annotations.rb ├── test_null_false.rb ├── test_parent.rb ├── test_sibling_default.rb └── test_true_sti.rb /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | coverage 3 | -------------------------------------------------------------------------------- /.document: -------------------------------------------------------------------------------- 1 | spec/**/*.rb 2 | lib/**/*.rb 3 | - 4 | README.md 5 | CHANGELOG.md 6 | TODO.md 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Describe your problem here. 2 | 3 | ## Commands 4 | 5 | ``` 6 | $ show your commands here. 7 | ``` 8 | 9 | ## Version 10 | 11 | - annotaterb version 12 | - rails version 13 | - ruby version 14 | - database version 15 | - database adapter version (if available) 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: 5 | - '*' 6 | 7 | push: 8 | branches: 9 | - '*' 10 | 11 | schedule: 12 | - cron: '0 0 * * *' 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | ruby: ['3.0', '3.1', '3.2', '3.3', '3.4'] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v5 25 | 26 | - name: Setup Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | bundler-cache: true 31 | 32 | - name: Run Unit tests 33 | run: bundle exec rake spec:unit 34 | 35 | - name: Run Standard linter 36 | run: bundle exec standardrb 37 | 38 | integration: 39 | runs-on: ubuntu-latest 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | adapter: [ 'mysql2', 'pg', 'sqlite3' ] 44 | ruby: ['3.0', '3.1', '3.2', '3.3', '3.4'] 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v5 49 | 50 | - name: Setup Ruby 51 | uses: ruby/setup-ruby@v1 52 | with: 53 | ruby-version: ${{ matrix.ruby }} 54 | bundler-cache: true 55 | 56 | - name: Start MySQL 57 | if: ${{ matrix.adapter == 'mysql2' }} 58 | run: sudo systemctl start mysql.service 59 | 60 | - name: Start and setup Postgres 61 | if: ${{ matrix.adapter == 'pg' }} 62 | run: | 63 | sudo systemctl start postgresql.service 64 | sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'root'" 65 | # https://github.com/actions/runner-images/issues/7678 66 | 67 | - name: Install dummyapp dependencies (${{ matrix.adapter }}) 68 | run: bundle install 69 | working-directory: spec/dummyapp 70 | env: 71 | DATABASE_ADAPTER: ${{ matrix.adapter }} 72 | 73 | - name: Run dummyapp migrations (${{ matrix.adapter }}) 74 | run: bin/rails db:create 75 | working-directory: spec/dummyapp 76 | env: 77 | DATABASE_ADAPTER: ${{ matrix.adapter }} 78 | 79 | - name: Run Integration tests (${{ matrix.adapter }}) 80 | run: bundle exec rake spec:integration 81 | env: 82 | DATABASE_ADAPTER: ${{ matrix.adapter }} -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL Analysis 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | schedule: 8 | - cron: '0 0 * * MON' 9 | 10 | jobs: 11 | CodeQL-Build: 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | # required for all workflows 16 | security-events: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v5 21 | with: 22 | # We must fetch at least the immediate parents so that if this is 23 | # a pull request then we can checkout the head. 24 | fetch-depth: 2 25 | 26 | # If this run was triggered by a pull request event, then checkout 27 | # the head of the pull request instead of the merge commit. 28 | - run: git checkout HEAD^2 29 | if: ${{ github.event_name == 'pull_request' }} 30 | 31 | # Initializes the CodeQL tools for scanning. 32 | - name: Initialize CodeQL 33 | uses: github/codeql-action/init@v3 34 | with: 35 | languages: ruby 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | # - name: Autobuild 40 | # uses: github/codeql-action/autobuild@v2 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .DS_Store 3 | .bundle 4 | .gems 5 | .rbenv-version 6 | .ruby-* 7 | .idea/ 8 | /.rbx 9 | /.rvmrc 10 | /.yardoc/* 11 | /Gemfile.lock 12 | /coverage/* 13 | /dist 14 | /doc/* 15 | /pkg/* 16 | /spec/debug.log 17 | .byebug_history 18 | .rspec_status 19 | /spec/dummyapp/Gemfile.lock 20 | /spec/dummyapp/log/ 21 | /spec/dummyapp/db/schema.rb 22 | /spec/dummyapp/db/development.sqlite3 23 | /spec/dummyapp/tmp/ 24 | /tmp/* 25 | 26 | # Until we offer a devcontainer setup on this project, ignoring its folder to 27 | # allow individual developpers to create a local one. 28 | .devcontainer/ 29 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation 3 | --require spec_helper -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | parallel: true 2 | format: progress 3 | ruby_version: 3.0 4 | ignore: 5 | - 'dummyapp/**/*' 6 | - 'spec/dummyapp/**/*' 7 | - 'spec/templates/**/*' 8 | -------------------------------------------------------------------------------- /.standard_todo.yml: -------------------------------------------------------------------------------- 1 | # Auto generated files with errors to ignore. 2 | # Remove from this list as you refactor files. 3 | --- 4 | ignore: 5 | - lib/annotate_rb/model_annotator/annotation_builder.rb: 6 | - Lint/FormatParameterMismatch 7 | - lib/annotate_rb/model_annotator/column_annotation/annotation_builder.rb: 8 | - Lint/FormatParameterMismatch 9 | - lib/annotate_rb/model_annotator/foreign_key_annotation/annotation_builder.rb: 10 | - Lint/FormatParameterMismatch 11 | - lib/annotate_rb/model_annotator/index_annotation/annotation_builder.rb: 12 | - Lint/FormatParameterMismatch 13 | - lib/annotate_rb/model_annotator/model_class_getter.rb: 14 | - Style/SlicingWithRange 15 | - lib/annotate_rb/route_annotator/header_generator.rb: 16 | - Style/SlicingWithRange 17 | - spec/lib/annotate_rb/route_annotator/annotator_spec.rb: 18 | - Lint/ConstantDefinitionInBlock 19 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --no-private 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome. 4 | 5 | If you would like to help make AnnotateRb better, here are some ways to contribute: 6 | - by reporting any issues you come across while using the gem 7 | - helping other people who report issues 8 | - by refactoring any parts of the codebase 9 | 10 | Unsure about where to start but would like to help? Open an issue and let me know! 11 | 12 | ## How to develop 13 | 14 | Refer to the [DEVELOPMENT](DEVELOPMENT.md) file and if it does not answer your questions, feel free to open an issue and let me know. 15 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | After reading this file, you should have the necessary information to make changes to AnnotateRb. 4 | 5 | ## Context around testing 6 | 7 | AnnotateRb is a tool that annotates ActiveRecord model files with their model schema. At the time of writing, ActiveRecord has implementations for Postgres, SQLite3, MySQL, and Trilogy, although it should support other adapters. 8 | Databases and adapters can differ in their behaviors, so it's important to test run unit tests as well as integration tests with different adapters. 9 | 10 | An example of database adapter differences: when creating a model migration, SQLite represents the id field as a `:integer` and MySQL represents it as `:bigint`. 11 | 12 | ## What is `/spec/dummyapp`? 13 | 14 | `/spec/dummyapp` contains a Rails app that is used in integration tests. It can be used for testing locally as well. 15 | 16 | When running `bundle install` within the context of dummyapp, specifying `DATABASE_ADAPTER` is required, possible values at the time of writing are `mysql2, pg, sqlite3`. 17 | This environment variable is required when running the dummyapp. 18 | 19 | ## On testing 20 | 21 | AnnotateRb uses RSpec as a testing framework for unit tests. 22 | 23 | AnnotateRb uses RSpec + Aruba to run integration tests. 24 | 25 | I have found integration tests hard to write because we are testing a command line interface. As far as I'm aware, there aren't ways to easily debug it (i.e. add `binding.pry` or `binding.irb` statements) due to RSpec + Aruba. 26 | 27 | **If there is a better way to do this, please let me know.** 28 | 29 | ## Writing integration test 30 | 31 | Refer to git history for examples of previous commits. 32 | 33 | When I run into errors with newly written integration tests, I run the gem in the context of the dummyapp (spec/dummyapp) using `DATABASE_ADAPTER=sqlite3 bundle exec annotaterb models` with debug statements. 34 | 35 | ## Linter 36 | 37 | AnnotateRb uses [StandardRb](https://github.com/standardrb/standard). This is open to changing in the future, but was chosen early on to spend as little time on configuring Rubocop. 38 | 39 | ## Development flow 40 | **If you intend to run integration tests locally, you will need to install the dependencies for dummyapp and setup the respective databases before being able to run them.** 41 | 42 | 1. Fork the repo 43 | 2. Make necessary changes 44 | 3. Run unit tests: `bundle exec rake spec:unit` 45 | 4. optional: Run integration tests `DATABASE_ADAPTER=sqlite3 bundle exec rake spec:integration` (setup) 46 | 5. Run StandardRb (linter) `bundle exec standardrb`, optionally can fix files using command `bundle exec standardrb --fix` (note: this can and will make changes to files) 47 | 6. Submit a pull request 48 | 49 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec 6 | 7 | gem "rake" 8 | gem "bigdecimal" 9 | gem "mutex_m" 10 | 11 | group :development, :test do 12 | gem "aruba", "~> 2.1.0", require: false 13 | gem "byebug" 14 | gem "guard-rspec", require: false 15 | 16 | gem "rspec" 17 | 18 | gem "standard", "~> 1.29.0" 19 | gem "terminal-notifier-guard", require: false 20 | 21 | platforms :mri, :mingw do 22 | gem "pry", require: false 23 | gem "pry-byebug", require: false 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # NOTE: The cmd option is now required due to the increasing number of ways 2 | # rspec may be run, below are examples of the most common uses. 3 | # * bundler: 'bundle exec rspec' 4 | # * bundler binstubs: 'bin/rspec' 5 | # * spring: 'bin/rsspec' (This will use spring if running and you have 6 | # installed the spring binstubs per the docs) 7 | # * zeus: 'zeus rspec' (requires the server to be started separetly) 8 | # * 'just' rspec: 'rspec' 9 | guard :rspec, cmd: "bundle exec rspec" do 10 | watch(%r{^spec/.+_spec\.rb$}) 11 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 12 | watch("spec/spec_helper.rb") { "spec" } 13 | 14 | # Rails example 15 | watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 16 | watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 17 | watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 18 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 19 | watch("config/routes.rb") { "spec/routing" } 20 | watch("app/controllers/application_controller.rb") { "spec/controllers" } 21 | watch("spec/rails_helper.rb") { "spec" } 22 | 23 | # Capybara features specs 24 | watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" } 25 | 26 | # Turnip features and steps 27 | watch(%r{^spec/acceptance/(.+)\.feature$}) 28 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" } 29 | end 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | You can redistribute it and/or modify it under either the terms of the 2 | 2-clause BSDL (see the file BSDL), or the conditions below: 3 | 4 | 1. You may make and give away verbatim copies of the source form of the 5 | software without restriction, provided that you duplicate all of the 6 | original copyright notices and associated disclaimers. 7 | 8 | 2. You may modify your copy of the software in any way, provided that 9 | you do at least ONE of the following: 10 | 11 | a) place your modifications in the Public Domain or otherwise 12 | make them Freely Available, such as by posting said 13 | modifications to Usenet or an equivalent medium, or by allowing 14 | the author to include your modifications in the software. 15 | 16 | b) use the modified software only within your corporation or 17 | organization. 18 | 19 | c) give non-standard binaries non-standard names, with 20 | instructions on where to get the original software distribution. 21 | 22 | d) make other distribution arrangements with the author. 23 | 24 | 3. You may distribute the software in object code or binary form, 25 | provided that you do at least ONE of the following: 26 | 27 | a) distribute the binaries and library files of the software, 28 | together with instructions (in the manual page or equivalent) 29 | on where to get the original distribution. 30 | 31 | b) accompany the distribution with the machine-readable source of 32 | the software. 33 | 34 | c) give non-standard binaries non-standard names, with 35 | instructions on where to get the original software distribution. 36 | 37 | d) make other distribution arrangements with the author. 38 | 39 | 4. You may modify and include the part of the software into any other 40 | software (possibly commercial). But some files in the distribution 41 | are not written by the author, so that they are not under these terms. 42 | 43 | For the list of those files and their copying conditions, see the 44 | file LEGAL. 45 | 46 | 5. The scripts and library files supplied as input to or produced as 47 | output from the software do not automatically fall under the 48 | copyright of the software, but belong to whomever generated them, 49 | and may be sold commercially, and may be aggregated with this 50 | software. 51 | 52 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 53 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 54 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 55 | PURPOSE. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler" 4 | require "bundler/gem_helper" 5 | require "rspec/core/rake_task" 6 | 7 | namespace :spec do 8 | RSpec::Core::RakeTask.new(:unit) do |test| 9 | test.pattern = "spec/lib/**/*_spec.rb" 10 | end 11 | 12 | RSpec::Core::RakeTask.new(:integration) do |test| 13 | test.pattern = "spec/integration/**/*_spec.rb" 14 | end 15 | end 16 | 17 | task spec: ["spec:unit", "spec:integration"] 18 | 19 | task default: ["spec:unit"] 20 | 21 | base_dir = File.join(File.dirname(__FILE__)) 22 | helper = Bundler::GemHelper.new(base_dir) 23 | 24 | helper.install 25 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 4.19.0 2 | -------------------------------------------------------------------------------- /annotaterb.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "annotaterb" 3 | spec.version = File.read("VERSION").strip 4 | spec.authors = ["Andrew W. Lee"] 5 | spec.email = ["git@drewlee.com"] 6 | 7 | spec.summary = <<~SUMMARY.strip 8 | A gem for generating annotations for Rails projects. 9 | SUMMARY 10 | spec.description = <<~DESCRIPTION.strip 11 | Annotates Rails/ActiveRecord Models, routes, fixtures, and others based on the database schema. 12 | DESCRIPTION 13 | spec.homepage = "https://github.com/drwl/annotaterb" 14 | spec.license = "BSD-2-Clause" 15 | spec.required_ruby_version = ">= 3.0.0" 16 | 17 | spec.metadata["homepage_uri"] = spec.homepage 18 | spec.metadata["source_code_uri"] = "https://github.com/drwl/annotaterb" 19 | spec.metadata["changelog_uri"] = "https://github.com/drwl/annotaterb/blob/main/CHANGELOG.md" 20 | spec.metadata["bug_tracker_uri"] = "https://github.com/drwl/annotaterb/issues" 21 | spec.metadata["rubygems_mfa_required"] = "true" 22 | 23 | spec.files = Dir["VERSION", "CHANGELOG.md", "LICENSE.txt", "README.md", "lib/**/*"] 24 | spec.bindir = "exe" 25 | spec.executables = Dir["exe/*"].map { |exe| File.basename(exe) } 26 | spec.require_paths = ["lib"] 27 | 28 | spec.add_dependency "activerecord", ">= 6.0.0" 29 | spec.add_dependency "activesupport", ">= 6.0.0" 30 | end 31 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "annotate_rb" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /exe/annotaterb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | if !File.exist?("./Rakefile") && !File.exist?("./Gemfile") 5 | abort "Please run annotaterb from the root of the project." 6 | end 7 | 8 | begin 9 | require "bundler" 10 | Bundler.setup 11 | rescue 12 | end 13 | 14 | $LOAD_PATH.unshift("#{__dir__}/../lib") 15 | 16 | require "annotate_rb" 17 | 18 | _exit_status = ::AnnotateRb::Runner.run(ARGV) 19 | 20 | # TODO: Return exit status 21 | # exit exit_status 22 | -------------------------------------------------------------------------------- /lib/annotate_rb.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "active_record" 4 | require "active_support" 5 | 6 | # Helper.fallback depends on this being required because it adds #present? to nil 7 | require "active_support/core_ext/object/blank" 8 | require "active_support/core_ext/class/subclasses" 9 | require "active_support/core_ext/string/inflections" 10 | 11 | require "rake" 12 | 13 | require_relative "annotate_rb/helper" 14 | require_relative "annotate_rb/core" 15 | require_relative "annotate_rb/commands" 16 | require_relative "annotate_rb/parser" 17 | require_relative "annotate_rb/runner" 18 | require_relative "annotate_rb/route_annotator" 19 | require_relative "annotate_rb/model_annotator" 20 | require_relative "annotate_rb/options" 21 | require_relative "annotate_rb/eager_loader" 22 | require_relative "annotate_rb/rake_bootstrapper" 23 | require_relative "annotate_rb/config_finder" 24 | require_relative "annotate_rb/config_loader" 25 | require_relative "annotate_rb/config_generator" 26 | 27 | module AnnotateRb 28 | end 29 | -------------------------------------------------------------------------------- /lib/annotate_rb/commands.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module Commands 5 | autoload :PrintVersion, "annotate_rb/commands/print_version" 6 | autoload :PrintHelp, "annotate_rb/commands/print_help" 7 | autoload :AnnotateModels, "annotate_rb/commands/annotate_models" 8 | autoload :AnnotateRoutes, "annotate_rb/commands/annotate_routes" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/annotate_rb/commands/annotate_models.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module Commands 5 | class AnnotateModels 6 | def call(options) 7 | puts "Annotating models" 8 | 9 | if options[:debug] 10 | puts "Running with debug mode, options:" 11 | pp options.to_h 12 | end 13 | 14 | # Eager load Models when we're annotating models 15 | AnnotateRb::EagerLoader.call(options) 16 | 17 | AnnotateRb::ModelAnnotator::Annotator.send(options[:target_action], options) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/annotate_rb/commands/annotate_routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module Commands 5 | class AnnotateRoutes 6 | def call(options) 7 | puts "Annotating routes" 8 | 9 | if options[:debug] 10 | puts "Running with debug mode, options:" 11 | pp options.to_h 12 | end 13 | 14 | AnnotateRb::RouteAnnotator::Annotator.send(options[:target_action], options) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/annotate_rb/commands/print_help.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module Commands 5 | class PrintHelp 6 | def initialize(parser) 7 | @parser = parser 8 | end 9 | 10 | def call(_options) 11 | puts @parser.help 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/annotate_rb/commands/print_version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module Commands 5 | class PrintVersion 6 | def call(_options) 7 | puts "AnnotateRb v#{Core.version}" 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/annotate_rb/config_finder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | class ConfigFinder 5 | DOTFILE = ".annotaterb.yml" 6 | 7 | class << self 8 | def find_project_root 9 | # We should expect this method to be called from a Rails project root and returning it 10 | # e.g. "/Users/drwl/personal/annotaterb/dummyapp" 11 | Dir.pwd 12 | end 13 | 14 | def find_project_dotfile 15 | file_path = File.expand_path(DOTFILE, find_project_root) 16 | 17 | return file_path if File.exist?(file_path) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/annotate_rb/config_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | class ConfigGenerator 5 | class << self 6 | # Returns unset configuration key-value pairs as yaml. 7 | # Useful when a config file was generated an older version of gem and new 8 | # settings get added. 9 | def unset_config_defaults 10 | user_defaults = ConfigLoader.load_config 11 | defaults = Options.from({}, {}).to_h 12 | 13 | differences = defaults.keys - user_defaults.keys 14 | result = defaults.slice(*differences) 15 | 16 | # Return empty string if no differences to avoid appending empty hash 17 | return "" if result.empty? 18 | 19 | # Generates proper YAML including the leading hyphens `---` header 20 | yml_content = YAML.dump(result, StringIO.new).string 21 | # Remove the header 22 | yml_content.sub("---", "") 23 | end 24 | 25 | def default_config_yml 26 | defaults_hash = Options.from({}, {}).to_h 27 | _yml_content = YAML.dump(defaults_hash, StringIO.new).string 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/annotate_rb/config_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "erb" 4 | 5 | module AnnotateRb 6 | # Raised when a configuration file is not found. 7 | class ConfigNotFoundError < StandardError 8 | end 9 | 10 | class ConfigLoader 11 | class << self 12 | def load_config 13 | config_path = ConfigFinder.find_project_dotfile 14 | 15 | if config_path 16 | load_yaml_configuration(config_path) 17 | else 18 | {} 19 | end 20 | end 21 | 22 | # Method from Rubocop::ConfigLoader 23 | def load_yaml_configuration(absolute_path) 24 | file_contents = read_file(absolute_path) 25 | yaml_code = ERB.new(file_contents).result 26 | 27 | hash = yaml_safe_load(yaml_code, absolute_path) || {} 28 | 29 | # TODO: Print config if debug flag/option is set 30 | 31 | raise(TypeError, "Malformed configuration in #{absolute_path}") unless hash.is_a?(Hash) 32 | 33 | hash 34 | end 35 | 36 | # Read the specified file, or exit with a friendly, concise message on 37 | # stderr. Care is taken to use the standard OS exit code for a "file not 38 | # found" error. 39 | # 40 | # Method from Rubocop::ConfigLoader 41 | def read_file(absolute_path) 42 | File.read(absolute_path, encoding: Encoding::UTF_8) 43 | rescue Errno::ENOENT 44 | raise ConfigNotFoundError, "Configuration file not found: #{absolute_path}" 45 | end 46 | 47 | # Method from Rubocop::ConfigLoader 48 | def yaml_safe_load(yaml_code, filename) 49 | yaml_safe_load!(yaml_code, filename) 50 | rescue 51 | if defined?(::SafeYAML) 52 | raise "SafeYAML is unmaintained, no longer needed and should be removed" 53 | end 54 | 55 | raise 56 | end 57 | 58 | # Method from Rubocop::ConfigLoader 59 | def yaml_safe_load!(yaml_code, filename) 60 | YAML.safe_load( 61 | yaml_code, permitted_classes: [Regexp, Symbol], aliases: true, filename: filename, symbolize_names: true 62 | ) 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/annotate_rb/core.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module Core 5 | class << self 6 | def version 7 | @version ||= File.read(File.expand_path("../../VERSION", __dir__)).strip 8 | end 9 | 10 | def load_rake_tasks 11 | return if @load_rake_tasks 12 | 13 | rake_tasks = Dir[File.join(File.dirname(__FILE__), "tasks", "**/*.rake")] 14 | 15 | rake_tasks.each do |task| 16 | load task 17 | end 18 | 19 | @load_rake_tasks = true 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/annotate_rb/eager_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | # Not sure what this does just yet 5 | class EagerLoader 6 | class << self 7 | def call(options) 8 | options[:require].count > 0 && options[:require].each { |path| require path } 9 | 10 | if defined?(::Zeitwerk) 11 | # Delegate to Zeitwerk to load stuff as needed 12 | # (Supports both Rails and non-Rails applications) 13 | elsif defined?(::Rails::Application) 14 | klass = ::Rails::Application.send(:subclasses).first 15 | klass.eager_load! 16 | else 17 | model_files = ModelAnnotator::ModelFilesGetter.call(options) 18 | model_files&.each do |model_file| 19 | require File.join(*model_file) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/annotate_rb/helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module Helper 5 | class << self 6 | def width(string) 7 | string.chars.inject(0) { |acc, elem| acc + ((elem.bytesize == 3) ? 2 : 1) } 8 | end 9 | 10 | # TODO: Find another implementation that doesn't depend on ActiveSupport 11 | def fallback(*args) 12 | args.compact.detect(&:present?) 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | autoload :Annotator, "annotate_rb/model_annotator/annotator" 6 | autoload :PatternGetter, "annotate_rb/model_annotator/pattern_getter" 7 | autoload :BadModelFileError, "annotate_rb/model_annotator/bad_model_file_error" 8 | autoload :FileNameResolver, "annotate_rb/model_annotator/file_name_resolver" 9 | autoload :SingleFileAnnotationRemover, "annotate_rb/model_annotator/single_file_annotation_remover" 10 | autoload :ModelClassGetter, "annotate_rb/model_annotator/model_class_getter" 11 | autoload :ModelFilesGetter, "annotate_rb/model_annotator/model_files_getter" 12 | autoload :SingleFileAnnotator, "annotate_rb/model_annotator/single_file_annotator" 13 | autoload :ModelWrapper, "annotate_rb/model_annotator/model_wrapper" 14 | autoload :AnnotationBuilder, "annotate_rb/model_annotator/annotation_builder" 15 | autoload :ColumnAnnotation, "annotate_rb/model_annotator/column_annotation" 16 | autoload :IndexAnnotation, "annotate_rb/model_annotator/index_annotation" 17 | autoload :ForeignKeyAnnotation, "annotate_rb/model_annotator/foreign_key_annotation" 18 | autoload :RelatedFilesListBuilder, "annotate_rb/model_annotator/related_files_list_builder" 19 | autoload :AnnotationDecider, "annotate_rb/model_annotator/annotation_decider" 20 | autoload :SingleFileAnnotatorInstruction, "annotate_rb/model_annotator/single_file_annotator_instruction" 21 | autoload :SingleFileRemoveAnnotationInstruction, "annotate_rb/model_annotator/single_file_remove_annotation_instruction" 22 | autoload :AnnotationDiffGenerator, "annotate_rb/model_annotator/annotation_diff_generator" 23 | autoload :AnnotationDiff, "annotate_rb/model_annotator/annotation_diff" 24 | autoload :ProjectAnnotator, "annotate_rb/model_annotator/project_annotator" 25 | autoload :ProjectAnnotationRemover, "annotate_rb/model_annotator/project_annotation_remover" 26 | autoload :AnnotatedFile, "annotate_rb/model_annotator/annotated_file" 27 | autoload :FileParser, "annotate_rb/model_annotator/file_parser" 28 | autoload :ZeitwerkClassGetter, "annotate_rb/model_annotator/zeitwerk_class_getter" 29 | autoload :CheckConstraintAnnotation, "annotate_rb/model_annotator/check_constraint_annotation" 30 | autoload :FileToParserMapper, "annotate_rb/model_annotator/file_to_parser_mapper" 31 | autoload :Components, "annotate_rb/model_annotator/components" 32 | autoload :Annotation, "annotate_rb/model_annotator/annotation" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotated_file.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module AnnotatedFile 6 | autoload :Generator, "annotate_rb/model_annotator/annotated_file/generator" 7 | autoload :Updater, "annotate_rb/model_annotator/annotated_file/updater" 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotated_file/updater.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module AnnotatedFile 6 | # Updates existing annotations 7 | class Updater 8 | def initialize(file_content, new_annotations, _annotation_position, parsed_file, options) 9 | @options = options 10 | 11 | @new_annotations = new_annotations 12 | @file_content = file_content 13 | 14 | @parsed_file = parsed_file 15 | end 16 | 17 | # @return [String] Returns the annotated file content to be written back to a file 18 | def update 19 | return "" if !@parsed_file.has_annotations? 20 | 21 | new_annotation = wrapped_content(@new_annotations) 22 | 23 | _content = @file_content.sub(@parsed_file.annotations) { new_annotation } 24 | end 25 | 26 | private 27 | 28 | def wrapped_content(content) 29 | wrapper_open = if @options[:wrapper_open] 30 | "# #{@options[:wrapper_open]}\n" 31 | else 32 | "" 33 | end 34 | 35 | wrapper_close = if @options[:wrapper_close] 36 | "# #{@options[:wrapper_close]}\n" 37 | else 38 | "" 39 | end 40 | 41 | _wrapped_info_block = "#{wrapper_open}#{content}#{wrapper_close}" 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module Annotation 6 | autoload :AnnotationBuilder, "annotate_rb/model_annotator/annotation/annotation_builder" 7 | autoload :MainHeader, "annotate_rb/model_annotator/annotation/main_header" 8 | autoload :SchemaHeader, "annotate_rb/model_annotator/annotation/schema_header" 9 | autoload :MarkdownHeader, "annotate_rb/model_annotator/annotation/markdown_header" 10 | autoload :SchemaFooter, "annotate_rb/model_annotator/annotation/schema_footer" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation/annotation_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module Annotation 6 | class AnnotationBuilder 7 | class Annotation < Components::Base 8 | attr_reader :version, :table_name, :table_comment, :max_size 9 | 10 | def initialize(options, **input) 11 | @options = options 12 | 13 | @version = input[:version] 14 | @table_name = input[:table_name] 15 | @table_comment = input[:table_comment] 16 | @max_size = input[:max_size] 17 | @model = input[:model] 18 | end 19 | 20 | def body 21 | [ 22 | MainHeader.new(version, @options[:include_version]), 23 | SchemaHeader.new(table_name, table_comment, @options), 24 | MarkdownHeader.new(max_size), 25 | *columns, 26 | IndexAnnotation::AnnotationBuilder.new(@model, @options).build, 27 | ForeignKeyAnnotation::AnnotationBuilder.new(@model, @options).build, 28 | CheckConstraintAnnotation::AnnotationBuilder.new(@model, @options).build, 29 | SchemaFooter.new 30 | ] 31 | end 32 | 33 | def build 34 | components = body.flatten 35 | 36 | if @options[:format_rdoc] 37 | components.map(&:to_rdoc).compact.join("\n") 38 | elsif @options[:format_yard] 39 | components.map(&:to_yard).compact.join("\n") 40 | elsif @options[:format_markdown] 41 | components.map(&:to_markdown).compact.join("\n") 42 | else 43 | components.map(&:to_default).compact.join("\n") 44 | end 45 | end 46 | 47 | private 48 | 49 | def columns 50 | @model.columns.map do |col| 51 | _component = ColumnAnnotation::AnnotationBuilder.new(col, @model, max_size, @options).build 52 | end 53 | end 54 | end 55 | 56 | def initialize(klass, options) 57 | @model = ModelWrapper.new(klass, options) 58 | @options = options 59 | end 60 | 61 | def build 62 | version = @model.migration_version 63 | table_name = @model.table_name 64 | table_comment = @model.connection.try(:table_comment, @model.table_name) 65 | max_size = @model.max_schema_info_width 66 | 67 | _annotation = Annotation.new(@options, 68 | version: version, table_name: table_name, table_comment: table_comment, 69 | max_size: max_size, model: @model).build 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation/main_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module Annotation 6 | class MainHeader < Components::Base 7 | # Annotate Models plugin use this header 8 | PREFIX = "== Schema Information" 9 | PREFIX_MD = "## Schema Information" 10 | 11 | attr_reader :version 12 | 13 | def initialize(version, include_version) 14 | @version = version 15 | @include_version = include_version 16 | end 17 | 18 | def to_markdown 19 | header = "# #{PREFIX_MD}" 20 | if @include_version && version > 0 21 | header += "\n# Schema version: #{version}" 22 | end 23 | 24 | header 25 | end 26 | 27 | def to_default 28 | header = "# #{PREFIX}" 29 | if @include_version && version > 0 30 | header += "\n# Schema version: #{version}" 31 | end 32 | 33 | header 34 | end 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation/markdown_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module Annotation 6 | class MarkdownHeader < Components::Base 7 | MD_NAMES_OVERHEAD = 6 8 | MD_TYPE_ALLOWANCE = 18 9 | 10 | attr_reader :max_size 11 | 12 | def initialize(max_size) 13 | @max_size = max_size 14 | end 15 | 16 | def to_markdown 17 | name_padding = max_size + MD_NAMES_OVERHEAD 18 | # standard:disable Lint/FormatParameterMismatch 19 | formatted_headers = format("# %-#{name_padding}.#{name_padding}s | %-#{MD_TYPE_ALLOWANCE}.#{MD_TYPE_ALLOWANCE}s | %s", 20 | "Name", 21 | "Type", 22 | "Attributes") 23 | # standard:enable Lint/FormatParameterMismatch 24 | 25 | <<~HEADER.strip 26 | # ### Columns 27 | # 28 | #{formatted_headers} 29 | # #{"-" * name_padding} | #{"-" * MD_TYPE_ALLOWANCE} | #{"-" * 27} 30 | HEADER 31 | end 32 | 33 | def to_default 34 | nil 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation/schema_footer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module Annotation 6 | class SchemaFooter < Components::Base 7 | def to_rdoc 8 | <<~OUTPUT 9 | #-- 10 | # == Schema Information End 11 | #++ 12 | OUTPUT 13 | end 14 | 15 | def to_default 16 | <<~OUTPUT 17 | # 18 | OUTPUT 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation/schema_header.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module Annotation 6 | class SchemaHeader < Components::Base 7 | class TableName < Components::Base 8 | attr_reader :name 9 | 10 | def initialize(name) 11 | @name = name 12 | end 13 | 14 | def to_default 15 | "# Table name: #{name}" 16 | end 17 | 18 | def to_markdown 19 | "# Table name: `#{name}`" 20 | end 21 | end 22 | 23 | attr_reader :table_name, :table_comment 24 | 25 | def initialize(table_name, table_comment, options) 26 | @table_name = table_name 27 | @table_comment = table_comment 28 | @options = options 29 | end 30 | 31 | def body 32 | [ 33 | Components::BlankCommentLine.new, 34 | TableName.new(name), 35 | Components::BlankCommentLine.new 36 | ] 37 | end 38 | 39 | def to_default 40 | body.map(&:to_default).join("\n") 41 | end 42 | 43 | def to_markdown 44 | body.map(&:to_markdown).join("\n") 45 | end 46 | 47 | private 48 | 49 | def display_table_comments? 50 | @options[:with_comment] && @options[:with_table_comments] 51 | end 52 | 53 | def name 54 | if display_table_comments? && table_comment 55 | formatted_comment = "(#{table_comment.gsub(/\n/, "\\n")})" 56 | 57 | "#{table_name}#{formatted_comment}" 58 | else 59 | table_name 60 | end 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation_decider.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | # Class that encapsulates the logic to decide whether to annotate a model file and its related files or not. 6 | class AnnotationDecider 7 | SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations' 8 | 9 | def initialize(file, options) 10 | @file = file 11 | @options = options 12 | end 13 | 14 | def annotate? 15 | return false if file_contains_skip_annotation 16 | 17 | begin 18 | klass = ModelClassGetter.call(@file, @options) 19 | 20 | return false unless klass.respond_to?(:descends_from_active_record?) 21 | 22 | # Skip annotating STI classes 23 | if @options[:exclude_sti_subclasses] && !klass.descends_from_active_record? 24 | return false 25 | end 26 | 27 | return false if klass.abstract_class? 28 | return false unless klass.table_exists? 29 | 30 | return true 31 | rescue BadModelFileError => e 32 | unless @options[:ignore_unknown_models] 33 | warn "Unable to process #{@file}: #{e.message}" 34 | warn "\t#{e.backtrace.join("\n\t")}" if @options[:trace] 35 | end 36 | rescue => e 37 | warn "Unable to process #{@file}: #{e.message}" 38 | warn "\t#{e.backtrace.join("\n\t")}" if @options[:trace] 39 | end 40 | 41 | false 42 | end 43 | 44 | private 45 | 46 | def file_contains_skip_annotation 47 | return false unless File.exist?(@file) 48 | 49 | /#{SKIP_ANNOTATION_PREFIX}.*/o.match?(File.read(@file)) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation_diff.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | # Plain old Ruby object for holding the differences 6 | class AnnotationDiff 7 | attr_reader :current_columns, :new_columns 8 | 9 | def initialize(current_columns, new_columns) 10 | @current_columns = current_columns.dup.freeze 11 | @new_columns = new_columns.dup.freeze 12 | end 13 | 14 | def changed? 15 | @changed ||= @current_columns != @new_columns 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotation_diff_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | # Compares the current file content and new annotation block and generates the column annotation differences 6 | class AnnotationDiffGenerator 7 | HEADER_PATTERN = /(^# Table name:.*?\n(#.*\r?\n)*\r?)/ 8 | # Example matches: 9 | # - "# id :uuid not null, primary key" 10 | # - "# title(length 255) :string not null" 11 | # - "# status(a/b/c) :string not null" 12 | # - "# created_at :datetime not null" 13 | # - "# updated_at :datetime not null" 14 | COLUMN_PATTERN = /^#[\t ]+[[\p{L}\p{N}_]*.`\[\]():]+(?:\(.*?\))?[\t ]+.+$/ 15 | class << self 16 | def call(file_content, annotation_block) 17 | new(file_content, annotation_block).generate 18 | end 19 | end 20 | 21 | # @param [String] file_content The current file content of the model file we intend to annotate 22 | # @param [String] annotation_block The annotation block we intend to write to the model file 23 | def initialize(file_content, annotation_block) 24 | @file_content = file_content 25 | @annotation_block = annotation_block 26 | end 27 | 28 | def generate 29 | # Ignore the Schema version line because it changes with each migration 30 | current_annotations = @file_content.match(HEADER_PATTERN).to_s 31 | new_annotations = @annotation_block.match(HEADER_PATTERN).to_s 32 | 33 | current_annotation_columns = if current_annotations.present? 34 | current_annotations.scan(COLUMN_PATTERN).sort 35 | else 36 | [] 37 | end 38 | 39 | new_annotation_columns = if new_annotations.present? 40 | new_annotations.scan(COLUMN_PATTERN).sort 41 | else 42 | [] 43 | end 44 | 45 | _result = AnnotationDiff.new(current_annotation_columns, new_annotation_columns) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/annotator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class Annotator 6 | class << self 7 | def do_annotations(options) 8 | new(options).do_annotations 9 | end 10 | 11 | def remove_annotations(options) 12 | new(options).remove_annotations 13 | end 14 | end 15 | 16 | def initialize(options) 17 | @options = options 18 | end 19 | 20 | def do_annotations 21 | ProjectAnnotator.new(@options).annotate 22 | end 23 | 24 | def remove_annotations 25 | ProjectAnnotationRemover.new(@options).remove_annotations 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/bad_model_file_error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class BadModelFileError < LoadError 6 | def to_s 7 | "file doesn't contain a valid model class" 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/check_constraint_annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module CheckConstraintAnnotation 6 | autoload :AnnotationBuilder, "annotate_rb/model_annotator/check_constraint_annotation/annotation_builder" 7 | autoload :Annotation, "annotate_rb/model_annotator/check_constraint_annotation/annotation" 8 | autoload :CheckConstraintComponent, "annotate_rb/model_annotator/check_constraint_annotation/check_constraint_component" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/check_constraint_annotation/annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module CheckConstraintAnnotation 6 | class Annotation 7 | HEADER_TEXT = "Check Constraints" 8 | 9 | def initialize(constraints) 10 | @constraints = constraints 11 | end 12 | 13 | def body 14 | [ 15 | Components::BlankCommentLine.new, 16 | Components::Header.new(HEADER_TEXT), 17 | Components::BlankCommentLine.new, 18 | *@constraints 19 | ] 20 | end 21 | 22 | def to_markdown 23 | body.map(&:to_markdown).join("\n") 24 | end 25 | 26 | def to_default 27 | body.map(&:to_default).join("\n") 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/check_constraint_annotation/annotation_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module CheckConstraintAnnotation 6 | class AnnotationBuilder 7 | def initialize(model, options) 8 | @model = model 9 | @options = options 10 | end 11 | 12 | def build 13 | return Components::NilComponent.new if !@options[:show_check_constraints] 14 | return Components::NilComponent.new unless @model.connection.respond_to?(:supports_check_constraints?) && 15 | @model.connection.supports_check_constraints? && @model.connection.respond_to?(:check_constraints) 16 | 17 | check_constraints = @model.connection.check_constraints(@model.table_name) 18 | return Components::NilComponent.new if check_constraints.empty? 19 | 20 | max_size = check_constraints.map { |check_constraint| check_constraint.name.size }.max + 1 21 | 22 | constraints = check_constraints.sort_by(&:name).map do |check_constraint| 23 | expression = if check_constraint.expression 24 | if check_constraint.validated? 25 | "(#{check_constraint.expression.squish})" 26 | else 27 | "(#{check_constraint.expression.squish}) NOT VALID".squish 28 | end 29 | end 30 | 31 | CheckConstraintComponent.new(check_constraint.name, expression, max_size) 32 | end 33 | 34 | _annotation = Annotation.new(constraints) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/check_constraint_annotation/check_constraint_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module CheckConstraintAnnotation 6 | class CheckConstraintComponent < Components::Base 7 | attr_reader :name, :expression, :max_size 8 | 9 | def initialize(name, expression, max_size) 10 | @name = name 11 | @expression = expression 12 | @max_size = max_size 13 | end 14 | 15 | def to_default 16 | # standard:disable Lint/FormatParameterMismatch 17 | sprintf("# %-#{max_size}.#{max_size}s %s", name, expression).rstrip 18 | # standard:enable Lint/FormatParameterMismatch 19 | end 20 | 21 | def to_markdown 22 | if expression 23 | sprintf("# * `%s`: `%s`", name, expression) 24 | else 25 | sprintf("# * `%s`", name) 26 | end 27 | end 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/column_annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ColumnAnnotation 6 | autoload :AttributesBuilder, "annotate_rb/model_annotator/column_annotation/attributes_builder" 7 | autoload :TypeBuilder, "annotate_rb/model_annotator/column_annotation/type_builder" 8 | autoload :ColumnWrapper, "annotate_rb/model_annotator/column_annotation/column_wrapper" 9 | autoload :AnnotationBuilder, "annotate_rb/model_annotator/column_annotation/annotation_builder" 10 | autoload :DefaultValueBuilder, "annotate_rb/model_annotator/column_annotation/default_value_builder" 11 | autoload :ColumnComponent, "annotate_rb/model_annotator/column_annotation/column_component" 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/column_annotation/annotation_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ColumnAnnotation 6 | class AnnotationBuilder 7 | def initialize(column, model, max_size, options) 8 | @column = column 9 | @model = model 10 | @max_size = max_size 11 | @options = options 12 | end 13 | 14 | def build 15 | column_attributes = @model.built_attributes[@column.name] 16 | formatted_column_type = TypeBuilder.new(@column, @options, @model.column_defaults).build 17 | 18 | display_column_comments = @options[:with_comment] && @options[:with_column_comments] 19 | display_column_comments &&= @model.with_comments? && @column.comment 20 | position_of_column_comment = @options[:position_of_column_comment] || Options::FLAG_OPTIONS[:position_of_column_comment] if display_column_comments 21 | 22 | max_attributes_size = @model.built_attributes.values.map { |v| v.join(", ").length }.max 23 | 24 | _component = ColumnComponent.new( 25 | column: @column, 26 | max_name_size: @max_size, 27 | type: formatted_column_type, 28 | attributes: column_attributes, 29 | position_of_column_comment: position_of_column_comment, 30 | max_attributes_size: max_attributes_size 31 | ) 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/column_annotation/column_wrapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ColumnAnnotation 6 | class ColumnWrapper 7 | def initialize(column, column_defaults, options) 8 | @column = column 9 | @column_defaults = column_defaults 10 | @options = options 11 | end 12 | 13 | def raw_default 14 | @column.default 15 | end 16 | 17 | def default_value 18 | @column_defaults[@column.name] 19 | end 20 | 21 | def default_string 22 | quote(default_value) 23 | end 24 | 25 | def type 26 | @column.type 27 | end 28 | 29 | def column_type_string 30 | if (@column.respond_to?(:bigint?) && @column.bigint?) || /\Abigint\b/ =~ @column.sql_type 31 | "bigint" 32 | else 33 | (@column.type || @column.sql_type).to_s 34 | end 35 | end 36 | 37 | def unsigned? 38 | @column.respond_to?(:unsigned?) && @column.unsigned? 39 | end 40 | 41 | def null 42 | @column.null 43 | end 44 | 45 | def precision 46 | @column.precision 47 | end 48 | 49 | def scale 50 | @column.scale 51 | end 52 | 53 | def limit 54 | @column.limit 55 | end 56 | 57 | def geometry_type? 58 | @column.respond_to?(:geometry_type) 59 | end 60 | 61 | def geometry_type 62 | # TODO: Check if we need to check if it responds before accessing the geometry type 63 | @column.geometry_type 64 | end 65 | 66 | def geometric_type? 67 | @column.respond_to?(:geometric_type) 68 | end 69 | 70 | def geometric_type 71 | # TODO: Check if we need to check if it responds before accessing the geometric type 72 | @column.geometric_type 73 | end 74 | 75 | def srid 76 | # TODO: Check if we need to check if it responds before accessing the srid 77 | @column.srid 78 | end 79 | 80 | def array? 81 | @column.respond_to?(:array) && @column.array 82 | end 83 | 84 | def name 85 | @column.name 86 | end 87 | 88 | def virtual? 89 | @column.respond_to?(:virtual?) && @column.virtual? 90 | end 91 | 92 | def default_function 93 | @column.default_function 94 | end 95 | 96 | private 97 | 98 | # Simple quoting for the default column value 99 | def quote(value) 100 | DefaultValueBuilder.new(value, @options).build 101 | end 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/column_annotation/default_value_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ColumnAnnotation 6 | class DefaultValueBuilder 7 | def initialize(value, options) 8 | @value = value 9 | @options = options 10 | end 11 | 12 | # @return [String] 13 | # Returns the value to get written to file by file.puts. Strings get written to file so escaped quoted strings 14 | # get written as quoted. For example, if `value: "\"some_string\""` then "some_string" gets written. 15 | # Same with arrays, if `value: "[\"a\", \"b\", \"c\"]"` then `["a", "b", "c"]` gets written. 16 | # 17 | # @example "\"some_string\"" 18 | # @example "NULL" 19 | # @example "1.2" 20 | def build 21 | if @value.is_a?(Array) 22 | quote_array(@value) 23 | else 24 | quote(@value) 25 | end 26 | end 27 | 28 | private 29 | 30 | def quote(value) 31 | return value.to_s.inspect if @options[:classes_default_to_s]&.include?(value.class.name) 32 | 33 | case value 34 | when NilClass then "NULL" 35 | when TrueClass then "TRUE" 36 | when FalseClass then "FALSE" 37 | when Float, Integer then value.to_s 38 | # BigDecimals need to be output in a non-normalized form and quoted. 39 | when BigDecimal then value.to_s("F") 40 | when String then value.inspect 41 | else 42 | value.inspect 43 | end 44 | end 45 | 46 | def quote_array(value) 47 | content = value.map { |v| quote(v) }.join(", ") 48 | "[" + content + "]" 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/column_annotation/type_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ColumnAnnotation 6 | class TypeBuilder 7 | # Don't show limit (#) on these column types 8 | # Example: show "integer" instead of "integer(4)" 9 | NO_LIMIT_COL_TYPES = %w[integer bigint boolean].freeze 10 | 11 | def initialize(column, options, column_defaults) 12 | # Passing `column_defaults` for posterity, don't actually need it here since it's not used 13 | @column = ColumnWrapper.new(column, column_defaults, options) 14 | @options = options 15 | end 16 | 17 | # Returns the formatted column type as a string. 18 | def build 19 | column_type = @column.column_type_string 20 | 21 | formatted_column_type = column_type 22 | 23 | is_special_type = %w[spatial geometry geography].include?(column_type) 24 | is_decimal_type = column_type == "decimal" 25 | 26 | if is_decimal_type 27 | formatted_column_type = "decimal(#{@column.precision}, #{@column.scale})" 28 | elsif @options[:show_virtual_columns] && @column.virtual? 29 | formatted_column_type = "virtual(#{column_type})" 30 | elsif is_special_type 31 | # Do nothing. Kept as a code fragment in case we need to do something here. 32 | elsif @column.limit && !@options[:format_yard] 33 | # Unsure if Column#limit will ever be an array. May be safe to remove. 34 | if !@column.limit.is_a?(Array) && !hide_limit? 35 | formatted_column_type = column_type + "(#{@column.limit})" 36 | end 37 | end 38 | 39 | formatted_column_type 40 | end 41 | 42 | private 43 | 44 | def hide_limit? 45 | excludes = 46 | if @options[:hide_limit_column_types].blank? 47 | NO_LIMIT_COL_TYPES 48 | else 49 | @options[:hide_limit_column_types].split(",") 50 | end 51 | 52 | excludes.include?(@column.column_type_string) 53 | end 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/components.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | # Shared annotation components 6 | module Components 7 | class Base 8 | # Methods default to #to_default, unless overridden by sub class 9 | def to_markdown 10 | to_default 11 | end 12 | 13 | def to_rdoc 14 | to_default 15 | end 16 | 17 | def to_yard 18 | to_default 19 | end 20 | 21 | def to_default 22 | raise NoMethodError, "Not implemented by class #{self.class}" 23 | end 24 | end 25 | 26 | class NilComponent < Base 27 | # Used when we want to return a component, but does not affect annotation generation. 28 | # It will get ignored when the consuming object calls Array#compact 29 | def to_default 30 | nil 31 | end 32 | end 33 | 34 | class LineBreak < Base 35 | def to_default 36 | "" 37 | end 38 | end 39 | 40 | class BlankCommentLine < Base 41 | def to_default 42 | "#" 43 | end 44 | end 45 | 46 | class Header < Base 47 | attr_reader :header 48 | 49 | def initialize(header) 50 | @header = header 51 | end 52 | 53 | def to_default 54 | "# #{header}" 55 | end 56 | 57 | def to_markdown 58 | "# ### #{header}" 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/file_name_resolver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class FileNameResolver 6 | class << self 7 | def call(filename_template, model_name, table_name) 8 | # e.g. with a model file name like "app/models/collapsed/example/test_model.rb" 9 | # and using a collapsed `model_name` such as "collapsed/test_model" 10 | model_name_without_namespace = model_name.split("/").last 11 | 12 | filename_template 13 | .gsub("%MODEL_NAME%", model_name) 14 | .gsub("%MODEL_NAME_WITHOUT_NS%", model_name_without_namespace) 15 | .gsub("%PLURALIZED_MODEL_NAME%", model_name.pluralize) 16 | .gsub("%TABLE_NAME%", table_name || model_name.pluralize) 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/file_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module FileParser 6 | autoload :AnnotationFinder, "annotate_rb/model_annotator/file_parser/annotation_finder" 7 | autoload :CustomParser, "annotate_rb/model_annotator/file_parser/custom_parser" 8 | autoload :ParsedFile, "annotate_rb/model_annotator/file_parser/parsed_file" 9 | autoload :ParsedFileResult, "annotate_rb/model_annotator/file_parser/parsed_file_result" 10 | autoload :YmlParser, "annotate_rb/model_annotator/file_parser/yml_parser" 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/file_parser/parsed_file_result.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module FileParser 6 | class ParsedFileResult 7 | def initialize( 8 | has_annotations:, 9 | has_skip_string:, 10 | annotations_changed:, 11 | annotations:, 12 | annotations_with_whitespace:, 13 | has_leading_whitespace:, 14 | has_trailing_whitespace:, 15 | annotation_position:, 16 | starts:, 17 | ends: 18 | ) 19 | @has_annotations = has_annotations 20 | @has_skip_string = has_skip_string 21 | @annotations_changed = annotations_changed 22 | @annotations = annotations 23 | @annotations_with_whitespace = annotations_with_whitespace 24 | @has_leading_whitespace = has_leading_whitespace 25 | @has_trailing_whitespace = has_trailing_whitespace 26 | @annotation_position = annotation_position 27 | @starts = starts 28 | @ends = ends 29 | end 30 | 31 | attr_reader :annotations, :annotation_position, :starts, :ends 32 | 33 | # Returns annotations with new line before and after if they exist 34 | attr_reader :annotations_with_whitespace 35 | 36 | def annotations_changed? 37 | @annotations_changed 38 | end 39 | 40 | def has_annotations? 41 | @has_annotations 42 | end 43 | 44 | def has_skip_string? 45 | @has_skip_string 46 | end 47 | 48 | def has_leading_whitespace? 49 | @has_leading_whitespace 50 | end 51 | 52 | def has_trailing_whitespace? 53 | @has_trailing_whitespace 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/file_parser/yml_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "psych" 4 | 5 | module AnnotateRb 6 | module ModelAnnotator 7 | module FileParser 8 | class YmlParser 9 | class << self 10 | def parse(string) 11 | _parser = new(string).tap(&:parse) 12 | end 13 | end 14 | 15 | attr_reader :comments, :starts, :ends 16 | 17 | def initialize(input) 18 | @input = input 19 | @comments = [] 20 | @starts = [] 21 | @ends = [] 22 | end 23 | 24 | def parse 25 | parse_comments 26 | parse_yml 27 | end 28 | 29 | private 30 | 31 | def parse_comments 32 | # Adds 0-indexed line numbers 33 | @input.split($/).each_with_index do |line, line_no| 34 | if line.strip.starts_with?("#") 35 | @comments << [line, line_no] 36 | end 37 | end 38 | end 39 | 40 | def parse_yml 41 | # https://docs.ruby-lang.org/en/master/Psych.html#module-Psych-label-Reading+to+Psych-3A-3ANodes-3A-3AStream+structure 42 | parser = Psych.parser 43 | begin 44 | parser.parse(@input) 45 | rescue Psych::SyntaxError => _e 46 | # "Dynamic fixtures with ERB" exist in Rails, and will cause Psych.parser to error 47 | # This is a hacky solution to get around this and still have it parse 48 | erb_yml = ERB.new(@input).result 49 | parser.parse(erb_yml) 50 | end 51 | 52 | stream = parser.handler.root 53 | 54 | if stream.children.any? 55 | doc = stream.children.first 56 | @starts << [nil, doc.start_line] 57 | @ends << [nil, doc.end_line] 58 | else 59 | # When parsing a yml file, streamer returns an instance of `Psych::Nodes::Stream` which is a subclass of 60 | # `Psych::Nodes::Node`. It along with children nodes, implement #start_line and #end_line. 61 | # 62 | # When parsing input that is only comments, the parser counts #start_line as the start of the file being 63 | # line 0. 64 | # 65 | # What we really want is where the "start" of the yml file would happen, which would be after comments. 66 | # This is stream#end_line. 67 | @starts << [nil, stream.end_line] 68 | @ends << [nil, stream.end_line] 69 | end 70 | end 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/file_to_parser_mapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class FileToParserMapper 6 | class UnsupportedFileTypeError < StandardError; end 7 | 8 | MAP = { 9 | ".rb" => FileParser::CustomParser, 10 | ".yml" => FileParser::YmlParser 11 | }.freeze 12 | 13 | class << self 14 | def map(file_name) 15 | extension = File.extname(file_name).downcase 16 | parser = MAP[extension] 17 | 18 | raise UnsupportedFileTypeError, "File '#{file_name}' does not have a supported file type." if parser.nil? 19 | 20 | parser 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/foreign_key_annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ForeignKeyAnnotation 6 | autoload :AnnotationBuilder, "annotate_rb/model_annotator/foreign_key_annotation/annotation_builder" 7 | autoload :Annotation, "annotate_rb/model_annotator/foreign_key_annotation/annotation" 8 | autoload :ForeignKeyComponent, "annotate_rb/model_annotator/foreign_key_annotation/foreign_key_component" 9 | autoload :ForeignKeyComponentBuilder, "annotate_rb/model_annotator/foreign_key_annotation/foreign_key_component_builder" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/foreign_key_annotation/annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ForeignKeyAnnotation 6 | class Annotation 7 | HEADER_TEXT = "Foreign Keys" 8 | 9 | def initialize(foreign_keys) 10 | @foreign_keys = foreign_keys 11 | end 12 | 13 | def body 14 | [ 15 | Components::BlankCommentLine.new, 16 | Components::Header.new(HEADER_TEXT), 17 | Components::BlankCommentLine.new, 18 | *@foreign_keys 19 | ] 20 | end 21 | 22 | def to_markdown 23 | body.map(&:to_markdown).join("\n") 24 | end 25 | 26 | def to_default 27 | body.map(&:to_default).join("\n") 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/foreign_key_annotation/annotation_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ForeignKeyAnnotation 6 | class AnnotationBuilder 7 | def initialize(model, options) 8 | @model = model 9 | @options = options 10 | end 11 | 12 | def build 13 | return Components::NilComponent.new if !@options[:show_foreign_keys] 14 | return Components::NilComponent.new unless @model.connection.respond_to?(:supports_foreign_keys?) && 15 | @model.connection.supports_foreign_keys? && @model.connection.respond_to?(:foreign_keys) 16 | 17 | foreign_keys = @model.connection.foreign_keys(@model.table_name) 18 | return Components::NilComponent.new if foreign_keys.empty? 19 | 20 | fks = foreign_keys.map do |fk| 21 | ForeignKeyComponentBuilder.new(fk, @options) 22 | end 23 | 24 | max_size = fks.map(&:formatted_name).map(&:size).max + 1 25 | 26 | foreign_key_components = fks.sort_by { |fk| [fk.formatted_name, fk.stringified_columns] }.map do |fk| 27 | # fk is a ForeignKeyComponentBuilder 28 | 29 | ForeignKeyComponent.new(fk.formatted_name, fk.constraints_info, fk.ref_info, max_size) 30 | end 31 | 32 | _annotation = Annotation.new(foreign_key_components) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/foreign_key_annotation/foreign_key_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ForeignKeyAnnotation 6 | class ForeignKeyComponent < Components::Base 7 | attr_reader :formatted_name, :constraints_info, :ref_info, :max_size 8 | 9 | def initialize(formatted_name, constraints_info, ref_info, max_size) 10 | @formatted_name = formatted_name 11 | @constraints_info = constraints_info 12 | @ref_info = ref_info 13 | @max_size = max_size 14 | end 15 | 16 | def to_markdown 17 | format("# * `%s`%s:\n# * **`%s`**", 18 | formatted_name, 19 | constraints_info.blank? ? "" : " (_#{constraints_info}_)", 20 | ref_info) 21 | end 22 | 23 | def to_default 24 | # standard:disable Lint/FormatParameterMismatch 25 | format("# %-#{max_size}.#{max_size}s %s %s", 26 | formatted_name, 27 | "(#{ref_info})", 28 | constraints_info).rstrip 29 | # standard:enable Lint/FormatParameterMismatch 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/foreign_key_annotation/foreign_key_component_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ForeignKeyAnnotation 6 | class ForeignKeyComponentBuilder 7 | attr_reader :foreign_key 8 | 9 | def initialize(foreign_key, options) 10 | @foreign_key = foreign_key 11 | @options = options 12 | end 13 | 14 | def formatted_name 15 | @formatted_name ||= if foreign_key.name.blank? 16 | foreign_key.column 17 | else 18 | @options[:show_complete_foreign_keys] ? foreign_key.name : foreign_key.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, "...") 19 | end 20 | end 21 | 22 | def stringified_columns 23 | @stringified_columns ||= stringify(foreign_key.column) 24 | end 25 | 26 | def stringified_primary_key 27 | @stringified_primary_key ||= stringify(foreign_key.primary_key) 28 | end 29 | 30 | def constraints_info 31 | @constraints_info ||= begin 32 | constraints_info = "" 33 | constraints_info += "ON DELETE => #{foreign_key.on_delete} " if foreign_key.on_delete 34 | constraints_info += "ON UPDATE => #{foreign_key.on_update} " if foreign_key.on_update 35 | constraints_info.strip 36 | end 37 | end 38 | 39 | def ref_info 40 | if foreign_key.column.is_a?(Array) # Composite foreign key using multiple columns 41 | "#{stringified_columns} => #{foreign_key.to_table}#{stringified_primary_key}" 42 | else 43 | "#{foreign_key.column} => #{foreign_key.to_table}.#{foreign_key.primary_key}" 44 | end 45 | end 46 | 47 | private 48 | 49 | # The fk columns or primary key might be composite (an Array), so format them into a string for the annotation 50 | def stringify(columns) 51 | columns.is_a?(Array) ? "[#{columns.join(", ")}]" : columns 52 | end 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/index_annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module IndexAnnotation 6 | autoload :AnnotationBuilder, "annotate_rb/model_annotator/index_annotation/annotation_builder" 7 | autoload :IndexComponent, "annotate_rb/model_annotator/index_annotation/index_component" 8 | autoload :Annotation, "annotate_rb/model_annotator/index_annotation/annotation" 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/index_annotation/annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module IndexAnnotation 6 | class Annotation 7 | HEADER_TEXT = "Indexes" 8 | 9 | def initialize(indexes) 10 | @indexes = indexes 11 | end 12 | 13 | def body 14 | [ 15 | Components::BlankCommentLine.new, 16 | Components::Header.new(HEADER_TEXT), 17 | Components::BlankCommentLine.new, 18 | *@indexes 19 | ] 20 | end 21 | 22 | def to_markdown 23 | body.map(&:to_markdown).join("\n") 24 | end 25 | 26 | def to_default 27 | body.map(&:to_default).join("\n") 28 | end 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/index_annotation/annotation_builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module IndexAnnotation 6 | class AnnotationBuilder 7 | def initialize(model, options) 8 | @model = model 9 | @options = options 10 | end 11 | 12 | def build 13 | return Components::NilComponent.new if !@options[:show_indexes] 14 | 15 | indexes = @model.retrieve_indexes_from_table 16 | return Components::NilComponent.new if indexes.empty? 17 | 18 | max_size = indexes.map { |index| index.name.size }.max + 1 19 | 20 | indexes = indexes.sort_by(&:name).map do |index| 21 | IndexComponent.new(index, max_size, @options) 22 | end 23 | 24 | _annotation = Annotation.new(indexes) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/model_files_getter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class ModelFilesGetter 6 | class << self 7 | # Return a list of the model files to annotate. 8 | # If we have command line arguments, they're assumed to the path 9 | # of model files from root dir. Otherwise we take all the model files 10 | # in the model_dir directory. 11 | def call(options) 12 | model_files = list_model_files_from_argument(options) 13 | 14 | return model_files if model_files.any? 15 | 16 | model_dirs = options[:model_dir].flat_map { |model_dir| Dir[model_dir] } 17 | model_dirs.each do |dir| 18 | Dir.chdir(dir) do 19 | list = if options[:ignore_model_sub_dir] 20 | Dir["*.rb"].map { |f| [dir, f] } 21 | else 22 | Dir["**/*.rb"] 23 | .reject { |f| f["concerns/"] } 24 | .map { |f| [dir, f] } 25 | end 26 | model_files.concat(list) 27 | end 28 | end 29 | 30 | if model_files.empty? 31 | warn "No models found in directory '#{options[:model_dir].join("', '")}'." 32 | warn "Either specify models on the command line, or use the --model-dir option." 33 | warn "Call 'annotaterb --help' for more info." 34 | # exit 1 # TODO: Return exit code back to caller. Right now it messes up RSpec being able to run 35 | end 36 | model_files 37 | end 38 | 39 | private 40 | 41 | def list_model_files_from_argument(options) 42 | return [] if options.get_state(:working_args).empty? 43 | 44 | specified_files = options.get_state(:working_args).map { |file| File.expand_path(file) } 45 | 46 | model_files = options[:model_dir].flat_map do |dir| 47 | absolute_dir_path = File.expand_path(dir) 48 | specified_files 49 | .find_all { |file| file.start_with?(absolute_dir_path) } 50 | .map { |file| [dir, file.sub("#{absolute_dir_path}/", "")] } 51 | end 52 | 53 | if model_files.size != specified_files.size 54 | warn "The specified file could not be found in directory '#{options[:model_dir].join("', '")}'." 55 | warn "Call 'annotaterb --help' for more info." 56 | # exit 1 # TODO: Return exit code back to caller. Right now it messes up RSpec being able to run 57 | end 58 | 59 | model_files 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/project_annotation_remover.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class ProjectAnnotationRemover 6 | def initialize(options) 7 | @options = options 8 | end 9 | 10 | def remove_annotations 11 | project_model_files = model_files 12 | 13 | removal_instructions = project_model_files.map do |path, filename| 14 | file = File.join(path, filename) 15 | 16 | if AnnotationDecider.new(file, @options).annotate? 17 | _instructions = build_instructions_for_file(file) 18 | end 19 | end.flatten.compact 20 | 21 | deannotated = removal_instructions.map do |instruction| 22 | if SingleFileAnnotationRemover.call_with_instructions(instruction) 23 | instruction.file 24 | end 25 | rescue => e 26 | warn "Unable to process #{File.join(instruction.file)}: #{e.message}" 27 | warn "\t" + e.backtrace.join("\n\t") if @options[:trace] 28 | end.flatten.compact 29 | 30 | if deannotated.empty? 31 | puts "Model files unchanged." 32 | else 33 | puts "Removed annotations (#{deannotated.length}) from: #{deannotated.join(", ")}" 34 | end 35 | end 36 | 37 | private 38 | 39 | def build_instructions_for_file(file) 40 | klass = ModelClassGetter.call(file, @options) 41 | 42 | instructions = [] 43 | 44 | klass.reset_column_information 45 | model_name = klass.name.underscore 46 | table_name = klass.table_name 47 | 48 | model_instruction = SingleFileRemoveAnnotationInstruction.new(file, @options) 49 | instructions << model_instruction 50 | 51 | related_files = RelatedFilesListBuilder.new(file, model_name, table_name, @options).build 52 | related_file_instructions = related_files.map do |f, _position_key| 53 | _instruction = SingleFileRemoveAnnotationInstruction.new(f, @options) 54 | end 55 | instructions.concat(related_file_instructions) 56 | 57 | instructions 58 | end 59 | 60 | def model_files 61 | @model_files ||= ModelFilesGetter.call(@options) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/project_annotator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class ProjectAnnotator 6 | def initialize(options) 7 | @options = options 8 | end 9 | 10 | def annotate 11 | project_model_files = model_files 12 | 13 | annotation_instructions = project_model_files.map do |path, filename| 14 | file = File.join(path, filename) 15 | 16 | if AnnotationDecider.new(file, @options).annotate? 17 | _instructions = build_instructions_for_file(file) 18 | end 19 | end.flatten.compact 20 | 21 | annotated = annotation_instructions.map do |instruction| 22 | if SingleFileAnnotator.call_with_instructions(instruction) 23 | instruction.file 24 | end 25 | end.compact 26 | 27 | if annotated.empty? 28 | puts "Model files unchanged." 29 | else 30 | puts "Annotated (#{annotated.length}): #{annotated.join(", ")}" 31 | end 32 | end 33 | 34 | private 35 | 36 | def build_instructions_for_file(file) 37 | start = Time.now 38 | klass = ModelClassGetter.call(file, @options) 39 | 40 | instructions = [] 41 | 42 | klass.reset_column_information 43 | annotation = Annotation::AnnotationBuilder.new(klass, @options).build 44 | model_name = klass.name.underscore 45 | 46 | # In multi-database configurations, it is possible for different models to have the same table name but live 47 | # in different databases. Here we are opting to use the table name to find related files only when the model 48 | # is using the primary connection. 49 | table_name = klass.table_name if klass.connection_specification_name == ActiveRecord::Base.name 50 | 51 | model_instruction = SingleFileAnnotatorInstruction.new(file, annotation, :position_in_class, @options) 52 | instructions << model_instruction 53 | 54 | related_files = RelatedFilesListBuilder.new(file, model_name, table_name, @options).build 55 | related_file_instructions = related_files.map do |f, position_key| 56 | _instruction = SingleFileAnnotatorInstruction.new(f, annotation, position_key, @options) 57 | end 58 | instructions.concat(related_file_instructions) 59 | 60 | if @options[:debug] 61 | puts "Built instructions for #{file} in #{Time.now - start}s" 62 | end 63 | 64 | instructions 65 | end 66 | 67 | def model_files 68 | @model_files ||= ModelFilesGetter.call(@options) 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/single_file_annotation_remover.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class SingleFileAnnotationRemover 6 | class << self 7 | def call_with_instructions(instruction) 8 | call(instruction.file, instruction.options) 9 | end 10 | 11 | def call(file_name, options = Options.from({})) 12 | return false unless File.exist?(file_name) 13 | old_content = File.read(file_name) 14 | 15 | parser_klass = FileToParserMapper.map(file_name) 16 | 17 | begin 18 | parsed_file = FileParser::ParsedFile.new(old_content, "", parser_klass, options).parse 19 | rescue FileParser::AnnotationFinder::MalformedAnnotation => e 20 | warn "Unable to process #{file_name}: #{e.message}" 21 | warn "\t" + e.backtrace.join("\n\t") if @options[:trace] 22 | return false 23 | end 24 | 25 | return false if !parsed_file.has_annotations? 26 | return false if parsed_file.has_skip_string? 27 | 28 | updated_file_content = old_content.sub(parsed_file.annotations_with_whitespace, "") 29 | 30 | File.open(file_name, "wb") { |f| f.puts updated_file_content } 31 | 32 | true 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/single_file_annotator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class SingleFileAnnotator 6 | class << self 7 | def call_with_instructions(instruction) 8 | call(instruction.file, instruction.annotation, instruction.position, instruction.options) 9 | end 10 | 11 | # Add a schema block to a file. If the file already contains 12 | # a schema info block (a comment starting with "== Schema Information"), 13 | # check if it matches the block that is already there. If so, leave it be. 14 | # If not, remove the old info block and write a new one. 15 | # 16 | # == Returns: 17 | # true or false depending on whether the file was modified. 18 | # 19 | # === Options (opts) 20 | # :force:: whether to update the file even if it doesn't seem to need it. 21 | # :position_in_*:: where to place the annotated section in fixture or model file, 22 | # :before, :top, :after or :bottom. Default is :before. 23 | # 24 | def call(file_name, annotation, annotation_position, options) 25 | return false unless File.exist?(file_name) 26 | old_content = File.read(file_name) 27 | 28 | parser_klass = FileToParserMapper.map(file_name) 29 | 30 | begin 31 | parsed_file = FileParser::ParsedFile.new(old_content, annotation, parser_klass, options).parse 32 | rescue FileParser::AnnotationFinder::MalformedAnnotation => e 33 | warn "Unable to process #{file_name}: #{e.message}" 34 | warn "\t" + e.backtrace.join("\n\t") if @options[:trace] 35 | return false 36 | end 37 | 38 | return false if parsed_file.has_skip_string? 39 | return false if !parsed_file.annotations_changed? && !options[:force] 40 | 41 | abort "AnnotateRb error. #{file_name} needs to be updated, but annotaterb was run with `--frozen`." if options[:frozen] 42 | 43 | updated_file_content = if !parsed_file.has_annotations? 44 | AnnotatedFile::Generator.new(old_content, annotation, annotation_position, parser_klass, parsed_file, options).generate 45 | elsif options[:force] 46 | AnnotatedFile::Generator.new(old_content, annotation, annotation_position, parser_klass, parsed_file, options).generate 47 | else 48 | AnnotatedFile::Updater.new(old_content, annotation, annotation_position, parsed_file, options).update 49 | end 50 | 51 | File.open(file_name, "wb") { |f| f.puts updated_file_content } 52 | 53 | true 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | # A plain old Ruby object (PORO) that contains all necessary information for SingleFileAnnotator 6 | class SingleFileAnnotatorInstruction 7 | def initialize(file, annotation, position, options) 8 | @file = file # Path to file 9 | @annotation = annotation # Annotation string 10 | @position = position # Position in the file where to write the annotation to 11 | @options = options 12 | end 13 | 14 | attr_reader :file, :annotation, :position, :options 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/single_file_remove_annotation_instruction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | # A plain old Ruby object (PORO) that contains all necessary information for SingleFileAnnotationRemover 6 | class SingleFileRemoveAnnotationInstruction 7 | def initialize(file, options) 8 | @file = file # Path to file 9 | @options = options 10 | end 11 | 12 | attr_reader :file, :options 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/annotate_rb/rake_bootstrapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | class RakeBootstrapper 5 | class << self 6 | def call(options) 7 | require "rake" 8 | load "./Rakefile" if File.exist?("./Rakefile") && !Rake::Task.task_defined?(:environment) 9 | 10 | begin 11 | Rake::Task[:environment].invoke 12 | rescue 13 | nil 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/annotate_rb/route_annotator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module RouteAnnotator 5 | autoload :Annotator, "annotate_rb/route_annotator/annotator" 6 | autoload :Helper, "annotate_rb/route_annotator/helper" 7 | autoload :HeaderGenerator, "annotate_rb/route_annotator/header_generator" 8 | autoload :BaseProcessor, "annotate_rb/route_annotator/base_processor" 9 | autoload :AnnotationProcessor, "annotate_rb/route_annotator/annotation_processor" 10 | autoload :RemovalProcessor, "annotate_rb/route_annotator/removal_processor" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/annotate_rb/route_annotator/annotation_processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This module provides methods for annotating config/routes.rb. 4 | module AnnotateRb 5 | module RouteAnnotator 6 | # This class is abstract class of classes adding and removing annotation to config/routes.rb. 7 | class AnnotationProcessor < BaseProcessor 8 | # @return [String] 9 | def execute 10 | if routes_file_exist? 11 | if update 12 | "#{routes_file} was annotated." 13 | else 14 | "#{routes_file} was not changed." 15 | end 16 | else 17 | "#{routes_file} could not be found." 18 | end 19 | end 20 | 21 | private 22 | 23 | def header 24 | @header ||= HeaderGenerator.generate(options) 25 | end 26 | 27 | def generate_new_content_array(content, header_position) 28 | magic_comments_map, content = Helper.extract_magic_comments_from_array(content) 29 | if %w[before top].include?(options[:position_in_routes]) 30 | new_content_array = [] 31 | new_content_array += magic_comments_map 32 | new_content_array << "" if magic_comments_map.any? 33 | new_content_array += header 34 | new_content_array << "" if content.first != "" 35 | new_content_array += content 36 | else 37 | # Ensure we have adequate trailing newlines at the end of the file to 38 | # ensure a blank line separating the content from the annotation. 39 | content << "" unless content.last == "" 40 | 41 | # We're moving something from the top of the file to the bottom, so ditch 42 | # the spacer we put in the first time around. 43 | content.shift if header_position == :before && content.first == "" 44 | 45 | new_content_array = magic_comments_map + content + header 46 | end 47 | 48 | # Make sure we end on a trailing newline. 49 | new_content_array << "" unless new_content_array.last == "" 50 | 51 | new_content_array 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/annotate_rb/route_annotator/annotator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module RouteAnnotator 5 | class Annotator 6 | class << self 7 | # TODO: Deprecate 8 | def do_annotations(options = {}) 9 | add_annotations(options) 10 | end 11 | 12 | def add_annotations(options = {}) 13 | new(options).add_annotations 14 | end 15 | 16 | def remove_annotations(options = {}) 17 | new(options).remove_annotations 18 | end 19 | end 20 | 21 | def initialize(options = {}) 22 | @options = options 23 | end 24 | 25 | def add_annotations 26 | routes_file = File.join("config", "routes.rb") 27 | AnnotationProcessor.execute(@options, routes_file).tap do |result| 28 | puts result 29 | end 30 | end 31 | 32 | def remove_annotations 33 | routes_file = File.join("config", "routes.rb") 34 | RemovalProcessor.execute(@options, routes_file).tap do |result| 35 | puts result 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/annotate_rb/route_annotator/removal_processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This module provides methods for annotating config/routes.rb. 4 | module AnnotateRb 5 | module RouteAnnotator 6 | # This class is abstract class of classes adding and removing annotation to config/routes.rb. 7 | class RemovalProcessor < BaseProcessor 8 | # @return [String] 9 | def execute 10 | if routes_file_exist? 11 | if update 12 | "Annotations were removed from #{routes_file}." 13 | else 14 | "#{routes_file} was not changed (Annotation did not exist)." 15 | end 16 | else 17 | "#{routes_file} could not be found." 18 | end 19 | end 20 | 21 | private 22 | 23 | def generate_new_content_array(content, header_position) 24 | if header_position == :before 25 | content.shift while content.first == "" 26 | elsif header_position == :after 27 | content.pop while content.last == "" 28 | end 29 | 30 | # Make sure we end on a trailing newline. 31 | content << "" unless content.last == "" 32 | 33 | # TODO: If the user buried it in the middle, we should probably see about 34 | # TODO: preserving a single line of space between the content above and 35 | # TODO: below... 36 | content 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/annotate_rb/runner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | class Runner 5 | class << self 6 | attr_reader :runner 7 | 8 | def run(args) 9 | self.runner = new 10 | 11 | runner.run(args) 12 | 13 | self.runner = nil 14 | end 15 | 16 | def running? 17 | !!runner 18 | end 19 | 20 | private 21 | 22 | attr_writer :runner 23 | end 24 | 25 | def run(args) 26 | config_file_options = ConfigLoader.load_config 27 | parser = Parser.new(args, {}) 28 | 29 | parsed_options = parser.parse 30 | remaining_args = parser.remaining_args 31 | 32 | options = config_file_options.merge(parsed_options) 33 | 34 | @options = Options.from(options, {working_args: remaining_args}) 35 | AnnotateRb::RakeBootstrapper.call(@options) 36 | 37 | if @options[:command] 38 | @options[:command].call(@options) 39 | else 40 | # TODO 41 | raise "Didn't specify a command" 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/annotate_rb/tasks/annotate_models_migrate.rake: -------------------------------------------------------------------------------- 1 | # These tasks are added to the project if you install annotate as a Rails plugin. 2 | # (They are not used to build annotate itself.) 3 | 4 | # Append annotations to Rake tasks for ActiveRecord, so annotate automatically gets 5 | # run after doing db:migrate. 6 | 7 | # Migration tasks are tasks that we'll "hook" into 8 | migration_tasks = %w[db:migrate db:migrate:up db:migrate:down db:migrate:reset db:migrate:redo db:rollback] 9 | 10 | # Support for data_migrate gem (https://github.com/ilyakatz/data-migrate) 11 | migration_tasks_with_data = migration_tasks.map { |task| "#{task}:with_data" } 12 | migration_tasks += migration_tasks_with_data 13 | 14 | if defined?(Rails::Application) && Rails.version.split(".").first.to_i >= 6 15 | require "active_record" 16 | 17 | databases = ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml 18 | 19 | # If there's multiple databases, this appends database specific rake tasks to `migration_tasks` 20 | ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |database_name| 21 | migration_tasks.concat(%w[db:migrate db:migrate:up db:migrate:down db:rollback].map { |task| "#{task}:#{database_name}" }) 22 | end 23 | end 24 | 25 | migration_tasks.each do |task| 26 | next unless Rake::Task.task_defined?(task) 27 | 28 | Rake::Task[task].enhance do # This block is ran after `task` completes 29 | task_name = Rake.application.top_level_tasks.last # The name of the task that was run, e.g. "db:migrate" 30 | 31 | Rake::Task[task_name].enhance do 32 | ::AnnotateRb::Runner.run(["models"]) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/annotaterb.rb: -------------------------------------------------------------------------------- 1 | # Gem names that follow naming convention work seamlessly. However, this gem is "annotaterb" where in code it is 2 | # AnnotateRb. Because of this, we need this file so that the rest of the library automatically gets required. 3 | 4 | require "annotate_rb" 5 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/config/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates a default configuration file, `.annotaterb.yml` in your 3 | Rails app project root. 4 | 5 | Example: 6 | `rails generate annotate_rb:config` 7 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/config/config_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "annotate_rb" 4 | 5 | module AnnotateRb 6 | module Generators 7 | class ConfigGenerator < ::Rails::Generators::Base 8 | def generate_config 9 | create_file ::AnnotateRb::ConfigFinder::DOTFILE do 10 | ::AnnotateRb::ConfigGenerator.default_config_yml 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/hook/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Adds a rake task into your Rails app's lib/tasks directory, that 3 | automatically annotates models when you do a db:migrate in 4 | development mode. 5 | 6 | Example: 7 | `rails generate annotate_rb:hook` 8 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/hook/hook_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "annotate_rb" 4 | 5 | module AnnotateRb 6 | module Generators 7 | class HookGenerator < ::Rails::Generators::Base 8 | source_root File.expand_path("templates", __dir__) 9 | 10 | def copy_hook_file 11 | copy_file "annotate_rb.rake", "lib/tasks/annotate_rb.rake" 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/hook/templates/annotate_rb.rake: -------------------------------------------------------------------------------- 1 | # This rake task was added by annotate_rb gem. 2 | 3 | # Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this 4 | if Rails.env.development? && ENV["ANNOTATERB_SKIP_ON_DB_TASKS"].nil? 5 | require "annotate_rb" 6 | 7 | AnnotateRb::Core.load_rake_tasks 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/install/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Generates a default configuration file and adds a rake task into 3 | your Rails app's lib/tasks directory, that automatically 4 | annotates models when you do a db:migrate in development mode. 5 | 6 | Example: 7 | `rails generate annotate_rb:install` 8 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "annotate_rb" 4 | 5 | module AnnotateRb 6 | module Generators 7 | class InstallGenerator < ::Rails::Generators::Base 8 | def install_hook_and_generate_defaults 9 | generate "annotate_rb:hook" 10 | generate "annotate_rb:config" 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/update_config/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Appends to .annotaterb.yml any missing default configuration 3 | key-value pairs. 4 | 5 | Example: 6 | `rails generate annotate_rb:update_config` 7 | -------------------------------------------------------------------------------- /lib/generators/annotate_rb/update_config/update_config_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "annotate_rb" 4 | 5 | module AnnotateRb 6 | module Generators 7 | class UpdateConfigGenerator < ::Rails::Generators::Base 8 | def generate_config 9 | insert_into_file ::AnnotateRb::ConfigFinder::DOTFILE do 10 | ::AnnotateRb::ConfigGenerator.unset_config_defaults 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /potato.md: -------------------------------------------------------------------------------- 1 | Colons can be used to align columns. 2 | 3 | | Tables | Are | Cool | 4 | | ------------- |:-------------:| -----:| 5 | | col 3 is | right-aligned | $1600 | 6 | | col 2 is | centered | $12 | 7 | | zebra stripes | are neat | $1 | 8 | 9 | There must be at least 3 dashes separating each header cell. 10 | The outer pipes (|) are optional, and you don't need to make the 11 | raw Markdown line up prettily. You can also use inline Markdown. 12 | 13 | Markdown | Less | Pretty 14 | --- | --- | --- 15 | *Still* | `renders` | **nicely** 16 | 1 | 2 | 3 17 | 18 | 19 | ## Route Map 20 | 21 |  Prefix | Verb | URI Pattern | Controller#Action 22 | --------- | ---------- | --------------- | -------------------- 23 | myaction1 | GET | /url1(.:format) | mycontroller1#action 24 | myaction2 | POST | /url2(.:format) | mycontroller2#action 25 |  myaction3 | DELETE-GET | /url3(.:format) | mycontroller3#action \n") 26 | 27 | 28 | 29 | Table name: `users` 30 | 31 | ### Columns 32 | 33 | Name | Type | Attributes 34 | ----------------------- | ------------------ | --------------------------- 35 | **`id`** | `integer` | `not null, primary key` 36 | **`foreign_thing_id`** | `integer` | `not null` 37 | 38 | ### Foreign Keys 39 | 40 | * `fk_rails_...` (_ON DELETE => on_delete_value ON UPDATE => on_update_value_): 41 | * **`foreign_thing_id => foreign_things.id`** 42 | -------------------------------------------------------------------------------- /spec/dummyapp/.rbenv-gemsets: -------------------------------------------------------------------------------- 1 | dummyapp 2 | -------------------------------------------------------------------------------- /spec/dummyapp/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 5 | gem "rails", "~> 7.1.0" 6 | 7 | # Lock the concurrent-ruby gem to version 1.3.4 to ensure compatibility with 8 | # the current specs. Reference: rails/rails#54260 9 | # TODO: Remove the line below when upgrading to Rails 7.1 or higher. 10 | gem "concurrent-ruby", "1.3.4" 11 | 12 | case ENV['DATABASE_ADAPTER'] # This feels so wrong 13 | when 'mysql2' 14 | gem 'mysql2', '>= 0.5', '< 1' 15 | when 'pg' 16 | gem 'pg', '>= 1.5', '< 2' 17 | when 'sqlite3' 18 | gem 'sqlite3', '>= 1.6', '< 2' 19 | else 20 | raise 'The environment variable DATABASE_ADAPTER must be one of mysql2, pg, or sqlite3' 21 | end 22 | 23 | # Use the Puma web server [https://github.com/puma/puma] 24 | gem "puma", "~> 6.0" 25 | 26 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 27 | # gem "kredis" 28 | 29 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 30 | # gem "bcrypt", "~> 3.1.7" 31 | 32 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 33 | gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] 34 | 35 | group :development, :test do 36 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 37 | gem "debug", platforms: %i[ mri mingw x64_mingw ] 38 | end 39 | 40 | group :development do 41 | gem "annotaterb", path: "../../" 42 | 43 | # Use console on exceptions pages [https://github.com/rails/web-console] 44 | gem "web-console" 45 | 46 | gem "pry-byebug" 47 | 48 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 49 | # gem "rack-mini-profiler" 50 | 51 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 52 | # gem "spring" 53 | end 54 | 55 | gem "bigdecimal" 56 | gem "logger" 57 | gem "mutex_m" 58 | -------------------------------------------------------------------------------- /spec/dummyapp/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | -------------------------------------------------------------------------------- /spec/dummyapp/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummyapp/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* Application styles */ 2 | -------------------------------------------------------------------------------- /spec/dummyapp/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummyapp/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/abstract_model.rb: -------------------------------------------------------------------------------- 1 | class AbstractModel < ApplicationRecord 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/collapsed/example/test_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Collapsed 4 | class TestModel < ApplicationRecord 5 | def self.table_name_prefix 6 | "collapsed_" 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/secondary/test_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Secondary::TestDefault < SecondaryRecord 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/secondary_record.rb: -------------------------------------------------------------------------------- 1 | class SecondaryRecord < ApplicationRecord 2 | self.abstract_class = true 3 | 4 | connects_to database: { writing: :secondary } 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/test_child_default.rb: -------------------------------------------------------------------------------- 1 | class TestChildDefault < ApplicationRecord 2 | belongs_to :test_default 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/test_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TestDefault < ApplicationRecord 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/test_null_false.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TestNullFalse < ApplicationRecord 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/test_parent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TestParent < ApplicationRecord 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/test_sibling_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TestSiblingDefault < TestDefault 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummyapp/app/models/test_true_sti.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class TestTrueSti < TestParent 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummyapp/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummyapp 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "application" %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/dummyapp/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /spec/dummyapp/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /spec/dummyapp/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args, exception: true) 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /spec/dummyapp/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /spec/dummyapp/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | # require "active_job/railtie" 7 | require "active_record/railtie" 8 | # require "active_storage/engine" 9 | require "action_controller/railtie" 10 | # require "action_mailer/railtie" 11 | # require "action_mailbox/engine" 12 | # require "action_text/engine" 13 | require "action_view/railtie" 14 | # require "action_cable/engine" 15 | require "rails/test_unit/railtie" 16 | 17 | # Require the gems listed in Gemfile, including any gems 18 | # you've limited to :test, :development, or :production. 19 | Bundler.require(*Rails.groups) 20 | 21 | module Dummyapp 22 | class Application < Rails::Application 23 | # Initialize configuration defaults for originally generated Rails version. 24 | config.load_defaults 7.1 25 | 26 | # Please, add to the `ignore` list any other `lib` subdirectories that do 27 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 28 | # Common ones are `templates`, `generators`, or `middleware`, for example. 29 | config.autoload_lib(ignore: %w(assets tasks)) 30 | 31 | # Configuration for the application, engines, and railties goes here. 32 | # 33 | # These settings can be overridden in specific environments using the files 34 | # in config/environments, which are processed later. 35 | # 36 | # config.time_zone = "Central Time (US & Canada)" 37 | # config.eager_load_paths << Rails.root.join("extras") 38 | 39 | # Don't generate system test files. 40 | config.generators.system_tests = nil 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/dummyapp/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /spec/dummyapp/config/credentials.yml.enc: -------------------------------------------------------------------------------- 1 | 1RwPy9l/HcpVhrX9yw9mgWHZNNJew47V3hEUe4IKRbbIkQJcJE04FoT9g/6iT8P5c/QonqChasecO6sC3Y8GiTkiERMxCqyF9IB1yfB3Ys4oBdA/T62sjSIF5wDfrkABAoUwkCqYvHIbb24g4gZUP5W7GTXD1nt04xHzriiq9lF919XS5eSOAtQTxnI7myGnoG0zhp2s8ls2HXU5JZIvKxqohHD+C+hIRGmX8DJB9fz8flzsBwMGcniqMTNtj4FEZvVzKPHDYTHApKbGkESn088+JeYQaFSu4hpf7hij+NGa7hfrLt2YnHyWiSEn3pB0NLiBRc0OKXvVzBUgIWxi2Eh93iI9+b8cC+8/7VUnvX+ZWcKhJHfhCakRxGrYEx5jvZhUEvsDoWC2itxHeGjz0s/aiiCvJd+825uf--BnmUR+/3J+2gQrDN--PMEUWkL/8zovfPz+RpMtFA== -------------------------------------------------------------------------------- /spec/dummyapp/config/database.yml: -------------------------------------------------------------------------------- 1 | <% if ENV['DATABASE_ADAPTER'] == 'mysql2' %> 2 | default: &default 3 | host: 127.0.0.1 4 | port: 3306 5 | adapter: mysql2 6 | username: root 7 | password: root 8 | encoding: utf8 9 | 10 | development: 11 | primary: 12 | <<: *default 13 | database: annotaterb_development 14 | <% if ENV['MULTI_DB_TEST'] == 'true' %> 15 | secondary: 16 | <<: *default 17 | database: secondary_annotaterb_development 18 | migrations_paths: db/secondary_migrate 19 | <% end %> 20 | <% end %> 21 | 22 | <% if ENV['DATABASE_ADAPTER'] == 'pg' %> 23 | default: &default 24 | host: 127.0.0.1 25 | port: 5432 26 | adapter: postgresql 27 | username: postgres 28 | password: root 29 | encoding: utf8 30 | 31 | development: 32 | primary: 33 | <<: *default 34 | database: annotaterb_development 35 | <% if ENV['MULTI_DB_TEST'] == 'true' %> 36 | secondary: 37 | <<: *default 38 | database: secondary_annotaterb_development 39 | migrations_paths: db/secondary_migrate 40 | <% end %> 41 | <% end %> 42 | 43 | <% if ENV['DATABASE_ADAPTER'] == 'sqlite3' %> 44 | default: &default 45 | adapter: sqlite3 46 | 47 | development: 48 | primary: 49 | <<: *default 50 | database: db/development.sqlite3 51 | <% if ENV['MULTI_DB_TEST'] == 'true' %> 52 | secondary: 53 | <<: *default 54 | database: secondary_annotaterb_development 55 | migrations_paths: db/secondary_migrate 56 | <% end %> 57 | <% end %> 58 | -------------------------------------------------------------------------------- /spec/dummyapp/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummyapp/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded any time 7 | # it changes. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.enable_reloading = true 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports. 15 | config.consider_all_requests_local = true 16 | 17 | # Enable server timing 18 | config.server_timing = true 19 | 20 | # Enable/disable caching. By default caching is disabled. 21 | # Run rails dev:cache to toggle caching. 22 | if Rails.root.join("tmp/caching-dev.txt").exist? 23 | config.action_controller.perform_caching = true 24 | config.action_controller.enable_fragment_cache_logging = true 25 | 26 | config.cache_store = :memory_store 27 | config.public_file_server.headers = { 28 | "Cache-Control" => "public, max-age=#{2.days.to_i}" 29 | } 30 | else 31 | config.action_controller.perform_caching = false 32 | 33 | config.cache_store = :null_store 34 | end 35 | 36 | # Print deprecation notices to the Rails logger. 37 | config.active_support.deprecation = :log 38 | 39 | # Raise exceptions for disallowed deprecations. 40 | config.active_support.disallowed_deprecation = :raise 41 | 42 | # Tell Active Support which deprecation messages to disallow. 43 | config.active_support.disallowed_deprecation_warnings = [] 44 | 45 | # Raise an error on page load if there are pending migrations. 46 | config.active_record.migration_error = :page_load 47 | 48 | # Highlight code that triggered database queries in logs. 49 | config.active_record.verbose_query_logs = true 50 | 51 | 52 | # Raises error for missing translations. 53 | # config.i18n.raise_on_missing_translations = true 54 | 55 | # Annotate rendered view with file names. 56 | # config.action_view.annotate_rendered_view_with_filenames = true 57 | 58 | # Raise error when a before_action's only/except options reference missing actions 59 | config.action_controller.raise_on_missing_callback_actions = true 60 | end 61 | -------------------------------------------------------------------------------- /spec/dummyapp/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | # The test environment is used exclusively to run your application's 4 | # test suite. You never need to work with it otherwise. Remember that 5 | # your test database is "scratch space" for the test suite and is wiped 6 | # and recreated between test runs. Don't rely on the data there! 7 | 8 | Rails.application.configure do 9 | # Settings specified here will take precedence over those in config/application.rb. 10 | 11 | # While tests run files are not watched, reloading is not necessary. 12 | config.enable_reloading = false 13 | 14 | # Eager loading loads your entire application. When running a single test locally, 15 | # this is usually not necessary, and can slow down your test suite. However, it's 16 | # recommended that you enable it in continuous integration systems to ensure eager 17 | # loading is working properly before deploying your code. 18 | config.eager_load = ENV["CI"].present? 19 | 20 | # Configure public file server for tests with Cache-Control for performance. 21 | config.public_file_server.enabled = true 22 | config.public_file_server.headers = { 23 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 24 | } 25 | 26 | # Show full error reports and disable caching. 27 | config.consider_all_requests_local = true 28 | config.action_controller.perform_caching = false 29 | config.cache_store = :null_store 30 | 31 | # Render exception templates for rescuable exceptions and raise for other exceptions. 32 | config.action_dispatch.show_exceptions = :rescuable 33 | 34 | # Disable request forgery protection in test environment. 35 | config.action_controller.allow_forgery_protection = false 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raise exceptions for disallowed deprecations. 41 | config.active_support.disallowed_deprecation = :raise 42 | 43 | # Tell Active Support which deprecation messages to disallow. 44 | config.active_support.disallowed_deprecation_warnings = [] 45 | 46 | # Raises error for missing translations. 47 | # config.i18n.raise_on_missing_translations = true 48 | 49 | # Annotate rendered view with file names. 50 | # config.action_view.annotate_rendered_view_with_filenames = true 51 | 52 | # Raise error when a before_action's only/except options reference missing actions 53 | config.action_controller.raise_on_missing_callback_actions = true 54 | end 55 | -------------------------------------------------------------------------------- /spec/dummyapp/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /spec/dummyapp/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn 8 | ] 9 | -------------------------------------------------------------------------------- /spec/dummyapp/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummyapp/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | 6 | # Rails.application.config.permissions_policy do |policy| 7 | # policy.camera :none 8 | # policy.gyroscope :none 9 | # policy.microphone :none 10 | # policy.usb :none 11 | # policy.fullscreen :self 12 | # policy.payment :self, "https://secure.example.com" 13 | # end 14 | -------------------------------------------------------------------------------- /spec/dummyapp/config/initializers/test_zeitwork_collapsed.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.autoloaders.main.collapse("#{Rails.root}/app/models/collapsed/example") -------------------------------------------------------------------------------- /spec/dummyapp/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t "hello" 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t("hello") %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # "true": "foo" 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at https://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /spec/dummyapp/config/master.key: -------------------------------------------------------------------------------- 1 | 185f7f871b76c64d62e4903006ba0508 -------------------------------------------------------------------------------- /spec/dummyapp/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } 9 | threads min_threads_count, max_threads_count 10 | 11 | # Specifies the `worker_timeout` threshold that Puma will use to wait before 12 | # terminating a worker in development environments. 13 | # 14 | worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" 15 | 16 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 17 | # 18 | port ENV.fetch("PORT") { 3000 } 19 | 20 | # Specifies the `environment` that Puma will run in. 21 | # 22 | environment ENV.fetch("RAILS_ENV") { "development" } 23 | 24 | # Specifies the `pidfile` that Puma will use. 25 | pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } 26 | 27 | # Specifies the number of `workers` to boot in clustered mode. 28 | # Workers are forked web server processes. If using threads and workers together 29 | # the concurrency of the application would be max `threads` * `workers`. 30 | # Workers do not work on JRuby or Windows (both of which do not support 31 | # processes). 32 | # 33 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 34 | 35 | # Use the `preload_app!` method when specifying a `workers` number. 36 | # This directive tells Puma to first boot the application and load code 37 | # before forking the application. This takes advantage of Copy On Write 38 | # process behavior so workers use less memory. 39 | # 40 | # preload_app! 41 | 42 | # Allow puma to be restarted by `bin/rails restart` command. 43 | plugin :tmp_restart 44 | -------------------------------------------------------------------------------- /spec/dummyapp/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Rails.application.routes.draw do 4 | root "articles#index" 5 | resources :resources 6 | resource :singular_resource 7 | 8 | get "/manual", to: "manual#show" 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummyapp/db/migrate/20230630123456_create_test_tables.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateTestTables < ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"] 4 | def change 5 | create_table :test_defaults do |t| 6 | t.boolean :boolean, default: false 7 | t.date :date, default: '2023-07-04' 8 | t.datetime :datetime, default: '2023-07-04 12:34:56 UTC' 9 | t.decimal :decimal, precision: 14, scale: 2, default: BigDecimal('43.21') 10 | t.float :float, default: 12.34 11 | t.integer :integer, default: 99 12 | t.string :string, default: 'hello world!' 13 | 14 | t.timestamps 15 | end 16 | 17 | create_table :test_null_falses do |t| 18 | t.binary :binary, null: false 19 | t.boolean :boolean, null: false 20 | t.date :date, null: false 21 | t.datetime :datetime, null: false 22 | t.decimal :decimal, precision: 14, scale: 2, null: false 23 | t.float :float, null: false 24 | t.integer :integer, null: false 25 | t.string :string, null: false 26 | t.text :text, null: false 27 | t.timestamp :timestamp, null: false 28 | 29 | t.timestamps 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/dummyapp/db/migrate/20240210100506_create_collapsed_test_models.rb: -------------------------------------------------------------------------------- 1 | class CreateCollapsedTestModels < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :collapsed_test_models do |t| 4 | t.string :name 5 | t.boolean :collapsed 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummyapp/db/migrate/20240628051901_add_index_to_test_null_false.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToTestNullFalse < ActiveRecord::Migration[7.0] 2 | def change 3 | add_index :test_null_falses, :date 4 | add_index :test_null_falses, [:boolean, :integer], name: "by_compound_bool_and_int" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummyapp/db/migrate/20240916190235_create_test_child_defaults.rb: -------------------------------------------------------------------------------- 1 | class CreateTestChildDefaults < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :test_child_defaults do |t| 4 | t.references :test_default, null: false, foreign_key: true 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummyapp/db/migrate/20250825003411_create_test_parents.rb: -------------------------------------------------------------------------------- 1 | class CreateTestParents < ActiveRecord::Migration[7.1] 2 | def change 3 | create_table :test_parents do |t| 4 | t.string :type 5 | t.string :something 6 | 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/dummyapp/db/secondary_migrate/20250324112726_create_test_defaults.rb: -------------------------------------------------------------------------------- 1 | class CreateTestDefaults < ActiveRecord::Migration[7.0] 2 | def change 3 | create_table :test_defaults do |t| 4 | t.string :string 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummyapp/db/secondary_schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[7.0].define(version: 2025_03_24_112726) do 14 | # These are extensions that must be enabled in order to support this database 15 | enable_extension "plpgsql" 16 | 17 | create_table "test_defaults", force: :cascade do |t| 18 | t.string "string" 19 | t.datetime "created_at", null: false 20 | t.datetime "updated_at", null: false 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /spec/dummyapp/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) 7 | # Character.create(name: "Luke", movie: movies.first) 8 | -------------------------------------------------------------------------------- /spec/dummyapp/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummyapp/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummyapp/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummyapp/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drwl/annotaterb/dd101d201e94366e9b9d0df213ebd68a8a67d2fd/spec/dummyapp/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /spec/dummyapp/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drwl/annotaterb/dd101d201e94366e9b9d0df213ebd68a8a67d2fd/spec/dummyapp/public/apple-touch-icon.png -------------------------------------------------------------------------------- /spec/dummyapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drwl/annotaterb/dd101d201e94366e9b9d0df213ebd68a8a67d2fd/spec/dummyapp/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummyapp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /spec/dummyapp/secondary_annotaterb_development: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drwl/annotaterb/dd101d201e94366e9b9d0df213ebd68a8a67d2fd/spec/dummyapp/secondary_annotaterb_development -------------------------------------------------------------------------------- /spec/dummyapp/test/fixtures/secondary/test_defaults.yml: -------------------------------------------------------------------------------- 1 | one: 2 | string: MyString 3 | 4 | two: 5 | string: MyString 6 | -------------------------------------------------------------------------------- /spec/dummyapp/test/fixtures/test_defaults.yml: -------------------------------------------------------------------------------- 1 | one: 2 | boolean: false 3 | date: 2023-07-04 4 | datetime: 2023-07-04 12:34:56 5 | decimal: 43.21 6 | float: 12.34 7 | integer: 99 8 | string: hello world! 9 | 10 | two: 11 | boolean: true 12 | date: 2023-09-04 13 | datetime: 2023-09-04 12:34:56 14 | decimal: 87.65 15 | float: 56.78 16 | integer: 100 17 | string: hello universe! 18 | -------------------------------------------------------------------------------- /spec/dummyapp/test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | class ActiveSupport::TestCase 6 | # Run tests in parallel with specified workers 7 | parallelize(workers: :number_of_processors) 8 | 9 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 10 | fixtures :all 11 | 12 | # Add more helper methods to be used by all tests here... 13 | end 14 | -------------------------------------------------------------------------------- /spec/integration/annotate_after_migration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate after running migrations", type: "aruba" do 6 | let(:command_timeout_seconds) { 10 } 7 | let(:migration_file) { "20231013230731_add_int_field_to_test_defaults.rb" } 8 | let(:models_dir) { "app/models" } 9 | 10 | it "adds annotations for the new field" do 11 | reset_database 12 | run_migrations 13 | 14 | # Start with the already annotated TestDefault model 15 | copy(model_template("test_default.rb"), models_dir) 16 | 17 | expected_test_default = read_file(model_template("test_default_updated.rb")) 18 | original_test_default = read_file(dummyapp_model("test_default.rb")) 19 | 20 | # Check that files have been copied over correctly 21 | expect(expected_test_default).not_to eq(original_test_default) 22 | 23 | copy(File.join(migrations_template_dir, migration_file), "db/migrate") 24 | 25 | # Apply this specific migration 26 | _run_migrations_cmd = run_command_and_stop("bin/rails db:migrate:up VERSION=20231013230731", fail_on_error: true, exit_timeout: command_timeout_seconds) 27 | _run_annotations_cmd = run_command_and_stop("bundle exec annotaterb models", fail_on_error: true, exit_timeout: command_timeout_seconds) 28 | 29 | annotated_test_default = read_file(dummyapp_model("test_default.rb")) 30 | 31 | expect(last_command_started).to be_successfully_executed 32 | expect(annotated_test_default).to eq(expected_test_default) 33 | end 34 | 35 | context "when the rake task that hooks into database migration exists" do 36 | before do 37 | _cmd = run_command_and_stop("bin/rails g annotate_rb:install", fail_on_error: true, exit_timeout: command_timeout_seconds) 38 | end 39 | 40 | it "annotations are automatically added during migration" do 41 | reset_database 42 | 43 | expected_test_default = read_file(model_template("test_default.rb")) 44 | original_test_default = read_file(dummyapp_model("test_default.rb")) 45 | 46 | # Check that files have been copied over correctly 47 | expect(expected_test_default).not_to eq(original_test_default) 48 | 49 | _run_migrations_cmd = run_command_and_stop("bin/rails db:migrate", fail_on_error: true, exit_timeout: command_timeout_seconds) 50 | 51 | annotated_test_default = read_file(dummyapp_model("test_default.rb")) 52 | 53 | expect(last_command_started).to be_successfully_executed 54 | expect(annotated_test_default).to eq(expected_test_default) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/integration/annotate_collapsed_models_nested_position_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate collapsed models with --nested-position", type: "aruba" do 6 | let(:models_dir) { "app/models" } 7 | let(:command_timeout_seconds) { 10 } 8 | 9 | context "when annotating collapsed models with --nested-position" do 10 | it "inserts annotation inside the module, above the class" do 11 | reset_database 12 | run_migrations 13 | 14 | expected = read_file(model_template("nested_position_collapsed_test_model.rb")) 15 | 16 | run_command_and_stop("bundle exec annotaterb models --nested-position", fail_on_error: false, exit_timeout: command_timeout_seconds) 17 | annotated = read_file(dummyapp_model("collapsed/example/test_model.rb")) 18 | expect(annotated).to eq(expected) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/integration/annotate_collapsed_models_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate collapsed models", type: "aruba" do 6 | let(:models_dir) { "app/models" } 7 | let(:command_timeout_seconds) { 10 } 8 | 9 | context "when annotating collapsed models" do 10 | it "annotates them correctly" do 11 | reset_database 12 | run_migrations 13 | 14 | expected_test_model = read_file(model_template("collapsed_test_model.rb")) 15 | 16 | original_test_model = read_file(dummyapp_model("collapsed/example/test_model.rb")) 17 | 18 | expect(expected_test_model).not_to eq(original_test_model) 19 | 20 | _cmd = run_command_and_stop("bundle exec annotaterb models", fail_on_error: true, exit_timeout: command_timeout_seconds) 21 | 22 | annotated_test_model = read_file(dummyapp_model("collapsed/example/test_model.rb")) 23 | 24 | expect(last_command_started).to be_successfully_executed 25 | expect(expected_test_model).to eq(annotated_test_model) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/integration/annotate_file_with_existing_annotations_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate a file with existing annotations", type: "aruba" do 6 | let(:command_timeout_seconds) { 10 } 7 | let(:models_dir) { "app/models" } 8 | let(:model_file) { "app/models/test_default.rb" } 9 | 10 | context "when using 'force' option and 'position: bottom'" do 11 | before do 12 | # Copy file with existing annotations at the top 13 | copy(model_template("test_default.rb"), "app/models") 14 | 15 | reset_database 16 | run_migrations 17 | end 18 | 19 | it "moves annotations to the bottom of the file" do 20 | expected_test_default = read_file(model_template("test_default_with_bottom_annotations.rb")) 21 | original_test_default = read_file(dummyapp_model("test_default.rb")) 22 | 23 | # Check that files have been copied over correctly 24 | expect(expected_test_default).not_to eq(original_test_default) 25 | 26 | _cmd = run_command_and_stop( 27 | "bundle exec annotaterb models #{model_file} --force --position bottom", 28 | fail_on_error: true, 29 | exit_timeout: command_timeout_seconds 30 | ) 31 | 32 | annotated_test_default = read_file(dummyapp_model("test_default.rb")) 33 | 34 | expect(last_command_started).to be_successfully_executed 35 | expect(annotated_test_default).to eq(expected_test_default) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/integration/annotate_model_with_foreign_key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate collapsed models", type: "aruba" do 6 | let(:models_dir) { "app/models" } 7 | let(:command_timeout_seconds) { 10 } 8 | 9 | it "annotates them correctly" do 10 | reset_database 11 | run_migrations 12 | 13 | expected_test_model = read_file(model_template("test_child_default.rb")) 14 | 15 | original_test_model = read_file(dummyapp_model("test_child_default.rb")) 16 | 17 | expect(expected_test_model).not_to eq(original_test_model) 18 | 19 | _cmd = run_command_and_stop("bundle exec annotaterb models", fail_on_error: true, exit_timeout: command_timeout_seconds) 20 | 21 | annotated_test_model = read_file(dummyapp_model("test_child_default.rb")) 22 | 23 | expect(last_command_started).to be_successfully_executed 24 | expect(expected_test_model).to eq(annotated_test_model) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/integration/annotate_models_in_multi_db_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate models in a multi-db environment with duplicate table names", type: "aruba" do 6 | include_context "when in a multi database environment" 7 | 8 | let(:command_timeout_seconds) { 10 } 9 | 10 | # Test that running `bundle exec annotate models` twice results in no changes on the second run 11 | it "does not change fixture annotations on second run" do 12 | reset_database 13 | run_migrations 14 | 15 | # First run (apply annotations) 16 | run_command_and_stop("bundle exec annotaterb models", fail_on_error: true, exit_timeout: command_timeout_seconds) 17 | 18 | # Second run (ensure no changes) 19 | run_command_and_stop("bundle exec annotaterb models", fail_on_error: true, exit_timeout: command_timeout_seconds) 20 | 21 | # Get output of the second run 22 | second_run_output = last_command_started.output 23 | 24 | # Ensure "Model files unchanged." is included in the output 25 | expect(second_run_output).to include("Model files unchanged.") 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/integration/annotate_routes_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate routes", type: "aruba" do 6 | let(:command_timeout_seconds) { 10 } 7 | 8 | let(:templates_dir) { File.join(aruba.config.root_directory, "spec/templates/") } 9 | let(:routes_file) { "config/routes.rb" } 10 | 11 | it "annotates a single file" do 12 | expected_routes_file = read_file(File.join(templates_dir, "routes.rb")) 13 | original_routes_file = read_file(routes_file) 14 | 15 | # Check that files have been copied over correctly 16 | expect(expected_routes_file).not_to eq(original_routes_file) 17 | 18 | _cmd = run_command_and_stop("bundle exec annotaterb routes", fail_on_error: true, exit_timeout: command_timeout_seconds) 19 | 20 | annotated_routes_file = read_file(routes_file) 21 | 22 | expect(last_command_started).to be_successfully_executed 23 | expect(annotated_routes_file).to eq(expected_routes_file) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/integration/annotate_single_file_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate a single file", type: "aruba" do 6 | let(:models_dir) { "app/models" } 7 | let(:command_timeout_seconds) { 10 } 8 | 9 | let(:model_file) { "app/models/test_default.rb" } 10 | 11 | it "annotates a single file" do 12 | reset_database 13 | run_migrations 14 | 15 | expected_test_default = read_file(model_template("test_default.rb")) 16 | expected_test_null_false = read_file(model_template("test_null_false.rb")) 17 | 18 | original_test_default = read_file(dummyapp_model("test_default.rb")) 19 | original_test_null_false = read_file(dummyapp_model("test_null_false.rb")) 20 | 21 | # Check that files have been copied over correctly 22 | expect(expected_test_default).not_to eq(original_test_default) 23 | expect(expected_test_null_false).not_to eq(original_test_null_false) 24 | 25 | _cmd = run_command_and_stop("bundle exec annotaterb models #{model_file}", fail_on_error: true, exit_timeout: command_timeout_seconds) 26 | 27 | annotated_test_default = read_file(dummyapp_model("test_default.rb")) 28 | annotated_test_null_false = read_file(dummyapp_model("test_null_false.rb")) 29 | 30 | expect(last_command_started).to be_successfully_executed 31 | expect(annotated_test_default).to eq(expected_test_default) 32 | expect(annotated_test_null_false).not_to eq(expected_test_null_false) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/integration/annotate_sti_model_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate STI models", type: "aruba" do 6 | let(:models_dir) { "app/models" } 7 | let(:command_timeout_seconds) { 10 } 8 | 9 | it "annotates them correctly" do 10 | reset_database 11 | run_migrations 12 | 13 | # Pseudo STI model does not have a `type` field, where the True STI model does. 14 | # Both inherit/subclass from another model. 15 | expected_pseudo_sti_model = read_file(model_template("test_sibling_default.rb")) 16 | original_pseudo_sti_model = read_file(dummyapp_model("test_sibling_default.rb")) 17 | expect(expected_pseudo_sti_model).not_to eq(original_pseudo_sti_model) 18 | 19 | expected_true_sti_model = read_file(model_template("test_true_sti.rb")) 20 | original_true_sti_model = read_file(dummyapp_model("test_true_sti.rb")) 21 | expect(expected_true_sti_model).not_to eq(original_true_sti_model) 22 | 23 | _cmd = run_command_and_stop("bundle exec annotaterb models", fail_on_error: true, exit_timeout: command_timeout_seconds) 24 | 25 | expect(last_command_started).to be_successfully_executed 26 | 27 | annotated_pseudo_sti_model = read_file(dummyapp_model("test_sibling_default.rb")) 28 | expect(expected_pseudo_sti_model).to eq(annotated_pseudo_sti_model) 29 | 30 | annotated_true_sti_model = read_file(dummyapp_model("test_true_sti.rb")) 31 | expect(expected_true_sti_model).to eq(annotated_true_sti_model) 32 | end 33 | 34 | it "does not annotate when excluding sti subclasses" do 35 | reset_database 36 | run_migrations 37 | 38 | expected_pseudo_sti_model = read_file(model_template("test_sibling_default.rb")) 39 | original_pseudo_sti_model = read_file(dummyapp_model("test_sibling_default.rb")) 40 | expect(expected_pseudo_sti_model).to_not eq(original_pseudo_sti_model) 41 | 42 | expected_true_sti_model = read_file(model_template("test_true_sti.rb")) 43 | original_true_sti_model = read_file(dummyapp_model("test_true_sti.rb")) 44 | expect(expected_true_sti_model).not_to eq(original_true_sti_model) 45 | 46 | _cmd = run_command_and_stop("bundle exec annotaterb models --exclude sti_subclasses", fail_on_error: true, exit_timeout: command_timeout_seconds) 47 | 48 | expect(last_command_started).to be_successfully_executed 49 | 50 | # Models that are subclassed without `type` field get annotated 51 | annotated_pseudo_sti_model = read_file(dummyapp_model("test_sibling_default.rb")) 52 | expect(expected_pseudo_sti_model).to eq(annotated_pseudo_sti_model) 53 | 54 | # STI classes do not get annotated 55 | annotated_true_sti_model = read_file(dummyapp_model("test_true_sti.rb")) 56 | expect(expected_true_sti_model).to_not eq(annotated_true_sti_model) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/integration/annotate_with_show_migration_option_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Annotate with --show-migration option", type: "aruba" do 6 | include_context "when in a multi database environment" 7 | 8 | let(:command_timeout_seconds) { 10 } 9 | let(:model_file) { "app/models/test_default.rb" } 10 | 11 | it "includes migration version numbers in annotations" do 12 | reset_database 13 | run_migrations 14 | 15 | # Get primary database version 16 | _primary_version_cmd = run_command_and_stop( 17 | "bundle exec rails db:version", 18 | fail_on_error: true, 19 | exit_timeout: command_timeout_seconds 20 | ) 21 | primary_version = last_command_started.stdout.match(/Current version: (\d+)/)[1] 22 | 23 | # Get secondary database version 24 | _secondary_version_cmd = run_command_and_stop( 25 | "bundle exec rails db:version:secondary", 26 | fail_on_error: true, 27 | exit_timeout: command_timeout_seconds 28 | ) 29 | secondary_version = last_command_started.stdout.match(/Current version: (\d+)/)[1] 30 | 31 | # Run annotation with --show-migration option 32 | _cmd = run_command_and_stop( 33 | "bundle exec annotaterb models --show-migration", 34 | fail_on_error: true, 35 | exit_timeout: command_timeout_seconds 36 | ) 37 | 38 | primary_content = read_file(dummyapp_model("test_default.rb")) 39 | secondary_content = read_file(dummyapp_model("secondary/test_default.rb")) 40 | 41 | expect(last_command_started).to be_successfully_executed 42 | 43 | # Verify that migration version numbers are included in the annotation 44 | expect(primary_content).to match(/# Schema version: #{primary_version}/) 45 | expect(secondary_content).to match(/# Schema version: #{secondary_version}/) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/integration/cli_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "CLI", type: "aruba" do 6 | let(:models_dir) { "app/models" } 7 | let(:command_timeout_seconds) { 10 } 8 | 9 | context "when running in a non-Rails project directory" do 10 | before do 11 | remove("Rakefile") if exist?("Rakefile") 12 | remove("Gemfile") if exist?("Gemfile") 13 | end 14 | 15 | let(:error_message) { "Please run annotaterb from the root of the project." } 16 | 17 | it "exits and outputs an error message" do 18 | _cmd = run_command("bundle exec annotaterb") 19 | 20 | expect(last_command_started).to have_exit_status(1) 21 | expect(last_command_started).to have_output_on_stderr(error_message) 22 | end 23 | end 24 | 25 | context "when running in a directory with a Rakefile and a Gemfile" do 26 | let(:help_banner_fragment) { "Usage: annotaterb [command] [options]" } 27 | 28 | it "outputs the help message" do 29 | _cmd = run_command("bundle exec annotaterb", fail_on_error: true, exit_timeout: command_timeout_seconds) 30 | 31 | expect(last_command_started).to be_successfully_executed 32 | expect(last_command_started.stdout).to include(help_banner_fragment) 33 | end 34 | 35 | it "annotates files that have not been annotated" do 36 | reset_database 37 | run_migrations 38 | 39 | expected_test_default = read_file(model_template("test_default.rb")) 40 | expected_test_null_false = read_file(model_template("test_null_false.rb")) 41 | 42 | original_test_default = read_file(dummyapp_model("test_default.rb")) 43 | original_test_null_false = read_file(dummyapp_model("test_null_false.rb")) 44 | 45 | expect(expected_test_default).not_to eq(original_test_default) 46 | expect(expected_test_null_false).not_to eq(original_test_null_false) 47 | 48 | _cmd = run_command_and_stop("bundle exec annotaterb models", fail_on_error: true, exit_timeout: command_timeout_seconds) 49 | 50 | annotated_test_default = read_file(dummyapp_model("test_default.rb")) 51 | annotated_test_null_false = read_file(dummyapp_model("test_null_false.rb")) 52 | 53 | expect(last_command_started).to be_successfully_executed 54 | expect(expected_test_default).to eq(annotated_test_default) 55 | expect(expected_test_null_false).to eq(annotated_test_null_false) 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/integration/rails_generator_install_spec.rb: -------------------------------------------------------------------------------- 1 | require "integration_spec_helper" 2 | 3 | RSpec.describe "Generator installs rake file", type: "aruba" do 4 | let(:command_timeout_seconds) { 10 } 5 | 6 | let(:rake_task_file) { "lib/tasks/annotate_rb.rake" } 7 | let(:rake_task) { File.join(aruba.config.root_directory, "lib/generators/annotate_rb/hook/templates/annotate_rb.rake") } 8 | let(:config_file) { ".annotaterb.yml" } 9 | 10 | let(:generator_install_command) { "bin/rails generate annotate_rb:install" } 11 | 12 | it "installs the rake file to Rails project" do 13 | # First check that the file doesn't exist in dummyapp 14 | expect(exist?(rake_task_file)).to be_falsey 15 | 16 | _cmd = run_command_and_stop(generator_install_command, fail_on_error: true, exit_timeout: command_timeout_seconds) 17 | 18 | installed_rake_task = read_file(rake_task_file) 19 | # Read the one in the actual gem 20 | actual_rake_task = read_file(rake_task) 21 | 22 | expect(last_command_started).to be_successfully_executed 23 | expect(installed_rake_task).to eq(actual_rake_task) 24 | end 25 | 26 | it "generates a default config file" do 27 | # First check that the file doesn't exist in dummyapp 28 | expect(exist?(config_file)).to be_falsey 29 | 30 | _cmd = run_command_and_stop(generator_install_command, fail_on_error: true, exit_timeout: command_timeout_seconds) 31 | 32 | expect(exist?(config_file)).to be_truthy 33 | 34 | expect(last_command_started).to be_successfully_executed 35 | end 36 | 37 | context "when the rake task already exists" do 38 | before do 39 | touch(rake_task_file) 40 | end 41 | 42 | it "returns the Thor cli" do 43 | # First check that the file exists in dummyapp 44 | expect(exist?(rake_task_file)).to be_truthy 45 | 46 | # TODO: Improve this so we don't have to rely on `exit_timeout` 47 | _cmd = run_command(generator_install_command, exit_timeout: 5) 48 | # Because the rake task file already exists, there will be a conflict in the Rails generator. 49 | # The prompt should look something like this: 50 | # 51 | # ... 52 | # generate annotate_rb:hook 53 | # rails generate annotate_rb:hook 54 | # conflict lib/tasks/annotate_rb.rake 55 | # Overwrite .../dummyapp/lib/tasks/annotate_rb.rake? (enter "h" for help) [Ynaqdhm] 56 | type("q") # Quit the command 57 | 58 | # When the file already exists, the default behavior is the Thor CLI prompts user on how to proceed 59 | # https://github.com/rails/thor/blob/a4d99cfc97691504d26d0d0aefc649a8f2e89b3c/spec/actions/create_file_spec.rb#L112 60 | expect(all_stdout).to include("conflict") 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/integration/rails_generator_update_config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "integration_spec_helper" 4 | 5 | RSpec.describe "Generator appends to config file", type: "aruba" do 6 | let(:command_timeout_seconds) { 10 } 7 | 8 | let(:config_file) { ".annotaterb.yml" } 9 | let(:config_file_content) do 10 | <<~YML.strip 11 | --- 12 | :classified_sort: true 13 | :exclude_controllers: true 14 | :exclude_factories: false 15 | :exclude_fixtures: false 16 | :exclude_helpers: true 17 | :exclude_scaffolds: true 18 | :exclude_serializers: false 19 | :exclude_sti_subclasses: false 20 | :exclude_tests: false 21 | :force: false 22 | :format_markdown: false 23 | :format_rdoc: false 24 | :format_yard: false 25 | YML 26 | end 27 | 28 | let(:generator_update_config_command) { "bin/rails generate annotate_rb:update_config" } 29 | 30 | it "appends missing configuration key-value pairs" do 31 | write_file(config_file, config_file_content) 32 | 33 | _cmd = run_command_and_stop(generator_update_config_command, fail_on_error: true, exit_timeout: command_timeout_seconds) 34 | 35 | changed_config_file = read_file(config_file) 36 | 37 | expect(last_command_started).to be_successfully_executed 38 | expect(config_file_content).not_to eq(changed_config_file) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/integration_spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "pry" 4 | require "aruba/rspec" 5 | 6 | Aruba.configure do |config| 7 | config.allow_absolute_paths = true 8 | end 9 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/annotate_models/annotate_models_annotating_a_file_frozen_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe AnnotateRb::ModelAnnotator::Annotator do 2 | include AnnotateTestHelpers 3 | 4 | describe "annotating a file" do 5 | let(:options) { AnnotateRb::Options.new({}) } 6 | 7 | before do 8 | @model_dir = Dir.mktmpdir("annotate_models") 9 | (@model_file_name, @file_content) = write_model "user.rb", <<~EOS 10 | class User < ActiveRecord::Base 11 | end 12 | EOS 13 | 14 | @klass = mock_class(:users, 15 | :id, 16 | [ 17 | mock_column("id", :integer), 18 | mock_column("name", :string, limit: 50) 19 | ]) 20 | @schema_info = AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder.new(@klass, options).build 21 | end 22 | 23 | # TODO: Check out why this test fails due to test pollution 24 | describe "frozen option" do 25 | it "should abort without existing annotation when frozen: true " do 26 | expect { annotate_one_file frozen: true }.to raise_error SystemExit, /user.rb needs to be updated, but annotaterb was run with `--frozen`./ 27 | end 28 | 29 | it "should abort with different annotation when frozen: true " do 30 | annotate_one_file 31 | 32 | another_schema_info = AnnotateRb::ModelAnnotator::Annotation::AnnotationBuilder.new( 33 | mock_class(:users, :id, [mock_column("id", :integer)]), 34 | options 35 | ).build 36 | 37 | @schema_info = another_schema_info 38 | 39 | expect { annotate_one_file frozen: true }.to raise_error SystemExit, /user.rb needs to be updated, but annotaterb was run with `--frozen`./ 40 | end 41 | 42 | it "should NOT abort with same annotation when frozen: true " do 43 | annotate_one_file 44 | expect { annotate_one_file frozen: true }.not_to raise_error 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/config_generator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AnnotateRb::ConfigGenerator do 4 | describe "#default_config_yml" do 5 | subject { described_class.default_config_yml } 6 | 7 | let(:example_config_pair) { {models: true} } 8 | 9 | it "returns yml containing defaults" do 10 | expect(subject).to be_a(String) 11 | 12 | # Might be a better way to do this 13 | parsed = YAML.safe_load( 14 | subject, permitted_classes: [Regexp, Symbol], aliases: true, symbolize_names: true 15 | ) 16 | 17 | expect(parsed).to include(**example_config_pair) 18 | end 19 | end 20 | 21 | describe "#unset_config_defaults" do 22 | subject { described_class.unset_config_defaults } 23 | 24 | context "when user config has missing keys" do 25 | before do 26 | allow(AnnotateRb::ConfigLoader).to receive(:load_config).and_return({models: true}) 27 | end 28 | 29 | it "returns yaml with missing defaults" do 30 | expect(subject).to be_a(String) 31 | expect(subject).not_to be_empty 32 | expect(subject).not_to include("{}") 33 | end 34 | end 35 | 36 | context "when user config has all default keys" do 37 | before do 38 | complete_config = AnnotateRb::Options.from({}, {}).to_h 39 | allow(AnnotateRb::ConfigLoader).to receive(:load_config).and_return(complete_config) 40 | end 41 | 42 | it "returns empty string instead of empty hash" do 43 | expect(subject).to eq("") 44 | expect(subject).not_to include("{}") 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/config_loader_spec.rb: -------------------------------------------------------------------------------- 1 | require "tempfile" 2 | 3 | RSpec.describe AnnotateRb::ConfigLoader do 4 | before do 5 | expect(AnnotateRb::ConfigFinder).to receive(:find_project_dotfile).and_return(dotfile) 6 | end 7 | 8 | describe ".load_config" do 9 | subject { described_class.load_config } 10 | 11 | context "there is no config file" do 12 | let(:dotfile) { nil } 13 | 14 | it { is_expected.to eq({}) } 15 | end 16 | 17 | context "there is a plain config file" do 18 | let(:tempfile) { Tempfile.new("annotaterb") } 19 | let(:dotfile) { tempfile.path } 20 | 21 | around do |example| 22 | File.write(tempfile.path, <<~YAML) 23 | :model_dir: 24 | - app/models 25 | YAML 26 | example.run 27 | tempfile.unlink 28 | end 29 | 30 | it "reads the dotfile successfully" do 31 | expect(subject[:model_dir]).to eq(["app/models"]) 32 | end 33 | end 34 | 35 | context "the config file has ERB in it" do 36 | let(:tempfile) { Tempfile.new("annotaterb") } 37 | let(:dotfile) { tempfile.path } 38 | 39 | around do |example| 40 | File.write(tempfile.path, <<~YAML) 41 | <% model_dir = %w[foo/models bar/models baz/models] %> 42 | :model_dir: <%= model_dir.inspect %> 43 | YAML 44 | example.run 45 | tempfile.unlink 46 | end 47 | 48 | it "reads the dotfile successfully" do 49 | expect(subject[:model_dir]).to eq(%w[foo/models bar/models baz/models]) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/core_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe AnnotateRb::Core do 2 | describe ".version" do 3 | subject { described_class.version } 4 | 5 | it { is_expected.to be_a(String) } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/model_annotator/annotation/main_header_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AnnotateRb::ModelAnnotator::Annotation::MainHeader do 4 | describe "#to_default" do 5 | subject { described_class.new(version, include_version).to_default } 6 | 7 | let(:version) { 0 } 8 | let(:include_version) { false } 9 | let(:expected_header) { "# == Schema Information" } 10 | 11 | it { is_expected.to eq(expected_header) } 12 | 13 | context "when version is non-zero and include version is true" do 14 | let(:version) { 100 } 15 | let(:include_version) { true } 16 | let(:expected_header) { "# == Schema Information\n# Schema version: 100" } 17 | 18 | it { is_expected.to eq(expected_header) } 19 | end 20 | 21 | context "when version is non-zero and include version is false" do 22 | let(:version) { 100 } 23 | let(:include_version) { false } 24 | 25 | it { is_expected.to eq(expected_header) } 26 | end 27 | 28 | context "when version is zero and include version is true" do 29 | let(:version) { 0 } 30 | let(:include_version) { true } 31 | 32 | it { is_expected.to eq(expected_header) } 33 | end 34 | end 35 | 36 | describe "#to_markdown" do 37 | subject { described_class.new(version, include_version).to_markdown } 38 | 39 | let(:version) { 0 } 40 | let(:include_version) { false } 41 | let(:expected_header) { "# ## Schema Information" } 42 | 43 | it { is_expected.to eq(expected_header) } 44 | 45 | context "when version is non-zero and include version is true" do 46 | let(:version) { 100 } 47 | let(:include_version) { true } 48 | let(:expected_header) { "# ## Schema Information\n# Schema version: 100" } 49 | 50 | it { is_expected.to eq(expected_header) } 51 | end 52 | 53 | context "when version is non-zero and include version is false" do 54 | let(:version) { 100 } 55 | let(:include_version) { false } 56 | 57 | it { is_expected.to eq(expected_header) } 58 | end 59 | 60 | context "when version is zero and include version is true" do 61 | let(:version) { 0 } 62 | let(:include_version) { true } 63 | 64 | it { is_expected.to eq(expected_header) } 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/model_annotator/annotation/markdown_header_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AnnotateRb::ModelAnnotator::Annotation::MarkdownHeader do 4 | subject { described_class.new(max_size) } 5 | let(:markdown_format) { subject.to_markdown } 6 | let(:default_format) { subject.to_default } 7 | 8 | context "using default format" do 9 | let(:max_size) { 0 } 10 | 11 | it { expect(default_format).to be_nil } 12 | end 13 | 14 | context "using markdown format" do 15 | let(:max_size) { 10 } 16 | let(:expected_header) do 17 | <<~HEADER.strip 18 | # ### Columns 19 | # 20 | # Name | Type | Attributes 21 | # ---------------- | ------------------ | --------------------------- 22 | HEADER 23 | end 24 | 25 | it "matches the expected header" do 26 | expect(markdown_format).to eq(expected_header) 27 | end 28 | end 29 | 30 | context "with a larger max size" do 31 | let(:max_size) { 20 } 32 | let(:expected_header) do 33 | <<~HEADER.strip 34 | # ### Columns 35 | # 36 | # Name | Type | Attributes 37 | # -------------------------- | ------------------ | --------------------------- 38 | HEADER 39 | end 40 | 41 | it "matches the expected header" do 42 | expect(markdown_format).to eq(expected_header) 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/model_annotator/annotation_diff_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AnnotateRb::ModelAnnotator::AnnotationDiff do 4 | describe "attributes" do 5 | subject { described_class.new(current_columns, new_columns) } 6 | let(:current_columns) { "some current columns string" } 7 | let(:new_columns) { "some new columns string" } 8 | 9 | it "returns the current columns" do 10 | expect(subject.current_columns).to eq(current_columns) 11 | end 12 | 13 | it "returns the new columns" do 14 | expect(subject.new_columns).to eq(new_columns) 15 | end 16 | end 17 | 18 | describe "#changed?" do 19 | subject { described_class.new(current_columns, new_columns).changed? } 20 | 21 | context "when the current and new columns are the same" do 22 | let(:current_columns) { "the same" } 23 | let(:new_columns) { "the same" } 24 | 25 | it { is_expected.to eq(false) } 26 | end 27 | 28 | context "when the current and new columns are different" do 29 | let(:current_columns) { "the current" } 30 | let(:new_columns) { "the new" } 31 | 32 | it { is_expected.to eq(true) } 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/model_annotator/column_annotation/column_wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AnnotateRb::ModelAnnotator::ColumnAnnotation::ColumnWrapper do 4 | include AnnotateTestHelpers 5 | 6 | describe "#default_string" do 7 | subject { described_class.new(column, column_defaults, options).default_string } 8 | let(:column) { mock_column("field", nil, default: value) } 9 | let(:column_defaults) { {"field" => value} } 10 | let(:options) { {} } 11 | 12 | context "when the value is nil" do 13 | let(:value) { nil } 14 | it 'returns string "NULL"' do 15 | is_expected.to eq("NULL") 16 | end 17 | end 18 | 19 | context "when the value is true" do 20 | let(:value) { true } 21 | it 'returns string "TRUE"' do 22 | is_expected.to eq("TRUE") 23 | end 24 | end 25 | 26 | context "when the value is false" do 27 | let(:value) { false } 28 | it 'returns string "FALSE"' do 29 | is_expected.to eq("FALSE") 30 | end 31 | end 32 | 33 | context "when the value is an integer" do 34 | let(:value) { 25 } 35 | it "returns the integer as a string" do 36 | is_expected.to eq("25") 37 | end 38 | end 39 | 40 | context "when the value is a float number" do 41 | context "when the value is like 25.6" do 42 | let(:value) { 25.6 } 43 | it "returns the float number as a string" do 44 | is_expected.to eq("25.6") 45 | end 46 | end 47 | 48 | context "when the value is like 1e-20" do 49 | let(:value) { 1e-20 } 50 | it "returns the float number as a string" do 51 | is_expected.to eq("1.0e-20") 52 | end 53 | end 54 | end 55 | 56 | context "when the value is a BigDecimal number" do 57 | let(:value) { BigDecimal("1.2") } 58 | it "returns the float number as a string" do 59 | is_expected.to eq("1.2") 60 | end 61 | end 62 | 63 | context "when the value is an array" do 64 | let(:value) { [BigDecimal("1.2")] } 65 | it "returns an array of which elements are converted to string" do 66 | is_expected.to eq("[1.2]") 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/model_annotator/column_annotation/default_value_builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AnnotateRb::ModelAnnotator::ColumnAnnotation::DefaultValueBuilder do 4 | describe "#build" do 5 | subject { described_class.new(value, config).build } 6 | let(:config) { {} } 7 | 8 | context "when value is a String" do 9 | let(:value) { "a random string" } 10 | 11 | it { is_expected.to eq("\"a random string\"") } 12 | end 13 | 14 | context "when value is nil" do 15 | let(:value) { nil } 16 | 17 | it { is_expected.to eq("NULL") } 18 | end 19 | 20 | context "when value is true" do 21 | let(:value) { true } 22 | 23 | it { is_expected.to eq("TRUE") } 24 | end 25 | 26 | context "when value is false" do 27 | let(:value) { false } 28 | 29 | it { is_expected.to eq("FALSE") } 30 | end 31 | 32 | context "when value is an Integer" do 33 | let(:value) { 42 } 34 | 35 | it { is_expected.to eq("42") } 36 | end 37 | 38 | context "when value is an Decimal" do 39 | let(:value) { 1.2 } 40 | 41 | it { is_expected.to eq("1.2") } 42 | end 43 | 44 | context "when value is a BigDecimal" do 45 | let(:value) { BigDecimal("1.2") } 46 | 47 | it { is_expected.to eq("1.2") } 48 | end 49 | 50 | context "when value is an Array" do 51 | context "array is empty" do 52 | let(:value) { [] } 53 | 54 | it { is_expected.to eq("[]") } 55 | end 56 | 57 | context "array has a String" do 58 | let(:value) { ["string"] } 59 | 60 | it { is_expected.to eq("[\"string\"]") } 61 | end 62 | 63 | context "array has Strings" do 64 | let(:value) { ["a", "string"] } 65 | 66 | it { is_expected.to eq("[\"a\", \"string\"]") } 67 | end 68 | 69 | context "array has Numbers" do 70 | let(:value) { [42, 1.2] } 71 | 72 | it { is_expected.to eq("[42, 1.2]") } 73 | end 74 | 75 | context "array has BigDecimals" do 76 | let(:value) { [BigDecimal("0.1"), BigDecimal("0.2")] } 77 | 78 | it { is_expected.to eq("[0.1, 0.2]") } 79 | end 80 | 81 | context "array has Booleans" do 82 | let(:value) { [true, false] } 83 | 84 | it { is_expected.to eq("[TRUE, FALSE]") } 85 | end 86 | end 87 | 88 | context "with specified `classes_default_to_s` config option" do 89 | let(:config) { {classes_default_to_s: ["Integer"]} } 90 | 91 | context "when value is an Integer" do 92 | let(:value) { 42 } 93 | 94 | it { is_expected.to eq("\"42\"") } 95 | end 96 | 97 | context "when config option includes unloaded class" do 98 | let(:config) { {classes_default_to_s: ["Locale", "Float"]} } 99 | let(:value) { 42 } 100 | 101 | it "does not fail" do 102 | expect(subject).to eq("42") 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/model_annotator/file_name_resolver_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe AnnotateRb::ModelAnnotator::FileNameResolver do 2 | describe ".call" do 3 | subject do 4 | described_class.call(filename_template, model_name, table_name) 5 | end 6 | 7 | context 'When model_name is "example_model" and table_name is "example_models"' do 8 | let(:model_name) { "example_model" } 9 | let(:table_name) { "example_models" } 10 | 11 | context "when filename_template is 'test/unit/%MODEL_NAME%_test.rb'" do 12 | let(:filename_template) { "test/unit/%MODEL_NAME%_test.rb" } 13 | 14 | it "returns the test path for a model" do 15 | is_expected.to eq "test/unit/example_model_test.rb" 16 | end 17 | end 18 | 19 | context "when filename_template is '/foo/bar/%MODEL_NAME%/testing.rb'" do 20 | let(:filename_template) { "/foo/bar/%MODEL_NAME%/testing.rb" } 21 | 22 | it "returns the additional glob" do 23 | is_expected.to eq "/foo/bar/example_model/testing.rb" 24 | end 25 | end 26 | 27 | context "when filename_template is '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb'" do 28 | let(:filename_template) { "/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb" } 29 | 30 | it "returns the additional glob" do 31 | is_expected.to eq "/foo/bar/example_models/testing.rb" 32 | end 33 | end 34 | 35 | context "when filename_template is 'test/fixtures/%TABLE_NAME%.yml'" do 36 | let(:filename_template) { "test/fixtures/%TABLE_NAME%.yml" } 37 | 38 | it "returns the fixture path for a model" do 39 | is_expected.to eq "test/fixtures/example_models.yml" 40 | end 41 | end 42 | end 43 | 44 | context 'When model_name is "parent/child" and table_name is "parent_children"' do 45 | let(:model_name) { "parent/child" } 46 | let(:table_name) { "parent_children" } 47 | 48 | context "when filename_template is 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml'" do 49 | let(:filename_template) { "test/fixtures/%PLURALIZED_MODEL_NAME%.yml" } 50 | 51 | it "returns the fixture path for a nested model" do 52 | is_expected.to eq "test/fixtures/parent/children.yml" 53 | end 54 | end 55 | end 56 | 57 | context 'When model_name is "collapsed/test_model" and table_name is "collapsed_test_model"' do 58 | let(:model_name) { "collapsed/test_model" } 59 | let(:table_name) { "collapsed_test_models" } 60 | 61 | context "when filename_template is 'spec/models/collapsed/example/%MODEL_NAME_WITHOUT_NS%_spec.rb'" do 62 | let(:filename_template) { "spec/models/collapsed/example/%MODEL_NAME_WITHOUT_NS%_spec.rb" } 63 | 64 | it "returns the custom spec path for a collapsed model" do 65 | is_expected.to eq "spec/models/collapsed/example/test_model_spec.rb" 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /spec/lib/annotate_rb/model_annotator/file_to_parser_mapper_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe AnnotateRb::ModelAnnotator::FileToParserMapper do 2 | describe ".map" do 3 | subject { described_class.map(file_name) } 4 | let(:custom_parser) { AnnotateRb::ModelAnnotator::FileParser::CustomParser } 5 | let(:yml_parser) { AnnotateRb::ModelAnnotator::FileParser::YmlParser } 6 | 7 | context "when it is a ruby file" do 8 | let(:file_name) { "some_path/script.rb" } 9 | 10 | it { is_expected.to eq(custom_parser) } 11 | end 12 | 13 | context "when it is a yml file" do 14 | let(:file_name) { "some_path/some_file.yml" } 15 | 16 | it { is_expected.to eq(yml_parser) } 17 | end 18 | 19 | context "when it is a non Ruby file" do 20 | let(:file_name) { "some_path/some_file.abc" } 21 | 22 | it "raises an error" do 23 | expect { subject }.to raise_error(described_class::UnsupportedFileTypeError) 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "rake" 3 | require "active_support" 4 | require "active_support/core_ext/string" 5 | require "active_support/core_ext/object/blank" 6 | require "active_support/core_ext/class/subclasses" 7 | require "active_support/core_ext/string/inflections" 8 | require "byebug" 9 | require "bigdecimal" 10 | require "tmpdir" 11 | 12 | require "annotate_rb" 13 | 14 | # Requires supporting files with custom matchers and macros, etc, 15 | # in ./support/ and its subdirectories. 16 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f } 17 | 18 | RSpec.configure do |config| 19 | config.filter_run_when_matching :focus 20 | config.include(SpecHelper::Aruba, type: :aruba) 21 | 22 | config.before(:example, type: :aruba) do 23 | copy_dummy_app_into_aruba_working_directory 24 | 25 | # Unset the bundler context from running annotaterb integration specs. 26 | # This way, when `run_command("bundle exec annotaterb")` runs, it runs as if it's within the context of dummyapp. 27 | unset_bundler_env_vars 28 | end 29 | 30 | # Enable flags like --only-failures and --next-failure 31 | config.example_status_persistence_file_path = ".rspec_status" 32 | 33 | # Disable RSpec exposing methods globally on `Module` and `main` 34 | config.disable_monkey_patching! 35 | 36 | config.expect_with :rspec do |c| 37 | c.syntax = :expect 38 | end 39 | 40 | config.order = :random 41 | 42 | config.include_context "isolated environment", :isolated_environment 43 | end 44 | -------------------------------------------------------------------------------- /spec/support/annotate_test_constants.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateTestConstants 4 | MAGIC_COMMENTS = [ 5 | "# encoding: UTF-8", 6 | "# coding: UTF-8", 7 | "# -*- coding: UTF-8 -*-", 8 | "#encoding: utf-8", 9 | "# encoding: utf-8", 10 | "# -*- encoding : utf-8 -*-", 11 | "# encoding: utf-8\n# frozen_string_literal: true", 12 | "# frozen_string_literal: true\n# encoding: utf-8", 13 | "# frozen_string_literal: true", 14 | "#frozen_string_literal: false", 15 | "# -*- frozen_string_literal : true -*-" 16 | ].freeze 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/aruba.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "aruba/rspec" 4 | 5 | module SpecHelper 6 | module Aruba 7 | def read_file(name) 8 | # Aruba's #read uses File.readlines, returning an Array 9 | read(name).join("\n") 10 | end 11 | 12 | def models_template_dir 13 | File.join(::Aruba.config.root_directory, "spec/templates/#{ENV["DATABASE_ADAPTER"]}") 14 | end 15 | 16 | def migrations_template_dir 17 | File.join(::Aruba.config.root_directory, "spec/templates/migrations") 18 | end 19 | 20 | def model_template(name) 21 | File.join(models_template_dir, name) 22 | end 23 | 24 | def dummyapp_model(name) 25 | File.join(aruba_working_directory, "app/models", name) 26 | end 27 | 28 | def aruba_working_directory 29 | File.expand_path("../../#{::Aruba.config.working_directory}", __dir__) 30 | end 31 | 32 | def dummy_app_directory 33 | File.expand_path("../../spec/dummyapp/", __dir__) 34 | end 35 | 36 | def copy_dummy_app_into_aruba_working_directory 37 | FileUtils.rm_rf(Dir.glob("#{aruba_working_directory}/**/*")) 38 | FileUtils.cp_r(Dir.glob("#{dummy_app_directory}/."), aruba_working_directory) 39 | end 40 | 41 | def reset_database 42 | run_command_and_stop("bin/rails db:drop db:create", fail_on_error: true, exit_timeout: 10) 43 | end 44 | 45 | def run_migrations 46 | run_command_and_stop("bin/rails db:migrate", fail_on_error: true, exit_timeout: 10) 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/support/custom_matchers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # From Rubocop 4 | 5 | RSpec::Matchers.define :exit_with_code do |code| 6 | supports_block_expectations 7 | 8 | actual = nil 9 | 10 | match do |block| 11 | begin 12 | block.call 13 | rescue SystemExit => e 14 | actual = e.status 15 | end 16 | actual && actual == code 17 | end 18 | 19 | failure_message do 20 | "expected block to call exit(#{code}) but exit" + 21 | (actual.nil? ? " not called" : "(#{actual}) was called") 22 | end 23 | 24 | failure_message_when_negated { "expected block not to call exit(#{code})" } 25 | 26 | description { "expect block to call exit(#{code})" } 27 | end 28 | -------------------------------------------------------------------------------- /spec/support/database_contexts.rb: -------------------------------------------------------------------------------- 1 | # Use this shared context to run specs in a simulated multi-database environment. 2 | # 3 | # This sets the `MULTI_DB_TEST` environment variable, which is used by the 4 | # dummy app's `database.yml` to include the secondary database configuration. 5 | RSpec.shared_context "when in a multi database environment" do 6 | before do 7 | set_environment_variable("MULTI_DB_TEST", "true") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/shared_contexts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tmpdir" 4 | 5 | RSpec.shared_context "isolated environment" do 6 | # Taken from Rubocop's shared_contexts.rb 7 | around do |example| 8 | Dir.mktmpdir do |tmpdir| 9 | # Make sure to expand all symlinks in the path first. Otherwise we may 10 | # get mismatched pathnames when loading config files later on. 11 | tmpdir = File.realpath(tmpdir) 12 | 13 | working_dir = File.join(tmpdir, "work") 14 | begin 15 | FileUtils.mkdir_p(working_dir) 16 | 17 | Dir.chdir(working_dir) { example.run } 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/templates/migrations/20231013230731_add_int_field_to_test_defaults.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddIntFieldToTestDefaults < ActiveRecord::Migration["#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}"] 4 | def change 5 | add_column :test_defaults, :int_field, :integer 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/templates/mysql2/collapsed_test_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: collapsed_test_models 6 | # 7 | # id :bigint not null, primary key 8 | # collapsed :boolean 9 | # name :string(255) 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | module Collapsed 14 | class TestModel < ApplicationRecord 15 | def self.table_name_prefix 16 | "collapsed_" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/mysql2/nested_position_collapsed_test_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Collapsed 4 | # == Schema Information 5 | # 6 | # Table name: collapsed_test_models 7 | # 8 | # id :bigint not null, primary key 9 | # collapsed :boolean 10 | # name :string(255) 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | class TestModel < ApplicationRecord 15 | def self.table_name_prefix 16 | "collapsed_" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/mysql2/test_child_default.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: test_child_defaults 4 | # 5 | # id :bigint not null, primary key 6 | # created_at :datetime not null 7 | # updated_at :datetime not null 8 | # test_default_id :bigint not null 9 | # 10 | # Indexes 11 | # 12 | # index_test_child_defaults_on_test_default_id (test_default_id) 13 | # 14 | # Foreign Keys 15 | # 16 | # fk_rails_... (test_default_id => test_defaults.id) 17 | # 18 | class TestChildDefault < ApplicationRecord 19 | belongs_to :test_default 20 | end 21 | -------------------------------------------------------------------------------- /spec/templates/mysql2/test_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :bigint not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float(24) default(12.34) 13 | # integer :integer default(99) 14 | # string :string(255) default("hello world!") 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # 18 | class TestDefault < ApplicationRecord 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/mysql2/test_default_updated.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :bigint not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float(24) default(12.34) 13 | # int_field :integer 14 | # integer :integer default(99) 15 | # string :string(255) default("hello world!") 16 | # created_at :datetime not null 17 | # updated_at :datetime not null 18 | # 19 | class TestDefault < ApplicationRecord 20 | end 21 | -------------------------------------------------------------------------------- /spec/templates/mysql2/test_default_with_bottom_annotations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class TestDefault < ApplicationRecord 3 | end 4 | 5 | # == Schema Information 6 | # 7 | # Table name: test_defaults 8 | # 9 | # id :bigint not null, primary key 10 | # boolean :boolean default(FALSE) 11 | # date :date default(Tue, 04 Jul 2023) 12 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 13 | # decimal :decimal(14, 2) default(43.21) 14 | # float :float(24) default(12.34) 15 | # integer :integer default(99) 16 | # string :string(255) default("hello world!") 17 | # created_at :datetime not null 18 | # updated_at :datetime not null 19 | # 20 | -------------------------------------------------------------------------------- /spec/templates/mysql2/test_null_false.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_null_falses 6 | # 7 | # id :bigint not null, primary key 8 | # binary :binary(65535) not null 9 | # boolean :boolean not null 10 | # date :date not null 11 | # datetime :datetime not null 12 | # decimal :decimal(14, 2) not null 13 | # float :float(24) not null 14 | # integer :integer not null 15 | # string :string(255) not null 16 | # text :text(65535) not null 17 | # timestamp :datetime not null 18 | # created_at :datetime not null 19 | # updated_at :datetime not null 20 | # 21 | # Indexes 22 | # 23 | # by_compound_bool_and_int (boolean,integer) 24 | # index_test_null_falses_on_date (date) 25 | # 26 | class TestNullFalse < ApplicationRecord 27 | end 28 | -------------------------------------------------------------------------------- /spec/templates/mysql2/test_parent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_parents 6 | # 7 | # id :integer not null, primary key 8 | # something :string 9 | # type :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | class TestParent < ApplicationRecord 14 | end 15 | -------------------------------------------------------------------------------- /spec/templates/mysql2/test_sibling_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :bigint not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float(24) default(12.34) 13 | # integer :integer default(99) 14 | # string :string(255) default("hello world!") 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # 18 | class TestSiblingDefault < TestDefault 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/mysql2/test_true_sti.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_parents 6 | # 7 | # id :bigint not null, primary key 8 | # something :string(255) 9 | # type :string(255) 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | class TestTrueSti < TestParent 14 | end 15 | -------------------------------------------------------------------------------- /spec/templates/pg/collapsed_test_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: collapsed_test_models 6 | # 7 | # id :bigint not null, primary key 8 | # collapsed :boolean 9 | # name :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | module Collapsed 14 | class TestModel < ApplicationRecord 15 | def self.table_name_prefix 16 | "collapsed_" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/pg/nested_position_collapsed_test_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Collapsed 4 | # == Schema Information 5 | # 6 | # Table name: collapsed_test_models 7 | # 8 | # id :bigint not null, primary key 9 | # collapsed :boolean 10 | # name :string 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | class TestModel < ApplicationRecord 15 | def self.table_name_prefix 16 | "collapsed_" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/pg/test_child_default.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: test_child_defaults 4 | # 5 | # id :bigint not null, primary key 6 | # created_at :datetime not null 7 | # updated_at :datetime not null 8 | # test_default_id :bigint not null 9 | # 10 | # Indexes 11 | # 12 | # index_test_child_defaults_on_test_default_id (test_default_id) 13 | # 14 | # Foreign Keys 15 | # 16 | # fk_rails_... (test_default_id => test_defaults.id) 17 | # 18 | class TestChildDefault < ApplicationRecord 19 | belongs_to :test_default 20 | end 21 | -------------------------------------------------------------------------------- /spec/templates/pg/test_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :bigint not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float default(12.34) 13 | # integer :integer default(99) 14 | # string :string default("hello world!") 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # 18 | class TestDefault < ApplicationRecord 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/pg/test_default_updated.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :bigint not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float default(12.34) 13 | # int_field :integer 14 | # integer :integer default(99) 15 | # string :string default("hello world!") 16 | # created_at :datetime not null 17 | # updated_at :datetime not null 18 | # 19 | class TestDefault < ApplicationRecord 20 | end 21 | -------------------------------------------------------------------------------- /spec/templates/pg/test_default_with_bottom_annotations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class TestDefault < ApplicationRecord 3 | end 4 | 5 | # == Schema Information 6 | # 7 | # Table name: test_defaults 8 | # 9 | # id :bigint not null, primary key 10 | # boolean :boolean default(FALSE) 11 | # date :date default(Tue, 04 Jul 2023) 12 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 13 | # decimal :decimal(14, 2) default(43.21) 14 | # float :float default(12.34) 15 | # integer :integer default(99) 16 | # string :string default("hello world!") 17 | # created_at :datetime not null 18 | # updated_at :datetime not null 19 | # 20 | -------------------------------------------------------------------------------- /spec/templates/pg/test_null_false.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_null_falses 6 | # 7 | # id :bigint not null, primary key 8 | # binary :binary not null 9 | # boolean :boolean not null 10 | # date :date not null 11 | # datetime :datetime not null 12 | # decimal :decimal(14, 2) not null 13 | # float :float not null 14 | # integer :integer not null 15 | # string :string not null 16 | # text :text not null 17 | # timestamp :datetime not null 18 | # created_at :datetime not null 19 | # updated_at :datetime not null 20 | # 21 | # Indexes 22 | # 23 | # by_compound_bool_and_int (boolean,integer) 24 | # index_test_null_falses_on_date (date) 25 | # 26 | class TestNullFalse < ApplicationRecord 27 | end 28 | -------------------------------------------------------------------------------- /spec/templates/pg/test_parent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_parents 6 | # 7 | # id :integer not null, primary key 8 | # something :string 9 | # type :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | class TestParent < ApplicationRecord 14 | end 15 | -------------------------------------------------------------------------------- /spec/templates/pg/test_sibling_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :bigint not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float default(12.34) 13 | # integer :integer default(99) 14 | # string :string default("hello world!") 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # 18 | class TestSiblingDefault < TestDefault 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/pg/test_true_sti.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_parents 6 | # 7 | # id :bigint not null, primary key 8 | # something :string 9 | # type :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | class TestTrueSti < TestParent 14 | end 15 | -------------------------------------------------------------------------------- /spec/templates/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Route Map 4 | # 5 | # Prefix Verb URI Pattern Controller#Action 6 | # root GET / articles#index 7 | # resources GET /resources(.:format) resources#index 8 | # POST /resources(.:format) resources#create 9 | # new_resource GET /resources/new(.:format) resources#new 10 | # edit_resource GET /resources/:id/edit(.:format) resources#edit 11 | # resource GET /resources/:id(.:format) resources#show 12 | # PATCH /resources/:id(.:format) resources#update 13 | # PUT /resources/:id(.:format) resources#update 14 | # DELETE /resources/:id(.:format) resources#destroy 15 | # new_singular_resource GET /singular_resource/new(.:format) singular_resources#new 16 | # edit_singular_resource GET /singular_resource/edit(.:format) singular_resources#edit 17 | # singular_resource GET /singular_resource(.:format) singular_resources#show 18 | # PATCH /singular_resource(.:format) singular_resources#update 19 | # PUT /singular_resource(.:format) singular_resources#update 20 | # DELETE /singular_resource(.:format) singular_resources#destroy 21 | # POST /singular_resource(.:format) singular_resources#create 22 | # manual GET /manual(.:format) manual#show 23 | 24 | Rails.application.routes.draw do 25 | root "articles#index" 26 | resources :resources 27 | resource :singular_resource 28 | 29 | get "/manual", to: "manual#show" 30 | end 31 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/collapsed_test_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: collapsed_test_models 6 | # 7 | # id :integer not null, primary key 8 | # collapsed :boolean 9 | # name :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | module Collapsed 14 | class TestModel < ApplicationRecord 15 | def self.table_name_prefix 16 | "collapsed_" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/nested_position_collapsed_test_model.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Collapsed 4 | # == Schema Information 5 | # 6 | # Table name: collapsed_test_models 7 | # 8 | # id :integer not null, primary key 9 | # collapsed :boolean 10 | # name :string 11 | # created_at :datetime not null 12 | # updated_at :datetime not null 13 | # 14 | class TestModel < ApplicationRecord 15 | def self.table_name_prefix 16 | "collapsed_" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/test_child_default.rb: -------------------------------------------------------------------------------- 1 | # == Schema Information 2 | # 3 | # Table name: test_child_defaults 4 | # 5 | # id :integer not null, primary key 6 | # created_at :datetime not null 7 | # updated_at :datetime not null 8 | # test_default_id :integer not null 9 | # 10 | # Indexes 11 | # 12 | # index_test_child_defaults_on_test_default_id (test_default_id) 13 | # 14 | # Foreign Keys 15 | # 16 | # test_default_id (test_default_id => test_defaults.id) 17 | # 18 | class TestChildDefault < ApplicationRecord 19 | belongs_to :test_default 20 | end 21 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/test_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :integer not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float default(12.34) 13 | # integer :integer default(99) 14 | # string :string default("hello world!") 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # 18 | class TestDefault < ApplicationRecord 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/test_default_updated.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :integer not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float default(12.34) 13 | # int_field :integer 14 | # integer :integer default(99) 15 | # string :string default("hello world!") 16 | # created_at :datetime not null 17 | # updated_at :datetime not null 18 | # 19 | class TestDefault < ApplicationRecord 20 | end 21 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/test_default_with_bottom_annotations.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class TestDefault < ApplicationRecord 3 | end 4 | 5 | # == Schema Information 6 | # 7 | # Table name: test_defaults 8 | # 9 | # id :integer not null, primary key 10 | # boolean :boolean default(FALSE) 11 | # date :date default(Tue, 04 Jul 2023) 12 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 13 | # decimal :decimal(14, 2) default(43.21) 14 | # float :float default(12.34) 15 | # integer :integer default(99) 16 | # string :string default("hello world!") 17 | # created_at :datetime not null 18 | # updated_at :datetime not null 19 | # 20 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/test_null_false.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_null_falses 6 | # 7 | # id :integer not null, primary key 8 | # binary :binary not null 9 | # boolean :boolean not null 10 | # date :date not null 11 | # datetime :datetime not null 12 | # decimal :decimal(14, 2) not null 13 | # float :float not null 14 | # integer :integer not null 15 | # string :string not null 16 | # text :text not null 17 | # timestamp :datetime not null 18 | # created_at :datetime not null 19 | # updated_at :datetime not null 20 | # 21 | # Indexes 22 | # 23 | # by_compound_bool_and_int (boolean,integer) 24 | # index_test_null_falses_on_date (date) 25 | # 26 | class TestNullFalse < ApplicationRecord 27 | end 28 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/test_parent.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_parents 6 | # 7 | # id :integer not null, primary key 8 | # something :string 9 | # type :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | class TestParent < ApplicationRecord 14 | end 15 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/test_sibling_default.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_defaults 6 | # 7 | # id :integer not null, primary key 8 | # boolean :boolean default(FALSE) 9 | # date :date default(Tue, 04 Jul 2023) 10 | # datetime :datetime default(Tue, 04 Jul 2023 12:34:56.000000000 UTC +00:00) 11 | # decimal :decimal(14, 2) default(43.21) 12 | # float :float default(12.34) 13 | # integer :integer default(99) 14 | # string :string default("hello world!") 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # 18 | class TestSiblingDefault < TestDefault 19 | end 20 | -------------------------------------------------------------------------------- /spec/templates/sqlite3/test_true_sti.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # == Schema Information 4 | # 5 | # Table name: test_parents 6 | # 7 | # id :integer not null, primary key 8 | # something :string 9 | # type :string 10 | # created_at :datetime not null 11 | # updated_at :datetime not null 12 | # 13 | class TestTrueSti < TestParent 14 | end 15 | --------------------------------------------------------------------------------