Elixir Migration Strategy for Codexis
Domain Analysis and Architecture
The current Ruby implementation of the ebook conversion system reveals several architectural challenges that can be addressed through an Elixir migration. This document outlines a bounded context approach to reimagining the system using Elixir/OTP principles while maintaining functional compatibility.
Our analysis of the existing tests has revealed tight coupling between filesystem operations, external tools, and business logic, creating fragile integration points. The Elixir implementation will establish clear domain boundaries and leverage OTP's supervision capabilities to build a more resilient system.
Strategic Goals
The migration to Elixir seeks to achieve several technical advantages:
Our primary goal is to leverage Elixir's actor model via OTP to create a fault-tolerant pipeline for ebook generation. The current Ruby implementation uses sequential processing with error propagation through exceptions, which fails to take advantage of parallel processing opportunities and lacks proper recovery mechanisms.
We aim to improve system observability with built-in telemetry, enabling operational insights that the current system lacks. Each stage of processing will emit telemetry events, creating a comprehensive view of the build process.
The design will embrace functional core, imperative shell architecture to separate pure business logic from side-effectful operations like file I/O and command execution. This will enhance testability and maintenance, addressing issues observed in the tightly coupled Ruby implementation.
Implementation Phases
Phase 1: Core Domain Model
The initial phase established the foundational domain model using domain-driven design principles. We successfully implemented the metadata management system, which drives all other processing.
The metadata domain uses algebraic data types (Elixir structs) with strict validation through contract programming. The implementation includes:
defmodule Codexis.Domain.Metadata do
@enforce_keys [:file_name]
defstruct [:file_name, :title, :creator, :identifier]
@doc "Validates metadata completeness and structural integrity"
@spec validate(t()) :: {:ok, t()} | {:error, [String.t()]}
def validate(%__MODULE__{} = metadata) do
# Validation logic using with expressions for composable validation
end
end
We've also established the build context and configuration management system:
defmodule Codexis.Domain.BuildConfig do
defstruct [:output_formats, :validation_level, :environment]
@doc "Creates a build configuration with sensible defaults"
def new(opts \\ []) do
# Default configuration with overrides from opts
end
end
Phase 2: Command Execution Framework (Completed)
Phase 2 focused on creating a robust command execution framework that addresses the fragility observed in the Ruby implementation. We've established a hexagonal architecture with proper ports and adapters for system integration.
The command execution domain uses behaviors to define contracts:
defmodule Codexis.Port.CommandExecutor do
@callback execute(command :: String.t()) :: {:ok, String.t()} | {:error, term()}
@callback execute_with_timeout(command :: String.t(), timeout :: integer()) ::
{:ok, String.t()} | {:error, :timeout} | {:error, term()}
end
We've implemented adapters for both production and testing:
defmodule Codexis.Adapter.SystemCommand do
@behaviour Codexis.Port.CommandExecutor
@impl true
def execute(command) do
# Implementation using Elixir's System module or Ports
end
@impl true
def execute_with_timeout(command, timeout) do
# Implementation with timeout handling
end
end
This phase established proper inversion of control, allowing us to inject dependencies for testing and to adapt to different environments.
Phase 3: PDF Generation Pipeline (Completed)
Phase 3 implemented the core PDF generation pipeline as a series of process managers using GenServers with proper supervision. This addresses the error handling issues observed in the Ruby implementation while leveraging Elixir's concurrency model.
The pipeline consists of interconnected GenServers responsible for different stages:
defmodule Codexis.Service.PdfBuilder do
use GenServer
defstruct [:metadata, :command_port, status: :pending, errors: []]
# Interface functions
def start_link(args), do: GenServer.start_link(__MODULE__, args)
def build(pid), do: GenServer.call(pid, :build)
# Callback implementations with staged processing
end
Each stage of the pipeline emits telemetry events:
def handle_call(:build, _from, state) do
start_time = System.monotonic_time()
result = with {:ok, state} <- prepare_environment(state),
{:ok, state} <- process_images(state),
{:ok, state} <- compile_latex(state),
{:ok, state} <- finalize_output(state) do
{:ok, state}
end
end_time = System.monotonic_time()
:telemetry.execute(
[:codexis, :pdf, :build],
%{duration: end_time - start_time},
%{metadata: state.metadata, result: result}
)
# Handle result and return
end
The supervision strategy handles failures gracefully:
defmodule Codexis.Supervisor.BuildPipeline do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
children = [
{Codexis.Service.EnvironmentManager, []},
{Codexis.Service.ImageProcessor, []},
{Codexis.Service.LatexCompiler, []},
{Codexis.Service.OutputProcessor, []}
]
Supervisor.init(children, strategy: :rest_for_one)
end
end
Phase 4: Format Conversion and Validation (Completed)
Phase 4 extended the system to support multiple output formats and implements comprehensive validation. This phase leveraged Elixir's pattern matching for format-specific processing while maintaining a consistent interface.
We've implemented format-specific builders:
defmodule Codexis.Service.FormatBuilder do
@callback build(metadata :: Codexis.Domain.Metadata.t(), config :: term()) ::
{:ok, output_path :: String.t()} | {:error, term()}
end
defmodule Codexis.Service.EpubBuilder do
@behaviour Codexis.Service.FormatBuilder
@impl true
def build(metadata, config) do
# EPUB-specific building logic
end
end
Validation uses a strategy pattern:
defmodule Codexis.Service.Validator do
@callback validate(path :: String.t(), format :: atom()) ::
{:ok, report :: map()} | {:error, term()}
end
defmodule Codexis.Service.PdfValidator do
@behaviour Codexis.Service.Validator
@impl true
def validate(path, :pdf) do
# PDF-specific validation using the command port
end
end
Phase 5: Publishing System (Completed)
We've successfully implemented a comprehensive publishing system with a primary focus on WikiJS integration. The publishing system enables direct content publication to WikiJS, supporting multiple content formats and automating asset management.
The core publisher facade provides a unified interface:
defmodule Codexis.Publisher do
@doc """
Publishes content to the specified target(s)
"""
@spec publish(content :: map(), targets :: atom() | [atom()], options :: keyword()) ::
{:ok, map()} | {:error, term()}
def publish(content, targets \\ :all, options \\ []) do
# Implementation that routes to target-specific publishers
end
end
The WikiJS publisher handles the specifics of WikiJS integration:
defmodule Codexis.Publisher.WikiJS do
use GenServer
@doc """
Publishes content to WikiJS
"""
def publish(content, options \\ []) do
GenServer.call(__MODULE__, {:publish, content, options})
end
# Server implementation handling communication with WikiJS API
end
Content formatting supports multiple formats:
defmodule Codexis.Publisher.WikiJS.Formatter do
@doc """
Converts content to WikiJS-compatible format
"""
def format(content, options \\ []) do
format = content_format(content)
case format do
:markdown -> format_markdown(content, options)
:html -> format_html(content, options)
:latex -> format_latex(content, options)
_ -> {:error, {:unsupported_format, format}}
end
end
end
Asset management handles image discovery and uploading:
defmodule Codexis.Publisher.WikiJS.AssetUploader do
@doc """
Uploads assets to WikiJS
"""
def upload(asset_path, options \\ []) do
# Implementation that manages asset uploads
end
end
Bidirectional synchronization is enabled through webhook integration:
defmodule Codexis.Publisher.WikiJS.WebhookHandler do
@doc """
Handles webhooks from WikiJS
"""
def handle(payload, event) do
# Implementation that processes different event types
end
end
This publishing system provides a crucial final stage in the content pipeline, enabling seamless integration with external knowledge management systems.
Phase 6: Integration and Migration (In Progress)
The current phase focuses on integrating all components and providing a smooth migration path from the existing Ruby system. This includes creating compatibility adapters for existing data and establishing a command-line interface that maintains backward compatibility.
We're creating a facade that provides a simple interface for external systems:
defmodule Codexis do
@moduledoc """
Main entry point for the Codexis system.
"""
@doc """
Builds an ebook from the given metadata file.
"""
@spec build_ebook(metadata_path :: String.t(), formats :: [atom()], opts :: keyword()) ::
{:ok, [output_path :: String.t()]} | {:error, term()}
def build_ebook(metadata_path, formats, opts \\ []) do
# High-level orchestration logic
end
end
Command-line interface integration will maintain compatibility with existing scripts:
defmodule Codexis.CLI do
def main(args) do
# Parse args and call appropriate Codexis functions
end
end
Testing Strategy
Our testing strategy addresses the shortcomings observed in the Ruby implementation by leveraging Elixir's powerful testing capabilities.
We've implemented property-based testing using StreamData for critical components:
defmodule Codexis.Domain.MetadataTest do
use ExUnit.Case
use ExUnitProperties
property "validates complete metadata" do
check all file_name <- string(:alphanumeric, min_length: 1),
title <- string(:printable),
creator <- string(:printable) do
metadata = %Codexis.Domain.Metadata{
file_name: file_name,
title: [%{type: "main", text: title}],
creator: [%{role: "author", text: creator}]
}
assert {:ok, _} = Codexis.Domain.Metadata.validate(metadata)
end
end
end
For integration tests, we use test doubles and mocks to isolate components:
defmodule Codexis.Service.PdfBuilderTest do
use ExUnit.Case
setup do
command_port = fn
"pdflatex " <> _ -> {:ok, "PDF successfully generated"}
"convert " <> _ -> {:ok, "Image processed"}
_ -> {:error, "Unknown command"}
end
%{command_port: command_port}
end
test "builds PDF from valid metadata", %{command_port: command_port} do
# Test implementation using the command_port function
end
end
End-to-end tests verify overall system behavior:
defmodule Codexis.IntegrationTest do
use ExUnit.Case
@tag :integration
test "builds PDF from fixture" do
# Integration test implementation
end
end
Monitoring and Observability
A key advantage of our Elixir implementation is comprehensive monitoring and observability. We've implemented telemetry throughout the system:
defmodule Codexis.Telemetry do
def setup do
:telemetry.attach(
"codexis-logger",
[:codexis, :build, :complete],
&Codexis.Telemetry.handle_event/4,
nil
)
end
def handle_event([:codexis, :build, :complete], measurements, metadata, _config) do
# Log and/or store metrics
end
end
This allows integration with monitoring systems like Prometheus through TelemetryMetricsPrometheus.
Current Status and Next Steps
The Elixir migration has made significant progress, with five of the six planned phases now complete. The core domain model, command execution framework, PDF generation pipeline, format conversion and validation, and publishing system are all fully operational. The WikiJS integration now provides a comprehensive publishing solution for the content pipeline.
Our next steps involve completing the integration and migration phase, focusing on:
- Finalizing the command-line interface to maintain backward compatibility with existing scripts and workflows.
- Implementing comprehensive integration tests across the entire system.
- Completing the CI/CD pipeline setup with automated testing and deployment.
- Enhancing documentation for system operators and content creators.
- Adding additional publishing targets beyond WikiJS to support diverse distribution needs.
Conclusion
This migration has successfully transformed the Ruby-based ebook conversion system into a robust Elixir/OTP application. The implementation addresses the architectural challenges identified in the previous system while leveraging Elixir's strengths in concurrency, fault tolerance, and functional programming.
The addition of the WikiJS integration completes the publishing pipeline, enabling seamless content distribution to knowledge management platforms. The system now provides a comprehensive solution for ebook generation, validation, and publication with improved reliability, maintainability, and observability.