cmdx 1.12.0 → 1.14.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -71
  3. data/LICENSE.txt +3 -20
  4. data/README.md +8 -7
  5. data/lib/cmdx/attribute.rb +21 -5
  6. data/lib/cmdx/chain.rb +18 -4
  7. data/lib/cmdx/context.rb +18 -0
  8. data/lib/cmdx/executor.rb +35 -30
  9. data/lib/cmdx/result.rb +45 -2
  10. data/lib/cmdx/task.rb +22 -1
  11. data/lib/cmdx/version.rb +1 -1
  12. data/mkdocs.yml +67 -37
  13. metadata +3 -57
  14. data/.cursor/prompts/docs.md +0 -12
  15. data/.cursor/prompts/llms.md +0 -8
  16. data/.cursor/prompts/rspec.md +0 -24
  17. data/.cursor/prompts/yardoc.md +0 -15
  18. data/.cursor/rules/cursor-instructions.mdc +0 -68
  19. data/.irbrc +0 -18
  20. data/.rspec +0 -4
  21. data/.rubocop.yml +0 -95
  22. data/.ruby-version +0 -1
  23. data/.yard-lint.yml +0 -174
  24. data/.yardopts +0 -7
  25. data/docs/.DS_Store +0 -0
  26. data/docs/assets/favicon.ico +0 -0
  27. data/docs/assets/favicon.svg +0 -1
  28. data/docs/attributes/coercions.md +0 -155
  29. data/docs/attributes/defaults.md +0 -77
  30. data/docs/attributes/definitions.md +0 -283
  31. data/docs/attributes/naming.md +0 -68
  32. data/docs/attributes/transformations.md +0 -63
  33. data/docs/attributes/validations.md +0 -336
  34. data/docs/basics/chain.md +0 -108
  35. data/docs/basics/context.md +0 -121
  36. data/docs/basics/execution.md +0 -96
  37. data/docs/basics/setup.md +0 -84
  38. data/docs/callbacks.md +0 -157
  39. data/docs/configuration.md +0 -314
  40. data/docs/deprecation.md +0 -145
  41. data/docs/getting_started.md +0 -126
  42. data/docs/index.md +0 -134
  43. data/docs/internationalization.md +0 -126
  44. data/docs/interruptions/exceptions.md +0 -52
  45. data/docs/interruptions/faults.md +0 -169
  46. data/docs/interruptions/halt.md +0 -216
  47. data/docs/logging.md +0 -94
  48. data/docs/middlewares.md +0 -191
  49. data/docs/outcomes/result.md +0 -194
  50. data/docs/outcomes/states.md +0 -66
  51. data/docs/outcomes/statuses.md +0 -65
  52. data/docs/retries.md +0 -121
  53. data/docs/stylesheets/extra.css +0 -42
  54. data/docs/tips_and_tricks.md +0 -157
  55. data/docs/workflows.md +0 -226
  56. data/examples/active_record_database_transaction.md +0 -27
  57. data/examples/active_record_query_tagging.md +0 -46
  58. data/examples/flipper_feature_flags.md +0 -50
  59. data/examples/paper_trail_whatdunnit.md +0 -39
  60. data/examples/redis_idempotency.md +0 -71
  61. data/examples/sentry_error_tracking.md +0 -46
  62. data/examples/sidekiq_async_execution.md +0 -29
  63. data/examples/stoplight_circuit_breaker.md +0 -36
  64. data/src/cmdx-dark-logo.png +0 -0
  65. data/src/cmdx-favicon.svg +0 -1
  66. data/src/cmdx-light-logo.png +0 -0
  67. data/src/cmdx-logo.svg +0 -1
data/docs/deprecation.md DELETED
@@ -1,145 +0,0 @@
1
- # Task Deprecation
2
-
3
- Manage legacy tasks gracefully with built-in deprecation support. Choose how to handle deprecated tasks—log warnings for awareness, issue Ruby warnings for development, or prevent execution entirely.
4
-
5
- ## Modes
6
-
7
- ### Raise
8
-
9
- Prevent task execution completely. Perfect for tasks that must no longer run.
10
-
11
- !!! warning
12
-
13
- Use `:raise` mode carefully—it will break existing workflows immediately.
14
-
15
- ```ruby
16
- class ProcessObsoleteAPI < CMDx::Task
17
- settings(deprecated: :raise)
18
-
19
- def work
20
- # Will never execute...
21
- end
22
- end
23
-
24
- result = ProcessObsoleteAPI.execute
25
- #=> raises CMDx::DeprecationError: "ProcessObsoleteAPI usage prohibited"
26
- ```
27
-
28
- ### Log
29
-
30
- Allow execution while tracking deprecation in logs. Ideal for gradual migrations.
31
-
32
- ```ruby
33
- class ProcessLegacyFormat < CMDx::Task
34
- settings(deprecated: :log)
35
-
36
- # Same
37
- settings(deprecated: true)
38
-
39
- def work
40
- # Executes but logs deprecation warning...
41
- end
42
- end
43
-
44
- result = ProcessLegacyFormat.execute
45
- result.successful? #=> true
46
-
47
- # Deprecation warning appears in logs:
48
- # WARN -- : DEPRECATED: ProcessLegacyFormat - migrate to replacement or discontinue use
49
- ```
50
-
51
- ### Warn
52
-
53
- Issue Ruby warnings visible during development and testing. Keeps production logs clean while alerting developers.
54
-
55
- ```ruby
56
- class ProcessOldData < CMDx::Task
57
- settings(deprecated: :warn)
58
-
59
- def work
60
- # Executes but emits Ruby warning...
61
- end
62
- end
63
-
64
- result = ProcessOldData.execute
65
- result.successful? #=> true
66
-
67
- # Ruby warning appears in stderr:
68
- # [ProcessOldData] DEPRECATED: migrate to a replacement or discontinue use
69
- ```
70
-
71
- ## Declarations
72
-
73
- ### Symbol or String
74
-
75
- ```ruby
76
- class OutdatedConnector < CMDx::Task
77
- # Symbol
78
- settings(deprecated: :raise)
79
-
80
- # String
81
- settings(deprecated: "warn")
82
- end
83
- ```
84
-
85
- ### Boolean or Nil
86
-
87
- ```ruby
88
- class OutdatedConnector < CMDx::Task
89
- # Deprecates with default :log mode
90
- settings(deprecated: true)
91
-
92
- # Skips deprecation
93
- settings(deprecated: false)
94
- settings(deprecated: nil)
95
- end
96
- ```
97
-
98
- ### Method
99
-
100
- ```ruby
101
- class OutdatedConnector < CMDx::Task
102
- # Symbol
103
- settings(deprecated: :deprecated?)
104
-
105
- def work
106
- # Your logic here...
107
- end
108
-
109
- private
110
-
111
- def deprecated?
112
- Time.now.year > 2024 ? :raise : false
113
- end
114
- end
115
- ```
116
-
117
- ### Proc or Lambda
118
-
119
- ```ruby
120
- class OutdatedConnector < CMDx::Task
121
- # Proc
122
- settings(deprecated: proc { Rails.env.development? ? :raise : :log })
123
-
124
- # Lambda
125
- settings(deprecated: -> { Current.tenant.legacy_mode? ? :warn : :raise })
126
- end
127
- ```
128
-
129
- ### Class or Module
130
-
131
- ```ruby
132
- class OutdatedTaskDeprecator
133
- def call(task)
134
- task.class.name.include?("Outdated")
135
- end
136
- end
137
-
138
- class OutdatedConnector < CMDx::Task
139
- # Class or Module
140
- settings(deprecated: OutdatedTaskDeprecator)
141
-
142
- # Instance
143
- settings(deprecated: OutdatedTaskDeprecator.new)
144
- end
145
- ```
@@ -1,126 +0,0 @@
1
- # Getting Started
2
-
3
- CMDx is a Ruby framework for building maintainable, observable business logic through composable command objects. It brings structure, consistency, and powerful developer tools to your business processes.
4
-
5
- **Common challenges:**
6
-
7
- - Inconsistent service object patterns across your codebase
8
- - Black boxes make debugging a nightmare
9
- - Fragile error handling erodes confidence
10
-
11
- **What you get:**
12
-
13
- - Consistent, standardized architecture
14
- - Built-in flow control and error handling
15
- - Composable, reusable workflows
16
- - Comprehensive logging for observability
17
- - Attribute validation with type coercions
18
- - Sensible defaults and developer-friendly APIs
19
-
20
- ## Installation
21
-
22
- Add CMDx to your Gemfile:
23
-
24
- ```sh
25
- gem install cmdx
26
-
27
- # - or -
28
-
29
- bundle add cmdx
30
- ```
31
-
32
- ## Configuration
33
-
34
- For Rails applications, run the following command to generate a global configuration file in `config/initializers/cmdx.rb`.
35
-
36
- ```bash
37
- rails generate cmdx:install
38
- ```
39
-
40
- If not using Rails, manually copy the [configuration file](https://github.com/drexed/cmdx/blob/main/lib/generators/cmdx/templates/install.rb).
41
-
42
- ## The CERO Pattern
43
-
44
- CMDx embraces the Compose, Execute, React, Observe (CERO, pronounced "zero") pattern—a simple yet powerful approach to building reliable business logic.
45
-
46
- ### Compose
47
-
48
- Build reusable, single-responsibility tasks with typed attributes, validation, and callbacks. Tasks can be chained together in workflows to create complex business processes from simple building blocks.
49
-
50
- ```ruby
51
- class AnalyzeMetrics < CMDx::Task
52
- def work
53
- # Your logic here...
54
- end
55
- end
56
- ```
57
-
58
- ### Execute
59
-
60
- Invoke tasks with a consistent API that always returns a result object. Execution automatically handles validation, type coercion, error handling, and logging. Arguments are validated and coerced before your task logic runs.
61
-
62
- ```ruby
63
- # Without args
64
- result = AnalyzeMetrics.execute
65
-
66
- # With args
67
- result = AnalyzeMetrics.execute(model: "blackbox", "sensitivity" => 3)
68
- ```
69
-
70
- ### React
71
-
72
- Every execution returns a result object with a clear outcome. Check the result's state (`success?`, `failed?`, `skipped?`) and access returned values, error messages, and metadata to make informed decisions.
73
-
74
- ```ruby
75
- if result.success?
76
- # Handle success
77
- elsif result.skipped?
78
- # Handle skipped
79
- elsif result.failed?
80
- # Handle failed
81
- end
82
- ```
83
-
84
- ### Observe
85
-
86
- Every task execution generates structured logs with execution chains, runtime metrics, and contextual metadata. Logs can be automatically correlated using chain IDs, making it easy to trace complex workflows and debug issues.
87
-
88
- ```log
89
- I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
90
- index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
91
-
92
- I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
93
- index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
94
- ```
95
-
96
- ## Task Generator
97
-
98
- Generate new CMDx tasks quickly using the built-in generator:
99
-
100
- ```bash
101
- rails generate cmdx:task ModerateBlogPost
102
- ```
103
-
104
- This creates a new task file with the basic structure:
105
-
106
- ```ruby
107
- # app/tasks/moderate_blog_post.rb
108
- class ModerateBlogPost < CMDx::Task
109
- def work
110
- # Your logic here...
111
- end
112
- end
113
- ```
114
-
115
- !!! tip
116
-
117
- Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument`
118
-
119
- ## Type safety
120
-
121
- CMDx includes built-in RBS (Ruby Type Signature) inline annotations throughout the codebase, providing type information for static analysis and editor support.
122
-
123
- - **Type checking** — Catch type errors before runtime using tools like Steep or TypeProf
124
- - **Better IDE support** — Enhanced autocomplete, navigation, and inline documentation
125
- - **Self-documenting code** — Clear method signatures and return types
126
- - **Refactoring confidence** — Type-aware refactoring reduces bugs
data/docs/index.md DELETED
@@ -1,134 +0,0 @@
1
- # CMDx
2
-
3
- Build business logic that's powerful, predictable, and maintainable.
4
-
5
- [![Version](https://img.shields.io/gem/v/cmdx)](https://rubygems.org/gems/cmdx)
6
- [![Build](https://github.com/drexed/cmdx/actions/workflows/ci.yml/badge.svg)](https://github.com/drexed/cmdx/actions/workflows/ci.yml)
7
- [![License](https://img.shields.io/github/license/drexed/cmdx)](https://github.com/drexed/cmdx/blob/main/LICENSE.txt)
8
-
9
- ---
10
-
11
- Say goodbye to messy service objects. CMDx (pronounced "Command X") helps you design business logic with clarity and consistency—build faster, debug easier, and ship with confidence.
12
-
13
- !!! note
14
-
15
- Documentation reflects the latest code on `main`. For version-specific documentation, please refer to the `docs/` directory within that version's tag.
16
-
17
- ## Requirements
18
-
19
- - Ruby: MRI 3.1+ or JRuby 9.4+.
20
-
21
- CMDx works with any Ruby framework. Rails support is built-in, but it's framework-agnostic at its core.
22
-
23
- ## Installation
24
-
25
- ```sh
26
- gem install cmdx
27
-
28
- # - or -
29
-
30
- bundle add cmdx
31
- ```
32
-
33
- ## Quick Example
34
-
35
- Build powerful business logic in four simple steps:
36
-
37
- ### 1. Compose
38
-
39
- === "Full Featured Task"
40
-
41
- ```ruby
42
- class AnalyzeMetrics < CMDx::Task
43
- register :middleware, CMDx::Middlewares::Correlate, id: -> { Current.request_id }
44
-
45
- on_success :track_analysis_completion!
46
-
47
- required :dataset_id, type: :integer, numeric: { min: 1 }
48
- optional :analysis_type, default: "standard"
49
-
50
- def work
51
- if dataset.nil?
52
- fail!("Dataset not found", code: 404)
53
- elsif dataset.unprocessed?
54
- skip!("Dataset not ready for analysis")
55
- else
56
- context.result = PValueAnalyzer.execute(dataset:, analysis_type:)
57
- context.analyzed_at = Time.now
58
-
59
- SendAnalyzedEmail.execute(user_id: Current.account.manager_id)
60
- end
61
- end
62
-
63
- private
64
-
65
- def dataset
66
- @dataset ||= Dataset.find_by(id: dataset_id)
67
- end
68
-
69
- def track_analysis_completion!
70
- dataset.update!(analysis_result_id: context.result.id)
71
- end
72
- end
73
- ```
74
-
75
- === "Minimum Viable Task"
76
-
77
- ```ruby
78
- class SendAnalyzedEmail < CMDx::Task
79
- def work
80
- user = User.find(context.user_id)
81
- MetricsMailer.analyzed(user).deliver_now
82
- end
83
- end
84
- ```
85
-
86
- ### 2. Execute
87
-
88
- ```ruby
89
- result = AnalyzeMetrics.execute(
90
- dataset_id: 123,
91
- "analysis_type" => "advanced"
92
- )
93
- ```
94
-
95
- ### 3. React
96
-
97
- ```ruby
98
- if result.success?
99
- puts "Metrics analyzed at #{result.context.analyzed_at}"
100
- elsif result.skipped?
101
- puts "Skipping analyzation due to: #{result.reason}"
102
- elsif result.failed?
103
- puts "Analyzation failed due to: #{result.reason} with code #{result.metadata[:code]}"
104
- end
105
- ```
106
-
107
- ### 4. Observe
108
-
109
- ```log
110
- I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
111
- index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
112
-
113
- I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
114
- index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
115
- ```
116
-
117
- Ready to dive in? Check out the [Getting Started](getting_started.md) guide to learn more.
118
-
119
- ## Ecosystem
120
-
121
- - [cmdx-rspec](https://github.com/drexed/cmdx-rspec) - RSpec test matchers
122
-
123
- For backwards compatibility of certain functionality:
124
-
125
- - [cmdx-i18n](https://github.com/drexed/cmdx-i18n) - 85+ translations, `v1.5.0` - `v1.6.2`
126
- - [cmdx-parallel](https://github.com/drexed/cmdx-parallel) - Parallel workflow tasks, `v1.6.1` - `v1.6.2`
127
-
128
- ## Contributing
129
-
130
- Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](https://github.com/drexed/cmdx/blob/main/CODE_OF_CONDUCT.md).
131
-
132
- ## License
133
-
134
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,126 +0,0 @@
1
- # Internationalization (i18n)
2
-
3
- CMDx supports 90+ languages out of the box for all error messages, validations, coercions, and faults. Error messages automatically adapt to the current `I18n.locale`, making it easy to build applications for global audiences.
4
-
5
- ## Usage
6
-
7
- All error messages are automatically localized based on your current locale:
8
-
9
- ```ruby
10
- class ProcessQuote < CMDx::Task
11
- attribute :price, type: :float
12
-
13
- def work
14
- # Your logic here...
15
- end
16
- end
17
-
18
- I18n.with_locale(:fr) do
19
- result = ProcessQuote.execute(price: "invalid")
20
- result.metadata[:messages][:price] #=> ["impossible de contraindre en float"]
21
- end
22
- ```
23
-
24
- ## Configuration
25
-
26
- CMDx uses the `I18n` gem for localization. In Rails, locales load automatically.
27
-
28
- ### Copy Locale Files
29
-
30
- Copy locale files to your Rails application's `config/locales` directory:
31
-
32
- ```bash
33
- rails generate cmdx:locale [LOCALE]
34
-
35
- # Eg: generate french locale
36
- rails generate cmdx:locale fr
37
- ```
38
-
39
- ### Available Locales
40
-
41
- - af - Afrikaans
42
- - ar - Arabic
43
- - az - Azerbaijani
44
- - be - Belarusian
45
- - bg - Bulgarian
46
- - bn - Bengali
47
- - bs - Bosnian
48
- - ca - Catalan
49
- - cnr - Montenegrin
50
- - cs - Czech
51
- - cy - Welsh
52
- - da - Danish
53
- - de - German
54
- - dz - Dzongkha
55
- - el - Greek
56
- - en - English
57
- - eo - Esperanto
58
- - es - Spanish
59
- - et - Estonian
60
- - eu - Basque
61
- - fa - Persian
62
- - fi - Finnish
63
- - fr - French
64
- - fy - Western Frisian
65
- - gd - Scottish Gaelic
66
- - gl - Galician
67
- - he - Hebrew
68
- - hi - Hindi
69
- - hr - Croatian
70
- - hu - Hungarian
71
- - hy - Armenian
72
- - id - Indonesian
73
- - is - Icelandic
74
- - it - Italian
75
- - ja - Japanese
76
- - ka - Georgian
77
- - kk - Kazakh
78
- - km - Khmer
79
- - kn - Kannada
80
- - ko - Korean
81
- - lb - Luxembourgish
82
- - lo - Lao
83
- - lt - Lithuanian
84
- - lv - Latvian
85
- - mg - Malagasy
86
- - mk - Macedonian
87
- - ml - Malayalam
88
- - mn - Mongolian
89
- - mr-IN - Marathi (India)
90
- - ms - Malay
91
- - nb - Norwegian Bokmål
92
- - ne - Nepali
93
- - nl - Dutch
94
- - nn - Norwegian Nynorsk
95
- - oc - Occitan
96
- - or - Odia
97
- - pa - Punjabi
98
- - pl - Polish
99
- - pt - Portuguese
100
- - rm - Romansh
101
- - ro - Romanian
102
- - ru - Russian
103
- - sc - Sardinian
104
- - sk - Slovak
105
- - sl - Slovenian
106
- - sq - Albanian
107
- - sr - Serbian
108
- - st - Southern Sotho
109
- - sv - Swedish
110
- - sw - Swahili
111
- - ta - Tamil
112
- - te - Telugu
113
- - th - Thai
114
- - tl - Tagalog
115
- - tr - Turkish
116
- - tt - Tatar
117
- - ug - Uyghur
118
- - uk - Ukrainian
119
- - ur - Urdu
120
- - uz - Uzbek
121
- - vi - Vietnamese
122
- - wo - Wolof
123
- - zh-CN - Chinese (Simplified)
124
- - zh-HK - Chinese (Hong Kong)
125
- - zh-TW - Chinese (Traditional)
126
- - zh-YUE - Chinese (Yue)
@@ -1,52 +0,0 @@
1
- # Interruptions - Exceptions
2
-
3
- Exception handling differs between `execute` and `execute!`. Choose the method that matches your error handling strategy.
4
-
5
- ## Exception Handling
6
-
7
- !!! warning "Important"
8
-
9
- Prefer `skip!` and `fail!` over raising exceptions—they signal intent more clearly.
10
-
11
- ### Non-bang execution
12
-
13
- Captures all exceptions and returns them as failed results:
14
-
15
- ```ruby
16
- class CompressDocument < CMDx::Task
17
- def work
18
- document = Document.find(context.document_id)
19
- document.compress!
20
- end
21
- end
22
-
23
- result = CompressDocument.execute(document_id: "unknown-doc-id")
24
- result.state #=> "interrupted"
25
- result.status #=> "failed"
26
- result.failed? #=> true
27
- result.reason #=> "[ActiveRecord::NotFoundError] record not found"
28
- result.cause #=> <ActiveRecord::NotFoundError>
29
- ```
30
-
31
- !!! note
32
-
33
- Use `exception_handler` with `execute` to send exceptions to APM tools before they become failed results.
34
-
35
- ### Bang execution
36
-
37
- Lets exceptions propagate naturally for standard Ruby error handling:
38
-
39
- ```ruby
40
- class CompressDocument < CMDx::Task
41
- def work
42
- document = Document.find(context.document_id)
43
- document.compress!
44
- end
45
- end
46
-
47
- begin
48
- CompressDocument.execute!(document_id: "unknown-doc-id")
49
- rescue ActiveRecord::NotFoundError => e
50
- puts "Handle exception: #{e.message}"
51
- end
52
- ```