-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
175: Practice Exercise: Building a Content Moderation Pipeline
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:
- Define a
ModerationRuleclass that takes a name and a block. - Define a
ContentPipelineclass that can register rules and process text. - Initialize a pipeline and register the three rules above in the order listed.
- Test your pipeline with these three strings and ensure they return the correct
:rejectedreason:"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")
- Test one string that passes all rules (e.g.,
"I really love learning Ruby!") and ensure it returns{ status: :approved }.
There are no comments for now.