rails_use_case 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +20 -0
- data/lib/rails/callable.rb +24 -0
- data/lib/rails/service.rb +114 -0
- data/lib/rails/use_case.rb +154 -0
- data/lib/rails/use_case/outcome.rb +34 -0
- data/lib/rails_use_case.rb +7 -0
- metadata +189 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e353dd38951526d2e38dd2007380d710c8c6bdec5634a0daeefc9fe4ce4ab469
|
4
|
+
data.tar.gz: c4624ac8f94c97634f84e03f89361947b0eea1da221e5654a718c5f015807285
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2637b2676bc1cce54bbda93b1ee5a3a62bed6bc3adde5ed5fca63730fcaa5ae6bc0161786bc236383a24b649caebb0f1c61211a745ec81030fbaee41375bb3e3
|
7
|
+
data.tar.gz: 55b19f9294a4a3993beb36e305e8a158f91b5a35ad16a4468a42c6822f60537802a05238ee2b82cd18acff27e59b5b3a1fdff6cd38a223132a330d4ecab4fbc7
|
data/README.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_support/concern'
|
4
|
+
|
5
|
+
module Rails
|
6
|
+
# Simple module to add a static call method to a class.
|
7
|
+
module Callable
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
class_methods do
|
11
|
+
def call(*args)
|
12
|
+
new.call(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :perform, :call
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
# @abstract
|
20
|
+
def call(*_args)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module Rails
|
7
|
+
# Abstract base class for all 3rd party services
|
8
|
+
#
|
9
|
+
# Provides:
|
10
|
+
# - Configuration (automatically loaded from `config/services/[service_name].yml`, available as `config`)
|
11
|
+
# - Logging (to separate log file `log/services/[service_name].log`, call via `logger.info(msg)`)
|
12
|
+
# - Call style invocation (like `PDFGenerationService.(some, params)`)
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# class Services::PDFGenerationService < Service
|
16
|
+
# def initialize
|
17
|
+
# super('pdf_generation')
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def call(action, *args)
|
21
|
+
# ... here happens the magic! ...
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# PDFGenerationService.(some, params)
|
26
|
+
#
|
27
|
+
# @abstract
|
28
|
+
class Service
|
29
|
+
include Rails::Callable
|
30
|
+
|
31
|
+
attr_reader :logger, :service_name, :config
|
32
|
+
|
33
|
+
# Constructor. Call this from the subclass with the service name, like `super 'pdf_generator'`.
|
34
|
+
# After that you can access the config and logger.
|
35
|
+
#
|
36
|
+
# @param [String] service_name Name of the service, like 'pdf_generator'.
|
37
|
+
# @raise [NotImplementedError] When this class is tried to be instantiated without subclass.
|
38
|
+
#
|
39
|
+
# @raise [RuntimeError] When no service_name is given
|
40
|
+
def initialize(service_name = nil)
|
41
|
+
raise NotImplementedError if self.class == Service
|
42
|
+
raise 'Please provide a service name!' if service_name.nil?
|
43
|
+
|
44
|
+
@service_name = service_name
|
45
|
+
|
46
|
+
setup_logger
|
47
|
+
setup_configuration
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Create the log file and sets @logger
|
52
|
+
private def setup_logger
|
53
|
+
log_path = Rails.root.join('log', 'services')
|
54
|
+
FileUtils.mkdir_p(log_path) unless Dir.exist?(log_path)
|
55
|
+
@logger = Logger.new(Rails.root.join('log', 'services', "#{@service_name}.log").to_s)
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
# Loads the configuration for that service and saves it to @config
|
60
|
+
# @raise [RuntimeError] When the config file doesn't exist
|
61
|
+
private def setup_configuration
|
62
|
+
shared_config_path = Rails.root.join('config', 'services', 'shared.yml')
|
63
|
+
config_path = Rails.root.join('config', 'services', "#{@service_name}.yml")
|
64
|
+
raise "Couldn't find the shared config file '#{shared_config_path}'." unless File.exist?(shared_config_path)
|
65
|
+
|
66
|
+
shared_config = load_config_file(shared_config_path)
|
67
|
+
|
68
|
+
if File.exist?(config_path)
|
69
|
+
service_config = load_config_file(config_path) || {}
|
70
|
+
@config = shared_config.merge(service_config)
|
71
|
+
else
|
72
|
+
@config = shared_config
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
private def load_config_file(path)
|
78
|
+
erb = File.read(path)
|
79
|
+
yaml = ERB.new(erb).result.strip
|
80
|
+
|
81
|
+
return {} if yaml.blank?
|
82
|
+
|
83
|
+
YAML.safe_load(yaml) || {}
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# Convenience method to get a secret. It looks for the key `services.<service_name>.<key>`
|
88
|
+
private def secret(key)
|
89
|
+
key = key.to_sym
|
90
|
+
base = Rails.application.secrets.services[@service_name.to_sym]
|
91
|
+
|
92
|
+
raise "No secrets entry found for 'services.#{@service_name}'" unless base
|
93
|
+
raise "No secrets entry found for 'services.#{@service_name}.#{key}'" unless base[key]
|
94
|
+
|
95
|
+
base[key]
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
# Abstract method for instance call. Implement this in the subclass!
|
100
|
+
# @raise [NotImplementedError] When this is not overwritten in the subclass
|
101
|
+
def call(options); end
|
102
|
+
|
103
|
+
|
104
|
+
# Allows call syntax on class level: SomeService.(some, args)
|
105
|
+
def self.call(*args)
|
106
|
+
new.(*args)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Allows to use rails view helpers
|
110
|
+
def helpers
|
111
|
+
ApplicationController.new.helpers
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model/validations'
|
4
|
+
require 'rails/use_case/outcome'
|
5
|
+
|
6
|
+
module Rails
|
7
|
+
# A UseCase is a class that contains high level business logic.
|
8
|
+
# It's used to keep controllers and models slim.
|
9
|
+
#
|
10
|
+
# The difference to a Service is that a Service contains low level
|
11
|
+
# non-domain code like communication with a API, generating an
|
12
|
+
# export, etc., while a UseCase contains high level domain logic
|
13
|
+
# like placing a item in the cart, submitting an order, etc.
|
14
|
+
#
|
15
|
+
# The logic of a UseCase is defined via steps. The next step is only
|
16
|
+
# executed when the previous ones returned a truthy value.
|
17
|
+
#
|
18
|
+
# The UseCase should assign the main record to @record. Calling save!
|
19
|
+
# without argument will try to save that record or raises an exception.
|
20
|
+
#
|
21
|
+
# A UseCase should raise the UseCase::Error exception for any
|
22
|
+
# problems.
|
23
|
+
#
|
24
|
+
# UseCase also includes ActiveModel::Validations for simple yet
|
25
|
+
# powerful validations. The validations are run automatically as first step.
|
26
|
+
#
|
27
|
+
# A UseCase can be called via .call(params) or .perform(params) and
|
28
|
+
# always returns a instance of UseCase::Outcome. params should be a hash.
|
29
|
+
class Rails::UseCase
|
30
|
+
include Callable
|
31
|
+
include ActiveModel::Validations
|
32
|
+
|
33
|
+
class Error < StandardError; end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
attr_reader :steps
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
# Will be called by Callable.call.
|
41
|
+
# @param params [Hash] The arguments for the UseCase as Hash
|
42
|
+
# so we can auto assign instance variables.
|
43
|
+
def call(params)
|
44
|
+
prepare params
|
45
|
+
process
|
46
|
+
|
47
|
+
successful_outcome
|
48
|
+
rescue UseCase::Error => e
|
49
|
+
failure_outcome e
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
# DSL to define a process step of the UseCase.
|
54
|
+
# You can use if/unless with a lambda in the options
|
55
|
+
# to conditionally skip the step.
|
56
|
+
# @param name [Symbol]
|
57
|
+
# @param options [Hash]
|
58
|
+
def self.step(name, options = {})
|
59
|
+
@steps ||= []
|
60
|
+
@steps << { name: name.to_sym, options: options }
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
# Will run the steps of the use case.
|
65
|
+
def process
|
66
|
+
self.class.steps.each do |step|
|
67
|
+
next if skip_step?(step)
|
68
|
+
next if send(step[:name])
|
69
|
+
|
70
|
+
raise UseCase::Error, "Step #{step[:name]} returned false"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
# Checks whether to skip a step.
|
76
|
+
# @param step [Hash]
|
77
|
+
def skip_step?(step)
|
78
|
+
if step[:options][:if]
|
79
|
+
proc = step[:options][:if]
|
80
|
+
result = instance_exec(&proc)
|
81
|
+
return true unless result
|
82
|
+
end
|
83
|
+
|
84
|
+
return false unless step[:options][:unless]
|
85
|
+
|
86
|
+
proc = step[:options][:unless]
|
87
|
+
result = instance_exec(&proc)
|
88
|
+
return true if result
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
# Prepare step. Runs automatically before the UseCase process starts.
|
93
|
+
# Sets all params as instance variables and then runs the validations.
|
94
|
+
# @param params [Hash]
|
95
|
+
def prepare(params)
|
96
|
+
params.each do |key, value|
|
97
|
+
instance_variable_set "@#{key}", value
|
98
|
+
end
|
99
|
+
|
100
|
+
break_when_invalid!
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
# @raises [UseCase::Error] When validations failed.
|
105
|
+
def break_when_invalid!
|
106
|
+
return true if valid?
|
107
|
+
|
108
|
+
raise UseCase::Error, errors.full_messages.join(', ')
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
# Saves the a ActiveRecord object. When the object can't be saved, the
|
113
|
+
# validation errors are pushed into the UseCase errors array and then
|
114
|
+
# a UseCase::Error is raised.
|
115
|
+
# @param record [ApplicationModel] Record to save.
|
116
|
+
# @raises [UseCase::Error] When record can't be saved.
|
117
|
+
private def save!(record = nil)
|
118
|
+
record ||= @record
|
119
|
+
|
120
|
+
return false unless record
|
121
|
+
return true if record.save
|
122
|
+
|
123
|
+
errors.add(
|
124
|
+
record.model_name.singular,
|
125
|
+
:invalid,
|
126
|
+
message: record.errors.full_messages.join(', ')
|
127
|
+
)
|
128
|
+
|
129
|
+
raise UseCase::Error, "#{record.class.name} is not valid"
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
# @return [UseCase::Outcome] Successful outcome.
|
134
|
+
private def successful_outcome
|
135
|
+
Outcome.new(
|
136
|
+
success: true,
|
137
|
+
record: @record,
|
138
|
+
errors: errors
|
139
|
+
)
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
# @param error [StandardError]
|
144
|
+
# @return [UseCase::Outcome] Failure outcome with exception set.
|
145
|
+
private def failure_outcome(error)
|
146
|
+
Outcome.new(
|
147
|
+
success: false,
|
148
|
+
record: @record,
|
149
|
+
errors: errors,
|
150
|
+
exception: error
|
151
|
+
)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
class UseCase
|
5
|
+
# Outcome of a UseCase
|
6
|
+
class Outcome
|
7
|
+
attr_reader :success, :errors, :record, :exception
|
8
|
+
|
9
|
+
# Constructor.
|
10
|
+
# @param success [Boolean] Wether the UseCase was successful.
|
11
|
+
# @param errors [Array|nil] ActiveModel::Validations error.
|
12
|
+
# @param record [ApplicationRecord|nil] The main record of the use case.
|
13
|
+
# @param exception [Rails::UseCase::Error|nil] The error which was raised.
|
14
|
+
def initialize(success:, errors: nil, record: nil, exception: nil)
|
15
|
+
@success = success
|
16
|
+
@errors = errors
|
17
|
+
@record = record
|
18
|
+
@exception = exception
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
# @return [Boolean] Whether the UseCase was successful.
|
23
|
+
def success?
|
24
|
+
@success
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# @return [Boolean] Whether the UseCase failed.
|
29
|
+
def failed?
|
30
|
+
!@success
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails_use_case
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Benjamin Klein
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: railties
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activemodel
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.1.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 4.1.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler-audit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.6'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.6'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.9'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-mocks
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.9'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.9'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.78'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.78'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop-rspec
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.37'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.37'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubygems-tasks
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: simplecov
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.17'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.17'
|
153
|
+
description: Rails UseCase and Service classes
|
154
|
+
email:
|
155
|
+
- bk@itws.de
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- README.md
|
161
|
+
- lib/rails/callable.rb
|
162
|
+
- lib/rails/service.rb
|
163
|
+
- lib/rails/use_case.rb
|
164
|
+
- lib/rails/use_case/outcome.rb
|
165
|
+
- lib/rails_use_case.rb
|
166
|
+
homepage: https://github.com/phortx/rails-use-case
|
167
|
+
licenses:
|
168
|
+
- MIT
|
169
|
+
metadata: {}
|
170
|
+
post_install_message:
|
171
|
+
rdoc_options: []
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
requirements:
|
176
|
+
- - ">="
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: '0'
|
179
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
180
|
+
requirements:
|
181
|
+
- - ">="
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
requirements: []
|
185
|
+
rubygems_version: 3.0.3
|
186
|
+
signing_key:
|
187
|
+
specification_version: 4
|
188
|
+
summary: Rails UseCase and Service classes
|
189
|
+
test_files: []
|