Skip to content

Commit 0257919

Browse files
committed
Simplify body builder using latest xmldot enhancements
1 parent 7a26718 commit 0257919

File tree

4 files changed

+82
-51
lines changed

4 files changed

+82
-51
lines changed

body.go

Lines changed: 5 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -73,64 +73,26 @@ func (body Body) Err() error {
7373
return body.err
7474
}
7575

76-
// initializeXMLIfEmpty initializes an empty XML string with a minimal root element
77-
// inferred from the first segment of the path.
78-
//
79-
// If xml is empty and path is non-empty, extracts the first path segment
80-
// (before '.', '@', or '[') and creates a root element with that name.
81-
//
82-
// Returns the initialized XML string (original xml if not empty, or new root element).
83-
func initializeXMLIfEmpty(xml, path string) string {
84-
// If already has content or path is empty, return as-is
85-
if xml != "" || path == "" {
86-
return xml
87-
}
88-
89-
// Extract first path segment as root element name
90-
firstDot := 0
91-
for i, ch := range path {
92-
if ch == '.' || ch == '@' || ch == '[' {
93-
firstDot = i
94-
break
95-
}
96-
}
97-
if firstDot == 0 {
98-
firstDot = len(path)
99-
}
100-
101-
rootName := path[:firstDot]
102-
return "<" + rootName + "></" + rootName + ">"
103-
}
104-
10576
// Set sets an XML element at the given path to the specified value
10677
//
10778
// The path uses dot notation to navigate the XML structure.
10879
// The value can be any type that xmldot supports (string, int, bool, etc.).
10980
//
110-
// If the body is empty, this will automatically create a minimal root element
111-
// based on the first segment of the path.
81+
// If the body is empty, xmldot will automatically create the XML structure
82+
// based on the path.
11283
//
11384
// If an error occurs, the error is stored and returned by String() or Err().
11485
// Once an error occurs, all subsequent operations are no-ops that preserve the error.
11586
//
116-
// Example:
117-
//
118-
// body := netconf.Body{}.
119-
// Set("config.system.hostname", "router1").
120-
// Set("config.system.domain-name", "example.com")
121-
// xml, err := body.String()
122-
//
12387
// Returns the Body for method chaining.
12488
func (body Body) Set(path string, value any) Body {
12589
// Short-circuit if already in error state
12690
if body.err != nil {
12791
return body
12892
}
12993

130-
// Initialize empty XML with root element if needed
131-
xml := initializeXMLIfEmpty(body.str, path)
132-
133-
result, err := xmldot.Set(xml, path, value)
94+
// xmldot now handles empty XML and multi-root detection automatically
95+
result, err := xmldot.Set(body.str, path, value)
13496
if err != nil {
13597
// Store error and return body with error state
13698
return Body{str: body.str, err: fmt.Errorf("Set(%q): %w", path, err)}
@@ -160,10 +122,7 @@ func (body Body) SetRaw(path, rawXML string) Body {
160122
return body
161123
}
162124

163-
// Initialize empty XML with root element if needed
164-
xml := initializeXMLIfEmpty(body.str, path)
165-
166-
result, err := xmldot.SetRaw(xml, path, rawXML)
125+
result, err := xmldot.SetRaw(body.str, path, rawXML)
167126
if err != nil {
168127
return Body{str: body.str, err: fmt.Errorf("SetRaw(%q): %w", path, err)}
169128
}

body_test.go

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,8 @@ func TestBodySetError(t *testing.T) {
269269
})
270270

271271
t.Run("invalid path", func(t *testing.T) {
272-
body := Body{}.Set("..invalid", "value")
272+
// Empty path should error - xmldot rejects empty paths
273+
body := Body{}.Set("", "value")
273274
_, err := body.String()
274275
if err == nil {
275276
t.Error("expected error for invalid path")
@@ -348,7 +349,7 @@ func TestBodySetAttrError(t *testing.T) {
348349
t.Run("SetAttr on invalid path", func(t *testing.T) {
349350
body := Body{}.
350351
Set("config.interface", "test").
351-
SetAttr("invalid..path", "attr", "value")
352+
SetAttr("", "attr", "value") // Empty path should error
352353

353354
_, err := body.String()
354355
if err == nil {
@@ -525,3 +526,74 @@ func ExampleBody_Set() {
525526
// Output: Configuration built successfully
526527
// XML length: 200 bytes
527528
}
529+
530+
// TestBodySetMultipleRoots tests that Body.Set() properly handles multiple root-level sibling elements
531+
func TestBodySetMultipleRoots(t *testing.T) {
532+
t.Run("ACL with deny and permit siblings", func(t *testing.T) {
533+
// This is the real-world use case from terraform-provider-iosxe ACLs
534+
// where we have both deny and permit at the same level
535+
body := Body{}.
536+
Set("sequence", "10").
537+
Set("deny.std-ace.ipv4-address-prefix", "10.0.0.0").
538+
Set("deny.std-ace.mask", "0.0.0.255").
539+
Set("permit.std-ace.ipv4-address-prefix", "192.168.0.0").
540+
Set("permit.std-ace.mask", "0.0.255.255")
541+
542+
xml, err := body.String()
543+
if err != nil {
544+
t.Fatalf("unexpected error: %v", err)
545+
}
546+
547+
t.Logf("Generated XML:\n%s", xml)
548+
549+
// Verify we have <sequence>, <deny>, and <permit> as separate root-level siblings
550+
if !strings.Contains(xml, "<sequence>10</sequence>") {
551+
t.Errorf("expected separate <sequence> element, got: %s", xml)
552+
}
553+
if !strings.Contains(xml, "<deny>") {
554+
t.Errorf("expected <deny> element, got: %s", xml)
555+
}
556+
if !strings.Contains(xml, "<permit>") {
557+
t.Errorf("expected <permit> element, got: %s", xml)
558+
}
559+
if !strings.Contains(xml, "<ipv4-address-prefix>10.0.0.0</ipv4-address-prefix>") {
560+
t.Errorf("expected deny prefix in XML, got: %s", xml)
561+
}
562+
if !strings.Contains(xml, "<ipv4-address-prefix>192.168.0.0</ipv4-address-prefix>") {
563+
t.Errorf("expected permit prefix in XML, got: %s", xml)
564+
}
565+
566+
// Verify it's NOT all nested inside <sequence> or duplicated
567+
// The XML should have the structure:
568+
// <sequence>10</sequence><deny>...</deny><permit>...</permit>
569+
// NOT: <sequence>10<deny>...</deny><permit>...</permit></sequence>
570+
if strings.Contains(xml, "<sequence>10<deny>") || strings.Contains(xml, "</deny></sequence>") {
571+
t.Errorf("deny should not be nested inside sequence, got: %s", xml)
572+
}
573+
})
574+
575+
t.Run("multiple different root elements", func(t *testing.T) {
576+
body := Body{}.
577+
Set("hostname", "router1").
578+
Set("domain", "example.com").
579+
Set("port", "8080")
580+
581+
xml, err := body.String()
582+
if err != nil {
583+
t.Fatalf("unexpected error: %v", err)
584+
}
585+
586+
t.Logf("Generated XML:\n%s", xml)
587+
588+
// All should be separate root elements
589+
if !strings.Contains(xml, "<hostname>router1</hostname>") {
590+
t.Errorf("expected hostname element, got: %s", xml)
591+
}
592+
if !strings.Contains(xml, "<domain>example.com</domain>") {
593+
t.Errorf("expected domain element, got: %s", xml)
594+
}
595+
if !strings.Contains(xml, "<port>8080</port>") {
596+
t.Errorf("expected port element, got: %s", xml)
597+
}
598+
})
599+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/netascode/go-netconf
33
go 1.24.0
44

55
require (
6-
github.com/netascode/xmldot v0.4.0
6+
github.com/netascode/xmldot v0.4.1
77
github.com/scrapli/scrapligo v1.3.3
88
)
99

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
22
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
33
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
44
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
5-
github.com/netascode/xmldot v0.4.0 h1:uGa7M4KJ/ECn3YXZ27vEX1mAPYPH3UgmOlYwBHq1L+Q=
6-
github.com/netascode/xmldot v0.4.0/go.mod h1:T0zddov+d7Sgam8cpJSOr155HiKyXwY58PE/iiuXbT8=
5+
github.com/netascode/xmldot v0.4.1 h1:Uw5qJRHOxUFOOzzcQt3F4+PYgHZC/YneRVU5UGknoqc=
6+
github.com/netascode/xmldot v0.4.1/go.mod h1:T0zddov+d7Sgam8cpJSOr155HiKyXwY58PE/iiuXbT8=
77
github.com/scrapli/scrapligo v1.3.3 h1:D9zj1QrOYNYAQ30YT7wfQBINvPGxvs5L5Lz+2LnL7V4=
88
github.com/scrapli/scrapligo v1.3.3/go.mod h1:pOWxVyPsQRrWTrkoSSDg05tjOqtWfLffAZtAsCc0w3M=
99
github.com/sirikothe/gotextfsm v1.1.0 h1:Hd6S3g4383e8b0awZQEPr+d1QPVxxnR/3NU1Kw4dI/Y=

0 commit comments

Comments
 (0)