axn 0.1.0.pre.alpha.1 → 0.1.0.pre.alpha.2

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.tool-versions +1 -0
  4. data/CHANGELOG.md +15 -1
  5. data/CONTRIBUTING.md +1 -1
  6. data/README.md +2 -2
  7. data/docs/.vitepress/config.mjs +18 -10
  8. data/docs/advanced/rough.md +2 -0
  9. data/docs/index.md +11 -3
  10. data/docs/{guide/index.md → intro/overview.md} +11 -32
  11. data/docs/recipes/memoization.md +46 -0
  12. data/docs/{usage → recipes}/testing.md +4 -2
  13. data/docs/reference/action-result.md +32 -9
  14. data/docs/reference/class.md +69 -12
  15. data/docs/reference/configuration.md +28 -15
  16. data/docs/reference/instance.md +96 -13
  17. data/docs/usage/setup.md +0 -2
  18. data/docs/usage/using.md +7 -15
  19. data/docs/usage/writing.md +45 -6
  20. data/lib/action/attachable/base.rb +43 -0
  21. data/lib/action/attachable/steps.rb +47 -0
  22. data/lib/action/attachable/subactions.rb +43 -0
  23. data/lib/action/attachable.rb +17 -0
  24. data/lib/action/{configuration.rb → core/configuration.rb} +1 -1
  25. data/lib/action/{context_facade.rb → core/context_facade.rb} +20 -30
  26. data/lib/action/{contract.rb → core/contract.rb} +16 -8
  27. data/lib/action/{exceptions.rb → core/exceptions.rb} +12 -1
  28. data/lib/action/{hoist_errors.rb → core/hoist_errors.rb} +3 -2
  29. data/lib/action/{swallow_exceptions.rb → core/swallow_exceptions.rb} +52 -7
  30. data/lib/axn/factory.rb +102 -0
  31. data/lib/axn/version.rb +1 -1
  32. data/lib/axn.rb +20 -10
  33. metadata +28 -23
  34. data/axn.gemspec +0 -45
  35. data/lib/action/organizer.rb +0 -41
  36. /data/docs/{usage → advanced}/conventions.md +0 -0
  37. /data/docs/{about/index.md → intro/about.md} +0 -0
  38. /data/docs/{advanced → recipes}/validating-user-input.md +0 -0
  39. /data/lib/action/{contract_validator.rb → core/contract_validator.rb} +0 -0
  40. /data/lib/action/{enqueueable.rb → core/enqueueable.rb} +0 -0
  41. /data/lib/action/{logging.rb → core/logging.rb} +0 -0
  42. /data/lib/action/{top_level_around_hook.rb → core/top_level_around_hook.rb} +0 -0
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Axn
4
+ class Factory
5
+ class << self
6
+ # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ParameterLists
7
+ def build(
8
+ # Builder-specific options
9
+ superclass: nil,
10
+ expose_return_as: :nil,
11
+
12
+ # Expose standard class-level options
13
+ exposes: {},
14
+ expects: {},
15
+ messages: {},
16
+ before: nil,
17
+ after: nil,
18
+ around: nil,
19
+
20
+ # Allow dynamically assigning rollback method
21
+ rollback: nil,
22
+ &block
23
+ )
24
+ args = block.parameters.each_with_object(_hash_with_default_array) { |(type, name), hash| hash[type] << name }
25
+
26
+ if args[:opt].present? || args[:req].present? || args[:rest].present?
27
+ raise ArgumentError,
28
+ "[Axn::Factory] Cannot convert block to action: block expects positional arguments"
29
+ end
30
+ raise ArgumentError, "[Axn::Factory] Cannot convert block to action: block expects a splat of keyword arguments" if args[:keyrest].present?
31
+
32
+ # TODO: is there any way to support default arguments? (if so, set allow_blank: true for those)
33
+ if args[:key].present?
34
+ raise ArgumentError,
35
+ "[Axn::Factory] Cannot convert block to action: block expects keyword arguments with defaults (ruby does not allow introspecting)"
36
+ end
37
+
38
+ expects = _hydrate_hash(expects)
39
+ exposes = _hydrate_hash(exposes)
40
+
41
+ Array(args[:keyreq]).each do |name|
42
+ expects[name] ||= {}
43
+ end
44
+
45
+ # NOTE: inheriting from wrapping class, so we can set default values (e.g. for HTTP headers)
46
+ Class.new(superclass || Object) do
47
+ include Action unless self < Action
48
+
49
+ define_method(:call) do
50
+ unwrapped_kwargs = Array(args[:keyreq]).each_with_object({}) do |name, hash|
51
+ hash[name] = public_send(name)
52
+ end
53
+
54
+ retval = instance_exec(**unwrapped_kwargs, &block)
55
+ expose(expose_return_as => retval) if expose_return_as.present?
56
+ end
57
+ end.tap do |axn| # rubocop: disable Style/MultilineBlockChain
58
+ expects.each do |name, opts|
59
+ axn.expects(name, **opts)
60
+ end
61
+
62
+ exposes.each do |name, opts|
63
+ axn.exposes(name, **opts)
64
+ end
65
+
66
+ axn.messages(**messages) if messages.present?
67
+
68
+ # Hooks
69
+ axn.before(before) if before.present?
70
+ axn.after(after) if after.present?
71
+ axn.around(around) if around.present?
72
+
73
+ # Rollback
74
+ if rollback.present?
75
+ raise ArgumentError, "[Axn::Factory] Rollback must be a callable" unless rollback.respond_to?(:call) && rollback.respond_to?(:arity)
76
+ raise ArgumentError, "[Axn::Factory] Rollback must be a callable with no arguments" unless rollback.arity.zero?
77
+
78
+ axn.define_method(:rollback) do
79
+ instance_exec(&rollback)
80
+ end
81
+ end
82
+
83
+ # Default exposure
84
+ axn.exposes(expose_return_as, allow_blank: true) if expose_return_as.present?
85
+ end
86
+ end
87
+ # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/ParameterLists
88
+
89
+ private
90
+
91
+ def _hash_with_default_array = Hash.new { |h, k| h[k] = [] }
92
+
93
+ def _hydrate_hash(given)
94
+ return given if given.is_a?(Hash)
95
+
96
+ Array(given).each_with_object({}) do |key, acc|
97
+ acc[key] = {}
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
data/lib/axn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Axn
4
- VERSION = "0.1.0-alpha.1"
4
+ VERSION = "0.1.0-alpha.2"
5
5
  end
data/lib/axn.rb CHANGED
@@ -4,19 +4,26 @@ module Axn; end
4
4
  require_relative "axn/version"
5
5
 
6
6
  require "interactor"
7
-
8
7
  require "active_support"
9
8
 
10
- require_relative "action/exceptions"
11
- require_relative "action/logging"
12
- require_relative "action/configuration"
13
- require_relative "action/top_level_around_hook"
14
- require_relative "action/contract"
15
- require_relative "action/swallow_exceptions"
16
- require_relative "action/hoist_errors"
9
+ require_relative "action/core/exceptions"
10
+ require_relative "action/core/logging"
11
+ require_relative "action/core/configuration"
12
+ require_relative "action/core/top_level_around_hook"
13
+ require_relative "action/core/contract"
14
+ require_relative "action/core/swallow_exceptions"
15
+ require_relative "action/core/hoist_errors"
16
+ require_relative "action/core/enqueueable"
17
+
18
+ require_relative "axn/factory"
19
+
20
+ require_relative "action/attachable"
17
21
 
18
- require_relative "action/organizer"
19
- require_relative "action/enqueueable"
22
+ def Axn(callable, **) # rubocop:disable Naming/MethodName
23
+ return callable if callable.is_a?(Class) && callable < Action
24
+
25
+ Axn::Factory.build(**, &callable)
26
+ end
20
27
 
21
28
  module Action
22
29
  def self.included(base)
@@ -37,6 +44,9 @@ module Action
37
44
 
38
45
  include Enqueueable
39
46
 
47
+ # --- Extensions ---
48
+ include Attachable
49
+
40
50
  # Allow additional automatic includes to be configured
41
51
  Array(Action.config.additional_includes).each { |mod| include mod }
42
52
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: axn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre.alpha.1
4
+ version: 0.1.0.pre.alpha.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kali Donovan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-03-24 00:00:00.000000000 Z
11
+ date: 2025-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -62,39 +62,44 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - ".rspec"
64
64
  - ".rubocop.yml"
65
+ - ".tool-versions"
65
66
  - CHANGELOG.md
66
67
  - CONTRIBUTING.md
67
68
  - LICENSE.txt
68
69
  - README.md
69
70
  - Rakefile
70
- - axn.gemspec
71
71
  - docs/.vitepress/config.mjs
72
- - docs/about/index.md
72
+ - docs/advanced/conventions.md
73
73
  - docs/advanced/rough.md
74
- - docs/advanced/validating-user-input.md
75
- - docs/guide/index.md
76
74
  - docs/index.md
75
+ - docs/intro/about.md
76
+ - docs/intro/overview.md
77
+ - docs/recipes/memoization.md
78
+ - docs/recipes/testing.md
79
+ - docs/recipes/validating-user-input.md
77
80
  - docs/reference/action-result.md
78
81
  - docs/reference/class.md
79
82
  - docs/reference/configuration.md
80
83
  - docs/reference/instance.md
81
- - docs/usage/conventions.md
82
84
  - docs/usage/setup.md
83
- - docs/usage/testing.md
84
85
  - docs/usage/using.md
85
86
  - docs/usage/writing.md
86
- - lib/action/configuration.rb
87
- - lib/action/context_facade.rb
88
- - lib/action/contract.rb
89
- - lib/action/contract_validator.rb
90
- - lib/action/enqueueable.rb
91
- - lib/action/exceptions.rb
92
- - lib/action/hoist_errors.rb
93
- - lib/action/logging.rb
94
- - lib/action/organizer.rb
95
- - lib/action/swallow_exceptions.rb
96
- - lib/action/top_level_around_hook.rb
87
+ - lib/action/attachable.rb
88
+ - lib/action/attachable/base.rb
89
+ - lib/action/attachable/steps.rb
90
+ - lib/action/attachable/subactions.rb
91
+ - lib/action/core/configuration.rb
92
+ - lib/action/core/context_facade.rb
93
+ - lib/action/core/contract.rb
94
+ - lib/action/core/contract_validator.rb
95
+ - lib/action/core/enqueueable.rb
96
+ - lib/action/core/exceptions.rb
97
+ - lib/action/core/hoist_errors.rb
98
+ - lib/action/core/logging.rb
99
+ - lib/action/core/swallow_exceptions.rb
100
+ - lib/action/core/top_level_around_hook.rb
97
101
  - lib/axn.rb
102
+ - lib/axn/factory.rb
98
103
  - lib/axn/version.rb
99
104
  - package.json
100
105
  - yarn.lock
@@ -114,14 +119,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
114
119
  requirements:
115
120
  - - ">="
116
121
  - !ruby/object:Gem::Version
117
- version: 3.1.0
122
+ version: 3.2.0
118
123
  required_rubygems_version: !ruby/object:Gem::Requirement
119
124
  requirements:
120
- - - ">"
125
+ - - ">="
121
126
  - !ruby/object:Gem::Version
122
- version: 1.3.1
127
+ version: '0'
123
128
  requirements: []
124
- rubygems_version: 3.4.10
129
+ rubygems_version: 3.5.22
125
130
  signing_key:
126
131
  specification_version: 4
127
132
  summary: A terse convention for business logic
data/axn.gemspec DELETED
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/axn/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "axn"
7
- spec.version = Axn::VERSION
8
- spec.authors = ["Kali Donovan"]
9
- spec.email = ["kali@teamshares.com"]
10
-
11
- spec.summary = "A terse convention for business logic"
12
- spec.description = "Pattern for writing callable service objects with contract validation and error swallowing"
13
- spec.homepage = "https://github.com/teamshares/axn"
14
- spec.license = "MIT"
15
-
16
- # NOTE: uses endless methods from 3, literal value omission from 3.1
17
- spec.required_ruby_version = ">= 3.1.0"
18
-
19
- # spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
20
- # spec.metadata["rubygems_mfa_required"] = "true"
21
-
22
- spec.metadata["homepage_uri"] = spec.homepage
23
- spec.metadata["source_code_uri"] = spec.homepage
24
- spec.metadata["changelog_uri"] = "https://github.com/teamshares/axn/blob/main/CHANGELOG.md"
25
-
26
- # Specify which files should be added to the gem when it is released.
27
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
28
- spec.files = Dir.chdir(__dir__) do
29
- `git ls-files -z`.split("\x0").reject do |f|
30
- (File.expand_path(f) == __FILE__) ||
31
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
32
- end
33
- end
34
- spec.bindir = "exe"
35
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
36
- spec.require_paths = ["lib"]
37
-
38
- # Core dependencies
39
- spec.add_dependency "activemodel", "> 7.0" # For contract validation
40
- spec.add_dependency "activesupport", "> 7.0" # For compact_blank and friends
41
-
42
- # NOTE: for inheritance support, need to specify a fork in consuming applications' Gemfile (see Gemfile here for syntax)
43
- spec.add_dependency "interactor", "3.1.2" # We're building on this scaffolding for organizing business logic
44
- spec.metadata["rubygems_mfa_required"] = "true"
45
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- #
4
- # CAUTION - ALPHA - TODO -- this code is extremely rough -- we have not yet gotten around to using it in production, so
5
- # consider this a work in progress / an indicator of things to come.
6
- #
7
-
8
- module Action
9
- # NOTE: replaces, rather than layers on, the upstream Interactor::Organizer module (only three methods, and
10
- # we want ability to implement a more complex interface where we pass options into the organized interactors)
11
- module Organizer
12
- def self.included(base)
13
- base.class_eval do
14
- include ::Action
15
-
16
- extend ClassMethods
17
- include InstanceMethods
18
- end
19
- end
20
-
21
- # NOTE: pulled unchanged from https://github.com/collectiveidea/interactor/blob/master/lib/interactor/organizer.rb
22
- module ClassMethods
23
- def organize(*interactors)
24
- @organized = interactors.flatten
25
- end
26
-
27
- def organized
28
- @organized ||= []
29
- end
30
- end
31
-
32
- module InstanceMethods
33
- # NOTE: override to use the `hoist_errors` method (internally, replaces call! with call + overrides to use @context directly)
34
- def call
35
- self.class.organized.each do |interactor|
36
- hoist_errors { interactor.call(@context) }
37
- end
38
- end
39
- end
40
- end
41
- end
File without changes
File without changes
File without changes
File without changes