cmdx 0.5.0 → 1.0.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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +31 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +203 -31
  23. data/docs/outcomes/statuses.md +275 -30
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +367 -25
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +405 -37
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -62
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -35
  121. data/lib/cmdx/run.rb +0 -39
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -20
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -2,8 +2,59 @@
2
2
 
3
3
  module CMDx
4
4
  module Utils
5
+ # Utility for adding ANSI color codes to terminal output.
6
+ #
7
+ # AnsiColor provides methods to colorize text output in terminal
8
+ # environments, supporting various colors and text modes for
9
+ # enhanced readability of logs and console output. Used extensively
10
+ # by CMDx's pretty formatters to provide visual distinction between
11
+ # different log levels, statuses, and metadata.
12
+ #
13
+ # @example Basic color usage
14
+ # Utils::AnsiColor.call("Error", color: :red)
15
+ # # => "\e[0;31;49mError\e[0m"
16
+ #
17
+ # @example Color with text modes
18
+ # Utils::AnsiColor.call("Warning", color: :yellow, mode: :bold)
19
+ # Utils::AnsiColor.call("Info", color: :blue, mode: :underline)
20
+ #
21
+ # @example Log severity coloring
22
+ # Utils::AnsiColor.call("ERROR", color: :red, mode: :bold)
23
+ # Utils::AnsiColor.call("WARN", color: :yellow)
24
+ # Utils::AnsiColor.call("INFO", color: :blue)
25
+ # Utils::AnsiColor.call("DEBUG", color: :light_black)
26
+ #
27
+ # @example Status indicator coloring
28
+ # Utils::AnsiColor.call("success", color: :green, mode: :bold)
29
+ # Utils::AnsiColor.call("failed", color: :red, mode: :bold)
30
+ # Utils::AnsiColor.call("skipped", color: :yellow)
31
+ #
32
+ # @example Available colors
33
+ # Utils::AnsiColor.call("Text", color: :red)
34
+ # Utils::AnsiColor.call("Text", color: :green)
35
+ # Utils::AnsiColor.call("Text", color: :blue)
36
+ # Utils::AnsiColor.call("Text", color: :light_cyan)
37
+ # Utils::AnsiColor.call("Text", color: :magenta)
38
+ #
39
+ # @example Available text modes
40
+ # Utils::AnsiColor.call("Bold", color: :white, mode: :bold)
41
+ # Utils::AnsiColor.call("Italic", color: :white, mode: :italic)
42
+ # Utils::AnsiColor.call("Underline", color: :white, mode: :underline)
43
+ # Utils::AnsiColor.call("Strikethrough", color: :white, mode: :strike)
44
+ #
45
+ # @see CMDx::ResultAnsi Uses this for result status coloring
46
+ # @see CMDx::LoggerAnsi Uses this for log severity coloring
47
+ # @see CMDx::LogFormatters::PrettyLine Uses this for colorized log output
48
+ # @see CMDx::LogFormatters::PrettyKeyValue Uses this for colorized key-value pairs
5
49
  module AnsiColor
6
50
 
51
+ # Available color codes for text coloring.
52
+ #
53
+ # Maps color names to their corresponding ANSI escape code numbers.
54
+ # Includes both standard and light variants of common colors for
55
+ # flexible visual styling in terminal environments.
56
+ #
57
+ # @return [Hash<Symbol, Integer>] mapping of color names to ANSI codes
7
58
  COLOR_CODES = {
8
59
  black: 30,
9
60
  red: 31,
@@ -23,6 +74,14 @@ module CMDx
23
74
  light_cyan: 96,
24
75
  light_white: 97
25
76
  }.freeze
77
+
78
+ # Available text mode codes for formatting.
79
+ #
80
+ # Maps text formatting mode names to their corresponding ANSI escape
81
+ # code numbers. Provides various text styling options including bold,
82
+ # italic, underline, and other visual effects.
83
+ #
84
+ # @return [Hash<Symbol, Integer>] mapping of mode names to ANSI codes
26
85
  MODE_CODES = {
27
86
  default: 0,
28
87
  bold: 1,
@@ -42,6 +101,42 @@ module CMDx
42
101
 
43
102
  module_function
44
103
 
104
+ # Apply ANSI color and mode formatting to text.
105
+ #
106
+ # Wraps the provided text with ANSI escape codes to apply the specified
107
+ # color and formatting mode. The resulting string will display with the
108
+ # requested styling in ANSI-compatible terminals and will gracefully
109
+ # degrade in non-ANSI environments.
110
+ #
111
+ # @param value [String] text to colorize
112
+ # @param color [Symbol] color name from COLOR_CODES
113
+ # @param mode [Symbol] text mode from MODE_CODES (defaults to :default)
114
+ # @return [String] text wrapped with ANSI escape codes
115
+ # @raise [KeyError] if color or mode is not found in the respective code maps
116
+ #
117
+ # @example Success message with green bold text
118
+ # AnsiColor.call("Success", color: :green, mode: :bold)
119
+ # # => "\e[1;32;49mSuccess\e[0m"
120
+ #
121
+ # @example Error message with red text
122
+ # AnsiColor.call("Error", color: :red)
123
+ # # => "\e[0;31;49mError\e[0m"
124
+ #
125
+ # @example Warning with yellow underlined text
126
+ # AnsiColor.call("Warning", color: :yellow, mode: :underline)
127
+ # # => "\e[4;33;49mWarning\e[0m"
128
+ #
129
+ # @example Debug info with dimmed light text
130
+ # AnsiColor.call("Debug info", color: :light_black, mode: :dim)
131
+ # # => "\e[2;90;49mDebug info\e[0m"
132
+ #
133
+ # @example Invalid color raises KeyError
134
+ # AnsiColor.call("Text", color: :invalid_color)
135
+ # # => KeyError: key not found: :invalid_color
136
+ #
137
+ # @note The escape sequence format is: \e[{mode};{color};49m{text}\e[0m
138
+ # @note The "49" represents the default background color
139
+ # @note The final "\e[0m" resets all formatting to default
45
140
  def call(value, color:, mode: :default)
46
141
  color_code = COLOR_CODES.fetch(color)
47
142
  mode_code = MODE_CODES.fetch(mode)
@@ -2,12 +2,60 @@
2
2
 
3
3
  module CMDx
4
4
  module Utils
5
+ # Utility for formatting timestamps in CMDx log entries.
6
+ #
7
+ # LogTimestamp provides consistent timestamp formatting across all CMDx
8
+ # log formatters, ensuring uniform time representation in logs regardless
9
+ # of the chosen output format. Uses ISO 8601 format with microsecond precision.
10
+ #
11
+ # @example Basic timestamp formatting
12
+ # Utils::LogTimestamp.call(Time.now)
13
+ # # => "2022-07-17T18:43:15.123456"
14
+ #
15
+ # @example Usage in log formatters
16
+ # timestamp = Utils::LogTimestamp.call(time.utc)
17
+ # log_entry = "#{severity} [#{timestamp}] #{message}"
18
+ #
19
+ # @example Consistent formatting across formatters
20
+ # # JSON formatter
21
+ # { "timestamp": Utils::LogTimestamp.call(time.utc) }
22
+ #
23
+ # # Line formatter
24
+ # "[#{Utils::LogTimestamp.call(time.utc)} ##{Process.pid}]"
25
+ #
26
+ # @see CMDx::LogFormatters::Json Uses this for JSON timestamp field
27
+ # @see CMDx::LogFormatters::Line Uses this for traditional log format
28
+ # @see CMDx::LogFormatters::Logstash Uses this for @timestamp field
5
29
  module LogTimestamp
6
30
 
31
+ # ISO 8601 datetime format with microsecond precision
32
+ # @return [String] strftime format string for consistent timestamp formatting
7
33
  DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%6N"
8
34
 
9
35
  module_function
10
36
 
37
+ # Formats a Time object as an ISO 8601 timestamp string.
38
+ #
39
+ # Converts the given time to a standardized string representation
40
+ # using ISO 8601 format with microsecond precision. This ensures
41
+ # consistent timestamp formatting across all CMDx log outputs.
42
+ #
43
+ # @param time [Time] Time object to format
44
+ # @return [String] ISO 8601 formatted timestamp with microseconds
45
+ #
46
+ # @example Current time formatting
47
+ # LogTimestamp.call(Time.now)
48
+ # # => "2022-07-17T18:43:15.123456"
49
+ #
50
+ # @example UTC time formatting for logs
51
+ # LogTimestamp.call(Time.now.utc)
52
+ # # => "2022-07-17T18:43:15.123456"
53
+ #
54
+ # @example Integration with log formatters
55
+ # def format_log_entry(severity, time, message)
56
+ # timestamp = LogTimestamp.call(time.utc)
57
+ # "#{severity} [#{timestamp}] #{message}"
58
+ # end
11
59
  def call(time)
12
60
  time.strftime(DATETIME_FORMAT)
13
61
  end
@@ -2,16 +2,83 @@
2
2
 
3
3
  module CMDx
4
4
  module Utils
5
+ # Utility for measuring execution time using monotonic clock.
6
+ #
7
+ # MonotonicRuntime provides accurate execution time measurement that is
8
+ # unaffected by system clock adjustments, leap seconds, or other time
9
+ # synchronization events. Uses Ruby's Process.clock_gettime with
10
+ # CLOCK_MONOTONIC for reliable performance measurements.
11
+ #
12
+ # @example Basic runtime measurement
13
+ # runtime = Utils::MonotonicRuntime.call do
14
+ # sleep(1.5)
15
+ # # ... task execution code ...
16
+ # end
17
+ # # => 1500 (milliseconds)
18
+ #
19
+ # @example Task execution timing
20
+ # class ProcessOrderTask < CMDx::Task
21
+ # def call
22
+ # runtime = Utils::MonotonicRuntime.call do
23
+ # # Complex business logic
24
+ # process_payment
25
+ # update_inventory
26
+ # send_confirmation
27
+ # end
28
+ # logger.info "Order processed in #{runtime}ms"
29
+ # end
30
+ # end
31
+ #
32
+ # @example Performance benchmarking
33
+ # fast_time = Utils::MonotonicRuntime.call { fast_algorithm }
34
+ # slow_time = Utils::MonotonicRuntime.call { slow_algorithm }
35
+ # puts "Fast algorithm is #{slow_time / fast_time}x faster"
36
+ #
37
+ # @see CMDx::Task Uses this internally to measure task execution time
38
+ # @see CMDx::Result#runtime Contains the measured execution time
5
39
  module MonotonicRuntime
6
40
 
7
41
  module_function
8
42
 
43
+ # Measures the execution time of a given block using monotonic clock.
44
+ #
45
+ # Executes the provided block and returns the elapsed time in milliseconds.
46
+ # Uses Process.clock_gettime with CLOCK_MONOTONIC to ensure accurate
47
+ # timing that is immune to system clock changes.
48
+ #
49
+ # @yield Block of code to measure execution time for
50
+ # @return [Integer] Execution time in milliseconds
51
+ #
52
+ # @example Simple timing measurement
53
+ # time_taken = MonotonicRuntime.call do
54
+ # expensive_operation
55
+ # end
56
+ # puts "Operation took #{time_taken}ms"
57
+ #
58
+ # @example Database query timing
59
+ # query_time = MonotonicRuntime.call do
60
+ # User.joins(:orders).where(active: true).count
61
+ # end
62
+ # logger.debug "Query executed in #{query_time}ms"
63
+ #
64
+ # @example API call timing with error handling
65
+ # api_time = MonotonicRuntime.call do
66
+ # begin
67
+ # external_api.fetch_data
68
+ # rescue => e
69
+ # logger.error "API call failed: #{e.message}"
70
+ # raise
71
+ # end
72
+ # end
73
+ # # Time is measured even if an exception occurs
74
+ #
75
+ # @note The block's return value is discarded; only execution time is returned
76
+ # @note Uses millisecond precision for practical performance monitoring
77
+ # @note Monotonic clock ensures accurate timing regardless of system clock changes
9
78
  def call(&)
10
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
79
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
11
80
  yield
12
- finish = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
13
-
14
- finish - start
81
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - now
15
82
  end
16
83
 
17
84
  end
@@ -2,14 +2,92 @@
2
2
 
3
3
  module CMDx
4
4
  module Utils
5
+ # Utility for generating method names with prefixes and suffixes.
6
+ #
7
+ # NameAffix provides flexible method name generation for dynamic method
8
+ # creation, delegation, and metaprogramming scenarios. Supports custom
9
+ # prefixes, suffixes, and complete name overrides for method naming
10
+ # conventions in CMDx's parameter and delegation systems.
11
+ #
12
+ # @example Basic prefix and suffix usage
13
+ # Utils::NameAffix.call(:name, "user", prefix: true, suffix: true)
14
+ # # => :user_name_user
15
+ #
16
+ # @example Custom prefix
17
+ # Utils::NameAffix.call(:email, "admin", prefix: "get_")
18
+ # # => :get_email
19
+ #
20
+ # @example Custom suffix
21
+ # Utils::NameAffix.call(:count, "items", suffix: "_total")
22
+ # # => :count_total
23
+ #
24
+ # @example Complete name override
25
+ # Utils::NameAffix.call(:original, "source", as: :custom_method)
26
+ # # => :custom_method
27
+ #
28
+ # @example Parameter delegation usage
29
+ # class MyTask < CMDx::Task
30
+ # required :user_id
31
+ #
32
+ # # Internally uses NameAffix for method generation
33
+ # # Creates methods like user_id, user_id?, etc.
34
+ # end
35
+ #
36
+ # @see CMDx::Parameter Uses this for parameter method name generation
37
+ # @see CMDx::CoreExt::Module Uses this for delegation method naming
5
38
  module NameAffix
6
39
 
40
+ # Proc for handling affix logic with boolean or custom values
41
+ # @return [Proc] processor for affix options that handles true/false and custom strings
7
42
  AFFIX = proc do |o, &block|
8
43
  o == true ? block.call : o
9
44
  end.freeze
10
45
 
11
46
  module_function
12
47
 
48
+ # Generates a method name with optional prefix and suffix.
49
+ #
50
+ # Creates a method name by combining the base method name with optional
51
+ # prefixes and suffixes. Supports boolean flags for default affixes or
52
+ # custom string values for specific naming patterns.
53
+ #
54
+ # @param method_name [Symbol, String] Base method name to transform
55
+ # @param source [String] Source identifier used for default prefix/suffix generation
56
+ # @param options [Hash] Configuration options for name generation
57
+ # @option options [Boolean, String] :prefix (false) Add prefix - true for "#{source}_", string for custom
58
+ # @option options [Boolean, String] :suffix (false) Add suffix - true for "_#{source}", string for custom
59
+ # @option options [Symbol] :as Override the entire generated name
60
+ #
61
+ # @return [Symbol] Generated method name with applied affixes
62
+ #
63
+ # @example Default prefix generation
64
+ # NameAffix.call(:method, "user", prefix: true)
65
+ # # => :user_method
66
+ #
67
+ # @example Custom prefix
68
+ # NameAffix.call(:method, "user", prefix: "get_")
69
+ # # => :get_method
70
+ #
71
+ # @example Default suffix generation
72
+ # NameAffix.call(:method, "user", suffix: true)
73
+ # # => :method_user
74
+ #
75
+ # @example Custom suffix
76
+ # NameAffix.call(:method, "user", suffix: "_count")
77
+ # # => :method_count
78
+ #
79
+ # @example Combined prefix and suffix
80
+ # NameAffix.call(:name, "user", prefix: "get_", suffix: "_value")
81
+ # # => :get_name_value
82
+ #
83
+ # @example Complete name override (ignores prefix/suffix)
84
+ # NameAffix.call(:original, "user", prefix: true, as: :custom)
85
+ # # => :custom
86
+ #
87
+ # @example Parameter method generation
88
+ # # CMDx internally uses this for parameter methods:
89
+ # NameAffix.call(:email, "user", suffix: "?") # => :email?
90
+ # NameAffix.call(:process, "order", prefix: "can_") # => :can_process
13
91
  def call(method_name, source, options = {})
14
92
  options[:as] || begin
15
93
  prefix = AFFIX.call(options[:prefix]) { "#{source}_" }
@@ -2,10 +2,92 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
+ # Custom validator for parameter validation using user-defined validation logic.
6
+ #
7
+ # The Custom validator allows you to implement your own validation logic by
8
+ # providing a callable validator class or object. This enables complex business
9
+ # rule validation that goes beyond the built-in validators.
10
+ #
11
+ # @example Basic custom validator
12
+ # class EmailDomainValidator
13
+ # def self.call(value, options)
14
+ # allowed_domains = options.dig(:custom, :domains) || ['example.com']
15
+ # domain = value.split('@').last
16
+ # allowed_domains.include?(domain)
17
+ # end
18
+ # end
19
+ #
20
+ # class ProcessUserTask < CMDx::Task
21
+ # required :email, custom: { validator: EmailDomainValidator }
22
+ # end
23
+ #
24
+ # @example Custom validator with options
25
+ # class ProcessUserTask < CMDx::Task
26
+ # required :email, custom: {
27
+ # validator: EmailDomainValidator,
28
+ # domains: ['company.com', 'partner.org'],
29
+ # message: "must be from an approved domain"
30
+ # }
31
+ # end
32
+ #
33
+ # @example Complex business logic validator
34
+ # class AgeValidator
35
+ # def self.call(value, options)
36
+ # min_age = options.dig(:custom, :min_age) || 18
37
+ # max_age = options.dig(:custom, :max_age) || 120
38
+ # value.between?(min_age, max_age)
39
+ # end
40
+ # end
41
+ #
42
+ # @example Proc-based validator
43
+ # class ProcessOrderTask < CMDx::Task
44
+ # required :discount, custom: {
45
+ # validator: ->(value, options) { value <= 50 },
46
+ # message: "cannot exceed 50%"
47
+ # }
48
+ # end
49
+ #
50
+ # @see CMDx::Parameter Parameter validation integration
51
+ # @see CMDx::ValidationError Raised when validation fails
5
52
  module Custom
6
53
 
7
54
  module_function
8
55
 
56
+ # Validates a parameter value using a custom validator.
57
+ #
58
+ # Calls the provided validator with the value and options, expecting
59
+ # a truthy return value for successful validation. If validation fails,
60
+ # raises a ValidationError with the configured or default message.
61
+ #
62
+ # @param value [Object] The parameter value to validate
63
+ # @param options [Hash] Validation configuration options
64
+ # @option options [Hash] :custom Custom validation configuration
65
+ # @option options [#call] :custom.validator Callable validator object/class
66
+ # @option options [String] :custom.message Custom error message
67
+ # @option options [Hash] :custom Additional options passed to validator
68
+ #
69
+ # @return [void]
70
+ # @raise [ValidationError] If the custom validator returns falsy
71
+ #
72
+ # @example Successful validation
73
+ # validator = ->(value, options) { value.length > 5 }
74
+ # Custom.call("hello world", custom: { validator: validator })
75
+ # # => passes without error
76
+ #
77
+ # @example Failed validation with default message
78
+ # validator = ->(value, options) { value > 100 }
79
+ # Custom.call(50, custom: { validator: validator })
80
+ # # => raises ValidationError: "is not valid"
81
+ #
82
+ # @example Failed validation with custom message
83
+ # validator = ->(value, options) { value.even? }
84
+ # Custom.call(7, custom: { validator: validator, message: "must be even" })
85
+ # # => raises ValidationError: "must be even"
86
+ #
87
+ # @example Validator with additional options
88
+ # validator = ->(value, opts) { value >= opts.dig(:custom, :minimum) }
89
+ # Custom.call(10, custom: { validator: validator, minimum: 5 })
90
+ # # => passes without error
9
91
  def call(value, options = {})
10
92
  return if options.dig(:custom, :validator).call(value, options)
11
93
 
@@ -2,10 +2,93 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
+ # Exclusion validator for parameter validation against forbidden values.
6
+ #
7
+ # The Exclusion validator ensures that parameter values are NOT within a
8
+ # specified set of forbidden values. It supports both array-based exclusion
9
+ # (specific values) and range-based exclusion (value ranges).
10
+ #
11
+ # @example Basic exclusion validation with array
12
+ # class ProcessOrderTask < CMDx::Task
13
+ # required :status, exclusion: { in: ['cancelled', 'refunded'] }
14
+ # required :priority, exclusion: { in: [0, -1] }
15
+ # end
16
+ #
17
+ # @example Range-based exclusion
18
+ # class ProcessUserTask < CMDx::Task
19
+ # required :age, exclusion: { in: 0..17 } # Must be 18 or older
20
+ # required :score, exclusion: { within: 90..100 } # Cannot be in top 10%
21
+ # end
22
+ #
23
+ # @example Custom error messages
24
+ # class ProcessOrderTask < CMDx::Task
25
+ # required :status, exclusion: {
26
+ # in: ['cancelled', 'refunded'],
27
+ # of_message: "cannot be cancelled or refunded"
28
+ # }
29
+ # required :age, exclusion: {
30
+ # in: 0..17,
31
+ # in_message: "must be %{min} or older"
32
+ # }
33
+ # end
34
+ #
35
+ # @example Exclusion validation behavior
36
+ # # Array exclusion
37
+ # Exclusion.call("active", exclusion: { in: ['cancelled'] }) # passes
38
+ # Exclusion.call("cancelled", exclusion: { in: ['cancelled'] }) # raises ValidationError
39
+ #
40
+ # # Range exclusion
41
+ # Exclusion.call(25, exclusion: { in: 0..17 }) # passes
42
+ # Exclusion.call(15, exclusion: { in: 0..17 }) # raises ValidationError
43
+ #
44
+ # @see CMDx::Validators::Inclusion For validating values must be in a set
45
+ # @see CMDx::Parameter Parameter validation integration
46
+ # @see CMDx::ValidationError Raised when validation fails
5
47
  module Exclusion
6
48
 
7
49
  extend self
8
50
 
51
+ # Validates that a parameter value is not in the excluded set.
52
+ #
53
+ # Checks that the value is not present in the specified array or range
54
+ # of forbidden values. Raises ValidationError if the value is found
55
+ # in the exclusion set.
56
+ #
57
+ # @param value [Object] The parameter value to validate
58
+ # @param options [Hash] Validation configuration options
59
+ # @option options [Hash] :exclusion Exclusion validation configuration
60
+ # @option options [Array, Range] :exclusion.in Values/range to exclude
61
+ # @option options [Array, Range] :exclusion.within Alias for :in
62
+ # @option options [String] :exclusion.of_message Error message for array exclusion
63
+ # @option options [String] :exclusion.in_message Error message for range exclusion
64
+ # @option options [String] :exclusion.within_message Alias for :in_message
65
+ # @option options [String] :exclusion.message General error message override
66
+ #
67
+ # @return [void]
68
+ # @raise [ValidationError] If value is found in the exclusion set
69
+ #
70
+ # @example Array exclusion validation
71
+ # Exclusion.call("pending", exclusion: { in: ['cancelled', 'failed'] })
72
+ # # => passes without error
73
+ #
74
+ # @example Failed array exclusion
75
+ # Exclusion.call("cancelled", exclusion: { in: ['cancelled', 'failed'] })
76
+ # # => raises ValidationError: "must not be one of: \"cancelled\", \"failed\""
77
+ #
78
+ # @example Range exclusion validation
79
+ # Exclusion.call(25, exclusion: { in: 0..17 })
80
+ # # => passes without error
81
+ #
82
+ # @example Failed range exclusion
83
+ # Exclusion.call(15, exclusion: { in: 0..17 })
84
+ # # => raises ValidationError: "must not be within 0 and 17"
85
+ #
86
+ # @example Custom error messages
87
+ # Exclusion.call("admin", exclusion: {
88
+ # in: ['admin', 'root'],
89
+ # of_message: "role is restricted"
90
+ # })
91
+ # # => raises ValidationError: "role is restricted"
9
92
  def call(value, options = {})
10
93
  values = options.dig(:exclusion, :in) ||
11
94
  options.dig(:exclusion, :within)
@@ -19,6 +102,11 @@ module CMDx
19
102
 
20
103
  private
21
104
 
105
+ # Raises validation error for array-based exclusion violations.
106
+ #
107
+ # @param values [Array] The excluded values array
108
+ # @param options [Hash] Validation options containing error messages
109
+ # @raise [ValidationError] With formatted error message
22
110
  def raise_of_validation_error!(values, options)
23
111
  values = values.map(&:inspect).join(", ")
24
112
  message = options.dig(:exclusion, :of_message) ||
@@ -32,6 +120,12 @@ module CMDx
32
120
  )
33
121
  end
34
122
 
123
+ # Raises validation error for range-based exclusion violations.
124
+ #
125
+ # @param min [Object] Range minimum value
126
+ # @param max [Object] Range maximum value
127
+ # @param options [Hash] Validation options containing error messages
128
+ # @raise [ValidationError] With formatted error message
35
129
  def raise_within_validation_error!(min, max, options)
36
130
  message = options.dig(:exclusion, :in_message) ||
37
131
  options.dig(:exclusion, :within_message) ||