github.com/emcfarlane/larking@v0.0.0-20220605172417-1704b45ee6c3/starlib/net/starlarkopenapi/openapi.go (about) 1 // Copyright 2022 Edward McFarlane. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package starlarkopenapi 6 7 // OpenAPI spec: 8 // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#dataTypeType 9 10 import ( 11 "bytes" 12 "context" 13 "encoding/base64" 14 "encoding/json" 15 "fmt" 16 "hash/crc32" 17 "io" 18 "io/ioutil" 19 "mime/multipart" 20 "net/http" 21 "net/url" 22 "path" 23 "sort" 24 "strconv" 25 "strings" 26 "time" 27 "unicode" 28 29 "github.com/emcfarlane/larking/starlib/net/starlarkhttp" 30 "github.com/emcfarlane/larking/starlib/starext" 31 "github.com/emcfarlane/larking/starlib/starlarkstruct" 32 "github.com/emcfarlane/larking/starlib/starlarkthread" 33 "github.com/go-openapi/spec" 34 "github.com/iancoleman/strcase" 35 starlarkjson "go.starlark.net/lib/json" 36 starlarktime "go.starlark.net/lib/time" 37 "go.starlark.net/starlark" 38 "gocloud.dev/runtimevar" 39 ) 40 41 func NewModule() *starlarkstruct.Module { 42 return &starlarkstruct.Module{ 43 Name: "openapi", 44 Members: starlark.StringDict{ 45 "open": starext.MakeBuiltin("openapi.open", Open), 46 }, 47 } 48 } 49 50 type Client struct { 51 // service encoding... 52 name string 53 variable *runtimevar.Variable 54 client *starlarkhttp.Client 55 56 val []byte // snapshot.Value 57 doc *spec.Swagger 58 svcs map[string]*Service //starlark.Value 59 } 60 61 var defaultClient = starlarkhttp.NewClient(http.DefaultClient) 62 63 func Open(thread *starlark.Thread, fnname string, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 64 var ( 65 addr string 66 name string 67 client = defaultClient 68 ) 69 if err := starlark.UnpackArgs(fnname, args, kwargs, "name", &name, "addr?", &addr, "client?", &client); err != nil { 70 return nil, err 71 } 72 73 ctx := starlarkthread.GetContext(thread) 74 75 variable, err := runtimevar.OpenVariable(ctx, name) 76 if err != nil { 77 return nil, err 78 } 79 80 c := &Client{ 81 name: name, 82 variable: variable, 83 client: client, 84 } 85 if _, err := c.load(ctx); err != nil { 86 variable.Close() //nolint 87 return nil, err 88 } 89 if err := starlarkthread.AddResource(thread, c); err != nil { 90 variable.Close() //nolint 91 return nil, err 92 } 93 return c, nil 94 } 95 96 func toSnakeCase(s string) string { 97 s = strings.Map(func(r rune) rune { 98 if unicode.IsLetter(r) || unicode.IsNumber(r) { 99 return r 100 } 101 // ignore variables 102 if r == '{' || r == '}' { 103 return -1 104 } 105 return '_' 106 }, s) 107 s = strcase.ToSnake(s) 108 s = strings.Trim(s, "_") 109 return s 110 } 111 112 func (c *Client) do( 113 thread *starlark.Thread, 114 fnname string, 115 req *starlarkhttp.Request, 116 ) (*starlarkhttp.Response, error) { 117 return c.client.Do(thread, fnname, req) 118 } 119 120 func (c *Client) load(ctx context.Context) (*spec.Swagger, error) { 121 ctx, cancel := context.WithTimeout(ctx, 1*time.Second) 122 defer cancel() 123 124 snap, err := c.variable.Latest(ctx) 125 if err != nil { 126 return nil, err 127 } 128 129 var b []byte 130 switch v := snap.Value.(type) { 131 case []byte: 132 b = v 133 case string: 134 b = []byte(v) 135 default: 136 return nil, fmt.Errorf("unhandled type: %v", v) 137 } 138 139 var doc spec.Swagger 140 if err := json.Unmarshal(b, &doc); err != nil { 141 return nil, err 142 } 143 c.val = b 144 c.doc = &doc 145 146 if err := spec.ExpandSpec(&doc, &spec.ExpandOptions{}); err != nil { 147 return nil, err 148 } 149 150 // build attrs 151 if doc.Paths == nil { 152 return &doc, nil 153 } 154 //attrs := make(map[string]*Service) 155 //attrNames := make([]string, 0, len(doc.Tags)) 156 //tagNames := make(map[string]string) 157 services := make(map[string]*Service) 158 159 for path, item := range doc.Paths.Paths { 160 key := toSnakeCase(path) 161 162 var count int 163 addMethod := func(op *spec.Operation, method string) { 164 count++ 165 var svcNames []string 166 for _, tag := range op.Tags { 167 svcNames = append(svcNames, strcase.ToSnake(tag)) 168 } 169 if len(svcNames) == 0 { 170 svcNames = append(svcNames, key) 171 } 172 173 mdName := strings.ToLower(method) + "_" + key 174 if id := op.ID; id != "" { 175 mdName = strcase.ToSnake(id) 176 } 177 178 m := &Method{ 179 c: c, 180 name: mdName, 181 path: path, 182 op: op, 183 //params: item.Parameters, 184 method: method, 185 } 186 187 for _, svcName := range svcNames { 188 svc, ok := services[svcName] 189 if !ok { 190 svc = &Service{ 191 name: svcName, 192 methods: make(map[string]*Method), 193 } 194 services[svcName] = svc 195 } 196 svc.methods[mdName] = m 197 } 198 } 199 200 if v := item.Get; v != nil { 201 addMethod(v, http.MethodGet) 202 } 203 if v := item.Put; v != nil { 204 addMethod(v, http.MethodPut) 205 } 206 if v := item.Post; v != nil { 207 addMethod(v, http.MethodPost) 208 } 209 if v := item.Delete; v != nil { 210 addMethod(v, http.MethodDelete) 211 } 212 if v := item.Options; v != nil { 213 addMethod(v, http.MethodOptions) 214 } 215 if v := item.Head; v != nil { 216 addMethod(v, http.MethodHead) 217 } 218 if v := item.Patch; v != nil { 219 addMethod(v, http.MethodPatch) 220 } 221 222 if count == 0 { 223 return nil, fmt.Errorf("missing operations for path: %s", path) 224 } 225 } 226 227 c.svcs = services 228 return &doc, nil 229 } 230 231 func (c *Client) makeURL(urlPath string, urlQuery url.Values) url.URL { 232 scheme := "http" 233 if x := c.doc.Schemes; len(x) > 0 { 234 scheme = x[0] 235 } 236 return url.URL{ 237 Scheme: scheme, 238 Host: c.doc.Host, 239 Path: path.Join(c.doc.BasePath, urlPath), 240 RawQuery: urlQuery.Encode(), 241 } 242 } 243 244 func (c *Client) String() string { return fmt.Sprintf("<client %q>", c.name) } 245 func (c *Client) Type() string { return "openapi.client" } 246 func (c *Client) Freeze() {} // immutable? 247 func (c *Client) Truth() starlark.Bool { return c.variable.CheckHealth() == nil } 248 func (c *Client) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable type: %s", c.Type()) } 249 func (c *Client) Close() error { 250 return c.variable.Close() 251 } 252 253 func (c *Client) Attr(name string) (starlark.Value, error) { 254 if s, ok := c.svcs[name]; ok { 255 return s, nil 256 } 257 if name == "schema" { 258 return starlark.String(string(c.val)), nil 259 } 260 return nil, nil 261 } 262 func (c *Client) AttrNames() []string { 263 names := make([]string, 0, len(c.svcs)) 264 for name := range c.svcs { 265 names = append(names, name) 266 } 267 sort.Strings(names) 268 return names 269 } 270 271 type Service struct { 272 name string 273 methods map[string]*Method 274 } 275 276 func (s *Service) String() string { return fmt.Sprintf("<service %q>", s.name) } 277 func (s *Service) Type() string { return "openapi.service" } 278 func (s *Service) Freeze() {} // immutable? 279 func (s *Service) Truth() starlark.Bool { return s.name != "" } 280 func (s *Service) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable type: %s", s.Type()) } 281 func (s *Service) Attr(name string) (starlark.Value, error) { 282 if m, ok := s.methods[name]; ok { 283 return m, nil 284 } 285 return nil, nil 286 } 287 func (s *Service) AttrNames() []string { 288 names := make([]string, 0, len(s.methods)) 289 for name := range s.methods { 290 names = append(names, name) 291 } 292 sort.Strings(names) 293 return names 294 } 295 296 type Method struct { 297 c *Client 298 299 name string 300 path string 301 op *spec.Operation 302 //params []spec.Parameter 303 method string 304 } 305 306 func (m *Method) String() string { return fmt.Sprintf("<method %q>", m.name) } 307 func (m *Method) Type() string { return "openapi.method" } 308 func (m *Method) Freeze() {} // immutable? 309 func (m *Method) Truth() starlark.Bool { return m.name != "" } 310 func (m *Method) Hash() (uint32, error) { return starlark.String(m.path).Hash() } 311 312 var ( 313 starlarkJSONEncode = starlarkjson.Module.Members["encode"].(*starlark.Builtin) 314 starlarkJSONDecode = starlarkjson.Module.Members["decode"].(*starlark.Builtin) 315 ) 316 317 func (m *Method) Name() string { return m.name } 318 func (m *Method) CallInternal(thread *starlark.Thread, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 319 ctx := starlarkthread.GetContext(thread) 320 hasArgs := len(args) > 0 321 //hasKwargs := len(kwargs) > 0 322 323 if hasArgs { 324 return nil, fmt.Errorf("unexpected args") 325 } 326 327 var ( 328 params = m.op.Parameters 329 vals = make([]interface{}, 0, len(params)) 330 pairsRequired []interface{} 331 pairsOptional []interface{} 332 ) 333 for i, param := range params { 334 kw := param.Name 335 switch typ := param.Type; typ { 336 case "array": 337 vals = append(vals, (*starlark.List)(nil)) 338 case "string": 339 vals = append(vals, "") 340 case "integer": 341 vals = append(vals, (int)(0)) 342 case "number": 343 vals = append(vals, (float64)(0)) 344 case "boolean": 345 vals = append(vals, (bool)(false)) 346 case "file": 347 // Tuple of (filename, source) where source 348 // accepts String, Bytes, Reader. 349 // content-type must be form data. 350 vals = append(vals, starlark.Value(nil)) // starlark.Tuple(nil)) 351 default: 352 if param.Schema == nil { 353 return nil, fmt.Errorf("unknown type: %s", typ) 354 } 355 // ??? 356 vals = append(vals, (*starlark.Value)(nil)) 357 } 358 if param.Required { 359 pairsRequired = append(pairsRequired, kw, &vals[i]) 360 } else { 361 pairsOptional = append(pairsOptional, kw+"?", &vals[i]) 362 } 363 } 364 365 pairs := append(pairsRequired, pairsOptional...) 366 if err := starlark.UnpackArgs(m.name, args, kwargs, pairs...); err != nil { 367 return nil, err 368 } 369 370 chooseType := func(typs []string) string { 371 var typ string 372 if n := len(typs); n > 0 { 373 typ = typs[0] 374 } else if n > 1 { 375 for _, altTyp := range typs[1:] { 376 if altTyp == "application/json" { 377 typ = "application/json" 378 } 379 } 380 } 381 return typ 382 } 383 384 var ( 385 urlPath = m.path 386 urlVals = make(url.Values) 387 headers = make(http.Header) 388 body io.Reader 389 formWriter *multipart.Writer 390 consumesType = chooseType(m.op.Consumes) 391 producesType = chooseType(m.op.Produces) 392 ) 393 394 headers.Set("Content-Type", consumesType) 395 headers.Set("Accepts", producesType) 396 397 for i, param := range params { 398 arg := vals[i] 399 if arg == nil { 400 continue // optional? 401 } 402 403 switch v := param.In; v { 404 case "body": 405 // create JSON? 406 switch typ := consumesType; typ { 407 case "application/json": 408 v, ok := arg.(starlark.Value) 409 if !ok { 410 return nil, fmt.Errorf("unknown body arg: %T %v", arg, arg) 411 } 412 rsp, err := starlark.Call( 413 thread, starlarkJSONEncode, starlark.Tuple{v}, nil, 414 ) 415 if err != nil { 416 return nil, err 417 } 418 body = strings.NewReader( 419 string(rsp.(starlark.String)), 420 ) 421 422 default: 423 return nil, fmt.Errorf("unknown consume type: %s", typ) 424 } 425 426 case "path": 427 key := "{" + param.Name + "}" 428 val := vals[i] 429 if i := strings.Index(urlPath, key); i == -1 { 430 return nil, fmt.Errorf("missing path variable: %s", key) 431 } else { 432 urlPath = fmt.Sprintf( 433 "%s%v%s", urlPath[:i], val, urlPath[i+len(key):], 434 ) 435 } 436 437 case "query": 438 switch v := arg.(type) { 439 case string: 440 urlVals.Set(param.Name, v) 441 case int: 442 urlVals.Set(param.Name, strconv.Itoa(v)) 443 case bool: 444 if v { 445 urlVals.Set(param.Name, "true") 446 } 447 case *starlark.List: 448 for i := 0; i < v.Len(); i++ { 449 switch v := v.Index(i).(type) { 450 case starlark.String: 451 urlVals.Set(param.Name, string(v)) 452 case starlark.Int: 453 x, _ := v.Int64() 454 urlVals.Set(param.Name, strconv.Itoa(int(x))) 455 case starlark.Bool: 456 if bool(v) { 457 urlVals.Set(param.Name, "true") 458 } 459 default: 460 return nil, fmt.Errorf("invalid param list type: %T %v", v, v) 461 } 462 } 463 default: 464 return nil, fmt.Errorf("unknown param type: %T %v", v, v) 465 } 466 467 case "header": 468 switch v := arg.(type) { 469 case string: 470 headers.Add(param.Name, v) 471 case int: 472 headers.Add(param.Name, strconv.Itoa(v)) 473 case bool: 474 if v { 475 headers.Add(param.Name, "true") 476 } 477 default: 478 return nil, fmt.Errorf("unknown header type: %T %v", v, v) 479 } 480 481 case "formData": 482 switch consumesType { 483 case "multipart/form-data": 484 if body == nil { 485 buf := new(bytes.Buffer) 486 formWriter = multipart.NewWriter(buf) 487 // TODO: check this is okay. 488 x := crc32.ChecksumIEEE([]byte(m.path)) 489 if err := formWriter.SetBoundary( 490 fmt.Sprintf("%x%x%x", x, x, x), 491 ); err != nil { 492 return nil, err 493 } 494 body = buf 495 } 496 497 switch param.Type { 498 case "file": 499 val, ok := arg.(starlark.Tuple) 500 if !ok || len(val) != 2 { 501 // TODO: better typed errors. 502 return nil, fmt.Errorf("expected tuple(filename, source) got %v", arg) 503 } 504 505 filename, ok := starlark.AsString(val[0]) 506 if !ok { 507 return nil, fmt.Errorf("filename must be a string, got %v", val[0]) 508 } 509 510 fw, err := formWriter.CreateFormFile(param.Name, filename) 511 if err != nil { 512 return nil, err 513 } 514 515 var r io.Reader 516 switch v := val[1].(type) { 517 case starlark.String: 518 r = strings.NewReader(string(v)) 519 case starlark.Bytes: 520 r = strings.NewReader(string(v)) 521 case io.Reader: 522 r = v 523 default: 524 return nil, fmt.Errorf("unknown form type: %T %v", v, v) 525 } 526 if _, err := io.Copy(fw, r); err != nil { 527 return nil, err 528 } 529 default: 530 // TODO: type handling 531 s := fmt.Sprintf("%v", arg) 532 if err := formWriter.WriteField(param.Name, s); err != nil { 533 return nil, err 534 } 535 } 536 537 case "application/x-www-form-urlencoded": 538 return nil, fmt.Errorf("unimplemented consume type: %s", consumesType) 539 540 default: 541 return nil, fmt.Errorf("unexpected consumes type %v for \"formData\"", consumesType) 542 } 543 544 default: 545 return nil, fmt.Errorf("unhandled parameter in: %s", v) 546 } 547 } 548 if formWriter != nil { 549 if err := formWriter.Close(); err != nil { 550 return nil, err 551 } 552 headers.Set("Content-Type", formWriter.FormDataContentType()) 553 } 554 555 u := m.c.makeURL(urlPath, urlVals) 556 557 urlStr := u.String() 558 559 req, err := http.NewRequestWithContext(ctx, m.method, urlStr, body) 560 if err != nil { 561 return nil, err 562 } 563 req.Header = headers 564 565 rsp, err := m.c.do(thread, m.name, &starlarkhttp.Request{ 566 Request: req, 567 }) 568 if err != nil { 569 return nil, err 570 } 571 defer rsp.Body.Close() 572 573 rspTyp, rspOk := m.op.Responses.StatusCodeResponses[rsp.StatusCode] 574 rspDef := m.op.Responses.Default 575 576 // Produce struct or array 577 switch typ := producesType; typ { 578 case "application/json": 579 rspBody, err := ioutil.ReadAll(rsp.Body) 580 if err != nil { 581 return nil, err 582 } 583 584 bodyStr := starlark.String(rspBody) 585 586 // Load schema 587 val, err := starlark.Call( 588 thread, starlarkJSONDecode, starlark.Tuple{bodyStr}, nil, 589 ) 590 if err != nil { 591 return nil, err 592 } 593 594 if rsp.StatusCode/100 != 2 { 595 return nil, fmt.Errorf("%s: %q", rsp.Status, val) 596 } 597 // Try to return a typed response. 598 if rspOk { 599 return toStruct(rspTyp.Schema, val) 600 } 601 if rspDef != nil { 602 return toStruct(rspDef.Schema, val) 603 } 604 // TODO: convert anyway? 605 return val, nil 606 607 default: 608 return nil, fmt.Errorf("%s: unknown produces type: %s", rsp.Status, typ) 609 } 610 } 611 612 func errKeyValue(schema *spec.Schema, want string, v starlark.Value) error { 613 return fmt.Errorf("invalid type for %s, want %s got %s", schema.ID, want, v.Type()) 614 } 615 616 func typeStr(schema *spec.Schema) string { 617 return strings.Join([]string(schema.Type), ",") 618 } 619 620 // TODO: build typed Dict and typed Lists. 621 func toStruct(schema *spec.Schema, v starlark.Value) (starlark.Value, error) { 622 623 switch v := v.(type) { 624 case *starlark.Dict: 625 if typ := typeStr(schema); typ != "object" { 626 return nil, errKeyValue(schema, "dict", v) 627 } 628 // TODO: typed structs? 629 //constructor := starlark.String(schema.ID) 630 constructor := starlarkstruct.Default 631 kwargs := v.Items() 632 633 // TODO: validate spec. 634 for _, kwarg := range kwargs { 635 k, ok := starlark.AsString(kwarg[0]) 636 if !ok { 637 return nil, fmt.Errorf("invalid key %s", k) 638 } 639 v := kwarg[1] 640 641 keySchema, ok := schema.Properties[k] 642 if !ok { 643 return nil, fmt.Errorf("unpexpected key %s", k) 644 } 645 646 x, err := toStruct(&keySchema, v) 647 if err != nil { 648 return nil, err 649 } 650 kwarg[1] = x 651 } 652 653 s := starlarkstruct.FromKeywords(constructor, kwargs) 654 return s, nil 655 656 case *starlark.List: 657 if typeStr(schema) != "array" { 658 return nil, errKeyValue(schema, "list", v) 659 } 660 if items := schema.Items; items == nil || items.Schema == nil { 661 return nil, fmt.Errorf("unepected items schema: %v", items) 662 } 663 keySchema := schema.Items.Schema 664 665 // TODO: validate spec. 666 for i := 0; i < v.Len(); i++ { 667 x, err := toStruct(keySchema, v.Index(i)) 668 if err != nil { 669 return nil, err 670 } 671 if err := v.SetIndex(i, x); err != nil { 672 return nil, err 673 } 674 } 675 return v, nil 676 677 case starlark.String: 678 switch typeStr(schema) { 679 case "string", "password": 680 return v, nil 681 case "byte", "binary": 682 data, err := base64.StdEncoding.DecodeString(string(v)) 683 if err != nil { 684 return nil, err 685 } 686 return starlark.Bytes(string(data)), nil 687 case "date": 688 t, err := time.Parse("2006-Jan-02", string(v)) 689 if err != nil { 690 return nil, err 691 } 692 return starlarktime.Time(t), nil 693 case "date-time": 694 t, err := time.Parse(time.RFC3339, string(v)) 695 if err != nil { 696 return nil, err 697 } 698 return starlarktime.Time(t), nil 699 default: 700 return v, nil // TODO: warn? 701 } 702 703 case starlark.Int: 704 if typeStr(schema) != "integer" { 705 return nil, errKeyValue(schema, "int", v) 706 } 707 return v, nil 708 709 case starlark.Float: 710 if typeStr(schema) != "number" { 711 return nil, errKeyValue(schema, "float", v) 712 } 713 return v, nil 714 715 case starlark.Bool: 716 if typeStr(schema) != "boolean" { 717 return nil, errKeyValue(schema, "bool", v) 718 } 719 return v, nil 720 721 default: 722 // TODO: validate spec? 723 return v, nil 724 } 725 } 726 727 func NewMessage(schema *spec.Schema, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 728 hasArgs := len(args) > 0 729 hasKwargs := len(kwargs) > 0 730 731 if hasArgs && len(args) > 1 { 732 return nil, fmt.Errorf("unexpected number of args") 733 } 734 735 if hasArgs && hasKwargs { 736 return nil, fmt.Errorf("unxpected args and kwargs") 737 } 738 739 if hasArgs { 740 return toStruct(schema, args[0]) 741 } 742 743 return nil, fmt.Errorf("TODO: kwargs") 744 }