AlecAivazis / survey

A golang library for building interactive and accessible prompts with full support for windows and posix terminals.
MIT License
4.09k stars 355 forks source link

Conditional prompts based on responses to previous prompts #156

Closed weshicks closed 6 years ago

weshicks commented 6 years ago

This is more of a question than a bug, but it might be a feature request. I did some digging, and I wasn't able to find anyone else asking for / about this so here goes.

Use case: I'm building a CLI using survey to handle questions / responses, and would like to know the "best" way to go about having conditional questions, that is, questions that only appear based on your responses to other questions.

Say you have a MultiSelect question that asks a user what their favorite food is. If they select "Pizza", I'd like to ask them questions about their pizza preferences, if they answer "Tacos" I'd like to ask them questions about tacos, and so on.

Is this possible with Survey, and I've just missed it in the docs? If not, is something like this within the scope of what this project is looking to solve for? Or should I structure my CLI application to have several smaller sub-survey questions? Ideally I'd like one consistent user flow through the whole process.

Any thoughts/feedback would be appreciated!

AlecAivazis commented 6 years ago

Hey @weshicks - thanks for submitting this!

So similar libraries use a field on their questions (usually called "when") to change the question list at runtime. However, most of the solutions I know of are not in statically typed languages so they can be nice one liners like

{
    ...
   when: answers => answers.favoriteFood == tacos
}

However, since we have to find a statically typable solution, the only thing I can think of is that the when field has a type signature of func (answers interface{}) bool which forces a lot of awkward type assertions in the user's code. Also, since the ultimate set of questions can be a rather gnarly graph the logic inside of a when can get prettty complicated. ie, if they chose pizza, and you asked them if they wanted tomatoes, you might want to ask them a third question like "fresh" or "diced" if they did want tomatoes - not a great example but hopefully you see what i'm saying.

Because of these 2 "problems", I haven't actually sat down to implement when. There is an old issue to capture this effort (#5), but every time I sit down to do it i end up coming to the same conclusion - while a little uglier, putting the branching logic in user land and calling survey.Ask multiple times is ultimately cleaner and exposes fewer interface{}s to the user which means it "feels" better imo.

weshicks commented 6 years ago

Ah, yeah I'm tracking with you on the complications that introducing that sort of structure could impose! Thanks for laying it out like that, makes sense. I think I can spend some time reimagining the solution to the problem I'm trying to solve with this in mind.

I'm still new to Go, but maybe somewhere down the line I'll play around with this concept further. In the meantime, feel free to close this! Thanks for this tool, it's really handy!

AlecAivazis commented 6 years ago

Awesome - i'm definitely interested in solving this problem in survey so if you have a solution that addresses these i'm definitely interested in checking it out!

I'm happy to hear it's been working for you. Please feel free to reach out if you run into problems. You can find me on the gophers slack If I don't respond here or if you want something more direct

tetafro commented 5 years ago

@AlecAivazis why do you think simple func() bool won't work? This is basically all the code you need:

type Conditioner func() bool

type Question struct {
    Name      string
    Prompt    Prompt
    Validate  Validator
    Transform Transformer
    Condition Conditioner
}

func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
    // ...

    for _, q := range qs {
        // Skip the question if the condition is not met
        if q.Condition != nil && !q.Condition() {
            continue
        }
answers := make(map[string]interface{})
questions := []*Question{
    {
        Name: "q1",
        Prompt: &Confirm{
            Message: "q1",
        },
    },
    {
        Name: "q2",
        Prompt: &Confirm{
            Message: "q2",
        },
        Condition: func() bool {
            return answers["q1"]
        },
    },
}
jamesalbert commented 5 years ago

@AlecAivazis I ran into this issue today. This would be really useful. Not sure if @tetafro wants to submit a pr, but if not I can go ahead and submit one if that looks good.

The only thing I would add is passing response interface{} to the provided Conditioner function because the response object isn't always going to be in the same scope as the survey.Question

e.g.:

type Conditioner func(response interface{}) bool
...
        for _, q := range qs {
        // Skip the question if the condition is not met
        if q.Condition != nil && !q.Condition(response) {
            continue
        }
AlecAivazis commented 5 years ago

It’s not that I don’t think you’re suggestion would work @tetafro, I’m pretty sure it would! I just want to make sure that we don’t just jump onto one specific solution because it would work.

I’m not sure that the additional API you suggested actually simplifies the overall logic that would be required if you split up the set of questions into two. As such, I’m not sure I think we should complicate the API to simplify the easy cases without it also helping the complicated ones

tetafro commented 5 years ago

split up the set of questions into two

@AlecAivazis quite simple, I agree. But I want to just write array of questions and run it :) I don't think that having basic tools for managing questions would be unnecessary API complication.

without it also helping the complicated ones

Can you provide an example of complicated case?