How would you do this population of a struct?

⚓ Rust    📅 2025-08-23    👤 surdeus    👁️ 4      

surdeus

This code is intended to:

  1. Create an Article instance from an Entry instance
  2. Entry may has Option<T> fields, so some may be None
  3. Article should always be defined

So my solution so far was:

  1. Make the fields of Article always some type U (not an Option)
  2. Implement a Default trait that creates a humble Article.
  3. Populate the fields needed.

What I am having trouble doing is to "populate the fields needed" (last step above). The reason is that the fields are optional, so it seems to add more repetition than I would like.

Below there is a sample. Note that it's not supposed to compile, just to get your opinion or suggestion in the problem above (or anything worth changing).

It's very beginner code so be patient please.

/*!
  Article represents one entry of the feed.
  Display is implemented to directly print it to STDOUT.
*/

// feel free to ignore all imports below.
use std::fmt::{self, Display, Formatter};

use colored::Colorize;
use feed_rs::model::{Entry, Text};
use html2text::{self, Error as HtmlError, config};
use unicode_segmentation::UnicodeSegmentation;

use crate::{AppError, AppResult, LINE_WIDTH, N_GRAPHEMES};

#[derive(Debug)]
pub struct Article {
    pub title: String,
    pub last_updated: String,
    pub published: String,
    pub url: String,
    pub summary: String,
    pub related_links: Vec<String>,
}

impl Default for Article {
    /// This default article is used for displaying some information
    /// When there was no error, but the parsed field is empty/None.
    /// That way we also always have a constant printed structure.
    fn default() -> Self {
        Article {
            title: String::from("No title found"),
            last_updated: String::from("No update found"),
            published: String::from("No date found"),
            url: String::from("No url found"),
            summary: String::from("No summary found"),
            related_links: vec![],
        }
    }
}


/// Turn the HTML into a string with a specific length.
pub fn parse_summary(s: Option<Text>) -> Result<Option<String>, HtmlError> {
    s.map(|mut t| {
        t.sanitize();
        let mut content: String = t.content.graphemes(true).take(N_GRAPHEMES).collect();
        content.push_str("...");
        config::rich().string_from_read(content.as_bytes(), LINE_WIDTH)
    })
    .transpose() // Opt<Res> --> Res<Opt>
}

/// Create a new Article from an Entry.
pub fn new_article(i: &Entry) -> AppResult {
    // Could be an Article associated function (AF), but this is fine.
    // I don't like to return AppResult from the AF.
    let summary = match parse_summary(i.summary.clone()) {
        Ok(s) => s,
        Err(r) => return Err(AppError::Html2TextError(r)),
    };
    let title = i.title.clone().map(|mut t| {
        t.sanitize();
        t.content
    });
    let last_updated = i.updated.map(|d| d.to_string());
    let published = i.updated.map(|d| d.to_string());
    let mut default_article = Article::default();

    // Below is how it was done prior to using ::default
    // I think this would be cleaner
    // But I don't know how to populate it now.
    // Using if title.is_some {...} else {...} would create a similar 
    // situation.
    Ok(Article {
        title: title.unwrap_or(String::from("No title found")),
        last_updated: last_updated.unwrap_or(String::from("No update found")),
        published: published.unwrap_or(String::from("No date found")),
        url: i.base.clone().unwrap_or(String::from("No url found")),
        summary: summary.unwrap_or(String::from("No summary found")),
        related_links: i.links.clone().into_iter().map(|l| l.href).collect(),
    })
}

impl Display for Article {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        // Write the msg and error to the formatter.

        let to_print = format!(
            "\n{header}\n- Title: {title}\n- Summary: {summary}\n- Url: {url}\n- Links: {links}\n- {date}\n\n",
            header = "Next Item".underline().bold(),
            // Opt<T> -> new Opt<&T> -> unwrap
            title = self.title.bold().yellow(),
            summary = self.summary,
            url = self.url.blue(),
            links = self.related_links.join(",\n").blue().dimmed().underline(),
            date = format!("Date: {} | Updated: {}", self.published, self.last_updated)
        );
        write!(f, "{}", to_print)
    }
}

PS: i also thought about if let but it may still be too much repetition.

3 posts - 2 participants

Read full topic

🏷️ Rust_feed