├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE ├── Rakefile ├── asynchronize.gemspec ├── changelog.md ├── lib └── asynchronize.rb ├── readme.md └── spec ├── minitest_helper.rb └── spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/github/gitignore/blob/master/Ruby.gitignore 2 | 3 | *.gem 4 | *.rbc 5 | /.config 6 | /coverage/ 7 | /InstalledFiles 8 | /pkg/ 9 | /spec/reports/ 10 | /spec/examples.txt 11 | /test/tmp/ 12 | /test/version_tmp/ 13 | /tmp/ 14 | 15 | # Used by dotenv library to load environment variables. 16 | # .env 17 | 18 | ## Specific to RubyMotion: 19 | .dat* 20 | .repl_history 21 | build/ 22 | *.bridgesupport 23 | build-iPhoneOS/ 24 | build-iPhoneSimulator/ 25 | 26 | ## Specific to RubyMotion (use of CocoaPods): 27 | # 28 | # We recommend against adding the Pods directory to your .gitignore. However 29 | # you should judge for yourself, the pros and cons are mentioned at: 30 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 31 | # 32 | # vendor/Pods/ 33 | 34 | ## Documentation cache and generated files: 35 | /.yardoc/ 36 | /_yardoc/ 37 | /doc/ 38 | /rdoc/ 39 | 40 | ## Environment normalization: 41 | /.bundle/ 42 | /vendor/bundle 43 | /lib/bundler/man/ 44 | 45 | # for a library or gem, you might want to ignore these files since the code is 46 | # intended to run in multiple environments; otherwise, check them in: 47 | Gemfile.lock 48 | # .ruby-version 49 | # .ruby-gemset 50 | 51 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 52 | .rvmrc 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - CC_TEST_REPORTER_ID=5779741f84b94f962138194592b1d6a3036b6b49ae0b800c4d75fea1ef4460c0 4 | language: ruby 5 | rvm: 6 | - 2.6.0 7 | - 2.3.8 8 | - jruby-9.2.5.0 9 | - rbx-3 10 | git: 11 | depth: false 12 | before_install: gem install bundler 13 | before_script: 14 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 15 | - chmod +x ./cc-test-reporter 16 | - ./cc-test-reporter before-build 17 | after_script: 18 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source :rubygems 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kenneth Cochran 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new do |t| 4 | t.pattern = "spec/*spec.rb" 5 | t.ruby_opts = ["--debug"] if RUBY_ENGINE == 'jruby' 6 | end 7 | 8 | desc "Run tests" 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /asynchronize.gemspec: -------------------------------------------------------------------------------- 1 | require 'date' 2 | code_repo = 'https://github.com/kennycoc/asynchronize' 3 | 4 | Gem::Specification.new do |s| 5 | s.name = 'asynchronize' 6 | s.version = '0.4.1' 7 | s.version = '0.4.2' 8 | s.date = Date.today.to_s 9 | s.summary = 'A declarative syntax for creating asynchronous methods.' 10 | s.description = %w{Asynchronize provides a declarative syntax for creating 11 | asynchronous methods. Sometimes you just want a regular 12 | thread without the overhead of a whole new layer of 13 | abstraction. Asynchronize provides a declarative syntax to 14 | wrap any method in a Thread.}.join(' ') 15 | s.author = 'Kenneth Cochran' 16 | s.email = 'kenneth.cochran101@gmail.com' 17 | s.files = [ 18 | 'lib/asynchronize.rb', 19 | 'spec/spec.rb', 20 | 'spec/minitest_helper.rb', 21 | 'readme.md', 22 | 'LICENSE', 23 | ] 24 | s.test_files = [ 25 | 'spec/spec.rb', 26 | 'spec/minitest_helper.rb' 27 | ] 28 | s.required_ruby_version = '>= 2.3' 29 | s.post_install_message = 'Making something cool with asynchronize? ' + 30 | 'Let me know at ' + code_repo 31 | s.homepage = code_repo 32 | s.license = 'MIT' 33 | s.add_development_dependency 'rake', '~> 12.3' 34 | s.add_development_dependency 'minitest', '~> 5.11' 35 | s.add_development_dependency 'simplecov', '~> 0.16' 36 | s.add_development_dependency 'pry', '~> 0.11' 37 | end 38 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 0.4.2 2 | - Update dependencies to latest version, and add testing for newest Ruby versions. 3 | 4 | ## 0.4.1 5 | - Update dependencies to latest version 6 | 7 | ## 0.4.0 8 | - Update API to be more consistent with built in threads. 9 | - Add ability to retrieve both post-block and pre-block data from thread. 10 | 11 | ## 0.3.0 12 | - Rewrite using Module.prepend. 13 | 14 | ## 0.2.1 15 | - Memory usage optimizations 16 | 17 | ## 0.2.0 18 | - `:return_value` is now always set on the thread. 19 | - Inheriting from a class that includes asynchronize no longer automatically 20 | asynchronizes methods that override asynchronized methods in the parent class. 21 | - various other bug fixes 22 | 23 | ## 0.1.2 24 | - Fixes a bug where method_added wouldn't be properly detected or called in 25 | classes that defined method_added before including asynchronize. 26 | 27 | ## 0.1.1 28 | - Fixes a bug where old method_added would be called more than once 29 | -------------------------------------------------------------------------------- /lib/asynchronize.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Include this module to allow a declarative syntax for defining asynch methods 3 | # 4 | # Defines only one method on the including class: `asynchronize` 5 | # 6 | module Asynchronize 7 | # Defines the asynchronize method 8 | def self.included(base) 9 | base.class_eval do 10 | ## 11 | # Call to asynchronize a method. 12 | # 13 | # This does two things 14 | # 1. Creates and prepends a module ::Asynchronized. 15 | # 2. Defines each of the passed methods on that module. 16 | # 17 | # Additional notes: 18 | # - The new methods wrap the old method within Thread.new. 19 | # - Subsequent calls only add methods to the existing Module. 20 | # - Will silently fail if the method has already been asynchronized 21 | # 22 | # @param methods [Symbol] The methods to be asynchronized. 23 | # @example To add any number of methods to be asynchronized. 24 | # asynchronize :method1, :method2, :methodn 25 | # 26 | def self.asynchronize(*methods) 27 | return if methods.empty? 28 | async_container = Asynchronize._get_container_for(self) 29 | Asynchronize._define_methods_on_object(methods, async_container) 30 | end 31 | end 32 | end 33 | 34 | private 35 | ## 36 | # Define methods on object 37 | # 38 | # For each method in the methods array 39 | # 40 | # - If method already defined, go to the next. 41 | # - If method does not exist, create it and go to the next. 42 | # 43 | # @param methods [Array] The methods to be bound. 44 | # @param obj [Object] The object for the methods to be defined on. 45 | # 46 | def self._define_methods_on_object(methods, obj) 47 | methods.each do |method| 48 | next if obj.methods.include?(method) 49 | obj.send(:define_method, method, _build_method) 50 | end 51 | end 52 | 53 | ## 54 | # Build Method 55 | # 56 | # This always returns the same Proc object. In it's own method for clarity. 57 | # 58 | # @return [Proc] The actual asynchronous method defined. 59 | # 60 | def self._build_method 61 | return Proc.new do |*args, &block| 62 | return Thread.new(args, block) do |thread_args, thread_block| 63 | Thread.current[:return_value] = super(*thread_args) 64 | next thread_block.call(Thread.current[:return_value]) if thread_block 65 | Thread.current[:return_value] 66 | end 67 | end 68 | end 69 | 70 | ## 71 | # Container setup 72 | # 73 | # Creates the container module that will hold our asynchronous wrappers. 74 | # 75 | # - If the container module is defined, return it. 76 | # - If the container module is not defined, create, prepend, and return it. 77 | # 78 | # @param obj [Class] The class the module should belong to. 79 | # @return [Module] The already prepended module to define our methods on. 80 | # 81 | def self._get_container_for(obj) 82 | if obj.const_defined?('Asynchronized') 83 | return obj.const_get('Asynchronized') 84 | else 85 | async_container = obj.const_set('Asynchronized', Module.new) 86 | obj.prepend async_container 87 | return async_container 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/icodesometime/asynchronize.svg?branch=master)](https://travis-ci.org/icodesometime/asynchronize) 2 | [![Maintainability](https://api.codeclimate.com/v1/badges/30d40e270a3d7a0775a9/maintainability)](https://codeclimate.com/github/kennycoc/asynchronize/maintainability) 3 | [![Test Coverage](https://api.codeclimate.com/v1/badges/30d40e270a3d7a0775a9/test_coverage)](https://codeclimate.com/github/kennycoc/asynchronize/test_coverage) 4 | # Asynchronize 5 | ### A declarative syntax for creating asynchronous methods. 6 | 7 | Find yourself writing the same boilerplate for all your asynchronous methods? 8 | Get dry with asynchronize. 9 | 10 | Just add to your Gemfile and `bundle` or install globally with 11 | `gem install asynchronize` 12 | 13 | ## Usage 14 | Create a class with asynchronized methods 15 | ```Ruby 16 | require 'asynchronize' 17 | class Test 18 | include Asynchronize 19 | # Can be called before or after method definitions. I prefer it at the top of classes. 20 | asynchronize :my_test, :my_other_test 21 | def my_test 22 | return 'testing' 23 | end 24 | def my_other_test 25 | #do stuff here too 26 | end 27 | end 28 | ``` 29 | 30 | Now, to call those methods. 31 | 32 | The method's return value can be accessed either with `Thread#value` or through 33 | the thread param `:return_value` (make sure the thread is finished first!) 34 | ```Ruby 35 | thread = Test.new.my_test 36 | puts thread.value # > testing 37 | puts thread[:return_value] # > testing 38 | ``` 39 | 40 | Or if called with a block, the method's return value will be passed as a 41 | parameter. In this case, the original function's return value is still 42 | accessible at `:return_value`, and `Thread#value` contains the value returned 43 | from the block. 44 | ```Ruby 45 | thread = Test.new.my_test do |return_value| 46 | return_value.length 47 | end 48 | puts thread.value # > 7 49 | puts thread[:return_value] # > testing 50 | ``` 51 | 52 | As you can see, it's just a regular thread. Make sure you call either 53 | `Thread#value` or`Thread#join` to ensure it completes before your process exits, 54 | and to catch any exceptions that may have been thrown! 55 | 56 | ## Inspiration 57 | While working on another project, I found myself writing this way too often: 58 | ```Ruby 59 | def method_name(args) 60 | Thread.new(args) do |targs| 61 | # Actual code. 62 | end 63 | end 64 | ``` 65 | It's extra typing, and adds an unneeded extra layer of nesting. I couldn't find 66 | an existing library that wasn't trying to add new layers of abstraction I didn't 67 | need; sometimes you just want a normal thread. Now, just call asynchronize to 68 | make any method asynchronous. 69 | 70 | ## Versioning Policy 71 | Beginning with version 1.0.0, this project will follow [Semantic 72 | Versioning](https://semver.org). Until then, the patch number (0.0.x) will be 73 | updated for any changes that do not affect the public interface. Versions that 74 | increment the minor number (0.x.0) will have at least one of the following. A new 75 | feature will be added, some feature will be deprecated, or some previously 76 | deprecated feature will be removed. Deprecated features will be removed on the 77 | very next version that increments the minor version number. 78 | 79 | ## FAQ 80 | ### Doesn't metaprogramming hurt performance? 81 | Not at all! What we're doing in this project actually works exactly like 82 | inheritance, so it won't be a problem. 83 | 84 | ### So, how does it work? 85 | When you `include Asynchronize` it creates an `asynchronize` method on your 86 | class. 87 | 88 | ```ruby 89 | require 'asynchronize' 90 | class Test 91 | include Asynchronize 92 | end 93 | Test.methods - Object.methods # > [:asynchronize] 94 | ``` 95 | 96 | The first time you call this method with any arguments, it creates a new 97 | module with the methods you define. It uses `Module#prepend` to insert itself at the top of the 98 | class's inheritance chain. This means that its methods are called before the class's own methods. 99 | 100 | ```ruby 101 | class Test 102 | asynchronize :my_test 103 | def my_test 104 | return 'testing' 105 | end 106 | end 107 | 108 | Test.constants # > [:Asynchronized] 109 | Test::Asynchronized.instance_methods # > [:my_test] 110 | Test.ancestors # > [Test::Asynchronized, Test, Asynchronize, Object, Kernel, BasicObject] 111 | ``` 112 | 113 | Where the implementation of Test::Asynchronized#my_test (and any other method) is like 114 | ```ruby 115 | return Thread.new(args, block) do |thread_args, thread_block| 116 | Thread.current[:return_value] = super(*thread_args) 117 | next thread_block.call(Thread.current[:return_value]) if thread_block 118 | Thread.current[:return_value] 119 | end 120 | ``` 121 | This implementation allows you to call asynchronize at the top of the class and 122 | then define the methods below. Since it changes how you interact with those 123 | method's return values, I thought it was important to allow this. 124 | 125 | ### Why do I need another gem? My code's bloated enough as it is? 126 | It's super tiny. Just a light wrapper around the existing language features. 127 | Seriously, it's just around forty lines of code. Actually, according to 128 | [cloc](https://www.npmjs.com/package/cloc) there's almost four times as many 129 | lines in the tests as the source. You should read it. I'd love feedback! 130 | 131 | ### Do you accept contributions? 132 | Absolutely! 133 | 1. Fork it (https://github.com/kennycoc/asynchronize/fork) 134 | 2. Create your feature branch (git checkout -b my-new-feature) 135 | 3. Commit your changes (git commit -am 'Add some feature') 136 | 4. Push to the branch (git push origin my-new-feature) 137 | 5. Create a new pull request. 138 | 139 | It's just `bundle` to install dependencies, and `rake` to run the tests. 140 | 141 | ### What's the difference between asynchronize and promises/async..await? 142 | Those and other similar projects aim to create an entirely new abstraction to 143 | use for interacting with threads. This project aims to be a light convenience 144 | wrapper around the existing language features. Just define a regular method, 145 | then interact with its result like a regular thread. 146 | 147 | ### What Ruby versions are supported? 148 | Ruby 2.3 and up. Unfortunately, Ruby versions prior to 2.0 do not support 149 | `Module#prepend` and are not supported. Ruby versions prior to 2.3 have a bug 150 | preventing usage of `super` with `define_method`. I'm unable to find a suitable 151 | workaround for this issue. (`method(__method__).super_method.call` causes 152 | problems when a method inherits from the asynchronized class.) 153 | 154 | Luckily, all major Ruby implementations support Ruby language version 2.3, so I 155 | don't see this as a huge problem. If anyone wants support for older versions, 156 | and knows how to work around this issue, feel free to submit a pull request. 157 | 158 | We explicitly test against the following versions: 159 | - Matz Ruby 2.6.0 160 | - Matz Ruby 2.3.8 161 | - JRuby 9.2.5.0 (ruby language version 2.5.x) 162 | - Rubinius 3.100 (ruby language version 2.3.1) 163 | 164 | ### Is it any good? 165 | [Yes](https://news.ycombinator.com/item?id=3067434) 166 | 167 | ### Does anyone like it? 168 | - It was [featured in Ruby Weekly!](https://rubyweekly.com/issues/402) 169 | - Also mentioned in russian dev news aggregator [dou.ua](https://dou.ua/lenta/digests/ruby-digest-19) 170 | ## License 171 | MIT 172 | -------------------------------------------------------------------------------- /spec/minitest_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'minitest/autorun' 5 | -------------------------------------------------------------------------------- /spec/spec.rb: -------------------------------------------------------------------------------- 1 | require './spec/minitest_helper.rb' 2 | require './lib/asynchronize.rb' 3 | 4 | class BasicSpec < Minitest::Test 5 | describe Asynchronize do 6 | before do 7 | class Test 8 | include Asynchronize 9 | def test(val=5) 10 | return val 11 | end 12 | def another_test(val=5) 13 | return val 14 | end 15 | end 16 | end 17 | after do 18 | BasicSpec.send(:remove_const, :Test) 19 | end 20 | 21 | describe "when we asynchronize a method" do 22 | it "should not return a thread unless we asynchronize it" do 23 | Test.new.test.class.wont_equal(Thread, "The method was not overwritten") 24 | end 25 | it "should not throw an error if the specified method does not exist" do 26 | Test.asynchronize :notamethod 27 | end 28 | it "should not create a method if the specified method does not exist" do 29 | Test.asynchronize :notamethod 30 | Test.methods(false).wont_include(:notamethod) 31 | end 32 | it "should not affect methods on other classes when called before" do 33 | Test.asynchronize :test 34 | class OtherBeforeTest 35 | def test 36 | end 37 | end 38 | Test.new.test.class.must_equal(Thread) 39 | OtherBeforeTest.new.test.class.wont_equal(Thread) 40 | end 41 | it "should not affect methods on other classes when called after" do 42 | class OtherAfterTest 43 | def test 44 | end 45 | end 46 | Test.asynchronize :test 47 | Test.new.test.class.must_equal(Thread) 48 | OtherAfterTest.new.test.class.wont_equal(Thread) 49 | end 50 | end 51 | 52 | describe "when we call asynchronized after defining the method" do 53 | before do 54 | Test.asynchronize :test 55 | end 56 | it "should return a thread" do 57 | Test.new.test.class.must_equal(Thread, 58 | "The asynchronized method without a block did not return a thread.") 59 | end 60 | it "should provide the result in return_value" do 61 | Test.new.test.join[:return_value].must_equal 5 62 | end 63 | it "should provide the result as a block parameter" do 64 | temp = 0 65 | Test.new.test do |res| 66 | temp = res 67 | end.join 68 | temp.must_equal 5, "temp is equal to #{temp}." 69 | end 70 | end 71 | 72 | describe "when we call asynchronized before defining the method" do 73 | before do 74 | class Test 75 | asynchronize :othertest 76 | def othertest 77 | return 5 78 | end 79 | end 80 | end 81 | it "should return a thread" do 82 | Test.new.othertest.class.must_equal(Thread, 83 | "The asynchronized method without a block did not return a thread.") 84 | end 85 | it "should provide the result in return_value" do 86 | Test.new.othertest.join[:return_value].must_equal 5 87 | end 88 | it "should provide the result as a block parameter" do 89 | temp = 0 90 | Test.new.othertest do |res| 91 | temp = res 92 | end.join 93 | temp.must_equal 5, "temp is equal to #{temp}." 94 | end 95 | end 96 | 97 | describe "when inheriting from another class" do 98 | before do 99 | class ChildClassTest < Test 100 | include Asynchronize 101 | def test 102 | return super + 1 103 | end 104 | end 105 | end 106 | after do 107 | BasicSpec.send(:remove_const, :ChildClassTest) 108 | end 109 | it "should be able to call super when it's been asynchronized" do 110 | class ChildClassTest 111 | asynchronize :test 112 | end 113 | ChildClassTest.new.test.join[:return_value].must_equal 6 114 | end 115 | it "should be able to call super when super has been asynchronized" do 116 | class Test 117 | asynchronize :another_test 118 | end 119 | class ChildClassTest 120 | def another_test 121 | return super.join[:return_value] + 1 122 | end 123 | end 124 | ChildClassTest.new.another_test.must_equal 6 125 | end 126 | end 127 | 128 | describe "when asynchronize is called" do 129 | it "should not define an Asynchronized container if there are no arguments" do 130 | Test.asynchronize 131 | Test.ancestors.find do |a| 132 | a.name.split('::').include? 'Asynchronized' 133 | end.must_be_nil 134 | end 135 | it "should not define two modules if we call it twice" do 136 | Test.asynchronize :test 137 | Test.asynchronize :another_test 138 | Test.ancestors.select do |a| 139 | a.name.split('::').include? 'Asynchronized' 140 | end.length.must_equal 1 141 | end 142 | end 143 | end 144 | end 145 | --------------------------------------------------------------------------------