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:
|
|
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.rbNote: 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 aConversionResultwhen conversion completes, or called withnilto 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 fromFilePaneland passes it toPreviewPanel#show_originalon_conversion_result— receives the result fromFilePaneland passes it toPreviewPanel#show_result, or returns the current output when called withnil(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/O — FileDialog 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