Search code examples
rubydesign-patternsarchitecturebusiness-logic

How to implement complex message validation/handling flow


I'm developing a web service (in Ruby) which needs to do a number of different things for each message it receives.

Before my web service can process a message it must do different things:

  • sanitizing (e.g. remove HTML/JS)
  • check format (e.g. valid email provided?)
  • check IP in blacklist
  • invoke 3rd party web service
  • plus 10-30 other things

I'm thinking about implementing a filter/composite filter architecture where each step/phase is a filter. For instance, I could have these filters

  • Sanitize input filter
  • Email filter
  • Country code filter
  • Blacklist filter

Each filter should be possible to reject a message, so I'm considering that a filter should raise/throw exceptions.

This will give a lot of flexibility and hopefully a codebase that are easy to understand.

How would you did this? And what are pros and cons of above design?


Solution

  • I would leave Exceptions for the cases when the filter itself actually broke down (e.g blacklist not available etc) and indicate the valid/invalid state either by true/false return values or, as you also suggested, throwing a tag.

    If you don't want to stop at first failure, but execute all filters anyway, you should choose the boolean return type and conjunct them together (success &= next_filter(msg))

    If I understood your situation correctly, the filter can both modify the message or check some other source for validity (e.g blacklist).

    So I would do it like this:

    module MessageFilters
    
      EmailValidator = ->(msg) do
        throw :failure unless msg.txt =~ /@/
      end
    
      HTMLSanitizer = ->(msg) do
        # this filter only modifies message, doesn't throw anything
        # msg.text.remove_all_html!
      end
    end
    
    class Message
    
      attr_accessor :filters
      def initialize
        @filters = []
      end
    
      def execute_filters!
        begin
          catch(:failure) do
            filters.each{|f| f.call self}
            true # if all filters pass, this is returned, else nil
          end
        rescue => e
          # Handle filter errors
        end
      end
    end
    
    message = Message.new
    
    message.filters << MessageFilters::EmailValidator
    message.filters << MessageFilters::HTMLSanitizer
    
    success = message.execute_filters!  # returns either true or nil