servactory 1.3.0 → 1.4.0

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