Make including VlAPIVersion in generated file as opt-in
[govpp.git] / cmd / binapi-generator / generator.go
1 // Copyright (c) 2017 Cisco and/or its affiliates.
2 //
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:
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
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.
14
15 package main
16
17 import (
18         "bufio"
19         "bytes"
20         "encoding/json"
21         "errors"
22         "flag"
23         "fmt"
24         "io"
25         "io/ioutil"
26         "os"
27         "os/exec"
28         "path/filepath"
29         "strings"
30         "unicode"
31
32         "github.com/bennyscetbun/jsongo"
33 )
34
35 var (
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.")
40 )
41
42 // MessageType represents the type of a VPP message.
43 type messageType int
44
45 const (
46         requestMessage messageType = iota // VPP request message
47         replyMessage                      // VPP reply message
48         eventMessage                      // VPP event message
49         otherMessage                      // other VPP message
50 )
51
52 const (
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
55 )
56
57 // context is a structure storing details of a particular code generation task
58 type context struct {
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
67 }
68
69 func main() {
70         flag.Parse()
71
72         if *inputFile == "" && *inputDir == "" {
73                 fmt.Fprintln(os.Stderr, "ERROR: input-file or input-dir must be specified")
74                 os.Exit(1)
75         }
76
77         var err, tmpErr error
78         if *inputFile != "" {
79                 // process one input file
80                 err = generateFromFile(*inputFile, *outputDir)
81                 if err != nil {
82                         fmt.Fprintf(os.Stderr, "ERROR: code generation from %s failed: %v\n", *inputFile, err)
83                 }
84         } else {
85                 // process all files in specified directory
86                 files, err := getInputFiles(*inputDir)
87                 if err != nil {
88                         fmt.Fprintf(os.Stderr, "ERROR: code generation failed: %v\n", err)
89                 }
90                 for _, file := range files {
91                         tmpErr = generateFromFile(file, *outputDir)
92                         if tmpErr != nil {
93                                 fmt.Fprintf(os.Stderr, "ERROR: code generation from %s failed: %v\n", file, err)
94                                 err = tmpErr // remember that the error occurred
95                         }
96                 }
97         }
98         if err != nil {
99                 os.Exit(1)
100         }
101 }
102
103 // getInputFiles returns all input files located in specified directory
104 func getInputFiles(inputDir string) ([]string, error) {
105         files, err := ioutil.ReadDir(inputDir)
106         if err != nil {
107                 return nil, fmt.Errorf("reading directory %s failed: %v", inputDir, err)
108         }
109         res := make([]string, 0)
110         for _, f := range files {
111                 if strings.HasSuffix(f.Name(), inputFileExt) {
112                         res = append(res, inputDir+"/"+f.Name())
113                 }
114         }
115         return res, nil
116 }
117
118 // generateFromFile generates Go bindings from one input JSON file
119 func generateFromFile(inputFile, outputDir string) error {
120         ctx, err := getContext(inputFile, outputDir)
121         if err != nil {
122                 return err
123         }
124         // read the file
125         ctx.inputData, err = readFile(inputFile)
126         if err != nil {
127                 return err
128         }
129
130         // parse JSON
131         jsonRoot, err := parseJSON(ctx.inputData)
132         if err != nil {
133                 return err
134         }
135
136         // create output directory
137         err = os.MkdirAll(ctx.packageDir, 0777)
138         if err != nil {
139                 return fmt.Errorf("creating output directory %s failed: %v", ctx.packageDir, err)
140         }
141
142         // open output file
143         f, err := os.Create(ctx.outputFile)
144         defer f.Close()
145         if err != nil {
146                 return fmt.Errorf("creating output file %s failed: %v", ctx.outputFile, err)
147         }
148         w := bufio.NewWriter(f)
149
150         // generate Go package code
151         err = generatePackage(ctx, w, jsonRoot)
152         if err != nil {
153                 return err
154         }
155
156         // go format the output file (non-fatal if fails)
157         exec.Command("gofmt", "-w", ctx.outputFile).Run()
158
159         return nil
160 }
161
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)
166         }
167
168         ctx := &context{inputFile: inputFile}
169         inputFileName := filepath.Base(inputFile)
170
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"
175         }
176
177         ctx.packageDir = outputDir + "/" + ctx.packageName + "/"
178         ctx.outputFile = ctx.packageDir + ctx.packageName + ".go"
179
180         return ctx, nil
181 }
182
183 // readFile reads content of a file into memory
184 func readFile(inputFile string) ([]byte, error) {
185
186         inputData, err := ioutil.ReadFile(inputFile)
187
188         if err != nil {
189                 return nil, fmt.Errorf("reading data from file failed: %v", err)
190         }
191
192         return inputData, nil
193 }
194
195 // parseJSON parses a JSON data into an in-memory tree
196 func parseJSON(inputData []byte) (*jsongo.JSONNode, error) {
197         root := jsongo.JSONNode{}
198
199         err := json.Unmarshal(inputData, &root)
200         if err != nil {
201                 return nil, fmt.Errorf("JSON unmarshall failed: %v", err)
202         }
203
204         return &root, nil
205
206 }
207
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)
212
213         // generate data types
214         ctx.inputBuff = bytes.NewBuffer(ctx.inputData)
215         ctx.inputLine = 0
216         ctx.types = make(map[string]string)
217         types := jsonRoot.Map("types")
218         for i := 0; i < types.Len(); i++ {
219                 typ := types.At(i)
220                 err := generateMessage(ctx, w, typ, true)
221                 if err != nil {
222                         return err
223                 }
224         }
225
226         // generate messages
227         ctx.inputBuff = bytes.NewBuffer(ctx.inputData)
228         ctx.inputLine = 0
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)
233                 if err != nil {
234                         return err
235                 }
236         }
237
238         // flush the data:
239         err := w.Flush()
240         if err != nil {
241                 return fmt.Errorf("flushing data to %s failed: %v", ctx.outputFile, err)
242         }
243
244         return nil
245 }
246
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")
251         }
252
253         msgName, ok := msg.At(0).Get().(string)
254         if !ok {
255                 return fmt.Errorf("invalid JSON for message specified, message name is %T, not a string", msg.At(0).Get())
256         }
257         structName := camelCaseName(strings.Title(msgName))
258
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() {
265                         fld := msg.At(j)
266                         if !isType {
267                                 // determine whether ths is a request / reply / other message
268                                 fieldName, ok := fld.At(1).Get().(string)
269                                 if ok {
270                                         if j == 2 {
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
278                                                 }
279                                         } else if j == 3 {
280                                                 if wasClientIndex && fieldName == "context" {
281                                                         // request needs "client_index" as the second member and "context" as the third member
282                                                         msgType = requestMessage
283                                                 }
284                                         }
285                                 }
286                         }
287                         err := processMessageField(ctx, &fields, fld, isType)
288                         if err != nil {
289                                 return err
290                         }
291                 }
292         }
293
294         // generate struct comment
295         generateMessageComment(ctx, w, structName, msgName, isType)
296
297         // generate struct header
298         fmt.Fprintln(w, "type", structName, "struct {")
299
300         // print out the fields
301         for _, field := range fields {
302                 fmt.Fprintln(w, field)
303         }
304
305         // generate end of the struct
306         fmt.Fprintln(w, "}")
307
308         // generate name getter
309         if isType {
310                 generateTypeNameGetter(w, structName, msgName)
311         } else {
312                 generateMessageNameGetter(w, structName, msgName)
313         }
314
315         // generate message type getter method
316         if !isType {
317                 generateMessageTypeGetter(w, structName, msgType)
318         }
319
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)
324         }
325
326         // generate message factory
327         if !isType {
328                 generateMessageFactory(w, structName)
329         }
330
331         // if this is a type, save it in the map for later use
332         if isType {
333                 ctx.types[fmt.Sprintf("vl_api_%s_t", msgName)] = structName
334         }
335
336         return nil
337 }
338
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")
343         }
344         fieldVppType, ok := fld.At(0).Get().(string)
345         if !ok {
346                 return fmt.Errorf("invalid JSON for message specified, field type is %T, not a string", fld.At(0).Get())
347         }
348         fieldName, ok := fld.At(1).Get().(string)
349         if !ok {
350                 return fmt.Errorf("invalid JSON for message specified, field name is %T, not a string", fld.At(1).Get())
351         }
352
353         // skip internal fields
354         fieldNameLower := strings.ToLower(fieldName)
355         if fieldNameLower == "crc" || fieldNameLower == "_vl_msg_id" {
356                 return nil
357         }
358         if !isType && len(*fields) == 0 && (fieldNameLower == "client_index" || fieldNameLower == "context") {
359                 return nil
360         }
361
362         fieldName = strings.TrimPrefix(fieldName, "_")
363         fieldName = camelCaseName(strings.Title(fieldName))
364
365         fieldStr := ""
366         isArray := false
367         arraySize := 0
368
369         fieldStr += "\t" + fieldName + " "
370         if fld.Len() > 2 {
371                 isArray = true
372                 arraySize = int(fld.At(2).Get().(float64))
373                 fieldStr += "[]"
374         }
375
376         dataType := translateVppType(ctx, fieldVppType, isArray)
377         fieldStr += dataType
378
379         if isArray {
380                 if arraySize == 0 {
381                         // variable sized array
382                         if fld.Len() > 3 {
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)
390                                         }
391                                 }
392                         }
393                 } else {
394                         // fixed size array
395                         fieldStr += fmt.Sprintf("\t`struc:\"[%d]%s\"`", arraySize, dataType)
396                 }
397         }
398
399         *fields = append(*fields, fieldStr)
400         return nil
401 }
402
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+"'")
408
409         fmt.Fprintln(w, "package "+ctx.packageName)
410
411         fmt.Fprintln(w, "import \""+apiImportPath+"\"")
412         fmt.Fprintln(w)
413
414         vlAPIVersion := rootNode.Map("vl_api_version").Get()
415         if *includeAPIVer {
416                 fmt.Fprintln(w, "// VlApiVersion contains version of the API.")
417                 fmt.Fprintln(w, "const VlAPIVersion = ", vlAPIVersion)
418                 fmt.Fprintln(w)
419         }
420 }
421
422 // generateMessageComment generates comment for a message into provider writer
423 func generateMessageComment(ctx *context, w io.Writer, structName string, msgName string, isType bool) {
424         fmt.Fprintln(w)
425         if isType {
426                 fmt.Fprintln(w, "// "+structName+" represents the VPP binary API data type '"+msgName+"'.")
427         } else {
428                 fmt.Fprintln(w, "// "+structName+" represents the VPP binary API message '"+msgName+"'.")
429         }
430
431         // print out the source of the generated message - the JSON
432         msgFound := false
433         msgTitle := "\"" + msgName + "\","
434         var msgIndent int
435         for {
436                 lineBuff, err := ctx.inputBuff.ReadBytes('\n')
437                 if err != nil {
438                         break
439                 }
440                 ctx.inputLine++
441                 line := string(lineBuff)
442
443                 if !msgFound {
444                         msgIndent = strings.Index(line, msgTitle)
445                         if msgIndent > -1 {
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)
453                                         msgFound = true
454                                 }
455                         }
456                 } else {
457                         if strings.IndexFunc(line, isNotSpace) < msgIndent {
458                                 break // end of the message in JSON
459                         }
460                         fmt.Fprint(w, "//", line)
461                 }
462         }
463         fmt.Fprintln(w, "//")
464 }
465
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+"\"")
470         fmt.Fprintln(w, "}")
471 }
472
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+"\"")
477         fmt.Fprintln(w, "}")
478 }
479
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")
489         } else {
490                 fmt.Fprintln(w, "\treturn api.OtherMessage")
491         }
492         fmt.Fprintln(w, "}")
493 }
494
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+"\"")
500         fmt.Fprintln(w, "}")
501 }
502
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+"{}")
507         fmt.Fprintln(w, "}")
508 }
509
510 // translateVppType translates the VPP data type into Go data type
511 func translateVppType(ctx *context, vppType string, isArray bool) string {
512         // basic types
513         switch vppType {
514         case "u8":
515                 if isArray {
516                         return "byte"
517                 }
518                 return "uint8"
519         case "i8":
520                 return "int8"
521         case "u16":
522                 return "uint16"
523         case "i16":
524                 return "int16"
525         case "u32":
526                 return "uint32"
527         case "i32":
528                 return "int32"
529         case "u64":
530                 return "uint64"
531         case "i64":
532                 return "int64"
533         case "f64":
534                 return "float64"
535         }
536
537         // typedefs
538         typ, ok := ctx.types[vppType]
539         if ok {
540                 return typ
541         }
542
543         panic(fmt.Sprintf("Unknown VPP type %s", vppType))
544 }
545
546 // camelCaseName returns correct name identifier (camelCase).
547 func camelCaseName(name string) (should string) {
548         // Fast path for simple cases: "_" and all lowercase.
549         if name == "_" {
550                 return name
551         }
552         allLower := true
553         for _, r := range name {
554                 if !unicode.IsLower(r) {
555                         allLower = false
556                         break
557                 }
558         }
559         if allLower {
560                 return name
561         }
562
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) {
570                         eow = true
571                 } else if runes[i+1] == '_' {
572                         // underscore; shift the remainder forward over any run of underscores
573                         eow = true
574                         n := 1
575                         for i+n+1 < len(runes) && runes[i+n+1] == '_' {
576                                 n++
577                         }
578
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]) {
581                                 n--
582                         }
583
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]) {
587                         // lower->non-lower
588                         eow = true
589                 }
590                 i++
591                 if !eow {
592                         continue
593                 }
594
595                 // [w,i) is a word.
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)
601                         }
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])
608                 }
609                 w = i
610         }
611         return string(runes)
612 }
613
614 // isNotSpace returns true if the rune is NOT a whitespace character.
615 func isNotSpace(r rune) bool {
616         return !unicode.IsSpace(r)
617 }
618
619 // commonInitialisms is a set of common initialisms that need to stay in upper case.
620 var commonInitialisms = map[string]bool{
621         "ACL":   true,
622         "API":   true,
623         "ASCII": true,
624         "CPU":   true,
625         "CSS":   true,
626         "DNS":   true,
627         "EOF":   true,
628         "GUID":  true,
629         "HTML":  true,
630         "HTTP":  true,
631         "HTTPS": true,
632         "ID":    true,
633         "IP":    true,
634         "ICMP":  true,
635         "JSON":  true,
636         "LHS":   true,
637         "QPS":   true,
638         "RAM":   true,
639         "RHS":   true,
640         "RPC":   true,
641         "SLA":   true,
642         "SMTP":  true,
643         "SQL":   true,
644         "SSH":   true,
645         "TCP":   true,
646         "TLS":   true,
647         "TTL":   true,
648         "UDP":   true,
649         "UI":    true,
650         "UID":   true,
651         "UUID":  true,
652         "URI":   true,
653         "URL":   true,
654         "UTF8":  true,
655         "VM":    true,
656         "XML":   true,
657         "XMPP":  true,
658         "XSRF":  true,
659         "XSS":   true,
660 }