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 }