├── .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 |
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 |
--------------------------------------------------------------------------------