1 package main
2
3 import (
4 "fmt"
5 "regexp"
6 "strconv"
7 "strings"
8 )
9
10
11
12
13 var (
14 newStructs []Struct
15 newEnums []Enum
16 )
17
18 var enums = make(map[string]Enum)
19
20 var types = map[string]Type{
21 "bool": Bool{},
22 "int8": Int8{},
23 "int16": Int16{},
24 "uint16": Uint16{},
25 "int32": Int32{},
26 "int64": Int64{},
27 "float64": Float64{},
28 "uint32": Uint32{},
29 "varint": Varint{},
30 "varlong": Varlong{},
31 "uuid": Uuid{},
32 "string": String{},
33 "nullable-string": NullableString{},
34 "bytes": Bytes{},
35 "nullable-bytes": NullableBytes{},
36 "varint-string": VarintString{},
37 "varint-bytes": VarintBytes{},
38 }
39
40
41 type LineScanner struct {
42 lineno int
43 buf string
44 nlat int
45 }
46
47 func (l *LineScanner) Ok() bool {
48 if l.nlat >= 0 {
49 return true
50 }
51 l.nlat = strings.IndexByte(l.buf, '\n')
52 return l.nlat >= 0
53 }
54
55 func (l *LineScanner) Peek() string {
56 return l.buf[:l.nlat]
57 }
58
59 func (l *LineScanner) Next() {
60 l.lineno++
61 l.buf = l.buf[l.nlat+1:]
62 l.nlat = -1
63 }
64
65
66
67
68
69
70 func (s *Struct) BuildFrom(scanner *LineScanner, key, level int) (done bool) {
71 fieldSpaces := strings.Repeat(" ", 2*(level+1))
72
73 var nextComment string
74 var err error
75
76 for !done && scanner.Ok() {
77 line := scanner.Peek()
78 if len(line) == 0 {
79 scanner.Next()
80 return true
81 }
82 if !strings.HasPrefix(line, fieldSpaces) {
83 return false
84 }
85
86 scanner.Next()
87
88 line = line[len(fieldSpaces):]
89 if strings.HasPrefix(line, "//") {
90 if nextComment != "" {
91 nextComment += "\n"
92 }
93 nextComment += line
94 continue
95 }
96
97
98
99
100
101 if strings.Contains(line, "ThrottleMillis") {
102 if nextComment != "" {
103 die("unexpected comment on ThrottleMillis: %s", nextComment)
104 }
105 s.Fields = append(s.Fields, parseThrottleMillis(line))
106 continue
107 }
108
109
110
111 if strings.Contains(line, "TimeoutMillis") && !strings.Contains(line, ":") {
112 if nextComment != "" {
113 die("unexpected comment on TimeoutMillis: %s", nextComment)
114 }
115 s.Fields = append(s.Fields, parseTimeoutMillis(line))
116 continue
117 }
118
119
120 fields := strings.Split(line, ": ")
121 if len(fields) != 2 || len(fields[0]) == 0 || len(fields[1]) == 0 {
122 die("improper struct field format on line %q (%d)", line, scanner.lineno)
123 }
124
125 f := StructField{
126 Comment: nextComment,
127 FieldName: fields[0],
128 MaxVersion: -1,
129 Tag: -1,
130 }
131 nextComment = ""
132
133 typ := fields[1]
134
135
136
137 if idx := strings.Index(typ, " // "); idx >= 0 {
138 f.MinVersion, f.MaxVersion, f.Tag, err = parseFieldComment(typ[idx:])
139 if err != nil {
140 die("unable to parse field comment on line %q: %v", line, err)
141 }
142 typ = typ[:idx]
143 }
144
145
146
147
148
149
150
151
152
153 var isArray,
154 isVarintArray,
155 isNullableArray bool
156 nullableVersion := 0
157 arrayLevel := strings.Count(typ, "[")
158 if arrayLevel > 0 {
159 if strings.HasPrefix(typ, "varint[") {
160 isVarintArray = true
161 typ = typ[len("varint"):]
162 } else if strings.HasPrefix(typ, "nullable") {
163 isNullableArray = true
164 typ = typ[len("nullable"):]
165 if strings.HasPrefix(typ, "-v") {
166 typ = typ[len("-v"):]
167 vend := strings.IndexByte(typ, '[')
168 if vend < 2 {
169 die("empty nullable array version number")
170 }
171 if typ[vend-1] != '+' {
172 die("max version number bound is unhandled in arrays")
173 }
174 if nullableVersion, err = strconv.Atoi(typ[:vend-1]); err != nil {
175 die("improper nullable array version number %q: %v", typ[:vend-1], err)
176 }
177 typ = typ[vend:]
178 }
179 }
180 typ = typ[arrayLevel : len(typ)-arrayLevel]
181 isArray = true
182 }
183
184
185 var hasDefault bool
186 var def string
187 if start := strings.IndexByte(typ, '('); start >= 0 {
188 end := strings.IndexByte(typ[start:], ')')
189 if end <= 0 {
190 die("invalid default: start %d, end %d", start, end)
191 }
192 hasDefault = true
193 def = typ[start+1 : start+end]
194 typ = typ[:start]
195 if len(f.Comment) > 0 {
196 f.Comment += "\n//\n"
197 }
198 f.Comment += "// This field has a default of " + def + "."
199 }
200
201 switch {
202 case strings.HasPrefix(typ, "=>") || strings.HasPrefix(typ, "nullable=>"):
203 newS := Struct{
204 FromFlexible: s.FromFlexible,
205 FlexibleAt: s.FlexibleAt,
206 Nullable: strings.HasPrefix(typ, "nullable"),
207 }
208 newS.Name = s.Name + f.FieldName
209 newS.Key = key
210 newS.Anonymous = true
211 if isArray {
212 if rename := typ[2:]; rename != "" {
213 newS.Name = s.Name + rename
214 } else {
215 newS.Name = strings.TrimSuffix(newS.Name, "s")
216 }
217 }
218 done = newS.BuildFrom(scanner, key, level+1)
219 f.Type = newS
220 newStructs = append(newStructs, newS)
221
222 case strings.HasPrefix(typ, "length-field-minus => "):
223 typ = strings.TrimPrefix(typ, "length-field-minus => ")
224 from, minus, err := parseFieldLength(typ)
225 if err != nil {
226 die("unable to parse field-length-bytes in %q: %v", typ, err)
227 }
228 f.Type = FieldLengthMinusBytes{
229 Field: from,
230 LengthMinus: minus,
231 }
232
233 case strings.HasPrefix(typ, "nullable-string-v"):
234 if typ[len(typ)-1] != '+' {
235 die("invalid missing + at end of nullable-string-v; nullable-strings cannot become nullable and then become non-nullable")
236 }
237 if nullableVersion, err = strconv.Atoi(typ[len("nullable-string-v") : len(typ)-1]); err != nil {
238 die("improper nullable string version number in %q: %v", typ, err)
239 }
240 f.Type = NullableString{
241 FromFlexible: s.FromFlexible,
242 NullableVersion: nullableVersion,
243 }
244
245 case strings.HasPrefix(typ, "enum-"):
246 typ = strings.TrimPrefix(typ, "enum-")
247 if _, ok := enums[typ]; !ok {
248 die("unknown enum %q on line %q", typ, line)
249 }
250 f.Type = enums[typ]
251
252 if hasDefault {
253 f.Type = f.Type.(Defaulter).SetDefault(def)
254 }
255
256 default:
257 got := types[typ]
258 if got == nil {
259 die("unknown type %q on line %q", typ, line)
260 }
261 if s, ok := got.(Struct); ok {
262
263
264
265
266
267
268 if s.WithNoEncoding && s.Key == -1 {
269 for i := range newStructs {
270 if newStructs[i].Name == s.Name {
271 newStructs[i].Key = key
272 }
273 }
274 s.Key = key
275 types[typ] = s
276 }
277 }
278 f.Type = got
279
280 if hasDefault {
281 f.Type = f.Type.(Defaulter).SetDefault(def)
282 }
283
284 if s.FromFlexible {
285 if setter, ok := f.Type.(FlexibleSetter); ok {
286 f.Type = setter.AsFromFlexible()
287 }
288 }
289 }
290
291
292
293 if isArray {
294 for arrayLevel > 1 {
295 f.Type = Array{Inner: f.Type, FromFlexible: s.FromFlexible}
296 arrayLevel--
297 }
298 f.Type = Array{
299 Inner: f.Type,
300 IsVarintArray: isVarintArray,
301 IsNullableArray: isNullableArray,
302 NullableVersion: nullableVersion,
303 FromFlexible: s.FromFlexible,
304 }
305 }
306
307 s.Fields = append(s.Fields, f)
308 }
309
310 return done
311 }
312
313
314
315
316
317
318 var fieldRe = regexp.MustCompile(`^ // (?:v(\d+)(?:\+|\-v(\d+))(?:, tag (\d+))?|tag (\d+))$`)
319
320 func parseFieldComment(in string) (min, max, tag int, err error) {
321 match := fieldRe.FindStringSubmatch(in)
322 if len(match) == 0 {
323 return 0, 0, 0, fmt.Errorf("invalid field comment %q", in)
324 }
325
326 if match[4] != "" {
327 tag, _ := strconv.Atoi(match[4])
328 return -1, -1, tag, nil
329 }
330
331 min, _ = strconv.Atoi(match[1])
332 max, _ = strconv.Atoi(match[2])
333 tag, _ = strconv.Atoi(match[3])
334 if match[2] == "" {
335 max = -1
336 } else if max < min {
337 return 0, 0, 0, fmt.Errorf("min %d > max %d on line %q", min, max, in)
338 }
339 if match[3] == "" {
340 tag = -1
341 }
342 return min, max, tag, nil
343 }
344
345 func parseFieldLength(in string) (string, int, error) {
346 lr := strings.Split(in, " - ")
347 if len(lr) != 2 {
348 return "", 0, fmt.Errorf("expected only two fields around ' = ', saw %d", len(lr))
349 }
350 length, err := strconv.Atoi(lr[1])
351 if err != nil {
352 return "", 0, fmt.Errorf("unable to parse length sub in %q", lr[1])
353 }
354 return lr[0], length, nil
355 }
356
357
358
359
360 var throttleRe = regexp.MustCompile(`^ThrottleMillis(?:\((\d+)\))?(?: // v(\d+)\+)?$`)
361
362 func parseThrottleMillis(in string) StructField {
363 match := throttleRe.FindStringSubmatch(in)
364 if len(match) == 0 {
365 die("throttle line does not match: %s", in)
366 }
367
368 typ := Throttle{}
369 typ.Switchup, _ = strconv.Atoi(match[1])
370
371 s := StructField{
372 MaxVersion: -1,
373 Tag: -1,
374 FieldName: "ThrottleMillis",
375 Type: typ,
376 }
377 s.MinVersion, _ = strconv.Atoi(match[2])
378
379 const switchupFmt = `// ThrottleMillis is how long of a throttle Kafka will apply to the client
380 // after this request.
381 // For Kafka < 2.0.0, the throttle is applied before issuing a response.
382 // For Kafka >= 2.0.0, the throttle is applied after issuing a response.
383 //
384 // This request switched at version %d.`
385
386 const static = `// ThrottleMillis is how long of a throttle Kafka will apply to the client
387 // after responding to this request.`
388
389 s.Comment = static
390 if typ.Switchup > 0 {
391 s.Comment = fmt.Sprintf(switchupFmt, typ.Switchup)
392 }
393
394 return s
395 }
396
397
398
399
400 var timeoutRe = regexp.MustCompile(`^TimeoutMillis(?:\((\d+)\))?(?: // v(\d+)\+)?$`)
401
402 func parseTimeoutMillis(in string) StructField {
403 match := timeoutRe.FindStringSubmatch(in)
404 if len(match) == 0 {
405 die("timeout line does not match: %s", in)
406 }
407
408 s := StructField{
409 Comment: `// TimeoutMillis is how long Kafka can wait before responding to this request.
410 // This field has no effect on Kafka's processing of the request; the request
411 // will continue to be processed if the timeout is reached. If the timeout is
412 // reached, Kafka will reply with a REQUEST_TIMED_OUT error.`,
413 MaxVersion: -1,
414 Tag: -1,
415 FieldName: "TimeoutMillis",
416 Type: Timeout{},
417 }
418 s.MinVersion, _ = strconv.Atoi(match[2])
419 def := "15000"
420 if match[1] != "" {
421 def = match[1]
422 }
423 s.Comment += "\n//\n// This field has a default of " + def + "."
424 s.Type = s.Type.(Defaulter).SetDefault(def)
425
426 return s
427 }
428
429
430
431
432
433
434 var notTopLevelRe = regexp.MustCompile(`^([A-Za-z0-9]+) => not top level(?:, (?:(no encoding)|(with version field))(?:, flexible v(\d+)\+)?)?$`)
435
436
437
438 func Parse(raw []byte) {
439 scanner := &LineScanner{
440 buf: string(raw),
441 nlat: -1,
442 }
443
444 var nextComment strings.Builder
445 resetComment := func() {
446 l := nextComment.Len()
447 nextComment.Reset()
448 nextComment.Grow(l)
449 }
450
451 for scanner.Ok() {
452 line := scanner.Peek()
453 scanner.Next()
454 if len(line) == 0 {
455 resetComment()
456 continue
457 }
458
459 if strings.HasPrefix(line, "//") {
460 if nextComment.Len() > 0 {
461 nextComment.WriteByte('\n')
462 }
463 nextComment.WriteString(line)
464 continue
465 }
466
467 s := Struct{
468 Comment: nextComment.String(),
469
470 FlexibleAt: -1,
471 }
472 resetComment()
473
474 topLevel := true
475 withVersionField, withNoEncoding := false, false
476
477 flexibleAt := -1
478 fromFlexible := false
479
480 nameMatch := notTopLevelRe.FindStringSubmatch(line)
481 name := line
482 parseNoEncodingFlexible := func() {
483 name = nameMatch[1]
484 if nameMatch[4] != "" {
485 flexible, err := strconv.Atoi(nameMatch[4])
486 if err != nil || flexible < 0 {
487 die("flexible version on line %q parse err: %v", line, err)
488 }
489 flexibleAt = flexible
490 fromFlexible = true
491 }
492 }
493 switch {
494 case len(nameMatch) == 0:
495 case nameMatch[2] != "":
496 withNoEncoding = true
497 parseNoEncodingFlexible()
498 case nameMatch[3] != "":
499 withVersionField = true
500 parseNoEncodingFlexible()
501 default:
502 name = nameMatch[1]
503 }
504
505 key := -1
506 save := func() {
507 s.Name = name
508 s.TopLevel = topLevel
509 s.WithVersionField = withVersionField
510 s.WithNoEncoding = withNoEncoding
511 s.Key = key
512 if !topLevel && fromFlexible {
513 s.FromFlexible = fromFlexible
514 s.FlexibleAt = flexibleAt
515 }
516
517 s.BuildFrom(scanner, key, 0)
518 types[name] = s
519 newStructs = append(newStructs, s)
520 }
521
522 if line != name {
523 topLevel = false
524 save()
525 continue
526 }
527
528 if strings.HasSuffix(name, "Response =>") {
529 last := strings.Replace(name, "Response =>", "Request", 1)
530 prior := &newStructs[len(newStructs)-1]
531 if prior.Name != last {
532 die("from %q does not refer to last message defn on line %q", last, line)
533 }
534 name = strings.TrimSuffix(name, " =>")
535 prior.ResponseKind = name
536 s.RequestKind = prior.Name
537 if prior.FromFlexible {
538 s.FlexibleAt = prior.FlexibleAt
539 s.FromFlexible = true
540 }
541 key = prior.Key
542 s.MaxVersion = prior.MaxVersion
543 save()
544 continue
545 }
546
547
548
549
550 delim := strings.Index(name, " =>")
551 if delim == -1 {
552 die("missing struct delimiter on line %q", line)
553 }
554 rem := name[delim+3:]
555 name = name[:delim]
556
557 if idx := strings.Index(rem, ", admin"); idx > 0 {
558 s.Admin = true
559 if rem[idx:] != ", admin" {
560 die("unknown content after \"admin\" in %q", rem[idx:])
561 }
562 rem = rem[:idx]
563 } else if idx := strings.Index(rem, ", group coordinator"); idx > 0 {
564 s.GroupCoordinator = true
565 if rem[idx:] != ", group coordinator" {
566 die("unknown content after \"group coordinator\" in %q", rem[idx:])
567 }
568 rem = rem[:idx]
569 } else if idx := strings.Index(rem, ", txn coordinator"); idx > 0 {
570 s.TxnCoordinator = true
571 if rem[idx:] != ", txn coordinator" {
572 die("unknown content q after \"txn coordinator\" in %q", rem[idx:])
573 }
574 rem = rem[:idx]
575 }
576
577 if strings.HasSuffix(rem, "+") {
578 const flexibleStr = ", flexible v"
579 if idx := strings.Index(rem, flexibleStr); idx == -1 {
580 die("missing flexible text on string ending with + in %q", rem)
581 } else {
582 flexible, err := strconv.Atoi(rem[idx+len(flexibleStr) : len(rem)-1])
583 if err != nil || flexible < 0 {
584 die("flexible version on line %q parse err: %v", line, err)
585 }
586 s.FlexibleAt = flexible
587 s.FromFlexible = true
588 rem = rem[:idx]
589 }
590 }
591
592 const maxStr = ", max version "
593 if idx := strings.Index(rem, maxStr); idx == -1 {
594 die("missing max version on line %q", line)
595 } else {
596 max, err := strconv.Atoi(rem[idx+len(maxStr):])
597 if err != nil {
598 die("max version on line %q parse err: %v", line, err)
599 }
600 s.MaxVersion = max
601 rem = rem[:idx]
602 }
603 const keyStr = " key "
604 if idx := strings.Index(rem, keyStr); idx == -1 {
605 die("missing key on line %q", line)
606 } else {
607 var err error
608 if key, err = strconv.Atoi(rem[idx+len(keyStr):]); err != nil {
609 die("key on line %q pare err: %v", line, err)
610 }
611 if key > maxKey {
612 maxKey = key
613 }
614 }
615
616 save()
617 }
618 }
619
620 func ParseEnums(raw []byte) {
621 scanner := &LineScanner{
622 buf: string(raw),
623 nlat: -1,
624 }
625
626 var nextComment strings.Builder
627 resetComment := func() {
628 l := nextComment.Len()
629 nextComment.Reset()
630 nextComment.Grow(l)
631 }
632
633 writeComment := func(line string) {
634 if nextComment.Len() > 0 {
635 nextComment.WriteByte('\n')
636 }
637 nextComment.WriteString(line)
638 }
639
640 getComment := func() string {
641 r := nextComment.String()
642 resetComment()
643 return r
644 }
645
646
647
648
649 enumNameRe := regexp.MustCompile(`^([A-Za-z]+) ([^ ]+) (camelcase )?\($`)
650
651
652 enumFieldRe := regexp.MustCompile(`^ {2}(\d+): ([A-Z_a-z]+)$`)
653
654 for scanner.Ok() {
655 line := scanner.Peek()
656 scanner.Next()
657 if len(line) == 0 {
658 resetComment()
659 continue
660 }
661
662 if strings.HasPrefix(line, "//") {
663 writeComment(line)
664 continue
665 }
666 nameMatch := enumNameRe.FindStringSubmatch(line)
667 if len(nameMatch) == 0 {
668 die("invalid enum name, unable to match `Name type (`")
669 }
670
671 e := Enum{
672 Comment: getComment(),
673
674 Name: nameMatch[1],
675 Type: types[nameMatch[2]],
676 CamelCase: nameMatch[3] != "",
677 }
678
679 var ev EnumValue
680 canStop := true
681 saveValue := func() {
682 ev.Comment = getComment()
683 e.Values = append(e.Values, ev)
684 if ev.Value == 0 {
685 e.HasZero = true
686 }
687 ev = EnumValue{}
688 canStop = true
689 }
690
691 out:
692 for scanner.Ok() {
693 line := scanner.Peek()
694 scanner.Next()
695
696 fieldMatch := enumFieldRe.FindStringSubmatch(line)
697
698 switch {
699 default:
700 die("unable to determine line %s", line)
701 case strings.HasPrefix(line, " //"):
702 canStop = false
703 writeComment(line)
704 case len(fieldMatch) > 0:
705 num, err := strconv.Atoi(fieldMatch[1])
706 if err != nil {
707 die("unable to convert to number on line %s", line)
708 }
709 ev.Value = num
710 ev.Word = fieldMatch[2]
711
712 saveValue()
713
714 case line == ")":
715 break out
716 }
717 }
718 if !canStop {
719 die("invalid enum ending with a comment")
720 }
721
722 enums[e.Name] = e
723 newEnums = append(newEnums, e)
724 }
725 }
726
View as plain text