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  }