servactory 3.1.0.rc2 → 3.1.0.rc3

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: 6a7c9b4d9ad2e7d15265ba278d740eb34e6df33590bf517a0b42d914dad46e22
4
- data.tar.gz: 21ad5c1814f6bd2c62221057547f5388cf928fd1f937f37131dab041d6325983
3
+ metadata.gz: c5acfb374e0e85d7779b99658fcb899007ca346ca63c71344800490428ac61dd
4
+ data.tar.gz: 94ff484e768908909aa856d63310af3c896f198357b88c3089fe9393de53bc88
5
5
  SHA512:
6
- metadata.gz: 9f231bf3c0662ec8285125cacd138d765622eb1ebad7e9eae5d95ae2dd84ad48f80a65783e208b3b9752e40e9bc755ca245bcf2f3b1c68b3724ca13a68d1f6a5
7
- data.tar.gz: 8fb3f9f3bcf2a49f26d4cff76885537559ba4173c78585c2432fc7bf814a8e587303dc941c36a7992800bff1c561661cc04ae20beffbc17dac4d5a0af64de387
6
+ metadata.gz: 61facdd48b89752c3f2bfe0735c87c24f83c225c2474608c9aa871f150003a996b2b4775ef25941f0916f45524383cdf1e1814a237b522add763782c2ec03e9b
7
+ data.tar.gz: 13202fe9276b8323cd83fc2c0919613eeabfd9d99b4d1cfac541ef7ed88739a84c6d5719a44ca76fe960c33aaf6a66b1b9c9353e12a1e5662b8e19df05a68fd3
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Anton Sokolov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -52,7 +52,7 @@ gem "servactory"
52
52
  ### Define service
53
53
 
54
54
  ```ruby
55
- class UserService::Authenticate < Servactory::Base
55
+ class Users::Authenticate < Servactory::Base
56
56
  input :email, type: String
57
57
  input :password, type: String
58
58
 
@@ -77,7 +77,7 @@ end
77
77
  ```ruby
78
78
  class SessionsController < ApplicationController
79
79
  def create
80
- service = UserService::Authenticate.call(**session_params)
80
+ service = Users::Authenticate.call(**session_params)
81
81
 
82
82
  if service.success?
83
83
  session[:current_user_id] = service.user.id
@@ -0,0 +1,134 @@
1
+ ja:
2
+ servactory:
3
+ common:
4
+ undefined_method:
5
+ missing_name: "[%{service_class_name}] %{error_text}"
6
+ methods:
7
+ call:
8
+ not_used: "[%{service_class_name}] 実行する処理がありません。`make`を使用するか、`call`メソッドを作成してください。"
9
+ cannot_be_overwritten: "[%{service_class_name}] 次のメソッドは上書きできません: %{list_of_methods}"
10
+ inputs:
11
+ undefined:
12
+ for_fetch: "[%{service_class_name}] 未定義の入力`%{input_name}`"
13
+ for_assign: "[%{service_class_name}] 未定義の入力`%{input_name}`"
14
+ validations:
15
+ must:
16
+ default_error: "[%{service_class_name}] 入力`%{input_name}`は「%{code}」を満たす必要があります"
17
+ syntax_error: "[%{service_class_name}] 入力`%{input_name}`の`%{code}`内の構文エラー: %{exception_message}"
18
+ dynamic_options:
19
+ consists_of:
20
+ required: "[%{service_class_name}] 入力コレクション`%{input_name}`の必須要素が不足しています"
21
+ wrong_type: "[%{service_class_name}] 入力コレクション`%{input_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
22
+ wrong_element_type: "[%{service_class_name}] 入力コレクション`%{input_name}`の要素の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
23
+ format:
24
+ default: "[%{service_class_name}] 入力`%{input_name}`は`%{format_name}`フォーマットに一致しません"
25
+ wrong_pattern: "[%{service_class_name}] 入力`%{input_name}`は`%{format_name}`フォーマットに一致しません"
26
+ wrong_type: "[%{service_class_name}] 入力`%{input_name}`は`%{format_name}`フォーマット検証のためにStringである必要があります"
27
+ unknown: "[%{service_class_name}] 入力`%{input_name}`に指定された`%{format_name}`フォーマットは不明です"
28
+ inclusion:
29
+ default: "[%{service_class_name}] `%{input_name}`の値が不正です。`%{input_inclusion}`のいずれかである必要がありますが、`%{value}`が渡されました"
30
+ invalid_option: "[%{service_class_name}] 入力`%{input_name}`の`%{option_name}`オプションに値がありません"
31
+ min:
32
+ default: "[%{service_class_name}] 入力`%{input_name}`は値`%{value}`を受け取りましたが、`%{option_value}`より小さいです"
33
+ max:
34
+ default: "[%{service_class_name}] 入力`%{input_name}`は値`%{value}`を受け取りましたが、`%{option_value}`より大きいです"
35
+ multiple_of:
36
+ default: "[%{service_class_name}] 入力`%{input_name}`の値`%{value}`は`%{option_value}`の倍数ではありません"
37
+ blank: "[%{service_class_name}] 入力`%{input_name}`の`%{option_name}`オプションに無効な値`%{option_value}`があります"
38
+ divided_by_0: "[%{service_class_name}] 入力`%{input_name}`の`%{option_name}`オプションに無効な値`%{option_value}`があります"
39
+ schema:
40
+ wrong_type: "[%{service_class_name}] 入力`%{input_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
41
+ wrong_element_type: "[%{service_class_name}] 入力ハッシュ`%{input_name}`の型が不正です。`%{key_name}`には`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
42
+ wrong_element_value: "[%{service_class_name}] 入力ハッシュ`%{input_name}`の値が不正です。`%{key_name}`には`%{expected_type}`型の値が期待されましたが、`%{given_type}`が渡されました"
43
+ target:
44
+ default: "[%{service_class_name}] 入力`%{input_name}`のターゲットが不正です。`%{expected_target}`である必要がありますが、`%{value}`が渡されました"
45
+ invalid_option: "[%{service_class_name}] 入力`%{input_name}`の`%{option_name}`オプションに値がありません"
46
+ required:
47
+ default_error:
48
+ default: "[%{service_class_name}] 必須の入力`%{input_name}`が不足しています"
49
+ type:
50
+ default_error:
51
+ default: "[%{service_class_name}] 入力`%{input_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
52
+ tools:
53
+ find_unnecessary:
54
+ error: "[%{service_class_name}] 予期しない属性: `%{unnecessary_attributes}`"
55
+ rules:
56
+ error: "[%{service_class_name}] 入力`%{input_name}`のオプションに競合があります: `%{conflict_code}`"
57
+ internals:
58
+ undefined:
59
+ for_fetch: "[%{service_class_name}] 未定義の内部属性`%{internal_name}`"
60
+ for_assign: "[%{service_class_name}] 未定義の内部属性`%{internal_name}`"
61
+ validations:
62
+ must:
63
+ default_error: "[%{service_class_name}] 内部属性`%{internal_name}`は「%{code}」を満たす必要があります"
64
+ syntax_error: "[%{service_class_name}] 内部属性`%{internal_name}`の`%{code}`内の構文エラー: %{exception_message}"
65
+ dynamic_options:
66
+ consists_of:
67
+ required: "[%{service_class_name}] 内部属性コレクション`%{internal_name}`の必須要素が不足しています"
68
+ wrong_type: "[%{service_class_name}] 内部属性コレクション`%{internal_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
69
+ wrong_element_type: "[%{service_class_name}] 内部属性コレクション`%{internal_name}`の要素の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
70
+ format:
71
+ default: "[%{service_class_name}] 内部属性`%{internal_name}`は`%{format_name}`フォーマットに一致しません"
72
+ wrong_pattern: "[%{service_class_name}] 内部属性`%{internal_name}`は`%{format_name}`フォーマットに一致しません"
73
+ wrong_type: "[%{service_class_name}] 内部属性`%{internal_name}`は`%{format_name}`フォーマット検証のためにStringである必要があります"
74
+ unknown: "[%{service_class_name}] 内部属性`%{internal_name}`に指定された`%{format_name}`フォーマットは不明です"
75
+ inclusion:
76
+ default: "[%{service_class_name}] `%{internal_name}`の値が不正です。`%{internal_inclusion}`のいずれかである必要がありますが、`%{value}`が渡されました"
77
+ invalid_option: "[%{service_class_name}] 内部属性`%{internal_name}`の`%{option_name}`オプションに値がありません"
78
+ min:
79
+ default: "[%{service_class_name}] 内部属性`%{internal_name}`は値`%{value}`を受け取りましたが、`%{option_value}`より小さいです"
80
+ max:
81
+ default: "[%{service_class_name}] 内部属性`%{internal_name}`は値`%{value}`を受け取りましたが、`%{option_value}`より大きいです"
82
+ multiple_of:
83
+ default: "[%{service_class_name}] 内部属性`%{internal_name}`の値`%{value}`は`%{option_value}`の倍数ではありません"
84
+ blank: "[%{service_class_name}] 内部属性`%{internal_name}`の`%{option_name}`オプションに無効な値`%{option_value}`があります"
85
+ divided_by_0: "[%{service_class_name}] 内部属性`%{internal_name}`の`%{option_name}`オプションに無効な値`%{option_value}`があります"
86
+ schema:
87
+ wrong_type: "[%{service_class_name}] 内部属性`%{internal_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
88
+ wrong_element_type: "[%{service_class_name}] 内部属性ハッシュ`%{internal_name}`の型が不正です。`%{key_name}`には`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
89
+ wrong_element_value: "[%{service_class_name}] 内部属性ハッシュ`%{internal_name}`の値が不正です。`%{key_name}`には`%{expected_type}`型の値が期待されましたが、`%{given_type}`が渡されました"
90
+ target:
91
+ default: "[%{service_class_name}] 内部属性`%{internal_name}`のターゲットが不正です。`%{expected_target}`である必要がありますが、`%{value}`が渡されました"
92
+ invalid_option: "[%{service_class_name}] 内部属性`%{internal_name}`の`%{option_name}`オプションに値がありません"
93
+ type:
94
+ default_error:
95
+ default: "[%{service_class_name}] 内部属性`%{internal_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
96
+ outputs:
97
+ undefined:
98
+ for_fetch: "[%{service_class_name}] 未定義の出力属性`%{output_name}`"
99
+ for_assign: "[%{service_class_name}] 未定義の出力属性`%{output_name}`"
100
+ validations:
101
+ must:
102
+ default_error: "[%{service_class_name}] 出力属性`%{output_name}`は「%{code}」を満たす必要があります"
103
+ syntax_error: "[%{service_class_name}] 出力属性`%{output_name}`の`%{code}`内の構文エラー: %{exception_message}"
104
+ dynamic_options:
105
+ consists_of:
106
+ required: "[%{service_class_name}] 出力属性コレクション`%{output_name}`の必須要素が不足しています"
107
+ wrong_type: "[%{service_class_name}] 出力属性コレクション`%{output_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
108
+ wrong_element_type: "[%{service_class_name}] 出力属性コレクション`%{output_name}`の要素の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
109
+ format:
110
+ default: "[%{service_class_name}] 出力属性`%{output_name}`は`%{format_name}`フォーマットに一致しません"
111
+ wrong_pattern: "[%{service_class_name}] 出力属性`%{output_name}`は`%{format_name}`フォーマットに一致しません"
112
+ wrong_type: "[%{service_class_name}] 出力属性`%{output_name}`は`%{format_name}`フォーマット検証のためにStringである必要があります"
113
+ unknown: "[%{service_class_name}] 出力属性`%{output_name}`に指定された`%{format_name}`フォーマットは不明です"
114
+ inclusion:
115
+ default: "[%{service_class_name}] `%{output_name}`の値が不正です。`%{output_inclusion}`のいずれかである必要がありますが、`%{value}`が渡されました"
116
+ invalid_option: "[%{service_class_name}] 出力属性`%{output_name}`の`%{option_name}`オプションに値がありません"
117
+ min:
118
+ default: "[%{service_class_name}] 出力属性`%{output_name}`は値`%{value}`を受け取りましたが、`%{option_value}`より小さいです"
119
+ max:
120
+ default: "[%{service_class_name}] 出力属性`%{output_name}`は値`%{value}`を受け取りましたが、`%{option_value}`より大きいです"
121
+ multiple_of:
122
+ default: "[%{service_class_name}] 出力属性`%{output_name}`の値`%{value}`は`%{option_value}`の倍数ではありません"
123
+ blank: "[%{service_class_name}] 出力属性`%{output_name}`の`%{option_name}`オプションに無効な値`%{option_value}`があります"
124
+ divided_by_0: "[%{service_class_name}] 出力属性`%{output_name}`の`%{option_name}`オプションに無効な値`%{option_value}`があります"
125
+ schema:
126
+ wrong_type: "[%{service_class_name}] 出力属性`%{output_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
127
+ wrong_element_type: "[%{service_class_name}] 出力属性ハッシュ`%{output_name}`の型が不正です。`%{key_name}`には`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
128
+ wrong_element_value: "[%{service_class_name}] 出力属性ハッシュ`%{output_name}`の値が不正です。`%{key_name}`には`%{expected_type}`型の値が期待されましたが、`%{given_type}`が渡されました"
129
+ target:
130
+ default: "[%{service_class_name}] 出力属性`%{output_name}`のターゲットが不正です。`%{expected_target}`である必要がありますが、`%{value}`が渡されました"
131
+ invalid_option: "[%{service_class_name}] 出力属性`%{output_name}`の`%{option_name}`オプションに値がありません"
132
+ type:
133
+ default_error:
134
+ default: "[%{service_class_name}] 出力属性`%{output_name}`の型が不正です。`%{expected_type}`が期待されましたが、`%{given_type}`が渡されました"
@@ -9,7 +9,7 @@ rails generate servactory:install [options]
9
9
  **Options:**
10
10
  - `--namespace` — Base namespace (default: `ApplicationService`)
11
11
  - `--path` — Path to install service files (default: `app/services`)
12
- - `--locales` — Locales to install (available: `en`, `ru`, `de`, `fr`, `es`, `it`)
12
+ - `--locales` — Locales to install (available: `en`, `ru`, `de`, `fr`, `es`, `it`, `ja`)
13
13
  - `--minimal` — Generate minimal setup without configuration examples
14
14
 
15
15
  ## Service
@@ -30,6 +30,22 @@ module Servactory
30
30
  @collection_mode_class_names = original.collection_mode_class_names.dup
31
31
  @hash_mode_class_names = original.hash_mode_class_names.dup
32
32
  @action_rescue_handlers = original.action_rescue_handlers.dup
33
+
34
+ rebind_dynamic_option_helpers!
35
+ end
36
+
37
+ private
38
+
39
+ def rebind_dynamic_option_helpers!
40
+ new_consists_of = Servactory::ToolKit::DynamicOptions::ConsistsOf
41
+ .use(collection_mode_class_names: @collection_mode_class_names)
42
+ new_schema = Servactory::ToolKit::DynamicOptions::Schema
43
+ .use(default_hash_mode_class_names: @hash_mode_class_names)
44
+
45
+ [@input_option_helpers, @internal_option_helpers, @output_option_helpers].each do |helpers|
46
+ helpers.replace(name: :consists_of, with: new_consists_of)
47
+ helpers.replace(name: :schema, with: new_schema)
48
+ end
33
49
  end
34
50
  end
35
51
  end
@@ -67,7 +67,8 @@ module Servactory
67
67
  .new(name: :optional, equivalent: { required: false }),
68
68
  Servactory::ToolKit::DynamicOptions::ConsistsOf
69
69
  .use(collection_mode_class_names: config.collection_mode_class_names),
70
- Servactory::ToolKit::DynamicOptions::Schema.use(default_hash_mode_class_names:),
70
+ Servactory::ToolKit::DynamicOptions::Schema
71
+ .use(default_hash_mode_class_names: config.hash_mode_class_names),
71
72
  Servactory::ToolKit::DynamicOptions::Inclusion.use
72
73
  ]
73
74
  end
@@ -76,7 +77,8 @@ module Servactory
76
77
  Set[
77
78
  Servactory::ToolKit::DynamicOptions::ConsistsOf
78
79
  .use(collection_mode_class_names: config.collection_mode_class_names),
79
- Servactory::ToolKit::DynamicOptions::Schema.use(default_hash_mode_class_names:),
80
+ Servactory::ToolKit::DynamicOptions::Schema
81
+ .use(default_hash_mode_class_names: config.hash_mode_class_names),
80
82
  Servactory::ToolKit::DynamicOptions::Inclusion.use
81
83
  ]
82
84
  end
@@ -85,7 +87,8 @@ module Servactory
85
87
  Set[
86
88
  Servactory::ToolKit::DynamicOptions::ConsistsOf
87
89
  .use(collection_mode_class_names: config.collection_mode_class_names),
88
- Servactory::ToolKit::DynamicOptions::Schema.use(default_hash_mode_class_names:),
90
+ Servactory::ToolKit::DynamicOptions::Schema
91
+ .use(default_hash_mode_class_names: config.hash_mode_class_names),
89
92
  Servactory::ToolKit::DynamicOptions::Inclusion.use
90
93
  ]
91
94
  end
@@ -6,7 +6,10 @@ module Servactory
6
6
  class ClassNamesCollection
7
7
  extend Forwardable
8
8
 
9
- def_delegators :@collection, :include?, :intersect?
9
+ def_delegators :@collection,
10
+ :merge,
11
+ :include?,
12
+ :intersect?
10
13
 
11
14
  def initialize(collection)
12
15
  @collection = collection
@@ -72,6 +72,20 @@ module Servactory
72
72
  helpers_index[name]
73
73
  end
74
74
 
75
+ # Replaces a helper by name with a new one, invalidating the index cache.
76
+ #
77
+ # @param name [Symbol] the helper name to replace
78
+ # @param with [Maintenance::Attributes::OptionHelper] the replacement helper
79
+ # @return [void]
80
+ def replace(name:, with:)
81
+ old = find_by(name:)
82
+ return unless old
83
+
84
+ @collection.delete(old)
85
+ @collection.add(with)
86
+ @helpers_index = nil
87
+ end
88
+
75
89
  private
76
90
 
77
91
  # Builds and caches a hash index for O(1) helper lookups.
@@ -29,30 +29,22 @@ module Servactory
29
29
  def process_input(context, warehouse, input)
30
30
  value = warehouse.fetch_input(input.name)
31
31
 
32
- input.options_for_checks.each do |check_key, check_options|
33
- error = process_option(context, input, value, check_key, check_options)
32
+ input.collection_of_options.validations_for_checks.each do |check_key, check_options, validation_class|
33
+ error = process_option(context, input, value, check_key, check_options, validation_class)
34
34
  return error if error.present?
35
35
  end
36
36
 
37
37
  nil
38
38
  end
39
39
 
40
- def process_option(context, input, value, check_key, check_options) # rubocop:disable Metrics/MethodLength
41
- validation_classes = input.collection_of_options.validation_classes
42
- return if validation_classes.empty?
43
-
44
- validation_classes.each do |validation_class|
45
- error_message = validation_class.check(
46
- context:,
47
- attribute: input,
48
- value:,
49
- check_key:,
50
- check_options:
51
- )
52
- return error_message if error_message.present?
53
- end
54
-
55
- nil
40
+ def process_option(context, input, value, check_key, check_options, validation_class)
41
+ validation_class.check(
42
+ context:,
43
+ attribute: input,
44
+ value:,
45
+ check_key:,
46
+ check_options:
47
+ )
56
48
  end
57
49
  end
58
50
  end
@@ -38,8 +38,8 @@ module Servactory
38
38
  def_delegators :@collection,
39
39
  :<<,
40
40
  :filter,
41
- :each_with_object,
42
- :map, :flat_map,
41
+ :each, :each_with_object,
42
+ :map,
43
43
  :size,
44
44
  :empty?
45
45
 
@@ -76,13 +76,33 @@ module Servactory
76
76
  end
77
77
  end
78
78
 
79
+ # Returns options that need validation checks as an array of tuples.
80
+ # Each tuple contains [check_key, check_options, validation_class],
81
+ # enabling direct dispatch without nested iteration.
82
+ #
83
+ # @return [Array<Array(Symbol, Object, Class)>] tuples for direct validation dispatch
84
+ def validations_for_checks
85
+ @validations_for_checks ||= filter(&:need_for_checks?).filter_map do |option|
86
+ next if option.validation_class.nil?
87
+
88
+ [option.name, extract_normalized_body_from(option:), option.validation_class]
89
+ end
90
+ end
91
+
79
92
  # Returns the first conflict code found among options.
80
93
  #
81
- # @return [Object, nil] conflict code or nil if no conflicts
94
+ # @return [Symbol, nil] conflict code or nil if no conflicts
82
95
  def defined_conflict_code
83
- flat_map { |option| resolve_conflicts_from(option:) }
84
- .reject(&:blank?)
85
- .first
96
+ each do |option|
97
+ next unless option.define_conflicts
98
+
99
+ option.define_conflicts.each do |conflict|
100
+ code = conflict.content.call
101
+ return code if code.present?
102
+ end
103
+ end
104
+
105
+ nil
86
106
  end
87
107
 
88
108
  # Finds an option by its name using indexed lookup.
@@ -111,16 +131,6 @@ module Servactory
111
131
  def extract_normalized_body_from(option:)
112
132
  option.value
113
133
  end
114
-
115
- # Resolves conflict codes from an option's define_conflicts.
116
- #
117
- # @param option [Option] the option to check for conflicts
118
- # @return [Array<Object>] array of conflict codes (may contain nils/blanks)
119
- def resolve_conflicts_from(option:)
120
- return [] unless option.define_conflicts
121
-
122
- option.define_conflicts.map { |conflict| conflict.content.call }
123
- end
124
134
  end
125
135
  end
126
136
  end
@@ -98,7 +98,7 @@ module Servactory
98
98
  content: ->(option:) { !option[:is].nil? }
99
99
  )
100
100
  ],
101
- need_for_checks: true,
101
+ need_for_checks: false,
102
102
  body_key: :is,
103
103
  body_fallback: nil,
104
104
  detect_advanced_mode: false,
@@ -19,14 +19,11 @@ module Servactory
19
19
 
20
20
  private
21
21
 
22
- def process(context:, attribute:, value:) # rubocop:disable Metrics/MethodLength
23
- attribute.options_for_checks.each do |check_key, check_options|
22
+ def process(context:, attribute:, value:)
23
+ attribute.collection_of_options.validations_for_checks.each do |check_key, check_options, validation_class|
24
24
  error = process_option(
25
- context:,
26
- attribute:,
27
- value:,
28
- check_key:,
29
- check_options:
25
+ context:, attribute:, value:,
26
+ check_key:, check_options:, validation_class:
30
27
  )
31
28
  return error if error.present?
32
29
  end
@@ -34,22 +31,14 @@ module Servactory
34
31
  nil
35
32
  end
36
33
 
37
- def process_option(context:, attribute:, value:, check_key:, check_options:) # rubocop:disable Metrics/MethodLength
38
- validation_classes = attribute.collection_of_options.validation_classes
39
- return if validation_classes.empty?
40
-
41
- validation_classes.each do |validation_class|
42
- error_message = validation_class.check(
43
- context:,
44
- attribute:,
45
- value:,
46
- check_key:,
47
- check_options:
48
- )
49
- return error_message if error_message.present?
50
- end
51
-
52
- nil
34
+ def process_option(context:, attribute:, value:, check_key:, check_options:, validation_class:)
35
+ validation_class.check(
36
+ context:,
37
+ attribute:,
38
+ value:,
39
+ check_key:,
40
+ check_options:
41
+ )
53
42
  end
54
43
  end
55
44
  end
@@ -37,7 +37,7 @@ module Servactory
37
37
  # - ServiceMockConfig - provides configuration for each stub
38
38
  # - ServiceMockBuilder - creates executor with configs
39
39
  # - RSpec Context - provides allow/receive/etc. methods
40
- class MockExecutor
40
+ class MockExecutor # rubocop:disable Metrics/ClassLength
41
41
  include Concerns::ErrorMessages
42
42
 
43
43
  # Creates a new mock executor.
@@ -170,7 +170,11 @@ module Servactory
170
170
  # @param config [ServiceMockConfig] Configuration with result/exception
171
171
  # @return [void]
172
172
  def apply_return_behavior(message_expectation, config)
173
- if config.failure? && config.bang_method?
173
+ if config.call_original?
174
+ message_expectation.and_call_original
175
+ elsif config.wrap_original?
176
+ message_expectation.and_wrap_original(&config.wrap_block)
177
+ elsif config.failure? && config.bang_method?
174
178
  message_expectation.and_raise(config.exception)
175
179
  else
176
180
  message_expectation.and_return(config.build_result)
@@ -77,6 +77,15 @@ module Servactory
77
77
  include Concerns::ServiceClassValidation
78
78
  include Concerns::ErrorMessages
79
79
 
80
+ RESULT_TYPE_TO_METHOD = {
81
+ success: :succeeds,
82
+ failure: :fails,
83
+ call_original: :and_call_original,
84
+ wrap_original: :and_wrap_original
85
+ }.freeze
86
+
87
+ private_constant :RESULT_TYPE_TO_METHOD
88
+
80
89
  # @return [Class] The Servactory service class being mocked
81
90
  attr_reader :service_class
82
91
 
@@ -188,6 +197,51 @@ module Servactory
188
197
  self
189
198
  end
190
199
 
200
+ # ============================================================
201
+ # RSpec Pass-Through API
202
+ # ============================================================
203
+
204
+ # Delegates to the original unmodified method.
205
+ # Useful for spy pattern or selective mocking.
206
+ #
207
+ # @return [ServiceMockBuilder] self for method chaining
208
+ #
209
+ # @example Pass-through (spy)
210
+ # allow_service(S).and_call_original
211
+ #
212
+ # @example Selective mocking with input matching
213
+ # allow_service(S).with(id: 1).and_call_original
214
+ def and_call_original
215
+ validate_not_in_sequential_mode!(:and_call_original)
216
+ validate_result_type_not_switched!(:and_call_original)
217
+
218
+ @config.result_type = :call_original
219
+ execute_or_re_execute_mock
220
+ self
221
+ end
222
+
223
+ # Wraps the original method with custom logic.
224
+ # Block receives the original method and call arguments.
225
+ #
226
+ # @yield [original, **inputs] Block wrapping the original
227
+ # @return [ServiceMockBuilder] self for method chaining
228
+ #
229
+ # @example Modify result
230
+ # allow_service(S).and_wrap_original do |original, **inputs|
231
+ # result = original.call(**inputs)
232
+ # # custom logic
233
+ # result
234
+ # end
235
+ def and_wrap_original(&block)
236
+ validate_not_in_sequential_mode!(:and_wrap_original)
237
+ validate_result_type_not_switched!(:and_wrap_original)
238
+
239
+ @config.result_type = :wrap_original
240
+ @config.wrap_block = block
241
+ execute_or_re_execute_mock
242
+ self
243
+ end
244
+
191
245
  # ============================================================
192
246
  # Sequential Call API
193
247
  # ============================================================
@@ -208,6 +262,7 @@ module Servactory
208
262
  # @raise [ArgumentError] if called without first calling succeeds/fails
209
263
  # @raise [OutputValidator::ValidationError] if outputs don't match service definition
210
264
  def then_succeeds(outputs_hash = {})
265
+ validate_not_passthrough!(:then_succeeds)
211
266
  validate_result_type_defined!(:then_succeeds)
212
267
 
213
268
  validate_outputs!(outputs_hash)
@@ -237,6 +292,7 @@ module Servactory
237
292
  #
238
293
  # @raise [ArgumentError] if called without first calling succeeds/fails
239
294
  def then_fails(exception_class = nil, type: :base, message:, meta: nil) # rubocop:disable Style/KeywordParametersOrder
295
+ validate_not_passthrough!(:then_fails)
240
296
  validate_result_type_defined!(:then_fails)
241
297
 
242
298
  finalize_current_to_sequence
@@ -281,22 +337,36 @@ module Servactory
281
337
 
282
338
  # Validates that result type is not being switched.
283
339
  #
284
- # Prevents accidental switching between succeeds() and fails().
340
+ # Prevents accidental switching between different result types.
285
341
  #
286
- # @param new_type [Symbol] :succeeds or :fails
342
+ # @param new_type [Symbol] :succeeds, :fails, :and_call_original, or :and_wrap_original
287
343
  # @raise [ArgumentError] if trying to switch result type
288
344
  # @return [void]
289
- def validate_result_type_not_switched!(new_type) # rubocop:disable Metrics/CyclomaticComplexity
345
+ def validate_result_type_not_switched!(new_type)
290
346
  return unless @config.result_type_defined?
291
347
 
292
- current_type = @config.success? ? :succeeds : :fails
293
- return if (new_type == :succeeds && current_type == :succeeds) ||
294
- (new_type == :fails && current_type == :fails)
348
+ current_type = RESULT_TYPE_TO_METHOD[@config.result_type]
349
+ return if new_type == current_type
295
350
 
296
351
  raise ArgumentError,
297
352
  "Cannot call #{new_type}() after #{current_type}() was already called. " \
298
- "#{new_type == :succeeds ? 'succeeds()' : 'fails()'} replaces the result type, " \
299
- "which is likely a mistake. Create a new mock if you need different behavior."
353
+ "This replaces the result type, which is likely a mistake. " \
354
+ "Create a new mock if you need different behavior."
355
+ end
356
+
357
+ # Validates that pass-through methods are not mixed with sequential responses.
358
+ #
359
+ # @param method_name [Symbol] The method being called
360
+ # @raise [ArgumentError] if current config is a pass-through type
361
+ # @return [void]
362
+ def validate_not_passthrough!(method_name)
363
+ return unless @config.call_original? || @config.wrap_original?
364
+
365
+ passthrough = @config.call_original? ? :and_call_original : :and_wrap_original
366
+
367
+ raise ArgumentError,
368
+ "Cannot call #{method_name}() after #{passthrough}(). " \
369
+ "Pass-through methods are not compatible with sequential responses."
300
370
  end
301
371
 
302
372
  # Validates outputs against service definition.
@@ -38,7 +38,8 @@ module Servactory
38
38
  :method_type,
39
39
  :outputs,
40
40
  :exception,
41
- :argument_matcher
41
+ :argument_matcher,
42
+ :wrap_block
42
43
 
43
44
  # Creates a new mock configuration.
44
45
  #
@@ -51,6 +52,7 @@ module Servactory
51
52
  @outputs = {}
52
53
  @exception = nil
53
54
  @argument_matcher = nil
55
+ @wrap_block = nil
54
56
  end
55
57
 
56
58
  # Checks if this is a success mock.
@@ -67,6 +69,20 @@ module Servactory
67
69
  result_type == :failure
68
70
  end
69
71
 
72
+ # Checks if this is a call_original mock.
73
+ #
74
+ # @return [Boolean] True if result_type is :call_original
75
+ def call_original?
76
+ result_type == :call_original
77
+ end
78
+
79
+ # Checks if this is a wrap_original mock.
80
+ #
81
+ # @return [Boolean] True if result_type is :wrap_original
82
+ def wrap_original?
83
+ result_type == :wrap_original
84
+ end
85
+
70
86
  # Checks if this mocks the .call! method.
71
87
  #
72
88
  # @return [Boolean] True if method_type is :call!
@@ -113,13 +129,14 @@ module Servactory
113
129
  # Creates a deep copy of this config.
114
130
  #
115
131
  # @return [ServiceMockConfig] New config with copied values
116
- def dup
132
+ def dup # rubocop:disable Metrics/AbcSize
117
133
  copy = self.class.new(service_class:)
118
134
  copy.result_type = result_type
119
135
  copy.method_type = method_type
120
136
  copy.outputs = outputs.dup
121
137
  copy.exception = exception
122
138
  copy.argument_matcher = argument_matcher
139
+ copy.wrap_block = wrap_block
123
140
  copy
124
141
  end
125
142
  end
@@ -31,7 +31,7 @@ module Servactory
31
31
  # Specify type directly as the option value:
32
32
  #
33
33
  # ```ruby
34
- # class ProcessUsersService < ApplicationService::Base
34
+ # class Users::Process < ApplicationService::Base
35
35
  # input :user_ids, type: Array, consists_of: Integer
36
36
  # input :tags, type: Array, consists_of: [String, Symbol]
37
37
  # input :scores, type: Array, consists_of: Float
@@ -88,7 +88,8 @@ module Servactory
88
88
  # Creates a ConsistsOf validator instance.
89
89
  #
90
90
  # @param option_name [Symbol] The option name (default: :consists_of)
91
- # @param collection_mode_class_names [Array<Class>] Valid collection types
91
+ # @param collection_mode_class_names [Servactory::Configuration::CollectionMode::ClassNamesCollection]
92
+ # Valid collection types
92
93
  # @return [Servactory::Maintenance::Attributes::OptionHelper]
93
94
  def self.use(option_name = :consists_of, collection_mode_class_names:)
94
95
  instance = new(option_name, :type, false)
@@ -98,7 +99,8 @@ module Servactory
98
99
 
99
100
  # Assigns the list of valid collection class names.
100
101
  #
101
- # @param collection_mode_class_names [Array<Class>] Collection types to accept
102
+ # @param collection_mode_class_names [Servactory::Configuration::CollectionMode::ClassNamesCollection]
103
+ # Collection types to accept
102
104
  # @return [void]
103
105
  def assign(collection_mode_class_names)
104
106
  @collection_mode_class_names = collection_mode_class_names
@@ -72,7 +72,7 @@ module Servactory
72
72
  # Specify format directly as the option value:
73
73
  #
74
74
  # ```ruby
75
- # class ValidateUserService < ApplicationService::Base
75
+ # class Users::Validate < ApplicationService::Base
76
76
  # input :uuid, type: String, format: :uuid
77
77
  # input :email, type: String, format: :email
78
78
  # input :password, type: String, format: :password
@@ -20,7 +20,7 @@ module Servactory
20
20
  # Use in your service definition:
21
21
  #
22
22
  # ```ruby
23
- # class CreateUserService < ApplicationService::Base
23
+ # class Users::Create < ApplicationService::Base
24
24
  # input :role, type: String, inclusion: { in: %w[admin user guest] }
25
25
  # input :status, type: Symbol, inclusion: { in: [:active, :inactive] }
26
26
  # input :level, type: Integer, inclusion: { in: [1, 2, 3, 4, 5] }
@@ -32,7 +32,7 @@ module Servactory
32
32
  # Specify inclusion values directly:
33
33
  #
34
34
  # ```ruby
35
- # class CreateUserService < ApplicationService::Base
35
+ # class Users::Create < ApplicationService::Base
36
36
  # input :role, type: String, inclusion: %w[admin user guest]
37
37
  # input :status, type: Symbol, inclusion: [:active, :inactive]
38
38
  # input :level, type: Integer, inclusion: 1..10
@@ -35,7 +35,7 @@ module Servactory
35
35
  # Use in your service definition:
36
36
  #
37
37
  # ```ruby
38
- # class ProcessDataService < ApplicationService::Base
38
+ # class Data::Process < ApplicationService::Base
39
39
  # input :count, type: Integer, max: 100
40
40
  # input :name, type: String, max: 255
41
41
  # input :items, type: Array, max: 50
@@ -47,7 +47,7 @@ module Servactory
47
47
  # Specify maximum value directly:
48
48
  #
49
49
  # ```ruby
50
- # class ProcessDataService < ApplicationService::Base
50
+ # class Data::Process < ApplicationService::Base
51
51
  # input :count, type: Integer, max: 100
52
52
  # input :name, type: String, max: 255
53
53
  # input :items, type: Array, max: 50
@@ -35,7 +35,7 @@ module Servactory
35
35
  # Use in your service definition:
36
36
  #
37
37
  # ```ruby
38
- # class ProcessDataService < ApplicationService::Base
38
+ # class Data::Process < ApplicationService::Base
39
39
  # input :age, type: Integer, min: 18
40
40
  # input :password, type: String, min: 8
41
41
  # input :tags, type: Array, min: 1
@@ -47,7 +47,7 @@ module Servactory
47
47
  # Specify minimum value directly:
48
48
  #
49
49
  # ```ruby
50
- # class ProcessDataService < ApplicationService::Base
50
+ # class Data::Process < ApplicationService::Base
51
51
  # input :age, type: Integer, min: 18
52
52
  # input :password, type: String, min: 8
53
53
  # input :tags, type: Array, min: 1
@@ -35,7 +35,7 @@ module Servactory
35
35
  # Use in your service definition:
36
36
  #
37
37
  # ```ruby
38
- # class ProcessOrderService < ApplicationService::Base
38
+ # class Orders::Process < ApplicationService::Base
39
39
  # input :quantity, type: Integer, multiple_of: 5
40
40
  # input :price, type: Float, multiple_of: 0.25
41
41
  # input :batch_size, type: Integer, multiple_of: 100
@@ -47,7 +47,7 @@ module Servactory
47
47
  # Specify divisor directly:
48
48
  #
49
49
  # ```ruby
50
- # class ProcessOrderService < ApplicationService::Base
50
+ # class Orders::Process < ApplicationService::Base
51
51
  # input :quantity, type: Integer, multiple_of: 5
52
52
  # input :price, type: Float, multiple_of: 0.25
53
53
  # input :batch_size, type: Integer, multiple_of: 100
@@ -29,7 +29,7 @@ module Servactory
29
29
  # Define schema in your service:
30
30
  #
31
31
  # ```ruby
32
- # class CreateUserService < ApplicationService::Base
32
+ # class Users::Create < ApplicationService::Base
33
33
  # input :user_data,
34
34
  # type: Hash,
35
35
  # schema: {
@@ -49,7 +49,7 @@ module Servactory
49
49
  # Specify schema definition directly:
50
50
  #
51
51
  # ```ruby
52
- # class CreateUserService < ApplicationService::Base
52
+ # class Users::Create < ApplicationService::Base
53
53
  # input :user_data,
54
54
  # type: Hash,
55
55
  # schema: {
@@ -127,7 +127,8 @@ module Servactory
127
127
  # Creates a Schema validator instance.
128
128
  #
129
129
  # @param option_name [Symbol] The option name (default: :schema)
130
- # @param default_hash_mode_class_names [Array<Class>] Valid Hash-like types
130
+ # @param default_hash_mode_class_names [Servactory::Configuration::HashMode::ClassNamesCollection]
131
+ # Valid Hash-like types
131
132
  # @return [Servactory::Maintenance::Attributes::OptionHelper]
132
133
  def self.use(option_name = :schema, default_hash_mode_class_names:)
133
134
  instance = new(option_name, :is, false)
@@ -137,7 +138,8 @@ module Servactory
137
138
 
138
139
  # Assigns the list of valid Hash-compatible class names.
139
140
  #
140
- # @param default_hash_mode_class_names [Array<Class>] Hash-like types to accept
141
+ # @param default_hash_mode_class_names [Servactory::Configuration::HashMode::ClassNamesCollection]
142
+ # Hash-like types to accept
141
143
  # @return [void]
142
144
  def assign(default_hash_mode_class_names)
143
145
  @default_hash_mode_class_names = default_hash_mode_class_names
@@ -37,8 +37,8 @@ module Servactory
37
37
  # Use in your service definition:
38
38
  #
39
39
  # ```ruby
40
- # class ProcessOrderService < ApplicationService::Base
41
- # input :service_class, type: Class, target: UserService
40
+ # class Orders::Process < ApplicationService::Base
41
+ # input :service_class, type: Class, target: Payments::Charge
42
42
  # input :handler_class, type: Class, target: [CreateHandler, UpdateHandler]
43
43
  # end
44
44
  # ```
@@ -48,7 +48,7 @@ module Servactory
48
48
  # Specify target class directly or as an array:
49
49
  #
50
50
  # ```ruby
51
- # input :service_class, type: Class, target: UserService
51
+ # input :service_class, type: Class, target: Payments::Charge
52
52
  # input :handler_class, type: Class, target: [CreateHandler, UpdateHandler]
53
53
  # ```
54
54
  #
@@ -5,7 +5,7 @@ module Servactory
5
5
  MAJOR = 3
6
6
  MINOR = 1
7
7
  PATCH = 0
8
- PRE = "rc2"
8
+ PRE = "rc3"
9
9
 
10
10
  STRING = [MAJOR, MINOR, PATCH, PRE].compact.join(".")
11
11
  end
data/lib/servactory.rb CHANGED
@@ -16,10 +16,6 @@ loader.inflector.inflect(
16
16
  )
17
17
  loader.setup
18
18
 
19
- # Eager load DSL to initialize Stroma::Registry.
20
- # Registry must be populated before any service class is defined.
21
- require_relative "servactory/dsl"
22
-
23
19
  module Servactory; end
24
20
 
25
21
  require "servactory/engine" if defined?(Rails::Engine)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: servactory
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0.rc2
4
+ version: 3.1.0.rc3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Sokolov
@@ -43,14 +43,20 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '0.4'
46
+ version: '0.5'
47
+ - - "<"
48
+ - !ruby/object:Gem::Version
49
+ version: '1.0'
47
50
  type: :runtime
48
51
  prerelease: false
49
52
  version_requirements: !ruby/object:Gem::Requirement
50
53
  requirements:
51
54
  - - ">="
52
55
  - !ruby/object:Gem::Version
53
- version: '0.4'
56
+ version: '0.5'
57
+ - - "<"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.0'
54
60
  - !ruby/object:Gem::Dependency
55
61
  name: zeitwerk
56
62
  requirement: !ruby/object:Gem::Requirement
@@ -184,6 +190,7 @@ executables: []
184
190
  extensions: []
185
191
  extra_rdoc_files: []
186
192
  files:
193
+ - LICENSE
187
194
  - README.md
188
195
  - Rakefile
189
196
  - config/locales/de.yml
@@ -191,6 +198,7 @@ files:
191
198
  - config/locales/es.yml
192
199
  - config/locales/fr.yml
193
200
  - config/locales/it.yml
201
+ - config/locales/ja.yml
194
202
  - config/locales/ru.yml
195
203
  - lib/generators/README.md
196
204
  - lib/generators/servactory/base.rb
@@ -357,7 +365,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
357
365
  - !ruby/object:Gem::Version
358
366
  version: '0'
359
367
  requirements: []
360
- rubygems_version: 4.0.4
368
+ rubygems_version: 4.0.6
361
369
  specification_version: 4
362
370
  summary: A set of tools for building reliable services of any complexity
363
371
  test_files: []