Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cargo-tracker/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ edition = "2024"
[dependencies]
assert_cmd = "2"
predicates = "2"
chrono = "0.4"
uuid = { version = "1", features = ["v4"] }
serde_json = "1"
217 changes: 216 additions & 1 deletion cargo-tracker/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,218 @@
mod shipment;
mod shipment_manager;

use crate::shipment::{Package, ShipmentStatus};
use crate::shipment_manager::ShipmentManager;
use std::io::{self, Write};
use std::process::exit;

fn print_help() {
println!("Available commands:");
println!(" add-shipment - Add a new shipment with tracking ID and destination");
println!(" add-package - Add a package to an existing shipment");
println!(" update-status - Update the status of a shipment");
println!(" view-shipment - View details of a specific shipment");
println!(" list-shipments - List all shipments (optionally filter by status)");
println!(" generate-report - Generate shipment/package reports");
println!(" clear - Clear the screen");
println!(" help - Show available commands");
println!(" exit - Exit the Cargo Tracker CLI");
}

fn read_input(prompt: &str) -> String {
print!("{}", prompt);
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
input.trim().to_string()
}

fn main() {
println!("Hello, world!");
println!("Welcome to Cargo Tracker 1.0!");
println!("Type 'help' to see a list of available commands.");

let mut manager = ShipmentManager::new();

loop {
let command = read_input("> ").to_lowercase();

match command.as_str() {
"help" => print_help(),
"add-shipment" => {
// Create a new shipment
let tracking_id = read_input("Please enter the Tracking ID: ");
if manager.get_shipment(&tracking_id).is_some() {
println!(
"Error: Shipment with tracking ID '{}' already exists.",
tracking_id
);
continue;
}
let destination = read_input("Please enter the destination: ");
let status = ShipmentStatus::Pending;
let time_of_departure = Some(chrono::Utc::now());
let shipment_id = if tracking_id.is_empty() {
None
} else {
Some(tracking_id.clone())
};
let shipment =
manager.create_shipment(status, destination, time_of_departure, shipment_id);
println!("Shipment Created\n\n");

// Let's add packages to the shipment
println!("Let's add packages. Type 'q' to quit");
let mut count = 0;
let mut package_num = 1;
loop {
let prompt = format!("Enter package #{} description: ", package_num);
let description = read_input(&prompt);
if description.trim().eq_ignore_ascii_case("q") {
break;
}
let package = Package::new(description);
shipment.add_package(package);
count += 1;
package_num += 1;
}
let tracking = &shipment.tracking_id;
println!(
"{} package{} added to shipment '{}'.",
count,
if count == 1 { "" } else { "s" },
tracking
);
}
"add-package" => {
// Add a package to an existing shipment
let tracking_id = read_input("Enter the Tracking ID of the shipment: ");
if tracking_id.is_empty() {
println!("Tracking ID cannot be empty.");
continue;
}
match manager.get_shipment(&tracking_id) {
Some(shipment) => {
let description = read_input("Enter package description: ");
if description.trim().is_empty() {
println!("Package description cannot be empty.");
return;
}
let package = Package::new(description);
shipment.add_package(package);
println!("Package added to shipment '{}'.", shipment.tracking_id);
}
None => {
println!("Shipment with Tracking ID '{}' not found.", tracking_id);
}
}
}
"update-status" => {
let tracking_id = read_input("Please enter the Tracking ID: ");
match manager.get_shipment(&tracking_id) {
Some(shipment) => {
let status_input =
read_input("Enter new status (Pending, InTransit, Delivered, Lost): ");

let new_status = match status_input.trim().to_lowercase().as_str() {
"pending" => ShipmentStatus::Pending,
"intransit" => ShipmentStatus::InTransit,
"delivered" => {
shipment.time_of_arrival = Some(chrono::Utc::now());
ShipmentStatus::Delivered
}
"lost" => ShipmentStatus::Lost,
_ => {
println!(
"Error: Invalid status. Valid options are: Pending, InTransit, Delivered, Lost."
);
return;
}
};
Comment on lines +124 to +130
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not exit the CLI on invalid status; continue the loop.

return ends the program. Stay in REPL and prompt again.

-                            _ => {
-                                println!(
-                                    "Error: Invalid status. Valid options are: Pending, InTransit, Delivered, Lost."
-                                );
-                                return;
-                            }
+                            _ => {
+                                println!(
+                                    "Error: Invalid status. Valid options are: Pending, InTransit, Delivered, Lost."
+                                );
+                                continue;
+                            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_ => {
println!(
"Error: Invalid status. Valid options are: Pending, InTransit, Delivered, Lost."
);
return;
}
};
_ => {
println!(
"Error: Invalid status. Valid options are: Pending, InTransit, Delivered, Lost."
);
continue;
}
};
🤖 Prompt for AI Agents
In cargo-tracker/src/main.rs around lines 124 to 130 the code prints an error on
invalid status and then calls return which exits the CLI; instead remove the
return and continue the input loop so the REPL keeps running. Replace the early
return with a continue (or otherwise loop-continue control flow) so after
printing the error the program re-prompts the user rather than terminating.

shipment.status = new_status.clone();
println!(
"Shipment '{}' status updated to {:?}.",
shipment.tracking_id, new_status
);
}
None => {
println!("Shipment with Tracking ID '{}' not found.", tracking_id);
}
}
}
"view-shipment" => {
let tracking_id = read_input("Please enter the Tracking ID: ");
match manager.get_shipment(&tracking_id) {
Some(shipment) => {
println!("Tracking ID: {}", shipment.tracking_id);
println!("Destination: {}", shipment.destination);
println!("Status: {:?}", shipment.status);
println!("Packages:");
for package in &shipment.packages {
println!("({}) - {}", package.id, package.description);
}
println!();
}
None => {
println!("Shipment with Tracking ID '{}' not found.", tracking_id);
}
}
}
"list-shipments" => {
// List all shipments, optionally filter by status
let filter = read_input(
"Filter by status; Pending, InTransit, Delivered, Lost: (leave empty for all): ",
);

let filter_trimmed = filter.trim().to_lowercase();
let status_filter = match filter_trimmed.as_str() {
"" => None,
"pending" => Some(ShipmentStatus::Pending),
"intransit" => Some(ShipmentStatus::InTransit),
"delivered" => Some(ShipmentStatus::Delivered),
"lost" => Some(ShipmentStatus::Lost),
_ => {
println!("No shipments found for status '{}'.", filter);
None
}
};
let filtered_shipments = manager.list_shipments(status_filter);

if filtered_shipments.is_empty() {
println!("No shipments found.");
} else {
for shipment in filtered_shipments {
println!(
"Tracking ID: {} | Destination: {} | Status: {:?} | Packages: {} | Departure: {} | Arrival: {}",
shipment.tracking_id,
shipment.destination,
shipment.status,
shipment.packages.len(),
shipment
.time_of_departure
.map(|t| t.to_rfc3339())
.unwrap_or_else(|| "N/A".to_string()),
shipment
.time_of_arrival
.map(|t| t.to_rfc3339())
.unwrap_or_else(|| "N/A".to_string()),
);
}
}
}
"generate-report" => {
// TODO: Implement generate-report logic
println!("(generate-report not yet implemented)");
}
"clear" => {
// Clear screen (works on most Unix terminals)
print!("\x1B[2J\x1B[1;1H");
}
"exit" => {
println!("Goodbye!");
exit(0);
}
"" => continue,
_ => println!("'{}' is not a valid command.", command),
}
}
}
82 changes: 82 additions & 0 deletions cargo-tracker/src/shipment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use chrono::{DateTime, Utc};
use serde_json::json;
use uuid::Uuid;

#[derive(Debug)]
pub struct Shipment {
pub tracking_id: String,
pub destination: String,
pub packages: Vec<Package>,
pub status: ShipmentStatus,
pub time_of_departure: Option<DateTime<Utc>>,
pub time_of_arrival: Option<DateTime<Utc>>,
}

impl Shipment {
pub fn new(
status: ShipmentStatus,
destination: String,
time_of_departure: Option<DateTime<Utc>>,
tracking_id: String,
) -> Self {
Self {
packages: Vec::new(),
tracking_id,
destination,
status,
time_of_departure,
time_of_arrival: None,
}
}

pub fn add_package(&mut self, package: Package) {
self.packages.push(package);
}

pub fn to_json_str(&self) -> String {
let packages_json = self
.packages
.iter()
.map(|p| {
json!({
"id": p.id.to_string(),
"description": p.description,
})
})
.collect::<Vec<_>>();

let json_obj = json!({
"tracking_id": self.tracking_id,
"destination": self.destination,
"status": format!("{:?}", self.status),
"time_of_departure": self.time_of_departure.map(|t| t.to_rfc3339()),
"time_of_arrival": self.time_of_arrival.map(|t| t.to_rfc3339()),
"packages": packages_json,
});

json_obj.to_string()
}
Comment on lines +36 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Refactor to use serde's Serialize derive instead of manual JSON construction.

Manually building JSON is error-prone and harder to maintain. Since serde_json is already a dependency, leverage serde's Serialize derive for type-safe, automatic serialization. This also addresses the improper use of Debug format for the status enum on line 51.

Add serde to Cargo.toml if not already present with the derive feature:

serde = { version = "1", features = ["derive"] }

Then refactor the structs:

+use serde::{Deserialize, Serialize};
 use chrono::{DateTime, Utc};
-use serde_json::json;
 use uuid::Uuid;

-#[derive(Debug)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Shipment {
     pub tracking_id: String,
     pub destination: String,
     pub packages: Vec<Package>,
     pub status: ShipmentStatus,
     pub time_of_departure: Option<DateTime<Utc>>,
     pub time_of_arrival: Option<DateTime<Utc>>,
 }
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
 pub enum ShipmentStatus {
     Pending,
     InTransit,
     Delivered,
     Lost,
 }
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Package {
     pub id: Uuid,
     pub description: String,
 }

Replace the entire to_json_str method:

     pub fn to_json_str(&self) -> String {
-        let packages_json = self
-            .packages
-            .iter()
-            .map(|p| {
-                json!({
-                    "id": p.id.to_string(),
-                    "description": p.description,
-                })
-            })
-            .collect::<Vec<_>>();
-
-        let json_obj = json!({
-            "tracking_id": self.tracking_id,
-            "destination": self.destination,
-            "status": format!("{:?}", self.status),
-            "time_of_departure": self.time_of_departure.map(|t| t.to_rfc3339()),
-            "time_of_arrival": self.time_of_arrival.map(|t| t.to_rfc3339()),
-            "packages": packages_json,
-        });
-
-        json_obj.to_string()
+        serde_json::to_string(self).unwrap_or_else(|_| String::from("{}"))
     }

}

#[derive(Debug, Clone, PartialEq)]
pub enum ShipmentStatus {
Pending,
InTransit,
Delivered,
Lost,
}

#[derive(Debug, Clone)]
pub struct Package {
pub id: Uuid,
pub description: String,
}

impl Package {
pub fn new(description: String) -> Self {
Self {
id: Uuid::new_v4(),
description,
}
}
}
45 changes: 45 additions & 0 deletions cargo-tracker/src/shipment_manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::shipment::{Shipment, ShipmentStatus};
use std::collections::HashMap;

pub struct ShipmentManager {
shipments: HashMap<String, Shipment>,
}

impl ShipmentManager {
pub fn new() -> Self {
ShipmentManager {
shipments: HashMap::new(),
}
}

pub fn create_shipment(
&mut self,
status: ShipmentStatus,
destination: String,
time_of_departure: Option<chrono::DateTime<chrono::Utc>>,
tracking_id: Option<String>,
) -> &mut Shipment {
let id = tracking_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let shipment = Shipment::new(status, destination, time_of_departure, id.clone());
self.shipments.insert(id.clone(), shipment);
self.shipments.get_mut(&id).unwrap()
}

pub fn get_shipment(&mut self, tracking_id: &str) -> Option<&mut Shipment> {
self.shipments.get_mut(tracking_id)
}

/// List all shipments, with optional status filter.
pub fn list_shipments(&self, status_filter: Option<ShipmentStatus>) -> Vec<&Shipment> {
self.shipments
.values()
.filter(|s| {
if let Some(ref status) = status_filter {
&s.status == status
} else {
true
}
})
.collect()
}
}
Loading