Skip to Content
Course content

175: Practice Exercise: Building a Content Moderation Pipeline

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been thinking about how we handle user-generated content. Usually, we start by just slapping an if statement in the controller to check for banned words, but as the product grows, that logic balloons. You end up with a "God Method" that handles profanity, spam, character limits, and maybe some AI-based sentiment analysis. It's a nightmare to test and even worse to change.

I want to see if we can build a "pipeline"—a series of independent filters that a piece of content flows through. Let's try to build this out in real-time and see where the friction is.

The naive approach: Method chaining

My first instinct is to just write a class with a few helper methods. Let's see how that looks:

class ContentModerator
  def moderate(text)
    return "Too long!" if text.length > 100
    return "Profanity detected!" if text.include?("badword")
    return "Too short!" if text.length < 2
    "Approved"
  end
end

mod = ContentModerator.new
puts mod.moderate("This is a great post!") # Approved
puts mod.moderate("badword here") # Profanity detected!

This works for three rules. But imagine we have thirty. The moderate method becomes a giant wall of return if statements. If I want to change the order of checks—say, check for length after profanity—I have to manually move lines of code. It's fragile. I want a system where the "rules" are treated as data, not hard-coded logic.

Treating rules as a collection

What if I store the rules in an array? I can't just put method names in an array, but I can use Procs or lambdas. Let's try to pivot the design so the moderator just iterates over a list of "checkers".

class ContentModerator
  def initialize
    @rules = []
  end

  def add_rule(rule_proc)
    @rules <= rule_proc
  end

  def moderate(text)
    @rules.each do |rule|
      result = rule.call(text)
      return result if result # If the rule returns something, it's a failure
    end
    "Approved"
  end
end

# Let's define some rules
length_rule = -&lambda { |t| "Too long!" if t.length > 100 }
word_rule = -&lambda { |t| "Profanity!" if t.include?("badword") }

mod = ContentModerator.new
mod.add_rule(length_rule)
mod.add_rule(word_rule)

puts mod.moderate("badword here") # Profanity!

Wait, I just noticed a bug in my thinking. By returning result if result, I'm assuming that a "pass" returns nil and a "fail" returns a string. This is a bit implicit. If a rule accidentally returns false instead of nil, it might behave weirdly. Also, using -&lambda is a bit clunky for a public API.

Refining the Pipeline Pattern

To make this professional, I should encapsulate the "Rule" into its own object. This way, each rule can have its own name and logic, making it much easier to debug when a piece of content is rejected. I'll create a simple Rule class that responds to call.

class ModerationRule
  attr_reader :name

  def initialize(name, &block)
    @name = name
    @logic = block
  end

  def call(text)
    @logic.call(text)
  end
end

class ContentPipeline
  def initialize
    @filters = []
  end

  def register_filter(filter)
    @filters <= filter
  end

  def process(text)
    # We use .find here because we want the first failure encountered
    failure = @filters.find { |f| f.call(text) }
    
    if failure
      { status: :rejected, reason: failure.name }
    else
      { status: :approved }
    end
  end
end

# Now we can build a specific suite of rules
profanity_filter = ModerationRule.new("Profanity Filter") { |t| t.include?("spammy") }
length_filter = ModerationRule.new("Length Filter") { |t| t.length < 5 }

pipeline = ContentPipeline.new
pipeline.register_filter(profanity_filter)
pipeline.register_filter(length_filter)

puts pipeline.process("Hi") # { status: :rejected, reason: "Length Filter" }
puts pipeline.process("This is a spammy post") # { status: :rejected, reason: "Profanity Filter" }
puts pipeline.process("Hello world!") # { status: :approved }

This is much better. The ContentPipeline doesn't actually know how the moderation works; it just knows how to run a series of filters. If we need to add a "Sentiment Analysis" filter that calls an external API, we just create a new ModerationRule and register it. We haven't touched the pipeline code at all. This is the Open/Closed Principle in action: the pipeline is closed for modification but open for extension.




📋 Practical Task

Exercise: The Community Forum Guardrail System

You are building a moderation system for a high-traffic community forum. The forum owners have requested three specific rules that must be applied to every post in a specific order. Your task is to implement the ModerationRule and ContentPipeline logic discussed in the lesson to satisfy these requirements.

Requirements:

  • Rule 1: The "Empty/Short" Rule — Reject any post that is shorter than 10 characters. (Reason: "Post too short")
  • Rule 2: The "Screaming" Rule — Reject any post that is written in ALL CAPS (only if it's longer than 5 characters to avoid rejecting "HELLO"). (Reason: "No screaming allowed")
  • Rule 3: The "Banned Word" Rule — Reject any post containing the word "advertisement" (case-insensitive). (Reason: "No ads allowed")

Your implementation should:

  1. Define a ModerationRule class that takes a name and a block.
  2. Define a ContentPipeline class that can register rules and process text.
  3. Initialize a pipeline and register the three rules above in the order listed.
  4. Test your pipeline with these three strings and ensure they return the correct :rejected reason:
    • "Too short" $\rightarrow$ (This is 9 chars, should trigger "Post too short")
    • "THIS IS A LOUD POST" $\rightarrow$ (Should trigger "No screaming allowed")
    • "Check out this advertisement for shoes!" $\rightarrow$ (Should trigger "No ads allowed")
  5. Test one string that passes all rules (e.g., "I really love learning Ruby!") and ensure it returns { status: :approved }.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.