├── .ci.gemfile ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG ├── MIT-LICENSE ├── README.rdoc ├── Rakefile ├── lib └── roda │ └── plugins │ └── message_bus.rb ├── roda-message_bus.gemspec └── spec └── roda-message_bus_spec.rb /.ci.gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "rake" 4 | gem "minitest-global_expectations" 5 | 6 | if RUBY_VERSION >= '2' 7 | gem "roda" 8 | gem "message_bus" 9 | gem 'json' 10 | else 11 | gem "roda", '2.0.0' 12 | gem "message_bus", '2.0.0' 13 | gem 'json', '<1.8.5' 14 | end 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | tests: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ubuntu-latest] 18 | ruby: [ "2.0.0", 2.1, 2.3, 2.4, 2.5, 2.6, 2.7, "3.0", 3.1, 3.2, 3.3, 3.4, jruby-9.3, jruby-9.4, jruby-10.0 ] 19 | include: 20 | - { os: ubuntu-22.04, ruby: "1.9.3" } 21 | - { os: ubuntu-22.04, ruby: jruby-9.1 } 22 | - { os: ubuntu-22.04, ruby: jruby-9.2 } 23 | runs-on: ${{ matrix.os }} 24 | name: ${{ matrix.ruby }} 25 | env: 26 | BUNDLE_GEMFILE: .ci.gemfile 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby }} 32 | bundler-cache: true 33 | - run: bundle exec rake 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /roda-message_bus-*.gem 2 | /rdoc 3 | /coverage 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | === 1.0.0 (2016-06-21) 2 | 3 | * Initial Public Release 4 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jeremy Evans 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = roda-message_bus 2 | 3 | roda-message_bus integrates message_bus into the roda web toolkit, 4 | allowing you to call message_bus only for specific paths, after 5 | any access control checks have been made. 6 | 7 | = Installation 8 | 9 | gem install roda-message_bus 10 | 11 | = Source Code 12 | 13 | Source code is available on GitHub at 14 | https://github.com/jeremyevans/roda-message_bus 15 | 16 | = Usage 17 | 18 | roda-message_bus is a roda plugin, so you need to load it into your roda 19 | application similar to other plugins: 20 | 21 | class App < Roda 22 | plugin :message_bus 23 | end 24 | 25 | In your routing block, you can use +r.message_bus+ to pass the routing 26 | to message bus. Generally you'll want to do this after making any 27 | access control checks: 28 | 29 | App.route do |r| 30 | r.on "room/:id" do |room_id| 31 | room_id = room_id.to_i 32 | raise unless current_user.has_access?(room_id) 33 | 34 | # Allows "/room/#{room_id}" channel by default 35 | r.message_bus 36 | 37 | view(:room) 38 | end 39 | end 40 | 41 | Routing will halt in +r.message_bus+ if the request is a message_bus 42 | request. If the request is not a message_bus request, execution will 43 | continue without yielding. 44 | 45 | If you want to, you can force a specific message_bus channel (or channels 46 | by passing an array) when calling r.message_bus: 47 | 48 | # Override channel to use (can also provide array of channels) 49 | r.message_bus("/room/#{room_id}/enters") 50 | 51 | The default channel if you don't pass an argument to +r.message_bus+ is the 52 | already routed path, including the SCRIPT_NAME if present. So if you call 53 | +r.message_bus+ at the top of the routing tree, you'll probably want to 54 | specify the channel unless you want to use the empty channel. 55 | 56 | If you pass a block to +r.message_bus+, it will be yielded to only 57 | if this is a message_bus request. 58 | 59 | r.message_bus do 60 | # executed right before passing control to message_bus 61 | end 62 | 63 | For roda-message_bus to work, in your MessageBus javascript code, you must 64 | set the +baseUrl+ to match the routing tree branch where you are calling 65 | +r.message_bus+: 66 | 67 | 68 | 71 | 72 | = License 73 | 74 | MIT 75 | 76 | = Author 77 | 78 | Jeremy Evans 79 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/clean" 2 | 3 | CLEAN.include ["roda-message_bus-*.gem", "rdoc", "coverage"] 4 | 5 | desc "Build thamble gem" 6 | task :package=>[:clean] do |p| 7 | sh %{#{FileUtils::RUBY} -S gem build roda-message_bus.gemspec} 8 | end 9 | 10 | ### Specs 11 | 12 | desc "Run specs" 13 | task :spec do 14 | sh "#{FileUtils::RUBY} #{"-w" if RUBY_VERSION >= '3'} #{'-W:strict_unused_block' if RUBY_VERSION >= '3.4'} spec/roda-message_bus_spec.rb" 15 | end 16 | 17 | task :default => :spec 18 | 19 | desc "Run specs with coverage" 20 | task :spec_cov do 21 | ENV['COVERAGE'] = '1' 22 | sh "#{FileUtils::RUBY} spec/roda-message_bus_spec.rb" 23 | end 24 | 25 | ### RDoc 26 | 27 | require "rdoc/task" 28 | 29 | RDoc::Task.new do |rdoc| 30 | rdoc.rdoc_dir = "rdoc" 31 | rdoc.options += ['--inline-source', '--line-numbers', '--title', 'roda-message_bus: MessageBus integration for Roda', '--main', 'README.rdoc', '-f', 'hanna'] 32 | rdoc.rdoc_files.add %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb" 33 | end 34 | 35 | -------------------------------------------------------------------------------- /lib/roda/plugins/message_bus.rb: -------------------------------------------------------------------------------- 1 | # frozen-string-literal: true 2 | 3 | require 'message_bus/rack/middleware' 4 | 5 | class Roda 6 | module RodaPlugins 7 | # The message_bus plugin allows for integrating the message_bus library into 8 | # Roda's routing tree. By default, MessageBus provides a Rack middlware to 9 | # work with any rack framework. However, that doesn't work well if you are 10 | # integrating access control into your routing tree. 11 | # 12 | # With the message_bus plugin, you can specify exactly where to pass control 13 | # to message_bus, which can be done after access controls have been checked. 14 | # Additionally, this allows to control which message_bus channels are allowed 15 | # for which requests, further enhancing security. 16 | # 17 | # It is still possible to use message_bus's user/group/site filtering when 18 | # using this support to filter allowed channels. 19 | # 20 | # # Use default MessageBus 21 | # plugin :message_bus 22 | # 23 | # # Use specific MessageBus implementation 24 | # plugin :message_bus, :message_bus=>MessageBus::Instance.new 25 | # 26 | # route do |r| 27 | # r.on "room/:id" do |room_id| 28 | # room_id = room_id.to_i 29 | # raise unless current_user.has_access?(room_id) 30 | # 31 | # # Uses "/room/#{room_id}" channel by default 32 | # r.message_bus 33 | # 34 | # # Override channel to use (can also provide array of channels) 35 | # r.message_bus("/room/#{room_id}/enters") 36 | # 37 | # # In addition to subscribing to channels, 38 | # # in Javascript on this page, set: 39 | # # 40 | # # MessageBus.baseUrl = "/room/<%= room_id %>/" 41 | # view('room') 42 | # end 43 | # end 44 | module MessageBus 45 | APP = proc{[404, {"Content-Type" => "text/html"}, ["Not Found"]]} 46 | 47 | def self.configure(app, config={}) 48 | app.opts[:message_bus_app] = ::MessageBus::Rack::Middleware.new(APP, config) 49 | end 50 | 51 | module ClassMethods 52 | def message_bus_app 53 | opts[:message_bus_app] 54 | end 55 | end 56 | 57 | module RequestMethods 58 | def message_bus(channels=nil) 59 | if remaining_path =~ /\A\/message-bus\// 60 | chans = env['message_bus.channels'] = {} 61 | post = self.POST 62 | channels ||= script_name + path_info.chomp(remaining_path) 63 | Array(channels).each do |channel| 64 | if val = post[channel] 65 | chans[channel] = val 66 | end 67 | end 68 | env['message_bus.seq'] = post['__seq'] 69 | yield if block_given? 70 | run roda_class.message_bus_app 71 | end 72 | end 73 | end 74 | end 75 | 76 | register_plugin(:message_bus, MessageBus) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /roda-message_bus.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'roda-message_bus' 3 | s.version = '1.0.0' 4 | s.platform = Gem::Platform::RUBY 5 | s.extra_rdoc_files = ["README.rdoc", "CHANGELOG", "MIT-LICENSE"] 6 | s.rdoc_options += ["--quiet", "--line-numbers", "--inline-source", '--title', 'roda-message_bus: MessageBus integration for Roda', '--main', 'README.rdoc'] 7 | s.license = "MIT" 8 | s.summary = "MessageBus integration for Roda" 9 | s.author = "Jeremy Evans" 10 | s.email = "code@jeremyevans.net" 11 | s.homepage = "https://github.com/jeremyevans/roda-message_bus" 12 | s.files = %w(MIT-LICENSE CHANGELOG README.rdoc) + Dir["lib/**/*.rb"] 13 | s.description = <=2.0.0') 19 | s.add_dependency('roda', '>=2.0.0') 20 | s.add_development_dependency('minitest') 21 | s.add_development_dependency "minitest-global_expectations" 22 | end 23 | -------------------------------------------------------------------------------- /spec/roda-message_bus_spec.rb: -------------------------------------------------------------------------------- 1 | if ENV.delete('COVERAGE') 2 | require 'simplecov' 3 | 4 | SimpleCov.start do 5 | enable_coverage :branch 6 | add_filter "/spec/" 7 | add_group('Missing'){|src| src.covered_percent < 100} 8 | add_group('Covered'){|src| src.covered_percent == 100} 9 | end 10 | end 11 | 12 | require 'roda' 13 | require 'message_bus' 14 | require 'json' 15 | ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins 16 | require 'minitest/global_expectations/autorun' 17 | 18 | $: << File.join(File.dirname(File.dirname(__FILE__)), 'lib') 19 | 20 | describe 'roda message_bus plugin' do 21 | def req(path, input={}, env={}) 22 | env = {"PATH_INFO"=>path, "REQUEST_METHOD" => "GET", "SCRIPT_NAME" => "", 'rack.input'=>true, 'rack.request.form_input'=>true, 'rack.request.form_hash'=>input}.merge(env) 23 | @app.call(env) 24 | end 25 | 26 | def body(path, input={}, env={}) 27 | s = String.new 28 | b = req(path, input, env)[2] 29 | b.each{|x| s << x} 30 | b.close if b.respond_to?(:close) 31 | s 32 | end 33 | 34 | def json_body(path, input={}, env={}) 35 | JSON.parse(body(path, input, env)) 36 | end 37 | 38 | before do 39 | @app = Class.new(Roda) 40 | @bus = MessageBus::Instance.new 41 | @bus.configure(:backend => :memory) 42 | @app.plugin :message_bus, :message_bus=>@bus 43 | @app 44 | end 45 | 46 | it "should handle message bus under a branch" do 47 | @app.route do |r| 48 | r.on "foo" do 49 | r.message_bus 50 | 'bar' 51 | end 52 | end 53 | 54 | body('/foo').must_equal 'bar' 55 | json_body('/foo/message-bus/1/poll', '/foo'=>'0', '__seq'=>1).must_equal [] 56 | @bus.publish '/foo', 'baz' 57 | json_body('/foo/message-bus/1/poll', '/foo'=>'0', '__seq'=>1).must_equal [{"global_id"=>1, "message_id"=>1, "channel"=>"/foo", "data"=>"baz"}] 58 | @bus.publish '/foo', 'baz1' 59 | json_body('/foo/message-bus/1/poll', '/foo'=>'0', '__seq'=>1).must_equal [{"global_id"=>1, "message_id"=>1, "channel"=>"/foo", "data"=>"baz"}, {"global_id"=>2, "message_id"=>2, "channel"=>"/foo", "data"=>"baz1"}] 60 | json_body('/foo/message-bus/1/poll', '/foo'=>'1', '__seq'=>1).must_equal [{"global_id"=>2, "message_id"=>2, "channel"=>"/foo", "data"=>"baz1"}] 61 | json_body('/foo/message-bus/1/poll', '/foo'=>'1').must_equal [{"global_id"=>2, "message_id"=>2, "channel"=>"/foo", "data"=>"baz1"}] 62 | json_body('/foo/message-bus/1/poll', '/bar'=>'0', '__seq'=>1).must_equal [] 63 | end 64 | 65 | it "should handle message bus with specific channels" do 66 | @app.route do |r| 67 | r.on "foo" do 68 | r.message_bus('/foo') 69 | 'bar' 70 | end 71 | end 72 | 73 | body('/foo').must_equal 'bar' 74 | json_body('/foo/message-bus/1/poll', '/foo'=>'0', '__seq'=>1).must_equal [] 75 | @bus.publish '/foo', 'baz' 76 | json_body('/foo/message-bus/1/poll', '/foo'=>'0', '__seq'=>1).must_equal [{"global_id"=>1, "message_id"=>1, "channel"=>"/foo", "data"=>"baz"}] 77 | @bus.publish '/foo', 'baz1' 78 | json_body('/foo/message-bus/1/poll', '/foo'=>'0', '__seq'=>1).must_equal [{"global_id"=>1, "message_id"=>1, "channel"=>"/foo", "data"=>"baz"}, {"global_id"=>2, "message_id"=>2, "channel"=>"/foo", "data"=>"baz1"}] 79 | json_body('/foo/message-bus/1/poll', '/foo'=>'1', '__seq'=>1).must_equal [{"global_id"=>2, "message_id"=>2, "channel"=>"/foo", "data"=>"baz1"}] 80 | json_body('/foo/message-bus/1/poll', '/foo'=>'1').must_equal [{"global_id"=>2, "message_id"=>2, "channel"=>"/foo", "data"=>"baz1"}] 81 | json_body('/foo/message-bus/1/poll', '/bar'=>'0', '__seq'=>1).must_equal [] 82 | end 83 | 84 | it "should support block passed to r.message_bus" do 85 | @app.route do |r| 86 | r.on "foo" do 87 | r.message_bus do 88 | r.POST['__seq'] = 1 89 | end 90 | "bar#{r.POST['__seq']}" 91 | end 92 | end 93 | 94 | body('/foo').must_equal 'bar' 95 | json_body('/foo/message-bus/1/poll', '/foo'=>'0').must_equal [] 96 | @bus.publish '/foo', 'baz' 97 | json_body('/foo/message-bus/1/poll', '/foo'=>'0').must_equal [{"global_id"=>1, "message_id"=>1, "channel"=>"/foo", "data"=>"baz"}] 98 | @bus.publish '/foo', 'baz1' 99 | json_body('/foo/message-bus/1/poll', '/foo'=>'0').must_equal [{"global_id"=>1, "message_id"=>1, "channel"=>"/foo", "data"=>"baz"}, {"global_id"=>2, "message_id"=>2, "channel"=>"/foo", "data"=>"baz1"}] 100 | json_body('/foo/message-bus/1/poll', '/foo'=>'1').must_equal [{"global_id"=>2, "message_id"=>2, "channel"=>"/foo", "data"=>"baz1"}] 101 | end 102 | end 103 | --------------------------------------------------------------------------------