Skip to content

Project: format converter

This project brings together every concept from Module 3 in a single useful application — a desktop tool that converts files between CSV, JSON, and XML formats, with a live preview of both the source and the converted output.

The app is genuinely useful. Ruby developers frequently need to move data between formats — log files, API responses, spreadsheet exports, configuration files. Having a desktop tool that handles this without requiring a terminal, a script, or an online converter has real everyday value.

Download the complete source and run it with:

1
2
bundle install
ruby main.rb

Download format_converter.zip

What the app does

Browse to a CSV, JSON, or XML file. The Original tab shows the raw file content immediately. Select a target format, click Convert, and the Converted tab shows the result. Save the output to a new file. The app handles nested data gracefully — JSON and XML structures are flattened to dot-notation columns when converting to CSV, and dot-notation columns are reconstructed into nested structures when converting back.

File structure

format_converter/
├── data/
├── main.rb
├── Gemfile
└── lib/
    ├── converter_frame.rb
    ├── models/
    │   └── converter.rb
    └── panels/
        ├── file_panel.rb
        └── preview_panel.rb

Note: the data folder contains some sample files for processing, some flat files and some nested.

The model — lib/models/converter.rb

The Converter class is the heart of the application. It knows nothing about widgets — it takes a file path and two format strings, and returns a ConversionResult struct containing the output and metadata about what happened.

The conversion pipeline has two stages: parse the source into an intermediate Ruby array-of-hashes representation, then serialize that representation into the target format. This intermediate representation is what makes all six conversion directions (CSV↔JSON, CSV↔XML, JSON↔XML) possible with clean, independent parser and serializer methods.

Format detection

Converter.detect_format(path) inspects the file extension and returns 'CSV', 'JSON', or 'XML'. The file panel calls this when a file is browsed and pre-selects the source format, though the user can override it.

The flat/nested problem

CSV is inherently flat — rows and columns, no nesting. JSON and XML support arbitrary depth. The model handles this in both directions:

Flattening — when the target is CSV and the source data is nested, flatten_record recursively walks the hash converting { "address" => { "city" => "Sydney" } } into { "address.city" => "Sydney" }. Arrays become semicolon-separated strings. The ConversionResult carries a flattened flag so the UI can notify the user.

Unflattening — when the source is a CSV with dot-notation column headers, unflatten_record reconstructs the nesting. "address.city" becomes address: { city: ... } again. This makes the round-trip lossless for hash structures: CSV → JSON → CSV → JSON produces identical output.

Look at flatten_record and unflatten_record in converter.rb — both are clean recursive methods that demonstrate how a small amount of Ruby can solve a non-trivial data transformation problem.

XML parsing

The app uses Nokogiri rather than Ruby’s stdlib REXML. Nokogiri’s API is more Ruby-idiomatic and it handles encoding and malformed input more gracefully. parse_xml_node recursively walks the Nokogiri document tree, and build_xml_node uses Nokogiri’s builder DSL to construct output. Look at how repeated sibling elements are collected into arrays — this is the natural XML-to-Ruby mapping.

The panels

lib/panels/file_panel.rb

The left panel manages everything related to the source file — browsing, format selection, conversion, and saving. It communicates with the frame via two keyword argument callbacks:

  • on_file_selected: — called with the file path when the user browses to a file. The frame uses this to load the original content into the preview panel.
  • on_convert: — called with a ConversionResult when conversion completes, or called with nil to retrieve the current output for saving.

The conversion itself runs in a background thread — Thread.new in on_convert, with call_after posting the result back to the main thread when done. This keeps the UI responsive for large files. The Wx::Timer.every(25) { Thread.pass } in main.rb is what makes this work reliably — see lesson 3.5 for the full explanation.

The source format choice includes an “Auto-detect” option. When selected, Converter.detect_format is called at conversion time. If the extension is unrecognised, the user is prompted to select manually.

lib/panels/preview_panel.rb

The right panel is a Wx::Notebook with two tabs — Original and Converted. Both use a monospace Wx::TextCtrl for display. The tab titles update to reflect the filename and conversion details.

show_original(path) reads the file and populates the Original tab — called immediately when the user browses, so they always see what they are working with before converting.

show_result(result) populates the Converted tab and switches to it automatically. A notice bar below the notebook appears when flattening or unflattening has occurred, explaining what happened to the data structure.

The frame — lib/converter_frame.rb

ConverterFrame is deliberately thin. It creates the splitter, instantiates both panels with their callbacks, and wires them together through two handler methods:

  • on_file_selected — receives the path from FilePanel and passes it to PreviewPanel#show_original
  • on_conversion_result — receives the result from FilePanel and passes it to PreviewPanel#show_result, or returns the current output when called with nil (the save path)

The frame never touches file content or conversion logic directly. It is a coordinator — each panel and the model are self-contained, and the frame provides the connections between them.

What this project demonstrates

Every concept from Module 3 appears here:

Event handling — button events, the close handler, notebook page switching triggered programmatically.

Application structure — the multi-file layout from lesson 3.2, with panels communicating through keyword argument callbacks rather than direct references, and a model class with no UI knowledge.

File I/OFileDialog for browsing and saving, File.read and File.write with encoding, error handling with rescue.

Threading — conversion runs in a background thread with call_after posting results to the main thread. The Wx::Timer.every(25) { Thread.pass } in main.rb is essential — without it, call_after blocks would not execute promptly on MRI Ruby.

Data transformation — the intermediate array-of-hashes representation, recursive flatten/unflatten, and the clean separation between parsers and serializers in the model.


Previous: Threading: keeping the UI responsive | Next: Module 4 — Rich Output