├── .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 |
--------------------------------------------------------------------------------