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 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,20 @@
1
+ # Rails Use Case gem
2
+
3
+ Opinionated gem for UseCases and Services in rails.
4
+
5
+
6
+ ## Setup
7
+
8
+ ```ruby
9
+ gem 'rails_use_case'
10
+ ```
11
+
12
+
13
+ ## Usage
14
+
15
+ TODO
16
+
17
+
18
+ ## License
19
+
20
+ MIT
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rails; end
4
+
5
+ require 'rails/callable'
6
+ require 'rails/use_case'
7
+ require 'rails/service'
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: []