ref for projection snapshots
This commit is contained in:
1 parent
21bcb63440
commit
5a3ffcb6bc
4 files changed
+91
-5
No files matched your search
Executable
BIN
Binary file not shown.
@@ -116,6 +116,7 @@ func Apply(s Schema, source map[string]any) (Projection, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
p := Projection{
|
p := Projection{
|
||||||
|
ItemID: uint32Of(source, s.ItemIdField),
|
||||||
ID: id,
|
ID: id,
|
||||||
SKU: sku,
|
SKU: sku,
|
||||||
Title: stringOf(source, s.TitleField),
|
Title: stringOf(source, s.TitleField),
|
||||||
@@ -180,6 +181,63 @@ func stringOf(m map[string]any, key string) string {
|
|||||||
return fmt.Sprintf("%v", v)
|
return fmt.Sprintf("%v", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uint32Of returns the value at key as a uint32. Best-effort coercion:
|
||||||
|
// json.Number / int / int64 / float (integral check) / truthy numeric
|
||||||
|
// string. Returns 0 for any missing / non-uint32-representable value —
|
||||||
|
// Projection.ItemID = 0 is the documented "no id" signal that the cart's
|
||||||
|
// overlay path uses to fall back to PRODUCT_BASE_URL.
|
||||||
|
func uint32Of(m map[string]any, key string) uint32 {
|
||||||
|
if key == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
v, ok := m[key]
|
||||||
|
if !ok || v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
switch x := v.(type) {
|
||||||
|
case uint32:
|
||||||
|
return x
|
||||||
|
case uint64:
|
||||||
|
if x > 0xFFFFFFFF {
|
||||||
|
return 0 // overflow – treat as unset, downstream falls back
|
||||||
|
}
|
||||||
|
return uint32(x)
|
||||||
|
case int:
|
||||||
|
if x < 0 || x > 0xFFFFFFFF {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint32(x)
|
||||||
|
case int64:
|
||||||
|
if x < 0 || x > 0xFFFFFFFF {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint32(x)
|
||||||
|
case float64:
|
||||||
|
if x != float64(uint32(x)) || x < 0 || x > 0xFFFFFFFF {
|
||||||
|
return 0 // fractional or out-of-range
|
||||||
|
}
|
||||||
|
return uint32(x)
|
||||||
|
case json.Number:
|
||||||
|
s := x.String()
|
||||||
|
n, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint32(n)
|
||||||
|
case string:
|
||||||
|
s := strings.TrimSpace(x)
|
||||||
|
if s == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
n, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return uint32(n)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// boolOf returns the value at key coerced through truthy. nil/missing keys
|
// boolOf returns the value at key coerced through truthy. nil/missing keys
|
||||||
// are false; strings lower-cased and matched against truthy; bools passed
|
// are false; strings lower-cased and matched against truthy; bools passed
|
||||||
// through; numerics "!= 0"; json.Number goes through Float64.
|
// through; numerics "!= 0"; json.Number goes through Float64.
|
||||||
|
|||||||
+21
-5
@@ -9,6 +9,7 @@
|
|||||||
// Mapping source JSON → Projection is per-tenant and lives in Schema.
|
// Mapping source JSON → Projection is per-tenant and lives in Schema.
|
||||||
// The consensus mapping for DefaultSchema() is:
|
// The consensus mapping for DefaultSchema() is:
|
||||||
//
|
//
|
||||||
|
// item_id ← int coercion of source[ItemIdField] (omitempty; 0 = unset)
|
||||||
// id ← IDPrefix + SKU (e.g. "sku:<SKU>")
|
// id ← IDPrefix + SKU (e.g. "sku:<SKU>")
|
||||||
// sku ← source[SKUField]
|
// sku ← source[SKUField]
|
||||||
// title ← source[TitleField] (omitempty, optional)
|
// title ← source[TitleField] (omitempty, optional)
|
||||||
@@ -48,8 +49,17 @@ import (
|
|||||||
//
|
//
|
||||||
// Field rationale:
|
// Field rationale:
|
||||||
//
|
//
|
||||||
// ID — opaque published identity; SKU alone is not a stable
|
// ItemID — opaque numeric catalog item id from the producer's
|
||||||
// cross-module id (different scopes reuse it).
|
// source (e.g. product service `/api/get/{sku}` integer
|
||||||
|
// `id` field, or the bygglagret raw row `item_id`).
|
||||||
|
// Carried here as uint32 to mirror go-cart-actor's
|
||||||
|
// proto messages.AddItem.ItemId which dedupes cart lines
|
||||||
|
// on equality. Zero means "producer didn't publish" — a
|
||||||
|
// legitimate value when the source has no integer id
|
||||||
|
// (the cart fall-back path uses HTTP PRODUCT_BASE_URL
|
||||||
|
// to recover the id in that case).
|
||||||
|
// ID — opaque published string identity; SKU alone is not a
|
||||||
|
// stable cross-module id (different scopes reuse it).
|
||||||
// SKU — domain identifier; the join key cart already uses.
|
// SKU — domain identifier; the join key cart already uses.
|
||||||
// PriceIncVat — display + checkout projection; NOT transactional truth.
|
// PriceIncVat — display + checkout projection; NOT transactional truth.
|
||||||
// System of record for the total is settled via the
|
// System of record for the total is settled via the
|
||||||
@@ -66,10 +76,16 @@ import (
|
|||||||
// tracking for non-stock items.
|
// tracking for non-stock items.
|
||||||
// Category / Brand / Image — display projection only.
|
// Category / Brand / Image — display projection only.
|
||||||
// UpdatedAt — version stamp for snapshot + revalidate.
|
// UpdatedAt — version stamp for snapshot + revalidate.
|
||||||
|
//
|
||||||
|
// WIRE-COMPAT: adding a field is forward+backward-safe. Old snapshot
|
||||||
|
// readers ignore unknown JSON keys; new readers populate the field from
|
||||||
|
// the JSON token. The on-disk snapshot binary header/record layout is
|
||||||
|
// unchanged \u2014 only the per-blob JSON payload gains a key.
|
||||||
type Projection struct {
|
type Projection struct {
|
||||||
ID string `json:"id"`
|
ItemID uint32 `json:"item_id,omitempty"`
|
||||||
SKU string `json:"sku"`
|
ID string `json:"id"`
|
||||||
Title string `json:"title,omitempty"`
|
SKU string `json:"sku"`
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
PriceIncVat money.Cents `json:"price_inc_vat"`
|
PriceIncVat money.Cents `json:"price_inc_vat"`
|
||||||
Currency string `json:"currency,omitempty"`
|
Currency string `json:"currency,omitempty"`
|
||||||
// TaxClass uses a string identifier (see platform/tax class constants).
|
// TaxClass uses a string identifier (see platform/tax class constants).
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ type Schema struct {
|
|||||||
CategoryField string `json:"category_field,omitempty"`
|
CategoryField string `json:"category_field,omitempty"`
|
||||||
BrandField string `json:"brand_field,omitempty"`
|
BrandField string `json:"brand_field,omitempty"`
|
||||||
ImageField string `json:"image_field,omitempty"`
|
ImageField string `json:"image_field,omitempty"`
|
||||||
|
// ItemIdField is the source-side key carrying the producer's catalog
|
||||||
|
// item id (uint32-equivalent). DefaultSchema points at "id" (matching
|
||||||
|
// the product service's /api/get/{sku} shape). Producers that omit
|
||||||
|
// an integer id field simply leave ItemIdField empty — consumers see
|
||||||
|
// Projection.ItemID=0 and fall back to their HTTP lookup.
|
||||||
|
ItemIdField string `json:"item_id_field,omitempty"`
|
||||||
|
|
||||||
// --- fixed / default values used when a source field is absent ---.
|
// --- fixed / default values used when a source field is absent ---.
|
||||||
FixedCurrency string `json:"fixed_currency,omitempty"`
|
FixedCurrency string `json:"fixed_currency,omitempty"`
|
||||||
@@ -118,6 +124,8 @@ func DefaultSchema() Schema {
|
|||||||
CategoryField: "category",
|
CategoryField: "category",
|
||||||
BrandField: "brand",
|
BrandField: "brand",
|
||||||
ImageField: "image",
|
ImageField: "image",
|
||||||
|
// Standard product service carries the integer catalog id under "id".
|
||||||
|
ItemIdField: "id",
|
||||||
UpdatedAtField: "updated_at",
|
UpdatedAtField: "updated_at",
|
||||||
|
|
||||||
FixedCurrency: "", // read from source if present
|
FixedCurrency: "", // read from source if present
|
||||||
@@ -139,6 +147,9 @@ func DefaultSchema() Schema {
|
|||||||
// The wire contract is class identifiers; platform/tax resolves them to
|
// The wire contract is class identifiers; platform/tax resolves them to
|
||||||
// numeric rates out of band. Shipping "25" as the class would break that
|
// numeric rates out of band. Shipping "25" as the class would break that
|
||||||
// resolution path on the consumer side.
|
// resolution path on the consumer side.
|
||||||
|
//
|
||||||
|
// ItemIdField: bygglagret rows carry the integer item id under "item_id"
|
||||||
|
// (matches the per-row log shape we see from the upstream product import).
|
||||||
func BygglagretSchema() Schema {
|
func BygglagretSchema() Schema {
|
||||||
return Schema{
|
return Schema{
|
||||||
Tenant: "bygglagret",
|
Tenant: "bygglagret",
|
||||||
@@ -152,6 +163,7 @@ func BygglagretSchema() Schema {
|
|||||||
CategoryField: "CategoryPath",
|
CategoryField: "CategoryPath",
|
||||||
BrandField: "Brand",
|
BrandField: "Brand",
|
||||||
ImageField: "PrimaryImage",
|
ImageField: "PrimaryImage",
|
||||||
|
ItemIdField: "item_id",
|
||||||
UpdatedAtField: "LastModifiedMs",
|
UpdatedAtField: "LastModifiedMs",
|
||||||
|
|
||||||
FixedCurrency: "SEK",
|
FixedCurrency: "SEK",
|
||||||
|
|||||||
Reference in new issue
Block a user