diff --git a/jira-wip/src/koans/00_greetings/00_greetings.rs b/jira-wip/src/koans/00_greetings/00_greetings.rs index 349c5de..57bd140 100644 --- a/jira-wip/src/koans/00_greetings/00_greetings.rs +++ b/jira-wip/src/koans/00_greetings/00_greetings.rs @@ -28,7 +28,7 @@ /// ~ Enjoy! ~ /// #[cfg(test)] -mod greetings { +mod tests { #[test] /// This is your starting block! /// diff --git a/jira-wip/src/koans/01_ticket/01_ticket.rs b/jira-wip/src/koans/01_ticket/01_ticket.rs index be34963..84dd522 100644 --- a/jira-wip/src/koans/01_ticket/01_ticket.rs +++ b/jira-wip/src/koans/01_ticket/01_ticket.rs @@ -1,78 +1,75 @@ -mod ticket { +/// We will begin our journey of building our own JIRA clone defining the cornerstone of +/// JIRA's experience: the ticket. +/// For now we want to limit ourselves to the essentials: each ticket will have a title +/// and a description. +/// No, not an ID yet. We will get to that in due time. +/// +/// There are various ways to represent a set of related pieces of information in Rust. +/// We'll go for a `struct`: a struct is quite similar to what you would call a class or +/// an object in object-oriented programming languages. +/// It is a collection of fields, each one with its own name. +/// Given that Rust is a strongly-typed language, we also need to specify a type for each +/// of those fields. +/// +/// Our definition of Ticket is incomplete - can you replace __ with what is missing to make +/// this snippet compile and the tests below succeed? +/// +/// You can find more about structs in the Rust Book: https://doc.rust-lang.org/book/ch05-01-defining-structs.html +pub struct Ticket { + title: String, + __: __ +} - /// We will begin our journey of building our own JIRA clone defining the cornerstone of - /// JIRA's experience: the ticket. - /// For now we want to limit ourselves to the essentials: each ticket will have a title - /// and a description. - /// No, not an ID yet. We will get to that in due time. - /// - /// There are various ways to represent a set of related pieces of information in Rust. - /// We'll go for a `struct`: a struct is quite similar to what you would call a class or - /// an object in object-oriented programming languages. - /// It is a collection of fields, each one with its own name. - /// Given that Rust is a strongly-typed language, we also need to specify a type for each - /// of those fields. - /// - /// Our definition of Ticket is incomplete - can you replace __ with what is missing to make - /// this snippet compile and the tests below succeed? - /// - /// You can find more about structs in the Rust Book: https://doc.rust-lang.org/book/ch05-01-defining-structs.html - pub struct Ticket { - title: String, - __ - } +/// `cfg` stands for configuration flag. +/// The #[cfg(_)] attribute is used to mark a section of the code for conditional compilation +/// based on the value of the specified flag. +/// #[cfg(test)] is used to mark sections of our codebase that should only be compiled +/// when running `cargo test`... +/// Yes, tests! +/// +/// You can put tests in different places in a Rust project, depending on what you are +/// trying to do: unit testing of private functions and methods, testing an internal API, +/// integration testing your crate from the outside, etc. +/// You can find more details on test organisation in the Rust book: +/// https://doc.rust-lang.org/book/ch11-03-test-organization.html +/// +/// Let it be said that tests are first-class citizens in the Rust ecosystem and you are +/// provided with a barebone test framework out of the box. +#[cfg(test)] +mod tests { + use super::*; - /// `cfg` stands for configuration flag. - /// The #[cfg(_)] attribute is used to mark a section of the code for conditional compilation - /// based on the value of the specified flag. - /// #[cfg(test)] is used to mark sections of our codebase that should only be compiled - /// when running `cargo test`... - /// Yes, tests! - /// - /// You can put tests in different places in a Rust project, depending on what you are - /// trying to do: unit testing of private functions and methods, testing an internal API, - /// integration testing your crate from the outside, etc. - /// You can find more details on test organisation in the Rust book: - /// https://doc.rust-lang.org/book/ch11-03-test-organization.html + /// The #[test] attribute is used to mark a function as a test for the compiler. + /// Tests take no arguments: when we run `cargo test`, this function will be invoked. + /// If it runs without raising any issue, the test is considered green - it passed. + /// If it panics (raises a fatal exception), then the test is considered red - it failed. /// - /// Let it be said that tests are first-class citizens in the Rust ecosystem and you are - /// provided with a barebone test framework out of the box. - #[cfg(test)] - mod tests { - use super::*; - - /// The #[test] attribute is used to mark a function as a test for the compiler. - /// Tests take no arguments: when we run `cargo test`, this function will be invoked. - /// If it runs without raising any issue, the test is considered green - it passed. - /// If it panics (raises a fatal exception), then the test is considered red - it failed. + /// `cargo test` reports on the number of failed tests at the end of each run, with some + /// associated diagnostics to make it easier to understand what went wrong exactly. + #[test] + fn your_first_ticket() { + /// `let` is used to create a variable: we are binding a new `Ticket` struct + /// to the name `ticket_one`. /// - /// `cargo test` reports on the number of failed tests at the end of each run, with some - /// associated diagnostics to make it easier to understand what went wrong exactly. - #[test] - fn your_first_ticket() { - /// `let` is used to create a variable: we are binding a new `Ticket` struct - /// to the name `ticket_one`. - /// - /// We said before that Rust is strongly typed, nonetheless we haven't specified - /// a type for `ticket_one`. - /// As most modern strongly typed programming languages, Rust provides type inference: - /// the compiler is smart enough to figure out the type of variables based on - /// their usage and it won't bother you unless the type is ambiguous. - let ticket_one = Ticket { - /// This `.into()` method call is here for a reason, but give us time. - /// We'll get there when it's the right moment. - title: "A ticket title".into(), - description: "A heart-breaking description".into() - }; + /// We said before that Rust is strongly typed, nonetheless we haven't specified + /// a type for `ticket_one`. + /// As most modern strongly typed programming languages, Rust provides type inference: + /// the compiler is smart enough to figure out the type of variables based on + /// their usage and it won't bother you unless the type is ambiguous. + let ticket_one = Ticket { + /// This `.into()` method call is here for a reason, but give us time. + /// We'll get there when it's the right moment. + title: "A ticket title".into(), + description: "A heart-breaking description".into() + }; - /// `assert_eq` is a macro (notice the ! at the end of the name). - /// It checks that the left argument (the expected value) is identical - /// to the right argument (the computed value). - /// If they are not, it panics - Rust's (almost) non-recoverable way to terminate a program. - /// In the case of tests, this is caught by the test framework and the test is marked as failed. - assert_eq!(ticket_one.title, "A ticket title"); - /// Field syntax: you use a dot to access the field of a struct. - assert_eq!(ticket_one.description, "A heart-breaking description"); - } + /// `assert_eq` is a macro (notice the ! at the end of the name). + /// It checks that the left argument (the expected value) is identical + /// to the right argument (the computed value). + /// If they are not, it panics - Rust's (almost) non-recoverable way to terminate a program. + /// In the case of tests, this is caught by the test framework and the test is marked as failed. + assert_eq!(ticket_one.title, "A ticket title"); + /// Field syntax: you use a dot to access the field of a struct. + assert_eq!(ticket_one.description, "A heart-breaking description"); } -} \ No newline at end of file +} diff --git a/jira-wip/src/koans/01_ticket/02_status.rs b/jira-wip/src/koans/01_ticket/02_status.rs index d74fb3d..1f7bb64 100644 --- a/jira-wip/src/koans/01_ticket/02_status.rs +++ b/jira-wip/src/koans/01_ticket/02_status.rs @@ -1,72 +1,70 @@ -mod status { - /// Ticket have two purposes in JIRA: capturing information about a task and tracking the - /// completion of the task itself. - /// - /// Let's add a new field to our `Ticket` struct, `status`. - /// For the time being, we'll work under the simplified assumption that the set of statuses - /// for a ticket is fixed and can't be customised by the user. - /// A ticket is either in the to-do column, in progress, blocked or done. - /// What is the best way to represent this information in Rust? - struct Ticket { - title: String, - description: String, - status: Status, - } +/// Ticket have two purposes in JIRA: capturing information about a task and tracking the +/// completion of the task itself. +/// +/// Let's add a new field to our `Ticket` struct, `status`. +/// For the time being, we'll work under the simplified assumption that the set of statuses +/// for a ticket is fixed and can't be customised by the user. +/// A ticket is either in the to-do column, in progress, blocked or done. +/// What is the best way to represent this information in Rust? +struct Ticket { + title: String, + description: String, + status: Status, +} - /// Rust's enums are perfect for this usecase. - /// Enum stands for enumeration: a type encoding the constraint that only a finite set of - /// values is possible. - /// Enums are great to encode semantic information in your code: making domain constraints - /// explicit. - /// - /// Each possible value of an enum is called a variant. By convention, they are Pascal-cased. - /// Check out the Rust book for more details on enums: - /// https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html - /// - /// Let's create a variant for each of the allowed statuses of our tickets. - pub enum Status { - ToDo, - __ - } +/// Rust's enums are perfect for this usecase. +/// Enum stands for enumeration: a type encoding the constraint that only a finite set of +/// values is possible. +/// Enums are great to encode semantic information in your code: making domain constraints +/// explicit. +/// +/// Each possible value of an enum is called a variant. By convention, they are Pascal-cased. +/// Check out the Rust book for more details on enums: +/// https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html +/// +/// Let's create a variant for each of the allowed statuses of our tickets. +pub enum Status { + ToDo, + __ +} - #[cfg(test)] - mod tests { - use super::*; +#[cfg(test)] +mod tests { + use super::*; - #[test] - fn a_blocked_ticket() { - // Let's create a blocked ticket. - let ticket = Ticket { - title: "A ticket title".into(), - description: "A heart-breaking description".into(), - status: __ - }; + #[test] + fn a_blocked_ticket() { + // Let's create a blocked ticket. + let ticket = Ticket { + title: "A ticket title".into(), + description: "A heart-breaking description".into(), + status: __ + }; - // Let's check that the status corresponds to what we expect. - // We can use pattern matching to take a different course of action based on the enum - // variant we are looking at. - // The Rust compiler will make sure that the match statement is exhaustive: it has to - // handle all variants in our enums. - // If not, the compiler will complain and reject our program. - // - // This is extremely useful when working on evolving codebases: if tomorrow we decide - // that tickets can also have `Backlog` as their status, the Rust compiler will - // highlight all code locations where we need to account for the new variant. - // No way to forget! + // Let's check that the status corresponds to what we expect. + // We can use pattern matching to take a different course of action based on the enum + // variant we are looking at. + // The Rust compiler will make sure that the match statement is exhaustive: it has to + // handle all variants in our enums. + // If not, the compiler will complain and reject our program. + // + // This is extremely useful when working on evolving codebases: if tomorrow we decide + // that tickets can also have `Backlog` as their status, the Rust compiler will + // highlight all code locations where we need to account for the new variant. + // No way to forget! + // + // Checkout the Rust Book for more details: + // https://doc.rust-lang.org/book/ch06-02-match.html + match ticket.status { + // Variant => Expression + Status::Blocked => println!("Great, as expected!"), + // If we want to take the same action for multiple variants, we can use a | to list them. + // Variant | Variant | ... | Variant => Expression // - // Checkout the Rust Book for more details: - // https://doc.rust-lang.org/book/ch06-02-match.html - match ticket.status { - // Variant => Expression - Status::Blocked => println!("Great, as expected!"), - // If we want to take the same action for multiple variants, we can use a | to list them. - // Variant | Variant | ... | Variant => Expression - // - // We are panicking in this case, thus making the test fail if this branch of our - // match statement gets executed. - Status::ToDo | Status::InProgress | Status::Done => panic!("The ticket is not blocked!") - } + // We are panicking in this case, thus making the test fail if this branch of our + // match statement gets executed. + Status::ToDo | Status::InProgress | Status::Done => panic!("The ticket is not blocked!") } } } diff --git a/jira-wip/src/koans/01_ticket/03_validation.rs b/jira-wip/src/koans/01_ticket/03_validation.rs index cf362bf..940cf38 100644 --- a/jira-wip/src/koans/01_ticket/03_validation.rs +++ b/jira-wip/src/koans/01_ticket/03_validation.rs @@ -1,84 +1,82 @@ -mod validation { - enum Status { - ToDo, - InProgress, - Blocked, - Done, - } +enum Status { + ToDo, + InProgress, + Blocked, + Done, +} - struct Ticket { - title: String, - description: String, - status: Status, - } +struct Ticket { + title: String, + description: String, + status: Status, +} - /// So far we have allowed any string as a valid title and description. - /// That's not what would happen in JIRA: we wouldn't allow tickets with an empty title, - /// for example. - /// Both title and description would also have length limitations: the Divine Comedy probably - /// shouldn't be allowed as a ticket description. - /// - /// We want to define a function that takes in a title, a description and a status and - /// performs validation: it panics if validation fails, it returns a `Ticket` if validation - /// succeeds. - /// - /// We will learn a better way to handle recoverable errors such as this one further along, - /// but let's rely on panic for the time being. - fn create_ticket(title: String, description: String, status: Status) -> Ticket { - todo!() - } +/// So far we have allowed any string as a valid title and description. +/// That's not what would happen in JIRA: we wouldn't allow tickets with an empty title, +/// for example. +/// Both title and description would also have length limitations: the Divine Comedy probably +/// shouldn't be allowed as a ticket description. +/// +/// We want to define a function that takes in a title, a description and a status and +/// performs validation: it panics if validation fails, it returns a `Ticket` if validation +/// succeeds. +/// +/// We will learn a better way to handle recoverable errors such as this one further along, +/// but let's rely on panic for the time being. +fn create_ticket(title: String, description: String, status: Status) -> Ticket { + todo!() + } - #[cfg(test)] - mod tests { - use super::*; - use fake::Fake; +#[cfg(test)] +mod tests { + use super::*; + use fake::Fake; - /// The #[should_panic] attribute inverts the usual behaviour for tests: if execution of - /// the test's function body causes a panic, the test is green; otherwise, it's red. - /// - /// This is quite handy to test unhappy path: in our case, what happens when invalid input - /// is passed to `create_ticket`. - #[test] - #[should_panic] - fn title_cannot_be_empty() { - // We don't really care about the description in this test. - // Hence we generate a random string, with length between 0 and 3000 characters - // using `fake`, a handy crate to generate random test data. - // - // We are using Rust's range syntax, 0..3000 - the lower-bound is included, the - // upper-bound is excluded. - // You can include the upper-bound using 0..=3000. - let description = (0..3000).fake(); + /// The #[should_panic] attribute inverts the usual behaviour for tests: if execution of + /// the test's function body causes a panic, the test is green; otherwise, it's red. + /// + /// This is quite handy to test unhappy path: in our case, what happens when invalid input + /// is passed to `create_ticket`. + #[test] + #[should_panic] + fn title_cannot_be_empty() { + // We don't really care about the description in this test. + // Hence we generate a random string, with length between 0 and 3000 characters + // using `fake`, a handy crate to generate random test data. + // + // We are using Rust's range syntax, 0..3000 - the lower-bound is included, the + // upper-bound is excluded. + // You can include the upper-bound using 0..=3000. + let description = (0..3000).fake(); - create_ticket("".into(), description, Status::ToDo); - } + create_ticket("".into(), description, Status::ToDo); + } - #[test] - #[should_panic] - fn title_cannot_be_longer_than_fifty_chars() { - let description = (0..3000).fake(); - // Let's generate a title longer than 51 chars. - let title = (51..10_000).fake(); + #[test] + #[should_panic] + fn title_cannot_be_longer_than_fifty_chars() { + let description = (0..3000).fake(); + // Let's generate a title longer than 51 chars. + let title = (51..10_000).fake(); - create_ticket(title, description, Status::ToDo); - } + create_ticket(title, description, Status::ToDo); + } - #[test] - #[should_panic] - fn description_cannot_be_longer_than_3000_chars() { - let description = (3001..10_000).fake(); - let title = (1..50).fake(); + #[test] + #[should_panic] + fn description_cannot_be_longer_than_3000_chars() { + let description = (3001..10_000).fake(); + let title = (1..50).fake(); - create_ticket(title, description, Status::ToDo); - } + create_ticket(title, description, Status::ToDo); + } - #[test] - fn valid_tickets_can_be_created() { - let description = (0..3000).fake(); - let title = (1..50).fake(); - let status = Status::Done; + #[test] + fn valid_tickets_can_be_created() { + let description = (0..3000).fake(); + let title = (1..50).fake(); + let status = Status::Done; - create_ticket(title, description, status); - } + create_ticket(title, description, status); } } diff --git a/jira-wip/src/koans/01_ticket/04_visibility.rs b/jira-wip/src/koans/01_ticket/04_visibility.rs index 04f4d24..507d5d8 100644 --- a/jira-wip/src/koans/01_ticket/04_visibility.rs +++ b/jira-wip/src/koans/01_ticket/04_visibility.rs @@ -1,101 +1,99 @@ -mod visibility { - /// You might have noticed the `mod XX` at the beginning of our koans. - /// `mod` stands for module: it's one of the tools Rust gives you to organise your code. - /// In particular, modules have an impact on the visibility of your structs, enums and functions. +/// You might have noticed the `mod XX` at the beginning of our koans' tests. +/// `mod` stands for module: it's one of the tools Rust gives you to organise your code. +/// In particular, modules have an impact on the visibility of your structs, enums and functions. +/// +/// We want to use this koan to explore the impact that modules have on the structure of your +/// projects and how you can leverage them to enforce encapsulation. +/// +/// You can find out more about modules and visibility in the Rust book: +/// https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html +pub mod ticket { + /// Structs, enums and functions defined in a module are visible to all other structs, + /// enums and functions in the same module - e.g. we can use `Ticket` in the signature + /// of `create_ticket` as our return type. /// - /// We want to use this koan to explore the impact that modules have on the structure of your - /// projects and how you can leverage them to enforce encapsulation. + /// That is no longer the case outside of the module where they are defined: all entities + /// in Rust are private by default, unless they prefixed with `pub`. /// - /// You can find out more about modules and visibility in the Rust book: - /// https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html - pub mod ticket { - /// Structs, enums and functions defined in a module are visible to all other structs, - /// enums and functions in the same module - e.g. we can use `Ticket` in the signature - /// of `create_ticket` as our return type. - /// - /// That is no longer the case outside of the module where they are defined: all entities - /// in Rust are private by default, unless they prefixed with `pub`. - /// - /// The same applies to fields in a struct. - /// Functions defined within the same module of a struct have access to all the fields of - /// the struct (e.g. `create_ticket` can create a `Ticket` by specifying its fields). - /// Outside of the module, those fields are inaccessible because they are considered - /// private by default, unless prefixed with pub. - enum Status { - ToDo, - InProgress, - Blocked, - Done, - } + /// The same applies to fields in a struct. + /// Functions defined within the same module of a struct have access to all the fields of + /// the struct (e.g. `create_ticket` can create a `Ticket` by specifying its fields). + /// Outside of the module, those fields are inaccessible because they are considered + /// private by default, unless prefixed with pub. + enum Status { + ToDo, + InProgress, + Blocked, + Done, + } - struct Ticket { - title: String, - description: String, - status: Status, - } + struct Ticket { + title: String, + description: String, + status: Status, + } - fn create_ticket(title: String, description: String, status: Status) -> Ticket { - if title.is_empty() { - panic!("Title cannot be empty!"); - } - if title.len() > 50 { - panic!("A title cannot be longer than 50 characters!"); - } - if description.len() > 3000 { - panic!("A description cannot be longer than 3000 characters!"); - } + fn create_ticket(title: String, description: String, status: Status) -> Ticket { + if title.is_empty() { + panic!("Title cannot be empty!"); + } + if title.len() > 50 { + panic!("A title cannot be longer than 50 characters!"); + } + if description.len() > 3000 { + panic!("A description cannot be longer than 3000 characters!"); + } - // Functions implicitly return the result of their last expression so we can omit - // the `return` keyword here. - Ticket { - title, - description, - status, - } + // Functions implicitly return the result of their last expression so we can omit + // the `return` keyword here. + Ticket { + title, + description, + status, } } +} - #[cfg(test)] - mod tests { - /// Add the necessary `pub` modifiers in the code above to avoid having the compiler - /// complaining about this use statement. - use super::ticket::{create_ticket, Status, Ticket}; +#[cfg(test)] +mod tests { + /// Add the necessary `pub` modifiers in the code above to avoid having the compiler + /// complaining about this use statement. + use super::ticket::{create_ticket, Status, Ticket}; - /// Be careful though! We don't want this function to compile after you have changed - /// visibility to make the use statement compile! - /// Once you have verified that it indeed doesn't compile, comment it out. - fn should_not_be_possible() { - let ticket: Ticket = - create_ticket("A title".into(), "A description".into(), Status::ToDo); + /// Be careful though! We don't want this function to compile after you have changed + /// visibility to make the use statement compile! + /// Once you have verified that it indeed doesn't compile, comment it out. + fn should_not_be_possible() { + let ticket: Ticket = + create_ticket("A title".into(), "A description".into(), Status::ToDo); - // You should be seeing this error when trying to run this koan: - // - // error[E0616]: field `description` of struct `path_to_enlightenment::visibility::ticket::Ticket` is private - // --> jira-wip/src/koans/01_ticket/04_visibility.rs:81:24 - // | - // 81 | assert_eq!(ticket.description, "A description"); - // | ^^^^^^^^^^^^^^^^^^ - // - // Once you have verified that the below does not compile, - // comment the line out to move on to the next koan! - assert_eq!(ticket.description, "A description"); - } + // You should be seeing this error when trying to run this koan: + // + // error[E0616]: field `description` of struct `path_to_enlightenment::visibility::ticket::Ticket` is private + // --> jira-wip/src/koans/01_ticket/04_visibility.rs:81:24 + // | + // 81 | assert_eq!(ticket.description, "A description"); + // | ^^^^^^^^^^^^^^^^^^ + // + // Once you have verified that the below does not compile, + // comment the line out to move on to the next koan! + assert_eq!(ticket.description, "A description"); + } - fn encapsulation_cannot_be_violated() { - // This should be impossible as well, with a similar error as the one encountered above. - // (It will throw a compilation error only after you have commented the faulty line - // in the previous test - next compilation stage!) - // - // This proves that `create_ticket` is now the only way to get a `Ticket` instance. - // It's impossible to create a ticket with an illegal title or description! - // - // Once you have verified that the below does not compile, - // comment the lines out to move on to the next koan! - let ticket = Ticket { - title: "A title".into(), - description: "A description".into(), - status: Status::ToDo, - }; - } + fn encapsulation_cannot_be_violated() { + // This should be impossible as well, with a similar error as the one encountered above. + // (It will throw a compilation error only after you have commented the faulty line + // in the previous test - next compilation stage!) + // + // This proves that `create_ticket` is now the only way to get a `Ticket` instance. + // It's impossible to create a ticket with an illegal title or description! + // + // Once you have verified that the below does not compile, + // comment the lines out to move on to the next koan! + let ticket = Ticket { + title: "A title".into(), + description: "A description".into(), + status: Status::ToDo, + }; } } diff --git a/jira-wip/src/koans/01_ticket/05_ownership.rs b/jira-wip/src/koans/01_ticket/05_ownership.rs index 2658059..5642028 100644 --- a/jira-wip/src/koans/01_ticket/05_ownership.rs +++ b/jira-wip/src/koans/01_ticket/05_ownership.rs @@ -1,121 +1,119 @@ -mod ownership { - /// Using modules and visibility modifiers we have now fully encapsulated the fields of our Ticket. - /// There is no way to create a Ticket instance skipping our validation. - /// At the same time though, we have made it impossible to access the fields of our struct, - /// because they are private! - /// - /// Let's fix that introducing a bunch of accessor methods providing **read-only** access - /// to the fields in a ticket. +/// Using modules and visibility modifiers we have now fully encapsulated the fields of our Ticket. +/// There is no way to create a Ticket instance skipping our validation. +/// At the same time though, we have made it impossible to access the fields of our struct, +/// because they are private! +/// +/// Let's fix that introducing a bunch of accessor methods providing **read-only** access +/// to the fields in a ticket. - /// Let's import the Status enum we defined in the previous exercise, we won't have to modify it. - use super::visibility::ticket::Status; +/// Let's import the Status enum we defined in the previous exercise, we won't have to modify it. +use super::visibility::ticket::Status; - /// Re-defining Ticket here because methods who need to access private fields - /// have to be defined in the same module of the struct itself, as we saw in the previous - /// exercise. - pub struct Ticket { - title: String, - description: String, - status: Status - } +/// Re-defining Ticket here because methods who need to access private fields +/// have to be defined in the same module of the struct itself, as we saw in the previous +/// exercise. +pub struct Ticket { + title: String, + description: String, + status: Status +} - /// Methods on a struct are defined in `impl` blocks. - impl Ticket { - /// The syntax looks very similar to the syntax to define functions. - /// There is only one peculiarity: if you want to access the struct in a method, - /// you need to take `self` as your first parameter in the method signature. - /// - /// You have three options, depending on what you are trying to accomplish: - /// - self - /// - &self - /// - &mut self - /// - /// We are now touching for the first time the topic of ownership, enforced by - /// the compiler via the (in)famous borrow-checker. - /// - /// In Rust, each value has an owner, statically determined at compile-time. - /// There is only one owner for each value at any given time. - /// Tracking ownership at compile-time is what makes it possible for Rust not to have - /// garbage collection without requiring the developer to manage memory explicitly - /// (most of the times). - /// - /// What can an owner do with a value `a`? - /// It can mutate it. - /// It can move ownership to another function or variable. - /// It can lend many immutable references (`&a`) to that value to other functions or variables. - /// It can lend a **single** mutable reference (`&mut a`) to that value to another - /// function or variable. - /// - /// What can you do with a shared immutable reference (`&a`) to a value? - /// You can read the value and create more immutable references. - /// - /// What can you do with a single mutable reference (`&mut a`) to a value? - /// You can mutate the underlying value. - /// - /// Ownership is embedded in the type system: each function has to declare in its signature - /// what kind of ownership level it requires for all its arguments. - /// If the caller cannot fulfill those requirements, they cannot call the function. - /// - /// In our case, we only need to read a field of our Ticket struct: it will be enough to ask - /// for an immutable reference to our struct. - /// - /// If this sounds a bit complicated/vague, hold on: it will get clearer as you - /// move through the exercises and work your way through a bunch of compiler errors: - /// the compiler is the best pair programming buddy to get familiar with ownership - /// and its rules. - /// To read more on ownership check: - /// https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html - pub fn title(&self) -> &String { - /// We are returning an immutable reference (&) to our title field. - /// This will allow us to access this field without being able to mutate it: - /// encapsulation is guaranteed and we can rest assured that our invariants - /// cannot be violated. - &self.title - } +/// Methods on a struct are defined in `impl` blocks. +impl Ticket { + /// The syntax looks very similar to the syntax to define functions. + /// There is only one peculiarity: if you want to access the struct in a method, + /// you need to take `self` as your first parameter in the method signature. + /// + /// You have three options, depending on what you are trying to accomplish: + /// - self + /// - &self + /// - &mut self + /// + /// We are now touching for the first time the topic of ownership, enforced by + /// the compiler via the (in)famous borrow-checker. + /// + /// In Rust, each value has an owner, statically determined at compile-time. + /// There is only one owner for each value at any given time. + /// Tracking ownership at compile-time is what makes it possible for Rust not to have + /// garbage collection without requiring the developer to manage memory explicitly + /// (most of the times). + /// + /// What can an owner do with a value `a`? + /// It can mutate it. + /// It can move ownership to another function or variable. + /// It can lend many immutable references (`&a`) to that value to other functions or variables. + /// It can lend a **single** mutable reference (`&mut a`) to that value to another + /// function or variable. + /// + /// What can you do with a shared immutable reference (`&a`) to a value? + /// You can read the value and create more immutable references. + /// + /// What can you do with a single mutable reference (`&mut a`) to a value? + /// You can mutate the underlying value. + /// + /// Ownership is embedded in the type system: each function has to declare in its signature + /// what kind of ownership level it requires for all its arguments. + /// If the caller cannot fulfill those requirements, they cannot call the function. + /// + /// In our case, we only need to read a field of our Ticket struct: it will be enough to ask + /// for an immutable reference to our struct. + /// + /// If this sounds a bit complicated/vague, hold on: it will get clearer as you + /// move through the exercises and work your way through a bunch of compiler errors: + /// the compiler is the best pair programming buddy to get familiar with ownership + /// and its rules. + /// To read more on ownership check: + /// https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html + pub fn title(&self) -> &String { + /// We are returning an immutable reference (&) to our title field. + /// This will allow us to access this field without being able to mutate it: + /// encapsulation is guaranteed and we can rest assured that our invariants + /// cannot be violated. + &self.title + } - /// Replace __ with the proper types to get accessor methods for the other two fields. - /// If you are asking yourself why we are returning &str instead of &String, check out: - /// https://blog.thoughtram.io/string-vs-str-in-rust/ - pub fn description(__) -> __ { - todo!() - } + /// Replace __ with the proper types to get accessor methods for the other two fields. + /// If you are asking yourself why we are returning &str instead of &String, check out: + /// https://blog.thoughtram.io/string-vs-str-in-rust/ + pub fn description(__) -> __ { + todo!() + } - pub fn status(__) -> __ { - todo!() - } + pub fn status(__) -> __ { + todo!() } +} - pub fn create_ticket(title: String, description: String, status: Status) -> Ticket { - if title.is_empty() { - panic!("Title cannot be empty!"); - } - if title.len() > 50 { - panic!("A title cannot be longer than 50 characters!"); - } - if description.len() > 3000 { - panic!("A description cannot be longer than 3000 characters!"); - } +pub fn create_ticket(title: String, description: String, status: Status) -> Ticket { + if title.is_empty() { + panic!("Title cannot be empty!"); + } + if title.len() > 50 { + panic!("A title cannot be longer than 50 characters!"); + } + if description.len() > 3000 { + panic!("A description cannot be longer than 3000 characters!"); + } - Ticket { - title, - description, - status, - } + Ticket { + title, + description, + status, } +} - #[cfg(test)] - mod tests { - use super::{create_ticket, Ticket}; - use super::super::visibility::ticket::Status; +#[cfg(test)] +mod tests { + use super::{create_ticket, Ticket}; + use super::super::visibility::ticket::Status; - fn verify_without_tampering() { - let ticket: Ticket = create_ticket("A title".into(), "A description".into(), Status::ToDo); + fn verify_without_tampering() { + let ticket: Ticket = create_ticket("A title".into(), "A description".into(), Status::ToDo); - /// Instead of accessing the field `ticket.description` we are calling the accessor - /// method, `ticket.description()`, which returns us a reference to the field value - /// and allows us to verify its value without having the chance to modify it. - assert_eq!(ticket.description(), "A description"); - assert_eq!(ticket.title(), "A title"); - } + /// Instead of accessing the field `ticket.description` we are calling the accessor + /// method, `ticket.description()`, which returns us a reference to the field value + /// and allows us to verify its value without having the chance to modify it. + assert_eq!(ticket.description(), "A description"); + assert_eq!(ticket.title(), "A title"); } -} \ No newline at end of file +} diff --git a/jira-wip/src/koans/01_ticket/06_traits.rs b/jira-wip/src/koans/01_ticket/06_traits.rs index 6882fd8..b430705 100644 --- a/jira-wip/src/koans/01_ticket/06_traits.rs +++ b/jira-wip/src/koans/01_ticket/06_traits.rs @@ -1,78 +1,76 @@ -mod traits { - use crate::path_to_enlightenment::visibility::ticket::Status; +use crate::path_to_enlightenment::visibility::ticket::Status; - /// You might have noticed that in the test for the previous koan we haven't checked if - /// the status returned by `.status()` matched the status we passed to `create_ticket`. - /// - /// That's because `assert_eq!(ticket.status(), Status::ToDo)` would have failed to compiled: - /// - /// error[E0369]: binary operation `==` cannot be applied to type `&path_to_enlightenment::visibility::ticket::Status` - /// --> jira-wip/src/koans/01_ticket/05_ownership.rs:128:13 - /// | - /// 128 | assert_eq!(ticket.status(), Status::ToDo); - /// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - /// | | - /// | &path_to_enlightenment::visibility::ticket::Status - /// | path_to_enlightenment::visibility::ticket::Status - /// | - /// = note: an implementation of `std::cmp::PartialEq` might be missing for `&path_to_enlightenment::visibility::ticket::Status` - /// - /// `assert_eq` requires that its arguments implement the `PartialEq` trait. - /// What is a trait? - /// Traits in Rust are very similar to interfaces in other programming languages: - /// a trait describes a behaviour/capability. - /// For example: - /// - /// ``` - /// pub trait Pay { - /// fn pay(self, amount: u64, currency: String) -> u64 - /// } - /// ``` - /// - /// In practical terms, a trait defines the signature of a collection of methods. - /// To implement a trait, a struct or an enum have to implement those methods - /// in an `impl Trait` block: - /// - /// ``` - /// impl Pay for TaxPayer { - /// fn pay(self, amount: u64, currency: String) -> u64 { - /// todo!() - /// } - /// } - /// ``` - /// - /// `PartialEq` is the trait that powers the == operator. - /// Its definition looks something like this (simplified): - /// ``` - /// pub trait PartialEq { - /// fn eq(&self, other: &Self) -> bool - /// } - /// ``` - /// It's slightly more complicated, with generic parameters, to allow comparing different types. - /// But let's roll with this simplified version for now. - /// - /// Let's implement it for Status! - impl PartialEq for Status { - fn eq(&self, other: &Status) -> bool { - // If you need to refresh the `match` syntax, checkout - // https://doc.rust-lang.org/book/ch06-02-match.html - match (self, other) { - __ - } +/// You might have noticed that in the test for the previous koan we haven't checked if +/// the status returned by `.status()` matched the status we passed to `create_ticket`. +/// +/// That's because `assert_eq!(ticket.status(), Status::ToDo)` would have failed to compiled: +/// +/// error[E0369]: binary operation `==` cannot be applied to type `&path_to_enlightenment::visibility::ticket::Status` +/// --> jira-wip/src/koans/01_ticket/05_ownership.rs:128:13 +/// | +/// 128 | assert_eq!(ticket.status(), Status::ToDo); +/// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/// | | +/// | &path_to_enlightenment::visibility::ticket::Status +/// | path_to_enlightenment::visibility::ticket::Status +/// | +/// = note: an implementation of `std::cmp::PartialEq` might be missing for `&path_to_enlightenment::visibility::ticket::Status` +/// +/// `assert_eq` requires that its arguments implement the `PartialEq` trait. +/// What is a trait? +/// Traits in Rust are very similar to interfaces in other programming languages: +/// a trait describes a behaviour/capability. +/// For example: +/// +/// ``` +/// pub trait Pay { +/// fn pay(self, amount: u64, currency: String) -> u64 +/// } +/// ``` +/// +/// In practical terms, a trait defines the signature of a collection of methods. +/// To implement a trait, a struct or an enum have to implement those methods +/// in an `impl Trait` block: +/// +/// ``` +/// impl Pay for TaxPayer { +/// fn pay(self, amount: u64, currency: String) -> u64 { +/// todo!() +/// } +/// } +/// ``` +/// +/// `PartialEq` is the trait that powers the == operator. +/// Its definition looks something like this (simplified): +/// ``` +/// pub trait PartialEq { +/// fn eq(&self, other: &Self) -> bool +/// } +/// ``` +/// It's slightly more complicated, with generic parameters, to allow comparing different types. +/// But let's roll with this simplified version for now. +/// +/// Let's implement it for Status! +impl PartialEq for Status { + fn eq(&self, other: &Status) -> bool { + // If you need to refresh the `match` syntax, checkout + // https://doc.rust-lang.org/book/ch06-02-match.html + match (self, other) { + __ } } +} - #[cfg(test)] - mod tests { - use super::*; +#[cfg(test)] +mod tests { + use super::*; - #[test] - fn test_equality() { - // Your goal is to make this test compile. - assert_eq!(Status::ToDo == Status::ToDo, true); - assert_eq!(Status::Done == Status::ToDo, false); - assert_eq!(Status::InProgress == Status::ToDo, false); - assert_eq!(Status::InProgress == Status::InProgress, true); - } + #[test] + fn test_equality() { + // Your goal is to make this test compile. + assert_eq!(Status::ToDo == Status::ToDo, true); + assert_eq!(Status::Done == Status::ToDo, false); + assert_eq!(Status::InProgress == Status::ToDo, false); + assert_eq!(Status::InProgress == Status::InProgress, true); } } diff --git a/jira-wip/src/koans/01_ticket/07_derive.rs b/jira-wip/src/koans/01_ticket/07_derive.rs index 8dba3df..58931bb 100644 --- a/jira-wip/src/koans/01_ticket/07_derive.rs +++ b/jira-wip/src/koans/01_ticket/07_derive.rs @@ -1,43 +1,41 @@ -mod derive { - /// Cool, we learned what a trait is and how to implement one. - /// I am sure you agree with us though: implementing PartialEq was quite tedious - /// and repetitive, a computer can surely do a better job without having to trouble us! - /// - /// The Rust team feels your pain, hence a handy feature: derive macros. - /// Derive macros are a code-generation tool: before code compilation kicks-in, derive - /// macros take as input the code they have been applied to (as a stream of tokens!) - /// and they have a chance to generate other code, as needed. - /// - /// For example, this `#[derive(PartialEq)]` will take as input the definition of our - /// enum and generate an implementation of PartialEq which is exactly equivalent to - /// the one we rolled out manually in the previous koan. - /// You can check the code generated by derive macros using `cargo expand`: - /// https://github.com/dtolnay/cargo-expand - /// - /// ```sh - /// cargo expand -p jira-wip path_to_enlightenment::derive - /// ``` - /// - /// PartialEq is not the only trait whose implementation can be derived automatically! - #[derive(PartialEq)] - pub enum Status { - ToDo, - InProgress, - Blocked, - Done, - } +/// Cool, we learned what a trait is and how to implement one. +/// I am sure you agree with us though: implementing PartialEq was quite tedious +/// and repetitive, a computer can surely do a better job without having to trouble us! +/// +/// The Rust team feels your pain, hence a handy feature: derive macros. +/// Derive macros are a code-generation tool: before code compilation kicks-in, derive +/// macros take as input the code they have been applied to (as a stream of tokens!) +/// and they have a chance to generate other code, as needed. +/// +/// For example, this `#[derive(PartialEq)]` will take as input the definition of our +/// enum and generate an implementation of PartialEq which is exactly equivalent to +/// the one we rolled out manually in the previous koan. +/// You can check the code generated by derive macros using `cargo expand`: +/// https://github.com/dtolnay/cargo-expand +/// +/// ```sh +/// cargo expand -p jira-wip path_to_enlightenment::derive +/// ``` +/// +/// PartialEq is not the only trait whose implementation can be derived automatically! +#[derive(PartialEq)] +pub enum Status { + ToDo, + InProgress, + Blocked, + Done, +} - #[cfg(test)] - mod tests { - use super::*; +#[cfg(test)] +mod tests { + use super::*; - #[test] - fn assertions() { - // Your goal is to make this test compile. - assert_eq!(Status::ToDo, Status::ToDo); - assert_ne!(Status::Done, Status::ToDo); - assert_ne!(Status::InProgress, Status::ToDo); - assert_eq!(Status::InProgress, Status::InProgress); - } + #[test] + fn assertions() { + // Your goal is to make this test compile. + assert_eq!(Status::ToDo, Status::ToDo); + assert_ne!(Status::Done, Status::ToDo); + assert_ne!(Status::InProgress, Status::ToDo); + assert_eq!(Status::InProgress, Status::InProgress); } } diff --git a/jira-wip/src/koans/01_ticket/08_recap.rs b/jira-wip/src/koans/01_ticket/08_recap.rs index 08d8bf0..050fd98 100644 --- a/jira-wip/src/koans/01_ticket/08_recap.rs +++ b/jira-wip/src/koans/01_ticket/08_recap.rs @@ -1,63 +1,61 @@ -mod recap { - /// We have come quite a long way now: from how to define a struct to traits and derive macros, - /// touching on tests, module system, visibility, ownership and method syntax. - /// Take a deep breath, stretch a bit, review what we have done. - /// - /// Then get ready to dive in the next section! - - #[derive(PartialEq, Debug)] - pub enum Status { - ToDo, - InProgress, - Blocked, - Done, - } +/// We have come quite a long way now: from how to define a struct to traits and derive macros, +/// touching on tests, module system, visibility, ownership and method syntax. +/// Take a deep breath, stretch a bit, review what we have done. +/// +/// Then get ready to dive in the next section! + +#[derive(PartialEq, Debug)] +pub enum Status { + ToDo, + InProgress, + Blocked, + Done, +} - pub struct Ticket { - title: String, - description: String, - status: Status, - } +pub struct Ticket { + title: String, + description: String, + status: Status, +} - impl Ticket { - pub fn title(&self) -> &String { - &self.title - } +impl Ticket { + pub fn title(&self) -> &String { + &self.title + } - pub fn description(&self) -> &String { - &self.description - } + pub fn description(&self) -> &String { + &self.description + } - pub fn status(&self) -> &Status { - &self.status - } + pub fn status(&self) -> &Status { + &self.status + } +} + +pub fn create_ticket(title: String, description: String, status: Status) -> Ticket { + if title.is_empty() { + panic!("Title cannot be empty!"); + } + if title.len() > 50 { + panic!("A title cannot be longer than 50 characters!"); + } + if description.len() > 3000 { + panic!("A description cannot be longer than 3000 characters!"); } - pub fn create_ticket(title: String, description: String, status: Status) -> Ticket { - if title.is_empty() { - panic!("Title cannot be empty!"); - } - if title.len() > 50 { - panic!("A title cannot be longer than 50 characters!"); - } - if description.len() > 3000 { - panic!("A description cannot be longer than 3000 characters!"); - } - - Ticket { - title, - description, - status, - } + Ticket { + title, + description, + status, } +} - #[cfg(test)] - mod tests { - #[test] - fn the_next_step_of_your_journey() { - let i_am_ready_to_continue = __; +#[cfg(test)] +mod tests { + #[test] + fn the_next_step_of_your_journey() { + let i_am_ready_to_continue = __; - assert!(i_am_ready_to_continue); - } + assert!(i_am_ready_to_continue); } } diff --git a/jira-wip/src/koans/02_ticket_store/01_store.rs b/jira-wip/src/koans/02_ticket_store/01_store.rs index 8323851..ad7ac3b 100644 --- a/jira-wip/src/koans/02_ticket_store/01_store.rs +++ b/jira-wip/src/koans/02_ticket_store/01_store.rs @@ -1,125 +1,123 @@ -mod store { - /// It's time to shift focus: our tickets are doing well, but they need a home. - /// A place where we can store them, search for them, retrieve them. - /// - /// We can use many different data structures to store and manage our tickets. - /// JIRA users rely heavily on ticket identifiers, e.g. RUST-2018 or COVID-19. - /// It's a unique label that unambiguously identifies a single ticket, - /// generally `-`. - /// We don't have the concept of a board yet, so we'll roll with a simple numerical id. - /// - /// What is the simplest data structure that allows us to fetch a ticket given its id? - /// It makes sense for us to use a HashMap, also known as a dictionary in other languages. - /// You can read more about the HashMap in Rust here: - /// https://doc.rust-lang.org/std/collections/struct.HashMap.html - use std::collections::HashMap; - /// Let's import what we worked on in the previous set of exercises. - use super::recap::Ticket; - - /// First we will create a TicketStore struct, with a `data` field of type HashMap. - /// - /// HashMap is a *generic* struct: we need to specify two types, one for the key, and one for - /// the stored value - HashMap. - /// - /// Let's set the value type to our Ticket, and we will use an unsigned integer for our ids. - struct TicketStore { - /// The collection of stored tickets. - data: HashMap, - } +/// It's time to shift focus: our tickets are doing well, but they need a home. +/// A place where we can store them, search for them, retrieve them. +/// +/// We can use many different data structures to store and manage our tickets. +/// JIRA users rely heavily on ticket identifiers, e.g. RUST-2018 or COVID-19. +/// It's a unique label that unambiguously identifies a single ticket, +/// generally `-`. +/// We don't have the concept of a board yet, so we'll roll with a simple numerical id. +/// +/// What is the simplest data structure that allows us to fetch a ticket given its id? +/// It makes sense for us to use a HashMap, also known as a dictionary in other languages. +/// You can read more about the HashMap in Rust here: +/// https://doc.rust-lang.org/std/collections/struct.HashMap.html +use std::collections::HashMap; +/// Let's import what we worked on in the previous set of exercises. +use super::recap::Ticket; + +/// First we will create a TicketStore struct, with a `data` field of type HashMap. +/// +/// HashMap is a *generic* struct: we need to specify two types, one for the key, and one for +/// the stored value - HashMap. +/// +/// Let's set the value type to our Ticket, and we will use an unsigned integer for our ids. +struct TicketStore { + /// The collection of stored tickets. + data: HashMap, +} - impl TicketStore { - /// Methods do not have to take self as a parameter. - /// This is the equivalent of a class/static method in other programming languages. - /// It can be invoked using `TicketStore::new()`. - pub fn new() -> TicketStore { - TicketStore { - // Note that the compiler can infer the types for our HashMaps' key-value pairs. - data: HashMap::new(), - } +impl TicketStore { + /// Methods do not have to take self as a parameter. + /// This is the equivalent of a class/static method in other programming languages. + /// It can be invoked using `TicketStore::new()`. + pub fn new() -> TicketStore { + TicketStore { + // Note that the compiler can infer the types for our HashMaps' key-value pairs. + data: HashMap::new(), } + } - /// We take `&mut self` because we will have to mutate our HashMap to insert a new - /// key-value pair. - pub fn save(&mut self, ticket: Ticket, id: u32) { - todo!() - } + /// We take `&mut self` because we will have to mutate our HashMap to insert a new + /// key-value pair. + pub fn save(&mut self, ticket: Ticket, id: u32) { + todo!() + } - pub fn get(&self, id: &u32) -> &Ticket { - todo!() - } - } + pub fn get(&self, id: &u32) -> &Ticket { + todo!() + } +} - #[cfg(test)] - mod tests { - use super::super::recap::{create_ticket, Status}; - use super::*; - use fake::{Fake, Faker}; - - /// Now let's put our TicketStore to use - /// - /// We are going to create a ticket, save it in our TicketStore and finally validate that - /// the ticket we have saved in our store is indeed the same ticket we created. - #[test] - fn a_ticket_with_a_home() { - let ticket = generate_ticket(Status::ToDo); - - // Pay special attention to the 'mut' keyword here: variables are immutable - // by default in Rust. - // The `mut` keyword is used to signal that you must pay special attention to the - // variable as it's likely to change later on in the function body. - let mut store = TicketStore::new(); - let ticket_id = Faker.fake(); - - // Here we need to create a clone of our `ticket` because `save` takes the `ticket` - // argument as value, thus taking ownership of its value out of the caller function - // into the method. - // But we need `ticket`'s value after this method call, to verify it matches what - // we retrieve. - // Hence the need to clone it, creating a copy of the value and passing that copy to - // the `save` method. - // - // (You might have to go back to the `recap` koan to derive a couple more traits - // for Ticket and Status...) - store.save(ticket.clone(), ticket_id); - - assert_eq!(store.get(&ticket_id), &ticket); - } +#[cfg(test)] +mod tests { + use super::super::recap::{create_ticket, Status}; + use super::*; + use fake::{Fake, Faker}; - /// We want our `get` method to panic when looking for an id to which there is no ticket - /// associated (for now). - /// - /// Rust has a way to handle this failure mode more gracefully, we will take a look - /// at it later. - #[test] - #[should_panic] - fn a_missing_ticket() { - let ticket_store = TicketStore::new(); - let ticket_id = Faker.fake(); - - ticket_store.get(&ticket_id); - } + /// Now let's put our TicketStore to use + /// + /// We are going to create a ticket, save it in our TicketStore and finally validate that + /// the ticket we have saved in our store is indeed the same ticket we created. + #[test] + fn a_ticket_with_a_home() { + let ticket = generate_ticket(Status::ToDo); + + // Pay special attention to the 'mut' keyword here: variables are immutable + // by default in Rust. + // The `mut` keyword is used to signal that you must pay special attention to the + // variable as it's likely to change later on in the function body. + let mut store = TicketStore::new(); + let ticket_id = Faker.fake(); + + // Here we need to create a clone of our `ticket` because `save` takes the `ticket` + // argument as value, thus taking ownership of its value out of the caller function + // into the method. + // But we need `ticket`'s value after this method call, to verify it matches what + // we retrieve. + // Hence the need to clone it, creating a copy of the value and passing that copy to + // the `save` method. + // + // (You might have to go back to the `recap` koan to derive a couple more traits + // for Ticket and Status...) + store.save(ticket.clone(), ticket_id); + + assert_eq!(store.get(&ticket_id), &ticket); + } + + /// We want our `get` method to panic when looking for an id to which there is no ticket + /// associated (for now). + /// + /// Rust has a way to handle this failure mode more gracefully, we will take a look + /// at it later. + #[test] + #[should_panic] + fn a_missing_ticket() { + let ticket_store = TicketStore::new(); + let ticket_id = Faker.fake(); + + ticket_store.get(&ticket_id); + } - /// This is not our desired behaviour for the final version of the ticket store - /// but it will do for now. - #[test] - fn inserting_a_ticket_with_an_existing_id_overwrites_previous_ticket() { - let first_ticket = generate_ticket(Status::ToDo); - let second_ticket = generate_ticket(Status::ToDo); - let ticket_id = Faker.fake(); - let mut store = TicketStore::new(); + /// This is not our desired behaviour for the final version of the ticket store + /// but it will do for now. + #[test] + fn inserting_a_ticket_with_an_existing_id_overwrites_previous_ticket() { + let first_ticket = generate_ticket(Status::ToDo); + let second_ticket = generate_ticket(Status::ToDo); + let ticket_id = Faker.fake(); + let mut store = TicketStore::new(); - store.save(first_ticket.clone(), ticket_id); - assert_eq!(store.get(&ticket_id), &first_ticket); + store.save(first_ticket.clone(), ticket_id); + assert_eq!(store.get(&ticket_id), &first_ticket); - store.save(second_ticket.clone(), ticket_id); - assert_eq!(store.get(&ticket_id), &second_ticket); - } + store.save(second_ticket.clone(), ticket_id); + assert_eq!(store.get(&ticket_id), &second_ticket); + } - fn generate_ticket(status: Status) -> Ticket { - let description = (0..3000).fake(); - let title = (1..50).fake(); + fn generate_ticket(status: Status) -> Ticket { + let description = (0..3000).fake(); + let title = (1..50).fake(); - create_ticket(title, description, status) - } + create_ticket(title, description, status) } } diff --git a/jira-wip/src/koans/02_ticket_store/02_option.rs b/jira-wip/src/koans/02_ticket_store/02_option.rs index 6770eca..c4637f4 100644 --- a/jira-wip/src/koans/02_ticket_store/02_option.rs +++ b/jira-wip/src/koans/02_ticket_store/02_option.rs @@ -1,106 +1,104 @@ -mod option { - use super::recap::Ticket; - use std::collections::HashMap; +use super::recap::Ticket; +use std::collections::HashMap; - struct TicketStore { - data: HashMap, - } +struct TicketStore { + data: HashMap, +} - impl TicketStore { - pub fn new() -> TicketStore { - TicketStore { - data: HashMap::new(), - } +impl TicketStore { + pub fn new() -> TicketStore { + TicketStore { + data: HashMap::new(), } + } - pub fn save(&mut self, ticket: Ticket, id: u32) { - self.data.insert(id, ticket); - } + pub fn save(&mut self, ticket: Ticket, id: u32) { + self.data.insert(id, ticket); + } - /// Trying to implement `get` in the previous koan might have caused you some issues due - /// to a signature mismatch: `get` on a HashMap returns an `Option<&Ticket>`, - /// not a `&Ticket`. - /// - /// What is an Option? - /// - /// In a nutshell, Rust does not have `null`: if a function returns a `Ticket` there is - /// no way for that `Ticket` not to be there. - /// If there is indeed the possibility of the function not being able to return a `Ticket`, - /// we need to express it in its return type. - /// That's where `Option` comes in (`Option` as in `Option`al, or at least that how - /// I think about it). - /// `Option` is an enum: - /// - /// ``` - /// enum Option { - /// Some(T), - /// None - /// } - /// ``` - /// `T` is a generic type parameter here: as we saw for HashMap, Rust allows you to be - /// generic over the types in your container. - /// The `None` variant means that the value is missing. - /// The `Some` variant instead tells you that you have a value. - /// - /// There is no way you can use the value in an `Option` without first checking the variant, - /// hence it is impossible to "forget" to handle `None` when writing code. - /// The compiler obliges you to handle both the happy and the unhappy case. - /// - /// For more details on `Option`, there is an exhaustive introduction in the Rust book: - /// https://doc.rust-lang.org/1.29.0/book/2018-edition/ch06-01-defining-an-enum.html#the-option-enum-and-its-advantages-over-null-values - pub fn get(&self, id: &u32) -> Option<&Ticket> { - todo!() - } - } + /// Trying to implement `get` in the previous koan might have caused you some issues due + /// to a signature mismatch: `get` on a HashMap returns an `Option<&Ticket>`, + /// not a `&Ticket`. + /// + /// What is an Option? + /// + /// In a nutshell, Rust does not have `null`: if a function returns a `Ticket` there is + /// no way for that `Ticket` not to be there. + /// If there is indeed the possibility of the function not being able to return a `Ticket`, + /// we need to express it in its return type. + /// That's where `Option` comes in (`Option` as in `Option`al, or at least that how + /// I think about it). + /// `Option` is an enum: + /// + /// ``` + /// enum Option { + /// Some(T), + /// None + /// } + /// ``` + /// `T` is a generic type parameter here: as we saw for HashMap, Rust allows you to be + /// generic over the types in your container. + /// The `None` variant means that the value is missing. + /// The `Some` variant instead tells you that you have a value. + /// + /// There is no way you can use the value in an `Option` without first checking the variant, + /// hence it is impossible to "forget" to handle `None` when writing code. + /// The compiler obliges you to handle both the happy and the unhappy case. + /// + /// For more details on `Option`, there is an exhaustive introduction in the Rust book: + /// https://doc.rust-lang.org/1.29.0/book/2018-edition/ch06-01-defining-an-enum.html#the-option-enum-and-its-advantages-over-null-values + pub fn get(&self, id: &u32) -> Option<&Ticket> { + todo!() + } +} - #[cfg(test)] - mod tests { - use super::super::recap::{create_ticket, Status}; - use super::*; - use fake::{Fake, Faker}; +#[cfg(test)] +mod tests { + use super::super::recap::{create_ticket, Status}; + use super::*; + use fake::{Fake, Faker}; - #[test] - fn a_ticket_with_a_home() { - let ticket = generate_ticket(Status::ToDo); - let mut store = TicketStore::new(); - let ticket_id = Faker.fake(); + #[test] + fn a_ticket_with_a_home() { + let ticket = generate_ticket(Status::ToDo); + let mut store = TicketStore::new(); + let ticket_id = Faker.fake(); - store.save(ticket.clone(), ticket_id); + store.save(ticket.clone(), ticket_id); - // Notice that, even when a ticket with the specified id exists in the store, - // it's returned as the `Some` variant of an `Option<&Ticket>`. - assert_eq!(store.get(&ticket_id), Some(&ticket)); - } + // Notice that, even when a ticket with the specified id exists in the store, + // it's returned as the `Some` variant of an `Option<&Ticket>`. + assert_eq!(store.get(&ticket_id), Some(&ticket)); + } - /// We want our `get` method to return `None` now, instead of panicking when looking for - /// an id to which there is no ticket associated. - #[test] - fn a_missing_ticket() { - let ticket_store = TicketStore::new(); - let ticket_id = Faker.fake(); + /// We want our `get` method to return `None` now, instead of panicking when looking for + /// an id to which there is no ticket associated. + #[test] + fn a_missing_ticket() { + let ticket_store = TicketStore::new(); + let ticket_id = Faker.fake(); - assert_eq!(ticket_store.get(&ticket_id), None); - } + assert_eq!(ticket_store.get(&ticket_id), None); + } - #[test] - fn inserting_a_ticket_with_an_existing_id_overwrites_previous_ticket() { - let first_ticket = generate_ticket(Status::ToDo); - let second_ticket = generate_ticket(Status::ToDo); - let mut store = TicketStore::new(); - let ticket_id = Faker.fake(); + #[test] + fn inserting_a_ticket_with_an_existing_id_overwrites_previous_ticket() { + let first_ticket = generate_ticket(Status::ToDo); + let second_ticket = generate_ticket(Status::ToDo); + let mut store = TicketStore::new(); + let ticket_id = Faker.fake(); - store.save(first_ticket.clone(), ticket_id); - assert_eq!(store.get(&ticket_id), Some(&first_ticket)); + store.save(first_ticket.clone(), ticket_id); + assert_eq!(store.get(&ticket_id), Some(&first_ticket)); - store.save(second_ticket.clone(), ticket_id); - assert_eq!(store.get(&ticket_id), Some(&second_ticket)); - } + store.save(second_ticket.clone(), ticket_id); + assert_eq!(store.get(&ticket_id), Some(&second_ticket)); + } - fn generate_ticket(status: Status) -> Ticket { - let description = (0..3000).fake(); - let title = (1..50).fake(); + fn generate_ticket(status: Status) -> Ticket { + let description = (0..3000).fake(); + let title = (1..50).fake(); - create_ticket(title, description, status) - } + create_ticket(title, description, status) } } diff --git a/jira-wip/src/koans/02_ticket_store/03_id_generation.rs b/jira-wip/src/koans/02_ticket_store/03_id_generation.rs index 048f597..0b6c5bb 100644 --- a/jira-wip/src/koans/02_ticket_store/03_id_generation.rs +++ b/jira-wip/src/koans/02_ticket_store/03_id_generation.rs @@ -1,111 +1,109 @@ -mod id_generation { - use std::collections::HashMap; - use super::recap::Ticket; +use std::collections::HashMap; +use super::recap::Ticket; + +/// Let's define a type-alias for our ticket id. +/// It's a lightweight technique to add a semantic layer to the underlying data type. +/// +/// The underlying type remains `u32`. +/// This remains valid code: +/// ``` +/// let number: u32 = 1; +/// let ticket_id: TicketId = number; +/// ``` +/// If we want to be sure we aren't mixing up ticket ids and `u32` variables with +/// a different semantic meaning, we would have to create a new type, +/// e.g. `struct TicketId(u32)`. +/// For now this doesn't feel necessary - we don't have many `u32`s flying around. +pub type TicketId = u32; + +// Feel free to add more fields to `TicketStore` to solve this koan! +struct TicketStore { + data: HashMap, +} + +impl TicketStore { + pub fn new() -> TicketStore + { + TicketStore { + data: HashMap::new(), + } + } - /// Let's define a type-alias for our ticket id. - /// It's a lightweight technique to add a semantic layer to the underlying data type. + /// So far we have taken the `id` as one the parameters of our `save` method. + /// + /// What happens when you call save passing two different tickets with the same id? + /// We have enforced with a test our expectation: the second ticket overwrites the first. + /// The other option would have been to error out. /// - /// The underlying type remains `u32`. - /// This remains valid code: - /// ``` - /// let number: u32 = 1; - /// let ticket_id: TicketId = number; - /// ``` - /// If we want to be sure we aren't mixing up ticket ids and `u32` variables with - /// a different semantic meaning, we would have to create a new type, - /// e.g. `struct TicketId(u32)`. - /// For now this doesn't feel necessary - we don't have many `u32`s flying around. - pub type TicketId = u32; - - // Feel free to add more fields to `TicketStore` to solve this koan! - struct TicketStore { - data: HashMap, + /// This isn't how JIRA works: you don't get to choose the id of your ticket, + /// it's generated for you and its uniqueness is guaranteed. + /// There is also another peculiarity: ids are integers and they are monotonically + /// increasing (the first ticket on a board will be `BOARDNAME-1`, the second + /// `BOARDNAME-2` and so on). + /// + /// We want the same behaviour in our clone, IronJira. + /// `TicketStore` will take care of generating an id for our ticket and the id + /// will be returned by `save` after insertion. + pub fn save(&mut self, ticket: Ticket) -> TicketId + { + let id = self.generate_id(); + self.data.insert(id, ticket); + id } - impl TicketStore { - pub fn new() -> TicketStore - { - TicketStore { - data: HashMap::new(), - } - } + pub fn get(&self, id: &TicketId) -> Option<&Ticket> { + self.data.get(id) + } - /// So far we have taken the `id` as one the parameters of our `save` method. - /// - /// What happens when you call save passing two different tickets with the same id? - /// We have enforced with a test our expectation: the second ticket overwrites the first. - /// The other option would have been to error out. - /// - /// This isn't how JIRA works: you don't get to choose the id of your ticket, - /// it's generated for you and its uniqueness is guaranteed. - /// There is also another peculiarity: ids are integers and they are monotonically - /// increasing (the first ticket on a board will be `BOARDNAME-1`, the second - /// `BOARDNAME-2` and so on). - /// - /// We want the same behaviour in our clone, IronJira. - /// `TicketStore` will take care of generating an id for our ticket and the id - /// will be returned by `save` after insertion. - pub fn save(&mut self, ticket: Ticket) -> TicketId - { - let id = self.generate_id(); - self.data.insert(id, ticket); - id - } - - pub fn get(&self, id: &TicketId) -> Option<&Ticket> { - self.data.get(id) - } + fn generate_id(__) -> TicketId { + todo!() + } +} - fn generate_id(__) -> TicketId { - todo!() - } - } +#[cfg(test)] +mod tests { + use super::*; + use super::super::recap::{create_ticket, Status}; + use fake::{Faker, Fake}; - #[cfg(test)] - mod tests { - use super::*; - use super::super::recap::{create_ticket, Status}; - use fake::{Faker, Fake}; + #[test] + fn a_ticket_with_a_home() + { + let ticket = generate_ticket(Status::ToDo); + let mut store = TicketStore::new(); - #[test] - fn a_ticket_with_a_home() - { - let ticket = generate_ticket(Status::ToDo); - let mut store = TicketStore::new(); + let ticket_id = store.save(ticket.clone()); - let ticket_id = store.save(ticket.clone()); + assert_eq!(store.get(&ticket_id), Some(&ticket)); + assert_eq!(ticket_id, 1); + } - assert_eq!(store.get(&ticket_id), Some(&ticket)); - assert_eq!(ticket_id, 1); - } + #[test] + fn a_missing_ticket() + { + let ticket_store = TicketStore::new(); + let ticket_id = Faker.fake(); - #[test] - fn a_missing_ticket() - { - let ticket_store = TicketStore::new(); - let ticket_id = Faker.fake(); + assert_eq!(ticket_store.get(&ticket_id), None); + } - assert_eq!(ticket_store.get(&ticket_id), None); - } + #[test] + fn id_generation_is_monotonic() + { + let n_tickets = 100; + let mut store = TicketStore::new(); - #[test] - fn id_generation_is_monotonic() - { - let n_tickets = 100; - let mut store = TicketStore::new(); - - for expected_id in 1..n_tickets { - let ticket = generate_ticket(Status::ToDo); - let ticket_id = store.save(ticket); - assert_eq!(expected_id, ticket_id); - } + for expected_id in 1..n_tickets { + let ticket = generate_ticket(Status::ToDo); + let ticket_id = store.save(ticket); + assert_eq!(expected_id, ticket_id); } + } - fn generate_ticket(status: Status) -> Ticket { - let description = (0..3000).fake(); - let title = (1..50).fake(); + fn generate_ticket(status: Status) -> Ticket { + let description = (0..3000).fake(); + let title = (1..50).fake(); - create_ticket(title, description, status) - } + create_ticket(title, description, status) } -} \ No newline at end of file +} diff --git a/jira-wip/src/koans/02_ticket_store/04_metadata.rs b/jira-wip/src/koans/02_ticket_store/04_metadata.rs index c716047..4ddb89f 100644 --- a/jira-wip/src/koans/02_ticket_store/04_metadata.rs +++ b/jira-wip/src/koans/02_ticket_store/04_metadata.rs @@ -1,153 +1,151 @@ -mod metadata { - use super::id_generation::TicketId; - use super::recap::Status; - /// `chrono` is the go-to crate in the Rust ecosystem when working with time. - /// `DateTime` deals with timezone-aware datetimes - it takes the timezone as a type parameter. - /// `DateTime` is the type for datetimes expressed in the coordinated universal time. - /// See: - /// - https://en.wikipedia.org/wiki/Coordinated_Universal_Time - /// - https://docs.rs/chrono/0.4.11/chrono/ - use chrono::{DateTime, Utc}; - use std::collections::HashMap; - - struct TicketStore { - data: HashMap, - current_id: TicketId, - } +use super::id_generation::TicketId; +use super::recap::Status; +/// `chrono` is the go-to crate in the Rust ecosystem when working with time. +/// `DateTime` deals with timezone-aware datetimes - it takes the timezone as a type parameter. +/// `DateTime` is the type for datetimes expressed in the coordinated universal time. +/// See: +/// - https://en.wikipedia.org/wiki/Coordinated_Universal_Time +/// - https://docs.rs/chrono/0.4.11/chrono/ +use chrono::{DateTime, Utc}; +use std::collections::HashMap; + +struct TicketStore { + data: HashMap, + current_id: TicketId, +} - /// When we retrieve a ticket we saved, we'd like to receive with it a bunch of metadata: - /// - the generated id; - /// - the datetime of its creation. - /// - /// Make the necessary changes without touching the types of the inputs and the returned - /// objects in our methods! - /// You can make inputs mutable, if needed. - impl TicketStore { - pub fn new() -> TicketStore { - TicketStore { - data: HashMap::new(), - current_id: 0, - } +/// When we retrieve a ticket we saved, we'd like to receive with it a bunch of metadata: +/// - the generated id; +/// - the datetime of its creation. +/// +/// Make the necessary changes without touching the types of the inputs and the returned +/// objects in our methods! +/// You can make inputs mutable, if needed. +impl TicketStore { + pub fn new() -> TicketStore { + TicketStore { + data: HashMap::new(), + current_id: 0, } + } - pub fn save(&mut self, ticket: Ticket) -> TicketId { - let id = self.generate_id(); - self.data.insert(id, ticket); - id - } + pub fn save(&mut self, ticket: Ticket) -> TicketId { + let id = self.generate_id(); + self.data.insert(id, ticket); + id + } - pub fn get(&self, id: &TicketId) -> Option<&Ticket> { - self.data.get(id) - } + pub fn get(&self, id: &TicketId) -> Option<&Ticket> { + self.data.get(id) + } - fn generate_id(&mut self) -> TicketId { - self.current_id += 1; - self.current_id - } + fn generate_id(&mut self) -> TicketId { + self.current_id += 1; + self.current_id } +} - #[derive(Debug, Clone, PartialEq)] - pub struct Ticket { - title: String, - description: String, - status: Status, - } +#[derive(Debug, Clone, PartialEq)] +pub struct Ticket { + title: String, + description: String, + status: Status, +} - impl Ticket { - pub fn title(&self) -> &String { - &self.title - } +impl Ticket { + pub fn title(&self) -> &String { + &self.title + } - pub fn description(&self) -> &String { - &self.description - } + pub fn description(&self) -> &String { + &self.description + } - pub fn status(&self) -> &Status { - &self.status - } + pub fn status(&self) -> &Status { + &self.status + } - // The datetime when the ticket was saved in the store, if it was saved. - pub fn created_at(&self) -> __ { - todo!() - } + // The datetime when the ticket was saved in the store, if it was saved. + pub fn created_at(&self) -> __ { + todo!() + } - // The id associated with the ticket when it was saved in the store, if it was saved. - pub fn id(&self) -> __ { - todo!() - } - } + // The id associated with the ticket when it was saved in the store, if it was saved. + pub fn id(&self) -> __ { + todo!() + } +} - pub fn create_ticket(title: String, description: String, status: Status) -> Ticket { - if title.is_empty() { - panic!("Title cannot be empty!"); - } - if title.len() > 50 { - panic!("A title cannot be longer than 50 characters!"); - } - if description.len() > 3000 { - panic!("A description cannot be longer than 3000 characters!"); - } +pub fn create_ticket(title: String, description: String, status: Status) -> Ticket { + if title.is_empty() { + panic!("Title cannot be empty!"); + } + if title.len() > 50 { + panic!("A title cannot be longer than 50 characters!"); + } + if description.len() > 3000 { + panic!("A description cannot be longer than 3000 characters!"); + } - Ticket { - title, - description, - status, - } + Ticket { + title, + description, + status, } +} - #[cfg(test)] - mod tests { - use super::*; - use fake::{Fake, Faker}; +#[cfg(test)] +mod tests { + use super::*; + use fake::{Fake, Faker}; - #[test] - fn ticket_creation() { - let ticket = generate_ticket(Status::ToDo); + #[test] + fn ticket_creation() { + let ticket = generate_ticket(Status::ToDo); - assert!(ticket.id().is_none()); - assert!(ticket.created_at().is_none()); - } + assert!(ticket.id().is_none()); + assert!(ticket.created_at().is_none()); + } - #[test] - fn a_ticket_with_a_home() { - let ticket = generate_ticket(Status::ToDo); - let mut store = TicketStore::new(); + #[test] + fn a_ticket_with_a_home() { + let ticket = generate_ticket(Status::ToDo); + let mut store = TicketStore::new(); - let ticket_id = store.save(ticket.clone()); - let retrieved_ticket = store.get(&ticket_id).unwrap(); + let ticket_id = store.save(ticket.clone()); + let retrieved_ticket = store.get(&ticket_id).unwrap(); - assert_eq!(Some(&ticket_id), retrieved_ticket.id()); - assert_eq!(&ticket.title, retrieved_ticket.title()); - assert_eq!(&ticket.description, retrieved_ticket.description()); - assert_eq!(&ticket.status, retrieved_ticket.status()); - assert!(retrieved_ticket.created_at().is_some()); - } + assert_eq!(Some(&ticket_id), retrieved_ticket.id()); + assert_eq!(&ticket.title, retrieved_ticket.title()); + assert_eq!(&ticket.description, retrieved_ticket.description()); + assert_eq!(&ticket.status, retrieved_ticket.status()); + assert!(retrieved_ticket.created_at().is_some()); + } - #[test] - fn a_missing_ticket() { - let ticket_store = TicketStore::new(); - let ticket_id = Faker.fake(); + #[test] + fn a_missing_ticket() { + let ticket_store = TicketStore::new(); + let ticket_id = Faker.fake(); - assert_eq!(ticket_store.get(&ticket_id), None); - } + assert_eq!(ticket_store.get(&ticket_id), None); + } - #[test] - fn id_generation_is_monotonic() { - let n_tickets = 100; - let mut store = TicketStore::new(); + #[test] + fn id_generation_is_monotonic() { + let n_tickets = 100; + let mut store = TicketStore::new(); - for expected_id in 1..n_tickets { - let ticket = generate_ticket(Status::ToDo); - let ticket_id = store.save(ticket); - assert_eq!(expected_id, ticket_id); - } + for expected_id in 1..n_tickets { + let ticket = generate_ticket(Status::ToDo); + let ticket_id = store.save(ticket); + assert_eq!(expected_id, ticket_id); } + } - fn generate_ticket(status: Status) -> Ticket { - let description = (0..3000).fake(); - let title = (1..50).fake(); + fn generate_ticket(status: Status) -> Ticket { + let description = (0..3000).fake(); + let title = (1..50).fake(); - create_ticket(title, description, status) - } + create_ticket(title, description, status) } } diff --git a/jira-wip/src/koans/02_ticket_store/05_type_as_constraints.rs b/jira-wip/src/koans/02_ticket_store/05_type_as_constraints.rs index 4e1cf00..f2e312f 100644 --- a/jira-wip/src/koans/02_ticket_store/05_type_as_constraints.rs +++ b/jira-wip/src/koans/02_ticket_store/05_type_as_constraints.rs @@ -1,168 +1,165 @@ -mod type_as_constraints { - use std::collections::HashMap; - use chrono::{DateTime, Utc}; - use super::recap::Status; - use super::id_generation::TicketId; - - /// We know that id and creation time will never be there before a ticket is saved, - /// while they will always be populated after `save` has been called. - /// - /// The approach we followed in the previous koan has its limitations: every time we - /// access `id` and `created_at` we need to keep track of the "life stage" of our ticket. - /// Has it been saved yet? Is it safe to unwrap those `Option`s? - /// That is unnecessary cognitive load and leads to errors down the line, - /// when writing new code or refactoring existing functionality. - /// - /// We can do better. - /// We can use types to better model our domain and constrain the behaviour of our code. - /// - /// Before `TicketStore::save` is called, we are dealing with a `TicketDraft`. - /// No `created_at`, no `id`, no `status`. - /// On the other side, `TicketStore::get` will return a `Ticket`, with a `created_at` and - /// an `id`. - /// - /// There will be no way to create a `Ticket` without passing through the store: - /// we will enforce `save` as the only way to produce a `Ticket` from a `TicketDraft`. - /// This will ensure as well that all tickets start in a `ToDo` status. - /// - /// Less room for errors, less ambiguity, you can understand the domain constraints - /// by looking at the signatures of the functions in our code. - /// - /// On the topic of type-driven development, checkout: - /// - https://fsharpforfunandprofit.com/series/designing-with-types.html - /// - https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ - /// - https://www.youtube.com/watch?v=PLFl95c-IiU - /// - - #[derive(Debug, Clone, PartialEq)] - pub struct TicketDraft { - __ - } - - #[derive(Debug, Clone, PartialEq)] - pub struct Ticket { - __ +use std::collections::HashMap; +use chrono::{DateTime, Utc}; +use super::recap::Status; +use super::id_generation::TicketId; + +/// We know that id and creation time will never be there before a ticket is saved, +/// while they will always be populated after `save` has been called. +/// +/// The approach we followed in the previous koan has its limitations: every time we +/// access `id` and `created_at` we need to keep track of the "life stage" of our ticket. +/// Has it been saved yet? Is it safe to unwrap those `Option`s? +/// That is unnecessary cognitive load and leads to errors down the line, +/// when writing new code or refactoring existing functionality. +/// +/// We can do better. +/// We can use types to better model our domain and constrain the behaviour of our code. +/// +/// Before `TicketStore::save` is called, we are dealing with a `TicketDraft`. +/// No `created_at`, no `id`, no `status`. +/// On the other side, `TicketStore::get` will return a `Ticket`, with a `created_at` and +/// an `id`. +/// +/// There will be no way to create a `Ticket` without passing through the store: +/// we will enforce `save` as the only way to produce a `Ticket` from a `TicketDraft`. +/// This will ensure as well that all tickets start in a `ToDo` status. +/// +/// Less room for errors, less ambiguity, you can understand the domain constraints +/// by looking at the signatures of the functions in our code. +/// +/// On the topic of type-driven development, checkout: +/// - https://fsharpforfunandprofit.com/series/designing-with-types.html +/// - https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ +/// - https://www.youtube.com/watch?v=PLFl95c-IiU +/// +#[derive(Debug, Clone, PartialEq)] +pub struct TicketDraft { + __ +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Ticket { + __ +} + +struct TicketStore { + data: HashMap, + current_id: TicketId, +} + +impl TicketStore { + pub fn new() -> TicketStore + { + TicketStore { + data: HashMap::new(), + current_id: 0, + } } - struct TicketStore { - data: HashMap, - current_id: TicketId, + pub fn save(&mut self, draft: TicketDraft) -> TicketId + { + let id = self.generate_id(); + + // We can use the "raw" constructor for `Ticket` here because the + // store is defined in the same module of `Ticket`. + // If you are importing `Ticket` from another module, + // `TicketStore::get` will indeed be the only way to get your hands on + // an instance of `Ticket`. + // This enforces our desired invariant: saving a draft in the store + // is the only way to "create" a `Ticket`. + let ticket = Ticket { + + }; + self.data.insert(id, ticket); + id } - impl TicketStore { - pub fn new() -> TicketStore - { - TicketStore { - data: HashMap::new(), - current_id: 0, - } - } + pub fn get(&self, id: &TicketId) -> Option<&Ticket> { + self.data.get(id) + } - pub fn save(&mut self, draft: TicketDraft) -> TicketId - { - let id = self.generate_id(); - - // We can use the "raw" constructor for `Ticket` here because the - // store is defined in the same module of `Ticket`. - // If you are importing `Ticket` from another module, - // `TicketStore::get` will indeed be the only way to get your hands on - // an instance of `Ticket`. - // This enforces our desired invariant: saving a draft in the store - // is the only way to "create" a `Ticket`. - let ticket = Ticket { - - }; - self.data.insert(id, ticket); - id - } - - pub fn get(&self, id: &TicketId) -> Option<&Ticket> { - self.data.get(id) - } - - fn generate_id(&mut self) -> TicketId { - self.current_id += 1; - self.current_id - } + fn generate_id(&mut self) -> TicketId { + self.current_id += 1; + self.current_id } - - impl TicketDraft { - pub fn title(&self) -> &String { todo!() } - pub fn description(&self) -> &String { todo!() } +} + +impl TicketDraft { + pub fn title(&self) -> &String { todo!() } + pub fn description(&self) -> &String { todo!() } +} + +impl Ticket { + pub fn title(&self) -> &String { todo!() } + pub fn description(&self) -> &String { todo!() } + pub fn status(&self) -> &Status { todo!() } + pub fn created_at(&self) -> &DateTime { todo!() } + pub fn id(&self) -> &TicketId { todo!() } +} + +pub fn create_ticket_draft(title: String, description: String) -> TicketDraft { + if title.is_empty() { + panic!("Title cannot be empty!"); + } + if title.len() > 50 { + panic!("A title cannot be longer than 50 characters!"); + } + if description.len() > 3000 { + panic!("A description cannot be longer than 3000 characters!"); } - impl Ticket { - pub fn title(&self) -> &String { todo!() } - pub fn description(&self) -> &String { todo!() } - pub fn status(&self) -> &Status { todo!() } - pub fn created_at(&self) -> &DateTime { todo!() } - pub fn id(&self) -> &TicketId { todo!() } + TicketDraft { + title, + description, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use fake::{Faker, Fake}; + + #[test] + fn a_ticket_with_a_home() + { + let draft = generate_ticket_draft(); + let mut store = TicketStore::new(); + + let ticket_id = store.save(draft.clone()); + let retrieved_ticket = store.get(&ticket_id).unwrap(); + + assert_eq!(&ticket_id, retrieved_ticket.id()); + assert_eq!(&draft.title, retrieved_ticket.title()); + assert_eq!(&draft.description, retrieved_ticket.description()); + assert_eq!(&Status::ToDo, retrieved_ticket.status()); } - pub fn create_ticket_draft(title: String, description: String) -> TicketDraft { - if title.is_empty() { - panic!("Title cannot be empty!"); - } - if title.len() > 50 { - panic!("A title cannot be longer than 50 characters!"); - } - if description.len() > 3000 { - panic!("A description cannot be longer than 3000 characters!"); - } + #[test] + fn a_missing_ticket() + { + let ticket_store = TicketStore::new(); + let ticket_id = Faker.fake(); - TicketDraft { - title, - description, - } + assert_eq!(ticket_store.get(&ticket_id), None); } - #[cfg(test)] - mod tests { - use super::*; - use fake::{Faker, Fake}; + #[test] + fn id_generation_is_monotonic() + { + let n_tickets = 100; + let mut store = TicketStore::new(); - #[test] - fn a_ticket_with_a_home() - { + for expected_id in 1..n_tickets { let draft = generate_ticket_draft(); - let mut store = TicketStore::new(); - - let ticket_id = store.save(draft.clone()); - let retrieved_ticket = store.get(&ticket_id).unwrap(); - - assert_eq!(&ticket_id, retrieved_ticket.id()); - assert_eq!(&draft.title, retrieved_ticket.title()); - assert_eq!(&draft.description, retrieved_ticket.description()); - assert_eq!(&Status::ToDo, retrieved_ticket.status()); - } - - #[test] - fn a_missing_ticket() - { - let ticket_store = TicketStore::new(); - let ticket_id = Faker.fake(); - - assert_eq!(ticket_store.get(&ticket_id), None); - } - - #[test] - fn id_generation_is_monotonic() - { - let n_tickets = 100; - let mut store = TicketStore::new(); - - for expected_id in 1..n_tickets { - let draft = generate_ticket_draft(); - let ticket_id = store.save(draft); - assert_eq!(expected_id, ticket_id); - } + let ticket_id = store.save(draft); + assert_eq!(expected_id, ticket_id); } + } - fn generate_ticket_draft() -> TicketDraft { - let description = (0..3000).fake(); - let title = (1..50).fake(); + fn generate_ticket_draft() -> TicketDraft { + let description = (0..3000).fake(); + let title = (1..50).fake(); - create_ticket_draft(title, description) - } + create_ticket_draft(title, description) } -} \ No newline at end of file +} diff --git a/jira-wip/src/koans/02_ticket_store/06_result.rs b/jira-wip/src/koans/02_ticket_store/06_result.rs index 62357d6..8bbe7b9 100644 --- a/jira-wip/src/koans/02_ticket_store/06_result.rs +++ b/jira-wip/src/koans/02_ticket_store/06_result.rs @@ -1,233 +1,231 @@ -mod result { - use super::id_generation::TicketId; - use super::recap::Status; - use chrono::{DateTime, Utc}; - use std::collections::HashMap; - use std::error::Error; - - /// The structure of our code is coming along quite nicely: it looks and feels like idiomatic - /// Rust and it models appropriately the domain we are tackling, JIRA. - /// - /// There is still something we can improve though: our validation logic when creating a new - /// draft. - /// Our previous function, `create_ticket_draft`, panicked when either the title or - /// the description failed our validation checks. - /// The caller has no idea that this can happen - the function signature looks quite innocent: - /// ``` - /// pub fn create_ticket_draft(title: String, description: String, status: Status) -> TicketDraft - /// ``` - /// Panics are generally not "caught" by the caller: they are meant to be used for states - /// that your program cannot recover from. - /// - /// For expected error scenarios, we can do a better job using `Result`: - /// ``` - /// pub fn create_ticket_draft(title: String, description: String, status: Status) -> Result - /// ``` - /// `Result` is an enum defined in the standard library, just like `Option`. - /// While `Option` encodes the possibility that some data might be missing, `Result` - /// encodes the idea that an operation can fail. - /// - /// Its definition looks something like this: - /// ``` - /// pub enum Result { - /// Ok(T), - /// Err(E) - /// } - /// ``` - /// The `Ok` variant is used to return the outcome of the function if its execution was successful. - /// The `Err` variant is used to return an error describing what went wrong. - /// - /// The error type, `E`, has to implement the `Error` trait from the standard library. - /// Let's archive our old `create_ticket_draft` function and let's define a new - /// `TicketDraft::new` method returning a `Result` to better set expectations with the caller. - #[derive(Debug, Clone, PartialEq)] - pub struct TicketDraft { - title: String, - description: String, - } +use super::id_generation::TicketId; +use super::recap::Status; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; +use std::error::Error; + +/// The structure of our code is coming along quite nicely: it looks and feels like idiomatic +/// Rust and it models appropriately the domain we are tackling, JIRA. +/// +/// There is still something we can improve though: our validation logic when creating a new +/// draft. +/// Our previous function, `create_ticket_draft`, panicked when either the title or +/// the description failed our validation checks. +/// The caller has no idea that this can happen - the function signature looks quite innocent: +/// ``` +/// pub fn create_ticket_draft(title: String, description: String, status: Status) -> TicketDraft +/// ``` +/// Panics are generally not "caught" by the caller: they are meant to be used for states +/// that your program cannot recover from. +/// +/// For expected error scenarios, we can do a better job using `Result`: +/// ``` +/// pub fn create_ticket_draft(title: String, description: String, status: Status) -> Result +/// ``` +/// `Result` is an enum defined in the standard library, just like `Option`. +/// While `Option` encodes the possibility that some data might be missing, `Result` +/// encodes the idea that an operation can fail. +/// +/// Its definition looks something like this: +/// ``` +/// pub enum Result { +/// Ok(T), +/// Err(E) +/// } +/// ``` +/// The `Ok` variant is used to return the outcome of the function if its execution was successful. +/// The `Err` variant is used to return an error describing what went wrong. +/// +/// The error type, `E`, has to implement the `Error` trait from the standard library. +/// Let's archive our old `create_ticket_draft` function and let's define a new +/// `TicketDraft::new` method returning a `Result` to better set expectations with the caller. +#[derive(Debug, Clone, PartialEq)] +pub struct TicketDraft { + title: String, + description: String, +} - impl TicketDraft { - pub fn title(&self) -> &String { - &self.title - } - pub fn description(&self) -> &String { - &self.description - } +impl TicketDraft { + pub fn title(&self) -> &String { + &self.title + } + pub fn description(&self) -> &String { + &self.description + } - pub fn new(title: String, description: String) -> Result { - if title.is_empty() { - return Err(ValidationError("Title cannot be empty!".to_string())); - } - if title.len() > 50 { - todo!() - } - if description.len() > 3000 { - todo!() - } - - let draft = TicketDraft { title, description }; - Ok(draft) + pub fn new(title: String, description: String) -> Result { + if title.is_empty() { + return Err(ValidationError("Title cannot be empty!".to_string())); } - } - - /// Our error struct, to be returned when validation fails. - /// It's a wrapper around a string, the validation error message. - /// Structs without field names are called tuple structs, you can read more about them - /// in the Rust book: - /// https://doc.rust-lang.org/book/ch05-01-defining-structs.html#using-tuple-structs-without-named-fields-to-create-different-types - #[derive(PartialEq, Debug, Clone)] - pub struct ValidationError(String); - - /// To use `ValidationError` as the `Err` variant in a `Result` we need to implement - /// the `Error` trait. - /// - /// The `Error` trait requires that our struct implements the `Debug` and `Display` traits, - /// because errors might be bubbled up all the way until they are shown to the end user. - /// We can derive `Debug`, but `Display` has to be implemented explicitly: - /// `Display` rules how your struct is printed out for user-facing input, hence it cannot be - /// derived automatically. - impl Error for ValidationError {} - - impl std::fmt::Display for ValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) + if title.len() > 50 { + todo!() + } + if description.len() > 3000 { + todo!() } - } - #[derive(Debug, Clone, PartialEq)] - pub struct Ticket { - id: TicketId, - title: String, - description: String, - status: Status, - created_at: DateTime, + let draft = TicketDraft { title, description }; + Ok(draft) } +} - struct TicketStore { - data: HashMap, - current_id: TicketId, - } +/// Our error struct, to be returned when validation fails. +/// It's a wrapper around a string, the validation error message. +/// Structs without field names are called tuple structs, you can read more about them +/// in the Rust book: +/// https://doc.rust-lang.org/book/ch05-01-defining-structs.html#using-tuple-structs-without-named-fields-to-create-different-types +#[derive(PartialEq, Debug, Clone)] +pub struct ValidationError(String); + +/// To use `ValidationError` as the `Err` variant in a `Result` we need to implement +/// the `Error` trait. +/// +/// The `Error` trait requires that our struct implements the `Debug` and `Display` traits, +/// because errors might be bubbled up all the way until they are shown to the end user. +/// We can derive `Debug`, but `Display` has to be implemented explicitly: +/// `Display` rules how your struct is printed out for user-facing input, hence it cannot be +/// derived automatically. +impl Error for ValidationError {} + +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} - impl TicketStore { - pub fn new() -> TicketStore { - TicketStore { - data: HashMap::new(), - current_id: 0, - } - } +#[derive(Debug, Clone, PartialEq)] +pub struct Ticket { + id: TicketId, + title: String, + description: String, + status: Status, + created_at: DateTime, +} - pub fn save(&mut self, draft: TicketDraft) -> TicketId { - let id = self.generate_id(); - let ticket = Ticket { - id, - title: draft.title, - description: draft.description, - status: Status::ToDo, - created_at: Utc::now(), - }; - self.data.insert(id, ticket); - id - } +struct TicketStore { + data: HashMap, + current_id: TicketId, +} - pub fn get(&self, id: &TicketId) -> Option<&Ticket> { - self.data.get(id) +impl TicketStore { + pub fn new() -> TicketStore { + TicketStore { + data: HashMap::new(), + current_id: 0, } + } - fn generate_id(&mut self) -> TicketId { - self.current_id += 1; - self.current_id - } + pub fn save(&mut self, draft: TicketDraft) -> TicketId { + let id = self.generate_id(); + let ticket = Ticket { + id, + title: draft.title, + description: draft.description, + status: Status::ToDo, + created_at: Utc::now(), + }; + self.data.insert(id, ticket); + id } - impl Ticket { - pub fn title(&self) -> &String { - &self.title - } - pub fn description(&self) -> &String { - &self.description - } - pub fn status(&self) -> &Status { - &self.status - } - pub fn created_at(&self) -> &DateTime { - &self.created_at - } - pub fn id(&self) -> &TicketId { - &self.id - } + pub fn get(&self, id: &TicketId) -> Option<&Ticket> { + self.data.get(id) + } + + fn generate_id(&mut self) -> TicketId { + self.current_id += 1; + self.current_id } +} - #[cfg(test)] - mod tests { - use super::*; - use fake::{Fake, Faker}; +impl Ticket { + pub fn title(&self) -> &String { + &self.title + } + pub fn description(&self) -> &String { + &self.description + } + pub fn status(&self) -> &Status { + &self.status + } + pub fn created_at(&self) -> &DateTime { + &self.created_at + } + pub fn id(&self) -> &TicketId { + &self.id + } +} - #[test] - fn title_cannot_be_empty() { - let description = (0..3000).fake(); +#[cfg(test)] +mod tests { + use super::*; + use fake::{Fake, Faker}; - let result = TicketDraft::new("".into(), description); - assert!(result.is_err()) - } + #[test] + fn title_cannot_be_empty() { + let description = (0..3000).fake(); - #[test] - fn title_cannot_be_longer_than_fifty_chars() { - let description = (0..3000).fake(); - // Let's generate a title longer than 51 chars. - let title = (51..10_000).fake(); + let result = TicketDraft::new("".into(), description); + assert!(result.is_err()) + } - let result = TicketDraft::new(title, description); - assert!(result.is_err()) - } + #[test] + fn title_cannot_be_longer_than_fifty_chars() { + let description = (0..3000).fake(); + // Let's generate a title longer than 51 chars. + let title = (51..10_000).fake(); - #[test] - fn description_cannot_be_longer_than_3000_chars() { - let description = (3001..10_000).fake(); - let title = (1..50).fake(); + let result = TicketDraft::new(title, description); + assert!(result.is_err()) + } - let result = TicketDraft::new(title, description); - assert!(result.is_err()) - } + #[test] + fn description_cannot_be_longer_than_3000_chars() { + let description = (3001..10_000).fake(); + let title = (1..50).fake(); - #[test] - fn a_ticket_with_a_home() { - let draft = generate_ticket_draft(); - let mut store = TicketStore::new(); + let result = TicketDraft::new(title, description); + assert!(result.is_err()) + } - let ticket_id = store.save(draft.clone()); - let retrieved_ticket = store.get(&ticket_id).unwrap(); + #[test] + fn a_ticket_with_a_home() { + let draft = generate_ticket_draft(); + let mut store = TicketStore::new(); - assert_eq!(&ticket_id, retrieved_ticket.id()); - assert_eq!(&draft.title, retrieved_ticket.title()); - assert_eq!(&draft.description, retrieved_ticket.description()); - assert_eq!(&Status::ToDo, retrieved_ticket.status()); - } + let ticket_id = store.save(draft.clone()); + let retrieved_ticket = store.get(&ticket_id).unwrap(); - #[test] - fn a_missing_ticket() { - let ticket_store = TicketStore::new(); - let ticket_id = Faker.fake(); + assert_eq!(&ticket_id, retrieved_ticket.id()); + assert_eq!(&draft.title, retrieved_ticket.title()); + assert_eq!(&draft.description, retrieved_ticket.description()); + assert_eq!(&Status::ToDo, retrieved_ticket.status()); + } - assert_eq!(ticket_store.get(&ticket_id), None); - } + #[test] + fn a_missing_ticket() { + let ticket_store = TicketStore::new(); + let ticket_id = Faker.fake(); - #[test] - fn id_generation_is_monotonic() { - let n_tickets = 100; - let mut store = TicketStore::new(); + assert_eq!(ticket_store.get(&ticket_id), None); + } + + #[test] + fn id_generation_is_monotonic() { + let n_tickets = 100; + let mut store = TicketStore::new(); - for expected_id in 1..n_tickets { - let draft = generate_ticket_draft(); - let ticket_id = store.save(draft); - assert_eq!(expected_id, ticket_id); - } + for expected_id in 1..n_tickets { + let draft = generate_ticket_draft(); + let ticket_id = store.save(draft); + assert_eq!(expected_id, ticket_id); } + } - fn generate_ticket_draft() -> TicketDraft { - let description = (0..3000).fake(); - let title = (1..50).fake(); + fn generate_ticket_draft() -> TicketDraft { + let description = (0..3000).fake(); + let title = (1..50).fake(); - TicketDraft::new(title, description).expect("Failed to create ticket") - } + TicketDraft::new(title, description).expect("Failed to create ticket") } } diff --git a/jira-wip/src/koans/02_ticket_store/07_vec.rs b/jira-wip/src/koans/02_ticket_store/07_vec.rs index 0eb6da2..7f408d9 100644 --- a/jira-wip/src/koans/02_ticket_store/07_vec.rs +++ b/jira-wip/src/koans/02_ticket_store/07_vec.rs @@ -1,231 +1,229 @@ -mod vec { - use super::id_generation::TicketId; - use super::recap::Status; - use chrono::{DateTime, Utc}; - use std::collections::HashMap; - use std::error::Error; - - /// Let's turn our attention again to our `TicketStore`. - /// We can create a ticket, we can retrieve a ticket. - /// - /// Let's implement a `list` method to retrieve all tickets currently in the store. - struct TicketStore { - data: HashMap, - current_id: TicketId, - } +use super::id_generation::TicketId; +use super::recap::Status; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; +use std::error::Error; + +/// Let's turn our attention again to our `TicketStore`. +/// We can create a ticket, we can retrieve a ticket. +/// +/// Let's implement a `list` method to retrieve all tickets currently in the store. +struct TicketStore { + data: HashMap, + current_id: TicketId, +} - impl TicketStore { - pub fn new() -> TicketStore { - TicketStore { - data: HashMap::new(), - current_id: 0, - } +impl TicketStore { + pub fn new() -> TicketStore { + TicketStore { + data: HashMap::new(), + current_id: 0, } + } - pub fn save(&mut self, draft: TicketDraft) -> TicketId { - let id = self.generate_id(); - let ticket = Ticket { - id, - title: draft.title, - description: draft.description, - status: Status::ToDo, - created_at: Utc::now(), - }; - self.data.insert(id, ticket); - id - } + pub fn save(&mut self, draft: TicketDraft) -> TicketId { + let id = self.generate_id(); + let ticket = Ticket { + id, + title: draft.title, + description: draft.description, + status: Status::ToDo, + created_at: Utc::now(), + }; + self.data.insert(id, ticket); + id + } - pub fn get(&self, id: &TicketId) -> Option<&Ticket> { - self.data.get(id) - } + pub fn get(&self, id: &TicketId) -> Option<&Ticket> { + self.data.get(id) + } - /// List will return a `Vec`. - /// Check the Rust book for a primer: https://doc.rust-lang.org/book/ch08-01-vectors.html - /// The Rust documentation for `HashMap` will also be handy: - /// https://doc.rust-lang.org/std/collections/struct.HashMap.html - pub fn list(&self) -> Vec<&Ticket> { - todo!() - } - - fn generate_id(&mut self) -> TicketId { - self.current_id += 1; - self.current_id - } + /// List will return a `Vec`. + /// Check the Rust book for a primer: https://doc.rust-lang.org/book/ch08-01-vectors.html + /// The Rust documentation for `HashMap` will also be handy: + /// https://doc.rust-lang.org/std/collections/struct.HashMap.html + pub fn list(&self) -> Vec<&Ticket> { + todo!() } - #[derive(Debug, Clone, PartialEq)] - pub struct TicketDraft { - title: String, - description: String, + fn generate_id(&mut self) -> TicketId { + self.current_id += 1; + self.current_id } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TicketDraft { + title: String, + description: String, +} + +impl TicketDraft { + pub fn title(&self) -> &String { + &self.title + } + pub fn description(&self) -> &String { + &self.description + } - impl TicketDraft { - pub fn title(&self) -> &String { - &self.title + pub fn new(title: String, description: String) -> Result { + if title.is_empty() { + return Err(ValidationError("Title cannot be empty!".to_string())); } - pub fn description(&self) -> &String { - &self.description + if title.len() > 50 { + return Err(ValidationError( + "A title cannot be longer than 50 characters!".to_string(), + )); } - - pub fn new(title: String, description: String) -> Result { - if title.is_empty() { - return Err(ValidationError("Title cannot be empty!".to_string())); - } - if title.len() > 50 { - return Err(ValidationError( - "A title cannot be longer than 50 characters!".to_string(), - )); - } - if description.len() > 3000 { - return Err(ValidationError( - "A description cannot be longer than 3000 characters!".to_string(), - )); - } - - let draft = TicketDraft { title, description }; - Ok(draft) + if description.len() > 3000 { + return Err(ValidationError( + "A description cannot be longer than 3000 characters!".to_string(), + )); } + + let draft = TicketDraft { title, description }; + Ok(draft) } +} - #[derive(PartialEq, Debug, Clone)] - pub struct ValidationError(String); +#[derive(PartialEq, Debug, Clone)] +pub struct ValidationError(String); - impl Error for ValidationError {} +impl Error for ValidationError {} - impl std::fmt::Display for ValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } - } - - #[derive(Debug, Clone, PartialEq)] - pub struct Ticket { - id: TicketId, - title: String, - description: String, - status: Status, - created_at: DateTime, - } +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} - impl Ticket { - pub fn title(&self) -> &String { - &self.title - } - pub fn description(&self) -> &String { - &self.description - } - pub fn status(&self) -> &Status { - &self.status - } - pub fn created_at(&self) -> &DateTime { - &self.created_at - } - pub fn id(&self) -> &TicketId { - &self.id - } - } +#[derive(Debug, Clone, PartialEq)] +pub struct Ticket { + id: TicketId, + title: String, + description: String, + status: Status, + created_at: DateTime, +} - #[cfg(test)] - mod tests { - use super::*; - use fake::{Fake, Faker}; +impl Ticket { + pub fn title(&self) -> &String { + &self.title + } + pub fn description(&self) -> &String { + &self.description + } + pub fn status(&self) -> &Status { + &self.status + } + pub fn created_at(&self) -> &DateTime { + &self.created_at + } + pub fn id(&self) -> &TicketId { + &self.id + } +} - #[test] - fn list_returns_all_tickets() { - let n_tickets = 100; - let mut store = TicketStore::new(); +#[cfg(test)] +mod tests { + use super::*; + use fake::{Fake, Faker}; - for _ in 0..n_tickets { - let draft = generate_ticket_draft(); - store.save(draft); - } + #[test] + fn list_returns_all_tickets() { + let n_tickets = 100; + let mut store = TicketStore::new(); - assert_eq!(n_tickets, store.list().len()); + for _ in 0..n_tickets { + let draft = generate_ticket_draft(); + store.save(draft); } - #[test] - fn on_a_single_ticket_list_and_get_agree() { - let mut store = TicketStore::new(); + assert_eq!(n_tickets, store.list().len()); + } - let draft = generate_ticket_draft(); - let id = store.save(draft); + #[test] + fn on_a_single_ticket_list_and_get_agree() { + let mut store = TicketStore::new(); - assert_eq!(vec![store.get(&id).unwrap()], store.list()); - } + let draft = generate_ticket_draft(); + let id = store.save(draft); - #[test] - fn list_returns_an_empty_vec_on_an_empty_store() { - let store = TicketStore::new(); + assert_eq!(vec![store.get(&id).unwrap()], store.list()); + } - assert!(store.list().is_empty()); - } + #[test] + fn list_returns_an_empty_vec_on_an_empty_store() { + let store = TicketStore::new(); - #[test] - fn title_cannot_be_empty() { - let description = (0..3000).fake(); + assert!(store.list().is_empty()); + } - let result = TicketDraft::new("".into(), description); - assert!(result.is_err()) - } + #[test] + fn title_cannot_be_empty() { + let description = (0..3000).fake(); - #[test] - fn title_cannot_be_longer_than_fifty_chars() { - let description = (0..3000).fake(); - // Let's generate a title longer than 51 chars. - let title = (51..10_000).fake(); + let result = TicketDraft::new("".into(), description); + assert!(result.is_err()) + } - let result = TicketDraft::new(title, description); - assert!(result.is_err()) - } + #[test] + fn title_cannot_be_longer_than_fifty_chars() { + let description = (0..3000).fake(); + // Let's generate a title longer than 51 chars. + let title = (51..10_000).fake(); - #[test] - fn description_cannot_be_longer_than_3000_chars() { - let description = (3001..10_000).fake(); - let title = (1..50).fake(); + let result = TicketDraft::new(title, description); + assert!(result.is_err()) + } - let result = TicketDraft::new(title, description); - assert!(result.is_err()) - } + #[test] + fn description_cannot_be_longer_than_3000_chars() { + let description = (3001..10_000).fake(); + let title = (1..50).fake(); - #[test] - fn a_ticket_with_a_home() { - let draft = generate_ticket_draft(); - let mut store = TicketStore::new(); + let result = TicketDraft::new(title, description); + assert!(result.is_err()) + } - let ticket_id = store.save(draft.clone()); - let retrieved_ticket = store.get(&ticket_id).unwrap(); + #[test] + fn a_ticket_with_a_home() { + let draft = generate_ticket_draft(); + let mut store = TicketStore::new(); - assert_eq!(&ticket_id, retrieved_ticket.id()); - assert_eq!(&draft.title, retrieved_ticket.title()); - assert_eq!(&draft.description, retrieved_ticket.description()); - assert_eq!(&Status::ToDo, retrieved_ticket.status()); - } + let ticket_id = store.save(draft.clone()); + let retrieved_ticket = store.get(&ticket_id).unwrap(); + + assert_eq!(&ticket_id, retrieved_ticket.id()); + assert_eq!(&draft.title, retrieved_ticket.title()); + assert_eq!(&draft.description, retrieved_ticket.description()); + assert_eq!(&Status::ToDo, retrieved_ticket.status()); + } - #[test] - fn a_missing_ticket() { - let ticket_store = TicketStore::new(); - let ticket_id = Faker.fake(); + #[test] + fn a_missing_ticket() { + let ticket_store = TicketStore::new(); + let ticket_id = Faker.fake(); - assert_eq!(ticket_store.get(&ticket_id), None); - } + assert_eq!(ticket_store.get(&ticket_id), None); + } - #[test] - fn id_generation_is_monotonic() { - let n_tickets = 100; - let mut store = TicketStore::new(); + #[test] + fn id_generation_is_monotonic() { + let n_tickets = 100; + let mut store = TicketStore::new(); - for expected_id in 1..n_tickets { - let draft = generate_ticket_draft(); - let ticket_id = store.save(draft); - assert_eq!(expected_id, ticket_id); - } + for expected_id in 1..n_tickets { + let draft = generate_ticket_draft(); + let ticket_id = store.save(draft); + assert_eq!(expected_id, ticket_id); } + } - fn generate_ticket_draft() -> TicketDraft { - let description = (0..3000).fake(); - let title = (1..50).fake(); + fn generate_ticket_draft() -> TicketDraft { + let description = (0..3000).fake(); + let title = (1..50).fake(); - TicketDraft::new(title, description).expect("Failed to create ticket") - } + TicketDraft::new(title, description).expect("Failed to create ticket") } } diff --git a/jira-wip/src/koans/02_ticket_store/08_delete_and_update.rs b/jira-wip/src/koans/02_ticket_store/08_delete_and_update.rs index b56b327..b19de8e 100644 --- a/jira-wip/src/koans/02_ticket_store/08_delete_and_update.rs +++ b/jira-wip/src/koans/02_ticket_store/08_delete_and_update.rs @@ -1,390 +1,388 @@ -mod delete_and_update { - use super::id_generation::TicketId; - use super::recap::Status; - use chrono::{DateTime, Utc}; - use std::collections::HashMap; - use std::error::Error; - - /// There are only two pieces missing: deleting a ticket and updating a ticket - /// in our `TicketStore`. - /// The update functionality will give us the possibility to change the `status` of - /// a ticket, the holy grail of our JIRA clone. - struct TicketStore { - data: HashMap, - current_id: TicketId, - } - - impl TicketStore { - pub fn new() -> TicketStore { - TicketStore { - data: HashMap::new(), - current_id: 0, - } - } - - pub fn save(&mut self, draft: TicketDraft) -> TicketId { - let id = self.generate_id(); - let timestamp = Utc::now(); - let ticket = Ticket { - id, - title: draft.title, - description: draft.description, - status: Status::ToDo, - created_at: timestamp.clone(), - // A new field, to keep track of the last time a ticket has been touched. - // It starts in sync with `created_at`, it gets updated when a ticket is updated. - updated_at: timestamp, - }; - self.data.insert(id, ticket); - id - } +use super::id_generation::TicketId; +use super::recap::Status; +use chrono::{DateTime, Utc}; +use std::collections::HashMap; +use std::error::Error; + +/// There are only two pieces missing: deleting a ticket and updating a ticket +/// in our `TicketStore`. +/// The update functionality will give us the possibility to change the `status` of +/// a ticket, the holy grail of our JIRA clone. +struct TicketStore { + data: HashMap, + current_id: TicketId, +} - pub fn get(&self, id: &TicketId) -> Option<&Ticket> { - self.data.get(id) +impl TicketStore { + pub fn new() -> TicketStore { + TicketStore { + data: HashMap::new(), + current_id: 0, } + } - pub fn list(&self) -> Vec<&Ticket> { - self.data.values().collect() - } + pub fn save(&mut self, draft: TicketDraft) -> TicketId { + let id = self.generate_id(); + let timestamp = Utc::now(); + let ticket = Ticket { + id, + title: draft.title, + description: draft.description, + status: Status::ToDo, + created_at: timestamp.clone(), + // A new field, to keep track of the last time a ticket has been touched. + // It starts in sync with `created_at`, it gets updated when a ticket is updated. + updated_at: timestamp, + }; + self.data.insert(id, ticket); + id + } - /// We take in an `id` and a `patch` struct: this allows us to constrain which of the - /// fields in a `Ticket` can actually be updated. - /// For example, we don't want users to be able to update the `id` or - /// the `created_at` field. - /// - /// If we had chosen a different strategy, e.g. implementing a `get_mut` method - /// to retrieve a mutable reference to a ticket and give the caller the possibility to edit - /// it as they wanted, we wouldn't have been able to uphold the same guarantees. - /// - /// If the `id` passed in matches a ticket in the store, we return the edited ticket. - /// If it doesn't, we return `None`. - pub fn update(&mut self, id: &TicketId, patch: TicketPatch) -> Option<&Ticket> { - todo!() - } + pub fn get(&self, id: &TicketId) -> Option<&Ticket> { + self.data.get(id) + } - /// If the `id` passed in matches a ticket in the store, we return the deleted ticket - /// with some additional metadata. - /// If it doesn't, we return `None`. - pub fn delete(&mut self, id: &TicketId) -> Option { - todo!() - } + pub fn list(&self) -> Vec<&Ticket> { + self.data.values().collect() + } - fn generate_id(&mut self) -> TicketId { - self.current_id += 1; - self.current_id - } + /// We take in an `id` and a `patch` struct: this allows us to constrain which of the + /// fields in a `Ticket` can actually be updated. + /// For example, we don't want users to be able to update the `id` or + /// the `created_at` field. + /// + /// If we had chosen a different strategy, e.g. implementing a `get_mut` method + /// to retrieve a mutable reference to a ticket and give the caller the possibility to edit + /// it as they wanted, we wouldn't have been able to uphold the same guarantees. + /// + /// If the `id` passed in matches a ticket in the store, we return the edited ticket. + /// If it doesn't, we return `None`. + pub fn update(&mut self, id: &TicketId, patch: TicketPatch) -> Option<&Ticket> { + todo!() + } + + /// If the `id` passed in matches a ticket in the store, we return the deleted ticket + /// with some additional metadata. + /// If it doesn't, we return `None`. + pub fn delete(&mut self, id: &TicketId) -> Option { + todo!() + } + + fn generate_id(&mut self) -> TicketId { + self.current_id += 1; + self.current_id } +} - /// We don't want to relax our constraints on what is an acceptable title or an acceptable - /// description for a ticket. - /// This means that we need to validate the `title` and the `description` in our `TicketPatch` - /// using the same rules we use for our `TicketDraft`. - /// - /// To keep it DRY, we introduce two new types whose constructors guarantee the invariants - /// we care about. - #[derive(Debug, Clone, PartialEq)] - pub struct TicketTitle(String); - - impl TicketTitle { - pub fn new(title: String) -> Result { - if title.is_empty() { - return Err(ValidationError("Title cannot be empty!".to_string())); - } - if title.len() > 50 { - return Err(ValidationError( - "A title cannot be longer than 50 characters!".to_string(), - )); - } - Ok(Self(title)) - } +/// We don't want to relax our constraints on what is an acceptable title or an acceptable +/// description for a ticket. +/// This means that we need to validate the `title` and the `description` in our `TicketPatch` +/// using the same rules we use for our `TicketDraft`. +/// +/// To keep it DRY, we introduce two new types whose constructors guarantee the invariants +/// we care about. +#[derive(Debug, Clone, PartialEq)] +pub struct TicketTitle(String); + +impl TicketTitle { + pub fn new(title: String) -> Result { + if title.is_empty() { + return Err(ValidationError("Title cannot be empty!".to_string())); + } + if title.len() > 50 { + return Err(ValidationError( + "A title cannot be longer than 50 characters!".to_string(), + )); + } + Ok(Self(title)) } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct TicketDescription(String); - #[derive(Debug, Clone, PartialEq)] - pub struct TicketDescription(String); - - impl TicketDescription { - pub fn new(description: String) -> Result { - if description.len() > 3000 { - Err(ValidationError( - "A description cannot be longer than 3000 characters!".to_string(), - )) - } else { - Ok(Self(description)) - } +impl TicketDescription { + pub fn new(description: String) -> Result { + if description.len() > 3000 { + Err(ValidationError( + "A description cannot be longer than 3000 characters!".to_string(), + )) + } else { + Ok(Self(description)) } } +} - /// `TicketPatch` constrains the fields that we consider editable. - /// - /// If a field is set the `Some`, its value will be updated to the specified value. - /// If a field is set to `None`, the field remains unchanged. - #[derive(Debug, Clone, PartialEq)] - pub struct TicketPatch { - pub title: Option, - pub description: Option, - pub status: Option, - } +/// `TicketPatch` constrains the fields that we consider editable. +/// +/// If a field is set the `Some`, its value will be updated to the specified value. +/// If a field is set to `None`, the field remains unchanged. +#[derive(Debug, Clone, PartialEq)] +pub struct TicketPatch { + pub title: Option, + pub description: Option, + pub status: Option, +} - /// With validation baked in our types, we don't have to worry anymore about the visibility - /// of those fields. - /// Our `TicketPatch` and our `TicketDraft` don't have an identity, an id, like a `Ticket` - /// saved in the store. - /// They are value objects, not entities, to borrow some terminology from Domain Driven Design. - /// - /// As long as we know that our invariants are upheld, we can let the user modify them - /// as much as they please. - /// We can thus get rid of the constructor and all the accessor methods. Pretty sweet, uh? - #[derive(Debug, Clone, PartialEq)] - pub struct TicketDraft { - pub title: TicketTitle, - pub description: TicketDescription, - } +/// With validation baked in our types, we don't have to worry anymore about the visibility +/// of those fields. +/// Our `TicketPatch` and our `TicketDraft` don't have an identity, an id, like a `Ticket` +/// saved in the store. +/// They are value objects, not entities, to borrow some terminology from Domain Driven Design. +/// +/// As long as we know that our invariants are upheld, we can let the user modify them +/// as much as they please. +/// We can thus get rid of the constructor and all the accessor methods. Pretty sweet, uh? +#[derive(Debug, Clone, PartialEq)] +pub struct TicketDraft { + pub title: TicketTitle, + pub description: TicketDescription, +} - /// A light wrapper around a deleted ticket to store some metadata (the deletion timestamp). - /// If we had a user system in place, we would also store the identity of the user - /// who performed the deletion. - #[derive(Debug, Clone, PartialEq)] - pub struct DeletedTicket { - ticket: Ticket, - deleted_at: DateTime, - } +/// A light wrapper around a deleted ticket to store some metadata (the deletion timestamp). +/// If we had a user system in place, we would also store the identity of the user +/// who performed the deletion. +#[derive(Debug, Clone, PartialEq)] +pub struct DeletedTicket { + ticket: Ticket, + deleted_at: DateTime, +} - impl DeletedTicket { - pub fn ticket(&self) -> &Ticket { - &self.ticket - } - pub fn deleted_at(&self) -> &DateTime { - &self.deleted_at - } - } +impl DeletedTicket { + pub fn ticket(&self) -> &Ticket { + &self.ticket + } + pub fn deleted_at(&self) -> &DateTime { + &self.deleted_at + } +} - #[derive(PartialEq, Debug, Clone)] - pub struct ValidationError(String); +#[derive(PartialEq, Debug, Clone)] +pub struct ValidationError(String); - impl Error for ValidationError {} +impl Error for ValidationError {} - impl std::fmt::Display for ValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) - } - } +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} - #[derive(Debug, Clone, PartialEq)] - pub struct Ticket { - id: TicketId, - title: TicketTitle, - description: TicketDescription, - status: Status, - created_at: DateTime, - updated_at: DateTime, - } +#[derive(Debug, Clone, PartialEq)] +pub struct Ticket { + id: TicketId, + title: TicketTitle, + description: TicketDescription, + status: Status, + created_at: DateTime, + updated_at: DateTime, +} - impl Ticket { - pub fn title(&self) -> &TicketTitle { - &self.title - } - pub fn description(&self) -> &TicketDescription { - &self.description - } - pub fn status(&self) -> &Status { - &self.status - } - pub fn created_at(&self) -> &DateTime { - &self.created_at - } - pub fn id(&self) -> &TicketId { - &self.id - } - pub fn updated_at(&self) -> &DateTime { - &self.updated_at - } +impl Ticket { + pub fn title(&self) -> &TicketTitle { + &self.title + } + pub fn description(&self) -> &TicketDescription { + &self.description + } + pub fn status(&self) -> &Status { + &self.status + } + pub fn created_at(&self) -> &DateTime { + &self.created_at + } + pub fn id(&self) -> &TicketId { + &self.id + } + pub fn updated_at(&self) -> &DateTime { + &self.updated_at + } +} + +#[cfg(test)] +mod tests { + use super::*; + use fake::{Fake, Faker}; + use std::time::Duration; + + #[test] + fn updating_nothing_leaves_the_updatable_fields_unchanged() { + let mut store = TicketStore::new(); + let draft = generate_ticket_draft(); + let ticket_id = store.save(draft.clone()); + + let patch = TicketPatch { + title: None, + description: None, + status: None, + }; + let updated_ticket = store.update(&ticket_id, patch).unwrap(); + + assert_eq!(draft.title, updated_ticket.title); + assert_eq!(draft.description, updated_ticket.description); + assert_eq!(Status::ToDo, updated_ticket.status); } - #[cfg(test)] - mod tests { - use super::*; - use fake::{Fake, Faker}; - use std::time::Duration; + #[test] + fn trying_to_update_a_missing_ticket_returns_none() { + let mut store = TicketStore::new(); + let ticket_id = Faker.fake(); + let patch = generate_ticket_patch(Status::Done); - #[test] - fn updating_nothing_leaves_the_updatable_fields_unchanged() { - let mut store = TicketStore::new(); - let draft = generate_ticket_draft(); - let ticket_id = store.save(draft.clone()); - - let patch = TicketPatch { - title: None, - description: None, - status: None, - }; - let updated_ticket = store.update(&ticket_id, patch).unwrap(); - - assert_eq!(draft.title, updated_ticket.title); - assert_eq!(draft.description, updated_ticket.description); - assert_eq!(Status::ToDo, updated_ticket.status); - } + assert_eq!(store.update(&ticket_id, patch), None); + } - #[test] - fn trying_to_update_a_missing_ticket_returns_none() { - let mut store = TicketStore::new(); - let ticket_id = Faker.fake(); - let patch = generate_ticket_patch(Status::Done); + #[test] + fn update_works() { + let mut store = TicketStore::new(); + let draft = generate_ticket_draft(); + let patch = generate_ticket_patch(Status::Done); + let ticket_id = store.save(draft.clone()); + + // Let's wait a bit, otherwise `created_at` and `updated_at` + // might turn out identical (ᴗ˳ᴗ) + std::thread::sleep(Duration::from_millis(100)); + let updated_ticket = store.update(&ticket_id, patch.clone()).unwrap(); + + assert_eq!(patch.title.unwrap(), updated_ticket.title); + assert_eq!(patch.description.unwrap(), updated_ticket.description); + assert_eq!(patch.status.unwrap(), updated_ticket.status); + assert_ne!(updated_ticket.created_at(), updated_ticket.updated_at()); + } - assert_eq!(store.update(&ticket_id, patch), None); - } + #[test] + fn delete_works() { + let mut store = TicketStore::new(); + let draft = generate_ticket_draft(); + let ticket_id = store.save(draft.clone()); + let ticket = store.get(&ticket_id).unwrap().to_owned(); - #[test] - fn update_works() { - let mut store = TicketStore::new(); - let draft = generate_ticket_draft(); - let patch = generate_ticket_patch(Status::Done); - let ticket_id = store.save(draft.clone()); - - // Let's wait a bit, otherwise `created_at` and `updated_at` - // might turn out identical (ᴗ˳ᴗ) - std::thread::sleep(Duration::from_millis(100)); - let updated_ticket = store.update(&ticket_id, patch.clone()).unwrap(); - - assert_eq!(patch.title.unwrap(), updated_ticket.title); - assert_eq!(patch.description.unwrap(), updated_ticket.description); - assert_eq!(patch.status.unwrap(), updated_ticket.status); - assert_ne!(updated_ticket.created_at(), updated_ticket.updated_at()); - } + let deleted_ticket = store.delete(&ticket_id).unwrap(); - #[test] - fn delete_works() { - let mut store = TicketStore::new(); - let draft = generate_ticket_draft(); - let ticket_id = store.save(draft.clone()); - let ticket = store.get(&ticket_id).unwrap().to_owned(); + assert_eq!(deleted_ticket.ticket(), &ticket); + assert_eq!(store.get(&ticket_id), None); + } - let deleted_ticket = store.delete(&ticket_id).unwrap(); + #[test] + fn deleting_a_missing_ticket_returns_none() { + let mut store = TicketStore::new(); + let ticket_id = Faker.fake(); - assert_eq!(deleted_ticket.ticket(), &ticket); - assert_eq!(store.get(&ticket_id), None); - } + assert_eq!(store.delete(&ticket_id), None); + } - #[test] - fn deleting_a_missing_ticket_returns_none() { - let mut store = TicketStore::new(); - let ticket_id = Faker.fake(); + #[test] + fn list_returns_all_tickets() { + let n_tickets = 100; + let mut store = TicketStore::new(); - assert_eq!(store.delete(&ticket_id), None); + for _ in 0..n_tickets { + let draft = generate_ticket_draft(); + store.save(draft); } - #[test] - fn list_returns_all_tickets() { - let n_tickets = 100; - let mut store = TicketStore::new(); + assert_eq!(n_tickets, store.list().len()); + } - for _ in 0..n_tickets { - let draft = generate_ticket_draft(); - store.save(draft); - } + #[test] + fn on_a_single_ticket_list_and_get_agree() { + let mut store = TicketStore::new(); - assert_eq!(n_tickets, store.list().len()); - } + let draft = generate_ticket_draft(); + let id = store.save(draft); - #[test] - fn on_a_single_ticket_list_and_get_agree() { - let mut store = TicketStore::new(); + assert_eq!(vec![store.get(&id).unwrap()], store.list()); + } - let draft = generate_ticket_draft(); - let id = store.save(draft); + #[test] + fn list_returns_an_empty_vec_on_an_empty_store() { + let store = TicketStore::new(); - assert_eq!(vec![store.get(&id).unwrap()], store.list()); - } + assert!(store.list().is_empty()); + } - #[test] - fn list_returns_an_empty_vec_on_an_empty_store() { - let store = TicketStore::new(); + #[test] + fn title_cannot_be_empty() { + assert!(TicketTitle::new("".into()).is_err()) + } - assert!(store.list().is_empty()); - } + #[test] + fn title_cannot_be_longer_than_fifty_chars() { + // Let's generate a title longer than 51 chars. + let title = (51..10_000).fake(); - #[test] - fn title_cannot_be_empty() { - assert!(TicketTitle::new("".into()).is_err()) - } + assert!(TicketTitle::new(title).is_err()) + } - #[test] - fn title_cannot_be_longer_than_fifty_chars() { - // Let's generate a title longer than 51 chars. - let title = (51..10_000).fake(); + #[test] + fn description_cannot_be_longer_than_3000_chars() { + let description = (3001..10_000).fake(); - assert!(TicketTitle::new(title).is_err()) - } + assert!(TicketDescription::new(description).is_err()) + } - #[test] - fn description_cannot_be_longer_than_3000_chars() { - let description = (3001..10_000).fake(); + #[test] + fn a_ticket_with_a_home() { + let draft = generate_ticket_draft(); + let mut store = TicketStore::new(); - assert!(TicketDescription::new(description).is_err()) - } + let ticket_id = store.save(draft.clone()); + let retrieved_ticket = store.get(&ticket_id).unwrap(); - #[test] - fn a_ticket_with_a_home() { - let draft = generate_ticket_draft(); - let mut store = TicketStore::new(); + assert_eq!(&ticket_id, retrieved_ticket.id()); + assert_eq!(&draft.title, retrieved_ticket.title()); + assert_eq!(&draft.description, retrieved_ticket.description()); + assert_eq!(&Status::ToDo, retrieved_ticket.status()); + assert_eq!(retrieved_ticket.created_at(), retrieved_ticket.updated_at()); + } - let ticket_id = store.save(draft.clone()); - let retrieved_ticket = store.get(&ticket_id).unwrap(); + #[test] + fn a_missing_ticket() { + let ticket_store = TicketStore::new(); + let ticket_id = Faker.fake(); - assert_eq!(&ticket_id, retrieved_ticket.id()); - assert_eq!(&draft.title, retrieved_ticket.title()); - assert_eq!(&draft.description, retrieved_ticket.description()); - assert_eq!(&Status::ToDo, retrieved_ticket.status()); - assert_eq!(retrieved_ticket.created_at(), retrieved_ticket.updated_at()); - } + assert_eq!(ticket_store.get(&ticket_id), None); + } - #[test] - fn a_missing_ticket() { - let ticket_store = TicketStore::new(); - let ticket_id = Faker.fake(); + #[test] + fn id_generation_is_monotonic() { + let n_tickets = 100; + let mut store = TicketStore::new(); - assert_eq!(ticket_store.get(&ticket_id), None); + for expected_id in 1..n_tickets { + let draft = generate_ticket_draft(); + let ticket_id = store.save(draft); + assert_eq!(expected_id, ticket_id); } + } - #[test] - fn id_generation_is_monotonic() { - let n_tickets = 100; - let mut store = TicketStore::new(); - - for expected_id in 1..n_tickets { - let draft = generate_ticket_draft(); - let ticket_id = store.save(draft); - assert_eq!(expected_id, ticket_id); - } - } + #[test] + fn ids_are_not_reused() { + let n_tickets = 100; + let mut store = TicketStore::new(); - #[test] - fn ids_are_not_reused() { - let n_tickets = 100; - let mut store = TicketStore::new(); - - for expected_id in 1..n_tickets { - let draft = generate_ticket_draft(); - let ticket_id = store.save(draft); - assert_eq!(expected_id, ticket_id); - assert!(store.delete(&ticket_id).is_some()); - } + for expected_id in 1..n_tickets { + let draft = generate_ticket_draft(); + let ticket_id = store.save(draft); + assert_eq!(expected_id, ticket_id); + assert!(store.delete(&ticket_id).is_some()); } + } - fn generate_ticket_draft() -> TicketDraft { - let description = TicketDescription::new((0..3000).fake()).unwrap(); - let title = TicketTitle::new((1..50).fake()).unwrap(); + fn generate_ticket_draft() -> TicketDraft { + let description = TicketDescription::new((0..3000).fake()).unwrap(); + let title = TicketTitle::new((1..50).fake()).unwrap(); - TicketDraft { title, description } - } + TicketDraft { title, description } + } - fn generate_ticket_patch(status: Status) -> TicketPatch { - let patch = generate_ticket_draft(); + fn generate_ticket_patch(status: Status) -> TicketPatch { + let patch = generate_ticket_draft(); - TicketPatch { - title: Some(patch.title), - description: Some(patch.description), - status: Some(status), - } + TicketPatch { + title: Some(patch.title), + description: Some(patch.description), + status: Some(status), } } } diff --git a/jira-wip/src/koans/02_ticket_store/09_store_recap.rs b/jira-wip/src/koans/02_ticket_store/09_store_recap.rs index 6d51d82..fc77f08 100644 --- a/jira-wip/src/koans/02_ticket_store/09_store_recap.rs +++ b/jira-wip/src/koans/02_ticket_store/09_store_recap.rs @@ -1,204 +1,202 @@ -/// The core work is now complete: we have implemented the functionality we wanted to have in -/// our JIRA clone. -/// -/// Nonetheless, we still can't probe our system interactively: there is no user interface. -/// That will be the focus of the next (and last) section. -/// -/// Take your time to review what you did - you have come a long way! -pub mod store_recap { - use super::id_generation::TicketId; - use chrono::{DateTime, Utc}; - use serde::{Deserialize, Serialize}; - use std::collections::HashMap; - use std::error::Error; - - #[derive(Debug, PartialEq)] - pub struct TicketStore { - data: HashMap, - current_id: TicketId, - } +//! The core work is now complete: we have implemented the functionality we wanted to have in +//! our JIRA clone. +//! +//! Nonetheless, we still can't probe our system interactively: there is no user interface. +//! That will be the focus of the next (and last) section. +//! +//! Take your time to review what you did - you have come a long way! +use super::id_generation::TicketId; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::error::Error; + +#[derive(Debug, PartialEq)] +pub struct TicketStore { + data: HashMap, + current_id: TicketId, +} - impl TicketStore { - pub fn new() -> TicketStore { - TicketStore { - data: HashMap::new(), - current_id: 0, - } +impl TicketStore { + pub fn new() -> TicketStore { + TicketStore { + data: HashMap::new(), + current_id: 0, } + } - pub fn save(&mut self, draft: TicketDraft) -> TicketId { - let id = self.generate_id(); - let timestamp = Utc::now(); - let ticket = Ticket { - id, - title: draft.title, - description: draft.description, - status: Status::ToDo, - created_at: timestamp.clone(), - updated_at: timestamp, - }; - self.data.insert(id, ticket); - id - } + pub fn save(&mut self, draft: TicketDraft) -> TicketId { + let id = self.generate_id(); + let timestamp = Utc::now(); + let ticket = Ticket { + id, + title: draft.title, + description: draft.description, + status: Status::ToDo, + created_at: timestamp.clone(), + updated_at: timestamp, + }; + self.data.insert(id, ticket); + id + } - pub fn get(&self, id: &TicketId) -> Option<&Ticket> { - self.data.get(id) - } + pub fn get(&self, id: &TicketId) -> Option<&Ticket> { + self.data.get(id) + } - pub fn list(&self) -> Vec<&Ticket> { - self.data.values().collect() - } + pub fn list(&self) -> Vec<&Ticket> { + self.data.values().collect() + } - pub fn update(&mut self, id: &TicketId, patch: TicketPatch) -> Option<&Ticket> { - if let Some(ticket) = self.data.get_mut(id) { - if let Some(title) = patch.title { - ticket.title = title - } - if let Some(description) = patch.description { - ticket.description = description - } - if let Some(status) = patch.status { - ticket.status = status - } - - ticket.updated_at = Utc::now(); - - Some(ticket) - } else { - None + pub fn update(&mut self, id: &TicketId, patch: TicketPatch) -> Option<&Ticket> { + if let Some(ticket) = self.data.get_mut(id) { + if let Some(title) = patch.title { + ticket.title = title } - } - - pub fn delete(&mut self, id: &TicketId) -> Option { - self.data.remove(id).map(|ticket| DeletedTicket { - ticket, - deleted_at: Utc::now(), - }) - } - - fn generate_id(&mut self) -> TicketId { - self.current_id += 1; - self.current_id - } - } - - #[derive(Debug, Clone, PartialEq)] - pub struct TicketTitle(String); - - impl TicketTitle { - pub fn new(title: String) -> Result { - if title.is_empty() { - return Err(ValidationError("Title cannot be empty!".to_string())); + if let Some(description) = patch.description { + ticket.description = description } - if title.len() > 50 { - return Err(ValidationError( - "A title cannot be longer than 50 characters!".to_string(), - )); + if let Some(status) = patch.status { + ticket.status = status } - Ok(Self(title)) - } - } - #[derive(Debug, Clone, PartialEq)] - pub struct TicketDescription(String); - - impl TicketDescription { - pub fn new(description: String) -> Result { - if description.len() > 3000 { - Err(ValidationError( - "A description cannot be longer than 3000 characters!".to_string(), - )) - } else { - Ok(Self(description)) - } + ticket.updated_at = Utc::now(); + + Some(ticket) + } else { + None } } - #[derive(Debug, Clone, PartialEq)] - pub struct TicketPatch { - pub title: Option, - pub description: Option, - pub status: Option, + pub fn delete(&mut self, id: &TicketId) -> Option { + self.data.remove(id).map(|ticket| DeletedTicket { + ticket, + deleted_at: Utc::now(), + }) } - #[derive(Debug, Clone, PartialEq)] - pub struct TicketDraft { - pub title: TicketTitle, - pub description: TicketDescription, + fn generate_id(&mut self) -> TicketId { + self.current_id += 1; + self.current_id } +} - #[derive(Debug, Clone, PartialEq)] - pub struct DeletedTicket { - ticket: Ticket, - deleted_at: DateTime, - } +#[derive(Debug, Clone, PartialEq)] +pub struct TicketTitle(String); - impl DeletedTicket { - pub fn ticket(&self) -> &Ticket { - &self.ticket +impl TicketTitle { + pub fn new(title: String) -> Result { + if title.is_empty() { + return Err(ValidationError("Title cannot be empty!".to_string())); } - pub fn deleted_at(&self) -> &DateTime { - &self.deleted_at + if title.len() > 50 { + return Err(ValidationError( + "A title cannot be longer than 50 characters!".to_string(), + )); } + Ok(Self(title)) } +} - #[derive(PartialEq, Debug, Clone)] - pub struct ValidationError(String); - - impl Error for ValidationError {} +#[derive(Debug, Clone, PartialEq)] +pub struct TicketDescription(String); - impl std::fmt::Display for ValidationError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0) +impl TicketDescription { + pub fn new(description: String) -> Result { + if description.len() > 3000 { + Err(ValidationError( + "A description cannot be longer than 3000 characters!".to_string(), + )) + } else { + Ok(Self(description)) } } +} - #[derive(PartialEq, Debug, Clone)] - pub enum Status { - ToDo, - InProgress, - Blocked, - Done, - } +#[derive(Debug, Clone, PartialEq)] +pub struct TicketPatch { + pub title: Option, + pub description: Option, + pub status: Option, +} - #[derive(Debug, Clone, PartialEq)] - pub struct Ticket { - id: TicketId, - title: TicketTitle, - description: TicketDescription, - status: Status, - created_at: DateTime, - updated_at: DateTime, - } +#[derive(Debug, Clone, PartialEq)] +pub struct TicketDraft { + pub title: TicketTitle, + pub description: TicketDescription, +} - impl Ticket { - pub fn title(&self) -> &TicketTitle { - &self.title - } - pub fn description(&self) -> &TicketDescription { - &self.description - } - pub fn status(&self) -> &Status { - &self.status - } - pub fn created_at(&self) -> &DateTime { - &self.created_at - } - pub fn id(&self) -> &TicketId { - &self.id - } - pub fn updated_at(&self) -> &DateTime { - &self.updated_at - } - } +#[derive(Debug, Clone, PartialEq)] +pub struct DeletedTicket { + ticket: Ticket, + deleted_at: DateTime, +} - #[cfg(test)] - mod tests { - #[test] - fn the_next_step_of_your_journey() { - let i_am_ready_to_continue = __; +impl DeletedTicket { + pub fn ticket(&self) -> &Ticket { + &self.ticket + } + pub fn deleted_at(&self) -> &DateTime { + &self.deleted_at + } +} - assert!(i_am_ready_to_continue); - } +#[derive(PartialEq, Debug, Clone)] +pub struct ValidationError(String); + +impl Error for ValidationError {} + +impl std::fmt::Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub enum Status { + ToDo, + InProgress, + Blocked, + Done, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Ticket { + id: TicketId, + title: TicketTitle, + description: TicketDescription, + status: Status, + created_at: DateTime, + updated_at: DateTime, +} + +impl Ticket { + pub fn title(&self) -> &TicketTitle { + &self.title + } + pub fn description(&self) -> &TicketDescription { + &self.description + } + pub fn status(&self) -> &Status { + &self.status + } + pub fn created_at(&self) -> &DateTime { + &self.created_at + } + pub fn id(&self) -> &TicketId { + &self.id + } + pub fn updated_at(&self) -> &DateTime { + &self.updated_at + } +} + +#[cfg(test)] +mod tests { + #[test] + fn the_next_step_of_your_journey() { + let i_am_ready_to_continue = __; + + assert!(i_am_ready_to_continue); } } diff --git a/jira-wip/src/koans/03_cli/00_cli.rs b/jira-wip/src/koans/03_cli/00_cli.rs index b6fc60a..df81f92 100644 --- a/jira-wip/src/koans/03_cli/00_cli.rs +++ b/jira-wip/src/koans/03_cli/00_cli.rs @@ -1,146 +1,144 @@ -/// There are many ways to expose the functionality we built to a user: an API, a GUI, etc. -/// We will go with something simpler, yet good enough to probe at our implementation -/// and touch with our own hands the fruit of our labor: a command line application, a CLI. -/// -/// Rust is well-equipped to write CLIs: we will be using `structopt`, a crate -/// that provides a derive macro to define a CLI interface declaratively. -/// -/// We define the structure of our commands, annotating each field appropriately, -/// and `#[derive(structopt::StructOpt)]` takes care of generating all the code -/// required to parse the user input as well as generating a detailed `--help` page -/// for the CLI itself and each of its subcommands. -/// -/// Comments on each of the field and each of the `Command` variant will be shown in the -/// help page of those commands! -/// -/// You can learn more about `structopt` looking at their documentation: -/// https://docs.rs/structopt/0.3.12/structopt/ -/// -/// You can see the code generated by `structopt` using `cargo expand`: -/// https://github.com/dtolnay/cargo-expand -/// -/// Fill in the missing fields! -/// -/// When you are ready, uncomment the appropriate lines from src/main.rs and -/// run `cargo run --bin jira-wip` in your terminal! -pub mod cli { - use super::store_recap::{TicketStore, Status, TicketDraft, TicketPatch, TicketTitle, TicketDescription}; - use super::id_generation::TicketId; - use std::error::Error; - use std::str::FromStr; - use std::fmt::Formatter; +//! There are many ways to expose the functionality we built to a user: an API, a GUI, etc. +//! We will go with something simpler, yet good enough to probe at our implementation +//! and touch with our own hands the fruit of our labor: a command line application, a CLI. +//! +//! Rust is well-equipped to write CLIs: we will be using `structopt`, a crate +//! that provides a derive macro to define a CLI interface declaratively. +//! +//! We define the structure of our commands, annotating each field appropriately, +//! and `#[derive(structopt::StructOpt)]` takes care of generating all the code +//! required to parse the user input as well as generating a detailed `--help` page +//! for the CLI itself and each of its subcommands. +//! +//! Comments on each of the field and each of the `Command` variant will be shown in the +//! help page of those commands! +//! +//! You can learn more about `structopt` looking at their documentation: +//! https://docs.rs/structopt/0.3.12/structopt/ +//! +//! You can see the code generated by `structopt` using `cargo expand`: +//! https://github.com/dtolnay/cargo-expand +//! +//! Fill in the missing fields! +//! +//! When you are ready, uncomment the appropriate lines from src/main.rs and +//! run `cargo run --bin jira-wip` in your terminal! +use super::store_recap::{TicketStore, Status, TicketDraft, TicketPatch, TicketTitle, TicketDescription}; +use super::id_generation::TicketId; +use std::error::Error; +use std::str::FromStr; +use std::fmt::Formatter; - #[derive(structopt::StructOpt, Clone)] - /// A small command-line interface to interact with a toy Jira clone, IronJira. - pub enum Command { - /// Create a ticket on your board. - Create { - __ - }, - /// Edit the details of an existing ticket. - Edit { - /// Id of the ticket you want to edit. - #[structopt(long)] - id: TicketId, - /// New status of the ticket. - #[structopt(long)] - status: Option, - /// New description of the ticket. - #[structopt(long)] - description: Option, - /// New title for your ticket. - #[structopt(long)] - title: Option, - }, - /// Delete a ticket from the store passing the ticket id. - Delete { - __ - }, - /// List all existing tickets. - List, - } +#[derive(structopt::StructOpt, Clone)] +/// A small command-line interface to interact with a toy Jira clone, IronJira. +pub enum Command { + /// Create a ticket on your board. + Create { + __ + }, + /// Edit the details of an existing ticket. + Edit { + /// Id of the ticket you want to edit. + #[structopt(long)] + id: TicketId, + /// New status of the ticket. + #[structopt(long)] + status: Option, + /// New description of the ticket. + #[structopt(long)] + description: Option, + /// New title for your ticket. + #[structopt(long)] + title: Option, + }, + /// Delete a ticket from the store passing the ticket id. + Delete { + __ + }, + /// List all existing tickets. + List, +} - /// `structopt` relies on `FromStr` to know how to parse our custom structs and enums - /// from the string passed in as input by a user. - /// - /// Parsing is fallible: we need to declare what error type we are going to return if - /// things go wrong and implement the `from_str` function. - impl FromStr for Status { - type Err = ParsingError; +/// `structopt` relies on `FromStr` to know how to parse our custom structs and enums +/// from the string passed in as input by a user. +/// +/// Parsing is fallible: we need to declare what error type we are going to return if +/// things go wrong and implement the `from_str` function. +impl FromStr for Status { + type Err = ParsingError; - fn from_str(s: &str) -> Result { - __ - } - } + fn from_str(s: &str) -> Result { + __ + } +} - impl FromStr for TicketTitle { - __ - } +impl FromStr for TicketTitle { + __ +} - impl FromStr for TicketDescription { - __ - } +impl FromStr for TicketDescription { + __ +} - /// Our error struct for parsing failures. - #[derive(Debug)] - pub struct ParsingError(String); +/// Our error struct for parsing failures. +#[derive(Debug)] +pub struct ParsingError(String); - impl Error for ParsingError { } +impl Error for ParsingError { } - impl std::fmt::Display for ParsingError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.0) - } +impl std::fmt::Display for ParsingError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.0) } +} - /// The core function: given a mutable reference to a `TicketStore` and a `Command`, - /// carry out the action specified by the user. - /// We use `Box` to avoid having to specify the exact failure modes of our - /// top-level handler. - /// - /// `dyn Error` is the syntax of a trait object, a more advanced topic that we will not be - /// touching in this workshop. - /// Check its section in the Rust book if you are curious: - /// https://doc.rust-lang.org/book/ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types - pub fn handle_command(ticket_store: &mut TicketStore, command: Command) -> Result<(), Box> { - match command { - Command::Create { description, title } => { - todo!() - } - Command::Edit { - id, - title, - description, - status, - } => { - todo!() - } - Command::Delete { ticket_id } => match ticket_store.delete(&ticket_id) { - Some(deleted_ticket) => println!( - "The following ticket has been deleted:\n{:?}", - deleted_ticket - ), - None => println!( - "There was no ticket associated to the ticket id {:?}", - ticket_id - ), - }, - Command::List => { - todo!() - } +/// The core function: given a mutable reference to a `TicketStore` and a `Command`, +/// carry out the action specified by the user. +/// We use `Box` to avoid having to specify the exact failure modes of our +/// top-level handler. +/// +/// `dyn Error` is the syntax of a trait object, a more advanced topic that we will not be +/// touching in this workshop. +/// Check its section in the Rust book if you are curious: +/// https://doc.rust-lang.org/book/ch17-02-trait-objects.html#using-trait-objects-that-allow-for-values-of-different-types +pub fn handle_command(ticket_store: &mut TicketStore, command: Command) -> Result<(), Box> { + match command { + Command::Create { description, title } => { + todo!() + } + Command::Edit { + id, + title, + description, + status, + } => { + todo!() + } + Command::Delete { ticket_id } => match ticket_store.delete(&ticket_id) { + Some(deleted_ticket) => println!( + "The following ticket has been deleted:\n{:?}", + deleted_ticket + ), + None => println!( + "There was no ticket associated to the ticket id {:?}", + ticket_id + ), + }, + Command::List => { + todo!() } - Ok(()) } + Ok(()) +} - #[cfg(test)] - mod tests { - use super::*; +#[cfg(test)] +mod tests { + use super::*; - #[test] - fn invalid_status_fails_to_be_parsed() - { - let invalid_status = "Not a good status"; - assert!(Status::from_str(invalid_status).is_err()); - } + #[test] + fn invalid_status_fails_to_be_parsed() + { + let invalid_status = "Not a good status"; + assert!(Status::from_str(invalid_status).is_err()); } } diff --git a/jira-wip/src/koans/03_cli/01_persistence.rs b/jira-wip/src/koans/03_cli/01_persistence.rs index a6bb568..b2186f3 100644 --- a/jira-wip/src/koans/03_cli/01_persistence.rs +++ b/jira-wip/src/koans/03_cli/01_persistence.rs @@ -1,85 +1,83 @@ -/// Playing around with the CLI using `cargo run --bin jira-wip` you might have noticed -/// that is quite tricky to actually exercise all the functionality we implemented: -/// the store is created anew for every execution, nothing is persisted! -/// -/// Time to put a remedy to that: we want to persist our store to disk between CLI invocations, -/// reloading it before performing the next command. -/// -/// We will be relying on the `serde` crate (`Ser`ialisation/`De`serialisation): -/// it can serialise data to many different file formats as long as your struct or enums -/// implement serde's `Serialize` trait. -/// `Deserialize`, instead, is needed for the opposite journey. -/// -/// You don't need to implement this manually: just add `#[derive(Serialize, Deserialize)]` -/// where needed in `store_recap` - the `load` and `save` functions should just work afterwards! -/// -/// Update `src/main.rs` appropriately afterwards to use the fruit of your labor! -pub mod persistence { - use super::store_recap::TicketStore; - use std::fs::read_to_string; - use std::path::Path; +//! Playing around with the CLI using `cargo run --bin jira-wip` you might have noticed +//! that is quite tricky to actually exercise all the functionality we implemented: +//! the store is created anew for every execution, nothing is persisted! +//! +//! Time to put a remedy to that: we want to persist our store to disk between CLI invocations, +//! reloading it before performing the next command. +//! +//! We will be relying on the `serde` crate (`Ser`ialisation/`De`serialisation): +//! it can serialise data to many different file formats as long as your struct or enums +//! implement serde's `Serialize` trait. +//! `Deserialize`, instead, is needed for the opposite journey. +//! +//! You don't need to implement this manually: just add `#[derive(Serialize, Deserialize)]` +//! where needed in `store_recap` - the `load` and `save` functions should just work afterwards! +//! +//! Update `src/main.rs` appropriately afterwards to use the fruit of your labor! +use super::store_recap::TicketStore; +use std::fs::read_to_string; +use std::path::Path; - /// Fetch authentication parameters from a configuration file, if available. - pub fn load(path: &Path) -> TicketStore { - println!("Reading data from {:?}", path); - // Read the data in memory, storing the value in a string - match read_to_string(path) { - Ok(data) => { - // Deserialize configuration from YAML format - serde_yaml::from_str(&data).expect("Failed to parse serialised data.") - } - Err(e) => match e.kind() { - // The file is missing - this is the first time you are using IronJira! - std::io::ErrorKind::NotFound => { - // Return default configuration - TicketStore::new() - } - // Something went wrong - crash the CLI with an error message. - _ => panic!("Failed to read data."), - }, +/// Fetch authentication parameters from a configuration file, if available. +pub fn load(path: &Path) -> TicketStore { + println!("Reading data from {:?}", path); + // Read the data in memory, storing the value in a string + match read_to_string(path) { + Ok(data) => { + // Deserialize configuration from YAML format + serde_yaml::from_str(&data).expect("Failed to parse serialised data.") } + Err(e) => match e.kind() { + // The file is missing - this is the first time you are using IronJira! + std::io::ErrorKind::NotFound => { + // Return default configuration + TicketStore::new() + } + // Something went wrong - crash the CLI with an error message. + _ => panic!("Failed to read data."), + }, } +} - /// Save tickets on disk in the right file. - pub fn save(ticket_store: &TicketStore, path: &Path) { - // Serialize data to YAML format - let content = serde_yaml::to_string(ticket_store).expect("Failed to serialize tickets"); - println!("Saving tickets to {:?}", path); - // Save to disk - std::fs::write(path, content).expect("Failed to write tickets to disk.") - } +/// Save tickets on disk in the right file. +pub fn save(ticket_store: &TicketStore, path: &Path) { + // Serialize data to YAML format + let content = serde_yaml::to_string(ticket_store).expect("Failed to serialize tickets"); + println!("Saving tickets to {:?}", path); + // Save to disk + std::fs::write(path, content).expect("Failed to write tickets to disk.") +} - #[cfg(test)] - mod tests { - use super::super::store_recap::{ - Status, TicketDescription, TicketDraft, TicketStore, TicketTitle, - }; - use super::*; - use fake::Fake; - use tempfile::NamedTempFile; +#[cfg(test)] +mod tests { + use super::super::store_recap::{ + Status, TicketDescription, TicketDraft, TicketStore, TicketTitle, + }; + use super::*; + use fake::Fake; + use tempfile::NamedTempFile; - #[test] - fn load_what_you_save() { - let mut store = TicketStore::new(); - let draft = generate_ticket_draft(); - store.save(draft); + #[test] + fn load_what_you_save() { + let mut store = TicketStore::new(); + let draft = generate_ticket_draft(); + store.save(draft); - // We use the `tempfile` crate to generate a temporary path on the fly - // which will be cleaned up at the end of the test. - // See https://docs.rs/tempfile/3.1.0/tempfile/ for more details. - let temp_path = NamedTempFile::new().unwrap().into_temp_path(); + // We use the `tempfile` crate to generate a temporary path on the fly + // which will be cleaned up at the end of the test. + // See https://docs.rs/tempfile/3.1.0/tempfile/ for more details. + let temp_path = NamedTempFile::new().unwrap().into_temp_path(); - save(&store, temp_path.as_ref()); - let loaded_store = load(temp_path.as_ref()); + save(&store, temp_path.as_ref()); + let loaded_store = load(temp_path.as_ref()); - assert_eq!(store, loaded_store); - } + assert_eq!(store, loaded_store); + } - fn generate_ticket_draft() -> TicketDraft { - let description = TicketDescription::new((0..3000).fake()).unwrap(); - let title = TicketTitle::new((1..50).fake()).unwrap(); + fn generate_ticket_draft() -> TicketDraft { + let description = TicketDescription::new((0..3000).fake()).unwrap(); + let title = TicketTitle::new((1..50).fake()).unwrap(); - TicketDraft { title, description } - } + TicketDraft { title, description } } } diff --git a/jira-wip/src/koans/03_cli/02_the_end.rs b/jira-wip/src/koans/03_cli/02_the_end.rs index b786984..39778bd 100644 --- a/jira-wip/src/koans/03_cli/02_the_end.rs +++ b/jira-wip/src/koans/03_cli/02_the_end.rs @@ -1,22 +1,20 @@ +/// It has been our pleasure to have you at the Rust London Code Dojo! +/// +/// We hope that the workshop helped you move forward in your Rust journey, +/// having fun toying around with rebuilding JIRA. +/// +/// If you have any feedback on the workshop, please reach out to rust@lpalmieri.com +/// If you found any typo, mistake or you think a section could be worded better, +/// please open a PR! +/// +/// ~ See you next time! ~ +/// +#[cfg(test)] mod the_end { - /// It has been our pleasure to have you at the Rust London Code Dojo! - /// - /// We hope that the workshop helped you move forward in your Rust journey, - /// having fun toying around with rebuilding JIRA. - /// - /// If you have any feedback on the workshop, please reach out to rust@lpalmieri.com - /// If you found any typo, mistake or you think a section could be worded better, - /// please open a PR! - /// - /// ~ See you next time! ~ - /// - #[cfg(test)] - mod the_end { - #[test] - fn the_end_of_your_journey() { - let i_am_done = __; + #[test] + fn the_end_of_your_journey() { + let i_am_done = __; - assert!(i_am_done); - } + assert!(i_am_done); } } diff --git a/koans-framework/src/lib.rs b/koans-framework/src/lib.rs index 66e1b6b..e644a16 100644 --- a/koans-framework/src/lib.rs +++ b/koans-framework/src/lib.rs @@ -88,7 +88,15 @@ impl KoanCollection { .lines() .filter(|l| !l.as_ref().unwrap().is_empty()) .filter(|l| &l.as_ref().unwrap().trim()[..2] != "//") // Ignores comments - .count(), + .map(|l| { + if l.unwrap().contains("mod") { + // Count the number of module declarations + 1 + } else { + 0 + } + }) + .sum(), Err(e) => { match e.kind() { ErrorKind::NotFound => { @@ -131,7 +139,11 @@ impl KoanCollection { let koan = self.next(); if let Some(koan) = koan { let koan_filename: String = koan.into(); - writeln!(file, "include!(\"koans/{:}.rs\");", koan_filename).unwrap(); + let include = format!( + "#[path = \"koans/{}.rs\"]\nmod {};\n", + koan_filename, koan.name + ); + writeln!(file, "{}", include).unwrap(); Ok(koan) } else { Err(()) diff --git a/koans-framework/src/main.rs b/koans-framework/src/main.rs index 81f633a..7c60e57 100644 --- a/koans-framework/src/main.rs +++ b/koans-framework/src/main.rs @@ -108,10 +108,11 @@ fn seek_the_path(koans: &KoanCollection) -> TestOutcome { fn run_tests(manifest_path: &Path, filter: Option<&str>) -> TestOutcome { // Tell cargo to return colored output, unless we are on Windows and the terminal // doesn't support it. - let mut color_option = "always"; - if cfg!(windows) && !Paint::enable_windows_ascii() { - color_option = "never"; - } + let color_option = if cfg!(windows) && !Paint::enable_windows_ascii() { + "never" + } else { + "always" + }; let mut args: Vec = vec![ "test".into(),