Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented OrderSelect #16

Merged
merged 33 commits into from Sep 1, 2022

Conversation

NovaliX-Dev
Copy link
Contributor

@NovaliX-Dev NovaliX-Dev commented Aug 13, 2022

Here is a first beta of the order_select question (Discussed in [#13]), but i'ts not complete.

I still have to :

  • implement the automatic test module, since I tested it manually

I intentionally not implemented many functions in the builder (choice, choice_with_default, separator, default_spearator, choice_with_default) for the following reasons :

  • Since the user will mix the order of the options in the list, separators aren't necessary,
  • "Choice" and "choice with default" don't seem necessary anymore to me, because the programmer can compose the list and set the order of the options before building the question.

Let me know if you want changes in the code.

Only tested on Windows 11.

@NovaliX-Dev
Copy link
Contributor Author

For the testing i removed the ui::assert_backend_snapshot!(backend), since it will change the view in the terminal with the and keys

@NovaliX-Dev
Copy link
Contributor Author

The implementation is now completed

Copy link
Owner

@Lutetium-Vanadium Lutetium-Vanadium left a comment

Choose a reason for hiding this comment

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

Rather than the filter and validate working on only the indices, I think it makes sense to implement AsRef<str> for ListItem and use a ChoiceList<Text<ListItem>>. This way if the validator or filterer requires the string associated with each option, they can access that as well. This would require many changes throughout, as it collapses order and choices to one member.

Also add integration tests

examples/order-select.rs Outdated Show resolved Hide resolved
src/question/mod.rs Outdated Show resolved Hide resolved
src/question/mod.rs Outdated Show resolved Hide resolved
src/question/order_select/builder.rs Outdated Show resolved Hide resolved
src/question/order_select/builder.rs Outdated Show resolved Hide resolved
src/question/order_select/mod.rs Outdated Show resolved Hide resolved
src/question/order_select/mod.rs Outdated Show resolved Hide resolved
src/question/order_select/mod.rs Outdated Show resolved Hide resolved
src/question/order_select/tests.rs Outdated Show resolved Hide resolved
src/question/order_select/tests.rs Outdated Show resolved Hide resolved
@NovaliX-Dev
Copy link
Contributor Author

Yeah, there is a ton of things to change. I'll do that in the next days.
Thanks for reporting things to change !

@NovaliX-Dev
Copy link
Contributor Author

I'm not really sure the use of ChoiceList<Text<ListItem>> will be the best option here.

What's the advantage of using ChoiceList<Text<ListItem>> ? A Vec<ListItem> is easier to implement and we can still access the str.

@Lutetium-Vanadium
Copy link
Owner

Vec<T> doesn't implement the List trait which is required for Select to work (see this bound). Vec doesn't implement List as it cannot implement the functions page_size and should_loop. As for the the Text, it is to allow multiline text. If you look at the implementation of the Text widget, it is relatively expensive to compute the line breaks, so this computation is memoized. That is why a regular &str is single line only. I hope this answers your question

@NovaliX-Dev
Copy link
Contributor Author

NovaliX-Dev commented Aug 27, 2022

Ok i'll stick with ChoiceList then, however it's crate private.
Does it mind if i have to make it public, since the user will have to access it directly ?

@NovaliX-Dev
Copy link
Contributor Author

NovaliX-Dev commented Aug 27, 2022

And also since it's a list of Choice enums, the user will have to unwrap the choices, which is a problem since the user will have to consume it and take it's ownership.

The only option i see here is to not use the Choice enum at all. Since we do not allow the separators, we can remove it completely.

@Lutetium-Vanadium
Copy link
Owner

Yeah, getting a decent API surface is difficult here because of the internals, but I think I have a found a good solution.

I'm going to push a change that converts ChoiceList<T> to a more generic SelectList<T>. In SelectList<T> the choices field will be just a Vec<T> (as opposed to the Vec<Choice<T>> which was previously there). I would also like you to add the following struct to order_select/mod.rs and re-export it in requestty::question:

/// The representation of each choice in an [`OrderSelect`].
///
/// It is different from [`ListItem`](crate::answer::ListItem) due to an implementation detail.
#[derive(Debug)]
pub struct OrderSelectItem {
    index: usize,
    text: Text<String>,
}

impl OrderSelectItem {
    /// The index of the choice
    pub fn index(&self) -> usize {
        self.index
    }

    /// The content of the choice -- it is what is displayed to the user
    pub fn text(&self) -> &str {
        &self.text.text
    }
}

impl Widget for OrderSelectItem {
    fn render<B: Backend>(
        &mut self,
        layout: &mut ui::layout::Layout,
        backend: &mut B,
    ) -> io::Result<()> {
        self.text.render(layout, backend)
    }

    fn height(&mut self, layout: &mut ui::layout::Layout) -> u16 {
        self.text.height(layout)
    }

    fn cursor_pos(&mut self, layout: ui::layout::Layout) -> (u16, u16) {
        self.text.cursor_pos(layout)
    }

    fn handle_key(&mut self, key: KeyEvent) -> bool {
        self.text.handle_key(key)
    }
}

Then, instead of using ChoiceList<Text<ListItem>>, we can use a SelectList<OrderSelectItem>.

OrderSelectBuilder::new can be changed as follows:

    pub(crate) fn new(name: String) -> Self {
        Self {
            opts: Options::new(name),
            order_select: SelectList::new(|_| true),
        }
    }

After making these changes, validate can take &[OrderSelectItem] and filter can take Vec<OrderSelectItem>. How does this sound?

@NovaliX-Dev
Copy link
Contributor Author

Alright i'll implement this right now

@NovaliX-Dev
Copy link
Contributor Author

By researching, i found that changing type back to ListItem is problematic because we have to convert from OrderSelectItem each time the display is updated because of the transform function

From a performance perspective i don't know if this is an issue or not

@NovaliX-Dev
Copy link
Contributor Author

NovaliX-Dev commented Aug 30, 2022

Maybe replacing OrderSelectItem by ListItem completely will be a great option here

Since SelectList is generic et not constrained to Choice type anymore, this should be fairly easy

Or otherwise we left as it is, after all they behave the same way here

@NovaliX-Dev
Copy link
Contributor Author

NovaliX-Dev commented Aug 30, 2022

Or we can implement a Trait for this, so we can keep this consistency

What do you think is the best option?

@Lutetium-Vanadium
Copy link
Owner

By the way, i don't think the snapshots folder should be visible here, they are only caches after all

They are not caches. When you run the test, it checks that the result produced matches the snapshot that already exists.

The exception is when you run the tests as cargo snapshot or cargo snapshott. If you see .cargo/config.toml, the above two commands are aliases for cargo insta test. And if you see here, cargo insta test just forces all snapshot assertions to pass so that you can collect all snapshots and review them without having to repeatedly run the tests.

To see more about snapshot testing, you can read the documentation of the insta library.


You can explain how to do the tests in the README file (or another) instead

Yes, I really need to write some documentation about the internal workings of the library.


By researching, i found that changing type back to ListItem is problematic because we have to convert from OrderSelectItem each time the display is updated because of the transform function

From a performance perspective i don't know if this is an issue or not

The transform function is only called at the end to display the final answer. So, the only thing that changes is that, the conversion happens in the implementation of finish instead of the end of the ask function.


Maybe replacing OrderSelectItem by ListItem completely will be a great option here

Since SelectList is generic et not constrained to Choice type anymore, this should be fairly easy

Or otherwise we left as it is, after all they behave the same way here

This would be great, after all OrderSelectItem is only there because of an implementation detail. However, the implementation detail is not Choice, but the Text widget for multiline text. see this

Comment on lines 214 to 233
/// NOTE: The boolean [`slice`] contains a boolean value for each index even if it is a
/// separator. However it is guaranteed that all the separator indices will be false.
///
/// # Examples
///
/// ```
/// use requestty::Question;
///
/// let order_select = Question::order_select("home_tasks")
/// //...
/// .validate(|tasks, previous_answers| {
/// if tasks[0].text() == "Make the bed" {
/// Err("You have to make the bed first".to_string())
/// } else {
/// Ok(())
/// }
/// })
/// //...
/// .build();
/// ```

Choose a reason for hiding this comment

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

Remove the "NOTE", and like the tests above, the then and else block are swapped. The condition is checking that the first one is "Make the bed", but if it is, it is returning an error that the first one should be to make the bed.

@NovaliX-Dev
Copy link
Contributor Author

What i was meaning was that instead of storing the snapshots on this repository, the developer can build the snapshots themselves before performing tests

That's why i suggested to add a category in README.md or another file, so the future developers know how to perform tests correctly

@NovaliX-Dev
Copy link
Contributor Author

NovaliX-Dev commented Aug 31, 2022

Also here how i'll implement back ListItem

  • I'll change ListItem into a new generic type GenericListItem<T> so i can put Text struct into it
  • To keep compatibility i added a Type alias ListItem = GenericListItem<String>
  • Before calling validation, filter, and transform function, i convert this type back to ListItem

What do you think about this way ?

@Lutetium-Vanadium
Copy link
Owner

I feel like I didn't convey my point across properly. I do not want to change the types for validate and filter. I only want to change them for transform. Thus OrderSelect will change as follows:

#[derive(Debug)]
pub(super) struct OrderSelect<'a> {
    choices: SelectList<OrderSelectItem>,
    max_index_width: usize,
    moving: bool,

    transform: Transform<'a, [ListItem]>, // Only thing that changes
    validate: Validate<'a, [OrderSelectItem]>,
    filter: Filter<'a, Vec<OrderSelectItem>>,
}

And the Prompt impl for OrderSelectPrompt will change as follows:

 impl Prompt for OrderSelectPrompt<'_, '_> {
    // ...
    type Output = Vec<ListItem>;
    // ...
    fn finish(self) -> Self::Output {
        // ...
        c.into_iter()
            .map(From::from)
            .collect()
    }
    // ...
}

And the From<Vec<OrderSelectItem>> impl in answer.rs can be deleted. As you can see, this doesn't require the type to converted to ListItem every time, only at the end once the answer has been finalised.

@Lutetium-Vanadium
Copy link
Owner

What i was meaning was that instead of storing the snapshots on this repository, the developer can build the snapshots themselves before performing tests

The way snapshot tests work is as following:

When you first write the test, you run cargo snapshot and cargo snapshott (both need to be run). This will generate files in the form of *.snap.new in the crossterm-snapshots and termion-snapshots directories.

Then you run cargo review which gives you an interactive prompt to manually review that the snapshots generated are as expected. These will convert the files generated in the previous step from *.snap.new to *.snap.

Only after all of this is done, can the tests be run how they usually are. When running a test which contains ui::assert_backend_snapshot!(backend), it checks that the backend supplied to the macro matches the one present in the *.snap file. If it does not then the test fails.

I urge you to also read the content of the links here.

I hope you understand why the snapshots need to be stored in the repository. If you have any questions, please feel free to ask.

That's why i suggested to add a category in README.md or another file, so the future developers know how to perform tests correctly

Yes, I agree with adding documentation for how to run the tests correctly. This is definitely needed, but I will get to it after some time.

@NovaliX-Dev
Copy link
Contributor Author

NovaliX-Dev commented Aug 31, 2022

Maybe what you're trying to say here is that, in my way, the developer needs to accept each snapshot one after another, which can be tedious

But there is a command to accept them all at once : cargo insta accept

That's why is was suggesting to remove this folders, because the snapshots can be reconstructed with this series of command:

  1. cargo snapshot
  2. cargo snapshott
  3. cargo insta accept

We can keep them if you want, it was just an important suggestion after all

@NovaliX-Dev
Copy link
Contributor Author

NovaliX-Dev commented Aug 31, 2022

By the way, the snapshott command doesn't work on windows, termion is missing his sys crate
Maybe i did a thing wrong

@NovaliX-Dev
Copy link
Contributor Author

NovaliX-Dev commented Aug 31, 2022

Also for the implementation you suggested, i'm not very comfortable with the fact that there is different types (and so different ways to access the data) for each functions of this builder, this can confuse the user

Yes, this can add consistency with the transform function of multi-select, but with the other functions of the same builder i think it will just do the opposite

Personally i would prefer to keep the same type (and so the same ways to access the data) for each function of the same builder here, to keep the consistency in the builder itself.
Also these function are usually configured at the same place, so if there is an inconsistency somewhere it will very noticeable

I hope you understand my point of view on this subject

@Lutetium-Vanadium
Copy link
Owner

I understand where you are coming from. The reason I suggested the change was that maintaining compatibility with multi-select's transform allows more code reuse as they have very significant overlap, both in type signature as well as purpose.

However, the point you raise is also valid. I leave the final decision of whether to change the type for transform up to you.

@Lutetium-Vanadium
Copy link
Owner

By the way, the snapshott command doesn't work on windows, termion is missing his sys crate Maybe i did a thing wrong

Yes, the termion library does not work on windows. It is a decision that the authors have made, and I have no control over it. That is one of the reasons why crossterm is the default backend.

Since you are running windows, don't worry about the termion snapshots, I will update them.

@Lutetium-Vanadium
Copy link
Owner

Maybe what you're trying to say here is that, in my way, the developer needs to accept each snapshot one after another, which can be tedious

But there is a command to accept them all at once : cargo insta accept

That's why is was suggesting to remove this folders, because the snapshots can be reconstructed with this series of command:

1. `cargo snapshot`

2. `cargo snapshott`

3. `cargo insta accept`

Yes, cargo insta accept does accept all the snapshots, but using it voids the purpose of the snapshot tests. The snapshot tests are meant to make sure that whatever is displayed on the screen is correct.

Take the following example. Let's say that through some change for a different purpose, accidentally in OrderSelect, the index being displayed is wrong (the error can be a lot more subtle than this, but this is just an example).

Currently as it is the snapshots are stored. They are checked to be correct when they are created and only then accepted. Then if the rendering changes by accident, the index in the test would not match the one in the snapshot, and so the test would fail.

If snapshots were not stored, and the 3 commands above were run, the snapshots created would include the wrong index as cargo insta accept accepts unconditionally. And so all tests would pass event though they shouldn't. The only way to prevent this is to check that every snapshot looks correct which would then convert the snapshot tests from being automated to manual. This would also prevent the snapshot checks in the CI from actually catching things that look wrong as it always accepts the snapshot as it is.


We can keep them if you want, it was just an important suggestion after all

We will keep them, but I feel it is important that you understand why we need to keep them

@NovaliX-Dev
Copy link
Contributor Author

Since the only difference between OrderSelectItem and ListItem is the type of the text (Text<String> against String), what about modifying ListItem so it can accept any type T and not only String ?

This way both of our requirements are satisfied, you have the ListItem and i have the same type for the builder functions

Also OrderSelectItem implements the Widget trait to be rendered, but we can render it‘s text directly instead, so in theory we can use ListItem which contains it

The only con is that we need to access the text attribute of the text, so compared to ListItem there is a little inconsistency here. But compared to the difference between ListItem and OrderSelectItem in the ways to access data this is way less noticeable in my opinion

@Lutetium-Vanadium
Copy link
Owner

I prefer to keep OrderSelectItem as a separate thing instead of making ListItem generic.

The reason that OrderSelectItem has a different access pattern than ListItem is that exposing the Text struct in the requestty interface is something I really want to avoid.

@NovaliX-Dev
Copy link
Contributor Author

Ok so OrderSelect should be ready now

Copy link
Owner

@Lutetium-Vanadium Lutetium-Vanadium left a comment

Choose a reason for hiding this comment

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

Most of it looks good. There are a couple of small changes here that need to be made. Other than this, the requestty-macro crate needs to be changed to allow for OrderSelect, the gifs need to be added and the snapshots need to be added. I will push a commit containing all of these changes, after which I will enable the CI for this PR. Please fix any errors that come with the CI

Comment on lines 302 to 306
impl Into<ListItem> for OrderSelectItem {
fn into(self) -> ListItem {
ListItem { index: self.initial_index, text: self.text.text }
}
}
Copy link
Owner

Choose a reason for hiding this comment

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

It is better to implement From<OrderSelectItem> for ListItem instead. See here.

}
b.set_fg(ui::style::Color::Reset)
})
.message("multi select")
Copy link
Owner

Choose a reason for hiding this comment

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

This is still multi-select

@NovaliX-Dev
Copy link
Contributor Author

Also now i understand for the snapshots folder, thanks for the explanation

@Lutetium-Vanadium Lutetium-Vanadium merged commit 09c9a0b into Lutetium-Vanadium:master Sep 1, 2022
@Lutetium-Vanadium
Copy link
Owner

Congrats, this PR has been merged!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants