Make the warnings for validating services more obvious
[govpp.git] / cmd / binapi-generator / parse.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         "errors"
19         "fmt"
20         "sort"
21         "strings"
22
23         "github.com/bennyscetbun/jsongo"
24 )
25
26 func getTypeByRef(ctx *context, ref string) *Type {
27         for _, typ := range ctx.packageData.Types {
28                 if ref == toApiType(typ.Name) {
29                         return &typ
30                 }
31         }
32         return nil
33 }
34
35 func getUnionSize(ctx *context, union *Union) (maxSize int) {
36         for _, field := range union.Fields {
37                 if typ := getTypeByRef(ctx, field.Type); typ != nil {
38                         if size := getSizeOfType(typ); size > maxSize {
39                                 maxSize = size
40                         }
41                 }
42         }
43         return
44 }
45
46 // toApiType returns name that is used as type reference in VPP binary API
47 func toApiType(name string) string {
48         return fmt.Sprintf("vl_api_%s_t", name)
49 }
50
51 // parsePackage parses provided JSON data into objects prepared for code generation
52 func parsePackage(ctx *context, jsonRoot *jsongo.JSONNode) (*Package, error) {
53         logf(" %s contains: %d services, %d messages, %d types, %d enums, %d unions (version: %s)",
54                 ctx.packageName,
55                 jsonRoot.Map("services").Len(),
56                 jsonRoot.Map("messages").Len(),
57                 jsonRoot.Map("types").Len(),
58                 jsonRoot.Map("enums").Len(),
59                 jsonRoot.Map("unions").Len(),
60                 jsonRoot.Map("vl_api_version").Get(),
61         )
62
63         pkg := Package{
64                 APIVersion: jsonRoot.Map("vl_api_version").Get().(string),
65                 RefMap:     make(map[string]string),
66         }
67
68         // parse enums
69         enums := jsonRoot.Map("enums")
70         pkg.Enums = make([]Enum, enums.Len())
71         for i := 0; i < enums.Len(); i++ {
72                 enumNode := enums.At(i)
73
74                 enum, err := parseEnum(ctx, enumNode)
75                 if err != nil {
76                         return nil, err
77                 }
78                 pkg.Enums[i] = *enum
79                 pkg.RefMap[toApiType(enum.Name)] = enum.Name
80         }
81
82         // parse types
83         types := jsonRoot.Map("types")
84         pkg.Types = make([]Type, types.Len())
85         for i := 0; i < types.Len(); i++ {
86                 typNode := types.At(i)
87
88                 typ, err := parseType(ctx, typNode)
89                 if err != nil {
90                         return nil, err
91                 }
92                 pkg.Types[i] = *typ
93                 pkg.RefMap[toApiType(typ.Name)] = typ.Name
94         }
95
96         // parse unions
97         unions := jsonRoot.Map("unions")
98         pkg.Unions = make([]Union, unions.Len())
99         for i := 0; i < unions.Len(); i++ {
100                 unionNode := unions.At(i)
101
102                 union, err := parseUnion(ctx, unionNode)
103                 if err != nil {
104                         return nil, err
105                 }
106                 pkg.Unions[i] = *union
107                 pkg.RefMap[toApiType(union.Name)] = union.Name
108         }
109
110         // parse messages
111         messages := jsonRoot.Map("messages")
112         pkg.Messages = make([]Message, messages.Len())
113         for i := 0; i < messages.Len(); i++ {
114                 msgNode := messages.At(i)
115
116                 msg, err := parseMessage(ctx, msgNode)
117                 if err != nil {
118                         return nil, err
119                 }
120                 pkg.Messages[i] = *msg
121         }
122
123         // parse services
124         services := jsonRoot.Map("services")
125         if services.GetType() == jsongo.TypeMap {
126                 pkg.Services = make([]Service, services.Len())
127                 for i, key := range services.GetKeys() {
128                         svcNode := services.At(key)
129
130                         svc, err := parseService(ctx, key.(string), svcNode)
131                         if err != nil {
132                                 return nil, err
133                         }
134                         pkg.Services[i] = *svc
135                 }
136
137                 // sort services
138                 sort.Slice(pkg.Services, func(i, j int) bool {
139                         // dumps first
140                         if pkg.Services[i].Stream != pkg.Services[j].Stream {
141                                 return pkg.Services[i].Stream
142                         }
143                         return pkg.Services[i].RequestType < pkg.Services[j].RequestType
144                 })
145         }
146
147         printPackage(&pkg)
148
149         return &pkg, nil
150 }
151
152 // printPackage prints all loaded objects for package
153 func printPackage(pkg *Package) {
154         if len(pkg.Enums) > 0 {
155                 logf("loaded %d enums:", len(pkg.Enums))
156                 for k, enum := range pkg.Enums {
157                         logf(" - enum #%d\t%+v", k, enum)
158                 }
159         }
160         if len(pkg.Unions) > 0 {
161                 logf("loaded %d unions:", len(pkg.Unions))
162                 for k, union := range pkg.Unions {
163                         logf(" - union #%d\t%+v", k, union)
164                 }
165         }
166         if len(pkg.Types) > 0 {
167                 logf("loaded %d types:", len(pkg.Types))
168                 for _, typ := range pkg.Types {
169                         logf(" - type: %q (%d fields)", typ.Name, len(typ.Fields))
170                 }
171         }
172         if len(pkg.Messages) > 0 {
173                 logf("loaded %d messages:", len(pkg.Messages))
174                 for _, msg := range pkg.Messages {
175                         logf(" - message: %q (%d fields)", msg.Name, len(msg.Fields))
176                 }
177         }
178         if len(pkg.Services) > 0 {
179                 logf("loaded %d services:", len(pkg.Services))
180                 for _, svc := range pkg.Services {
181                         var info string
182                         if svc.Stream {
183                                 info = "(STREAM)"
184                         } else if len(svc.Events) > 0 {
185                                 info = fmt.Sprintf("(EVENTS: %v)", svc.Events)
186                         }
187                         logf(" - service: %q -> %q %s", svc.RequestType, svc.ReplyType, info)
188                 }
189         }
190 }
191
192 // parseEnum parses VPP binary API enum object from JSON node
193 func parseEnum(ctx *context, enumNode *jsongo.JSONNode) (*Enum, error) {
194         if enumNode.Len() == 0 || enumNode.At(0).GetType() != jsongo.TypeValue {
195                 return nil, errors.New("invalid JSON for enum specified")
196         }
197
198         enumName, ok := enumNode.At(0).Get().(string)
199         if !ok {
200                 return nil, fmt.Errorf("enum name is %T, not a string", enumNode.At(0).Get())
201         }
202         enumType, ok := enumNode.At(enumNode.Len() - 1).At("enumtype").Get().(string)
203         if !ok {
204                 return nil, fmt.Errorf("enum type invalid or missing")
205         }
206
207         enum := Enum{
208                 Name: enumName,
209                 Type: enumType,
210         }
211
212         // loop through enum entries, skip first (name) and last (enumtype)
213         for j := 1; j < enumNode.Len()-1; j++ {
214                 if enumNode.At(j).GetType() == jsongo.TypeArray {
215                         entry := enumNode.At(j)
216
217                         if entry.Len() < 2 || entry.At(0).GetType() != jsongo.TypeValue || entry.At(1).GetType() != jsongo.TypeValue {
218                                 return nil, errors.New("invalid JSON for enum entry specified")
219                         }
220
221                         entryName, ok := entry.At(0).Get().(string)
222                         if !ok {
223                                 return nil, fmt.Errorf("enum entry name is %T, not a string", entry.At(0).Get())
224                         }
225                         entryVal := entry.At(1).Get()
226
227                         enum.Entries = append(enum.Entries, EnumEntry{
228                                 Name:  entryName,
229                                 Value: entryVal,
230                         })
231                 }
232         }
233
234         return &enum, nil
235 }
236
237 // parseUnion parses VPP binary API union object from JSON node
238 func parseUnion(ctx *context, unionNode *jsongo.JSONNode) (*Union, error) {
239         if unionNode.Len() == 0 || unionNode.At(0).GetType() != jsongo.TypeValue {
240                 return nil, errors.New("invalid JSON for union specified")
241         }
242
243         unionName, ok := unionNode.At(0).Get().(string)
244         if !ok {
245                 return nil, fmt.Errorf("union name is %T, not a string", unionNode.At(0).Get())
246         }
247         unionCRC, ok := unionNode.At(unionNode.Len() - 1).At("crc").Get().(string)
248         if !ok {
249                 return nil, fmt.Errorf("union crc invalid or missing")
250         }
251
252         union := Union{
253                 Name: unionName,
254                 CRC:  unionCRC,
255         }
256
257         // loop through union fields, skip first (name) and last (crc)
258         for j := 1; j < unionNode.Len()-1; j++ {
259                 if unionNode.At(j).GetType() == jsongo.TypeArray {
260                         fieldNode := unionNode.At(j)
261
262                         field, err := parseField(ctx, fieldNode)
263                         if err != nil {
264                                 return nil, err
265                         }
266
267                         union.Fields = append(union.Fields, *field)
268                 }
269         }
270
271         return &union, nil
272 }
273
274 // parseType parses VPP binary API type object from JSON node
275 func parseType(ctx *context, typeNode *jsongo.JSONNode) (*Type, error) {
276         if typeNode.Len() == 0 || typeNode.At(0).GetType() != jsongo.TypeValue {
277                 return nil, errors.New("invalid JSON for type specified")
278         }
279
280         typeName, ok := typeNode.At(0).Get().(string)
281         if !ok {
282                 return nil, fmt.Errorf("type name is %T, not a string", typeNode.At(0).Get())
283         }
284         typeCRC, ok := typeNode.At(typeNode.Len() - 1).At("crc").Get().(string)
285         if !ok {
286                 return nil, fmt.Errorf("type crc invalid or missing")
287         }
288
289         typ := Type{
290                 Name: typeName,
291                 CRC:  typeCRC,
292         }
293
294         // loop through type fields, skip first (name) and last (crc)
295         for j := 1; j < typeNode.Len()-1; j++ {
296                 if typeNode.At(j).GetType() == jsongo.TypeArray {
297                         fieldNode := typeNode.At(j)
298
299                         field, err := parseField(ctx, fieldNode)
300                         if err != nil {
301                                 return nil, err
302                         }
303
304                         typ.Fields = append(typ.Fields, *field)
305                 }
306         }
307
308         return &typ, nil
309 }
310
311 // parseMessage parses VPP binary API message object from JSON node
312 func parseMessage(ctx *context, msgNode *jsongo.JSONNode) (*Message, error) {
313         if msgNode.Len() == 0 || msgNode.At(0).GetType() != jsongo.TypeValue {
314                 return nil, errors.New("invalid JSON for message specified")
315         }
316
317         msgName, ok := msgNode.At(0).Get().(string)
318         if !ok {
319                 return nil, fmt.Errorf("message name is %T, not a string", msgNode.At(0).Get())
320         }
321         msgCRC, ok := msgNode.At(msgNode.Len() - 1).At("crc").Get().(string)
322         if !ok {
323
324                 return nil, fmt.Errorf("message crc invalid or missing")
325         }
326
327         msg := Message{
328                 Name: msgName,
329                 CRC:  msgCRC,
330         }
331
332         // loop through message fields, skip first (name) and last (crc)
333         for j := 1; j < msgNode.Len()-1; j++ {
334                 if msgNode.At(j).GetType() == jsongo.TypeArray {
335                         fieldNode := msgNode.At(j)
336
337                         field, err := parseField(ctx, fieldNode)
338                         if err != nil {
339                                 return nil, err
340                         }
341
342                         msg.Fields = append(msg.Fields, *field)
343                 }
344         }
345
346         return &msg, nil
347 }
348
349 // parseField parses VPP binary API object field from JSON node
350 func parseField(ctx *context, field *jsongo.JSONNode) (*Field, error) {
351         if field.Len() < 2 || field.At(0).GetType() != jsongo.TypeValue || field.At(1).GetType() != jsongo.TypeValue {
352                 return nil, errors.New("invalid JSON for field specified")
353         }
354
355         fieldType, ok := field.At(0).Get().(string)
356         if !ok {
357                 return nil, fmt.Errorf("field type is %T, not a string", field.At(0).Get())
358         }
359         fieldName, ok := field.At(1).Get().(string)
360         if !ok {
361                 return nil, fmt.Errorf("field name is %T, not a string", field.At(1).Get())
362         }
363         var fieldLength float64
364         if field.Len() >= 3 {
365                 fieldLength, ok = field.At(2).Get().(float64)
366                 if !ok {
367                         return nil, fmt.Errorf("field length is %T, not an int", field.At(2).Get())
368                 }
369         }
370         var fieldLengthFrom string
371         if field.Len() >= 4 {
372                 fieldLengthFrom, ok = field.At(3).Get().(string)
373                 if !ok {
374                         return nil, fmt.Errorf("field length from is %T, not a string", field.At(3).Get())
375                 }
376         }
377
378         return &Field{
379                 Name:     fieldName,
380                 Type:     fieldType,
381                 Length:   int(fieldLength),
382                 SizeFrom: fieldLengthFrom,
383         }, nil
384 }
385
386 // parseService parses VPP binary API service object from JSON node
387 func parseService(ctx *context, svcName string, svcNode *jsongo.JSONNode) (*Service, error) {
388         if svcNode.Len() == 0 || svcNode.At("reply").GetType() != jsongo.TypeValue {
389                 return nil, errors.New("invalid JSON for service specified")
390         }
391
392         svc := Service{
393                 Name:        ctx.moduleName + "." + svcName,
394                 RequestType: svcName,
395         }
396
397         if replyNode := svcNode.At("reply"); replyNode.GetType() == jsongo.TypeValue {
398                 reply, ok := replyNode.Get().(string)
399                 if !ok {
400                         return nil, fmt.Errorf("service reply is %T, not a string", replyNode.Get())
401                 }
402                 if reply != "null" {
403                         svc.ReplyType = reply
404                 }
405         }
406
407         // stream service (dumps)
408         if streamNode := svcNode.At("stream"); streamNode.GetType() == jsongo.TypeValue {
409                 var ok bool
410                 svc.Stream, ok = streamNode.Get().(bool)
411                 if !ok {
412                         return nil, fmt.Errorf("service stream is %T, not a string", streamNode.Get())
413                 }
414         }
415
416         // events service (event subscription)
417         if eventsNode := svcNode.At("events"); eventsNode.GetType() == jsongo.TypeArray {
418                 for j := 0; j < eventsNode.Len(); j++ {
419                         event := eventsNode.At(j).Get().(string)
420                         svc.Events = append(svc.Events, event)
421                 }
422         }
423
424         // validate service
425         if svc.IsEventService() {
426                 if !strings.HasPrefix(svc.RequestType, "want_") {
427                         log.Warnf("Unusual EVENTS SERVICE: %+v\n"+
428                                 "- events service %q does not have 'want_' prefix in request.",
429                                 svc, svc.Name)
430                 }
431         } else if svc.IsDumpService() {
432                 if !strings.HasSuffix(svc.RequestType, "_dump") ||
433                         !strings.HasSuffix(svc.ReplyType, "_details") {
434                         log.Warnf("Unusual STREAM SERVICE: %+v\n"+
435                                 "- stream service %q does not have '_dump' suffix in request or reply does not have '_details' suffix.",
436                                 svc, svc.Name)
437                 }
438         } else if svc.IsRequestService() {
439                 if !strings.HasSuffix(svc.ReplyType, "_reply") {
440                         log.Warnf("Unusual REQUEST SERVICE: %+v\n"+
441                                 "- service %q does not have '_reply' suffix in reply.",
442                                 svc, svc.Name)
443                 }
444         }
445
446         return &svc, nil
447 }
448
449 // convertToGoType translates the VPP binary API type into Go type
450 func convertToGoType(ctx *context, binapiType string) (typ string) {
451         if t, ok := binapiTypes[binapiType]; ok {
452                 // basic types
453                 typ = t
454         } else if r, ok := ctx.packageData.RefMap[binapiType]; ok {
455                 // specific types (enums/types/unions)
456                 typ = camelCaseName(r)
457         } else {
458                 // fallback type
459                 log.Warnf("found unknown VPP binary API type %q, using byte", binapiType)
460                 typ = "byte"
461         }
462         return typ
463 }