7a4387548e7628688f738e071cd20bd4f2383da6
[govpp.git] / cmd / binapi-generator / main.go
1 // Copyright (c) 2018 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         "bytes"
19         "encoding/json"
20         "flag"
21         "fmt"
22         "io/ioutil"
23         "os"
24         "os/exec"
25         "path/filepath"
26         "strings"
27
28         "github.com/bennyscetbun/jsongo"
29         "github.com/sirupsen/logrus"
30
31         "git.fd.io/govpp.git/version"
32 )
33
34 var (
35         theInputFile = flag.String("input-file", "", "Input file with VPP API in JSON format.")
36         theInputDir  = flag.String("input-dir", "/usr/share/vpp/api", "Input directory with VPP API files in JSON format.")
37         theOutputDir = flag.String("output-dir", ".", "Output directory where package folders will be generated.")
38
39         includeAPIVer      = flag.Bool("include-apiver", true, "Include APIVersion constant for each module.")
40         includeServices    = flag.Bool("include-services", true, "Include RPC service api and client implementation.")
41         includeComments    = flag.Bool("include-comments", false, "Include JSON API source in comments for each object.")
42         includeBinapiNames = flag.Bool("include-binapi-names", false, "Include binary API names in struct tag.")
43
44         continueOnError = flag.Bool("continue-onerror", false, "Continue with next file on error.")
45         debugMode       = flag.Bool("debug", os.Getenv("GOVPP_DEBUG") != "", "Enable debug mode.")
46
47         printVersion = flag.Bool("version", false, "Prints current version and exits.")
48 )
49
50 func main() {
51         flag.Parse()
52
53         if flag.NArg() > 1 {
54                 flag.Usage()
55                 os.Exit(1)
56         }
57
58         if flag.NArg() > 0 {
59                 switch cmd := flag.Arg(0); cmd {
60                 case "version":
61                         fmt.Fprintln(os.Stdout, version.Verbose())
62                         os.Exit(0)
63
64                 default:
65                         fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
66                         flag.Usage()
67                         os.Exit(2)
68                 }
69         }
70
71         if *printVersion {
72                 fmt.Fprintln(os.Stdout, version.Info())
73                 os.Exit(0)
74         }
75
76         if *debugMode {
77                 logrus.SetLevel(logrus.DebugLevel)
78                 logrus.Info("debug mode enabled")
79         }
80
81         if err := run(*theInputFile, *theInputDir, *theOutputDir, *continueOnError); err != nil {
82                 logrus.Errorln("binapi-generator:", err)
83                 os.Exit(1)
84         }
85 }
86
87 func run(inputFile, inputDir string, outputDir string, continueErr bool) error {
88         if inputFile == "" && inputDir == "" {
89                 return fmt.Errorf("input-file or input-dir must be specified")
90         }
91
92         if inputFile != "" {
93                 // process one input file
94                 if err := generateFromFile(inputFile, outputDir); err != nil {
95                         return fmt.Errorf("code generation from %s failed: %v\n", inputFile, err)
96                 }
97         } else {
98                 // process all files in specified directory
99                 dir, err := filepath.Abs(inputDir)
100                 if err != nil {
101                         return fmt.Errorf("invalid input directory: %v\n", err)
102                 }
103                 files, err := getInputFiles(inputDir, 1)
104                 if err != nil {
105                         return fmt.Errorf("problem getting files from input directory: %v\n", err)
106                 } else if len(files) == 0 {
107                         return fmt.Errorf("no input files found in input directory: %v\n", dir)
108                 }
109                 for _, file := range files {
110                         if err := generateFromFile(file, outputDir); err != nil {
111                                 if continueErr {
112                                         logrus.Warnf("code generation from %s failed: %v (error ignored)\n", file, err)
113                                         continue
114                                 } else {
115                                         return fmt.Errorf("code generation from %s failed: %v\n", file, err)
116                                 }
117                         }
118                 }
119         }
120
121         return nil
122 }
123
124 // getInputFiles returns all input files located in specified directory
125 func getInputFiles(inputDir string, deep int) (files []string, err error) {
126         entries, err := ioutil.ReadDir(inputDir)
127         if err != nil {
128                 return nil, fmt.Errorf("reading directory %s failed: %v", inputDir, err)
129         }
130         for _, e := range entries {
131                 if e.IsDir() && deep > 0 {
132                         nestedDir := filepath.Join(inputDir, e.Name())
133                         if nested, err := getInputFiles(nestedDir, deep-1); err != nil {
134                                 return nil, err
135                         } else {
136                                 files = append(files, nested...)
137                         }
138                 } else if strings.HasSuffix(e.Name(), inputFileExt) {
139                         files = append(files, filepath.Join(inputDir, e.Name()))
140                 }
141         }
142         return files, nil
143 }
144
145 func parseInputJSON(inputData []byte) (*jsongo.JSONNode, error) {
146         jsonRoot := new(jsongo.JSONNode)
147         if err := json.Unmarshal(inputData, jsonRoot); err != nil {
148                 return nil, fmt.Errorf("unmarshalling JSON failed: %v", err)
149         }
150         return jsonRoot, nil
151 }
152
153 // generateFromFile generates Go package from one input JSON file
154 func generateFromFile(inputFile, outputDir string) error {
155         // create generator context
156         ctx, err := newContext(inputFile, outputDir)
157         if err != nil {
158                 return err
159         }
160
161         logf("------------------------------------------------------------")
162         logf("module: %s", ctx.moduleName)
163         logf(" - input: %s", ctx.inputFile)
164         logf(" - output: %s", ctx.outputFile)
165         logf("------------------------------------------------------------")
166
167         // prepare options
168         ctx.includeAPIVersion = *includeAPIVer
169         ctx.includeComments = *includeComments
170         ctx.includeBinapiNames = *includeBinapiNames
171         ctx.includeServices = *includeServices
172
173         // read API definition from input file
174         ctx.inputData, err = ioutil.ReadFile(ctx.inputFile)
175         if err != nil {
176                 return fmt.Errorf("reading input file %s failed: %v", ctx.inputFile, err)
177         }
178         // parse JSON data into objects
179         jsonRoot, err := parseInputJSON(ctx.inputData)
180         if err != nil {
181                 return fmt.Errorf("parsing JSON input failed: %v", err)
182         }
183         ctx.packageData, err = parsePackage(ctx, jsonRoot)
184         if err != nil {
185                 return fmt.Errorf("parsing package %s failed: %v", ctx.packageName, err)
186         }
187
188         // generate Go package code
189         var buf bytes.Buffer
190         if err := generatePackage(ctx, &buf); err != nil {
191                 return fmt.Errorf("generating code for package %s failed: %v", ctx.packageName, err)
192         }
193
194         // create output directory
195         packageDir := filepath.Dir(ctx.outputFile)
196         if err := os.MkdirAll(packageDir, 0775); err != nil {
197                 return fmt.Errorf("creating output dir %s failed: %v", packageDir, err)
198         }
199         // write generated code to output file
200         if err := ioutil.WriteFile(ctx.outputFile, buf.Bytes(), 0666); err != nil {
201                 return fmt.Errorf("writing to output file %s failed: %v", ctx.outputFile, err)
202         }
203
204         // go format the output file (fail probably means the output is not compilable)
205         cmd := exec.Command("gofmt", "-w", ctx.outputFile)
206         if output, err := cmd.CombinedOutput(); err != nil {
207                 return fmt.Errorf("gofmt failed: %v\n%s", err, string(output))
208         }
209
210         return nil
211 }
212
213 func logf(f string, v ...interface{}) {
214         if *debugMode {
215                 logrus.Debugf(f, v...)
216         }
217 }