github.com/wheelercj/pm2md@v0.0.11/cmd/root.go (about) 1 // Copyright 2023 Chris Wheeler 2 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 7 // http://www.apache.org/licenses/LICENSE-2.0 8 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package cmd 16 17 import ( 18 _ "embed" 19 "fmt" 20 "os" 21 "strings" 22 23 "github.com/spf13/cobra" 24 ) 25 26 //go:embed default.tmpl 27 var defaultTmplStr string 28 29 //go:embed minimal.tmpl 30 var minimalTmplStr string 31 32 const defaultTmplName = "default.tmpl" 33 // const minimalTmplName = "minimal.tmpl" 34 35 const short = "Convert a Postman collection to markdown documentation" 36 const jsonHelp = "You can get a JSON file from Postman by exporting a collection as a v2.1 collection" 37 const github = "More help available here: github.com/wheelercj/pm2md" 38 const version = "v0.0.11 (you can check for updates here: https://github.com/wheelercj/pm2md/releases)" 39 const example = ` pm2md collection.json 40 pm2md collection.json output.md 41 pm2md collection.json --template=custom.tmpl 42 pm2md test collection.json custom.tmpl expected.md` 43 44 var Statuses string 45 var CustomTmplPath string 46 var GetDefault bool 47 var GetMinimal bool 48 var ConfirmReplaceExistingFile bool 49 50 var rootCmd = &cobra.Command{ 51 Use: "pm2md [postman_export.json [output.md]]", 52 Short: short, 53 Long: fmt.Sprintf("%s\n\n%s.\n%s", short, jsonHelp, github), 54 Example: example, 55 Version: version, 56 Args: argsFunc, 57 RunE: runFunc, 58 } 59 60 // argsFunc does some input validation on the command args and flags. 61 func argsFunc(cmd *cobra.Command, args []string) error { 62 if len(args) == 0 && (GetDefault || GetMinimal) { 63 return nil 64 } 65 if err := cobra.MinimumNArgs(1)(cmd, args); err != nil { 66 return err 67 } 68 if err := cobra.MaximumNArgs(2)(cmd, args); err != nil { 69 return err 70 } 71 if args[0] != "-" && !strings.HasSuffix(strings.ToLower(args[0]), ".json") { 72 return fmt.Errorf("%q must be \"-\" or end with \".json\"", args[0]) 73 } 74 if len(CustomTmplPath) > 0 && !strings.HasSuffix(CustomTmplPath, ".tmpl") { 75 return fmt.Errorf("%q must end with \".tmpl\"", CustomTmplPath) 76 } 77 return nil 78 } 79 80 // runFunc parses command args and flags, generates plaintext, and saves the result to a 81 // file or prints to stdout. 82 func runFunc(cmd *cobra.Command, args []string) error { 83 destPath, destFile, collection, statusRanges, err := parseInput(cmd, args) 84 if err != nil { 85 return err 86 } 87 if destFile != os.Stdout { 88 defer destFile.Close() 89 } 90 91 err = generateText( 92 collection, 93 destFile, 94 CustomTmplPath, 95 statusRanges, 96 ) 97 if err != nil { 98 fmt.Fprintln(os.Stderr, err) 99 } else if destPath != "-" { 100 fmt.Fprintf(os.Stderr, "Created %q\n", destPath) 101 } 102 return nil 103 } 104 105 // parseInput parses command args and flags, opens the destination file, and returns all 106 // of these results. 107 func parseInput(cmd *cobra.Command, args []string) (string, *os.File, map[string]any, [][]int, error) { 108 if GetDefault { 109 fileName := exportText("default", ".tmpl", defaultTmplStr) 110 fmt.Fprintf(os.Stderr, "Created %q\n", fileName) 111 if len(args) == 0 { 112 os.Exit(0) 113 } 114 } 115 if GetMinimal { 116 fileName := exportText("minimal", ".tmpl", minimalTmplStr) 117 fmt.Fprintf(os.Stderr, "Created %q\n", fileName) 118 if len(args) == 0 { 119 os.Exit(0) 120 } 121 } 122 123 jsonPath := args[0] 124 var destPath string 125 if len(args) == 2 { 126 destPath = args[1] 127 } 128 129 statusRanges, err := parseStatusRanges(Statuses) 130 if err != nil { 131 return "", nil, nil, nil, err 132 } 133 134 var jsonBytes []byte 135 if jsonPath == "-" { 136 jsonBytes, err = ScanStdin() 137 } else { 138 jsonBytes, err = os.ReadFile(jsonPath) 139 } 140 if err != nil { 141 return "", nil, nil, nil, err 142 } 143 collection, err := parseCollection(jsonBytes) 144 if err != nil { 145 return "", nil, nil, nil, err 146 } 147 148 collectionName := collection["info"].(map[string]any)["name"].(string) 149 destFile, destPath, err := openDestFile(destPath, collectionName, ConfirmReplaceExistingFile) 150 if err != nil { 151 return "", nil, nil, nil, err 152 } 153 154 return destPath, destFile, collection, statusRanges, nil 155 } 156 157 // Execute adds all child commands to the root command and sets flags appropriately. 158 // This is called by main.main(). It only needs to happen once to the rootCmd. 159 func Execute() { 160 err := rootCmd.Execute() 161 if err != nil { 162 os.Exit(1) 163 } 164 } 165 166 func init() { 167 rootCmd.AddCommand(testCmd) 168 169 rootCmd.Flags().StringVarP( 170 &Statuses, 171 "statuses", 172 "s", 173 "", 174 "Include only the sample responses with status codes in given range(s)", 175 ) 176 rootCmd.Flags().StringVarP( 177 &CustomTmplPath, 178 "template", 179 "t", 180 "", 181 "Use a custom template for the output", 182 ) 183 rootCmd.Flags().BoolVarP( 184 &GetDefault, 185 "get-default", 186 "d", 187 false, 188 "Creates a file of the default template for customization", 189 ) 190 rootCmd.Flags().BoolVarP( 191 &GetMinimal, 192 "get-minimal", 193 "m", 194 false, 195 "Creates a file of a minimal template for customization", 196 ) 197 rootCmd.Flags().BoolVar( 198 &ConfirmReplaceExistingFile, 199 "replace", 200 false, 201 "Confirm whether to replace a chosen existing output file", 202 ) 203 rootCmd.Flags().MarkHidden("replace") 204 } 205 206 // openDestFile gets the destination file and its path. If the given destination path is 207 // "-", the destination file is os.Stdout. If the given destination path is empty, a new 208 // file is created with a path based on the collection name and the returned path will 209 // be different from the given one. If the given destination path refers to an existing 210 // file and confirmation to replace an existing file is not given, an error is returned. 211 // Any returned file is open. 212 func openDestFile(destPath, collectionName string, confirmReplaceExistingFile bool) (*os.File, string, error) { 213 if destPath == "-" { 214 return os.Stdout, destPath, nil 215 } 216 if len(destPath) == 0 { 217 fileName := FormatFileName(collectionName) 218 if len(fileName) == 0 { 219 fileName = "collection" 220 } 221 destPath = CreateUniqueFileName(fileName, ".md") 222 } else if FileExists(destPath) && !confirmReplaceExistingFile { 223 return nil, "", fmt.Errorf("file %q already exists. Run the command again with the --replace flag to confirm replacing it", destPath) 224 } 225 destFile, err := os.Create(destPath) 226 if err != nil { 227 return nil, "", fmt.Errorf("os.Create: %s", err) 228 } 229 return destFile, destPath, nil 230 }