diff --git a/.rubocop.yml b/.rubocop.yml index 8da907e..70fadf8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -15,6 +15,9 @@ Lint/AmbiguousBlockAssociation: Lint/ScriptPermission: Enabled: false +Lint/RescueWithoutErrorClass: + Enabled: false + Layout/MultilineMethodCallIndentation: Enabled: false @@ -22,7 +25,7 @@ Layout/EmptyLinesAroundClassBody: Enabled: false Metrics/AbcSize: - Max: 32 + Max: 35 Metrics/PerceivedComplexity: Max: 10 @@ -57,7 +60,7 @@ Style/CommentAnnotation: Style/Documentation: Enabled: false -Style/FileName: +Naming/FileName: Enabled: false Style/GuardClause: @@ -114,13 +117,13 @@ Style/AsciiComments: Style/EmptyMethod: EnforcedStyle: expanded -Style/VariableNumber: +Naming/VariableNumber: Enabled: false -Style/PredicateName: +Naming/PredicateName: Enabled: false -Style/AccessorMethodName: +Naming/AccessorMethodName: Enabled: false Style/YodaCondition: @@ -135,5 +138,8 @@ Style/MultipleComparison: Style/StructInheritance: Enabled: false +Style/NonNilCheck: + Enabled: false + Performance/RedundantBlockCall: Enabled: false diff --git a/README.md b/README.md index 2a9fb95..4cb7a01 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,28 @@ end mato.process("Hello!").render_html # "

HELLO!

\n" ``` +### Timeout + +There is `timeout` option to kill filters in a specified time in seconds: + +```ruby +mato = Mato.define do |config| + config.append_html_filter(FooFilter, timeout: timeout_in_sec, on_timeout: callback) +end +``` + +If you set `on_error` callback, you can omit `on_timeout` callback. + +### Errors in Filters + +There is `on_error` callback to rescue errors in filters: + +```ruby +mato = Mato.define do |config| + config.append_html_filter(FooFilter, on_error: callback) +end +``` + ## Installation Add this line to your application's Gemfile: diff --git a/lib/mato.rb b/lib/mato.rb index 42312f2..44d2d0d 100644 --- a/lib/mato.rb +++ b/lib/mato.rb @@ -5,6 +5,8 @@ require_relative "./mato/config" require_relative "./mato/processor" require_relative "./mato/converter" +require_relative "./mato/rescue" +require_relative "./mato/timeout" # filter classes require_relative "./mato/html_filters/token_link" diff --git a/lib/mato/config.rb b/lib/mato/config.rb index 1bd8796..315f122 100644 --- a/lib/mato/config.rb +++ b/lib/mato/config.rb @@ -75,34 +75,33 @@ def configure(&block) block.call(self) end - def append_text_filter(text_filter) + def append_text_filter(text_filter, timeout: nil, on_timeout: nil, on_error: nil) raise "text_filter must respond to call()" unless text_filter.respond_to?(:call) - text_filters.push(text_filter) + text_filters.push(wrap(text_filter, timeout: timeout, on_timeout: on_timeout, on_error: on_error)) end - def prepend_text_filter(text_filter) - raise "text_filter must respond to call()" unless text_filter.respond_to?(:call) - text_filters.unshift(text_filter) - end - - def append_markdown_filter(markdown_filter) - raise "markdown_filter must respond to call()" unless markdown_filter.respond_to?(:call) - markdown_filters.push(markdown_filter) - end - - def prepend_markdown_filter(markdown_filter) + def append_markdown_filter(markdown_filter, timeout: nil, on_timeout: nil, on_error: nil) raise "markdown_filter must respond to call()" unless markdown_filter.respond_to?(:call) - markdown_filters.unshift(markdown_filter) + markdown_filters.push(wrap(markdown_filter, timeout: timeout, on_timeout: on_timeout, on_error: on_error)) end - def append_html_filter(html_filter) + def append_html_filter(html_filter, timeout: nil, on_timeout: nil, on_error: nil) raise "html_filter must respond to call()" unless html_filter.respond_to?(:call) - html_filters.push(html_filter) + html_filters.push(wrap(html_filter, timeout: timeout, on_timeout: on_timeout, on_error: on_error)) end - def prepend_html_filter(html_filter) - raise "html_filter must respond to call()" unless html_filter.respond_to?(:call) - html_filters.unshift(html_filter) + private + + def wrap(filter, timeout:, on_timeout:, on_error:) + if timeout + filter = Mato::Timeout.new(filter, timeout: timeout, on_timeout: on_timeout || on_error) + end + + if on_error + filter = Mato::Rescue.new(filter, on_error: on_error) + end + + filter end end end diff --git a/lib/mato/rescue.rb b/lib/mato/rescue.rb new file mode 100644 index 0000000..f22dcf3 --- /dev/null +++ b/lib/mato/rescue.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Mato + class Rescue + attr_reader :filter + attr_reader :on_error + + def initialize(filter, on_error:) + @filter = filter + @on_error = on_error + end + + def call(content) + filter.call(content) + rescue => e + on_error.call(e) + content + end + end +end diff --git a/lib/mato/timeout.rb b/lib/mato/timeout.rb new file mode 100644 index 0000000..7fc76d1 --- /dev/null +++ b/lib/mato/timeout.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'timeout' + +module Mato + class Timeout + attr_reader :filter + attr_reader :duration_sec + attr_reader :on_timeout + + def initialize(filter, timeout:, on_timeout:) + @filter = filter + @duration_sec = timeout + @on_timeout = on_timeout + + unless on_timeout + raise ArgumentError, "Missing on_timeout callback" + end + end + + def call(content) + ::Timeout.timeout(duration_sec) do + filter.call(content) + end + rescue ::Timeout::Error => e + on_timeout.call(e) + content + end + end +end diff --git a/test/mato_rescue_test.rb b/test/mato_rescue_test.rb new file mode 100644 index 0000000..ce50c82 --- /dev/null +++ b/test/mato_rescue_test.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require_relative './test_helper' + +class MatoRescueTest < MyTest + def test_text_filters + error = nil + mato = Mato.define do |config| + config.append_text_filter(->(text) { + raise 'Hello!' + }, on_error: ->(e) { error = e }) + end + + doc = mato.process('Hello, world!') + + assert do + doc.render_html == "

Hello, world!

\n" + end + + assert do + error.message == 'Hello!' + end + end +end diff --git a/test/mato_timeout_test.rb b/test/mato_timeout_test.rb new file mode 100644 index 0000000..4ff07e2 --- /dev/null +++ b/test/mato_timeout_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require_relative './test_helper' + +class MatoTimeoutTest < MyTest + def test_text_filters + timeout = nil + mato = Mato.define do |config| + config.append_text_filter(->(text) { + sleep + }, timeout: 0.1, on_timeout: ->(e) { timeout = e }) + end + + doc = mato.process('Hello, world!') + + assert do + doc.render_html == "

Hello, world!

\n" + end + + assert do + timeout != nil + end + end + + def test_text_filters_with_on_error + timeout = nil + mato = Mato.define do |config| + config.append_text_filter(->(text) { + sleep + }, timeout: 0.1, on_error: ->(e) { timeout = e }) + end + + doc = mato.process('Hello, world!') + + assert do + doc.render_html == "

Hello, world!

\n" + end + + assert do + timeout != nil + end + end +end