fixes
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package eprel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExtractFromAttrs builds the nested EPREL object used by V1 process items and
|
||||
// the dashboard product API (id / label / pdf / energy_class / energy_scale).
|
||||
// Returns nil when no usable EPREL fields are present.
|
||||
//
|
||||
// Nested attrs["eprel"] may use flat export aliases (eprel_id, eprel_label, …);
|
||||
// those are normalized onto the public keys. Flat eprel_* keys fill any gaps.
|
||||
func ExtractFromAttrs(attrs map[string]any) any {
|
||||
if attrs == nil {
|
||||
return nil
|
||||
}
|
||||
out := map[string]any{}
|
||||
if e, ok := attrs["eprel"]; ok && e != nil {
|
||||
switch t := e.(type) {
|
||||
case map[string]any:
|
||||
for k, v := range t {
|
||||
if s := attrString(v); s == "" {
|
||||
continue
|
||||
} else {
|
||||
out[strings.TrimSpace(k)] = s
|
||||
}
|
||||
}
|
||||
default:
|
||||
if s := attrString(e); s != "" {
|
||||
return map[string]any{"id": s}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
put := func(dstKey string, keys ...string) {
|
||||
if attrString(out[dstKey]) != "" {
|
||||
return
|
||||
}
|
||||
for _, k := range keys {
|
||||
if s := attrString(attrs[k]); s != "" {
|
||||
out[dstKey] = s
|
||||
return
|
||||
}
|
||||
// Also accept aliases already copied from a nested object.
|
||||
if s := attrString(out[k]); s != "" {
|
||||
out[dstKey] = s
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
put("id", "eprel_id", "EPRELID", "eprelId")
|
||||
put("label", "eprel_label", "eprel_label_url")
|
||||
put("pdf", "eprel_pdf", "eprel_pdf_url")
|
||||
put("energy_class", "eprel_energy_class", "energyClass")
|
||||
put("energy_scale", "eprel_energy_scale", "energyClassRange", "energyScale")
|
||||
return NormalizeShape(out)
|
||||
}
|
||||
|
||||
// NormalizeShape keeps only the public EPREL keys with non-empty string values.
|
||||
// Alias keys (eprel_id, …) are folded onto id/label/pdf/energy_class/energy_scale.
|
||||
func NormalizeShape(v any) any {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
m, ok := v.(map[string]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if len(m) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := map[string]any{}
|
||||
for k, val := range m {
|
||||
if s := attrString(val); s != "" {
|
||||
out[strings.TrimSpace(k)] = s
|
||||
}
|
||||
}
|
||||
fold := func(dst string, aliases ...string) {
|
||||
if attrString(out[dst]) != "" {
|
||||
for _, a := range aliases {
|
||||
delete(out, a)
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, a := range aliases {
|
||||
if s := attrString(out[a]); s != "" {
|
||||
out[dst] = s
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, a := range aliases {
|
||||
delete(out, a)
|
||||
}
|
||||
}
|
||||
fold("id", "eprel_id", "EPRELID", "eprelId")
|
||||
fold("label", "eprel_label", "eprel_label_url")
|
||||
fold("pdf", "eprel_pdf", "eprel_pdf_url")
|
||||
fold("energy_class", "eprel_energy_class", "energyClass")
|
||||
fold("energy_scale", "eprel_energy_scale", "energyClassRange", "energyScale")
|
||||
|
||||
canonical := map[string]any{}
|
||||
for _, k := range []string{"id", "label", "pdf", "energy_class", "energy_scale"} {
|
||||
if s := attrString(out[k]); s != "" {
|
||||
canonical[k] = s
|
||||
}
|
||||
}
|
||||
if len(canonical) == 0 {
|
||||
return nil
|
||||
}
|
||||
return canonical
|
||||
}
|
||||
|
||||
func attrString(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t)
|
||||
case float64:
|
||||
// JSON numbers for numeric eprel ids.
|
||||
if t == float64(int64(t)) {
|
||||
return strings.TrimSpace(fmt.Sprintf("%.0f", t))
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprint(t))
|
||||
case int:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case int64:
|
||||
return fmt.Sprintf("%d", t)
|
||||
case map[string]any:
|
||||
for _, k := range []string{"value", "#text", "text", "id"} {
|
||||
if s := attrString(t[k]); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
default:
|
||||
s := strings.TrimSpace(fmt.Sprint(v))
|
||||
if s == "" || s == "<nil>" {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package eprel
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestExtractFromAttrs_flatKeys(t *testing.T) {
|
||||
got := ExtractFromAttrs(map[string]any{
|
||||
"eprel_id": "1632113",
|
||||
"eprel_label": "https://eprel.ec.europa.eu/api/product/1632113/labels?format=png",
|
||||
"eprel_pdf_url": "https://eprel.ec.europa.eu/fiches/x.pdf",
|
||||
"eprel_energy_class": "A",
|
||||
"brand": "Samsung",
|
||||
})
|
||||
m, ok := got.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("type=%T", got)
|
||||
}
|
||||
if m["id"] != "1632113" || m["label"] == nil || m["pdf"] == nil || m["energy_class"] != "A" {
|
||||
t.Fatalf("got=%v", m)
|
||||
}
|
||||
if _, leak := m["eprel_id"]; leak {
|
||||
t.Fatalf("alias leaked: %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFromAttrs_nestedAliasesNormalized(t *testing.T) {
|
||||
got := ExtractFromAttrs(map[string]any{
|
||||
"eprel": map[string]any{
|
||||
"eprel_id": "2208111",
|
||||
"eprel_label": "https://eprel.ec.europa.eu/api/product/2208111/labels?format=png",
|
||||
"eprel_pdf": "https://eprel.ec.europa.eu/fiches/y.pdf",
|
||||
"eprel_energy_class": "E",
|
||||
"source": "eprel_api",
|
||||
},
|
||||
})
|
||||
m, ok := got.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("type=%T", got)
|
||||
}
|
||||
if m["id"] != "2208111" {
|
||||
t.Fatalf("id=%v", m["id"])
|
||||
}
|
||||
if m["label"] == nil || m["pdf"] == nil {
|
||||
t.Fatalf("missing label/pdf: %v", m)
|
||||
}
|
||||
if m["energy_class"] != "E" {
|
||||
t.Fatalf("energy_class=%v", m["energy_class"])
|
||||
}
|
||||
if _, ok := m["eprel_id"]; ok {
|
||||
t.Fatalf("eprel_id alias must be folded: %v", m)
|
||||
}
|
||||
if _, ok := m["source"]; ok {
|
||||
t.Fatalf("non-canonical keys must be dropped: %v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFromAttrs_nestedPartialFilledFromFlat(t *testing.T) {
|
||||
got := ExtractFromAttrs(map[string]any{
|
||||
"eprel": map[string]any{
|
||||
"id": "1632113",
|
||||
"label": "https://eprel.ec.europa.eu/api/product/1632113/labels?format=png",
|
||||
},
|
||||
"eprel_pdf": "https://eprel.ec.europa.eu/fiches/z.pdf",
|
||||
"eprel_energy_class": "A",
|
||||
})
|
||||
m, ok := got.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("type=%T", got)
|
||||
}
|
||||
if m["id"] != "1632113" || m["pdf"] == nil || m["energy_class"] != "A" {
|
||||
t.Fatalf("got=%v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeShape_empty(t *testing.T) {
|
||||
if NormalizeShape(nil) != nil {
|
||||
t.Fatal("nil")
|
||||
}
|
||||
if NormalizeShape(map[string]any{}) != nil {
|
||||
t.Fatal("empty")
|
||||
}
|
||||
if NormalizeShape(map[string]any{"eprel_id": ""}) != nil {
|
||||
t.Fatal("blank")
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,16 @@ func TestNormalizeAndExtractID(t *testing.T) {
|
||||
if got := ExtractID(mapped2); got != "777" {
|
||||
t.Fatalf("mapped key=%q", got)
|
||||
}
|
||||
attrsOnly := map[string]any{"eprel_id": "888"}
|
||||
if got := ExtractID(nil, nil, attrsOnly); got != "888" {
|
||||
t.Fatalf("attrs extract=%q", got)
|
||||
}
|
||||
if got := ExtractID(map[string]any{"eprel_id": "0000"}); got != "" {
|
||||
t.Fatalf("placeholder must be empty, got %q", got)
|
||||
}
|
||||
if got := NormalizeID("0000"); got != "" {
|
||||
t.Fatalf("NormalizeID placeholder=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientFetch_httptest(t *testing.T) {
|
||||
|
||||
@@ -16,32 +16,34 @@ var eprelIDKeys = []string{
|
||||
}
|
||||
|
||||
// NormalizeID coerces XML/API values (string, number, {"#text": ...}) to a trimmed ID.
|
||||
// Placeholder feed values like "0" / "0000" become empty so the EPREL step is not
|
||||
// falsely "skipped" after a doomed fetch attempt.
|
||||
func NormalizeID(v any) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
var s string
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(t)
|
||||
s = strings.TrimSpace(t)
|
||||
case json.Number:
|
||||
s := strings.TrimSpace(t.String())
|
||||
s = strings.TrimSpace(t.String())
|
||||
if i := strings.IndexByte(s, '.'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
return s
|
||||
case float64:
|
||||
if t != t { // NaN
|
||||
return ""
|
||||
}
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
s = strconv.FormatInt(int64(t), 10)
|
||||
case float32:
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
s = strconv.FormatInt(int64(t), 10)
|
||||
case int:
|
||||
return strconv.Itoa(t)
|
||||
s = strconv.Itoa(t)
|
||||
case int64:
|
||||
return strconv.FormatInt(t, 10)
|
||||
s = strconv.FormatInt(t, 10)
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(t), 10)
|
||||
s = strconv.FormatInt(int64(t), 10)
|
||||
case json.RawMessage:
|
||||
var decoded any
|
||||
if err := json.Unmarshal(t, &decoded); err != nil {
|
||||
@@ -55,14 +57,44 @@ func NormalizeID(v any) string {
|
||||
if text, ok := t["text"]; ok {
|
||||
return NormalizeID(text)
|
||||
}
|
||||
return ""
|
||||
case []any:
|
||||
for _, item := range t {
|
||||
if id := NormalizeID(item); id != "" {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return ""
|
||||
case []string:
|
||||
for _, item := range t {
|
||||
if id := NormalizeID(item); id != "" {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return ""
|
||||
default:
|
||||
s := strings.TrimSpace(fmt.Sprint(t))
|
||||
s = strings.TrimSpace(fmt.Sprint(t))
|
||||
if s == "" || s == "<nil>" {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
if isPlaceholderEPRELID(s) {
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func isPlaceholderEPRELID(id string) bool {
|
||||
id = strings.TrimSpace(id)
|
||||
if id == "" {
|
||||
return true
|
||||
}
|
||||
for _, r := range id {
|
||||
if r != '0' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValidID reports whether v normalizes to a non-empty EPREL registration id.
|
||||
@@ -70,8 +102,8 @@ func IsValidID(v any) bool {
|
||||
return NormalizeID(v) != ""
|
||||
}
|
||||
|
||||
// ExtractID finds an EPREL ID in mapped and/or raw product field maps
|
||||
// (vendor feeds often use <EPRELID/> / eprel_id).
|
||||
// ExtractID finds an EPREL ID in mapped, raw, and/or attribute field maps
|
||||
// (vendor feeds often use <EPRELID/> / eprel_id; specs parse may put it only in attrs).
|
||||
func ExtractID(sources ...map[string]any) string {
|
||||
for _, src := range sources {
|
||||
if src == nil {
|
||||
|
||||
Reference in New Issue
Block a user