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 }