github.com/wheelercj/pm2md@v0.0.11/cmd/generate_text.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 "encoding/json" 19 "fmt" 20 "os" 21 "slices" 22 "strconv" 23 "strings" 24 "text/template" 25 ) 26 27 // generateText converts a collection to plaintext and saves it into the given open file 28 // without closing the file. `Seek(0, 0)` is then called on the file so the file pointer 29 // is at the beginning of the file unless an error occurs. If the given template path is 30 // empty, the default template is used. If any status ranges are given, responses with 31 // statuses outside those ranges are removed from the collection. A `level` integer 32 // property is added to each "item" and each "response" object within the collection. 33 // The level starts at 1 for the outermost item object and increases by 1 for each level 34 // of item nesting. 35 func generateText(collection map[string]any, openAnsFile *os.File, tmplPath string, statusRanges [][]int) error { 36 filterResponsesByStatus(collection, statusRanges) 37 addLevelProperty(collection) 38 39 tmplName, tmplStr, err := loadTmpl(tmplPath) 40 if err != nil { 41 return err 42 } 43 44 return executeTmpl(collection, openAnsFile, tmplName, tmplStr) 45 } 46 47 // parseCollection converts a collection from a slice of bytes of JSON to a map. 48 func parseCollection(jsonBytes []byte) (map[string]any, error) { 49 var collection map[string]any 50 if err := json.Unmarshal(jsonBytes, &collection); err != nil { 51 return nil, err 52 } 53 if collection["info"].(map[string]any)["schema"] != "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" { 54 return nil, fmt.Errorf("unknown JSON schema. When exporting from Postman, export as Collection v2.1") 55 } 56 57 return collection, nil 58 } 59 60 // parseStatusRanges converts a string of status ranges to a slice of slices of 61 // integers. The slice may be nil, but any inner slices each have two elements: the 62 // start and end of the range. Example inputs: "200", "200-299", "200-299,400-499", 63 // "200-200". 64 func parseStatusRanges(statusesStr string) ([][]int, error) { 65 if len(statusesStr) == 0 { 66 return nil, nil 67 } 68 statusRangeStrs := strings.Split(statusesStr, ",") 69 statusRanges := make([][]int, len(statusRangeStrs)) 70 for i, statusRangeStr := range statusRangeStrs { 71 startAndEnd := strings.Split(statusRangeStr, "-") 72 if len(startAndEnd) > 2 { 73 return nil, fmt.Errorf("invalid status format. There should be zero or one dashes in %s", statusRangeStr) 74 } 75 start, err := strconv.Atoi(startAndEnd[0]) 76 if err != nil { 77 return nil, fmt.Errorf("invalid status range format. Expected an integer, got %q", startAndEnd[0]) 78 } 79 end := start 80 if len(startAndEnd) > 1 { 81 end, err = strconv.Atoi(startAndEnd[1]) 82 if err != nil { 83 return nil, fmt.Errorf("invalid status range format. Expected an integer, got %q", startAndEnd[1]) 84 } 85 } 86 statusRanges[i] = make([]int, 2) 87 statusRanges[i][0] = start 88 statusRanges[i][1] = end 89 } 90 91 return statusRanges, nil 92 } 93 94 // filterResponsesByStatus removes all sample responses with status codes outside the 95 // given range(s). If no status ranges are given, the collection remains unchanged. 96 func filterResponsesByStatus(collection map[string]any, statusRanges [][]int) { 97 if len(statusRanges) == 0 { 98 return 99 } 100 items := collection["item"].([]any) 101 _filterResponsesByStatus(items, statusRanges) 102 } 103 104 func _filterResponsesByStatus(items []any, statusRanges [][]int) { 105 for _, itemAny := range items { 106 item := itemAny.(map[string]any) 107 if subItemsAny, ok := item["item"]; ok { // if item is a folder 108 _filterResponsesByStatus(subItemsAny.([]any), statusRanges) 109 } else { // if item is an endpoint 110 responses := item["response"].([]any) 111 for j := len(responses) - 1; j >= 0; j-- { 112 response := responses[j].(map[string]any) 113 inRange := false 114 for _, statusRange := range statusRanges { 115 code := int(response["code"].(float64)) 116 if code >= statusRange[0] && code <= statusRange[1] { 117 inRange = true 118 break 119 } 120 } 121 if !inRange { 122 responses = slices.Delete(responses, j, j+1) 123 item["response"] = responses 124 } 125 } 126 } 127 } 128 } 129 130 // addLevelProperty adds a "level" property within each "item" and each "response" 131 // object. The level starts at 1 for the outermost item object and increases by 1 for 132 // each level of item nesting. 133 func addLevelProperty(collection map[string]any) { 134 items := collection["item"].([]any) 135 _addLevelProperty(items, 1) 136 } 137 138 func _addLevelProperty(items []any, level int) { 139 for _, itemAny := range items { 140 item := itemAny.(map[string]any) 141 item["level"] = level 142 if subItemsAny, ok := item["item"]; ok { // if item is a folder 143 _addLevelProperty(subItemsAny.([]any), level+1) 144 } else { // if item is an endpoint 145 responses := item["response"].([]any) 146 for _, responseAny := range responses { 147 response := responseAny.(map[string]any) 148 response["level"] = level 149 } 150 } 151 } 152 } 153 154 // executeTmpl uses a template and FuncMap to convert the collection to plaintext 155 // and saves to the given open destination file without closing it. `Seek(0, 0)` is then 156 // called on the file so the file pointer is at the beginning of the file unless an 157 // error occurs. 158 func executeTmpl(collection map[string]any, openAnsFile *os.File, tmplName, tmplStr string) error { 159 tmpl, err := template.New(tmplName).Funcs(funcMap).Parse(tmplStr) 160 if err != nil { 161 return fmt.Errorf("template parsing error: %s", err) 162 } 163 164 err = tmpl.Execute(openAnsFile, collection) 165 if err != nil { 166 return err 167 } 168 169 openAnsFile.Seek(0, 0) 170 return nil 171 }