aspectual 0.1.1 → 0.9.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 +5 -5
- data/.github/workflows/rubocop.yml +37 -0
- data/.github/workflows/ruby.yml +37 -0
- data/.rubocop.yml +39 -0
- data/Gemfile +7 -1
- data/README.md +66 -6
- data/aspectual.gemspec +17 -16
- data/lib/aspectual/method_construction.rb +121 -0
- data/lib/aspectual/version.rb +3 -1
- data/lib/aspectual.rb +100 -60
- data/spec/lib/aspectual_block_args_spec.rb +74 -0
- data/spec/lib/aspectual_complex_args_spec.rb +115 -0
- data/spec/lib/aspectual_inheritance_spec.rb +1247 -0
- data/spec/lib/aspectual_kwargs_spec.rb +80 -0
- data/spec/lib/aspectual_non_word_ending_character_spec.rb +128 -0
- data/spec/lib/aspectual_positional_args_spec.rb +74 -0
- data/spec/lib/aspectual_return_value_spec.rb +186 -0
- data/spec/lib/aspectual_scoping_spec.rb +132 -0
- data/spec/lib/aspectual_spec.rb +232 -169
- metadata +21 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 817c7afcb4818012c3ff63a90b9cd1d98b5c485fc9fe9970cb6b2e44a24328cd
|
|
4
|
+
data.tar.gz: 9bb74cc472397a9c2dbc7c9360bd5e63801ced7f54cc46b241e6911239f924ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 601bf77bb2417fd3c2592c1f8774050ed233c35840ba44fcfd763f86446cbb1e9c1dc3e0808debacd505410fc19d10b57b49c81df9a42f6ed1ffec1321dbd38d
|
|
7
|
+
data.tar.gz: 77d715ff59e1b2773d085e5cefee6e5496521414f4a19ae70371dad02f2ba60fc69d7d43bdc4a9009a184cfca8be9f3570578d3e90f84c16fac776eb59f33199
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
2
|
+
# They are provided by a third-party and are governed by
|
|
3
|
+
# separate terms of service, privacy policy, and support
|
|
4
|
+
# documentation.
|
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
|
7
|
+
|
|
8
|
+
name: Rubocop verification
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: [ "main" ]
|
|
13
|
+
pull_request:
|
|
14
|
+
branches: [ "main" ]
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
lint:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
ruby-version: ['3.1', '3.2', '3.3', '3.4', '4.0']
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- name: Set up Ruby
|
|
29
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
|
30
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
|
31
|
+
# uses: ruby/setup-ruby@v1
|
|
32
|
+
uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0
|
|
33
|
+
with:
|
|
34
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
35
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
36
|
+
- name: Run rubocop
|
|
37
|
+
run: bundle exec rubocop
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
|
2
|
+
# They are provided by a third-party and are governed by
|
|
3
|
+
# separate terms of service, privacy policy, and support
|
|
4
|
+
# documentation.
|
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
|
7
|
+
|
|
8
|
+
name: Ruby version compatibility
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
push:
|
|
12
|
+
branches: [ "main" ]
|
|
13
|
+
pull_request:
|
|
14
|
+
branches: [ "main" ]
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
ruby-version: ['3.1', '3.2', '3.3', '3.4', '4.0']
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- name: Set up Ruby
|
|
29
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
|
30
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
|
31
|
+
# uses: ruby/setup-ruby@v1
|
|
32
|
+
uses: ruby/setup-ruby@8e41b362d2589a22a44c1cfa214b3c83052c195b # v1.318.0
|
|
33
|
+
with:
|
|
34
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
35
|
+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
|
36
|
+
- name: Run tests
|
|
37
|
+
run: bundle exec rspec
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
AllCops:
|
|
4
|
+
NewCops: enable
|
|
5
|
+
Metrics/BlockLength:
|
|
6
|
+
Exclude:
|
|
7
|
+
# rspec blocks can get very long
|
|
8
|
+
- 'spec/**/*'
|
|
9
|
+
Metrics/ClassLength:
|
|
10
|
+
Exclude:
|
|
11
|
+
# our spec Classes can get very long
|
|
12
|
+
- 'spec/**/*'
|
|
13
|
+
Metrics/ModuleLength:
|
|
14
|
+
CountAsOne:
|
|
15
|
+
- array
|
|
16
|
+
- hash
|
|
17
|
+
- method_call
|
|
18
|
+
Metrics/MethodLength:
|
|
19
|
+
CountAsOne:
|
|
20
|
+
- array
|
|
21
|
+
- hash
|
|
22
|
+
- method_call
|
|
23
|
+
Naming/VariableNumber:
|
|
24
|
+
EnforcedStyle: snake_case
|
|
25
|
+
RSpec/ExampleLength:
|
|
26
|
+
CountAsOne:
|
|
27
|
+
- array
|
|
28
|
+
- hash
|
|
29
|
+
- method_call
|
|
30
|
+
RSpec/NestedGroups:
|
|
31
|
+
Max: 4
|
|
32
|
+
RSpec/RepeatedExample:
|
|
33
|
+
Enabled: false
|
|
34
|
+
Style/TrailingCommaInArrayLiteral:
|
|
35
|
+
EnforcedStyleForMultiline: consistent_comma
|
|
36
|
+
Style/TrailingCommaInArguments:
|
|
37
|
+
EnforcedStyleForMultiline: consistent_comma
|
|
38
|
+
Style/TrailingCommaInHashLiteral:
|
|
39
|
+
EnforcedStyleForMultiline: consistent_comma
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -12,11 +12,11 @@ Add this line to your application's Gemfile:
|
|
|
12
12
|
|
|
13
13
|
And then execute:
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
bundle install
|
|
16
16
|
|
|
17
17
|
Or install it yourself as:
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
gem install aspectual
|
|
20
20
|
|
|
21
21
|
## Usage
|
|
22
22
|
|
|
@@ -24,20 +24,80 @@ Extend the module
|
|
|
24
24
|
|
|
25
25
|
extend Aspectual
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
### Implicit method aspects
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Aspectual supports implicitly defining your aspects directly before a method.
|
|
30
|
+
This method of defining aspects will hook into the next defined method,
|
|
31
|
+
assigning those aspects to that method:
|
|
30
32
|
|
|
31
|
-
aspects before: :logging, after: :more_logging
|
|
33
|
+
aspects before: :logging, around: fancy_logging, after: :more_logging
|
|
32
34
|
def foo
|
|
33
35
|
"foo"
|
|
34
36
|
end
|
|
35
37
|
|
|
36
|
-
aspects before: [:notify_user, :notify_admin], after: [:remove_temp_files, :play_sound]
|
|
38
|
+
aspects before: [:notify_user, :notify_admin], around: [:setup_and_teardown, :play_background_music], after: [:remove_temp_files, :play_sound]
|
|
37
39
|
def bar
|
|
38
40
|
"bar"
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
### Explicit method aspects
|
|
44
|
+
|
|
45
|
+
Aspectual supports defining your aspects associated with a specific method,
|
|
46
|
+
which will be added either immediately (if the method is defined) or when that
|
|
47
|
+
method is first defined on the associated class.
|
|
48
|
+
|
|
49
|
+
def foo
|
|
50
|
+
"foo"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
aspects :foo, before: :logging, around: fancy_logging, after: :more_logging
|
|
54
|
+
|
|
55
|
+
aspects :bar, before: [:notify_user, :notify_admin], around: [:setup_and_teardown, :play_background_music], after: [:remove_temp_files, :play_sound]
|
|
56
|
+
|
|
57
|
+
def bar
|
|
58
|
+
"bar"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
### Inheriting aspects
|
|
62
|
+
|
|
63
|
+
Aspectual supports defining aspects in a parent class, when those methods will
|
|
64
|
+
be defined in a child class. Aspectual will attach the aspects to the next
|
|
65
|
+
definition of the named method, to support child classes inheriting aspected
|
|
66
|
+
behaviors.
|
|
67
|
+
|
|
68
|
+
class Foo
|
|
69
|
+
aspects :foo, before: :logging, around: fancy_logging, after: :more_logging
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class Bar < Foo
|
|
73
|
+
def foo
|
|
74
|
+
"foo"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
This behavior means that defining an aspect on an interface that is implemented
|
|
79
|
+
will require a call to `super` to invoke those aspects:
|
|
80
|
+
|
|
81
|
+
class Foo
|
|
82
|
+
aspects :foo, before: :logging, around: fancy_logging, after: :more_logging
|
|
83
|
+
def foo
|
|
84
|
+
"foo"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class Bar < Foo
|
|
89
|
+
def foo
|
|
90
|
+
super + "bar"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class Baz < Foo
|
|
95
|
+
# Does not call aspects defined in Foo
|
|
96
|
+
def foo
|
|
97
|
+
"baz"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
41
101
|
## Contributing
|
|
42
102
|
|
|
43
103
|
1. Fork it
|
data/aspectual.gemspec
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'English'
|
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
|
3
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
6
|
require 'aspectual/version'
|
|
5
7
|
|
|
6
8
|
Gem::Specification.new do |spec|
|
|
7
|
-
spec.name =
|
|
9
|
+
spec.name = 'aspectual'
|
|
8
10
|
spec.version = Aspectual::VERSION
|
|
9
|
-
spec.authors = [
|
|
10
|
-
spec.email = [
|
|
11
|
-
spec.description =
|
|
11
|
+
spec.authors = ['Fell Sunderland']
|
|
12
|
+
spec.email = ['agentantelope+aspectual@gmail.com']
|
|
13
|
+
spec.description = '
|
|
12
14
|
A simple gem to support minimal Aspect Oriented Programming in ruby.
|
|
13
|
-
|
|
14
|
-
spec.summary =
|
|
15
|
-
spec.homepage =
|
|
16
|
-
spec.license =
|
|
15
|
+
'
|
|
16
|
+
spec.summary = 'A gem to help with AOP.'
|
|
17
|
+
spec.homepage = 'https://github.com/AgentAntelope/aspectual'
|
|
18
|
+
spec.license = 'MIT'
|
|
17
19
|
|
|
18
|
-
spec.files = `git ls-files`.split(
|
|
20
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR).reject { |f| f.end_with?('gem') }
|
|
19
21
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
20
|
-
spec.
|
|
21
|
-
|
|
22
|
+
spec.require_paths = ['lib']
|
|
23
|
+
|
|
24
|
+
spec.required_ruby_version = '>= 3.1'
|
|
22
25
|
|
|
23
|
-
spec.
|
|
24
|
-
spec.add_development_dependency "rake"
|
|
25
|
-
spec.add_development_dependency "rspec"
|
|
26
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
26
27
|
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aspectual
|
|
4
|
+
# Used to build up aspect methods to replace the methods Aspectual wraps
|
|
5
|
+
module MethodConstruction
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def define_aspect_method(method_name:, position:, aspect:)
|
|
9
|
+
# This is to prevent us from looping because we're about to define some
|
|
10
|
+
# methods.
|
|
11
|
+
@_defining_method = true
|
|
12
|
+
|
|
13
|
+
with_method_name = with_aspect_method_name(
|
|
14
|
+
target: method_name, position:, feature: aspect,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
with_method_proc = build_with_method_proc(aspect:, position:, method_name:)
|
|
18
|
+
|
|
19
|
+
define_method(with_method_name, with_method_proc)
|
|
20
|
+
|
|
21
|
+
scope_method_to_parent(
|
|
22
|
+
scope_method_name: method_name,
|
|
23
|
+
new_method_name: with_method_name,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
alias_method_chain(target: method_name, feature: aspect, position:)
|
|
27
|
+
|
|
28
|
+
@_defining_method = false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def build_with_method_proc(aspect:, position:, method_name:)
|
|
32
|
+
without_method_name = without_aspect_method_name(
|
|
33
|
+
target: method_name, position:, feature: aspect,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
case position
|
|
37
|
+
when BEFORE_ASPECT
|
|
38
|
+
before_lambda(aspect:, without_method_name:)
|
|
39
|
+
when AROUND_ASPECT
|
|
40
|
+
around_lambda(aspect:, without_method_name:)
|
|
41
|
+
when AFTER_ASPECT
|
|
42
|
+
after_lambda(aspect:, without_method_name:)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def before_lambda(aspect:, without_method_name:)
|
|
47
|
+
lambda do |*args, **kwargs, &blk|
|
|
48
|
+
# first, call the aspect
|
|
49
|
+
send(aspect, *args, **kwargs, &blk)
|
|
50
|
+
|
|
51
|
+
# then, call the method without the aspect
|
|
52
|
+
send(without_method_name, *args, **kwargs, &blk)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def around_lambda(aspect:, without_method_name:)
|
|
57
|
+
lambda do |*args, **kwargs, &blk|
|
|
58
|
+
send(aspect, *args, **kwargs, &proc do
|
|
59
|
+
send(without_method_name, *args, **kwargs, &blk)
|
|
60
|
+
end)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def after_lambda(aspect:, without_method_name:)
|
|
65
|
+
lambda do |*args, **kwargs, &blk|
|
|
66
|
+
# first, call the method without the aspect
|
|
67
|
+
send(without_method_name, *args, **kwargs, &blk)
|
|
68
|
+
|
|
69
|
+
# then, call the aspect
|
|
70
|
+
send(aspect, *args, **kwargs, &blk)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def scope_method_to_parent(scope_method_name:, new_method_name:)
|
|
75
|
+
if public_method_defined?(scope_method_name)
|
|
76
|
+
public new_method_name
|
|
77
|
+
elsif protected_method_defined?(scope_method_name)
|
|
78
|
+
protected new_method_name
|
|
79
|
+
elsif private_method_defined?(scope_method_name)
|
|
80
|
+
private new_method_name
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# adapted from ActiveSupport
|
|
85
|
+
def alias_method_chain(target:, feature:, position:)
|
|
86
|
+
with_method_name = with_aspect_method_name(target:, position:, feature:)
|
|
87
|
+
without_method_name = without_aspect_method_name(target:, position:, feature:)
|
|
88
|
+
|
|
89
|
+
alias_method(without_method_name, target)
|
|
90
|
+
alias_method(target, with_method_name)
|
|
91
|
+
|
|
92
|
+
scope_method_to_parent(
|
|
93
|
+
scope_method_name: without_method_name,
|
|
94
|
+
new_method_name: target,
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def with_aspect_method_name(target:, position:, feature:)
|
|
99
|
+
target, punctuation = *safe_target(target:)
|
|
100
|
+
|
|
101
|
+
# Produce something like: :target_with_position_feature (with possible
|
|
102
|
+
# punctuation on the end)
|
|
103
|
+
:"#{target}_with_#{position}_#{feature}#{punctuation}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def without_aspect_method_name(target:, position:, feature:)
|
|
107
|
+
target, punctuation = *safe_target(target:)
|
|
108
|
+
|
|
109
|
+
# Produce something like: :target_without_position_feature (with possible
|
|
110
|
+
# punctuation on the end)
|
|
111
|
+
:"#{target}_without_#{position}_#{feature}#{punctuation}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def safe_target(target:)
|
|
115
|
+
# Extract punctuation on methods ending in one of !, ?, or = since e.g.
|
|
116
|
+
# target?_without_position_feature should properly be
|
|
117
|
+
# target_without_position_feature?.
|
|
118
|
+
target.to_s.scan(/(.*)([?!=])?$/).flatten
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
data/lib/aspectual/version.rb
CHANGED
data/lib/aspectual.rb
CHANGED
|
@@ -1,85 +1,125 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
4
|
+
# Aspectual allows Aspect Oriented Programming in Ruby.
|
|
3
5
|
module Aspectual
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
6
|
+
require_relative 'aspectual/method_construction'
|
|
7
|
+
|
|
8
|
+
include MethodConstruction
|
|
9
|
+
|
|
10
|
+
# This order is important to ensure we're assigning aspects in the correct
|
|
11
|
+
# order: Before comes before around, so needs to be added to the method after
|
|
12
|
+
VALID_ASPECTS = [
|
|
13
|
+
AROUND_ASPECT = :around,
|
|
14
|
+
BEFORE_ASPECT = :before,
|
|
15
|
+
AFTER_ASPECT = :after,
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
BLANK_ASPECT = VALID_ASPECTS.to_h { [_1, [].freeze] }.freeze
|
|
19
|
+
|
|
20
|
+
NEXT_METHOD_ADDED_KEY = :_next_method_added
|
|
21
|
+
|
|
22
|
+
# Special default value for next method added
|
|
23
|
+
DEFAULT_ASPECTS = { NEXT_METHOD_ADDED_KEY => BLANK_ASPECT }.freeze
|
|
24
|
+
|
|
25
|
+
def aspects(*method_names, before: [], around: [], after: [])
|
|
26
|
+
method_names = [NEXT_METHOD_ADDED_KEY] if method_names.empty?
|
|
27
|
+
method_names.each do |method_name|
|
|
28
|
+
merge_aspects(
|
|
29
|
+
method_name:,
|
|
30
|
+
new_aspects: {
|
|
31
|
+
BEFORE_ASPECT => Array(before),
|
|
32
|
+
AROUND_ASPECT => Array(around),
|
|
33
|
+
AFTER_ASPECT => Array(after),
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# If we already defined the method we're adding aspects for, we need to
|
|
38
|
+
# process that straight away.
|
|
39
|
+
next unless method_defined?(method_name) || private_method_defined?(method_name)
|
|
40
|
+
|
|
41
|
+
add_aspects_to_method(method_name:)
|
|
26
42
|
end
|
|
27
43
|
end
|
|
28
44
|
|
|
29
|
-
|
|
45
|
+
def method_added(method_name)
|
|
46
|
+
add_aspects_to_method(method_name:)
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
@_before_aspects || @_after_aspects
|
|
48
|
+
super
|
|
33
49
|
end
|
|
34
50
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
@_defining_method = true
|
|
38
|
-
|
|
39
|
-
with_aspect_method_name = method_symbol.to_s + "_with_#{position}_" + aspect.to_s
|
|
40
|
-
without_aspect_method_name = method_symbol.to_s + "_without_#{position}_" + aspect.to_s
|
|
51
|
+
def defined_aspects
|
|
52
|
+
return @defined_aspects if defined?(@defined_aspects)
|
|
41
53
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
aspect_proc = lambda {|*args| send(aspect, *args); send(without_aspect_method_name, *args)}
|
|
45
|
-
when "around"
|
|
46
|
-
aspect_proc = lambda {|*args| send(aspect, *args, &Proc.new{send(without_aspect_method_name, *args)})}
|
|
47
|
-
when "after"
|
|
48
|
-
aspect_proc = lambda {|*args| send(without_aspect_method_name, *args); send(aspect, *args)}
|
|
54
|
+
aspected_ancestor = ancestors[1..].detect do |klass|
|
|
55
|
+
klass.is_a?(Aspectual)
|
|
49
56
|
end
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
@defined_aspects = Marshal.load(
|
|
59
|
+
Marshal.dump(
|
|
60
|
+
aspected_ancestor&.defined_aspects || DEFAULT_ASPECTS,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
end
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
private
|
|
54
66
|
|
|
55
|
-
|
|
67
|
+
attr_writer :defined_aspects
|
|
56
68
|
|
|
57
|
-
|
|
69
|
+
def can_define_aspect?(aspects:, method_name:)
|
|
70
|
+
# If there are no defined aspects we have nothing to do
|
|
71
|
+
aspects.values.any?(&:any?) &&
|
|
72
|
+
# the method we're adding aspects to needs to have been defined
|
|
73
|
+
(method_defined?(method_name) || private_method_defined?(method_name))
|
|
58
74
|
end
|
|
59
75
|
|
|
60
|
-
def
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
def add_aspects_to_method(method_name:)
|
|
77
|
+
return if @_defining_method
|
|
78
|
+
|
|
79
|
+
aspects = calculate_aspects(method_name:)
|
|
80
|
+
|
|
81
|
+
return unless can_define_aspect?(aspects:, method_name:)
|
|
82
|
+
|
|
83
|
+
# Remove the aspects for this method, in case we see multiple definitions
|
|
84
|
+
# of a single method (i.e. inheritance)
|
|
85
|
+
defined_aspects.delete(method_name)
|
|
86
|
+
|
|
87
|
+
aspects.each do |position, positional_aspects|
|
|
88
|
+
positional_aspects.each do |aspect|
|
|
89
|
+
define_aspect_method(method_name:, position:, aspect:)
|
|
90
|
+
end
|
|
68
91
|
end
|
|
69
92
|
end
|
|
70
93
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
94
|
+
def calculate_aspects(method_name:)
|
|
95
|
+
merge_aspects(
|
|
96
|
+
method_name:,
|
|
97
|
+
new_aspects: defined_aspects.fetch(NEXT_METHOD_ADDED_KEY, BLANK_ASPECT),
|
|
98
|
+
)
|
|
76
99
|
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
# Blank out the aspects for the next method added
|
|
101
|
+
defined_aspects[NEXT_METHOD_ADDED_KEY] = BLANK_ASPECT
|
|
79
102
|
|
|
80
|
-
|
|
81
|
-
|
|
103
|
+
defined_aspects[method_name] || BLANK_ASPECT
|
|
104
|
+
end
|
|
82
105
|
|
|
83
|
-
|
|
106
|
+
def merge_aspects(method_name:, new_aspects:)
|
|
107
|
+
return defined_aspects if new_aspects == BLANK_ASPECT
|
|
108
|
+
|
|
109
|
+
self.defined_aspects = defined_aspects.merge(
|
|
110
|
+
{ method_name => new_aspects },
|
|
111
|
+
) do |_aspected_method_name, old_config, new_config|
|
|
112
|
+
old_config.merge(new_config) do |aspect, old_aspects, new_aspects|
|
|
113
|
+
case aspect
|
|
114
|
+
when BEFORE_ASPECT, AROUND_ASPECT
|
|
115
|
+
# The before and around aspects have to be reversed so that when
|
|
116
|
+
# multiple aspects are added to one method, the first declared method
|
|
117
|
+
# will be added last.
|
|
118
|
+
(new_aspects.reverse + old_aspects).uniq
|
|
119
|
+
else
|
|
120
|
+
(old_aspects + new_aspects).uniq
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
84
124
|
end
|
|
85
125
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../lib/aspectual'
|
|
4
|
+
|
|
5
|
+
class BlockArgsTestClass
|
|
6
|
+
extend Aspectual
|
|
7
|
+
|
|
8
|
+
attr_reader :methods_called
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
# This is to ensure that all methods are properly called within the
|
|
12
|
+
# context of the current instance of this object.
|
|
13
|
+
@methods_called = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def block_aspect_method(&block)
|
|
17
|
+
methods_called << "block_aspect_method_block_result_#{block.call}"
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def block_around_aspect_method(&block)
|
|
22
|
+
methods_called << 'before_block_block_around_aspect_method_block'
|
|
23
|
+
block.call
|
|
24
|
+
methods_called << 'after_block_block_around_aspect_method_block'
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
aspects before: :block_aspect_method
|
|
29
|
+
def before_block_test_method(&block)
|
|
30
|
+
methods_called << "before_block_test_method_block_result_#{block.call}"
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
aspects around: :block_around_aspect_method
|
|
35
|
+
def around_block_test_method(&block)
|
|
36
|
+
methods_called << "around_block_test_method_block_result_#{block.call}"
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
aspects after: :block_aspect_method
|
|
41
|
+
def after_block_test_method(&block)
|
|
42
|
+
methods_called << "after_block_test_method_block_result_#{block.call}"
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe Aspectual do
|
|
48
|
+
describe 'methods with block arguments' do
|
|
49
|
+
it 'can handle before aspects' do
|
|
50
|
+
test_instance = BlockArgsTestClass.new.before_block_test_method { 'called' }
|
|
51
|
+
expect(test_instance.methods_called).to eq(%w[
|
|
52
|
+
block_aspect_method_block_result_called
|
|
53
|
+
before_block_test_method_block_result_called
|
|
54
|
+
])
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'can handle around aspects' do
|
|
58
|
+
test_instance = BlockArgsTestClass.new.around_block_test_method { 'called' }
|
|
59
|
+
expect(test_instance.methods_called).to eq(%w[
|
|
60
|
+
before_block_block_around_aspect_method_block
|
|
61
|
+
around_block_test_method_block_result_called
|
|
62
|
+
after_block_block_around_aspect_method_block
|
|
63
|
+
])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'can handle after aspects' do
|
|
67
|
+
test_instance = BlockArgsTestClass.new.after_block_test_method { 'called' }
|
|
68
|
+
expect(test_instance.methods_called).to eq(%w[
|
|
69
|
+
after_block_test_method_block_result_called
|
|
70
|
+
block_aspect_method_block_result_called
|
|
71
|
+
])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|