typestar

fetch_tours.rs in Rust

A real CLI: clap parses the flags, reqwest fetches, serde models the reply.

use std::error::Error;
use std::time::Duration;

use clap::Parser;
use serde::Deserialize;

/// Fetch the tour list for a language.
#[derive(Parser, Debug)]
#[command(name = "fetch-tours", version)]
struct Cli {
    /// Language slug, such as rust or sql
    lang: String,

    /// Base URL of the server
    #[arg(long, default_value = "https://typestar.io")]
    base: String,

    /// Only show tours with at least this many steps
    #[arg(long, default_value_t = 0)]
    min_steps: usize,

    /// Print the raw JSON as well
    #[arg(short, long)]
    verbose: bool,
}

#[derive(Debug, Deserialize)]
struct Paths {
    lang: String,
    tours: Vec<Tour>,
}

#[derive(Debug, Deserialize)]
struct Tour {
    slug: String,
    title: String,
    sections: Vec<Section>,
}

#[derive(Debug, Deserialize)]
struct Section {
    title: String,
    steps: Vec<Step>,
}

#[derive(Debug, Deserialize)]
struct Step {
    idx: u32,
    title: String,
}

impl Tour {
    fn step_count(&self) -> usize {
        self.sections.iter().map(|s| s.steps.len()).sum()
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let cli = Cli::parse();

    let client = reqwest::Client::builder()
        .timeout(Duration::from_secs(15))
        .user_agent("fetch-tours/1.0")
        .build()?;

    let url = format!("{}/api/paths?lang={}", cli.base, cli.lang);
    let response = client.get(&url).send().await?.error_for_status()?;
    let body = response.text().await?;

    if cli.verbose {
        println!("{} bytes from {url}", body.len());
    }

    let paths: Paths = serde_json::from_str(&body)?;
    println!("{} has {} tours", paths.lang, paths.tours.len());

    for tour in paths.tours.iter().filter(|t| t.step_count() >= cli.min_steps) {
        println!("{:<12} {:<32} {:>3} steps", tour.slug, tour.title,
                 tour.step_count());
        if cli.verbose {
            for section in &tour.sections {
                println!("    {} ({})", section.title, section.steps.len());
            }
        }
    }

    let widest = paths.tours.iter().max_by_key(|t| t.step_count());
    if let Some(tour) = widest {
        println!("largest tour: {}", tour.title);
    }
    Ok(())
}

How it works

  1. The Cli struct is the whole interface, help text included.
  2. The models mirror the API response the tool consumes.
  3. Errors from three crates all funnel through Box<dyn Error>.

Keywords and builtins used here

The run, in numbers

Lines
93
Characters to type
1932
Tokens
486
Three-star pace
115 tpm

At the three-star pace of 115 tokens a minute, this run takes about 254 seconds.

Type this snippet

Step 1 of 1 in Encore, step 12 of 12 in CLI tools & HTTP clients.

← Previous