This guide assumes you've never written Rust before. We'll go step-by-step!
- Open your terminal
- Run:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh - Follow the prompts (just press Enter to accept defaults)
- Close and reopen your terminal
- Verify installation:
rustc --versionandcargo --version
- Go to https://www.football-data.org/client/register
- Register for a free account
- Copy your API token - you'll need it later
Cargo is Rust's package manager (like npm for Node.js). It handles:
- Creating new projects
- Managing dependencies
- Building and running your code
- Running tests
# Navigate to your project folder (you're already here!)
cd /Users/adamoldin/Documents/Sidogrejer/pl-table-cli
# Initialize a new Rust project
cargo init
# See what was created
ls -laCargo.toml- Your project's configuration file (like package.json)src/main.rs- Your main entry point.gitignore- Already configured for Rust
# Run the default "Hello, world!" program
cargo runYou should see:
Compiling pl-table-cli v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
Running `target/debug/pl-table-cli`
Hello, world!
fn main() {
println!("Hello, world!");
}Explanation:
fn main()- Every Rust program starts here (likemain()in C or Java)println!- A macro (note the!) that prints to console;- Statements end with semicolons
let name = "Arsenal"; // Immutable (can't change)
let mut score = 0; // Mutable (can change)
score += 3; // Now score is 3let position: u32 = 1; // Unsigned 32-bit integer
let points: i32 = 52; // Signed 32-bit integer
let name: String = String::from("Arsenal");
let ratio: f64 = 2.5; // 64-bit floatlet s1 = "Arsenal"; // &str - string slice (borrowed, immutable)
let s2 = String::from("Arsenal"); // String - owned, can grow/shrinkOpen Cargo.toml and replace the [dependencies] section with:
[dependencies]
clap = { version = "4.5", features = ["derive"] }
reqwest = { version = "0.11", features = ["json", "blocking"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
comfy-table = "7.1"
anyhow = "1.0"
dotenvy = "0.15"- clap - Parses command-line arguments (e.g.,
--team Arsenal) - reqwest - Makes HTTP requests to the API
- serde - Converts JSON to Rust structs and vice versa
- serde_json - Works with serde for JSON specifically
- comfy-table - Creates pretty tables in the terminal
- anyhow - Makes error handling easier
- dotenvy - Reads
.envfiles for secrets
cargo buildThis will download all dependencies (might take a minute the first time).
touch .envAdd this line (replace YOUR_API_KEY with your actual key from football-data.org):
FOOTBALL_DATA_API_KEY=your_actual_api_key_here
touch .env.exampleAdd this line:
FOOTBALL_DATA_API_KEY=your_api_key_here
Make sure .env is in your .gitignore so you don't commit your API key:
echo ".env" >> .gitignoreModules organize code into separate files/folders. Think of them like folders for your code.
src/
├── main.rs # Entry point - coordinates everything
├── cli.rs # Defines command-line arguments
├── api/ # Folder for API-related code
│ ├── mod.rs # Declares this folder as a module
│ ├── client.rs # HTTP client code
│ └── models.rs # Data structures
└── display/ # Folder for display-related code
├── mod.rs # Declares this folder as a module
└── table.rs # Table formatting code
mod cli; // Tells Rust to look for src/cli.rs
mod api; // Tells Rust to look for src/api/mod.rs
mod display; // Tells Rust to look for src/display/mod.rsEvery value in Rust has ONE owner. When the owner goes out of scope, the value is dropped.
{
let s = String::from("hello"); // s owns this string
} // s goes out of scope, string is freed automaticallyYou can "borrow" a value without taking ownership:
fn print_length(s: &String) { // &String = borrowed reference
println!("Length: {}", s.len());
} // s is returned to the caller
let my_string = String::from("hello");
print_length(&my_string); // Borrow it
println!("{}", my_string); // Still works! We still own itFunctions that can fail return Result<T, E>:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
// Using it:
match divide(10, 2) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
// Or with ? operator (propagates errors):
let result = divide(10, 2)?; // Returns early if ErrInstead of null, Rust uses Option<T>:
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
match some_number {
Some(n) => println!("Got: {}", n),
None => println!("Got nothing"),
}struct Team {
name: String,
points: u32,
position: u32,
}
let arsenal = Team {
name: String::from("Arsenal"),
points: 52,
position: 1,
};
println!("{} has {} points", arsenal.name, arsenal.points);#[derive(Debug, Clone)] // Auto-implements Debug and Clone traits
struct Team {
name: String,
}
let team = Team { name: String::from("Arsenal") };
println!("{:?}", team); // Debug print: Team { name: "Arsenal" }Here's the order I recommend (ask me for help with each step!):
- Create module files - Set up the folder structure
- Define CLI arguments - What commands will your app accept?
- Test CLI - Make sure
--helpworks
- Define data models - Create structs for the API response
- Create API client - Function to fetch data
- Test API call - Just print the raw JSON first
- Parse JSON - Convert API response to your structs
- Basic table - Display without colors first
- Add colors - Make it pretty!
- Team filter - Implement
--teamflag - Form display - Implement
--formflag - Error handling - Make errors user-friendly
# Check if your code compiles (fast, doesn't produce binary)
cargo check
# Compile your project
cargo build
# Compile with optimizations (slower build, faster runtime)
cargo build --release
# Build and run
cargo run
# Run with arguments
cargo run -- --help
cargo run -- --team Arsenal
# Format your code
cargo fmt
# Check for common mistakes
cargo clippy
# Run tests
cargo test
# Clean build artifacts
cargo clean- Ask me questions! I'm here to explain concepts and help you debug
- Tell me which step you're on, and I'll break it down further
- The Rust Book: https://doc.rust-lang.org/book/
- Rust by Example: https://doc.rust-lang.org/rust-by-example/
- Docs: https://docs.rs/ (documentation for all crates)
Q: What's the difference between String and &str?
A: String is owned/mutable, &str is a borrowed view into a string. Use String when you need to own/modify, &str for reading.
Q: What does & mean?
A: It's a reference (borrow). You're borrowing the value without taking ownership.
Q: What does ? do?
A: It's the "try" operator. If the Result is Err, it returns early from the function. If it's Ok, it unwraps the value.
Q: Why do I need mut?
A: Variables are immutable by default in Rust. You need mut to make them mutable.
Q: What's a trait? A: Like an interface - it defines behavior that types can implement.
Tell me when you're ready to begin, and which step you want to tackle first! I'll give you:
- Detailed instructions for that step
- Code examples
- Explanations of what each part does
- Common mistakes to avoid
Let's build this together!