go.ligato.io/vpp-agent/v3@v3.5.0/plugins/restapi/jsonschema/converter/converter.go (about) 1 package converter 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "path" 8 "regexp" 9 "strings" 10 11 "github.com/alecthomas/jsonschema" 12 "github.com/sirupsen/logrus" 13 "google.golang.org/protobuf/proto" 14 "google.golang.org/protobuf/types/descriptorpb" 15 "google.golang.org/protobuf/types/pluginpb" 16 ) 17 18 const ( 19 messageDelimiter = "+" 20 ) 21 22 // Converter is everything you need to convert protos to JSONSchemas: 23 type Converter struct { 24 AllFieldsRequired bool 25 AllowNullValues bool 26 DisallowAdditionalProperties bool 27 DisallowBigIntsAsStrings bool 28 PrefixSchemaFilesWithPackage bool 29 UseJSONFieldnamesOnly bool 30 UseProtoAndJSONFieldnames bool 31 logger *logrus.Logger 32 sourceInfo *sourceCodeInfo 33 messageTargets []string 34 } 35 36 // New returns a configured *Converter: 37 func New(logger *logrus.Logger) *Converter { 38 return &Converter{ 39 logger: logger, 40 } 41 } 42 43 // ConvertFrom tells the convert to work on the given input: 44 func (c *Converter) ConvertFrom(rd io.Reader) (*pluginpb.CodeGeneratorResponse, error) { 45 c.logger.Debug("Reading code generation request") 46 input, err := io.ReadAll(rd) 47 if err != nil { 48 c.logger.WithError(err).Error("Failed to read request") 49 return nil, err 50 } 51 52 req := &pluginpb.CodeGeneratorRequest{} 53 err = proto.Unmarshal(input, req) 54 if err != nil { 55 c.logger.WithError(err).Error("Can't unmarshal input") 56 return nil, err 57 } 58 59 c.logger.Debug("Converting input") 60 return c.convert(req) 61 } 62 63 func (c *Converter) parseGeneratorParameters(parameters string) { 64 for _, parameter := range strings.Split(parameters, ",") { 65 switch parameter { 66 case "all_fields_required": 67 c.AllFieldsRequired = true 68 case "allow_null_values": 69 c.AllowNullValues = true 70 case "debug": 71 c.logger.SetLevel(logrus.DebugLevel) 72 case "disallow_additional_properties": 73 c.DisallowAdditionalProperties = true 74 case "disallow_bigints_as_strings": 75 c.DisallowBigIntsAsStrings = true 76 case "json_fieldnames": 77 c.UseJSONFieldnamesOnly = true 78 case "prefix_schema_files_with_package": 79 c.PrefixSchemaFilesWithPackage = true 80 case "proto_and_json_fieldnames": 81 c.UseProtoAndJSONFieldnames = true 82 } 83 84 // look for specific message targets 85 // message types are separated by messageDelimiter "+" 86 // examples: 87 // messages=[foo+bar] 88 // messages=[foo] 89 rx := regexp.MustCompile(`messages=\[([^\]]+)\]`) 90 if matches := rx.FindStringSubmatch(parameter); len(matches) == 2 { 91 c.messageTargets = strings.Split(matches[1], messageDelimiter) 92 } 93 } 94 } 95 96 // Converts a proto "ENUM" into a JSON-Schema: 97 func (c *Converter) convertEnumType(enum *descriptorpb.EnumDescriptorProto) (jsonschema.Type, error) { 98 99 // Prepare a new jsonschema.Type for our eventual return value: 100 jsonSchemaType := jsonschema.Type{ 101 Version: jsonschema.Version, 102 } 103 104 // Generate a description from src comments (if available) 105 if src := c.sourceInfo.GetEnum(enum); src != nil { 106 jsonSchemaType.Description = formatDescription(src) 107 } 108 109 // Note: not setting type specification(oneof string and integer), because explicitly saying which 110 // values are valid (and any other is invalid) is enough specification what can be used 111 // (this also overcome bug in example creator https://json-schema-faker.js.org/ that doesn't select 112 // correct type for enum value but rather chooses random type from oneof and cast value to that type) 113 // 114 // Allow both strings and integers: 115 // jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: "string"}) 116 // jsonSchemaType.OneOf = append(jsonSchemaType.OneOf, &jsonschema.Type{Type: "integer"}) 117 118 // Add the allowed values: 119 for _, enumValue := range enum.Value { 120 jsonSchemaType.Enum = append(jsonSchemaType.Enum, enumValue.Name) 121 jsonSchemaType.Enum = append(jsonSchemaType.Enum, enumValue.Number) 122 } 123 124 return jsonSchemaType, nil 125 } 126 127 // Converts a proto file into a JSON-Schema: 128 func (c *Converter) convertFile(file *descriptorpb.FileDescriptorProto) ([]*pluginpb.CodeGeneratorResponse_File, error) { 129 // Input filename: 130 protoFileName := path.Base(file.GetName()) 131 132 // Prepare a list of responses: 133 var response []*pluginpb.CodeGeneratorResponse_File 134 135 // user wants specific messages 136 genSpecificMessages := len(c.messageTargets) > 0 137 138 // Warn about multiple messages / enums in files: 139 if !genSpecificMessages && len(file.GetMessageType()) > 1 { 140 c.logger.WithField("schemas", len(file.GetMessageType())).WithField("proto_filename", protoFileName).Warn("protoc-gen-jsonschema will create multiple MESSAGE schemas from one proto file") 141 } 142 143 if len(file.GetEnumType()) > 1 { 144 c.logger.WithField("schemas", len(file.GetMessageType())).WithField("proto_filename", protoFileName).Warn("protoc-gen-jsonschema will create multiple ENUM schemas from one proto file") 145 } 146 147 // Generate standalone ENUMs: 148 if len(file.GetMessageType()) == 0 { 149 for _, enum := range file.GetEnumType() { 150 jsonSchemaFileName := c.generateSchemaFilename(file, enum.GetName()) 151 c.logger.WithField("proto_filename", protoFileName).WithField("enum_name", enum.GetName()).WithField("jsonschema_filename", jsonSchemaFileName).Info("Generating JSON-schema for stand-alone ENUM") 152 153 // Convert the ENUM: 154 enumJSONSchema, err := c.convertEnumType(enum) 155 if err != nil { 156 c.logger.WithError(err).WithField("proto_filename", protoFileName).Error("Failed to convert") 157 return nil, err 158 } 159 160 // Marshal the JSON-Schema into JSON: 161 jsonSchemaJSON, err := json.MarshalIndent(enumJSONSchema, "", " ") 162 if err != nil { 163 c.logger.WithError(err).Error("Failed to encode jsonSchema") 164 return nil, err 165 } 166 167 // Add a response: 168 resFile := &pluginpb.CodeGeneratorResponse_File{ 169 Name: proto.String(jsonSchemaFileName), 170 Content: proto.String(string(jsonSchemaJSON)), 171 } 172 response = append(response, resFile) 173 } 174 } else { 175 // Otherwise process MESSAGES (packages): 176 pkg, ok := c.relativelyLookupPackage(globalPkg, file.GetPackage()) 177 if !ok { 178 return nil, fmt.Errorf("no such package found: %s", file.GetPackage()) 179 } 180 181 for _, msg := range file.GetMessageType() { 182 // skip if we are only generating schema for specific messages 183 if genSpecificMessages && !contains(c.messageTargets, msg.GetName()) { 184 continue 185 } 186 187 jsonSchemaFileName := c.generateSchemaFilename(file, msg.GetName()) 188 c.logger.WithField("proto_filename", protoFileName).WithField("msg_name", msg.GetName()).WithField("jsonschema_filename", jsonSchemaFileName).Info("Generating JSON-schema for MESSAGE") 189 190 // Convert the message: 191 messageJSONSchema, err := c.convertMessageType(pkg, msg) 192 if err != nil { 193 c.logger.WithError(err).WithField("proto_filename", protoFileName).Error("Failed to convert") 194 return nil, err 195 } 196 197 // Marshal the JSON-Schema into JSON: 198 jsonSchemaJSON, err := json.MarshalIndent(messageJSONSchema, "", " ") 199 if err != nil { 200 c.logger.WithError(err).Error("Failed to encode jsonSchema") 201 return nil, err 202 } 203 204 // Add a response: 205 resFile := &pluginpb.CodeGeneratorResponse_File{ 206 Name: proto.String(jsonSchemaFileName), 207 Content: proto.String(string(jsonSchemaJSON)), 208 } 209 response = append(response, resFile) 210 } 211 } 212 213 return response, nil 214 } 215 216 func (c *Converter) convert(req *pluginpb.CodeGeneratorRequest) (*pluginpb.CodeGeneratorResponse, error) { 217 c.parseGeneratorParameters(req.GetParameter()) 218 219 generateTargets := make(map[string]bool) 220 for _, file := range req.GetFileToGenerate() { 221 generateTargets[file] = true 222 } 223 224 c.sourceInfo = newSourceCodeInfo(req.GetProtoFile()) 225 res := &pluginpb.CodeGeneratorResponse{} 226 for _, file := range req.GetProtoFile() { 227 if file.GetPackage() == "" { 228 c.logger.WithField("filename", file.GetName()).Warn("Proto file doesn't specify a package") 229 continue 230 } 231 232 for _, msg := range file.GetMessageType() { 233 c.logger.WithField("msg_name", msg.GetName()).WithField("package_name", file.GetPackage()).Debug("Loading a message") 234 c.registerType(file.Package, msg) 235 } 236 237 for _, en := range file.GetEnumType() { 238 c.logger.WithField("enum_name", en.GetName()).WithField("package_name", file.GetPackage()).Debug("Loading an enum") 239 c.registerEnum(file.Package, en) 240 } 241 242 if _, ok := generateTargets[file.GetName()]; ok { 243 c.logger.WithField("filename", file.GetName()).Debug("Converting file") 244 converted, err := c.convertFile(file) 245 if err != nil { 246 res.Error = proto.String(fmt.Sprintf("Failed to convert %s: %v", file.GetName(), err)) 247 return res, err 248 } 249 res.File = append(res.File, converted...) 250 } 251 } 252 return res, nil 253 } 254 255 func (c *Converter) generateSchemaFilename(file *descriptorpb.FileDescriptorProto, protoName string) string { 256 if c.PrefixSchemaFilesWithPackage { 257 return fmt.Sprintf("%s/%s.jsonschema", file.GetPackage(), protoName) 258 } 259 return fmt.Sprintf("%s.jsonschema", protoName) 260 } 261 262 func contains(haystack []string, needle string) bool { 263 for i := 0; i < len(haystack); i++ { 264 if haystack[i] == needle { 265 return true 266 } 267 } 268 269 return false 270 }