├── .github ├── rubocop.gemfile ├── rubocop.gemfile.lock └── workflows │ └── docs-lint.yml ├── .mdlrc ├── .rubocop-md.yml ├── LICENSE ├── README.md ├── assets └── map.png ├── examples ├── gems │ ├── README.md │ ├── shared-factory │ │ ├── .gitignore │ │ ├── .pryrc │ │ ├── .rubocop.yml │ │ ├── Gemfile │ │ ├── Gemfile.dev │ │ ├── Gemfile.runtime │ │ ├── README.md │ │ ├── Rakefile │ │ ├── bin │ │ │ └── console │ │ ├── lib │ │ │ ├── shared-factory.rb │ │ │ └── shared │ │ │ │ ├── factory.rb │ │ │ │ └── factory │ │ │ │ └── faker.rb │ │ └── shared-factory.gemspec │ ├── shared-rubocop │ │ ├── .gitignore │ │ ├── .pryrc │ │ ├── .rspec │ │ ├── .rubocop.yml │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── README.md │ │ ├── Rakefile │ │ ├── bin │ │ │ ├── console │ │ │ └── setup │ │ ├── config │ │ │ ├── base.yml │ │ │ └── rspec.yml │ │ ├── lib │ │ │ ├── shared-rubocop.rb │ │ │ └── shared │ │ │ │ ├── rubocop.rb │ │ │ │ └── rubocop │ │ │ │ └── cop │ │ │ │ └── lint_env.rb │ │ ├── shared-rubocop.gemspec │ │ └── spec │ │ │ ├── cop │ │ │ └── lint_env_spec.rb │ │ │ └── spec_helper.rb │ └── shared-testing │ │ ├── .gitignore │ │ ├── .pryrc │ │ ├── .rubocop.yml │ │ ├── Gemfile │ │ ├── Gemfile.dev │ │ ├── Gemfile.runtime │ │ ├── README.md │ │ ├── Rakefile │ │ ├── lib │ │ ├── common │ │ │ ├── testing.rb │ │ │ └── testing │ │ │ │ ├── ext │ │ │ │ ├── action_dispatch_test_response.rb │ │ │ │ ├── combustion_bundler_patch.rb │ │ │ │ └── n_plus_one_control_isolator.rb │ │ │ │ ├── helpers │ │ │ │ ├── file_fixture_helper.rb │ │ │ │ ├── json_response.rb │ │ │ │ └── mailbox_for_helper.rb │ │ │ │ ├── rails_configuration.rb │ │ │ │ ├── rspec_configuration.rb │ │ │ │ └── shared_contexts │ │ │ │ ├── active_job.rb │ │ │ │ ├── csrf.rb │ │ │ │ └── shared_request.rb │ │ └── shared-testing.rb │ │ └── shared-testing.gemspec └── generators │ ├── README.md │ ├── engine │ ├── USAGE │ ├── engine_generator.rb │ └── templates │ │ ├── %name%.gemspec.tt │ │ ├── .bundle │ │ └── config │ │ ├── .gitignore.tt │ │ ├── .pryrc │ │ ├── .rspec │ │ ├── .rubocop.yml.tt │ │ ├── Gemfile │ │ ├── Gemfile.dev │ │ ├── Gemfile.runtime │ │ ├── README.md.tt │ │ ├── Rakefile.tt │ │ ├── bin │ │ ├── console.tt │ │ └── rails.tt │ │ ├── config │ │ └── .keep │ │ ├── db │ │ └── migrate │ │ │ └── .keep │ │ ├── lib │ │ ├── %name%.rb.tt │ │ └── %name% │ │ │ ├── engine.rb.tt │ │ │ └── version.rb.tt │ │ └── spec │ │ ├── factories │ │ └── .keep │ │ ├── internal │ │ ├── config │ │ │ ├── database.yml.tt │ │ │ └── storage.yml.tt │ │ └── db │ │ │ └── schema.rb │ │ ├── rails_helper.rb.tt │ │ └── spec_helper.rb │ └── gem │ ├── USAGE │ ├── gem_generator.rb │ └── templates │ ├── %name%.gemspec.tt │ ├── .gitignore.tt │ ├── .pryrc │ ├── .rspec │ ├── .rubocop.yml.tt │ ├── Gemfile │ ├── Gemfile.dev │ ├── Gemfile.runtime │ ├── README.md.tt │ ├── Rakefile.tt │ ├── bin │ └── console.tt │ ├── lib │ ├── %name%.rb.tt │ └── %name% │ │ └── version.rb.tt │ └── spec │ ├── rails_helper.rb │ └── spec_helper.rb ├── forspell.dict ├── guides ├── gemfiles.md └── testing.md └── scripts ├── bundler ├── README.md ├── component.rb └── eval_gemfile_patch.rb ├── combustion ├── README.md └── combustion_bundler_patch.rb ├── dirty-ci ├── README.md └── is-dirty └── engem-cli ├── README.md └── engem /.github/rubocop.gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" do 2 | gem "rubocop-md", "~> 0.3" 3 | gem "standard", "~> 0.3.0" 4 | end 5 | -------------------------------------------------------------------------------- /.github/rubocop.gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.4.0) 5 | jaro_winkler (1.5.4) 6 | parallel (1.19.1) 7 | parser (2.7.1.2) 8 | ast (~> 2.4.0) 9 | rainbow (3.0.0) 10 | rexml (3.2.4) 11 | rubocop (0.82.0) 12 | jaro_winkler (~> 1.5.1) 13 | parallel (~> 1.10) 14 | parser (>= 2.7.0.1) 15 | rainbow (>= 2.2.2, < 4.0) 16 | rexml 17 | ruby-progressbar (~> 1.7) 18 | unicode-display_width (>= 1.4.0, < 2.0) 19 | rubocop-md (0.3.2) 20 | rubocop (~> 0.60) 21 | rubocop-performance (1.5.2) 22 | rubocop (>= 0.71.0) 23 | ruby-progressbar (1.10.1) 24 | standard (0.3.0) 25 | rubocop (~> 0.82.0) 26 | rubocop-performance (~> 1.5.2) 27 | unicode-display_width (1.7.0) 28 | 29 | PLATFORMS 30 | ruby 31 | 32 | DEPENDENCIES 33 | rubocop-md (~> 0.3)! 34 | standard (~> 0.3.0)! 35 | 36 | BUNDLED WITH 37 | 2.1.2 38 | -------------------------------------------------------------------------------- /.github/workflows/docs-lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - "**/*.md" 9 | pull_request: 10 | paths: 11 | - "**/*.md" 12 | 13 | jobs: 14 | markdownlint: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: 2.6 21 | - name: Run Markdown linter 22 | run: | 23 | gem install mdl 24 | mdl **/*.md 25 | rubocop: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: 2.6 32 | - name: Lint Markdown files with RuboCop 33 | run: | 34 | gem install bundler 35 | bundle install --gemfile .github/rubocop.gemfile --jobs 4 --retry 3 36 | bundle exec --gemfile .github/rubocop.gemfile rubocop -c .rubocop-md.yml 37 | forspell: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v2 41 | - name: Install Hunspell 42 | run: | 43 | sudo apt-get install hunspell 44 | - uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: 2.6 47 | - name: Run Forspell 48 | run: | 49 | gem install forspell 50 | forspell *.md 51 | liche: 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - name: Set up Go 56 | uses: actions/setup-go@v1 57 | with: 58 | go-version: 1.13.x 59 | - name: Run liche 60 | env: 61 | GO111MODULE: "on" 62 | run: | 63 | export PATH=$PATH:$(go env GOPATH)/bin 64 | go get -u github.com/raviqqe/liche 65 | liche *.md 66 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules "~MD013", "~MD033", "~MD024" 2 | -------------------------------------------------------------------------------- /.rubocop-md.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | standard: config/base.yml 3 | 4 | require: 5 | - standard/cop/semantic_blocks 6 | - rubocop-md 7 | 8 | AllCops: 9 | Include: 10 | - '**/*.md' 11 | 12 | Standard/SemanticBlocks: 13 | Enabled: false 14 | 15 | Lint/Void: 16 | Exclude: 17 | - '**/*.md' 18 | 19 | Lint/DuplicateMethods: 20 | Exclude: 21 | - '**/*.md' 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vladimir Dementyev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Engems 2 | 3 | This repository contains examples/scripts for building Rails component-based architecture on top of engines and gems. 4 | 5 | 6 |

7 | 8 | Engems map 9 | 10 |

11 | 12 | ## Guides 13 | 14 | - [Organizing dependencies/gemfiles](./guides/gemfiles.md) 15 | - [Testing](./guides/testing.md) 16 | 17 | ## Examples 18 | 19 | - [Generators](./examples/generators) 20 | - [Local gems](./examples/gems) 21 | 22 | ## Scripts 23 | 24 | - [bin/engem](./scripts/engem-cli)—CLI to manage components from the project's root 25 | 26 | ## Resources 27 | 28 | - "Between monoliths and microservices" at RailsConf ([slides and video](https://noti.st/palkan/VWPOSd/between-monoliths-and-microservices)) 29 | - "Engine-ering Rails apps" at Saint-P Ruby ([slides](https://speakerdeck.com/palkan/saint-p-ruby-meetup-engine-ering-rails-apps)) 30 | -------------------------------------------------------------------------------- /assets/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palkan/engems/6b824b182676dedc10ba88ea3aa5764e36f40e14/assets/map.png -------------------------------------------------------------------------------- /examples/gems/README.md: -------------------------------------------------------------------------------- 1 | # Local gems 2 | 3 | These are the examples of _local_ gems used by engines in the component-based Rails applications: 4 | 5 | - [shared-rubocop](./shared-rubocop)—One RuboCop configuraiton to use in all libraries. 6 | - [shared-testing](./shared-testing)—RSpec & testing tools configuration. 7 | - [shared-factory](./shared-factory)—A tiny wrapper over `factory_bot`. 8 | 9 | **NOTE:** All these gems could be generalized and pushed to a public or private package registry to share between the projects. 10 | (We actually did this with some of them and already released 😉). 11 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/internal/log/*.log 5 | spec/internal/tmp/ 6 | spec/examples.txt 7 | tmp/ 8 | /node_modules 9 | /public/packs* 10 | yarn-debug.log* 11 | .yarn-integrity 12 | package-lock.json 13 | yarn-error.log 14 | .offspring 15 | .rspec-local 16 | /.pry_history 17 | /test-results 18 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/.pryrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | root_pryrc = File.join(__dir__, "../../.pryrc") 4 | if File.file?(root_pryrc) 5 | load(root_pryrc) 6 | end 7 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | shared-rubocop: config/base.yml 3 | 4 | Naming/FileName: 5 | Exclude: 6 | - "lib/shared-factory.rb" 7 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) do |repo_name| 6 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 7 | "https://github.com/#{repo_name}.git" 8 | end 9 | 10 | gemspec 11 | 12 | eval_gemfile "./Gemfile.runtime" 13 | eval_gemfile "./Gemfile.dev" 14 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/Gemfile.dev: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | gem "shared-rubocop", path: "../shared-rubocop" 4 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/Gemfile.runtime: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palkan/engems/6b824b182676dedc10ba88ea3aa5764e36f40e14/examples/gems/shared-factory/Gemfile.runtime -------------------------------------------------------------------------------- /examples/gems/shared-factory/README.md: -------------------------------------------------------------------------------- 1 | # Shared Factory 2 | 3 | [FactoryBot](https://github.com/thoughtbot/factory_bot) utilities for apps and engines. 4 | 5 | Includes: 6 | - [faker](https://github.com/stympy/faker) (with only English locale loaded) 7 | - `ActiveSupport.on_load(:factory_bot)` hook to configure `factory_bot` prior to loading 8 | definitions 9 | - `factory_bot_rails` (if Rails is defined) 10 | 11 | ## Usage 12 | 13 | Require it instead of `factory_bot` (or `factory_bot_rails`) and use as always: 14 | 15 | ```ruby 16 | require "shared-factory" 17 | ``` 18 | 19 | ### Active Support load hook 20 | 21 | The load hook could be used to tell FactoryBot where to look for factory definitions: 22 | 23 | ```ruby 24 | ActiveSupport.on_load(:factory_bot) do 25 | FactoryBot.definition_file_paths.unshift File.join(__dir__, "../spec/factories") 26 | end 27 | ``` 28 | 29 | ## Why separate gem and not a part of `shared-testing`? 30 | 31 | Factories could be used not only in test env, but in development and production 32 | (e.g., for DB seeds and mailers previews). 33 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "common-factory" 6 | 7 | require "pry" 8 | Pry.start 9 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/lib/shared-factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "common/factory" 4 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/lib/shared/factory.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "shared/factory/faker" 4 | require "factory_bot" 5 | 6 | # Run load hooks before loading definitions to 7 | # allow engines to register their factories 8 | ActiveSupport.run_load_hooks(:factory_bot, FactoryBot) 9 | 10 | # And only after that load Rails integration 11 | require "factory_bot_rails" 12 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/lib/shared/factory/faker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Faker load tons of useless locales by default 4 | # (see https://github.com/stympy/faker/tree/master/lib/locales) 5 | # 6 | # And it's impossible to configure it( 7 | # (see https://github.com/stympy/faker/blob/v1.9.3/lib/faker.rb#L14-L15) 8 | # 9 | # It's likely that we'll start using Estonian or Armenian or Mandarin 10 | # for test/seed data in the nearest future. 11 | 12 | # Read also https://evilmartians.com/chronicles/rails-profiling-story-or-how-i-caught-faker-trying-to-teach-my-app-australian-slang 13 | 14 | # First, ensure i18n is loaded 15 | require "i18n" 16 | 17 | # Then, stub the `I18n` module with no-op one 18 | save_i18n = Object.__send__(:const_get, :I18n) 19 | 20 | # rubocop:disable Style/MethodMissingSuper, Style/MissingRespondToMissing 21 | i18n_stub = Module.new do 22 | class << self 23 | def load_path 24 | [] 25 | end 26 | 27 | def backend 28 | self 29 | end 30 | 31 | def initialized? 32 | false 33 | end 34 | 35 | def method_missing(*) 36 | end 37 | end 38 | end 39 | # rubocop:enable Style/MethodMissingSuper, Style/MissingRespondToMissing 40 | 41 | # We don't want to show warnings 42 | Kernel.silence_warnings do 43 | Object.__send__(:const_set, :I18n, i18n_stub) 44 | 45 | # Load fake and make it think like it's loading all the locales 46 | # to i18n 47 | require "faker" 48 | 49 | # Restore the original `I18n` module 50 | Object.__send__(:const_set, :I18n, save_i18n) 51 | end 52 | 53 | # Load only `en` locales 54 | faker_spec = Gem.loaded_specs["faker"] 55 | I18n.load_path += Dir[File.join(faker_spec.full_gem_path, "lib", "locales", "en/*.yml")] 56 | I18n.reload! if I18n.backend.initialized? 57 | -------------------------------------------------------------------------------- /examples/gems/shared-factory/shared-factory.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path("lib", __dir__) 4 | 5 | rails_version = File.read(File.join(__dir__, "../../.rails-version")) 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "shared-factory" 9 | s.version = "0.1.0" 10 | s.authors = ["Vicinity"] 11 | s.summary = "Common factory bot utils" 12 | 13 | s.files = Dir["{config,lib}/**/*", "Rakefile"] 14 | s.require_paths = ["lib"] 15 | 16 | s.add_dependency "rails", rails_version 17 | 18 | s.add_dependency "factory_bot_rails", "~> 5.1" 19 | s.add_dependency "faker", "~> 2.7" 20 | 21 | s.add_development_dependency "bundler", ">= 2" 22 | s.add_development_dependency "pry-byebug", ">= 3.4" 23 | s.add_development_dependency "rake", "~> 13.0" 24 | 25 | # Internal 26 | s.add_development_dependency "shared-rubocop" 27 | end 28 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/.pryrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | root_pryrc = File.join(__dir__, "../../.pryrc") 4 | if File.file?(root_pryrc) 5 | load(root_pryrc) 6 | end 7 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format Fuubar 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | shared-rubocop: config/base.yml 3 | 4 | Naming/FileName: 5 | Exclude: 6 | - "lib/shared-rubocop.rb" 7 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) do |repo_name| 6 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 7 | "https://github.com/#{repo_name}.git" 8 | end 9 | 10 | gemspec 11 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | common-rubocop (0.1.0) 5 | fuubar (>= 2.5) 6 | rspec-instafail (>= 1.0) 7 | rspec_junit_formatter (>= 0.4) 8 | rubocop (= 0.75.1) 9 | rubocop-performance (= 1.5.1) 10 | rubocop-rails (= 2.4.0) 11 | rubocop-rspec 12 | standard (= 0.1.6) 13 | 14 | GEM 15 | remote: https://rubygems.org/ 16 | specs: 17 | ast (2.4.0) 18 | byebug (11.1.1) 19 | coderay (1.1.2) 20 | diff-lcs (1.3) 21 | fuubar (2.5.0) 22 | rspec-core (~> 3.0) 23 | ruby-progressbar (~> 1.4) 24 | jaro_winkler (1.5.4) 25 | method_source (0.9.2) 26 | parallel (1.19.1) 27 | parser (2.7.0.4) 28 | ast (~> 2.4.0) 29 | pry (0.12.2) 30 | coderay (~> 1.1.0) 31 | method_source (~> 0.9.0) 32 | pry-byebug (3.8.0) 33 | byebug (~> 11.0) 34 | pry (~> 0.10) 35 | rack (2.2.2) 36 | rainbow (3.0.0) 37 | rake (13.0.1) 38 | rspec (3.9.0) 39 | rspec-core (~> 3.9.0) 40 | rspec-expectations (~> 3.9.0) 41 | rspec-mocks (~> 3.9.0) 42 | rspec-core (3.9.1) 43 | rspec-support (~> 3.9.1) 44 | rspec-expectations (3.9.1) 45 | diff-lcs (>= 1.2.0, < 2.0) 46 | rspec-support (~> 3.9.0) 47 | rspec-instafail (1.0.0) 48 | rspec 49 | rspec-mocks (3.9.1) 50 | diff-lcs (>= 1.2.0, < 2.0) 51 | rspec-support (~> 3.9.0) 52 | rspec-support (3.9.2) 53 | rspec_junit_formatter (0.4.1) 54 | rspec-core (>= 2, < 4, != 2.12.0) 55 | rubocop (0.75.1) 56 | jaro_winkler (~> 1.5.1) 57 | parallel (~> 1.10) 58 | parser (>= 2.6) 59 | rainbow (>= 2.2.2, < 4.0) 60 | ruby-progressbar (~> 1.7) 61 | unicode-display_width (>= 1.4.0, < 1.7) 62 | rubocop-performance (1.5.1) 63 | rubocop (>= 0.71.0) 64 | rubocop-rails (2.4.0) 65 | rack (>= 1.1) 66 | rubocop (>= 0.72.0) 67 | rubocop-rspec (1.38.1) 68 | rubocop (>= 0.68.1) 69 | ruby-progressbar (1.10.1) 70 | standard (0.1.6) 71 | rubocop (~> 0.75.0) 72 | rubocop-performance (~> 1.5.0) 73 | unicode-display_width (1.6.1) 74 | 75 | PLATFORMS 76 | ruby 77 | 78 | DEPENDENCIES 79 | bundler (>= 2) 80 | common-rubocop! 81 | pry-byebug (>= 3.6) 82 | rake (>= 10.0) 83 | rspec (>= 3.9) 84 | 85 | BUNDLED WITH 86 | 2.0.2 87 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/README.md: -------------------------------------------------------------------------------- 1 | # Shared RuboCop 2 | 3 | RuboCop configuration for usage in a project. 4 | 5 | Includes: 6 | - base config from [standard](https://github.com/testdouble/standard) 7 | - [`rubocop-rspec`](https://github.com/rubocop-hq/rubocop-rspec) cops 8 | 9 | ## Installation 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | ```ruby 14 | gem "shared-rubocop", path: "gems/shared-rubocop" 15 | ``` 16 | 17 | or to your engine's Gemfile and `.gemspec`: 18 | 19 | ```ruby 20 | # Gemfile 21 | gem "shared-rubocop", path: "../../gems/shared-rubocop" 22 | 23 | # .gemspec 24 | s.add_development_dependency "shared-rubocop" 25 | ``` 26 | 27 | ## Usage 28 | 29 | Add to your `.rubocop.yml`: 30 | 31 | ```yml 32 | inherit_gem: 33 | shared-rubocop: config/base.yml 34 | ``` 35 | 36 | ## Custom cops 37 | 38 | - `Lint/Env` – checks for the presence of `ENV` or `Rails.env`. 39 | 40 | Use it to prevent environment dependent checks in your code: 41 | 42 | ```yml 43 | require: 44 | - shared/rubocop/cop/lint_env 45 | 46 | Lint/Env: 47 | Enabled: true 48 | Exclude: 49 | - 'lib/tasks/**/*' 50 | - 'config/environments/**/*' 51 | - 'config/*.rb' 52 | - 'spec/*_helper.rb' 53 | - 'spec/**/support/**/*' 54 | - 'Rakefile' 55 | - 'Gemfile' 56 | ``` 57 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | begin 6 | require "bundler/setup" 7 | rescue LoadError 8 | puts "You must `gem install bundler` and `bundle install` to run rake tasks" 9 | end 10 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "common/rubocop" 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 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/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 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/config/base.yml: -------------------------------------------------------------------------------- 1 | inherit_from: "./rspec.yml" 2 | 3 | require: 4 | - shared/rubocop 5 | - rubocop-rails 6 | - rubocop-performance 7 | - standard/cop/semantic_blocks 8 | 9 | inherit_gem: 10 | standard: config/base.yml 11 | 12 | AllCops: 13 | TargetRubyVersion: 2.6 14 | Exclude: 15 | - 'bin/*' 16 | - 'tmp/**/*' 17 | - 'node_modules/**/*' 18 | - 'vendor/**/*' 19 | DisplayCopNames: true 20 | 21 | Style/FrozenStringLiteralComment: 22 | Enabled: true 23 | 24 | Standard/SemanticBlocks: 25 | Enabled: false 26 | 27 | Lint/UselessAssignment: 28 | Exclude: 29 | - 'spec/**/*.rb' 30 | - 'test/**/*.rb' 31 | 32 | Lint/AmbiguousBlockAssociation: 33 | Exclude: 34 | - 'spec/**/*.rb' 35 | - 'test/**/*.rb' 36 | 37 | Rails/Output: 38 | Enabled: true 39 | Exclude: 40 | - 'lib/tasks/**/*' 41 | 42 | Gemspec/OrderedDependencies: 43 | Enabled: true 44 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/config/rspec.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rspec 3 | 4 | RSpec/Focus: 5 | Enabled: true 6 | 7 | RSpec/EmptyExampleGroup: 8 | Enabled: true 9 | CustomIncludeMethods: 10 | - succeed 11 | - failed 12 | 13 | RSpec/EmptyLineAfterExampleGroup: 14 | Enabled: true 15 | 16 | RSpec/EmptyLineAfterFinalLet: 17 | Enabled: true 18 | 19 | RSpec/EmptyLineAfterHook: 20 | Enabled: true 21 | 22 | RSpec/EmptyLineAfterSubject: 23 | Enabled: true 24 | 25 | RSpec/HooksBeforeExamples: 26 | Enabled: true 27 | 28 | RSpec/ImplicitExpect: 29 | Enabled: true 30 | 31 | RSpec/IteratedExpectation: 32 | Enabled: true 33 | 34 | RSpec/LetBeforeExamples: 35 | Enabled: true 36 | 37 | RSpec/MissingExampleGroupArgument: 38 | Enabled: true 39 | 40 | RSpec/ReceiveCounts: 41 | Enabled: true 42 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/lib/shared-rubocop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "common/rubocop" 4 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/lib/shared/rubocop.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubocop" 4 | require "standard" 5 | 6 | module Shared 7 | module Rubocop 8 | end 9 | end 10 | 11 | # Do not use the root app RuboCop exclusions in 12 | # isolated gems stored in the same repo 13 | # See https://github.com/rubocop-hq/rubocop/pull/4329 14 | RuboCop::ConfigLoader.ignore_parent_exclusion = true 15 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/lib/shared/rubocop/cop/lint_env.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rubocop/rspec/language" 4 | 5 | module RuboCop 6 | module Cop 7 | module Lint 8 | # This cops checks for direct usage of ENV variables and 9 | # Rails.env 10 | class Env < RuboCop::Cop::Cop 11 | MSG_ENV = "Avoid direct usage of ENV in application code" 12 | MSG_RAILS_ENV = "Avoid direct usage of Rails.env in application code" 13 | USAGE_MSG = ", use configuration parameters instead" 14 | 15 | def_node_matcher :env?, <<~PATTERN 16 | {(const nil? :ENV) (const (cbase) :ENV)} 17 | PATTERN 18 | 19 | def_node_matcher :rails_env?, <<~PATTERN 20 | (send {(const nil? :Rails) (const (cbase) :Rails)} :env) 21 | PATTERN 22 | 23 | def on_const(node) 24 | return unless env?(node) 25 | add_offense(node.parent, location: :selector, message: MSG_ENV + USAGE_MSG) 26 | end 27 | 28 | def on_send(node) 29 | return unless rails_env?(node) 30 | add_offense(node.parent, location: :selector, message: MSG_RAILS_ENV + USAGE_MSG) 31 | end 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/shared-rubocop.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | Gem::Specification.new do |s| 7 | s.name = "shared-rubocop" 8 | s.version = "0.1.0" 9 | s.authors = ["Vicinity"] 10 | s.summary = "Common RuboCop plugins and configuration" 11 | s.description = "Common RuboCop plugins and configuration" 12 | s.license = "MIT" 13 | 14 | s.files = Dir["{config,lib}/**/*", "Rakefile"] 15 | 16 | s.add_dependency "rubocop-rails" 17 | s.add_dependency "rubocop-rspec" 18 | s.add_dependency "standard", "~> 0.3.0" 19 | 20 | s.add_development_dependency "bundler", ">= 2" 21 | s.add_development_dependency "pry-byebug", ">= 3.6" 22 | s.add_development_dependency "rake", ">= 10.0" 23 | s.add_development_dependency "rspec", ">= 3.9" 24 | 25 | # Formatters 26 | # Progressbar-like formatter for RSpec 27 | s.add_dependency "fuubar", ">= 2.5" 28 | s.add_dependency "rspec-instafail", ">= 1.0" 29 | s.add_dependency "rspec_junit_formatter", ">= 0.4" 30 | end 31 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/spec/cop/lint_env_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "shared/rubocop/cop/lint_env" 4 | 5 | describe RuboCop::Cop::Lint::Env, :config do 6 | subject(:cop) { described_class.new(config) } 7 | 8 | it "rejects ENV usage" do 9 | inspect_source("a = ENV['x']") 10 | expect(cop.offenses.size).to eq(1) 11 | expect(cop.messages.first).to include("Avoid direct usage of ENV in application code") 12 | end 13 | 14 | it "rejects ::ENV usage" do 15 | inspect_source("a = ::ENV['x']") 16 | expect(cop.offenses.size).to eq(1) 17 | expect(cop.messages.first).to include("Avoid direct usage of ENV in application code") 18 | end 19 | 20 | it "rejects Rails.env usage" do 21 | inspect_source("a = Rails.env.production?") 22 | expect(cop.offenses.size).to eq(1) 23 | expect(cop.messages.first).to include("Avoid direct usage of Rails.env in application code") 24 | end 25 | 26 | it "reject Rails.env with condition" do 27 | inspect_source(<<~SOURCE 28 | if Rails.env.production? 29 | true 30 | end 31 | SOURCE 32 | ) 33 | expect(cop.offenses.size).to eq(1) 34 | expect(cop.messages.first).to include("Avoid direct usage of Rails.env in application code") 35 | end 36 | 37 | it "accepts nested constant" do 38 | inspect_source("a = Custom::ENV") 39 | expect(cop.offenses).to be_empty 40 | end 41 | 42 | it "accepts nested constant with dot" do 43 | inspect_source("a = Custom.ENV") 44 | expect(cop.offenses).to be_empty 45 | end 46 | 47 | it "accepts other Rails.x methods" do 48 | inspect_source("a = Rails.envy") 49 | expect(cop.offenses).to be_empty 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /examples/gems/shared-rubocop/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "pry-byebug" 5 | require "rspec" 6 | 7 | require "rubocop" 8 | require "rubocop/rspec/support" 9 | 10 | RSpec.configure do |config| 11 | config.filter_run_when_matching :focus 12 | config.example_status_persistence_file_path = "tmp/rspec_examples.txt" 13 | 14 | if config.files_to_run.one? 15 | config.default_formatter = "doc" 16 | end 17 | 18 | config.order = :random 19 | Kernel.srand config.seed 20 | end 21 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/.pryrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | root_pryrc = File.join(__dir__, "../../.pryrc") 4 | if File.file?(root_pryrc) 5 | load(root_pryrc) 6 | end 7 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | shared-rubocop: config/base.yml 3 | 4 | Naming/FileName: 5 | Exclude: 6 | - "lib/shared-testing.rb" 7 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) do |repo_name| 6 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 7 | "https://github.com/#{repo_name}.git" 8 | end 9 | 10 | gemspec 11 | 12 | eval_gemfile "./Gemfile.runtime" 13 | eval_gemfile "./Gemfile.dev" 14 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/Gemfile.dev: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | gem "shared-rubocop", path: "../shared-rubocop" 4 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/Gemfile.runtime: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile "../shared-factory/Gemfile.runtime" 4 | gem "shared-factory", path: "../shared-factory" 5 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/README.md: -------------------------------------------------------------------------------- 1 | # Shared Testing 2 | 3 | RSpec configurations and utils for usage in gems, engines and apps. 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | ```ruby 10 | gem "shared-rubocop", path: "gems/shared-rubocop" 11 | ``` 12 | 13 | or to your engine's Gemfile and `.gemspec`: 14 | 15 | ```ruby 16 | # Gemfile 17 | gem "shared-rubocop", path: "../../gems/shared-rubocop" 18 | 19 | # .gemspec 20 | s.add_development_dependency "shared-rubocop" 21 | ``` 22 | 23 | ## Usage 24 | 25 | This gem provides two configurations, which could be loaded via: 26 | - `require "shared/testing/rspec_configuration"` – this is RSpec core configuration, 27 | which must be added to your `spec_helper.rb`; it doesn't contain anything Rails specific 28 | - `require "shared/testing/rails_configuration" – typical RSpec config for Rails, which 29 | includes a lot of usefule stuff (see below); require it in your `rails_helper.rb`. 30 | 31 | ### Tools 32 | 33 | Here is the list of gems included into Rails config: 34 | - [rspec-rails](https://relishapp.com/rspec/rspec-rails/docs) 35 | - [shared-factory](../shared-factory/README.md) 36 | - [isolator](https://github.com/palkan/isolator) 37 | - [n_plus_one_control](https://github.com/palkan/n_plus_one_control) (verbose by default) 38 | - [shoulda-matchers](https://matchers.shoulda.io) 39 | - [test-prof](https://test-prof.evilmartians.io) 40 | - [ActiveSupport::Testing::TimeHelpers](https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html) 41 | 42 | You can also add a dummy Rails app (in case of gem/engine) using [`combustion`](https://github.com/pat/combustion) gem, which is included, too. For example: 43 | 44 | ```ruby 45 | require "shared/testing/rails_configuration" 46 | 47 | require "combustion" 48 | 49 | Combustion.initialize! :action_controller, :active_record, :active_job, :action_mailer do 50 | config.logger = Logger.new(nil) 51 | config.log_level = :fatal 52 | 53 | config.active_record.raise_in_transactional_callbacks = true 54 | 55 | config.active_job.queue_adapter = :test 56 | end 57 | ``` 58 | 59 | Additional configuration could be added under `spec/internal`. 60 | 61 | ### Helpers 62 | 63 | - `fixture_file_path(*args)` – returns path to a file fixture (based on [`fixture_path`](https://relishapp.com/rspec/rspec-rails/docs/file-fixture)) 64 | 65 | - `mails_for(email_or_user)` - returns a proxy for user's emails; useful to test that an email 66 | has been sent and it's contents, for example: 67 | 68 | ```ruby 69 | mailbox = mails_for(user) # or mails_for(email) 70 | expect { subject }.to change(mailbox, :size).by(1) 71 | expect(mailbox.last.subject).to eq "Hey, you got an email!" 72 | ``` 73 | 74 | ### Shared Contexts 75 | 76 | - `"active_job:perform"` (`active_job: :perform`) – perform enqueued Active Job jobs automatically 77 | - `"csrf:on"` (`csrf: :on`) – turn on CSRF protection for `ActionController::Base` (it's off by default in specs) 78 | - `"shared:request"` (`type: :request`, i.e. included automatically) – add `json_response` method and declare `subject` for request specs (see [testing guide](../../docs/testing/rails_controllers_requests.md)). 79 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | 5 | begin 6 | require "bundler/setup" 7 | rescue LoadError 8 | puts "You must `gem install bundler` and `bundle install` to run rake tasks" 9 | end 10 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/ext/action_dispatch_test_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ActionDispatch 4 | # Make response less verbose (for rspec failure messages). 5 | # 6 | # Before: 7 | # expected `#, 10 | # @charset="utf-8", @cache_control={:no_cache=>true}, 11 | # @etag=nil>.success?` to return true, got false 12 | # 13 | # After: 14 | # expected `TestResponseYou are being redirected..." 16 | # >.success?` to return true, got false 17 | class TestResponse 18 | def inspect 19 | "TestResponse" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/ext/combustion_bundler_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Hijack Bundler.require from Combustion.initialize! to load engine-specific group 4 | Combustion.singleton_class.prepend(Module.new do 5 | def initialize!(*args, bundler_groups: nil, **kwargs) 6 | if bundler_groups 7 | original_require = Bundler.method(:require) 8 | Bundler.define_singleton_method(:require) { |*| original_require.call(*bundler_groups) } 9 | end 10 | 11 | super 12 | ensure 13 | Bundler.define_singleton_method(:require, original_require) if bundler_groups 14 | end 15 | end) 16 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/ext/n_plus_one_control_isolator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Disable Isolator in n_plus_one_control examples 4 | # 5 | # Rails 5 triggers `after_commit` callbacks depending on the properties of the outer transaction: 6 | # if it has `joinable: false` then callbacks are triggered. 7 | # `n_plus_one_control` also uses a non-joinable transaction, but it's not counted as _safe_ by Isolator, 8 | # 'cause it opens after the test example transaction (`transactional_tests` feature). 9 | # 10 | # Links: 11 | # - https://github.com/palkan/n_plus_one_control/blob/d13567e60c18bb2e85e606abee4c096383623992/lib/n_plus_one_control/executor.rb#L59 12 | NPlusOneControl::Executor.singleton_class.prepend(Module.new do 13 | def with_transaction(*) 14 | Isolator.disable { super } 15 | end 16 | end) 17 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/helpers/file_fixture_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # See https://relishapp.com/rspec/rspec-rails/docs/file-fixture 4 | module Common::Testing 5 | module FileFixtureHelper 6 | def fixture_file_path(*paths) 7 | File.join(RSpec.configuration.file_fixture_path, *paths) 8 | end 9 | end 10 | end 11 | 12 | RSpec.configure do |config| 13 | config.include Common::Testing::FileFixtureHelper 14 | end 15 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/helpers/json_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Common::Testing 4 | module JSONResponse 5 | # Return cached parsed request response 6 | def json_response 7 | @json_response ||= begin 8 | # ensure request has been made 9 | request 10 | JSON.parse(response.body) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/helpers/mailbox_for_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Common::Testing 4 | module MailboxForHelper 5 | # Proxy object for accessing 6 | # test deliveries to the specified email 7 | # address 8 | class Mailbox 9 | attr_reader :email 10 | 11 | delegate :count, :size, :last, to: :deliveries 12 | 13 | def initialize(email) 14 | @email = email 15 | end 16 | 17 | def deliveries 18 | ActionMailer::Base.deliveries.select do |mail| 19 | mail.destinations.include?(email) 20 | end 21 | end 22 | end 23 | 24 | # Return a mailbox for user or email 25 | def mails_for(user_or_email) 26 | if ActionMailer::Base.delivery_method != :test 27 | raise "You can only use `mailbox` helper when " \ 28 | "ActionMailer::Base.delivery_method is `:test`" 29 | end 30 | 31 | email = user_or_email.respond_to?(:email) ? 32 | user_or_email.email : 33 | user_or_email 34 | 35 | Mailbox.new(email) 36 | end 37 | end 38 | end 39 | 40 | RSpec.configure do |config| 41 | config.include Common::Testing::MailboxForHelper 42 | end 43 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/rails_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "shared-factory" 4 | 5 | require "rspec/rails" 6 | 7 | require "isolator" 8 | require "n_plus_one_control/rspec" 9 | require "shoulda-matchers" 10 | require "timecop" 11 | require "with_model" 12 | 13 | require "shared/testing/ext/action_dispatch_test_response" 14 | require "shared/testing/ext/n_plus_one_control_isolator" 15 | 16 | require "test_prof/recipes/rspec/before_all" 17 | require "test_prof/recipes/rspec/let_it_be" 18 | require "test_prof/recipes/logging" 19 | require "test_prof/recipes/rspec/any_fixture" 20 | require "test_prof/ext/active_record_refind" 21 | require "test_prof/any_fixture/dsl" 22 | require "test_prof/recipes/rspec/factory_default" 23 | require "test_prof/recipes/rspec/sample" 24 | 25 | Dir[File.join(__dir__, "helpers/**/*.rb")].each { |f| require f } 26 | Dir[File.join(__dir__, "shared_contexts/**/*.rb")].each { |f| require f } 27 | 28 | Shoulda::Matchers.configure do |config| 29 | config.integrate do |with| 30 | with.test_framework :rspec 31 | with.library :rails 32 | end 33 | end 34 | 35 | NPlusOneControl.verbose = true 36 | 37 | # Define whether we're withing engine or app 38 | root = defined?(ENGINE_ROOT) ? Pathname.new(ENGINE_ROOT) : Rails.root 39 | 40 | Dir[root.join("spec/support/**/*.rb")].each { |f| require f } 41 | 42 | RSpec.configure do |config| 43 | config.fixture_path = root.join("spec/fixtures") 44 | 45 | # Add `fixture_file_upload` 46 | config.include ActionDispatch::TestProcess 47 | # Add FactoryBot methods 48 | config.include FactoryBot::Syntax::Methods 49 | # Add `travel_to` 50 | config.include ActiveSupport::Testing::TimeHelpers 51 | # Add `with_model` 52 | config.extend WithModel 53 | 54 | config.use_transactional_fixtures = true 55 | 56 | # See https://relishapp.com/rspec/rspec-rails/docs 57 | config.infer_spec_type_from_file_location! 58 | 59 | unless ENV["FULLTRACE"] 60 | config.filter_rails_from_backtrace! 61 | 62 | # Request/Rack middlewares 63 | config.filter_gems_from_backtrace "railties", "rack", "rack-test" 64 | end 65 | 66 | config.after(:each) do 67 | # Make sure every example starts with the current time 68 | Timecop.return 69 | travel_back 70 | 71 | # Clear ActiveJob jobs 72 | if defined?(ActiveJob) && ActiveJob::QueueAdapters::TestAdapter === ActiveJob::Base.queue_adapter 73 | ActiveJob::Base.queue_adapter.enqueued_jobs.clear 74 | ActiveJob::Base.queue_adapter.performed_jobs.clear 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/rspec_configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rspec" 4 | require "rspec/instafail" 5 | require "rspec_junit_formatter" 6 | require "rspec/support/spec/shell_out" 7 | 8 | RSpec.configure do |config| 9 | include RSpec::Support::ShellOut 10 | 11 | config.expect_with :rspec do |expectations| 12 | # This option will default to `true` in RSpec 4. It makes the `description` 13 | # and `failure_message` of custom matchers include text for helper methods 14 | # defined using `chain`, e.g.: 15 | # be_bigger_than(2).and_smaller_than(4).description 16 | # # => "be bigger than 2 and smaller than 4" 17 | # ...rather than: 18 | # # => "be bigger than 2" 19 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 20 | end 21 | 22 | config.mock_with :rspec do |mocks| 23 | # Prevents you from mocking or stubbing a method that does not exist on 24 | # a real object. This is generally recommended, and will default to 25 | # `true` in RSpec 4. 26 | mocks.verify_partial_doubles = true 27 | end 28 | 29 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 30 | # have no way to turn it off -- the option exists only for backwards 31 | # compatibility in RSpec 3). It causes shared context metadata to be 32 | # inherited by the metadata hash of host groups and examples, rather than 33 | # triggering implicit auto-inclusion in groups with matching metadata. 34 | config.shared_context_metadata_behavior = :apply_to_host_groups 35 | 36 | config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| 37 | next if metadata.key?(:type) 38 | match = metadata[:location].match(%r{/spec/([^/]+)/}) 39 | next unless match 40 | metadata[:type] = match[1].then do |path| 41 | next path unless path.respond_to?(:singularize) 42 | path.singularize 43 | end.to_sym 44 | end 45 | 46 | config.filter_run_when_matching :focus 47 | config.example_status_persistence_file_path = "tmp/rspec_examples.txt" 48 | config.run_all_when_everything_filtered = true 49 | 50 | if config.files_to_run.one? 51 | # Use the documentation formatter for detailed output, 52 | # unless a formatter has already been configured 53 | # (e.g. via a command-line flag). 54 | config.default_formatter = "doc" 55 | end 56 | 57 | unless ENV["FULLTRACE"] 58 | config.filter_gems_from_backtrace "factory_bot" 59 | config.filter_gems_from_backtrace "graphql" 60 | config.filter_gems_from_backtrace "test-prof" 61 | end 62 | 63 | # Always include the spec itself to the backtrace. 64 | # That also helps to avoid a full stack printing when 65 | # everything have been filtered out. 66 | config.backtrace_inclusion_patterns << %r{/spec/} 67 | 68 | config.order = :random 69 | Kernel.srand config.seed 70 | end 71 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/shared_contexts/active_job.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_context "active_job:perform" do 4 | around do |ex| 5 | was_perform = ActiveJob::Base.queue_adapter.perform_enqueued_jobs 6 | ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true 7 | ex.run 8 | ActiveJob::Base.queue_adapter.perform_enqueued_jobs = was_perform 9 | end 10 | end 11 | 12 | RSpec.configure do |config| 13 | config.include_context "active_job:perform", active_job: :perform 14 | end 15 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/shared_contexts/csrf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Turn on forgery protection for API requests 4 | # (to make sure that we handle (=ignore) normal CSRF protection) 5 | shared_context "csrf:on" do 6 | around do |ex| 7 | save = ActionController::Base.allow_forgery_protection 8 | ActionController::Base.allow_forgery_protection = true 9 | ex.run 10 | ActionController::Base.allow_forgery_protection = save 11 | end 12 | end 13 | 14 | RSpec.configure do |config| 15 | config.include_context "csrf:on", csrf: :on 16 | end 17 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/common/testing/shared_contexts/shared_request.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | shared_context "shared:request" do 4 | subject { request; response } # rubocop:disable Style/Semicolon 5 | end 6 | 7 | RSpec.configure do |config| 8 | config.include_context "shared:request", type: :request 9 | config.include Common::Testing::JSONResponse, type: :request 10 | end 11 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/lib/shared-testing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "common/testing" 4 | -------------------------------------------------------------------------------- /examples/gems/shared-testing/shared-testing.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | 6 | rails_version = File.read(File.join(__dir__, "../../.rails-version")) 7 | 8 | Gem::Specification.new do |s| 9 | s.name = "shared-testing" 10 | s.version = "0.1.0" 11 | s.authors = ["Vicinity"] 12 | s.summary = "Common configuration for RSpec and Dummy application" 13 | s.description = "Common configuration for RSpec and Dummy application" 14 | 15 | s.files = Dir["{config,lib}/**/*", "Rakefile"] 16 | 17 | # Core 18 | s.add_dependency "rails", rails_version 19 | s.add_dependency "rspec-rails", "~> 4.0.0" 20 | 21 | # Tools 22 | s.add_dependency "combustion", "~> 1.3" 23 | s.add_dependency "isolator", "~> 0.6.2" 24 | s.add_dependency "n_plus_one_control", "~> 0.3.1" 25 | s.add_dependency "shoulda-matchers" 26 | s.add_dependency "test-prof", "~> 0.10.1" 27 | s.add_dependency "timecop", "~> 0.9" 28 | s.add_dependency "with_model", "~> 2.1" 29 | 30 | # Formatters 31 | # Progressbar-like formatter for RSpec 32 | s.add_dependency "fuubar", "~> 2.5" 33 | s.add_dependency "rspec-instafail", "~> 1.0" 34 | s.add_dependency "rspec_junit_formatter", "~> 0.4" 35 | 36 | # Internal 37 | s.add_dependency "shared-factory" 38 | 39 | s.add_development_dependency "bundler", ">= 2" 40 | s.add_development_dependency "pry-byebug", "~> 3.6" 41 | s.add_development_dependency "rake", "~> 13.0" 42 | 43 | # Internal 44 | s.add_development_dependency "shared-rubocop" 45 | end 46 | -------------------------------------------------------------------------------- /examples/generators/README.md: -------------------------------------------------------------------------------- 1 | # Rails Generators 2 | 3 | We came up with convinient Rails generators to create new engines and local gems. 4 | You can use them as an example (they are too opinionated for general use). 5 | 6 | Copy the contents of the `generators/` folder to you `lib/generators` to add them to your app. 7 | That would add two generators: 8 | 9 | - `rails g engine ` 10 | - `rails g gem `. 11 | -------------------------------------------------------------------------------- /examples/generators/engine/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | This generator creates skeleton of basic engine. 3 | 4 | Example: 5 | rails generate engine 6 | -------------------------------------------------------------------------------- /examples/generators/engine/engine_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EngineGenerator < Rails::Generators::NamedBase 4 | source_root File.expand_path("templates", __dir__) 5 | 6 | def create_engine_file 7 | directory(".", "engines/#{name}") 8 | 9 | chmod "engines/#{name}/bin/console", 0o755, verbose: false 10 | chmod "engines/#{name}/bin/rails", 0o755, verbose: false 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/%name%.gemspec.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path("lib", __dir__) 4 | 5 | require "<%= name %>/version" 6 | 7 | # We store Rails version in the project's root to share between gems 8 | rails_version = File.read(File.join(__dir__, "../../.rails-version")) 9 | 10 | Gem::Specification.new do |s| 11 | s.name = "<%= name %>" 12 | s.version = <%= class_name %>::VERSION 13 | s.authors = ["Vicinity"] 14 | s.summary = "TODO:" 15 | 16 | s.files = Dir["{app,config,db,lib}/**/*"] 17 | s.require_paths = ["lib"] 18 | 19 | s.add_dependency "rails", rails_version 20 | s.add_dependency "pg", "~> 1.0" 21 | 22 | s.add_development_dependency "bootsnap", ">= 1.4.3" 23 | s.add_development_dependency "brakeman", "~> 4.7" 24 | s.add_development_dependency "bundler", ">= 2.0" 25 | s.add_development_dependency "pry-byebug", ">= 3.4" 26 | s.add_development_dependency "rake", "~> 13.0" 27 | 28 | # Internal 29 | s.add_development_dependency "shared-rubocop" 30 | s.add_development_dependency "shared-testing" 31 | end 32 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_GEMFILE: "../../Gemfile" 3 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/.gitignore.tt: -------------------------------------------------------------------------------- 1 | log/*.log 2 | pkg/ 3 | spec/internal/log/*.log 4 | spec/internal/tmp/ 5 | spec/examples.txt 6 | tmp/ 7 | /node_modules 8 | /public/packs* 9 | yarn-debug.log* 10 | .yarn-integrity 11 | package-lock.json 12 | yarn-error.log 13 | .offspring 14 | .rspec-local 15 | /.pry_history 16 | /test-results 17 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/.pryrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | root_pryrc = File.join(__dir__, "../../.pryrc") 4 | if File.file?(root_pryrc) 5 | load(root_pryrc) 6 | end 7 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format Fuubar 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/.rubocop.yml.tt: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | shared-rubocop: 3 | - config/base.yml 4 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) do |repo_name| 6 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 7 | "https://github.com/#{repo_name}.git" 8 | end 9 | 10 | gemspec 11 | 12 | eval_gemfile "./Gemfile.runtime" 13 | eval_gemfile "./Gemfile.dev" 14 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/Gemfile.dev: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile "../../gems/shared-testing/Gemfile.runtime" 4 | gem "shared-testing", path: "../../gems/shared-testing" 5 | 6 | gem "shared-rubocop", path: "../../gems/shared-rubocop" 7 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/Gemfile.runtime: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palkan/engems/6b824b182676dedc10ba88ea3aa5764e36f40e14/examples/generators/engine/templates/Gemfile.runtime -------------------------------------------------------------------------------- /examples/generators/engine/templates/README.md.tt: -------------------------------------------------------------------------------- 1 | # <%= class_name %> 2 | 3 | TODO: Write purpose and description. 4 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/Rakefile.tt: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, 3 | # and they will automatically be available to Rake. 4 | 5 | require "bundler/setup" 6 | require "<%= name %>" 7 | require_relative "spec/rails_helper" 8 | 9 | begin 10 | require "rspec/core/rake_task" 11 | RSpec::Core::RakeTask.new(:spec) 12 | rescue LoadError 13 | end 14 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/bin/console.tt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "pry" 6 | require "<%= name %>" 7 | require_relative "../spec/rails_helper" 8 | 9 | Pry.start 10 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/bin/rails.tt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | 6 | ENGINE_ROOT = File.expand_path("..", __dir__) 7 | ENGINE_PATH = File.expand_path("../lib/<%= name %>/engine", __dir__) 8 | 9 | require_relative "../spec/rails_helper" 10 | 11 | require "rails/engine/commands" 12 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/config/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palkan/engems/6b824b182676dedc10ba88ea3aa5764e36f40e14/examples/generators/engine/templates/config/.keep -------------------------------------------------------------------------------- /examples/generators/engine/templates/db/migrate/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palkan/engems/6b824b182676dedc10ba88ea3aa5764e36f40e14/examples/generators/engine/templates/db/migrate/.keep -------------------------------------------------------------------------------- /examples/generators/engine/templates/lib/%name%.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails" 4 | 5 | require "<%= name %>/version" 6 | 7 | module <%= class_name %> 8 | class << self 9 | # Configure table_name_prefix for all the models from the engine 10 | # if their could be collision with other engines or the main app 11 | # (useful during the gradual migration to engined-architecture) 12 | def table_name_prefix 13 | "" 14 | end 15 | end 16 | end 17 | 18 | require "<%= name %>/engine" 19 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/lib/%name%/engine.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails/engine" 4 | 5 | module <%= class_name %> 6 | class << self 7 | def configure 8 | yield Engine.config 9 | end 10 | end 11 | 12 | class Engine < ::Rails::Engine 13 | isolate_namespace <%= class_name %> 14 | 15 | config.autoload_paths += Dir["#{config.root}/app/**/concerns"] 16 | 17 | initializer "<%= name %>" do |app| 18 | app.config.paths["db/migrate"].concat(config.paths["db/migrate"].expanded) 19 | 20 | # For migration_context (used for checking pending migrations) 21 | ActiveRecord::Migrator.migrations_paths += config.paths["db/migrate"].expanded.flatten 22 | 23 | engine_factories_path = root.join("spec", "factories") 24 | 25 | # This hook is provided by shared-factory gem 26 | ActiveSupport.on_load(:factory_bot) do 27 | FactoryBot.definition_file_paths.unshift engine_factories_path 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/lib/%name%/version.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= class_name %> 4 | VERSION = "0.1.0" 5 | end 6 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/spec/factories/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palkan/engems/6b824b182676dedc10ba88ea3aa5764e36f40e14/examples/generators/engine/templates/spec/factories/.keep -------------------------------------------------------------------------------- /examples/generators/engine/templates/spec/internal/config/database.yml.tt: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: postgresql 3 | url: <%%= ENV["DATABASE_URL"].sub("postgres://", "postgis://") %> 4 | pool: <%%= ENV["DB_POOL_SIZE"] || 5 %> 5 | timeout: 5000 6 | database: engine_<%= name %>_test 7 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/spec/internal/config/storage.yml.tt: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("../../tmp/storage") %> 4 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/spec/internal/db/schema.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ActiveRecord::Schema.define do 4 | end 5 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/spec/rails_helper.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Engine root is used by rails_configuration to correctly 4 | # load fixtures and support files 5 | ENGINE_ROOT = Pathname.new(File.expand_path("..", __dir__)) 6 | 7 | ENV["RAILS_ENV"] = "test" 8 | ENV["BOOTSNAP_CACHE_DIR"] = File.join(ENGINE_ROOT, "spec", "internal", "tmp", "cache") 9 | 10 | require "bootsnap/setup" 11 | 12 | require "combustion" 13 | require "shared/testing/ext/combustion_bundler_patch" 14 | 15 | begin 16 | # Add another Rails part, e.g. 17 | # Combustion.initialize! :active_record, :active_job, :active_storage, :action_mailer, :action_controller 18 | Combustion.initialize! :active_record, bundler_groups: :<%= name %> do 19 | config.logger = Logger.new(nil) 20 | config.log_level = :fatal 21 | 22 | config.autoloader = :zeitwerk 23 | 24 | # Always use test adapter for active_job 25 | # config.active_job.queue_adapter = :test 26 | # 27 | # Always use test service to active_storage 28 | # config.active_storage.service = :test 29 | # 30 | # Enable verbose logging for active_record 31 | # config.active_record.verbose_query_logs = true 32 | end 33 | rescue => e 34 | # Fail fast if application couldn't be loaded 35 | $stdout.puts "Failed to load the app: #{e.message}\n#{e.backtrace.take(5).join("\n")}" 36 | exit(1) 37 | end 38 | 39 | # if you need url helpers (e.g. with active storage) 40 | # Rails.application.default_url_options[:host] = "localhost" 41 | # <%= class_name %>::Engine.routes.default_url_options[:host] = "localhost" 42 | 43 | require "shared/testing/rails_configuration" 44 | 45 | # action_policy helpers 46 | # require "action_policy/rspec" 47 | # require "action_policy/rspec/dsl" 48 | 49 | # Additional RSpec configuration 50 | # 51 | # RSpec.configure do |config| 52 | # config.after(:suite) do 53 | # # Cleanup attachments generated during tests 54 | # FileUtils.rm_rf(ActiveStorage::Blob.service.root) 55 | # end 56 | # end 57 | -------------------------------------------------------------------------------- /examples/generators/engine/templates/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "pry-byebug" 5 | 6 | require "shared/testing/rspec_configuration" 7 | -------------------------------------------------------------------------------- /examples/generators/gem/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | This generator creates skeleton of a shared gem for the project. 3 | 4 | Example: 5 | rails generate gem 6 | -------------------------------------------------------------------------------- /examples/generators/gem/gem_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class GemGenerator < Rails::Generators::NamedBase 4 | source_root File.expand_path("templates", __dir__) 5 | 6 | def create_engine_file 7 | directory(".", "gems/#{name}") 8 | 9 | chmod "gems/#{name}/bin/console", 0o755, verbose: false 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/%name%.gemspec.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path("lib", __dir__) 4 | 5 | require "<%= name %>/version" 6 | 7 | Gem::Specification.new do |s| 8 | s.name = "<%= name %>" 9 | s.version = <%= class_name %>::VERSION 10 | s.authors = ["Vicinity"] 11 | s.summary = "TODO:" 12 | 13 | s.files = Dir["{config,lib}/**/*", "Rakefile"] 14 | s.require_paths = ["lib"] 15 | 16 | s.add_development_dependency "bundler", ">= 2" 17 | s.add_development_dependency "pry-byebug", ">= 3.4" 18 | s.add_development_dependency "rake", "~> 13.0" 19 | 20 | # Internal 21 | s.add_development_dependency "common-rubocop" 22 | s.add_development_dependency "common-testing" 23 | end 24 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/.gitignore.tt: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | spec/internal/log/*.log 5 | spec/internal/tmp/ 6 | spec/examples.txt 7 | tmp/ 8 | /node_modules 9 | /public/packs* 10 | yarn-debug.log* 11 | .yarn-integrity 12 | package-lock.json 13 | yarn-error.log 14 | .offspring 15 | .rspec-local 16 | /.pry_history 17 | /test-results 18 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/.pryrc: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | root_pryrc = File.join(__dir__, "../../.pryrc") 4 | if File.file?(root_pryrc) 5 | load(root_pryrc) 6 | end 7 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format Fuubar 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/.rubocop.yml.tt: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | shared-rubocop: config/base.yml 3 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) do |repo_name| 6 | repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") 7 | "https://github.com/#{repo_name}.git" 8 | end 9 | 10 | gemspec 11 | 12 | eval_gemfile "./Gemfile.runtime" 13 | eval_gemfile "./Gemfile.dev" 14 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/Gemfile.dev: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | eval_gemfile "../shared-testing/Gemfile.runtime" 4 | gem "shared-testing", path: "../shared-testing" 5 | 6 | gem "shared-rubocop", path: "../shared-rubocop" 7 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/Gemfile.runtime: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palkan/engems/6b824b182676dedc10ba88ea3aa5764e36f40e14/examples/generators/gem/templates/Gemfile.runtime -------------------------------------------------------------------------------- /examples/generators/gem/templates/README.md.tt: -------------------------------------------------------------------------------- 1 | # <%= class_name %> 2 | 3 | TODO: Write purpose and description. 4 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/Rakefile.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/bin/console.tt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "<%= name %>" 6 | 7 | require "pry" 8 | Pry.start 9 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/lib/%name%.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "<%= name %>/version" 4 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/lib/%name%/version.rb.tt: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module <%= class_name %> 4 | VERSION = "0.1.0" 5 | end 6 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | ENV["RAILS_ENV"] = "test" 4 | 5 | require "shared/testing/rails_configuration" 6 | -------------------------------------------------------------------------------- /examples/generators/gem/templates/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/setup" 4 | require "pry-byebug" 5 | 6 | require "shared/testing/rspec_configuration" 7 | -------------------------------------------------------------------------------- /forspell.dict: -------------------------------------------------------------------------------- 1 | # Format: one word per line. Empty lines and #-comments are supported too. 2 | # If you want to add word with its forms, you can write 'word: example' (without quotes) on the line, 3 | # where 'example' is existing word with the same possible forms (endings) as your word. 4 | # Example: deduplicate: duplicate 5 | Engems 6 | Engine-ering 7 | microservices 8 | -------------------------------------------------------------------------------- /guides/gemfiles.md: -------------------------------------------------------------------------------- 1 | # Gemfiles and gemspecs organization 2 | 3 | ## `Gemfile.dev`, `Gemfile.runtime` and `.gemspec` 4 | 5 | We use the following approach to define gems/engines dependencies and requirements: 6 | 7 | - `/.gemspec` – this is Gem specification; it defines **requirements** for this gem (for runtime and development); all required libraries (even _local_) must be specified in the gemspec: we use this information when checking _dirtyness_ of gems on CI (see [dirty-ci](../scripts/dirty-ci/README.md)) 8 | - `/Gemfile.runtime` – defines where to find non-RubyGems **runtime** dependencies (local and GitHub); this file is used by other gems via the [`eval_gemfile`](../scripts/bundler/README.md#eval_gemfile) method. 9 | - `/Gemfile.dev` – defines where to find non-RubyGems **development** dependencies (local and GitHub); always _includes_ (via `eval_gemfile`) the `Gemfile.runtime` file. 10 | - `/Gemfile` – only used for isolated development and contains the following: 11 | 12 | ```ruby 13 | source "https://rubygems.org" 14 | 15 | gemspec 16 | 17 | eval_gemfile "./Gemfile.dev" 18 | eval_gemfile "./Gemfile.runtime" 19 | ``` 20 | 21 | ## Universal Gemfile 22 | 23 | One of the problems of component-based architecture is dependencies synchronization between engines (including the root application). 24 | 25 | One possible solution for that is to use a **single lockfile** (`Gemfile.lock`) for all apps. We use the root application `Gemfile.lock` (since that's the one used in production). 26 | 27 | To do that, we need to configure a Gemfile path for components. It could be done via: 28 | 29 | - `BUNDLE_GEMFILE="../../Gemfile"` env variable (assuming your component is located at `/engines/`). 30 | - Alternatively, you can update a local bundler config: `bundle config --local gemfile="../../Gemfile"` (Make sure your `BUNDLE_APP_CONFIG` env var is not set or equal to `"/.bundle"`). 31 | 32 | To take into account components development dependencies, we need to specify them in the root Gemfile (see [`component`](../scripts/bundler/README.md#component)). 33 | 34 | Then, we need to make sure that we test our components in isolation. For that we use a custom bundle group (named as component). 35 | 36 | If you're using dummy Rails apps to test engines, you need to change the `config/application.rb` file the following way: 37 | 38 | ```diff 39 | - Bundler.require(*Rails.groups) 40 | + Bundler.require(:my_component) 41 | ``` 42 | 43 | If you're using [Combustion](./testing.md), then you need to [patch it](../scripts/combustion/README.md): 44 | 45 | ```ruby 46 | Combustion.initialize! :active_record, bundler_groups: :my_component do 47 | # ... 48 | end 49 | ``` 50 | -------------------------------------------------------------------------------- /guides/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | ## Combustion 4 | 5 | [Combustion](https://github.com/pat/combustion) allows you to minimize the burden of keeping a dummy Rails app in an engine's test suite. 6 | 7 | That's how we configure it: 8 | 9 | ```ruby 10 | require "combustion" 11 | 12 | begin 13 | Combustion.initialize! :active_record do 14 | config.logger = Logger.new(nil) 15 | config.log_level = :fatal 16 | 17 | # For Rails 6 18 | config.autoloader = :zeitwerk 19 | 20 | # Always use test adapter for active_job 21 | # config.active_job.queue_adapter = :test 22 | # 23 | # Always use test service to active_storage 24 | # config.active_storage.service = :test 25 | # 26 | # Enable verbose logging for active_record 27 | # config.active_record.verbose_query_logs = true 28 | end 29 | rescue => e 30 | # Fail fast if application couldn't be loaded 31 | $stdout.puts "Failed to load the app: #{e.message}\n#{e.backtrace.take(5).join("\n")}" 32 | exit(1) 33 | end 34 | ``` 35 | 36 | ## CI 37 | 38 | Component-based architecture has a benefit of isolated testing: each component has its test suite. 39 | That means, that we can avoid running tests for components, which haven't been "changed", for every commit. 40 | 41 | We use a helper tool ([is-dirty](../scripts/is-dirty/README.md)) for that. 42 | -------------------------------------------------------------------------------- /scripts/bundler/README.md: -------------------------------------------------------------------------------- 1 | # Bundler extensions for Engems 2 | 3 | ## `#component` method 4 | 5 | [Source](./component.rb) 6 | 7 | This helper method is used to define a component (engine) as a dependency when using a [shared Gemfile.lock](../../guides/gemfiles.md) for all engines. 8 | 9 | It does the following three things: 10 | 11 | - Define the component as a dependency for `:default` and its own **named group** (`gem "component", path: "engines/component"`, group: [:default, :component]). 12 | - Loads the component's `Gemfile.runtime` if any to the same groups (`eval_gemfile "engines/component/Gemfile.runtime"`). 13 | - Loads the component's `Gemfile.dev` if any to the same groups (`eval_gemfile "engines/component/Gemfile.dev"`). 14 | - Adds all development dependencies from the `component.gemspec` **only to the component group** (similar to `gem "dev-dep", group: [:component]). 15 | 16 | ## `#eval_gemfile` patch 17 | 18 | [Source](./eval_gemfile_patch.rb) 19 | 20 | This patch helps to avoid Bundler warnings regarding duplicate gems. It checks whether a Gemfile has been already 21 | included and skip it, if so. 22 | -------------------------------------------------------------------------------- /scripts/bundler/component.rb: -------------------------------------------------------------------------------- 1 | return if Bundler::Dsl.instance_methods.include?(:component) 2 | 3 | class Bundler::Dsl 4 | def component(name, namespace: "engines") 5 | # NOTE: Change to the project root path. 6 | Dir.chdir(__dir__) do 7 | component_group = name.to_sym 8 | 9 | group :default do 10 | # Add engine as a dependency 11 | gemspec name: name, path: "#{namespace}/#{name}", development_group: component_group 12 | 13 | # Add runtime non-RubyGems specs 14 | if File.readable?("#{namespace}/#{name}/Gemfile.runtime") 15 | eval_gemfile "#{namespace}/#{name}/Gemfile.runtime" 16 | end 17 | end 18 | 19 | group component_group do 20 | # Add dev non-RubyGems specs 21 | if File.readable?("#{namespace}/#{name}/Gemfile.dev") 22 | eval_gemfile "#{namespace}/#{name}/Gemfile.dev" 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /scripts/bundler/eval_gemfile_patch.rb: -------------------------------------------------------------------------------- 1 | return if Bundler::Dsl.instance_methods.include?(:eval_gemfile_original) 2 | 3 | class Bundler::Dsl 4 | alias eval_gemfile_original eval_gemfile 5 | 6 | def eval_gemfile(gemfile, contents = nil) 7 | expanded_gemfile_path = Pathname.new(gemfile).expand_path(@gemfile&.parent) 8 | return if @gemfiles.any? { |path| path == expanded_gemfile_path } 9 | 10 | eval_gemfile_original(gemfile, contents) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /scripts/combustion/README.md: -------------------------------------------------------------------------------- 1 | # Combustion patches 2 | 3 | The [patch](./combustion_bundler_patch.rb) allows specifying Bundler groups to load for Combustion application. 4 | -------------------------------------------------------------------------------- /scripts/combustion/combustion_bundler_patch.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Hijack Bundler.require from Combustion.initialize! to load engine-specific group 4 | Combustion.singleton_class.prepend(Module.new do 5 | def initialize!(*args, bundler_groups: nil, **kwargs) 6 | if bundler_groups 7 | original_require = Bundler.method(:require) 8 | Bundler.define_singleton_method(:require) { |*| original_require.call(*bundler_groups) } 9 | end 10 | 11 | super 12 | ensure 13 | Bundler.define_singleton_method(:require, original_require) if bundler_groups 14 | end 15 | end) 16 | -------------------------------------------------------------------------------- /scripts/dirty-ci/README.md: -------------------------------------------------------------------------------- 1 | # Dirty checking for CI 2 | 3 | Utility tool ([`is-dirty`](./is-dirty)) to check whether the changes to the gem/engine have been made 4 | comparing to the master branch. 5 | 6 | It automatically builds a dependency tree for all local gems/engines (using `.gemspec` files) 7 | and considers a gem _dirty_ iff there are changes in its code or in any of its dependencies. 8 | 9 | **NOTE:** When the current branch is master then everything is considered _dirty_ (to make sure that we run 10 | all tests on master). You can additional _always-dirty_ branches by updating the `ALWAYS_RUN` constant. 11 | 12 | ## Usage 13 | 14 | ```sh 15 | .ci/is-dirty my_component || \ 16 | (cd engines/my_component && bundle exec rspec) 17 | ``` 18 | 19 | ## Refs 20 | 21 | Inspired by this post [https://medium.com/@dan_manges/the-modular-monolith-rails-architecture-fb1023826fc4](https://medium.com/@dan_manges/the-modular-monolith-rails-architecture-fb1023826fc4). 22 | -------------------------------------------------------------------------------- /scripts/dirty-ci/is-dirty: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # frozen_string_literal: true 4 | 5 | require "set" 6 | 7 | module DirtyCI 8 | # The list of branches which should not 9 | # use dirty checking and run all checks always 10 | ALWAYS_RUN = %w[master].freeze 11 | 12 | module_function 13 | 14 | def build_dependency_index 15 | pattern = File.join(__dir__, "../{engines,gems}/*/*.gemspec") 16 | 17 | gemspecs = Dir.glob(pattern).map do |gemspec_file| 18 | Gem::Specification.load(gemspec_file) 19 | end 20 | 21 | names = gemspecs.each_with_object({}) do |gemspec, hash| 22 | hash[gemspec.name] = [] 23 | end 24 | 25 | tree = gemspecs.each_with_object(names) do |gemspec, hash| 26 | deps = Set.new(gemspec.dependencies.map(&:name)) + Set.new(gemspec.development_dependencies.map(&:name)) 27 | local_deps = deps & Set.new(names.keys) 28 | local_deps.each do |local_dep| 29 | hash[local_dep] << gemspec.name 30 | end 31 | end 32 | 33 | # invert tree to show which gem depends on what 34 | tree.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |(name, deps), index| 35 | deps.each { |dep| index[dep] << name } 36 | index 37 | end 38 | end 39 | 40 | def dirty_libraries 41 | changed_files = `git diff $(git merge-base origin/master HEAD) --name-only`.split("\n") 42 | raise "failed to get changed files" unless $?.success? 43 | 44 | changed_files.each_with_object(Set.new) do |file, changeset| 45 | case file 46 | when %r{^gems/([^\/]+)} 47 | changeset << Regexp.last_match[1] 48 | when %r{^engines/([^\/]+)} 49 | changeset << Regexp.last_match[1] 50 | end 51 | 52 | changeset 53 | end 54 | end 55 | 56 | def dirty?(name) 57 | current_branch = `git rev-parse --abbrev-ref HEAD`.tr("/", "--").chomp 58 | return true if ALWAYS_RUN.include?(current_branch) 59 | 60 | libs = dirty_libraries 61 | 62 | return true if libs.include?(name) 63 | 64 | deps = build_dependency_index[name] 65 | 66 | (deps & libs).any? 67 | end 68 | end 69 | 70 | if ARGV.size == 1 71 | lib_name = ARGV[0] 72 | if DirtyCI.dirty?(lib_name) 73 | $stdout.puts "[Dirty Check] #{lib_name} is dirty" 74 | exit(1) 75 | else 76 | $stdout.puts "[Dirty Check] No changes for #{lib_name}. Skip" 77 | exit(0) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /scripts/engem-cli/README.md: -------------------------------------------------------------------------------- 1 | # `bin/engem` 2 | 3 | [Engem CLI](./engem) is a utility tool which helps you to manege engines and gems from the app's root directory. 4 | 5 | Put in in your `bin/` folder and use like this: 6 | 7 | ```sh 8 | # show available commands 9 | bin/engem --help 10 | 11 | # run a specific test 12 | bin/engem core_by rspec spec/models/core_by/city.rb:5 13 | 14 | # run Rails console 15 | bin/engem core_by console 16 | 17 | # runs `bundle install`, `rubocop` and `rspec` by default 18 | bin/engem core_by build 19 | 20 | # you can omit running rspec/rubocop by providing `--skip-rspec`/`--skip-rubocop` option: 21 | bin/engem shared-rubocop build --skip-rspec 22 | 23 | # generate a migration 24 | bin/engem core_by rails g migration 25 | 26 | # engem automatically detects gems and engines (i.e. libs ubder engines/ and gems/ directories) 27 | # you don't have to specify whether it's a gem or engine 28 | bin/engem shared-testing build 29 | 30 | # you can run command for all engines/gems at once by using "all" name 31 | bin/engem all build 32 | 33 | # or just for engines 34 | bin/engem all-engines build 35 | 36 | # or just for gems 37 | bin/engem all-gems build 38 | 39 | # the execution halts as soon as the command fails for one of the engines/gems; 40 | # to disable this fail-fast behaviour use `--ignore-failures` switch 41 | bin/engem all build --ignore-failures 42 | ``` 43 | -------------------------------------------------------------------------------- /scripts/engem-cli/engem: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | begin 5 | require "thor" 6 | require "bundler" 7 | rescue LoadError 8 | require "bundler/inline" 9 | 10 | gemfile do 11 | source "https://rubygems.org" 12 | gem "thor" 13 | end 14 | 15 | require "thor" 16 | end 17 | 18 | require "shellwords" 19 | 20 | class EngineGemCLI < Thor 21 | class CommandFailedException < StandardError; end 22 | 23 | RESERVED_COMMANDS = %i[all gems engines].freeze 24 | 25 | class_option :ignore_failures, type: :boolean, default: false 26 | 27 | def self.exit_on_failure? 28 | true 29 | end 30 | 31 | desc "NAME bundle [OPTIONS]", "Run Engine's `bundle` command" 32 | def bundle(*args) 33 | exec_command("bundle", *args) 34 | end 35 | 36 | %w[rake rubocop rspec].each do |cmd| 37 | desc "NAME #{cmd} [OPTIONS]", "Run Engine's `#{cmd}` command" 38 | define_method(cmd) do |*args| 39 | bundle(*(["exec", cmd] + args)) 40 | end 41 | end 42 | 43 | desc "NAME console", "Run Engine's console" 44 | def console 45 | exec_command("./bin/console") 46 | end 47 | 48 | desc "NAME rails", "Run Engine's Rails commands" 49 | def rails(*args) 50 | exec_command("./bin/rails", *args) 51 | end 52 | 53 | desc "NAME yarn", "Run Engine's Yarn commands" 54 | def yarn(*args) 55 | exec_command("yarn", *args) 56 | end 57 | 58 | desc "NAME build", "Install deps, run linters and tests for the engine" 59 | method_option :skip_rspec, type: :boolean, default: false 60 | method_option :skip_rubocop, type: :boolean, default: false 61 | def build 62 | bundle("--quiet") 63 | rubocop("--safe-auto-correct") unless options["skip_rubocop"] 64 | rspec unless options["skip_rspec"] 65 | end 66 | 67 | def method_missing(engine_or_gem, *args) # rubocop:disable Style/MissingRespondToMissing 68 | options = 69 | case engine_or_gem.to_s 70 | when "all" 71 | {all: true} 72 | when "all-gems" 73 | {all_gems: true} 74 | when "all-engines" 75 | {all_engines: true} 76 | when *engines 77 | {engine: engine_or_gem.to_s} 78 | when *gems 79 | {engine: engine_or_gem.to_s, gem: true} 80 | end 81 | 82 | return super if options.nil? 83 | 84 | self.class.start(args, class_options: options) 85 | end 86 | 87 | private 88 | 89 | def engine 90 | @engine ||= options[:engine] || ENV["ENGINE"] 91 | end 92 | 93 | def engines 94 | @engines ||= Dir["*", base: "engines"] 95 | end 96 | 97 | def gems 98 | @gems ||= Dir["*", base: "gems"] 99 | end 100 | 101 | def engine_root 102 | @engine_root ||= "#{options[:gem] ? "gems" : "engines"}/#{engine}" 103 | end 104 | 105 | def app_root 106 | @app_root ||= File.expand_path("..", __dir__) 107 | end 108 | 109 | def exec_command(*args) 110 | if options[:all] || options[:all_gems] || options[:all_engines] 111 | if options[:all] || options[:all_gems] 112 | gems.each do |gem| 113 | subshell "gems/#{gem}", *args 114 | end 115 | end 116 | 117 | if options[:all] || options[:all_engines] 118 | engines.each do |engine| 119 | subshell "engines/#{engine}", *args 120 | end 121 | end 122 | else 123 | if engine.nil? || engine.empty? 124 | # Try to find engine name from args 125 | @engine = args.each do |arg| 126 | next unless match = arg&.match(%r{^(engines|gems)/(?[a-z_]+)}) # rubocop:disable Lint/AssignmentInCondition 127 | break match[:name] 128 | end 129 | 130 | if engine 131 | args.map! { |arg| arg ? arg.sub("#{engine_root}/", "") : arg } 132 | else 133 | raise "Engine name has not been provided." 134 | end 135 | end 136 | 137 | subshell(engine_root, *args) 138 | end 139 | end 140 | 141 | def subshell(dir, cmd, *args) 142 | Bundler.with_original_env do 143 | new_root = File.join(app_root, dir) 144 | 145 | Dir.chdir(new_root) do 146 | return if Kernel.system(cmd, *args) 147 | end 148 | end 149 | 150 | msg = "Command '#{cmd}' (from #{dir}) returned non-zero exit code" 151 | 152 | $stdout.puts msg 153 | raise CommandFailedException, msg unless options["ignore_failures"] 154 | end 155 | end 156 | 157 | begin 158 | EngineGemCLI.start(ARGV) 159 | rescue EngineGemCLI::CommandFailedException => e 160 | if ENV["DEBUG"] 161 | puts "Command failed with: #{e.message}" 162 | puts e.backtrace.take(15).join("\n") 163 | end 164 | exit(1) 165 | end 166 | --------------------------------------------------------------------------------