Elixir Migration Patterns
This document provides concrete examples and patterns for migrating Ruby code to Elixir, focusing on key architectural patterns that will be used throughout the replatforming effort.
Table of Contents
- Command Execution Pattern
- Error Handling with
{:ok, result} | {:error, reason} - Configuration Management with Application
- Process Supervision Strategies
- Concurrency Patterns for Format Generation
1. Command Execution Pattern
Current Ruby Implementation
In our Ruby codebase, we have two main command execution patterns:
System::CommandExecutor- A low-level command execution utilityBookProcessing::Utilities::CommandRunner- A higher-level wrapper with domain-specific functionality
Example Ruby Implementation:
# Low-level command execution
module System
class CommandExecutor
def self.execute(command, options = {})
stdout, stderr, status = Open3.capture3(command)
if status.success?
{ success: true, stdout: stdout, stderr: stderr, exit_code: status.exitstatus }
else
{ success: false, stdout: stdout, stderr: stderr, exit_code: status.exitstatus }
end
end
end
end
# Domain-specific command runner
module BookProcessing
module Utilities
class CommandRunner
def initialize(logger)
@logger = logger
end
def run_latex_command(command, working_dir)
@logger.info("Running LaTeX command: #{command}")
Dir.chdir(working_dir) do
result = System::CommandExecutor.execute(command)
if result[:success]
@logger.info("Command completed successfully")
return result
else
@logger.error("Command failed with exit code #{result[:exit_code]}")
@logger.error("Error output: #{result[:stderr]}")
raise CommandExecutionError.new(result[:stderr], result[:exit_code])
end
end
end
end
end
end
Elixir Implementation Pattern
In Elixir, we'll implement command execution using a combination of System.cmd/3 for simple commands and Port for more complex scenarios requiring streaming or interactive communication.
Basic Command Execution Module
defmodule Codexis.System.CommandExecutor do
@moduledoc """
Low-level command execution utility.
Provides functions for executing system commands and handling their results.
"""
@doc """
Executes a system command and returns a structured result.
## Parameters
- `command`: String command to execute
- `args`: List of command arguments
- `opts`: Keyword list of options (see `System.cmd/3` for available options)
## Returns
- `{:ok, result}` where result is a map containing :stdout, :stderr, and :exit_code
- `{:error, reason}` if the command fails to execute
## Examples
iex> Codexis.System.CommandExecutor.execute("echo", ["hello"])
{:ok, %{stdout: "hello\\n", stderr: "", exit_code: 0}}
iex> Codexis.System.CommandExecutor.execute("invalid_command", [])
{:error, :command_not_found}
"""
def execute(command, args \\ [], opts \\ []) do
try do
# Merge default options with provided options
merged_opts = Keyword.merge([stderr_to_stdout: false], opts)
# Execute command
{stdout, exit_code} = System.cmd(command, args, merged_opts)
# Extract stderr if captured separately
stderr = if opts[:stderr_to_stdout], do: "", else: get_stderr(opts)
if exit_code == 0 do
{:ok, %{stdout: stdout, stderr: stderr, exit_code: exit_code}}
else
{:error, %{reason: :non_zero_exit, stdout: stdout, stderr: stderr, exit_code: exit_code}}
end
rescue
e in ErlangError ->
case e do
%ErlangError{original: :enoent} ->
{:error, :command_not_found}
_ ->
{:error, %{reason: :execution_failed, message: Exception.message(e)}}
end
end
end
@doc """
Executes a command in a specific working directory.
## Parameters
- `command`: String command to execute
- `args`: List of command arguments
- `working_dir`: Directory to execute the command in
- `opts`: Additional options for `execute/3`
## Returns
Same as `execute/3`
"""
def execute_in_dir(command, args \\ [], working_dir, opts \\ []) do
# Add cd option to change directory
dir_opts = Keyword.put(opts, :cd, working_dir)
execute(command, args, dir_opts)
end
# Helper to extract stderr from options
defp get_stderr(opts) do
case Keyword.get(opts, :stderr_to_stdout) do
true -> ""
_ ->
case Keyword.get(opts, :stderr_file) do
nil -> ""
file -> File.read!(file)
end
end
end
end
Domain-Specific Command Runner
defmodule Codexis.BookProcessing.CommandRunner do
@moduledoc """
Domain-specific command runner for book processing operations.
Provides higher-level functions for executing commands related to book processing,
with appropriate logging and error handling.
"""
require Logger
alias Codexis.System.CommandExecutor
@doc """
Runs a LaTeX command in the specified working directory.
## Parameters
- `command`: LaTeX command to run
- `args`: Command arguments
- `working_dir`: Directory containing LaTeX source files
- `opts`: Additional options
## Returns
- `{:ok, result}` on success
- `{:error, reason}` on failure
## Examples
iex> Codexis.BookProcessing.CommandRunner.run_latex_command("pdflatex", ["book.tex"], "/path/to/book")
{:ok, %{stdout: "...", stderr: "", exit_code: 0}}
"""
def run_latex_command(command, args, working_dir, opts \\ []) do
Logger.info("Running LaTeX command: #{command} #{Enum.join(args, " ")}")
case CommandExecutor.execute_in_dir(command, args, working_dir, opts) do
{:ok, result} ->
Logger.info("LaTeX command completed successfully")
{:ok, result}
{:error, %{reason: :non_zero_exit} = error} ->
Logger.error("LaTeX command failed with exit code #{error.exit_code}")
Logger.error("Error output: #{error.stderr}")
{:error, {:latex_error, error}}
{:error, reason} ->
Logger.error("Failed to execute LaTeX command: #{inspect(reason)}")
{:error, {:command_execution_failed, reason}}
end
end
@doc """
Runs a Calibre command for ebook conversion.
## Parameters
- `command`: Calibre command (e.g., "ebook-convert")
- `args`: Command arguments
- `working_dir`: Working directory
- `opts`: Additional options
## Returns
- `{:ok, result}` on success
- `{:error, reason}` on failure
"""
def run_calibre_command(command, args, working_dir, opts \\ []) do
Logger.info("Running Calibre command: #{command} #{Enum.join(args, " ")}")
case CommandExecutor.execute_in_dir(command, args, working_dir, opts) do
{:ok, result} ->
Logger.info("Calibre command completed successfully")
{:ok, result}
{:error, %{reason: :non_zero_exit} = error} ->
Logger.error("Calibre command failed with exit code #{error.exit_code}")
Logger.error("Error output: #{error.stderr}")
{:error, {:calibre_error, error}}
{:error, reason} ->
Logger.error("Failed to execute Calibre command: #{inspect(reason)}")
{:error, {:command_execution_failed, reason}}
end
end
end
Advanced Command Execution with Port
For more complex scenarios, such as streaming output or interactive commands, we can use Elixir's Port module:
defmodule Codexis.System.StreamingCommandExecutor do
@moduledoc """
Provides streaming command execution capabilities for long-running processes.
"""
require Logger
@doc """
Executes a command and streams its output to a callback function.
## Parameters
- `command`: Command to execute
- `args`: Command arguments
- `callback`: Function to call with each line of output
- `opts`: Additional options
## Returns
- `{:ok, exit_code}` on successful completion
- `{:error, reason}` on failure
"""
def stream_command(command, args, callback, opts \\ []) do
# Create a port to execute the command
port = Port.open({:spawn_executable, find_executable(command)}, [
{:args, args},
:binary,
:exit_status,
:use_stdio,
:stderr_to_stdout
] ++ port_options(opts))
# Process the command output
process_output(port, "", callback, opts)
end
# Process output from the port
defp process_output(port, buffer, callback, opts) do
receive do
{^port, {:data, data}} ->
# Process received data
{new_buffer, lines} = process_data(buffer <> data)
# Call callback for each complete line
Enum.each(lines, &callback.(&1))
# Continue processing
process_output(port, new_buffer, callback, opts)
{^port, {:exit_status, status}} ->
# Command has completed
if buffer != "" do
callback.(buffer)
end
if status == 0 do
{:ok, status}
else
{:error, {:non_zero_exit, status}}
end
other ->
Logger.warn("Unexpected message received by port: #{inspect(other)}")
process_output(port, buffer, callback, opts)
end
end
# Process data into complete lines
defp process_data(data) do
case String.split(data, "\n", parts: :infinity) do
[buffer] ->
{buffer, []}
parts ->
buffer = List.last(parts)
lines = Enum.take(parts, length(parts) - 1)
{buffer, lines}
end
end
# Find the executable path
defp find_executable(command) do
case System.find_executable(command) do
nil -> raise "Command not found: #{command}"
path -> path
end
end
# Extract port-specific options
defp port_options(opts) do
Keyword.take(opts, [:cd, :env])
end
end
Usage Examples
Basic Command Execution
# Simple command execution
case Codexis.System.CommandExecutor.execute("pdflatex", ["book.tex"]) do
{:ok, result} ->
IO.puts("Command succeeded with output: #{result.stdout}")
{:error, %{reason: :non_zero_exit} = error} ->
IO.puts("Command failed with exit code #{error.exit_code}")
IO.puts("Error: #{error.stderr}")
{:error, reason} ->
IO.puts("Failed to execute command: #{inspect(reason)}")
end
Domain-Specific Command Execution
# Running a LaTeX command
case Codexis.BookProcessing.CommandRunner.run_latex_command("pdflatex", ["book.tex"], "/path/to/book") do
{:ok, result} ->
IO.puts("LaTeX compilation successful")
{:error, {:latex_error, error}} ->
IO.puts("LaTeX compilation failed: #{error.stderr}")
{:error, reason} ->
IO.puts("Command execution failed: #{inspect(reason)}")
end
Streaming Command Execution
# Stream output from a long-running command
callback = fn line ->
IO.puts(">> #{line}")
# You could also parse the output for progress information
if String.contains?(line, "Processing page") do
# Update progress indicator
end
end
case Codexis.System.StreamingCommandExecutor.stream_command("pdflatex", ["book.tex"], callback) do
{:ok, _} ->
IO.puts("Command completed successfully")
{:error, reason} ->
IO.puts("Command failed: #{inspect(reason)}")
end
Key Advantages of the Elixir Pattern
-
Explicit Error Handling: The
{:ok, result} | {:error, reason}pattern makes error handling explicit and encourages proper handling of all cases. -
Structured Returns: Results are returned as structured maps rather than relying on multiple return values.
-
Pattern Matching: Elixir's pattern matching makes it easy to handle different error cases elegantly.
-
Concurrency Support: The
Port-based implementation supports concurrent command execution and can be supervised within an OTP application. -
Streaming Capability: The streaming executor allows processing output incrementally, which is valuable for long-running commands.
-
Testability: The functional design makes it easier to test command execution by mocking the low-level functions.
Migration Path
When migrating command execution code from Ruby to Elixir:
-
Identify Command Patterns: Catalog all command execution patterns in the Ruby codebase.
-
Map to Elixir Equivalents:
- Simple commands →
CommandExecutor.execute/3 - Directory-specific commands →
CommandExecutor.execute_in_dir/4 - Long-running commands →
StreamingCommandExecutor.stream_command/4
- Simple commands →
-
Convert Error Handling:
- Ruby exceptions → Elixir
{:error, reason}tuples - Success returns → Elixir
{:ok, result}tuples
- Ruby exceptions → Elixir
-
Update Calling Code:
- Use pattern matching to handle command results
- Implement proper error propagation
-
Add Supervision:
- For critical command execution, consider adding supervision
Testing Strategies
defmodule Codexis.System.CommandExecutorTest do
use ExUnit.Case
alias Codexis.System.CommandExecutor
describe "execute/3" do
test "successfully executes a command" do
{:ok, result} = CommandExecutor.execute("echo", ["hello"])
assert result.stdout == "hello\n"
assert result.exit_code == 0
end
test "handles non-existent commands" do
assert {:error, :command_not_found} = CommandExecutor.execute("non_existent_command", [])
end
test "handles commands that return non-zero exit codes" do
{:error, error} = CommandExecutor.execute("sh", ["-c", "exit 1"])
assert error.exit_code == 1
assert error.reason == :non_zero_exit
end
end
end
2. Error Handling with {:ok, result} | {:error, reason}
Coming soon
3. Configuration Management with Application
Coming soon
4. Process Supervision Strategies
Coming soon
5. Concurrency Patterns for Format Generation
Coming soon