mu-action 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e178de7158d182e1527dd7b23ede20e67f9010e4bc588869fe433ebd4f2d1c85
4
- data.tar.gz: 424feef48df8a60bd7c9aaf5ea755744d804f818e54fe2dd6d9a2ec4aea70595
3
+ metadata.gz: 5765a5a8a0a0af6628f4b36d5e7dde4337eb44ed1e44275208b151276d4de412
4
+ data.tar.gz: 6c906cf41b59a6fde0b7b4482d3029fca4a48f2767a4958f0d9d54a9ed661d79
5
5
  SHA512:
6
- metadata.gz: 7776bdf1fbb430638cb882abf2558d397b8dea4d7ea69c237ab9c3d9f0d3ddd5f05f4c66bb2cad79e16f02d2fb6b8a0b14d99bcba2bac01c99b5e8249763cc8d
7
- data.tar.gz: aa9b022415a7905aa29ab543fed97763842e2b6ccf18e08e4b91b62479bf38651eb2dc518c9c5b7f49f3413f80de896886e534ac03473fabaaf6d82c1c250ccd
6
+ metadata.gz: 9c6f9145debfaee371e5330f8922d8739489358e5aaf7aa6d6d4f5ab1c445a880725a9314d3bf1d7ceddc1b9209a41ac68638dc9aba40985280d295f08250468
7
+ data.tar.gz: 6a5b5a71d34add535fcb0672963a99c8414e8038776d376cc04bb81c8bc9d7fbe68561a7c2f7ef5ab10740ddaf02b6348bd85e9c8f8fedd7afd32e4e2126bd14
data/AGENTS.md ADDED
@@ -0,0 +1,32 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure & Module Organization
4
+ - `lib/mu/action` holds the core interactor implementation; mirror this layout when adding new components so `lib/mu/action/foo.rb` pairs with `Mu::Action::Foo`.
5
+ - `spec/` mirrors the library structure with RSpec examples; add a matching `_spec.rb` file for every public entry point.
6
+ - `sig/` provides RBS signatures that keep types honest—update them alongside code changes.
7
+ - `bin/` hosts contributor tooling (`check`, `console`, `test_readme_examples`); treat `readme_examples.rb` as generated output.
8
+
9
+ ## Build, Test, and Development Commands
10
+ - `bin/check` aggregates lint, spec, and README example runs for a pre-PR smoke test. Use this instead of `rake`.
11
+ - `bundle exec rspec spec/mu/action/hook_spec.rb` executes targeted tests; leave focused specs checked in only when necessary.
12
+ - `bundle exec rubocop` enforces style; let it guide formatting instead of manual tweaks.
13
+ - `bundle exec steep check` validates Steep signatures against implementation.
14
+ - `bin/test_readme_examples --extract-only` regenerates `readme_examples.rb` after documentation updates.
15
+
16
+ ## Coding Style & Naming Conventions
17
+ - Target Ruby 3.1 with two-space indentation and trailing commas only when required.
18
+ - RuboCop enforces double-quoted strings and general style—prefer fixes via `bundle exec rubocop -A` over manual edits.
19
+ - Match class and module names to their file paths (e.g., `Mu::Action::Result` lives in `lib/mu/action/result.rb`), and keep method names snake_case verbs.
20
+ - Favor small, composable interactors with explicit `prop` declarations and `Success`/`Failure` returns to stay idiomatic.
21
+
22
+ ## Testing Guidelines
23
+ - Write RSpec examples that describe observable behavior; structure files as `describe Mu::Action::Feature` with nested `context` blocks.
24
+ - Cover both success and failure branches, including metadata expectations when hooks mutate state.
25
+ - Keep factories lightweight—inline doubles or `let` helpers beat global fixtures for clarity.
26
+ - Run `bundle exec rspec` locally and ensure README examples still execute via `bin/check` whenever documentation changes.
27
+
28
+ ## Commit & Pull Request Guidelines
29
+ - Follow the existing Conventional Commit style (`feat:`, `chore:`, `docs:`) visible in `git log --oneline`.
30
+ - Keep commits scoped to a single concern and include any signature or README updates in the same change.
31
+ - Open pull requests with a brief summary, linked issues (if any), and mention any developer-facing changes or new scripts.
32
+ - State that `bin/check` passes; add screenshots or console snippets only when behavior changes are user-visible.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-10-30
4
+
5
+ ### Added
6
+ - Support registering `before`, `after`, and `around` hooks by method name.
7
+ - Steep type-checking infrastructure: Steepfile, comprehensive RBS signatures for `Mu::Action`.
8
+ - Guard plugin for steep type checking.
9
+
10
+ ### Changed
11
+ - `bin/check` now runs `bundle exec steep check` as part of the default contributor workflow.
12
+ - Guard automation reorganized to load the new Steep guard explicitly.
13
+ - Updated Bundler metadata to 2.7.2.
14
+
15
+ ### Documentation
16
+ - Added `AGENTS.md` with contributor and workflow guidelines.
17
+
3
18
  ## [0.1.0] - 2025-07-23
4
19
 
5
20
  ### Added
data/Guardfile CHANGED
@@ -1,5 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "guard/steep"
4
+
5
+ guard :rubocop, cli: "--format progress" do
6
+ watch(/.+\.rb$/)
7
+ watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { File.dirname(_1[0]) }
8
+ end
9
+
10
+ guard :steep, command: "bundle exec steep check" do
11
+ watch("Steepfile")
12
+ watch(%r{^sig/.+\.rbs$})
13
+ watch(%r{^lib/.+\.rb$})
14
+ end
15
+
3
16
  guard :rspec, cmd: "bundle exec rspec" do
4
17
  require "guard/rspec/dsl"
5
18
  dsl = Guard::RSpec::Dsl.new(self)
@@ -19,8 +32,3 @@ guard :rspec, cmd: "bundle exec rspec" do
19
32
  watch(%r{^lib/mu/action/(.+)\.rb$}) { "spec/mu/action_spec.rb" }
20
33
  watch(%r{^lib/mu/action\.rb$}) { Dir["spec/*"] }
21
34
  end
22
-
23
- guard :rubocop, cli: "--format progress" do
24
- watch(/.+\.rb$/)
25
- watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
26
- end
data/README.md CHANGED
@@ -124,6 +124,41 @@ result = ProcessPayment.call(amount: 100.0, card_token: "tok_123")
124
124
  assert result.value[:payment_id] == "pay_123"
125
125
  ```
126
126
 
127
+ Hooks can also be registered by method name if you prefer keeping the logic in dedicated instance methods:
128
+
129
+ ```ruby
130
+ class ProcessPayment
131
+ include Mu::Action
132
+
133
+ prop :amount, Float
134
+ prop :card_token, String
135
+
136
+ before :prepare_logging
137
+ after :finalize_logging
138
+ around :wrap_in_transaction
139
+
140
+ def prepare_logging
141
+ meta[:started_at] = Time.now
142
+ Rails.logger.info "Processing payment for $#{@amount}"
143
+ end
144
+
145
+ def finalize_logging
146
+ meta[:completed_at] = Time.now
147
+ Rails.logger.info "Payment processing completed"
148
+ end
149
+
150
+ def wrap_in_transaction(chain)
151
+ ActiveRecord::Base.transaction { chain.call }
152
+ end
153
+
154
+ def call
155
+ Success(payment_id: "pay_123")
156
+ end
157
+ end
158
+ ```
159
+
160
+ Around hook methods may also receive no arguments and use `yield`, or accept two arguments `(action, chain)` to mirror the block-based signature.
161
+
127
162
  ### Custom Result Types
128
163
 
129
164
  Define typed results for better API contracts:
data/Steepfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ target :lib do
4
+ signature "sig"
5
+ check "lib"
6
+
7
+ configure_code_diagnostics(Steep::Diagnostic::Ruby.default)
8
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "guard/plugin"
4
+
5
+ # steep:ignore:start
6
+ module Guard
7
+ # Guard plugin that runs Steep type checking whenever files change.
8
+ class Steep < Plugin
9
+ def initialize(options = {})
10
+ super
11
+ @command = options.fetch(:command, "bundle exec steep check")
12
+ end
13
+
14
+ def start = run_steep
15
+ def run_all = run_steep
16
+ def run_on_additions(_paths) = run_steep
17
+ def run_on_modifications(_paths) = run_steep
18
+ def run_on_removals(_paths) = run_steep
19
+
20
+ private
21
+
22
+ def run_steep
23
+ UI.info "📐 Running Steep..."
24
+ success = system(@command)
25
+ UI.error "Steep check failed" unless success
26
+ end
27
+ end
28
+ end
29
+ # steep:ignore:end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Mu
4
4
  module Action
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
data/lib/mu/action.rb CHANGED
@@ -24,7 +24,7 @@ module Mu
24
24
  module MetaPropAdder
25
25
  def new(...)
26
26
  unless @meta_prop_added
27
- prop :meta, Hash, default: -> { {} }
27
+ prop :meta, Hash, default: -> { {} } # steep:ignore UnannotatedEmptyCollection
28
28
  @meta_prop_added = true
29
29
  end
30
30
  instance = super
@@ -65,7 +65,7 @@ module Mu
65
65
  # Base result class that wraps action outcomes with success/failure state
66
66
  # and metadata. Extended by Success and Failure classes for pattern matching.
67
67
  class Result < Literal::Struct
68
- prop :meta, Hash, default: -> { {} }
68
+ prop :meta, Hash, default: -> { {} } # steep:ignore UnannotatedEmptyCollection
69
69
 
70
70
  attr_reader :meta
71
71
  end
@@ -111,9 +111,9 @@ module Mu
111
111
  # Class methods added to action classes when including Mu::Action.
112
112
  # Provides hook registration, execution methods, and result type definition.
113
113
  module ClassMethods
114
- def around(&block) = around_hooks << block
115
- def before(&block) = before_hooks << block
116
- def after(&block) = after_hooks << block
114
+ def around(*method_names, &block) = register_hook(around_hooks, method_names, block, name: :around)
115
+ def before(*method_names, &block) = register_hook(before_hooks, method_names, block, name: :before)
116
+ def after(*method_names, &block) = register_hook(after_hooks, method_names, block, name: :after)
117
117
 
118
118
  def around_hooks = (@around_hooks ||= [])
119
119
  def before_hooks = (@before_hooks ||= [])
@@ -140,6 +140,40 @@ module Mu
140
140
 
141
141
  const_set(:Success, success_class)
142
142
  end
143
+
144
+ private
145
+
146
+ def register_hook(collection, method_names, block, name:)
147
+ ensure_valid_hook_inputs(method_names, block, name:)
148
+ additions = normalize_hook_inputs(method_names, block, name:)
149
+ collection.concat(additions)
150
+ end
151
+
152
+ def normalize_hook_inputs(method_names, block, name:)
153
+ if block
154
+ [block]
155
+ else
156
+ symbolize_hook_names(method_names, name:)
157
+ end
158
+ end
159
+
160
+ def ensure_valid_hook_inputs(method_names, block, name:)
161
+ return unless block && method_names.any?
162
+
163
+ raise ArgumentError, "#{name} hooks accept either a block or method names, not both"
164
+ end
165
+
166
+ def symbolize_hook_names(method_names, name:)
167
+ raise ArgumentError, "#{name} hook requires a block or method name" if method_names.empty?
168
+
169
+ method_names.map do |method_name|
170
+ unless method_name.respond_to?(:to_sym)
171
+ raise ArgumentError, "Invalid #{name} hook identifier: #{method_name.inspect}"
172
+ end
173
+
174
+ method_name.to_sym
175
+ end
176
+ end
143
177
  end
144
178
 
145
179
  # rubocop:disable Naming/MethodName
@@ -171,18 +205,60 @@ module Mu
171
205
 
172
206
  private
173
207
 
174
- def run_before_hooks = self.class.before_hooks.each { instance_exec(&_1) }
175
- def run_after_hooks = self.class.after_hooks.each { instance_exec(&_1) }
208
+ def run_before_hooks = self.class.before_hooks.each { execute_simple_hook(_1) }
209
+ def run_after_hooks = self.class.after_hooks.each { execute_simple_hook(_1) }
176
210
 
177
211
  def build_around_chain(&block)
178
212
  chain = block
179
213
  self.class.around_hooks.reverse_each do |hook|
180
214
  previous = chain
181
- chain = -> { instance_exec(self, previous, &hook) }
215
+ chain = build_around_wrapper(hook, previous)
182
216
  end
183
217
  chain
184
218
  end
185
219
 
220
+ def execute_simple_hook(hook)
221
+ if hook.is_a?(Proc)
222
+ # @type var hook: ^(*untyped) -> untyped
223
+ return instance_exec(&hook)
224
+ end
225
+
226
+ send(hook)
227
+ end
228
+
229
+ def build_around_wrapper(hook, previous)
230
+ case hook
231
+ when Proc
232
+ lambda do
233
+ # @type var hook: ^(*untyped) -> untyped
234
+ instance_exec(self, previous, &hook)
235
+ end
236
+ else
237
+ -> { invoke_around_method(hook, previous) }
238
+ end
239
+ end
240
+
241
+ def invoke_around_method(hook, previous)
242
+ method_name = hook.to_sym
243
+ method_object = resolve_method(method_name)
244
+ arguments = around_arguments(method_object, previous)
245
+ method_object.bind(self).call(*arguments, &previous)
246
+ end
247
+
248
+ def around_arguments(method_object, previous)
249
+ params = method_object.parameters.reject { _1.first == :block }
250
+ return [] if params.empty?
251
+ return [previous] if params.length == 1
252
+
253
+ [self, previous]
254
+ end
255
+
256
+ def resolve_method(method_name)
257
+ self.class.instance_method(method_name)
258
+ rescue NameError
259
+ raise NoMethodError, "Undefined hook method ##{method_name} for #{self.class}"
260
+ end
261
+
186
262
  def call
187
263
  raise NotImplementedError, "You must implement the call method"
188
264
  end
data/sig/kernel.rbs ADDED
@@ -0,0 +1,3 @@
1
+ module Kernel
2
+ def instance_exec: (*untyped) { (*untyped) -> untyped } -> untyped
3
+ end
data/sig/literal.rbs ADDED
@@ -0,0 +1,13 @@
1
+ module Literal
2
+ module Properties
3
+ def prop: (Symbol, untyped, *untyped, ?default: untyped) -> untyped
4
+ def _Any: () -> untyped
5
+ end
6
+
7
+ class Struct
8
+ extend Properties
9
+
10
+ def self.prop: (Symbol, untyped, *untyped, ?default: untyped) -> untyped
11
+ def self.literal_properties: () -> ::Array[untyped]
12
+ end
13
+ end
data/sig/mu/action.rbs CHANGED
@@ -1,6 +1,116 @@
1
1
  module Mu
2
2
  module Action
3
- VERSION: String
4
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+ include Kernel
4
+
5
+ VERSION: ::String
6
+ attr_reader meta: ::Hash[untyped, untyped]
7
+
8
+ interface _ActionInstance
9
+ def meta: () -> ::Hash[untyped, untyped]
10
+ def class: () -> _ActionClass
11
+ def run: () -> ::Mu::Action::Result
12
+ def run!: () -> ::Mu::Action::Result
13
+ end
14
+
15
+ interface _ActionClass
16
+ def literal_properties: () -> ::Array[untyped]
17
+ def before_hooks: () -> ::Array[untyped]
18
+ def after_hooks: () -> ::Array[untyped]
19
+ def around_hooks: () -> ::Array[untyped]
20
+ def const_get: (Symbol) -> ::Class
21
+ def instance_method: (Symbol) -> UnboundMethod
22
+ def new: (*untyped, **untyped) -> _ActionInstance
23
+ end
24
+
25
+ def self.included: (untyped base) -> void
26
+ def self.before_hooks: () -> ::Array[untyped]
27
+ def self.after_hooks: () -> ::Array[untyped]
28
+ def self.around_hooks: () -> ::Array[untyped]
29
+
30
+ module MetaPropAdder
31
+ include Literal::Properties
32
+
33
+ def new: (*untyped, **untyped) -> untyped
34
+ def inherited: (untyped subclass) -> void
35
+ end
36
+
37
+ module HookPropagator
38
+ def inherited: (untyped subclass) -> void
39
+ def before_hooks: () -> ::Array[untyped]
40
+ def after_hooks: () -> ::Array[untyped]
41
+ def around_hooks: () -> ::Array[untyped]
42
+ end
43
+
44
+ class FailureError < StandardError
45
+ attr_reader error: untyped
46
+ attr_reader meta: ::Hash[untyped, untyped]
47
+
48
+ def initialize: (untyped error, ?meta: ::Hash[untyped, untyped]) -> void
49
+ end
50
+
51
+ class Result < Literal::Struct
52
+ attr_reader meta: ::Hash[untyped, untyped]
53
+
54
+ def initialize: (*untyped, **untyped) -> void
55
+ end
56
+
57
+ class Success < Result
58
+ attr_reader value: untyped
59
+
60
+ def initialize: (*untyped, **untyped) -> void
61
+ def success?: () -> true
62
+ def failure?: () -> false
63
+ end
64
+
65
+ class Failure < Result
66
+ attr_reader error: untyped
67
+
68
+ def initialize: (*untyped, **untyped) -> void
69
+ def success?: () -> false
70
+ def failure?: () -> true
71
+ end
72
+
73
+ module Initializer
74
+ def initialize: (*untyped, **untyped) -> void
75
+ def initialize_meta: () -> void
76
+ def meta: () -> ::Hash[untyped, untyped]
77
+ def self.literal_properties: () -> ::Array[untyped]
78
+ end
79
+
80
+ module ClassMethods
81
+ include Literal::Properties
82
+
83
+ def around: (*Symbol) ?{ (*untyped) -> untyped } -> ::Array[untyped]
84
+ def before: (*Symbol) ?{ (*untyped) -> untyped } -> ::Array[untyped]
85
+ def after: (*Symbol) ?{ (*untyped) -> untyped } -> ::Array[untyped]
86
+ def around_hooks: () -> ::Array[untyped]
87
+ def before_hooks: () -> ::Array[untyped]
88
+ def after_hooks: () -> ::Array[untyped]
89
+ def call: (*untyped, **untyped) -> untyped
90
+ def call!: (*untyped, **untyped) -> untyped
91
+ def result: (untyped type) -> ::Class
92
+ def register_hook: (untyped collection, untyped method_names, ::Proc? block, name: Symbol) -> untyped
93
+ def normalize_hook_inputs: (untyped method_names, ::Proc? block, name: Symbol) -> untyped
94
+ def ensure_valid_hook_inputs: (untyped method_names, ::Proc? block, name: Symbol) -> void
95
+ def symbolize_hook_names: (untyped method_names, name: Symbol) -> ::Array[Symbol]
96
+ def const_set: (Symbol, ::Class) -> ::Class
97
+ def new: (*untyped, **untyped) -> _ActionInstance
98
+ end
99
+
100
+ def Success: (untyped value) -> Success
101
+ def Failure: (untyped error, **untyped) -> bot
102
+ def run: () -> Result
103
+ def run!: () -> Result
104
+ def result_class: () -> ::Class
105
+ def with_hooks: () { () -> Result } -> Result
106
+ def run_before_hooks: () -> ::Array[untyped]
107
+ def run_after_hooks: () -> ::Array[untyped]
108
+ def build_around_chain: () { () -> Result } -> ::Proc
109
+ def execute_simple_hook: (::Proc | ::Symbol) -> untyped
110
+ def build_around_wrapper: ((::Proc | ::Symbol), ::Proc) -> ::Proc
111
+ def invoke_around_method: (::Symbol, ::Proc) -> untyped
112
+ def around_arguments: (UnboundMethod, ::Proc) -> ::Array[untyped]
113
+ def resolve_method: (Symbol) -> UnboundMethod
114
+ def call: () -> untyped
5
115
  end
6
116
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mu-action
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Buduroi
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-07-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: literal
@@ -34,6 +33,7 @@ extra_rdoc_files: []
34
33
  files:
35
34
  - ".rspec"
36
35
  - ".rubocop.yml"
36
+ - AGENTS.md
37
37
  - CHANGELOG.md
38
38
  - CLAUDE.md
39
39
  - CODE_OF_CONDUCT.md
@@ -41,8 +41,12 @@ files:
41
41
  - LICENSE.txt
42
42
  - README.md
43
43
  - Rakefile
44
+ - Steepfile
45
+ - lib/guard/steep.rb
44
46
  - lib/mu/action.rb
45
47
  - lib/mu/action/version.rb
48
+ - sig/kernel.rbs
49
+ - sig/literal.rbs
46
50
  - sig/mu/action.rbs
47
51
  homepage: https://github.com/budu/mu-action
48
52
  licenses:
@@ -53,7 +57,6 @@ metadata:
53
57
  source_code_uri: https://github.com/budu/mu-action
54
58
  changelog_uri: https://github.com/budu/mu-action/blob/main/CHANGELOG.md
55
59
  rubygems_mfa_required: 'true'
56
- post_install_message:
57
60
  rdoc_options: []
58
61
  require_paths:
59
62
  - lib
@@ -68,8 +71,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
71
  - !ruby/object:Gem::Version
69
72
  version: '0'
70
73
  requirements: []
71
- rubygems_version: 3.5.22
72
- signing_key:
74
+ rubygems_version: 3.7.2
73
75
  specification_version: 4
74
76
  summary: Modern interactor pattern with type safety and metadata tracking
75
77
  test_files: []