A first minimal working project in Rust
I want to learn Rust since months (even years). At the same time, I want to use Neovim for it. I struggle with this IDE though: something always does not work. I finally managed to get it work properly after months, even if the Rust LSP commands are not available (again). I have color highlighting and LSP autocompletions. I tocuhed the power for Rustancean.vim but it broke again. Fine for now. As a sidenote, if something works or it works better, commit it. Anyway.
I completeed the rustlings to get a grisp on the language, learn the rust book for 2 years. I watched videos on Youtube (some of Jon Gjengset, Code to the Moon, [Let’s Get Rusty], …). I finally started some Exercism exercises. It feels boring though. I have no incentive to complete them because they feel useless. I struggle more with algorithm than the language itself.
I need to create something a bit useful.
Meanwhile, I crawl the web and my RSS feeds are always full. I discovered (also) years ago the french digital open data. The french digital services publish open data regularly.
I want a small and easy project that can evolve. A small project where I can use a bit of everything with Rust: database, API, CLI and eventually more.
The french postcodes are perfect for it. Why is it useful? Because you may not want to rely on a third-party API for this. Here is the small data set: a commune (group of village or small cities) has a code, a name, a routing label (think the official post office name) and a postcode. A first small step would be to put this JSON into a SQLite database. How to read the file? Deserialize it in a rust struct? Open a sqlite database and execute queries? Use CLI arguments to apply some rules…
After irregular weeks or months, I finally made it. Here’s the current simple “script”: (updating color syntaxing is on the todo list :)
use rusqlite::{Connection, Result};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Commune {
#[serde(rename(deserialize = "codeCommune"))]
code: String,
#[serde(rename(deserialize = "nomCommune"))]
name: String,
#[serde(rename(deserialize = "libelleAcheminement"))]
routing_label: String,
#[serde(rename(deserialize = "codePostal"))]
postcode: String,
}
fn create_db(conn: &Connection) -> Result<()> {
conn.execute(
"CREATE TABLE IF NOT EXISTS commune (
code TEXT PRIMARY_KEY,
name TEXT NOT NULL,
routingLabel TEXT NOT NULL,
postcode TEXT NOT NULL
)",
{}
)?;
Ok(())
}
fn seed(conn: &Connection) -> Result<()> {
let f = std::fs::read_to_string("postcodes.json").expect("Could not open the source file.");
let mut insert_commune = conn.prepare("INSERT INTO commune (name, code, routingLabel, postcode) VALUES (?1, ?2, ?3, ?4)")?;
let communes: Vec<Commune> = serde_json::from_str(&f).expect("Could not read values inside the source file properly.");
for commune in &communes {
insert_commune.execute(
(
&commune.name,
&commune.code,
&commune.routing_label,
&commune.postcode,
),
)?;
}
Ok(())
}
fn main() -> Result<()> {
let db_path = "./postcodes.sqlite";
let conn = Connection::open(db_path)?;
let _ = create_db(&conn);
seed(&conn)
}
A beauty of it is I can call this project finished because I turned a JSON into a SQLite database already. The original 4.8MB postcodes.json is 3 times smaller into the 1.6MB sqlite database.
The project can be “done”, but I can extend it:
- add a CLI to build or update the database with commands such as an input file, an sqlite filename, only updating the db.
- improve the sqlite database schema: one table with four fields works. A bit YOLO but it can be better.
- optimise the load: insert the query in bulk instead of one by one (which is slow), and more
- add a simple REST API with GET methods and filters
- attach more relational data to the database
- extend the CLI to be a “sqlite database builder”: a tool to transform JSONs (or other formats) into a sqlite database.
Let’s see how this will evolve, but I am finally happy about it! To be continued.