github.com/suntong/easygen@v5.3.0+incompatible/easygen.go (about)

     1  ////////////////////////////////////////////////////////////////////////////
     2  // Package: easygen
     3  // Purpose: Easy to use universal code/text generator
     4  // Authors: Tong Sun (c) 2015-2021, All rights reserved
     5  ////////////////////////////////////////////////////////////////////////////
     6  
     7  /*
     8  
     9  Package easygen is an easy to use universal code/text generator library.
    10  
    11  It can be used as a text or html generator for arbitrary purposes with arbitrary data and templates.
    12  
    13  It can be used as a code generator, or anything that is structurally repetitive. Some command line parameter handling code generator are provided as examples, including the Go's built-in flag package, and the viper & cobra package.
    14  
    15  Many examples have been provided to showcase its functionality, and different ways to use it.
    16  
    17  */
    18  package easygen
    19  
    20  import (
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"os"
    26  	"path/filepath"
    27  	"regexp"
    28  	"strings"
    29  
    30  	"gopkg.in/yaml.v3"
    31  )
    32  
    33  ////////////////////////////////////////////////////////////////////////////
    34  // Constant and data type/structure definitions
    35  
    36  ////////////////////////////////////////////////////////////////////////////
    37  // Global variables definitions
    38  
    39  // EgData, EasyGen key type
    40  type EgKey = string
    41  
    42  // EgData, EasyGen driven Data
    43  type EgData map[EgKey]interface{}
    44  
    45  // Opts holds the actual values from the command line parameters
    46  var Opts = Options{ExtYaml: ".yaml", ExtJson: ".json", ExtTmpl: ".tmpl"}
    47  
    48  ////////////////////////////////////////////////////////////////////////////
    49  // Function definitions
    50  
    51  // ReadDataFiles reads in the driving data from the given file, which can
    52  // be optionally without the defined extension, and can be a comma-separated
    53  // string for multiple data files.
    54  func ReadDataFiles(fileName string) EgData {
    55  	var m EgData
    56  	for _, dataFn := range strings.Split(fileName, ",") {
    57  		m = ReadDataFile(dataFn, m)
    58  		if Opts.Debug >= 1 {
    59  			fmt.Fprintf(os.Stderr, "[%s] After reading file %s:\n  %+v\n", progname, dataFn, m)
    60  		}
    61  	}
    62  	return m
    63  }
    64  
    65  // ReadDataFile reads in the driving data from the given file, which can
    66  // be optionally without the defined extension
    67  func ReadDataFile(fileName string, ms ...EgData) EgData {
    68  	if IsExist(fileName + Opts.ExtYaml) {
    69  		return ReadYamlFile(fileName+Opts.ExtYaml, ms...)
    70  	} else if IsExist(fileName + Opts.ExtJson) {
    71  		return ReadJsonFile(fileName+Opts.ExtJson, ms...)
    72  	} else if IsExist(fileName) {
    73  		verbose("Reading exist Data File", 3)
    74  		fext := filepath.Ext(fileName)
    75  		fext = fext[1:] // ignore the leading "."
    76  		if regexp.MustCompile(`(?i)^y`).MatchString(fext) {
    77  			verbose("Reading YAML file", 3)
    78  			return ReadYamlFile(fileName, ms...)
    79  		} else if regexp.MustCompile(`(?i)^j`).MatchString(fext) {
    80  			return ReadJsonFile(fileName, ms...)
    81  		} else {
    82  			checkError(fmt.Errorf("Unsupported file extension for DataFile '%s'", fileName))
    83  		}
    84  	} else if fileName == "-" {
    85  		// from stdin
    86  		// Yaml format is a superset of JSON, it read Json file just as fine
    87  		return ReadYamlFile(fileName)
    88  	}
    89  	checkError(fmt.Errorf("DataFile '%s' cannot be found", fileName))
    90  	return nil
    91  }
    92  
    93  // ReadYamlFile reads given YAML file as EgData
    94  func ReadYamlFile(fileName string, ms ...EgData) EgData {
    95  	var source []byte
    96  	var err error
    97  	if fileName == "-" {
    98  		source, err = ioutil.ReadAll(os.Stdin)
    99  		checkError(err)
   100  	} else {
   101  		source, err = ioutil.ReadFile(fileName)
   102  		checkError(err)
   103  	}
   104  
   105  	m := EgData{}
   106  	if len(ms) > 0 {
   107  		m = ms[0]
   108  	}
   109  
   110  	err = yaml.Unmarshal(source, &m)
   111  	checkError(err)
   112  
   113  	return m
   114  }
   115  
   116  // ReadJsonFile reads given JSON file as EgData
   117  func ReadJsonFile(fileName string, ms ...EgData) EgData {
   118  	source, err := ioutil.ReadFile(fileName)
   119  	checkError(err)
   120  
   121  	m := EgData{}
   122  	if len(ms) > 0 {
   123  		m = ms[0]
   124  	}
   125  
   126  	err = json.Unmarshal(source, &m)
   127  	checkError(err)
   128  
   129  	//fmt.Printf("] Input %v\n", m)
   130  	return m
   131  }
   132  
   133  // IsExist checks if the given file exist
   134  func IsExist(fileName string) bool {
   135  	//fmt.Printf("] Checking %s\n", fileName)
   136  	_, err := os.Stat(fileName)
   137  	return err == nil || os.IsExist(err)
   138  	// CAUTION! os.IsExist(err) != !os.IsNotExist(err)
   139  	// https://gist.github.com/mastef/05f46d3ab2f5ed6a6787#file-isexist_vs_isnotexist-go-L35-L56
   140  }
   141  
   142  // Process will process the standard easygen input: the `fileName` is for both template and data file name, and produce output from the template according to the corresponding driving data.
   143  // Process() is using the V3's calling convention and *only* works properly in V4+ in the case that there is only one fileName passed to it. If need to pass more files, use Process2() instead.
   144  func Process(t Template, wr io.Writer, fileNames ...string) error {
   145  	return Process2(t, wr, fileNames[0], fileNames[:1]...)
   146  }
   147  
   148  // Process2 will process the case that *both* template and data file names are given, and produce output according to the given template and driving data files,
   149  // specified via fileNameTempl and fileNames respectively.
   150  // fileNameTempl can be a comma-separated string giving many template files
   151  func Process2(t Template, wr io.Writer, fileNameTempl string, fileNames ...string) error {
   152  	for _, dataFn := range fileNames {
   153  		for _, templateFn := range strings.Split(fileNameTempl, ",") {
   154  			err := Process1(t, wr, templateFn, dataFn)
   155  			checkError(err)
   156  		}
   157  	}
   158  	return nil
   159  }
   160  
   161  // Process1 will process a *single* case where both template and data file names are given, and produce output according to the given template and driving data files,
   162  // specified via fileNameTempl and fileName respectively.
   163  // fileNameTempl is not a comma-separated string, but for a single template file.
   164  // However, the fileName can be a comma-separated string for multiple data files.
   165  func Process1(t Template, wr io.Writer, fileNameTempl string, fileName string) error {
   166  	m := ReadDataFiles(fileName)
   167  
   168  	// template file
   169  	fileName = fileNameTempl
   170  	fileNameT := fileNameTempl
   171  	if IsExist(fileName + Opts.ExtTmpl) {
   172  		fileNameT = fileName + Opts.ExtTmpl
   173  	} else {
   174  		// guard against that fileNameTempl passed with Opts.ExtYaml extension
   175  		if fileName[len(fileName)-len(Opts.ExtYaml):] == Opts.ExtYaml {
   176  			idx := strings.LastIndex(fileName, ".")
   177  			fileName = fileName[:idx]
   178  			if IsExist(fileName + Opts.ExtTmpl) {
   179  				fileNameT = fileName + Opts.ExtTmpl
   180  			}
   181  		} else if IsExist(fileName) {
   182  			// fileNameTempl passed with Opts.ExtTmpl already
   183  			fileNameT = fileName
   184  		}
   185  	}
   186  
   187  	return Execute(t, wr, fileNameT, m)
   188  }
   189  
   190  // Execute0 will execute the Template given as strTempl with the given data map `m` (i.e., no template file and no data file).
   191  // It parses text template strTempl then applies it to to the specified data
   192  // object m, and writes the output to wr. If an error occurs executing the
   193  // template or writing its output, execution stops, but partial results may
   194  // already have been written to the output writer. A template may be
   195  // executed safely in parallel, although if parallel executions share a
   196  // Writer the output may be interleaved.
   197  func Execute0(t Template, wr io.Writer, strTempl string, m EgData) error {
   198  	verbose("Execute with template string: "+strTempl, 2)
   199  	tmpl, err := t.Parse(strTempl)
   200  	checkError(err)
   201  	return tmpl.Execute(wr, m)
   202  }
   203  
   204  // Execute will execute the Template from fileNameT on the given data map `m`.
   205  func Execute(t Template, wr io.Writer, fileNameT string, m EgData) error {
   206  	// 1. Check locally
   207  	verbose("Checking for template locally: "+fileNameT, 2)
   208  	if !IsExist(fileNameT) {
   209  		// 2. Check under /etc/
   210  		command := filepath.Base(os.Args[0])
   211  		templateFile := fmt.Sprintf("/etc/%s/%s", command, fileNameT)
   212  		verbose("Checking at "+templateFile, 2)
   213  		if IsExist(templateFile) {
   214  			fileNameT = templateFile
   215  		} else {
   216  			// 3. Check where executable is
   217  			ex, e := os.Executable()
   218  			if e != nil {
   219  				return e
   220  			}
   221  			fileNameT = filepath.Dir(ex) + string(filepath.Separator) + fileNameT
   222  			verbose("Checking at "+fileNameT, 2)
   223  			if !IsExist(fileNameT) {
   224  				checkError(fmt.Errorf("Template file '%s' cannot be found", fileNameT))
   225  			}
   226  		}
   227  	}
   228  
   229  	tn, err := t.ParseFiles(fileNameT)
   230  	checkError(err)
   231  
   232  	return tn.ExecuteTemplate(wr, filepath.Base(fileNameT), m)
   233  }
   234  
   235  // Process0 will produce output according to the driving data *without* a template file, using the string from strTempl as the template
   236  func Process0(t Template, wr io.Writer, strTempl string, fileNames ...string) error {
   237  	fileName := fileNames[0]
   238  	m := ReadDataFiles(fileName)
   239  
   240  	tmpl, err := t.Parse(strTempl)
   241  	checkError(err)
   242  	return tmpl.Execute(wr, m)
   243  }
   244  
   245  ////////////////////////////////////////////////////////////////////////////
   246  // Support Function definitions
   247  
   248  // Exit if error occurs
   249  func checkError(err error) {
   250  	if err != nil {
   251  		fmt.Fprintf(os.Stderr, "[%s] Fatal error - %s\n", progname, err)
   252  		os.Exit(1)
   253  	}
   254  }
   255  
   256  // verbose will print info to stderr according to the verbose level setting
   257  func verbose(step string, level int) {
   258  	if Opts.Debug >= level {
   259  		print("[", progname, "] ", step, "\n")
   260  	}
   261  }