diff --git a/CHANGELOG.md b/CHANGELOG.md index 187649dc59..c76ef19324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ Management of holds is internal, but there are queries for looking up holds on accounts. Holds are also reflected in the `x/bank` module's `SpendableBalances` query. * Add new MaxSupply param to marker module and deprecate MaxTotalSupply. [#1292](https://github.com/provenance-io/provenance/issues/1292). +* Add hidden docgen command to output documentation in different formats. [#1468](https://github.com/provenance-io/provenance/issues/1468). ### Improvements diff --git a/cmd/provenanced/cmd/docgen.go b/cmd/provenanced/cmd/docgen.go new file mode 100644 index 0000000000..f59b6e1ea1 --- /dev/null +++ b/cmd/provenanced/cmd/docgen.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + "github.com/spf13/cobra/doc" + + "github.com/cosmos/cosmos-sdk/version" +) + +var docGenCmdStart = fmt.Sprintf("%s docgen", version.AppName) + +const ( + FlagMarkdown = "markdown" + FlagYaml = "yaml" + FlagRst = "rst" + FlagManpage = "manpage" +) + +func GetDocGenCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "docgen (--markdown) (--yaml) (--rst) (--manpages) [flags]", + Short: "Generates cli documentation for the Provenance Blockchain.", + Long: `Generates cli documentation for the Provenance Blockchain. +Various documentation formats can be generated, including markdown, YAML, RST, and man pages. +To ensure the command's success, you must specify at least one format. +A successful command will not only generate files in the selected formats but also create the target directory if it doesn't already exist.`, + Example: fmt.Sprintf("%s '/tmp' --yaml --markdown", docGenCmdStart), + Hidden: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + markdown, err := cmd.Flags().GetBool(FlagMarkdown) + if err != nil { + return err + } + yaml, err := cmd.Flags().GetBool(FlagYaml) + if err != nil { + return err + } + rst, err := cmd.Flags().GetBool(FlagRst) + if err != nil { + return err + } + manpage, err := cmd.Flags().GetBool(FlagManpage) + if err != nil { + return err + } + + if !markdown && !yaml && !rst && !manpage { + return fmt.Errorf("at least one doc type must be specified") + } + + dir := args[0] + if !exists(dir) { + err = os.Mkdir(dir, 0755) + if err != nil { + return err + } + } + + if markdown { + err = doc.GenMarkdownTree(cmd.Root(), dir) + if err != nil { + return err + } + } + if yaml { + err = doc.GenYamlTree(cmd.Root(), dir) + if err != nil { + return err + } + } + if rst { + err = doc.GenReSTTree(cmd.Root(), dir) + if err != nil { + return err + } + } + if manpage { + err = doc.GenManTree(cmd.Root(), nil, dir) + if err != nil { + return err + } + } + + return nil + }, + } + + cmd.Flags().Bool(FlagMarkdown, false, "Generate documentation in the format of markdown pages.") + cmd.Flags().Bool(FlagYaml, false, "Generate documentation in the format of yaml.") + cmd.Flags().Bool(FlagRst, false, "Generate documentation in the format of rst.") + cmd.Flags().Bool(FlagManpage, false, "Generate documentation in the format of manpages.") + + return cmd +} + +func exists(dir string) bool { + _, err := os.Stat(dir) + return err == nil +} diff --git a/cmd/provenanced/cmd/docgen_test.go b/cmd/provenanced/cmd/docgen_test.go new file mode 100644 index 0000000000..9b92feda7e --- /dev/null +++ b/cmd/provenanced/cmd/docgen_test.go @@ -0,0 +1,186 @@ +package cmd_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/server" + sdksim "github.com/cosmos/cosmos-sdk/simapp" + genutiltest "github.com/cosmos/cosmos-sdk/x/genutil/client/testutil" + provenancecmd "github.com/provenance-io/provenance/cmd/provenanced/cmd" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/log" +) + +func TestDocGen(t *testing.T) { + tests := []struct { + name string + target string + createTarget bool + flags []string + err string + extensions []string + }{ + { + name: "failure - no flags specified", + target: "tmp", + createTarget: true, + err: "at least one doc type must be specified", + }, + { + name: "failure - unsupported flag format", + target: "tmp", + flags: []string{"--bad"}, + createTarget: true, + err: "unknown flag: --bad", + }, + { + name: "failure - invalid target directory", + target: "/tmp/tmp2/tmp3", + flags: []string{"--yaml"}, + createTarget: false, + err: "mkdir %s: no such file or directory", + }, + { + name: "failure - bad yaml value", + target: "tmp", + createTarget: true, + flags: []string{"--yaml=xyz"}, + err: "invalid argument \"xyz\" for \"--yaml\" flag: strconv.ParseBool: parsing \"xyz\": invalid syntax", + }, + { + name: "failure - bad rst value", + target: "tmp", + createTarget: true, + flags: []string{"--rst=xyz"}, + err: "invalid argument \"xyz\" for \"--rst\" flag: strconv.ParseBool: parsing \"xyz\": invalid syntax", + }, + { + name: "failure - bad markdown value", + target: "tmp", + createTarget: true, + flags: []string{"--markdown=xyz"}, + err: "invalid argument \"xyz\" for \"--markdown\" flag: strconv.ParseBool: parsing \"xyz\": invalid syntax", + }, + { + name: "failure - bad manpage value", + target: "tmp", + createTarget: true, + flags: []string{"--manpage=xyz"}, + err: "invalid argument \"xyz\" for \"--manpage\" flag: strconv.ParseBool: parsing \"xyz\": invalid syntax", + }, + { + name: "success - yaml is generated", + target: "tmp", + createTarget: true, + flags: []string{"--yaml"}, + extensions: []string{".yaml"}, + }, + { + name: "success - rst is generated", + target: "tmp", + createTarget: true, + flags: []string{"--rst"}, + extensions: []string{".rst"}, + }, + { + name: "success - manpage is generated", + target: "tmp", + createTarget: true, + flags: []string{"--manpage"}, + extensions: []string{".1"}, + }, + { + name: "success - markdown is generated", + target: "tmp", + createTarget: true, + flags: []string{"--markdown"}, + extensions: []string{".md"}, + }, + { + name: "success - multiple types supported", + target: "tmp", + createTarget: true, + flags: []string{"--markdown", "--yaml"}, + extensions: []string{".md", ".yaml"}, + }, + { + name: "success - generates a new directory", + target: "tmp2", + createTarget: false, + flags: []string{"--yaml"}, + extensions: []string{".md", ".yaml"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + home := t.TempDir() + + targetPath := filepath.Join(home, tc.target) + if tc.createTarget { + require.NoError(t, os.Mkdir(targetPath, 0755), "Mkdir successfully created directory") + } + + logger := log.NewNopLogger() + cfg, err := genutiltest.CreateDefaultTendermintConfig(home) + require.NoError(t, err, "Created default tendermint config") + + appCodec := sdksim.MakeTestEncodingConfig().Codec + err = genutiltest.ExecInitCmd(testMbm, home, appCodec) + require.NoError(t, err, "Executed init command") + + serverCtx := server.NewContext(viper.New(), cfg, logger) + clientCtx := client.Context{}.WithCodec(appCodec).WithHomeDir(home) + + ctx := context.Background() + ctx = context.WithValue(ctx, client.ClientContextKey, &clientCtx) + ctx = context.WithValue(ctx, server.ServerContextKey, serverCtx) + + cmd := provenancecmd.GetDocGenCmd() + args := append([]string{targetPath}, tc.flags...) + cmd.SetArgs(args) + + if len(tc.err) > 0 { + err := cmd.ExecuteContext(ctx) + require.Error(t, err, "should throw an error") + expected := tc.err + if strings.Contains(expected, "%s") { + expected = fmt.Sprintf(expected, targetPath) + } + require.Equal(t, expected, err.Error(), "should return the correct error") + files, err := os.ReadDir(targetPath) + if err != nil { + require.Equal(t, 0, len(files), "should not generate files when failed") + } + } else { + err := cmd.ExecuteContext(ctx) + require.NoError(t, err, "should not return an error") + + files, err := os.ReadDir(targetPath) + require.NoError(t, err, "ReadDir should not return an error") + require.NotZero(t, len(files), "should generate files when successful") + + for _, file := range files { + ext := filepath.Ext(file.Name()) + + contains := false + for _, extension := range tc.extensions { + contains = contains || ext == extension + } + require.True(t, contains, "should generate files with correct extension") + } + } + + if _, err := os.Stat(targetPath); err != nil { + require.NoError(t, os.RemoveAll(targetPath), "RemoveAll should be able to remove the temporary target directory") + } + }) + } +} diff --git a/cmd/provenanced/cmd/root.go b/cmd/provenanced/cmd/root.go index d85cf9ad07..da030bcbdb 100644 --- a/cmd/provenanced/cmd/root.go +++ b/cmd/provenanced/cmd/root.go @@ -154,6 +154,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) { AddMetaAddressCmd(), snapshot.Cmd(newApp), GetPreUpgradeCmd(), + GetDocGenCmd(), ) fixDebugPubkeyRawTypeFlag(rootCmd) diff --git a/go.mod b/go.mod index bf0128d8dc..e01334a0e7 100644 --- a/go.mod +++ b/go.mod @@ -68,6 +68,7 @@ require ( github.com/cosmos/gorocksdb v1.2.0 // indirect github.com/cosmos/iavl v0.19.6 // indirect github.com/cosmos/ledger-cosmos-go v0.12.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/creachadair/taskgroup v0.3.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -144,6 +145,7 @@ require ( github.com/prometheus/procfs v0.9.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rs/cors v1.8.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/go.sum b/go.sum index 245f3211be..8316dfb04a 100644 --- a/go.sum +++ b/go.sum @@ -383,6 +383,7 @@ github.com/cosmos/ledger-cosmos-go v0.12.2 h1:/XYaBlE2BJxtvpkHiBm97gFGSGmYGKunKy github.com/cosmos/ledger-cosmos-go v0.12.2/go.mod h1:ZcqYgnfNJ6lAXe4HPtWgarNEY+B74i+2/8MhZw4ziiI= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM= github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk= @@ -1031,6 +1032,7 @@ github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=