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  }