├── .dockerignore ├── .document ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .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 │ │ ├── application_record.rb │ │ ├── collapsed │ │ │ └── example │ │ │ │ └── test_model.rb │ │ ├── test_child_default.rb │ │ ├── test_default.rb │ │ └── test_null_false.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 │ │ ├── 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 │ └── seeds.rb ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ └── robots.txt └── test │ └── test_helper.rb ├── integration ├── annotate_after_migration_spec.rb ├── annotate_collapsed_models_spec.rb ├── annotate_file_with_existing_annotations_spec.rb ├── annotate_model_with_foreign_key_spec.rb ├── annotate_routes_spec.rb ├── annotate_single_file_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 │ ├── 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_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 └── shared_contexts.rb └── templates ├── migrations └── 20231013230731_add_int_field_to_test_defaults.rb ├── mysql2 ├── 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 ├── pg ├── 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 ├── routes.rb └── sqlite3 ├── 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 /.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: ['2.7', '3.0', '3.1', '3.2', '3.3'] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 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: [ '2.7', '3.0', '3.1', '3.2', '3.3' ] 45 | 46 | steps: 47 | - name: Checkout 48 | uses: actions/checkout@v4 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@v4 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 | /tmp/* 24 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation 3 | --require spec_helper -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: 2 | - .rubocop_todo.yml 3 | 4 | require: 5 | - rubocop-rake 6 | - rubocop-rspec 7 | 8 | AllCops: 9 | Exclude: 10 | - 'vendor/**/*' 11 | - 'spec/fixtures/**/*' 12 | - 'tmp/**/*' 13 | - 'spec/integration/**/*' 14 | NewCops: enable 15 | 16 | Metrics/BlockLength: 17 | Exclude: 18 | - 'spec/**/*.rb' 19 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | parallel: true 2 | format: progress 3 | ruby_version: 2.7 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 | 9 | group :development, :test do 10 | gem "aruba", "~> 2.1.0", require: false 11 | gem "byebug" 12 | gem "guard-rspec", require: false 13 | 14 | gem "rspec" 15 | 16 | gem "standard", "~> 1.29.0" 17 | gem "terminal-notifier-guard", require: false 18 | 19 | platforms :mri, :mingw do 20 | gem "pry", require: false 21 | gem "pry-byebug", require: false 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /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.15.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 = ">= 2.7.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 | # Generates proper YAML including the leading hyphens `---` header 17 | yml_content = YAML.dump(result, StringIO.new).string 18 | # Remove the header 19 | yml_content.sub("---", "") 20 | end 21 | 22 | def default_config_yml 23 | defaults_hash = Options.from({}, {}).to_h 24 | _yml_content = YAML.dump(defaults_hash, StringIO.new).string 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/annotate_rb/config_loader.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | # Raised when a configuration file is not found. 5 | class ConfigNotFoundError < StandardError 6 | end 7 | 8 | class ConfigLoader 9 | class << self 10 | def load_config 11 | config_path = ConfigFinder.find_project_dotfile 12 | 13 | if config_path 14 | load_yaml_configuration(config_path) 15 | else 16 | {} 17 | end 18 | end 19 | 20 | # Method from Rubocop::ConfigLoader 21 | def load_yaml_configuration(absolute_path) 22 | file_contents = read_file(absolute_path) 23 | 24 | hash = yaml_safe_load(file_contents, absolute_path) || {} 25 | 26 | # TODO: Print config if debug flag/option is set 27 | 28 | raise(TypeError, "Malformed configuration in #{absolute_path}") unless hash.is_a?(Hash) 29 | 30 | hash 31 | end 32 | 33 | # Read the specified file, or exit with a friendly, concise message on 34 | # stderr. Care is taken to use the standard OS exit code for a "file not 35 | # found" error. 36 | # 37 | # Method from Rubocop::ConfigLoader 38 | def read_file(absolute_path) 39 | File.read(absolute_path, encoding: Encoding::UTF_8) 40 | rescue Errno::ENOENT 41 | raise ConfigNotFoundError, "Configuration file not found: #{absolute_path}" 42 | end 43 | 44 | # Method from Rubocop::ConfigLoader 45 | def yaml_safe_load(yaml_code, filename) 46 | yaml_safe_load!(yaml_code, filename) 47 | rescue 48 | if defined?(::SafeYAML) 49 | raise "SafeYAML is unmaintained, no longer needed and should be removed" 50 | end 51 | 52 | raise 53 | end 54 | 55 | # Method from Rubocop::ConfigLoader 56 | def yaml_safe_load!(yaml_code, filename) 57 | YAML.safe_load( 58 | yaml_code, permitted_classes: [Regexp, Symbol], aliases: true, filename: filename, symbolize_names: true 59 | ) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /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?(::Rails::Application) 11 | if defined?(::Zeitwerk) 12 | # Delegate to Zeitwerk to load stuff as needed 13 | else 14 | klass = ::Rails::Application.send(:subclasses).first 15 | klass.eager_load! 16 | end 17 | else 18 | model_files = ModelAnnotator::ModelFilesGetter.call(options) 19 | model_files&.each do |model_file| 20 | require model_file 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /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 | if @options.get_state(:current_version).nil? 63 | migration_version = begin 64 | ActiveRecord::Migrator.current_version 65 | rescue 66 | 0 67 | end 68 | 69 | @options.set_state(:current_version, migration_version) 70 | end 71 | 72 | version = @options.get_state(:current_version) 73 | table_name = @model.table_name 74 | table_comment = @model.connection.try(:table_comment, @model.table_name) 75 | max_size = @model.max_schema_info_width 76 | 77 | _annotation = Annotation.new(@options, 78 | version: version, table_name: table_name, table_comment: table_comment, 79 | max_size: max_size, model: @model).build 80 | end 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /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 | klass_is_a_class = klass.is_a?(Class) 21 | # Methods such as #superclass only exist on a class. Because of how the code is structured, `klass` could be a 22 | # module that does not support the #superclass method, so we want to return early. 23 | return false if !klass_is_a_class 24 | 25 | klass_inherits_active_record_base = klass < ActiveRecord::Base 26 | klass_is_not_abstract = klass.respond_to?(:abstract_class?) && !klass.abstract_class? 27 | klass_table_exists = klass.respond_to?(:table_exists?) && klass.table_exists? 28 | 29 | not_sure_this_conditional = (!@options[:exclude_sti_subclasses] || !(klass.superclass < ActiveRecord::Base && klass.table_name == klass.superclass.table_name)) 30 | 31 | annotate_conditions = [ 32 | klass_is_a_class, 33 | klass_inherits_active_record_base, 34 | not_sure_this_conditional, 35 | klass_is_not_abstract, 36 | klass_table_exists 37 | ] 38 | 39 | to_annotate = annotate_conditions.all? 40 | 41 | return to_annotate 42 | rescue BadModelFileError => e 43 | unless @options[:ignore_unknown_models] 44 | warn "Unable to process #{@file}: #{e.message}" 45 | warn "\t" + e.backtrace.join("\n\t") if @options[:trace] 46 | end 47 | rescue => e 48 | warn "Unable to process #{@file}: #{e.message}" 49 | warn "\t" + e.backtrace.join("\n\t") if @options[:trace] 50 | end 51 | 52 | false 53 | end 54 | 55 | private 56 | 57 | def file_contains_skip_annotation 58 | file_string = File.exist?(@file) ? File.read(@file) : "" 59 | 60 | /#{SKIP_ANNOTATION_PREFIX}.*/o.match?(file_string) 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /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 ]+[\w*.`\[\]():]+(?:\(.*?\))?[\t ]+.+$/ 15 | 16 | class << self 17 | def call(file_content, annotation_block) 18 | new(file_content, annotation_block).generate 19 | end 20 | end 21 | 22 | # @param [String] file_content The current file content of the model file we intend to annotate 23 | # @param [String] annotation_block The annotation block we intend to write to the model file 24 | def initialize(file_content, annotation_block) 25 | @file_content = file_content 26 | @annotation_block = annotation_block 27 | end 28 | 29 | def generate 30 | # Ignore the Schema version line because it changes with each migration 31 | current_annotations = @file_content.match(HEADER_PATTERN).to_s 32 | new_annotations = @annotation_block.match(HEADER_PATTERN).to_s 33 | 34 | current_annotation_columns = if current_annotations.present? 35 | current_annotations.scan(COLUMN_PATTERN).sort 36 | else 37 | [] 38 | end 39 | 40 | new_annotation_columns = if new_annotations.present? 41 | new_annotations.scan(COLUMN_PATTERN).sort 42 | else 43 | [] 44 | end 45 | 46 | _result = AnnotationDiff.new(current_annotation_columns, new_annotation_columns) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /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 | is_primary_key = is_column_primary_key?(@model, @column.name) 16 | 17 | table_indices = @model.retrieve_indexes_from_table 18 | column_indices = table_indices.select { |ind| ind.columns.include?(@column.name) } 19 | column_defaults = @model.column_defaults 20 | 21 | column_attributes = AttributesBuilder.new(@column, @options, is_primary_key, column_indices, column_defaults).build 22 | formatted_column_type = TypeBuilder.new(@column, @options, column_defaults).build 23 | 24 | display_column_comments = @options[:with_comment] && @options[:with_column_comments] 25 | col_name = if display_column_comments && @model.with_comments? && @column.comment 26 | "#{@column.name}(#{@column.comment.gsub(/\n/, '\\n')})" 27 | else 28 | @column.name 29 | end 30 | 31 | _component = ColumnComponent.new(col_name, @max_size, formatted_column_type, column_attributes) 32 | end 33 | 34 | private 35 | 36 | # TODO: Simplify this conditional 37 | def is_column_primary_key?(model, column_name) 38 | if model.primary_key 39 | if model.primary_key.is_a?(Array) 40 | # If the model has multiple primary keys, check if this column is one of them 41 | if model.primary_key.collect(&:to_sym).include?(column_name.to_sym) 42 | return true 43 | end 44 | elsif column_name.to_sym == model.primary_key.to_sym 45 | # If model has 1 primary key, check if this column is it 46 | return true 47 | end 48 | end 49 | 50 | false 51 | end 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/column_annotation/column_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module ColumnAnnotation 6 | class ColumnComponent < Components::Base 7 | MD_TYPE_ALLOWANCE = 18 8 | BARE_TYPE_ALLOWANCE = 16 9 | 10 | attr_reader :name, :max_size, :type, :attributes 11 | 12 | def initialize(name, max_size, type, attributes) 13 | @name = name 14 | @max_size = max_size 15 | @type = type 16 | @attributes = attributes 17 | end 18 | 19 | def to_rdoc 20 | # standard:disable Lint/FormatParameterMismatch 21 | format("# %-#{max_size}.#{max_size}s%s", 22 | "*#{name}*::", 23 | attributes.unshift(type).join(", ")).rstrip 24 | # standard:enable Lint/FormatParameterMismatch 25 | end 26 | 27 | def to_yard 28 | res = "" 29 | res += sprintf("# @!attribute #{name}") + "\n" 30 | 31 | ruby_class = if @column.respond_to?(:array) && @column.array 32 | "Array<#{map_col_type_to_ruby_classes(type)}>" 33 | else 34 | map_col_type_to_ruby_classes(type) 35 | end 36 | 37 | res += sprintf("# @return [#{ruby_class}]") 38 | 39 | res 40 | end 41 | 42 | def to_markdown 43 | name_remainder = max_size - name.length - non_ascii_length(name) 44 | type_remainder = (MD_TYPE_ALLOWANCE - 2) - type.length 45 | 46 | # standard:disable Lint/FormatParameterMismatch 47 | format("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", 48 | name, 49 | " ", 50 | type, 51 | " ", 52 | attributes.join(", ").rstrip).gsub("``", " ").rstrip 53 | # standard:enable Lint/FormatParameterMismatch 54 | end 55 | 56 | def to_default 57 | format("# %s:%s %s", 58 | mb_chars_ljust(name, max_size), 59 | mb_chars_ljust(type, BARE_TYPE_ALLOWANCE), 60 | attributes.join(", ")).rstrip 61 | end 62 | 63 | private 64 | 65 | def mb_chars_ljust(string, length) 66 | string = string.to_s 67 | padding = length - Helper.width(string) 68 | if padding.positive? 69 | string + (" " * padding) 70 | else 71 | string[0..(length - 1)] 72 | end 73 | end 74 | 75 | def map_col_type_to_ruby_classes(col_type) 76 | case col_type 77 | when "integer" then Integer.to_s 78 | when "float" then Float.to_s 79 | when "decimal" then BigDecimal.to_s 80 | when "datetime", "timestamp", "time" then Time.to_s 81 | when "date" then Date.to_s 82 | when "text", "string", "binary", "inet", "uuid" then String.to_s 83 | when "json", "jsonb" then Hash.to_s 84 | when "boolean" then "Boolean" 85 | end 86 | end 87 | 88 | def non_ascii_length(string) 89 | string.to_s.chars.count { |element| !element.ascii_only? } 90 | end 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /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) 22 | end 23 | 24 | _annotation = Annotation.new(indexes) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/index_annotation/index_component.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | module IndexAnnotation 6 | class IndexComponent < Components::Base 7 | attr_reader :index, :max_size 8 | 9 | def initialize(index, max_size) 10 | @index = index 11 | @max_size = max_size 12 | end 13 | 14 | def to_default 15 | unique_info = index.unique ? " UNIQUE" : "" 16 | 17 | nulls_not_distinct_info = if index.try(:nulls_not_distinct) 18 | " NULLS NOT DISTINCT" 19 | else 20 | "" 21 | end 22 | 23 | value = index.try(:where).try(:to_s) 24 | where_info = if value.present? 25 | " WHERE #{value}" 26 | else 27 | "" 28 | end 29 | 30 | value = index.try(:using).try(:to_sym) 31 | using_info = if value.present? && value != :btree 32 | " USING #{value}" 33 | else 34 | "" 35 | end 36 | 37 | # standard:disable Lint/FormatParameterMismatch 38 | sprintf( 39 | "# %-#{max_size}.#{max_size}s %s%s%s%s%s", 40 | index.name, 41 | "(#{columns_info.join(",")})", 42 | unique_info, 43 | nulls_not_distinct_info, 44 | where_info, 45 | using_info 46 | ).rstrip 47 | # standard:enable Lint/FormatParameterMismatch 48 | end 49 | 50 | def to_markdown 51 | unique_info = index.unique ? " _unique_" : "" 52 | 53 | nulls_not_distinct_info = if index.try(:nulls_not_distinct) 54 | " _nulls_not_distinct_" 55 | else 56 | "" 57 | end 58 | 59 | value = index.try(:where).try(:to_s) 60 | where_info = if value.present? 61 | " _where_ #{value}" 62 | else 63 | "" 64 | end 65 | 66 | value = index.try(:using).try(:to_sym) 67 | using_info = if value.present? && value != :btree 68 | " _using_ #{value}" 69 | else 70 | "" 71 | end 72 | 73 | details = sprintf( 74 | "%s%s%s%s", 75 | unique_info, 76 | nulls_not_distinct_info, 77 | where_info, 78 | using_info 79 | ).strip 80 | details = " (#{details})" unless details.blank? 81 | 82 | sprintf( 83 | "# * `%s`%s:\n# * **`%s`**", 84 | index.name, 85 | details, 86 | columns_info.join("`**\n# * **`") 87 | ) 88 | end 89 | 90 | private 91 | 92 | def columns_info 93 | Array(index.columns).map do |col| 94 | if index.try(:orders) && index.orders[col.to_s] 95 | "#{col} #{index.orders[col.to_s].upcase}" 96 | else 97 | col.to_s.gsub("\r", '\r').gsub("\n", '\n') 98 | end 99 | end 100 | end 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/annotate_rb/model_annotator/model_class_getter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AnnotateRb 4 | module ModelAnnotator 5 | class ModelClassGetter 6 | class << self 7 | # Retrieve the classes belonging to the model names we're asked to process 8 | # Check for namespaced models in subdirectories as well as models 9 | # in subdirectories without namespacing. 10 | def call(file, options) 11 | use_zeitwerk = defined?(::Rails) && ::Rails.try(:autoloaders).try(:zeitwerk_enabled?) 12 | 13 | if use_zeitwerk 14 | klass = ZeitwerkClassGetter.call(file, options) 15 | return klass if klass 16 | end 17 | 18 | model_path = file.gsub(/\.rb$/, "") 19 | options[:model_dir].each { |dir| model_path = model_path.gsub(/^#{dir}/, "").gsub(/^\//, "") } 20 | 21 | begin 22 | get_loaded_model(model_path, file) || raise(BadModelFileError.new) 23 | rescue LoadError 24 | # this is for non-rails projects, which don't get Rails auto-require magic 25 | file_path = File.expand_path(file) 26 | if File.file?(file_path) && Kernel.require(file_path) 27 | retry 28 | elsif /\//.match?(model_path) 29 | model_path = model_path.split("/")[1..-1].join("/").to_s 30 | retry 31 | else 32 | raise 33 | end 34 | end 35 | end 36 | 37 | private 38 | 39 | # Retrieve loaded model class 40 | def get_loaded_model(model_path, file) 41 | loaded_model_class = get_loaded_model_by_path(model_path) 42 | return loaded_model_class if loaded_model_class 43 | 44 | # We cannot get loaded model when `model_path` is loaded by Rails 45 | # auto_load/eager_load paths. Try all possible model paths one by one. 46 | absolute_file = File.expand_path(file) 47 | model_paths = 48 | $LOAD_PATH.select { |path| absolute_file.include?(path) } 49 | .map { |path| absolute_file.sub(path, "").sub(/\.rb$/, "").sub(/^\//, "") } 50 | model_paths 51 | .map { |path| get_loaded_model_by_path(path) } 52 | .find { |loaded_model| !loaded_model.nil? } 53 | end 54 | 55 | # Retrieve loaded model class by path to the file where it's supposed to be defined. 56 | def get_loaded_model_by_path(model_path) 57 | ::ActiveSupport::Inflector.constantize(::ActiveSupport::Inflector.camelize(model_path)) 58 | rescue StandardError, LoadError 59 | # Revert to the old way but it is not really robust 60 | ObjectSpace.each_object(::Class) 61 | .select do |c| 62 | Class === c && # note: we use === to avoid a bug in activesupport 2.3.14 OptionMerger vs. is_a? 63 | c.ancestors.respond_to?(:include?) && # to fix FactoryGirl bug, see https://github.com/ctran/annotate_models/pull/82 64 | c.ancestors.include?(::ActiveRecord::Base) 65 | end.detect { |c| ::ActiveSupport::Inflector.underscore(c.to_s) == model_path } 66 | end 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /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 | else 36 | model_files 37 | end 38 | end 39 | 40 | private 41 | 42 | def list_model_files_from_argument(options) 43 | return [] if options.get_state(:working_args).empty? 44 | 45 | specified_files = options.get_state(:working_args).map { |file| File.expand_path(file) } 46 | 47 | model_files = options[:model_dir].flat_map do |dir| 48 | absolute_dir_path = File.expand_path(dir) 49 | specified_files 50 | .find_all { |file| file.start_with?(absolute_dir_path) } 51 | .map { |file| [dir, file.sub("#{absolute_dir_path}/", "")] } 52 | end 53 | 54 | if model_files.size != specified_files.size 55 | warn "The specified file could not be found in directory '#{options[:model_dir].join("', '")}'." 56 | warn "Call 'annotaterb --help' for more info." 57 | # exit 1 # TODO: Return exit code back to caller. Right now it messes up RSpec being able to run 58 | end 59 | 60 | model_files 61 | end 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /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 | table_name = klass.table_name 46 | 47 | model_instruction = SingleFileAnnotatorInstruction.new(file, annotation, :position_in_class, @options) 48 | instructions << model_instruction 49 | 50 | related_files = RelatedFilesListBuilder.new(file, model_name, table_name, @options).build 51 | related_file_instructions = related_files.map do |f, position_key| 52 | _instruction = SingleFileAnnotatorInstruction.new(f, annotation, position_key, @options) 53 | end 54 | instructions.concat(related_file_instructions) 55 | 56 | if @options[:debug] 57 | puts "Built instructions for #{file} in #{Time.now - start}s" 58 | end 59 | 60 | instructions 61 | end 62 | 63 | def model_files 64 | @model_files ||= ModelFilesGetter.call(@options) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /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/base_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 BaseProcessor 8 | class << self 9 | # @param options [Hash] 10 | # @param routes_file [String] 11 | # @return [String] 12 | def execute(options, routes_file) 13 | new(options, routes_file).execute 14 | end 15 | 16 | private :new 17 | end 18 | 19 | def initialize(options, routes_file) 20 | @options = options 21 | @routes_file = routes_file 22 | end 23 | 24 | # @return [Boolean] 25 | def update 26 | if existing_text == new_text 27 | false 28 | else 29 | write(new_text) 30 | true 31 | end 32 | end 33 | 34 | def routes_file_exist? 35 | File.exist?(routes_file) 36 | end 37 | 38 | private 39 | 40 | attr_reader :options, :routes_file 41 | 42 | def generate_new_content_array(_content, _header_position) 43 | raise NoMethodError 44 | end 45 | 46 | def existing_text 47 | @existing_text ||= File.read(routes_file) 48 | end 49 | 50 | # @return [String] 51 | def new_text 52 | content, header_position = strip_annotations(existing_text) 53 | new_content = generate_new_content_array(content, header_position) 54 | new_content.join("\n") 55 | end 56 | 57 | def write(text) 58 | File.open(routes_file, "wb") { |f| f.puts(text) } 59 | end 60 | 61 | # TODO: write the method doc using ruby rdoc formats 62 | # This method returns an array of 'real_content' and 'header_position'. 63 | # 'header_position' will either be :before, :after, or 64 | # a number. If the number is > 0, the 65 | # annotation was found somewhere in the 66 | # middle of the file. If the number is 67 | # zero, no annotation was found. 68 | def strip_annotations(content) 69 | real_content = [] 70 | mode = :content 71 | header_position = 0 72 | 73 | content.split(/\n/, -1).each_with_index do |line, line_number| 74 | if mode == :header && line !~ /\s*#/ 75 | mode = :content 76 | real_content << line unless line.blank? 77 | elsif mode == :content 78 | if /^\s*#\s*== Route.*$/.match?(line) 79 | header_position = line_number + 1 # index start's at 0 80 | mode = :header 81 | else 82 | real_content << line 83 | end 84 | end 85 | end 86 | 87 | real_content_and_header_position(real_content, header_position) 88 | end 89 | 90 | def real_content_and_header_position(real_content, header_position) 91 | # By default assume the annotation was found in the middle of the file 92 | 93 | # ... unless we have evidence it was at the beginning ... 94 | return real_content, :before if header_position == 1 95 | 96 | # ... or that it was at the end. 97 | return real_content, :after if header_position >= real_content.count 98 | 99 | # and the default 100 | [real_content, header_position] 101 | end 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /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 | def run(args) 7 | new.run(args) 8 | end 9 | end 10 | 11 | def run(args) 12 | config_file_options = ConfigLoader.load_config 13 | parser = Parser.new(args, {}) 14 | 15 | parsed_options = parser.parse 16 | remaining_args = parser.remaining_args 17 | 18 | options = config_file_options.merge(parsed_options) 19 | 20 | @options = Options.from(options, {working_args: remaining_args}) 21 | AnnotateRb::RakeBootstrapper.call(@options) 22 | 23 | if @options[:command] 24 | @options[:command].call(@options) 25 | else 26 | # TODO 27 | raise "Didn't specify a command" 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /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].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.0.8" 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", "~> 5.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 | -------------------------------------------------------------------------------- /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/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/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/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) || abort("\n== Command #{args} failed ==") 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 | config.load_defaults "#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}" 24 | 25 | # Configuration for the application, engines, and railties goes here. 26 | # 27 | # These settings can be overridden in specific environments using the files 28 | # in config/environments, which are processed later. 29 | # 30 | # config.time_zone = "Central Time (US & Canada)" 31 | # config.eager_load_paths << Rails.root.join("extras") 32 | 33 | # Don't generate system test files. 34 | config.generators.system_tests = nil 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /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 | <% end %> 15 | 16 | <% if ENV['DATABASE_ADAPTER'] == 'pg' %> 17 | default: &default 18 | host: 127.0.0.1 19 | port: 5432 20 | adapter: postgresql 21 | username: postgres 22 | password: root 23 | encoding: utf8 24 | 25 | development: 26 | primary: 27 | <<: *default 28 | database: annotaterb_development 29 | <% end %> 30 | 31 | <% if ENV['DATABASE_ADAPTER'] == 'sqlite3' %> 32 | default: &default 33 | adapter: sqlite3 34 | 35 | development: 36 | primary: 37 | <<: *default 38 | database: db/development.sqlite3 39 | <% end %> 40 | -------------------------------------------------------------------------------- /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.cache_classes = false 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 | # Uncomment if you wish to allow Action Cable access from any origin. 59 | # config.action_cable.disable_request_forgery_protection = true 60 | end 61 | -------------------------------------------------------------------------------- /spec/dummyapp/config/environments/production.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 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both threaded web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 20 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 21 | # config.require_master_key = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? 26 | 27 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 28 | # config.asset_host = "http://assets.example.com" 29 | 30 | # Specifies the header that your server uses for sending files. 31 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache 32 | # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX 33 | 34 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 35 | # config.force_ssl = true 36 | 37 | # Include generic and useful information about system operation, but avoid logging too much 38 | # information to avoid inadvertent exposure of personally identifiable information (PII). 39 | config.log_level = :info 40 | 41 | # Prepend all log lines with the following tags. 42 | config.log_tags = [ :request_id ] 43 | 44 | # Use a different cache store in production. 45 | # config.cache_store = :mem_cache_store 46 | 47 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 48 | # the I18n.default_locale when a translation cannot be found). 49 | config.i18n.fallbacks = true 50 | 51 | # Don't log any deprecations. 52 | config.active_support.report_deprecations = false 53 | 54 | # Use default logging formatter so that PID and timestamp are not suppressed. 55 | config.log_formatter = ::Logger::Formatter.new 56 | 57 | # Use a different logger for distributed setups. 58 | # require "syslog/logger" 59 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name") 60 | 61 | if ENV["RAILS_LOG_TO_STDOUT"].present? 62 | logger = ActiveSupport::Logger.new(STDOUT) 63 | logger.formatter = config.log_formatter 64 | config.logger = ActiveSupport::TaggedLogging.new(logger) 65 | end 66 | 67 | # Do not dump schema after migrations. 68 | config.active_record.dump_schema_after_migration = false 69 | end 70 | -------------------------------------------------------------------------------- /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 | # Turn false under Spring and add config.action_view.cache_template_loading = true. 12 | config.cache_classes = true 13 | 14 | # Eager loading loads your whole application. When running a single test locally, 15 | # this probably isn't necessary. It's a good idea to do in a continuous integration 16 | # system, or in some way before deploying your code. 17 | config.eager_load = ENV["CI"].present? 18 | 19 | # Configure public file server for tests with Cache-Control for performance. 20 | config.public_file_server.enabled = true 21 | config.public_file_server.headers = { 22 | "Cache-Control" => "public, max-age=#{1.hour.to_i}" 23 | } 24 | 25 | # Show full error reports and disable caching. 26 | config.consider_all_requests_local = true 27 | config.action_controller.perform_caching = false 28 | config.cache_store = :null_store 29 | 30 | # Raise exceptions instead of rendering exception templates. 31 | config.action_dispatch.show_exceptions = false 32 | 33 | # Disable request forgery protection in test environment. 34 | config.action_controller.allow_forgery_protection = false 35 | 36 | # Print deprecation notices to the stderr. 37 | config.active_support.deprecation = :stderr 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 | # Raises error for missing translations. 46 | # config.i18n.raise_on_missing_translations = true 47 | 48 | # Annotate rendered view with file names. 49 | # config.action_view.annotate_rendered_view_with_filenames = true 50 | end 51 | -------------------------------------------------------------------------------- /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 and inline scripts 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-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 filtered from the log file. Use this to limit dissemination of 4 | # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported 5 | # 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 | # Define an application-wide HTTP permissions policy. For further 2 | # information see https://developers.google.com/web/updates/2018/06/feature-policy 3 | # 4 | # Rails.application.config.permissions_policy do |f| 5 | # f.camera :none 6 | # f.gyroscope :none 7 | # f.microphone :none 8 | # f.usb :none 9 | # f.fullscreen :self 10 | # f.payment :self, "https://secure.example.com" 11 | # end 12 | -------------------------------------------------------------------------------- /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/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/b66f119ed154f818b3b4937d487e6a76bd17be28/spec/dummyapp/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /spec/dummyapp/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drwl/annotaterb/b66f119ed154f818b3b4937d487e6a76bd17be28/spec/dummyapp/public/apple-touch-icon.png -------------------------------------------------------------------------------- /spec/dummyapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drwl/annotaterb/b66f119ed154f818b3b4937d487e6a76bd17be28/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/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_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_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/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 | end 21 | -------------------------------------------------------------------------------- /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/check_constraint_annotation/annotation_builder_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AnnotateRb::ModelAnnotator::CheckConstraintAnnotation::AnnotationBuilder do 4 | include AnnotateTestHelpers 5 | 6 | describe "#build" do 7 | subject { described_class.new(model, options).build } 8 | let(:default_format) { subject.to_default } 9 | let(:markdown_format) { subject.to_markdown } 10 | 11 | let(:model) do 12 | instance_double( 13 | AnnotateRb::ModelAnnotator::ModelWrapper, 14 | connection: connection, 15 | table_name: "Foo" 16 | ) 17 | end 18 | let(:connection) do 19 | mock_connection([], [], check_constraints) 20 | end 21 | let(:options) { AnnotateRb::Options.new({show_check_constraints: true}) } 22 | let(:check_constraints) do 23 | [ 24 | mock_check_constraint("alive", "age < 150"), 25 | mock_check_constraint("must_be_adult", "age >= 18"), 26 | mock_check_constraint("missing_expression", nil), 27 | mock_check_constraint("multiline_test", <<~SQL) 28 | CASE 29 | WHEN (age >= 18) THEN (age <= 21) 30 | ELSE true 31 | END 32 | SQL 33 | ] 34 | end 35 | 36 | context "when show_check_constraints option is false" do 37 | let(:options) { AnnotateRb::Options.new({show_check_constraints: false}) } 38 | 39 | it { is_expected.to be_a(AnnotateRb::ModelAnnotator::Components::NilComponent) } 40 | end 41 | 42 | context "using default format" do 43 | let(:expected_result) do 44 | <<~RESULT.strip 45 | # 46 | # Check Constraints 47 | # 48 | # alive (age < 150) 49 | # missing_expression 50 | # multiline_test (CASE WHEN (age >= 18) THEN (age <= 21) ELSE true END) 51 | # must_be_adult (age >= 18) 52 | RESULT 53 | end 54 | 55 | it "annotates the check constraints" do 56 | expect(default_format).to eq(expected_result) 57 | end 58 | end 59 | 60 | context "using markdown format" do 61 | let(:expected_result) do 62 | <<~RESULT.strip 63 | # 64 | # ### Check Constraints 65 | # 66 | # * `alive`: `(age < 150)` 67 | # * `missing_expression` 68 | # * `multiline_test`: `(CASE WHEN (age >= 18) THEN (age <= 21) ELSE true END)` 69 | # * `must_be_adult`: `(age >= 18)` 70 | RESULT 71 | end 72 | 73 | it "annotates the check constraints" do 74 | expect(markdown_format).to eq(expected_result) 75 | end 76 | end 77 | 78 | context "when model connection does not support check constraints" do 79 | let(:connection) do 80 | conn_options = {supports_check_constraints?: false} 81 | 82 | mock_connection([], [], [], conn_options) 83 | end 84 | 85 | it { expect(default_format).to be_nil } 86 | end 87 | 88 | context "when check constraints is empty" do 89 | let(:connection) do 90 | conn_options = {supports_check_constraints?: true} 91 | 92 | mock_connection([], [], [], conn_options) 93 | end 94 | 95 | it { expect(default_format).to be_nil } 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /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/lib/annotate_rb/runner_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe AnnotateRb::Runner do 4 | subject(:runner) { described_class.new } 5 | 6 | before do 7 | $stdout = StringIO.new 8 | $stderr = StringIO.new 9 | end 10 | 11 | after do 12 | $stdout = STDOUT 13 | $stderr = STDERR 14 | end 15 | 16 | describe "help option" do 17 | describe "-h/-?/--help" do 18 | it "shows help text" do 19 | runner.run(["-h"]) 20 | 21 | expect($stdout.string).to include(AnnotateRb::Parser::BANNER_STRING) 22 | end 23 | end 24 | 25 | describe "help" do 26 | it "shows help text" do 27 | runner.run(["help"]) 28 | 29 | expect($stdout.string).to include(AnnotateRb::Parser::BANNER_STRING) 30 | end 31 | end 32 | end 33 | 34 | describe "version option" do 35 | describe "-v/--version" do 36 | it "shows version text" do 37 | runner.run(["-v"]) 38 | 39 | version_string = AnnotateRb::Core.version 40 | 41 | expect($stdout.string).to include(version_string) 42 | end 43 | end 44 | 45 | describe "version" do 46 | it "shows version text" do 47 | runner.run(["version"]) 48 | 49 | version_string = AnnotateRb::Core.version 50 | 51 | expect($stdout.string).to include(version_string) 52 | end 53 | end 54 | end 55 | 56 | describe "Annotating models" do 57 | let(:args) { ["models"] } 58 | let(:command_double) { instance_double(AnnotateRb::Commands::AnnotateModels) } 59 | 60 | before do 61 | allow(AnnotateRb::Commands::AnnotateModels).to receive(:new).and_return(command_double) 62 | allow(command_double).to receive(:call) 63 | end 64 | 65 | it "calls the annotate models command" do 66 | runner.run(args) 67 | 68 | expect(command_double).to have_received(:call) 69 | end 70 | end 71 | 72 | describe "Annotating routes" do 73 | let(:args) { ["routes"] } 74 | let(:command_double) { instance_double(AnnotateRb::Commands::AnnotateRoutes) } 75 | 76 | before do 77 | allow(AnnotateRb::Commands::AnnotateRoutes).to receive(:new).and_return(command_double) 78 | allow(command_double).to receive(:call) 79 | end 80 | 81 | it "calls the annotate routes command" do 82 | runner.run(args) 83 | 84 | expect(command_double).to have_received(:call) 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/lib/tasks/annotate_models_migrate_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "ActiveRecord migration rake task hooks" do 2 | before do 3 | Rake.application = Rake::Application.new 4 | 5 | # Stub migration tasks 6 | %w[db:migrate db:migrate:up db:migrate:down db:migrate:reset db:rollback].each do |task| 7 | Rake::Task.define_task(task) 8 | end 9 | 10 | Rake::Task.define_task("db:migrate:redo") do 11 | Rake::Task["db:rollback"].invoke 12 | Rake::Task["db:migrate"].invoke 13 | end 14 | 15 | Rake.load_rakefile("annotate_rb/tasks/annotate_models_migrate.rake") 16 | 17 | Rake.application.instance_variable_set(:@top_level_tasks, [subject]) 18 | end 19 | 20 | describe "db:migrate" do 21 | it "should annotate model files" do 22 | expect(AnnotateRb::Runner).to receive(:run).with(a_collection_including("models")) 23 | Rake.application.top_level 24 | end 25 | end 26 | 27 | describe "db:migrate:up" do 28 | it "should annotate model files" do 29 | expect(AnnotateRb::Runner).to receive(:run).with(a_collection_including("models")) 30 | Rake.application.top_level 31 | end 32 | end 33 | 34 | describe "db:migrate:down" do 35 | it "should annotate model files" do 36 | expect(AnnotateRb::Runner).to receive(:run).with(a_collection_including("models")) 37 | Rake.application.top_level 38 | end 39 | end 40 | 41 | describe "db:migrate:reset" do 42 | it "should annotate model files" do 43 | expect(AnnotateRb::Runner).to receive(:run).with(a_collection_including("models")) 44 | Rake.application.top_level 45 | end 46 | end 47 | 48 | describe "db:rollback" do 49 | it "should annotate model files" do 50 | expect(AnnotateRb::Runner).to receive(:run).with(a_collection_including("models")) 51 | Rake.application.top_level 52 | end 53 | end 54 | 55 | describe "db:migrate:redo" do 56 | it "should annotate model files after all migration tasks" do 57 | # Hooked 3 times by db:rollback, db:migrate, and db:migrate:redo tasks 58 | expect(AnnotateRb::Runner).to receive(:run).with(a_collection_including("models")).exactly(3).times 59 | 60 | Rake.application.top_level 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /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.include(SpecHelper::Aruba, type: :aruba) 20 | 21 | config.before(:example, type: :aruba) do 22 | copy_dummy_app_into_aruba_working_directory 23 | 24 | # Unset the bundler context from running annotaterb integration specs. 25 | # This way, when `run_command("bundle exec annotaterb")` runs, it runs as if it's within the context of dummyapp. 26 | unset_bundler_env_vars 27 | end 28 | 29 | # Enable flags like --only-failures and --next-failure 30 | config.example_status_persistence_file_path = ".rspec_status" 31 | 32 | # Disable RSpec exposing methods globally on `Module` and `main` 33 | config.disable_monkey_patching! 34 | 35 | config.expect_with :rspec do |c| 36 | c.syntax = :expect 37 | end 38 | 39 | config.order = :random 40 | 41 | config.include_context "isolated environment", :isolated_environment 42 | end 43 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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 | --------------------------------------------------------------------------------