Hello, Rustaceans! Welcome back to our journey of learning Rust and building an open-source tool. In this episode, we're going to explore how to handle external commands and dependencies. It's like making sure you have all the ingredients before starting to cook your favorite dish. Let's get started!
Dealing with External Commands
In our previous adventures, we've worked with environment variables, command handling, and fetching default values. Now, we want to ensure that the user has all the necessary tools installed on their system to run our instruction commands. It's like checking if you have a can opener before trying to open a can of beans! π
Before we dive into the code, let me explain the intuition behind our approach. We want to create a function that will check if the required external commands are installed on the user's system. If not, it will print the list of installation commands. It's like a friendly reminder for the user to install the necessary tools.
Now, let's start by adding a new function called handle_external_commands
:
fn handle_external_commands(command: &Instructions) {
// (implementation details will be explained below)
}
Updating the Main Function
Before we implement the handle_external_commands
function, let's update the main function to use it. We'll also add error handling for the deserialization of the Instructions
struct:
let command: Result<Instructions, serde_json::Error> =
serde_json::from_str(&response.choices[0].message.content);
// match the command and if error is found send request again
match command {
Ok(command) => {
handle_external_commands(&command);
println!("{}", command.instruction_commands[0]);
}
Err(_) => {
let response: ApiResponse = get_response(query, tokens).await?;
let command: Instructions =
serde_json::from_str(&response.choices[0].message.content).expect(
"Error in parsing the response, Please try again with a different query",
);
handle_external_commands(&command);
println!("{}", command.instruction_commands[0]);
}
}
Implementing the handle_external_commands
Function
Now, let's dive into the implementation of the handle_external_commands
function. We'll iterate through the external_commands
and check if they're installed using the which
command. If not, we'll print the corresponding installation commands:
fn handle_external_commands(command: &Instructions) {
let mut found_one = false;
command
.external_commands
.iter()
.enumerate()
.for_each(|(index, tool)| {
let output = Command::new("which").arg(tool.trim()).output();
match output {
Ok(output) => {
if !output.status.success() {
if !found_one {
println!("Run the following commands to install the required tools:");
found_one = true;
}
println!("{}", command.external_install[index].to_string())
}
}
Err(_) => {}
}
});
}
The handle_external_commands
function starts by initializing a found_one
variable to false
. This variable will be used to print the "Run the following commands to install the required tools:" message only once. We don't want to annoy the user, do we? π
Then, it iterates through the external_commands
using enumerate
and for_each
. For each command, it checks if the command is installed using the which
command. If the command is not installed, it prints the corresponding installation command from the external_install
vector.
Improving Error Handling and Response Parsing
Hello again, Rustaceans! In this episode, we will improve our error handling and response parsing when fetching instruction commands. It's like double-checking your work for any mistakes. Let's dive in!
Introducing the handle_request
Function
Before we dive into the code, let's understand the intuition behind our approach. We want to create a function that will handle the request to fetch instruction commands and will make three attempts to parse the response before giving up. It's like trying to open a stubborn jarβyou give it a few tries before asking someone else for help.
Now, let's introduce a new function called handle_request
:
async fn handle_request(query: String, tokens: u32) -> Result<(), Box<dyn Error>> {
let mut command = None;
for _ in 0..3 {
let response: ApiResponse = get_response(query.clone(), tokens).await?;
if let Ok(parsed_command) =
serde_json::from_str::<Instructions>(&response.choices[0].message.content)
{
command = Some(parsed_command);
break;
}
}
match command {
Some(command) => {
handle_external_commands(&command);
println!("{}", command.instruction_commands[0]);
}
None => {
println!("Error in parsing the response, Please try again with a different query");
}
}
Ok(())
}
The handle_request
function takes two arguments: query
and tokens
. Inside the function, we initialize a mutable variable command
to None
. We then loop three times to get the response from the get_response
function and attempt to parse the response. If the parsing is successful, we set command
to Some(parsed_command)
and break out of the loop.
After the loop, we match the command
variable. If there's a valid command, we proceed with handling external commands and printing the instruction command. If the command is still None
, we print an error message suggesting the user try again with a different query. It's like asking your GPS for alternative routes when the suggested one doesn't work.
Updating the main
Function
Now, let's update the main
function to use the handle_request
function:
async fn main() -> Result<(), Box<dyn Error>> {
let arguments = Arguments::from_args();
let tokens = 100;
// Code for setting up query and OS variables...
handle_request(query, tokens).await?;
}
We've replaced the previous response handling code in the main
function with a single call to handle_request
, simplifying the code and improving readability.
Refining the handle_external_commands
Function
Lastly, let's make a small change to the handle_external_commands
function to ensure that the index is in bounds when accessing the external_install
vector:
fn handle_external_commands(command: &Instructions) {
// ... (previous code)
command
.external_commands
.iter()
.enumerate()
.for_each(|(index, tool)| {
// ... (previous code)
if !output.status.success() {
if !found_one {
println!("Run the following commands to install the required tools:");
found_one = true;
}
println!(
"{}",
command
.external_install
.get(index)
.expect("Index out of bounds")
);
}
// ... (previous code)
});
}
We've replaced the direct index access with the get
method, which returns an Option
. We then use the expect
method to handle the case when the index is out of bounds, providing a more meaningful error message.
Recap of Everything We Have Done Till Now
Here is What Our Structure Looks Like Now
High level overview
Here's the step-by-step explanation of the high-level overview flowchart:
Start: The program begins execution.
Load environment variables: The program loads the environment variables from the .env file (if available) using the
dotenv
crate.Parse command line arguments: The program parses the command line arguments provided by the user using the
clap
crate and stores them in theArgs
struct.Config command: If the user selected the "Config" command, the program proceeds to update the .env file or display system information.
Update .env file: If the user provided tokens as an argument, the program appends the tokens to the .env file.
Display system information: If the user used the
-d
or--display
flag, the program prints the operating system and the default token value.
Search command: If the user selected the "Search" command, the program proceeds to handle the user's query.
Handle request: The program starts the process of handling the user's query, which involves making API calls, parsing the response, and executing external commands if needed.
Call the API: The program sends a request to the OpenAI API with the user's query and the token count.
Parse API response: After receiving the API response, the program attempts to parse the response into an
Instructions
struct.Execute external commands: If the response contains external commands, the program checks if they are installed and suggests installation instructions if necessary.
Display instructions: Finally, the program prints the instructions obtained from the API response for the user to follow.
This high-level overview provides a summary of how the program executes different tasks based on the command provided by the user and interacts with the OpenAI API to provide the desired results.
Wrapping Up
In this episode, we've improved error handling and response parsing by introducing the handle_request
function, simplifying the main
function, and refining the handle_external_commands
function. These changes make our code more robust and user-friendly.
And that's all for today's episode! As always, stay tuned for more exciting Rust adventures. It's a wild, wild Rust world out there, and we're just getting started! Happy coding, and may the force of the Ferris, the Rust mascot, be with you! π¦π
Cover: Bhupesh