servactory 1.1.0

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +116 -0
  3. data/Rakefile +12 -0
  4. data/lib/servactory/base.rb +11 -0
  5. data/lib/servactory/configuration.rb +18 -0
  6. data/lib/servactory/context/configuration.rb +23 -0
  7. data/lib/servactory/context/dsl.rb +58 -0
  8. data/lib/servactory/context/store.rb +51 -0
  9. data/lib/servactory/errors/base.rb +7 -0
  10. data/lib/servactory/errors/failure.rb +7 -0
  11. data/lib/servactory/errors/input_argument_error.rb +7 -0
  12. data/lib/servactory/errors/internal_argument_error.rb +7 -0
  13. data/lib/servactory/errors/output_argument_error.rb +7 -0
  14. data/lib/servactory/input_arguments/checks/base.rb +23 -0
  15. data/lib/servactory/input_arguments/checks/inclusion.rb +45 -0
  16. data/lib/servactory/input_arguments/checks/must.rb +75 -0
  17. data/lib/servactory/input_arguments/checks/required.rb +54 -0
  18. data/lib/servactory/input_arguments/checks/type.rb +84 -0
  19. data/lib/servactory/input_arguments/collection.rb +19 -0
  20. data/lib/servactory/input_arguments/dsl.rb +27 -0
  21. data/lib/servactory/input_arguments/input_argument.rb +79 -0
  22. data/lib/servactory/input_arguments/tools/check.rb +74 -0
  23. data/lib/servactory/input_arguments/tools/find_unnecessary.rb +32 -0
  24. data/lib/servactory/input_arguments/tools/prepare.rb +89 -0
  25. data/lib/servactory/input_arguments/tools/rules.rb +38 -0
  26. data/lib/servactory/input_arguments/workbench.rb +40 -0
  27. data/lib/servactory/inputs.rb +7 -0
  28. data/lib/servactory/internal_arguments/checks/base.rb +17 -0
  29. data/lib/servactory/internal_arguments/checks/type.rb +55 -0
  30. data/lib/servactory/internal_arguments/collection.rb +19 -0
  31. data/lib/servactory/internal_arguments/dsl.rb +27 -0
  32. data/lib/servactory/internal_arguments/internal_argument.rb +33 -0
  33. data/lib/servactory/internal_arguments/tools/prepare.rb +58 -0
  34. data/lib/servactory/internal_arguments/workbench.rb +27 -0
  35. data/lib/servactory/output_arguments/checks/base.rb +17 -0
  36. data/lib/servactory/output_arguments/checks/type.rb +55 -0
  37. data/lib/servactory/output_arguments/collection.rb +19 -0
  38. data/lib/servactory/output_arguments/dsl.rb +27 -0
  39. data/lib/servactory/output_arguments/output_argument.rb +40 -0
  40. data/lib/servactory/output_arguments/tools/conflicts.rb +34 -0
  41. data/lib/servactory/output_arguments/tools/prepare.rb +58 -0
  42. data/lib/servactory/output_arguments/workbench.rb +31 -0
  43. data/lib/servactory/result.rb +21 -0
  44. data/lib/servactory/stage/dsl.rb +29 -0
  45. data/lib/servactory/stage/factory.rb +15 -0
  46. data/lib/servactory/stage/handyman.rb +35 -0
  47. data/lib/servactory/stage/method.rb +21 -0
  48. data/lib/servactory/stage/methods.rb +15 -0
  49. data/lib/servactory/utils.rb +11 -0
  50. data/lib/servactory/version.rb +11 -0
  51. data/lib/servactory.rb +34 -0
  52. metadata +237 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6ba22a3302a9dde10fcadfc2cc5777e83589e86862b8e96484ddd3ad62e78782
4
+ data.tar.gz: 4fa891477fb78494b3df34bb50f3b5bea018d331471662e235d91c49d82b9e6c
5
+ SHA512:
6
+ metadata.gz: c4c5e7cbf5c7708cc468f25152086d974c1ecfee76222bade55215c4f14ee9b0b20194755338152ac21e72845e8c1358f4e185b08b6db08319c48990b5cf43ef
7
+ data.tar.gz: 70c1325c6b7de415565d41f4471798d1349c0627eeca7a329b876d703500b338acb725a5bd5adc41578656488541a13690358d377799c493f8e8ba719bb1c710
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Servactory
2
+
3
+ A set of tools for building reliable services of any complexity.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby >= 2.7
8
+
9
+ ## Getting started
10
+
11
+ ### Conventions
12
+
13
+ - Services are subclasses of `Servactory::Base` and are located in the `app/services` directory. It is common practice to create and inherit from `ApplicationService::Base`, which is a subclass of `Servactory::Base`.
14
+ - Name services by what they do, not by what they accept. Try to use verbs in names. For example, `UsersService::Create` instead of `UsersService::Creation`.
15
+
16
+ ### Installation
17
+
18
+ Add this to `Gemfile`:
19
+
20
+ ```ruby
21
+ gem "servactory"
22
+ ```
23
+
24
+ And execute:
25
+
26
+ ```shell
27
+ bundle install
28
+ ```
29
+
30
+ ### Preparation
31
+
32
+ We recommend that you first prepare the following files in your project.
33
+
34
+ #### ApplicationService::Errors
35
+
36
+ ```ruby
37
+ # app/services/application_service/errors.rb
38
+
39
+ module ApplicationService
40
+ module Errors
41
+ class InputArgumentError < Servactory::Errors::InputArgumentError; end
42
+ class OutputArgumentError < Servactory::Errors::OutputArgumentError; end
43
+ class InternalArgumentError < Servactory::Errors::InternalArgumentError; end
44
+
45
+ class Failure < Servactory::Errors::Failure; end
46
+ end
47
+ end
48
+ ```
49
+
50
+ #### ApplicationService::Base
51
+
52
+ ```ruby
53
+ # app/services/application_service/base.rb
54
+
55
+ module ApplicationService
56
+ class Base < Servactory::Base
57
+ configuration do
58
+ input_argument_error_class ApplicationService::Errors::InputArgumentError
59
+ output_argument_error_class ApplicationService::Errors::OutputArgumentError
60
+ internal_argument_error_class ApplicationService::Errors::InternalArgumentError
61
+
62
+ failure_class ApplicationService::Errors::Failure
63
+ end
64
+ end
65
+ end
66
+ ```
67
+
68
+ ## Usage
69
+
70
+ ### Minimal example
71
+
72
+ ```ruby
73
+ class SendService < ApplicationService::Base
74
+ stage { make :something }
75
+
76
+ private
77
+
78
+ def something
79
+ # ...
80
+ end
81
+ end
82
+ ```
83
+
84
+ ### Inputs
85
+
86
+ ```ruby
87
+ class SendService < ApplicationService::Base
88
+ input :user, type: User
89
+
90
+ stage { make :something }
91
+
92
+ private
93
+
94
+ def something
95
+ # ...
96
+ end
97
+ end
98
+ ```
99
+
100
+ ### Outputs
101
+
102
+ ```ruby
103
+ class SendService < ApplicationService::Base
104
+ input :user, type: User
105
+
106
+ output :notification, type: Notification
107
+
108
+ stage { make :something }
109
+
110
+ private
111
+
112
+ def something
113
+ self.notification = Notification.create!
114
+ end
115
+ end
116
+ ```
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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ class Base
5
+ include Context::DSL
6
+ include InputArguments::DSL
7
+ include InternalArguments::DSL
8
+ include OutputArguments::DSL
9
+ include Stage::DSL
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ class Configuration
5
+ attr_accessor :input_argument_error_class,
6
+ :internal_argument_error_class,
7
+ :output_argument_error_class,
8
+ :failure_class
9
+
10
+ def initialize
11
+ @input_argument_error_class = Servactory::Errors::InputArgumentError
12
+ @internal_argument_error_class = Servactory::Errors::InternalArgumentError
13
+ @output_argument_error_class = Servactory::Errors::OutputArgumentError
14
+
15
+ @failure_class = Servactory::Errors::Failure
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Context
5
+ class Configuration
6
+ def input_argument_error_class(input_argument_error_class)
7
+ Servactory.configuration.input_argument_error_class = input_argument_error_class
8
+ end
9
+
10
+ def output_argument_error_class(output_argument_error_class)
11
+ Servactory.configuration.output_argument_error_class = output_argument_error_class
12
+ end
13
+
14
+ def internal_argument_error_class(internal_argument_error_class)
15
+ Servactory.configuration.internal_argument_error_class = internal_argument_error_class
16
+ end
17
+
18
+ def failure_class(failure_class)
19
+ Servactory.configuration.failure_class = failure_class
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Context
5
+ module DSL
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ def call!(arguments) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
12
+ @context_store ||= Store.new(self)
13
+
14
+ assign_data_with(arguments)
15
+
16
+ input_arguments_workbench.find_unnecessary!
17
+ input_arguments_workbench.check_rules!
18
+ output_arguments_workbench.find_conflicts_in!(collection_of_internal_arguments:)
19
+
20
+ prepare_data
21
+
22
+ input_arguments_workbench.check!
23
+
24
+ stage_handyman.run_methods!
25
+
26
+ Servactory::Result.prepare_for(
27
+ context: context_store.context,
28
+ collection_of_output_arguments:
29
+ )
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :context_store
35
+
36
+ def assign_data_with(arguments)
37
+ input_arguments_workbench.assign(context: context_store.context, arguments:) # 1
38
+ internal_arguments_workbench.assign(context: context_store.context) # 2
39
+ output_arguments_workbench.assign(context: context_store.context) # 3
40
+ stage_handyman&.assign(context: context_store.context) # 4
41
+ end
42
+
43
+ def prepare_data
44
+ input_arguments_workbench.prepare # 1
45
+
46
+ output_arguments_workbench.prepare # 2
47
+ internal_arguments_workbench.prepare # 3
48
+ end
49
+
50
+ def configuration(&)
51
+ context_configuration = Servactory::Context::Configuration.new
52
+
53
+ context_configuration.instance_eval(&)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Context
5
+ class Store
6
+ attr_reader :context
7
+
8
+ def initialize(service_class)
9
+ @context = service_class.new
10
+
11
+ service_class.class_eval(service_class_template_with(context))
12
+ end
13
+
14
+ private
15
+
16
+ # EXAMPLE:
17
+ #
18
+ # attr_reader(:inputs)
19
+ #
20
+ # def assign_inputs(inputs)
21
+ # @inputs = inputs
22
+ # end
23
+ #
24
+ # def fail_input!(input_attribute_name, message, prefix: true)
25
+ # message_text = prefix ? "[#{context.class.name}] Custom `\#{input_attribute_name}` input error: " : ""
26
+ #
27
+ # message_text += message
28
+ #
29
+ # raise Servactory.configuration.input_argument_error_class, message_text
30
+ # end
31
+ #
32
+ def service_class_template_with(context)
33
+ <<-RUBY
34
+ attr_reader(:inputs)
35
+
36
+ def assign_inputs(inputs)
37
+ @inputs = inputs
38
+ end
39
+
40
+ def fail_input!(input_attribute_name, message, prefix: true)
41
+ message_text = prefix ? "[#{context.class.name}] Custom `\#{input_attribute_name}` input error: " : ""
42
+
43
+ message_text += message
44
+
45
+ raise Servactory.configuration.input_argument_error_class, message_text
46
+ end
47
+ RUBY
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Errors
5
+ class Base < StandardError; end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Errors
5
+ class Failure < Servactory::Errors::Base; end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Errors
5
+ class InputArgumentError < Servactory::Errors::Base; end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Errors
5
+ class InternalArgumentError < Servactory::Errors::Base; end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module Errors
5
+ class OutputArgumentError < Servactory::Errors::Base; end
6
+ end
7
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ module Checks
6
+ class Base
7
+ def initialize
8
+ @errors = []
9
+ end
10
+
11
+ attr_reader :errors
12
+
13
+ protected
14
+
15
+ def add_error(message, **arguments)
16
+ message = message.call(**arguments) if message.is_a?(Proc)
17
+
18
+ errors.push(message)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ module Checks
6
+ class Inclusion < Base
7
+ DEFAULT_MESSAGE = lambda do |service_class_name:, input:|
8
+ "[#{service_class_name}] Wrong value in `#{input.name}`, must be one of `#{input.inclusion}`"
9
+ end
10
+
11
+ private_constant :DEFAULT_MESSAGE
12
+
13
+ def self.check(context:, input:, value:, check_key:, **)
14
+ return unless should_be_checked_for?(input, check_key)
15
+
16
+ new(context:, input:, value:).check
17
+ end
18
+
19
+ def self.should_be_checked_for?(input, check_key)
20
+ check_key == :inclusion && input.inclusion_present?
21
+ end
22
+
23
+ ##########################################################################
24
+
25
+ def initialize(context:, input:, value:)
26
+ super()
27
+
28
+ @context = context
29
+ @input = input
30
+ @value = value
31
+ end
32
+
33
+ def check
34
+ return if @input.inclusion.include?(@value)
35
+
36
+ add_error(
37
+ DEFAULT_MESSAGE,
38
+ service_class_name: @context.class.name,
39
+ input: @input
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ module Checks
6
+ class Must < Base
7
+ DEFAULT_MESSAGE = lambda do |service_class_name:, input:, code:|
8
+ "[#{service_class_name}] Input `#{input.name}` " \
9
+ "must \"#{code.to_s.humanize(capitalize: false, keep_id_suffix: true)}\""
10
+ end
11
+
12
+ private_constant :DEFAULT_MESSAGE
13
+
14
+ def self.check(context:, input:, value:, check_key:, check_options:)
15
+ return unless should_be_checked_for?(input, check_key)
16
+
17
+ new(context:, input:, value:, check_options:).check
18
+ end
19
+
20
+ def self.should_be_checked_for?(input, check_key)
21
+ check_key == :must && input.must_present?
22
+ end
23
+
24
+ ##########################################################################
25
+
26
+ def initialize(context:, input:, value:, check_options:)
27
+ super()
28
+
29
+ @context = context
30
+ @input = input
31
+ @value = value
32
+ @check_options = check_options
33
+ end
34
+
35
+ def check
36
+ @check_options.each do |code, options|
37
+ message = call_or_fetch_message_from(code, options)
38
+
39
+ next if message.blank?
40
+
41
+ add_error(
42
+ DEFAULT_MESSAGE,
43
+ service_class_name: @context.class.name,
44
+ input: @input,
45
+ code:
46
+ )
47
+ end
48
+
49
+ errors
50
+ end
51
+
52
+ private
53
+
54
+ def call_or_fetch_message_from(code, options) # rubocop:disable Metrics/MethodLength
55
+ check, message = options.values_at(:is, :message)
56
+
57
+ return if check.call(value: @value)
58
+
59
+ message.presence || DEFAULT_MESSAGE
60
+ rescue StandardError => e
61
+ message_text =
62
+ "[#{@context.class.name}] Syntax error inside `#{code}` of " \
63
+ "`#{@input.name}` input: [#{e.class}] #{e.message}"
64
+
65
+ add_error(
66
+ message_text,
67
+ service_class_name: @context.class.name,
68
+ input: @input,
69
+ code:
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ module Checks
6
+ class Required < Base
7
+ DEFAULT_MESSAGE = lambda do |service_class_name:, input:, value:|
8
+ if input.array? && value.present?
9
+ "[#{service_class_name}] Required element in input array `#{input.name}` is missing"
10
+ else
11
+ "[#{service_class_name}] Required input `#{input.name}` is missing"
12
+ end
13
+ end
14
+
15
+ private_constant :DEFAULT_MESSAGE
16
+
17
+ def self.check(context:, input:, value:, **)
18
+ return unless should_be_checked_for?(input)
19
+
20
+ new(context:, input:, value:).check
21
+ end
22
+
23
+ def self.should_be_checked_for?(input)
24
+ input.required?
25
+ end
26
+
27
+ ##########################################################################
28
+
29
+ def initialize(context:, input:, value:)
30
+ super()
31
+
32
+ @context = context
33
+ @input = input
34
+ @value = value
35
+ end
36
+
37
+ def check # rubocop:disable Metrics/MethodLength
38
+ if @input.array? && @value.present?
39
+ return if @value.respond_to?(:all?) && @value.all?(&:present?)
40
+ elsif @value.present?
41
+ return
42
+ end
43
+
44
+ add_error(
45
+ DEFAULT_MESSAGE,
46
+ service_class_name: @context.class.name,
47
+ input: @input,
48
+ value: @value
49
+ )
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ module Checks
6
+ class Type < Base
7
+ DEFAULT_MESSAGE = lambda do |service_class_name:, input:, expected_type:, given_type:|
8
+ if input.array?
9
+ "[#{service_class_name}] Wrong type in input array `#{input.name}`, expected `#{expected_type}`"
10
+ else
11
+ "[#{service_class_name}] Wrong type of input `#{input.name}`, " \
12
+ "expected `#{expected_type}`, " \
13
+ "got `#{given_type}`"
14
+ end
15
+ end
16
+
17
+ private_constant :DEFAULT_MESSAGE
18
+
19
+ def self.check(context:, input:, value:, check_key:, check_options:)
20
+ return unless should_be_checked_for?(input, value, check_key)
21
+
22
+ new(context:, input:, value:, types: check_options).check
23
+ end
24
+
25
+ def self.should_be_checked_for?(input, value, check_key)
26
+ check_key == :types && (
27
+ input.required? || (
28
+ input.optional? && !input.default.nil?
29
+ ) || (
30
+ input.optional? && !value.nil?
31
+ )
32
+ )
33
+ end
34
+
35
+ ##########################################################################
36
+
37
+ def initialize(context:, input:, value:, types:)
38
+ super()
39
+
40
+ @context = context
41
+ @input = input
42
+ @value = value
43
+ @types = types
44
+ end
45
+
46
+ def check # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
47
+ return if prepared_types.any? do |type|
48
+ if @input.array?
49
+ prepared_value.is_a?(::Array) &&
50
+ prepared_value.respond_to?(:all?) && prepared_value.all?(type)
51
+ else
52
+ prepared_value.is_a?(type)
53
+ end
54
+ end
55
+
56
+ add_error(
57
+ DEFAULT_MESSAGE,
58
+ service_class_name: @context.class.name,
59
+ input: @input,
60
+ expected_type: prepared_types.join(", "),
61
+ given_type: prepared_value.class.name
62
+ )
63
+ end
64
+
65
+ ########################################################################
66
+
67
+ def prepared_types
68
+ @prepared_types ||=
69
+ @types.map do |type|
70
+ if type.is_a?(String)
71
+ Object.const_get(type)
72
+ else
73
+ type
74
+ end
75
+ end
76
+ end
77
+
78
+ def prepared_value
79
+ @prepared_value ||= @input.optional? && !@input.default.nil? ? @input.default : @value
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ class Collection
6
+ # NOTE: http://words.steveklabnik.com/beware-subclassing-ruby-core-classes
7
+ extend Forwardable
8
+ def_delegators :@collection, :<<, :each, :map
9
+
10
+ def initialize(*)
11
+ @collection = []
12
+ end
13
+
14
+ def names
15
+ map(&:name)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ module DSL
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ private
12
+
13
+ def input(name, **options)
14
+ collection_of_input_arguments << InputArgument.new(name, **options)
15
+ end
16
+
17
+ def collection_of_input_arguments
18
+ @collection_of_input_arguments ||= Collection.new
19
+ end
20
+
21
+ def input_arguments_workbench
22
+ @input_arguments_workbench ||= Workbench.work_with(collection_of_input_arguments)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end