Support imported type aliases
[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         theInputTypes = flag.String("input-types", "", "Types input file with VPP API in JSON format. (split by comma)")
37         theInputDir   = flag.String("input-dir", "/usr/share/vpp/api", "Input directory with VPP API files in JSON format.")
38         theOutputDir  = flag.String("output-dir", ".", "Output directory where package folders will be generated.")
39
40         includeAPIVer      = flag.Bool("include-apiver", true, "Include APIVersion constant for each module.")
41         includeServices    = flag.Bool("include-services", true, "Include RPC service api and client implementation.")
42         includeComments    = flag.Bool("include-comments", false, "Include JSON API source in comments for each object.")
43         includeBinapiNames = flag.Bool("include-binapi-names", false, "Include binary API names in struct tag.")
44
45         continueOnError = flag.Bool("continue-onerror", false, "Continue with next file on error.")
46         debugMode       = flag.Bool("debug", os.Getenv("GOVPP_DEBUG") != "", "Enable debug mode.")
47
48         printVersion = flag.Bool("version", false, "Prints current version and exits.")
49 )
50
51 func main() {
52         flag.Parse()
53
54         if flag.NArg() > 1 {
55                 flag.Usage()
56                 os.Exit(1)
57         }
58
59         if flag.NArg() > 0 {
60                 switch cmd := flag.Arg(0); cmd {
61                 case "version":
62                         fmt.Fprintln(os.Stdout, version.Verbose())
63                         os.Exit(0)
64
65                 default:
66                         fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
67                         flag.Usage()
68                         os.Exit(2)
69                 }
70         }
71
72         if *printVersion {
73                 fmt.Fprintln(os.Stdout, version.Info())
74                 os.Exit(0)
75         }
76
77         if *debugMode {
78                 logrus.SetLevel(logrus.DebugLevel)
79                 logrus.Info("debug mode enabled")
80         }
81
82         if err := run(*theInputFile, *theInputDir, *theOutputDir, *continueOnError); err != nil {
83                 logrus.Errorln("binapi-generator:", err)
84                 os.Exit(1)
85         }
86 }
87
88 func run(inputFile, inputDir string, outputDir string, continueErr bool) (err error) {
89         if inputFile == "" && inputDir == "" {
90                 return fmt.Errorf("input-file or input-dir must be specified")
91         }
92
93         var typesPkgs []*context
94         if *theInputTypes != "" {
95                 types := strings.Split(*theInputTypes, ",")
96                 typesPkgs, err = loadTypesPackages(types...)
97                 if err != nil {
98                         return fmt.Errorf("loading types input failed: %v", err)
99                 }
100         }
101
102         if inputFile != "" {
103                 // process one input file
104                 if err := generateFromFile(inputFile, outputDir, typesPkgs); err != nil {
105                         return fmt.Errorf("code generation from %s failed: %v\n", inputFile, err)
106                 }
107         } else {
108                 // process all files in specified directory
109                 dir, err := filepath.Abs(inputDir)
110                 if err != nil {
111                         return fmt.Errorf("invalid input directory: %v\n", err)
112                 }
113                 files, err := getInputFiles(inputDir, 1)
114                 if err != nil {
115                         return fmt.Errorf("problem getting files from input directory: %v\n", err)
116                 } else if len(files) == 0 {
117                         return fmt.Errorf("no input files found in input directory: %v\n", dir)
118                 }
119                 for _, file := range files {
120                         if err := generateFromFile(file, outputDir, typesPkgs); err != nil {
121                                 if continueErr {
122                                         logrus.Warnf("code generation from %s failed: %v (error ignored)\n", file, err)
123                                         continue
124                                 } else {
125                                         return fmt.Errorf("code generation from %s failed: %v\n", file, err)
126                                 }
127                         }
128                 }
129         }
130
131         return nil
132 }
133
134 // getInputFiles returns all input files located in specified directory
135 func getInputFiles(inputDir string, deep int) (files []string, err error) {
136         entries, err := ioutil.ReadDir(inputDir)
137         if err != nil {
138                 return nil, fmt.Errorf("reading directory %s failed: %v", inputDir, err)
139         }
140         for _, e := range entries {
141                 if e.IsDir() && deep > 0 {
142                         nestedDir := filepath.Join(inputDir, e.Name())
143                         if nested, err := getInputFiles(nestedDir, deep-1); err != nil {
144                                 return nil, err
145                         } else {
146                                 files = append(files, nested...)
147                         }
148                 } else if strings.HasSuffix(e.Name(), inputFileExt) {
149                         files = append(files, filepath.Join(inputDir, e.Name()))
150                 }
151         }
152         return files, nil
153 }
154
155 func parseInputJSON(inputData []byte) (*jsongo.Node, error) {
156         jsonRoot := new(jsongo.Node)
157         if err := json.Unmarshal(inputData, jsonRoot); err != nil {
158                 return nil, fmt.Errorf("unmarshalling JSON failed: %v", err)
159         }
160         return jsonRoot, nil
161 }
162
163 // generateFromFile generates Go package from one input JSON file
164 func generateFromFile(inputFile, outputDir string, typesPkgs []*context) error {
165         // create generator context
166         ctx, err := newContext(inputFile, outputDir)
167         if err != nil {
168                 return err
169         }
170
171         logf("------------------------------------------------------------")
172         logf("module: %s", ctx.moduleName)
173         logf(" - input: %s", ctx.inputFile)
174         logf(" - output: %s", ctx.outputFile)
175         logf("------------------------------------------------------------")
176
177         // prepare options
178         ctx.includeAPIVersion = *includeAPIVer
179         ctx.includeComments = *includeComments
180         ctx.includeBinapiNames = *includeBinapiNames
181         ctx.includeServices = *includeServices
182
183         // read API definition from input file
184         ctx.inputData, err = ioutil.ReadFile(ctx.inputFile)
185         if err != nil {
186                 return fmt.Errorf("reading input file %s failed: %v", ctx.inputFile, err)
187         }
188         // parse JSON data into objects
189         jsonRoot, err := parseInputJSON(ctx.inputData)
190         if err != nil {
191                 return fmt.Errorf("parsing JSON input failed: %v", err)
192         }
193         ctx.packageData, err = parsePackage(ctx, jsonRoot)
194         if err != nil {
195                 return fmt.Errorf("parsing package %s failed: %v", ctx.packageName, err)
196         }
197
198         if len(typesPkgs) > 0 {
199                 err = loadTypeAliases(ctx, typesPkgs)
200                 if err != nil {
201                         return fmt.Errorf("loading type aliases failed: %v", err)
202                 }
203         }
204
205         // generate Go package code
206         var buf bytes.Buffer
207         if err := generatePackage(ctx, &buf); err != nil {
208                 return fmt.Errorf("generating code for package %s failed: %v", ctx.packageName, err)
209         }
210
211         // create output directory
212         packageDir := filepath.Dir(ctx.outputFile)
213         if err := os.MkdirAll(packageDir, 0775); err != nil {
214                 return fmt.Errorf("creating output dir %s failed: %v", packageDir, err)
215         }
216         // write generated code to output file
217         if err := ioutil.WriteFile(ctx.outputFile, buf.Bytes(), 0666); err != nil {
218                 return fmt.Errorf("writing to output file %s failed: %v", ctx.outputFile, err)
219         }
220
221         // go format the output file (fail probably means the output is not compilable)
222         cmd := exec.Command("gofmt", "-w", ctx.outputFile)
223         if output, err := cmd.CombinedOutput(); err != nil {
224                 return fmt.Errorf("gofmt failed: %v\n%s", err, string(output))
225         }
226
227         return nil
228 }
229
230 func loadTypesPackages(types ...string) ([]*context, error) {
231         var ctxs []*context
232         for _, inputFile := range types {
233                 // create generator context
234                 ctx, err := newContext(inputFile, "")
235                 if err != nil {
236                         return nil, err
237                 }
238                 // read API definition from input file
239                 ctx.inputData, err = ioutil.ReadFile(ctx.inputFile)
240                 if err != nil {
241                         return nil, fmt.Errorf("reading input file %s failed: %v", ctx.inputFile, err)
242                 }
243                 // parse JSON data into objects
244                 jsonRoot, err := parseInputJSON(ctx.inputData)
245                 if err != nil {
246                         return nil, fmt.Errorf("parsing JSON input failed: %v", err)
247                 }
248                 ctx.packageData, err = parsePackage(ctx, jsonRoot)
249                 if err != nil {
250                         return nil, fmt.Errorf("parsing package %s failed: %v", ctx.packageName, err)
251                 }
252                 ctxs = append(ctxs, ctx)
253         }
254         return ctxs, nil
255 }
256
257 func loadTypeAliases(ctx *context, typesCtxs []*context) error {
258         for _, t := range ctx.packageData.Types {
259                 for _, c := range typesCtxs {
260                         if _, ok := ctx.packageData.Imports[t.Name]; ok {
261                                 break
262                         }
263                         for _, at := range c.packageData.Types {
264                                 if at.Name != t.Name {
265                                         continue
266                                 }
267                                 if len(at.Fields) != len(t.Fields) {
268                                         continue
269                                 }
270                                 ctx.packageData.Imports[t.Name] = Import{
271                                         Package: c.packageName,
272                                 }
273                         }
274                 }
275         }
276         for _, t := range ctx.packageData.Aliases {
277                 for _, c := range typesCtxs {
278                         if _, ok := ctx.packageData.Imports[t.Name]; ok {
279                                 break
280                         }
281                         for _, at := range c.packageData.Aliases {
282                                 if at.Name != t.Name {
283                                         continue
284                                 }
285                                 if at.Length != t.Length {
286                                         continue
287                                 }
288                                 if at.Type != t.Type {
289                                         continue
290                                 }
291                                 ctx.packageData.Imports[t.Name] = Import{
292                                         Package: c.packageName,
293                                 }
294                         }
295                 }
296         }
297         for _, t := range ctx.packageData.Enums {
298                 for _, c := range typesCtxs {
299                         if _, ok := ctx.packageData.Imports[t.Name]; ok {
300                                 break
301                         }
302                         for _, at := range c.packageData.Enums {
303                                 if at.Name != t.Name {
304                                         continue
305                                 }
306                                 if at.Type != t.Type {
307                                         continue
308                                 }
309                                 ctx.packageData.Imports[t.Name] = Import{
310                                         Package: c.packageName,
311                                 }
312                         }
313                 }
314         }
315         for _, t := range ctx.packageData.Unions {
316                 for _, c := range typesCtxs {
317                         if _, ok := ctx.packageData.Imports[t.Name]; ok {
318                                 break
319                         }
320                         for _, at := range c.packageData.Unions {
321                                 if at.Name != t.Name {
322                                         continue
323                                 }
324                                 ctx.packageData.Imports[t.Name] = Import{
325                                         Package: c.packageName,
326                                 }
327                         }
328                 }
329         }
330         return nil
331 }
332
333 func logf(f string, v ...interface{}) {
334         if *debugMode {
335                 logrus.Debugf(f, v...)
336         }
337 }