Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ func PatchConfig(path []string, value any) error {

b, err := yaml.Patch(b, path, value)
if err != nil {
// deleting a path that doesn't exist is a no-op, not an error
if value == nil && errors.Is(err, yaml.ErrPathNotExist) {
return nil
}
return err
}

Expand Down
20 changes: 19 additions & 1 deletion pkg/yaml/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ func patch(in []byte, path []string, value any) ([]byte, error) {
} else {
// parent value is nil (use parent indent + 2)
paste = addIndent(paste, pKey.Column+1)

// An explicit null scalar on the key's line (e.g. "streams: null", as emitted by
// yaml.Marshal of an empty map) can't hold a child: pasting one after it yields
// "streams: null\n x: y", which fails to parse ("mapping values are not allowed
// in this context"). Strip the scalar so the key becomes a bare mapping parent,
// then append the child. A bare/implicit-null value ("streams:") carries no token
// (pVal.Value == "") and keeps the original behaviour below.
if value != nil && pVal.Value != "" && pVal.Line == pKey.Line {
vOff := lineOffset(in, pVal.Line) + pVal.Column - 1
colon := bytes.LastIndexByte(in[:vOff], ':')
eol := lineOffset(in, pKey.Line+1)
if eol < 0 {
eol = len(in)
}
return join(in[:colon+1], paste, in[eol:]), nil
}
}

_, i1 := nodeBounds(in, pKey)
Expand Down Expand Up @@ -137,9 +153,11 @@ func nodeBounds(in []byte, node *yaml.Node) (offset0, offset1 int) {
return
}

var ErrPathNotExist = errors.New("yaml: path not exist")

func addToEnd(in []byte, path []string, value any) ([]byte, error) {
if len(path) != 2 || value == nil {
return nil, errors.New("yaml: path not exist")
return nil, ErrPathNotExist
}

v := map[string]map[string]any{
Expand Down
24 changes: 24 additions & 0 deletions pkg/yaml/yaml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ func TestPatch(t *testing.T) {
value: []string{"val1"},
expect: "streams:\n camera1: url1\nhomekit:\n camera1:\n name: dummy\n pairings:\n - val1\n",
},
{
// yaml.Marshal of an empty streams map emits the explicit token "streams: null";
// adding the first stream back must not produce "streams: null\n camera1: ...".
name: "explicit null scalar parent",
src: "streams: null\nwebrtc:\n listen: \":8555\"\n",
path: []string{"streams", "camera1"},
value: "val1",
expect: "streams:\n camera1: val1\nwebrtc:\n listen: \":8555\"\n",
},
{
name: "explicit null scalar only",
src: "streams: null\n",
path: []string{"streams", "camera1"},
value: "val1",
expect: "streams:\n camera1: val1\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -107,3 +123,11 @@ func TestPatch(t *testing.T) {
})
}
}

func TestPatchDeleteMissing(t *testing.T) {
// Deleting a path whose parent does not exist routes through addToEnd and
// must return the ErrPathNotExist sentinel so callers can treat a missing
// delete target as a no-op via errors.Is.
_, err := Patch([]byte("streams:\n camera1: url1\n"), []string{"homekit", "camera1"}, nil)
require.ErrorIs(t, err, ErrPathNotExist)
}