Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
package catalog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/descrybe/descrybe-v2/apps/api/internal/feeds"
|
||||
)
|
||||
|
||||
// linkFeedSpecificationsIntoProduct expands mapped_data.specifications HTML/flat
|
||||
// blobs into attribute_key→value maps and merges them into attributes when empty.
|
||||
// Mutates item in place for GET product responses (does not persist).
|
||||
func linkFeedSpecificationsIntoProduct(item map[string]any) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
mapped, _ := item["mapped_data"].(map[string]any)
|
||||
if mapped == nil {
|
||||
return
|
||||
}
|
||||
linked := extractLinkedSpecs(mapped)
|
||||
if len(linked) == 0 {
|
||||
return
|
||||
}
|
||||
// Prefer structured object in response mapped_data for the Attributes/Feed UI.
|
||||
if specs := mapped["specifications"]; isLooseSpecBlob(specs) {
|
||||
mapped["specifications"] = linked
|
||||
item["mapped_data"] = mapped
|
||||
} else if specs := mapped["specs"]; isLooseSpecBlob(specs) {
|
||||
mapped["specs"] = linked
|
||||
item["mapped_data"] = mapped
|
||||
}
|
||||
attrs := asStringAnyMap(item["attributes"])
|
||||
if len(attrs) == 0 {
|
||||
item["attributes"] = linked
|
||||
item["has_attributes"] = true
|
||||
return
|
||||
}
|
||||
merged := make(map[string]any, len(attrs)+len(linked))
|
||||
for k, v := range attrs {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range linked {
|
||||
if existing, ok := merged[k]; ok && strings.TrimSpace(stringifyAny(existing)) != "" {
|
||||
continue
|
||||
}
|
||||
merged[k] = v
|
||||
}
|
||||
item["attributes"] = merged
|
||||
item["has_attributes"] = true
|
||||
}
|
||||
|
||||
func extractLinkedSpecs(mapped map[string]any) map[string]any {
|
||||
out := map[string]any{}
|
||||
put := func(label, value string) {
|
||||
k := feeds.CanonicalAttributeKey(label)
|
||||
if k == "" || strings.TrimSpace(value) == "" {
|
||||
return
|
||||
}
|
||||
if _, exists := out[k]; exists {
|
||||
return
|
||||
}
|
||||
out[k] = value
|
||||
}
|
||||
for _, key := range []string{"specifications", "specs"} {
|
||||
v, ok := mapped[key]
|
||||
if !ok || v == nil {
|
||||
continue
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
for _, p := range feeds.ParseSpecifications(t) {
|
||||
put(p.Label, p.Value)
|
||||
}
|
||||
case map[string]any:
|
||||
for k, val := range t {
|
||||
if strings.EqualFold(k, "_raw") {
|
||||
if s, ok := val.(string); ok {
|
||||
for _, p := range feeds.ParseSpecifications(s) {
|
||||
put(p.Label, p.Value)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
put(k, stringifyAny(val))
|
||||
}
|
||||
case map[string]string:
|
||||
for k, val := range t {
|
||||
if strings.EqualFold(k, "_raw") {
|
||||
for _, p := range feeds.ParseSpecifications(val) {
|
||||
put(p.Label, p.Value)
|
||||
}
|
||||
continue
|
||||
}
|
||||
put(k, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Scalar mapped fields → standard attribute keys (net_height, eprel_id, …).
|
||||
for _, src := range []string{
|
||||
"warranty", "eprel_id", "eprel",
|
||||
"netwidth", "net_width", "netheight", "net_height",
|
||||
"netdepth", "net_depth", "netmass", "net_mass",
|
||||
"productmodel", "product_model",
|
||||
"visina", "sirina", "globina", "teza",
|
||||
} {
|
||||
if s := stringifyAny(mapped[src]); s != "" {
|
||||
put(src, s)
|
||||
}
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func isLooseSpecBlob(v any) bool {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t) != ""
|
||||
case map[string]any:
|
||||
_, hasRaw := t["_raw"]
|
||||
return hasRaw
|
||||
case map[string]string:
|
||||
_, hasRaw := t["_raw"]
|
||||
return hasRaw
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func asStringAnyMap(v any) map[string]any {
|
||||
switch t := v.(type) {
|
||||
case map[string]any:
|
||||
return t
|
||||
case map[string]string:
|
||||
out := make(map[string]any, len(t))
|
||||
for k, val := range t {
|
||||
out[k] = val
|
||||
}
|
||||
return out
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func stringifyAny(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t)
|
||||
case float64, float32, int, int64, bool:
|
||||
return strings.TrimSpace(fmt.Sprint(t))
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprint(t))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user