1 // Copyright (c) 2017 Cisco and/or its affiliates.
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at:
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
32 "github.com/bennyscetbun/jsongo"
36 inputFile = flag.String("input-file", "", "Input JSON file.")
37 inputDir = flag.String("input-dir", ".", "Input directory with JSON files.")
38 outputDir = flag.String("output-dir", ".", "Output directory where package folders will be generated.")
39 includeAPIVer = flag.Bool("include-apiver", false, "Wether to include VlAPIVersion in generated file.")
42 // MessageType represents the type of a VPP message.
46 requestMessage messageType = iota // VPP request message
47 replyMessage // VPP reply message
48 eventMessage // VPP event message
49 otherMessage // other VPP message
53 apiImportPath = "git.fd.io/govpp.git/api" // import path of the govpp API
54 inputFileExt = ".json" // filename extension of files that should be processed as the input
57 // context is a structure storing details of a particular code generation task
59 inputFile string // file with input JSON data
60 inputData []byte // contents of the input file
61 inputBuff *bytes.Buffer // contents of the input file currently being read
62 inputLine int // currently processed line in the input file
63 outputFile string // file with output data
64 packageName string // name of the Go package being generated
65 packageDir string // directory where the package source files are located
66 types map[string]string // map of the VPP typedef names to generated Go typedef names
72 if *inputFile == "" && *inputDir == "" {
73 fmt.Fprintln(os.Stderr, "ERROR: input-file or input-dir must be specified")
79 // process one input file
80 err = generateFromFile(*inputFile, *outputDir)
82 fmt.Fprintf(os.Stderr, "ERROR: code generation from %s failed: %v\n", *inputFile, err)
85 // process all files in specified directory
86 files, err := getInputFiles(*inputDir)
88 fmt.Fprintf(os.Stderr, "ERROR: code generation failed: %v\n", err)
90 for _, file := range files {
91 tmpErr = generateFromFile(file, *outputDir)
93 fmt.Fprintf(os.Stderr, "ERROR: code generation from %s failed: %v\n", file, err)
94 err = tmpErr // remember that the error occurred
103 // getInputFiles returns all input files located in specified directory
104 func getInputFiles(inputDir string) ([]string, error) {
105 files, err := ioutil.ReadDir(inputDir)
107 return nil, fmt.Errorf("reading directory %s failed: %v", inputDir, err)
109 res := make([]string, 0)
110 for _, f := range files {
111 if strings.HasSuffix(f.Name(), inputFileExt) {
112 res = append(res, inputDir+"/"+f.Name())
118 // generateFromFile generates Go bindings from one input JSON file
119 func generateFromFile(inputFile, outputDir string) error {
120 ctx, err := getContext(inputFile, outputDir)
125 ctx.inputData, err = readFile(inputFile)
131 jsonRoot, err := parseJSON(ctx.inputData)
136 // create output directory
137 err = os.MkdirAll(ctx.packageDir, 0777)
139 return fmt.Errorf("creating output directory %s failed: %v", ctx.packageDir, err)
143 f, err := os.Create(ctx.outputFile)
146 return fmt.Errorf("creating output file %s failed: %v", ctx.outputFile, err)
148 w := bufio.NewWriter(f)
150 // generate Go package code
151 err = generatePackage(ctx, w, jsonRoot)
156 // go format the output file (non-fatal if fails)
157 exec.Command("gofmt", "-w", ctx.outputFile).Run()
162 // getContext returns context details of the code generation task
163 func getContext(inputFile, outputDir string) (*context, error) {
164 if !strings.HasSuffix(inputFile, inputFileExt) {
165 return nil, fmt.Errorf("invalid input file name %s", inputFile)
168 ctx := &context{inputFile: inputFile}
169 inputFileName := filepath.Base(inputFile)
171 ctx.packageName = inputFileName[0:strings.Index(inputFileName, ".")]
172 if ctx.packageName == "interface" {
173 // 'interface' cannot be a package name, it is a go keyword
174 ctx.packageName = "interfaces"
177 ctx.packageDir = outputDir + "/" + ctx.packageName + "/"
178 ctx.outputFile = ctx.packageDir + ctx.packageName + ".go"
183 // readFile reads content of a file into memory
184 func readFile(inputFile string) ([]byte, error) {
186 inputData, err := ioutil.ReadFile(inputFile)
189 return nil, fmt.Errorf("reading data from file failed: %v", err)
192 return inputData, nil
195 // parseJSON parses a JSON data into an in-memory tree
196 func parseJSON(inputData []byte) (*jsongo.JSONNode, error) {
197 root := jsongo.JSONNode{}
199 err := json.Unmarshal(inputData, &root)
201 return nil, fmt.Errorf("JSON unmarshall failed: %v", err)
208 // generatePackage generates Go code of a package from provided JSON
209 func generatePackage(ctx *context, w *bufio.Writer, jsonRoot *jsongo.JSONNode) error {
210 // generate file header
211 generatePackageHeader(ctx, w, jsonRoot)
213 // generate data types
214 ctx.inputBuff = bytes.NewBuffer(ctx.inputData)
216 ctx.types = make(map[string]string)
217 types := jsonRoot.Map("types")
218 for i := 0; i < types.Len(); i++ {
220 err := generateMessage(ctx, w, typ, true)
227 ctx.inputBuff = bytes.NewBuffer(ctx.inputData)
229 messages := jsonRoot.Map("messages")
230 for i := 0; i < messages.Len(); i++ {
231 msg := messages.At(i)
232 err := generateMessage(ctx, w, msg, false)
241 return fmt.Errorf("flushing data to %s failed: %v", ctx.outputFile, err)
247 // generateMessage generates Go code of one VPP message encoded in JSON into provided writer
248 func generateMessage(ctx *context, w io.Writer, msg *jsongo.JSONNode, isType bool) error {
249 if msg.Len() == 0 || msg.At(0).GetType() != jsongo.TypeValue {
250 return errors.New("invalid JSON for message specified")
253 msgName, ok := msg.At(0).Get().(string)
255 return fmt.Errorf("invalid JSON for message specified, message name is %T, not a string", msg.At(0).Get())
257 structName := camelCaseName(strings.Title(msgName))
259 // generate struct fields into the slice & determine message type
260 fields := make([]string, 0)
261 msgType := otherMessage
262 wasClientIndex := false
263 for j := 0; j < msg.Len(); j++ {
264 if jsongo.TypeArray == msg.At(j).GetType() {
267 // determine whether ths is a request / reply / other message
268 fieldName, ok := fld.At(1).Get().(string)
271 if fieldName == "client_index" {
272 // "client_index" as the second member, this might be an event message or a request
273 msgType = eventMessage
274 wasClientIndex = true
275 } else if fieldName == "context" {
276 // reply needs "context" as the second member
277 msgType = replyMessage
280 if wasClientIndex && fieldName == "context" {
281 // request needs "client_index" as the second member and "context" as the third member
282 msgType = requestMessage
287 err := processMessageField(ctx, &fields, fld, isType)
294 // generate struct comment
295 generateMessageComment(ctx, w, structName, msgName, isType)
297 // generate struct header
298 fmt.Fprintln(w, "type", structName, "struct {")
300 // print out the fields
301 for _, field := range fields {
302 fmt.Fprintln(w, field)
305 // generate end of the struct
308 // generate name getter
310 generateTypeNameGetter(w, structName, msgName)
312 generateMessageNameGetter(w, structName, msgName)
315 // generate message type getter method
317 generateMessageTypeGetter(w, structName, msgType)
320 // generate CRC getter
321 crcIf := msg.At(msg.Len() - 1).At("crc").Get()
322 if crc, ok := crcIf.(string); ok {
323 generateCrcGetter(w, structName, crc)
326 // generate message factory
328 generateMessageFactory(w, structName)
331 // if this is a type, save it in the map for later use
333 ctx.types[fmt.Sprintf("vl_api_%s_t", msgName)] = structName
339 // processMessageField process JSON describing one message field into Go code emitted into provided slice of message fields
340 func processMessageField(ctx *context, fields *[]string, fld *jsongo.JSONNode, isType bool) error {
341 if fld.Len() < 2 || fld.At(0).GetType() != jsongo.TypeValue || fld.At(1).GetType() != jsongo.TypeValue {
342 return errors.New("invalid JSON for message field specified")
344 fieldVppType, ok := fld.At(0).Get().(string)
346 return fmt.Errorf("invalid JSON for message specified, field type is %T, not a string", fld.At(0).Get())
348 fieldName, ok := fld.At(1).Get().(string)
350 return fmt.Errorf("invalid JSON for message specified, field name is %T, not a string", fld.At(1).Get())
353 // skip internal fields
354 fieldNameLower := strings.ToLower(fieldName)
355 if fieldNameLower == "crc" || fieldNameLower == "_vl_msg_id" {
358 if !isType && len(*fields) == 0 && (fieldNameLower == "client_index" || fieldNameLower == "context") {
362 fieldName = strings.TrimPrefix(fieldName, "_")
363 fieldName = camelCaseName(strings.Title(fieldName))
369 fieldStr += "\t" + fieldName + " "
372 arraySize = int(fld.At(2).Get().(float64))
376 dataType := translateVppType(ctx, fieldVppType, isArray)
381 // variable sized array
383 // array size is specified by another field
384 arraySizeField := string(fld.At(3).Get().(string))
385 arraySizeField = camelCaseName(strings.Title(arraySizeField))
386 // find & update the field that specifies the array size
387 for i, f := range *fields {
388 if strings.Contains(f, fmt.Sprintf("\t%s ", arraySizeField)) {
389 (*fields)[i] += fmt.Sprintf("\t`struc:\"sizeof=%s\"`", fieldName)
395 fieldStr += fmt.Sprintf("\t`struc:\"[%d]%s\"`", arraySize, dataType)
399 *fields = append(*fields, fieldStr)
403 // generatePackageHeader generates package header into provider writer
404 func generatePackageHeader(ctx *context, w io.Writer, rootNode *jsongo.JSONNode) {
405 fmt.Fprintln(w, "// Code generated by govpp binapi-generator DO NOT EDIT.")
406 fmt.Fprintln(w, "// Package "+ctx.packageName+" represents the VPP binary API of the '"+ctx.packageName+"' VPP module.")
407 fmt.Fprintln(w, "// Generated from '"+ctx.inputFile+"'")
409 fmt.Fprintln(w, "package "+ctx.packageName)
411 fmt.Fprintln(w, "import \""+apiImportPath+"\"")
414 vlAPIVersion := rootNode.Map("vl_api_version").Get()
416 fmt.Fprintln(w, "// VlApiVersion contains version of the API.")
417 fmt.Fprintln(w, "const VlAPIVersion = ", vlAPIVersion)
422 // generateMessageComment generates comment for a message into provider writer
423 func generateMessageComment(ctx *context, w io.Writer, structName string, msgName string, isType bool) {
426 fmt.Fprintln(w, "// "+structName+" represents the VPP binary API data type '"+msgName+"'.")
428 fmt.Fprintln(w, "// "+structName+" represents the VPP binary API message '"+msgName+"'.")
431 // print out the source of the generated message - the JSON
433 msgTitle := "\"" + msgName + "\","
436 lineBuff, err := ctx.inputBuff.ReadBytes('\n')
441 line := string(lineBuff)
444 msgIndent = strings.Index(line, msgTitle)
446 prefix := line[:msgIndent]
447 suffix := line[msgIndent+len(msgTitle):]
448 // If no other non-whitespace character then we are at the message header.
449 if strings.IndexFunc(prefix, isNotSpace) == -1 && strings.IndexFunc(suffix, isNotSpace) == -1 {
450 fmt.Fprintf(w, "// Generated from '%s', line %d:\n", ctx.inputFile, ctx.inputLine)
451 fmt.Fprintln(w, "//")
452 fmt.Fprint(w, "//", line)
457 if strings.IndexFunc(line, isNotSpace) < msgIndent {
458 break // end of the message in JSON
460 fmt.Fprint(w, "//", line)
463 fmt.Fprintln(w, "//")
466 // generateMessageNameGetter generates getter for original VPP message name into the provider writer
467 func generateMessageNameGetter(w io.Writer, structName string, msgName string) {
468 fmt.Fprintln(w, "func (*"+structName+") GetMessageName() string {")
469 fmt.Fprintln(w, "\treturn \""+msgName+"\"")
473 // generateTypeNameGetter generates getter for original VPP type name into the provider writer
474 func generateTypeNameGetter(w io.Writer, structName string, msgName string) {
475 fmt.Fprintln(w, "func (*"+structName+") GetTypeName() string {")
476 fmt.Fprintln(w, "\treturn \""+msgName+"\"")
480 // generateMessageTypeGetter generates message factory for the generated message into the provider writer
481 func generateMessageTypeGetter(w io.Writer, structName string, msgType messageType) {
482 fmt.Fprintln(w, "func (*"+structName+") GetMessageType() api.MessageType {")
483 if msgType == requestMessage {
484 fmt.Fprintln(w, "\treturn api.RequestMessage")
485 } else if msgType == replyMessage {
486 fmt.Fprintln(w, "\treturn api.ReplyMessage")
487 } else if msgType == eventMessage {
488 fmt.Fprintln(w, "\treturn api.EventMessage")
490 fmt.Fprintln(w, "\treturn api.OtherMessage")
495 // generateCrcGetter generates getter for CRC checksum of the message definition into the provider writer
496 func generateCrcGetter(w io.Writer, structName string, crc string) {
497 crc = strings.TrimPrefix(crc, "0x")
498 fmt.Fprintln(w, "func (*"+structName+") GetCrcString() string {")
499 fmt.Fprintln(w, "\treturn \""+crc+"\"")
503 // generateMessageFactory generates message factory for the generated message into the provider writer
504 func generateMessageFactory(w io.Writer, structName string) {
505 fmt.Fprintln(w, "func New"+structName+"() api.Message {")
506 fmt.Fprintln(w, "\treturn &"+structName+"{}")
510 // translateVppType translates the VPP data type into Go data type
511 func translateVppType(ctx *context, vppType string, isArray bool) string {
538 typ, ok := ctx.types[vppType]
543 panic(fmt.Sprintf("Unknown VPP type %s", vppType))
546 // camelCaseName returns correct name identifier (camelCase).
547 func camelCaseName(name string) (should string) {
548 // Fast path for simple cases: "_" and all lowercase.
553 for _, r := range name {
554 if !unicode.IsLower(r) {
563 // Split camelCase at any lower->upper transition, and split on underscores.
564 // Check each word for common initialisms.
565 runes := []rune(name)
566 w, i := 0, 0 // index of start of word, scan
567 for i+1 <= len(runes) {
568 eow := false // whether we hit the end of a word
569 if i+1 == len(runes) {
571 } else if runes[i+1] == '_' {
572 // underscore; shift the remainder forward over any run of underscores
575 for i+n+1 < len(runes) && runes[i+n+1] == '_' {
579 // Leave at most one underscore if the underscore is between two digits
580 if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
584 copy(runes[i+1:], runes[i+n+1:])
585 runes = runes[:len(runes)-n]
586 } else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
596 word := string(runes[w:i])
597 if u := strings.ToUpper(word); commonInitialisms[u] {
598 // Keep consistent case, which is lowercase only at the start.
599 if w == 0 && unicode.IsLower(runes[w]) {
600 u = strings.ToLower(u)
602 // All the common initialisms are ASCII,
603 // so we can replace the bytes exactly.
604 copy(runes[w:], []rune(u))
605 } else if w > 0 && strings.ToLower(word) == word {
606 // already all lowercase, and not the first word, so uppercase the first character.
607 runes[w] = unicode.ToUpper(runes[w])
614 // isNotSpace returns true if the rune is NOT a whitespace character.
615 func isNotSpace(r rune) bool {
616 return !unicode.IsSpace(r)
619 // commonInitialisms is a set of common initialisms that need to stay in upper case.
620 var commonInitialisms = map[string]bool{