Files

189 lines
5.4 KiB
Go
Raw Permalink Normal View History

package feeds
import "strings"
// MappingSuggestion is a suggested source→target pair from schema extraction.
type MappingSuggestion struct {
Source string `json:"source"`
Target string `json:"target"`
Confidence string `json:"confidence"`
Score float64 `json:"score"`
}
var sourceAliases = map[string][]string{
"gtin": {"gtin"},
"ean": {"gtin"},
"upc": {"gtin"},
"barcode": {"gtin"},
"title": {"title"},
"name": {"title"},
"productname": {"title"},
"brand": {"brand"},
"manufacturer": {"brand"},
"description": {"description"},
"desc": {"description"},
"price": {"price"},
"regularprice": {"price"},
"saleprice": {"sale_price", "price"},
"currency": {"currency"},
"image": {"image_url", "main_image", "image"},
"imageurl": {"image_url", "main_image", "image"},
"imagelink": {"image_url", "main_image", "image"},
"mainimage": {"image_url", "main_image", "image"},
"mainimageurl": {"image_url", "main_image", "image"},
"link": {"product_url"},
"url": {"product_url"},
"producturl": {"product_url"},
"sku": {"sku"},
"mpn": {"mpn"},
"category": {"category"},
"availability": {"availability"},
"stockstatus": {"availability", "stock_status"},
"stock": {"stock"},
"quantity": {"stock"},
"qty": {"stock"},
"color": {"color"},
"size": {"size"},
"material": {"material"},
"weight": {"weight"},
"netmass": {"weight"},
"purchaseprice": {"purchase_price", "price"},
"buyprice": {"purchase_price"},
"cost": {"purchase_price"},
"costprice": {"purchase_price"},
"officiallink": {"official_link"},
"warranty": {"warranty"},
"garancija": {"warranty"},
"service": {"service"},
"servis": {"service"},
"productmodel": {"product_model"},
"model": {"product_model"},
"modelnumber": {"product_model"},
"eprelid": {"eprel_id"},
"eprel": {"eprel_id"},
"specifications": {"specs", "specifications"},
"specs": {"specs", "specifications"},
"specification": {"specs", "specifications"},
"techspecs": {"specs", "specifications"},
}
func normalizeSuggestKey(raw string) string {
s := strings.ToLower(strings.TrimSpace(raw))
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
}
}
return b.String()
}
func leafSuggestKey(path string) string {
leaf := path
if i := strings.LastIndex(path, "/"); i >= 0 {
leaf = path[i+1:]
}
if i := strings.LastIndex(leaf, ":"); i >= 0 {
leaf = leaf[i+1:]
}
return normalizeSuggestKey(leaf)
}
// SuggestTarget returns the best canonical target key for a single source name/path,
// or "" when no alias matches. Used during schema extract to annotate fields.
func SuggestTarget(source string) string {
keys := []string{normalizeSuggestKey(source), leafSuggestKey(source)}
for _, key := range keys {
if key == "" {
continue
}
if aliases, ok := sourceAliases[key]; ok && len(aliases) > 0 {
return aliases[0]
}
// Exact identity for known-looking keys already in alias map as targets.
for _, aliases := range sourceAliases {
for _, a := range aliases {
if a == key {
return a
}
}
}
}
return ""
}
// SuggestMappings fuzzy-matches schema fields onto enabled target keys (1:1).
func SuggestMappings(schema []SchemaField, enabledTargets []string) []MappingSuggestion {
enabled := make(map[string]struct{}, len(enabledTargets))
for _, t := range enabledTargets {
t = strings.TrimSpace(t)
if t != "" {
enabled[t] = struct{}{}
}
}
if len(schema) == 0 || len(enabled) == 0 {
return nil
}
used := map[string]struct{}{}
type scored struct {
MappingSuggestion
order int
}
var candidates []scored
for i, f := range schema {
keys := []string{
normalizeSuggestKey(f.FieldName),
leafSuggestKey(f.Path),
normalizeSuggestKey(f.Path),
}
var hit *MappingSuggestion
for _, key := range keys {
if key == "" {
continue
}
if _, ok := enabled[key]; ok {
if _, taken := used[key]; !taken {
hit = &MappingSuggestion{Source: f.Path, Target: key, Confidence: "exact", Score: 1}
break
}
}
if aliases, ok := sourceAliases[key]; ok {
for _, a := range aliases {
if _, ok := enabled[a]; !ok {
continue
}
if _, taken := used[a]; taken {
continue
}
hit = &MappingSuggestion{Source: f.Path, Target: a, Confidence: "alias", Score: 0.95}
break
}
if hit != nil {
break
}
}
}
if hit != nil {
candidates = append(candidates, scored{MappingSuggestion: *hit, order: i})
}
}
// Prefer higher score, then earlier schema order.
for i := 0; i < len(candidates); i++ {
for j := i + 1; j < len(candidates); j++ {
if candidates[j].Score > candidates[i].Score ||
(candidates[j].Score == candidates[i].Score && candidates[j].order < candidates[i].order) {
candidates[i], candidates[j] = candidates[j], candidates[i]
}
}
}
out := make([]MappingSuggestion, 0, len(candidates))
for _, c := range candidates {
if _, taken := used[c.Target]; taken {
continue
}
used[c.Target] = struct{}{}
out = append(out, c.MappingSuggestion)
}
return out
}