clear_logic 0.1.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.
@@ -0,0 +1,8 @@
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
+ task default: :spec
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
Binary file
@@ -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
@@ -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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClearLogic
4
+ class CatchedError
5
+ attr_reader :error
6
+
7
+ def initialize(error)
8
+ @error = error
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClearLogic
4
+ class FailureError
5
+ attr_reader :status
6
+
7
+ def initialize(status)
8
+ @status = status
9
+ end
10
+ end
11
+ 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)