simple_logic_step 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e2737a12e74fc2921b346c7b85969a81cdf864183a0f8973589bfebcccc7d17f
4
+ data.tar.gz: aee1c8f00b420357605eb7df88d85e63d270775fdd852cae90261332ad8be4e8
5
+ SHA512:
6
+ metadata.gz: a8877a574ed4e66fea2b4bc0c365c7fe93fe6c8eaa0e8cbd024b1978d87959be8f3204c63d906418feaf91ba210bad9c5730c7e8dbeae465b2cea210f2e48070
7
+ data.tar.gz: dfd574f9db424b1d866f4d3ea4d7f0ddfc6637a73b9819791a939a2aef747eec06e44e556d1be82887024361857d6c692d369cf186e706f4b5ab6d3e5f1496eb
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,161 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ NewCops: enable
4
+ Exclude:
5
+ - 'examples/**/*.rb'
6
+
7
+ Style/StringLiterals:
8
+ Enabled: true
9
+ EnforcedStyle: single_quotes
10
+
11
+ Style/StringLiteralsInInterpolation:
12
+ Enabled: true
13
+ EnforcedStyle: single_quotes
14
+
15
+ Metrics/BlockLength:
16
+ Enabled: true
17
+ Max: 125
18
+
19
+ Layout/LineLength:
20
+ Max: 120
21
+
22
+ Gemspec/DeprecatedAttributeAssignment: # new in 1.30
23
+ Enabled: true
24
+ Gemspec/RequireMFA: # new in 1.23
25
+ Enabled: true
26
+ Layout/LineContinuationLeadingSpace: # new in 1.31
27
+ Enabled: true
28
+ Layout/LineContinuationSpacing: # new in 1.31
29
+ Enabled: true
30
+ Layout/LineEndStringConcatenationIndentation: # new in 1.18
31
+ Enabled: true
32
+ Layout/SpaceBeforeBrackets: # new in 1.7
33
+ Enabled: true
34
+ Lint/AmbiguousAssignment: # new in 1.7
35
+ Enabled: true
36
+ Lint/AmbiguousOperatorPrecedence: # new in 1.21
37
+ Enabled: true
38
+ Lint/AmbiguousRange: # new in 1.19
39
+ Enabled: true
40
+ Lint/ConstantOverwrittenInRescue: # new in 1.31
41
+ Enabled: true
42
+ Lint/DeprecatedConstants: # new in 1.8
43
+ Enabled: true
44
+ Lint/DuplicateBranch: # new in 1.3
45
+ Enabled: true
46
+ Lint/DuplicateMagicComment: # new in 1.37
47
+ Enabled: true
48
+ Lint/DuplicateRegexpCharacterClassElement: # new in 1.1
49
+ Enabled: true
50
+ Lint/EmptyBlock: # new in 1.1
51
+ Enabled: true
52
+ Lint/EmptyClass: # new in 1.3
53
+ Enabled: true
54
+ Lint/EmptyInPattern: # new in 1.16
55
+ Enabled: true
56
+ Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21
57
+ Enabled: true
58
+ Lint/LambdaWithoutLiteralBlock: # new in 1.8
59
+ Enabled: true
60
+ Lint/NoReturnInBeginEndBlocks: # new in 1.2
61
+ Enabled: true
62
+ Lint/NonAtomicFileOperation: # new in 1.31
63
+ Enabled: true
64
+ Lint/NumberedParameterAssignment: # new in 1.9
65
+ Enabled: true
66
+ Lint/OrAssignmentToConstant: # new in 1.9
67
+ Enabled: true
68
+ Lint/RedundantDirGlobSort: # new in 1.8
69
+ Enabled: true
70
+ Lint/RefinementImportMethods: # new in 1.27
71
+ Enabled: true
72
+ Lint/RequireRangeParentheses: # new in 1.32
73
+ Enabled: true
74
+ Lint/RequireRelativeSelfPath: # new in 1.22
75
+ Enabled: true
76
+ Lint/SymbolConversion: # new in 1.9
77
+ Enabled: true
78
+ Lint/ToEnumArguments: # new in 1.1
79
+ Enabled: true
80
+ Lint/TripleQuotes: # new in 1.9
81
+ Enabled: true
82
+ Lint/UnexpectedBlockArity: # new in 1.5
83
+ Enabled: true
84
+ Lint/UnmodifiedReduceAccumulator: # new in 1.1
85
+ Enabled: true
86
+ Lint/UselessRuby2Keywords: # new in 1.23
87
+ Enabled: true
88
+ Naming/BlockForwarding: # new in 1.24
89
+ Enabled: true
90
+ Security/CompoundHash: # new in 1.28
91
+ Enabled: true
92
+ Security/IoMethods: # new in 1.22
93
+ Enabled: true
94
+ Style/ArgumentsForwarding: # new in 1.1
95
+ Enabled: true
96
+ Style/CollectionCompact: # new in 1.2
97
+ Enabled: true
98
+ Style/DocumentDynamicEvalDefinition: # new in 1.1
99
+ Enabled: true
100
+ Style/EmptyHeredoc: # new in 1.32
101
+ Enabled: true
102
+ Style/EndlessMethod: # new in 1.8
103
+ Enabled: true
104
+ Style/EnvHome: # new in 1.29
105
+ Enabled: true
106
+ Style/FetchEnvVar: # new in 1.28
107
+ Enabled: true
108
+ Style/FileRead: # new in 1.24
109
+ Enabled: true
110
+ Style/FileWrite: # new in 1.24
111
+ Enabled: true
112
+ Style/HashConversion: # new in 1.10
113
+ Enabled: true
114
+ Style/HashExcept: # new in 1.7
115
+ Enabled: true
116
+ Style/IfWithBooleanLiteralBranches: # new in 1.9
117
+ Enabled: true
118
+ Style/InPatternThen: # new in 1.16
119
+ Enabled: true
120
+ Style/MagicCommentFormat: # new in 1.35
121
+ Enabled: true
122
+ Style/MapCompactWithConditionalBlock: # new in 1.30
123
+ Enabled: true
124
+ Style/MapToHash: # new in 1.24
125
+ Enabled: true
126
+ Style/MultilineInPatternThen: # new in 1.16
127
+ Enabled: true
128
+ Style/NegatedIfElseCondition: # new in 1.2
129
+ Enabled: true
130
+ Style/NestedFileDirname: # new in 1.26
131
+ Enabled: true
132
+ Style/NilLambda: # new in 1.3
133
+ Enabled: true
134
+ Style/NumberedParameters: # new in 1.22
135
+ Enabled: true
136
+ Style/NumberedParametersLimit: # new in 1.22
137
+ Enabled: true
138
+ Style/ObjectThen: # new in 1.28
139
+ Enabled: true
140
+ Style/OpenStructUse: # new in 1.23
141
+ Enabled: true
142
+ Style/OperatorMethodCall: # new in 1.37
143
+ Enabled: true
144
+ Style/QuotedSymbols: # new in 1.16
145
+ Enabled: true
146
+ Style/RedundantArgument: # new in 1.4
147
+ Enabled: true
148
+ Style/RedundantEach: # new in 1.38
149
+ Enabled: true
150
+ Style/RedundantInitialize: # new in 1.27
151
+ Enabled: true
152
+ Style/RedundantSelfAssignmentBranch: # new in 1.19
153
+ Enabled: true
154
+ Style/RedundantStringEscape: # new in 1.37
155
+ Enabled: true
156
+ Style/SelectByRegexp: # new in 1.22
157
+ Enabled: true
158
+ Style/StringChars: # new in 1.12
159
+ Enabled: true
160
+ Style/SwapValues: # new in 1.1
161
+ Enabled: true
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2023-03-19
4
+
5
+ - Initial release
@@ -0,0 +1,84 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or
22
+ advances of any kind
23
+ * Trolling, insulting or derogatory comments, and personal or political attacks
24
+ * Public or private harassment
25
+ * Publishing others' private information, such as a physical or email
26
+ address, without their explicit permission
27
+ * Other conduct which could reasonably be considered inappropriate in a
28
+ professional setting
29
+
30
+ ## Enforcement Responsibilities
31
+
32
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33
+
34
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35
+
36
+ ## Scope
37
+
38
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39
+
40
+ ## Enforcement
41
+
42
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at differencialx@gmail.com. All complaints will be reviewed and investigated promptly and fairly.
43
+
44
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45
+
46
+ ## Enforcement Guidelines
47
+
48
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49
+
50
+ ### 1. Correction
51
+
52
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53
+
54
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55
+
56
+ ### 2. Warning
57
+
58
+ **Community Impact**: A violation through a single incident or series of actions.
59
+
60
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61
+
62
+ ### 3. Temporary Ban
63
+
64
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65
+
66
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67
+
68
+ ### 4. Permanent Ban
69
+
70
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71
+
72
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
73
+
74
+ ## Attribution
75
+
76
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77
+ available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78
+
79
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80
+
81
+ [homepage]: https://www.contributor-covenant.org
82
+
83
+ For answers to common questions about this code of conduct, see the FAQ at
84
+ https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in simple_logic_step.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'rspec', '~> 3.0'
11
+
12
+ gem 'rubocop', '~> 1.21'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Al Bal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # Simple Logic Step
2
+
3
+ I believe that some of developers faced a situation when you can't convince your **customer** | **project manager** | **team lead** | **teammates** to use any of existing business logic handler, as they think it:
4
+ - has no value for business
5
+ - is hard to integrate
6
+ - needs to be learned be developers
7
+ - is no guarantee that this gem will be well maintained in the future
8
+ - is developed by no name author
9
+
10
+ But you still want to make your controllers and models as thin as possible.
11
+ If such situation is familiar for you then this gem is for you.
12
+ This is a one file gem, just copy `Service` class from `lib/simple_logic_step.rb` to your project and specs for it from `spec/simple_logic_step/logic_step_spec.rb`.
13
+
14
+ ## Installation
15
+
16
+ Install the gem and add to the application's Gemfile by executing:
17
+
18
+ $ bundle add simple_logic_step
19
+
20
+ If bundler is not being used to manage dependencies, install the gem by executing:
21
+
22
+ $ gem install simple_logic_step
23
+
24
+ Gemfile:
25
+
26
+ gem 'simple_logic_step'
27
+
28
+ ## Usage
29
+
30
+ 1. Create class inherited from `SimpleLogicStep::Service` or the class name you chose during copying `Service` class to your project.
31
+
32
+ ```ruby
33
+ class YourService < SimpleLogicStep::Service
34
+ # ...
35
+ end
36
+ ```
37
+
38
+ 2. Define `#process` method with your business logic
39
+
40
+ ```ruby
41
+ class YourService < SimpleLogicStep::Service
42
+ def process
43
+ # Put your logic here
44
+ end
45
+ end
46
+ ```
47
+
48
+ 3. Call your service
49
+ ```ruby
50
+ YourService.call(params: { name: 'name' }) # => instance of YourService
51
+ YourService.call(params: { name: 'name' }).success? # => true
52
+ YourService.call(params: { name: 'name' }).failure? # => false
53
+ ```
54
+
55
+ ### Context
56
+
57
+ Params should be passed as kwargs to `.call` method, inside `#process` method you can access them via `ctx` method. `ctx` is a simple hash
58
+
59
+ ```ruby
60
+ class YourService < SimpleLogicStep::Service
61
+ def process
62
+ ctx[:params] # => { name: 'name' }
63
+ end
64
+ end
65
+ ```
66
+
67
+ ### How to fail_step
68
+
69
+ By default service will end execution as success, if you need to fail it call `fail_step` method
70
+
71
+ ```ruby
72
+ # Method signature
73
+ # semantic - represents flag which can be used to handle failures differently
74
+ # use it when failure? flag is not enough to handle service result
75
+ # message - specify error message
76
+ fail_step(semantic: nil, message: nil)
77
+ ```
78
+
79
+ ```ruby
80
+ class YourService < SimpleLogicStep::Service
81
+ def process
82
+ fail(semantic: :not_found, message: 'Record not found')
83
+ end
84
+ end
85
+
86
+ service = YourService.call
87
+ service.failure? # => true
88
+ service.success? # => false
89
+ service.semantic # => :not_found
90
+ service.message # => 'Record not found'
91
+ ```
92
+
93
+ ### How to use "never nesting" approach
94
+ "Never nesting" approach - is the approach when you use guard clause to handle negative cases until you reach positive case.
95
+
96
+ ```ruby
97
+ class YourService < SimpleLogicStep::Service
98
+ CONFLICT = :conflict
99
+ NOT_FOUND = :not_found
100
+ BAD_REQUEST = :bad_request
101
+
102
+ attr_reader :record
103
+
104
+ def process
105
+ if ctx[:flag] == 'bad_request'
106
+ fail_step(semantic: BAD_REQUEST, message: 'Oops, bad request')
107
+ return
108
+ end
109
+
110
+ if ctx[:flag] == 'not_found'
111
+ fail_step(semantic: NOT_FOUND, message: 'Oops, not found')
112
+ return
113
+ end
114
+
115
+ if ctx[:flag] == 'conflict'
116
+ fail_step(semantic: CONFLICT, message: 'Oops, conflict')
117
+ return
118
+ end
119
+
120
+ @record = { id: 1, name: 'john' }
121
+ end
122
+ end
123
+
124
+ def handle_conflict(result)
125
+ {
126
+ status: 409,
127
+ message: result.message
128
+ }
129
+ end
130
+
131
+ def handle_not_found(result)
132
+ {
133
+ status: 404,
134
+ message: result.message
135
+ }
136
+ end
137
+
138
+ def handle_bad_request(result)
139
+ {
140
+ status: 400,
141
+ message: result.message
142
+ }
143
+ end
144
+
145
+ def handle_success(result)
146
+ {
147
+ status: 200,
148
+ data: result.record
149
+ }
150
+ end
151
+
152
+ def lets_imagine_it_is_controller_action
153
+ service = YourService.call(flag: %w[success bad_request not_found conflict].sample)
154
+
155
+ # Block will be executed only if service failed
156
+ # #success? method works in the similar way, but only when service success
157
+ service.failure? do |result|
158
+ case result.semantic
159
+ when YourService::CONFLICT
160
+ # return will end method execution
161
+ return handle_conflict(result)
162
+ when YourService::NOT_FOUND
163
+ # return will end method execution
164
+ return handle_not_found(result)
165
+ when YourService::BAD_REQUEST
166
+ # return will end method execution
167
+ return handle_bad_request(result)
168
+ end
169
+ end
170
+
171
+ handle_success(service)
172
+ end
173
+
174
+ lets_imagine_it_is_controller_action # => {:status=>200, :data=>{:id=>1, :name=>"john"}}
175
+ lets_imagine_it_is_controller_action # => {:status=>409, :message=>"Oops, conflict"}
176
+ lets_imagine_it_is_controller_action # => {:status=>400, :message=>"Oops, bad request"}
177
+ lets_imagine_it_is_controller_action # => {:status=>404, :message=>"Oops, not found"}
178
+ ```
179
+ More complex example [See it here](https://github.com/differencialx/simple_logic_step/blob/main/examples/controller.rb)
180
+
181
+ ### Prepare method
182
+
183
+ If you need to make some preparation before `#process` call use `#prepare` method
184
+
185
+ ```ruby
186
+ class YourService < SimpleLogicStep::Service
187
+ attr_reader :result
188
+
189
+ def prepare
190
+ ctx[:name] = ctx[:name].capitalize
191
+ end
192
+
193
+ def process
194
+ @result = "Hello #{ctx[:name]}"
195
+ end
196
+ end
197
+
198
+ YourService.call(name: 'john').result # "Hello John"
199
+ ```
200
+
201
+ You can also add modify [`#call` method ](https://github.com/differencialx/simple_logic_step/blob/main/lib/simple_logic_step.rb#L23) and add post process method or whatever you need.
202
+
203
+
204
+
205
+ ## License
206
+
207
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,290 @@
1
+ # Copied base service class
2
+
3
+ class Service
4
+ attr_reader :ctx, :semantic, :message
5
+
6
+ def self.call(**ctx)
7
+ new(**ctx).call
8
+ end
9
+
10
+ def initialize(**ctx)
11
+ @success = true
12
+ @semantic = nil
13
+ @ctx = ctx
14
+ @message = nil
15
+ end
16
+
17
+ def call
18
+ prepare
19
+ process
20
+ self
21
+ end
22
+
23
+ # Implement business logic here
24
+ def process
25
+ raise 'Not implemented'
26
+ end
27
+
28
+ def failure?
29
+ if block_given?
30
+ yield(self) unless success
31
+ else
32
+ !success
33
+ end
34
+ end
35
+
36
+ def success?
37
+ if block_given?
38
+ yield(self) if success
39
+ else
40
+ success
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ attr_reader :success
47
+
48
+ # Redefine this method in case
49
+ # if you need to do some preparations
50
+ # before #process call
51
+ def prepare; end
52
+
53
+ def fail_step(semantic: nil, message: nil)
54
+ @success = false
55
+ add_semantic(semantic)
56
+ @message = message
57
+ end
58
+
59
+ def pass_step(semantic: nil, message: nil)
60
+ @success = true
61
+ add_semantic(semantic)
62
+ @message = message
63
+ end
64
+
65
+ def add_semantic(semantic)
66
+ @semantic = semantic
67
+ end
68
+ end
69
+
70
+ # Model
71
+
72
+ class SomeModel
73
+ attr_accessor :id, :name, :description, :errors
74
+
75
+ def initialize(name:, description:)
76
+ @id = nil
77
+ @name = name
78
+ @description = description
79
+ @errors = []
80
+ end
81
+
82
+ def valid?
83
+ if @name == 'invalid_name'
84
+ @errors << 'Name is invalid'
85
+ return false
86
+ end
87
+
88
+ true
89
+ end
90
+
91
+ def save
92
+ @id = 1
93
+
94
+ true
95
+ end
96
+
97
+ def as_json
98
+ {
99
+ id: @id,
100
+ name: @name,
101
+ description: @description
102
+ }
103
+ end
104
+ end
105
+
106
+ # Validation service
107
+
108
+ class ValidateParams < Service
109
+ attr_reader :model, :errors
110
+
111
+ def process
112
+ @model = SomeModel.new(**ctx[:params])
113
+
114
+ return if @model.valid?
115
+
116
+ @errors = @model.errors
117
+
118
+ fail_step(semantic: 400)
119
+ end
120
+ end
121
+
122
+ # Permission service
123
+
124
+ class CheckPermissions < Service
125
+ attr_reader :errors
126
+
127
+ def process
128
+ return if ctx[:model].name != 'no_permissions'
129
+
130
+ @errors = ['You do not have a permissions']
131
+
132
+ fail_step(semantic: 403)
133
+ end
134
+ end
135
+
136
+ class Create < Service
137
+ CONFLICT = 409
138
+ SERVER_ERROR = 'server_error'
139
+
140
+ attr_reader :errors
141
+
142
+ def process
143
+ if ctx[:model].name == 'conflict'
144
+ fail_step(semantic: CONFLICT, message: 'Conflict during creation, try again later')
145
+ return
146
+ end
147
+
148
+ if ctx[:model].name == 'server_error'
149
+ fail_step(semantic: SERVER_ERROR)
150
+ return
151
+ end
152
+
153
+ ctx[:model].save
154
+ end
155
+ end
156
+
157
+ class SomeModelsController
158
+ # This is a pseudo controller
159
+
160
+ def initialize(create_params:)
161
+ @create_params = create_params
162
+ end
163
+
164
+ def create
165
+ validator = ValidateParams.call(params: @create_params)
166
+
167
+ validator.failure? do |result|
168
+ return common_client_error_handler(result)
169
+ end
170
+
171
+ permission = CheckPermissions.call(model: validator.model)
172
+
173
+ permission.failure? do |result|
174
+ return common_client_error_handler(result)
175
+ end
176
+
177
+ creator = Create.call(model: validator.model)
178
+
179
+ creator.failure? do |result|
180
+ case result.semantic
181
+ when Create::CONFLICT
182
+ return common_client_error_handler(result)
183
+ when Create::SERVER_ERROR
184
+ return common_server_error_handler(result)
185
+ end
186
+ end
187
+
188
+ { data: creator.ctx[:model].as_json, status: 201 }
189
+ end
190
+
191
+ private
192
+
193
+ def common_client_error_handler(result)
194
+ {
195
+ errors: result.errors || [result.message],
196
+ status: result.semantic
197
+ }
198
+ end
199
+
200
+ def common_server_error_handler(result)
201
+ puts ' ---> Report to error tracker'
202
+
203
+ {
204
+ errors: ['Oops, something went wrong'],
205
+ status: 500
206
+ }
207
+ end
208
+ end
209
+
210
+ def assert_equal(actual, expected, message)
211
+ if actual == expected
212
+ p "PASSED: #{message}"
213
+ else
214
+ p "FAILED: #{message}"
215
+ end
216
+ end
217
+
218
+ assert_equal(
219
+ SomeModelsController.new(
220
+ create_params: {
221
+ name: 'invalid_name',
222
+ description: 'Description'
223
+ }
224
+ ).create,
225
+ {
226
+ errors: ['Name is invalid'],
227
+ status: 400
228
+ },
229
+ 'When invalid'
230
+ )
231
+
232
+ assert_equal(
233
+ SomeModelsController.new(
234
+ create_params: {
235
+ name: 'no_permissions',
236
+ description: 'Description'
237
+ }
238
+ ).create,
239
+ {
240
+ errors: ['You do not have a permissions'],
241
+ status: 403
242
+ },
243
+ 'When forbidden'
244
+ )
245
+
246
+ assert_equal(
247
+ SomeModelsController.new(
248
+ create_params: {
249
+ name: 'conflict',
250
+ description: 'Description'
251
+ }
252
+ ).create,
253
+ {
254
+ errors: ['Conflict during creation, try again later'],
255
+ status: 409
256
+ },
257
+ 'When conflict'
258
+ )
259
+
260
+ assert_equal(
261
+ SomeModelsController.new(
262
+ create_params: {
263
+ name: 'server_error',
264
+ description: 'Description'
265
+ }
266
+ ).create,
267
+ {
268
+ errors: ['Oops, something went wrong'],
269
+ status: 500
270
+ },
271
+ 'When server error'
272
+ )
273
+
274
+ assert_equal(
275
+ SomeModelsController.new(
276
+ create_params: {
277
+ name: 'success',
278
+ description: 'Description'
279
+ }
280
+ ).create,
281
+ {
282
+ data: {
283
+ id: 1,
284
+ name: 'success',
285
+ description: 'Description'
286
+ },
287
+ status: 201
288
+ },
289
+ 'When success'
290
+ )
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleLogicStep
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'simple_logic_step/version'
4
+
5
+ module SimpleLogicStep
6
+ # Copy from here
7
+ class Service
8
+ attr_reader :ctx, :semantic, :message
9
+
10
+ def self.call(**ctx)
11
+ new(**ctx).call
12
+ end
13
+
14
+ def initialize(**ctx)
15
+ @success = true
16
+ @semantic = nil
17
+ @ctx = ctx
18
+ @message = nil
19
+ end
20
+
21
+ def call
22
+ prepare
23
+ process
24
+ self
25
+ end
26
+
27
+ # Implement business logic here
28
+ def process
29
+ raise 'Not implemented'
30
+ end
31
+
32
+ def failure?
33
+ if block_given?
34
+ yield(self) unless success
35
+ else
36
+ !success
37
+ end
38
+ end
39
+
40
+ def success?
41
+ if block_given?
42
+ yield(self) if success
43
+ else
44
+ success
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :success
51
+
52
+ # Redefine this method in case
53
+ # if you need to do some preparations
54
+ # before #process call
55
+ def prepare; end
56
+
57
+ def fail_step(semantic: nil, message: nil)
58
+ @success = false
59
+ add_semantic(semantic)
60
+ @message = message
61
+ end
62
+
63
+ def pass_step(semantic: nil, message: nil)
64
+ @success = true
65
+ add_semantic(semantic)
66
+ @message = message
67
+ end
68
+
69
+ def add_semantic(semantic)
70
+ @semantic = semantic
71
+ end
72
+ end
73
+ # to here
74
+ end
@@ -0,0 +1,4 @@
1
+ module SimpleLogicStep
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/simple_logic_step/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'simple_logic_step'
7
+ spec.version = SimpleLogicStep::VERSION
8
+ spec.authors = ['Al Bal']
9
+ spec.email = ['differencialx@gmail.com']
10
+
11
+ spec.summary = 'Simple service object implementation'
12
+ spec.description = <<~HEREDOC
13
+ I believe that some of developers faced a situation when you can't convince your customer | project manager | team lead | teammates to use any of existing business logic handler, as they think it:
14
+ - has no value for business
15
+ - is hard to integrate
16
+ - needs to be learned be developers
17
+ - is no guarantee that this gem will be well maintained in the future
18
+ - is developed by no name author
19
+
20
+ But you still want to make your controllers and models as thin as possible.
21
+ If such situation is familiar for you then this gem is for you.
22
+ This is a one file gem, just copy `Service` class from `lib/simple_logic_step.rb` to your project and specs for it from `spec/simple_logic_step/logic_step_spec.rb`.
23
+ HEREDOC
24
+ spec.homepage = 'https://github.com/differencialx/simple_logic_step'
25
+ spec.license = 'MIT'
26
+ spec.required_ruby_version = '>= 2.6.0'
27
+
28
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
29
+ spec.metadata['rubygems_mfa_required'] = 'true'
30
+ spec.metadata['homepage_uri'] = spec.homepage
31
+ spec.metadata['source_code_uri'] = 'https://github.com/differencialx/simple_logic_step'
32
+ spec.metadata['changelog_uri'] = 'https://github.com/differencialx/simple_logic_step/blob/main/CHANGELOG.md'
33
+
34
+ spec.files = Dir.chdir(__dir__) do
35
+ `git ls-files -z`.split("\x0").reject do |f|
36
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
37
+ end
38
+ end
39
+ spec.bindir = 'exe'
40
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
41
+ spec.require_paths = ['lib']
42
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: simple_logic_step
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Al Bal
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-03-20 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ I believe that some of developers faced a situation when you can't convince your customer | project manager | team lead | teammates to use any of existing business logic handler, as they think it:
15
+ - has no value for business
16
+ - is hard to integrate
17
+ - needs to be learned be developers
18
+ - is no guarantee that this gem will be well maintained in the future
19
+ - is developed by no name author
20
+
21
+ But you still want to make your controllers and models as thin as possible.
22
+ If such situation is familiar for you then this gem is for you.
23
+ This is a one file gem, just copy `Service` class from `lib/simple_logic_step.rb` to your project and specs for it from `spec/simple_logic_step/logic_step_spec.rb`.
24
+ email:
25
+ - differencialx@gmail.com
26
+ executables: []
27
+ extensions: []
28
+ extra_rdoc_files: []
29
+ files:
30
+ - ".rspec"
31
+ - ".rubocop.yml"
32
+ - CHANGELOG.md
33
+ - CODE_OF_CONDUCT.md
34
+ - Gemfile
35
+ - LICENSE.txt
36
+ - README.md
37
+ - Rakefile
38
+ - examples/controller.rb
39
+ - lib/simple_logic_step.rb
40
+ - lib/simple_logic_step/version.rb
41
+ - sig/simple_logic_step.rbs
42
+ - simple_logic_step.gemspec
43
+ homepage: https://github.com/differencialx/simple_logic_step
44
+ licenses:
45
+ - MIT
46
+ metadata:
47
+ allowed_push_host: https://rubygems.org/
48
+ rubygems_mfa_required: 'true'
49
+ homepage_uri: https://github.com/differencialx/simple_logic_step
50
+ source_code_uri: https://github.com/differencialx/simple_logic_step
51
+ changelog_uri: https://github.com/differencialx/simple_logic_step/blob/main/CHANGELOG.md
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 2.6.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 3.3.7
68
+ signing_key:
69
+ specification_version: 4
70
+ summary: Simple service object implementation
71
+ test_files: []