├── .gitignore ├── Gemfile ├── lib ├── multi_block │ ├── array.rb │ ├── core_ext.rb │ ├── version.rb │ └── implementation.rb └── multi_block.rb ├── CHANGELOG.md ├── Rakefile ├── MIT-LICENSE.txt ├── .github └── workflows │ └── test.yml ├── multi_block.gemspec ├── spec └── multi_block_spec.rb ├── README.md └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/multi_block/array.rb: -------------------------------------------------------------------------------- 1 | class Array 2 | include MultiBlock::Array 3 | end 4 | -------------------------------------------------------------------------------- /lib/multi_block/core_ext.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | include MultiBlock::Object 3 | end 4 | -------------------------------------------------------------------------------- /lib/multi_block/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MultiBlock 4 | VERSION = "1.2" 5 | end 6 | -------------------------------------------------------------------------------- /lib/multi_block.rb: -------------------------------------------------------------------------------- 1 | require 'named_proc' 2 | 3 | require_relative 'multi_block/version' 4 | require_relative 'multi_block/implementation' 5 | require_relative 'multi_block/core_ext' 6 | 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.2 4 | 5 | * Update project setup to 2021 6 | * Re-release 7 | 8 | ## 1.1 9 | 10 | * Updates for 2015 Rubies, drop support for Ruby 1 11 | * Simplify activating Array extension via: require 'multi_block/array' 12 | 13 | ## 1.0 14 | 15 | * Initial Release 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # # # 4 | # Get gemspec info 5 | 6 | gemspec_file = Dir['*.gemspec'].first 7 | gemspec = eval File.read(gemspec_file), binding, gemspec_file 8 | info = "#{gemspec.name} | #{gemspec.version} | " \ 9 | "#{gemspec.runtime_dependencies.size} dependencies | " \ 10 | "#{gemspec.files.size} files" 11 | 12 | # # # 13 | # Gem build and install task 14 | 15 | desc info 16 | task :gem do 17 | puts info + "\n\n" 18 | print " "; sh "gem build #{gemspec_file}" 19 | FileUtils.mkdir_p 'pkg' 20 | FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", 'pkg' 21 | puts; sh %{gem install --no-document pkg/#{gemspec.name}-#{gemspec.version}.gem} 22 | end 23 | 24 | # # # 25 | # Start an IRB session with the gem loaded 26 | 27 | desc "#{gemspec.name} | IRB" 28 | task :irb do 29 | sh "irb -I ./lib -r #{gemspec.name.tr '-', '/'}" 30 | end 31 | 32 | # # # 33 | # Run Specs 34 | 35 | require 'rspec/core/rake_task' 36 | RSpec::Core::RakeTask.new(:spec) 37 | 38 | task :default => :spec 39 | -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT LICENSE 2 | 3 | Copyright (c) 2011, 2015 Jan Lelis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Ruby ${{ matrix.ruby }} (${{ matrix.os }}) 8 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 9 | strategy: 10 | matrix: 11 | ruby: 12 | - 3.1 13 | - 3.0 14 | - 2.7 15 | - 2.6 16 | os: 17 | - ubuntu-latest 18 | - macos-latest 19 | runs-on: ${{matrix.os}} 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{matrix.ruby}} 26 | bundler-cache: true 27 | - name: Run tests 28 | run: bundle exec rake 29 | 30 | test-windows: 31 | name: Ruby ${{ matrix.ruby }} (windows-latest) 32 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 33 | strategy: 34 | matrix: 35 | ruby: 36 | - 3.0 37 | - 2.7 38 | - 2.6 39 | runs-on: windows-latest 40 | steps: 41 | - uses: actions/checkout@v2 42 | - name: Set up Ruby 43 | uses: ruby/setup-ruby@v1 44 | with: 45 | ruby-version: ${{matrix.ruby}} 46 | bundler-cache: true 47 | -------------------------------------------------------------------------------- /multi_block.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.expand_path('../lib/multi_block/version', __FILE__) 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "multi_block" 7 | s.version = MultiBlock::VERSION 8 | s.authors = ["Jan Lelis"] 9 | s.email = "hi@ruby.consulting" 10 | s.homepage = "https://github.com/janlelis/multi_block" 11 | s.summary = 'MultiBlock is a mini framework for passing multiple blocks to methods.' 12 | s.description = 'MultiBlock is a mini framework for passing multiple blocks to methods. It uses "named procs" to accomplish this in a nice way. The receiving method can either yield all blocks, or just call specific ones, identified by order or name. ' 13 | s.required_ruby_version = '>= 1.9.3' 14 | s.files = Dir.glob %w{multi_block.gemspec lib/multi_block.rb lib/multi_block/version.rb lib/multi_block/implementation.rb lib/multi_block/core_ext.rb lib/multi_block/array.rb spec/multi_block_spec.rb} 15 | s.extra_rdoc_files = ["README.md", "MIT-LICENSE.txt", "CHANGELOG.md", "CODE_OF_CONDUCT.md"] 16 | s.license = 'MIT' 17 | s.add_dependency 'named_proc', '~> 1.2' 18 | s.add_development_dependency 'rspec', '~> 3.2' 19 | s.add_development_dependency 'rake', '~> 13.0' 20 | end 21 | -------------------------------------------------------------------------------- /lib/multi_block/implementation.rb: -------------------------------------------------------------------------------- 1 | module MultiBlock 2 | # Multiple block transformation method 3 | def self.[](*proc_array) 4 | # Create hash representation, proc_array will still get used sometimes 5 | proc_hash = {} 6 | proc_array.each{ |proc| 7 | proc_hash[proc.name] = proc if proc.respond_to?(:name) 8 | } 9 | 10 | # Build yielder proc 11 | Proc.new{ |*proc_names_and_args| 12 | if proc_names_and_args.empty? # call all procs 13 | ret = proc_array.map(&:call) 14 | 15 | proc_array.size == 1 ? ret.first : ret 16 | else 17 | proc_names, *proc_args = *proc_names_and_args 18 | 19 | if proc_names.is_a? Hash # keys: proc_names, values: args 20 | proc_names.map{ |proc_name, proc_args| 21 | proc = proc_name.is_a?(Integer) ? proc_array[proc_name] : proc_hash[proc_name.to_sym] 22 | proc or raise LocalJumpError, "wrong block name given (#{proc_name})" 23 | 24 | [proc, Array(proc_args)] 25 | }.map{ |proc, proc_args| 26 | proc.call(*proc_args) 27 | } 28 | 29 | else 30 | ret = Array(proc_names).map{ |proc_name| 31 | proc = proc_name.is_a?(Integer) ? proc_array[proc_name] : proc_hash[proc_name.to_sym] 32 | proc or raise LocalJumpError, "wrong block name given (#{proc_name})" 33 | 34 | [proc, Array(proc_args)] 35 | }.map{ |proc, proc_args| 36 | proc.call(*proc_args) 37 | } 38 | 39 | ret.size == 1 ? ret.first : ret 40 | 41 | end 42 | end 43 | } 44 | end 45 | 46 | # Low level mixins 47 | module Object 48 | private 49 | 50 | def blocks 51 | MultiBlock 52 | end 53 | end 54 | 55 | # Optional Array mixin 56 | module Array 57 | def to_proc 58 | ::MultiBlock[*self] 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/multi_block_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require_relative '../lib/multi_block' 3 | 4 | describe "Object#blocks" do 5 | it "returns the MutliBlock constant (for calling [] on it)" do 6 | expect( blocks ).to be MultiBlock 7 | end 8 | end 9 | 10 | describe "MultiBlock#[]" do 11 | describe "yield without arguments" do 12 | it "calls every block and returns array of results" do 13 | def null 14 | yield 15 | end 16 | 17 | expect( null(&blocks[ 18 | proc{5}, 19 | proc{6}, 20 | ]) ).to eq [5,6] 21 | end 22 | end 23 | 24 | describe "yield with symbol" do 25 | it "calls the specified proc, other args get passed" do 26 | def symbol 27 | yield :success, "Code Brawl!" 28 | end 29 | 30 | expect( symbol(&blocks[ 31 | proc{5}, 32 | proc.success{|e| e.swapcase}, 33 | proc.error{6}, 34 | ]) ).to eq "cODE bRAWL!" 35 | end 36 | 37 | it 'will raise LocalJumpError if proc name is wrong' do 38 | def wrong_name 39 | yield :wrong, "Code Brawl!" 40 | end 41 | 42 | expect{ 43 | wrong_name(&blocks[ 44 | proc{5}, 45 | proc.success{|e| e.swapcase}, 46 | proc.error{6}, 47 | ]) 48 | }.to raise_exception(LocalJumpError) 49 | end 50 | end 51 | 52 | describe "yield with integer" do 53 | it "calls the n-th proc, other args get passed" do 54 | def integer 55 | yield 2 56 | end 57 | 58 | expect( integer(&blocks[ 59 | proc{5}, 60 | proc.success{|e| e.swapcase}, 61 | proc.error{6}, 62 | ]) ).to eq 6 63 | end 64 | end 65 | 66 | describe "yield with array" do 67 | it "calls all procs, indentified by symbol or integer, other args get passed" do 68 | def array 69 | yield [:success, :error], "Code Brawl!" 70 | end 71 | 72 | expect( array(&blocks[ 73 | proc{5}, 74 | proc.success{|e| e.swapcase}, 75 | proc.error{|e| e.downcase}, 76 | ]) ).to eq ["cODE bRAWL!", "code brawl!"] 77 | end 78 | end 79 | 80 | describe "yield with hash" do 81 | it "takes keys as proc names and passes values as proc args" do 82 | def hash 83 | yield success: "Code Brawl!", error: [500, "Internal Brawl Error"] 84 | end 85 | 86 | expect( hash(&blocks[ 87 | proc{5}, 88 | proc.success{|e| e.swapcase}, 89 | proc.error{|no, msg| "Error #{no}: #{msg}"}, 90 | ]).sort ).to eq ["Error 500: Internal Brawl Error", "cODE bRAWL!"] 91 | end 92 | end 93 | end 94 | 95 | describe "Array#to_proc" do 96 | it "works without blocks helper" do 97 | def another_method 98 | yield 99 | end 100 | 101 | proc_array = [ 102 | proc{5}, 103 | proc{6}, 104 | ] 105 | proc_array.send :extend, MultiBlock::Array 106 | 107 | expect( another_method(&proc_array) ).to eq [5,6] 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MultiBlock [](https://badge.fury.io/rb/multi_block) [](https://github.com/janlelis/multi_block/actions?query=workflow%3ATest) 2 | 3 | MultiBlock is a mini framework for passing multiple blocks to methods. It uses [named procs](https://github.com/janlelis/named_proc) to accomplish this with a simple syntax. The receiving method can either yield all blocks, or just call specific ones, identified by order or name. 4 | 5 | Currently supports CRuby only. 6 | 7 | ## Setup 8 | 9 | Add to Gemfile: 10 | 11 | gem 'multi_block' 12 | 13 | 14 | ## Usage 15 | ### Defining methods that use multiple blocks 16 | 17 | The first argument given to yield always defines the desired block(s). The other arguments get directly passed to the block(s): 18 | 19 | yield # calls all given procs without args 20 | yield :success # calls :success proc without args 21 | yield :success, "Code Brawl!" # calls :success proc with message 22 | yield 1 # calls first proc (:success in this case) 23 | yield [:success, :bonus] # calls :success and :bonus without args 24 | yield [:success, :bonus], "Code Brawl!" # calls both procs with same arg 25 | yield success: "Code Brawl!", # calls each keyed proc, 26 | error: [500, "Internal Brawl Error"] # values are the args 27 | 28 | 29 | ### Calling methods with multiple blocks 30 | 31 | Consider these two example methods: 32 | 33 | # calls the :success block if everything worked well 34 | def ajax 35 | yield rand(6) != 0 ? :success : :error 36 | end 37 | 38 | # calls the n-th block 39 | def dice 40 | random_number = rand(6) 41 | yield random_number, random_number + 1 42 | end 43 | 44 | 45 | Multiple blocks can be passed using `blocks`: 46 | 47 | ajax &blocks[ 48 | proc.success{ puts "Yeah!" }, 49 | proc.error { puts "Error..." }, 50 | ] 51 | 52 | 53 | The dice method could, for example, be called in this way: 54 | 55 | dice &blocks[ 56 | proc{ ":(" }, 57 | proc{ ":/" }, 58 | proc{ ":O" }, 59 | proc{ ":b" }, 60 | proc{ ":P" }, 61 | proc{ rand(42) != 0 ? ":)" : ":D"}, 62 | ] 63 | 64 | 65 | ## Bonus sugar: Array extension 66 | 67 | If you like the slim `&to_proc` operator, you can further optimize the syntax by activating the core extension for array: 68 | 69 | require 'multi_block/array' 70 | 71 | Now you do not need the `blocks` helper anymore. Instead just do: 72 | 73 | do_something, some_argument, &[ 74 | proc.easy_way{ 75 | # do it the easy way 76 | }, 77 | proc.complex_way{ 78 | # use complex heuristics, etc. 79 | }, 80 | ] 81 | 82 | 83 | ## MIT License 84 | 85 | See the original gist: https://gist.github.com/4b2f5fd0b45118e46d0f 86 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at conduct@janlelis.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: https://contributor-covenant.org 74 | [version]: https://contributor-covenant.org/version/1/4/ 75 | --------------------------------------------------------------------------------