servactory 3.1.0.rc3 → 3.1.0.rc4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -0
  3. data/lib/generators/README.md +1 -1
  4. data/lib/generators/servactory/extension/USAGE +1 -1
  5. data/lib/generators/servactory/install/USAGE +2 -2
  6. data/lib/generators/servactory/rspec/USAGE +4 -4
  7. data/lib/generators/servactory/rspec/rspec_generator.rb +2 -2
  8. data/lib/generators/servactory/service/USAGE +5 -5
  9. data/lib/servactory/actions/collection.rb +21 -5
  10. data/lib/servactory/actions/stages/collection.rb +19 -1
  11. data/lib/servactory/actions/tools/rules.rb +1 -1
  12. data/lib/servactory/actions/tools/runner.rb +1 -1
  13. data/lib/servactory/configuration/collection_mode/class_names_collection.rb +1 -1
  14. data/lib/servactory/configuration/configurable.rb +1 -1
  15. data/lib/servactory/configuration/factory.rb +30 -3
  16. data/lib/servactory/configuration/hash_mode/class_names_collection.rb +2 -1
  17. data/lib/servactory/configuration/option_helpers/option_helpers_collection.rb +150 -37
  18. data/lib/servactory/context/warehouse/inputs.rb +1 -1
  19. data/lib/servactory/context/workspace/inputs.rb +2 -2
  20. data/lib/servactory/context/workspace/internals.rb +2 -2
  21. data/lib/servactory/context/workspace/outputs.rb +2 -2
  22. data/lib/servactory/context/workspace.rb +7 -7
  23. data/lib/servactory/info/builder.rb +4 -3
  24. data/lib/servactory/inputs/dsl.rb +5 -1
  25. data/lib/servactory/inputs/input.rb +1 -2
  26. data/lib/servactory/internals/dsl.rb +7 -1
  27. data/lib/servactory/maintenance/attributes/base.rb +4 -1
  28. data/lib/servactory/maintenance/attributes/collection.rb +61 -6
  29. data/lib/servactory/maintenance/options/collection.rb +46 -15
  30. data/lib/servactory/maintenance/options/option.rb +3 -12
  31. data/lib/servactory/maintenance/options/registrar.rb +2 -12
  32. data/lib/servactory/maintenance/validations/checkers/type.rb +23 -5
  33. data/lib/servactory/outputs/dsl.rb +7 -1
  34. data/lib/servactory/result.rb +21 -8
  35. data/lib/servactory/test_kit/rspec/helpers/exception_validator.rb +112 -0
  36. data/lib/servactory/test_kit/rspec/helpers/input_validator.rb +3 -1
  37. data/lib/servactory/test_kit/rspec/helpers/mock_executor.rb +125 -129
  38. data/lib/servactory/test_kit/rspec/helpers/service_inputs_guard.rb +98 -0
  39. data/lib/servactory/test_kit/rspec/helpers/service_inputs_matcher.rb +75 -0
  40. data/lib/servactory/test_kit/rspec/helpers/service_mock_builder.rb +110 -45
  41. data/lib/servactory/test_kit/rspec/helpers/service_mock_config.rb +26 -8
  42. data/lib/servactory/test_kit/rspec/helpers/wrap_block_adapter.rb +74 -0
  43. data/lib/servactory/test_kit/rspec/helpers.rb +4 -0
  44. data/lib/servactory/test_kit/rspec/matchers/base/attribute_matcher.rb +72 -16
  45. data/lib/servactory/test_kit/rspec/matchers/base/message_expectation.rb +194 -0
  46. data/lib/servactory/test_kit/rspec/matchers/base/submatcher.rb +8 -0
  47. data/lib/servactory/test_kit/rspec/matchers/concerns/option_helper_rules.rb +148 -0
  48. data/lib/servactory/test_kit/rspec/matchers/concerns/proc_message.rb +117 -0
  49. data/lib/servactory/test_kit/rspec/matchers/concerns/required_message.rb +92 -0
  50. data/lib/servactory/test_kit/rspec/matchers/have_service_input_matcher.rb +27 -4
  51. data/lib/servactory/test_kit/rspec/matchers/have_service_internal_matcher.rb +10 -3
  52. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/required_submatcher.rb +58 -23
  53. data/lib/servactory/test_kit/rspec/matchers/submatchers/input/valid_with_submatcher.rb +124 -59
  54. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/message_submatcher.rb +133 -39
  55. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/must_submatcher.rb +192 -17
  56. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/target_submatcher.rb +4 -2
  57. data/lib/servactory/test_kit/rspec/matchers/submatchers/shared/types_submatcher.rb +6 -0
  58. data/lib/servactory/test_kit/rspec/matchers.rb +15 -0
  59. data/lib/servactory/tool_kit/dynamic_options/consists_of.rb +29 -8
  60. data/lib/servactory/tool_kit/dynamic_options/format.rb +2 -3
  61. data/lib/servactory/tool_kit/dynamic_options/inclusion.rb +1 -1
  62. data/lib/servactory/tool_kit/dynamic_options/max.rb +1 -1
  63. data/lib/servactory/tool_kit/dynamic_options/min.rb +1 -1
  64. data/lib/servactory/tool_kit/dynamic_options/multiple_of.rb +33 -5
  65. data/lib/servactory/tool_kit/dynamic_options/must.rb +2 -3
  66. data/lib/servactory/tool_kit/dynamic_options/schema.rb +34 -21
  67. data/lib/servactory/tool_kit/dynamic_options/target.rb +1 -1
  68. data/lib/servactory/utils.rb +13 -3
  69. data/lib/servactory/version.rb +1 -1
  70. data/lib/servactory.rb +3 -0
  71. metadata +10 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5acfb374e0e85d7779b99658fcb899007ca346ca63c71344800490428ac61dd
4
- data.tar.gz: 94ff484e768908909aa856d63310af3c896f198357b88c3089fe9393de53bc88
3
+ metadata.gz: f404944fef3e958464a2287b2aa1ca008cd5c6a04b0de7dbf3099f871878eee2
4
+ data.tar.gz: fd55568c5b5a48003ce84f1cb9aa64adbae0140b707517f99186ab2064127d94
5
5
  SHA512:
6
- metadata.gz: 61facdd48b89752c3f2bfe0735c87c24f83c225c2474608c9aa871f150003a996b2b4775ef25941f0916f45524383cdf1e1814a237b522add763782c2ec03e9b
7
- data.tar.gz: 13202fe9276b8323cd83fc2c0919613eeabfd9d99b4d1cfac541ef7ed88739a84c6d5719a44ca76fe960c33aaf6a66b1b9c9353e12a1e5662b8e19df05a68fd3
6
+ metadata.gz: a2a7b707d93c5e210c9c0eefb54bbfd3374ac48fda5afbcce8669ec4eda8f1358c063a9a7c8121b3ea6c651fc4c8748e380f72c8a33ca13ab586d39b89fbe505
7
+ data.tar.gz: 36f8a525ca8a622771136ed8955a74091fd2a17a3aa444871c711048c45f60c6e612eaec28cd7220f1663762095fac203d9cb7996b7630448cc03d2964763cd7
data/README.md CHANGED
@@ -96,6 +96,11 @@ class SessionsController < ApplicationController
96
96
  end
97
97
  ```
98
98
 
99
+ ## 🧩 Ecosystem
100
+
101
+ - [**Stroma**](https://github.com/servactory/stroma) — A foundation for building modular, extensible DSLs in Ruby
102
+ - [**OpenTelemetry Instrumentation**](https://github.com/servactory/opentelemetry-instrumentation-servactory) — Automatic tracing of service calls and actions with span attributes and failure detection
103
+
99
104
  ## 🤝 Contributing
100
105
 
101
106
  We love contributions! Check out our [Contributing Guide](https://servactory.com/CONTRIBUTING) to get started.
@@ -19,7 +19,7 @@ rails generate servactory:service NAME [inputs] [options]
19
19
  ```
20
20
 
21
21
  **Arguments:**
22
- - `NAME` — Service name (e.g., `Users::Create`, `ProcessOrder`)
22
+ - `NAME` — Service name (e.g., `Users::Create`, `Orders::Process`)
23
23
  - `inputs` — Input attributes with optional types (e.g., `email:string name:String user:User`)
24
24
 
25
25
  **Options:**
@@ -47,7 +47,7 @@ Usage in Service:
47
47
 
48
48
  Then use the DSL method in your services:
49
49
 
50
- class MyService < ApplicationService::Base
50
+ class Users::Create < ApplicationService::Base
51
51
  my_extension!(my_setting: "value")
52
52
 
53
53
  # ...
@@ -6,7 +6,7 @@ Usage:
6
6
 
7
7
  Options:
8
8
  --namespace=NAMESPACE Base namespace for services (default: ApplicationService)
9
- --locales=LOCALES Locales to install (available: en, ru, de, fr, es, it)
9
+ --locales=LOCALES Locales to install (available: en, ru, de, fr, es, it, ja)
10
10
  --minimal Generate minimal setup without configuration examples
11
11
  --path=PATH Path to install service files (default: app/services)
12
12
 
@@ -21,7 +21,7 @@ Examples:
21
21
  Creates app/services/my_app/services/ with all files
22
22
 
23
23
  rails generate servactory:install --locales=en,de,fr
24
- Also copies locale files to config/locales/ (available: en, ru, de, fr, es, it)
24
+ Also copies locale files to config/locales/ (available: en, ru, de, fr, es, it, ja)
25
25
 
26
26
  rails generate servactory:install --minimal
27
27
  Creates minimal setup without configuration examples
@@ -5,7 +5,7 @@ Usage:
5
5
  rails generate servactory:rspec NAME [inputs] [options]
6
6
 
7
7
  Arguments:
8
- NAME The name of the service (e.g., Users::Create, ProcessOrder)
8
+ NAME The name of the service (e.g., Users::Create, Orders::Process)
9
9
  inputs Input attributes with optional types (e.g., email:string, user:User)
10
10
 
11
11
  Options:
@@ -33,14 +33,14 @@ Examples:
33
33
  rails generate servactory:rspec Users::Create email:String name:String
34
34
  Creates spec/services/users/create_spec.rb with Ruby class types
35
35
 
36
- rails generate servactory:rspec ProcessOrder user:User event:Event
36
+ rails generate servactory:rspec Orders::Process user:User event:Event
37
37
  Creates spec with custom model/class type inputs
38
38
 
39
39
  rails generate servactory:rspec Users::Create --path=lib/my_gem/services
40
40
  Creates spec/my_gem/services/users/create_spec.rb for gem/library use
41
41
 
42
- rails generate servactory:rspec ProcessOrder --call-method=call
42
+ rails generate servactory:rspec Orders::Process --call-method=call
43
43
  Creates spec using .call instead of .call!
44
44
 
45
- rails generate servactory:rspec CacheData --skip-pending
45
+ rails generate servactory:rspec Caches::Write --skip-pending
46
46
  Creates spec without pending placeholder
@@ -78,8 +78,8 @@ module Servactory
78
78
  when "Float" then "1.0"
79
79
  when "TrueClass", "[TrueClass, FalseClass]" then "true"
80
80
  when "FalseClass" then "false"
81
- when "Array" then "[]"
82
- when "Hash" then "{}"
81
+ when "Array" then '["example"]'
82
+ when "Hash" then '{ key: "value" }'
83
83
  when "Symbol" then ":example"
84
84
  when "Date" then "Date.current"
85
85
  when "DateTime" then "DateTime.current"
@@ -5,7 +5,7 @@ Usage:
5
5
  rails generate servactory:service NAME [inputs] [options]
6
6
 
7
7
  Arguments:
8
- NAME The name of the service (e.g., Users::Create, ProcessOrder)
8
+ NAME The name of the service (e.g., Users::Create, Orders::Process)
9
9
  inputs Input attributes with optional types (e.g., email:string, user:User)
10
10
 
11
11
  Options:
@@ -38,14 +38,14 @@ Examples:
38
38
  rails generate servactory:service Users::Create email:String name:String
39
39
  Creates service with typed inputs (capitalized Ruby class names)
40
40
 
41
- rails generate servactory:service ProcessOrder user:User event:Event
41
+ rails generate servactory:service Orders::Process user:User event:Event
42
42
  Creates service with custom model/class type inputs
43
43
 
44
- rails generate servactory:service ProcessOrder role:Admin::Users::Role
44
+ rails generate servactory:service Orders::Process role:Admin::Users::Role
45
45
  Creates service with nested namespace type
46
46
 
47
- rails generate servactory:service SendEmail recipient
47
+ rails generate servactory:service Emails::Send recipient
48
48
  Creates service with untyped input (defaults to String)
49
49
 
50
- rails generate servactory:service CacheData key:String --skip-output
50
+ rails generate servactory:service Caches::Write key:String --skip-output
51
51
  Creates service without default output declaration
@@ -30,10 +30,6 @@ module Servactory
30
30
  class Collection
31
31
  extend Forwardable
32
32
 
33
- # @!method <<(action)
34
- # Adds an action to the collection.
35
- # @param action [Action] The action to add
36
- # @return [Set] The updated collection
37
33
  # @!method each
38
34
  # Iterates over all actions in the collection.
39
35
  # @yield [Action] Each action in the collection
@@ -50,7 +46,7 @@ module Servactory
50
46
  # @!method empty?
51
47
  # Checks if the collection is empty.
52
48
  # @return [Boolean] true if no actions registered
53
- def_delegators :@collection, :<<, :each, :to_h, :sort_by, :size, :empty?
49
+ def_delegators :@collection, :each, :to_h, :sort_by, :size, :empty?
54
50
 
55
51
  # Creates a new actions collection.
56
52
  #
@@ -59,6 +55,26 @@ module Servactory
59
55
  @collection = collection
60
56
  end
61
57
 
58
+ # Duplicates the collection, resetting memoized caches.
59
+ #
60
+ # @param original [Collection] The collection being duplicated
61
+ # @return [void]
62
+ def initialize_dup(original)
63
+ super
64
+ @collection = original.instance_variable_get(:@collection).dup
65
+ @sorted_by_position = nil
66
+ end
67
+
68
+ # Adds an action to the collection, invalidating the sorted cache.
69
+ #
70
+ # @param action [Action] The action to add
71
+ # @return [Collection] self
72
+ def <<(action)
73
+ @collection << action
74
+ @sorted_by_position = nil
75
+ self
76
+ end
77
+
62
78
  # Returns a new collection with actions sorted by position.
63
79
  #
64
80
  # @return [Collection] New collection with actions in position order
@@ -6,12 +6,30 @@ module Servactory
6
6
  class Collection
7
7
  extend Forwardable
8
8
 
9
- def_delegators :@collection, :<<, :each, :merge, :to_h, :sort_by, :size, :empty?
9
+ def_delegators :@collection, :each, :to_h, :sort_by, :size, :empty?
10
10
 
11
11
  def initialize(collection = Set.new)
12
12
  @collection = collection
13
13
  end
14
14
 
15
+ def initialize_dup(original)
16
+ super
17
+ @collection = original.instance_variable_get(:@collection).dup
18
+ @sorted_by_position = nil
19
+ end
20
+
21
+ def <<(stage)
22
+ @collection << stage
23
+ @sorted_by_position = nil
24
+ self
25
+ end
26
+
27
+ def merge(other)
28
+ @collection.merge(other)
29
+ @sorted_by_position = nil
30
+ self
31
+ end
32
+
15
33
  def sorted_by_position
16
34
  @sorted_by_position ||= Collection.new(sort_by(&:position))
17
35
  end
@@ -56,7 +56,7 @@ module Servactory
56
56
  end
57
57
 
58
58
  def raise_message_with!(formatted_text)
59
- raise @context.config.failure_class.new(
59
+ raise @context.class.config.failure_class.new(
60
60
  type: :base,
61
61
  message: @context.send(:servactory_service_info).translate(
62
62
  "methods.cannot_be_overwritten",
@@ -179,7 +179,7 @@ module Servactory
179
179
  # @param exception [Exception] The exception to handle
180
180
  # @return [Exception, nil] The exception if handled, nil otherwise
181
181
  def rescue_with_handler(exception) # rubocop:disable Metrics/MethodLength
182
- _, handler = @context.config.action_rescue_handlers.reverse_each.detect do |class_or_name, _|
182
+ _, handler = @context.class.config.action_rescue_handlers.reverse_each.detect do |class_or_name, _|
183
183
  if (detected_exception = Servactory::Utils.constantize_class(class_or_name))
184
184
  detected_exception === exception # rubocop:disable Style/CaseEquality
185
185
  end
@@ -6,7 +6,7 @@ module Servactory
6
6
  class ClassNamesCollection
7
7
  extend Forwardable
8
8
 
9
- def_delegators :@collection, :merge, :intersect?
9
+ def_delegators :@collection, :merge, :intersect?, :to_a
10
10
 
11
11
  def initialize(collection)
12
12
  @collection = collection
@@ -63,7 +63,7 @@ module Servactory
63
63
 
64
64
  def default_input_option_helpers(config)
65
65
  Set[
66
- Servactory::Maintenance::Attributes::OptionHelper
66
+ Servactory::Maintenance::Options::Helper
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),
@@ -60,15 +60,15 @@ module Servactory
60
60
  end
61
61
 
62
62
  def input_option_helpers(input_option_helpers)
63
- @config.input_option_helpers.merge(input_option_helpers)
63
+ register_option_helpers_with(:input_option_helpers, @config.input_option_helpers, input_option_helpers)
64
64
  end
65
65
 
66
66
  def internal_option_helpers(internal_option_helpers)
67
- @config.internal_option_helpers.merge(internal_option_helpers)
67
+ register_option_helpers_with(:internal_option_helpers, @config.internal_option_helpers, internal_option_helpers)
68
68
  end
69
69
 
70
70
  def output_option_helpers(output_option_helpers)
71
- @config.output_option_helpers.merge(output_option_helpers)
71
+ register_option_helpers_with(:output_option_helpers, @config.output_option_helpers, output_option_helpers)
72
72
  end
73
73
 
74
74
  def collection_mode_class_names(collection_mode_class_names)
@@ -113,6 +113,19 @@ module Servactory
113
113
  value.is_a?(TrueClass) || value.is_a?(FalseClass)
114
114
  end
115
115
 
116
+ def register_option_helpers_with(config_name, collection, option_helpers)
117
+ option_helpers.each do |option_helper|
118
+ case collection.register(option_helper)
119
+ when :reserved
120
+ raise_error_about_reserved_option_helper_with(config_name, option_helper.name)
121
+ when :duplicated
122
+ raise_error_about_duplicated_option_helper_with(config_name, option_helper.name)
123
+ end
124
+ end
125
+
126
+ collection
127
+ end
128
+
116
129
  ##########################################################################
117
130
 
118
131
  def raise_error_about_wrong_exception_class_with(config_name, value)
@@ -142,6 +155,20 @@ module Servactory
142
155
  "The `#{value.inspect}` value must be `TrueClass` or `FalseClass`. " \
143
156
  "See configuration example here: https://servactory.com/guide/configuration"
144
157
  end
158
+
159
+ def raise_error_about_reserved_option_helper_with(config_name, option_helper_name)
160
+ raise ArgumentError,
161
+ "Error in `#{config_name}` configuration. " \
162
+ "The `#{option_helper_name}` option helper name is reserved by a built-in option helper. " \
163
+ "See configuration example here: https://servactory.com/guide/configuration"
164
+ end
165
+
166
+ def raise_error_about_duplicated_option_helper_with(config_name, option_helper_name)
167
+ raise ArgumentError,
168
+ "Error in `#{config_name}` configuration. " \
169
+ "The `#{option_helper_name}` option helper is already defined in this configuration. " \
170
+ "See configuration example here: https://servactory.com/guide/configuration"
171
+ end
145
172
  end
146
173
  end
147
174
  end
@@ -9,7 +9,8 @@ module Servactory
9
9
  def_delegators :@collection,
10
10
  :merge,
11
11
  :include?,
12
- :intersect?
12
+ :intersect?,
13
+ :to_a
13
14
 
14
15
  def initialize(collection)
15
16
  @collection = collection
@@ -9,7 +9,20 @@ module Servactory
9
9
  #
10
10
  # OptionHelpersCollection provides a unified interface for storing and
11
11
  # querying Helper instances used by the configuration system.
12
- # It wraps a Set to ensure uniqueness and delegates common enumeration methods.
12
+ # It keeps helpers in an insertion-ordered Hash keyed by helper name,
13
+ # so every name resolves to exactly one helper.
14
+ #
15
+ # ## Built-in Helpers
16
+ #
17
+ # Helpers passed to the constructor are built-in: their names are
18
+ # reserved, and only `replace` may swap them (used to rebind built-in
19
+ # dynamic options to the configuration of a subclass).
20
+ #
21
+ # ## Inheritance
22
+ #
23
+ # A collection accepts one helper per name. A duplicate, made when
24
+ # a service class is inherited, may replace a helper registered before
25
+ # duplication in place without affecting the original.
13
26
  #
14
27
  # ## Usage
15
28
  #
@@ -17,84 +30,184 @@ module Servactory
17
30
  # to manage registered option helpers:
18
31
  #
19
32
  # ```ruby
20
- # collection = OptionHelpersCollection.new
21
- # collection << helper
33
+ # collection = OptionHelpersCollection.new(builtin_helpers)
34
+ # collection.register(helper) # => :registered
35
+ # collection << other_helper # => collection, or raises ArgumentError
36
+ # collection.merge([third_helper]) # => collection, or raises ArgumentError
22
37
  #
23
38
  # collection.find_by(name: :must) # => helper instance
24
39
  # collection.dynamic_options # => filtered OptionHelpersCollection
25
40
  # ```
26
41
  #
42
+ # ## Enumeration
43
+ #
44
+ # The collection includes Enumerable and yields helpers in registration
45
+ # order. Enumerable methods return plain values rather than collections:
46
+ # `filter` and `map` return Arrays, `find` returns a helper or nil, and
47
+ # `each_with_object` returns the memo object. Use `dynamic_options`
48
+ # to get a filtered OptionHelpersCollection.
49
+ #
27
50
  # ## Performance
28
51
  #
29
- # The collection uses memoization for frequently accessed data:
30
- # - `helpers_index` - cached hash for O(1) lookups by name
52
+ # Registration and lookup by name are O(1) Hash operations without
53
+ # cached indexes, so helpers registered after attributes are declared
54
+ # are visible to subsequent declarations.
31
55
  #
32
56
  class OptionHelpersCollection
33
- extend Forwardable
34
-
35
- def_delegators :@collection,
36
- :<<,
37
- :filter,
38
- :each_with_object,
39
- :map,
40
- :merge
57
+ include Enumerable
41
58
 
42
- # Initializes the collection with an optional pre-built Set.
59
+ # Initializes the collection with built-in helpers.
43
60
  #
44
- # @param collection [Set] initial set of helpers
61
+ # @param builtin_helpers [Enumerable<Maintenance::Options::Helper>] helpers whose names are reserved
45
62
  # @return [OptionHelpersCollection]
46
- def initialize(collection = Set.new)
47
- @collection = collection
63
+ def initialize(builtin_helpers = [])
64
+ @helpers = builtin_helpers.to_h { |helper| [helper.name, helper] }
65
+ @builtin_names = @helpers.keys.to_set.freeze
66
+ @own_names = Set.new
48
67
  end
49
68
 
50
- # Duplicates the collection, resetting memoized caches.
69
+ # Duplicates the collection so that registrations do not leak into the original.
70
+ #
71
+ # Helpers of the original may be replaced once in the duplicate.
51
72
  #
52
73
  # @param original [OptionHelpersCollection] the collection being duplicated
53
74
  # @return [void]
54
75
  def initialize_dup(original)
55
76
  super
56
- @collection = original.instance_variable_get(:@collection).dup
57
- @helpers_index = nil
77
+ @helpers = original.instance_variable_get(:@helpers).dup
78
+ @own_names = Set.new
79
+ end
80
+
81
+ # Iterates over helpers in registration order.
82
+ #
83
+ # @yieldparam helper [Maintenance::Options::Helper] each registered helper
84
+ # @return [OptionHelpersCollection, Enumerator] self, or an enumerator without a block
85
+ def each(&block)
86
+ return enum_for(:each) unless block_given?
87
+
88
+ @helpers.each_value(&block)
89
+ self
90
+ end
91
+
92
+ # Registers a helper under its name.
93
+ #
94
+ # A helper with a new name is appended; a helper replacing one registered
95
+ # before duplication keeps its position. The helpers are left unchanged
96
+ # unless the result is `:registered`.
97
+ #
98
+ # @param helper [Maintenance::Options::Helper] the helper to register
99
+ # @return [Symbol] `:registered`, `:skipped` when this very helper is
100
+ # already registered (the name then counts as registered by this collection
101
+ # itself), `:reserved` when the name belongs to a built-in helper,
102
+ # or `:duplicated` when this collection already registered the name itself
103
+ # since it was created or duplicated
104
+ def register(helper)
105
+ name = helper.name
106
+
107
+ if @helpers[name].equal?(helper)
108
+ @own_names << name
109
+ return :skipped
110
+ end
111
+
112
+ return :reserved if @builtin_names.include?(name)
113
+ return :duplicated if @own_names.include?(name)
114
+
115
+ @own_names << name
116
+ @helpers[name] = helper
117
+ :registered
118
+ end
119
+
120
+ # Registers a helper under its name, raising when the name is rejected.
121
+ #
122
+ # Follows `register`: a helper registered before duplication may be
123
+ # replaced once. Re-adding this very helper leaves the helpers unchanged
124
+ # but still counts as this collection registering the name itself, so
125
+ # after re-adding an inherited helper a different helper with that name
126
+ # raises instead of replacing it.
127
+ #
128
+ # @param helper [Maintenance::Options::Helper] the helper to register
129
+ # @return [OptionHelpersCollection] self
130
+ # @raise [ArgumentError] when the name belongs to a built-in helper
131
+ # or this collection already registered the name itself
132
+ def <<(helper)
133
+ case register(helper)
134
+ when :reserved
135
+ raise_error_about_reserved_name_with(helper.name)
136
+ when :duplicated
137
+ raise_error_about_duplicated_name_with(helper.name)
138
+ end
139
+
140
+ self
141
+ end
142
+
143
+ # Registers every helper of the given enumerables in order with `<<`.
144
+ #
145
+ # Helpers preceding a rejected one stay registered.
146
+ #
147
+ # A built-in helper coming from another service's collection counts as
148
+ # a reserved name unless it is the very helper of this collection. Each
149
+ # service rebinds the built-in dynamic options (`consists_of`, `schema`)
150
+ # to its own configuration, so merging another service's whole
151
+ # collection raises.
152
+ #
153
+ # @param enums [Array<Enumerable<Maintenance::Options::Helper>>] the helpers to register
154
+ # @return [OptionHelpersCollection] self
155
+ # @raise [ArgumentError] when `<<` rejects a helper
156
+ def merge(*enums)
157
+ enums.each { |helpers| helpers.each { |helper| self << helper } }
158
+ self
58
159
  end
59
160
 
60
161
  # Returns a new collection containing only dynamic option helpers.
61
162
  #
62
163
  # @return [OptionHelpersCollection] filtered collection of dynamic helpers
63
164
  def dynamic_options
64
- OptionHelpersCollection.new(filter(&:dynamic_option?))
165
+ each_with_object(OptionHelpersCollection.new) do |helper, collection|
166
+ collection.register(helper) if helper.dynamic_option?
167
+ end
65
168
  end
66
169
 
67
- # Finds a helper by its name using indexed lookup.
170
+ # Finds a helper by its name with a direct Hash lookup.
68
171
  #
69
172
  # @param name [Symbol] the helper name to find
70
173
  # @return [Maintenance::Options::Helper, nil] the found helper or nil
71
174
  def find_by(name:)
72
- helpers_index[name]
175
+ @helpers[name]
73
176
  end
74
177
 
75
- # Replaces a helper by name with a new one, invalidating the index cache.
178
+ # Replaces a built-in helper by name with a new one, keeping its position.
179
+ #
180
+ # Names that do not belong to a built-in helper are ignored.
76
181
  #
77
- # @param name [Symbol] the helper name to replace
78
- # @param with [Maintenance::Attributes::OptionHelper] the replacement helper
182
+ # @param name [Symbol] the built-in helper name to replace
183
+ # @param with [Maintenance::Options::Helper] the replacement helper
79
184
  # @return [void]
80
185
  def replace(name:, with:)
81
- old = find_by(name:)
82
- return unless old
186
+ return unless @builtin_names.include?(name)
83
187
 
84
- @collection.delete(old)
85
- @collection.add(with)
86
- @helpers_index = nil
188
+ @helpers[name] = with
87
189
  end
88
190
 
89
191
  private
90
192
 
91
- # Builds and caches a hash index for O(1) helper lookups.
193
+ # Raises an error about a name that belongs to a built-in helper.
92
194
  #
93
- # @return [Hash{Symbol => Maintenance::Options::Helper}] helper names mapped to Helper instances
94
- def helpers_index
95
- @helpers_index ||= each_with_object({}) do |helper, index|
96
- index[helper.name] = helper
97
- end
195
+ # @param name [Symbol] the rejected helper name
196
+ # @raise [ArgumentError] always
197
+ def raise_error_about_reserved_name_with(name)
198
+ raise ArgumentError,
199
+ "The `#{name}` option helper name is reserved by a built-in option helper. " \
200
+ "See configuration example here: https://servactory.com/guide/configuration"
201
+ end
202
+
203
+ # Raises an error about a name this collection already registered itself.
204
+ #
205
+ # @param name [Symbol] the rejected helper name
206
+ # @raise [ArgumentError] always
207
+ def raise_error_about_duplicated_name_with(name)
208
+ raise ArgumentError,
209
+ "The `#{name}` option helper is already registered in this collection. " \
210
+ "See configuration example here: https://servactory.com/guide/configuration"
98
211
  end
99
212
  end
100
213
  end
@@ -45,7 +45,7 @@ module Servactory
45
45
  # @param _args [Array] Method arguments (unused)
46
46
  # @return [Object] Input value or predicate result
47
47
  def method_missing(name, *_args)
48
- predicate = @context.config.predicate_methods_enabled && name.end_with?("?")
48
+ predicate = @context.class.config.predicate_methods_enabled && name.end_with?("?")
49
49
 
50
50
  input_name = predicate ? name.to_s.chomp("?").to_sym : name
51
51
 
@@ -38,7 +38,7 @@ module Servactory
38
38
  private
39
39
 
40
40
  def fetch_with(name:, &_block)
41
- predicate = @context.config.predicate_methods_enabled && name.end_with?("?")
41
+ predicate = @context.class.config.predicate_methods_enabled && name.end_with?("?")
42
42
 
43
43
  input_name = predicate ? name.to_s.chomp("?").to_sym : name
44
44
 
@@ -67,7 +67,7 @@ module Servactory
67
67
  def resolve_input(name)
68
68
  return true if @collection_of_inputs.find_by(name:)
69
69
 
70
- @context.config.predicate_methods_enabled &&
70
+ @context.class.config.predicate_methods_enabled &&
71
71
  name.to_s.end_with?("?") &&
72
72
  @collection_of_inputs.find_by(name: name.to_s.chomp("?").to_sym)
73
73
  end
@@ -60,7 +60,7 @@ module Servactory
60
60
  end
61
61
 
62
62
  def fetch_with(name:, &_block)
63
- predicate = @context.config.predicate_methods_enabled && name.end_with?("?")
63
+ predicate = @context.class.config.predicate_methods_enabled && name.end_with?("?")
64
64
 
65
65
  internal_name = predicate ? name.to_s.chomp("?").to_sym : name
66
66
  internal = @collection_of_internals.find_by(name: internal_name)
@@ -89,7 +89,7 @@ module Servactory
89
89
  def resolve_internal(name)
90
90
  return true if @collection_of_internals.find_by(name:)
91
91
 
92
- @context.config.predicate_methods_enabled &&
92
+ @context.class.config.predicate_methods_enabled &&
93
93
  name.to_s.end_with?("?") &&
94
94
  @collection_of_internals.find_by(name: name.to_s.chomp("?").to_sym)
95
95
  end
@@ -52,7 +52,7 @@ module Servactory
52
52
  end
53
53
 
54
54
  def fetch_with(name:, &_block)
55
- predicate = @context.config.predicate_methods_enabled && name.end_with?("?")
55
+ predicate = @context.class.config.predicate_methods_enabled && name.end_with?("?")
56
56
 
57
57
  output_name = predicate ? name.to_s.chomp("?").to_sym : name
58
58
  output = @collection_of_outputs.find_by(name: output_name)
@@ -67,7 +67,7 @@ module Servactory
67
67
  def resolve_output(name)
68
68
  return true if @collection_of_outputs.find_by(name:)
69
69
 
70
- @context.config.predicate_methods_enabled &&
70
+ @context.class.config.predicate_methods_enabled &&
71
71
  name.to_s.end_with?("?") &&
72
72
  @collection_of_outputs.find_by(name: name.to_s.chomp("?").to_sym)
73
73
  end