diff --git a/.gitignore b/.gitignore index 79026c4..573dfdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /rust-book +/rustlings context.md ideas.md **/target/ diff --git a/CLAUDE.md b/CLAUDE.md index 894f9f1..6b0f96d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,6 +35,31 @@ **Stretch goal:** [Optional extension] ``` +### Exercise Validation (CRITICAL) +**BEFORE providing any exercise, I MUST:** +1. **Mental trace-through** - Mentally implement the solution to verify it's solvable +2. **Signature verification** - Check all function signatures are correct for the requirements +3. **Trait bound validation** - Ensure trait bounds match what the implementation actually needs +4. **Test compatibility** - Verify the test cases will work with correct implementations +5. **Compilation check** - Ensure starter code compiles (even with todo!() placeholders) + +### File Structure (REQUIRED - Rustlings Style) +**README.md contains:** +- 2-3 sentence concept explanation (like Rustlings) +- What you'll practice (bullet points) +- Links to relevant documentation +- KEEP IT MINIMAL - no dense walls of text +- NO starter code templates or expected output + +**Code files contain:** +- ONLY clean starter code with correct function signatures +- ONLY necessary imports and trait bounds +- ONLY test functions using #[cfg(test)] for verification +- Minimal TODO comments explaining what to implement +- NO explanations or tutorials in comments + +**Rustlings principle:** Minimal README, focus on code. Student learns by making tests pass. + ### Exercise Types Per Milestone - 2 general exercises (core programming concepts) - 1 crypto-specific exercise (domain building) @@ -76,6 +101,8 @@ ## Learning Constraints to Respect - Student quits when frustrated - keep exercises achievable +- **Incorrect exercise design destroys confidence - ALWAYS validate before providing** +- **Unsolvable exercises are worse than no exercises - verify signatures and bounds** - Avoid "copy-paste progress" - ensure understanding - Exercises must be tightly coupled to building (not random drills) - Prefer modifying existing code over starting from scratch diff --git a/exercises/01-generic-cache-system/Cargo.toml b/exercises/01-generic-cache-system/Cargo.toml new file mode 100644 index 0000000..599ae2d --- /dev/null +++ b/exercises/01-generic-cache-system/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "generic_function_toolkit" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/exercises/01-generic-cache-system/README.md b/exercises/01-generic-cache-system/README.md new file mode 100644 index 0000000..f605c1c --- /dev/null +++ b/exercises/01-generic-cache-system/README.md @@ -0,0 +1,15 @@ +# Generic Cache System + +Build a simple key-value cache that can store any types. You'll write generic functions that manage cached data, demonstrating how generics let you write one implementation that works with strings, numbers, or any other type. + +## What you'll practice + +- Writing generic functions with multiple type parameters `` +- Using trait bounds to constrain what types can be used +- Working with `HashMap` and generic collections +- Combining `Option` and `Result` with generics + +## Further information + +- [Generic Data Types](https://doc.rust-lang.org/book/ch10-01-syntax.html) +- [Using Trait Bounds](https://doc.rust-lang.org/book/ch10-02-traits.html#using-trait-bounds-to-conditionally-implement-methods) \ No newline at end of file diff --git a/exercises/01-generic-cache-system/src/main.rs b/exercises/01-generic-cache-system/src/main.rs new file mode 100644 index 0000000..c2d2b2c --- /dev/null +++ b/exercises/01-generic-cache-system/src/main.rs @@ -0,0 +1,112 @@ +use std::collections::HashMap; +use std::hash::Hash; + +fn cache_get(map: &HashMap, key: K) -> Option<&V> { + for (k, v) in map.iter() { + if *k == key { + return Some(&v); + } + } + return None; +} + +fn cache_insert(cache: &mut HashMap, key: K, value: V) -> Option { + for (k, v) in cache.iter() { + let return_value = *v; + if *k == key { + //update value + cache.insert(key, value); + } + //"there was an existing record with this key which we wrote over" + //"and the key we wrote over was return_value" + return Some(return_value); + } + // insert new value + cache.insert(key, value); + // "there wasnt an existing record with this key" + return None; +} + +fn cache_compute_if_absent(cache: &mut HashMap, key: K, compute: F) -> &V +where + F: Fn() -> V, +{ + cache.entry(key).or_insert_with(compute) +} + +fn batch_lookup<'a, K: Eq + Hash, V>(cache: &'a HashMap, keys: &[K]) -> Vec<&'a V> { + let mut result = Vec::new(); + + for key in keys { + if let Some(value) = cache.get(key) { + result.push(value); + } + } + return result; +} + +fn main() { + println!("let's go!"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cache_get_found() { + let mut cache = HashMap::new(); + cache.insert("key1", 42); + assert_eq!(cache_get(&cache, &"key1"), Some(&42)); + } + + #[test] + fn test_cache_get_missing() { + let cache: HashMap<&str, i32> = HashMap::new(); + assert_eq!(cache_get(&cache, &"missing"), None); + } + + #[test] + fn test_cache_insert_new() { + let mut cache = HashMap::new(); + assert_eq!(cache_insert(&mut cache, "new", 100), None); + assert_eq!(cache.get("new"), Some(&100)); + } + + #[test] + fn test_cache_insert_existing() { + let mut cache = HashMap::new(); + cache.insert("key", 50); + assert_eq!(cache_insert(&mut cache, "key", 75), Some(50)); + assert_eq!(cache.get("key"), Some(&75)); + } + + #[test] + fn test_compute_if_absent_missing() { + let mut cache = HashMap::new(); + let result = cache_compute_if_absent(&mut cache, "compute", || "computed".to_string()); + assert_eq!(result, &"computed".to_string()); + assert_eq!(cache.get("compute"), Some(&"computed".to_string())); + } + + #[test] + fn test_compute_if_absent_exists() { + let mut cache = HashMap::new(); + cache.insert("existing", "original".to_string()); + let result = + cache_compute_if_absent(&mut cache, "existing", || "should not run".to_string()); + assert_eq!(result, &"original".to_string()); + } + + #[test] + fn test_batch_lookup() { + let mut cache = HashMap::new(); + cache.insert(1, "one"); + cache.insert(3, "three"); + cache.insert(5, "five"); + + let keys = [1, 2, 3, 4, 5]; + let results = batch_lookup(&cache, &keys); + assert_eq!(results, vec![&"one", &"three", &"five"]); + } +} diff --git a/exercises/02-generic-structs/Cargo.toml b/exercises/02-generic-structs/Cargo.toml new file mode 100644 index 0000000..3c4b781 --- /dev/null +++ b/exercises/02-generic-structs/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "generic_structs" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/exercises/02-generic-structs/README.md b/exercises/02-generic-structs/README.md new file mode 100644 index 0000000..a7251c7 --- /dev/null +++ b/exercises/02-generic-structs/README.md @@ -0,0 +1,15 @@ +# Generic Message Queue + +Build a message queue system that can handle different types of messages. You'll create generic structs and enums that can queue up emails, notifications, tasks, or any other message type without code duplication. + +## What you'll practice + +- Defining generic structs that hold collections of any type +- Creating generic enums for different message states +- Writing implementation blocks with generic type parameters +- Using associated functions vs instance methods with generics + +## Further information + +- [Generic Data Types](https://doc.rust-lang.org/book/ch10-01-syntax.html) +- [Method Definitions](https://doc.rust-lang.org/book/ch05-03-method-syntax.html) \ No newline at end of file diff --git a/exercises/02-generic-structs/src/main.rs b/exercises/02-generic-structs/src/main.rs new file mode 100644 index 0000000..768c780 --- /dev/null +++ b/exercises/02-generic-structs/src/main.rs @@ -0,0 +1,102 @@ +use std::collections::VecDeque; + +// TODO: Make this struct generic over message type T +// It should hold a queue of messages and track total processed count +struct MessageQueue { + queue: VecDeque, + processed_count: usize, +} + +// TODO: Make this enum generic over T for message data and E for error data +// Messages can be Pending, Processing, or Failed with error info +enum MessageStatus { + Pending(String), + Processing(String), + Failed(String, String), // message, error +} + +// TODO: Create a generic struct for message metadata +// Should store the message of type T and a priority level (u8) +struct PriorityMessage { + content: String, + priority: u8, +} + +// TODO: Implement methods for MessageQueue +impl MessageQueue { + // Create new empty queue + fn new() -> Self { + todo!("Create new message queue") + } + + // Add message to back of queue + fn enqueue(&mut self, message: String) { + todo!("Add message to queue") + } + + // Remove and return next message from front, increment processed count + fn dequeue(&mut self) -> Option { + todo!("Remove next message") + } + + // Get current queue length + fn len(&self) -> usize { + todo!("Return queue length") + } + + // Get total number of processed messages + fn total_processed(&self) -> usize { + todo!("Return processed count") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_queue_creation() { + let queue: MessageQueue = MessageQueue::new(); + assert_eq!(queue.len(), 0); + assert_eq!(queue.total_processed(), 0); + } + + #[test] + fn test_enqueue_dequeue() { + let mut queue = MessageQueue::new(); + queue.enqueue("First message".to_string()); + queue.enqueue("Second message".to_string()); + + assert_eq!(queue.len(), 2); + assert_eq!(queue.dequeue(), Some("First message".to_string())); + assert_eq!(queue.len(), 1); + assert_eq!(queue.total_processed(), 1); + } + + #[test] + fn test_message_status_variants() { + let pending: MessageStatus = MessageStatus::Pending("Task 1".to_string()); + let failed: MessageStatus = MessageStatus::Failed("Task 2".to_string(), "Connection timeout".to_string()); + + match pending { + MessageStatus::Pending(msg) => assert_eq!(msg, "Task 1"), + _ => panic!("Should be Pending"), + } + } + + #[test] + fn test_priority_message() { + let high_priority: PriorityMessage = PriorityMessage { + content: "Urgent task".to_string(), + priority: 1, + }; + + let low_priority: PriorityMessage = PriorityMessage { + content: 42, + priority: 5, + }; + + assert_eq!(high_priority.priority, 1); + assert_eq!(low_priority.content, 42); + } +} diff --git a/exercises/03-basic-traits/Cargo.toml b/exercises/03-basic-traits/Cargo.toml new file mode 100644 index 0000000..dde6b5d --- /dev/null +++ b/exercises/03-basic-traits/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "basic_traits" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/exercises/03-basic-traits/README.md b/exercises/03-basic-traits/README.md new file mode 100644 index 0000000..6ea9e31 --- /dev/null +++ b/exercises/03-basic-traits/README.md @@ -0,0 +1,15 @@ +# Configuration System Traits + +Build a configuration system where different components (database, cache, logger) can be configured uniformly. You'll define traits that allow different types to be configured, validated, and reset in a standard way. + +## What you'll practice + +- Defining traits that establish contracts between types +- Implementing the same trait for different struct types +- Using traits as function parameters with `impl Trait` syntax +- Creating functions that work with any type implementing specific traits + +## Further information + +- [Traits: Defining Shared Behavior](https://doc.rust-lang.org/book/ch10-02-traits.html) +- [Traits as Parameters](https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters) \ No newline at end of file diff --git a/exercises/03-basic-traits/src/main.rs b/exercises/03-basic-traits/src/main.rs new file mode 100644 index 0000000..c1c8d6f --- /dev/null +++ b/exercises/03-basic-traits/src/main.rs @@ -0,0 +1,119 @@ +// TODO: Define a trait called `Configurable` with these methods: +// - configure(&mut self, setting: &str, value: &str) -> Result<(), String> +// - is_valid(&self) -> bool +// - reset(&mut self) +trait Configurable { + // Add your methods here +} + +struct DatabaseConfig { + host: String, + port: u16, + max_connections: u32, +} + +struct CacheConfig { + size_mb: u32, + ttl_seconds: u64, + enabled: bool, +} + +struct LoggerConfig { + level: String, + output_file: Option, + timestamp_format: String, +} + +// TODO: Implement Configurable for DatabaseConfig +// configure should handle: "host", "port", "max_connections" +// is_valid should return true if host is not empty and port > 0 +// reset should set host="localhost", port=5432, max_connections=10 + +// TODO: Implement Configurable for CacheConfig +// configure should handle: "size_mb", "ttl_seconds", "enabled" +// is_valid should return true if size_mb > 0 and ttl_seconds > 0 +// reset should set size_mb=100, ttl_seconds=300, enabled=true + +// TODO: Implement Configurable for LoggerConfig +// configure should handle: "level", "output_file", "timestamp_format" +// is_valid should return true if level is one of: "debug", "info", "warn", "error" +// reset should set level="info", output_file=None, timestamp_format="%Y-%m-%d %H:%M:%S" + +// TODO: Write a function `apply_config` that takes any Configurable and a list of (key, value) pairs +// It should try to configure each setting and return a Vec of error messages for failed configurations +fn apply_config(config: &mut impl Configurable, settings: &[(&str, &str)]) -> Vec { + todo!("Apply configuration settings") +} + +// TODO: Write a function `validate_and_reset_if_invalid` that takes any Configurable +// If the config is invalid, reset it and return true. If valid, return false. +fn validate_and_reset_if_invalid(config: &mut impl Configurable) -> bool { + todo!("Validate config and reset if invalid") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_database_config() { + let mut db = DatabaseConfig { + host: "".to_string(), + port: 0, + max_connections: 0, + }; + + assert!(!db.is_valid()); + db.reset(); + assert!(db.is_valid()); + assert_eq!(db.host, "localhost"); + assert_eq!(db.port, 5432); + } + + #[test] + fn test_cache_configure() { + let mut cache = CacheConfig { + size_mb: 0, + ttl_seconds: 0, + enabled: false, + }; + + cache.reset(); + assert!(cache.is_valid()); + + assert!(cache.configure("size_mb", "200").is_ok()); + assert_eq!(cache.size_mb, 200); + + assert!(cache.configure("invalid_key", "value").is_err()); + } + + #[test] + fn test_logger_validation() { + let mut logger = LoggerConfig { + level: "invalid".to_string(), + output_file: None, + timestamp_format: "".to_string(), + }; + + assert!(!logger.is_valid()); + assert!(validate_and_reset_if_invalid(&mut logger)); + assert!(logger.is_valid()); + assert_eq!(logger.level, "info"); + } + + #[test] + fn test_apply_config() { + let mut db = DatabaseConfig { + host: "".to_string(), + port: 0, + max_connections: 0, + }; + + let settings = [("host", "example.com"), ("port", "3306"), ("invalid", "value")]; + let errors = apply_config(&mut db, &settings); + + assert_eq!(errors.len(), 1); // Only "invalid" should fail + assert_eq!(db.host, "example.com"); + assert_eq!(db.port, 3306); + } +} diff --git a/exercises/04-generic-methods/Cargo.toml b/exercises/04-generic-methods/Cargo.toml new file mode 100644 index 0000000..5ec95c5 --- /dev/null +++ b/exercises/04-generic-methods/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "generic_methods" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/exercises/04-generic-methods/README.md b/exercises/04-generic-methods/README.md new file mode 100644 index 0000000..d67a6b0 --- /dev/null +++ b/exercises/04-generic-methods/README.md @@ -0,0 +1,15 @@ +# Generic Methods & Implementations + +Build a generic data transformer that can convert, filter, and manipulate collections of any type. You'll practice writing methods on generic structs and using multiple type parameters in implementation blocks. + +## What you'll practice + +- Writing methods for generic structs with `impl` blocks +- Using multiple type parameters in methods like `` +- Associated functions vs instance methods on generic types +- Method chaining with generic return types + +## Further information + +- [Method Syntax](https://doc.rust-lang.org/book/ch05-03-method-syntax.html) +- [Generic Data Types](https://doc.rust-lang.org/book/ch10-01-syntax.html) \ No newline at end of file diff --git a/exercises/04-generic-methods/src/main.rs b/exercises/04-generic-methods/src/main.rs new file mode 100644 index 0000000..63ca8d7 --- /dev/null +++ b/exercises/04-generic-methods/src/main.rs @@ -0,0 +1,122 @@ +// TODO: Make this struct generic over type T +// It should store a collection of items and provide transformation methods +struct DataTransformer { + items: Vec, +} + +// TODO: Implement methods for DataTransformer +impl DataTransformer { + // TODO: Associated function to create new transformer with empty collection + fn new() -> Self { + todo!("Create new DataTransformer") + } + + // TODO: Associated function to create transformer from existing vector + fn from_vec(items: Vec) -> Self { + todo!("Create DataTransformer from vector") + } + + // TODO: Method to add an item to the collection + fn add(&mut self, item: i32) { + todo!("Add item to collection") + } + + // TODO: Method to get length of collection + fn len(&self) -> usize { + todo!("Return length") + } + + // TODO: Method to transform each item using a closure and return new DataTransformer + // This should work: transformer.map(|x| x * 2) + fn map(&self, f: F) -> DataTransformer + where + F: Fn(&i32) -> U, + { + todo!("Transform each item") + } + + // TODO: Method to filter items using a predicate and return new DataTransformer with same type + fn filter(&self, predicate: F) -> DataTransformer + where + F: Fn(&i32) -> bool, + { + todo!("Filter items") + } + + // TODO: Method to find first item matching predicate + fn find(&self, predicate: F) -> Option<&i32> + where + F: Fn(&i32) -> bool, + { + todo!("Find first matching item") + } + + // TODO: Method to collect all items into a vector + fn collect(self) -> Vec { + todo!("Collect into vector") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_creation_and_basic_ops() { + let mut transformer: DataTransformer = DataTransformer::new(); + assert_eq!(transformer.len(), 0); + + transformer.add(10); + transformer.add(20); + assert_eq!(transformer.len(), 2); + + let from_vec: DataTransformer = DataTransformer::from_vec(vec!["hello".to_string(), "world".to_string()]); + assert_eq!(from_vec.len(), 2); + } + + #[test] + fn test_map_transformation() { + let transformer = DataTransformer::from_vec(vec![1, 2, 3, 4]); + let doubled: DataTransformer = transformer.map(|x| x * 2); + assert_eq!(doubled.collect(), vec![2, 4, 6, 8]); + + let strings: DataTransformer = transformer.map(|x| x.to_string()); + assert_eq!(strings.collect(), vec!["1", "2", "3", "4"]); + } + + #[test] + fn test_filter() { + let transformer = DataTransformer::from_vec(vec![1, 2, 3, 4, 5, 6]); + let evens = transformer.filter(|x| *x % 2 == 0); + assert_eq!(evens.collect(), vec![2, 4, 6]); + } + + #[test] + fn test_find() { + let transformer = DataTransformer::from_vec(vec![10, 20, 30, 40]); + assert_eq!(transformer.find(|x| *x > 25), Some(&30)); + assert_eq!(transformer.find(|x| *x > 50), None); + } + + #[test] + fn test_method_chaining() { + let result = DataTransformer::from_vec(vec![1, 2, 3, 4, 5, 6]) + .filter(|x| *x % 2 == 0) // Keep evens: [2, 4, 6] + .map(|x| x * 10) // Multiply by 10: [20, 40, 60] + .collect(); + + assert_eq!(result, vec![20, 40, 60]); + } + + #[test] + fn test_different_types() { + let string_transformer = DataTransformer::from_vec(vec!["rust".to_string(), "is".to_string(), "awesome".to_string()]); + + let lengths: DataTransformer = string_transformer.map(|s| s.len()); + assert_eq!(lengths.collect(), vec![4, 2, 7]); + + let long_words = DataTransformer::from_vec(vec!["a".to_string(), "hello".to_string(), "world".to_string()]) + .filter(|s| s.len() > 3); + assert_eq!(long_words.collect(), vec!["hello".to_string(), "world".to_string()]); + } +}