brickset 0.1.1

BrickSet API bindings
Documentation
use brickset::v3::Response;
use brickset::v3::response::{CheckUserHashResponse, LegoComDetails, LoginResponse, GetSetsResponse};
use brickset::v3::request::{OrderBy, CheckUserHash, BricksetRequest, ENDPOINT, Login, GetSets, GetSetsParameters};
use dotenv;
use reqwest::Client;
use std::{env, fs::File, io::Write};

#[tokio::main]
async fn main() {
    if dotenv::from_filename(".env.examples").is_err() {
        println!("Did not find .env.examples! If BRICKSET_KEY and BRICKSET_USERNAME aren't set in your environment, the example will crash.");
    }

    // load stuff from the environment
    let api_key = env::var("BRICKSET_KEY")
        .expect("Missing environment variable BRICKSET_KEY");
    let username = env::var("BRICKSET_USERNAME")
        .expect("Missing environment variable BRICKSET_USERNAME");

    let mut client = reqwest::Client::default();

    // log into BrickSet
    let user_hash = log_into_brickset(&mut client, &api_key, &username).await;

    // build the getSets request URI and body
    let params = GetSetsParameters::new()
        .wanted_by_user(true)
        .order_by(OrderBy::PiecesDESC)
        .page_size(500);
    let builder = GetSets::new(&api_key, Some(&user_hash), params);
    let dest = ENDPOINT.join(builder.method_name()).expect("encoding url");
    let mut body = url::form_urlencoded::Serializer::new(String::new());
    builder.encode_query(&mut body).expect("encoding body");
    let body = body.finish();

    // create a reqwest::Request
    let request = client.post(dest)
        .header(reqwest::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
        .header(reqwest::header::CONTENT_LENGTH, body.as_bytes().len())
        .body(body)
        .build()
        .expect("building request");

    // execute the request
    let response = client.execute(request)
        .await
        .expect("executing request")
        .text()
        .await
        .expect("getting response body");

    // process the response
    let sets: GetSetsResponse = match serde_json::from_str(&response).expect("from_str") {
        Response::Ok(sets) => sets,
        Response::Err(err) => {
            panic!("BrickSet error on getSets: {err}")
        },
    };

    // print results
    println!("User has {} sets in wantlist", sets.matches);

    let unknown = "Unknown".to_string();

    for set in sets.sets.iter() {
        println!("{}-{} {}", set.number, set.number_variant, set.name.as_ref().unwrap_or(&unknown));
        println!("  Theme: {} (Subtheme: {})", set.theme.as_ref().unwrap_or(&unknown), set.subtheme.as_ref().unwrap_or(&unknown));
        println!("  Pieces: {}", set.pieces.map_or_else(|| unknown.clone(), |p| p.to_string()));
        println!("  Availability: {}", set.availability.as_ref().unwrap_or(&unknown));
        println!("  Pricing on LEGO.com:");
        print_pricing("USD", &set.lego_com.united_states);
        print_pricing("CAD", &set.lego_com.canada);
        print_pricing("EUR", &set.lego_com.germany);
        print_pricing("GBP", &set.lego_com.united_kingdom);
    }
}

fn print_pricing(tag: &str, details: &LegoComDetails) {
    if let Some(price) = details.retail_price {
        print!("  - {price:.2} {tag}");
        if let Some(last_available) = details.date_last_available {
            print!(" Last available: {}", last_available.format("%Y-%m-%d"));
        }
        println!("");
    }
}

async fn log_into_brickset(client: &mut Client, api_key: &str, username: &str) -> String {
    println!("Logging in...");

    // try to log in using cached token
    if dotenv::from_filename(".env.examples.generated").is_ok() {
        if let Some(user_hash) = env::var("BRICKSET_USER_HASH").ok() {
            // build the checkUserHash request URI and body
            let builder = CheckUserHash::new(api_key, &user_hash);
            let dest = ENDPOINT.join(builder.method_name()).expect("encoding url");
            let mut body = url::form_urlencoded::Serializer::new(String::new());
            builder.encode_query(&mut body).expect("encoding body");
            let body = body.finish();

            // create a reqwest::Request
            let request = client.post(dest)
                .header(reqwest::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
                .header(reqwest::header::CONTENT_LENGTH, body.as_bytes().len())
                .body(body)
                .build()
                .expect("building request");

            // execute the request
            let response = client.execute(request)
                .await
                .expect("executing request")
                .text()
                .await
                .expect("getting response body");

            // process the response
            match serde_json::from_str(&response).expect("from_str") {
                Response::Ok(CheckUserHashResponse {}) => {
                    println!("Logging in using cached token");
                    return user_hash;
                }
                Response::Err(err) => {
                    println!("Could not log in with cached token: {err}")
                },
            }
        }
    }

    // couldn't log in using cached token, so ask for a password
    println!("Username: {username}");
    let password = rpassword::prompt_password("Password: ").unwrap();

    // build the login request URI and body
    let builder = Login::new(api_key, username, &password);
    let dest = ENDPOINT.join(builder.method_name()).expect("encoding url");
    let mut body = url::form_urlencoded::Serializer::new(String::new());
    builder.encode_query(&mut body).expect("encoding body");
    let body = body.finish();

    // create a reqwest::Request
    let request = client.post(dest)
        .header(reqwest::header::CONTENT_TYPE, "application/x-www-form-urlencoded")
        .header(reqwest::header::CONTENT_LENGTH, body.as_bytes().len())
        .body(body)
        .build()
        .expect("building request");

    // execute the request
    let response = client.execute(request)
        .await
        .expect("executing request")
        .text()
        .await
        .expect("getting response body");

    // process the response
    match serde_json::from_str(&response).expect("from_str") {
        Response::Ok(LoginResponse { hash }) => {
            println!("Successfully logged in");

            // successfully logged in, save the new token
            let fp = File::options()
                .create(true)
                .write(true)
                .truncate(true)
                .open(".env.examples.generated");

            if let Ok(mut fp) = fp {
                fp.write_fmt(format_args!("BRICKSET_USER_HASH={:?}", hash))
                    .expect("write_fmt");
            }

            return hash;
        }
        Response::Err(err) => {
            println!("Could not log in: {err}");
            panic!("Could not log in: {err}")
        }
    }
}