Skip to content

Commit

Permalink
feat: add WithExportTypeNameFunc option
Browse files Browse the repository at this point in the history
fixes #36
  • Loading branch information
meblum authored and twpayne committed Aug 22, 2024
1 parent 19ded90 commit ac4d079
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 5 deletions.
9 changes: 7 additions & 2 deletions cmd/goxmlstruct/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
intType = flag.String("int-type", xmlstruct.DefaultIntType, "int type")
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")
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 All @@ -43,7 +44,7 @@ func run() error {
*packageName = ""
}

generator := xmlstruct.NewGenerator(
options := []xmlstruct.GeneratorOption{
xmlstruct.WithCharDataFieldName(*charDataFieldName),
xmlstruct.WithFormatSource(*formatSource),
xmlstruct.WithHeader(*header),
Expand All @@ -59,7 +60,11 @@ func run() error {
xmlstruct.WithUsePointersForOptionalFields(*usePointersForOptionalFields),
xmlstruct.WithUseRawToken(*useRawToken),
xmlstruct.WithEmptyElements(!*noEmptyElements),
)
}
if *noExport {
options = append(options, xmlstruct.WithExportTypeNameFunc(xmlstruct.DefaultUnexportNameFunc))
}
generator := xmlstruct.NewGenerator(options...)

if flag.NArg() == 0 {
if err := generator.ObserveReader(os.Stdin); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion element.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (e *element) writeGoType(w io.Writer, options *generateOptions, indentPrefi
}
}
if topLevelElement, ok := options.namedTypes[childElement.name]; ok {
fmt.Fprintf(w, "%s", options.exportNameFunc(topLevelElement.name))
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 {
Expand Down
15 changes: 14 additions & 1 deletion generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Generator struct {
charDataFieldName string
elemNameSuffix string
exportNameFunc ExportNameFunc
exportTypeNameFunc ExportNameFunc
exportRenames map[string]string
formatSource bool
header string
Expand Down Expand Up @@ -94,6 +95,14 @@ func WithExportNameFunc(exportNameFunc ExportNameFunc) GeneratorOption {
}
}

// WithExportTypeNameFunc sets the export name function for the generated Go source Types.
// This is useful when unexported types are desired.
func WithExportTypeNameFunc(exportTypeNameFunc ExportNameFunc) GeneratorOption {
return func(g *Generator) {
g.exportTypeNameFunc = exportTypeNameFunc
}
}

// WithExportRenames sets the export renames. It is overridden by
// WithExportRenameFunc.
func WithExportRenames(exportRenames map[string]string) GeneratorOption {
Expand Down Expand Up @@ -236,6 +245,9 @@ func NewGenerator(options ...GeneratorOption) *Generator {
for _, option := range options {
option(g)
}
if g.exportTypeNameFunc == nil {
g.exportTypeNameFunc = g.exportNameFunc
}
return g
}

Expand All @@ -247,6 +259,7 @@ func (g *Generator) Generate() ([]byte, error) {
charDataFieldName: g.charDataFieldName,
elemNameSuffix: g.elemNameSuffix,
exportNameFunc: g.exportNameFunc,
exportTypeNameFunc: g.exportTypeNameFunc,
header: g.header,
importPackageNames: make(map[string]struct{}),
intType: g.intType,
Expand Down Expand Up @@ -298,7 +311,7 @@ func (g *Generator) Generate() ([]byte, error) {
typesBuilder := &strings.Builder{}
typeNames := make(map[string]struct{})
for _, typeElement := range typeElements {
typeName := options.exportNameFunc(typeElement.name)
typeName := options.exportTypeNameFunc(typeElement.name)
if _, ok := typeNames[typeName]; ok {
return nil, fmt.Errorf("%s: duplicate type name", typeName)
}
Expand Down
27 changes: 27 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,33 @@ func TestGenerator(t *testing.T) {
`}`,
),
},
{
name: "unexported_named_types",
options: []xmlstruct.GeneratorOption{
xmlstruct.WithNamedTypes(true),
xmlstruct.WithExportTypeNameFunc(xmlstruct.DefaultUnexportNameFunc),
},
xmlStr: joinLines(
`<ID>`,
` <B>`,
` <c/>`,
` </B>`,
`</ID>`,
),
expectedStr: joinLines(
xmlstruct.DefaultHeader,
``,
`package main`,
``,
`type b struct {`,
"\tC struct{} `xml:\"c\"`",
`}`,
``,
`type id struct {`,
"\tB b `xml:\"B\"`",
`}`,
),
},
{
name: "no_package",
options: []xmlstruct.GeneratorOption{
Expand Down
24 changes: 23 additions & 1 deletion xmlstruct.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ var (
nonIdentifierRuneRx = regexp.MustCompile(`[^\pL\pN]`)

// DefaultExportNameFunc returns name.Local with kebab- and snakecase words
// converted to camelcase and any Id suffix converted to ID.
// converted to UpperCamelCase and any Id suffix converted to ID.
DefaultExportNameFunc = func(name xml.Name) string {
localName := kebabOrSnakeCaseWordBoundaryRx.ReplaceAllStringFunc(name.Local, func(s string) string {
return strings.ToUpper(s[len(s)-1:])
Expand All @@ -55,6 +55,27 @@ var (
}
return string(runes)
}

// DefaultUnexportNameFunc returns name.Local with kebab- and snakecase words
// converted to lowerCamelCase
// Any ID prefix is converted to id, and any Id suffix converted to ID.
DefaultUnexportNameFunc = func(name xml.Name) string {
localName := kebabOrSnakeCaseWordBoundaryRx.ReplaceAllStringFunc(name.Local, func(s string) string {
return strings.ToUpper(s[len(s)-1:])
})
localName = nonIdentifierRuneRx.ReplaceAllLiteralString(localName, "_")
runes := []rune(localName)
runes[0] = unicode.ToLower(runes[0])
if len(runes) > 1 {
if runes[len(runes)-2] == 'I' && runes[len(runes)-1] == 'd' {
runes[len(runes)-1] = 'D'
}
if runes[0] == 'i' && runes[1] == 'D' {
runes[1] = 'd'
}
}
return string(runes)
}
)

var (
Expand Down Expand Up @@ -98,6 +119,7 @@ type generateOptions struct {
charDataFieldName string
elemNameSuffix string
exportNameFunc ExportNameFunc
exportTypeNameFunc ExportNameFunc
header string
importPackageNames map[string]struct{}
intType string
Expand Down
68 changes: 68 additions & 0 deletions xmlstruct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,71 @@ func TestDefaultExportNameFunc(t *testing.T) {
})
}
}

func TestDefaultUnexportNameFunc(t *testing.T) {
t.Parallel()

for _, tc := range []struct {
localName string
expected string
}{
{
localName: "id",
expected: "id",
},
{
localName: "ID",
expected: "id",
},
{
localName: "Id",
expected: "id",
},
{
localName: "camelCase",
expected: "camelCase",
},
{
localName: "camelCaseId",
expected: "camelCaseID",
},
{
localName: "kebab-case",
expected: "kebabCase",
},
{
localName: "kebab--case",
expected: "kebabCase",
},
{
localName: "kebab-id",
expected: "kebabID",
},
{
localName: "snake_case",
expected: "snakeCase",
},
{
localName: "snake__case",
expected: "snakeCase",
},
{
localName: "snake-id",
expected: "snakeID",
},
{
localName: "+",
expected: "_",
},
} {
tc := tc
t.Run(tc.localName, func(t *testing.T) {
t.Parallel()

xmlName := xml.Name{
Local: tc.localName,
}
assert.Equal(t, tc.expected, DefaultUnexportNameFunc(xmlName))
})
}
}

0 comments on commit ac4d079

Please sign in to comment.