Dog fact

I just discovered this dog api, which has a facts endpoint. By default, it seems to give you one random fact. Fun!

❯ curl -s https://dogapi.dog/api/v2/facts | jq '.data[].attributes.body'
"Greyhounds are the fastest dogs on earth, with speeds of up to 45 miles per hour."

I decided to make a version in Rust.

cargo new dogfact
cd dogfact
cargo add reqwest --features=blocking
cargo add serde_json
  use serde_json::Value;

  const URL: &str = "https://dogapi.dog/api/v2/facts";

  fn main() {
      let response = reqwest::blocking::get(URL).expect("Could not get request!");

      let body = response.text().expect("Could not get text!");

      let v: Value = serde_json::from_str(&body).expect("Could not parse JSON!");

      println!("{}", v["data"][0]["attributes"]["body"]);
  }

But that prints a JSON string (as does the jq version). I decided print a Rust string instead.

      let json_string = v["data"][0]["attributes"]["body"].clone();

      let rust_string = serde_json::from_value::<String>(json_string).unwrap();

      println!("{rust_string}");

That way, I could pipe the results to cowsay without worrying about embedded quotes or whatever.

❯ dogfact | cowsay
 _________________________________________
/ The oldest dog on record – a Queensland \
| Heeler named Bluey – was 29 years, 5    |
\ months old.                             /
 -----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Cowsay is an old Perl program. You can change the cow to something else if you like. However, I was shocked to find that there was no dog. So I made one.

❯ cat /usr/share/cowsay/cows/dog.cow 
$the_cow = <<EOC;
         $thoughts          |\\|\\
          $thoughts        ..    \\       .
           $thoughts     o--     \\\\    / @)
                  v__///\\\\\\\\__/ @
                    {           }
                     {  } \\\\\\{  }
                     <_|      <_|
EOC

Now our dog fact can be given to us by a dog!

❯ dogfact | cowsay -f dog
 _____________________________________
/ Dachshunds were originally bred for \
\ fighting badgers.                   /
 -------------------------------------
         \          |\|\
          \        ..    \       .
           \     o--     \\    / @)
                  v__///\\\\__/ @
                    {           }
                     {  } \\\{  }
                     <_|      <_|

Next, I decided to add the dog and balloon directly. To reformat the text to go in the balloon, we'll use textwrap

cargo add textwrap

Since Rust strings are UTF-8, I used Unicode characters– not just ASCII– to draw the dog and the balloon.

  use serde_json::Value;
  use std::borrow::Cow;

  const COLUMNS: usize = 40;

  const H: char = '─';
  const V: char = '│';
  const UL: char = '╭';
  const UR: char = '╮';
  const LL: char = '╰';
  const LR: char = '╯';
  const DH: char = '┬';

  const DOG: &[&str] = &[
      "    │",
      "    │            ┏╮┏╮",
      "    │           ╭┛┻┛┻╮     ╭━╮",
      "    │         ▅━╯▋┈▋┈┃     ╰╮┃",
      "    ╰───────  ┣━━━━━╯╰━━━━━╮┃┃", 
      "              ╰━━━━┓       ┗╯┃",
      "                   ┃┏┓┏━┳┳┓┏━╯",
      "                   ┗┛┗┛ ┗┛┗┛",
  ];

  const URL: &str = "https://dogapi.dog/api/v2/facts";

  fn main() {
      let response = reqwest::blocking::get(URL).expect("Could not get request!");

      let body = response.text().expect("Could not get text!");

      let v: Value = serde_json::from_str(&body).expect("Could not parse JSON!");

      let json_string = v["data"][0]["attributes"]["body"].clone();

      let rust_string = serde_json::from_value::<String>(json_string).unwrap();

      let message = textwrap::wrap(&rust_string, COLUMNS);
      let balloon = construct_balloon(&message);
      for line in &balloon {
          println!("{line}");
      }
      for line in DOG {
          println!("{line}");
      }
  }

  fn construct_balloon(message: &[Cow<str>]) -> Vec<String> {
      let max = message
          .iter()
          .map(|line| line.len())
          .max()
          .unwrap_or(COLUMNS);
      let max = std::cmp::max(max, 5);

      let mut tmp = [0; 4];
      let h = H.encode_utf8(&mut tmp);
      let top_line = h.repeat(max + 2);
      let bottom_line = h.repeat(max - 1);

      let mut balloon = vec![];
      balloon.push(format!(" {UL}{top_line}{UR}"));
      for line in message.iter() {
          balloon.push(format!(" {V} {:max$} {V}", line));
      }
      balloon.push(format!(" {LL}{H}{H}{DH}{bottom_line}{LR}"));

      balloon
  }

Now we can just run dogfact

❯ dogfact
 ╭──────────────────────────────────────────╮
 │ The wetness of a dog's nose is essential │
 │ for determining what direction a smell   │
 │ is coming from.                          │
 ╰──┬───────────────────────────────────────╯
    │
    │            ┏╮┏╮
    │           ╭┛┻┛┻╮     ╭━╮
    │         ▅━╯▋┈▋┈┃     ╰╮┃
    ╰───────  ┣━━━━━╯╰━━━━━╮┃┃
              ╰━━━━┓       ┗╯┃
                   ┃┏┓┏━┳┳┓┏━╯
                   ┗┛┗┛ ┗┛┗┛

Huh, this web page has extra vertical whitespace causing breaks in the lines. Here's what it looks like in my editor.

./screenshot.png

The Perl cowsay has a bunch of options. Perhaps I should add some of those. Or, at least, make it optional to draw the dog and balloon. ¯\_(ツ)_/¯