├── .gitignore ├── .irbrc ├── .rspec ├── .rubocop.yml ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── aspector.gemspec ├── benchmarks ├── after_benchmark.rb ├── applying_benchmark.rb ├── around_advice_benchmark.rb ├── around_benchmark.rb ├── before_benchmark.rb ├── benchmark_helper.rb ├── combined_benchmark.rb ├── method_invocation_benchmark.rb └── raw_benchmark.rb ├── examples ├── activerecord_hooks.rb ├── around_example.rb ├── aspector_apply_example.rb ├── aspector_example.rb ├── cache_aspect.rb ├── design_by_contract.rb ├── exception_handler.rb ├── exception_handler2.rb ├── implicit_method_option_test.rb ├── instance_aspect.rb ├── interception_options_example.rb ├── logging_aspect.rb ├── multiple_aspects_on_same.rb ├── process_aspector.rb └── retry_aspect.rb ├── lib ├── aspector.rb └── aspector │ ├── advice.rb │ ├── advice │ ├── builder.rb │ ├── metadata.rb │ ├── method_matcher.rb │ └── params.rb │ ├── base.rb │ ├── base │ ├── class_methods.rb │ ├── dsl.rb │ ├── status.rb │ └── storage.rb │ ├── deferred │ ├── logic.rb │ └── option.rb │ ├── errors.rb │ ├── extensions │ ├── module.rb │ └── object.rb │ ├── interception.rb │ ├── interceptions_storage.rb │ ├── logger.rb │ ├── logging.rb │ ├── method_template.rb │ ├── refinements │ ├── class.rb │ └── string.rb │ └── version.rb └── spec ├── benchmarks_spec.rb ├── examples_spec.rb ├── functionals ├── aspect_applied_multiple_times_on_same_spec.rb ├── aspect_for_multiple_targets_spec.rb ├── aspect_interception_options_accessing_spec.rb ├── aspect_on_a_class_spec.rb ├── aspect_on_an_instance_spec.rb ├── aspector_spec.rb ├── aspects_combined_spec.rb ├── aspects_execution_order_spec.rb └── aspects_on_private_methods_spec.rb ├── spec_helper.rb ├── support └── class_builder.rb └── units ├── advice ├── builder_spec.rb ├── metadata_spec.rb └── method_matcher_spec.rb ├── advice_spec.rb ├── advices ├── after_spec.rb ├── around_spec.rb ├── before_filter_spec.rb ├── before_spec.rb └── raw_spec.rb ├── base ├── class_methods_spec.rb ├── dsl_spec.rb ├── status_spec.rb └── storage_spec.rb ├── base_spec.rb ├── deferred ├── logic_spec.rb └── option_spec.rb ├── extensions ├── module_spec.rb └── object_spec.rb ├── logger_spec.rb ├── logging_spec.rb ├── object_extension_spec.rb ├── refinements ├── class_spec.rb └── string_spec.rb └── special_chars_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | performance-tests/output 3 | coverage 4 | rdoc 5 | doc 6 | .yardoc 7 | .bundle 8 | pkg 9 | .svn 10 | .DS_Store 11 | *.tmproj 12 | *.swp 13 | .redcar 14 | .rbx 15 | -------------------------------------------------------------------------------- /.irbrc: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib')) 2 | 3 | require 'aspector' 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AlignParameters: 2 | Enabled: false 3 | ClassLength: 4 | CountComments: false 5 | Max: 200 6 | LineLength: 7 | Max: 99 8 | MethodLength: 9 | CountComments: false 10 | Max: 15 11 | Metrics/AbcSize: 12 | Max: 25 13 | AllCops: 14 | Exclude: 15 | - bin/**/* 16 | - /.gemspec/ 17 | - !ruby/regexp /old_and_unused\.rb$/ 18 | - lib/aspector/interception.rb 19 | - lib/aspector/extensions/object.rb 20 | Include: 21 | - Rakefile 22 | - aspector.gemspec 23 | Style/MultilineOperationIndentation: 24 | EnforcedStyle: indented 25 | SupportedStyles: 26 | - aligned 27 | - indented 28 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | aspector 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.5 4 | - 2.1.7 5 | - 2.2.2 6 | - 2.2.3 7 | - 2.3.0 8 | script: 9 | - bundle exec rubocop lib 10 | - bundle exec rubocop spec 11 | - bundle exec rubocop examples 12 | - bundle exec rubocop benchmarks 13 | - bundle exec rspec spec 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Aspector 2 | 3 | ## Changelog 4 | 5 | ### 0.14.1 6 | 7 | * Code structure refactoring 8 | * Ruby 2.0 branch drop (no refinements) 9 | * Full YARD documentation 10 | * Code cleanup 11 | * Changelog.md to CHANGELOG.md 12 | * Deferred namespace for deferred elements (options and logic) 13 | * Method matcher refactoring 14 | * MethodMatcher is now used only internally in the Aspector::Advice space, so it was renamed to Aspector::Advice::MethodMatcher and it is now a part of the Aspector::Advice namespace 15 | * Multiple Boolean like methods now fully Boolean (no more nils returned) 16 | * Few naming convention tweaks 17 | * Logging class refactoring 18 | * Advice name now refers to its object_id not 19 | * Extracted storing deferred logic and other options inside Aspector::Base::Storage to remove one of the base class responsibilities 20 | * Changed enable to enable! and disable to disable! for enabling/disabling aspects 21 | * Changed disabled? to positive enabled? By default aspect is enabled 22 | * Extracted aspect activity status to Aspector::Base::Status to manage if a given aspect should be applied or not 23 | * Forwardable for Aspect::Interception to cleanup the internal API 24 | * Refinements for String (casting string into an instance variable name) 25 | * Refinements for Class (detecting instance method type: private/protected/public) 26 | * More specs 27 | * Separated whole DSL logic into Aspector::Base::Dsl module 28 | * Moved metadata to an Advice namespace (Aspector::Advice::Metadata) 29 | * Moved Aspector::Advice _create_aspect_ into an Aspector::Advice::Builder to separate parameter formatting and building logic 30 | * Separated general Ruby injected code into ::Module and ::Object extensions 31 | * Moved base class methods into Aspector::Base::ClassMethods 32 | * Extracted internal errors into Aspector::Errors module 33 | * Removed Aspector::Advice#to_s - never used (not even for DEBUG logging) 34 | * Moved Advice::Metadata fetching from dsl into Metadata class level (Metadata#before etc) 35 | * Advice no longer accepts MethodMatcher in the initializer - instead it accepts array with methods that we want to match and builds matcher internally - that way we have a single matcher building (it happens in one place) 36 | * Renamed the aop_ method prefix to aspector_advice_ - it seems more natural and easier for debug 37 | * When creating advice (Advice.new) it now validates that either match_methods or block is provided 38 | * When creating advice (Advice.new) it now validates that we want to build advice of a supported type 39 | * Added instance usage example to examples/ 40 | * Added multiple aspects for the same element examples 41 | * Aspector::Advice::Params for working with dynamic params on aspects creating 42 | * Fixed misguiding naming on AspectInstances and renamed to InterceptionStorage 43 | * Fixed aspect_instances to interceptions_storage 44 | * Added benchmarks to spec suit to ensure that they pass after each change 45 | * Moved method template to a separate file to clean the interception 46 | * Added Rubocop to the whole project except interception.rb and object.rb 47 | * Rubocop remarks 48 | * Added memory and performance benchmarks based on the base benchmarks 49 | * Refactoring of module.rb to extract common parts to a single method 50 | * Renamed aop_ prefix to aspector_ so when we do private_methods on a module it will be more precise what to those methods are related 51 | * Cleaned method accessibility - some public methods were used only in a private scope so they became private 52 | * Improved interception storage usage - now only interceptions that are not set to existing_methods_only are stored (less memory used) 53 | 54 | ### 0.14.0 55 | 56 | * Created instance of an aspect can be applied to multiple targets(classes/instances) 57 | * aspect_arg replaced with interception_arg because of multiple possible targets with different options 58 | * aspect_arg no longer works - accessing aspect can be done via interception (interception.aspect) 59 | * Interception options inherit all the default aspect options + options per interception 60 | * Specs for interception_arg 61 | * Specs for accessing interception options 62 | * Specs that validate if interception options inherit aspect default options 63 | * Logger proc printing instead of evaluating fix 64 | * jruby support drop - there's no 2.0 syntax for jruby yet. Once it's out of beta - no further work needs to be done (works for me on beta) 65 | * drop 1.8 and 1.9 - even ruby guys recommend upgrading 66 | * added rubocop for spec/, examples/ and benchmarks/ 67 | * fixed examples issues (they were not working anymore) 68 | * fixed benchmarks (outdated libs, etc) 69 | * added examples execution to rspec - now rspec will notify if any example is not working. That way we will be able to provide code changes without having to worry that they will break examples 70 | * added way more specs (93% code coverage) 71 | * added rubocop to travis 72 | * some minor cleanups 73 | * moved from rvmrc (deprecated) to ruby-gemset and ruby-version 74 | * removed unused gems 75 | * added simplecov to track code coverage 76 | * updated syntax to match 2.2.2 77 | * moved versioning to Aspector::VERSION 78 | * gemspec cleanup 79 | * benchmarks now use RubyProf new version 80 | * benchmarks moved to /benchmarks 81 | * all the examples work again 82 | * lib/aspector load order rearrange 83 | * aspector advice rearrange - now it doesn't have to use integer values with constants 84 | * before_filter as a separate TYPE - that way we don't have to "if" anything - works out of the box easier 85 | * some method optimization or rewrites for many methods - works the same, looks way better 86 | * advices metadata cleanup - no need for mandatory_options anymore 87 | * API CHANGE: old_methods_only replaced with existing_methods_only - when I started to use aspector the "old" was hard to understand - existing is way more straightforward 88 | * base disabled? now returns either true or false (no nil since it might change) 89 | * boolean methods that return nil return now true/false 90 | * before advices are now picked from before and before_filter advices 91 | * METHOD_TEMPLATE update with new logger logic 92 | * METHOD_TEMPLATE indentations are now more readable 93 | * base_class_methods now generate all the bind methods automatically based on Aspector::Advice::TYPES 94 | * deferred options to_s cleanup 95 | * New logger that is based on Ruby default logger (::Logger) - same log levels, same API (except *args for our contextes) 96 | * Logger is now much smaller 97 | * Logging - constanize is now easier to read but the API stays the same (fallback as well) 98 | * Method matcher#to_s uses now the & syntax for inspect 99 | * ObjectExtension #returns removed - it was never used (throw was never used) 100 | * Spec helper cleanup 101 | * Rspec no longer needs monkey patching (config.disable_monkey_patching!) 102 | * Class building logic is now wrapped in a module instead of a single method - all the class "building" and "cloning" happens in one place 103 | * Added some pendings for future improvement 104 | * skip_if_false for a before that should act as before_filter removed (since before filter is now a separate aspect) 105 | * version dump 106 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | group :development do 4 | gem 'rspec' 5 | gem 'rubocop' 6 | gem 'pry' 7 | gem 'simplecov' 8 | gem 'ruby-prof' 9 | gem 'yard' 10 | end 11 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | ast (2.1.0) 5 | astrolabe (1.3.1) 6 | parser (~> 2.2) 7 | coderay (1.1.0) 8 | diff-lcs (1.2.5) 9 | docile (1.1.5) 10 | json (1.8.3) 11 | method_source (0.8.2) 12 | parser (2.2.3.0) 13 | ast (>= 1.1, < 3.0) 14 | powerpack (0.1.1) 15 | pry (0.10.3) 16 | coderay (~> 1.1.0) 17 | method_source (~> 0.8.1) 18 | slop (~> 3.4) 19 | rainbow (2.0.0) 20 | rspec (3.4.0) 21 | rspec-core (~> 3.4.0) 22 | rspec-expectations (~> 3.4.0) 23 | rspec-mocks (~> 3.4.0) 24 | rspec-core (3.4.1) 25 | rspec-support (~> 3.4.0) 26 | rspec-expectations (3.4.0) 27 | diff-lcs (>= 1.2.0, < 2.0) 28 | rspec-support (~> 3.4.0) 29 | rspec-mocks (3.4.0) 30 | diff-lcs (>= 1.2.0, < 2.0) 31 | rspec-support (~> 3.4.0) 32 | rspec-support (3.4.1) 33 | rubocop (0.35.1) 34 | astrolabe (~> 1.3) 35 | parser (>= 2.2.3.0, < 3.0) 36 | powerpack (~> 0.1) 37 | rainbow (>= 1.99.1, < 3.0) 38 | ruby-progressbar (~> 1.7) 39 | tins (<= 1.6.0) 40 | ruby-prof (0.15.9) 41 | ruby-progressbar (1.7.5) 42 | simplecov (0.11.1) 43 | docile (~> 1.1.0) 44 | json (~> 1.8) 45 | simplecov-html (~> 0.10.0) 46 | simplecov-html (0.10.0) 47 | slop (3.6.0) 48 | tins (1.6.0) 49 | yard (0.8.7.6) 50 | 51 | PLATFORMS 52 | ruby 53 | 54 | DEPENDENCIES 55 | pry 56 | rspec 57 | rubocop 58 | ruby-prof 59 | simplecov 60 | yard 61 | 62 | BUNDLED WITH 63 | 1.10.6 64 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # More info at https://github.com/guard/guard#readme 2 | 3 | guard 'bundler' do 4 | watch('Gemfile') 5 | end 6 | 7 | #guard 'shell' do 8 | # watch(%r{^(lib|spec)/.+\.rb$}) { `rspec spec` } 9 | #end 10 | 11 | guard 'rspec', :notification => false do 12 | watch(%r{^(lib|spec)/.+\.rb$}) { "spec" } 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Guoliang Cao, Maciej Mensfeld 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aspector 2 | 3 | Aspector = ASPECT Oriented Ruby programming 4 | 5 | ## Deprecated 6 | 7 | Aspector is deprecated. 8 | 9 | We recommend Ruby native [Module#prepend](https://ruby-doc.org/core-2.5.0/Module.html) functionality to build up aspects. 10 | 11 | ## About 12 | 13 | Aspector allows to use [aspect oriented programming](https://en.wikipedia.org/wiki/Aspect-oriented_programming) with Ruby. 14 | 15 | Aspector allows adding additional behavior to existing code (an advice) without modifying the code itself, instead separately specifying which code is modified via a "pointcut" specification. 16 | 17 | ## Highlights 18 | 19 | * Encapsulate logic as aspects and apply to multiple targets easily 20 | * Support before/before_filter/after/around advices 21 | * Work anywhere - inside/outside the target class, before/after methods are created 22 | * Use regexp matching to apply advices to multiple methods 23 | * Small codebase, intuitive API 24 | * Conditional aspects disabling/enabling 25 | * Standarized logging API 26 | * Aspects are applicable to both classes/modules and instances 27 | * Object extensions for easier usage 28 | 29 | ## Example usages 30 | 31 | Aspector should be used whenever you have a cross-cutting concerns, especially when they don't perform any business logic. For example use can use it to provide things like: 32 | 33 | * Logging 34 | * Monitoring 35 | * Performance benchmarking 36 | * Any type of transactions wrapping 37 | * Events producing for systems like Apache Kafka 38 | * Etc... 39 | 40 | ## Installation 41 | 42 | ```bash 43 | gem install aspector 44 | ``` 45 | 46 | or put it inside of your Gemfile: 47 | 48 | ```bash 49 | gem 'aspector' 50 | ``` 51 | 52 | ## Examples 53 | 54 | To see how to use Aspector, please review examples that are in [examples directory](examples/). 55 | 56 | If you need more detailed examples, please review files in [spec/functionals](spec/functionals) and [spec/units/advices](spec/units/advices). 57 | 58 | Here's a simple example how Aspector can be used: 59 | 60 | ```ruby 61 | class ExampleClass 62 | def test 63 | puts 'test' 64 | end 65 | end 66 | 67 | aspect = Aspector do 68 | target do 69 | def do_this 70 | puts 'do_this' 71 | end 72 | end 73 | 74 | before :test, :do_this 75 | 76 | before :test do 77 | puts 'do_that' 78 | end 79 | end 80 | 81 | aspect.apply(ExampleClass) 82 | element = ExampleClass.new 83 | element.test 84 | aspect.disable! 85 | element.test 86 | 87 | # Expected output 88 | # do_this 89 | # do_that 90 | # test 91 | # test 92 | ``` 93 | 94 | ## Configuration options 95 | 96 | Aspector is really easy to use. After installation it doesn't require any additional configuration. You can however set two environments variables that are related to logging: 97 | 98 | | ENV variable name | Description | 99 | |--------------------|--------------------------------------------------------------------------------------| 100 | | ASPECTOR_LOGGER | Any logger class you want to use (if you don't want to use Aspector standard logger) | 101 | | ASPECTOR_LOG_LEVEL | DEBUG < INFO < WARN < ERROR < FATAL < UNKNOWN | 102 | 103 | Aspector::Logger inherits from a [standard Ruby logger](http://ruby-doc.org/stdlib-2.2.0/libdoc/logger/rdoc/Logger.html). Log levels and the API are pretty standard. You can however use yor own: 104 | 105 | ```ruby 106 | ASPECTOR_LOGGER='MyApp::Logger' ASPECTOR_LOG_LEVEL='any log level' ruby aspected_stuff.rb 107 | ``` 108 | 109 | ## Default and apply options 110 | 111 | Here are options that you can use when creating or applying a single aspect: 112 | 113 | | Option name | Type | Description | 114 | |-----------------------|------------------------------------------|-----------------------------------------------------------------------------------| 115 | | except | Symbol, String, Regexp or Array of those | Will apply aspect to all the methods except those listed | 116 | | name | String | Advice name (really useful only for debugging) | 117 | | methods | Array of Symbol, String, Regexp | Method names (or regexp for matching) to which we should apply given aspect | 118 | | method | Symbol, String, Regexp or Array of those | Acts as methods but accepts a single name of method (or a single regexp) | 119 | | existing_methods_only | Boolean (true/false) - default: false | Will apply aspect only to already defined methods | 120 | | new_methods_only | Boolean (true/false) - default: false | Will apply aspect only to methods that were defined after aspect was applied | 121 | | private_methods | Boolean (true/false) - default: false | Should the aspect be applied to private methods as well (public only by default) | 122 | | class_methods | Boolean (true/false) - default: false | Should the aspect for instance methods of class methods of a given element | 123 | | method_arg | Boolean (true/false) - default: false | Do we want to have access to base method arguments in the aspect method/block | 124 | | interception_arg | Boolean (true/false) - default: false | Do we want to have access to the interception instance in the aspect method/block | 125 | 126 | ## Contributing to aspector 127 | 128 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 129 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 130 | * Fork the project 131 | * Start a feature/bugfix branch 132 | * Commit and push until you are happy with your contribution 133 | * Make sure to add specs for it. This is important so I don't break it in a future version unintentionally. 134 | * If it is a new functionality or feature please provide examples 135 | * Please benchmark any functionality that might have a performance impact 136 | * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 137 | 138 | ## Copyright 139 | 140 | Copyright (c) 2015 Guoliang Cao, Maciej Mensfeld. See LICENSE.txt for further details. 141 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | require 'rake' 4 | require 'rspec/core' 5 | require 'rspec/core/rake_task' 6 | 7 | begin 8 | Bundler.setup(:default, :development) 9 | rescue Bundler::BundlerError => e 10 | $stderr.puts e.message 11 | $stderr.puts 'Run `bundle install` to install missing gems' 12 | exit e.status_code 13 | end 14 | 15 | RSpec::Core::RakeTask.new(:spec) do |spec| 16 | spec.pattern = FileList['spec/**/*_spec.rb'] 17 | end 18 | 19 | task default: :spec 20 | -------------------------------------------------------------------------------- /aspector.gemspec: -------------------------------------------------------------------------------- 1 | require 'aspector/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'aspector' 5 | s.version = Aspector::VERSION 6 | 7 | s.authors = ['Guoliang Cao', 'Maciej Mensfeld'] 8 | s.date = %w( 2015-07-07 ) 9 | s.email = ['gcao99@gmail.com', 'maciej@mensfeld.pl'] 10 | s.summary = %w( Aspect Oriented Ruby Programming library ) 11 | s.homepage = 'http://github.com/gcao/aspector' 12 | s.licenses = %w( MIT ) 13 | s.description = %w() 14 | s.rubygems_version = %w( 1.6.2 ) 15 | 16 | s.files = `git ls-files -z`.split("\x0") 17 | s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 19 | s.require_paths = %w( lib ) 20 | end 21 | -------------------------------------------------------------------------------- /benchmarks/after_benchmark.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/benchmark_helper') 2 | 3 | # Example class that we use with aspector 4 | class Klass 5 | aspector do 6 | after :test, :after_test 7 | end 8 | 9 | def test_no_aspect; end 10 | 11 | def test; end 12 | 13 | def after_test(_result); end 14 | end 15 | 16 | instance = Klass.new 17 | 18 | benchmark 'instance.test_no_aspect' do 19 | instance.test_no_aspect 20 | end 21 | 22 | benchmark 'instance.test' do 23 | instance.test 24 | end 25 | -------------------------------------------------------------------------------- /benchmarks/applying_benchmark.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/benchmark_helper') 2 | 3 | ITERATIONS = 10 4 | 5 | # Class to which we will bind with aspect 6 | class Klass 7 | def test(input) 8 | input.upcase! 9 | end 10 | end 11 | 12 | # Around aspect for benchmarking 13 | class AroundAspect < Aspector::Base 14 | around :test do |proxy, *args, &block| 15 | begin 16 | proxy.call(*args, &block) 17 | rescue 18 | nil 19 | end 20 | end 21 | end 22 | 23 | benchmark 'Around advice good' do 24 | AroundAspect.apply(Klass) 25 | end 26 | -------------------------------------------------------------------------------- /benchmarks/around_advice_benchmark.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/benchmark_helper') 2 | 3 | # Class to which we will bind with aspect 4 | class Klass 5 | def test(input) 6 | input.upcase! 7 | end 8 | end 9 | 10 | # Around aspect for benchmarking 11 | class AroundAspect < Aspector::Base 12 | around :test do |proxy, *args, &block| 13 | begin 14 | proxy.call(*args, &block) 15 | rescue 16 | nil 17 | end 18 | end 19 | end 20 | 21 | AroundAspect.apply(Klass) 22 | 23 | instance = Klass.new 24 | 25 | benchmark 'Around advice good' do 26 | instance.test('good') 27 | end 28 | 29 | benchmark 'Around advice bad' do 30 | instance.test(nil) 31 | end 32 | -------------------------------------------------------------------------------- /benchmarks/around_benchmark.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/benchmark_helper') 2 | 3 | # Class to which we will bind with aspect 4 | class Klass 5 | aspector do 6 | around :test, :around_test 7 | end 8 | 9 | def test_no_aspect; end 10 | 11 | def test; end 12 | 13 | def around_test(proxy, *_args, &block) 14 | proxy.call(&block) 15 | end 16 | end 17 | 18 | instance = Klass.new 19 | 20 | benchmark 'Around good' do 21 | instance.test('good') 22 | end 23 | 24 | benchmark 'Around bad' do 25 | instance.test(nil) 26 | end 27 | -------------------------------------------------------------------------------- /benchmarks/before_benchmark.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/benchmark_helper') 2 | 3 | # Class to which we will bind with aspect 4 | class Klass 5 | aspector do 6 | before :test, :before_test 7 | end 8 | 9 | def test_no_aspect; end 10 | 11 | def test; end 12 | 13 | def before_test; end 14 | end 15 | 16 | instance = Klass.new 17 | 18 | benchmark 'instance.test_no_aspect' do 19 | instance.test_no_aspect 20 | end 21 | 22 | benchmark 'instance.test' do 23 | instance.test 24 | end 25 | -------------------------------------------------------------------------------- /benchmarks/benchmark_helper.rb: -------------------------------------------------------------------------------- 1 | # Helper methods for benchmarking aspector using rubyprof 2 | # To run a given benchmark file just use following command: 3 | # bundle exec benchmarks/given_file.rb 4 | # Or if you want to benchmark something else than process time: 5 | # TYPE=memory bundle exec benchmarks/given_file.rb 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 7 | 8 | require 'rubygems' 9 | require 'ruby-prof' 10 | require 'aspector' 11 | 12 | ITERATIONS = 20_000 13 | # We disable GC so it won't mess with our benchmarking 14 | # Don't do this in production 15 | GC.disable 16 | 17 | # Types of benchmarks that we want to use 18 | TYPES = { 19 | cpu: RubyProf::CPU_TIME, 20 | memory: RubyProf::MEMORY, 21 | time: RubyProf::PROCESS_TIME 22 | } 23 | 24 | # Current benchmarking type 25 | TYPE = TYPES[(ENV['TYPE'] || 'cpu').to_sym] || TYPES[:time] 26 | 27 | # Will print results in the way we want 28 | def print_result(result, description) 29 | printer = RubyProf::FlatPrinter.new(result) 30 | print "#{'-' * 50} #{description}\n" 31 | printer.print($stdout) 32 | end 33 | 34 | # Benchmarks and prints results 35 | # @param [String] benchmark description 36 | # @yield Block of code that we want to benchmark 37 | def benchmark(description) 38 | RubyProf.measure_mode = TYPE 39 | 40 | RubyProf.start 41 | ITERATIONS.times { yield } 42 | result = RubyProf.stop 43 | 44 | print_result(result, description) 45 | end 46 | -------------------------------------------------------------------------------- /benchmarks/combined_benchmark.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/benchmark_helper') 2 | 3 | # Class to which we will bind with aspect 4 | class Klass 5 | aspector do 6 | before :test, :before_test 7 | after :test, :after_test 8 | around :test, :around_test 9 | end 10 | 11 | def test_no_aspect; end 12 | 13 | def test; end 14 | 15 | def before_test; end 16 | 17 | def after_test(_result); end 18 | 19 | def around_test(proxy, &block) 20 | proxy.call(&block) 21 | end 22 | end 23 | 24 | instance = Klass.new 25 | 26 | benchmark 'instance.test_no_aspect' do 27 | instance.test_no_aspect 28 | end 29 | 30 | benchmark 'instance.test' do 31 | instance.test 32 | end 33 | -------------------------------------------------------------------------------- /benchmarks/method_invocation_benchmark.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/benchmark_helper') 2 | 3 | # Class with method invokation 4 | class Klass 5 | def do_something; end 6 | 7 | def test 8 | do_something 9 | end 10 | 11 | do_something_method = instance_method(:do_something) 12 | 13 | define_method :test_with_method_object do 14 | do_something_method.bind(self).call 15 | end 16 | end 17 | 18 | instance = Klass.new 19 | 20 | benchmark 'instance.test' do 21 | instance.test 22 | end 23 | 24 | benchmark 'instance.test_with_method_object' do 25 | instance.test_with_method_object 26 | end 27 | -------------------------------------------------------------------------------- /benchmarks/raw_benchmark.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/benchmark_helper') 2 | 3 | # Class to which we will bind with aspect 4 | class Klass 5 | aspector do 6 | raw :test do |method, _aspect| 7 | # rubocop:disable Eval 8 | eval <<-CODE 9 | alias #{method}_without_aspect #{method} 10 | 11 | define_method :#{method} do 12 | return #{method}_without_aspect unless _aspect.enabled? 13 | before_#{method} 14 | #{method}_without_aspect 15 | end 16 | CODE 17 | end 18 | end 19 | 20 | def test_no_aspect; end 21 | 22 | def test; end 23 | 24 | def before_test; end 25 | end 26 | 27 | instance = Klass.new 28 | 29 | benchmark 'instance.test_no_aspect' do 30 | instance.test_no_aspect 31 | end 32 | 33 | benchmark 'instance.test' do 34 | instance.test 35 | end 36 | -------------------------------------------------------------------------------- /examples/activerecord_hooks.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Class that fakes the ActiveRecord class 5 | class ARClass 6 | def initialize 7 | end 8 | 9 | def save 10 | end 11 | end 12 | 13 | # Our ActiveRecord hooks aspect 14 | class ActiveRecordHooks < Aspector::Base 15 | default private_methods: true 16 | 17 | before :initialize do 18 | puts "Before creating #{self.class.name} instance" 19 | end 20 | 21 | before :save do 22 | puts "Before saving #{self.class.name} instance" 23 | end 24 | end 25 | 26 | ActiveRecordHooks.apply(ARClass) 27 | 28 | ar = ARClass.new 29 | ar.save 30 | -------------------------------------------------------------------------------- /examples/around_example.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test(arg) 7 | puts "test(#{arg}) 1" 8 | yield arg 9 | puts "test(#{arg}) 2" 10 | end 11 | end 12 | 13 | aspector(ExampleClass) do 14 | target do 15 | def do_this(proxy, arg, &block) 16 | puts "do_this(#{arg}) 1" 17 | proxy.call arg, &block 18 | puts "do_this(#{arg}) 2" 19 | end 20 | end 21 | 22 | around :test, :do_this 23 | 24 | around :test, name: 'advice2' do |proxy, arg, &block| 25 | puts "advice2(#{arg}) 1" 26 | proxy.call arg, &block 27 | puts "advice2(#{arg}) 2" 28 | end 29 | end 30 | 31 | ExampleClass.new.test 'x' do |arg| 32 | puts "block(#{arg})" 33 | end 34 | -------------------------------------------------------------------------------- /examples/aspector_apply_example.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test 7 | puts 'test' 8 | end 9 | end 10 | 11 | aspect = Aspector do 12 | target do 13 | def do_this 14 | puts 'do_this' 15 | end 16 | end 17 | 18 | before :test, :do_this 19 | 20 | before :test do 21 | puts 'do_that' 22 | end 23 | end 24 | 25 | aspect.apply(ExampleClass) 26 | element = ExampleClass.new 27 | element.test 28 | aspect.disable! 29 | element.test 30 | -------------------------------------------------------------------------------- /examples/aspector_example.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test 7 | puts 'test' 8 | end 9 | end 10 | 11 | aspector(ExampleClass) do 12 | target do 13 | def do_this 14 | puts 'do_this' 15 | end 16 | end 17 | 18 | before :test, :do_this 19 | 20 | before :test do 21 | puts 'do_that' 22 | end 23 | end 24 | 25 | ExampleClass.new.test 26 | -------------------------------------------------------------------------------- /examples/cache_aspect.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test 7 | puts 'test' 8 | 1 9 | end 10 | 11 | def test2 12 | puts 'test2' 13 | 2 14 | end 15 | end 16 | 17 | # A simple cache engine 18 | class SimpleCache 19 | @data = {} 20 | 21 | def self.cache(key, ttl) 22 | found = @data[key] # found is like [time, value] 23 | 24 | if found 25 | puts "Found in cache: #{key}" 26 | insertion_time, value = *found 27 | 28 | return value if Time.now < insertion_time + ttl 29 | 30 | puts "Expired: #{key}" 31 | end 32 | 33 | value = yield 34 | @data[key] = [Time.now, value] 35 | value 36 | end 37 | end 38 | 39 | # Aspect used to wrap methods with caching logic 40 | class CacheAspect < Aspector::Base 41 | default ttl: 60 42 | 43 | around interception_arg: true, method_arg: true do |interception, method, proxy, &block| 44 | key = method 45 | ttl = interception.options[:ttl] 46 | 47 | SimpleCache.cache key, ttl do 48 | proxy.call(&block) 49 | end 50 | end 51 | end 52 | 53 | CacheAspect.apply ExampleClass, method: :test, ttl: 2 54 | CacheAspect.apply ExampleClass, method: :test2 55 | 56 | instance = ExampleClass.new 57 | 58 | # Will store value in cache 59 | instance.test 60 | instance.test2 61 | 62 | # Will get value from cache 63 | instance.test 64 | instance.test2 65 | 66 | sleep 3 67 | 68 | instance.test # Cache expired 69 | instance.test2 # Cache is still valid 70 | -------------------------------------------------------------------------------- /examples/design_by_contract.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Design by contract example class 5 | class ExampleClass 6 | def initialize 7 | @transactions = [] 8 | @total = 0 9 | end 10 | 11 | def buy(price) 12 | @transactions << price 13 | @total += price 14 | end 15 | 16 | def sell(price) 17 | @transactions << price # Wrong 18 | @total -= price 19 | end 20 | end 21 | 22 | # Object extensions 23 | class Object 24 | def assert(bool, message = 'Assertion failure') 25 | return if bool 26 | 27 | $stderr.puts message 28 | $stderr.puts caller 29 | end 30 | end 31 | 32 | # Aspect that we will apply 33 | class ContractExample < Aspector::Base 34 | before do |price, &_block| 35 | assert price > 0, "Price is #{price}, should be greater than 0" 36 | end 37 | 38 | after result_arg: false do |*_, &_block| 39 | sum = @transactions.reduce(&:+) 40 | assert @total == sum, "Total(#{@total}) and sum of transactions(#{sum}) do not match" 41 | end 42 | end 43 | 44 | ContractExample.apply ExampleClass, methods: %w( buy sell ) 45 | 46 | instance = ExampleClass.new 47 | instance.buy(10) 48 | instance.sell(10) 49 | instance.sell(-10) 50 | -------------------------------------------------------------------------------- /examples/exception_handler.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test(input) 7 | puts input.upcase 8 | end 9 | end 10 | 11 | # Aspect used to handle exceptions 12 | class ExceptionHandler < Aspector::Base 13 | target do 14 | def handle_exception(proxy, *args, &block) 15 | proxy.call(*args, &block) 16 | rescue => e 17 | puts "Rescued: #{e}" 18 | end 19 | end 20 | 21 | around :handle_exception 22 | end 23 | 24 | ExceptionHandler.apply(ExampleClass, method: :test) 25 | 26 | a = ExampleClass.new 27 | a.test('good') 28 | a.test(nil) 29 | -------------------------------------------------------------------------------- /examples/exception_handler2.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def self.test(input) 7 | puts input.upcase 8 | end 9 | 10 | def test(input) 11 | puts input.upcase 12 | end 13 | end 14 | 15 | # Aspect used to handle exceptions 16 | class ExceptionHandler < Aspector::Base 17 | target do 18 | def handle_exception(proxy, *args, &block) 19 | proxy.call(*args, &block) 20 | rescue => e 21 | puts "Rescued: #{e}" 22 | end 23 | end 24 | 25 | around :handle_exception 26 | end 27 | 28 | ExceptionHandler.apply(ExampleClass, method: :test, class_methods: true) 29 | 30 | ExampleClass.test('good') 31 | ExampleClass.test(nil) 32 | 33 | ExceptionHandler.apply(ExampleClass, method: :test) 34 | 35 | instance = ExampleClass.new 36 | instance.test('good') 37 | instance.test(nil) 38 | -------------------------------------------------------------------------------- /examples/implicit_method_option_test.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test 7 | puts 'test' 8 | end 9 | end 10 | 11 | # Aspect that we want to use 12 | class ImplicitMethodOptionTest < Aspector::Base 13 | # Apply advice to options[:method] and options[:methods] if no target method is given 14 | # before options[:method], options[:methods] do 15 | before do 16 | puts 'before' 17 | end 18 | end 19 | 20 | ImplicitMethodOptionTest.apply(ExampleClass, method: :test) 21 | 22 | ExampleClass.new.test 23 | -------------------------------------------------------------------------------- /examples/instance_aspect.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example that shows how to use Aspector with a single instance 5 | class ExampleClass 6 | def test 7 | puts 'test' 8 | end 9 | end 10 | 11 | # Aspect that will be applied on an instance 12 | class InstanceAspect < Aspector::Base 13 | target do 14 | def do_this 15 | puts 'do_this' 16 | end 17 | end 18 | 19 | before :test, :do_this 20 | 21 | before :test do 22 | puts 'do_that' 23 | end 24 | end 25 | 26 | asp_instance = ExampleClass.new 27 | not_instance = ExampleClass.new 28 | 29 | InstanceAspect.apply(asp_instance) 30 | 31 | asp_instance.test # This instance will have an aspect added 32 | not_instance.test # This instance wont have aspect added 33 | -------------------------------------------------------------------------------- /examples/interception_options_example.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # This example shows how can we access and use interception options 5 | # Interception options are all the options that come from the default 6 | # aspect options and directly from aspect applying 7 | # Note that if you apply aspect instance to the same elements and 8 | # you don't change options - they will be the same for all the classes/instances 9 | # to which you apply this aspect. If you change apply parameters - the interception 10 | # parameters will differ as well. 11 | 12 | # Example class to which we will apply our aspects 13 | class Example1Class 14 | def exec 15 | puts "#{Example1Class} exec execution" 16 | end 17 | end 18 | 19 | # Example class2 to which we will apply our aspects 20 | class Example2Class 21 | def exec_different 22 | puts "#{Example2Class} exec_different execution" 23 | end 24 | end 25 | 26 | # Aspect used to wrap methods from example classes 27 | class ExampleAspect < Aspector::Base 28 | default super_option: 60, key_option: 2 29 | 30 | around interception_arg: true, method_arg: true do |interception, method, proxy, &block| 31 | # Here we fetch options from both - interception and aspect class 32 | # Aspect options are transfered directly to all the instances of an aspect and to all 33 | # the interceptions. However they have a lower priority then interception direct options 34 | # so they can be overwritten by them 35 | method = interception.options[:method] 36 | key_option = interception.options[:key_option] 37 | super_option = interception.options[:super_option] 38 | interception_option = interception.options[:interception_option] 39 | 40 | puts "super_option value: #{super_option}" 41 | puts "key_option value: #{key_option}" 42 | 43 | proxy.call(&block) 44 | 45 | puts "interception_option value: #{interception_option}" 46 | puts "method value: #{method}" 47 | end 48 | end 49 | 50 | ExampleAspect.apply Example1Class, method: :exec, interception_option: 2 51 | ExampleAspect.apply Example2Class, method: :exec_different 52 | 53 | instance1 = Example1Class.new 54 | instance2 = Example2Class.new 55 | 56 | instance1.exec 57 | instance2.exec_different 58 | 59 | # --- instance1 60 | # Expected output 61 | # super_option value: 60 62 | # key_option value: 2 63 | # Example1Class exec execution 64 | # interception_option value: 2 65 | # method value: exec 66 | # --- instance2 67 | # super_option value: 60 68 | # key_option value: 2 69 | # Example2Class exec_different execution 70 | # interception_option value: 71 | # method value: exec_different 72 | -------------------------------------------------------------------------------- /examples/logging_aspect.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test(input) 7 | input.upcase 8 | end 9 | end 10 | 11 | # Aspect used as a logging hookup 12 | class LoggingAspect < Aspector::Base 13 | ALL_METHODS = /.*/ 14 | 15 | around ALL_METHODS, except: :class, method_arg: true do |method, proxy, *args, &block| 16 | class_method = "#{self.class}.#{method}" 17 | puts "Entering #{class_method}: #{args.join(',')}" 18 | result = proxy.call(*args, &block) 19 | puts "Exiting #{class_method}: #{result}" 20 | result 21 | end 22 | end 23 | 24 | LoggingAspect.apply(ExampleClass) 25 | puts 'LoggingAspect is applied' 26 | 27 | instance = ExampleClass.new 28 | instance.test 'input' 29 | 30 | LoggingAspect.disable! 31 | puts 'LoggingAspect is disabled' 32 | instance.test 'input' 33 | 34 | LoggingAspect.enable! 35 | puts 'LoggingAspect is enabled' 36 | instance.test 'input' 37 | -------------------------------------------------------------------------------- /examples/multiple_aspects_on_same.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test 7 | puts 'test' 8 | end 9 | end 10 | 11 | aspector(ExampleClass) do 12 | before :test do 13 | puts 'do_before' 14 | end 15 | end 16 | 17 | aspector(ExampleClass) do 18 | after :test do |result| 19 | puts 'do_after' 20 | result 21 | end 22 | end 23 | 24 | ExampleClass.new.test 25 | -------------------------------------------------------------------------------- /examples/process_aspector.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Aspect used to provide process logging 5 | class ProcessLogging < Aspector::Base 6 | after :spawn do |return_value, *args| 7 | $stderr.puts args[0].inspect 8 | return_value 9 | end 10 | end 11 | 12 | ProcessLogging.apply(Process, class_methods: true) 13 | Process.spawn('pwd') 14 | -------------------------------------------------------------------------------- /examples/retry_aspect.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 2 | require 'aspector' 3 | 4 | # Example class to which we will apply our aspects 5 | class ExampleClass 6 | def test 7 | puts 'test' 8 | fail 9 | end 10 | end 11 | 12 | # Aspect that will be used as a retry with counting 13 | class RetryAspect < Aspector::Base 14 | target do 15 | def retry_this(proxy, &block) 16 | proxy.call(&block) 17 | rescue 18 | @retry_count ||= 3 19 | @retry_count -= 1 20 | 21 | if @retry_count == 0 22 | @retry_count = nil 23 | raise 24 | end 25 | 26 | retry 27 | end 28 | end 29 | 30 | around :retry_this 31 | end 32 | 33 | RetryAspect.apply(ExampleClass, method: :test) 34 | 35 | instance = ExampleClass.new 36 | 37 | begin 38 | instance.test 39 | rescue 40 | puts 'Fails after 3 retries' 41 | end 42 | -------------------------------------------------------------------------------- /lib/aspector.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | logger 3 | forwardable 4 | erb 5 | singleton 6 | ).each { |lib| require lib } 7 | 8 | base_path = File.dirname(__FILE__) + '/aspector' 9 | 10 | %w( 11 | refinements/class 12 | refinements/string 13 | errors 14 | version 15 | logging 16 | logger 17 | advice 18 | advice/method_matcher 19 | advice/metadata 20 | advice/params 21 | advice/builder 22 | interception 23 | interceptions_storage 24 | method_template 25 | base/class_methods 26 | base/dsl 27 | base/storage 28 | base/status 29 | base 30 | deferred/logic 31 | deferred/option 32 | extensions/module 33 | extensions/object 34 | ).each { |scope| require "#{base_path}/#{scope}" } 35 | -------------------------------------------------------------------------------- /lib/aspector/advice.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # A representation of a given type of advice 3 | class Advice 4 | # All available advices types that we support 5 | TYPES = %i( 6 | before 7 | before_filter 8 | after 9 | around 10 | raw 11 | ) 12 | 13 | # What prefix should generated methods have 14 | METHOD_PREFIX = 'aspector_advice' 15 | 16 | attr_reader :options, :advice_code, :advice_block, :with_method 17 | 18 | # Defines methods that allow us to check if an advice is of a given type 19 | TYPES.each do |type_name| 20 | # Defines constants like BEFORE, AFTER,etc 21 | const_set(type_name.to_s.upcase, type_name) 22 | 23 | # @return [Boolean] is advice of a given type? 24 | # @example Check if advice is an after, before or before_filter 25 | # advice.after? #=> true 26 | # advice.before? #=> false 27 | # advice.before_filter? #=> false 28 | define_method :"#{type_name}?" do 29 | @type == type_name 30 | end 31 | end 32 | 33 | # @param type [Symbol] type of advice that we want to build (see TYPES) 34 | # @param match_methods [Array] 35 | # methods and other elements that we want to match 36 | # @param with_method [Symbol, nil] method name what should be invoked (or nil if block should 37 | # be invoked) 38 | # @param options [Hash] hash with options for this advice 39 | # @param block [Proc, nil] block that should be invoked - not required if 40 | # with_method is not nil 41 | # @raise [Aspector::Errors::InvalidAdviceType] raised when we want to create an advice of a 42 | # type that is not supported 43 | # @raise [Aspector::Errors::CodeBlockRequired] raised when we dont provide with_method or block 44 | def initialize(type, match_methods, with_method, options = {}, &block) 45 | # Proceed only with supported advices types 46 | fail Errors::InvalidAdviceType, type unless TYPES.include?(type) 47 | # Proceed only when theres a with_method or a block that we will apply 48 | fail Errors::CodeBlockRequired unless with_method || block 49 | 50 | @type = type 51 | @options = options 52 | @advice_block = block 53 | @method_matcher = MethodMatcher.new(*match_methods) 54 | 55 | if with_method.is_a? Symbol 56 | @with_method = with_method 57 | else 58 | @advice_code = with_method 59 | @with_method = "#{METHOD_PREFIX}_#{object_id}" 60 | end 61 | end 62 | 63 | # @param method [String] name of a method that we want to check 64 | # @param interception [Aspector::Interception] interception that provides us with a context in 65 | # which we check for method mathing 66 | # @return [Boolean] true if provided method matches and we can apply advice to it 67 | def match?(method, interception) 68 | # If nothing in the provided matcher matches, we need to quit 69 | return false unless @method_matcher.any?(method, interception) 70 | # If it matches and there are no exceptions - it means we should match 71 | return true unless @options[:except] 72 | 73 | # We have to create a second matcher here, tu check the exceptations 74 | @except ||= MethodMatcher.new(@options[:except]) 75 | 76 | !@except.any?(method) 77 | end 78 | 79 | # @param logic [Aspector::Deferred::Logic] deferred logic that we want to use 80 | # @return [Boolean] should it use this logic 81 | def use_deferred_logic?(logic) 82 | @method_matcher.include?(logic) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/aspector/advice/builder.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | class Advice 3 | # Used to build an aspect instance from different set of options 4 | # When we build an advice, the default API tells us to provide list of arguments from which 5 | # first we have methods to which we want to apply aspect, and then optional hash options. 6 | # Here we need to separate those parts, make some additional checking and preparation and 7 | # then build an Aspector::Advice 8 | # @note Separating arguments (methods_and_options) into arguments, with_method and options 9 | # can be tricky, so before you change anything, please make sure you understand 10 | # logic that is behind it 11 | class Builder 12 | # @param advice_type [symbol] type of an advice that we want to build (before, after, etc) 13 | # @param methods_and_options - methods and options based on which we want to create an Advice 14 | # @param block [Proc] block of code 15 | # @return [Aspector::Advice::Builder] builder instance 16 | # @example 17 | # Aspector::Advice:Builder.new( 18 | # Aspector::Advice::Metadata::BEFORE, 19 | # *[:exec, :run, method_args: true, ttl: 1] 20 | # ) 21 | def initialize(advice_type, *methods_and_options, &block) 22 | @meta_data = Aspector::Advice::Metadata.public_send(advice_type) 23 | @params = Aspector::Advice::Params.new(methods_and_options, block) 24 | @methods_and_options = methods_and_options.tap(&:flatten!) 25 | @block = block 26 | end 27 | 28 | # Builds an Aspector::Advice instance 29 | # @return [Aspector::Advice] advice instance 30 | # @raise [Aspector::Errors::CodeBlockRequired] raised when we require a block 31 | # of code, but none provided 32 | # @example 33 | # builder.build #=> Aspector::Advice instance 34 | def build 35 | fail Errors::CodeBlockRequired if @meta_data.raw? && !@block 36 | 37 | Advice.new( 38 | @meta_data.advice_type, 39 | @params.methods, 40 | @params.with_method, 41 | @meta_data.default_options.merge(@params.options), 42 | &@block 43 | ) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/aspector/advice/metadata.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | class Advice 3 | # Metadata object for Advice model 4 | # It stores informations that are useful when building an advice of a given type 5 | # Technically it is useful only for the after case when it has some default arguments 6 | # but we keep it also for the future development, when we might add more types 7 | class Metadata 8 | attr_reader :advice_type, :default_options 9 | 10 | # @param [Symbol] advice_type that we want to build 11 | # @param [Hash] default_options for given advice type 12 | # @return [Aspector::Advice::Metadata] medatada informations about given advice type 13 | # @example Create metadata for :before type 14 | # Aspector::Advice::Metadata.new(:before) #=> metadata instance 15 | # @example Get default metadata for before type 16 | # Aspector::Advice::Metadata::BEFORE #=> before metadata instance 17 | def initialize(advice_type, default_options = {}) 18 | @advice_type = advice_type 19 | @default_options = default_options 20 | end 21 | 22 | Advice::TYPES.each do |type| 23 | # @return [Boolean] is advice object for a given type of advice 24 | # @example Checking if this advice is of a given type 25 | # advice.raw? #=> false 26 | # advice.before? #=> true 27 | # advice.before_filter? #=> true 28 | # advice.after? #=> true 29 | # advice.around? #=> true 30 | define_method :"#{type}?" do 31 | @advice_type == type 32 | end 33 | end 34 | 35 | class << self 36 | Advice::TYPES.each do |type| 37 | # Allows us to easily get metadata of a given advice type 38 | # @return [Aspector::Advice::Metadata] metadata instance 39 | # @example Get Metadata about after advice 40 | # Aspector::Advice::Metadata.after 41 | define_method type do 42 | Object.const_get("Aspector::Advice::Metadata::#{type.to_s.upcase}") 43 | end 44 | end 45 | end 46 | 47 | # Metadata for all the before advices 48 | BEFORE = new Aspector::Advice::BEFORE 49 | # Metadata for all the before filter advices 50 | BEFORE_FILTER = new Aspector::Advice::BEFORE_FILTER 51 | # Metadata for all the after advices 52 | AFTER = new Aspector::Advice::AFTER, result_arg: true 53 | # Metadata for all the around advices 54 | AROUND = new Aspector::Advice::AROUND 55 | # Metadata for all the raw advices 56 | RAW = new Aspector::Advice::RAW 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/aspector/advice/method_matcher.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | class Advice 3 | # Class used to check if a given method matched our match data 4 | # @note It is used internally in the advice class only 5 | class MethodMatcher 6 | extend Forwardable 7 | 8 | # The include? will check if we have in match_data a provided item 9 | def_delegator :@match_data, :include? 10 | 11 | # @param match_data 12 | # [Array] 13 | # single match details about which method we should match. 14 | # As for whole aspector, it can be a string with a method name, symbolized method name, 15 | # regexp, deffered logic or deffered option (as a single element) 16 | # @return [Aspector::Advice::MethodMatcher] method matcher instance for given match_data 17 | # @example Create matcher for a symbol and regexp 18 | # Aspector::Advice::MethodMatcher.new(:exec, /exil.*/) 19 | def initialize(*match_data) 20 | @match_data = match_data.tap(&:flatten!) 21 | end 22 | 23 | # Informs us if a given method is matched by any of our match data 24 | # @param method [String] method that we want to check 25 | # @param aspect [Aspector::Interception, nil] optional interception when a match item is a 26 | # Deferred::Logic or DefferedOption 27 | # @return [Boolean] does a given method match our match_data 28 | # @example check if method matches either :calculate or /subst.*/ 29 | # any?('substract') #=> true 30 | # any?('sub') #=> false 31 | # any?('calculate!') #=> false 32 | # any?('calculate') #=> true 33 | def any?(method, aspect = nil) 34 | @match_data.any? do |item| 35 | matches?(item, method, aspect) 36 | end 37 | end 38 | 39 | # @return [String] stringed list of all elements that were in match_data 40 | # @example Print this method_matcher 41 | # method_matcher.to_s #=> 'execution, run, (?-mix:a.*)' 42 | def to_s 43 | @match_data.map(&:inspect).join(', ') 44 | end 45 | 46 | private 47 | 48 | # @return [Boolean] checks if a match_item matches a given method 49 | # @param match_item 50 | # [String, Symbol, Regexp, Aspector::Deferred::Logic, Aspector::Deferred::Option] 51 | # single match item of a given class 52 | # @param method [String] method that we want to check 53 | # @param interception [Aspector::Interception, nil] optional interception when 54 | # a match item is a Deferred::Logic or DefferedOption 55 | # @example Check if /exe.*/ matches method 56 | # matches?(/exe.*/, 'exec') #=> true 57 | # matches?(/exe.*/, 'ex') #=> false 58 | # matches?(/exe.*/, 'run') #=> false 59 | # matches?(/exe.*/, 'execute') #=> true 60 | def matches?(match_item, method, interception = nil) 61 | case match_item 62 | when String 63 | match_item == method 64 | when Regexp 65 | !(match_item =~ method).nil? 66 | when Symbol 67 | match_item.to_s == method 68 | when Deferred::Logic 69 | matches_deferred_logic?(match_item, method, interception) 70 | when Deferred::Option 71 | matches_deferred_option?(match_item, method, interception) 72 | else 73 | fail Errors::UnsupportedItemClass, match_item.class 74 | end 75 | end 76 | 77 | # @return [Boolean] checks if a deferred logic match item matches the method 78 | # @param match_item [Aspector::Deferred::Logic] deferred logic over which we match 79 | # @param method [String] method that we want to check 80 | # @param interception [Aspector::Interception] interception that owns the deferred logic 81 | def matches_deferred_logic?(match_item, method, interception) 82 | value = interception.deferred_logic_results(match_item) 83 | return false unless value 84 | 85 | self.class.new(value).any?(method) 86 | end 87 | 88 | # @return [Boolean] checks if a deferred option match item matches the method 89 | # @param match_item [Aspector::Deferred::Option] deferred option over which we match 90 | # @param method [String] method that we want to check 91 | # @param interception [Aspector::Interception] interception that owns the deferred option 92 | def matches_deferred_option?(match_item, method, interception) 93 | value = interception.options[match_item.key] 94 | return false unless value 95 | 96 | self.class.new(value).any?(method) 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/aspector/advice/params.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | class Advice 3 | # Allows extracting methods and additional options from single array 4 | # When we build an advice, the default API tells us to provide list of arguments from which 5 | # first we have methods to which we want to apply aspect, and then optional hash options. 6 | # Here we need to separate those parts, make some additional checking and preparation and 7 | # then build an Aspector::Advice 8 | # @note Separating arguments (methods_and_options) into arguments, with_method and options 9 | # can be tricky, so before you change anything, please make sure you understand 10 | # logic that is behind it 11 | class Params 12 | def initialize(methods_and_options, block) 13 | @methods_and_options = methods_and_options.tap(&:flatten!) 14 | @block = block 15 | end 16 | 17 | # @return [Hash] hash with options for advice that we are creating 18 | # @note This will use default options for a given advice and will merge to it any 19 | # optional options provided in @meta_data hash 20 | # @example No options in the @meta_data 21 | # @meta_data = [:exec] 22 | # options #=> only default aspect metadata options 23 | # @example Options in the @meta_data 24 | # @meta_data = [:exec, { ttl: 10 }] 25 | # options #=> { result_arg: true, ttl: 10 } // Hash with default options + options 26 | def options 27 | options? ? @methods_and_options.last : {} 28 | end 29 | 30 | # @return [Symbol, String, nil] with_method that should be executed when aspect is used 31 | # @example Methods names, no block and options 32 | # @methods_and_options = [:exec, /exe.*/, :before_exe, { method_arg: true }] 33 | # @block = nil 34 | # with_method #=> :before_exec 35 | # @example Methods names and nothing else 36 | # @methods_and_options = [:exec, :run] 37 | # @block = nil 38 | # with_method #=> :run 39 | # @example Methods names and options (no block) 40 | # @methods_and_options = [:exec, :run, { ttl: 1 }] 41 | # @block = nil 42 | # with_method #=> :run 43 | # @example Everything including block 44 | # @methods_and_options = [:exec, :run, { ttl: 1 }] 45 | # @block = -> {} 46 | # with_method #=> nil 47 | def with_method 48 | # If we've provided a block, there can't be a with_method. We assume that all 49 | # method names are methods to which we should bind with the aspect advice 50 | return nil unless with_method? 51 | # If there's no block given and there are no options (which should be as a last arg) 52 | # we assume that the last method is the method that should be executed 53 | return @methods_and_options.last unless options? 54 | 55 | # If we're here than it means that there were no block and there are options 56 | # It means that in the @methods_and_options we have both with_method and options 57 | # so with_method name is before all options (that are last) 58 | @methods_and_options[@methods_and_options.size - 2] 59 | end 60 | 61 | # @return [Array] formatted array with list of methods for which we should 62 | # apply the aspect 63 | # @note For consistancy all symbol method names will be casted to strings 64 | def methods 65 | methods = extracted_methods 66 | 67 | methods.map! do |method| 68 | method.is_a?(Symbol) ? method.to_s : method 69 | end 70 | 71 | methods += [ 72 | Deferred::Option.new(:method), 73 | Deferred::Option.new(:methods) 74 | ] if methods.empty? 75 | 76 | methods 77 | end 78 | 79 | private 80 | 81 | # @return [Boolean] true if there's no block provided. It means that we should take 82 | # the last method name from the @methods_and_options (not the last element but 83 | # last string/symbol) and assume that this is a method that should be executed 84 | # when aspect is being used 85 | # @example 86 | # @methods_and_options = [:exec, /exe.*/, :before_exe, { method_arg: true }] 87 | # @block = nil 88 | # with_method? #=> true 89 | # with_method #=> :before_exec 90 | def with_method? 91 | @block.nil? 92 | end 93 | 94 | # @return [Boolean] true if we have options in methods_and_options variable 95 | # @note Those are the optional options provided as a last argument for 96 | # DSL building methods (before, after, etc) 97 | def options? 98 | @methods_and_options.last.is_a? Hash 99 | end 100 | 101 | # Extracts methods for which we should apply the aspect 102 | # @note This just extracts them from the @methods_and_options, further processing happens 103 | # in the '#methods' method 104 | def extracted_methods 105 | # If there are no extra options and there is a block of code, then the @methods_and_options 106 | # is composed only from methods to which we should apply the aspect 107 | return @methods_and_options if !options? && !with_method? 108 | # If there are options and no block, it means that two last parameters in the array are 109 | # the method that should be executed on the aspect and aspect options 110 | return @methods_and_options[0...-2] if options? && with_method? 111 | # Otherwise it means that we have a block and options 112 | @methods_and_options[0...-1] 113 | end 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /lib/aspector/base.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Base aspector class from which each created aspect must inherit 3 | # It provides all the features to build aspects including a simple Dsl and some additional 4 | # class and instance methods 5 | class Base 6 | extend Dsl 7 | extend ClassMethods 8 | 9 | # @return [Boolean] is this aspect enabled 10 | def enabled? 11 | self.class.status.enabled? 12 | end 13 | 14 | # Applies aspect instance into a given target class/module/instance 15 | # @param target [Class] any object (or class or module) to apply this aspect 16 | # @param options [Hash] set of options that we can pass to the aspect that will be applied 17 | # @return [Aspector::Interception] applied interception 18 | # @example Apply aspect to a ExampleClass 19 | # aspect.apply(ExampleClass) 20 | # @example Apply aspect to an instance 21 | # aspect.apply(object, , method: :run) 22 | # @example Apply aspect to a ExtendingModule 23 | # aspect.apply(ExtendingModule, , method: :run) 24 | def apply(target, options = {}) 25 | Interception.new( 26 | self, 27 | target, 28 | self.class.storage.default_options.merge(options) 29 | ).apply 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/aspector/base/class_methods.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | class Base 3 | # Class methods for Aspector::Base 4 | module ClassMethods 5 | extend Forwardable 6 | 7 | # Invokations of those methods should be delegated to #storage object 8 | %i( 9 | logger status 10 | ).each do |method| 11 | def_delegator :storage, method 12 | end 13 | 14 | # Invokations of those methods should be delegated to #status object 15 | %i( 16 | disable! enable! enabled? 17 | ).each do |method| 18 | def_delegator :status, method 19 | end 20 | 21 | # @return [Advice::Base::Storage] storage instance for this class 22 | # @example Get logger from storage 23 | # storage.logger #=> logger instance 24 | def storage 25 | @storage ||= Storage.new(self) 26 | end 27 | 28 | # Applies this aspect to provided class/method/instance 29 | # @param target [Class] target class/method/instance to which we want to apply this aspect 30 | # @param rest Optional arguments that might contain more elements to which we want to 31 | # apply the aspect and/or options 32 | # @example Apply to a single class without additional things 33 | # apply(SingleClass) 34 | # @example Apply to a single module with additional options 35 | # apply(SingleModule, option_key: 1234) 36 | # @example Apply at once to module and class with additional options 37 | # apply(SingleClass, SingleModule, option_attribute: true) 38 | def apply(target, *rest) 39 | options = rest.last.is_a?(Hash) ? rest.pop : {} 40 | 41 | targets = rest.unshift target 42 | targets.map do |apply_target| 43 | logger.info 'apply', apply_target, options.inspect 44 | instance = new 45 | instance.send :apply, apply_target, options 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/aspector/base/dsl.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | class Base 3 | # Methods that allow us to work with aspects (define them, etc) 4 | # They are included in the Base class but we removed them for visibility reasons 5 | # @note They are marked as private because we use them only inside of the 6 | # aspect that we want to define. This is a real "interface" for the programmers 7 | # that want to use this gem - everything else is private 8 | module Dsl 9 | private 10 | 11 | Aspector::Advice::TYPES.each do |type_name| 12 | # Defines methods that allow us to build aspects 13 | # @param methods_and_options All the arguments that we want to pass. It should contain 14 | # all the methods names (or regexp matches) for which we want to apply our aspect and 15 | # optional (not required) options that we want to provide to the aspect. 16 | # So: first we have to provide methods and regexps for which we want to apply aspect 17 | # the we can provide and optional hash arguments with any option that we want 18 | # We might also skip the methods definitions but then we need to provide them when we 19 | # apply a given aspect 20 | # @note If no block givem it will use the last method name provided as an invokation 21 | # method that will be executed then aspect is applied 22 | # 23 | # @example Simple example for before 24 | # before :run do 25 | # puts 'Executing a before action!' 26 | # end 27 | # 28 | # @example before example with a method that will be executed. The :before_run will 29 | # be executed before the run execution. However if we would provide a block, then 30 | # the block would be evaluated for both methods 31 | # 32 | # before :run, :before_run 33 | # 34 | # @example Define aspect only with additional options (without method names that we will 35 | # provide when we will apply the aspect) 36 | # 37 | # after timeout: 10, connection_pool: 20 do 38 | # end 39 | define_method type_name do |*methods_and_options, &block| 40 | advice = Aspector::Advice::Builder.new(type_name, methods_and_options, &block).build 41 | storage.advices << advice 42 | advice 43 | end 44 | 45 | private type_name 46 | end 47 | 48 | # Allows to set default options for a given aspect 49 | # @param options [Hash] hash with default options for this aspect 50 | # @note We can also pass options that are not specifically aspect settings but general 51 | # options that we will want to use in the aspected code (see the examples below) 52 | # 53 | # @example Create an aspect with default options 54 | # class EmptyAspect < Aspector::Base 55 | # default private_methods: true 56 | # end 57 | # 58 | # @example Create an aspect with default options used by aspector and some extra parameters 59 | # that we want to use inside code (options that does not influence the aspector behaviour) 60 | # 61 | # class ExampleAspect < Aspector::Base 62 | # default private_methods: true, super_option: 60, key_option: 2 63 | # 64 | # before :run, interception_arg: true do |interception| 65 | # print "#{interception.options[:super_option]}\n" 66 | # print "#{interception.options[:key_option]}\n" 67 | # end 68 | # end 69 | def default(options) 70 | storage.default_options.merge!(options) 71 | end 72 | 73 | # Allows us to define code that will be applied to the target class/module/instance 74 | # together with an aspect 75 | # @note When we define a new target, it will store built deferred logic into 76 | # this class storage deferred logics array 77 | # @param code [Proc] code that we want to apply to a target class 78 | # @param block [Proc] block with code if we want to declare it directly 79 | # @raise [Aspector::Errors::CodeBlockRequired] raised when we require a block 80 | # of code, but none provided 81 | # @example Define aspect with a target method that will be applied on a ExampleClass 82 | # class ExampleAspect < Aspector::Base 83 | # target do 84 | # def run! 85 | # puts 'Running!' 86 | # end 87 | # end 88 | # end 89 | # 90 | # class ExampleClass 91 | # end 92 | # 93 | # ExampleAspect.apply(ExampleClass) 94 | # ExampleClass.new.run! #=> 'Running!' 95 | # 96 | # @example Define target with a variable that contains code that should be executed on a 97 | # target class/module/instance 98 | # class ExampleAspect < Aspector::Base 99 | # action = Proc.new do 100 | # def run! 101 | # puts 'Running!' 102 | # end 103 | # end 104 | # 105 | # target action 106 | # end 107 | # 108 | # class ExampleClass 109 | # end 110 | # 111 | # ExampleAspect.apply(ExampleClass) 112 | # ExampleClass.new.run! 113 | def target(code = nil, &block) 114 | fail Errors::CodeBlockRequired unless code || block_given? 115 | 116 | logic = Deferred::Logic.new(code || block) 117 | # Since it is deferred, we need to store it for future use 118 | storage.deferred_logics << logic 119 | logic 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/aspector/base/status.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | class Base 3 | # Object responsible for storing given aspect status 4 | # If aspect is enabled - its code will be executed after the aspect as applied 5 | # If aspect is disabled - its code won't be executed after the aspect is applied 6 | # @note This is stored on a class level (per aspect class) 7 | # @example Create a status and check it 8 | # status = Aspector::Base::Status.new 9 | # status.enabled? #=> true 10 | # status.disable! 11 | # status.enabled? #=> false 12 | # status.enable! 13 | # status.enabled? #=> true 14 | class Status 15 | # @return [Aspector::Base::Status] status instance 16 | # @note We're enabled by default 17 | def initialize 18 | @enabled = true 19 | end 20 | 21 | # Set status of an aspect to disabled 22 | # @example Set status to disabled 23 | # status.disable! 24 | def disable! 25 | @enabled = false 26 | end 27 | 28 | # Set status of an aspect to enabled 29 | # @example Set status to enabled 30 | # status.enable! 31 | def enable! 32 | @enabled = true 33 | end 34 | 35 | # Is the object for which we've created status object enabled? 36 | # @return [Boolean] Is this status object enabled (true if yes) 37 | # @example Check if we're enabled 38 | # status.enabled? #=> true 39 | def enabled? 40 | @enabled == true 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/aspector/base/storage.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | class Base 3 | # Internal Base subclasses storage for storing any objects/informations 4 | # that we need to run Aspector 5 | # It was extracted from the Aspector::Base because we used to store a lot 6 | # of information there and by storing everything directly there, we've 7 | # added yet another responsibility to Aspector::Base class (and its subclasses) 8 | class Storage 9 | attr_accessor :advices 10 | attr_accessor :default_options 11 | attr_accessor :deferred_logics 12 | attr_reader :logger 13 | attr_reader :status 14 | 15 | # @param base [Aspector::Base] base class for which we create this storage - this accepts 16 | # also any Aspector::Base descendant class 17 | # @note Note that we store stuff per class - not per instance (Aspector::Base.storage) 18 | # @example Create an storage instance for Aspector::Base class 19 | # Aspector::Base::Storage.new(Aspector::Base) #=> storage instance 20 | def initialize(base) 21 | @base = base 22 | @advices = [] 23 | @default_options = {} 24 | @deferred_logics = [] 25 | @logger = Aspector::Logger.new(@base) 26 | @status = Aspector::Base::Status.new 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/aspector/deferred/logic.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Module containing elements that are dereffered 3 | module Deferred 4 | # Object that is used to store and apply deferred logic 5 | # This is used to apply logic defined in the aspect directly to a target class 6 | # by using the "target" method. 7 | # That way we can add an extra logic (methods and other elements) to a target class 8 | # directly from an aspect itself 9 | # 10 | # @example This example will shows how the whole deferred logic works 11 | # target do 12 | # def run 13 | # puts 'Running!' 14 | # end 15 | # end 16 | # 17 | # DummyClass.new.run #=> 'Running!' 18 | # 19 | # @example Creating deferred logic and applying it to a target class/module 20 | # code = lambda do 21 | # def run 22 | # puts 'Running!' 23 | # end 24 | # end 25 | # 26 | # logic = Aspector::Deferred::Logic.new(code) 27 | # DummyClass.new.run #=> undefined method run 28 | # logic.apply(DummyClass) 29 | # DummyClass.new.run #=> 'Running!' 30 | class Logic 31 | # @param code [Proc] block of code that should be evaluated in a target class/module context 32 | # @raise [Aspector::Deferred::Logic::InvalidCodeClass] raised when we try to use something 33 | # else than a Proc as a code 34 | # @return [Aspector::Deferred::Logic] deferred logic instance that can be used to apply 35 | # logic to any class/module 36 | def initialize(code) 37 | fail Errors::CodeBlockRequired, code.class unless code.is_a?(Proc) 38 | @code = code 39 | end 40 | 41 | # Applies code to a given target class 42 | # @param target [Class, Module] class or module to which we want to apply code 43 | # @param args [Array] arguments that should be passed to a block 44 | # that we will be evaluating in a target class/module context 45 | # @example Simple example without additional arguments 46 | # logic.code #=> -> { def run; end } 47 | # logic.apply(DummyClass) 48 | # DummyClass.new.run 49 | # @example Applying block that has addditional arguments 50 | # logic.code #=> -> (attr_name) { attr_accessor attr_name } 51 | # logic.apply(DummyClass, :name) 52 | # dummy = DummyClass.new 53 | # dummy.name = 'Test' 54 | # dummy.name #=> 'Test' 55 | def apply(target, *args) 56 | target.class_exec(*args, &@code) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/aspector/deferred/option.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Module containing elements that are dereffered 3 | module Deferred 4 | # Object used to store deferred options that can be used when we create aspects 5 | # We can use them as a "virtual" attributes that can be used but will be replaced 6 | # with proper values later on. Thanks to that we may build aspects that are not 7 | # bind yet to given methods, that are not yet populated with any aspect options, etc 8 | # This allows us to build better aspects that can be adapted easier to any class/module 9 | class Option 10 | attr_reader :key 11 | 12 | # @return [Aspector::Deferred::Option] single deferred option 13 | # @param key [Symbol] deferred option key that we can provide later 14 | # @example Create deferred option with a :methods key 15 | # Aspector::Deferred::Option.new(:methods) #=> deferred options instance 16 | def initialize(key = nil) 17 | @key = key 18 | end 19 | 20 | # Allows us to reference options that are not yet defined in the aspect 21 | # @note They will be later on replaced with "normal" options 22 | # @return [Aspector::Deferred::Option] returns self 23 | # @param [Symbol] key that we want to retrieve 24 | # 25 | # @example Build an after block for methods (options is a deferred option) 26 | # before options[:methods] do 27 | # values << 'do_this' 28 | # end 29 | def [](key) 30 | @key = key 31 | self 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/aspector/errors.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Module containing internal errors used in Aspector 3 | module Errors 4 | # Raised when we require a block of code but not provided 5 | class CodeBlockRequired < StandardError; end 6 | # Raised when we want to match item of a class that we dont support 7 | class UnsupportedItemClass < StandardError; end 8 | # Raised when we want to work with advice type that is not supported 9 | class InvalidAdviceType < StandardError; end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/aspector/extensions/module.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | module Extensions 3 | # Module extensions that we need to make aspector work with methods that will be defined 4 | # after an aspect instance was binded to a class/module 5 | module Module 6 | using Refinements::String 7 | 8 | private 9 | 10 | # Invoked when a new instance method was added to a class/module 11 | # This will be invoked for instance methods 12 | # This method triggers applying all the aspects from a given interception 13 | # to this newly created method 14 | # @param [Symbol] method name of a method that was just defined 15 | def aspector_instance_method_added(method) 16 | aspector_method_added(self, method) 17 | end 18 | 19 | # Invoked when a new class method was added to a class/module 20 | # This will be invoked for class methods 21 | # This method triggers applying all the aspects from a given interception 22 | # to this newly created method 23 | # @param [Symbol] method name of a method that was just defined 24 | def aspector_singleton_method_added(method) 25 | aspector_method_added(singleton_class, method) 26 | end 27 | 28 | # Triggers applying aspects on a newly created method 29 | # @param target [Class] class on which the method was defined. Keep in mind that 30 | # it might be a normal class for instance methods or a singleton class of a 31 | # class when applying on a class level 32 | # @param method [Symbol] method name of a method that was just defined 33 | def aspector_method_added(target, method) 34 | interceptions_storage = target.instance_variable_get(:@interceptions_storage) 35 | aspector_applied_flag = "aspector_applied_#{method}".to_instance_variable_name! 36 | 37 | return if target.instance_variable_get(:@aspector_creating_method) 38 | return if interceptions_storage.nil? 39 | return if interceptions_storage.empty? 40 | return if target.instance_variable_get(aspector_applied_flag) 41 | 42 | begin 43 | target.instance_variable_set(aspector_applied_flag, true) 44 | interceptions_storage.apply_to_method(method.to_s) 45 | ensure 46 | target.instance_variable_set(aspector_applied_flag, nil) 47 | end 48 | end 49 | end 50 | end 51 | end 52 | 53 | ::Module.send(:include, Aspector::Extensions::Module) 54 | -------------------------------------------------------------------------------- /lib/aspector/extensions/object.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Global classes extensions required to make Aspector work 3 | module Extensions 4 | # Extensions that must be included into the Object class 5 | module Object 6 | private 7 | 8 | # Direct aspect biding for multiple (optionally) classes 9 | # @param args multiple classes to which we want to bind and as a last parameter optional 10 | # options for aspect 11 | # @param [Proc] block with the aspect code 12 | # @return [Aspector::Base] aspector base anonymous descendant class 13 | # @note You can also access the class body with target block as 14 | # presented in one of the examples 15 | # @note Internally it uses the Aspector method from this module 16 | # @note It will create a single aspect class that will be applied to all the classes 17 | # @example Bind aspector to TestClass and DummyClass on a :run method 18 | # aspector(TestClass, DummyClass) do 19 | # before :run do 20 | # puts "This is it!" 21 | # end 22 | # end 23 | # 24 | # @example Bind aspector and add an extra method to the classes by using target block 25 | # aspector(TestClass, DummyClass) do 26 | # target do 27 | # def :run 28 | # end 29 | # end 30 | # 31 | # before :run do 32 | # puts "This is it!" 33 | # end 34 | # end 35 | # 36 | def aspector(*args, &block) 37 | options = args.last.is_a?(Hash) ? args.pop : {} 38 | 39 | aspect = Aspector(options, &block) 40 | 41 | aspect.apply(self) if self.is_a? Module 42 | args.each { |target| aspect.apply(target) } 43 | 44 | aspect 45 | end 46 | 47 | # Method that "imitates" a class like behaviour for building aspects 48 | # Pretty usefull when we want to define aspects that will be applied to multiple targets 49 | # @see /examples/aspector_apply_example.rb 50 | # @param options [Hash] hash with additional options for the aspect (if any) 51 | # @param block [Proc] block with the aspect code 52 | # @return [Aspector::Base] aspector base anonymous descendant class 53 | # @note You can also access the class body with target block as 54 | # presented in one of the examples for aspector method 55 | # @example Create an aspect that can be attached 56 | # aspect = Aspector(class_methods: true) do 57 | # before :test do 58 | # puts "This is it!" 59 | # end 60 | # end 61 | # 62 | # aspect.apply(AspectedClass) 63 | # AspectedClass.test #=> aspect will be invoked here 64 | def Aspector(options = {}, &block) 65 | klass = Class.new(Aspector::Base) 66 | klass.class_eval { default(options) } 67 | klass.class_eval(&block) if block_given? 68 | klass 69 | end 70 | end 71 | end 72 | end 73 | 74 | Object.send(:include, Aspector::Extensions::Object) 75 | -------------------------------------------------------------------------------- /lib/aspector/interception.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | module Aspector 4 | # Interception acts as a proxy between the Aspector::Base instance and all the elements to 5 | # which we want to apply to given elements 6 | # Why don't we apply it directly from the aspect instance? Well if we would do this, it would 7 | # disallow having different options in the apply() method and all the options would be the same 8 | # Using the interception in between, we can have different set of options for each target 9 | # For example we can apply the same aspect for different set of methods for each target, etc 10 | class Interception 11 | extend Forwardable 12 | using Refinements::Class 13 | 14 | def_delegator :aspect, :enabled? 15 | 16 | attr_reader :aspect, :target, :options 17 | 18 | # @param aspect [Aspector::Base] aspector instance that owns this interception 19 | # @param target [Class, Module] element on which we will apply this interception 20 | # @param options [Hash] hash with options for this interception 21 | # @return [Aspector::Interception] interception instance 22 | def initialize(aspect, target, options) 23 | @aspect = aspect 24 | @target = target 25 | @options = options 26 | @wrapped_methods = {} 27 | end 28 | 29 | # @return [Array] advices that should be applied by this inteception 30 | # @note All advices are defined on an aspect class level 31 | def advices 32 | aspect.class.storage.advices 33 | end 34 | 35 | # @return [Aspector::Logger] logger instance 36 | def logger 37 | @logger ||= Logging.build(self) 38 | end 39 | 40 | # Will apply all the advices to a given target based on provided options 41 | def apply 42 | invoke_deferred_logics 43 | return if advices.empty? 44 | 45 | define_methods_for_advice_blocks 46 | add_to_instances unless @options[:existing_methods_only] 47 | apply_to_methods unless @options[:new_methods_only] 48 | add_method_hooks unless @options[:existing_methods_only] 49 | # TODO: clear deferred logic results if they are not used in any advice 50 | self 51 | end 52 | 53 | # Applies advices to a given method 54 | # @note This method is public only because we use it from define_method on a module 55 | # @note If will figure out the method scope (private/public/protected) on its own 56 | # @param method [Symbol] method to which we want to apply this interception 57 | def apply_to_method(method) 58 | filtered_advices = filter_advices advices, method 59 | return if filtered_advices.empty? 60 | 61 | logger.debug 'apply-to-method', method 62 | 63 | scope ||= context.instance_method_type(method) 64 | 65 | recreate_method method, filtered_advices, scope 66 | end 67 | 68 | private 69 | 70 | # Defines on a target element new methods that will contain advices logic 71 | # as long as their blocks. Then we can invoke advices logic as a normal methods 72 | # In a way it just casts a block to methods for peformance reasons 73 | # If we have advices that just execute already existing methods, this won't create 74 | # anything 75 | # @note All methods like this are set to private - they should stay as an internal 76 | # implementation detail 77 | def define_methods_for_advice_blocks 78 | advices.each do |advice| 79 | next if advice.raw? 80 | next unless advice.advice_block 81 | context.send :define_method, advice.with_method, advice.advice_block 82 | context.send :private, advice.with_method 83 | end 84 | end 85 | 86 | # context is where advices will be applied (i.e. where methods are modified), 87 | # can be different from target because when the target is an instance and 88 | # we want to apply to instance methods, we need to use element singleton_class 89 | # @return [Class] context on which we will apply advices 90 | def context 91 | return @target if @target.is_a?(Module) && !@options[:class_methods] 92 | 93 | @target.singleton_class 94 | end 95 | 96 | # Will apply all the advices to all methods that match 97 | def apply_to_methods 98 | # If method/methods option is set and all are String or Symbol, apply to those only, instead of 99 | # iterating through all methods 100 | methods = [@options[:method] || @options[:methods]] 101 | methods.compact! 102 | methods.flatten! 103 | 104 | if !methods.empty? && methods.all?{ |method| method.is_a?(String) || method.is_a?(Symbol) } 105 | methods.each do |method| 106 | apply_to_method(method.to_s) 107 | end 108 | 109 | return 110 | end 111 | 112 | context.public_instance_methods.each do |method| 113 | apply_to_method(method.to_s) 114 | end 115 | 116 | context.protected_instance_methods.each do |method| 117 | apply_to_method(method.to_s) 118 | end 119 | 120 | if @options[:private_methods] 121 | context.private_instance_methods.each do |method| 122 | apply_to_method(method.to_s) 123 | end 124 | end 125 | end 126 | 127 | # @param logic Deferred logic for which we want to get the results 128 | # @return Deferred logic invokation results 129 | def deferred_logic_results(logic) 130 | @deferred_logic_results[logic] 131 | end 132 | 133 | # @param method [String] method name for which we want to get the original method 134 | # @return [UnboundMethod] original method that we wrapped around 135 | def get_wrapped_method_of(method) 136 | @wrapped_methods[method] 137 | end 138 | 139 | # Invokes deferred logics blocks on a target element and stores deferred logic 140 | # invokations results 141 | def invoke_deferred_logics 142 | logics = @aspect.class.storage.deferred_logics 143 | return if logics.empty? 144 | 145 | logics.each do |logic| 146 | result = logic.apply context, aspect 147 | if advices.detect { |advice| advice.use_deferred_logic? logic } 148 | @deferred_logic_results ||= {} 149 | @deferred_logic_results[logic] = result 150 | end 151 | end 152 | end 153 | 154 | # Saves references to interceptions on a given target (its context) level 155 | # The reference is stored there only for advices that are not being applied 156 | # for existing methods only. The storage is used to remember interceptions 157 | # that should be applied for methods that were defined after the aspect 158 | # was applied 159 | def add_to_instances 160 | # Store only those interceptions that are not marked to be used for existing methods only 161 | return if options[:existing_methods_only] 162 | 163 | interceptions_storage = context.instance_variable_get(:@interceptions_storage) 164 | unless interceptions_storage 165 | interceptions_storage = InterceptionsStorage.new 166 | context.instance_variable_set(:@interceptions_storage, interceptions_storage) 167 | end 168 | interceptions_storage << self 169 | end 170 | 171 | # Redefines singleton_method_added and method_added methods so they are monitored 172 | # If new method is added we will apply to it appropriate advices 173 | def add_method_hooks 174 | eigen_class = @target.singleton_class 175 | 176 | if @options[:class_methods] 177 | return unless @target.is_a?(Module) 178 | 179 | orig_singleton_method_added = @target.method(:singleton_method_added) 180 | 181 | eigen_class.send :define_method, :singleton_method_added do |method| 182 | aspector_singleton_method_added(method) 183 | orig_singleton_method_added.call(method) 184 | end 185 | else 186 | if @target.is_a? Module 187 | orig_method_added = @target.method(:method_added) 188 | else 189 | orig_method_added = eigen_class.method(:method_added) 190 | end 191 | 192 | eigen_class.send :define_method, :method_added do |method| 193 | aspector_instance_method_added(method) 194 | orig_method_added.call(method) 195 | end 196 | end 197 | end 198 | 199 | # Picks only advices that should be applied on a given method 200 | # @param advices [Array] all the advices that we want to filter 201 | # @param method [String] method name for which we want to pick proper advices 202 | # @return [Array] advices that match given method 203 | def filter_advices(advices, method) 204 | advices.select do |advice| 205 | advice.match?(method, self) 206 | end 207 | end 208 | 209 | # Recreates a given method applying all the advices one by one 210 | # @param method [String] method name of a method that we want to recreate 211 | # @param advices [Array] all the advices that 212 | # should be applied (after filtering) 213 | # @param scope [Symbol] method visibility (private, protected, public) 214 | def recreate_method(method, advices, scope) 215 | context.instance_variable_set(:@aspector_creating_method, true) 216 | 217 | raw_advices = advices.select(&:raw?) 218 | 219 | if raw_advices.size > 0 220 | raw_advices.each do |advice| 221 | if @target.is_a?(Module) && !@options[:class_methods] 222 | @target.class_exec method, self, &advice.advice_block 223 | else 224 | @target.instance_exec method, self, &advice.advice_block 225 | end 226 | end 227 | 228 | return if raw_advices.size == advices.size 229 | end 230 | 231 | begin 232 | @wrapped_methods[method] = context.instance_method(method) 233 | rescue 234 | # ignore undefined method error 235 | if @options[:existing_methods_only] 236 | logger.log Logging::WARN, 'method-not-found', method 237 | end 238 | 239 | return 240 | end 241 | 242 | before_advices = advices.select(&:before?) + advices.select(&:before_filter?) 243 | after_advices = advices.select(&:after?) 244 | around_advices = advices.select(&:around?) 245 | 246 | (around_advices.size - 1).downto(1) do |i| 247 | advice = around_advices[i] 248 | recreate_method_with_advices method, [], [], advice 249 | end 250 | 251 | recreate_method_with_advices( 252 | method, 253 | before_advices, 254 | after_advices, 255 | around_advices.first 256 | ) 257 | 258 | context.send scope, method if scope != :public 259 | ensure 260 | context.send :remove_instance_variable, :@aspector_creating_method 261 | end 262 | 263 | # Recreates method with given advices. It applies the MethodTemplate::TEMPLATE 264 | # @param method [String] method name of a method that we want to recreate 265 | # @param before_advices [Array] before advices that should be applied 266 | # @param after_advices [Array] after advices that should be applied 267 | # @param around_advice [Aspector::Advice] single around advice 268 | # @note We can recreate method only with single around advice at once - so we loop if we have 269 | # more than a single around advice 270 | def recreate_method_with_advices( 271 | method, 272 | before_advices, 273 | after_advices, 274 | around_advice 275 | ) 276 | aspect = @aspect 277 | logger = @logger 278 | interception = self 279 | orig_method = get_wrapped_method_of method 280 | 281 | code = MethodTemplate::TEMPLATE.result(binding) 282 | logger.debug 'generate-code', method, code 283 | context.class_eval code, __FILE__, __LINE__ + 4 284 | end 285 | end 286 | end 287 | -------------------------------------------------------------------------------- /lib/aspector/interceptions_storage.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Storage for interceptions on an adviced class level 3 | # We need to store them on an aspected object in case we want to apply aspects on newly 4 | # defined (after the aspect applience) methods for objects 5 | # @note We should store here only interceptions that have existing_methods_only set to false/nil 6 | class InterceptionsStorage < Array 7 | # Applies all the interceptions to a given method 8 | # @param method [String] name of method to which we want to apply 9 | def apply_to_method(method) 10 | each do |interception| 11 | interception.apply_to_method method 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/aspector/logger.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Default logger for Aspector gem 3 | # @note String joining and logging can be resource consuming, so watch out what you log, 4 | # especially when not in debug mode 5 | class Logger < ::Logger 6 | attr_reader :context 7 | 8 | # @param context [Aspector::Base, Class] context in which we want to log information 9 | # @note it will log more details if it is an Aspector internal context 10 | # @note It will try to detect log level based on ASPECTOR_LOG_LEVEL and will fallback to 11 | # ::Logger::ERROR level it if fails 12 | # @return [Aspector::Logger] logger instance 13 | def initialize(context) 14 | super(STDOUT) 15 | @context = context 16 | @level = (ENV['ASPECTOR_LOG_LEVEL'] || ::Logger::ERROR).to_i 17 | end 18 | 19 | # Sets up all the logging methods 20 | %i( debug info warn error fatal ).each do |level| 21 | define_method level do |*args| 22 | # We pass it as a block, so it won't be evaluated unless we really want to log it 23 | # If we would pass it as a method invocation (not as a block), it would evaluate it 24 | # even if we would not log it afterwards because the log level doesnt match. 25 | super(nil) { message(*args) } 26 | end 27 | end 28 | 29 | private 30 | 31 | # Creates a full messages that we want to log 32 | # @param args any arguments that we want to log based on 33 | # @return [String] message string for logging - provides additional context information 34 | # @example Create a message based on a single argument 35 | # message('action taken') #=> 'Aspector::Base | ExampleClass | action taken' 36 | def message(*args) 37 | msg = [] 38 | 39 | if context.is_a? Aspector::Base 40 | msg << context.class.to_s 41 | msg << context.target.to_s 42 | else 43 | msg << context.to_s 44 | end 45 | 46 | msg += args 47 | 48 | msg.join(' | ') 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/aspector/logging.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Class used as a wrapper to get logging instances 3 | # It provides logger detection (from ENV) and creates a logger instance with a proper context 4 | module Logging 5 | class << self 6 | # @param context [Aspector::Base, Class] context in which we want to log information 7 | # @note it will log more details if it is an Aspector internal context 8 | # @return [Aspector::Logger, Logger] aspector logger that logs in a given context 9 | # or provided logger 10 | # @example Create a logger instance inside Aspector::Base 11 | # @logger ||= Aspector::Logging.build(self) 12 | def build(context) 13 | (deconstantize(ENV['ASPECTOR_LOGGER'] || 'Aspector::Logger')).new(context) 14 | end 15 | 16 | private 17 | 18 | # @param klass_name [String, Symbol] stringified class/module name that we want to convert 19 | # to a real working class/module instance 20 | # @return [Class] class/module that was fetched from its string version 21 | # @note If it cannot detect a valid class based on the name, 22 | # it will return an Aspector::Logger instance as a fallback 23 | # @example Get a class DummyClass based on its 'DummyClass' string 24 | # deconstantize('DummyClass') #=> 'DummyClass' 25 | # @example Get a namespaced class reference 26 | # deconstantize('Users::UserRole') #=> User::UserRole 27 | # @example Try to get class that doesn't exist 28 | # deconstantize('NonExistingClass') #=> Aspector::Logger 29 | def deconstantize(klass_name) 30 | Object.const_get(klass_name.to_s) 31 | rescue NameError 32 | $stderr.puts "#{klass_name} is not a valid constant name!" 33 | Aspector::Logger 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/aspector/method_template.rb: -------------------------------------------------------------------------------- 1 | # We ignore this because this module contains a method template and it won't be shorter 2 | # rubocop:disable ModuleLength 3 | 4 | module Aspector 5 | # ERB template that will be used to define the new version of method to which we will bind 6 | # with the aspect interception. 7 | # @note We use it with class_eval and method definitions instead of prepend because of 8 | # performance reasons 9 | # @note Keep in mind that lines that start with % are evaluated in the interception context 10 | module MethodTemplate 11 | # ERB template for method recreation 12 | TEMPLATE = ERB.new <<-CODE, nil, '%<>' 13 | % if around_advice 14 | wrapped_method = instance_method(:<%= method %>) 15 | % end 16 | 17 | define_method :<%= method %> do |*args, &block| 18 | % if logger.debug? 19 | logger.debug '<%= method %>', 'enter-generated-method' 20 | % end 21 | 22 | unless aspect.enabled? 23 | % if logger.debug? 24 | logger.debug '<%= method %>', 'exit--generated-method' 25 | % end 26 | 27 | return orig_method.bind(self).call(*args, &block) 28 | end 29 | 30 | % before_advices.each do |advice| 31 | % if logger.debug? 32 | logger.debug '<%= method %>', 'before-invoke-advice', '<%= advice.name %>' 33 | % end 34 | 35 | % if advice.advice_code 36 | result = (<%= advice.advice_code %>) 37 | % else 38 | result = <%= advice.with_method %> <% 39 | if advice.options[:interception_arg] %>interception, <% end %><% 40 | if advice.options[:method_arg] %>'<%= method %>', <% end 41 | %>*args 42 | % end 43 | 44 | % if logger.debug? 45 | logger.debug '<%= method %>', 'after--invoke-advice', '<%= advice.name %>' 46 | % end 47 | 48 | % if advice.before_filter? 49 | unless result 50 | % if logger.debug? 51 | logger.debug '<%= method %>', 'exit-due-to-before-filter', '<%= advice.name %>' 52 | % end 53 | 54 | return 55 | end 56 | % end 57 | % end 58 | 59 | % if around_advice 60 | % if logger.debug? 61 | logger.debug '<%= method %>', 'before-invoke-advice', '<%= around_advice.name %>' 62 | % end 63 | 64 | % if around_advice.advice_code 65 | result = ( 66 | <%= 67 | around_advice 68 | .advice_code 69 | .gsub( 70 | 'INVOKE_PROXY', 71 | 'wrapped_method.bind(self).call(*args, &block)' 72 | ) 73 | %> 74 | ) 75 | % else 76 | % if logger.debug? 77 | proxy = lambda do |*args, &block| 78 | logger.debug '<%= method %>', 'before-invoke-proxy' 79 | res = wrapped_method.bind(self).call *args, &block 80 | logger.debug '<%= method %>', 'after--invoke-proxy' 81 | res 82 | end 83 | result = <%= around_advice.with_method %> <% 84 | if around_advice.options[:interception_arg] %>interception, <% end %><% 85 | if around_advice.options[:method_arg] %>'<%= method %>', <% end 86 | %>proxy, *args, &block 87 | % else 88 | result = <%= around_advice.with_method %> <% 89 | if around_advice.options[:interception_arg] %>interception, <% end %><% 90 | if around_advice.options[:method_arg] %>'<%= method %>', <% end 91 | %>wrapped_method.bind(self), *args, &block 92 | % end 93 | % end 94 | 95 | % if logger.debug? 96 | logger.debug '<%= method %>', 'after--invoke-advice', '<%= around_advice.name %>' 97 | % end 98 | % else 99 | # Invoke original method 100 | % if logger.debug? 101 | logger.debug '<%= method %>', 'before-wrapped-method' 102 | % end 103 | 104 | result = orig_method.bind(self).call *args, &block 105 | % if logger.debug? 106 | logger.debug '<%= method %>', 'after--wrapped-method' 107 | % end 108 | % end 109 | 110 | % unless after_advices.empty? 111 | % after_advices.each do |advice| 112 | % if logger.debug? 113 | logger.debug '<%= method %>', 'before-invoke-advice', '<%= advice.name %>' 114 | % end 115 | 116 | % if advice.advice_code 117 | result = (<%= advice.advice_code %>) 118 | % else 119 | % if advice.options[:result_arg] 120 | result = <%= advice.with_method %> <% 121 | if advice.options[:interception_arg] %>interception, <% end %><% 122 | if advice.options[:method_arg] %>'<%= method %>', <% end %><% 123 | if advice.options[:result_arg] %>result, <% end 124 | %>*args 125 | % else 126 | <%= advice.with_method %> <% 127 | if advice.options[:interception_arg] %>interception, <% end %><% 128 | if advice.options[:method_arg] %>'<%= method %>', <% end 129 | %>*args 130 | % end 131 | % end 132 | 133 | % if logger.debug? 134 | logger.debug '<%= method %>', 'after--invoke-advice', '<%= advice.name %>' 135 | % end 136 | % end 137 | % end 138 | 139 | % if logger.debug? 140 | logger.debug '<%= method %>', 'exit--generated-method' 141 | % end 142 | 143 | result 144 | end 145 | CODE 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/aspector/refinements/class.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Refinements that are used inside Aspector 3 | module Refinements 4 | # Class refinements used inside Aspector 5 | # They allow us to check what type of instance method (public, protected, private) it is 6 | module Class 7 | refine ::Class do 8 | # @param [String, Symbol] method_name that we want to check 9 | # @return [Boolean] true if there's a method with provided method_name and this 10 | # method is private. Otherwise false 11 | # @example Check if a given instance method is private 12 | # DummyClass.instance_method_private?(:run) #=> true 13 | def instance_method_private?(method_name) 14 | private_instance_methods.include?(method_name.to_sym) 15 | end 16 | 17 | # @param [String, Symbol] method_name that we want to check 18 | # @return [Boolean] true if there's a method with provided method_name and this 19 | # method is protected. Otherwise false 20 | # @example Check if a given instance method is protected 21 | # DummyClass.instance_method_protected?(:run) #=> true 22 | def instance_method_protected?(method_name) 23 | protected_instance_methods.include?(method_name.to_sym) 24 | end 25 | 26 | # @param [String, Symbol] method_name that we want to check 27 | # @return [Boolean] true if there's a method with provided method_name and this 28 | # method is public. Otherwise false 29 | # @example Check if a given instance method is public 30 | # DummyClass.instance_method_public?(:run) #=> true 31 | def instance_method_public?(method_name) 32 | public_instance_methods.include?(method_name.to_sym) 33 | end 34 | 35 | # @param [String, Symbol] method_name that we want to check 36 | # @return [Symbol] what type of method it is (private, protected, public) 37 | # @note If this method is non of types we assume that it is public, so method_missing 38 | # will work without any issues (or any other Ruby magic) 39 | # @example Get a given instance method type 40 | # DummyClass.instance_method_type(:run) #=> :protected 41 | # DummyClass.instance_method_type(:run_now) #=> :private 42 | # DummyClass.instance_method_type(:run_scheduled) #=> :public 43 | def instance_method_type(method_name) 44 | return :private if instance_method_private?(method_name) 45 | return :protected if instance_method_protected?(method_name) 46 | return :public if instance_method_public?(method_name) 47 | 48 | :public 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/aspector/refinements/string.rb: -------------------------------------------------------------------------------- 1 | module Aspector 2 | # Refinements that are used inside Aspector 3 | module Refinements 4 | # String refinements used inside Aspector 5 | module String 6 | # Regexp that is used to sanitize string, so it can be used as a instance variable name 7 | VARIABLE_REGEXP = %r{[?!=+\-\*/\^\|&\[\]<>%~]} 8 | 9 | refine ::String do 10 | # Converts a given string to a string that can be used as an instance variable 11 | # @return [String] converted string that can be used as an instance variable 12 | # @example Convert 'not a variable' to a variable string 13 | # 'not a variable'.to_instance_variable_name! #=> '@not_a_variable' 14 | def to_instance_variable_name! 15 | gsub! VARIABLE_REGEXP, '_' 16 | replace "@#{self}" 17 | end 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/aspector/version.rb: -------------------------------------------------------------------------------- 1 | # Aspector namespace module 2 | module Aspector 3 | # Current Aspector gem version 4 | VERSION = '0.15.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/benchmarks_spec.rb: -------------------------------------------------------------------------------- 1 | # This spec will execute all the examples to check if they are working 2 | # By default all of them should work just fine 3 | # Note that it won't check if the aspector works in examples as it should 4 | # This is covered in unit and functional specs - here we check that all examples run 5 | # without problems 6 | 7 | require 'spec_helper' 8 | require 'stringio' 9 | 10 | # Kernel 11 | module Kernel 12 | # Method used to catch the STDIO from all the examples 13 | # Examples by default print some stuff to the output - we don't want that 14 | # in our specs, thats why we catch it 15 | # We're only interested if all examples work as they should (without any errors) 16 | def capture_stdout 17 | out = StringIO.new 18 | $stdout = out 19 | yield 20 | out 21 | ensure 22 | $stdout = STDOUT 23 | end 24 | 25 | # Same as capture_stdout but to silence stderr 26 | def capture_stderr 27 | out = StringIO.new 28 | $stderr = out 29 | yield 30 | out 31 | ensure 32 | $stderr = STDOUT 33 | end 34 | end 35 | 36 | RSpec.describe 'Aspector benchmarks' do 37 | Dir.glob( 38 | File.join( 39 | File.dirname(__FILE__), 40 | '..', 41 | 'benchmarks/**/*.rb' 42 | ) 43 | ).each do |example_file| 44 | context "benchmark file: #{example_file}" do 45 | it 'should run without any errors' do 46 | capture_stdout do 47 | capture_stderr do 48 | Process.fork do 49 | require example_file 50 | end 51 | end 52 | end 53 | 54 | _pid, status = Process.wait2 55 | expect(status.exitstatus).to eq 0 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/examples_spec.rb: -------------------------------------------------------------------------------- 1 | # This spec will execute all the examples to check if they are working 2 | # By default all of them should work just fine 3 | # Note that it won't check if the aspector works in examples as it should 4 | # This is covered in unit and functional specs - here we check that all examples run 5 | # without problems 6 | 7 | require 'spec_helper' 8 | require 'stringio' 9 | 10 | # Kernel 11 | module Kernel 12 | # Method used to catch the STDIO from all the examples 13 | # Examples by default print some stuff to the output - we don't want that 14 | # in our specs, thats why we catch it 15 | # We're only interested if all examples work as they should (without any errors) 16 | def capture_stdout 17 | out = StringIO.new 18 | $stdout = out 19 | yield 20 | out 21 | ensure 22 | $stdout = STDOUT 23 | end 24 | 25 | # Same as capture_stdout but to silence stderr 26 | def capture_stderr 27 | out = StringIO.new 28 | $stderr = out 29 | yield 30 | out 31 | ensure 32 | $stderr = STDOUT 33 | end 34 | end 35 | 36 | RSpec.describe 'Aspector examples' do 37 | Dir.glob( 38 | File.join( 39 | File.dirname(__FILE__), 40 | '..', 41 | 'examples/*.rb' 42 | ) 43 | ).each do |example_file| 44 | context "examples file: #{example_file}" do 45 | it 'should run without any errors' do 46 | capture_stdout do 47 | capture_stderr do 48 | Process.fork do 49 | require example_file 50 | end 51 | end 52 | end 53 | 54 | _pid, status = Process.wait2 55 | expect(status.exitstatus).to eq 0 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/functionals/aspect_applied_multiple_times_on_same_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector do 4 | let(:klass) do 5 | ClassBuilder.build do 6 | attr_accessor :counter 7 | 8 | def initialize 9 | @counter = 0 10 | end 11 | 12 | def exec 13 | values << 'exec' 14 | end 15 | 16 | def count 17 | self.counter += 1 18 | end 19 | end 20 | end 21 | 22 | let(:instance) { klass.new } 23 | 24 | let(:aspect_klass) do 25 | ClassBuilder.inherit(Aspector::Base) do 26 | attr_accessor :call_count 27 | 28 | def initialize 29 | @call_count = 0 30 | end 31 | 32 | before interception_arg: true do |interception| 33 | interception.aspect.call_count += 1 34 | end 35 | 36 | before :count 37 | end 38 | end 39 | 40 | let(:aspect) { aspect_klass.new } 41 | 42 | context 'binding single aspect multiple times to a single element' do 43 | let(:amount) { rand(100) } 44 | 45 | before do 46 | amount.times { aspect.apply klass, method: :exec } 47 | end 48 | 49 | it 'should apply it multiple times and wrap it' do 50 | instance.exec 51 | 52 | expect(aspect.call_count).to eq amount 53 | expect(instance.counter).to eq amount 54 | expect(instance.values).to eq %w( exec ) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/functionals/aspect_for_multiple_targets_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector do 4 | let(:klass1) do 5 | ClassBuilder.build do 6 | def exec1 7 | values << 'exec1' 8 | end 9 | end 10 | end 11 | 12 | let(:klass2) do 13 | ClassBuilder.build do 14 | def exec2 15 | values << 'exec2' 16 | end 17 | end 18 | end 19 | 20 | let(:instance1) { klass1.new } 21 | let(:instance2) { klass2.new } 22 | 23 | let(:aspect_klass) do 24 | ClassBuilder.inherit(Aspector::Base) do 25 | attr_accessor :call_count 26 | 27 | def initialize 28 | @call_count = 0 29 | end 30 | 31 | before interception_arg: true do |interception| 32 | interception.aspect.call_count += 1 33 | end 34 | end 35 | end 36 | 37 | let(:aspect) { aspect_klass.new } 38 | 39 | context 'binding single aspect to multiple different targets' do 40 | before do 41 | aspect.apply klass1, method: :exec1 42 | aspect.apply klass2, methods: %w( exec2 ) 43 | end 44 | 45 | it 'should work' do 46 | instance1.exec1 47 | instance2.exec2 48 | 49 | expect(aspect.call_count).to eq 2 50 | expect(instance1.values).to eq %w( exec1 ) 51 | expect(instance2.values).to eq %w( exec2 ) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/functionals/aspect_interception_options_accessing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Aspector options access' do 4 | subject { klass.new } 5 | let(:interception_options) { { rand => rand } } 6 | 7 | context 'accessing interception options' do 8 | let(:klass) do 9 | ClassBuilder.build do 10 | attr_accessor :interception_options 11 | end 12 | end 13 | 14 | before do 15 | aspect.apply klass, interception_options.merge(method: :exec) 16 | end 17 | 18 | context 'before aspect' do 19 | let(:aspect) do 20 | ClassBuilder.inherit(Aspector::Base) do 21 | default aspect_field: 60 22 | 23 | before interception_arg: true do |interception| 24 | self.interception_options = interception.options 25 | end 26 | end 27 | end 28 | 29 | it 'should be able to reach interception options that were merged with aspect options' do 30 | subject.exec 31 | expected = interception_options.merge(method: :exec).merge(aspect_field: 60) 32 | expect(subject.interception_options).to eq expected 33 | end 34 | 35 | it 'still should exec original method' do 36 | subject.exec 37 | expect(subject.values).to eq %w( exec-result ) 38 | end 39 | end 40 | 41 | context 'before_filter aspect' do 42 | let(:aspect) do 43 | ClassBuilder.inherit(Aspector::Base) do 44 | default aspect_field: 60 45 | 46 | before_filter interception_arg: true do |interception| 47 | self.interception_options = interception.options 48 | end 49 | end 50 | end 51 | 52 | it 'should be able to reach interception options that were merged with aspect options' do 53 | subject.exec 54 | expected = interception_options.merge(method: :exec).merge(aspect_field: 60) 55 | expect(subject.interception_options).to eq expected 56 | end 57 | 58 | it 'still should exec original method' do 59 | subject.exec 60 | expect(subject.values).to eq %w( exec-result ) 61 | end 62 | end 63 | 64 | context 'around aspect' do 65 | let(:aspect) do 66 | ClassBuilder.inherit(Aspector::Base) do 67 | default aspect_field: 60 68 | 69 | around interception_arg: true do |interception, proxy, &block| 70 | self.interception_options = interception.options 71 | proxy.call(&block) 72 | end 73 | end 74 | end 75 | 76 | it 'should be able to reach interception options that were merged with aspect options' do 77 | subject.exec 78 | expected = interception_options.merge(method: :exec).merge(aspect_field: 60) 79 | expect(subject.interception_options).to eq expected 80 | end 81 | 82 | it 'still should exec original method' do 83 | subject.exec 84 | expect(subject.values).to eq %w( exec-result ) 85 | end 86 | end 87 | 88 | context 'after aspect' do 89 | let(:aspect) do 90 | ClassBuilder.inherit(Aspector::Base) do 91 | default aspect_field: 60 92 | 93 | after interception_arg: true do |interception, result| 94 | self.interception_options = interception.options 95 | result 96 | end 97 | end 98 | end 99 | 100 | it 'should be able to reach interception options that were merged with aspect options' do 101 | subject.exec 102 | expected = interception_options.merge(method: :exec).merge(aspect_field: 60) 103 | expect(subject.interception_options).to eq expected 104 | end 105 | 106 | it 'still should exec original method' do 107 | subject.exec 108 | expect(subject.values).to eq %w( exec-result ) 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/functionals/aspect_on_a_class_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Aspector for a class' do 4 | context 'public class methods' do 5 | subject do 6 | ClassBuilder.build do 7 | class << self 8 | def values 9 | @values ||= [] 10 | end 11 | 12 | def exec 13 | values << 'class-exec-result' 14 | end 15 | end 16 | end 17 | end 18 | 19 | context 'before aspect' do 20 | before do 21 | aspector(subject, class_methods: true) do 22 | before :exec do 23 | values << 'class-exec-before' 24 | end 25 | end 26 | end 27 | 28 | it 'should work' do 29 | subject.exec 30 | expect(subject.values).to eq %w( class-exec-before class-exec-result ) 31 | end 32 | end 33 | 34 | context 'around aspect' do 35 | before do 36 | aspector(subject, class_methods: true) do 37 | around :exec do |proxy, &block| 38 | values << 'class-exec-around-before' 39 | result = proxy.call(&block) 40 | values << 'class-exec-around-after' 41 | result 42 | end 43 | end 44 | end 45 | 46 | it 'should work' do 47 | expected = %w( class-exec-around-before class-exec-result class-exec-around-after ) 48 | subject.exec 49 | expect(subject.values).to eq expected 50 | end 51 | end 52 | 53 | context 'after aspect' do 54 | before do 55 | aspector(subject, class_methods: true) do 56 | after :exec do |result| 57 | values << 'class-exec-after' 58 | result 59 | end 60 | end 61 | end 62 | 63 | it 'should work' do 64 | subject.exec 65 | expect(subject.values).to eq %w( class-exec-result class-exec-after ) 66 | end 67 | end 68 | end 69 | 70 | context 'private class methods' do 71 | subject do 72 | ClassBuilder.build do 73 | class << self 74 | def values 75 | @values ||= [] 76 | end 77 | 78 | private 79 | 80 | def exec 81 | values << 'class-exec-result' 82 | end 83 | end 84 | end 85 | end 86 | 87 | context 'before aspect' do 88 | before do 89 | aspector(subject, class_methods: true) do 90 | before :exec do 91 | values << 'class-exec-before(public_methods_only)' 92 | end 93 | end 94 | 95 | aspector(subject, class_methods: true, private_methods: true) do 96 | before :exec do 97 | values << 'class-exec-before' 98 | end 99 | end 100 | end 101 | 102 | it 'should work' do 103 | subject.send(:exec) 104 | expect(subject.values).to eq %w( class-exec-before class-exec-result ) 105 | end 106 | end 107 | 108 | context 'around aspect' do 109 | before do 110 | aspector(subject, class_methods: true) do 111 | around :exec do |proxy, &block| 112 | values << 'class-exec-around-before(public_methods_only)' 113 | result = proxy.call(&block) 114 | values << 'class-exec-around-after(public_methods_only)' 115 | result 116 | end 117 | end 118 | 119 | aspector(subject, class_methods: true, private_methods: true) do 120 | around :exec do |proxy, &block| 121 | values << 'class-exec-around-before' 122 | result = proxy.call(&block) 123 | values << 'class-exec-around-after' 124 | result 125 | end 126 | end 127 | end 128 | 129 | it 'should work' do 130 | expected = %w( class-exec-around-before class-exec-result class-exec-around-after ) 131 | subject.send(:exec) 132 | expect(subject.values).to eq expected 133 | end 134 | end 135 | 136 | context 'after aspect' do 137 | before do 138 | aspector(subject, class_methods: true) do 139 | after :exec do |result| 140 | values << 'class-exec-after(public_methods_only)' 141 | result 142 | end 143 | end 144 | 145 | aspector(subject, class_methods: true, private_methods: true) do 146 | after :exec do |result| 147 | values << 'class-exec-after' 148 | result 149 | end 150 | end 151 | end 152 | 153 | it 'should work' do 154 | subject.send(:exec) 155 | expect(subject.values).to eq %w( class-exec-result class-exec-after ) 156 | end 157 | end 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /spec/functionals/aspect_on_an_instance_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Advices on private methods' do 4 | subject { klass.new } 5 | 6 | let(:klass) do 7 | ClassBuilder.build 8 | end 9 | 10 | let(:instance1) { klass.new } 11 | let(:instance2) { klass.new } 12 | 13 | context 'before aspect' do 14 | before do 15 | aspector(instance1) do 16 | before :exec do 17 | values << 'exec-before' 18 | end 19 | end 20 | end 21 | 22 | it 'should bind only to one instance to which we want to bind' do 23 | instance1.exec 24 | instance2.exec 25 | expect(instance1.values).to eq %w( exec-before exec-result ) 26 | expect(instance2.values).to eq %w( exec-result ) 27 | end 28 | end 29 | 30 | context 'around aspect' do 31 | before do 32 | aspector(instance1) do 33 | around :exec do |proxy, &block| 34 | values << 'exec-around-before' 35 | result = proxy.call(&block) 36 | values << 'exec-around-after' 37 | result 38 | end 39 | end 40 | end 41 | 42 | it 'should bind only to one instance to which we want to bind' do 43 | instance1.exec 44 | instance2.exec 45 | expect(instance1.values).to eq %w( exec-around-before exec-result exec-around-after ) 46 | expect(instance2.values).to eq %w( exec-result ) 47 | end 48 | end 49 | 50 | context 'after aspect' do 51 | before do 52 | aspector(instance1) do 53 | after :exec do |_result| 54 | values << 'exec-after' 55 | end 56 | end 57 | end 58 | 59 | it 'should bind only the aspect that binds to private methods' do 60 | instance1.exec 61 | instance2.exec 62 | expect(instance1.values).to eq %w( exec-result exec-after ) 63 | expect(instance2.values).to eq %w( exec-result ) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/functionals/aspector_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector do 4 | let(:klass) { ClassBuilder.build } 5 | subject { klass.new } 6 | 7 | context 'binding multiple aspects in a single place' do 8 | before do 9 | aspector(klass) do 10 | before(:exec) { values << 'first-aspect' } 11 | end 12 | 13 | aspector(klass) do 14 | before(:exec) { values << 'second-aspect' } 15 | end 16 | end 17 | 18 | it 'should work' do 19 | subject.exec 20 | expect(subject.values).to eq %w( second-aspect first-aspect exec-result ) 21 | end 22 | end 23 | 24 | context 'when we try to treat Aspect as a regular class' do 25 | let(:aspect_klass) do 26 | ClassBuilder.inherit(Aspector::Base) do 27 | before(:exec) { values << 'before-exec' } 28 | end 29 | end 30 | 31 | before do 32 | aspect_klass.apply(klass) 33 | end 34 | 35 | it 'should work' do 36 | subject.exec 37 | expect(subject.values).to eq %w( before-exec exec-result ) 38 | end 39 | end 40 | 41 | context 'when we want to apply aspect multiple times' do 42 | let(:aspect_klass) do 43 | Aspector do 44 | before(:exec) { values << 'before-exec' } 45 | end 46 | end 47 | 48 | before do 49 | aspect_klass.apply(klass) 50 | aspect_klass.apply(klass) 51 | end 52 | 53 | it 'should apply aspect multiple times' do 54 | subject.exec 55 | expect(subject.values).to eq %w( before-exec before-exec exec-result ) 56 | end 57 | end 58 | 59 | context 'when we set new_methods_only to true' do 60 | let(:aspect_klass) do 61 | Aspector do 62 | before(:exec) { values << 'before-exec' } 63 | end 64 | end 65 | 66 | before do 67 | aspect_klass.apply(klass, new_methods_only: true) 68 | end 69 | 70 | context 'and we try to apply it to already existing method' do 71 | it 'should not apply to existing methods' do 72 | subject.exec 73 | expect(subject.values).to eq %w( exec-result ) 74 | end 75 | end 76 | 77 | context 'and we try to apply it to a new method' do 78 | let(:klass) do 79 | ClassBuilder.raw do 80 | def values 81 | @values ||= [] 82 | end 83 | end 84 | end 85 | 86 | before do 87 | klass.send :define_method, :exec do 88 | values << 'exec-result' 89 | end 90 | end 91 | 92 | it 'should apply to new methods' do 93 | subject.exec 94 | expect(subject.values).to eq %w( before-exec exec-result ) 95 | end 96 | end 97 | end 98 | 99 | context 'when we set existing_methods_only to true' do 100 | let(:aspect_klass) do 101 | Aspector do 102 | before(:exec) { values << 'before-exec' } 103 | end 104 | end 105 | 106 | before do 107 | aspect_klass.apply(klass, existing_methods_only: true) 108 | end 109 | 110 | context 'and we try to apply it to already existing method' do 111 | it 'should apply to existing methods' do 112 | subject.exec 113 | expect(subject.values).to eq %w( before-exec exec-result ) 114 | end 115 | end 116 | 117 | context 'and we try to apply it to a new method' do 118 | let(:klass) do 119 | ClassBuilder.raw do 120 | def values 121 | @values ||= [] 122 | end 123 | end 124 | end 125 | 126 | before do 127 | klass.send :define_method, :exec do 128 | values << 'exec-result' 129 | end 130 | end 131 | 132 | it 'should not apply to new methods' do 133 | subject.exec 134 | expect(subject.values).to eq %w( exec-result ) 135 | end 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /spec/functionals/aspects_combined_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Aspects combined' do 4 | let(:klass) { ClassBuilder.build } 5 | subject { klass.new } 6 | 7 | context 'when we want to combine multiple different aspects' do 8 | before do 9 | aspector(klass) do 10 | before :exec do 11 | values << 'exec-before' 12 | end 13 | 14 | after :exec do |result| 15 | values << 'exec-after' 16 | result 17 | end 18 | 19 | around :exec do |proxy, &block| 20 | values << 'exec-around-before' 21 | result = proxy.call(&block) 22 | values << 'exec-around-after' 23 | result 24 | end 25 | end 26 | end 27 | 28 | it 'should work' do 29 | expected = %w( 30 | exec-before exec-around-before exec-result exec-around-after exec-after 31 | ) 32 | 33 | subject.exec 34 | expect(subject.values).to eq expected 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/functionals/aspects_execution_order_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Aspect execution order' do 4 | let(:klass) { ClassBuilder.build } 5 | subject { klass.new } 6 | 7 | context 'when we apply aspects in certain order' do 8 | before do 9 | aspector(klass) do 10 | before :exec do 11 | values << 'exec-before1' 12 | end 13 | 14 | after :exec do |result| 15 | values << 'exec-after1' 16 | result 17 | end 18 | 19 | around :exec do |proxy, &block| 20 | values << 'exec-around-before1' 21 | result = proxy.call(&block) 22 | values << 'exec-around-after1' 23 | result 24 | end 25 | 26 | before :exec do 27 | values << 'exec-before2' 28 | end 29 | 30 | after :exec do |result| 31 | values << 'exec-after2' 32 | result 33 | end 34 | 35 | around :exec do |proxy, &block| 36 | values << 'exec-around-before2' 37 | result = proxy.call(&block) 38 | values << 'exec-around-after2' 39 | result 40 | end 41 | end 42 | end 43 | 44 | it 'should use this order' do 45 | expected = %w( 46 | exec-before1 47 | exec-before2 48 | exec-around-before1 49 | exec-around-before2 50 | exec-result 51 | exec-around-after2 52 | exec-around-after1 53 | exec-after1 54 | exec-after2 55 | ) 56 | 57 | subject.exec 58 | expect(subject.values).to eq expected 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/functionals/aspects_on_private_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Aspects on private methods' do 4 | subject { klass.new } 5 | 6 | let(:klass) do 7 | ClassBuilder.build do 8 | private :exec 9 | end 10 | end 11 | 12 | context 'before aspect' do 13 | before do 14 | aspector(klass) do 15 | before :exec do 16 | values << 'exec-before(public_methods_only)' 17 | end 18 | end 19 | 20 | aspector(klass, private_methods: true) do 21 | before :exec do 22 | values << 'exec-before' 23 | end 24 | end 25 | end 26 | 27 | it 'should bind only the aspect that binds to private methods' do 28 | subject.send :exec 29 | expect(subject.values).to eq %w( exec-before exec-result ) 30 | end 31 | end 32 | 33 | context 'around aspect' do 34 | before do 35 | aspector(klass) do 36 | around :exec do |proxy, &block| 37 | values << 'exec-around-before(public_methods_only)' 38 | result = proxy.call(&block) 39 | values << 'exec-around-after(public_methods_only)' 40 | result 41 | end 42 | end 43 | 44 | aspector(klass, private_methods: true) do 45 | around :exec do |proxy, &block| 46 | values << 'exec-around-before' 47 | result = proxy.call(&block) 48 | values << 'exec-around-after' 49 | result 50 | end 51 | end 52 | end 53 | 54 | it 'should bind only the aspect that binds to private methods' do 55 | subject.send :exec 56 | expect(subject.values).to eq %w( exec-around-before exec-result exec-around-after ) 57 | end 58 | end 59 | 60 | context 'after aspect' do 61 | before do 62 | aspector(klass) do 63 | after :exec do |result| 64 | values << 'exec-after(public_methods_only)' 65 | result 66 | end 67 | end 68 | 69 | aspector(klass, private_methods: true) do 70 | after :exec do |result| 71 | values << 'exec-after' 72 | result 73 | end 74 | end 75 | end 76 | 77 | it 'should bind only the aspect that binds to private methods' do 78 | subject.send :exec 79 | expect(subject.values).to eq %w( exec-result exec-after ) 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'simplecov' 4 | require 'pry' 5 | 6 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) 7 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 8 | 9 | # Don't include unnecessary stuff into rcov 10 | SimpleCov.start do 11 | add_filter '/vendor/' 12 | add_filter '/gems/' 13 | add_filter '/.bundle/' 14 | add_filter '/spec/' 15 | merge_timeout 600 16 | end 17 | 18 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 19 | 20 | RSpec.configure do |config| 21 | config.disable_monkey_patching! 22 | 23 | config.expect_with :rspec do |expectations| 24 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 25 | end 26 | end 27 | 28 | require 'aspector' 29 | 30 | ENV['ASPECTOR_LOG_LEVEL'] ||= ::Logger::WARN.to_s 31 | -------------------------------------------------------------------------------- /spec/support/class_builder.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable NestedMethodDefinition 2 | # Class builder helps creating anonymous classes that we can use to spec our aspector 3 | # We need co create new class instances to have an "empty" and "clear" class for each spec 4 | # This module acts as an interface to create classes 5 | module ClassBuilder 6 | class << self 7 | # Builds a new anonymous class with a predefined methods 8 | # @param block [Proc, nil] block that should be evaluated (if given) 9 | # @return [Class] created anonymous class 10 | def build(&block) 11 | klass = raw do 12 | def values 13 | @values ||= [] 14 | end 15 | 16 | # This is a method that we use as a place to bind to 17 | # @note This method accepts dummy not used parameter but we keep it 18 | # so we can exec with parameter if we need 19 | def exec(_param = nil) 20 | values << 'exec-result' 21 | end 22 | end 23 | 24 | klass.class_eval(&block) if block_given? 25 | klass 26 | end 27 | 28 | # Creates an empty class without any predefined methods 29 | # @param block [Proc, nil] block that should be evaluated (if given) 30 | # @return [Class] created anonymous class 31 | def raw(&block) 32 | Class.new(&block) 33 | end 34 | 35 | # This method allows us to create a class that inherits from any other 36 | # @param klass [Class] any class from which we want to inherit in our anonymous class 37 | # @param block [Proc] a block of code that should be evaluated in a new anonymous class body 38 | # @return [Class] new anonymous class 39 | def inherit(klass, &block) 40 | Class.new(klass, &block) 41 | end 42 | end 43 | end 44 | # rubocop:enable NestedMethodDefinition 45 | -------------------------------------------------------------------------------- /spec/units/advice/builder_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Advice::Builder do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/advice/metadata_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Advice::Metadata do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/advice/method_matcher_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Advice::MethodMatcher do 4 | describe '#initialize' do 5 | pending 6 | end 7 | 8 | describe '#any?' do 9 | subject { described_class.new(match_data) } 10 | let(:match1) { double } 11 | let(:match2) { double } 12 | let(:match_data) { [match1, match2] } 13 | let(:method) { double } 14 | let(:aspect) { double } 15 | 16 | context 'should check for every if none' do 17 | before do 18 | expect(subject) 19 | .to receive(:matches?) 20 | .with(match1, method, aspect) 21 | .and_return(false) 22 | 23 | expect(subject) 24 | .to receive(:matches?) 25 | .with(match2, method, aspect) 26 | .and_return(false) 27 | end 28 | 29 | it { expect(subject.any?(method, aspect)).to eq false } 30 | end 31 | end 32 | 33 | describe '#to_s' do 34 | let(:match_data) { [rand, rand] } 35 | subject { described_class.new(match_data) } 36 | 37 | it 'should use inspected match_data as a string representation of MethodMatcher' do 38 | expect(subject.to_s).to eq match_data.map(&:inspect).join(', ') 39 | end 40 | end 41 | 42 | describe '#matches?' do 43 | subject { described_class.new([match_item]) } 44 | 45 | context 'String matching - when we want to match method by string name' do 46 | let(:method) { rand.to_s } 47 | let(:match_item) { method } 48 | 49 | it 'should match it' do 50 | expect(subject.send(:matches?, match_item, method)).to eq true 51 | end 52 | end 53 | 54 | context 'Regular expression matching - when we want to match method by regexp' do 55 | let(:method) { rand.to_s + 'constant-part' } 56 | let(:match_item) { /constant/ } 57 | 58 | it 'should match it' do 59 | expect(subject.send(:matches?, match_item, method)).to eq true 60 | end 61 | end 62 | 63 | context 'deferred logic matching' do 64 | let(:match_item) { Aspector::Deferred::Logic.new(-> {}) } 65 | let(:method) { double } 66 | 67 | before do 68 | expect(method) 69 | .to receive(:deferred_logic_results) 70 | .with(match_item) 71 | .and_return(/test/) 72 | end 73 | 74 | it 'should get aspect deferred logic results and match them' do 75 | expect(subject.send(:matches?, match_item, 'test', method)).to eq true 76 | end 77 | end 78 | 79 | context 'deferred option matching' do 80 | let(:match_item) { Aspector::Deferred::Option.new[:methods] } 81 | let(:method) { double } 82 | 83 | before do 84 | expect(method) 85 | .to receive(:options) 86 | .and_return(methods: /test/) 87 | end 88 | 89 | it 'should get aspect options value for a proper key and match it' do 90 | expect(subject.send(:matches?, match_item, 'test', method)).to eq true 91 | end 92 | end 93 | 94 | context 'unsupported item class' do 95 | let(:method) { rand.to_s } 96 | let(:match_item) { [] } 97 | 98 | it 'should fail with a proper exception' do 99 | error = Aspector::Errors::UnsupportedItemClass 100 | expect { subject.send(:matches?, match_item, method) }.to raise_error error 101 | end 102 | end 103 | end 104 | 105 | describe '#matches_deferred_logic?' do 106 | pending 107 | end 108 | 109 | describe '#matches_deferred_option?' do 110 | pending 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/units/advice_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Advice do 4 | subject { described_class.new } 5 | 6 | describe '.new' do 7 | pending 8 | end 9 | 10 | describe '#name' do 11 | pending 12 | end 13 | 14 | describe '#with_method' do 15 | pending 16 | end 17 | 18 | describe '#match?' do 19 | pending 20 | end 21 | 22 | describe '#raw?' do 23 | pending 24 | end 25 | 26 | describe '#before?' do 27 | pending 28 | end 29 | 30 | describe '#after?' do 31 | pending 32 | end 33 | 34 | describe '#around?' do 35 | pending 36 | end 37 | 38 | describe '#type_name' do 39 | pending 40 | end 41 | 42 | describe '#use_deferred_logic?' do 43 | pending 44 | end 45 | 46 | describe '#to_s' do 47 | pending 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/units/advices/after_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'After advices' do 4 | subject { klass.new } 5 | 6 | context 'standard case' do 7 | let(:klass) do 8 | ClassBuilder.build do 9 | def after_exec(result) 10 | values << 'after-exec' 11 | result 12 | end 13 | end 14 | end 15 | 16 | before do 17 | aspector(klass) do 18 | after :exec, :after_exec 19 | end 20 | end 21 | 22 | it 'should execute after_exec' do 23 | expect(subject) 24 | .to receive(:after_exec) 25 | .once 26 | 27 | subject.exec 28 | end 29 | 30 | it 'should work' do 31 | subject.exec 32 | expect(subject.values).to eq %w( exec-result after-exec ) 33 | end 34 | end 35 | 36 | context 'logic in string' do 37 | let(:klass) { ClassBuilder.build } 38 | 39 | before do 40 | aspector(klass) do 41 | after :exec, <<-CODE 42 | values << 'after-exec' 43 | result 44 | CODE 45 | end 46 | end 47 | 48 | it 'should work' do 49 | subject.exec 50 | expect(subject.values).to eq %w( exec-result after-exec ) 51 | end 52 | end 53 | 54 | context 'logic in block' do 55 | let(:klass) { ClassBuilder.build } 56 | 57 | before do 58 | aspector(klass) do 59 | after :exec do |result| 60 | values << 'after_value' 61 | result 62 | end 63 | end 64 | end 65 | 66 | it 'should work' do 67 | subject.exec 68 | expect(subject.values).to eq %w( exec-result after_value ) 69 | end 70 | end 71 | 72 | context 'when we want to use method that is defined after aspector binding' do 73 | let(:klass) do 74 | ClassBuilder.build do 75 | aspector do 76 | after :exec, :after_exec 77 | end 78 | 79 | def after_exec(result) 80 | values << 'after-exec' 81 | result 82 | end 83 | end 84 | end 85 | 86 | it 'should be able to use it' do 87 | subject.exec 88 | expect(subject.values).to eq %w( exec-result after-exec ) 89 | end 90 | end 91 | 92 | context 'method with parameters' do 93 | let(:klass) do 94 | ClassBuilder.build do 95 | def after_exec(method, result, *exec_params) 96 | values << "after-exec(#{method})" 97 | values << exec_params 98 | values.flatten! 99 | result 100 | end 101 | end 102 | end 103 | 104 | let(:exec_param) { rand } 105 | 106 | before do 107 | aspector(klass) do 108 | after :exec, :after_exec, method_arg: true 109 | end 110 | end 111 | 112 | it 'should execute after_exec with method args' do 113 | expect(subject) 114 | .to receive(:after_exec) 115 | .with('exec', ['exec-result'], exec_param) 116 | .once 117 | 118 | subject.exec(exec_param) 119 | end 120 | 121 | it 'should work' do 122 | subject.exec(exec_param) 123 | expect(subject.values).to eq %w( exec-result after-exec(exec) ) << exec_param 124 | end 125 | end 126 | 127 | context 'method with method_arg' do 128 | let(:klass) do 129 | ClassBuilder.build do 130 | def after_exec(method, result) 131 | values << "after_exec(#{method})" 132 | result 133 | end 134 | end 135 | end 136 | 137 | before do 138 | aspector(klass) do 139 | after :exec, :after_exec, method_arg: true 140 | end 141 | end 142 | 143 | it 'should execute after_exec with method args' do 144 | expect(subject) 145 | .to receive(:after_exec) 146 | .with('exec', ['exec-result']) 147 | .once 148 | 149 | subject.exec 150 | end 151 | 152 | it 'should work' do 153 | subject.exec 154 | expect(subject.values).to eq %w( exec-result after_exec(exec) ) 155 | end 156 | end 157 | 158 | context 'new result from aspect block' do 159 | let(:klass) { ClassBuilder.build } 160 | 161 | before do 162 | aspector(klass) do 163 | after :exec do |_result| 164 | values << 'after_value' 165 | 'block-result' 166 | end 167 | end 168 | end 169 | 170 | it 'should return it by default' do 171 | expect(subject.exec).to eq 'block-result' 172 | expect(subject.values).to eq %w( exec-result after_value ) 173 | end 174 | end 175 | 176 | context 'when result_arg is set to false' do 177 | let(:klass) do 178 | ClassBuilder.build do 179 | def exec 180 | values << 'exec-result' 181 | 'exec-result' 182 | end 183 | end 184 | end 185 | 186 | before do 187 | aspector(klass) do 188 | after :exec, result_arg: false do 189 | values << 'do_after' 190 | 'do_after' 191 | end 192 | end 193 | end 194 | 195 | it 'should return original method return value' do 196 | expect(subject.exec).to eq 'exec-result' 197 | end 198 | 199 | it 'should still execute the after block' do 200 | subject.exec 201 | expect(subject.values).to eq %w( exec-result do_after ) 202 | end 203 | end 204 | 205 | describe 'implicit method option' do 206 | context 'when we want to bind to a single method' do 207 | let(:klass) do 208 | ClassBuilder.build do 209 | def after_method(result) 210 | values << 'after-method' 211 | result 212 | end 213 | end 214 | end 215 | 216 | before do 217 | aspector klass, method: %i( exec ) do 218 | after :after_method 219 | end 220 | end 221 | 222 | it 'should work' do 223 | subject.exec 224 | expect(subject.values).to eq %w( exec-result after-method ) 225 | end 226 | end 227 | 228 | context 'when we want to bind to multiple methods' do 229 | let(:klass) do 230 | ClassBuilder.build do 231 | def after_method(result) 232 | values << 'after-method' 233 | result 234 | end 235 | 236 | def exec_new 237 | values << 'exec-new' 238 | end 239 | end 240 | end 241 | 242 | before do 243 | aspector klass, methods: %i( exec exec_new ) do 244 | after :after_method 245 | end 246 | end 247 | 248 | it 'should work for method 1' do 249 | subject.exec 250 | expect(subject.values).to eq %w( exec-result after-method ) 251 | end 252 | 253 | it 'should work for method 2' do 254 | subject.exec_new 255 | expect(subject.values).to eq %w( exec-new after-method ) 256 | end 257 | end 258 | end 259 | 260 | describe 'disabling aspect' do 261 | let(:klass) do 262 | ClassBuilder.build do 263 | def after_method(result) 264 | values << 'after-method' 265 | result 266 | end 267 | end 268 | end 269 | 270 | let(:aspect) do 271 | aspector klass, method: %i( exec ) do 272 | after :after_method 273 | end 274 | end 275 | 276 | before do 277 | aspect.disable! 278 | end 279 | 280 | context 'when we define an aspect and we disable it' do 281 | it 'should not work' do 282 | subject.exec 283 | expect(subject.values).to eq %w( exec-result ) 284 | end 285 | 286 | context 'and we enable it back' do 287 | it 'should work again' do 288 | subject.exec 289 | expect(subject.values).to eq %w( exec-result ) 290 | aspect.enable! 291 | subject.exec 292 | expect(subject.values).to eq %w( exec-result exec-result after-method ) 293 | end 294 | end 295 | end 296 | end 297 | 298 | context 'all methods except' do 299 | let(:klass) do 300 | ClassBuilder.build do 301 | def after_exec(result) 302 | values << 'after-exec' 303 | result 304 | end 305 | 306 | def except_exec 307 | values << 'except-exec' 308 | end 309 | end 310 | end 311 | 312 | before do 313 | aspector(klass) do 314 | after(/^ex.*/, :after_exec, except: :except_exec) 315 | end 316 | end 317 | 318 | it 'should work with exec' do 319 | subject.exec 320 | expect(subject.values).to eq %w( exec-result after-exec ) 321 | end 322 | 323 | it 'should not work with except_exec' do 324 | subject.except_exec 325 | expect(subject.values).to eq %w( except-exec ) 326 | end 327 | end 328 | end 329 | -------------------------------------------------------------------------------- /spec/units/advices/around_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Around advices' do 4 | subject { klass.new } 5 | 6 | context 'standard case' do 7 | let(:klass) do 8 | ClassBuilder.build do 9 | def around_exec(proxy, &block) 10 | values << 'before-exec' 11 | result = proxy.call(&block) 12 | values << 'after-exec' 13 | result 14 | end 15 | end 16 | end 17 | 18 | before do 19 | aspector(klass) do 20 | around :exec, :around_exec 21 | end 22 | end 23 | 24 | it 'should wrap around the exec method and work' do 25 | subject.exec 26 | expect(subject.values).to eq %w( before-exec exec-result after-exec ) 27 | end 28 | end 29 | 30 | context 'logic in string' do 31 | let(:klass) { ClassBuilder.build } 32 | 33 | before do 34 | aspector(klass) do 35 | around :exec, <<-CODE 36 | values << 'before-exec' 37 | result = INVOKE_PROXY 38 | values << 'after-exec' 39 | result 40 | CODE 41 | end 42 | end 43 | 44 | it 'should wrap around the exec method and work' do 45 | subject.exec 46 | expect(subject.values).to eq %w( before-exec exec-result after-exec ) 47 | end 48 | end 49 | 50 | context 'logic in block' do 51 | let(:klass) { ClassBuilder.build } 52 | 53 | before do 54 | aspector(klass) do 55 | around :exec do |proxy, &block| 56 | values << 'before-exec' 57 | result = proxy.call(&block) 58 | values << 'after-exec' 59 | result 60 | end 61 | end 62 | end 63 | 64 | it 'should wrap around the exec method and work' do 65 | subject.exec 66 | expect(subject.values).to eq %w( before-exec exec-result after-exec ) 67 | end 68 | end 69 | 70 | context 'when we want to use method that is defined after aspector binding' do 71 | let(:klass) do 72 | ClassBuilder.build do 73 | aspector do 74 | around :exec, :around_exec 75 | end 76 | 77 | def around_exec(proxy, &block) 78 | values << 'before-exec' 79 | result = proxy.call(&block) 80 | values << 'after-exec' 81 | result 82 | end 83 | end 84 | end 85 | 86 | it 'should be able to use it' do 87 | subject.exec 88 | expect(subject.values).to eq %w( before-exec exec-result after-exec ) 89 | end 90 | end 91 | 92 | context 'method with parameters' do 93 | let(:klass) do 94 | ClassBuilder.build do 95 | def around_exec(method, proxy, *exec_params, &block) 96 | values << "before-exec(#{method})" 97 | result = proxy.call(&block) 98 | values << exec_params 99 | values << "after-exec(#{method})" 100 | values.flatten! 101 | result 102 | end 103 | end 104 | end 105 | 106 | let(:exec_param) { rand } 107 | 108 | before do 109 | aspector(klass) do 110 | around :exec, :around_exec, method_arg: true 111 | end 112 | end 113 | 114 | it 'should work' do 115 | subject.exec(exec_param) 116 | expected = (%w( before-exec(exec) exec-result ) << exec_param) + %w( after-exec(exec) ) 117 | expect(subject.values).to eq expected 118 | end 119 | end 120 | 121 | context 'method with method_arg' do 122 | let(:klass) do 123 | ClassBuilder.build do 124 | def around_exec(method, proxy, &block) 125 | values << "before-exec(#{method})" 126 | result = proxy.call(&block) 127 | values << "after-exec(#{method})" 128 | result 129 | end 130 | end 131 | end 132 | 133 | before do 134 | aspector(klass) do 135 | around :exec, :around_exec, method_arg: true 136 | end 137 | end 138 | 139 | it 'should work' do 140 | subject.exec 141 | expect(subject.values).to eq %w( before-exec(exec) exec-result after-exec(exec) ) 142 | end 143 | end 144 | 145 | context 'new result from aspect block' do 146 | let(:klass) { ClassBuilder.build } 147 | 148 | before do 149 | aspector(klass) do 150 | around :exec do |proxy, &block| 151 | values << 'before-exec' 152 | proxy.call(&block) 153 | values << 'after-exec' 154 | 'block-result' 155 | end 156 | end 157 | end 158 | 159 | it 'should return them by default' do 160 | expect(subject.exec).to eq 'block-result' 161 | expect(subject.values).to eq %w( before-exec exec-result after-exec ) 162 | end 163 | end 164 | 165 | describe 'implicit method option' do 166 | context 'when we want to bind to a single method' do 167 | let(:klass) do 168 | ClassBuilder.build do 169 | def around_method(proxy, &block) 170 | values << 'before-method' 171 | result = proxy.call(&block) 172 | values << 'after-method' 173 | result 174 | end 175 | end 176 | end 177 | 178 | before do 179 | aspector klass, method: %i( exec ) do 180 | around :around_method 181 | end 182 | end 183 | 184 | it 'should work' do 185 | subject.exec 186 | expect(subject.values).to eq %w( before-method exec-result after-method ) 187 | end 188 | end 189 | 190 | context 'when we want to bind to multiple methods' do 191 | let(:klass) do 192 | ClassBuilder.build do 193 | def around_method(proxy, &block) 194 | values << 'before-method' 195 | result = proxy.call(&block) 196 | values << 'after-method' 197 | result 198 | end 199 | 200 | def exec_new 201 | values << 'exec-new' 202 | end 203 | end 204 | end 205 | 206 | before do 207 | aspector klass, methods: %i( exec exec_new ) do 208 | around :around_method 209 | end 210 | end 211 | 212 | it 'should work for method 1' do 213 | subject.exec 214 | expect(subject.values).to eq %w( before-method exec-result after-method ) 215 | end 216 | 217 | it 'should work for method 2' do 218 | subject.exec_new 219 | expect(subject.values).to eq %w( before-method exec-new after-method ) 220 | end 221 | end 222 | end 223 | 224 | context 'disabling aspect' do 225 | let(:klass) do 226 | ClassBuilder.build do 227 | def around_exec(proxy, &block) 228 | values << 'before-exec' 229 | result = proxy.call(&block) 230 | values << 'after-exec' 231 | result 232 | end 233 | end 234 | end 235 | 236 | let(:aspect) do 237 | aspector(klass) do 238 | around :exec, :around_exec 239 | end 240 | end 241 | 242 | before do 243 | aspect.disable! 244 | end 245 | 246 | context 'when we define an aspect and we disable it' do 247 | it 'should not work' do 248 | subject.exec 249 | expect(subject.values).to eq %w( exec-result ) 250 | end 251 | 252 | context 'and we enable it back' do 253 | it 'should work again' do 254 | subject.exec 255 | expect(subject.values).to eq %w( exec-result ) 256 | aspect.enable! 257 | subject.exec 258 | expect(subject.values).to eq %w( exec-result before-exec exec-result after-exec ) 259 | end 260 | end 261 | end 262 | end 263 | 264 | context 'disabling aspect' do 265 | let(:klass) do 266 | ClassBuilder.build do 267 | def around_exec(proxy, &block) 268 | values << 'before-exec' 269 | result = proxy.call(&block) 270 | values << 'after-exec' 271 | result 272 | end 273 | end 274 | end 275 | 276 | let(:aspect) do 277 | aspector(klass) do 278 | around :exec, :around_exec 279 | end 280 | end 281 | 282 | before do 283 | aspect.disable! 284 | end 285 | 286 | context 'when we define an aspect and we disable it' do 287 | it 'should not work' do 288 | subject.exec 289 | expect(subject.values).to eq %w( exec-result ) 290 | end 291 | end 292 | 293 | context 'and we enable it back' do 294 | it 'should not work' do 295 | subject.exec 296 | expect(subject.values).to eq %w( exec-result ) 297 | aspect.enable! 298 | subject.exec 299 | expect(subject.values).to eq %w( exec-result before-exec exec-result after-exec ) 300 | end 301 | end 302 | end 303 | 304 | context 'all methods except' do 305 | let(:klass) do 306 | ClassBuilder.build do 307 | def around_exec(proxy, &block) 308 | values << 'before-exec' 309 | result = proxy.call(&block) 310 | values << 'after-exec' 311 | result 312 | end 313 | 314 | def except_exec 315 | values << 'except-exec' 316 | end 317 | end 318 | end 319 | 320 | before do 321 | aspector(klass) do 322 | around(/^ex.*/, :around_exec, except: :except_exec) 323 | end 324 | end 325 | 326 | it 'should work with exec' do 327 | subject.exec 328 | expect(subject.values).to eq %w( before-exec exec-result after-exec ) 329 | end 330 | 331 | it 'should not work with except_exec' do 332 | subject.except_exec 333 | expect(subject.values).to eq %w( except-exec ) 334 | end 335 | end 336 | end 337 | -------------------------------------------------------------------------------- /spec/units/advices/before_filter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Before filter advices' do 4 | subject { klass.new } 5 | 6 | context 'standard cases' do 7 | before do 8 | aspector(klass) do 9 | before_filter :exec, :before_exec 10 | end 11 | end 12 | 13 | context 'when code returns false' do 14 | let(:klass) do 15 | ClassBuilder.build do 16 | def before_exec 17 | values << 'before_exec' 18 | false 19 | end 20 | end 21 | end 22 | 23 | it 'should execute before_exec' do 24 | expect(subject) 25 | .to receive(:before_exec) 26 | .once 27 | 28 | subject.exec 29 | end 30 | 31 | it 'should not execute original code' do 32 | subject.exec 33 | expect(subject.values).to eq %w( before_exec ) 34 | end 35 | end 36 | 37 | context 'when code returns nil' do 38 | let(:klass) do 39 | ClassBuilder.build do 40 | def before_exec 41 | values << 'before_exec' 42 | nil 43 | end 44 | end 45 | end 46 | 47 | it 'should execute before_exec' do 48 | expect(subject) 49 | .to receive(:before_exec) 50 | .once 51 | 52 | subject.exec 53 | end 54 | 55 | it 'should not execute original code' do 56 | subject.exec 57 | expect(subject.values).to eq %w( before_exec ) 58 | end 59 | end 60 | 61 | context 'when code doesnt return false or nil' do 62 | let(:klass) do 63 | ClassBuilder.build do 64 | def before_exec 65 | values << 'before_exec' 66 | end 67 | end 68 | end 69 | 70 | it 'should execute before_exec' do 71 | expect(subject) 72 | .to receive(:before_exec) 73 | .once 74 | 75 | subject.exec 76 | end 77 | 78 | it 'should work' do 79 | subject.exec 80 | expect(subject.values).to eq %w( before_exec exec-result ) 81 | end 82 | end 83 | end 84 | 85 | context 'logic in string' do 86 | let(:klass) { ClassBuilder.build } 87 | 88 | before do 89 | aspector(klass) do 90 | before_filter :exec, <<-CODE 91 | values << 'before-exec' 92 | CODE 93 | end 94 | end 95 | 96 | it 'should work' do 97 | subject.exec 98 | expect(subject.values).to eq %w( before-exec exec-result ) 99 | end 100 | end 101 | 102 | context 'logic in block' do 103 | let(:klass) { ClassBuilder.build } 104 | 105 | before do 106 | aspector(klass) do 107 | before_filter :exec do 108 | values << 'before-exec' 109 | end 110 | end 111 | end 112 | 113 | it 'should work' do 114 | subject.exec 115 | expect(subject.values).to eq %w( before-exec exec-result ) 116 | end 117 | end 118 | 119 | context 'when we want to use method that is defined after aspector binding' do 120 | let(:klass) do 121 | ClassBuilder.build do 122 | aspector do 123 | before_filter :exec, :before_exec 124 | end 125 | 126 | def before_exec 127 | values << 'before-exec' 128 | end 129 | end 130 | end 131 | 132 | it 'should be able to use it' do 133 | subject.exec 134 | expect(subject.values).to eq %w( before-exec exec-result ) 135 | end 136 | end 137 | 138 | context 'method with method_arg' do 139 | let(:klass) do 140 | ClassBuilder.build do 141 | def before_exec(method) 142 | values << "before_exec(#{method})" 143 | end 144 | end 145 | end 146 | 147 | before do 148 | aspector(klass) do 149 | before_filter :exec, :before_exec, method_arg: true 150 | end 151 | end 152 | 153 | it 'should execute before_exec with method args' do 154 | expect(subject) 155 | .to receive(:before_exec) 156 | .with('exec') 157 | .once 158 | 159 | subject.exec 160 | end 161 | 162 | it 'should work' do 163 | subject.exec 164 | expect(subject.values).to eq %w( before_exec(exec) exec-result ) 165 | end 166 | end 167 | 168 | describe 'implicit method option' do 169 | context 'when we want to bind to a single method' do 170 | let(:klass) do 171 | ClassBuilder.build do 172 | def before_method 173 | values << 'before-method' 174 | end 175 | end 176 | end 177 | 178 | before do 179 | aspector klass, method: %i( exec ) do 180 | before_filter :before_method 181 | end 182 | end 183 | 184 | it 'should work' do 185 | subject.exec 186 | expect(subject.values).to eq %w( before-method exec-result ) 187 | end 188 | end 189 | 190 | context 'when we want to bind to multiple methods' do 191 | let(:klass) do 192 | ClassBuilder.build do 193 | def before_method 194 | values << 'before-method' 195 | end 196 | 197 | def exec_new 198 | values << 'exec-new' 199 | end 200 | end 201 | end 202 | 203 | before do 204 | aspector klass, methods: %i( exec exec_new ) do 205 | before_filter :before_method 206 | end 207 | end 208 | 209 | it 'should work for method 1' do 210 | subject.exec 211 | expect(subject.values).to eq %w( before-method exec-result ) 212 | end 213 | 214 | it 'should work for method 2' do 215 | subject.exec_new 216 | expect(subject.values).to eq %w( before-method exec-new ) 217 | end 218 | end 219 | end 220 | 221 | context 'disabling aspect' do 222 | let(:klass) do 223 | ClassBuilder.build do 224 | def before_exec 225 | values << 'before-exec' 226 | end 227 | end 228 | end 229 | 230 | let(:aspect) do 231 | aspector(klass) do 232 | before_filter :exec, :before_exec 233 | end 234 | end 235 | 236 | before do 237 | aspect.disable! 238 | end 239 | 240 | context 'when we define an aspect and we disable it' do 241 | it 'should not work' do 242 | subject.exec 243 | expect(subject.values).to eq %w( exec-result ) 244 | end 245 | end 246 | 247 | context 'and we enable it back' do 248 | it 'should not work' do 249 | subject.exec 250 | expect(subject.values).to eq %w( exec-result ) 251 | aspect.enable! 252 | subject.exec 253 | expect(subject.values).to eq %w( exec-result before-exec exec-result ) 254 | end 255 | end 256 | end 257 | 258 | context 'all methods except' do 259 | let(:klass) do 260 | ClassBuilder.build do 261 | def before_exec 262 | values << 'before-exec' 263 | end 264 | 265 | def except_exec 266 | values << 'except-exec' 267 | end 268 | end 269 | end 270 | 271 | before do 272 | aspector(klass) do 273 | before_filter(/^ex.*/, :before_exec, except: :except_exec) 274 | end 275 | end 276 | 277 | it 'should work with exec' do 278 | subject.exec 279 | expect(subject.values).to eq %w( before-exec exec-result ) 280 | end 281 | 282 | it 'should not work with except_exec' do 283 | subject.except_exec 284 | expect(subject.values).to eq %w( except-exec ) 285 | end 286 | end 287 | end 288 | -------------------------------------------------------------------------------- /spec/units/advices/before_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Before advices' do 4 | subject { klass.new } 5 | 6 | context 'standard case' do 7 | let(:klass) do 8 | ClassBuilder.build do 9 | def before_exec 10 | values << 'before_exec' 11 | end 12 | end 13 | end 14 | 15 | before do 16 | aspector(klass) do 17 | before :exec, :before_exec 18 | end 19 | end 20 | 21 | it 'should execute before_exec' do 22 | expect(subject) 23 | .to receive(:before_exec) 24 | .once 25 | 26 | subject.exec 27 | end 28 | 29 | it 'should work' do 30 | subject.exec 31 | expect(subject.values).to eq %w( before_exec exec-result ) 32 | end 33 | end 34 | 35 | context 'logic in string' do 36 | let(:klass) { ClassBuilder.build } 37 | 38 | before do 39 | aspector(klass) do 40 | before :exec, <<-CODE 41 | values << 'before-exec' 42 | CODE 43 | end 44 | end 45 | 46 | it 'should work' do 47 | subject.exec 48 | expect(subject.values).to eq %w( before-exec exec-result ) 49 | end 50 | end 51 | 52 | context 'logic in block' do 53 | let(:klass) { ClassBuilder.build } 54 | 55 | before do 56 | aspector(klass) do 57 | before :exec do 58 | values << 'before-exec' 59 | end 60 | end 61 | end 62 | 63 | it 'should work' do 64 | subject.exec 65 | expect(subject.values).to eq %w( before-exec exec-result ) 66 | end 67 | end 68 | 69 | context 'when we want to use method that is defined after aspector binding' do 70 | let(:klass) do 71 | ClassBuilder.build do 72 | aspector do 73 | before :exec, :before_exec 74 | end 75 | 76 | def before_exec 77 | values << 'before-exec' 78 | end 79 | end 80 | end 81 | 82 | it 'should be able to use it' do 83 | subject.exec 84 | expect(subject.values).to eq %w( before-exec exec-result ) 85 | end 86 | end 87 | 88 | context 'method with method_arg' do 89 | let(:klass) do 90 | ClassBuilder.build do 91 | def before_exec(method) 92 | values << "before_exec(#{method})" 93 | end 94 | end 95 | end 96 | 97 | before do 98 | aspector(klass) do 99 | before :exec, :before_exec, method_arg: true 100 | end 101 | end 102 | 103 | it 'should execute before_exec with method args' do 104 | expect(subject) 105 | .to receive(:before_exec) 106 | .with('exec') 107 | .once 108 | 109 | subject.exec 110 | end 111 | 112 | it 'should work' do 113 | subject.exec 114 | expect(subject.values).to eq %w( before_exec(exec) exec-result ) 115 | end 116 | end 117 | 118 | describe 'implicit method option' do 119 | context 'when we want to bind to a single method' do 120 | let(:klass) do 121 | ClassBuilder.build do 122 | def before_method 123 | values << 'before-method' 124 | end 125 | end 126 | end 127 | 128 | before do 129 | aspector klass, method: %i( exec ) do 130 | before :before_method 131 | end 132 | end 133 | 134 | it 'should work' do 135 | subject.exec 136 | expect(subject.values).to eq %w( before-method exec-result ) 137 | end 138 | end 139 | 140 | context 'when we want to bind to multiple methods' do 141 | let(:klass) do 142 | ClassBuilder.build do 143 | def before_method 144 | values << 'before-method' 145 | end 146 | 147 | def exec_new 148 | values << 'exec-new' 149 | end 150 | end 151 | end 152 | 153 | before do 154 | aspector klass, methods: %i( exec exec_new ) do 155 | before :before_method 156 | end 157 | end 158 | 159 | it 'should work for method 1' do 160 | subject.exec 161 | expect(subject.values).to eq %w( before-method exec-result ) 162 | end 163 | 164 | it 'should work for method 2' do 165 | subject.exec_new 166 | expect(subject.values).to eq %w( before-method exec-new ) 167 | end 168 | end 169 | end 170 | 171 | context 'disabling aspect' do 172 | let(:klass) do 173 | ClassBuilder.build do 174 | def before_exec 175 | values << 'before-exec' 176 | end 177 | end 178 | end 179 | 180 | let(:aspect) do 181 | aspector(klass) do 182 | before :exec, :before_exec 183 | end 184 | end 185 | 186 | before do 187 | aspect.disable! 188 | end 189 | 190 | context 'when we define an aspect and we disable it' do 191 | it 'should not work' do 192 | subject.exec 193 | expect(subject.values).to eq %w( exec-result ) 194 | end 195 | end 196 | 197 | context 'and we enable it back' do 198 | it 'should not work' do 199 | subject.exec 200 | expect(subject.values).to eq %w( exec-result ) 201 | aspect.enable! 202 | subject.exec 203 | expect(subject.values).to eq %w( exec-result before-exec exec-result ) 204 | end 205 | end 206 | end 207 | 208 | context 'all methods except' do 209 | let(:klass) do 210 | ClassBuilder.build do 211 | def before_exec 212 | values << 'before-exec' 213 | end 214 | 215 | def except_exec 216 | values << 'except-exec' 217 | end 218 | end 219 | end 220 | 221 | before do 222 | aspector(klass) do 223 | before(/^ex.*/, :before_exec, except: :except_exec) 224 | end 225 | end 226 | 227 | it 'should work with exec' do 228 | subject.exec 229 | expect(subject.values).to eq %w( before-exec exec-result ) 230 | end 231 | 232 | it 'should not work with except_exec' do 233 | subject.except_exec 234 | expect(subject.values).to eq %w( except-exec ) 235 | end 236 | end 237 | end 238 | -------------------------------------------------------------------------------- /spec/units/advices/raw_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Raw advices' do 4 | subject { klass.new } 5 | 6 | context 'standard case' do 7 | let(:klass) do 8 | ClassBuilder.build do 9 | def before_exec 10 | values << 'before_exec' 11 | end 12 | end 13 | end 14 | 15 | before do 16 | aspector(klass) do 17 | raw :exec do 18 | alias_method :exec_without_aspect, :exec 19 | 20 | def exec 21 | values << 'exec-result' 22 | exec_without_aspect 23 | end 24 | end 25 | end 26 | end 27 | 28 | it 'should execute exec_without_aspect' do 29 | expect(subject) 30 | .to receive(:exec_without_aspect) 31 | .once 32 | 33 | subject.exec 34 | end 35 | 36 | it 'should work' do 37 | subject.exec 38 | expect(subject.values).to eq %w( exec-result exec-result ) 39 | end 40 | end 41 | 42 | context 'when we want to use method that is defined after aspector binding' do 43 | let(:klass) do 44 | ClassBuilder.build do 45 | aspector do 46 | raw :exec do 47 | alias_method :exec_without_aspect, :exec 48 | 49 | def exec 50 | after_defined_method 51 | exec_without_aspect 52 | end 53 | end 54 | end 55 | 56 | def after_defined_method 57 | values << 'after-defined' 58 | end 59 | end 60 | end 61 | 62 | it 'should be able to use it' do 63 | subject.exec 64 | expect(subject.values).to eq %w( after-defined exec-result ) 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/units/base/class_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Aspector::Base class methods' do 4 | subject { ClassBuilder.inherit(Aspector::Base) } 5 | 6 | describe '.enable' do 7 | pending 8 | end 9 | 10 | describe '.disable' do 11 | pending 12 | end 13 | 14 | describe '.logger' do 15 | pending 16 | end 17 | 18 | describe '.advices' do 19 | pending 20 | end 21 | 22 | describe '.default_options' do 23 | pending 24 | end 25 | 26 | describe '.apply' do 27 | pending 28 | end 29 | 30 | describe '.default' do 31 | context 'when there are no default options' do 32 | let(:options) { { rand => rand } } 33 | let(:storage) { subject.send(:storage) } 34 | 35 | before do 36 | storage.default_options = {} 37 | end 38 | 39 | it 'should assign provided options' do 40 | subject.send(:default, options) 41 | 42 | expect(storage.default_options).to eq options 43 | end 44 | end 45 | 46 | context 'when there are default options' do 47 | let(:options) { { rand => rand } } 48 | let(:default_options) { { rand => rand } } 49 | let(:storage) { subject.send(:storage) } 50 | 51 | before do 52 | storage.default_options = default_options 53 | end 54 | 55 | it 'should merge with existing options' do 56 | subject.send(:default, options) 57 | 58 | defaults = options.merge(default_options) 59 | expect(storage.default_options).to eq defaults 60 | end 61 | end 62 | end 63 | 64 | describe '.before' do 65 | subject do 66 | ClassBuilder.inherit(Aspector::Base) do 67 | before :test, :do_before 68 | end 69 | end 70 | 71 | let(:advices) { subject.send(:storage).advices } 72 | let(:advice) { advices.first } 73 | 74 | it 'should create an advice' do 75 | expect(subject.send(:storage).advices.size).to eq 1 76 | end 77 | 78 | it 'created advice should be a before one' do 79 | expect(advice.before?).to eq true 80 | expect(advice.before_filter?).to eq false 81 | expect(advice.after?).to eq false 82 | expect(advice.around?).to eq false 83 | expect(advice.raw?).to eq false 84 | end 85 | 86 | it 'skip_if_false for this advice should not be true' do 87 | expect(advice.options[:skip_if_false]).not_to eq true 88 | end 89 | 90 | it 'should create a advice with a do_before method' do 91 | expect(advice.with_method).to eq :do_before 92 | end 93 | end 94 | 95 | describe '.before_filter' do 96 | subject do 97 | ClassBuilder.inherit(Aspector::Base) do 98 | before_filter :test, :do_before 99 | end 100 | end 101 | 102 | let(:advices) { subject.send(:storage).advices } 103 | let(:advice) { advices.first } 104 | 105 | it 'should create an advice' do 106 | expect(subject.send(:storage).advices.size).to eq 1 107 | end 108 | 109 | it 'created advice should be only a before one' do 110 | expect(advice.before_filter?).to eq true 111 | expect(advice.before?).to eq false 112 | expect(advice.after?).to eq false 113 | expect(advice.around?).to eq false 114 | expect(advice.raw?).to eq false 115 | end 116 | 117 | it 'should create a advice with a do_before method' do 118 | expect(advice.with_method).to eq :do_before 119 | end 120 | end 121 | 122 | describe '.after' do 123 | subject do 124 | ClassBuilder.inherit(Aspector::Base) do 125 | after :test, :do_after 126 | end 127 | end 128 | 129 | let(:advices) { subject.send(:storage).advices } 130 | let(:advice) { advices.first } 131 | 132 | it 'should create an advice' do 133 | expect(subject.send(:storage).advices.size).to eq 1 134 | end 135 | 136 | it 'created advice should be a before one' do 137 | expect(advice.after?).to eq true 138 | expect(advice.before_filter?).to eq false 139 | expect(advice.before?).to eq false 140 | expect(advice.around?).to eq false 141 | expect(advice.raw?).to eq false 142 | end 143 | 144 | it 'skip_if_false for this advice should not be true' do 145 | expect(advice.options[:skip_if_false]).not_to eq true 146 | end 147 | 148 | it 'should create a advice with a do_after method' do 149 | expect(advice.with_method).to eq :do_after 150 | end 151 | end 152 | 153 | describe '.around' do 154 | subject do 155 | ClassBuilder.inherit(Aspector::Base) do 156 | around :test, :do_around 157 | end 158 | end 159 | 160 | let(:advices) { subject.send(:storage).advices } 161 | let(:advice) { advices.first } 162 | 163 | it 'should create an advice' do 164 | expect(subject.send(:storage).advices.size).to eq 1 165 | end 166 | 167 | it 'created advice should be a around one' do 168 | expect(advice.after?).to eq false 169 | expect(advice.before?).to eq false 170 | expect(advice.around?).to eq true 171 | expect(advice.raw?).to eq false 172 | end 173 | 174 | it 'skip_if_false for this advice should not be true' do 175 | expect(advice.options[:skip_if_false]).not_to eq true 176 | end 177 | 178 | it 'should create a advice with a do_around method' do 179 | expect(advice.with_method).to eq :do_around 180 | end 181 | end 182 | 183 | describe '.raw' do 184 | subject do 185 | ClassBuilder.inherit(Aspector::Base) do 186 | raw :test do 187 | end 188 | end 189 | end 190 | 191 | let(:advices) { subject.send(:storage).advices } 192 | let(:advice) { advices.first } 193 | 194 | it 'should create an advice' do 195 | expect(subject.send(:storage).advices.size).to eq 1 196 | end 197 | 198 | it 'created advice should be a around one' do 199 | expect(advice.after?).to eq false 200 | expect(advice.before_filter?).to eq false 201 | expect(advice.before?).to eq false 202 | expect(advice.around?).to eq false 203 | expect(advice.raw?).to eq true 204 | end 205 | 206 | it 'skip_if_false for this advice should not be true' do 207 | expect(advice.options[:skip_if_false]).not_to eq true 208 | end 209 | end 210 | 211 | describe '.target' do 212 | context 'when there is no code and no block' do 213 | let(:code) { nil } 214 | 215 | it 'should raise a proper error' do 216 | error = Aspector::Errors::CodeBlockRequired 217 | expect { subject.send(:target, code) }.to raise_error(error) 218 | end 219 | end 220 | 221 | context 'when there is code and no block given' do 222 | let(:code) { -> {} } 223 | 224 | it 'should not raise ArgumentError' do 225 | expect { subject.send(:target, code) {} }.not_to raise_error 226 | end 227 | end 228 | 229 | context 'when there is block given and no code' do 230 | let(:code) { nil } 231 | 232 | it 'should not raise ArgumentError' do 233 | expect { subject.send(:target, code) {} }.not_to raise_error 234 | end 235 | end 236 | 237 | context 'when there is code and block given' do 238 | let(:code) { -> {} } 239 | 240 | it 'should not raise ArgumentError' do 241 | expect { subject.send(:target, code) {} }.not_to raise_error 242 | end 243 | end 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /spec/units/base/dsl_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Base::Dsl do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/base/status_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Base::Status do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/base/storage_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Base::Storage do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Base do 4 | let(:klass) { ClassBuilder.build } 5 | let(:aspect) do 6 | Aspector do 7 | before :exec do 8 | values << 'do_before' 9 | end 10 | end 11 | end 12 | 13 | subject { klass.new } 14 | 15 | describe 'default options' do 16 | context 'when we create aspector with default options' do 17 | subject do 18 | Aspector do 19 | default exec: 'value' 20 | end 21 | end 22 | 23 | it 'should store them' do 24 | expect(subject.send(:storage).default_options[:exec]).to eq 'value' 25 | end 26 | end 27 | end 28 | 29 | describe '#apply' do 30 | context 'applying to a single target' do 31 | it 'should apply a given code to the method' do 32 | aspect.apply(klass) 33 | subject.exec 34 | expect(subject.values).to eq %w( do_before exec-result ) 35 | end 36 | end 37 | 38 | context 'applying to multiple targets at once' do 39 | let(:klass1) { ClassBuilder.build } 40 | let(:klass2) { ClassBuilder.build } 41 | 42 | let(:instance1) { klass1.new } 43 | let(:instance2) { klass2.new } 44 | 45 | before { aspect.apply(klass1, klass2) } 46 | 47 | it 'should apply to all the targets' do 48 | instance1.exec 49 | expect(instance1.values).to eq %w( do_before exec-result ) 50 | 51 | instance2.exec 52 | expect(instance2.values).to eq %w( do_before exec-result ) 53 | end 54 | end 55 | 56 | context 'when we try to apply to nonexisting method' do 57 | let(:aspect) do 58 | Aspector do 59 | before do 60 | # dummy advice 61 | end 62 | end 63 | end 64 | 65 | it 'should not do anything (should not fail)' do 66 | expect { aspect.apply(klass, methods: 'not_exist') }.not_to raise_error 67 | end 68 | end 69 | end 70 | 71 | describe '#target' do 72 | context 'adding method to targeted class/module' do 73 | before do 74 | aspector(klass) do 75 | target do 76 | def before_exec 77 | values << 'before-exec' 78 | end 79 | end 80 | 81 | before :exec, :before_exec 82 | end 83 | end 84 | 85 | it 'should add to target' do 86 | subject.exec 87 | expect(subject.values).to eq %w( before-exec exec-result ) 88 | end 89 | end 90 | 91 | context 'when we provide an aspect as an argument to a #target method' do 92 | let(:aspect_class) do 93 | Class.new(Aspector::Base) do 94 | target do |aspect| 95 | define_method :before_exec do 96 | values << "before_exec(#{aspect.class})" 97 | end 98 | end 99 | 100 | before :exec, :before_exec 101 | end 102 | end 103 | 104 | it 'should add it to target' do 105 | aspect_class.apply(klass) 106 | 107 | subject.exec 108 | expect(subject.values).to eq ["before_exec(#{aspect_class})", 'exec-result'] 109 | end 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/units/deferred/logic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Deferred::Logic do 4 | describe '.new' do 5 | pending 6 | end 7 | 8 | describe '#apply' do 9 | context 'block in deferred logic' do 10 | let(:klass) do 11 | ClassBuilder.build do 12 | def self.exec 13 | 'class-exec' 14 | end 15 | end 16 | end 17 | 18 | let(:logic) { described_class.new(-> { exec }) } 19 | 20 | it 'should work with block' do 21 | expect(logic.apply(klass)).to eq 'class-exec' 22 | end 23 | end 24 | 25 | context 'block with argument in deferred logic' do 26 | let(:argument_value) { rand } 27 | let(:klass) { ClassBuilder.build } 28 | let(:logic) { described_class.new(->(arg) { arg }) } 29 | 30 | it 'block can take an argument' do 31 | expect(logic.apply(klass, argument_value)).to eq argument_value 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/units/deferred/option_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Deferred::Option do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/extensions/module_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Extensions::Module do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/extensions/object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Extensions::Object do 4 | describe '.aspector' do 5 | pending 6 | end 7 | 8 | describe '.Aspector' do 9 | pending 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/units/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Logger do 4 | let(:context) { '' } 5 | subject { described_class.new(context) } 6 | 7 | describe '.new' do 8 | it 'should store a context' do 9 | expect(subject.context).to eq context 10 | end 11 | 12 | it 'should set a log level' do 13 | expect(subject.level).to_not be_nil 14 | end 15 | end 16 | 17 | describe '#postfix' do 18 | pending 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/units/logging_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Logging do 4 | subject { described_class } 5 | let(:context) { double } 6 | 7 | describe '.build' do 8 | before do 9 | subject.instance_variable_set('@logger', logger) 10 | end 11 | 12 | after do 13 | subject.instance_variable_set('@logger', nil) 14 | end 15 | 16 | let(:logger) { nil } 17 | let(:klass_name) { 'NonExisting' } 18 | 19 | context 'and given logger class doesnt exist' do 20 | before do 21 | expect(ENV) 22 | .to receive(:[]) 23 | .and_return(klass_name) 24 | .at_least(:once) 25 | 26 | expect($stderr) 27 | .to receive(:puts) 28 | .with("#{klass_name} is not a valid constant name!") 29 | end 30 | 31 | it 'should use Aspector::Logger' do 32 | expect(subject.build(context)).to be_instance_of(Aspector::Logger) 33 | end 34 | end 35 | 36 | context 'and given logger class exists' do 37 | let(:dummy_logger) { 'DummyLogger' } 38 | 39 | before do 40 | expect(ENV) 41 | .to receive(:[]) 42 | .and_return(dummy_logger) 43 | .at_least(:once) 44 | 45 | expect(Object) 46 | .to receive(:const_get) 47 | .with(dummy_logger) 48 | .and_return(Aspector::Logger) 49 | end 50 | 51 | it 'should use it' do 52 | expect(subject.build(context)).to be_instance_of(Aspector::Logger) 53 | end 54 | end 55 | end 56 | 57 | describe '.deconstantize' do 58 | context 'and given logger class doesnt exist' do 59 | let(:klass_name) { 'NonExisting' } 60 | 61 | before do 62 | expect($stderr) 63 | .to receive(:puts) 64 | .with("#{klass_name} is not a valid constant name!") 65 | end 66 | 67 | it 'should return internal logger class' do 68 | expect(subject.send(:deconstantize, klass_name)).to eq Aspector::Logger 69 | end 70 | end 71 | 72 | context 'and given logger class exists' do 73 | let(:klass_name) { 'Aspector::Logger' } 74 | 75 | before do 76 | expect($stderr) 77 | .not_to receive(:puts) 78 | end 79 | 80 | it 'should return it deconstantized' do 81 | expect(subject.send(:deconstantize, klass_name)).to eq Aspector::Logger 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/units/object_extension_spec.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gcao/aspector/c82396dc8678bf632959fdaf25aa28e76b0e4605/spec/units/object_extension_spec.rb -------------------------------------------------------------------------------- /spec/units/refinements/class_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Refinements::Class do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/refinements/string_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Aspector::Refinements::String do 4 | pending 5 | end 6 | -------------------------------------------------------------------------------- /spec/units/special_chars_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe 'Special chars in method names' do 4 | subject { klass.new } 5 | 6 | methods_postfixes = %w( ? ! = ) 7 | full_methods = %w( + - * / ~ | % & ^ < > [] []= ) 8 | 9 | context 'special characters in instance method names' do 10 | methods_postfixes.each do |char| 11 | context "when method name contains '#{char}'" do 12 | let(:klass) do 13 | ClassBuilder.build do 14 | aspector do 15 | before "exec#{char}", :exec 16 | end 17 | 18 | define_method "exec#{char}" do |*_args| 19 | values << char 20 | end 21 | end 22 | end 23 | 24 | it 'should handle it without any issues' do 25 | subject.send "exec#{char}" 26 | expect(subject.values).to eq ['exec-result', char] 27 | end 28 | end 29 | end 30 | 31 | full_methods.each do |char| 32 | context "when method name is '#{char}'" do 33 | let(:klass) do 34 | ClassBuilder.build do 35 | aspector do 36 | before char, :exec 37 | end 38 | 39 | define_method char do |*_args| 40 | values << char 41 | end 42 | end 43 | end 44 | 45 | it 'should handle it without any issues' do 46 | subject.send(char, 1) 47 | expect(subject.values).to eq ['exec-result', char] 48 | end 49 | end 50 | end 51 | end 52 | 53 | context 'special characters in class method names' do 54 | methods_postfixes.each do |char| 55 | context "when method name contains '#{char}'" do 56 | self.class.send :define_method, :methods_postfixes do 57 | methods_postfixes 58 | end 59 | 60 | let(:klass) do 61 | ClassBuilder.build do 62 | aspector class_methods: true do 63 | before "exec#{char}", :exec 64 | end 65 | 66 | class << self 67 | def values 68 | @values ||= [] 69 | end 70 | 71 | def exec(*_args) 72 | values << 'exec-result' 73 | end 74 | 75 | methods_postfixes.each do |char| 76 | define_method "exec#{char}" do |*args| 77 | values << args.first 78 | end 79 | end 80 | end 81 | end 82 | end 83 | 84 | it 'should handle it without any issues' do 85 | klass.send("exec#{char}", char) 86 | expect(klass.values).to eq ['exec-result', char] 87 | end 88 | end 89 | end 90 | 91 | full_methods.each do |char| 92 | context "when method name is '#{char}'" do 93 | self.class.send :define_method, :full_methods_names do 94 | full_methods 95 | end 96 | 97 | let(:klass) do 98 | ClassBuilder.build do 99 | aspector class_methods: true do 100 | before char, :exec 101 | end 102 | 103 | class << self 104 | def values 105 | @values ||= [] 106 | end 107 | 108 | def exec(*_args) 109 | values << 'exec-result' 110 | end 111 | 112 | full_methods_names.each do |char| 113 | define_method char do |*args| 114 | values << args.first 115 | end 116 | end 117 | end 118 | end 119 | end 120 | 121 | it 'should handle it without any issues' do 122 | klass.send(char, char) 123 | expect(klass.values).to eq ['exec-result', char] 124 | end 125 | end 126 | end 127 | end 128 | end 129 | --------------------------------------------------------------------------------