diff --git a/go.mod b/go.mod index 918ec29..3cb85ac 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/TransactionAuthorizationProtocol/tap-go -go 1.26.2 +go 1.26.6 require ( github.com/google/uuid v1.6.0 diff --git a/types.go b/types.go index 2b802f4..3d64cbc 100644 --- a/types.go +++ b/types.go @@ -24,7 +24,7 @@ type Agent struct { ID string `json:"@id"` Type string `json:"@type,omitempty"` Role string `json:"role,omitempty"` - For ForField `json:"for,omitempty"` + For ForField `json:"for,omitzero"` Name string `json:"name,omitempty"` NameHash string `json:"nameHash,omitempty"` LEICode string `json:"lei:leiCode,omitempty"` @@ -66,6 +66,11 @@ func (f ForField) IsEmpty() bool { return len(f.values) == 0 } +// IsZero reports whether the ForField has no values; omitzero reads it. +func (f ForField) IsZero() bool { + return len(f.values) == 0 +} + // MarshalJSON marshals the ForField as a string (single value) or array (multiple values). func (f ForField) MarshalJSON() ([]byte, error) { if len(f.values) == 0 { @@ -79,6 +84,13 @@ func (f ForField) MarshalJSON() ([]byte, error) { // UnmarshalJSON unmarshals the ForField from a string or array of strings. func (f *ForField) UnmarshalJSON(data []byte) error { + // null into a non-pointer is a no-op returning no error, so the branch + // below would take it and produce [""]. + if string(data) == "null" { + f.values = nil + return nil + } + // Try single string first var single string if err := json.Unmarshal(data, &single); err == nil { diff --git a/types_test.go b/types_test.go index f292a04..ef09549 100644 --- a/types_test.go +++ b/types_test.go @@ -224,3 +224,54 @@ func TestInvoice_JSONRoundTrip(t *testing.T) { t.Errorf("LineItems mismatch: got %v", got.LineItems) } } + +func TestAgent_OwnerlessOmitsForKey(t *testing.T) { + data, err := json.Marshal(Agent{ID: "did:pkh:eip155:1:0xabc", Role: "SettlementAddress"}) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if bytes.Contains(data, []byte(`"for"`)) { + t.Errorf("ownerless agent must omit the for key, got %s", data) + } + + var got Agent + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !got.For.IsEmpty() { + t.Errorf("round-tripped ownerless agent declares an owner: %v", got.For.Values()) + } +} + +func TestForField_UnmarshalNull(t *testing.T) { + var f ForField + if err := json.Unmarshal([]byte("null"), &f); err != nil { + t.Fatalf("unmarshal null: %v", err) + } + if !f.IsEmpty() { + t.Errorf("null must decode to no values, got %v", f.Values()) + } + if f.String() != "" { + t.Errorf("null must decode to the empty string, got %q", f.String()) + } +} + +func TestAgent_ForNullDecodesToNoOwner(t *testing.T) { + var got Agent + if err := json.Unmarshal([]byte(`{"@id":"did:pkh:eip155:1:0xabc","for":null}`), &got); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if !got.For.IsEmpty() { + t.Errorf(`"for": null must decode to no owner, got %v`, got.For.Values()) + } +} + +func TestForField_EmptySliceOmitsKey(t *testing.T) { + data, err := json.Marshal(Agent{ID: "did:eg:agent", For: NewForField()}) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if bytes.Contains(data, []byte(`"for"`)) { + t.Errorf("empty ForField must omit the for key, got %s", data) + } +}