clear_logic 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)