home ~ projects ~ socials

Create An Atom Feed In Rust

Introduction

This is what I'm doing to make an Atom feed for my site. More notes below the code.

//! ```cargo
//! [dependencies]
//! atom_syndication = "0.12.2"
//! chrono = { version = "0.4.26", features = ["std"] }
//! uuid = { version = "1.6.1", features = ["v5"] }
//! ```

use atom_syndication::*;
use chrono::prelude::*;
use std::collections::BTreeMap;
use std::fs;
use uuid::Uuid;


fn main() {

  let feed_constant_uuid4 = Uuid::try_parse(
    "b71fc261-fc1a-4220-b43f-2be4b216c0b2"
  ).unwrap();

  let generation_date = chrono::offset::Utc::now().fixed_offset();

  let author = Person {
    name: "Alan W. Smith".to_string(), 
    email: None,
    uri: Some("https://www.example.com/".to_string())
  };

  let entry_uuid5 = format!("urn:uuid:{}", Uuid::new_v5(
    &feed_constant_uuid4, b"unque_file_id_alfa")
      .as_hyphenated()
      .to_string()
  );

  let entry_title = Text {
    value: "This Is A Title".to_string(),
    base: None, 
    lang: None,
    r#type: TextType::Text
  };

  let tz_hour_offset = 5; 
  let tz = FixedOffset::west_opt(tz_hour_offset * 60 * 60).unwrap();
  let updated_date = NaiveDateTime::parse_from_str("2023-12-17 10:33:55", "%Y-%m-%d %H:%M:%S")
    .unwrap()
    .and_local_timezone(tz)
    .unwrap();

  let entry_content = Some(
    Content {
      base: None, 
      lang: None, 
      value: Some("<p>Hello, world</p>".to_string()),
      src: Some("https://www.example.com/some_path.html".to_string()),
      content_type: Some("html".to_string()),
    }
  );

  let entry = Entry {
      id: entry_uuid5,
      title: entry_title,
      authors: vec![author.clone()],
      categories: vec![],
      content: entry_content,
      summary: None, 
      source: None,
      rights: None, 
      published: None, 
      links: vec![],
      contributors: vec![],
      updated: updated_date,
      extensions: BTreeMap::new() 
  };

  let entries = vec![entry];

  let feed_id = format!(
    "urn:uuid:{}",
    feed_constant_uuid4.as_hyphenated()
  );

  let feed_link = Link{
    href: "https://www.example.com/atom.xml".to_string(),
    rel: "self".to_string(),
    hreflang: None, 
    length: None, 
    mime_type: None,
    title: None
  };

  let feed = Feed {
    title: "My Example Feed".into(),
    id: feed_id,
    updated: generation_date,
    authors: vec![author],
    links: vec![feed_link],
    entries,
    ..Default::default()
  };

  let config = WriteConfig {
    write_document_declaration: true,
    indent_size: Some(2),
  };

  let file = fs::OpenOptions::new()
    .create(true)
    .truncate(true)
    .write(true)
    .open("/Users/alan/Desktop/atom-test.xml").unwrap();

  // output the file
  feed.write_with_config(&file, config).unwrap();

  // print it for demo
  println!("{}", feed.to_string());
}

Results

<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>My Example Feed</title>
  <id>urn:uuid:b71fc261-fc1a-4220-b43f-2be4b216c0b2</id>
  <updated>2024-01-02T23:29:27.999342+00:00</updated>
  <author>
    <name>Alan W. Smith</name>
    <uri>https://www.example.com/</uri>
  </author>
  <link href="https://www.example.com/atom.xml" rel="self"/>
  <entry>
    <title>This Is A Title</title>
    <id>urn:uuid:b1507332-d46c-5bcf-929a-f89c59e01bac</id>
    <updated>2023-12-17T10:33:55+05:00</updated>
    <author>
      <name>Alan W. Smith</name>
      <uri>https://www.example.com/</uri>
    </author>
    <content type="html" src="https://www.example.com/some_path.html">&lt;p&gt;Hello, world&lt;/p&gt;</content>
  </entry>
</feed>

Notes

  • This is for an Atom feed. A similar crate exists for RSS (linked below)
  • I went with Atom based off >this post>https://peterbabic.dev/blog/using-uuid-in-atom-feed/>
  • The UUIDs work by creating a UUID4 that stays the same every time the feed is generated and then a UUID5 for each file based off an ID that's attached to the file
  • The UUID5 is a combination of the main feeds UUID4 + the unique ID for the file. Given those two inputs the output is always the same. That lets the individual entry IDs stay the same even if their content changes.
  • The updated_date is a atom_syndication::FixedDateTime which is an alias of chrono::DateTime<::chrono::FixedOffset> from the chrono crate
  • My content file dates don't have a timezone (e.g. 2023-07-15 18:02:07). The tz_hour_offset is my offset from GMT which I use to calculate the data to get to the DateTime that's needed
  • The rest of the date code converts to the necessary object format that then outputs with a timezone like: 2023-07-15T18:02:07+05:00 in the feed
  • There's a bunch of things set to None across the code. The docs (linked below) list all those details
  • The content comes out as HTML escaped characters. (e.g. <br> becomes &lt;br&gt; I've seen a bunch of other feeds that use CDATE, but the escaping works just fine in my reader (NetNewsWire)
  • I put two outputs in the example. The first one outputs to a file with a config attached that pretty prints the contents. The second one outputs a string. I haven't figure out a way to pretty print that though
  • I'm not sure what the 'rel' should be. I tried "canonical" at first, but it didn't work with RSS Parrot. I've now changed it to "self" which is what I've now seen on other feeds. We'll see what happens
-- end of line --

References