github.com/christoph-karpowicz/db_mediator@v0.0.0-20210207102849-61a28a1071d8/internal/server/cfg/parser.go (about)

     1  package cfg
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/christoph-karpowicz/db_mediator/internal/util"
     9  )
    10  
    11  const (
    12  	TO_CLAUSE    = "TO"
    13  	WHERE_CLAUSE = "WHERE"
    14  )
    15  
    16  type linkParserError struct {
    17  	errMsg string
    18  }
    19  
    20  func (e *linkParserError) Error() string {
    21  	return fmt.Sprintf("[link parser] %s", e.errMsg)
    22  }
    23  
    24  type mappingParserError struct {
    25  	errMsg string
    26  }
    27  
    28  func (e *mappingParserError) Error() string {
    29  	return fmt.Sprintf("[mapping parser] %s", e.errMsg)
    30  }
    31  
    32  type matcherParserError struct {
    33  	errMsg string
    34  }
    35  
    36  func (e *matcherParserError) Error() string {
    37  	return fmt.Sprintf("['match by' parser] %s", e.errMsg)
    38  }
    39  
    40  // ParseLink uses regexp to split the link string into smaller parts.
    41  func ParseLink(link string) (map[string]string, error) {
    42  	result := make(map[string]string)
    43  	ptrn := `(?iU)^\s*` +
    44  		`\[(?P<` + PSUBEXP_SOURCE_NODE + `>[^\.,\s]+)\.(?P<` + PSUBEXP_SOURCE_COLUMN + `>[^\.,\s]+|"[^\.,]+")(\s+)?(?P<` + PSUBEXP_SOURCE_WHERE + `>` + WHERE_CLAUSE + `\s+[^\s]+.+)?\]` +
    45  		`\s+` + TO_CLAUSE + `\s+` +
    46  		`\[(?P<` + PSUBEXP_TARGET_NODE + `>[^\.,\s]+)\.(?P<` + PSUBEXP_TARGET_COLUMN + `>[^\.,\s]+|"[^\.,]+")(\s+)?(?P<` + PSUBEXP_TARGET_WHERE + `>` + WHERE_CLAUSE + `\s+[^\s]+.+)?\]` +
    47  		`\s*$`
    48  	compiledPtrn := regexp.MustCompile(ptrn)
    49  	matches := compiledPtrn.FindStringSubmatch(link)
    50  	subNames := compiledPtrn.SubexpNames()
    51  
    52  	if len(matches) == 0 {
    53  		return nil, validateLink(link)
    54  	}
    55  
    56  	for i, match := range matches {
    57  		// Skip the first, empty element.
    58  		if i == 0 {
    59  			continue
    60  		}
    61  
    62  		if util.StringSliceContains([]string{PSUBEXP_SOURCE_WHERE, PSUBEXP_TARGET_WHERE}, subNames[i]) {
    63  			parsedWhere := ParseLinkWhere(match)
    64  			result[subNames[i]] = parsedWhere
    65  		} else {
    66  			result[subNames[i]] = match
    67  		}
    68  	}
    69  	result["cmd"] = link
    70  
    71  	return result, nil
    72  }
    73  
    74  func validateLink(link string) error {
    75  	errorsArr := make([]string, 0)
    76  	errorsArr = append(errorsArr, "error in: "+link)
    77  	var err error = nil
    78  
    79  	linkTrimmed := strings.Trim(link, " ")
    80  
    81  	// Source part of the link.
    82  	sourcePartPtrn := regexp.MustCompile(`^\[.+\].*`)
    83  	sourcePartPtrnMatched := sourcePartPtrn.MatchString(linkTrimmed)
    84  	if !sourcePartPtrnMatched {
    85  		errorsArr = append(errorsArr, "a link has to start with a source in square brackets")
    86  	}
    87  	sourceWherePtrn := regexp.MustCompile(`^\[.+\s+` + WHERE_CLAUSE + `\s*\]\s+` + TO_CLAUSE + `.+`)
    88  	if sourcePartPtrnMatched && sourceWherePtrn.MatchString(linkTrimmed) {
    89  		errorsArr = append(errorsArr, "where clause in the source has to be followed by one or more conditions")
    90  	}
    91  	sourceColumnPtrn := regexp.MustCompile(`^\[([^\.,\s]+)\.([^\.,\s]+|"[^\.,]+").*\]\s+` + TO_CLAUSE + `.+`)
    92  	if sourcePartPtrnMatched && !sourceColumnPtrn.MatchString(linkTrimmed) {
    93  		errorsArr = append(errorsArr, "source node or column name is missing")
    94  	}
    95  
    96  	// Middle part of the link.
    97  	middlePartPtrn := regexp.MustCompile(`^\[.+\]\s+` + TO_CLAUSE + `\s+\[.+\]$`)
    98  	if !middlePartPtrn.MatchString(linkTrimmed) {
    99  		errorsArr = append(errorsArr, "there has to be a '"+TO_CLAUSE+"' keyword between the source and target")
   100  	}
   101  
   102  	// Target part of the link.
   103  	targetPartPtrn := regexp.MustCompile(`.*\[.+\]$`)
   104  	targetPartPtrnMatched := targetPartPtrn.MatchString(linkTrimmed)
   105  	if !targetPartPtrnMatched {
   106  		errorsArr = append(errorsArr, "a link has to end with a target in square brackets")
   107  	}
   108  	targetWherePtrn := regexp.MustCompile(`.*` + TO_CLAUSE + `\s+\[.+\s+` + WHERE_CLAUSE + `\s*\]$`)
   109  	if targetPartPtrnMatched && targetWherePtrn.MatchString(linkTrimmed) {
   110  		errorsArr = append(errorsArr, "where clause in the target has to be followed by one or more conditions")
   111  	}
   112  	targetColumnPtrn := regexp.MustCompile(`.+\[([^\.,\s]+)\.([^\.,\s]+|"[^\.,]+").*\]\s*$`)
   113  	if sourcePartPtrnMatched && !targetColumnPtrn.MatchString(linkTrimmed) {
   114  		errorsArr = append(errorsArr, "target node or column name is missing")
   115  	}
   116  
   117  	// no specific erros found
   118  	if len(errorsArr) == 1 {
   119  		errorsArr = append(errorsArr, "there's a syntax error in the link")
   120  	}
   121  
   122  	if len(errorsArr) > 1 {
   123  		errorsArrJoined := strings.Join(errorsArr, "\n")
   124  		err = &linkParserError{errMsg: errorsArrJoined}
   125  	}
   126  	return err
   127  }
   128  
   129  // ParseLinkWhere uses regexp to split the link's where clause into smaller parts.
   130  func ParseLinkWhere(where string) string {
   131  	ptrn := `(?iU)^\s*` + WHERE_CLAUSE + `\s+`
   132  	compiledPtrn := regexp.MustCompile(ptrn)
   133  	result := compiledPtrn.ReplaceAll([]byte(where), []byte(""))
   134  	resultAsString := string(result)
   135  
   136  	return resultAsString
   137  }
   138  
   139  // ParseMapping uses regexp to split the mapping string into smaller parts.
   140  func ParseMapping(mapping string) (map[string]string, error) {
   141  	result := make(map[string]string)
   142  	ptrn := `(?iU)^\s*` +
   143  		`(?P<` + PSUBEXP_SOURCE_NODE + `>[^\.,]+)\.(?P<` + PSUBEXP_SOURCE_COLUMN + `>[^\.,]+)` +
   144  		`\s+` + TO_CLAUSE + `\s+` +
   145  		`(?P<` + PSUBEXP_TARGET_NODE + `>[^\.,]+)\.(?P<` + PSUBEXP_TARGET_COLUMN + `>[^\.,]+)` +
   146  		`\s*$`
   147  	compiledPtrn := regexp.MustCompile(ptrn)
   148  	matches := compiledPtrn.FindStringSubmatch(mapping)
   149  	subNames := compiledPtrn.SubexpNames()
   150  
   151  	if len(matches) == 0 {
   152  		return nil, validateMapping(mapping)
   153  	}
   154  
   155  	for i, match := range matches {
   156  		// Skip the first, empty element.
   157  		if i == 0 {
   158  			continue
   159  		}
   160  		result[subNames[i]] = removeQuotes(match)
   161  	}
   162  
   163  	return result, nil
   164  }
   165  
   166  func removeQuotes(match string) string {
   167  	result := match
   168  	targetColumnPtrn := regexp.MustCompile(`^".+"$`)
   169  	if len(match) > 0 && targetColumnPtrn.MatchString(match) {
   170  		result = match[1 : len(match)-1]
   171  	}
   172  	return result
   173  }
   174  
   175  func validateMapping(mapping string) error {
   176  	errorsArr := make([]string, 0)
   177  	errorsArr = append(errorsArr, "error in: "+mapping)
   178  	var err error = nil
   179  
   180  	mappingTrimmed := strings.Trim(mapping, " ")
   181  	spacePtrn := regexp.MustCompile(`\s+`)
   182  	mappingSplit := spacePtrn.Split(mappingTrimmed, -1)
   183  
   184  	if len(mappingSplit) > 2 && mappingSplit[1] != TO_CLAUSE {
   185  		errorsArr = append(errorsArr, "there has to be a '"+TO_CLAUSE+"' keyword between the source and target nodes")
   186  	}
   187  	if len(mappingSplit) == 2 && mappingSplit[0] == TO_CLAUSE {
   188  		errorsArr = append(errorsArr, "there has to be a source column before the '"+TO_CLAUSE+"' keyword")
   189  	}
   190  	if len(mappingSplit) == 2 && mappingSplit[1] == TO_CLAUSE {
   191  		errorsArr = append(errorsArr, "there has to be a target column after the '"+TO_CLAUSE+"' keyword")
   192  	}
   193  	if len(mappingSplit) > 3 {
   194  		errorsArr = append(errorsArr, "there's redundant data in the mapping")
   195  	}
   196  	if len(mappingSplit) < 3 && len(errorsArr) == 1 {
   197  		errorsArr = append(errorsArr, "there's too little data in the mapping")
   198  	}
   199  
   200  	// no specific erros found
   201  	if len(errorsArr) == 1 {
   202  		errorsArr = append(errorsArr, "there's a syntax error in the mapping")
   203  	}
   204  
   205  	if len(errorsArr) > 1 {
   206  		errorsArrJoined := strings.Join(errorsArr, "\n")
   207  		err = &mappingParserError{errMsg: errorsArrJoined}
   208  	}
   209  	return err
   210  }
   211  
   212  // ParseIdsMatcherMethod prepares "ids" method's arguments.
   213  func ParseIdsMatcherMethod(args []string) ([][]string, error) {
   214  	argsSplt := make([][]string, 0)
   215  	for _, arg := range args {
   216  		argSplt := strings.Split(arg, ".")
   217  		argsSplt = append(argsSplt, argSplt)
   218  	}
   219  
   220  	validationErr := validateIdsMatcherMethod(args, argsSplt)
   221  	if validationErr != nil {
   222  		return nil, validationErr
   223  	}
   224  
   225  	return argsSplt, nil
   226  }
   227  
   228  func validateIdsMatcherMethod(args []string, argsSplt [][]string) error {
   229  	errorsArr := make([]string, 0)
   230  	var err error = nil
   231  
   232  	if len(args) > 2 {
   233  		errorsArr = append(errorsArr, "too many arguments given for this match method")
   234  	}
   235  	if len(args) < 2 {
   236  		errorsArr = append(errorsArr, "too few arguments given for this match method")
   237  	}
   238  	if len(args) == 2 && (len(argsSplt[0]) != 2 || len(argsSplt[1]) != 2) {
   239  		errorsArr = append(errorsArr, "each argument has to consist of node name and ID column name separated by a dot")
   240  	}
   241  	if len(errorsArr) == 0 && argsSplt[0][0] == argsSplt[1][0] {
   242  		errorsArr = append(errorsArr, "\"ids\" match method accepts only external ID column names from different nodes")
   243  	}
   244  
   245  	if len(errorsArr) > 0 {
   246  		errorsArrJoined := strings.Join(errorsArr, "\n")
   247  		err = &matcherParserError{errMsg: errorsArrJoined}
   248  	}
   249  	return err
   250  }