servactory 1.1.0

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