Skip to content

Commit

Permalink
Add support for unexpected elements
Browse files Browse the repository at this point in the history
  • Loading branch information
curtis.brown committed Nov 27, 2023
1 parent 017df3e commit 9491a05
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 13 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.idea
/cmd/goxmlstruct/goxmlstruct
4 changes: 4 additions & 0 deletions cmd/goxmlstruct/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ var (
topLevelAttributes = flag.Bool("top-level-attributes", xmlstruct.DefaultTopLevelAttributes, "include top level attributes")
usePointersForOptionalFields = flag.Bool("use-pointers-for-optional-fields", xmlstruct.DefaultUsePointersForOptionalFields, "use pointers for optional fields")
useRawToken = flag.Bool("use-raw-token", xmlstruct.DefaultUseRawToken, "use encoding/xml.Decoder.RawToken")
supportUnexpectedElements = flag.Bool("support-unexpected-elements", xmlstruct.DefaultSupportUnexpectedElements, "create field of Node type with xml:any tag on each struct to handle unexpected elements on parse")
unexpectedElementTypeName = flag.String("unexpected-element-type-name", xmlstruct.DefaultUnexpectedElementTypeName, "name for the namedType to contain any unexpected elements encountered on parsing; default: UnexpectedElement")
)

func run() error {
Expand All @@ -46,6 +48,8 @@ func run() error {
xmlstruct.WithTopLevelAttributes(*topLevelAttributes),
xmlstruct.WithUsePointersForOptionalFields(*usePointersForOptionalFields),
xmlstruct.WithUseRawToken(*useRawToken),
xmlstruct.WithSupportUnexpectedElements(*supportUnexpectedElements),
xmlstruct.WithUnexpectedElementTypeName(*unexpectedElementTypeName),
)

if flag.NArg() == 0 {
Expand Down
21 changes: 18 additions & 3 deletions element.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import (
"bytes"
"encoding/xml"
"fmt"

Check failure on line 6 in element.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(github.com/twpayne/go-xmlstruct) (gci)
"io"

"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"io"

Check failure on line 9 in element.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gci`-ed with --skip-generated -s standard -s default -s prefix(github.com/twpayne/go-xmlstruct) (gci)
)

// An element describes an observed XML element, its attributes, chardata, and
Expand Down Expand Up @@ -214,7 +213,23 @@ func (e *element) writeGoType(w io.Writer, options *generateOptions, indentPrefi
return err
}
}
fmt.Fprintf(w, " `xml:\"%s\"`\n", childElement.name.Local)

if e.name.Local == options.unexpectedElementTypeName {
switch childElement.name.Local {
case "Attrs":
fmt.Fprint(w, " `xml:\",any,attr\"`\n")
case "Content":
fmt.Fprintf(w, " `xml:\",innerxml\"`\n")
case "Nodes":
fmt.Fprintf(w, " `xml:\",any\"`\n")
}
} else {
if childElement.name.Local == fmt.Sprintf("%ss", options.unexpectedElementTypeName) {
fmt.Fprintf(w, " `xml:\",any\"`\n")
} else {
fmt.Fprintf(w, " `xml:\"%s\"`\n", childElement.name.Local)
}
}
}

fmt.Fprintf(w, "%s}", indentPrefix)
Expand Down
100 changes: 100 additions & 0 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ var (
SkipDir = fs.SkipDir
//lint:ignore ST1012 SkipFile is not an error
SkipFile = errors.New("skip file") //nolint:errname

unexpectedElement, unexpectedElements *element
)

// A Generator observes XML documents and generates Go structs into which the
Expand All @@ -41,6 +43,8 @@ type Generator struct {
typeOrder map[xml.Name]int
usePointersForOptionalFields bool
useRawToken bool
supportUnexpectedElements bool
unexpectedElementTypeName string
typeElements map[xml.Name]*element
}

Expand Down Expand Up @@ -148,6 +152,23 @@ func WithUseRawToken(useRawToken bool) GeneratorOption {
}
}

// WithSupportUnexpectedElements sets whether to support unexpected elements
// on structs through use of a field of the catch-all type Node and xml:any
// struct tag on each generated struct

Check failure on line 157 in generator.go

View workflow job for this annotation

GitHub Actions / lint

Comment should end in a period (godot)
func WithSupportUnexpectedElements(supportUnexpectedElements bool) GeneratorOption {
return func(g *Generator) {
g.supportUnexpectedElements = supportUnexpectedElements
}
}

// WithUnexpectedElementTypeName specifies the name of the named type to contain
// any unexpected elements encountered during parsing

Check failure on line 165 in generator.go

View workflow job for this annotation

GitHub Actions / lint

Comment should end in a period (godot)
func WithUnexpectedElementTypeName(unexpectedElementTypeName string) GeneratorOption {
return func(g *Generator) {
g.unexpectedElementTypeName = unexpectedElementTypeName
}
}

// NewGenerator returns a new Generator with the given options.
func NewGenerator(options ...GeneratorOption) *Generator {
g := &Generator{
Expand All @@ -164,6 +185,8 @@ func NewGenerator(options ...GeneratorOption) *Generator {
typeOrder: make(map[xml.Name]int),
usePointersForOptionalFields: DefaultUsePointersForOptionalFields,
useRawToken: DefaultUseRawToken,
supportUnexpectedElements: DefaultSupportUnexpectedElements,
unexpectedElementTypeName: DefaultUnexpectedElementTypeName,
typeElements: make(map[xml.Name]*element),
}
g.exportNameFunc = func(name xml.Name) string {
Expand All @@ -189,9 +212,21 @@ func (g *Generator) Generate() ([]byte, error) {
intType: g.intType,
preserveOrder: g.preserveOrder,
usePointersForOptionalFields: g.usePointersForOptionalFields,
supportUnexpectedElements: g.supportUnexpectedElements,
unexpectedElementTypeName: g.unexpectedElementTypeName,
}

var typeElements []*element

if g.supportUnexpectedElements {
initializeUnexpectedElements(g.unexpectedElementTypeName)
options.importPackageNames["encoding/xml"] = struct{}{}
options.namedTypes = make(map[xml.Name]*element)
options.namedTypes[xml.Name{Local: g.unexpectedElementTypeName}] = unexpectedElement
g.typeElements[xml.Name{Local: g.unexpectedElementTypeName}] = unexpectedElement
appendUnexpectedElements(&g.typeElements, g.namedTypes, g.unexpectedElementTypeName)
}

if g.namedTypes {
options.namedTypes = maps.Clone(g.typeElements)
options.simpleTypes = make(map[xml.Name]struct{})
Expand All @@ -216,6 +251,13 @@ func (g *Generator) Generate() ([]byte, error) {
aExportedName := options.exportNameFunc(a.name)
bExportedName := options.exportNameFunc(b.name)
switch {
// Force unexpected element struct to the top of the source
case aExportedName == g.unexpectedElementTypeName && bExportedName != g.unexpectedElementTypeName:
return -1
case aExportedName == g.unexpectedElementTypeName && bExportedName == g.unexpectedElementTypeName:
return 0
case aExportedName != g.unexpectedElementTypeName && bExportedName == g.unexpectedElementTypeName:
return 1
case aExportedName < bExportedName:
return -1
case aExportedName == bExportedName:
Expand Down Expand Up @@ -356,3 +398,61 @@ FOR:
}
}
}

func appendUnexpectedElements(elementsMap *map[xml.Name]*element, createNamedTypes bool, unexpectedElementTypeName string) {
unexpectedElementsTypeName := fmt.Sprintf("%ss", unexpectedElementTypeName)
for _, element := range *elementsMap {
if element.name.Local == unexpectedElementTypeName {
continue
}
if createNamedTypes == false {
appendUnexpectedElements(&element.childElements, createNamedTypes, unexpectedElementTypeName)
}
element.childElements[xml.Name{Local: unexpectedElementsTypeName}] = unexpectedElements
element.repeatedChildren[xml.Name{Local: unexpectedElementsTypeName}] = struct{}{}
}
}

func initializeUnexpectedElements(UnexpectedElementTypeName string) {

Check failure on line 416 in generator.go

View workflow job for this annotation

GitHub Actions / lint

captLocal: `UnexpectedElementTypeName' should not be capitalized (gocritic)
unexpectedElementTypeNameWithPointerSymbol := fmt.Sprintf("*%s", UnexpectedElementTypeName)
UnexpectedElementsTypeName := fmt.Sprintf("%ss", UnexpectedElementTypeName)
unexpectedElement = &element{
name: xml.Name{Local: UnexpectedElementTypeName},
childElements: map[xml.Name]*element{
xml.Name{Local: "XMLName"}: &element{

Check failure on line 422 in generator.go

View workflow job for this annotation

GitHub Actions / lint

File is not `gofmt`-ed with `-s` (gofmt)
name: xml.Name{Local: "XMLName"},
charDataValue: value{
unexpectedElementTypeName: "xml.Name",
},
},
xml.Name{Local: "Attrs"}: &element{
name: xml.Name{Local: "Attrs"},
charDataValue: value{
unexpectedElementTypeName: "xml.Attr",
},
},
xml.Name{Local: "Content"}: &element{
name: xml.Name{Local: "Content"},
charDataValue: value{
unexpectedElementTypeName: "[]byte",
},
},
xml.Name{Local: "Nodes"}: &element{
name: xml.Name{Local: "Nodes"},
charDataValue: value{
unexpectedElementTypeName: unexpectedElementTypeNameWithPointerSymbol,
},
},
},
repeatedChildren: map[xml.Name]struct{}{
xml.Name{Local: "Attrs"}: struct{}{},
xml.Name{Local: "Nodes"}: struct{}{},
},
}
unexpectedElements = &element{
name: xml.Name{Local: UnexpectedElementsTypeName},
charDataValue: value{
unexpectedElementTypeName: unexpectedElementTypeNameWithPointerSymbol,
},
}
}
Loading

0 comments on commit 9491a05

Please sign in to comment.