Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
name: CI
on: [push]

on:
push:
branches:
- main
pull_request:

jobs:
rspec:
strategy:
fail-fast: false
matrix:
include:
- { ruby: '2.7', rails: '5.2' }
- { ruby: '2.7', rails: '6.0' }
- { ruby: '3.0', rails: '6.1' }
- { ruby: '3.1', rails: '7.0' }
- { ruby: '3.2', rails: '7.1' }
- { ruby: '3.3', rails: '7.2' }
- { ruby: '3.4', rails: '8.0' }
- { ruby: '3.2', rails: '7.2' }
- { ruby: '3.3', rails: '8.0' }
- { ruby: '3.4', rails: '8.1' }
runs-on: ubuntu-latest
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/rails.${{ matrix.rails }}.gemfile
Expand All @@ -30,6 +32,6 @@ jobs:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
ruby-version: 3.2
bundler-cache: true
- run: bundle exec rubocop
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ inherit_mode:
- Exclude

AllCops:
TargetRubyVersion: 2.7
TargetRubyVersion: 3.2
Exclude:
- gemfiles/*
43 changes: 25 additions & 18 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,48 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2024-09-27 05:26:10 UTC using RuboCop version 1.65.0.
# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 100`
# on 2025-11-29 07:18:24 UTC using RuboCop version 1.81.7.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.

# Offense count: 3
# Configuration parameters: EnforcedStyle, AllowedGems, Include.
# Configuration parameters: EnforcedStyle, AllowedGems.
# SupportedStyles: Gemfile, gems.rb, gemspec
# Include: **/*.gemspec, **/Gemfile, **/gems.rb
Gemspec/DevelopmentDependencies:
Exclude:
- 'operations.gemspec'

# Offense count: 5
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
# Offense count: 6
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
Metrics/AbcSize:
Max: 28
Exclude:
- 'lib/operations/command.rb'
- 'lib/operations/form/base.rb'
- 'lib/operations/form/builder.rb'

# Offense count: 1
# Configuration parameters: CountComments, CountAsOne.
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ClassLength:
Max: 149
Exclude:
- 'lib/operations/command.rb'

# Offense count: 2
# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Exclude:
- 'lib/operations/components/idempotency.rb'
- 'lib/operations/form/base.rb'

# Offense count: 1
# Configuration parameters: CountComments, CountAsOne.
# Configuration parameters: CountComments, Max, CountAsOne.
Metrics/ModuleLength:
Max: 144
Exclude:
- 'lib/operations/form/base.rb'

# Offense count: 3
# Configuration parameters: CountKeywordArgs, MaxOptionalParameters.
# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters.
Metrics/ParameterLists:
Max: 7

# Offense count: 1
# Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
RSpec/VerifiedDoubles:
Exclude:
- 'spec/operations/form_spec.rb'
- 'lib/operations/command.rb'
- 'lib/operations/form/builder.rb'
3 changes: 1 addition & 2 deletions Appraisals
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# frozen_string_literal: true

%w[5.2 6.0 6.1 7.0 7.1 7.2 8.0].each do |version|
%w[7.2 8.0 8.1].each do |version|
appraise "rails.#{version}" do
gem "activerecord", "~> #{version}.0"
gem "activesupport", "~> #{version}.0"
gem "sqlite3", version > "7.0" ? "~> 2.1" : "~> 1.4"
end
end
24 changes: 2 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Where `Operations::Contract` is actually a [Dry::Validation::Contract](https://d

Everything in the framework is built with the composition over inheritance approach in mind. An instance of `Operations::Command` essentially runs a pipeline through the steps passed into the initializer. In this particular case, the passed parameters will be validated by the contract and if everything is good, will be passed into the operation body.

**Important:** the whole operation pipeline (except [callbacks](#callbacks-on-success-on-failure)) is wrapped within a transaction by default. This behavior can be adjusted by changing `Operations::Configuration#transaction` (see [Configuration](#configuration) section).
**Important:** the whole operation pipeline (except [callbacks](#callbacks-on-success-on-failure)) is wrapped within a transaction by default.

### Operation Result

Expand Down Expand Up @@ -353,24 +353,6 @@ end

Of course, it is possible to use [dry-auto_inject](https://dry-rb.org/gems/dry-auto_inject/) along with [dry-container](https://dry-rb.org/gems/dry-container/) to make things even fancier.

### Configuration

The gem has a global default configuration:

```ruby
Operations.configure(
error_reporter: -> (message, payload) { Sentry.capture_message(message, extra: payload) },
)
```

But also, a configuration instance can be passed directly to a Command initializer (for example, to switch off the wrapping DB transaction for a single operation):

```ruby
Operations::Command.new(..., configuration: Operations.default_config.new(transaction: -> {}))
```

It is possible to call `configuration_instance.new` to receive an updated configuration instance since it is a `Dry::Struct`

### Preconditions

When we need to check against the application state, preconditions are coming to help. Obviously, we can do all those checks in Contract rule definitions but it is great to have separate kinds of components (a separate stage in the pipeline) for this particular reason as it gives the ability to check them in isolation.
Expand Down Expand Up @@ -701,9 +683,7 @@ Sometimes we need to run further application state modifications outside of the

The key difference besides one running after operation success and another - after failure, is that `on_success` runs after the transaction commit. This means that if one operation calls another operation inside of it and the inner one has `on_success` callbacks defined - the callbacks are going to be executed only after the outermost transaction is committed successfully.

To achieve this, the framework utilizes the [after_commit_everywhere](https://github.com/Envek/after_commit_everywhere) gem and the behavior is configurable using `Operations::Configuration#after_commit` option.

It is a good idea to use these callbacks to schedule some jobs instead of just running inline code since if callback execution fails - the failure will be ignored and the operation is still going to be successful. Though the failure from both callbacks will be reported using `Operations::Configuration#error_reporter` and using Sentry by default.
Transaction handling and callback scheduling are managed by [OmniService](https://rubygems.org/gems/omni_service), which uses Rails' native `transaction.after_commit` mechanism (requires Rails 7.2+).

```ruby
class Comment::Update
Expand Down
15 changes: 0 additions & 15 deletions gemfiles/rails.6.0.gemfile

This file was deleted.

15 changes: 0 additions & 15 deletions gemfiles/rails.6.1.gemfile

This file was deleted.

15 changes: 0 additions & 15 deletions gemfiles/rails.7.0.gemfile

This file was deleted.

15 changes: 0 additions & 15 deletions gemfiles/rails.7.1.gemfile

This file was deleted.

1 change: 0 additions & 1 deletion gemfiles/rails.7.2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
gem "activerecord", "~> 7.2.0"
gem "activesupport", "~> 7.2.0"
gem "sqlite3", "~> 2.1"

gemspec path: "../"
1 change: 0 additions & 1 deletion gemfiles/rails.8.0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
gem "activerecord", "~> 8.0.0"
gem "activesupport", "~> 8.0.0"
gem "sqlite3", "~> 2.1"

gemspec path: "../"
5 changes: 2 additions & 3 deletions gemfiles/rails.5.2.gemfile → gemfiles/rails.8.1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ gem "rubocop", require: false
gem "rubocop-performance", require: false
gem "rubocop-rails", require: false
gem "rubocop-rspec", require: false
gem "activerecord", "~> 5.2.0"
gem "activesupport", "~> 5.2.0"
gem "sqlite3", "~> 1.4"
gem "activerecord", "~> 8.1.0"
gem "activesupport", "~> 8.1.0"

gemspec path: "../"
24 changes: 3 additions & 21 deletions lib/operations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
require "active_support/core_ext/module/delegation"
require "active_support/inflector/inflections"
require "active_model/naming"
require "after_commit_everywhere"
require "omni_service"
require "operations/version"
require "operations/types"
require "operations/inspect"
require "operations/configuration"
require "operations/contract"
require "operations/contract/messages_resolver"
require "operations/convenience"
Expand All @@ -26,24 +25,7 @@

# The root gem module
module Operations
class Error < StandardError
end

DEFAULT_ERROR_REPORTER = ->(message, payload) { Sentry.capture_message(message, extra: payload) }
DEFAULT_TRANSACTION = ->(&block) { ActiveRecord::Base.transaction(requires_new: true, &block) }
DEFAULT_AFTER_COMMIT = ->(&block) { AfterCommitEverywhere.after_commit(&block) }

class << self
attr_reader :default_config

def configure(configuration = nil, **options)
@default_config = (configuration || Configuration).new(**options)
end
end
end

configure(
error_reporter: DEFAULT_ERROR_REPORTER,
transaction: DEFAULT_TRANSACTION,
after_commit: DEFAULT_AFTER_COMMIT
)
class Operations::Error < StandardError
end
Loading