...

Source file src/github.com/twmb/franz-go/generate/parse.go

Documentation: github.com/twmb/franz-go/generate

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strconv"
     7  	"strings"
     8  )
     9  
    10  // If you are looking here, yes this is a shoddy parser, but it does the job.
    11  
    12  // newStructs and newEnums are top level structs that we print at the end.
    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  // LineScanner is a shoddy scanner that allows us to peek an entire line.
    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  // BuildFrom parses a struct, building it from the current scanner. Level
    66  // signifies how nested this struct is. Top level structs have a level of 0.
    67  //
    68  // If a blank line is ever encountered, the struct is done being built and
    69  // the done status is bubbled up through all recursion levels.
    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 { // struct we were working on is done
    79  			scanner.Next()
    80  			return true
    81  		}
    82  		if !strings.HasPrefix(line, fieldSpaces) {
    83  			return false // inner level struct done
    84  		}
    85  
    86  		scanner.Next() // we will be keeping this line, so skip the scanner to the next now
    87  
    88  		line = line[len(fieldSpaces):]
    89  		if strings.HasPrefix(line, "//") { // buffer comments
    90  			if nextComment != "" {
    91  				nextComment += "\n"
    92  			}
    93  			nextComment += line
    94  			continue
    95  		}
    96  
    97  		// ThrottleMillis is a special field:
    98  		// - we die if there is preceding documentation
    99  		// - there can only be a minimum version, no max
   100  		// - no tags
   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  		// TimeoutMillis can be a special field, or it can be standard
   110  		// (for misc).
   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  		// Fields are name on left, type on right.
   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  		// We parse field comments first; this is everything following
   136  		// a // after the field.
   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  		// Now we do some array processing. Arrays can be nested
   146  		// (although they are not nested in our definitions since
   147  		// the flexible tag support).
   148  		// We count the array depth here, check if it is encoded
   149  		// specially, and remove any array decoration.
   150  		//
   151  		// This does not support special modifiers at any level but
   152  		// the outside level.
   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  		// Now we check for defaults.
   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=>"): // nested struct; recurse
   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 // for kmsg generating ordering purposes
   210  			newS.Anonymous = true
   211  			if isArray {
   212  				if rename := typ[2:]; rename != "" { // allow rename hint after `=>`; braces were stripped above
   213  					newS.Name = s.Name + rename
   214  				} else {
   215  					newS.Name = strings.TrimSuffix(newS.Name, "s") // make plural singular
   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 => "): // special bytes referencing another field
   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: // type is known, lookup and set
   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  				// If this field's struct type specified no encoding, then it
   263  				// is not anonymous, but it is tied to a request and should be
   264  				// ordered by that request when generating code.
   265  				//
   266  				// The default key is -1, so if we still have they key, we fix
   267  				// it and also fix the key in the newStructs slice.
   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  		// Finally, this field is an array, wrap what we parsed in an
   292  		// array (perhaps multilevel).
   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  // 0: entire line
   314  // 1: min version, if versioned
   315  // 2: max version, if versioned, if exists
   316  // 3: tag, if versioned, if exists
   317  // 4: tag, if not versioned
   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] != "" { // not versioned
   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  // 0: entire thing
   358  // 1: optional version switched to post-reply throttling
   359  // 2: optional version introduced
   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  // 0: entire thing
   398  // 1: optional default
   399  // 2: optional version introduced
   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" // default to 15s for all timeouts
   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  // 0: entire thing
   430  // 1: name
   431  // 2: "no encoding" if present
   432  // 3: "with version field" if present
   433  // 4: flexible version if present
   434  var notTopLevelRe = regexp.MustCompile(`^([A-Za-z0-9]+) => not top level(?:, (?:(no encoding)|(with version field))(?:, flexible v(\d+)\+)?)?$`)
   435  
   436  // Parse parses the raw contents of a messages file and adds all newly
   437  // parsed structs to newStructs.
   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 { // allow for arbitrary comments
   455  			resetComment()
   456  			continue
   457  		}
   458  
   459  		if strings.HasPrefix(line, "//") { // comment? keep and continue
   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, // by default, not flexible
   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: // simply "not top level"
   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 { // if we trimmed, this is not top level
   523  			topLevel = false
   524  			save()
   525  			continue
   526  		}
   527  
   528  		if strings.HasSuffix(name, "Response =>") { // response following a request?
   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  		// At this point, we are dealing with a top level request.
   548  		// The order, l to r, is key, version, admin/group/txn.
   549  		// We strip and process r to l.
   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  	// 1: name
   647  	// 2: type
   648  	// 3: if camel case (optional)
   649  	enumNameRe := regexp.MustCompile(`^([A-Za-z]+) ([^ ]+) (camelcase )?\($`)
   650  	// 1: value (number)
   651  	// 2: word (meaning)
   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 { // allow for arbitrary empty lines
   658  			resetComment()
   659  			continue
   660  		}
   661  
   662  		if strings.HasPrefix(line, "//") { // comment? keep and continue
   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