servactory 1.3.0 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9766f87f647b0873a6671d88811141a77b571fb15ac5ee6b10b78e2e5696ca6
4
- data.tar.gz: c36ee73abad2d9241f6b48190cbf3351fe8c9c40bc7cb9bdfdfb09c04e5d05f8
3
+ metadata.gz: d4ffa3b926585163837e461a8a1b129a81f951f88a8d715dd6e74297e0a37ddd
4
+ data.tar.gz: 1d1a3326828e282ad0f46bc76e2754fb44e0dbd5f2e00c452cd6e856866bcbbd
5
5
  SHA512:
6
- metadata.gz: 11b27258c9a302cb989eed03fe4f1325c03855f58fd3a79b6cad7a3846fa8e9395f780fe0d9a2dd30c9b044d4e98df4677857c93c275f9bfe8780d6ebefe5d5e
7
- data.tar.gz: 57800e68b8c8bece089513dcf0b0ff8da6ab2efb33a4d4a694b11995dae51b7dd0496b1b0f4b6c0ba403db5c6037432e43371d68bea5b56bb05844d98fb36e8e
6
+ metadata.gz: 07c4e1f255a4fabe953a3c1381229000b564b80fc0e4f1c08547fed23dcc9cb9b31ebab678d76f894ce55e5506120d412fab26bf214f433f00cf43d958b502d7
7
+ data.tar.gz: 85cadb2169b968a0e9cda6f9279d9be37b01acdc0f7f91ca2b7f8458e89a219213ad317b84002d1b7684bb695a73b4c5a3a6c092a4e3a61c05984e2ee027a164
data/README.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  A set of tools for building reliable services of any complexity.
4
4
 
5
+ [![Gem version](https://img.shields.io/gem/v/servactory?logo=rubygems&logoColor=fff)](https://rubygems.org/gems/servactory)
6
+
7
+ ## Contents
8
+
9
+ - [Requirements](https://github.com/afuno/servactory/edit/main/README.md#requirements)
10
+ - [Getting started](https://github.com/afuno/servactory/edit/main/README.md#getting-started)
11
+ - [Conventions](https://github.com/afuno/servactory/edit/main/README.md#conventions)
12
+ - [Installation](https://github.com/afuno/servactory/edit/main/README.md#installation)
13
+ - [Preparation](https://github.com/afuno/servactory/edit/main/README.md#preparation)
14
+ - [Usage](https://github.com/afuno/servactory/edit/main/README.md#usage)
15
+ - [Minimal example](https://github.com/afuno/servactory#minimal-example)
16
+ - [Input attributes](https://github.com/afuno/servactory#input-attributes)
17
+ - [Isolated usage](https://github.com/afuno/servactory#isolated-usage)
18
+ - [As an internal argument](https://github.com/afuno/servactory#isolated-usage)
19
+ - [Optional inputs](https://github.com/afuno/servactory#optional-inputs)
20
+ - [An array of specific values](https://github.com/afuno/servactory#an-array-of-specific-values)
21
+ - [Inclusion](https://github.com/afuno/servactory#inclusion)
22
+ - [Must](https://github.com/afuno/servactory#must)
23
+ - [Output attributes](https://github.com/afuno/servactory/edit/main/README.md#output-attributes)
24
+ - [Internal attributes](https://github.com/afuno/servactory/edit/main/README.md#internal-attributes)
25
+ - [Result](https://github.com/afuno/servactory/edit/main/README.md#result)
26
+
5
27
  ## Requirements
6
28
 
7
29
  - Ruby >= 2.7
@@ -70,7 +92,7 @@ end
70
92
  ### Minimal example
71
93
 
72
94
  ```ruby
73
- class SendService < ApplicationService::Base
95
+ class MinimalService < ApplicationService::Base
74
96
  stage { make :something }
75
97
 
76
98
  private
@@ -85,10 +107,10 @@ end
85
107
 
86
108
  #### Isolated usage
87
109
 
88
- With this approach, all input attributes are available only from `inputs`.
110
+ With this approach, all input attributes are available only from `inputs`. This is default behaviour.
89
111
 
90
112
  ```ruby
91
- class UserService::Accept < ApplicationService::Base
113
+ class UsersService::Accept < ApplicationService::Base
92
114
  input :user, type: User
93
115
 
94
116
  stage { make :accept! }
@@ -106,7 +128,7 @@ end
106
128
  With this approach, all input attributes are available from `inputs` as well as directly from the context.
107
129
 
108
130
  ```ruby
109
- class UserService::Accept < ApplicationService::Base
131
+ class UsersService::Accept < ApplicationService::Base
110
132
  input :user, type: User, internal: true
111
133
 
112
134
  stage { make :accept! }
@@ -119,6 +141,59 @@ class UserService::Accept < ApplicationService::Base
119
141
  end
120
142
  ```
121
143
 
144
+ #### Optional inputs
145
+
146
+ By default, all inputs are required. To make an input optional, specify `false` in the `required` option.
147
+
148
+ ```ruby
149
+ class UsersService::Create < ApplicationService::Base
150
+ input :first_name, type: String, internal: true
151
+ input :middle_name, type: String, required: false
152
+ input :last_name, type: String, internal: true
153
+
154
+ # ...
155
+ end
156
+ ```
157
+
158
+ #### An array of specific values
159
+
160
+ ```ruby
161
+ class PymentsService::Send < ApplicationService::Base
162
+ input :invoice_numbers, type: String, array: true
163
+
164
+ # ...
165
+ end
166
+ ```
167
+
168
+ #### Inclusion
169
+
170
+ ```ruby
171
+ class EventService::Send < ApplicationService::Base
172
+ input :event_name, type: String, inclusion: %w[created rejected approved]
173
+
174
+ # ...
175
+ end
176
+ ```
177
+
178
+ #### Must
179
+
180
+ Sometimes there are cases that require the implementation of a specific input attribute check. In such cases `must` can help.
181
+
182
+ ```ruby
183
+ class PymentsService::Send < ApplicationService::Base
184
+ input :invoice_numbers,
185
+ type: String,
186
+ array: true,
187
+ must: {
188
+ be_6_characters: {
189
+ is: ->(value:) { value.all? { |id| id.size == 6 } }
190
+ }
191
+ }
192
+
193
+ # ...
194
+ end
195
+ ```
196
+
122
197
  ### Output attributes
123
198
 
124
199
  ```ruby
@@ -35,11 +35,16 @@ module Servactory
35
35
 
36
36
  attr_reader :context_store
37
37
 
38
- def assign_data_with(arguments)
39
- input_arguments_workbench.assign(context: context_store.context, arguments: arguments) # 1
40
- internal_arguments_workbench.assign(context: context_store.context) # 2
41
- output_arguments_workbench.assign(context: context_store.context) # 3
42
- stage_handyman&.assign(context: context_store.context) # 4
38
+ def assign_data_with(arguments) # rubocop:disable Metrics/AbcSize
39
+ input_arguments_workbench.assign(
40
+ context: context_store.context,
41
+ arguments: arguments,
42
+ collection_of_input_options: collection_of_input_options
43
+ )
44
+
45
+ internal_arguments_workbench.assign(context: context_store.context)
46
+ output_arguments_workbench.assign(context: context_store.context)
47
+ stage_handyman&.assign(context: context_store.context)
43
48
  end
44
49
 
45
50
  def prepare_data
@@ -5,7 +5,7 @@ module Servactory
5
5
  module Checks
6
6
  class Inclusion < Base
7
7
  DEFAULT_MESSAGE = lambda do |service_class_name:, input:|
8
- "[#{service_class_name}] Wrong value in `#{input.name}`, must be one of `#{input.inclusion}`"
8
+ "[#{service_class_name}] Wrong value in `#{input.name}`, must be one of `#{input.inclusion[:in]}`"
9
9
  end
10
10
 
11
11
  private_constant :DEFAULT_MESSAGE
@@ -31,7 +31,7 @@ module Servactory
31
31
  end
32
32
 
33
33
  def check
34
- return if @input.inclusion.include?(@value)
34
+ return if @input.inclusion[:in].include?(@value)
35
35
 
36
36
  add_error(
37
37
  DEFAULT_MESSAGE,
@@ -57,10 +57,12 @@ module Servactory
57
57
  return if check.call(value: @value)
58
58
 
59
59
  message.presence || DEFAULT_MESSAGE
60
- rescue StandardError => _e
60
+ rescue StandardError => e
61
61
  message_text =
62
62
  "[#{@context.class.name}] Syntax error inside `#{code}` of `#{@input.name}` input"
63
63
 
64
+ puts "#{message_text}: #{e}"
65
+
64
66
  add_error(
65
67
  message_text,
66
68
  service_class_name: @context.class.name,
@@ -11,13 +11,21 @@ module Servactory
11
11
  private
12
12
 
13
13
  def input(name, **options)
14
- collection_of_input_arguments << InputArgument.new(name, **options)
14
+ collection_of_input_arguments << InputArgument.new(
15
+ name,
16
+ collection_of_options: collection_of_input_options,
17
+ **options
18
+ )
15
19
  end
16
20
 
17
21
  def collection_of_input_arguments
18
22
  @collection_of_input_arguments ||= Collection.new
19
23
  end
20
24
 
25
+ def collection_of_input_options
26
+ @collection_of_input_options ||= OptionsCollection.new
27
+ end
28
+
21
29
  def input_arguments_workbench
22
30
  @input_arguments_workbench ||= Workbench.work_with(collection_of_input_arguments)
23
31
  end
@@ -2,86 +2,187 @@
2
2
 
3
3
  module Servactory
4
4
  module InputArguments
5
- class InputArgument
5
+ class InputArgument # rubocop:disable Metrics/ClassLength
6
6
  ARRAY_DEFAULT_VALUE = ->(is: false, message: nil) { { is: is, message: message } }
7
7
 
8
8
  attr_reader :name,
9
- :types,
10
- :inclusion,
11
- :must,
12
- :array,
13
- :required,
14
- :internal,
15
- :default
16
-
17
- def initialize(name, type:, **options)
9
+ :collection_of_options,
10
+ :types
11
+
12
+ def initialize(name, collection_of_options:, type:, **options)
18
13
  @name = name
14
+ @collection_of_options = collection_of_options
19
15
  @types = Array(type)
20
16
 
21
- @inclusion = options.fetch(:inclusion, nil)
22
- @must = options.fetch(:must, nil)
23
- @array = prepare_advanced_for(options.fetch(:array, ARRAY_DEFAULT_VALUE.call))
24
- @required = options.fetch(:required, true)
25
- @internal = options.fetch(:internal, false)
26
- @default = options.fetch(:default, nil)
27
- end
17
+ add_basic_options_with(options)
28
18
 
29
- def options_for_checks
30
- {
31
- types: types,
32
- inclusion: inclusion,
33
- must: must,
34
- required: required,
35
- # internal: internal,
36
- default: default
37
- }
38
- end
19
+ @collection_of_options.each do |option|
20
+ self.class.attr_reader(:"#{option.name}")
39
21
 
40
- def prepare_advanced_for(value)
41
- if value.is_a?(Hash)
42
- ARRAY_DEFAULT_VALUE.call(
43
- is: value.fetch(:is, false),
44
- message: value.fetch(:message, nil)
45
- )
46
- else
47
- ARRAY_DEFAULT_VALUE.call(is: value)
22
+ instance_variable_set(:"@#{option.name}", option.value)
48
23
  end
49
24
  end
50
25
 
51
- def conflict_code
52
- return :required_vs_default if required? && default_value_present?
53
- return :array_vs_array if array? && types.include?(Array)
54
- return :array_vs_inclusion if array? && inclusion_present?
26
+ def add_basic_options_with(options)
27
+ # Check Class: Servactory::InputArguments::Checks::Required
28
+ add_required_option_with(options)
55
29
 
56
- nil
30
+ # Check Class: Servactory::InputArguments::Checks::Type
31
+ add_array_option_with(options)
32
+ add_default_option_with(options)
33
+
34
+ # Check Class: Servactory::InputArguments::Checks::Inclusion
35
+ add_inclusion_option_with(options)
36
+
37
+ # Check Class: Servactory::InputArguments::Checks::Must
38
+ add_must_option_with(options)
39
+
40
+ # Check Class: nil
41
+ add_internal_option_with(options)
42
+ end
43
+
44
+ def add_required_option_with(options) # rubocop:disable Metrics/MethodLength
45
+ collection_of_options << Option.new(
46
+ name: :required,
47
+ input: self,
48
+ check_class: Servactory::InputArguments::Checks::Required,
49
+ define_input_methods: lambda do
50
+ <<-RUBY
51
+ def required?
52
+ Servactory::Utils.boolean?(required[:is])
53
+ end
54
+
55
+ def optional?
56
+ !required?
57
+ end
58
+ RUBY
59
+ end,
60
+ define_conflicts: lambda do
61
+ <<-RUBY
62
+ return :required_vs_default if required? && default_value_present?
63
+ RUBY
64
+ end,
65
+ need_for_checks: true,
66
+ value_key: :is,
67
+ value_fallback: true,
68
+ **options
69
+ )
57
70
  end
58
71
 
59
- def inclusion_present?
60
- inclusion.is_a?(Array) && inclusion.present?
72
+ def add_array_option_with(options) # rubocop:disable Metrics/MethodLength
73
+ collection_of_options << Option.new(
74
+ name: :array,
75
+ input: self,
76
+ check_class: Servactory::InputArguments::Checks::Type,
77
+ define_input_methods: lambda do
78
+ <<-RUBY
79
+ def array?
80
+ Servactory::Utils.boolean?(array[:is])
81
+ end
82
+ RUBY
83
+ end,
84
+ define_conflicts: lambda do
85
+ <<-RUBY
86
+ return :array_vs_array if array? && types.include?(Array)
87
+ return :array_vs_inclusion if array? && inclusion_present?
88
+ RUBY
89
+ end,
90
+ need_for_checks: false,
91
+ value_key: :is,
92
+ value_fallback: false,
93
+ **options
94
+ )
61
95
  end
62
96
 
63
- def must_present?
64
- must.present?
97
+ def add_default_option_with(options) # rubocop:disable Metrics/MethodLength
98
+ collection_of_options << Option.new(
99
+ name: :default,
100
+ input: self,
101
+ check_class: Servactory::InputArguments::Checks::Type,
102
+ define_input_methods: lambda do
103
+ <<-RUBY
104
+ def default_value_present?
105
+ !default.nil?
106
+ end
107
+ RUBY
108
+ end,
109
+ need_for_checks: true,
110
+ value_fallback: nil,
111
+ with_advanced_mode: false,
112
+ **options
113
+ )
65
114
  end
66
115
 
67
- def array?
68
- Servactory::Utils.boolean?(array[:is])
116
+ def add_inclusion_option_with(options) # rubocop:disable Metrics/MethodLength
117
+ collection_of_options << Option.new(
118
+ name: :inclusion,
119
+ input: self,
120
+ check_class: Servactory::InputArguments::Checks::Inclusion,
121
+ define_input_methods: lambda do
122
+ <<-RUBY
123
+ def inclusion_present?
124
+ inclusion[:in].is_a?(Array) && inclusion[:in].present?
125
+ end
126
+ RUBY
127
+ end,
128
+ need_for_checks: true,
129
+ value_key: :in,
130
+ value_fallback: nil,
131
+ **options
132
+ )
69
133
  end
70
134
 
71
- def required?
72
- Servactory::Utils.boolean?(required)
135
+ def add_must_option_with(options) # rubocop:disable Metrics/MethodLength
136
+ collection_of_options << Option.new(
137
+ name: :must,
138
+ input: self,
139
+ check_class: Servactory::InputArguments::Checks::Must,
140
+ define_input_methods: lambda do
141
+ <<-RUBY
142
+ def must_present?
143
+ must.present?
144
+ end
145
+ RUBY
146
+ end,
147
+ need_for_checks: true,
148
+ value_key: :is,
149
+ value_fallback: nil,
150
+ with_advanced_mode: false,
151
+ **options
152
+ )
73
153
  end
74
154
 
75
- def optional?
76
- !required?
155
+ def add_internal_option_with(options) # rubocop:disable Metrics/MethodLength
156
+ collection_of_options << Option.new(
157
+ name: :internal,
158
+ input: self,
159
+ define_input_methods: lambda do
160
+ <<-RUBY
161
+ def internal?
162
+ Servactory::Utils.boolean?(internal[:is])
163
+ end
164
+ RUBY
165
+ end,
166
+ need_for_checks: false,
167
+ check_class: nil,
168
+ value_key: :is,
169
+ value_fallback: false,
170
+ **options
171
+ )
77
172
  end
78
173
 
79
- def internal?
80
- Servactory::Utils.boolean?(internal)
174
+ def options_for_checks
175
+ {
176
+ types: types
177
+ }.merge(
178
+ collection_of_options.options_for_checks
179
+ )
81
180
  end
82
181
 
83
- def default_value_present?
84
- !default.nil?
182
+ def conflict_code
183
+ instance_eval(collection_of_options.defined_conflicts)
184
+
185
+ nil
85
186
  end
86
187
 
87
188
  def with_conflicts?
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ class Option
6
+ DEFAULT_VALUE = ->(key:, value:, message: nil) { { key => value, message: message } }
7
+
8
+ private_constant :DEFAULT_VALUE
9
+
10
+ attr_reader :name,
11
+ :check_class,
12
+ :define_conflicts,
13
+ :need_for_checks,
14
+ :value_key,
15
+ :value
16
+
17
+ def initialize(
18
+ name:,
19
+ input:,
20
+ check_class:,
21
+ need_for_checks:,
22
+ value_fallback:,
23
+ value_key: nil,
24
+ define_input_methods: nil,
25
+ define_conflicts: nil,
26
+ with_advanced_mode: true,
27
+ **options
28
+ ) # do
29
+ @name = name.to_sym
30
+ @check_class = check_class
31
+ @define_conflicts = define_conflicts
32
+ @need_for_checks = need_for_checks
33
+ @value_key = value_key
34
+
35
+ @value = prepare_value_for(options, value_fallback: value_fallback, with_advanced_mode: with_advanced_mode)
36
+
37
+ input.instance_eval(define_input_methods.call) if define_input_methods.present?
38
+ end
39
+
40
+ def need_for_checks?
41
+ need_for_checks
42
+ end
43
+
44
+ private
45
+
46
+ def prepare_value_for(options, value_fallback:, with_advanced_mode:)
47
+ return options.fetch(@name, value_fallback) unless with_advanced_mode
48
+
49
+ prepare_advanced_for(
50
+ value: options.fetch(@name, DEFAULT_VALUE.call(key: value_key, value: value_fallback)),
51
+ value_fallback: value_fallback
52
+ )
53
+ end
54
+
55
+ def prepare_advanced_for(value:, value_fallback:)
56
+ if value.is_a?(Hash)
57
+ DEFAULT_VALUE.call(
58
+ key: value_key,
59
+ value: value.fetch(value_key, value_fallback),
60
+ message: value.fetch(:message, nil)
61
+ )
62
+ else
63
+ DEFAULT_VALUE.call(key: value_key, value: value)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Servactory
4
+ module InputArguments
5
+ class OptionsCollection
6
+ # NOTE: http://words.steveklabnik.com/beware-subclassing-ruby-core-classes
7
+ extend Forwardable
8
+ def_delegators :@collection, :<<, :each, :select, :map
9
+
10
+ def initialize(*)
11
+ @collection = []
12
+ end
13
+
14
+ def check_classes
15
+ select { |option| option.check_class.present? }.map(&:check_class).uniq
16
+ end
17
+
18
+ def options_for_checks
19
+ select(&:need_for_checks?).to_h do |option|
20
+ value = if option.value.is_a?(Hash)
21
+ option.value.key?(:is) ? option.value.fetch(:is) : option.value
22
+ else
23
+ option.value
24
+ end
25
+
26
+ [option.name, value]
27
+ end
28
+ end
29
+
30
+ def defined_conflicts
31
+ map { |option| option.define_conflicts&.call }.reject(&:blank?).uniq.join
32
+ end
33
+ end
34
+ end
35
+ end
@@ -8,10 +8,11 @@ module Servactory
8
8
  new(...).check!
9
9
  end
10
10
 
11
- def initialize(context, incoming_arguments, collection_of_input_arguments)
11
+ def initialize(context, incoming_arguments, collection_of_input_arguments, collection_of_input_options)
12
12
  @context = context
13
13
  @incoming_arguments = incoming_arguments
14
14
  @collection_of_input_arguments = collection_of_input_arguments
15
+ @collection_of_input_options = collection_of_input_options
15
16
 
16
17
  @errors = []
17
18
  end
@@ -58,20 +59,15 @@ module Servactory
58
59
  ########################################################################
59
60
 
60
61
  def check_classes
61
- [
62
- Servactory::InputArguments::Checks::Required,
63
- Servactory::InputArguments::Checks::Type,
64
- Servactory::InputArguments::Checks::Inclusion,
65
- Servactory::InputArguments::Checks::Must
66
- ]
62
+ @collection_of_input_options.check_classes
67
63
  end
68
64
 
69
65
  ########################################################################
70
66
 
71
67
  def raise_errors
72
- return if @errors.empty?
68
+ return if (tmp_errors = @errors.reject(&:blank?).uniq).empty?
73
69
 
74
- raise Servactory.configuration.input_argument_error_class, @errors.first
70
+ raise Servactory.configuration.input_argument_error_class, tmp_errors.first
75
71
  end
76
72
  end
77
73
  end
@@ -11,9 +11,10 @@ module Servactory
11
11
  @collection_of_input_arguments = collection_of_input_arguments
12
12
  end
13
13
 
14
- def assign(context:, arguments:)
14
+ def assign(context:, arguments:, collection_of_input_options:)
15
15
  @context = context
16
16
  @incoming_arguments = arguments
17
+ @collection_of_input_options = collection_of_input_options
17
18
  end
18
19
 
19
20
  def find_unnecessary!
@@ -29,12 +30,14 @@ module Servactory
29
30
  end
30
31
 
31
32
  def check!
32
- Tools::Check.check!(context, @incoming_arguments, collection_of_input_arguments)
33
+ Tools::Check.check!(context, @incoming_arguments, collection_of_input_arguments, collection_of_input_options)
33
34
  end
34
35
 
35
36
  private
36
37
 
37
- attr_reader :context, :collection_of_input_arguments
38
+ attr_reader :context,
39
+ :collection_of_input_arguments,
40
+ :collection_of_input_options
38
41
  end
39
42
  end
40
43
  end
@@ -3,7 +3,7 @@
3
3
  module Servactory
4
4
  module VERSION
5
5
  MAJOR = 1
6
- MINOR = 3
6
+ MINOR = 4
7
7
  PATCH = 0
8
8
 
9
9
  STRING = [MAJOR, MINOR, PATCH].join(".")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servactory
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Sokolov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-06 00:00:00.000000000 Z
11
+ date: 2023-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -178,6 +178,8 @@ files:
178
178
  - lib/servactory/input_arguments/collection.rb
179
179
  - lib/servactory/input_arguments/dsl.rb
180
180
  - lib/servactory/input_arguments/input_argument.rb
181
+ - lib/servactory/input_arguments/option.rb
182
+ - lib/servactory/input_arguments/options_collection.rb
181
183
  - lib/servactory/input_arguments/tools/check.rb
182
184
  - lib/servactory/input_arguments/tools/find_unnecessary.rb
183
185
  - lib/servactory/input_arguments/tools/prepare.rb