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