ChatGPT wrote a Rust program for me that generates an RSS feed from Markdown files

A number of people kept telling me to get an RSS feed for my blog. It's a pretty standard feature for any content management system, but of course, because I like Rust, I use mdBook instead. As much as I like it, it doesn't automatically generate an RSS feed. There are a couple of crates I could set up as a preprocessor for mdBook, but they either don't work or don't do what I want. But hey, didn't I just say I like Rust? Yes... I did. But no... I don't want to write an RSS feed generator based on the Markdown files. If I had my own junior software developer, I could just tell them to write it for me—but I don't, do I?

Actually... maybe I do. How about ChatGPT? Isn't this exactly what it's meant for? Without dwelling on it too much, I found myself writing the following prompt:

Create code in Rust that goes through all directories recursively and reads all *.md (markdown) files. Based on those files, it creates an RSS feed stored in rss.xml. The title, publication date, author, URL and description of the RSS items should be taken from the markdown files themselves, which are stored at the beginning of those files in the following manner: 
--- 
title: <title as string>
pub_date: <time and date as string>
author: <author as string>
url: <URL>
description: <description as string> 
--- 
Please make the code Rust idiomatic.

Along with a detailed explanation, I got the following code back from my junior dev:

use serde::Deserialize;
use std::{fs, io, path::Path};
use walkdir::WalkDir;
use rss::{ChannelBuilder, ItemBuilder, Channel};
use std::fs::File;

#[derive(Debug, Deserialize)]
struct FrontMatter {
    title: String,
    pub_date: String,
    author: String,
    url: String,
    description: String,
}

// Function to parse front matter from a markdown file
fn parse_front_matter(content: &str) -> Option<FrontMatter> {
    let delimiter = "---";
    let parts: Vec<&str> = content.splitn(3, delimiter).collect();
    if parts.len() == 3 {
        serde_yaml::from_str(parts[1]).ok()
    } else {
        None
    }
}

// Function to process a markdown file and extract the RSS item information
fn process_markdown_file(path: &Path) -> Option<rss::Item> {
    if let Ok(content) = fs::read_to_string(path) {
        if let Some(front_matter) = parse_front_matter(&content) {
            let item = ItemBuilder::default()
                .title(Some(front_matter.title))
                .pub_date(Some(front_matter.pub_date))
                .author(Some(front_matter.author))
                .link(Some(front_matter.url))
                .description(Some(front_matter.description))
                .build();
            return Some(item);
        }
    }
    None
}

// Function to traverse directories and process all markdown files
fn collect_markdown_files(dir: &str) -> Vec<rss::Item> {
    let mut items = Vec::new();

    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
        let path = entry.path();
        if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("md") {
            if let Some(item) = process_markdown_file(path) {
                items.push(item);
            }
        }
    }

    items
}

fn main() -> io::Result<()> {
    // Define the directory to search for markdown files
    let directory = "./";

    // Collect markdown files and generate RSS items
    let items = collect_markdown_files(directory);

    // Build the RSS feed
    let channel = ChannelBuilder::default()
        .title("My Markdown RSS Feed")
        .link("http://example.com")
        .description("RSS feed generated from markdown files")
        .items(items)
        .build();

    // Write the RSS feed to an XML file
    let file = File::create("rss.xml")?;
    channel.pretty_write_to(file)?;

    Ok(())
}

It looks kinda okay, but does it work? Of course not.

I tried to build it, and it didn’t even compile—I'd expect the code from my junior dev to at least compile. But okay, let's try another prompt:

I'm getting this error: 
91 | channel.pretty_write_to(file)?; | ^^^^^^^^^^^^^^^------ two arguments of type `u8` and `usize` are missing

My junior dev, not thinking too much, spat out a fix:

use std::io::Write; // Import Write trait for pretty_write_to

fn main() -> io::Result<()> {
    // Define the directory to search for markdown files
    let directory = "./";

    // Collect markdown files and generate RSS items
    let rss_items = collect_markdown_files(directory);

    // Build the RSS feed with sorted items
    let channel = ChannelBuilder::default()
        .title("My Markdown RSS Feed")
        .link("http://example.com")
        .description("RSS feed generated from markdown files")
        .items(rss_items.into_iter().map(|rss_item| rss_item.item).collect::<Vec<_>>())
        .build();

    // Write the RSS feed to an XML file with pretty formatting
    let mut file = File::create("rss.xml")?;
    
    // Set indentation settings (2 spaces per indent)
    channel.pretty_write_to(&mut file, b' ', 2)?;

    Ok(())
}

Okay, it fixed the mentioned error. Does it compile now? Nope... Another prompt:

now I'm getting this error:

Compiling mdrss v0.1.0 (/home/user/dev/mdrss) warning: unused import: `Channel` --> src/main.rs:3:11 | 3 | use rss::{Channel, ChannelBuilder, ItemBuilder}; | ^^^^^^^ | = note: `#[warn(unused_imports)]` on by default warning: unused `Result` that must be used --> src/main.rs:91:5 | 91 | channel.pretty_write_to(&mut file, b' ', 2); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this `Result` may be an `Err` variant, which should be handled = note: `#[warn(unused_must_use)]` on by default help: use `let _ = ...` to ignore the resulting value | 91 | let _ = channel.pretty_write_to(&mut file, b' ', 2); | +++++++ warning: `mdrss` (bin "mdrss") generated 2 warnings (run `cargo fix --bin "mdrss"` to apply 1 suggestion) Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.75s

Say what you will, my junior dev doesn’t give up. Took the prompt and gave me the fix back:

use serde::Deserialize;
use std::{fs, io, path::Path};
use walkdir::WalkDir;
use rss::{ChannelBuilder, ItemBuilder};
use chrono::{DateTime, Utc};
use chrono::format::ParseError;
use std::fs::File;
use std::io::Write;

// Struct to hold the parsed front matter
#[derive(Debug, Deserialize)]
struct FrontMatter {
    title: String,
    pub_date: String,
    author: String,
    url: String,
    description: String,
}

// Function to parse the publication date as a `DateTime<Utc>`
fn parse_pub_date(date_str: &str) -> Result<DateTime<Utc>, ParseError> {
    // Assuming the date in the markdown files is in RFC3339 format (e.g., "2023-09-13T00:00:00Z")
    date_str.parse::<DateTime<Utc>>()
}

// Function to parse front matter from a markdown file
fn parse_front_matter(content: &str) -> Option<FrontMatter> {
    let delimiter = "---";
    let parts: Vec<&str> = content.splitn(3, delimiter).collect();
    if parts.len() == 3 {
        serde_yaml::from_str(parts[1]).ok()
    } else {
        None
    }
}

// Struct to hold an RSS item along with its parsed publication date
struct RssItem {
    pub_date: DateTime<Utc>,
    item: rss::Item,
}

// Function to process a markdown file and extract the RSS item information
fn process_markdown_file(path: &Path) -> Option<RssItem> {
    if let Ok(content) = fs::read_to_string(path) {
        if let Some(front_matter) = parse_front_matter(&content) {
            if let Ok(pub_date) = parse_pub_date(&front_matter.pub_date) {
                let item = ItemBuilder::default()
                    .title(Some(front_matter.title))
                    .pub_date(Some(front_matter.pub_date))
                    .author(Some(front_matter.author))
                    .link(Some(front_matter.url))
                    .description(Some(front_matter.description))
                    .build();
                
                return Some(RssItem { pub_date, item });
            }
        }
    }
    None
}

// Function to traverse directories and process all markdown files
fn collect_markdown_files(dir: &str) -> Vec<RssItem> {
    let mut items = Vec::new();

    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
        let path = entry.path();
        if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("md") {
            if let Some(rss_item) = process_markdown_file(path) {
                items.push(rss_item);
            }
        }
    }

    // Sort items by publication date (descending)
    items.sort_by(|a, b| b.pub_date.cmp(&a.pub_date));

    items
}

fn main() -> io::Result<()> {
    // Define the directory to search for markdown files
    let directory = "./";

    // Collect markdown files and generate RSS items
    let rss_items = collect_markdown_files(directory);

    // Build the RSS feed with sorted items
    let channel = ChannelBuilder::default()
        .title("My Markdown RSS Feed")
        .link("http://example.com")
        .description("RSS feed generated from markdown files")
        .items(rss_items.into_iter().map(|rss_item| rss_item.item).collect::<Vec<_>>())
        .build();

    // Write the RSS feed to an XML file with pretty formatting
    let mut file = File::create("rss.xml")?;
    
    // Set indentation settings (2 spaces per indent)
    channel.pretty_write_to(&mut file, b' ', 2).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;

    Ok(())
}

Nice! That at least compiles! It even highlighted the key changes. After cleaning it up a bit and removing some unused imports, I built and tested the program with my mdBook content, and it properly generated the rss.xml file. Nice indeed. So, as with any junior dev, let’s push it a bit further. Here’s another prompt:

can you now make a change to the code so that the directory path is provided as a command line argument?

Again, along with some explanation, it proudly came back with the code:

use serde::Deserialize;
use std::{fs, io, path::Path};
use walkdir::WalkDir;
use rss::{ChannelBuilder, ItemBuilder};
use chrono::{DateTime, Utc};
use chrono::format::ParseError;
use std::fs::File;
use std::io::Write;
use std::env; // To access command line arguments

// Struct to hold the parsed front matter
#[derive(Debug, Deserialize)]
struct FrontMatter {
    title: String,
    pub_date: String,
    author: String,
    url: String,
    description: String,
}

// Function to parse the publication date as a `DateTime<Utc>`
fn parse_pub_date(date_str: &str) -> Result<DateTime<Utc>, ParseError> {
    // Assuming the date in the markdown files is in RFC3339 format (e.g., "2023-09-13T00:00:00Z")
    date_str.parse::<DateTime<Utc>>()
}

// Function to parse front matter from a markdown file
fn parse_front_matter(content: &str) -> Option<FrontMatter> {
    let delimiter = "---";
    let parts: Vec<&str> = content.splitn(3, delimiter).collect();
    if parts.len() == 3 {
        serde_yaml::from_str(parts[1]).ok()
    } else {
        None
    }
}

// Struct to hold an RSS item along with its parsed publication date
struct RssItem {
    pub_date: DateTime<Utc>,
    item: rss::Item,
}

// Function to process a markdown file and extract the RSS item information
fn process_markdown_file(path: &Path) -> Option<RssItem> {
    if let Ok(content) = fs::read_to_string(path) {
        if let Some(front_matter) = parse_front_matter(&content) {
            if let Ok(pub_date) = parse_pub_date(&front_matter.pub_date) {
                let item = ItemBuilder::default()
                    .title(Some(front_matter.title))
                    .pub_date(Some(front_matter.pub_date))
                    .author(Some(front_matter.author))
                    .link(Some(front_matter.url))
                    .description(Some(front_matter.description))
                    .build();
                
                return Some(RssItem { pub_date, item });
            }
        }
    }
    None
}

// Function to traverse directories and process all markdown files
fn collect_markdown_files(dir: &str) -> Vec<RssItem> {
    let mut items = Vec::new();

    for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
        let path = entry.path();
        if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("md") {
            if let Some(rss_item) = process_markdown_file(path) {
                items.push(rss_item);
            }
        }
    }

    // Sort items by publication date (descending)
    items.sort_by(|a, b| b.pub_date.cmp(&a.pub_date));

    items
}

fn main() -> io::Result<()> {
    // Get the directory path from the command line arguments
    let args: Vec<String> = env::args().collect();
    let directory = if args.len() > 1 {
        &args[1] // Use the provided directory
    } else {
        "./" // Default to the current directory
    };

    // Collect markdown files and generate RSS items
    let rss_items = collect_markdown_files(directory);

    // Build the RSS feed with sorted items
    let channel = ChannelBuilder::default()
        .title("My Markdown RSS Feed")
        .link("http://example.com")
        .description("RSS feed generated from markdown files")
        .items(rss_items.into_iter().map(|rss_item| rss_item.item).collect::<Vec<_>>())
        .build();

    // Write the RSS feed to an XML file with pretty formatting
    let mut file = File::create("rss.xml")?;
    
    // Set indentation settings (2 spaces per indent)
    channel.pretty_write_to(&mut file, b' ', 2).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;

    Ok(())
}

Well, what happened to the idiomatic Rust?

are there any more places in the code where you can be more idiomatic?

Okay, that’s a bit better:

use serde::Deserialize;
use std::{fs, io, path::{Path, PathBuf}};
use walkdir::WalkDir;
use rss::{ChannelBuilder, ItemBuilder};
use chrono::{DateTime, Utc};
use std::fs::File;
use std::io::Write;
use std::env;
use std::ffi::OsString;

// Struct to hold the parsed front matter
#[derive(Debug, Deserialize)]
struct FrontMatter {
    title: String,
    pub_date: String,
    author: String,
    url: String,
    description: String,
}

// Function to parse the publication date as a `DateTime<Utc>`
fn parse_pub_date(date_str: &str) -> Result<DateTime<Utc>, chrono::format::ParseError> {
    date_str.parse::<DateTime<Utc>>()
}

// Function to parse front matter from a markdown file
fn parse_front_matter(content: &str) -> Option<FrontMatter> {
    let delimiter = "---";
    let parts: Vec<&str> = content.splitn(3, delimiter).collect();
    if parts.len() == 3 {
        serde_yaml::from_str(parts[1]).ok()
    } else {
        None
    }
}

// Struct to hold an RSS item along with its parsed publication date
struct RssItem {
    pub_date: DateTime<Utc>,
    item: rss::Item,
}

// Function to process a markdown file and extract the RSS item information
fn process_markdown_file(path: &Path) -> Option<RssItem> {
    // Using `ok()` and `and_then()` to chain the logic more concisely
    fs::read_to_string(path).ok().and_then(|content| {
        parse_front_matter(&content).and_then(|front_matter| {
            parse_pub_date(&front_matter.pub_date).ok().map(|pub_date| {
                let item = ItemBuilder::default()
                    .title(Some(front_matter.title))
                    .pub_date(Some(front_matter.pub_date))
                    .author(Some(front_matter.author))
                    .link(Some(front_matter.url))
                    .description(Some(front_matter.description))
                    .build();
                
                RssItem { pub_date, item }
            })
        })
    })
}

// Function to traverse directories and process all markdown files
fn collect_markdown_files(dir: &Path) -> Vec<RssItem> {
    WalkDir::new(dir)
        .into_iter()
        .filter_map(|entry| entry.ok()) // Handle invalid directory entries
        .filter(|entry| {
            entry.path().is_file() && entry.path().extension().and_then(|s| s.to_str()) == Some("md")
        })
        .filter_map(|entry| process_markdown_file(entry.path()))
        .collect::<Vec<_>>() // Collect all valid markdown files
}

// Main function to handle command line arguments and generate the RSS feed
fn main() -> io::Result<()> {
    // Get the directory path from command line arguments, defaulting to current directory
    let directory: PathBuf = env::args_os().nth(1)
        .map(PathBuf::from)  // Convert to PathBuf
        .unwrap_or_else(|| PathBuf::from("./"));

    // Collect markdown files and generate RSS items
    let mut rss_items = collect_markdown_files(&directory);

    // Sort items by publication date (descending)
    rss_items.sort_by(|a, b| b.pub_date.cmp(&a.pub_date));

    // Build the RSS feed with sorted items
    let channel = ChannelBuilder::default()
        .title("My Markdown RSS Feed")
        .link("http://example.com")
        .description("RSS feed generated from markdown files")
        .items(rss_items.into_iter().map(|rss_item| rss_item.item).collect::<Vec<_>>())
        .build();

    // Write the RSS feed to an XML file with pretty formatting
    let mut file = File::create("rss.xml")?;
    channel.pretty_write_to(&mut file, b' ', 2)
        .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;

    Ok(())
}

But are we done? Of course not. I actually wanted this to be a Rust library so that I can use it as a pre-processor for mdBook.

Based on the last code you provided, make a Rust library with one API function. This function should take two parameters: 
- path to the directory with markdown files 
- destination path to the generated rss.xml file

This is where I actually got caught by surprise. Not only did it provide the code, but also unit and integration tests. Yes, you read that right. When was the last time you got a set of unit and integration tests from your dev (without even asking for them)?

I was very excited. Although the tests didn’t work, the attitude of my junior dev was commendable. Despite some exchanges with it, it wasn’t able to fix the tests it provided, which was a bit disappointing, but maybe that was my fault. I’m a junior prompt engineer, after all.

So, after cleaning it up a bit and making it actually work, the library was done.

But I wasn’t finished yet, so I made a few more requests. Here are the prompts:

Now, how can I use this library to automatically generate rss.xml file when generating my mdBook using Rust mdBook?
How can I publish my mdrss_lib library so that I can use it with cargo install or as part of dependencies in cargo.toml?
And if I wanted to publish my mdbook-rss-preprocessor so that people can download it and use it with cargo install?
for the mdrss_lib, please create a cli application that will take two cli arguments: - path do the directory with md files - path to rss.xml

Although I'm not going to paste them here, for all of those prompts, I received fairly decent answers, with code and explanations. If you're curious, here’s the link to the entire chat exchange.

However, I now have my RSS feed generator! Here are the results:

  • mdrss - A library that handles RSS generation, which has even been published on crates.io.
  • mdrss-cli - A CLI application that utilizes the library, which I now use for RSS generation based on my mdBook files.

Is it production code? Is it proper, idiomatic Rust? Did ChatGPT generate it with no errors? Although the problem that ChatGPT had to solve was trivial, the answer to these questions is resounding NO. However, it was good enough for me to make quick fixes and get the functionality I needed in a matter of minutes instead of hours (note that I'm not a Rust developer). An experienced Rust developers would do a much better job and probably faster, but let’s face it, I don’t have any working for me for free. I don't think ChatGPT is ready to replace a proper software engineer. But in this particular case, the sad reality is that, if not for ChatGPT, I still wouldn’t have an RSS feed on my blog—I'm just too lazy to develop it myself. So, although I never thought I’d say this, I finally found a reason to use ChatGPT for something that is coding-related.