github.com/cbroglie/openapi2proto@v0.0.0-20171004221549-76b8501da882/openapi.go (about) 1 package openapi2proto 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "net/url" 8 "path" 9 "regexp" 10 "strings" 11 ) 12 13 // APIDefinition is the base struct for containing OpenAPI spec 14 // declarations. 15 type APIDefinition struct { 16 FileName string // internal use to pass file path 17 Swagger string `yaml:"swagger" json:"swagger"` 18 Info struct { 19 Title string `yaml:"title" json:"title"` 20 Description string `yaml:"description" json:"description"` 21 Version string `yaml:"version" json:"version"` 22 } `yaml:"info" json:"info"` 23 Host string `yaml:"host" json:"host"` 24 Schemes []string `yaml:"schemes" json:"schemes"` 25 BasePath string `yaml:"basePath" json:"basePath"` 26 Produces []string `yaml:"produces" json:"produces"` 27 Paths map[string]*Path `yaml:"paths" json:"paths"` 28 Definitions map[string]*Items `yaml:"definitions" json:"definitions"` 29 Parameters map[string]*Items `yaml:"parameters" json:"parameters"` 30 } 31 32 // Path represents all of the endpoints and parameters available for a single 33 // path. 34 type Path struct { 35 Get *Endpoint `yaml:"get" json:"get"` 36 Put *Endpoint `yaml:"put" json:"put"` 37 Post *Endpoint `yaml:"post" json:"post"` 38 Delete *Endpoint `yaml:"delete" json:"delete"` 39 Parameters Parameters `yaml:"parameters" json:"parameters"` 40 } 41 42 // Parameters is a slice of request parameters for a single endpoint. 43 type Parameters []*Items 44 45 // Response represents the response object in an OpenAPI spec. 46 type Response struct { 47 Description string `yaml:"description" json:"description"` 48 Schema *Items `yaml:"schema" json:"schema"` 49 } 50 51 // Endpoint represents an endpoint for a path in an OpenAPI spec. 52 type Endpoint struct { 53 Summary string `yaml:"summary" json:"summary"` 54 Description string `yaml:"description" json:"description"` 55 Parameters Parameters `yaml:"parameters" json:"parameters"` 56 Tags []string `yaml:"tags" json:"tags"` 57 Responses map[string]*Response `yaml:"responses" json:"responses"` 58 } 59 60 // Model represents a model definition from an OpenAPI spec. 61 type Model struct { 62 Properties map[string]*Items `yaml:"properties" json:"properties"` 63 Name string 64 Depth int 65 } 66 67 // Items represent Model properties in an OpenAPI spec. 68 type Items struct { 69 Description string `yaml:"description,omitempty" json:"description,omitempty"` 70 // scalar 71 Type interface{} `yaml:"type" json:"type"` 72 Format interface{} `yaml:"format,omitempty" json:"format,omitempty"` 73 Enum []string `yaml:"enum,omitempty" json:"enum,omitempty"` 74 75 ProtoTag int `yaml:"x-proto-tag" json:"x-proto-tag"` 76 77 // Map type 78 AdditionalProperties *Items `yaml:"additionalProperties" json:"additionalProperties"` 79 80 // ref another Model 81 Ref string `yaml:"$ref"json:"$ref"` 82 83 // is an array 84 Items *Items `yaml:"items" json:"items"` 85 86 // for request parameters 87 In string `yaml:"in" json:"in"` 88 Schema *Items `yaml:"schema" json:"schema"` 89 90 // is an other Model 91 Model `yaml:",inline"` 92 } 93 94 func (i Items) Comment() string { 95 return prepComment(i.Description, " ") 96 } 97 98 func (i Items) HasComment() bool { 99 return i.Description != "" 100 } 101 102 func protoScalarType(name string, typ, frmt interface{}, indx int) string { 103 frmat := format(frmt) 104 switch typ.(type) { 105 case string: 106 switch typ.(string) { 107 case "string": 108 return fmt.Sprintf("string %s = %d", name, indx) 109 case "bytes": 110 return fmt.Sprintf("bytes %s = %d", name, indx) 111 case "number": 112 if frmat == "" { 113 frmat = "double" 114 } 115 return fmt.Sprintf("%s %s = %d", frmat, name, indx) 116 case "integer": 117 if frmat == "" { 118 frmat = "int32" 119 } 120 return fmt.Sprintf("%s %s = %d", frmat, name, indx) 121 case "boolean": 122 return fmt.Sprintf("bool %s = %d", name, indx) 123 case "null": 124 return fmt.Sprintf("google.protobuf.NullValue %s = %d", name, indx) 125 } 126 } 127 128 return "" 129 } 130 131 func refDatas(ref string) (string, string) { 132 // split on '#/' 133 refDatas := strings.SplitN(ref, "#/", 2) 134 // check for references outside of this spec 135 if len(refDatas) > 1 { 136 return refDatas[0], refDatas[1] 137 } 138 return ref, "" 139 } 140 141 // $ref should be in the format of: 142 // {import path}#/{definitions|parameters}/{typeName} 143 // this will produce 144 func refType(ref string, defs map[string]*Items) (string, string) { 145 var ( 146 rawPkg string 147 pkg string 148 itemType string 149 ) 150 151 rawPkg, itemType = refDatas(ref) 152 153 if rawPkg != "" || 154 strings.HasSuffix(ref, ".json") || 155 strings.HasSuffix(ref, ".yaml") { 156 if rawPkg == "" { 157 rawPkg = ref 158 } 159 // if URL, parse it 160 if strings.HasPrefix(rawPkg, "http") { 161 u, err := url.Parse(rawPkg) 162 if err != nil { 163 log.Fatalf("invalid external reference URL: %s: %s", ref, err) 164 } 165 rawPkg = u.Path 166 } 167 168 rawPkg = path.Clean(rawPkg) 169 rawPkg = strings.TrimPrefix(rawPkg, "/") 170 rawPkg = strings.ToLower(rawPkg) 171 // take out possible file types 172 rawPkg = strings.TrimSuffix(rawPkg, path.Ext(rawPkg)) 173 rawPkg = strings.TrimLeft(rawPkg, "/.") 174 } 175 176 // in case it's a nested reference 177 itemType = strings.TrimPrefix(itemType, "definitions/") 178 itemType = strings.TrimPrefix(itemType, "parameters/") 179 itemType = strings.TrimPrefix(itemType, "responses/") 180 itemType = strings.TrimSuffix(itemType, ".yaml") 181 itemType = strings.TrimSuffix(itemType, ".json") 182 itemType = strings.TrimSuffix(itemType, ".proto") 183 if i, ok := defs[itemType]; ok { 184 if i.Type != "object" && !(i.Type == "string" && len(i.Enum) > 0) { 185 typ, ok := i.Type.(string) 186 if !ok { 187 log.Fatalf("invalid $ref object referenced with a type of %s", i.Type) 188 } 189 itemType = typ 190 } 191 } 192 if rawPkg != "" { 193 pkg = rawPkg + ".proto" 194 if itemType != "" { 195 rawPkg = rawPkg + "/" + itemType 196 } 197 dir, name := path.Split(rawPkg) 198 if !strings.Contains(name, ".") { 199 itemType = strings.Replace(dir, "/", ".", -1) + strings.Title(name) 200 } 201 } 202 return itemType, pkg 203 } 204 205 func refDef(name, ref string, index int, defs map[string]*Items) string { 206 itemType, _ := refType(ref, defs) 207 return fmt.Sprintf("%s %s = %d", itemType, name, index) 208 } 209 210 // ProtoMessage will generate a set of fields for a protobuf v3 schema given the 211 // current Items and information. 212 func (i *Items) ProtoMessage(msgName, name string, defs map[string]*Items, indx *int, depth int) string { 213 *indx++ 214 if i.ProtoTag != 0 { 215 *indx = i.ProtoTag 216 } 217 index := *indx 218 name = strings.Replace(name, "-", "_", -1) 219 220 if i.Ref != "" { 221 return refDef(name, i.Ref, index, defs) 222 } 223 224 // for parameters 225 if i.Schema != nil { 226 if i.Schema.Ref != "" { 227 return refDef(name, i.Schema.Ref, index, defs) 228 } 229 return protoComplex(i.Schema, i.Schema.Type.(string), msgName, name, defs, indx, depth) 230 } 231 232 switch i.Type.(type) { 233 case string: 234 return protoComplex(i, i.Type.(string), msgName, name, defs, indx, depth) 235 case []interface{}: 236 types := i.Type.([]interface{}) 237 hasNull := false 238 var otherTypes []string 239 for _, itp := range types { 240 tp := itp.(string) 241 if strings.ToLower(tp) == "null" { 242 hasNull = true 243 continue 244 } 245 otherTypes = append(otherTypes, tp) 246 } 247 // non-nullable fields with multiple types? Make it an Any. 248 if !hasNull || len(otherTypes) > 1 { 249 if depth >= 0 { 250 return fmt.Sprintf("google.protobuf.Any %s = %d", name, *indx) 251 } 252 return "" 253 } 254 255 if depth < 0 { 256 return "" 257 } 258 259 switch otherTypes[0] { 260 case "string": 261 return fmt.Sprintf("google.protobuf.StringValue %s = %d", name, *indx) 262 case "number": 263 frmat := format(i.Format) 264 if frmat == "" { 265 frmat = "Double" 266 } else { 267 frmat = strings.Title(frmat) 268 } 269 return fmt.Sprintf("google.protobuf.%sValue %s = %d", frmat, name, *indx) 270 case "integer": 271 frmat := format(i.Format) 272 if frmat == "" { 273 frmat = "Int32" 274 } 275 frmat = strings.Title(frmat) 276 // unsigned ints :\ 277 if strings.HasPrefix(frmat, "Ui") { 278 frmat = strings.TrimPrefix(frmat, "Ui") 279 frmat = "UI" + frmat 280 } 281 return fmt.Sprintf("google.protobuf.%sValue %s = %d", frmat, name, *indx) 282 case "bytes": 283 return fmt.Sprintf("google.protobuf.BytesValue %s = %d", name, *indx) 284 case "boolean": 285 return fmt.Sprintf("google.protobuf.BoolValue %s = %d", name, *indx) 286 default: 287 if depth >= 0 { 288 return fmt.Sprintf("google.protobuf.Any %s = %d", name, *indx) 289 } 290 } 291 } 292 293 if depth >= 0 { 294 return protoScalarType(name, i.Type, i.Format, index) 295 } 296 return "" 297 } 298 299 func protoComplex(i *Items, typ, msgName, name string, defs map[string]*Items, index *int, depth int) string { 300 switch typ { 301 case "object": 302 // check for map declaration 303 if i.AdditionalProperties != nil { 304 var itemType string 305 switch { 306 case i.AdditionalProperties.Ref != "": 307 itemType, _ = refType(i.AdditionalProperties.Ref, defs) 308 case i.AdditionalProperties.Type != nil: 309 itemType = i.AdditionalProperties.Type.(string) 310 } 311 return fmt.Sprintf("map<string, %s> %s = %d", itemType, name, *index) 312 } 313 314 // check for referenced schema object (parameters/fields) 315 if i.Schema != nil { 316 if i.Schema.Ref != "" { 317 return refDef(indent(depth+1)+name, i.Schema.Ref, *index, defs) 318 } 319 } 320 321 // otherwise, normal object model 322 i.Model.Name = strings.Title(name) 323 msgStr := i.Model.ProtoModel(i.Model.Name, depth+1, defs) 324 if depth < 0 { 325 return msgStr 326 } 327 return fmt.Sprintf("%s\n%s%s %s = %d", msgStr, indent(depth+1), i.Model.Name, name, *index) 328 case "array": 329 if i.Items != nil { 330 // CHECK FOR SCALAR 331 pt := protoScalarType(name, i.Items.Type, i.Items.Format, *index) 332 if pt != "" { 333 return fmt.Sprintf("repeated %s", pt) 334 } 335 336 // CHECK FOR REF 337 if i.Items.Ref != "" { 338 return "repeated " + refDef(name, i.Items.Ref, *index, defs) 339 } 340 341 // breaks on 'Class' :\ 342 if !strings.HasSuffix(name, "ss") { 343 i.Items.Model.Name = strings.Title(strings.TrimSuffix(name, "s")) 344 } else { 345 i.Items.Model.Name = strings.Title(name) 346 } 347 msgStr := i.Items.Model.ProtoModel(i.Items.Model.Name, depth+1, defs) 348 return fmt.Sprintf("%s\n%srepeated %s %s = %d", msgStr, indent(depth+1), i.Items.Model.Name, name, *index) 349 } 350 351 case "string": 352 if len(i.Enum) > 0 { 353 var eName string 354 // breaks on 'Class' :\ 355 if !strings.HasSuffix(name, "ss") { 356 eName = strings.TrimSuffix(name, "s") 357 } else { 358 eName = name 359 } 360 361 eName = strings.Title(eName) 362 363 if msgName != "" { 364 eName = strings.Title(msgName) + "_" + eName 365 } 366 367 msgStr := ProtoEnum(eName, i.Enum, depth+1) 368 if depth < 0 { 369 return msgStr 370 } 371 return fmt.Sprintf("%s\n%s%s %s = %d", msgStr, indent(depth+1), eName, name, *index) 372 } 373 if depth >= 0 { 374 return protoScalarType(name, i.Type, i.Format, *index) 375 } 376 default: 377 if depth >= 0 { 378 return protoScalarType(name, i.Type, i.Format, *index) 379 } 380 } 381 return "" 382 } 383 384 // ProtoEnum will generate a protobuf v3 enum declaration from 385 // the given info. 386 func ProtoEnum(name string, enums []string, depth int) string { 387 s := struct { 388 Name string 389 Enum []string 390 Depth int 391 }{ 392 name, enums, depth, 393 } 394 var b bytes.Buffer 395 err := protoEnumTmpl.Execute(&b, s) 396 if err != nil { 397 log.Fatal("unable to protobuf model: ", err) 398 } 399 return b.String() 400 } 401 402 func PathMethodToName(path, method string) string { 403 var name string 404 path = strings.TrimSuffix(path, ".json") 405 path = strings.Replace(path, "-", " ", -1) 406 path = strings.Replace(path, ".", " ", -1) 407 path = strings.Replace(path, "/", " ", -1) 408 // Strip out illegal-for-identifier characters in the path, including any query string. 409 // Note that query strings are illegal in swagger paths, but some tooling seems to tolerate them. 410 re := regexp.MustCompile(`[\{\}\[\]()/\.]|\?.*`) 411 path = re.ReplaceAllString(path, "") 412 for _, nme := range strings.Fields(path) { 413 name += strings.Title(nme) 414 } 415 return strings.Title(method) + name 416 } 417 418 // ProtoMessage will return a protobuf message declaration 419 // based on the response schema. If the response is an array 420 // type, it will get wrapped in a generic message with a single 421 // 'items' field to contain the array. 422 func (r *Response) ProtoMessage(endpointName string, defs map[string]*Items) string { 423 name := endpointName + "Response" 424 if r.Schema == nil { 425 return "" 426 } 427 switch r.Schema.Type { 428 case "object": 429 return r.Schema.Model.ProtoModel(name, 0, defs) 430 case "array": 431 model := &Model{Properties: map[string]*Items{"items": r.Schema}} 432 return model.ProtoModel(name, 0, defs) 433 default: 434 return "" 435 } 436 } 437 438 func (r *Response) responseName(endpointName string) string { 439 if r.Schema == nil { 440 return "google.protobuf.Empty" 441 } 442 switch r.Schema.Type { 443 case "object", "array": 444 return endpointName + "Response" 445 default: 446 switch r.Schema.Ref { 447 case "": 448 return "google.protobuf.Empty" 449 default: 450 return strings.Title( 451 strings.TrimSuffix( 452 path.Base(r.Schema.Ref), 453 path.Ext(r.Schema.Ref), 454 )) 455 } 456 } 457 } 458 459 func includeBody(parent, child Parameters) string { 460 params := append(parent, child...) 461 for _, param := range params { 462 if param.In == "body" { 463 return param.Name 464 } 465 } 466 return "" 467 } 468 469 var lineStart = regexp.MustCompile(`^`) 470 var newLine = regexp.MustCompile(`\n`) 471 472 func prepComment(comment, space string) string { 473 if comment == "" { 474 return "" 475 } 476 comment = lineStart.ReplaceAllString(comment, space+"// ") 477 comment = newLine.ReplaceAllString(comment, "\n"+space+"// ") 478 comment = strings.TrimRight(comment, "/ ") 479 if !strings.HasSuffix(comment, "\n") { 480 comment += "\n" 481 } 482 return comment 483 } 484 485 func (e *Endpoint) protoEndpoint(annotate bool, parentParams Parameters, base, path, method string) string { 486 reqName := "google.protobuf.Empty" 487 endpointName := PathMethodToName(path, method) 488 path = base + path 489 490 var bodyAttr string 491 if len(parentParams)+len(e.Parameters) > 0 { 492 bodyAttr = includeBody(parentParams, e.Parameters) 493 reqName = endpointName + "Request" 494 } 495 496 respName := "google.protobuf.Empty" 497 if resp, ok := e.Responses["200"]; ok { 498 respName = resp.responseName(endpointName) 499 } else if resp, ok := e.Responses["201"]; ok { 500 respName = resp.responseName(endpointName) 501 } 502 503 comment := e.Summary 504 if comment != "" && e.Description != "" { 505 if !strings.HasSuffix(comment, "\n") { 506 comment += "\n" 507 } 508 comment += "\n" 509 } 510 511 if e.Description != "" { 512 comment += e.Description 513 } 514 515 comment = prepComment(comment, " ") 516 517 tData := struct { 518 Annotate bool 519 Method string 520 Name string 521 RequestName string 522 ResponseName string 523 Path string 524 IncludeBody bool 525 BodyAttr string 526 Comment string 527 HasComment bool 528 }{ 529 annotate, 530 method, 531 endpointName, 532 reqName, 533 respName, 534 path, 535 (bodyAttr != ""), 536 bodyAttr, 537 comment, 538 (comment != ""), 539 } 540 541 var b bytes.Buffer 542 err := protoEndpointTmpl.Execute(&b, tData) 543 if err != nil { 544 log.Fatal("unable to protobuf model: ", err) 545 } 546 return b.String() 547 } 548 549 func (e *Endpoint) protoMessages(parentParams Parameters, endpointName string, defs map[string]*Items) string { 550 var out bytes.Buffer 551 msg := e.Parameters.ProtoMessage(parentParams, endpointName, defs) 552 if msg != "" { 553 out.WriteString(msg + "\n\n") 554 } 555 556 if resp, ok := e.Responses["200"]; ok { 557 msg := resp.ProtoMessage(endpointName, defs) 558 if msg != "" { 559 out.WriteString(msg + "\n\n") 560 } 561 } else if resp, ok := e.Responses["201"]; ok { 562 msg := resp.ProtoMessage(endpointName, defs) 563 if msg != "" { 564 out.WriteString(msg + "\n\n") 565 } 566 } 567 return out.String() 568 } 569 570 // ProtoEndpoints will return any protobuf v3 endpoints for gRPC 571 // service declarations. 572 func (p *Path) ProtoEndpoints(annotate bool, base, path string) string { 573 574 var out bytes.Buffer 575 if p.Get != nil { 576 msg := p.Get.protoEndpoint(annotate, p.Parameters, base, path, "get") 577 out.WriteString(msg + "\n") 578 } 579 if p.Put != nil { 580 msg := p.Put.protoEndpoint(annotate, p.Parameters, base, path, "put") 581 out.WriteString(msg + "\n") 582 } 583 if p.Post != nil { 584 msg := p.Post.protoEndpoint(annotate, p.Parameters, base, path, "post") 585 out.WriteString(msg + "\n") 586 } 587 if p.Delete != nil { 588 msg := p.Delete.protoEndpoint(annotate, p.Parameters, base, path, "delete") 589 out.WriteString(msg + "\n") 590 } 591 592 return strings.TrimSuffix(out.String(), "\n") 593 } 594 595 // ProtoMessages will return protobuf v3 messages that represents 596 // the request Parameters of the endpoints within this path declaration 597 // and any custom response messages not listed in the definitions. 598 func (p *Path) ProtoMessages(path string, defs map[string]*Items) string { 599 var out bytes.Buffer 600 if p.Get != nil { 601 endpointName := PathMethodToName(path, "get") 602 msg := p.Get.protoMessages(p.Parameters, endpointName, defs) 603 if msg != "" { 604 out.WriteString(msg) 605 } 606 } 607 if p.Put != nil { 608 endpointName := PathMethodToName(path, "put") 609 msg := p.Put.protoMessages(p.Parameters, endpointName, defs) 610 if msg != "" { 611 out.WriteString(msg) 612 } 613 } 614 if p.Post != nil { 615 endpointName := PathMethodToName(path, "post") 616 msg := p.Post.protoMessages(p.Parameters, endpointName, defs) 617 if msg != "" { 618 out.WriteString(msg) 619 } 620 } 621 if p.Delete != nil { 622 endpointName := PathMethodToName(path, "delete") 623 msg := p.Delete.protoMessages(p.Parameters, endpointName, defs) 624 if msg != "" { 625 out.WriteString(msg) 626 } 627 } 628 629 return strings.TrimSuffix(out.String(), "\n") 630 } 631 632 func paramsToProps(parent, child Parameters, defs map[string]*Items) map[string]*Items { 633 props := map[string]*Items{} 634 // combine all parameters for endpoint 635 for _, item := range child { 636 props[findRefName(item, defs)] = item 637 } 638 for _, item := range parent { 639 props[findRefName(item, defs)] = item 640 } 641 return props 642 } 643 644 func findRefName(i *Items, defs map[string]*Items) string { 645 if i.Name != "" { 646 return i.Name 647 } 648 649 itemType := strings.TrimPrefix(i.Ref, "#/parameters/") 650 item, ok := defs[itemType] 651 652 if !ok { 653 return path.Base(itemType) 654 } 655 656 return item.Name 657 } 658 659 // ProtoMessage will return a protobuf v3 message that represents 660 // the request Parameters. 661 func (p Parameters) ProtoMessage(parent Parameters, endpointName string, defs map[string]*Items) string { 662 m := &Model{Properties: paramsToProps(parent, p, defs)} 663 664 // do nothing, no props and should be a google.protobuf.Empty 665 if len(m.Properties) == 0 { 666 return "" 667 } 668 669 var b bytes.Buffer 670 m.Name = endpointName + "Request" 671 m.Depth = 0 672 673 s := struct { 674 *Model 675 Defs map[string]*Items 676 }{m, defs} 677 err := protoMsgTmpl.Execute(&b, s) 678 if err != nil { 679 log.Fatal("unable to protobuf parameters: ", err) 680 } 681 return b.String() 682 } 683 684 // ProtoModel will return a protobuf v3 message that represents 685 // the current Model. 686 func (m *Model) ProtoModel(name string, depth int, defs map[string]*Items) string { 687 var b bytes.Buffer 688 m.Name = name 689 m.Depth = depth 690 s := struct { 691 *Model 692 Defs map[string]*Items 693 }{m, defs} 694 err := protoMsgTmpl.Execute(&b, s) 695 if err != nil { 696 log.Fatal("unable to protobuf model: ", err) 697 } 698 return b.String() 699 } 700 701 func format(fmt interface{}) string { 702 format := "" 703 if fmt != nil { 704 format = fmt.(string) 705 } 706 return format 707 708 }