## Context-based programming is DRY

Recently, I needed to document firewall rules for a cluster of machines. The document needed to spell out rules by machine, but that meant that connections between machines were described in two places: the machine requesting the connection, and the one servicing it.

Context-based programming gave me an easy to describe a connection in a single place, but generate a document that connection in two places.

I can also use the same source file to generate the firewall rules themselves.

### List of machines

I begin by putting the description of each connection in a new file, conn.rb. By wrapping everything in a function, the description can be evaluated whenever desired.

This example shows two connections: one from the application server to the database, and another from the background worker to the database:

def connections
#   from    to  port
tcp :app    :db 5432
tcp :worker :db 5432
end


Another file, main.rb, will load conn.rb and control the context. To get a list of all machines mentioned, tcp is defined as a function that accumulates both of the connection's machines into a set:

require 'set'
require './conn.rb'

$machines = Set.new def tcp client, server, port$machines << client << server
end


By calling connections in this context, we can print out all of the mentioned machines:

connections
p $machines  #<Set: {:app, :db, :worker}>  ### Connections by machine Now that we have a list of all the machines, we can generate a document with one section per machine. Each machine's section can have a table for inbound connections and another for outbound connections. First, looping over each machine and generating its section: $m = nil
for $m in$machines
puts "# #{$m} #" puts # Inbound connections... # Outbound connections... end  After a machine's heading is output, we'll define tcp to collect rows for inbound connections, call connections to gather them, then output them:  # Inbound connections...$inbound = ""
def tcp client, server, port
$inbound << table_row(client, port) if server ==$m
end

connections

print_subsection "Inbound", $inbound  Likewise for outbound connections:  # Outbound connections...$outbound = ""
def tcp client, server, port
$outbound << table_row(server, port) if client ==$m
end

connections

print_subsection "Outbound", $outbound  We can now generate a document describing each machine's connections: # app # ## Outbound ## | db | 5432 | # db # ## Inbound ## | app | 5432 | | worker | 5432 | # worker # ## Outbound ## | db | 5432 |  ### Final source code require 'set' require './conn.rb' def table_row machine, port format "| %-6s | %4i |\n", machine, port end def print_subsection heading, body unless body.empty? puts "## #{heading} ##" puts puts body puts end end$machines = Set.new

def tcp client, server, port
$machines << client << server end connections$m = nil
for $m in$machines
puts "# #{$m} #" puts # Inbound connections...$inbound = ""
def tcp client, server, port
$inbound << table_row(client, port) if server ==$m
end

connections

print_subsection "Inbound", $inbound # Outbound connections...$outbound = ""
def tcp client, server, port
$outbound << table_row(server, port) if client ==$m
end

connections

print_subsection "Outbound", \$outbound
end