forked from danil/goxsd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgenerate.go
390 lines (328 loc) · 9 KB
/
generate.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
package goxsd
import (
"bytes"
"fmt"
"go/token"
"go/types"
"io"
"strings"
"text/template"
"unicode"
"golang.org/x/tools/imports"
)
var (
// Struct field generated from an element attribute
// TODO: add omitempty if minOccurences = 0
attr = "{{ define \"Attr\" }}{{ (lintTitle .Name) }} {{ (lint .Type) }} `xml:\"{{ .Name }}{{ (omitEmpty .OmitEmpty) }},attr\"`{{ end }}"
// Struct field generated from an element child element
child = "{{ define \"Child\" }}{{ (lintTitle .Name) }} {{ if .List }}[]{{ end }}{{ (typeName (fieldType .)) }} `xml:\"{{ .Name }}{{ (omitEmpty .OmitEmpty) }}\"`{{ end }}"
// Struct field generated from the character data of an element
cdata = `{{ define "Cdata" }}{{ printf "%s %s ` + "`xml:\\\",cdata\\\"`" + `" (lintTitle .Name) (lint .Type) }}{{ end }}`
// Struct generated from a non-trivial element (with children and/or attributes)
// TODO: Add the Annotation comments for both the Structs and Fields.
elem = `
// {{ typeName .StructName }} is generated from an XSD element.
type {{ typeName .StructName }} struct {
{{- range $a := .Attribs }}
{{ template "Attr" $a }}{{ end }}
{{- range $c := .Children }}
{{ template "Child" $c }}{{ end }}
{{- if .Cdata }}
{{ template "Cdata" . }}{{ end -}}
}
`
)
var (
// The initialism pairs are based on the commonInitialisms found in golang/lint
// https://github.com/golang/lint/blob/4946cea8b6efd778dc31dc2dbeb919535e1b7529/lint.go#L698-L738
//
initialismPairs = []string{
"Api", "API",
"Ascii", "ASCII",
"Cpu", "CPU",
"Css", "CSS",
"Dns", "DNS",
"Eof", "EOF",
"Guid", "GUID",
"Html", "HTML",
"Https", "HTTPS",
"Http", "HTTP",
"Id", "ID",
"Ip", "IP",
"Json", "JSON",
"Lhs", "LHS",
"Qps", "QPS",
"Ram", "RAM",
"Rhs", "RHS",
"Rpc", "RPC",
"Sla", "SLA",
"Smtp", "SMTP",
"Sql", "SQL",
"Ssh", "SSH",
"Tcp", "TCP",
"Tls", "TLS",
"Ttl", "TTL",
"Udp", "UDP",
"Uid", "UID",
"Ui", "UI",
"Uuid", "UUID",
"Uri", "URI",
"Url", "URL",
"Utf8", "UTF8",
"Vm", "VM",
"Xml", "XML",
"Xsrf", "XSRF",
"Xss", "XSS",
}
initialisms = strings.NewReplacer(initialismPairs...)
)
// Generator is responsible for generating Go structs based on a given XML
// schema tree.
type Generator struct {
Package string
Prefix string
Exported bool
Translator func(string) string
types map[string]struct{}
}
func (g Generator) Do(out io.Writer, roots []*xmlTree) error {
g.types = make(map[string]struct{})
if g.Translator == nil {
g.Translator = func(s string) string { return s }
}
tt, err := prepareTemplates(g.Prefix, g.Exported, g.Translator)
if err != nil {
return fmt.Errorf("could not prepare templates: %s", err)
}
var res bytes.Buffer
if g.Package != "" {
fmt.Fprintf(&res, `
// generated by goxsd; DO NOT EDIT
package %s
type (
AnyURIXML string
BooleanXML bool
DateTimeXSD time.Time
DateXSD time.Time
DecimalXML float64
DurationXML time.Duration
Float64XML float64
IntXML int
IntegerXML int
LanguageXML string
LongIntXML int64
NameXML string
NonNegativeIntegerXML string
NormalizedStringXML string
PositiveIntegerXML uint
ShortIntXML int16
StringXML string
TimeXSD time.Time
TokenXML string
UnsignedShortIntXML uint16
)
`, g.Package)
}
for _, e := range roots {
if err := g.execute(e, tt, &res); err != nil {
return err
}
}
buf, err := imports.Process("", res.Bytes(), &imports.Options{
Fragment: true,
Comments: true,
TabIndent: true,
TabWidth: 8,
})
if err != nil {
return err
}
if _, err := io.Copy(out, bytes.NewBuffer(buf)); err != nil {
return err
}
return nil
}
func (g Generator) execute(root *xmlTree, tt *template.Template, out io.Writer) error {
root.StructName = root.Type
if goPrimitiveType(root.Type) {
root.StructName = root.Name
}
if _, ok := g.types[root.StructName]; ok {
return nil
}
if err := tt.Execute(out, root); err != nil {
return err
}
g.types[root.StructName] = struct{}{}
for _, e := range root.Children {
if !primitiveType(e) {
if err := g.execute(e, tt, out); err != nil {
return err
}
}
}
return nil
}
func prepareTemplates(prefix string, exported bool, translator func(string) string) (*template.Template, error) {
typeName := func(name string) string {
if goPrimitiveType(name) {
return name
}
if prefix != "" {
name = prefix + strings.Title(name)
}
if exported {
name = strings.Title(name)
}
return lint(name, translator)
}
omitEmpty := func(empty bool) string {
if !empty {
return ""
}
return ",omitempty"
}
fmap := template.FuncMap{
"lint": func(s string) string { return lint(s, translator) },
"lintTitle": func(s string) string { return lintTitle(s, translator) },
"typeName": typeName,
"fieldType": fieldType,
"omitEmpty": omitEmpty,
}
tt := template.New("yyy").Funcs(fmap)
if _, err := tt.Parse(attr); err != nil {
return nil, err
}
if _, err := tt.Parse(cdata); err != nil {
return nil, err
}
if _, err := tt.Parse(child); err != nil {
return nil, err
}
if _, err := tt.Parse(elem); err != nil {
return nil, err
}
return tt, nil
}
// If this is a chardata field, the field type must point to a
// struct, even if the element type is a built-in primitive.
func fieldType(e *xmlTree) (res string) {
if e.Cdata {
res = e.Name
} else {
res = e.Type
}
if !e.List && e.OmitEmpty {
res = "*" + res
}
return
}
func omitEmpty(e *xmlTree) string {
if e.OmitEmpty {
return ",omitempty"
}
return ""
}
func primitiveType(e *xmlTree) bool {
if e.Cdata {
return false
}
return goPrimitiveType(e.Type)
}
func goPrimitiveType(t string) bool {
t = strings.Replace(t, "*", "", 1)
switch t {
case "bool", "BooleanXML",
"time.Duration", "DurationXML",
"string", "AnyURIXML", "StringXML", "NameXML", "NormalizedStringXML", "TokenXML",
"int", "IntXML", "IntegerXML",
"int64", "LongIntXML",
"int16", "ShortIntXML",
"uint", "PositiveIntegerXML", "NonNegativeIntegerXML", "UnsignedShortIntXML",
"float64", "DecimalXML", "Float64XML",
"time.Time", "DateTimeXSD", "DateXSD", "TimeXSD":
return true
}
return false
}
// lint converts XML identifiers to Go identifiers.
//
// Valid Go Identifiers:
// identifier = letter { letter | unicode_digit }
// See https://golang.org/ref/spec#Identifiers
//
// Valid XML Identifiers:
// See http://www.w3schools.com/xml/xml_elements.asp
//
// XML Naming Rules
// XML elements must follow these naming rules:
//
// Element names are case-sensitive
// Element names must start with a letter or underscore
// Element names cannot start with the letters xml (or XML, or Xml, etc)
// Element names can contain letters, digits, hyphens, underscores, and periods
// Element names cannot contain spaces
// Any name can be used, no words are reserved (except xml).
func lint(str string, translator func(string) string) string {
if str == "" {
// FIXME: is this an error? Why was a default type not set earlier?
return "string"
}
// Is the string a Go builtin type?
// TODO: This is pulling in two packages to check if the type is a Go builtin. Is this necessary?
if t, _ := types.Eval(token.NewFileSet(), nil, 0, str); t.IsType() {
return str
}
fields := strings.FieldsFunc(str, func(r rune) bool {
// Underscores are valid characters in Go identifiers, but I have not seen it
// used often in Go code and without consistency the generated code violates
// the principle of least astonishment. It is easier to expect all generated
// code to be in CamelCase instead of a potential mixture of Snake/CamelCase.
//
// FIXME: This transformation introduces a higher risk of name collisions.
// Add a check for name collisions and/or add a predictable rule to resolve
// name collisions such as appending an incrementing number to the conflicting
// identifier.
return r == ' ' || r == '-' || r == '_'
})
imported := importedType(str)
// Convert to Pascal/CamelCase
for i := range fields {
if imported && i == 0 {
continue
}
s := strings.Title(fields[i])
s = initialisms.Replace(s)
s = translator(s)
fields[i] = s
}
// TODO: if the user has not requested exported types, convert first letter to
// lowercase.
// This code is meant to handle unicode, but not all all unicode characters have
// a lowercase. Use unicode.IsLower() to detect this condition and add a default
// lowercase prefix such as an ASCII 'x'.
//r, n := utf8.DecodeRuneInString(fields[0])
//fields[0] = string(unicode.ToLower(r)) + fields[0][n:]
return strings.Join(fields, "")
}
func importedType(s string) bool {
if len(s) < 3 {
return false
}
if strings.ContainsRune(s, ' ') {
return false
}
// Is ASCII.
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
if s[0] == '.' || s[len(s)-1] == '.' {
return false
}
return strings.Count(s, ".") == 1
}
func lintTitle(s string, translator func(string) string) string {
return lint(strings.Title(s), translator)
}