cmdx 1.8.0 → 1.9.1

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +3 -3
  4. data/.cursor/prompts/llms.md +1 -3
  5. data/.cursor/prompts/yardoc.md +1 -0
  6. data/.irbrc +14 -2
  7. data/CHANGELOG.md +64 -45
  8. data/LLM.md +159 -53
  9. data/README.md +26 -83
  10. data/docs/.DS_Store +0 -0
  11. data/docs/assets/favicon.ico +0 -0
  12. data/docs/assets/favicon.svg +1 -0
  13. data/docs/attributes/coercions.md +12 -24
  14. data/docs/attributes/defaults.md +3 -16
  15. data/docs/attributes/definitions.md +16 -30
  16. data/docs/attributes/naming.md +3 -13
  17. data/docs/attributes/transformations.md +63 -0
  18. data/docs/attributes/validations.md +14 -33
  19. data/docs/basics/chain.md +14 -23
  20. data/docs/basics/context.md +13 -22
  21. data/docs/basics/execution.md +8 -26
  22. data/docs/basics/setup.md +8 -19
  23. data/docs/callbacks.md +19 -32
  24. data/docs/deprecation.md +8 -25
  25. data/docs/getting_started.md +109 -76
  26. data/docs/index.md +132 -0
  27. data/docs/internationalization.md +6 -18
  28. data/docs/interruptions/exceptions.md +10 -16
  29. data/docs/interruptions/faults.md +8 -25
  30. data/docs/interruptions/halt.md +12 -27
  31. data/docs/logging.md +7 -17
  32. data/docs/middlewares.md +13 -29
  33. data/docs/outcomes/result.md +21 -38
  34. data/docs/outcomes/states.md +8 -22
  35. data/docs/outcomes/statuses.md +10 -21
  36. data/docs/stylesheets/extra.css +42 -0
  37. data/docs/tips_and_tricks.md +7 -46
  38. data/docs/workflows.md +23 -38
  39. data/examples/active_record_query_tagging.md +46 -0
  40. data/examples/paper_trail_whatdunnit.md +39 -0
  41. data/lib/cmdx/attribute.rb +88 -6
  42. data/lib/cmdx/attribute_registry.rb +20 -0
  43. data/lib/cmdx/attribute_value.rb +56 -10
  44. data/lib/cmdx/callback_registry.rb +31 -2
  45. data/lib/cmdx/chain.rb +34 -1
  46. data/lib/cmdx/coercion_registry.rb +18 -0
  47. data/lib/cmdx/coercions/array.rb +2 -0
  48. data/lib/cmdx/coercions/big_decimal.rb +3 -0
  49. data/lib/cmdx/coercions/boolean.rb +5 -0
  50. data/lib/cmdx/coercions/complex.rb +2 -0
  51. data/lib/cmdx/coercions/date.rb +4 -0
  52. data/lib/cmdx/coercions/date_time.rb +5 -0
  53. data/lib/cmdx/coercions/float.rb +2 -0
  54. data/lib/cmdx/coercions/hash.rb +4 -0
  55. data/lib/cmdx/coercions/integer.rb +2 -0
  56. data/lib/cmdx/coercions/rational.rb +2 -0
  57. data/lib/cmdx/coercions/string.rb +2 -0
  58. data/lib/cmdx/coercions/symbol.rb +2 -0
  59. data/lib/cmdx/coercions/time.rb +5 -0
  60. data/lib/cmdx/configuration.rb +119 -3
  61. data/lib/cmdx/context.rb +36 -0
  62. data/lib/cmdx/deprecator.rb +6 -3
  63. data/lib/cmdx/errors.rb +22 -0
  64. data/lib/cmdx/executor.rb +136 -7
  65. data/lib/cmdx/faults.rb +14 -0
  66. data/lib/cmdx/identifier.rb +2 -0
  67. data/lib/cmdx/locale.rb +3 -0
  68. data/lib/cmdx/log_formatters/json.rb +2 -0
  69. data/lib/cmdx/log_formatters/key_value.rb +2 -0
  70. data/lib/cmdx/log_formatters/line.rb +2 -0
  71. data/lib/cmdx/log_formatters/logstash.rb +2 -0
  72. data/lib/cmdx/log_formatters/raw.rb +2 -0
  73. data/lib/cmdx/middleware_registry.rb +20 -0
  74. data/lib/cmdx/middlewares/correlate.rb +11 -0
  75. data/lib/cmdx/middlewares/runtime.rb +4 -0
  76. data/lib/cmdx/middlewares/timeout.rb +4 -0
  77. data/lib/cmdx/pipeline.rb +24 -5
  78. data/lib/cmdx/railtie.rb +13 -0
  79. data/lib/cmdx/result.rb +133 -2
  80. data/lib/cmdx/task.rb +103 -8
  81. data/lib/cmdx/utils/call.rb +2 -0
  82. data/lib/cmdx/utils/condition.rb +3 -0
  83. data/lib/cmdx/utils/format.rb +5 -0
  84. data/lib/cmdx/validator_registry.rb +18 -0
  85. data/lib/cmdx/validators/exclusion.rb +2 -0
  86. data/lib/cmdx/validators/format.rb +2 -0
  87. data/lib/cmdx/validators/inclusion.rb +2 -0
  88. data/lib/cmdx/validators/length.rb +14 -0
  89. data/lib/cmdx/validators/numeric.rb +14 -0
  90. data/lib/cmdx/validators/presence.rb +2 -0
  91. data/lib/cmdx/version.rb +4 -1
  92. data/lib/cmdx/workflow.rb +10 -0
  93. data/lib/cmdx.rb +9 -0
  94. data/lib/generators/cmdx/locale_generator.rb +0 -1
  95. data/lib/generators/cmdx/templates/install.rb +9 -0
  96. data/mkdocs.yml +122 -0
  97. data/src/cmdx-dark-logo.png +0 -0
  98. data/src/cmdx-favicon.svg +1 -0
  99. data/src/cmdx-light-logo.png +0 -0
  100. data/src/cmdx-logo.svg +1 -0
  101. metadata +14 -3
  102. data/lib/cmdx/freezer.rb +0 -51
  103. data/src/cmdx-logo.png +0 -0
data/lib/cmdx/faults.rb CHANGED
@@ -11,6 +11,14 @@ module CMDx
11
11
 
12
12
  extend Forwardable
13
13
 
14
+ # Returns the result that caused this fault.
15
+ #
16
+ # @return [Result] The result instance
17
+ #
18
+ # @example
19
+ # fault.result.reason # => "Validation failed"
20
+ #
21
+ # @rbs @result: Result
14
22
  attr_reader :result
15
23
 
16
24
  def_delegators :result, :task, :context, :chain
@@ -24,6 +32,8 @@ module CMDx
24
32
  # @example
25
33
  # fault = Fault.new(task_result)
26
34
  # fault.result.reason # => "Task validation failed"
35
+ #
36
+ # @rbs (Result result) -> void
27
37
  def initialize(result)
28
38
  @result = result
29
39
 
@@ -41,6 +51,8 @@ module CMDx
41
51
  # @example
42
52
  # Fault.for?(UserTask, AdminUserTask)
43
53
  # # => true if fault.task is a UserTask or AdminUserTask
54
+ #
55
+ # @rbs (*Class tasks) -> Class
44
56
  def for?(*tasks)
45
57
  temp_fault = Class.new(self) do
46
58
  def self.===(other)
@@ -62,6 +74,8 @@ module CMDx
62
74
  # @example
63
75
  # Fault.matches? { |fault| fault.result.metadata[:critical] }
64
76
  # # => true if fault has critical metadata
77
+ #
78
+ # @rbs () { (Fault) -> bool } -> Class
65
79
  def matches?(&block)
66
80
  raise ArgumentError, "block required" unless block_given?
67
81
 
@@ -18,6 +18,8 @@ module CMDx
18
18
  # @example Generate a unique identifier
19
19
  # CMDx::Identifier.generate
20
20
  # # => "01890b2c-1234-5678-9abc-def123456789"
21
+ #
22
+ # @rbs () -> String
21
23
  def generate
22
24
  if SecureRandom.respond_to?(:uuid_v7)
23
25
  SecureRandom.uuid_v7
data/lib/cmdx/locale.rb CHANGED
@@ -8,6 +8,7 @@ module CMDx
8
8
 
9
9
  extend self
10
10
 
11
+ # @rbs EN: Hash[String, untyped]
11
12
  EN = YAML.load_file(CMDx.gem_path.join("lib/locales/en.yml")).freeze
12
13
  private_constant :EN
13
14
 
@@ -34,6 +35,8 @@ module CMDx
34
35
  # @example With fallback
35
36
  # Locale.translate("missing.key", default: "Custom fallback message")
36
37
  # # => "Custom fallback message"
38
+ #
39
+ # @rbs ((String | Symbol) key, **untyped options) -> String
37
40
  def translate(key, **options)
38
41
  options[:default] ||= EN.dig("en", *key.to_s.split("."))
39
42
  return ::I18n.t(key, **options) if defined?(::I18n)
@@ -21,6 +21,8 @@ module CMDx
21
21
  # @example Basic usage
22
22
  # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
23
23
  # # => '{"severity":"INFO","timestamp":"2024-01-15T10:30:45.123456Z","progname":"MyApp","pid":12345,"message":"User logged in"}\n'
24
+ #
25
+ # @rbs (String severity, Time time, String? progname, String message) -> String
24
26
  def call(severity, time, progname, message)
25
27
  hash = {
26
28
  severity:,
@@ -21,6 +21,8 @@ module CMDx
21
21
  # @example Basic usage
22
22
  # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
23
23
  # # => "severity=INFO timestamp=2024-01-15T10:30:45.123456Z progname=MyApp pid=12345 message=User logged in\n"
24
+ #
25
+ # @rbs (String severity, Time time, String? progname, String message) -> String
24
26
  def call(severity, time, progname, message)
25
27
  hash = {
26
28
  severity:,
@@ -21,6 +21,8 @@ module CMDx
21
21
  # @example Basic usage
22
22
  # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
23
23
  # # => "I, [2024-01-15T10:30:45.123456Z #12345] INFO -- MyApp: User logged in\n"
24
+ #
25
+ # @rbs (String severity, Time time, String? progname, String message) -> String
24
26
  def call(severity, time, progname, message)
25
27
  "#{severity[0]}, [#{time.utc.iso8601(6)} ##{Process.pid}] #{severity} -- #{progname}: #{message}\n"
26
28
  end
@@ -22,6 +22,8 @@ module CMDx
22
22
  # @example Basic usage
23
23
  # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
24
24
  # # => '{"severity":"INFO","progname":"MyApp","pid":12345,"message":"User logged in","@version":"1","@timestamp":"2024-01-15T10:30:45.123456Z"}\n'
25
+ #
26
+ # @rbs (String severity, Time time, String? progname, String message) -> String
25
27
  def call(severity, time, progname, message)
26
28
  hash = {
27
29
  severity:,
@@ -22,6 +22,8 @@ module CMDx
22
22
  # @example Basic usage
23
23
  # logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
24
24
  # # => "User logged in\n"
25
+ #
26
+ # @rbs (String severity, Time time, String? progname, String message) -> String
25
27
  def call(severity, time, progname, message)
26
28
  "#{message}\n"
27
29
  end
@@ -9,6 +9,14 @@ module CMDx
9
9
  # they were registered.
10
10
  class MiddlewareRegistry
11
11
 
12
+ # Returns the ordered collection of middleware entries.
13
+ #
14
+ # @return [Array<Array>] Array of middleware-options pairs
15
+ #
16
+ # @example
17
+ # registry.registry # => [[LoggingMiddleware, {level: :debug}], [AuthMiddleware, {}]]
18
+ #
19
+ # @rbs @registry: Array[Array[untyped]]
12
20
  attr_reader :registry
13
21
  alias to_a registry
14
22
 
@@ -19,6 +27,8 @@ module CMDx
19
27
  # @example
20
28
  # registry = MiddlewareRegistry.new
21
29
  # registry = MiddlewareRegistry.new([[MyMiddleware, {option: 'value'}]])
30
+ #
31
+ # @rbs (?Array[Array[untyped]] registry) -> void
22
32
  def initialize(registry = [])
23
33
  @registry = registry
24
34
  end
@@ -29,6 +39,8 @@ module CMDx
29
39
  #
30
40
  # @example
31
41
  # new_registry = registry.dup
42
+ #
43
+ # @rbs () -> MiddlewareRegistry
32
44
  def dup
33
45
  self.class.new(registry.map(&:dup))
34
46
  end
@@ -46,6 +58,8 @@ module CMDx
46
58
  # @example
47
59
  # registry.register(LoggingMiddleware, at: 0, log_level: :debug)
48
60
  # registry.register(AuthMiddleware, at: -1, timeout: 30)
61
+ #
62
+ # @rbs (untyped middleware, ?at: Integer, **untyped options) -> self
49
63
  def register(middleware, at: -1, **options)
50
64
  registry.insert(at, [middleware, options])
51
65
  self
@@ -59,6 +73,8 @@ module CMDx
59
73
  #
60
74
  # @example
61
75
  # registry.deregister(LoggingMiddleware)
76
+ #
77
+ # @rbs (untyped middleware) -> self
62
78
  def deregister(middleware)
63
79
  registry.reject! { |mw, _opts| mw == middleware }
64
80
  self
@@ -79,6 +95,8 @@ module CMDx
79
95
  # result = registry.call!(my_task) do |processed_task|
80
96
  # processed_task.execute
81
97
  # end
98
+ #
99
+ # @rbs (untyped task) { (untyped) -> untyped } -> untyped
82
100
  def call!(task, &)
83
101
  raise ArgumentError, "block required" unless block_given?
84
102
 
@@ -96,6 +114,8 @@ module CMDx
96
114
  # @yieldparam task [Object] The processed task object
97
115
  #
98
116
  # @return [Object] Result of the block execution or next middleware call
117
+ #
118
+ # @rbs (Integer index, untyped task) { (untyped) -> untyped } -> untyped
99
119
  def recursively_call_middleware(index, task, &block)
100
120
  return yield(task) if index >= registry.size
101
121
 
@@ -12,6 +12,7 @@ module CMDx
12
12
 
13
13
  extend self
14
14
 
15
+ # @rbs THREAD_KEY: Symbol
15
16
  THREAD_KEY = :cmdx_correlate
16
17
 
17
18
  # Retrieves the current correlation ID from thread-local storage.
@@ -20,6 +21,8 @@ module CMDx
20
21
  #
21
22
  # @example Get current correlation ID
22
23
  # Correlate.id # => "550e8400-e29b-41d4-a716-446655440000"
24
+ #
25
+ # @rbs () -> String?
23
26
  def id
24
27
  Thread.current[THREAD_KEY]
25
28
  end
@@ -31,6 +34,8 @@ module CMDx
31
34
  #
32
35
  # @example Set correlation ID
33
36
  # Correlate.id = "abc-123-def"
37
+ #
38
+ # @rbs (String id) -> String
34
39
  def id=(id)
35
40
  Thread.current[THREAD_KEY] = id
36
41
  end
@@ -41,6 +46,8 @@ module CMDx
41
46
  #
42
47
  # @example Clear correlation ID
43
48
  # Correlate.clear
49
+ #
50
+ # @rbs () -> nil
44
51
  def clear
45
52
  Thread.current[THREAD_KEY] = nil
46
53
  end
@@ -58,6 +65,8 @@ module CMDx
58
65
  # perform_operation
59
66
  # end
60
67
  # # Previous ID is restored
68
+ #
69
+ # @rbs (String new_id) { () -> untyped } -> untyped
61
70
  def use(new_id)
62
71
  old_id = id
63
72
  self.id = new_id
@@ -92,6 +101,8 @@ module CMDx
92
101
  # Correlate.call(task, id: -> { "dynamic-#{Time.now.to_i}" }, &block)
93
102
  # @example Conditional correlation
94
103
  # Correlate.call(task, if: :enable_correlation, &block)
104
+ #
105
+ # @rbs (Task task, **untyped options) { () -> untyped } -> untyped
95
106
  def call(task, **options, &)
96
107
  return yield unless Utils::Condition.evaluate(task, options)
97
108
 
@@ -32,6 +32,8 @@ module CMDx
32
32
  # Runtime.call(task, if: :enable_profiling, &block)
33
33
  # @example Disable runtime measurement
34
34
  # Runtime.call(task, unless: :skip_profiling, &block)
35
+ #
36
+ # @rbs (Task task, **untyped options) { () -> untyped } -> untyped
35
37
  def call(task, **options)
36
38
  return yield unless Utils::Condition.evaluate(task, options)
37
39
 
@@ -49,6 +51,8 @@ module CMDx
49
51
  # timing measurements that are not affected by system clock changes.
50
52
  #
51
53
  # @return [Integer] Current monotonic time in milliseconds
54
+ #
55
+ # @rbs () -> Integer
52
56
  def monotonic_time
53
57
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
54
58
  end
@@ -21,6 +21,8 @@ module CMDx
21
21
  extend self
22
22
 
23
23
  # Default timeout limit in seconds when none is specified.
24
+ #
25
+ # @rbs DEFAULT_LIMIT: Integer
24
26
  DEFAULT_LIMIT = 3
25
27
 
26
28
  # Middleware entry point that enforces execution time limits.
@@ -51,6 +53,8 @@ module CMDx
51
53
  # Timeout.call(task, seconds: -> { calculate_timeout }, &block)
52
54
  # @example Conditional timeout control
53
55
  # Timeout.call(task, if: :enable_timeout, &block)
56
+ #
57
+ # @rbs (Task task, **untyped options) { () -> untyped } -> untyped
54
58
  def call(task, **options, &)
55
59
  return yield unless Utils::Condition.evaluate(task, options)
56
60
 
data/lib/cmdx/pipeline.rb CHANGED
@@ -6,7 +6,14 @@ module CMDx
6
6
  # and handling breakpoints that can interrupt execution at specific task statuses.
7
7
  class Pipeline
8
8
 
9
- # @return [Workflow] The workflow instance being executed
9
+ # Returns the workflow being executed by this pipeline.
10
+ #
11
+ # @return [Workflow] The workflow instance
12
+ #
13
+ # @example
14
+ # pipeline.workflow.context[:status] # => "processing"
15
+ #
16
+ # @rbs @workflow: Workflow
10
17
  attr_reader :workflow
11
18
 
12
19
  # @param workflow [Workflow] The workflow to execute
@@ -15,6 +22,8 @@ module CMDx
15
22
  #
16
23
  # @example
17
24
  # pipeline = Pipeline.new(my_workflow)
25
+ #
26
+ # @rbs (Workflow workflow) -> void
18
27
  def initialize(workflow)
19
28
  @workflow = workflow
20
29
  end
@@ -27,6 +36,8 @@ module CMDx
27
36
  #
28
37
  # @example
29
38
  # Pipeline.execute(my_workflow)
39
+ #
40
+ # @rbs (Workflow workflow) -> void
30
41
  def self.execute(workflow)
31
42
  new(workflow).execute
32
43
  end
@@ -40,9 +51,11 @@ module CMDx
40
51
  # @example
41
52
  # pipeline = Pipeline.new(my_workflow)
42
53
  # pipeline.execute
54
+ #
55
+ # @rbs () -> void
43
56
  def execute
44
57
  workflow.class.pipeline.each do |group|
45
- next unless Utils::Condition.evaluate(workflow, group.options, workflow)
58
+ next unless Utils::Condition.evaluate(workflow, group.options)
46
59
 
47
60
  breakpoints = group.options[:breakpoints] ||
48
61
  workflow.class.settings[:breakpoints] ||
@@ -65,6 +78,8 @@ module CMDx
65
78
  #
66
79
  # @example
67
80
  # execute_group_tasks(group, ["failed", "skipped"])
81
+ #
82
+ # @rbs (untyped group, Array[String] breakpoints) -> void
68
83
  def execute_group_tasks(group, breakpoints)
69
84
  case strategy = group.options[:strategy]
70
85
  when NilClass, /sequential/ then execute_tasks_in_sequence(group, breakpoints)
@@ -85,6 +100,8 @@ module CMDx
85
100
  #
86
101
  # @example
87
102
  # execute_tasks_in_sequence(group, ["failed", "skipped"])
103
+ #
104
+ # @rbs (untyped group, Array[String] breakpoints) -> void
88
105
  def execute_tasks_in_sequence(group, breakpoints)
89
106
  group.tasks.each do |task|
90
107
  task_result = task.execute(workflow.context)
@@ -107,19 +124,21 @@ module CMDx
107
124
  #
108
125
  # @example
109
126
  # execute_tasks_in_parallel(group, ["failed"])
127
+ #
128
+ # @rbs (untyped group, Array[String] breakpoints) -> void
110
129
  def execute_tasks_in_parallel(group, breakpoints)
111
- raise "install the `parallel` gem to use this feature" unless defined?(::Parallel)
130
+ raise "install the `parallel` gem to use this feature" unless defined?(Parallel)
112
131
 
113
132
  parallel_options = group.options.slice(:in_threads, :in_processes)
114
133
  throwable_result = nil
115
134
 
116
- ::Parallel.each(group.tasks, **parallel_options) do |task|
135
+ Parallel.each(group.tasks, **parallel_options) do |task|
117
136
  Chain.current = workflow.chain
118
137
 
119
138
  task_result = task.execute(workflow.context)
120
139
  next unless breakpoints.include?(task_result.status)
121
140
 
122
- raise ::Parallel::Break, throwable_result = task_result
141
+ raise Parallel::Break, throwable_result = task_result
123
142
  end
124
143
 
125
144
  return if throwable_result.nil?
data/lib/cmdx/railtie.rb CHANGED
@@ -21,6 +21,8 @@ module CMDx
21
21
  # # This initializer runs automatically when Rails starts
22
22
  # # It will load locales like en.yml, es.yml, fr.yml if they exist
23
23
  # # in the CMDx gem's locales directory
24
+ #
25
+ # @rbs (untyped app) -> void
24
26
  initializer("cmdx.configure_locales") do |app|
25
27
  Array(app.config.i18n.available_locales).each do |locale|
26
28
  path = CMDx.gem_path.join("lib/locales/#{locale}.yml")
@@ -32,5 +34,16 @@ module CMDx
32
34
  ::I18n.reload!
33
35
  end
34
36
 
37
+ # Configures the backtrace cleaner for CMDx in a Rails environment.
38
+ #
39
+ # Sets the backtrace cleaner to the Rails backtrace cleaner.
40
+ #
41
+ # @rbs () -> void
42
+ initializer("cmdx.backtrace_cleaner") do
43
+ CMDx.configuration.backtrace_cleaner = lambda do |backtrace|
44
+ Rails.backtrace_cleaner.clean(backtrace)
45
+ end
46
+ end
47
+
35
48
  end
36
49
  end