clear_logic 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +86 -0
- data/LICENSE.txt +21 -0
- data/README.md +482 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/clear_logic-0.1.0.gem +0 -0
- data/clear_logic.gemspec +48 -0
- data/lib/clear_logic.rb +26 -0
- data/lib/clear_logic/context/builder.rb +53 -0
- data/lib/clear_logic/errors/catched_error.rb +11 -0
- data/lib/clear_logic/errors/failure_error.rb +11 -0
- data/lib/clear_logic/logger/adapter.rb +36 -0
- data/lib/clear_logic/logger/default.rb +24 -0
- data/lib/clear_logic/matcher.rb +53 -0
- data/lib/clear_logic/result.rb +54 -0
- data/lib/clear_logic/service.rb +55 -0
- data/lib/clear_logic/step_adapters/stride.rb +58 -0
- data/lib/clear_logic/types.rb +28 -0
- data/lib/clear_logic/version.rb +5 -0
- metadata +223 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'clear_logic'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
Binary file
|
data/clear_logic.gemspec
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'clear_logic/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'clear_logic'
|
9
|
+
spec.version = ClearLogic::VERSION
|
10
|
+
spec.authors = ['bezrukavyi']
|
11
|
+
spec.email = ['yaroslav.bezrukavyi@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'Clear result'
|
14
|
+
spec.description = 'Clear result'
|
15
|
+
spec.homepage = 'https://github.com/bezrukavyi/clear_logic'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
19
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
20
|
+
if spec.respond_to?(:metadata)
|
21
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
22
|
+
else
|
23
|
+
raise 'RubyGems 2.0 or newer is required to protect against ' \
|
24
|
+
'public gem pushes.'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Specify which files should be added to the gem when it is released.
|
28
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
29
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
30
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
31
|
+
end
|
32
|
+
spec.bindir = 'exe'
|
33
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
34
|
+
spec.require_paths = ['lib']
|
35
|
+
|
36
|
+
spec.add_dependency 'dry-matcher', '>= 0.7'
|
37
|
+
spec.add_dependency 'dry-monads', '>= 0.3.1'
|
38
|
+
spec.add_dependency 'dry-transaction', '>= 0.12.0'
|
39
|
+
spec.add_dependency 'dry-initializer', '>= 2.5.0'
|
40
|
+
spec.add_dependency 'dry-types', '>= 1.0.0'
|
41
|
+
spec.add_dependency 'dry-inflector'
|
42
|
+
|
43
|
+
spec.add_development_dependency 'bundler', '~> 1.17'
|
44
|
+
spec.add_development_dependency 'pry'
|
45
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
46
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
47
|
+
spec.add_development_dependency 'ffaker'
|
48
|
+
end
|
data/lib/clear_logic.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
require 'pry'
|
6
|
+
|
7
|
+
require 'dry-types'
|
8
|
+
require 'dry-initializer'
|
9
|
+
require 'dry-transaction'
|
10
|
+
require 'dry-matcher'
|
11
|
+
require 'dry-monads'
|
12
|
+
|
13
|
+
require 'clear_logic/logger/default'
|
14
|
+
require 'clear_logic/logger/adapter'
|
15
|
+
require 'clear_logic/errors/failure_error'
|
16
|
+
require 'clear_logic/errors/catched_error'
|
17
|
+
require 'clear_logic/version'
|
18
|
+
require 'clear_logic/types'
|
19
|
+
require 'clear_logic/result'
|
20
|
+
require 'clear_logic/step_adapters/stride'
|
21
|
+
require 'clear_logic/matcher'
|
22
|
+
require 'clear_logic/context/builder'
|
23
|
+
require 'clear_logic/service'
|
24
|
+
|
25
|
+
module ClearLogic
|
26
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClearLogic
|
4
|
+
class ContextBuilder
|
5
|
+
def self.call
|
6
|
+
Class.new do
|
7
|
+
extend ::Dry::Initializer
|
8
|
+
|
9
|
+
attr_reader :args
|
10
|
+
attr_accessor :catched_error, :failure_error, :service, :exit_success, :step
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
@args = args
|
14
|
+
super(*args)
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
@additional_opts ||= {}
|
19
|
+
@additional_opts[key]
|
20
|
+
end
|
21
|
+
|
22
|
+
def []=(key, value)
|
23
|
+
@additional_opts ||= {}
|
24
|
+
@additional_opts[key] = value
|
25
|
+
end
|
26
|
+
|
27
|
+
def exit_success?
|
28
|
+
exit_success == true
|
29
|
+
end
|
30
|
+
|
31
|
+
def catched_error?
|
32
|
+
!catched_error.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
def failure_error?
|
36
|
+
!failure_error.nil?
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_h
|
40
|
+
{
|
41
|
+
catched_error: catched_error,
|
42
|
+
failure_error: failure_error,
|
43
|
+
service: service.class,
|
44
|
+
exit_success: exit_success,
|
45
|
+
step: step,
|
46
|
+
options: @additional_opts,
|
47
|
+
args: args
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClearLogic
|
4
|
+
module Logger
|
5
|
+
class Adapter
|
6
|
+
attr_reader :service_class, :logger_class, :log_path
|
7
|
+
|
8
|
+
def initialize(service_class)
|
9
|
+
@service_class = service_class
|
10
|
+
@logger_class = service_class.logger_class
|
11
|
+
@log_path = service_class.logger_options[:log_path] || default_log_path
|
12
|
+
end
|
13
|
+
|
14
|
+
def logger
|
15
|
+
@logger ||= create_logger
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def create_logger
|
21
|
+
system('mkdir', '-p', path) unless Dir.exist?(path)
|
22
|
+
|
23
|
+
logger_class.new(log_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def path
|
27
|
+
File.dirname(log_path)
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_log_path
|
31
|
+
file_name = Dry::Inflector.new.underscore(service_class.name.gsub('::', '/'))
|
32
|
+
File.join(ENV['BUNDLE_GEMFILE'], "log/#{file_name}.log").gsub!('Gemfile/', '')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ClearLogic
|
2
|
+
module Logger
|
3
|
+
class Default < ::Logger
|
4
|
+
DATE_FORMAT = '%y-%m-%d %H:%M:%S.%3N '.freeze
|
5
|
+
FORMAT = "[%s#%d#%d] %5s -- %s: %s\n".freeze
|
6
|
+
|
7
|
+
def format_message(severity, time, progname, context)
|
8
|
+
thread_id = Thread.current.object_id % 100_000
|
9
|
+
|
10
|
+
format(FORMAT, format_datetime(time), Process.pid, thread_id, severity, progname, pretty_view(context))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def format_datetime(time)
|
16
|
+
time.strftime(DATE_FORMAT)
|
17
|
+
end
|
18
|
+
|
19
|
+
def pretty_view(context)
|
20
|
+
JSON.pretty_generate(context.to_h)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClearLogic
|
4
|
+
class Matcher
|
5
|
+
CASES = %i[success failure].freeze
|
6
|
+
|
7
|
+
def self.call(*args)
|
8
|
+
new.matcher.call(*args) { |on| yield(on) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def matcher
|
12
|
+
dry_cases = CASES.each_with_object({}) do |one_case, case_list|
|
13
|
+
case_list[one_case] = send("#{one_case}_case")
|
14
|
+
end
|
15
|
+
|
16
|
+
Dry::Matcher.new(dry_cases)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def success_case
|
22
|
+
Dry::Matcher::Case.new(
|
23
|
+
match: ->(result) { result.success? },
|
24
|
+
resolve: ->(result) { result }
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def failure_case
|
29
|
+
Dry::Matcher::Case.new(
|
30
|
+
match: ->(result, *patterns) { patterns.any? ? case_patterns(result, patterns) : result.failure? },
|
31
|
+
resolve: ->(result) { result }
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def case_patterns(result, patterns)
|
36
|
+
patterns.any? do |pattern|
|
37
|
+
return false unless respond_to?("#{pattern}_pattern", private: true)
|
38
|
+
|
39
|
+
send("#{pattern}_pattern", result)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
ClearLogic::Result::DEFAULT_ERRORS.each do |error_type|
|
44
|
+
define_method "#{error_type}_pattern" do |result|
|
45
|
+
return false unless result.failure?
|
46
|
+
|
47
|
+
result.context.respond_to?(:failure_error) &&
|
48
|
+
!result.context.failure_error.nil? &&
|
49
|
+
result.context.failure_error.status == error_type
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
Dry::Monads::Result::Success.class_eval do
|
3
|
+
alias context success
|
4
|
+
end
|
5
|
+
|
6
|
+
Dry::Monads::Result::Failure.class_eval do
|
7
|
+
alias context failure
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClearLogic
|
11
|
+
module Result
|
12
|
+
DEFAULT_ERRORS = %i[
|
13
|
+
unauthorized
|
14
|
+
forbidden
|
15
|
+
not_found
|
16
|
+
invalid
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def self.included(base)
|
22
|
+
base.extend(ClassMethdos)
|
23
|
+
|
24
|
+
base.errors(*DEFAULT_ERRORS)
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethdos
|
28
|
+
def errors(*errors_methods)
|
29
|
+
errors_methods.each do |error_type|
|
30
|
+
define_method(error_type) do |context|
|
31
|
+
context.failure_error ||= ClearLogic::FailureError.new(error_type)
|
32
|
+
|
33
|
+
failure(context)
|
34
|
+
end
|
35
|
+
|
36
|
+
private error_type
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def success(context)
|
42
|
+
Dry::Monads::Result::Success.new(context)
|
43
|
+
end
|
44
|
+
|
45
|
+
def exit_success(context)
|
46
|
+
context.exit_success = true
|
47
|
+
success(context)
|
48
|
+
end
|
49
|
+
|
50
|
+
def failure(context)
|
51
|
+
Dry::Monads::Result::Failure.new(context)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClearLogic
|
4
|
+
class Service
|
5
|
+
include Dry::Transaction
|
6
|
+
include ClearLogic::Result
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_accessor :context_class, :logger_instance, :logger_options,
|
10
|
+
:logger_class
|
11
|
+
|
12
|
+
def call(*args)
|
13
|
+
new.call(args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_context
|
17
|
+
self.context_class = ClearLogic::ContextBuilder.call
|
18
|
+
end
|
19
|
+
|
20
|
+
def context(name, type = nil, **options)
|
21
|
+
context_class.class_eval do
|
22
|
+
method = options.delete(:as) || :option
|
23
|
+
send(method, name, type, **options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def logger(logger_class, log_all: false, log_path: nil)
|
28
|
+
self.logger_options = { log_all: log_all, log_path: log_path }
|
29
|
+
self.logger_class = logger_class
|
30
|
+
self.logger_instance = ClearLogic::Logger::Adapter.new(self).logger
|
31
|
+
end
|
32
|
+
|
33
|
+
def inherited(base)
|
34
|
+
base.class_eval do
|
35
|
+
attr_reader :context
|
36
|
+
|
37
|
+
build_context
|
38
|
+
|
39
|
+
logger ClearLogic::Logger::Default
|
40
|
+
|
41
|
+
step :initialize_context
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def initialize_context(args)
|
46
|
+
@context = self.class.context_class.new(*args.flatten)
|
47
|
+
context.service = self
|
48
|
+
|
49
|
+
success(context)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/transaction/errors'
|
4
|
+
|
5
|
+
module ClearLogic
|
6
|
+
module StepAdapters
|
7
|
+
class Stride
|
8
|
+
include Dry::Monads::Result::Mixin
|
9
|
+
include ClearLogic::Result
|
10
|
+
|
11
|
+
attr_reader :operation, :options, :args, :context
|
12
|
+
|
13
|
+
def call(operation, options, args)
|
14
|
+
@operation = operation
|
15
|
+
@options = options
|
16
|
+
@args = args
|
17
|
+
@context = args.flatten.first
|
18
|
+
|
19
|
+
options[:rescue] ||= {}
|
20
|
+
|
21
|
+
context.step = options[:step_name]
|
22
|
+
|
23
|
+
return success(context) if context.exit_success?
|
24
|
+
|
25
|
+
result = operation.call(context)
|
26
|
+
|
27
|
+
log_result
|
28
|
+
|
29
|
+
return result if result.success?
|
30
|
+
|
31
|
+
failure_method
|
32
|
+
rescue *Array(options[:rescue].keys) => e
|
33
|
+
catch_error(e)
|
34
|
+
end
|
35
|
+
|
36
|
+
def catch_error(error)
|
37
|
+
context.catched_error = error
|
38
|
+
|
39
|
+
log_result
|
40
|
+
|
41
|
+
rescue_method = options[:rescue][error.class]
|
42
|
+
rescue_method ? context.service.send(rescue_method, context) : failure(context)
|
43
|
+
end
|
44
|
+
|
45
|
+
def log_result
|
46
|
+
return unless options[:log] || context.service.class.logger_options[:log_all]
|
47
|
+
|
48
|
+
context.service.class.logger_instance.info(context)
|
49
|
+
end
|
50
|
+
|
51
|
+
def failure_method
|
52
|
+
options[:failure] ? context.service.send(options[:failure], context) : failure(context)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Dry::Transaction::StepAdapters.register(:stride, ClearLogic::StepAdapters::Stride.new)
|