pkg.re/essentialkaos/ek@v12.36.0+incompatible/knf/knf_parser.go (about)

     1  package knf
     2  
     3  // ////////////////////////////////////////////////////////////////////////////////// //
     4  //                                                                                    //
     5  //                         Copyright (c) 2021 ESSENTIAL KAOS                          //
     6  //      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
     7  //                                                                                    //
     8  // ////////////////////////////////////////////////////////////////////////////////// //
     9  
    10  import (
    11  	"bufio"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"path"
    16  	"regexp"
    17  	"strings"
    18  	"sync"
    19  )
    20  
    21  // ////////////////////////////////////////////////////////////////////////////////// //
    22  
    23  const (
    24  	_COMMENT_SYMBOL       = "#"
    25  	_SECTION_START_SYMBOL = "["
    26  	_SECTION_END_SYMBOL   = "["
    27  	_PROP_DELIMITER       = ":"
    28  	_MACRO_START_SYMBOL   = "{"
    29  	_MACRO_END_SYMBOL     = "}"
    30  	_MACRO_DELIMITER      = ":"
    31  )
    32  
    33  // ////////////////////////////////////////////////////////////////////////////////// //
    34  
    35  // macroRE is a regexp for extracting macroses
    36  var macroRE = regexp.MustCompile(`\{([\w\-]+):([\w\-]+)\}`)
    37  
    38  // ////////////////////////////////////////////////////////////////////////////////// //
    39  
    40  // readKNFFile reads KNF file
    41  func readKNFFile(file string) (*Config, error) {
    42  	fd, err := os.OpenFile(path.Clean(file), os.O_RDONLY, 0)
    43  
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  
    48  	defer fd.Close()
    49  
    50  	config, err := readKNFData(fd)
    51  
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  
    56  	config.file = file
    57  
    58  	return config, nil
    59  }
    60  
    61  // readKNFData reads data from given reader
    62  func readKNFData(r io.Reader) (*Config, error) {
    63  	reader := bufio.NewReader(r)
    64  	scanner := bufio.NewScanner(reader)
    65  
    66  	config := &Config{
    67  		data: make(map[string]string),
    68  		mx:   &sync.RWMutex{},
    69  	}
    70  
    71  	var isDataRead bool
    72  	var section string
    73  	var lineNum int
    74  
    75  	for scanner.Scan() {
    76  		line := strings.Trim(scanner.Text(), " \t")
    77  		lineNum++
    78  
    79  		if line == "" || strings.HasPrefix(line, _COMMENT_SYMBOL) {
    80  			continue
    81  		}
    82  
    83  		isDataRead = true
    84  
    85  		if strings.HasPrefix(line, _SECTION_START_SYMBOL) &&
    86  			strings.HasPrefix(line, _SECTION_END_SYMBOL) {
    87  			section = strings.Trim(line, "[]")
    88  			config.data[strings.ToLower(section)] = "!"
    89  			config.sections = append(config.sections, section)
    90  			continue
    91  		}
    92  
    93  		if section == "" {
    94  			return nil, fmt.Errorf("Error at line %d: Data defined before section", lineNum)
    95  		}
    96  
    97  		propName, propValue, err := parseKNFProperty(line, config)
    98  
    99  		if err != nil {
   100  			return nil, fmt.Errorf("Error at line %d: %w", lineNum, err)
   101  		}
   102  
   103  		fullPropName := genPropName(section, propName)
   104  
   105  		if config.HasProp(fullPropName) {
   106  			return nil, fmt.Errorf("Error at line %d: Property \"%s\" defined more than once", lineNum, propName)
   107  		}
   108  
   109  		config.props = append(config.props, fullPropName)
   110  		config.data[strings.ToLower(fullPropName)] = propValue
   111  	}
   112  
   113  	if !isDataRead {
   114  		return nil, fmt.Errorf("Configuration file doesn't contain any valid data")
   115  	}
   116  
   117  	return config, scanner.Err()
   118  }
   119  
   120  // parseKNFProperty parses line with property name and value
   121  func parseKNFProperty(line string, config *Config) (string, string, error) {
   122  	di := strings.Index(line, _PROP_DELIMITER)
   123  
   124  	if di == -1 {
   125  		return "", "", fmt.Errorf("Property must have \":\" as a delimiter")
   126  	}
   127  
   128  	name, value := line[:di], line[di+1:]
   129  
   130  	name = strings.Trim(name, " \t")
   131  	value = strings.Trim(value, " \t")
   132  
   133  	if !strings.Contains(value, _MACRO_START_SYMBOL) &&
   134  		!strings.Contains(value, _MACRO_END_SYMBOL) {
   135  		return name, value, nil
   136  	}
   137  
   138  	var err error
   139  
   140  	value, err = evalMacroses(value, config)
   141  
   142  	if err != nil {
   143  		return "", "", err
   144  	}
   145  
   146  	return name, value, nil
   147  }
   148  
   149  // evalMacroses evaluates all macroses in given string
   150  func evalMacroses(value string, config *Config) (string, error) {
   151  	macroses := macroRE.FindAllStringSubmatch(value, -1)
   152  
   153  	for _, macros := range macroses {
   154  		full, section, prop := macros[0], macros[1], macros[2]
   155  
   156  		if !config.HasProp(genPropName(section, prop)) {
   157  			return "", fmt.Errorf("Unknown property %s", full)
   158  		}
   159  
   160  		propVal := config.GetS(genPropName(section, prop))
   161  		value = strings.ReplaceAll(value, full, propVal)
   162  	}
   163  
   164  	return value, nil
   165  }
   166  
   167  // genPropName generates "full property name" which contains section and
   168  // property name
   169  func genPropName(section, prop string) string {
   170  	return section + _PROP_DELIMITER + prop
   171  }