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

RSDK-9284 - automate CLI flag parsing #4581

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

stuqdog
Copy link
Member

@stuqdog stuqdog commented Nov 22, 2024

Note to reviewers: this is currently a POC and definitely not ready for prime time. Before I go ahead and make changes to all existing methods, I wanted to get buy-in from folks on this as an approach. Once we have agreement on the shape of this implementation, I'll do the (verbose, but mechanical) work of changing existing methods which should hopefully be trivial to review despite having an estimated large loc diff.

The nice thing about this approach is it provides safe, easy, typeful access to flag data while requiring minimal change in how developers create actions (they have to define a struct with their flag fields and the Action field is now populated slightly differently), but there should be an easily replicable pattern in the code base such that this is not harmful.

A notable shortcoming of this approach is that the names of the fields in the struct must match (or fuzzy match, taking account for differences in snake/camel/kebab case) the names of the flag. Since a developer is defining the struct, we don't currently have a way to enforce that things are correct. I do think it would be possible using reflection to have some sort of assert that the flags and the fields of the struct fuzzy match, but I fear this will require some decent refactoring and is more of a "programmatic enforcement" issue rather than a "automate flag parsing" issue and so should be done in a future PR.

One other (minor) restriction: the fields on the struct must be public. The reflection library can't access them if they're private, leading to a runtime error.

One other thing to note: I don't think we can get away with not requiring a CLI.context in the structful methods that we're asking users to now define, as certain existing methods (DownloadModuleAction, e.g.) use fields on the ctx within the method. This means that if a user wants to access flags the old way, we can't stop them. Again, this might be resolvable but probably should go in a separate ticket.

@viambot viambot added the safe to test This pull request is marked safe to test from a trusted zone label Nov 22, 2024
@viambot viambot added safe to test This pull request is marked safe to test from a trusted zone and removed safe to test This pull request is marked safe to test from a trusted zone labels Nov 22, 2024
camelFormattedName = matchAllCap.ReplaceAllString(camelFormattedName, "${1}-${2}")
camelFormattedName = strings.ToLower(camelFormattedName)

return ctx.Value(camelFormattedName)
Copy link
Member Author

Choose a reason for hiding this comment

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

It seems like some guardrails might be nice here, but I'm not sure that we can say at compile time what's safe and what's not. Even if an argument is non-optional, I expect there are cases where a nil value is normal and expected.

cli/app.go Outdated
Comment on lines 267 to 275
type foo struct {
FooFoo string
Bar int
}

func doFoo(foo foo, ctx *cli.Context) error {
fmt.Printf("FooFoo is %s and Bar is %v.", foo.FooFoo, foo.Bar)
return nil
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This will get deleted in the final project obviously, but I wanted to show an example of what new development would look like. Also, it highlights how we successfully set a field FooFoo with a flag of foo-foo

@stuqdog
Copy link
Member Author

stuqdog commented Nov 22, 2024

fyi @dgottlieb since we discussed this idea a bit during the scope doc process.

@stuqdog stuqdog marked this pull request as draft November 22, 2024 20:29
@stuqdog stuqdog marked this pull request as ready for review November 22, 2024 20:46
@benjirewis
Copy link
Member

cc @zaporter-work

@zaporter-work
Copy link
Member

zaporter-work commented Nov 25, 2024

This seems like a good improvement -- I like the flags object (hopefully people will catch name-mismatches manually). However, If you're interested in other options, I've enjoyed this pattern for some of my personal projects. It takes a bit of naming-awareness, but it is fairly simple. This uses urfavecli/v3 instead of v2 (though it might be compatible with v2):
main.go:

func main() {
	cmd := &cli.Command{
		Name: "sudoku",
		Commands: []*cli.Command{
			createStatsCli(),
			createDataCli(),
			createGraphCli(),
			createEvaluateCli(),
			createExperimentCli(),
			lambda.CreateLambdaCli(),
		},
	}
	if err := cmd.Run(context.Background(), os.Args); err != nil {
		log.Fatalln("error:", err)
	}
}

example stat.go

func createStatsCli() *cli.Command {
	graphPath := ""
        printUnfinishedSteps := false
	action := func(_ context.Context, _ *cli.Command) error {
                // SOME VERY SHORT FUNCTION
                // (that uses the vars in the parent scope)
		graph, err := LoadGraphFromFile(graphPath)
		if err != nil {
			return err
		}

		var pStats []ProblemStats
		for _, p := range graph.Problems {
			pStats = append(pStats, p.GenStats(graph))
		}
		PrintStatsInfo(pStats, printUnfinishedSteps)
		return nil
	}

	return &cli.Command{
		Name: "stats",
		Arguments: []cli.Argument{
			&cli.StringArg{
				Name:        "graph",
				Destination: &graphPath,
			},
		},
		Flags: []cli.Flag{
			&cli.BoolFlag{
				Name:        "print-unfinished-steps",
				Destination: &printUnfinishedSteps,
			},
		},
		Action: action,
                // can support nested Commands via
               // Commands: []*cli.Command{moreCreateInvocations()}
	}
}

This has the benefit of not using runtime reflection -- though it does scatter a lot of the cli tree out into different functions instead of one giant object. I like it for personal projects because the Destination: &var means I don't have to worry about any magic strings in contexts. (it also makes it easy to use Arguments instead of just Flags)


No pressure to use this though! Just wanted to throw something out there to consider while you restructure the flag parsing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
safe to test This pull request is marked safe to test from a trusted zone
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants