github.com/wheelercj/pm2md@v0.0.11/cmd/utils.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  	"bufio"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"strings"
    24  )
    25  
    26  // FileExists checks if a given file or folder exists on the device.
    27  func FileExists(path string) bool {
    28  	_, err := os.Stat(path)
    29  	return !errors.Is(err, os.ErrNotExist)
    30  }
    31  
    32  // CreateUniqueFileName returns the given file name and extension (concatenated) if no
    33  // file with them exists. Otherwise, a period and a number are inserted before the
    34  // extension to make it unique. The extension must be empty or be a period followed by
    35  // one or more characters. The function panics if the given file name is empty, if the
    36  // extension is only ".", or if the extension is not empty but does not start with a
    37  // period.
    38  func CreateUniqueFileName(fileName, extension string) string {
    39  	if len(fileName) == 0 {
    40  		panic("The file name must not be empty")
    41  	}
    42  	if extension == "." || (len(extension) > 0 && !strings.HasPrefix(extension, ".")) {
    43  		panic("Extension must be empty or be a period followed by one or more characters")
    44  	}
    45  	uniqueFileName := fileName + extension
    46  	for i := 1; FileExists(uniqueFileName); i++ {
    47  		uniqueFileName = fileName + "." + fmt.Sprint(i) + extension
    48  	}
    49  	return uniqueFileName
    50  }
    51  
    52  // FormatFileName takes a file name excluding any file extension and changes it, if
    53  // necessary, to be compatible with all major platforms. Each invalid file name
    54  // character is replaced with a dash, and characters that a file name cannot start or
    55  // end with are trimmed. The invalid characters are `#<>$+%&/\\*|{}!?`'\"=: @`,
    56  // and the invalid start or end characters are ` ._-`.
    57  func FormatFileName(fileName string) string {
    58  	invalidChars := "#<>$+%&/\\*|{}!?`'\"=: @"
    59  	invalidEdgeChars := " ._-"
    60  
    61  	result := make([]byte, len(fileName))
    62  	for i := range fileName {
    63  		if strings.Contains(invalidChars, string(fileName[i])) {
    64  			result[i] = '-'
    65  		} else {
    66  			result[i] = fileName[i]
    67  		}
    68  	}
    69  
    70  	return strings.Trim(string(result), invalidEdgeChars)
    71  }
    72  
    73  // ScanStdin reads input from stdin until it finds EOF or a different error, and then
    74  // returns any input all at once. If EOF is found, the returned error is nil.
    75  func ScanStdin() ([]byte, error) {
    76  	lines := make([]string, 0)
    77  	scanner := bufio.NewScanner(os.Stdin)
    78  	for scanner.Scan() {
    79  		lines = append(lines, scanner.Text())
    80  	}
    81  	if err := scanner.Err(); err != nil {
    82  		return nil, fmt.Errorf("stdin scan error: %s", err)
    83  	}
    84  	return []byte(strings.Join(lines, "\n")), nil
    85  }
    86  
    87  // exportText creates a new file with a unique name based on the given base name (no
    88  // existing file will ever be replaced), saves the given content into it, and returns
    89  // the new file's name. The given file extension must be empty or be a period followed
    90  // by one or more characters.
    91  func exportText(baseName, ext, content string) string {
    92  	uniqueName := CreateUniqueFileName(baseName, ext)
    93  	file, err := os.Create(uniqueName)
    94  	if err != nil {
    95  		fmt.Fprintln(os.Stderr, err)
    96  		os.Exit(1)
    97  	}
    98  	defer file.Close()
    99  	_, err = file.Write([]byte(content))
   100  	if err != nil {
   101  		fmt.Fprintln(os.Stderr, err)
   102  		os.Exit(1)
   103  	}
   104  
   105  	return uniqueName
   106  }
   107  
   108  // AssertGenerateNoDiff converts JSON to plaintext and asserts the result is the same as
   109  // wanted text. wantPath is the path to an existing file containing the wanted output.
   110  // If the given template path is empty, the default template is used. If any status
   111  // ranges are given, responses with statuses outside those ranges will not be present in
   112  // the result.
   113  func AssertGenerateNoDiff(jsonPath, tmplPath, wantPath string, statusRanges [][]int) error {
   114  	jsonBytes, err := os.ReadFile(jsonPath)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	openAnsFile, err := os.CreateTemp("", "pm2md_*.md")
   119  	if err != nil {
   120  		return err
   121  	}
   122  	defer os.Remove(openAnsFile.Name())
   123  	defer openAnsFile.Close()
   124  	wantBytes, err := os.ReadFile(wantPath)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	collection, err := parseCollection(jsonBytes)
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	err = generateText(
   135  		collection,
   136  		openAnsFile,
   137  		tmplPath,
   138  		statusRanges,
   139  	)
   140  	if err != nil {
   141  		return err
   142  	}
   143  	fileInfo, err := openAnsFile.Stat()
   144  	if err != nil {
   145  		return err
   146  	}
   147  	ansBytes := make([]byte, fileInfo.Size())
   148  	_, err = openAnsFile.Read(ansBytes)
   149  	if err != nil && err != io.EOF {
   150  		return err
   151  	}
   152  
   153  	ans := strings.ReplaceAll(string(ansBytes), "\r\n", "\n")
   154  	want := strings.ReplaceAll(string(wantBytes), "\r\n", "\n")
   155  
   156  	return AssertNoDiff(ans, want, "\n")
   157  }
   158  
   159  // AssertNoDiff compares two strings, asserting they have the same number of lines and
   160  // the same content on each line. The strings have lines separated by linesep.
   161  func AssertNoDiff(ans, want, linesep string) error {
   162  	if ans == want {
   163  		return nil
   164  	}
   165  	ansSlice := strings.Split(ans, linesep)
   166  	wantSlice := strings.Split(want, linesep)
   167  	for i := 0; i < len(ansSlice); i++ {
   168  		if i >= len(wantSlice) {
   169  			return fmt.Errorf(
   170  				"actual output longer than expected (want %d lines, got %d).\nContinues with\n  %q",
   171  				len(wantSlice), len(ansSlice), ansSlice[i],
   172  			)
   173  		}
   174  		if ansSlice[i] != wantSlice[i] {
   175  			return fmt.Errorf(
   176  				"difference on line %d\nwant:\n  %q\ngot:\n  %q",
   177  				i+1, wantSlice[i], ansSlice[i],
   178  			)
   179  		}
   180  	}
   181  	if len(ansSlice) < len(wantSlice) {
   182  		return fmt.Errorf(
   183  			"actual output shorter than expected (want %d lines, got %d).\nShould continue with\n  %q",
   184  			len(wantSlice), len(ansSlice), wantSlice[len(ansSlice)],
   185  		)
   186  	}
   187  
   188  	return fmt.Errorf("the actual and expected strings don't match for an unknown reason")
   189  }