191 lines
4.2 KiB
Go
191 lines
4.2 KiB
Go
package processing
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// knownKeyAliases maps vendor / feed keys onto canonical standard-field keys.
|
||
|
|
var knownKeyAliases = map[string]string{
|
||
|
|
"ean": "gtin",
|
||
|
|
"ean13": "gtin",
|
||
|
|
"barcode": "gtin",
|
||
|
|
"sku": "sku",
|
||
|
|
"product_name": "name",
|
||
|
|
"title": "name",
|
||
|
|
"producttitle": "name",
|
||
|
|
"desc": "description",
|
||
|
|
"body": "description",
|
||
|
|
"shortdescription": "description",
|
||
|
|
"product_type": "category",
|
||
|
|
"producttype": "category",
|
||
|
|
"brand_name": "brand",
|
||
|
|
"manufacturer": "brand",
|
||
|
|
"mainimage": "image",
|
||
|
|
"main_image": "image",
|
||
|
|
"image_url": "image",
|
||
|
|
"imageurl": "image",
|
||
|
|
"purchaseprice": "price",
|
||
|
|
"purchase_price": "price",
|
||
|
|
"sellingprice": "price",
|
||
|
|
"netwidth": "width",
|
||
|
|
"netheight": "height",
|
||
|
|
"netdepth": "depth",
|
||
|
|
"netmass": "weight",
|
||
|
|
"weight_kg": "weight",
|
||
|
|
"eprelid": "eprel_id",
|
||
|
|
"stockstatus": "stock_status",
|
||
|
|
"stock": "stock",
|
||
|
|
"spec": "specifications",
|
||
|
|
"specs": "specifications",
|
||
|
|
"specification": "specifications",
|
||
|
|
}
|
||
|
|
|
||
|
|
// NormalizeMapped flattens aliases, trims strings, drops empty/#text wrappers,
|
||
|
|
// and coerces obvious zero-dimension placeholders to empty (not fake "0").
|
||
|
|
func NormalizeMapped(mapped, raw map[string]any) map[string]any {
|
||
|
|
out := make(map[string]any)
|
||
|
|
mergeNormalized(out, raw)
|
||
|
|
mergeNormalized(out, mapped) // mapped wins
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
func mergeNormalized(dst, src map[string]any) {
|
||
|
|
if src == nil {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
for k, v := range src {
|
||
|
|
canon := canonicalizeKey(k)
|
||
|
|
nv := normalizeValue(canon, v)
|
||
|
|
if nv == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if _, exists := dst[canon]; exists && isEmptyValue(nv) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
dst[canon] = nv
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func canonicalizeKey(k string) string {
|
||
|
|
compact := strings.ToLower(strings.TrimSpace(k))
|
||
|
|
compact = strings.ReplaceAll(compact, "-", "_")
|
||
|
|
compact = strings.ReplaceAll(compact, " ", "_")
|
||
|
|
noUnderscore := strings.ReplaceAll(compact, "_", "")
|
||
|
|
if alias, ok := knownKeyAliases[compact]; ok {
|
||
|
|
return alias
|
||
|
|
}
|
||
|
|
if alias, ok := knownKeyAliases[noUnderscore]; ok {
|
||
|
|
return alias
|
||
|
|
}
|
||
|
|
return compact
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeValue(key string, v any) any {
|
||
|
|
if v == nil {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
switch t := v.(type) {
|
||
|
|
case string:
|
||
|
|
s := strings.TrimSpace(t)
|
||
|
|
if s == "" {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if isDimensionKey(key) && isZeroishString(s) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return SanitizeText(s)
|
||
|
|
case float64:
|
||
|
|
if isDimensionKey(key) && t == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return t
|
||
|
|
case float32:
|
||
|
|
if isDimensionKey(key) && t == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return float64(t)
|
||
|
|
case int:
|
||
|
|
if isDimensionKey(key) && t == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return t
|
||
|
|
case int64:
|
||
|
|
if isDimensionKey(key) && t == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return t
|
||
|
|
case bool:
|
||
|
|
return t
|
||
|
|
case map[string]any:
|
||
|
|
if text, ok := t["#text"]; ok {
|
||
|
|
return normalizeValue(key, text)
|
||
|
|
}
|
||
|
|
if text, ok := t["text"]; ok {
|
||
|
|
return normalizeValue(key, text)
|
||
|
|
}
|
||
|
|
nested := make(map[string]any, len(t))
|
||
|
|
for nk, nv := range t {
|
||
|
|
if nn := normalizeValue(canonicalizeKey(nk), nv); nn != nil {
|
||
|
|
nested[canonicalizeKey(nk)] = nn
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(nested) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return nested
|
||
|
|
case []any:
|
||
|
|
if len(t) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
out := make([]any, 0, len(t))
|
||
|
|
for _, item := range t {
|
||
|
|
if nn := normalizeValue(key, item); nn != nil {
|
||
|
|
out = append(out, nn)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(out) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return out
|
||
|
|
default:
|
||
|
|
s := strings.TrimSpace(fmt.Sprint(t))
|
||
|
|
if s == "" || s == "<nil>" {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if isDimensionKey(key) && isZeroishString(s) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return SanitizeText(s)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func isDimensionKey(key string) bool {
|
||
|
|
switch key {
|
||
|
|
case "width", "height", "depth", "weight", "length", "net_width", "net_height", "net_depth", "net_mass":
|
||
|
|
return true
|
||
|
|
default:
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func isZeroishString(s string) bool {
|
||
|
|
s = strings.TrimSpace(strings.ToLower(s))
|
||
|
|
return s == "0" || s == "0.0" || s == "0,0" || s == "0.00"
|
||
|
|
}
|
||
|
|
|
||
|
|
func isEmptyValue(v any) bool {
|
||
|
|
if v == nil {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
switch t := v.(type) {
|
||
|
|
case string:
|
||
|
|
return strings.TrimSpace(t) == ""
|
||
|
|
case map[string]any:
|
||
|
|
return len(t) == 0
|
||
|
|
case []any:
|
||
|
|
return len(t) == 0
|
||
|
|
default:
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|