diff --git a/contrib/multiple_databases_repository_sample_app/.gitignore b/contrib/multiple_databases_repository_sample_app/.gitignore new file mode 100644 index 0000000000..c4bd440cfd --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/.gitignore @@ -0,0 +1,20 @@ +!/log/.keep +!/storage/.keep +!/tmp/.keep +.byebug_history +.yarn-integrity +/.bundle +/config/master.key +/db/*.sqlite3 +/db/*.sqlite3-journal +/elm-stuff +/log/* +/node_modules +/public/assets +/public/packs +/public/packs-test +/storage/* +/tmp/* +/yarn-error.log +coverage +yarn-debug.log* diff --git a/contrib/multiple_databases_repository_sample_app/.rspec b/contrib/multiple_databases_repository_sample_app/.rspec new file mode 100644 index 0000000000..c99d2e7396 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/contrib/multiple_databases_repository_sample_app/Gemfile b/contrib/multiple_databases_repository_sample_app/Gemfile new file mode 100644 index 0000000000..ecdd2e1742 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/Gemfile @@ -0,0 +1,38 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +ruby '2.6.6' + +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails', '~> 6.0.3', '>= 6.0.3.1' +# Use sqlite3 as the database for Active Record +gem 'sqlite3', '~> 1.4' +# Use Puma as the app server +gem 'puma', '~> 4.1' +# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder +gem 'jbuilder', '~> 2.7' +# Use Active Model has_secure_password +# gem 'bcrypt', '~> 3.1.7' + +group :development, :test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] + gem 'rspec-rails' + gem 'database_cleaner' + gem 'rails_event_store-rspec' +end + +group :development do + # Access an interactive console on exception pages or by calling 'console' anywhere in the code. + gem 'web-console', '>= 3.3.0' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] + +gem 'dry-struct' +gem 'dry-types' + +gem 'ruby_event_store', path: '../../ruby_event_store' +gem 'rails_event_store_active_record', path: '../../rails_event_store_active_record' +gem 'rails_event_store', path: '../../rails_event_store' diff --git a/contrib/multiple_databases_repository_sample_app/README.md b/contrib/multiple_databases_repository_sample_app/README.md new file mode 100644 index 0000000000..b600c3f026 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/README.md @@ -0,0 +1,5 @@ +# Domain description + +TDB + +[Event storming board](https://miro.com/app/embed/o9J_krLAGIQ=/?) diff --git a/contrib/multiple_databases_repository_sample_app/Rakefile b/contrib/multiple_databases_repository_sample_app/Rakefile new file mode 100644 index 0000000000..e85f913914 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/contrib/multiple_databases_repository_sample_app/app/assets/config/manifest.js b/contrib/multiple_databases_repository_sample_app/app/assets/config/manifest.js new file mode 100644 index 0000000000..591819335f --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/app/assets/config/manifest.js @@ -0,0 +1,2 @@ +//= link_tree ../images +//= link_directory ../stylesheets .css diff --git a/contrib/multiple_databases_repository_sample_app/app/assets/stylesheets/application.css b/contrib/multiple_databases_repository_sample_app/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..d05ea0f511 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/contrib/multiple_databases_repository_sample_app/app/controllers/application_controller.rb b/contrib/multiple_databases_repository_sample_app/app/controllers/application_controller.rb new file mode 100644 index 0000000000..09705d12ab --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/contrib/multiple_databases_repository_sample_app/app/helpers/application_helper.rb b/contrib/multiple_databases_repository_sample_app/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/contrib/multiple_databases_repository_sample_app/app/jobs/application_job.rb b/contrib/multiple_databases_repository_sample_app/app/jobs/application_job.rb new file mode 100644 index 0000000000..d394c3d106 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/contrib/multiple_databases_repository_sample_app/app/models/application_record.rb b/contrib/multiple_databases_repository_sample_app/app/models/application_record.rb new file mode 100644 index 0000000000..10a4cba84d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/contrib/multiple_databases_repository_sample_app/app/views/layouts/application.html.erb b/contrib/multiple_databases_repository_sample_app/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..f06473dab7 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/app/views/layouts/application.html.erb @@ -0,0 +1,14 @@ + + + + Sample + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag 'application', media: 'all' %> + + + + <%= yield %> + + diff --git a/contrib/multiple_databases_repository_sample_app/config.ru b/contrib/multiple_databases_repository_sample_app/config.ru new file mode 100644 index 0000000000..f7ba0b527b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config.ru @@ -0,0 +1,5 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/contrib/multiple_databases_repository_sample_app/config/application.rb b/contrib/multiple_databases_repository_sample_app/config/application.rb new file mode 100644 index 0000000000..c2fe0ab597 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/application.rb @@ -0,0 +1,38 @@ +require_relative 'boot' + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +# require "active_storage/engine" +require "action_controller/railtie" +# require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" +require "action_view/railtie" +# require "action_cable/engine" +# require "sprockets/railtie" +# require "rails/test_unit/railtie" + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Sample + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 6.0 + + # Settings in config/environments/* take precedence over those specified here. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. + config.paths.add 'orders/lib', eager_load: true + config.paths.add 'payments/lib', eager_load: true + config.paths.add 'shipping/lib', eager_load: true + + # Don't generate system test files. + config.generators.system_tests = nil + end +end diff --git a/contrib/multiple_databases_repository_sample_app/config/boot.rb b/contrib/multiple_databases_repository_sample_app/config/boot.rb new file mode 100644 index 0000000000..30f5120df6 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/boot.rb @@ -0,0 +1,3 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/contrib/multiple_databases_repository_sample_app/config/credentials.yml.enc b/contrib/multiple_databases_repository_sample_app/config/credentials.yml.enc new file mode 100644 index 0000000000..7a43605f93 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/credentials.yml.enc @@ -0,0 +1 @@ +d1WzgYIe3R/fpHfTZfMP5wrp9uDbDMQc2SBiBqg5sqbHZKnTNktELx1YrV9xCcJqUa/V7RBEvsfXBAuu/5luTXkK/sKKL81mlcNuy57i09y7LjQ+ojRB6aWpTcquXM8RSDimSmjKQd9QKDNLxRKGK4wHhBWDa4+4urG2n72+6iVH4JvycVFv5Wgzkhe+su22rraZOKg7LLE9Nvrwrwf1Is1mDHJdxTZqnS4JzWljGbCGFO5AXhyEPp/zHJ/B1zooCcEeO/TUVpxU9OB1bjhIslKY0S+JFPsheY56eYv6WOEpt9EZAPNe4ZEj71H131kVZyD9SZajq+bUCJePJuKjMRjL2w/pkrKXXi9yz4W4bDsKvM2Oz4CNOtXePYe9G+YauExNYEO79oghgxz1UFUQEy1WGZfi/pj6I2mj--BY9vm19PlrY42f7d--DMlEvIHGiG+9KuB6ArAYkA== \ No newline at end of file diff --git a/contrib/multiple_databases_repository_sample_app/config/database.yml b/contrib/multiple_databases_repository_sample_app/config/database.yml new file mode 100644 index 0000000000..38f4810f4d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/database.yml @@ -0,0 +1,64 @@ +# SQLite. Versions 3.8.0 and up are supported. +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +# +default: &default + adapter: sqlite3 + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + timeout: 5000 + +development: + primary: + <<: *default + database: db/development.sqlite3 + orders: + <<: *default + database: db/orders_development.sqlite3 + migrations_paths: db/orders + payments: + <<: *default + database: db/payments_development.sqlite3 + migrations_paths: db/payments + shipping: + <<: *default + database: db/shipping_development.sqlite3 + migrations_paths: db/shipping + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + primary: + <<: *default + database: db/test.sqlite3 + orders: + <<: *default + database: db/orders_test.sqlite3 + migrations_paths: db/orders + payments: + <<: *default + database: db/payments_test.sqlite3 + migrations_paths: db/payments + shipping: + <<: *default + database: db/shipping_test.sqlite3 + migrations_paths: db/shipping + +production: + primary: + <<: *default + database: db/production.sqlite3 + orders: + <<: *default + database: db/orders_production.sqlite3 + migrations_paths: db/orders + payments: + <<: *default + database: db/payments_production.sqlite3 + migrations_paths: db/payments + shipping: + <<: *default + database: db/shipping_production.sqlite3 + migrations_paths: db/shipping diff --git a/contrib/multiple_databases_repository_sample_app/config/environment.rb b/contrib/multiple_databases_repository_sample_app/config/environment.rb new file mode 100644 index 0000000000..426333bb46 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/contrib/multiple_databases_repository_sample_app/config/environments/development.rb b/contrib/multiple_databases_repository_sample_app/config/environments/development.rb new file mode 100644 index 0000000000..7eafa4acaf --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/environments/development.rb @@ -0,0 +1,49 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? + config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{2.days.to_i}" + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + + # Raises error for missing translations. + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + config.number_generator_factory = ->{ Orders::NumberGenerator.new } +end diff --git a/contrib/multiple_databases_repository_sample_app/config/environments/production.rb b/contrib/multiple_databases_repository_sample_app/config/environments/production.rb new file mode 100644 index 0000000000..adfbaa8a5a --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/environments/production.rb @@ -0,0 +1,94 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment). + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "sample_production" + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session + + config.number_generator_factory = -> { Orders::NumberGenerator.new } +end diff --git a/contrib/multiple_databases_repository_sample_app/config/environments/test.rb b/contrib/multiple_databases_repository_sample_app/config/environments/test.rb new file mode 100644 index 0000000000..1308f3401a --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/environments/test.rb @@ -0,0 +1,40 @@ +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! + +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + config.cache_store = :null_store + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations. + # config.action_view.raise_on_missing_translations = true + + config.number_generator_factory = ->{ Orders::FakeNumberGenerator.new } +end diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/application_controller_renderer.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000000..89d2efab2b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/backtrace_silencers.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000000..59385cdf37 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/content_security_policy.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/content_security_policy.rb new file mode 100644 index 0000000000..41c43016f1 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/content_security_policy.rb @@ -0,0 +1,28 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Set the nonce only to specific directives +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/cookies_serializer.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000000..5a6a32d371 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/filter_parameter_logging.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000000..4a994e1e7b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/inflections.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/inflections.rb new file mode 100644 index 0000000000..ac033bf9dc --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/mime_types.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/mime_types.rb new file mode 100644 index 0000000000..dc1899682b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/rails_event_store.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/rails_event_store.rb new file mode 100644 index 0000000000..f76b95061d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/rails_event_store.rb @@ -0,0 +1,41 @@ +require 'rails_event_store' +require 'aggregate_root' +require 'arkency/command_bus' + +Rails.configuration.to_prepare do + events_class_remapping = {} + + Rails.configuration.event_repository = + RailsEventStoreActiveRecord::EventRepository.new(serializer: JSON) + Rails.configuration.event_store = RailsEventStore::Client.new( + repository: Rails.configuration.event_repository, + mapper: RubyEventStore::Mappers::Default.new( + events_class_remapping: events_class_remapping + ), + dispatcher: RubyEventStore::ComposedDispatcher.new( + RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)), + RubyEventStore::Dispatcher.new), + ) + Rails.configuration.command_bus = Arkency::CommandBus.new + + AggregateRoot.configure do |config| + config.default_event_store = Rails.configuration.event_store + end + + # Subscribe event handlers below + # Rails.configuration.event_store.tap do |store| + # store.subscribe(InvoiceReadModel.new, to: [InvoicePrinted]) + # store.subscribe(->(event) { SendOrderConfirmation.new.call(event) }, to: [OrderSubmitted]) + # store.subscribe_to_all_events(->(event) { Rails.logger.info(event.type) }) + # end + + # Register command handlers below + # Rails.configuration.command_bus.tap do |bus| + # bus.register(PrintInvoice, Invoicing::OnPrint.new) + # bus.register(SubmitOrder, ->(cmd) { Orders::OnSubmitOrder.new.call(cmd) }) + # end + + Orders.setup(Rails.configuration) + Payments.setup(Rails.configuration) + Shipping.setup(Rails.configuration) +end diff --git a/contrib/multiple_databases_repository_sample_app/config/initializers/wrap_parameters.rb b/contrib/multiple_databases_repository_sample_app/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000000..bbfc3961bf --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/contrib/multiple_databases_repository_sample_app/config/locales/en.yml b/contrib/multiple_databases_repository_sample_app/config/locales/en.yml new file mode 100644 index 0000000000..cf9b342d0a --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/contrib/multiple_databases_repository_sample_app/config/puma.rb b/contrib/multiple_databases_repository_sample_app/config/puma.rb new file mode 100644 index 0000000000..5ed4437744 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/puma.rb @@ -0,0 +1,38 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/contrib/multiple_databases_repository_sample_app/config/routes.rb b/contrib/multiple_databases_repository_sample_app/config/routes.rb new file mode 100644 index 0000000000..96a6bd1396 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/config/routes.rb @@ -0,0 +1,4 @@ +Rails.application.routes.draw do + mount RailsEventStore::Browser => '/res' if Rails.env.development? + # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html +end diff --git a/contrib/multiple_databases_repository_sample_app/db/migrate/20200604152933_create_event_store_events.rb b/contrib/multiple_databases_repository_sample_app/db/migrate/20200604152933_create_event_store_events.rb new file mode 100644 index 0000000000..c583aaea5b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/migrate/20200604152933_create_event_store_events.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class CreateEventStoreEvents < ActiveRecord::Migration[4.2] + def change + postgres = ActiveRecord::Base.connection.adapter_name == "PostgreSQL" + sqlite = ActiveRecord::Base.connection.adapter_name == "SQLite" + rails_42 = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0") + enable_extension "pgcrypto" if postgres + create_table(:event_store_events_in_streams, force: false) do |t| + t.string :stream, null: false + t.integer :position, null: true + if postgres + t.references :event, null: false, type: :uuid + else + t.references :event, null: false, type: :string, limit: 36 + end + t.datetime :created_at, null: false + end + add_index :event_store_events_in_streams, [:stream, :position], unique: true + add_index :event_store_events_in_streams, [:created_at] + add_index :event_store_events_in_streams, [:stream, :event_id], unique: true + + if postgres + create_table(:event_store_events, id: :uuid, default: 'gen_random_uuid()', force: false) do |t| + t.string :event_type, null: false + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false + end + else + create_table(:event_store_events, id: false, force: false) do |t| + t.string :id, limit: 36, primary_key: true, null: false + t.string :event_type, null: false + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false + end + if sqlite && rails_42 + add_index :event_store_events, :id, unique: true + end + end + add_index :event_store_events, :created_at + add_index :event_store_events, :event_type + end +end diff --git a/contrib/multiple_databases_repository_sample_app/db/orders/20200604152933_create_event_store_events.rb b/contrib/multiple_databases_repository_sample_app/db/orders/20200604152933_create_event_store_events.rb new file mode 100644 index 0000000000..c583aaea5b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/orders/20200604152933_create_event_store_events.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class CreateEventStoreEvents < ActiveRecord::Migration[4.2] + def change + postgres = ActiveRecord::Base.connection.adapter_name == "PostgreSQL" + sqlite = ActiveRecord::Base.connection.adapter_name == "SQLite" + rails_42 = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0") + enable_extension "pgcrypto" if postgres + create_table(:event_store_events_in_streams, force: false) do |t| + t.string :stream, null: false + t.integer :position, null: true + if postgres + t.references :event, null: false, type: :uuid + else + t.references :event, null: false, type: :string, limit: 36 + end + t.datetime :created_at, null: false + end + add_index :event_store_events_in_streams, [:stream, :position], unique: true + add_index :event_store_events_in_streams, [:created_at] + add_index :event_store_events_in_streams, [:stream, :event_id], unique: true + + if postgres + create_table(:event_store_events, id: :uuid, default: 'gen_random_uuid()', force: false) do |t| + t.string :event_type, null: false + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false + end + else + create_table(:event_store_events, id: false, force: false) do |t| + t.string :id, limit: 36, primary_key: true, null: false + t.string :event_type, null: false + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false + end + if sqlite && rails_42 + add_index :event_store_events, :id, unique: true + end + end + add_index :event_store_events, :created_at + add_index :event_store_events, :event_type + end +end diff --git a/contrib/multiple_databases_repository_sample_app/db/orders_schema.rb b/contrib/multiple_databases_repository_sample_app/db/orders_schema.rb new file mode 100644 index 0000000000..01923995c3 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/orders_schema.rb @@ -0,0 +1,34 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2020_06_04_152933) do + + create_table "event_store_events", id: :string, limit: 36, force: :cascade do |t| + t.string "event_type", null: false + t.binary "metadata" + t.binary "data", null: false + t.datetime "created_at", null: false + t.index ["created_at"], name: "index_event_store_events_on_created_at" + t.index ["event_type"], name: "index_event_store_events_on_event_type" + end + + create_table "event_store_events_in_streams", force: :cascade do |t| + t.string "stream", null: false + t.integer "position" + t.string "event_id", limit: 36, null: false + t.datetime "created_at", null: false + t.index ["created_at"], name: "index_event_store_events_in_streams_on_created_at" + t.index ["stream", "event_id"], name: "index_event_store_events_in_streams_on_stream_and_event_id", unique: true + t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true + end + +end diff --git a/contrib/multiple_databases_repository_sample_app/db/payments/20200604152933_create_event_store_events.rb b/contrib/multiple_databases_repository_sample_app/db/payments/20200604152933_create_event_store_events.rb new file mode 100644 index 0000000000..c583aaea5b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/payments/20200604152933_create_event_store_events.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class CreateEventStoreEvents < ActiveRecord::Migration[4.2] + def change + postgres = ActiveRecord::Base.connection.adapter_name == "PostgreSQL" + sqlite = ActiveRecord::Base.connection.adapter_name == "SQLite" + rails_42 = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0") + enable_extension "pgcrypto" if postgres + create_table(:event_store_events_in_streams, force: false) do |t| + t.string :stream, null: false + t.integer :position, null: true + if postgres + t.references :event, null: false, type: :uuid + else + t.references :event, null: false, type: :string, limit: 36 + end + t.datetime :created_at, null: false + end + add_index :event_store_events_in_streams, [:stream, :position], unique: true + add_index :event_store_events_in_streams, [:created_at] + add_index :event_store_events_in_streams, [:stream, :event_id], unique: true + + if postgres + create_table(:event_store_events, id: :uuid, default: 'gen_random_uuid()', force: false) do |t| + t.string :event_type, null: false + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false + end + else + create_table(:event_store_events, id: false, force: false) do |t| + t.string :id, limit: 36, primary_key: true, null: false + t.string :event_type, null: false + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false + end + if sqlite && rails_42 + add_index :event_store_events, :id, unique: true + end + end + add_index :event_store_events, :created_at + add_index :event_store_events, :event_type + end +end diff --git a/contrib/multiple_databases_repository_sample_app/db/payments_schema.rb b/contrib/multiple_databases_repository_sample_app/db/payments_schema.rb new file mode 100644 index 0000000000..01923995c3 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/payments_schema.rb @@ -0,0 +1,34 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2020_06_04_152933) do + + create_table "event_store_events", id: :string, limit: 36, force: :cascade do |t| + t.string "event_type", null: false + t.binary "metadata" + t.binary "data", null: false + t.datetime "created_at", null: false + t.index ["created_at"], name: "index_event_store_events_on_created_at" + t.index ["event_type"], name: "index_event_store_events_on_event_type" + end + + create_table "event_store_events_in_streams", force: :cascade do |t| + t.string "stream", null: false + t.integer "position" + t.string "event_id", limit: 36, null: false + t.datetime "created_at", null: false + t.index ["created_at"], name: "index_event_store_events_in_streams_on_created_at" + t.index ["stream", "event_id"], name: "index_event_store_events_in_streams_on_stream_and_event_id", unique: true + t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true + end + +end diff --git a/contrib/multiple_databases_repository_sample_app/db/schema.rb b/contrib/multiple_databases_repository_sample_app/db/schema.rb new file mode 100644 index 0000000000..01923995c3 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/schema.rb @@ -0,0 +1,34 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2020_06_04_152933) do + + create_table "event_store_events", id: :string, limit: 36, force: :cascade do |t| + t.string "event_type", null: false + t.binary "metadata" + t.binary "data", null: false + t.datetime "created_at", null: false + t.index ["created_at"], name: "index_event_store_events_on_created_at" + t.index ["event_type"], name: "index_event_store_events_on_event_type" + end + + create_table "event_store_events_in_streams", force: :cascade do |t| + t.string "stream", null: false + t.integer "position" + t.string "event_id", limit: 36, null: false + t.datetime "created_at", null: false + t.index ["created_at"], name: "index_event_store_events_in_streams_on_created_at" + t.index ["stream", "event_id"], name: "index_event_store_events_in_streams_on_stream_and_event_id", unique: true + t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true + end + +end diff --git a/contrib/multiple_databases_repository_sample_app/db/seeds.rb b/contrib/multiple_databases_repository_sample_app/db/seeds.rb new file mode 100644 index 0000000000..1beea2accd --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). +# +# Examples: +# +# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) +# Character.create(name: 'Luke', movie: movies.first) diff --git a/contrib/multiple_databases_repository_sample_app/db/shipping/20200604152933_create_event_store_events.rb b/contrib/multiple_databases_repository_sample_app/db/shipping/20200604152933_create_event_store_events.rb new file mode 100644 index 0000000000..c583aaea5b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/shipping/20200604152933_create_event_store_events.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +class CreateEventStoreEvents < ActiveRecord::Migration[4.2] + def change + postgres = ActiveRecord::Base.connection.adapter_name == "PostgreSQL" + sqlite = ActiveRecord::Base.connection.adapter_name == "SQLite" + rails_42 = Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new("5.0.0") + enable_extension "pgcrypto" if postgres + create_table(:event_store_events_in_streams, force: false) do |t| + t.string :stream, null: false + t.integer :position, null: true + if postgres + t.references :event, null: false, type: :uuid + else + t.references :event, null: false, type: :string, limit: 36 + end + t.datetime :created_at, null: false + end + add_index :event_store_events_in_streams, [:stream, :position], unique: true + add_index :event_store_events_in_streams, [:created_at] + add_index :event_store_events_in_streams, [:stream, :event_id], unique: true + + if postgres + create_table(:event_store_events, id: :uuid, default: 'gen_random_uuid()', force: false) do |t| + t.string :event_type, null: false + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false + end + else + create_table(:event_store_events, id: false, force: false) do |t| + t.string :id, limit: 36, primary_key: true, null: false + t.string :event_type, null: false + t.binary :metadata + t.binary :data, null: false + t.datetime :created_at, null: false + end + if sqlite && rails_42 + add_index :event_store_events, :id, unique: true + end + end + add_index :event_store_events, :created_at + add_index :event_store_events, :event_type + end +end diff --git a/contrib/multiple_databases_repository_sample_app/db/shipping/20200622154807_shipping_process_state.rb b/contrib/multiple_databases_repository_sample_app/db/shipping/20200622154807_shipping_process_state.rb new file mode 100644 index 0000000000..06c5dd105e --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/shipping/20200622154807_shipping_process_state.rb @@ -0,0 +1,9 @@ +class ShippingProcessState < ActiveRecord::Migration[6.0] + def change + create_table :shipping_process_state, id: :string, primary_key: :order_id do |t| + t.integer :customer_id + t.integer :delivery_address_id + t.binary :states + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/db/shipping_schema.rb b/contrib/multiple_databases_repository_sample_app/db/shipping_schema.rb new file mode 100644 index 0000000000..8345c08e49 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/db/shipping_schema.rb @@ -0,0 +1,40 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2020_06_22_154807) do + + create_table "event_store_events", id: :string, limit: 36, force: :cascade do |t| + t.string "event_type", null: false + t.binary "metadata" + t.binary "data", null: false + t.datetime "created_at", null: false + t.index ["created_at"], name: "index_event_store_events_on_created_at" + t.index ["event_type"], name: "index_event_store_events_on_event_type" + end + + create_table "event_store_events_in_streams", force: :cascade do |t| + t.string "stream", null: false + t.integer "position" + t.string "event_id", limit: 36, null: false + t.datetime "created_at", null: false + t.index ["created_at"], name: "index_event_store_events_in_streams_on_created_at" + t.index ["stream", "event_id"], name: "index_event_store_events_in_streams_on_stream_and_event_id", unique: true + t.index ["stream", "position"], name: "index_event_store_events_in_streams_on_stream_and_position", unique: true + end + + create_table "shipping_process_state", primary_key: "order_id", id: :string, force: :cascade do |t| + t.integer "customer_id" + t.integer "delivery_address_id" + t.binary "states" + end + +end diff --git a/contrib/multiple_databases_repository_sample_app/lib/command.rb b/contrib/multiple_databases_repository_sample_app/lib/command.rb new file mode 100644 index 0000000000..e8d4cee961 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/lib/command.rb @@ -0,0 +1,11 @@ +require 'dry-struct' + +class Command < Dry::Struct::Value + Invalid = Class.new(StandardError) + + def self.new(*) + super + rescue Dry::Struct::Error => doh + raise Invalid, doh + end +end diff --git a/contrib/multiple_databases_repository_sample_app/lib/command_handler.rb b/contrib/multiple_databases_repository_sample_app/lib/command_handler.rb new file mode 100644 index 0000000000..9132e92f7e --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/lib/command_handler.rb @@ -0,0 +1,21 @@ +module CommandHandler + def initialize(event_store) + @repository = AggregateRoot::InstrumentedRepository.new( + AggregateRoot::Repository.new(event_store), + ActiveSupport::Notifications + ) + end + + def with_aggregate(aggregate, aggregate_id, &block) + stream = stream_name(aggregate.class, aggregate_id) + repository.with_aggregate(aggregate, stream, &block) + end + + private + attr_reader :repository + + def stream_name(aggregate_class, aggregate_id) + "#{aggregate_class.name}$#{aggregate_id}" + end +end + diff --git a/contrib/multiple_databases_repository_sample_app/lib/event.rb b/contrib/multiple_databases_repository_sample_app/lib/event.rb new file mode 100644 index 0000000000..c4d59f83b0 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/lib/event.rb @@ -0,0 +1,55 @@ +require "ruby_event_store" +require 'time' +require 'json' +require 'dry-struct' +require 'dry-types' +require 'rspec/core' + +module Types + include Dry::Types() + + EventId = Types::Coercible::String.default { SecureRandom.uuid } + Metadata = Types.Constructor(RubyEventStore::Metadata) { |value| RubyEventStore::Metadata.new(value.to_h) }.default { RubyEventStore::Metadata.new } +end + + +class Event < Dry::Struct + transform_keys(&:to_sym) + + attribute :event_id, Types::EventId + attribute :metadata, Types::Metadata + alias :message_id :event_id + + def self.new(data: {}, metadata: {}, **args) + super(args.merge(data).merge(metadata: metadata)) + end + + def self.inherited(klass) + super + klass.define_singleton_method(:event_type) do |value| + klass.define_method(:event_type) do + value + end + end + end + + def timestamp + metadata[:timestamp] && Time.parse(metadata[:timestamp]) + end + + def data + to_h.reject{|k,_| [:event_id, :metadata].include?(k) } + end + + def event_type + self.class.name + end + + def ==(other_event) + other_event.instance_of?(self.class) && + other_event.event_id.eql?(event_id) && + other_event.data.eql?(data) + end + + alias_method :eql?, :== +end diff --git a/contrib/multiple_databases_repository_sample_app/lib/types.rb b/contrib/multiple_databases_repository_sample_app/lib/types.rb new file mode 100644 index 0000000000..955f08da96 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/lib/types.rb @@ -0,0 +1,8 @@ +require 'dry-types' + +module Types + ID = Types::Strict::Integer + UUID = Types::Strict::String.constrained(format: /\A[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\z/i) + TransactionId = Types::Strict::String.constrained(format: /\A[0-9a-fA-F]{32}\z/i) + OrderNumber = Types::Strict::String.constrained(format: /\A\d{4}\/\d{2}\/\d+\z/i) +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders.rb new file mode 100644 index 0000000000..dc6f64cee8 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require_relative 'orders/application_record' +require_relative '../../lib/event' +require_relative '../../lib/types' +require_relative '../../lib/command' +require_relative '../../lib/command_handler' + +require_dependency 'orders/add_item_to_basket' +require_dependency 'orders/remove_item_from_basket' +require_dependency 'orders/submit_order' + +require_dependency 'orders/item_added_to_basket' +require_dependency 'orders/item_removed_from_basket' +require_dependency 'orders/order_submitted' + +require_dependency 'orders/on_add_item_to_basket' +require_dependency 'orders/on_remove_item_from_basket' +require_dependency 'orders/on_submit_order' + +require_dependency 'orders/order' +require_dependency 'orders/order_line' +require_dependency 'orders/number_generator' +require_dependency 'orders/fake_number_generator' + +module Orders + def self.setup(config) + @@command_bus = config.command_bus + @@public_event_store = RailsEventStore::Client.new( + repository: config.event_repository, + mapper: RubyEventStore::Mappers::Default.new( + events_class_remapping: events_class_remapping + ), + dispatcher: RubyEventStore::ComposedDispatcher.new( + RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)), + RubyEventStore::Dispatcher.new), + ) + @@module_event_store = RailsEventStore::Client.new( + repository: RailsEventStoreActiveRecord::EventRepository.new( + model_factory: RailsEventStoreActiveRecord::WithAbstractBaseClass.new(Orders::ApplicationRecord), serializer: JSON), + dispatcher: RubyEventStore::ComposedDispatcher.new( + RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)), + RubyEventStore::Dispatcher.new), + ) + + # Subscribe public event handlers below + public_event_store.tap do |store| + end + + # Subscribe private event handlers below + event_store.tap do |store| + store.subscribe(PrepareOrderProcess.new(store, command_bus), to: [ + ItemAddedToBasket, + ItemRemovedFromBasket, + OrderSubmitted, + ]) + end + + # Register commands handled by this module below + command_bus.tap do |bus| + bus.register(Orders::SubmitOrder, Orders::OnSubmitOrder.new(event_store, number_generator_factory: config.number_generator_factory)) + bus.register(Orders::AddItemToBasket, Orders::OnAddItemToBasket.new(event_store)) + bus.register(Orders::RemoveItemFromBasket, Orders::OnRemoveItemFromBasket.new(event_store)) + bus.register(Orders::PlaceOrder, ->(cmd) { public_event_store.publish(OrderPlaced.new(**cmd)) }) + end + end + + def self.events_class_remapping + { + 'new-order' => 'Orders::OrderPlaced', + } + end + + def self.command_bus + @@command_bus + end + + def self.public_event_store + @@public_event_store + end + + def self.event_store + @@module_event_store + end + + def self.setup? + command_bus && event_store && public_event_store + end + + class OrderPlaced < Event + event_type 'new-order' + attribute :order_id, Types::UUID + attribute :customer_id, Types::ID + attribute :delivery_address_id, Types::ID + attribute :payment_method_id, Types::ID + attribute :amount, Types::Coercible::Decimal + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/.keep b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/add_item_to_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/add_item_to_basket.rb new file mode 100644 index 0000000000..f0e4ab35de --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/add_item_to_basket.rb @@ -0,0 +1,6 @@ +module Orders + class AddItemToBasket < Command + attribute :order_id, Types::UUID + attribute :product_id, Types::Coercible::Integer + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/application_record.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/application_record.rb new file mode 100644 index 0000000000..28321d9aa2 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/application_record.rb @@ -0,0 +1,8 @@ +require 'active_record' + +module Orders + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + connects_to database: { writing: :orders, reading: :orders } + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/fake_number_generator.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/fake_number_generator.rb new file mode 100644 index 0000000000..eb10681ddf --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/fake_number_generator.rb @@ -0,0 +1,7 @@ +module Orders + class FakeNumberGenerator + def call + "2019/01/60" + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_added_to_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_added_to_basket.rb new file mode 100644 index 0000000000..9eed977c17 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_added_to_basket.rb @@ -0,0 +1,6 @@ +module Orders + class ItemAddedToBasket < Event + attribute :order_id, Types::UUID + attribute :product_id, Types::ID + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_removed_from_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_removed_from_basket.rb new file mode 100644 index 0000000000..5005735db3 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/item_removed_from_basket.rb @@ -0,0 +1,6 @@ +module Orders + class ItemRemovedFromBasket < Event + attribute :order_id, Types::UUID + attribute :product_id, Types::ID + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/number_generator.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/number_generator.rb new file mode 100644 index 0000000000..ed60d6cf92 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/number_generator.rb @@ -0,0 +1,7 @@ +module Orders + class NumberGenerator + def call + Time.current.strftime("%Y/%m/#{SecureRandom.random_number(100)}") + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_add_item_to_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_add_item_to_basket.rb new file mode 100644 index 0000000000..7f9ae9b052 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_add_item_to_basket.rb @@ -0,0 +1,11 @@ +module Orders + class OnAddItemToBasket + include CommandHandler + + def call(command) + with_aggregate(Order.new(command.order_id), command.order_id) do |order| + order.add_item(command.product_id) + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_remove_item_from_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_remove_item_from_basket.rb new file mode 100644 index 0000000000..b287f29b1d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_remove_item_from_basket.rb @@ -0,0 +1,11 @@ +module Orders + class OnRemoveItemFromBasket + include CommandHandler + + def call(command) + with_aggregate(Order.new(command.order_id), command.order_id) do |order| + order.remove_item(command.product_id) + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_submit_order.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_submit_order.rb new file mode 100644 index 0000000000..1b872aa019 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/on_submit_order.rb @@ -0,0 +1,29 @@ +module Orders + class OnSubmitOrder + include CommandHandler + + def initialize(event_store, number_generator_factory:) + @repository = AggregateRoot::InstrumentedRepository.new( + AggregateRoot::Repository.new(event_store), + ActiveSupport::Notifications + ) + @number_generator = number_generator_factory.call + end + + def call(command) + with_aggregate(Order.new(command.order_id), command.order_id) do |order| + order_number = number_generator.call + order.submit( + order_number, + command.customer_id, + command.delivery_address_id, + command.payment_method_id + ) + end + end + + private + + attr_accessor :number_generator + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order.rb new file mode 100644 index 0000000000..7389784ba3 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order.rb @@ -0,0 +1,77 @@ +require 'aggregate_root' + +module Orders + class Order + include AggregateRoot + + AlreadySubmitted = Class.new(StandardError) + NotSubmitted = Class.new(StandardError) + MissingCustomer = Class.new(StandardError) + + def initialize(id) + @id = id + @state = :draft + @order_lines = [] + end + + def submit(order_number, customer_id, delivery_address_id, payment_method_id) + raise AlreadySubmitted if @state == :submitted + raise MissingCustomer unless customer_id + apply OrderSubmitted.new(data: { + order_id: @id, + order_number: order_number, + customer_id: customer_id, + delivery_address_id: delivery_address_id, + payment_method_id: payment_method_id, + }) + end + + def add_item(product_id) + raise AlreadySubmitted unless @state == :draft + apply ItemAddedToBasket.new(data: {order_id: @id, product_id: product_id}) + end + + def remove_item(product_id) + raise AlreadySubmitted unless @state == :draft + apply ItemRemovedFromBasket.new(data: {order_id: @id, product_id: product_id}) + end + + on OrderSubmitted do |event| + @customer_id = event.data[:customer_id] + @number = event.data[:order_number] + @state = :submitted + end + + on ItemAddedToBasket do |event| + product_id = event.data[:product_id] + order_line = find_order_line(product_id) + unless order_line + order_line = create_order_line(product_id) + @order_lines << order_line + end + order_line.increase_quantity + end + + on ItemRemovedFromBasket do |event| + product_id = event.data[:product_id] + order_line = find_order_line(product_id) + return unless order_line + order_line.decrease_quantity + remove_order_line(order_line) if order_line.empty? + end + + private + + def find_order_line(product_id) + @order_lines.select { |line| line.product_id == product_id }.first + end + + def create_order_line(product_id) + OrderLine.new(product_id) + end + + def remove_order_line(order_line) + @order_lines.delete(order_line) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_line.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_line.rb new file mode 100644 index 0000000000..b565444568 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_line.rb @@ -0,0 +1,31 @@ +module Orders + class OrderLine + include Comparable + attr_reader :product_id + + def initialize(product_id) + @product_id = product_id + @quantity = 0 + end + + def increase_quantity + @quantity += 1 + end + + def decrease_quantity + @quantity -= 1 + end + + def empty? + @quantity == 0 + end + + def <=>(other) + self.product_id <=> other.product_id + end + + private + + attr_accessor :quantity + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_submitted.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_submitted.rb new file mode 100644 index 0000000000..4d29900b80 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/order_submitted.rb @@ -0,0 +1,9 @@ +module Orders + class OrderSubmitted < Event + attribute :order_id, Types::UUID + attribute :order_number, Types::OrderNumber + attribute :customer_id, Types::ID + attribute :delivery_address_id, Types::ID + attribute :payment_method_id, Types::ID + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/place_order.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/place_order.rb new file mode 100644 index 0000000000..45f92d0571 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/place_order.rb @@ -0,0 +1,9 @@ +module Orders + class PlaceOrder < Command + attribute :order_id, Types::UUID + attribute :customer_id, Types::Coercible::Integer + attribute :delivery_address_id, Types::Coercible::Integer + attribute :payment_method_id, Types::Coercible::Integer + attribute :amount, Types::Decimal + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/prepare_order_process.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/prepare_order_process.rb new file mode 100644 index 0000000000..7137f27286 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/prepare_order_process.rb @@ -0,0 +1,64 @@ +module Orders + class PrepareOrderProcess + def initialize(event_store, bus) + @event_store = event_store + @bus = bus + end + + def call(event) + with_linked(event) do |state| + bus.call(state.command) if state.done? + end + end + + private + attr_reader :event_store, :bus + + class State + def initialize(events) + @data = { amount: 0.to_d } + events.each{|e| apply(e)} + end + + def done? + data[:amount] > 0 && data[:order_id] + end + + def command + Orders::PlaceOrder.new(**data) + end + + private + attr_reader :data + + def apply(event) + case event + when Orders::ItemAddedToBasket + data[:amount] += 10.to_d + when Orders::ItemRemovedFromBasket + data[:amount] -= 10.to_d + when Orders::OrderSubmitted + data.merge!(event.data.slice( + :order_id, + :order_number, + :customer_id, + :delivery_address_id, + :payment_method_id + )) + else + raise ArgumentError.new("Not suported domain event") + end + end + end + + def with_linked(event) + stream = "PreparationProcess$#{event.order_id}" + event_store.link( + event.event_id, + stream_name: stream + ) + yield State.new(event_store.read.stream(stream)) + rescue RubyEventStore::EventDuplicatedInStream + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/remove_item_from_basket.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/remove_item_from_basket.rb new file mode 100644 index 0000000000..2c8b065bb1 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/remove_item_from_basket.rb @@ -0,0 +1,6 @@ +module Orders + class RemoveItemFromBasket < Command + attribute :order_id, Types::UUID + attribute :product_id, Types::Coercible::Integer + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/lib/orders/submit_order.rb b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/submit_order.rb new file mode 100644 index 0000000000..62ea8645a1 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/lib/orders/submit_order.rb @@ -0,0 +1,8 @@ +module Orders + class SubmitOrder < Command + attribute :order_id, Types::UUID + attribute :customer_id, Types::Coercible::Integer + attribute :delivery_address_id, Types::Coercible::Integer + attribute :payment_method_id, Types::Coercible::Integer + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/add_item_to_basket_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/add_item_to_basket_spec.rb new file mode 100644 index 0000000000..f1486db5ea --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/spec/add_item_to_basket_spec.rb @@ -0,0 +1,38 @@ +require_relative 'spec_helper' + +module Orders + RSpec.describe AddItemToBasket do + let(:aggregate_id) { SecureRandom.uuid } + let(:stream) { "Orders::Order$#{aggregate_id}" } + let(:customer_id) { 997 } + let(:address_id) { 998 } + let(:payment_method_id) { 999 } + let(:product_id) { 123 } + let(:order_number) { "2019/01/60" } + + it 'item is added to draft order' do + Orders.act(AddItemToBasket.new(order_id: aggregate_id, product_id: product_id)) + expect(Orders.event_store).to have_published( + an_event(ItemAddedToBasket) + .with_data(order_id: aggregate_id, product_id: product_id) + ) + end + + it 'no add allowed to submitted order' do + Orders.arrange(stream, [ + ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id}), + OrderSubmitted.new(data: { + order_id: aggregate_id, + order_number: order_number, + customer_id: customer_id, + delivery_address_id: address_id, + payment_method_id: payment_method_id, + }) + ]) + + expect do + Orders.act(AddItemToBasket.new(order_id: aggregate_id, product_id: product_id)) + end.to raise_error(Order::AlreadySubmitted) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/order_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/order_spec.rb new file mode 100644 index 0000000000..cdbec04180 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/spec/order_spec.rb @@ -0,0 +1,6 @@ +require_relative 'spec_helper' + +module Orders + RSpec.describe Order do + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/prepare_order_process_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/prepare_order_process_spec.rb new file mode 100644 index 0000000000..2e6f04ded4 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/spec/prepare_order_process_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +module Orders + RSpec.describe PrepareOrderProcess do + let(:order_id) { SecureRandom.uuid } + let(:stream) { "Orders::Order$#{order_id}" } + let(:customer_id) { 997 } + let(:address_id) { 998 } + let(:payment_method_id) { 999 } + let(:product_id) { 123 } + let(:order_number) { "2019/01/60" } + + it 'works' do + Orders.act(AddItemToBasket.new(order_id: order_id, product_id: product_id)) + Orders.act(AddItemToBasket.new(order_id: order_id, product_id: product_id)) + Orders.act(SubmitOrder.new( + order_id: order_id, + customer_id: customer_id, + delivery_address_id: address_id, + payment_method_id: payment_method_id, + )) + + expect(Orders.public_event_store).to have_published( + an_event(Orders::OrderPlaced).with_data( + order_id: order_id, + customer_id: customer_id, + delivery_address_id: address_id, + payment_method_id: payment_method_id, + amount: 20.to_d, + ) + ) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/remove_item_from_basket_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/remove_item_from_basket_spec.rb new file mode 100644 index 0000000000..d634f20dc1 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/spec/remove_item_from_basket_spec.rb @@ -0,0 +1,39 @@ +require_relative 'spec_helper' + +module Orders + RSpec.describe RemoveItemFromBasket do + let(:aggregate_id) { SecureRandom.uuid } + let(:stream) { "Orders::Order$#{aggregate_id}" } + let(:customer_id) { 997 } + let(:address_id) { 998 } + let(:payment_method_id) { 999 } + let(:product_id) { 123 } + let(:order_number) { "2019/01/60" } + + it 'item is removed from draft order' do + Orders.arrange(stream, [ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id})]) + Orders.act(RemoveItemFromBasket.new(order_id: aggregate_id, product_id: product_id)) + expect(Orders.event_store).to have_published( + an_event(ItemRemovedFromBasket) + .with_data(order_id: aggregate_id, product_id: product_id) + ) + end + + it 'no remove allowed to created order' do + Orders.arrange(stream, [ + ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id}), + OrderSubmitted.new(data: { + order_id: aggregate_id, + order_number: order_number, + customer_id: customer_id, + delivery_address_id: address_id, + payment_method_id: payment_method_id, + }) + ]) + + expect do + Orders.act(RemoveItemFromBasket.new(order_id: aggregate_id, product_id: product_id)) + end.to raise_error(Order::AlreadySubmitted) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/spec_helper.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/spec_helper.rb new file mode 100644 index 0000000000..6f6b503cfc --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/spec/spec_helper.rb @@ -0,0 +1,22 @@ +ENV['RAILS_ENV'] = 'test' + +require_relative '../lib/orders' + +module Orders + def self.arrange(stream, events, event_store: Orders.event_store) + event_store.append(events, stream_name: stream) + end + + def self.act(command, bus: Orders.command_bus) + bus.call(command) + end +end + +unless Orders.setup? + Configuration = Struct.new(:event_repository, :command_bus, :number_generator_factory) + Orders.setup(Configuration.new( + RubyEventStore::InMemoryRepository.new, + Arkency::CommandBus.new, + ->{ Orders::FakeNumberGenerator.new }, + )) +end diff --git a/contrib/multiple_databases_repository_sample_app/orders/spec/submit_order_spec.rb b/contrib/multiple_databases_repository_sample_app/orders/spec/submit_order_spec.rb new file mode 100644 index 0000000000..9c95b66086 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/orders/spec/submit_order_spec.rb @@ -0,0 +1,68 @@ +require_relative 'spec_helper' + +module Orders + RSpec.describe SubmitOrder do + let(:aggregate_id) { SecureRandom.uuid } + let(:stream) { "Orders::Order$#{aggregate_id}" } + let(:customer_id) { 997 } + let(:address_id) { 998 } + let(:payment_method_id) { 999 } + let(:product_id) { 123 } + let(:order_number) { "2019/01/60" } + + it 'order is submitted' do + Orders.arrange(stream, [ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id})]) + Orders.act(SubmitOrder.new( + order_id: aggregate_id, + customer_id: customer_id, + delivery_address_id: address_id, + payment_method_id: payment_method_id, + )) + + expect(Orders.event_store).to have_published( + an_event(OrderSubmitted) + .with_data( + order_id: aggregate_id, + order_number: order_number, + customer_id: customer_id, + delivery_address_id: address_id, + payment_method_id: payment_method_id, + ) + ) + end + + it 'could not create order where customer is not given' do + expect do + Orders.act(SubmitOrder.new( + order_id: aggregate_id, + customer_id: nil, + delivery_addredd_id: nil, + payment_method_id: nil + )) + end.to raise_error(Command::Invalid) + end + + it 'already created order could not be created again' do + another_customer_id = 998 + Orders.arrange(stream, [ + ItemAddedToBasket.new(data: {order_id: aggregate_id, product_id: product_id}), + OrderSubmitted.new(data: { + order_id: aggregate_id, + order_number: order_number, + customer_id: customer_id, + delivery_address_id: address_id, + payment_method_id: payment_method_id, + }) + ]) + + expect do + Orders.act(SubmitOrder.new( + order_id: aggregate_id, + customer_id: another_customer_id, + delivery_address_id: address_id, + payment_method_id: payment_method_id, + )) + end.to raise_error(Order::AlreadySubmitted) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments.rb new file mode 100644 index 0000000000..2d6b430a15 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require_relative 'payments/application_record' +require_relative '../../lib/event' +require_relative '../../lib/types' +require_relative '../../lib/command' +require_relative '../../lib/command_handler' + +require_dependency 'payments/payment_authorized' +require_dependency 'payments/payment_captured' +require_dependency 'payments/payment_released' +require_dependency 'payments/payment_expired' + +require_dependency 'payments/authorize_payment' +require_dependency 'payments/capture_payment' +require_dependency 'payments/release_payment' +require_dependency 'payments/set_payment_as_expired' +require_dependency 'payments/complete_payment' + +require_dependency 'payments/on_authorize_payment' +require_dependency 'payments/on_capture_payment' +require_dependency 'payments/on_release_payment' +require_dependency 'payments/on_expire_payment' +require_dependency 'payments/on_complete_payment' + +require_dependency 'payments/payment' +require_dependency 'payments/payment_process' + +module Payments + def self.setup(config) + @@command_bus = config.command_bus + @@public_event_store = RailsEventStore::Client.new( + repository: config.event_repository, + mapper: RubyEventStore::Mappers::Default.new( + events_class_remapping: events_class_remapping + ), + dispatcher: RubyEventStore::ComposedDispatcher.new( + RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)), + RubyEventStore::Dispatcher.new), + ) + @@module_event_store = RailsEventStore::Client.new( + repository: RailsEventStoreActiveRecord::EventRepository.new( + model_factory: RailsEventStoreActiveRecord::WithAbstractBaseClass.new(Payments::ApplicationRecord), serializer: JSON), + dispatcher: RubyEventStore::ComposedDispatcher.new( + RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)), + RubyEventStore::Dispatcher.new), + ) + + # Subscribe public event handlers below + public_event_store.tap do |store| + store.subscribe(InitiatePayment.new(command_bus), to: ['new-order']) + end + + # Subscribe private event handlers below + event_store.tap do |store| + store.subscribe(PaymentProcess.new(store, command_bus), to: [ + PaymentAuthorized, + PaymentCaptured, + PaymentExpired, + ]) + end + + # Register commands handled by this module below + command_bus.tap do |bus| + bus.register(Payments::AuthorizePayment, Payments::OnAuthorizePayment.new(event_store)) + bus.register(Payments::SetPaymentAsExpired, Payments::OnExpirePayment.new(event_store)) + bus.register(Payments::CapturePayment, Payments::OnCapturePayment.new(event_store)) + bus.register(Payments::ReleasePayment, Payments::OnReleasePayment.new(event_store)) + bus.register(Payments::CompletePayment, Payments::OnCompletePayment.new(public_event_store)) + end + end + + def self.events_class_remapping + { + 'new-order' => 'Payments::PaymentInitiated', + 'payment-completed' => 'Payments::PaymentCompleted', + } + end + + def self.command_bus + @@command_bus rescue nil + end + + def self.public_event_store + @@public_event_store rescue nil + end + + def self.event_store + @@module_event_store rescue nil + end + + def self.setup? + command_bus && event_store && public_event_store + end + + class PaymentInitiated < Event + event_type 'new-order' + attribute :order_id, Types::UUID + attribute :amount, Types::Coercible::Decimal + end + + class PaymentCompleted < Event + event_type 'payment-completed' + attribute :transaction_id, Types::Coercible::String + attribute :order_id, Types::UUID + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/.keep b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/application_record.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/application_record.rb new file mode 100644 index 0000000000..fa51873d0e --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/application_record.rb @@ -0,0 +1,6 @@ +module Payments + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + connects_to database: { writing: :payments, reading: :payments } + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/authorize_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/authorize_payment.rb new file mode 100644 index 0000000000..c3a8293113 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/authorize_payment.rb @@ -0,0 +1,7 @@ +module Payments + class AuthorizePayment < Command + attribute :transaction_id, Types::Coercible::String + attribute :order_id, Types::UUID + attribute :amount, Types::Decimal + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/capture_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/capture_payment.rb new file mode 100644 index 0000000000..e210af707c --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/capture_payment.rb @@ -0,0 +1,5 @@ +module Payments + class CapturePayment < Command + attribute :transaction_id, Types::Coercible::String + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/complete_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/complete_payment.rb new file mode 100644 index 0000000000..eba2e0f109 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/complete_payment.rb @@ -0,0 +1,6 @@ +module Payments + class CompletePayment < Command + attribute :transaction_id, Types::Coercible::String + attribute :order_id, Types::UUID + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/initiate_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/initiate_payment.rb new file mode 100644 index 0000000000..8a94af7f66 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/initiate_payment.rb @@ -0,0 +1,13 @@ +module Payments + class InitiatePayment + def initialize(bus) + @bus = bus + end + + def call(event) + @bus.call(AuthorizePayment.new(**event.data.merge( + transaction_id: SecureRandom.hex(16), + ))) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_authorize_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_authorize_payment.rb new file mode 100644 index 0000000000..d609c0a26f --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_authorize_payment.rb @@ -0,0 +1,11 @@ +module Payments + class OnAuthorizePayment + include CommandHandler + + def call(command) + with_aggregate(Payment.new, command.transaction_id) do |payment| + payment.authorize(command.transaction_id, command.order_id, command.amount) + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_capture_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_capture_payment.rb new file mode 100644 index 0000000000..fd7b5f3307 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_capture_payment.rb @@ -0,0 +1,11 @@ +module Payments + class OnCapturePayment + include CommandHandler + + def call(command) + with_aggregate(Payment.new, command.transaction_id) do |payment| + payment.capture + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_complete_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_complete_payment.rb new file mode 100644 index 0000000000..dd4df82ee8 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_complete_payment.rb @@ -0,0 +1,14 @@ +module Payments + class OnCompletePayment + def initialize(event_store) + @event_store = event_store + end + + def call(command) + event_store.publish(Payments::PaymentCompleted.new(**command)) + end + + private + attr_reader :event_store + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_expire_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_expire_payment.rb new file mode 100644 index 0000000000..2a6ac21f1c --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_expire_payment.rb @@ -0,0 +1,11 @@ +module Payments + class OnExpirePayment + include CommandHandler + + def call(command) + with_aggregate(Payment.new, command.transaction_id) do |payment| + payment.expire + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_release_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_release_payment.rb new file mode 100644 index 0000000000..022acbfc7c --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/on_release_payment.rb @@ -0,0 +1,11 @@ +module Payments + class OnReleasePayment + include CommandHandler + + def call(command) + with_aggregate(Payment.new, command.transaction_id) do |payment| + payment.release + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment.rb new file mode 100644 index 0000000000..403d9ff0e2 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment.rb @@ -0,0 +1,91 @@ +module Payments + class Payment + include AggregateRoot + + AlreadyAuthorized = Class.new(StandardError) + NotAuthorized = Class.new(StandardError) + AlreadyCaptured = Class.new(StandardError) + AlreadyReleased = Class.new(StandardError) + + def initialize + @state = nil + end + + def authorize(transaction_id, order_id, amount) + raise AlreadyAuthorized unless initiated? + apply(PaymentAuthorized.new(data: { + transaction_id: transaction_id, + order_id: order_id, + amount: amount + })) + end + + def expire + raise NotAuthorized unless authorized? + raise AlreadyCaptured if captured? + raise AlreadyReleased if released? + apply(PaymentExpired.new(data: { + transaction_id: @transaction_id, + })) + end + + def capture + raise NotAuthorized unless authorized? + raise AlreadyCaptured if captured? + apply(PaymentCaptured.new(data: { + transaction_id: @transaction_id, + })) + end + + def release + raise NotAuthorized unless authorized? + raise AlreadyCaptured if captured? + raise AlreadyReleased if released? + apply(PaymentReleased.new(data: { + transaction_id: @transaction_id, + })) + end + + private + + on PaymentAuthorized do |event| + @state = :authorized + @transaction_id = event.transaction_id + @order_id = event.order_id + @amount = event.amount + end + + on PaymentExpired do |event| + @state = :expired + end + + on PaymentCaptured do |event| + @state = :captured + end + + on PaymentReleased do |event| + @state = :released + end + + def initiated? + @state == nil + end + + def authorized? + @state != nil + end + + def expired? + @state == :expired + end + + def captured? + @state == :captured + end + + def released? + @state == :released + end + end +end + diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_authorized.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_authorized.rb new file mode 100644 index 0000000000..663740160b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_authorized.rb @@ -0,0 +1,7 @@ +module Payments + class PaymentAuthorized < Event + attribute :order_id, Types::UUID + attribute :transaction_id, Types::TransactionId + attribute :amount, Types::Coercible::Decimal + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_captured.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_captured.rb new file mode 100644 index 0000000000..5834f3d901 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_captured.rb @@ -0,0 +1,5 @@ +module Payments + class PaymentCaptured < Event + attribute :transaction_id, Types::TransactionId + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_expired.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_expired.rb new file mode 100644 index 0000000000..8b96e1d4c7 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_expired.rb @@ -0,0 +1,5 @@ +module Payments + class PaymentExpired < Event + attribute :transaction_id, Types::TransactionId + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_process.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_process.rb new file mode 100644 index 0000000000..cc0b66a22b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_process.rb @@ -0,0 +1,75 @@ +module Payments + class PaymentProcess + def initialize(event_store, bus) + @event_store = event_store + @bus = bus + end + + def call(event) + with_linked(event) do |state| + bus.call(state.command) if state.done? + end + end + + private + attr_reader :event_store, :bus + + class State + def initialize(events) + @data = {} + @state = nil + @action = nil + events.each{|e| apply(e)} + end + + def done? + !!action + end + + def command + case action + when :complete + Payments::CompletePayment.new(**data) + when :release + Payments::ReleasePayment.new(**data.slice(:transaction_id)) + else + raise ArgumentError.new("Unknown action") + end + end + + private + attr_reader :data, :state, :action + + def apply(event) + case event + when Payments::PaymentAuthorized + data[:transaction_id] = event.transaction_id + data[:order_id] = event.order_id + @state = :authorized + when Payments::PaymentCaptured + @action = :complete if authorized? + @state = :captured + when Payments::PaymentExpired + @action = :release if authorized? + @state = :expired + else + raise ArgumentError.new("Not suported domain event") + end + end + + def authorized? + state == :authorized + end + end + + def with_linked(event) + stream = "PaymentProcess$#{event.transaction_id}" + event_store.link( + event.event_id, + stream_name: stream + ) + yield State.new(event_store.read.stream(stream)) + rescue RubyEventStore::EventDuplicatedInStream + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_released.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_released.rb new file mode 100644 index 0000000000..0f85f9b77e --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/payment_released.rb @@ -0,0 +1,5 @@ +module Payments + class PaymentReleased < Event + attribute :transaction_id, Types::TransactionId + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/release_payment.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/release_payment.rb new file mode 100644 index 0000000000..6e7ab47ed6 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/release_payment.rb @@ -0,0 +1,5 @@ +module Payments + class ReleasePayment < Command + attribute :transaction_id, Types::Coercible::String + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/lib/payments/set_payment_as_expired.rb b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/set_payment_as_expired.rb new file mode 100644 index 0000000000..9fa24c75a4 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/lib/payments/set_payment_as_expired.rb @@ -0,0 +1,5 @@ +module Payments + class SetPaymentAsExpired < Command + attribute :transaction_id, Types::Coercible::String + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/initiate_payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/initiate_payment_spec.rb new file mode 100644 index 0000000000..08e532997d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/spec/initiate_payment_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +module Payments + RSpec.describe InitiatePayment do + let(:order_id) { SecureRandom.uuid } + + it 'works' do + Payments.public_event_store.publish( + PaymentInitiated.new( + order_id: order_id, + amount: 20.to_d, + ) + ) + + expect(Payments.event_store).to have_published( + an_event(PaymentAuthorized).with_data( + transaction_id: kind_of(String), + order_id: order_id, + amount: 20.to_d, + ) + ) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/on_authorize_payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/on_authorize_payment_spec.rb new file mode 100644 index 0000000000..e95775c24c --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/spec/on_authorize_payment_spec.rb @@ -0,0 +1,22 @@ +require_relative 'spec_helper' + +module Payments + RSpec.describe OnAuthorizePayment do + it 'authorize payment' do + transaction_id = SecureRandom.hex(16) + order_id = SecureRandom.uuid + + Payments.act(AuthorizePayment.new(transaction_id: transaction_id, order_id: order_id, amount: 20.to_d)) + + expect(Payments.event_store).to have_published( + an_event(PaymentAuthorized) + .with_data( + transaction_id: transaction_id, + order_id: order_id, + amount: 20.to_d + ) + ) + end + end +end + diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/on_capture_payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/on_capture_payment_spec.rb new file mode 100644 index 0000000000..455ec0fd50 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/spec/on_capture_payment_spec.rb @@ -0,0 +1,19 @@ +require_relative 'spec_helper' + +module Payments + RSpec.describe OnCapturePayment do + it 'capture payment' do + transaction_id = SecureRandom.hex(16) + order_id = SecureRandom.uuid + stream = "Payments::Payment$#{transaction_id}" + + Payments.arrange(stream, [PaymentAuthorized.new(data: {transaction_id: transaction_id, order_id: order_id, amount: 20.to_d})]) + Payments.act(CapturePayment.new(transaction_id: transaction_id)) + + expect(Payments.event_store).to have_published( + an_event(PaymentCaptured) + .with_data(transaction_id: transaction_id) + ) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/on_release_payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/on_release_payment_spec.rb new file mode 100644 index 0000000000..0584d79939 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/spec/on_release_payment_spec.rb @@ -0,0 +1,19 @@ +require_relative 'spec_helper' + +module Payments + RSpec.describe OnReleasePayment do + it 'release payment' do + transaction_id = SecureRandom.hex(16) + order_id = SecureRandom.uuid + stream = "Payments::Payment$#{transaction_id}" + + Payments.arrange(stream, [PaymentAuthorized.new(data: {transaction_id: transaction_id, order_id: order_id, amount: 20.to_d})]) + Payments.act(ReleasePayment.new(transaction_id: transaction_id)) + + expect(Payments.event_store).to have_published( + an_event(PaymentReleased) + .with_data(transaction_id: transaction_id) + ) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/payment_process_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/payment_process_spec.rb new file mode 100644 index 0000000000..2c90a69e5d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/spec/payment_process_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +module Payments + RSpec.describe PaymentProcess do + let(:transaction_id) { SecureRandom.hex(16) } + let(:order_id) { SecureRandom.uuid } + + it 'complete captured payment' do + Payments.act(AuthorizePayment.new(transaction_id: transaction_id, order_id: order_id, amount: 20.to_d)) + Payments.act(CapturePayment.new(transaction_id: transaction_id, order_id: order_id)) + + expect(Payments.public_event_store).to have_published( + an_event(Payments::PaymentCompleted).with_data( + transaction_id: transaction_id, + order_id: order_id, + ) + ) + end + + it 'release expired payment' do + Payments.act(AuthorizePayment.new(transaction_id: transaction_id, order_id: order_id, amount: 20.to_d)) + Payments.act(SetPaymentAsExpired.new(transaction_id: transaction_id)) + + expect(Payments.public_event_store).not_to have_published( + an_event(Payments::PaymentCompleted) + ) + + expect(Payments.event_store).to have_published( + an_event(Payments::PaymentReleased).with_data( + transaction_id: transaction_id, + ) + ) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/payment_spec.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/payment_spec.rb new file mode 100644 index 0000000000..f0126f3f4d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/spec/payment_spec.rb @@ -0,0 +1,112 @@ +require_relative 'spec_helper' + +module Payments + RSpec.describe Payment do + it 'authorize' do + payment = Payment.new + expect do + payment.authorize(transaction_id, order_id, 20.to_d) + end.to apply( + an_event(PaymentAuthorized) + .with_data( + transaction_id: transaction_id, + order_id: order_id, + amount: 20.to_d, + ) + ).in(payment) + end + + it 'should not allow for double authorization' do + expect do + authorized_payment.authorize(transaction_id, order_id, 20.to_d) + end.to raise_error(Payment::AlreadyAuthorized) + end + + it 'should capture authorized payment' do + payment = authorized_payment + + expect do + payment.capture + end.to apply( + an_event(PaymentCaptured) + .with_data(transaction_id: transaction_id) + ).in(payment) + end + + it 'must not capture not authorized payment' do + expect do + Payment.new.capture + end.to raise_error(Payment::NotAuthorized) + end + + it 'should not allow for double capture' do + expect do + captured_payment.capture + end.to raise_error(Payment::AlreadyCaptured) + end + + it 'authorization could be released' do + payment = authorized_payment + + expect do + payment.release + end.to apply( + an_event(PaymentReleased) + .with_data(transaction_id: transaction_id) + ).in(payment) + end + + it 'must not release not captured payment' do + expect do + captured_payment.release + end.to raise_error(Payment::AlreadyCaptured) + end + + it 'must not release not authorized payment' do + expect do + Payment.new.release + end.to raise_error(Payment::NotAuthorized) + end + + it 'should not allow for double release' do + expect do + released_payment.release + end.to raise_error(Payment::AlreadyReleased) + end + + let(:transaction_id) { SecureRandom.hex(16) } + let(:order_id) { SecureRandom.uuid } + + def authorized_payment + Payment.new.tap do |payment| + payment.apply( + PaymentAuthorized.new(data: { + transaction_id: transaction_id, + order_id: order_id, + amount: 20.to_d, + }) + ) + end + end + + def captured_payment + authorized_payment.tap do |payment| + payment.apply( + PaymentCaptured.new(data: { + transaction_id: transaction_id, + }) + ) + end + end + + def released_payment + captured_payment.tap do |payment| + payment.apply( + PaymentReleased.new(data: { + transaction_id: transaction_id, + }) + ) + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/payments/spec/spec_helper.rb b/contrib/multiple_databases_repository_sample_app/payments/spec/spec_helper.rb new file mode 100644 index 0000000000..5fb442c0ff --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/payments/spec/spec_helper.rb @@ -0,0 +1,37 @@ +require 'active_record' + +ENV['RAILS_ENV'] ||= 'test' +if ActiveRecord::Base.configurations.empty? + configuration = YAML::load(File.open('../config/database.yml')) + ActiveRecord::Base.configurations = configuration + ActiveRecord::Base.establish_connection(configuration[ENV['RAILS_ENV']]["payments"]) + begin + ActiveRecord::Migration.maintain_test_schema! + rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 + end + require 'arkency/command_bus' + require 'rails_event_store' + require 'aggregate_root' +end + +require_relative '../lib/payments' + +module Payments + def self.arrange(stream, events, event_store: Payments.event_store) + event_store.append(events, stream_name: stream) + end + + def self.act(command, bus: Payments.command_bus) + bus.call(command) + end +end + +unless Payments.setup? + Configuration = Struct.new(:event_repository, :command_bus) + Payments.setup(Configuration.new( + RubyEventStore::InMemoryRepository.new, + Arkency::CommandBus.new, + )) +end diff --git a/contrib/multiple_databases_repository_sample_app/public/404.html b/contrib/multiple_databases_repository_sample_app/public/404.html new file mode 100644 index 0000000000..2be3af26fc --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

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

+
+

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

+
+ + diff --git a/contrib/multiple_databases_repository_sample_app/public/422.html b/contrib/multiple_databases_repository_sample_app/public/422.html new file mode 100644 index 0000000000..c08eac0d1d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

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

+
+

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

+
+ + diff --git a/contrib/multiple_databases_repository_sample_app/public/500.html b/contrib/multiple_databases_repository_sample_app/public/500.html new file mode 100644 index 0000000000..78a030af22 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

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

+
+ + diff --git a/contrib/multiple_databases_repository_sample_app/public/apple-touch-icon-precomposed.png b/contrib/multiple_databases_repository_sample_app/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/multiple_databases_repository_sample_app/public/apple-touch-icon.png b/contrib/multiple_databases_repository_sample_app/public/apple-touch-icon.png new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/multiple_databases_repository_sample_app/public/favicon.ico b/contrib/multiple_databases_repository_sample_app/public/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/multiple_databases_repository_sample_app/public/robots.txt b/contrib/multiple_databases_repository_sample_app/public/robots.txt new file mode 100644 index 0000000000..c19f78ab68 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/public/robots.txt @@ -0,0 +1 @@ +# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping.rb new file mode 100644 index 0000000000..4db2c0e274 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require_relative 'shipping/application_record' +require_relative '../../lib/event' +require_relative '../../lib/types' +require_relative '../../lib/command' +require_relative '../../lib/command_handler' + +require_dependency 'shipping/package_shipped' + +require_dependency 'shipping/ship_package' + +require_dependency 'shipping/on_ship_package' + +require_dependency 'shipping/package' +require_dependency 'shipping/shipping_process' + +module Shipping + def self.setup(config) + @@command_bus = config.command_bus + @@public_event_store = RailsEventStore::Client.new( + repository: config.event_repository, + mapper: RubyEventStore::Mappers::Default.new( + events_class_remapping: events_class_remapping + ), + dispatcher: RubyEventStore::ComposedDispatcher.new( + RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)), + RubyEventStore::Dispatcher.new), + ) + @@module_event_store = RailsEventStore::Client.new( + repository: RailsEventStoreActiveRecord::EventRepository.new( + model_factory: RailsEventStoreActiveRecord::WithAbstractBaseClass.new(Shipping::ApplicationRecord), serializer: JSON), + dispatcher: RubyEventStore::ComposedDispatcher.new( + RubyEventStore::ImmediateAsyncDispatcher.new(scheduler: RailsEventStore::ActiveJobScheduler.new(serializer: JSON)), + RubyEventStore::Dispatcher.new), + ) + + # Subscribe public event handlers below + public_event_store.tap do |store| + store.subscribe(ShippingProcess.new(event_store, command_bus), to: ['new-order']) + store.subscribe(ShippingProcess.new(event_store, command_bus), to: ['payment-completed']) + end + + # Subscribe private event handlers below + event_store.tap do |store| + store.subscribe(->(ev) { + public_event_store.publish(OrderSent.new(**ev.data.slice( + :order_id, :tracking_number, :estimated_delivery_date)) + ) + }, to: [Shipping::PackageShipped] + ) + end + + # Register commands handled by this module below + command_bus.tap do |bus| + bus.register(Shipping::ShipPackage, Shipping::OnShipPackage.new(event_store)) + end + end + + def self.events_class_remapping + { + 'new-order' => 'Shipping::OrderPlaced', + 'payment-completed' => 'Shipping::OrderPaid', + 'order-sent' => 'Shipping::OrderSent', + } + end + + def self.command_bus + @@command_bus + end + + def self.public_event_store + @@public_event_store + end + + def self.event_store + @@module_event_store + end + + def self.setup? + command_bus && event_store && public_event_store + end + + class OrderPlaced < Event + event_type 'new-order' + attribute :order_id, Types::UUID + attribute :customer_id, Types::ID + attribute :delivery_address_id, Types::ID + end + + class OrderPaid < Event + event_type 'payment-completed' + attribute :order_id, Types::UUID + end + + class OrderSent < Event + event_type 'order-sent' + attribute :order_id, Types::UUID + attribute :tracking_number, Types::String + attribute :estimated_delivery_date, Types::JSON::Date + end +end diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/.keep b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/application_record.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/application_record.rb new file mode 100644 index 0000000000..2e348c816b --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/application_record.rb @@ -0,0 +1,8 @@ +require 'active_record' + +module Shipping + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + connects_to database: { writing: :shipping, reading: :shipping } + end +end diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/on_ship_package.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/on_ship_package.rb new file mode 100644 index 0000000000..100d855062 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/on_ship_package.rb @@ -0,0 +1,14 @@ +module Shipping + class OnShipPackage + include CommandHandler + + def call(command) + with_aggregate(Package.new(command.order_id), command.order_id) do |package| + package.ship_to( + command.customer_id, + command.delivery_address_id, + ) + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package.rb new file mode 100644 index 0000000000..1e180c0e9d --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package.rb @@ -0,0 +1,28 @@ +module Shipping + class Package + include AggregateRoot + + def initialize(order_id) + @order_id = order_id + end + + def ship_to(customer_id, address_id) + apply(PackageShipped.new(data: { + order_id: order_id, + customer_id: customer_id, + delivery_address_id: address_id, + tracking_number: SecureRandom.hex(16), + estimated_delivery_date: 2.days.from_now.to_date, + })) + end + + private + attr_reader :order_id + + on PackageShipped do |event| + @state = :ready + @tracking_number = event.tracking_number + end + end +end + diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package_shipped.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package_shipped.rb new file mode 100644 index 0000000000..d453e7b4df --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/package_shipped.rb @@ -0,0 +1,9 @@ +module Shipping + class PackageShipped < Event + attribute :order_id, Types::UUID + attribute :customer_id, Types::ID + attribute :delivery_address_id, Types::ID + attribute :tracking_number, Types::String + attribute :estimated_delivery_date, Types::JSON::Date + end +end diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/ship_package.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/ship_package.rb new file mode 100644 index 0000000000..792c884cfc --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/ship_package.rb @@ -0,0 +1,7 @@ +module Shipping + class ShipPackage < Command + attribute :order_id, Types::UUID + attribute :customer_id, Types::ID + attribute :delivery_address_id, Types::ID + end +end diff --git a/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/shipping_process.rb b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/shipping_process.rb new file mode 100644 index 0000000000..0ca3391889 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/lib/shipping/shipping_process.rb @@ -0,0 +1,60 @@ +module Shipping + class ShippingProcess + def initialize(event_store, bus) + @event_store = event_store + @bus = bus + end + + def call(event) + with_linked(event) do |state| + bus.call(state.command) if state.done? + end + end + + private + attr_reader :event_store, :bus + + class State < ApplicationRecord + self.table_name = :shipping_process_state + serialize :states, Array + + def intialize + self.states = [] + end + + def done? + states.include?(:placed) && states.include?(:paid) + end + + def command + Shipping::ShipPackage.new( + order_id: order_id, + customer_id: customer_id, + delivery_address_id: delivery_address_id, + ) + end + + def apply(event) + case event + when Shipping::OrderPlaced + self.customer_id = event.customer_id + self.delivery_address_id = event.delivery_address_id + states << :placed + when Shipping::OrderPaid + states << :paid + else + raise ArgumentError.new("Not suported domain event") + end + end + end + + def with_linked(event) + State.transaction do + state = State.lock.find_or_create_by!(order_id: event.order_id) + state.apply(event) + state.save! + yield state + end + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_process_spec.rb b/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_process_spec.rb new file mode 100644 index 0000000000..a4ba2b7016 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_process_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +module Shipping + RSpec.describe ShippingProcess do + let(:order_id) { SecureRandom.uuid } + let(:customer_id) { 123 } + let(:address_id) { 999 } + + it 'works' do + Shipping.public_event_store.publish(OrderPlaced.new( + order_id: order_id, + customer_id: customer_id, + delivery_address_id: address_id, + )) + Shipping.public_event_store.publish(OrderPaid.new( + order_id: order_id, + )) + + expect(Shipping.event_store).to have_published( + an_event(Shipping::PackageShipped).with_data( + order_id: order_id, + customer_id: customer_id, + delivery_address_id: address_id, + tracking_number: kind_of(String), + estimated_delivery_date: 2.days.from_now.to_date, + ) + ) + + expect(Shipping.public_event_store).to have_published( + an_event(Shipping::OrderSent).with_data( + order_id: order_id, + tracking_number: kind_of(String), + estimated_delivery_date: 2.days.from_now.to_date, + ) + ) + end + end +end diff --git a/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_spec.rb b/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_spec.rb new file mode 100644 index 0000000000..ed529f09ac --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/spec/shipping_spec.rb @@ -0,0 +1,4 @@ +require_relative 'spec_helper' + +RSpec.describe Shipping do +end diff --git a/contrib/multiple_databases_repository_sample_app/shipping/spec/spec_helper.rb b/contrib/multiple_databases_repository_sample_app/shipping/spec/spec_helper.rb new file mode 100644 index 0000000000..aaa2235dc0 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/shipping/spec/spec_helper.rb @@ -0,0 +1,21 @@ +ENV['RAILS_ENV'] ||= 'test' + +require_relative '../lib/shipping' + +module Shipping + def self.arrange(stream, events, event_store: Shipping.event_store) + event_store.append(events, stream_name: stream) + end + + def self.act(command, bus: Shipping.command_bus) + bus.call(command) + end +end + +unless Shipping.setup? + Configuration = Struct.new(:event_repository, :command_bus) + Shipping.setup(Configuration.new( + RubyEventStore::InMemoryRepository.new, + Arkency::CommandBus.new, + )) +end diff --git a/contrib/multiple_databases_repository_sample_app/spec/orders_spec.rb b/contrib/multiple_databases_repository_sample_app/spec/orders_spec.rb new file mode 100644 index 0000000000..752c4215fe --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/spec/orders_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' + +path = Rails.root.join('orders/spec') +Dir.glob("#{path}/**/*_spec.rb") do |file| + require file +end diff --git a/contrib/multiple_databases_repository_sample_app/spec/payments_spec.rb b/contrib/multiple_databases_repository_sample_app/spec/payments_spec.rb new file mode 100644 index 0000000000..095a3c45cd --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/spec/payments_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' + +path = Rails.root.join('payments/spec') +Dir.glob("#{path}/**/*_spec.rb") do |file| + require file +end diff --git a/contrib/multiple_databases_repository_sample_app/spec/rails_helper.rb b/contrib/multiple_databases_repository_sample_app/spec/rails_helper.rb new file mode 100644 index 0000000000..00345af7c0 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/spec/rails_helper.rb @@ -0,0 +1,64 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../config/environment', __dir__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/contrib/multiple_databases_repository_sample_app/spec/shipping_spec.rb b/contrib/multiple_databases_repository_sample_app/spec/shipping_spec.rb new file mode 100644 index 0000000000..35528697e3 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/spec/shipping_spec.rb @@ -0,0 +1,6 @@ +require 'rails_helper' + +path = Rails.root.join('shipping/spec') +Dir.glob("#{path}/**/*_spec.rb") do |file| + require file +end diff --git a/contrib/multiple_databases_repository_sample_app/spec/spec_helper.rb b/contrib/multiple_databases_repository_sample_app/spec/spec_helper.rb new file mode 100644 index 0000000000..ce33d66df6 --- /dev/null +++ b/contrib/multiple_databases_repository_sample_app/spec/spec_helper.rb @@ -0,0 +1,96 @@ +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end