Skip to content

Commit

Permalink
feat: generate compact types
Browse files Browse the repository at this point in the history
fixes #41
  • Loading branch information
meblum authored and twpayne committed Aug 26, 2024
1 parent ac4d079 commit 39132a9
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 10 deletions.
2 changes: 2 additions & 0 deletions cmd/goxmlstruct/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var (
namedRoot = flag.Bool("named-root", xmlstruct.DefaultNamedRoot, "create an XMLName field for the root element")
namedTypes = flag.Bool("named-types", xmlstruct.DefaultNamedTypes, "create named types for all elements")
noExport = flag.Bool("no-export", false, "create unexported types")
compactTypes = flag.Bool("compact-types", xmlstruct.DefaultCompactTypes, "create compact types")
output = flag.String("output", "", "output filename")
packageName = flag.String("package-name", "main", "package name")
preserveOrder = flag.Bool("preserve-order", xmlstruct.DefaultPreserveOrder, "preserve order of types and fields")
Expand Down Expand Up @@ -53,6 +54,7 @@ func run() error {
xmlstruct.WithNameFunc(nameFunc),
xmlstruct.WithNamedRoot(*namedRoot),
xmlstruct.WithNamedTypes(*namedTypes),
xmlstruct.WithCompactTypes(*compactTypes),
xmlstruct.WithPackageName(*packageName),
xmlstruct.WithPreserveOrder(*preserveOrder),
xmlstruct.WithTimeLayout(*timeLayout),
Expand Down
57 changes: 48 additions & 9 deletions element.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ func (e *element) writeGoType(w io.Writer, options *generateOptions, indentPrefi
})
} else {
slices.SortFunc(childElements, func(a, b *element) int {
aExportedName := options.exportNameFunc(a.name)
bExportedName := options.exportNameFunc(b.name)
aExportedName := exportedNameWithoutSuffix(a, options)
bExportedName := exportedNameWithoutSuffix(b, options)
switch {
case aExportedName < bExportedName:
return -1
Expand All @@ -192,8 +192,7 @@ func (e *element) writeGoType(w io.Writer, options *generateOptions, indentPrefi
}

for _, childElement := range childElements {
exportedChildName := options.exportNameFunc(childElement.name) + options.elemNameSuffix

exportedChildName := exportedName(childElement, options)
if _, ok := fieldNames[exportedChildName]; ok {
fieldNames[exportedChildName] = struct{}{}
}
Expand All @@ -207,18 +206,58 @@ func (e *element) writeGoType(w io.Writer, options *generateOptions, indentPrefi
fmt.Fprintf(w, "*")
}
}
if topLevelElement, ok := options.namedTypes[childElement.name]; ok {

currentChild := childElement
if options.compactTypes {
currentChild = firstNotContainerElement(childElement)
}
if topLevelElement, ok := options.namedTypes[currentChild.name]; ok {
fmt.Fprintf(w, "%s", options.exportTypeNameFunc(topLevelElement.name))
} else if _, ok := options.simpleTypes[childElement.name]; ok {
fmt.Fprintf(w, "%s", childElement.charDataValue.goType(options))
} else if _, ok := options.simpleTypes[currentChild.name]; ok {
fmt.Fprintf(w, "%s", currentChild.charDataValue.goType(options))
} else {
if err := childElement.writeGoType(w, options, indentPrefix+"\t"); err != nil {
if err := currentChild.writeGoType(w, options, indentPrefix+"\t"); err != nil {
return err
}
}
fmt.Fprintf(w, " `xml:\"%s\"`\n", childElement.name.Local)
fmt.Fprintf(w, " `xml:\"%s\"`\n", attrName(childElement, options.compactTypes))
}

fmt.Fprintf(w, "%s}", indentPrefix)
return nil
}

func (e *element) isContainer() bool {
return len(e.childElements) == 1 && len(e.attrValues) == 0 && e.charDataValue.observations == 0
}

func firstNotContainerElement(el *element) *element {
if el.isContainer() {
for _, v := range el.childElements {
return firstNotContainerElement(v)
}
}
return el
}

func exportedName(el *element, options *generateOptions) string {
return exportedNameWithoutSuffix(el, options) + options.elemNameSuffix
}

func exportedNameWithoutSuffix(el *element, options *generateOptions) string {
if el.isContainer() && options.compactTypes {
for _, v := range el.childElements {
return exportedNameWithoutSuffix(v, options)
}
}
return options.exportNameFunc(el.name)
}

func attrName(el *element, compactTypes bool) string {
if el.isContainer() && compactTypes {
for _, v := range el.childElements {
return el.name.Local + ">" + attrName(v, true)
}
}
return el.name.Local
}
20 changes: 19 additions & 1 deletion generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Generator struct {
nameFunc NameFunc
namedRoot bool
namedTypes bool
compactTypes bool
order int
packageName string
preserveOrder bool
Expand Down Expand Up @@ -168,6 +169,13 @@ func WithNamedTypes(namedTypes bool) GeneratorOption {
}
}

// WithCompactTypes sets whether to generate compact types.
func WithCompactTypes(compactTypes bool) GeneratorOption {
return func(o *Generator) {
o.compactTypes = compactTypes
}
}

// WithPackageName sets the package name of the generated Go source.
func WithPackageName(packageName string) GeneratorOption {
return func(g *Generator) {
Expand Down Expand Up @@ -226,6 +234,7 @@ func NewGenerator(options ...GeneratorOption) *Generator {
nameFunc: DefaultNameFunc,
namedRoot: DefaultNamedRoot,
namedTypes: DefaultNamedTypes,
compactTypes: DefaultCompactTypes,
packageName: DefaultPackageName,
preserveOrder: DefaultPreserveOrder,
timeLayout: DefaultTimeLayout,
Expand Down Expand Up @@ -264,6 +273,7 @@ func (g *Generator) Generate() ([]byte, error) {
importPackageNames: make(map[string]struct{}),
intType: g.intType,
namedRoot: g.namedRoot,
compactTypes: g.compactTypes,
preserveOrder: g.preserveOrder,
usePointersForOptionalFields: g.usePointersForOptionalFields,
emptyElements: g.emptyElements,
Expand All @@ -276,6 +286,12 @@ func (g *Generator) Generate() ([]byte, error) {
var typeElements []*element
if g.namedTypes {
options.namedTypes = maps.Clone(g.typeElements)
options.namedTypes = make(map[xml.Name]*element)
for k, v := range g.typeElements {
if !options.compactTypes || !v.isContainer() || v.root {
options.namedTypes[k] = v
}
}
options.simpleTypes = make(map[xml.Name]struct{})
for name, element := range options.namedTypes {
if len(element.attrValues) != 0 || len(element.childElements) != 0 || element.root {
Expand All @@ -294,7 +310,9 @@ func (g *Generator) Generate() ([]byte, error) {
return g.typeOrder[a.name] - g.typeOrder[b.name]
})
} else {
slices.SortFunc(typeElements, func(a, b *element) int {
slices.SortFunc(typeElements, func(a, b *element) int { ////////////////
// aExportedName := exportedName(a, &options)
// bExportedName := exportedName(b, &options)
aExportedName := options.exportNameFunc(a.name)
bExportedName := options.exportNameFunc(b.name)
switch {
Expand Down
145 changes: 145 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,151 @@ func TestGenerator(t *testing.T) {
`}`,
),
},
{
name: "compact_types",
options: []xmlstruct.GeneratorOption{
xmlstruct.WithImports(false),
xmlstruct.WithNamedRoot(false),
xmlstruct.WithPackageName(""),
xmlstruct.WithHeader(""),
xmlstruct.WithImports(false),
xmlstruct.WithCompactTypes(true),
},
xmlStr: joinLines(
`<a>`,
` <b foo="">`,
` <c>bar `,
` <d>`,
` <e></e>`,
` <f>`,
` <g></g>`,
` <g></g>`,
` </f>`,
` </d>`,
` </c>`,
` </b>`,
` <h>`,
` <i><j>bar</j></i>`,
` </h>`,
`</a>`,
),
expectedStr: joinLines(
`type A struct {`,
` B struct {`,
"\t\tFoo string `xml:\"foo,attr\"`",
` C struct {`,
"\t\t\tCharData string `xml:\",chardata\"`",
` D struct {`,
"\t\t\t\tE struct{} `xml:\"e\"`",
"\t\t\t\tG struct{} `xml:\"f>g\"`",
"\t\t\t} `xml:\"d\"`",
"\t\t} `xml:\"c\"`",
"\t} `xml:\"b\"`",
"\tJ string `xml:\"h>i>j\"`",
`}`,
),
},
{
name: "compact_named_types",
options: []xmlstruct.GeneratorOption{
xmlstruct.WithImports(false),
xmlstruct.WithPackageName(""),
xmlstruct.WithHeader(""),
xmlstruct.WithCompactTypes(true),
xmlstruct.WithNamedTypes(true),
},
xmlStr: joinLines(
`<a>`,
` <b foo="">`,
` <c>bar `,
` <d>`,
` <e></e>`,
` <f>`,
` <g></g>`,
` <g></g>`,
` </f>`,
` </d>`,
` </c>`,
` </b>`,
` <h>`,
` <i><j>bar</j></i>`,
` </h>`,
`</a>`,
),
expectedStr: joinLines(
"type A struct {",
"\tB B `xml:\"b\"`", //nolint:dupword
"\tJ string `xml:\"h>i>j\"`",
"}",
"",
"type B struct {",
"\tFoo string `xml:\"foo,attr\"`",
"\tC C `xml:\"c\"`", //nolint:dupword
"}",
"",
"type C struct {",
"\tCharData string `xml:\",chardata\"`",
"\tD D `xml:\"d\"`", //nolint:dupword
"}",
"",
"type D struct {",
"\tE struct{} `xml:\"e\"`",
"\tG struct{} `xml:\"f>g\"`",
"}",
),
},
{
name: "compact_named_types_innermost_attribute",
options: []xmlstruct.GeneratorOption{
xmlstruct.WithImports(false),
xmlstruct.WithPackageName(""),
xmlstruct.WithHeader(""),
xmlstruct.WithCompactTypes(true),
xmlstruct.WithNamedTypes(true),
},
xmlStr: joinLines(
`<a>`,
` <b>`,
` <c foo="">`,
` </c>`,
` </b>`,
`</a>`,
),
expectedStr: joinLines(
"type A struct {",
"\tC C `xml:\"b>c\"`", //nolint:dupword
"}",
"",
"type C struct {",
"\tFoo string `xml:\"foo,attr\"`",
"}",
),
},
{
name: "compact_types_order",
options: []xmlstruct.GeneratorOption{
xmlstruct.WithImports(false),
xmlstruct.WithPackageName(""),
xmlstruct.WithHeader(""),
xmlstruct.WithCompactTypes(true),
},
xmlStr: joinLines(
`<a>`,
` <d>`,
` <b>`,
` </b>`,
` </d>`,
` <c>`,
` </c>`,
`</a>`,
),
expectedStr: joinLines(
"type A struct {",
"\tB struct{} `xml:\"d>b\"`",
"\tC struct{} `xml:\"c\"`",
"}",
),
},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions xmlstruct.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
DefaultIntType = "int"
DefaultNamedRoot = false
DefaultNamedTypes = false
DefaultCompactTypes = false
DefaultPackageName = "main"
DefaultPreserveOrder = false
DefaultTimeLayout = "2006-01-02T15:04:05Z"
Expand Down Expand Up @@ -125,6 +126,7 @@ type generateOptions struct {
intType string
namedRoot bool
namedTypes map[xml.Name]*element
compactTypes bool
preserveOrder bool
simpleTypes map[xml.Name]struct{}
usePointersForOptionalFields bool
Expand Down

0 comments on commit 39132a9

Please sign in to comment.