Fix codec fallback and generate type imports
[govpp.git] / binapigen / generator.go
1 //  Copyright (c) 2020 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 binapigen
16
17 import (
18         "bytes"
19         "fmt"
20         "go/format"
21         "io/ioutil"
22         "os"
23         "path"
24         "path/filepath"
25         "regexp"
26
27         "github.com/sirupsen/logrus"
28
29         "git.fd.io/govpp.git/binapigen/vppapi"
30 )
31
32 type Options struct {
33         VPPVersion string // version of VPP that produced API files
34
35         FilesToGenerate []string // list of API files to generate
36
37         ImportPrefix       string // defines import path prefix for importing types
38         ImportTypes        bool   // generate packages for import types
39         IncludeAPIVersion  bool   // include constant with API version string
40         IncludeComments    bool   // include parts of original source in comments
41         IncludeBinapiNames bool   // include binary API names as struct tag
42         IncludeServices    bool   // include service interface with client implementation
43         IncludeVppVersion  bool   // include info about used VPP version
44 }
45
46 type Generator struct {
47         Options
48
49         Files       []*File
50         FilesByPath map[string]*File
51         FilesByName map[string]*File
52
53         enumsByName   map[string]*Enum
54         aliasesByName map[string]*Alias
55         structsByName map[string]*Struct
56         unionsByName  map[string]*Union
57
58         genfiles []*GenFile
59 }
60
61 func New(opts Options, apifiles []*vppapi.File) (*Generator, error) {
62         g := &Generator{
63                 Options:       opts,
64                 FilesByPath:   make(map[string]*File),
65                 FilesByName:   make(map[string]*File),
66                 enumsByName:   map[string]*Enum{},
67                 aliasesByName: map[string]*Alias{},
68                 structsByName: map[string]*Struct{},
69                 unionsByName:  map[string]*Union{},
70         }
71
72         logrus.Debugf("adding %d VPP API files to generator", len(apifiles))
73         for _, apifile := range apifiles {
74                 filename := apifile.Path
75                 if filename == "" {
76                         filename = apifile.Name
77                 }
78                 if _, ok := g.FilesByPath[filename]; ok {
79                         return nil, fmt.Errorf("duplicate file name: %q", filename)
80                 }
81                 if _, ok := g.FilesByName[apifile.Name]; ok {
82                         return nil, fmt.Errorf("duplicate file: %q", apifile.Name)
83                 }
84
85                 file, err := newFile(g, apifile)
86                 if err != nil {
87                         return nil, err
88                 }
89                 g.Files = append(g.Files, file)
90                 g.FilesByPath[filename] = file
91                 g.FilesByName[apifile.Name] = file
92
93                 logrus.Debugf("added file %q (path: %v)", apifile.Name, apifile.Path)
94                 if len(file.Imports) > 0 {
95                         logrus.Debugf(" - %d imports: %v", len(file.Imports), file.Imports)
96                 }
97         }
98
99         if len(opts.FilesToGenerate) > 0 {
100                 logrus.Debugf("Checking %d files to generate: %v", len(opts.FilesToGenerate), opts.FilesToGenerate)
101                 for _, genfile := range opts.FilesToGenerate {
102                         file, ok := g.FilesByPath[genfile]
103                         if !ok {
104                                 file, ok = g.FilesByName[genfile]
105                                 if !ok {
106                                         return nil, fmt.Errorf("no API file found for: %v", genfile)
107                                 }
108                         }
109                         file.Generate = true
110                         if opts.ImportTypes {
111                                 // generate all imported files
112                                 for _, impFile := range file.importedFiles(g) {
113                                         impFile.Generate = true
114                                 }
115                         }
116                 }
117         } else {
118                 logrus.Debugf("Files to generate not specified, marking all %d files to generate", len(g.Files))
119                 for _, file := range g.Files {
120                         file.Generate = true
121                 }
122         }
123
124         logrus.Debugf("Resolving imported types")
125         for _, file := range g.Files {
126                 if !file.Generate {
127                         // skip resolving for non-generated files
128                         continue
129                 }
130                 var importedFiles []*File
131                 for _, impFile := range file.importedFiles(g) {
132                         if !impFile.Generate {
133                                 // exclude imports of non-generated files
134                                 continue
135                         }
136                         importedFiles = append(importedFiles, impFile)
137                 }
138                 file.loadTypeImports(g, importedFiles)
139         }
140
141         return g, nil
142 }
143
144 func (g *Generator) Generate() error {
145         if len(g.genfiles) == 0 {
146                 return fmt.Errorf("no files to generate")
147         }
148
149         logrus.Infof("Generating %d files", len(g.genfiles))
150         for _, genfile := range g.genfiles {
151                 if err := writeSourceTo(genfile.filename, genfile.Content()); err != nil {
152                         return fmt.Errorf("writing source for RPC package %s failed: %v", genfile.filename, err)
153                 }
154         }
155         return nil
156 }
157
158 type GenFile struct {
159         *Generator
160         filename  string
161         file      *File
162         outputDir string
163         buf       bytes.Buffer
164 }
165
166 func (g *Generator) NewGenFile(filename string) *GenFile {
167         f := &GenFile{
168                 Generator: g,
169                 filename:  filename,
170         }
171         g.genfiles = append(g.genfiles, f)
172         return f
173 }
174
175 func (f *GenFile) Content() []byte {
176         return f.buf.Bytes()
177 }
178
179 func writeSourceTo(outputFile string, b []byte) error {
180         // create output directory
181         packageDir := filepath.Dir(outputFile)
182         if err := os.MkdirAll(packageDir, 0775); err != nil {
183                 return fmt.Errorf("creating output dir %s failed: %v", packageDir, err)
184         }
185
186         // format generated source code
187         gosrc, err := format.Source(b)
188         if err != nil {
189                 _ = ioutil.WriteFile(outputFile, b, 0666)
190                 return fmt.Errorf("formatting source code failed: %v", err)
191         }
192
193         // write generated code to output file
194         if err := ioutil.WriteFile(outputFile, gosrc, 0666); err != nil {
195                 return fmt.Errorf("writing to output file %s failed: %v", outputFile, err)
196         }
197
198         lines := bytes.Count(gosrc, []byte("\n"))
199         logf("wrote %d lines (%d bytes) of code to: %q", lines, len(gosrc), outputFile)
200
201         return nil
202 }
203
204 func listImports(genfile *GenFile) map[string]string {
205         var importPath = genfile.ImportPrefix
206         if importPath == "" {
207                 importPath = resolveImportPath(genfile.outputDir)
208                 logrus.Debugf("resolved import path: %s", importPath)
209         }
210         imports := map[string]string{}
211         for _, imp := range genfile.file.imports {
212                 if _, ok := imports[imp]; !ok {
213                         imports[imp] = path.Join(importPath, imp)
214                 }
215         }
216         return imports
217 }
218
219 func resolveImportPath(outputDir string) string {
220         absPath, err := filepath.Abs(outputDir)
221         if err != nil {
222                 panic(err)
223         }
224         modRoot := findModuleRoot(absPath)
225         if modRoot == "" {
226                 logrus.Fatalf("module root not found at: %s", absPath)
227         }
228         modPath := findModulePath(path.Join(modRoot, "go.mod"))
229         if modPath == "" {
230                 logrus.Fatalf("module path not found")
231         }
232         relDir, err := filepath.Rel(modRoot, absPath)
233         if err != nil {
234                 panic(err)
235         }
236         return filepath.Join(modPath, relDir)
237 }
238
239 func findModuleRoot(dir string) (root string) {
240         if dir == "" {
241                 panic("dir not set")
242         }
243         dir = filepath.Clean(dir)
244
245         // Look for enclosing go.mod.
246         for {
247                 if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
248                         return dir
249                 }
250                 d := filepath.Dir(dir)
251                 if d == dir {
252                         break
253                 }
254                 dir = d
255         }
256         return ""
257 }
258
259 var (
260         modulePathRE = regexp.MustCompile(`module[ \t]+([^ \t\r\n]+)`)
261 )
262
263 func findModulePath(file string) string {
264         data, err := ioutil.ReadFile(file)
265         if err != nil {
266                 return ""
267         }
268         m := modulePathRE.FindSubmatch(data)
269         if m == nil {
270                 return ""
271         }
272         return string(m[1])
273 }