Skip to content

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

Trackbacks

No Trackbacks

Comments

Display comments as Linear | Threaded

No comments

Add Comment

E-Mail addresses will not be displayed and will only be used for E-Mail notifications.
To leave a comment you must approve it via e-mail, which will be sent to your address after submission.
Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.
Form options