pluggaloid-1.7.0/0000755000004100000410000000000014052673053013703 5ustar www-datawww-datapluggaloid-1.7.0/test/0000755000004100000410000000000014052673053014662 5ustar www-datawww-datapluggaloid-1.7.0/test/helper.rb0000644000004100000410000000035114052673053016465 0ustar www-datawww-data# -*- coding: utf-8 -*- module PluggaloidTestHelper def eval_all_events(delayer=Delayer, &block) native = Thread.list block.() if block delayer.run while not(delayer.empty? and (Thread.list - native).empty?) end end pluggaloid-1.7.0/test/collect_test.rb0000644000004100000410000000674614052673053017710 0ustar www-datawww-data# frozen_string_literal: true require 'bundler/setup' require 'minitest/autorun' require 'pluggaloid' require_relative 'helper' describe(Pluggaloid::Plugin) do include PluggaloidTestHelper before do Delayer.default = Delayer.generate_class(priority: %i, default: :normal) Pluggaloid::Plugin.clear! end it 'collect' do Pluggaloid::Plugin.create(:event) do defevent :list, prototype: [Integer, Pluggaloid::COLLECT] filter_list do |i, yielder| i.times(&yielder.method(:<<)) [i, yielder] end end assert_equal([0, 1, 2], Pluggaloid::Event[:list].collect(3).to_a) end it 'fail when collect with undefined' do Pluggaloid::Plugin.create(:event) do filter_list do |i, yielder| i.times(&yielder.method(:<<)) [i, yielder] end end assert_raises(Pluggaloid::UndefinedCollectionIndexError) do Pluggaloid::Event[:list].collect(3) end end it 'success caller arguments include Pluggaloid::COLLECT when collect with undefined' do Pluggaloid::Plugin.create(:event) do filter_list do |i, yielder| i.times(&yielder.method(:<<)) [i, yielder] end end assert_equal([0, 1, 2], Pluggaloid::Event[:list].collect(3, Pluggaloid::COLLECT).to_a) end it 'success different caller argument specific and prototype definition' do Pluggaloid::Plugin.create(:event) do defevent :list, prototype: [Integer, Pluggaloid::COLLECT] filter_list do |i, yielder| i.times(&yielder.method(:<<)) [i, yielder] end end assert_equal([0, 1, 2], Pluggaloid::Event[:list].collect(Pluggaloid::COLLECT, 3).to_a) end describe 'collection' do it 'add' do Pluggaloid::Plugin.create(:event) do defevent :list, prototype: [Integer, Pluggaloid::COLLECT] defevent :insert, prototype: [Pluggaloid::STREAM] collection(:list, 3) do |collector| subscribe(:insert).each do |i| collector.add(i * 3) end end end eval_all_events do Pluggaloid::Event[:insert].call([2, 5]) end assert_equal([6, 15], Pluggaloid::Event[:list].collect(3).to_a) end it 'delete' do Pluggaloid::Plugin.create(:event) do defevent :list, prototype: [Integer, Pluggaloid::COLLECT] defevent :destroy, prototype: [Pluggaloid::STREAM] collection(:list, 3) do |collector| collector << 1 << 2 << 3 subscribe(:destroy).each do |i| collector.delete(i) end end end eval_all_events do Pluggaloid::Event[:destroy].call([2, 4]) end assert_equal([1, 3], Pluggaloid::Event[:list].collect(3).to_a) end it 'rewind' do added = [] deleted = [] Pluggaloid::Plugin.create(:event) do defevent :list, prototype: [Integer, Pluggaloid::COLLECT] collection(:list, 3) do |collector| collector << 1 << 2 << 3 on_rewind do |e| collector.rewind do |lst| e end end end subscribe(:list__add, 3) do |elm| added += elm end subscribe(:list__delete, 3) do |elm| deleted += elm end end eval_all_events do Pluggaloid::Event[:rewind].call([2, 3, 4]) end assert_equal([2, 3, 4], Pluggaloid::Event[:list].collect(3).to_a) assert_equal([1, 2, 3, 4], added) assert_equal([1], deleted) end end end pluggaloid-1.7.0/test/reactive_test.rb0000644000004100000410000001524314052673053020055 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'bundler/setup' require 'minitest/autorun' require 'pluggaloid' require_relative 'helper' describe(Pluggaloid::Plugin) do include PluggaloidTestHelper before do Delayer.default = Delayer.generate_class(priority: %i, default: :normal) Pluggaloid::Plugin.clear! end it "subscribe" do sum = [] Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1) do |v| sum = v end end eval_all_events do Pluggaloid::Event[:increase].call(1, [:one]) Pluggaloid::Event[:increase].call(2, [:two]) end assert_equal(%i[one], sum) end it "subscribe 2" do sum = [] Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Pluggaloid::STREAM, Integer] subscribe(:increase, 1) do |v| sum = v end end eval_all_events do Pluggaloid::Event[:increase].call([:one], 1) Pluggaloid::Event[:increase].call([:two], 2) end assert_equal(%i[one], sum) end it "raises subscribe without definition" do assert_raises Pluggaloid::UndefinedStreamIndexError do Pluggaloid::Plugin.create(:event) do subscribe(:increase, 1) { ; } end end end it "subscribe? first value" do Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1) do |v| end end assert(Pluggaloid::Plugin[:event].subscribe?(:increase, 1)) refute(Pluggaloid::Plugin[:event].subscribe?(:increase, 2)) end it "subscribe? last value" do Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Pluggaloid::STREAM, Integer] subscribe(:increase, 1) do |v| end end assert(Pluggaloid::Plugin[:event].subscribe?(:increase, 1)) refute(Pluggaloid::Plugin[:event].subscribe?(:increase, 2)) end it "subscribe? returns always true if plugin listener exist" do Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] on_increase do |i, y| end end assert(Pluggaloid::Plugin[:event].subscribe?(:increase, 1)) assert(Pluggaloid::Plugin[:event].subscribe?(:increase, 2)) end it "subscribe enumerable" do sum = [] Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1).each do |v| sum = v end end eval_all_events do Pluggaloid::Event[:increase].call(1, [:one]) Pluggaloid::Event[:increase].call(2, [:two]) end assert_equal(:one, sum) end it "detach" do sum = 0 subscriber = nil Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Pluggaloid::STREAM] subscriber = subscribe(:increase) do |v| sum += v end end eval_all_events do Pluggaloid::Event[:increase].call(1) end assert_equal(1, sum, "It should execute subscriber when event called") eval_all_events do Pluggaloid::Plugin[:event].detach subscriber Pluggaloid::Event[:increase].call(1) end assert_equal(1, sum, "It should not execute detached subscriber when event called") end it "subscribe enumerable chain" do sum = [] Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1).map{|v| v.to_s }.each do |v| sum << v end end eval_all_events do Pluggaloid::Event[:increase].call(1, [:one]) Pluggaloid::Event[:increase].call(2, [:two]) Pluggaloid::Event[:increase].call(1, [:three]) end assert_equal(%w[one three], sum) end it "subscribe each_slice" do sum = [] Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1).each_slice(3).each do |v| sum << v end end eval_all_events do Pluggaloid::Event[:increase].call(1, 100.times) end assert_equal([0, 1, 2], sum.first) assert_equal([96, 97, 98], sum.last) assert_equal(33, sum.size) eval_all_events do Pluggaloid::Event[:increase].call(1, [100, 101]) end assert_equal([99, 100, 101], sum.last) assert_equal(34, sum.size) end it "throttle" do sum = [] Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1).throttle(0.001).each do |v| sum << v end end eval_all_events do Pluggaloid::Event[:increase].call(1, (1..10).to_a) Pluggaloid::Event[:increase].call(1, (11..20).to_a) end assert_equal(1, sum.last) assert_equal(1, sum.size) sleep 0.01 eval_all_events do Pluggaloid::Event[:increase].call(1, (21..30).to_a) end assert_equal(21, sum.last) assert_equal(2, sum.size) end it "debounce" do sum = [] Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1).debounce(0.01).each do |v| sum << v end end eval_all_events do Pluggaloid::Event[:increase].call(1, (1..10).to_a) Pluggaloid::Event[:increase].call(1, (11..20).to_a) end assert_equal(0, sum.size) sleep 0.02 Delayer.run assert_equal(1, sum.size) assert_equal(20, sum.last) end it "buffer" do sum = [] Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1).buffer(0.01).each do |v| sum << v end end eval_all_events do Pluggaloid::Event[:increase].call(1, (1..10).to_a) Pluggaloid::Event[:increase].call(1, (11..20).to_a) end assert_equal(0, sum.size) sleep 0.1 Delayer.run Pluggaloid::Event[:increase].call(1, [21]) Delayer.run assert_equal(1, sum.size) assert_equal((1..20).to_a, sum.last) end it "merge" do sum = sum1 = sum2 = 0 Pluggaloid::Plugin.create(:event) do defevent :increase, prototype: [Integer, Pluggaloid::STREAM] subscribe(:increase, 1).each do |v| sum1 += v end subscribe(:increase, 2).each do |v| sum2 += v end subscribe(:increase, 1).merge(subscribe(:increase, 2)).each do |v| sum += v end end eval_all_events do Pluggaloid::Event[:increase].call(1, (1..10).to_a) Pluggaloid::Event[:increase].call(2, (11..20).to_a) end assert_equal((1..10).to_a.sum, sum1) assert_equal((11..20).to_a.sum, sum2) assert_equal((1..20).to_a.sum, sum) end end pluggaloid-1.7.0/test/handler_tag_test.rb0000644000004100000410000000665014052673053020525 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'bundler/setup' require 'minitest/autorun' require 'pluggaloid' require_relative 'helper' describe(Pluggaloid::HandlerTag) do include PluggaloidTestHelper before do Delayer.default = Delayer.generate_class(priority: %i, default: :normal) Pluggaloid::Plugin.clear! @pluggaloid = Pluggaloid.new(Delayer.default) end describe 'plugin tag' do before do a = b = c = d = fd = tag_a = tag_b = tag_c = tag_d = nil lst = @lst = [] @plugin = @pluggaloid.Plugin.create :parent do a = on_a{ lst << :a } tag_a = handler_tag :tag_a tag_b = handler_tag :tag_b tag_c = handler_tag :tag_c tag_d = handler_tag :tag_d b = on_b(tags: tag_a){ lst << :b } c = on_c(tags: [tag_a, tag_b]){ lst << :c } handler_tag tag_d do d = on_d(tags: [tag_b]){ lst << :d } handler_tag tag_c do fd = filter_d{|&stop| stop.call } end end end @a, @b, @c, @d, @fd = a, b, c, d, fd @tag_a, @tag_b, @tag_c, @tag_d = tag_a, tag_b, tag_c, tag_d end it 'plugin has 4 listeners' do assert_equal 4, @plugin.listeners.count end it 'listener a has no tag' do assert_empty @a.tags end it 'listener b has tag_a' do assert_equal Set.new([@tag_a]), @b.tags end it 'listener c has tag_a and tag_b' do assert_equal Set.new([@tag_a, @tag_b]), @c.tags end it 'listener d has tag_b and tag_d' do assert_equal Set.new([@tag_b, @tag_d]), @d.tags end it 'filter fd has tag_c and tag_d' do assert_equal Set.new([@tag_c, @tag_d]), @fd.tags end describe 'detach tag_a' do before do @plugin.detach(@tag_a) @pluggaloid.Event[:a].call @pluggaloid.Event[:b].call @pluggaloid.Event[:c].call eval_all_events(@pluggaloid.Delayer) end it 'should not call event in tag' do assert_equal [:a], @lst end end describe 'detach tag_c' do before do @pluggaloid.Event[:a].call @pluggaloid.Event[:b].call @pluggaloid.Event[:c].call @pluggaloid.Event[:d].call end describe 'detach' do before do @plugin.detach(@tag_c) eval_all_events(@pluggaloid.Delayer) end it 'should not call filter' do assert_equal [:a, :b, :c, :d], @lst end end describe 'does not detach' do before do eval_all_events(@pluggaloid.Delayer) end it 'should call filter' do assert_equal [:a, :b, :c], @lst end end end describe 'each' do it 'should count of listeners and filters are 2' do assert_equal 2, @tag_a.count end it 'should count of listeners are 2 in tag_a' do assert_equal 2, @tag_a.listeners.count end it 'should there are no filter in tag_a' do assert_equal 0, @tag_a.filters.count end it 'should count of listeners are 2 in tag_b' do assert_equal 2, @tag_b.listeners.count end it 'should there are no filter in tag_b' do assert_equal 0, @tag_b.filters.count end it 'should there are no listener in tag_c' do assert_equal 0, @tag_c.listeners.count end it 'should has one filter in tag_c' do assert_equal 1, @tag_c.filters.count end end end end pluggaloid-1.7.0/test/handle_in_generate_test.rb0000644000004100000410000000250014052673053022036 0ustar www-datawww-data# frozen_string_literal: true require 'bundler/setup' require 'minitest/autorun' require 'pluggaloid' require_relative 'helper' describe(Pluggaloid::Plugin) do include PluggaloidTestHelper before do Delayer.default = Delayer.generate_class(priority: %i, default: :normal) Pluggaloid::Plugin.clear! end it 'hoge' do refute Pluggaloid::Event[:tick].subscribe? end describe 'event' do before do log = @log = [] listener = nil Pluggaloid::Plugin.create(:event) do defevent :list, prototype: [Integer, Pluggaloid::STREAM] generate(:list, 1) do |yielder| log << [:g1, :start] on_tick do |digit| log << [:g1, digit] yielder << digit end end listener = subscribe(:list, 1) do |stream| ; end end @listener = listener eval_all_events end it 'subscribed event which define in generate block' do assert Pluggaloid::Event[:tick].subscribe? end describe 'unsubscribe generate stream' do before do Pluggaloid::Plugin.create(:event).detach(@listener) eval_all_events end it 'unsubscribed event which define in generate block' do refute Pluggaloid::Event[:tick].subscribe? end end end end pluggaloid-1.7.0/test/mirage_test.rb0000644000004100000410000000666314052673053017525 0ustar www-datawww-data# frozen_string_literal: true require 'bundler/setup' require 'minitest/autorun' require 'pluggaloid' require_relative 'helper' describe(Pluggaloid::Plugin) do include PluggaloidTestHelper it 'resume' do k = Class.new do include Pluggaloid::Mirage end a = k.new assert_equal(a, Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: a.pluggaloid_mirage_id)) end it 'custom mirage id' do k = Class.new do include Pluggaloid::Mirage def initialize(a) @a = a end def generate_pluggaloid_mirage_id @a.to_s end end a = k.new('same') b = k.new('differ') assert_equal('same', a.pluggaloid_mirage_id) assert_equal(a, Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: a.pluggaloid_mirage_id)) assert_equal('differ', b.pluggaloid_mirage_id) assert_equal(b, Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: b.pluggaloid_mirage_id)) end it 'not confuse other mirage classese' do definition = ->(*) do include Pluggaloid::Mirage def initialize(a) @a = a end def generate_pluggaloid_mirage_id @a.to_s end end k = Class.new(&definition) l = Class.new(&definition) a = k.new('same') b = l.new('same') c = l.new('differ') refute_equal(k.to_s, l.to_s) assert_equal('same', a.pluggaloid_mirage_id) assert_equal('same', b.pluggaloid_mirage_id) assert_equal('differ', c.pluggaloid_mirage_id) assert_equal(b, Pluggaloid::Mirage.unwrap(namespace: l.to_s, id: 'same')) assert_equal(c, Pluggaloid::Mirage.unwrap(namespace: l.to_s, id: 'differ')) assert_equal(a, Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: 'same')) assert_raises(Pluggaloid::ArgumentError) { Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: 'differ') } end it 'inherit class' do k = Class.new do include Pluggaloid::Mirage def initialize(a) @a = a end def generate_pluggaloid_mirage_id @a.to_s end end l = Class.new(k) a = k.new('same') b = l.new('same') c = l.new('differ') refute_equal(k.to_s, l.to_s) assert_equal('same', a.pluggaloid_mirage_id) assert_equal('same', b.pluggaloid_mirage_id) assert_equal('differ', c.pluggaloid_mirage_id) assert_equal(b, Pluggaloid::Mirage.unwrap(namespace: l.to_s, id: 'same')) assert_equal(c, Pluggaloid::Mirage.unwrap(namespace: l.to_s, id: 'differ')) assert_equal(a, Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: 'same')) assert_raises(Pluggaloid::ArgumentError) { Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: 'differ') } end it 'inherit before include mirage' do k = Class.new do def initialize(a) @a = a end def generate_pluggaloid_mirage_id @a.to_s end end l = Class.new(k) k.include Pluggaloid::Mirage a = k.new('same') b = l.new('same') c = l.new('differ') refute_equal(k.to_s, l.to_s) assert_equal('same', a.pluggaloid_mirage_id) assert_equal('same', b.pluggaloid_mirage_id) assert_equal('differ', c.pluggaloid_mirage_id) assert_equal(b, Pluggaloid::Mirage.unwrap(namespace: l.to_s, id: 'same')) assert_equal(c, Pluggaloid::Mirage.unwrap(namespace: l.to_s, id: 'differ')) assert_equal(a, Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: 'same')) assert_raises(Pluggaloid::ArgumentError) { Pluggaloid::Mirage.unwrap(namespace: k.to_s, id: 'differ') } end end pluggaloid-1.7.0/test/generate_test.rb0000644000004100000410000000546314052673053020050 0ustar www-datawww-data# frozen_string_literal: true require 'bundler/setup' require 'minitest/autorun' require 'pluggaloid' require_relative 'helper' describe(Pluggaloid::Plugin) do include PluggaloidTestHelper before do Delayer.default = Delayer.generate_class(priority: %i, default: :normal) Pluggaloid::Plugin.clear! end describe 'event' do before do log = @log = [] Pluggaloid::Plugin.create(:event) do defevent :list, prototype: [Integer, Pluggaloid::STREAM] generate(:list, 1) do |yielder| log << [:g1, :start] on_tick do |digit| log << [:g1, digit] yielder << digit end end end end it 'does not call generate block in not subscribed' do assert_equal([], @log) end it 'does not call generate block in subscribed other caller arguments' do Pluggaloid::Plugin.create(:event) do subscribe(:list, 2) {} end assert_equal([], @log) end describe 'after subscribe' do before do listener = nil Pluggaloid::Plugin.create(:event) do listener = subscribe(:list, 1) {} end @listener = listener eval_all_events end it 'call generate block' do assert_equal([[:g1, :start]], @log) end it 'call once generate block if subscribe twice' do Pluggaloid::Plugin.create(:event) do subscribe(:list, 1) {} end eval_all_events assert_equal([[:g1, :start]], @log) end describe 'detach all listeners' do before do Pluggaloid::Plugin.create(:event).detach(@listener) Pluggaloid::Plugin.call(:event, 1, [69]) eval_all_events end it 'should detach handlers in generate block after unsubscribed' do assert_equal([[:g1, :start]], @log) end end end describe 'after add_event_listener' do before do listener = nil Pluggaloid::Plugin.create(:event) do listener = on_list { |_num, _lst| ; } end @listener = listener eval_all_events end it 'call generate block' do assert_equal([[:g1, :start]], @log) end it 'call once generate block if subscribe twice' do Pluggaloid::Plugin.create(:event) do on_list { |_num, _lst| ; } end eval_all_events assert_equal([[:g1, :start]], @log) end describe 'detach all listeners' do before do Pluggaloid::Plugin.create(:event).detach(@listener) Pluggaloid::Plugin.call(:event, 1, [105]) eval_all_events end it 'detach handlers in generate block after unsubscribed' do assert_equal([[:g1, :start]], @log) end end end end end pluggaloid-1.7.0/test/multi_vm_test.rb0000644000004100000410000000357014052673053020107 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'bundler/setup' require 'minitest/autorun' require 'pluggaloid' require_relative 'helper' describe(Pluggaloid) do include PluggaloidTestHelper before do Delayer.default = Delayer.generate_class(priority: %i, default: :normal) Pluggaloid::Plugin.clear! end it "should take new plugin classes" do pluggaloid = Pluggaloid.new(Delayer.default) assert_equal Delayer.default, pluggaloid.Delayer, "" assert_includes(pluggaloid.Plugin.ancestors, Pluggaloid::Plugin, "Plugin should subbclass of Pluggaloid::Plugin") assert_includes(pluggaloid.Event.ancestors, Pluggaloid::Event, "Event should subbclass of Pluggaloid::Event") assert_includes(pluggaloid.Listener.ancestors, Pluggaloid::Listener, "Listener should subbclass of Pluggaloid::Listener") assert_includes(pluggaloid.Filter.ancestors, Pluggaloid::Filter, "Filter should subbclass of Pluggaloid::Filter") end it "call event in new vm" do vm_a = Pluggaloid.new(Delayer.generate_class(priority: %i, default: :normal)) vm_b = Pluggaloid.new(Delayer.generate_class(priority: %i, default: :normal)) foo = bar = false vm_a.Plugin.create(:foo_plugin) do on_foo do foo = true end end vm_b.Plugin.create(:bar_plugin) do on_bar do bar = true end end assert_equal(%i, vm_a.Plugin.instances_name) assert_equal(%i, vm_b.Plugin.instances_name) eval_all_events(vm_a.Delayer) do vm_a.Plugin.call(:foo) vm_a.Plugin.call(:bar) end assert foo, "Event foo should be called" refute bar, "Event bar should not be called" foo = bar = false eval_all_events(vm_b.Delayer) do vm_b.Plugin.call(:foo) vm_b.Plugin.call(:bar) end refute foo, "Event foo should not be called" assert bar, "Event bar should be called" end end pluggaloid-1.7.0/test/plugin_test.rb0000644000004100000410000001435314052673053017552 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'bundler/setup' require 'minitest/autorun' require 'pluggaloid' require_relative 'helper' describe(Pluggaloid::Plugin) do include PluggaloidTestHelper before do Delayer.default = Delayer.generate_class(priority: %i, default: :normal) Pluggaloid::Plugin.clear! end it "fire and filtering event, receive a plugin" do sum = 0 Pluggaloid::Plugin.create(:event) do on_increase do |v| sum += v end filter_increase do |v| [v * 2] end end eval_all_events do Pluggaloid::Event[:increase].call(1) end assert_equal(2, sum) end it "filter in another thread" do success = filter_thread = nil Pluggaloid::Plugin.create(:event) do on_thread do end on_unhandled do success = true end filter_thread do filter_thread = Thread.current [] end end eval_all_events do Pluggaloid::Event[:thread].call end assert filter_thread assert_equal Thread.current, filter_thread Pluggaloid::Event.filter_another_thread = true filter_thread = nil eval_all_events do Pluggaloid::Event[:thread].call Pluggaloid::Event[:unhandled].call end assert filter_thread, "The filter doesn't run." assert success, "Event :unhandled doesn't run." refute_equal Thread.current, filter_thread, 'The filter should execute in not a main thread' end it "uninstall" do sum = 0 Pluggaloid::Plugin.create(:event) do on_increase do |v| sum += v end filter_increase do |v| [v * 2] end end eval_all_events do Pluggaloid::Plugin.create(:event).uninstall Pluggaloid::Event[:increase].call(1) end assert_equal(0, sum) end it "detach" do sum = 0 event = filter = nil Pluggaloid::Plugin.create(:event) do event = on_increase do |v| sum += v end filter = filter_increase do |v| [v * 2] end end eval_all_events do Pluggaloid::Event[:increase].call(1) end assert_equal(2, sum, "It should execute filter when event called") eval_all_events do Pluggaloid::Plugin[:event].detach filter Pluggaloid::Event[:increase].call(1) end assert_equal(3, sum, "It should not execute detached filter when event called") eval_all_events do Pluggaloid::Plugin.create(:event).detach event Pluggaloid::Event[:increase].call(1) end assert_equal(3, sum, "It should not executed detached event") end it "get plugin list" do assert_empty(Pluggaloid::Plugin.plugin_list, "Plugin list must empty in first time") Pluggaloid::Plugin.create(:plugin_0) assert_equal(%i, Pluggaloid::Plugin.plugin_list, "The new plugin should appear plugin list") Pluggaloid::Plugin.create(:plugin_1) assert_equal(%i, Pluggaloid::Plugin.plugin_list, "The new plugin should appear plugin list") end it "dsl method defevent" do Pluggaloid::Plugin.create :defevent do defevent :increase, prototype: [Integer] end assert_equal([Integer], Pluggaloid::Event[:increase].options[:prototype]) assert_equal(Pluggaloid::Plugin[:defevent], Pluggaloid::Event[:increase].options[:plugin]) end describe "unload hook" do before do @value = value = [] Pluggaloid::Plugin.create(:temporary) { on_unload { value << 2 } on_unload { value << 1 } } Pluggaloid::Plugin.create(:eternal) { on_unload { raise "try to unload eternal plugin" } } end it 'should not call unload event it is not unload' do assert_empty(@value) end describe 'unload temporary plugin' do before do eval_all_events do Pluggaloid::Plugin.create(:temporary).uninstall end end it 'was called unload hooks' do assert_equal([1, 2].sort, @value.sort) end end end describe "defdsl" do it "simple dsl" do Pluggaloid::Plugin.create :dsl_def do defdsl :twice do |number| number * 2 end end dsl_use = Pluggaloid::Plugin[:dsl_use] assert_equal(4, dsl_use.twice(2)) assert_equal(0, dsl_use.twice(0)) assert_equal(-26, dsl_use.twice(-13)) end it "callback dsl" do Pluggaloid::Plugin.create :dsl_def do defdsl :rejector do |value, &condition| value.reject(&condition) end end dsl_use = Pluggaloid::Plugin.create(:dsl_use) assert_equal([2, 4, 6], dsl_use.rejector(1..6){ |d| 0 != (d & 1) }) end end it 'raises NoDefaultDelayerError if Delayer do not have default delayer' do Delayer.default = nil Pluggaloid::Plugin.clear! assert_raises(Pluggaloid::NoDefaultDelayerError) do Pluggaloid::Plugin.create(:raises) do on_no_default_delayer_error do; end end end end describe "named listener" do it 'raises when duplicate slug registered to equally events' do assert_raises(Pluggaloid::DuplicateListenerSlugError) do Pluggaloid::Plugin.create :duplicate_slug do on_a(slug: :duplicate){} on_a(slug: :duplicate){} end end end it 'was not raises when duplicate slug registered to another events' do Pluggaloid::Plugin.create :duplicate_slug do on_a(slug: :duplicate){} on_b(slug: :duplicate){} end end it 'successful when dupliacte name registered to another events' do a = b = nil Pluggaloid::Plugin.create :duplicate_name do a = on_a(name: "duplicate"){} b = on_b(name: "duplicate"){} end assert_equal("duplicate", a.name, 'a.name should be "duplicate"') assert_equal("duplicate", b.name, 'b.name should be "duplicate"') end it 'include listener slug and name in Pluggaloid::Listener#inspect' do a = nil Pluggaloid::Plugin.create :inspect do a = on_a(slug: :inspect_slug, name: "inspect name"){} end assert(a.inspect.include?("inspect_slug"), 'Pluggaloid::Listener.inspect does not include slug') assert(a.inspect.include?("inspect name"), 'Pluggaloid::Listener.inspect does not include name') end end it 'call undefined method in plugin context' do assert_raises(NameError) do Pluggaloid::Plugin.create(:raises) do undefined_call end end end end pluggaloid-1.7.0/README.md0000644000004100000410000000505214052673053015164 0ustar www-datawww-data# Pluggaloid mikutterのプラグイン機構です。 登録したプラグイン同士がイベントを使って通信できるようになります。 [![toshia](https://circleci.com/gh/toshia/pluggaloid.svg?style=svg)](https://circleci.com/gh/toshia/pluggaloid) ## Installation Add this line to your application's Gemfile: ```ruby gem 'pluggaloid' ``` And then execute: $ bundle Or install it yourself as: $ gem install pluggaloid ## Usage ```ruby require 'pluggaloid' main = Pluggaloid.new(Delayer.generate_class(priority: %i, default: :normal)) main.Plugin.create(:write_to_stdout) do on_logging do |message| puts "logging: #{message}" end end main.Plugin.call(:logging, "boot.") main.Plugin.call(:logging, "event test.") main.Plugin.call(:logging, "exit.") main.Delayer.run while not main.Delayer.empty? ``` ### Pluggaloid::new Pluggaloid::newは、プラグイン機構を制御するためのDelayer, Plugin, Event, Listener, Filterのサブクラスを新しく作って返すメソッドです。 戻り値はStructで、それぞれのクラス名がメンバの名前になっています。 | Member | Description | |----------|-------------------------------------| | Delayer | Pluggaloid::new に渡したDelayer | | Plugin | Pluggaloid::Plugin のサブクラス | | Event | Pluggaloid::Event のサブクラス | | Listener | Pluggaloid::Listener のサブクラス | | Filter | Pluggaloid::Filter のサブクラス | コンストラクタの唯一の引数には `Delayer.generate_class(priority: %i, default: :normal)`のように、優先順位付きでデフォルト優先度が設定されたDelayerを渡します。 ## Reactive Filter ### each_slice(times) _times_ 要素ずつブロックに渡して繰り返します。 要素数が _times_ で割り切れないときは、要素が _times_ 個になるまで待ちます。 ### throttle(sec) 最後に要素を受信してから、 _sec_ 秒の間に受信した要素を捨てます。 ### debounce(sec) _sec_ 秒要素を受信しなかった場合、最後の要素を送信する。 ### buffer(sec) _sec_ 秒の間に受信した要素を、 _sec_ 秒ごとに配列にまとめて送信する。 ## Contributing 1. Fork it ( https://github.com/toshia/pluggaloid/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request pluggaloid-1.7.0/.circleci/0000755000004100000410000000000014052673053015536 5ustar www-datawww-datapluggaloid-1.7.0/.circleci/config.yml0000644000004100000410000000151414052673053017527 0ustar www-datawww-dataversion: '2.1' executors: ruby: parameters: tag: type: string docker: - image: circleci/ruby:<< parameters.tag >> jobs: build: parameters: ruby-version: type: string executor: name: ruby tag: << parameters.ruby-version >> steps: - checkout - run: name: Which bundler? command: bundle -v - run: command: bundle install --path vendor/bundle - run: name: test command: bundle exec rake test workflows: build: jobs: - build: name: 'ruby-2.5' ruby-version: '2.5.9' - build: name: 'ruby-2.6' ruby-version: '2.6.7' - build: name: 'ruby-2.7' ruby-version: '2.7.3' - build: name: 'ruby-3.0' ruby-version: '3.0.1' pluggaloid-1.7.0/.gitignore0000644000004100000410000000017514052673053015676 0ustar www-datawww-data/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log vendor/pluggaloid-1.7.0/Rakefile0000644000004100000410000000025614052673053015353 0ustar www-datawww-datarequire "bundler/gem_tasks" require "rake/testtask" Rake::TestTask.new do |t| t.test_files = FileList['test/*_test.rb'] t.warning = true t.verbose = true end pluggaloid-1.7.0/lib/0000755000004100000410000000000014052673053014451 5ustar www-datawww-datapluggaloid-1.7.0/lib/pluggaloid.rb0000644000004100000410000000220414052673053017123 0ustar www-datawww-datarequire "pluggaloid/version" require 'pluggaloid/collection' require "pluggaloid/plugin" require 'pluggaloid/stream' require 'pluggaloid/event' require "pluggaloid/identity" require "pluggaloid/handler" require 'pluggaloid/listener' require 'pluggaloid/subscriber' require 'pluggaloid/filter' require 'pluggaloid/stream_generator' require "pluggaloid/handler_tag" require 'pluggaloid/mirage' require 'pluggaloid/error' require 'delayer' module Pluggaloid VM = Struct.new(*%i, keyword_init: true) class PrototypeStream; end class PrototypeCollect; end STREAM = PrototypeStream.new.freeze COLLECT = PrototypeCollect.new.freeze def self.new(delayer) vm = VM.new(Delayer: delayer, Plugin: Class.new(Plugin), Event: Class.new(Event), Listener: Class.new(Listener), Filter: Class.new(Filter), HandlerTag: Class.new(HandlerTag), Subscriber: Class.new(Subscriber), StreamGenerator: Class.new(StreamGenerator)) vm.Plugin.vm = vm.Event.vm = vm end end pluggaloid-1.7.0/lib/pluggaloid/0000755000004100000410000000000014052673053016600 5ustar www-datawww-datapluggaloid-1.7.0/lib/pluggaloid/version.rb0000644000004100000410000000005214052673053020607 0ustar www-datawww-datamodule Pluggaloid VERSION = '1.7.0' end pluggaloid-1.7.0/lib/pluggaloid/handler_tag.rb0000644000004100000410000000505714052673053021404 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'securerandom' =begin rdoc = リスナをまとめて管理するプラグイン Pluggaloid::Listener や、 Pluggaloid::Filter をまとめて扱うための仕組み。 Pluggaloid::Plugin#add_event などの引数 _tags:_ に、このインスタンスを設定する。 == インスタンスの作成 Pluggaloid::Plugin#handler_tag を使って生成する。 Pluggaloid::HandlerTag の _plugin:_ 引数には、レシーバ(Pluggaloid::Plugin)が渡される。 Pluggaloid::HandlerTag は、このプラグインの中でだけ使える。複数のプラグインのリスナ をまとめて管理することはできない。 == リスナにタグをつける Pluggaloid::Plugin#add_event または Pluggaloid::Plugin#add_event_filter の _tags:_ 引数にこれのインスタンスを渡す。 == このタグがついたListenerやFilterを取得する Enumerable をincludeしていて、リスナやフィルタを取得することができる。 また、 - Pluggaloid::HandlerTag#listeners で、 Pluggaloid::Listener だけ - Pluggaloid::HandlerTag#filters で、 Pluggaloid::Filter だけ を対象にした Enumerator を取得することができる == このタグがついたリスナを全てdetachする Pluggaloid::Plugin#detach の第一引数に Pluggaloid::HandlerTag の インスタンスを渡すことで、そのHandlerTagがついたListener、Filterは全てデタッチ される =end class Pluggaloid::HandlerTag < Pluggaloid::Identity include Enumerable # ==== Args # [name:] タグの名前(String | nil) def initialize(plugin:, **kwrest) super(**kwrest) @plugin = plugin end # このTagがついている Pluggaloid::Listener と Pluggaloid::Filter を全て列挙する # ==== Return # Enumerable def each(&block) if block Enumerator.new do |y| listeners{|x| y << x } filters{|x| y << x } end.each(&block) else Enumerator.new do |y| listeners{|x| y << x } filters{|x| y << x } end end end # このTagがついている Pluggaloid::Listener を全て列挙する # ==== Return # Enumerable def listeners(&block) if block listeners.each(&block) else @plugin.to_enum(:listeners).lazy.select{|l| l.tags.include?(self) } end end # このTagがついている Pluggaloid::Filter を全て列挙する # ==== Return # Enumerable def filters(&block) if block filters.each(&block) else @plugin.to_enum(:filters).lazy.select{|l| l.tags.include?(self) } end end end pluggaloid-1.7.0/lib/pluggaloid/listener.rb0000644000004100000410000000156514052673053020761 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'securerandom' require 'set' class Pluggaloid::Listener < Pluggaloid::Handler # プラグインコールバックをこれ以上実行しない。 def self.cancel! throw :plugin_exit, false end # ==== Args # [event] 監視するEventのインスタンス # [name:] 名前(String | nil) # [slug:] イベントリスナスラッグ(Symbol | nil) # [tags:] Pluggaloid::HandlerTag|Array リスナのタグ # [&callback] コールバック def initialize(event, **kwrest, &callback) super @callback = callback event.add_listener(self) end # イベントを実行する # ==== Args # [*args] イベントの引数 def call(*args) @callback.call(*args, &self.class.method(:cancel!)) end # このリスナを削除する # ==== Return # self def detach @event.delete_listener(self) self end end pluggaloid-1.7.0/lib/pluggaloid/stream_generator.rb0000644000004100000410000000370014052673053022466 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'securerandom' require 'set' class Pluggaloid::StreamGenerator < Pluggaloid::Handler attr_reader :accepted_hash def initialize(event, *specs, plugin:, **kwrest, &callback) raise Pluggaloid::UndefinedStreamIndexError, 'To call generate(%{event}), it must define prototype arguments include `Pluggaloid::STREAM\'.' % {event: event.name} unless event.stream_index super(event, **kwrest) @callback = callback @specs = specs.freeze @accepted_hash = @event.argument_hash(specs, nil) @last_subscribe_state = @event.subscribe?(*@specs) @plugin = plugin subscribe_start if @last_subscribe_state @event.register_stream_generator(self) end def on_subscribed if !@last_subscribe_state @last_subscribe_state = true subscribe_start end end def on_unsubscribed subscribe_state = @event.subscribe_hash?(@accepted_hash) if @last_subscribe_state && !subscribe_state @last_subscribe_state = false subscribe_stop end end # このリスナを削除する # ==== Return # self def detach @event.delete_stream_generator(self) @yielder&.die @yielder = nil self end private def subscribe_start @tag = @plugin.handler_tag do @yielder = Yielder.new(@event, args: @specs) @callback.call(@yielder) end end def subscribe_stop @plugin.detach(@tag) @yielder.die @yielder = nil end class Yielder def initialize(event, args:) @event = event @args = args.freeze @alive = true end def bulk_add(lst) raise Pluggaloid::NoReceiverError, "All event listener of #{self.class} already detached." if die? args = @args.dup args.insert(@event.stream_index, lst) @event.call(*args) end def add(value) bulk_add([value]) end alias_method :<<, :add def die? !@alive end def die @alive = false freeze end end end pluggaloid-1.7.0/lib/pluggaloid/plugin.rb0000644000004100000410000002471014052673053020427 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'instance_storage' require 'delayer' require 'securerandom' require 'set' # プラグインの本体。 # DSLを提供し、イベントやフィルタの管理をする module Pluggaloid class Plugin include InstanceStorage class << self attr_writer :vm def vm @vm ||= begin raise Pluggaloid::NoDefaultDelayerError, "Default Delayer was not set." unless Delayer.default vm = Pluggaloid::VM.new( Delayer: Delayer.default, Plugin: self, Event: Pluggaloid::Event, Listener: Pluggaloid::Listener, Filter: Pluggaloid::Filter, HandlerTag: Pluggaloid::HandlerTag, Subscriber: Pluggaloid::Subscriber, StreamGenerator: Pluggaloid::StreamGenerator ) vm.Event.vm = vm end end # プラグインのインスタンスを返す。 # ブロックが渡された場合、そのブロックをプラグインのインスタンスのスコープで実行する # ==== Args # [plugin_name] プラグイン名 # ==== Return # Plugin def create(plugin_name, &body) self[plugin_name].instance_eval(&body) if body self[plugin_name] end # イベントを宣言する。 # ==== Args # [event_name] イベント名 # [options] 以下のキーを持つHash # :prototype :: 引数の数と型。Arrayで、type_strictが解釈できる条件を設定する # :priority :: Delayerの優先順位 def defevent(event_name, options = {}) vm.Event[event_name].options = options end # イベント _event_name_ を発生させる # ==== Args # [event_name] イベント名 # [*args] イベントの引数 # ==== Return # Delayer def call(event_name, *args) vm.Event[event_name].call(*args) end # 引数 _args_ をフィルタリングした結果を返す # ==== Args # [*args] 引数 # ==== Return # フィルタされた引数の配列 def filtering(event_name, *args) vm.Event[event_name].filtering(*args) end # フィルタ _event_name_ を実行し、defeventでPluggaloid::COLLECTと # 宣言されている引数の結果を列挙するEnumeratorを返す # ==== Args # [event_name] イベント名(String | Symbol) # [*specs] Pluggaloid::COLLECT以外の引数 # ==== Return # [Enumerator] def collect(event_name, *specs) vm.Event[event_name].collect(*specs) end # 互換性のため def uninstall(plugin_name) self[plugin_name].uninstall end # 互換性のため def filter_cancel! vm.Filter.cancel! end alias plugin_list instances_name alias __clear_aF4e__ clear! def clear! if defined?(@vm) and @vm @vm.Event.clear! @vm = nil end __clear_aF4e__() end end # プラグインの名前 attr_reader :name # spec attr_accessor :spec # 最初にプラグインがロードされた時刻(uninstallされるとリセットする) attr_reader :defined_time # ==== Args # [plugin_name] プラグイン名 def initialize(*args) super @defined_time = Time.new.freeze @events = Set.new @filters = Set.new end # イベントリスナを新しく登録する # ==== Args # [event] 監視するEventのインスタンス # [name:] 名前(String | nil) # [slug:] イベントリスナスラッグ(Symbol | nil) # [tags:] Pluggaloid::HandlerTag|Array リスナのタグ # [&callback] コールバック # ==== Return # Pluggaloid::Listener def add_event(event_name, **kwrest, &callback) result = vm.Listener.new(vm.Event[event_name], **kwrest, &callback) @events << result result end # イベントフィルタを新しく登録する # ==== Args # [event_name] イベント名(String | Symbol) # [name:] 名前(String | nil) # [slug:] フィルタスラッグ(Symbol | nil) # [tags:] Pluggaloid::HandlerTag|Array フィルタのタグ # [&callback] コールバック # ==== Return # Pluggaloid::Filter def add_event_filter(event_name, **kwrest, &callback) result = vm.Filter.new(vm.Event[event_name], **kwrest, &callback) @filters << result result end def generate(event_name, *specs, **kwrest, &block) vm.StreamGenerator.new(vm.Event[event_name], *specs, plugin: self, **kwrest, &block) end def subscribe(event_name, *specs, **kwrest, &block) if block result = vm.Subscriber.new(vm.Event[event_name], *specs, **kwrest, &block) @events << result result else Stream.new( Enumerator.new do |yielder| @events << vm.Subscriber.new(vm.Event[event_name], *specs, **kwrest) do |stream| stream.each(&yielder.method(:<<)) end end.lazy ) end end def subscribe?(event_name, *specs) vm.Event[event_name].subscribe?(*specs) end def collect(event_name, *specs) self.class.collect(event_name, *specs) end # 追加・削除がフィルタに反映されるコレクションオブジェクトを作成する。 # 同時に _event_name_ にフィルタが定義され、フィルタが呼ばれると # その時点のコレクションオブジェクトの内容を全て列挙する。 # フィルタと対になるコレクションオブジェクトは、 _&block_ の引数として渡される。 # ==== Args # [event_name] イベント名(String | Symbol) # [*specs] Pluggaloid::COLLECT以外の引数 # [&block] コレクションオブジェクトを受け取って一度だけ実行されるblock # ==== Return # _&block_ の戻り値 def collection(event_name, *specs, &block) event = vm.Event[event_name] mutation = Pluggaloid::Collection.new(event, *specs) add_event_filter(event_name, name: 'collection(%s line %i)' % block.source_location) do |*args| if mutation.argument_hash_same?(args) mutation.values.each(&args[event.collect_index].method(:<<)) end args end block.call(mutation) end # このプラグインのHandlerTagを作る。 # ブロックが渡された場合は、ブロックの中を実行し、ブロックの中で定義された # Handler全てにTagを付与する。 # ==== Args # [slug] スラッグ # [name] タグ名 # ==== Return # Pluggaloid::HandlerTag def handler_tag(slug=SecureRandom.uuid, name=slug, &block) tag = case slug when String, Symbol vm.HandlerTag.new(slug: slug.to_sym, name: name.to_s, plugin: self) when vm.HandlerTag slug else raise Pluggaloid::TypeError, "Argument `slug' must be instance of Symbol, String or Pluggaloid::HandlerTag, but given #{slug.class}." end if block handlers = @events + @filters block.(tag) (@events + @filters - handlers).each do |handler| handler.add_tag(tag) end else tag end end # イベントリスナを列挙する # ==== Return # Set of Pluggaloid::Listener def listeners(&block) if block @events.each(&block) else @events.dup end end # フィルタを列挙する # ==== Return # Set of Pluggaloid::Filter def filters(&block) if block @filters.each(&block) else @filters.dup end end # イベントを削除する。 # 引数は、Pluggaloid::ListenerかPluggaloid::Filterのみ(on_*やfilter_*の戻り値)。 # 互換性のため、二つ引数がある場合は第一引数は無視され、第二引数が使われる。 # ==== Args # [*args] 引数 # ==== Return # self def detach(*args) listener = args.last case listener when vm.Listener, vm.Subscriber @events.delete(listener) listener.detach when vm.Filter @filters.delete(listener) listener.detach when Enumerable listener.each(&method(:detach)) else raise ArgumentError, "Argument must be Pluggaloid::Listener, Pluggaloid::Filter, Pluggaloid::HandlerTag or Enumerable. But given #{listener.class}." end self end # このプラグインを破棄する # ==== Return # self def uninstall vm.Event[:unload].call(self.name) vm.Delayer.new do @events.map(&:detach) @filters.map(&:detach) self.class.destroy name end self end # イベント _event_name_ を宣言する # ==== Args # [event_name] イベント名 # [options] イベントの定義 def defevent(event_name, options={}) vm.Event[event_name].defevent({ plugin: self, **options }) end # DSLメソッドを新しく追加する。 # 追加されたメソッドは呼ぶと &callback が呼ばれ、その戻り値が返される。引数も順番通り全て &callbackに渡される # ==== Args # [dsl_name] 新しく追加するメソッド名 # [&callback] 実行されるメソッド # ==== Return # self def defdsl(dsl_name, &callback) self.class.instance_eval { define_method(dsl_name, &callback) } self end # プラグインが Plugin.uninstall される時に呼ばれるブロックを登録する。 def onunload(&callback) add_event(:unload) do |plugin_slug| if plugin_slug == self.name callback.call end end end alias :on_unload :onunload # マジックメソッドを追加する。 # on_?name :: add_event(name) # filter_?name :: add_event_filter(name) def method_missing(method, *args, **kwrest, &proc) method_name = method.to_s case when method_name.start_with?('on') event_name = method_name[(method_name[2] == '_' ? 3 : 2)..method_name.size] add_event(event_name.to_sym, *args, **kwrest, &proc) when method_name.start_with?('filter') event_name = method_name[(method_name[6] == '_' ? 7 : 6)..method_name.size] add_event_filter(event_name.to_sym, **kwrest, &proc) else super end end private def vm self.class.vm end end end pluggaloid-1.7.0/lib/pluggaloid/error.rb0000644000004100000410000000064014052673053020256 0ustar www-datawww-data# -*- coding: utf-8 -*- module Pluggaloid class Error < ::StandardError; end class ArgumentError < Error; end class TypeError < Error; end class FilterError < Error; end class NoDefaultDelayerError < Error; end class DuplicateListenerSlugError < Error; end class UndefinedStreamIndexError < Error; end class UndefinedCollectionIndexError < Error; end class NoReceiverError < Error; end end pluggaloid-1.7.0/lib/pluggaloid/mirage.rb0000644000004100000410000000525514052673053020400 0ustar www-datawww-data# frozen_string_literal: true module Pluggaloid module Mirage MIRAGE_ID_BASE_NUMBER = 36 module Extend # `Pluggaloid::Mirage` をincludeしたClassのうち、`pluggaloid_mirage_identity` # メソッドを呼ばれたインスタンスを記録するオブジェクトを返す。 # 戻り値は、以下のメソッドに応答すること。 # - `repository#[](String id)` idに対応するオブジェクトを返す # - `repository#[]=(String id, self obj)` objを記録する # Class毎に適したコンテナを返すようにoverrideすること def pluggaloid_mirage_repository @pluggaloid_mirage_repository ||= {} end # `Pluggaloid::Mirage` をincludeしたClassのnamespaceを返す。 # namespaceはStringで、 `Pluggaloid::Mirage` をincludeしたほかのClassと # 重複しない。 # 同じClassであれば、別のPluggaloid host(Pluggaloid::VMやプロセス)でも # 同じ値を返す。 def pluggaloid_mirage_namespace -to_s end def inherited(klass) Mirage.pluggaloid_mirage_classes[klass.pluggaloid_mirage_namespace] = klass end end def self.unwrap(namespace:, id:) klass = pluggaloid_mirage_classes[namespace] if klass result = klass.pluggaloid_mirage_repository[id] unless result&.is_a?(Pluggaloid::Mirage) # nilの場合は常にraise raise ArgumentError, "The id `#{id}' was not found." end result else raise ArgumentError, "The namespace `#{namespace}' was not found." end end def self.included(klass) klass.extend(Extend) pluggaloid_mirage_classes[klass.pluggaloid_mirage_namespace] = klass end def self.pluggaloid_mirage_classes @pluggaloid_mirage_classes ||= {} end # このClassのなかで、Pluggaloid::Mirageがインスタンスを同定するためのid(String)を返す。 # このメソッドではなく、 `generate_pluggaloid_mirage_id` をoverrideすること def pluggaloid_mirage_id generate_pluggaloid_mirage_id.freeze.tap do |id| self.class.pluggaloid_mirage_repository[id] = self Mirage.pluggaloid_mirage_classes[self.class.pluggaloid_mirage_namespace] ||= self.class end end def pluggaloid_mirage_namespace self.class.pluggaloid_mirage_namespace end private # このClassのなかで、Pluggaloid::Mirageがインスタンスを同定するためのid(String)を返す。 # Class毎に適したコンテナを返すようにoverrideすること def generate_pluggaloid_mirage_id object_id.to_s(MIRAGE_ID_BASE_NUMBER) end end end pluggaloid-1.7.0/lib/pluggaloid/identity.rb0000644000004100000410000000106414052673053020757 0ustar www-datawww-data# -*- coding: utf-8 -*- =begin rdoc slugと名前をもつオブジェクト。 これの参照を直接持たずとも、slugで一意に参照したり、表示名を設定することができる =end class Pluggaloid::Identity attr_reader :name, :slug # ==== Args # [name:] 名前(String | nil) # [slug:] ハンドラスラッグ(Symbol | nil) def initialize(slug: SecureRandom.uuid, name: slug) @name = name.to_s.freeze @slug = slug.to_sym end def inspect "#<#{self.class} slug: #{slug.inspect}, name: #{name.inspect}>" end end pluggaloid-1.7.0/lib/pluggaloid/event.rb0000644000004100000410000001630614052673053020254 0ustar www-datawww-data# -*- coding: utf-8 -*- class Pluggaloid::Event Lock = Mutex.new include InstanceStorage # オプション。以下のキーを持つHash # :prototype :: 引数の数と型。Arrayで、type_strictが解釈できる条件を設定する # :priority :: Delayerの優先順位 attr_accessor :options # フィルタを別のスレッドで実行する。偽ならメインスレッドでフィルタを実行する @filter_another_thread = false def initialize(*args) super @options = {} @listeners = [].freeze @filters = [].freeze @subscribers = Hash.new { |h, k| h[k] = [] } @stream_generators = Hash.new { |h, k| h[k] = Set.new } end def prototype @options[:prototype] end # イベント _event_name_ を宣言する # ==== Args # [new_options] イベントの定義 def defevent(new_options) @options.merge!(new_options) if collect_index new_proto = self.prototype.dup new_proto[self.collect_index] = Pluggaloid::STREAM collection_add_event.defevent(prototype: new_proto) collection_delete_event.defevent(prototype: new_proto) end self end def vm self.class.vm end # イベントの優先順位を取得する # ==== Return # プラグインの優先順位 def priority if @options.has_key? :priority @options[:priority] end end # イベントを引数 _args_ で発生させる # ==== Args # [*args] イベントの引数 # ==== Return # Delayerか、イベントを待ち受けているリスナがない場合はnil def call(*args) if self.class.filter_another_thread if @filters.empty? vm.Delayer.new(*Array(priority)) do call_all_listeners(args) end else Thread.new do filtered_args = filtering(*args) if filtered_args.is_a? Array vm.Delayer.new(*Array(priority)) do call_all_listeners(filtered_args) end end end end else vm.Delayer.new(*Array(priority)) do args = filtering(*args) if not @filters.empty? call_all_listeners(args) if args.is_a? Array end end end # 引数 _args_ をフィルタリングした結果を返す # ==== Args # [*args] 引数 # ==== Return # フィルタされた引数の配列 def filtering(*args) catch(:filter_exit) { @filters.reduce(args){ |acm, event_filter| event_filter.filtering(*acm) } } end def add_listener(listener) case listener when Pluggaloid::Listener Lock.synchronize do if @listeners.map(&:slug).include?(listener.slug) raise Pluggaloid::DuplicateListenerSlugError, "Listener slug #{listener.slug} already exists." end @listeners = [*@listeners, listener].freeze end @stream_generators.values.each do |generators| generators.each(&:on_subscribed) end when Pluggaloid::Subscriber accepted_hash = listener.accepted_hash Lock.synchronize do @subscribers[accepted_hash] << listener end @stream_generators.fetch(accepted_hash, nil)&.each(&:on_subscribed) else raise Pluggaloid::ArgumentError, "First argument must be Pluggaloid::Listener or Pluggaloid::Subscriber, but given #{listener.class}." end self end # subscribe(_*specs_) で、ストリームの受信をしようとしているリスナが定義されていればtrueを返す。 # on_* で通常のイベントリスナが登録されて居る場合は、 _*specs_ の内容に関わらず常にtrueを返す。 def subscribe?(*specs) !@listeners.empty? || @subscribers.key?(argument_hash(specs, nil)) end def subscribe_hash?(hash) # :nodoc: !@listeners.empty? || @subscribers.key?(hash) end def delete_listener(listener) Lock.synchronize do @listeners = @listeners.dup @listeners.delete(listener) @listeners.freeze end self end def delete_subscriber(listener) Lock.synchronize do ss = @subscribers[listener.accepted_hash] ss.delete(listener) if ss.empty? @subscribers.delete(listener.accepted_hash) end end @stream_generators.fetch(listener.accepted_hash, nil)&.each(&:on_unsubscribed) self end def delete_stream_generator(listener) Lock.synchronize do ss = @stream_generators[listener.accepted_hash] ss.delete(listener) if ss.empty? @stream_generators.delete(listener.accepted_hash) end end self end # イベントフィルタを追加する # ==== Args # [event_filter] イベントフィルタ(Filter) # ==== Return # self def add_filter(event_filter) unless event_filter.is_a? Pluggaloid::Filter raise Pluggaloid::ArgumentError, "First argument must be Pluggaloid::Filter, but given #{event_filter.class}." end @filters = [*@filters, event_filter].freeze self end # イベントフィルタを削除する # ==== Args # [event_filter] イベントフィルタ(EventFilter) # ==== Return # self def delete_filter(event_filter) Lock.synchronize do @filters = @filters.dup @filters.delete(event_filter) @filters.freeze end self end def argument_hash(args, exclude_index) args.each_with_index.map do |item, i| if i != exclude_index item.hash end end.compact.freeze end def stream_index unless defined?(@stream_index) @stream_index = self.prototype&.index(Pluggaloid::STREAM) end @stream_index end def collect_index unless defined?(@collect_index) @collect_index = self.prototype&.index(Pluggaloid::COLLECT) end @collect_index end # defeventで定義されたprototype引数に _Pluggaloid::COLLECT_ を含むイベントに対して使える。 # フィルタの _Pluggaloid::COLLECT_ 引数に空の配列を渡して実行したあと、その配列を返す。 # ==== Args # [*args] Pluggaloid::COLLECT 以外の引数のリスト # ==== Return # [Array] フィルタ実行結果 def collect(*args) specified_index = args.index(Pluggaloid::COLLECT) specified_index&.yield_self(&args.method(:delete_at)) insert_index = collect_index || specified_index if insert_index Enumerator.new do |yielder| cargs = args.dup cargs.insert(insert_index, yielder) filtering(*cargs) end else raise Pluggaloid::UndefinedCollectionIndexError, 'To call collect(), it must define prototype arguments include `Pluggaloid::COLLECT\'.' end end def register_stream_generator(stream_generator) @stream_generators[stream_generator.accepted_hash] << stream_generator self end def collection_add_event self.class['%{name}__add' % {name: name}] end def collection_delete_event self.class['%{name}__delete' % {name: name}] end private def call_all_listeners(args) if stream_index @subscribers[argument_hash(args, stream_index)]&.each do |subscriber| subscriber.call(*args) end end catch(:plugin_exit) do @listeners.each do |listener| listener.call(*args) end end end class << self attr_accessor :filter_another_thread, :vm alias __clear_aF4e__ clear! def clear! @filter_another_thread = false __clear_aF4e__() end end clear! end pluggaloid-1.7.0/lib/pluggaloid/filter.rb0000644000004100000410000000303514052673053020413 0ustar www-datawww-data# -*- coding: utf-8 -*- class Pluggaloid::Filter < Pluggaloid::Handler NotConverted = Class.new THROUGH = NotConverted.new.freeze # フィルタ内部で使う。フィルタの実行をキャンセルする。Plugin#filtering はfalseを返し、 # イベントのフィルタの場合は、そのイベントの実行自体をキャンセルする。 # また、 _result_ が渡された場合、Event#filtering の戻り値は _result_ になる。 def self.cancel!(result=false) throw :filter_exit, result end CANCEL_PROC = method(:cancel!) # ==== Args # [event] 監視するEventのインスタンス # [name:] 名前(String | nil) # [slug:] フィルタスラッグ(Symbol | nil) # [tags:] Pluggaloid::HandlerTag|Array フィルタのタグ # [&callback] コールバック def initialize(event, **kwrest, &callback) kwrest[:name] ||= '%s line %i' % callback.source_location super(event, **kwrest) @callback = callback event.add_filter self end # イベントを実行する # ==== Args # [*args] イベントの引数 # ==== Return # 加工後の引数の配列 def filtering(*args) length = args.size result = @callback.call(*args, &CANCEL_PROC) case when THROUGH == result args when length != result.size raise Pluggaloid::FilterError, "filter changes arguments length (#{length} to #{result.size})" else result end end # このリスナを削除する # ==== Return # self def detach @event.delete_filter(self) self end end pluggaloid-1.7.0/lib/pluggaloid/handler.rb0000644000004100000410000000265414052673053020551 0ustar www-datawww-data# -*- coding: utf-8 -*- =begin rdoc イベントのListenerやFilterのスーパクラス。 イベントに関連付けたり、タグを付けたりできる =end class Pluggaloid::Handler < Pluggaloid::Identity Lock = Mutex.new attr_reader :tags # ==== Args # [event] 監視するEventのインスタンス # [name:] 名前(String | nil) # [slug:] ハンドラスラッグ(Symbol | nil) # [tags:] Pluggaloid::HandlerTag|Array リスナのタグ # [&callback] コールバック def initialize(event, tags: [], **kwrest) raise Pluggaloid::TypeError, "Argument `event' must be instance of Pluggaloid::Event, but given #{event.class}." unless event.is_a? Pluggaloid::Event super(**kwrest) @event = event _tags = tags.is_a?(Pluggaloid::HandlerTag) ? [tags] : Array(tags) _tags.each{|t| raise "#{t} is not a Pluggaloid::HandlerTag" unless t.is_a?(Pluggaloid::HandlerTag) } @tags = Set.new(_tags).freeze end def add_tag(tag) raise Pluggaloid::TypeError, "Argument `tag' must be instance of Pluggaloid::HandlerTag, but given #{tag.class}." unless tag.is_a? Pluggaloid::HandlerTag Lock.synchronize do @tags = Set.new([tag, *@tags]).freeze end self end def remove_tag(tag) Lock.synchronize do @tags -= tag @tags.freeze end self end def inspect "#<#{self.class} event: #{@event.name.inspect}, slug: #{slug.inspect}, name: #{name.inspect}>" end end pluggaloid-1.7.0/lib/pluggaloid/subscriber.rb0000644000004100000410000000205014052673053021265 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'securerandom' require 'set' class Pluggaloid::Subscriber < Pluggaloid::Handler attr_reader :accepted_hash # ==== Args # [event] 監視するEventのインスタンス # [name:] 名前(String | nil) # [slug:] イベントリスナスラッグ(Symbol | nil) # [tags:] Pluggaloid::HandlerTag|Array リスナのタグ # [&callback] コールバック def initialize(event, *specs, **kwrest, &callback) raise Pluggaloid::UndefinedStreamIndexError, 'To call subscribe(%{event}), it must define prototype arguments include `Pluggaloid::STREAM\'.' % {event: event.name} unless event.stream_index super(event, **kwrest) @callback = callback @accepted_hash = @event.argument_hash(specs, nil) event.add_listener(self) end # イベントを実行する # ==== Args # [stream] イベントの引数 def call(*args) @callback.call(args[@event.stream_index]) end # このリスナを削除する # ==== Return # self def detach @event.delete_subscriber(self) self end end pluggaloid-1.7.0/lib/pluggaloid/stream.rb0000644000004100000410000000406014052673053020420 0ustar www-datawww-data# frozen_string_literal: true module Pluggaloid class Stream include Enumerable def initialize(enumerator) @enumerator = enumerator end def throttle(sec) throttling = 0 @enumerator.select do |item| r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) if throttling <= r0 throttling = r0 + sec end end end def debounce(sec) throttling_promise = nil Stream.new( Enumerator.new do |yielder| @enumerator.each do |item| throttling_promise&.cancel throttling_promise = Delayer.new(delay: sec) do yielder << item end end end.lazy ) end def buffer(sec) throttling_promise = nil buffer = [] Stream.new( Enumerator.new do |yielder| @enumerator.each do |item| buffer << item throttling_promise ||= Delayer.new(delay: sec) do yielder << buffer.freeze buffer = [] throttling_promise = nil end end end.lazy ) end def merge(*streams) Stream.new(Merge.new(self, *streams).lazy) end (Enumerator.instance_methods - Enumerator.superclass.instance_methods).each do |method_name| define_method(method_name) do |*rest, **kwrest, &block| if kwrest.empty? r = @enumerator.__send__(method_name, *rest, &block) else r = @enumerator.__send__(method_name, *rest, **kwrest, &block) end if r.is_a?(Enumerator::Lazy) Pluggaloid::Stream.new(r) else r end end end class Merge include Enumerable def initialize(*sources) @sources = sources end def each(&block) fiber = Fiber.new do loop do block.call(Fiber.yield) end end fiber.resume @sources.each do |source| source.each(&fiber.method(:resume)) end self end end end end pluggaloid-1.7.0/lib/pluggaloid/collection.rb0000644000004100000410000000222114052673053021255 0ustar www-datawww-data# frozen_string_literal: true module Pluggaloid class Collection attr_reader :values def initialize(event, *args) @event = event args[event.collect_index] = nil @args = args.freeze @spec = argument_hash(args) @values = [].freeze end def add(*v) rewind do |primitive| primitive + v end end alias_method :<<, :add def delete(*v) rewind do |primitive| primitive - v end end def rewind(&block) new_values = block.(@values.dup) added, deleted = new_values - @values, @values - new_values @values = new_values.freeze unless added.empty? args = @args.dup args[@event.collect_index] = added @event.collection_add_event.call(*args) end unless deleted.empty? args = @args.dup args[@event.collect_index] = deleted @event.collection_delete_event.call(*args) end self end def argument_hash_same?(specs) @spec == argument_hash(specs) end private def argument_hash(specs) @event.argument_hash(specs, @event.collect_index) end end end pluggaloid-1.7.0/Gemfile0000644000004100000410000000013714052673053015177 0ustar www-datawww-datasource 'https://rubygems.org' # Specify your gem's dependencies in pluggaloid.gemspec gemspec pluggaloid-1.7.0/pluggaloid.gemspec0000644000004100000410000000214614052673053017402 0ustar www-datawww-data# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'pluggaloid/version' Gem::Specification.new do |spec| spec.name = "pluggaloid" spec.version = Pluggaloid::VERSION spec.authors = ["Toshiaki Asai"] spec.email = ["toshi.alternative@gmail.com"] spec.summary = %q{Extensible plugin system} spec.description = %q{Pluggaloid is extensible plugin system for mikutter.} spec.homepage = "https://rubygems.org/gems/pluggaloid" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.required_ruby_version = '>= 2.5.0' spec.add_dependency 'delayer', '>= 1.1.0', '< 2.0' spec.add_dependency 'instance_storage', ">= 1.0.0", "< 2.0.0" spec.add_development_dependency "bundler" spec.add_development_dependency "rake", ">= 12.3.2" spec.add_development_dependency "minitest", ">= 5.11.3" end pluggaloid-1.7.0/LICENSE.txt0000644000004100000410000000205614052673053015531 0ustar www-datawww-dataCopyright (c) 2015 Toshiaki Asai MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.