feat(client-go): add WriteArrayElements default-fill helper and document semantics

Regenerate Go proto types from mxaccess_gateway.proto so MxSparseArray,
MxSparseElement, and MxValue_SparseArrayValue appear in the generated
package; add MxSparseArray/MxSparseElement type aliases to types.go;
add Session.WriteArrayElements and the unexported buildSparseArrayValue
builder; add three unit tests covering the sparse oneof structure,
empty-map case, and the round-trip through WriteArrayElements; update
README with default-fill reset semantics and auto-normalize note.
This commit is contained in:
Joseph Doherty
2026-06-18 03:01:55 -04:00
parent 0702551c25
commit b7f29f3048
5 changed files with 1442 additions and 524 deletions
+12
View File
@@ -145,6 +145,18 @@ the unchanged elements included. For example, to change 2 elements of a
the 2 new ones). Sending only the 2 changed values overwrites the attribute the 2 new ones). Sending only the 2 changed values overwrites the attribute
with a 2-element array. with a 2-element array.
`Session.WriteArrayElements` offers a default-fill shorthand: pass only the
indices you want to set along with a `totalLength`. The gateway expands the
sparse representation into a full array before forwarding to MXAccess — every
unmentioned index receives the element type's zero value (boolean `false`,
integer `0`, float `0.0`, string `""`, time = Unix epoch). This is a **RESET**
of unmentioned indices, not a preserve of existing values. Use the full-array
form (read-modify-write) when existing element values must be preserved.
`AddItem` (and `AddItem2`) now auto-normalize a bare attribute name to the `[]`
array address form expected by MXAccess, so callers do not need to append `[]`
themselves. Both forms are accepted; duplicates are deduplicated by the gateway.
## Galaxy Repository browse ## Galaxy Repository browse
The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a The `GalaxyRepository` service (proto package `galaxy_repository.v1`) is a
File diff suppressed because it is too large Load Diff
@@ -666,3 +666,101 @@ func authorizationFromContext(ctx context.Context) string {
} }
return values[0] return values[0]
} }
// ---------------------------------------------------------------------------
// WriteArrayElements / buildSparseArrayValue unit tests
// ---------------------------------------------------------------------------
func TestBuildSparseArrayValueSetsSparseOneof(t *testing.T) {
elements := map[uint32]*MxValue{
2: Int32Value(99),
0: Int32Value(10),
}
v := buildSparseArrayValue(DataTypeInteger, 5, elements)
sa, ok := v.Kind.(*pb.MxValue_SparseArrayValue)
if !ok {
t.Fatalf("Kind is %T, want *pb.MxValue_SparseArrayValue", v.Kind)
}
got := sa.SparseArrayValue
if got.GetElementDataType() != DataTypeInteger {
t.Errorf("ElementDataType = %v, want DataTypeInteger", got.GetElementDataType())
}
if got.GetTotalLength() != 5 {
t.Errorf("TotalLength = %d, want 5", got.GetTotalLength())
}
if len(got.GetElements()) != 2 {
t.Fatalf("len(Elements) = %d, want 2", len(got.GetElements()))
}
// Elements must be sorted by index (ascending).
if got.GetElements()[0].GetIndex() != 0 {
t.Errorf("Elements[0].Index = %d, want 0", got.GetElements()[0].GetIndex())
}
if got.GetElements()[0].GetValue().GetInt32Value() != 10 {
t.Errorf("Elements[0].Value = %v, want 10", got.GetElements()[0].GetValue())
}
if got.GetElements()[1].GetIndex() != 2 {
t.Errorf("Elements[1].Index = %d, want 2", got.GetElements()[1].GetIndex())
}
if got.GetElements()[1].GetValue().GetInt32Value() != 99 {
t.Errorf("Elements[1].Value = %v, want 99", got.GetElements()[1].GetValue())
}
}
func TestBuildSparseArrayValueEmptyMapProducesEmptyElements(t *testing.T) {
v := buildSparseArrayValue(DataTypeBoolean, 4, map[uint32]*MxValue{})
sa, ok := v.Kind.(*pb.MxValue_SparseArrayValue)
if !ok {
t.Fatalf("Kind is %T, want *pb.MxValue_SparseArrayValue", v.Kind)
}
if len(sa.SparseArrayValue.GetElements()) != 0 {
t.Errorf("len(Elements) = %d, want 0", len(sa.SparseArrayValue.GetElements()))
}
if sa.SparseArrayValue.GetTotalLength() != 4 {
t.Errorf("TotalLength = %d, want 4", sa.SparseArrayValue.GetTotalLength())
}
}
func TestWriteArrayElementsSendsWriteCommandWithSparseOneof(t *testing.T) {
fake := &fakeGatewayServer{
invokeReply: &pb.MxCommandReply{
SessionId: "session-1",
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE,
ProtocolStatus: &pb.ProtocolStatus{
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
},
},
}
client, cleanup := newBufconnClient(t, fake)
defer cleanup()
session := NewSessionForID(client, "session-1")
err := session.WriteArrayElements(
context.Background(),
1, 2,
DataTypeFloat,
10,
map[uint32]*MxValue{3: FloatValue(1.5)},
0,
)
if err != nil {
t.Fatalf("WriteArrayElements() error = %v", err)
}
cmd := fake.invokeRequest.GetCommand()
if cmd.GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_WRITE {
t.Fatalf("command kind = %s, want WRITE", cmd.GetKind())
}
val := cmd.GetWrite().GetValue()
sa, ok := val.Kind.(*pb.MxValue_SparseArrayValue)
if !ok {
t.Fatalf("value kind is %T, want *pb.MxValue_SparseArrayValue", val.Kind)
}
if sa.SparseArrayValue.GetTotalLength() != 10 {
t.Errorf("TotalLength = %d, want 10", sa.SparseArrayValue.GetTotalLength())
}
if sa.SparseArrayValue.GetElementDataType() != DataTypeFloat {
t.Errorf("ElementDataType = %v, want DataTypeFloat", sa.SparseArrayValue.GetElementDataType())
}
}
+52
View File
@@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"sort"
"sync" "sync"
"time" "time"
@@ -580,6 +581,57 @@ func (s *Session) WriteRaw(ctx context.Context, serverHandle, itemHandle int32,
}) })
} }
// WriteArrayElements writes a sparse, default-filled array: only the given
// elements (index → scalar value) are set; every unmentioned index up to
// totalLength is written as the element type's default (false / 0 / "" / Unix
// epoch for time). The gateway expands the sparse representation into a full
// array write before forwarding to MXAccess — this is a RESET of unmentioned
// indices, not a preserve. Neither RESET semantics nor the original array
// content are retained.
//
// elementDataType must be a scalar MXAccess type (Boolean, Integer, Float,
// Double, String, or Time). totalLength must be at least as large as the
// highest index in elements plus one.
func (s *Session) WriteArrayElements(
ctx context.Context,
serverHandle, itemHandle int32,
elementDataType MxDataType,
totalLength uint32,
elements map[uint32]*MxValue,
userID int32,
) error {
return s.Write(ctx, serverHandle, itemHandle, buildSparseArrayValue(elementDataType, totalLength, elements), userID)
}
// buildSparseArrayValue constructs the MxValue carrying an MxSparseArray oneof
// arm from a map of index → scalar MxValue. Keys are visited in ascending
// order so the produced slice is deterministic (important for test assertions).
func buildSparseArrayValue(elementDataType MxDataType, totalLength uint32, elements map[uint32]*MxValue) *MxValue {
indices := make([]uint32, 0, len(elements))
for idx := range elements {
indices = append(indices, idx)
}
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
sparseElements := make([]*MxSparseElement, 0, len(elements))
for _, idx := range indices {
sparseElements = append(sparseElements, &MxSparseElement{
Index: idx,
Value: elements[idx],
})
}
return &MxValue{
Kind: &pb.MxValue_SparseArrayValue{
SparseArrayValue: &MxSparseArray{
ElementDataType: elementDataType,
TotalLength: totalLength,
Elements: sparseElements,
},
},
}
}
// PingRaw sends a diagnostic PING command and returns the raw reply. // PingRaw sends a diagnostic PING command and returns the raw reply.
// The message is echoed back by the gateway in the reply's DiagnosticMessage field. // The message is echoed back by the gateway in the reply's DiagnosticMessage field.
func (s *Session) PingRaw(ctx context.Context, message string) (*MxCommandReply, error) { func (s *Session) PingRaw(ctx context.Context, message string) (*MxCommandReply, error) {
+7
View File
@@ -36,6 +36,13 @@ type (
Value = pb.MxValue Value = pb.MxValue
// MxArray is the protobuf representation of an MXAccess array value. // MxArray is the protobuf representation of an MXAccess array value.
MxArray = pb.MxArray MxArray = pb.MxArray
// MxSparseArray is the write-only protobuf type for default-fill partial
// array writes. The gateway expands it to a full array before forwarding
// to MXAccess: unmentioned indices receive the element type's default value
// (boolean false, integer 0, float 0.0, string "", time = Unix epoch).
MxSparseArray = pb.MxSparseArray
// MxSparseElement is one index/value pair inside an MxSparseArray.
MxSparseElement = pb.MxSparseElement
// MxStatusProxy mirrors the MXAccess MXSTATUS_PROXY structure. // MxStatusProxy mirrors the MXAccess MXSTATUS_PROXY structure.
MxStatusProxy = pb.MxStatusProxy MxStatusProxy = pb.MxStatusProxy
// ProtocolStatus is the gateway-level status carried on every reply. // ProtocolStatus is the gateway-level status carried on every reply.