Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RequestGen

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

 RequestGen

Request builder generator with Go generate.

Building Request Builder without Pain.

Avatar for Yo-An Lin

Yo-An Lin

March 29, 2022
Tweet

More Decks by Yo-An Lin

Other Decks in Programming

Transcript

  1. What’s request builder? • A request builder helps you build

    up the HTTP request object for the API. • A request builder contains a set of methods that help you set the parameters.
  2. What’s request builder generator • A request builder generator helps

    you generate the request builder • With a simple schema or con fi guration. • DRY (Do not repeat yourself)
  3. Why write a request builder generator • Di ff erent

    Authentication Methods (Some are very complicated) • Response structure could be di ff erent. • Some exchanges return paginated results.
  4. //go:generate requestgen —type PlaceOrderRequest type PlaceOrderRequest struct { client requestgen.APIClient

    symbol string `param:"symbol,required"` } Add the go generate command with your type name
  5. //go:generate requestgen —type PlaceOrderRequest type PlaceOrderRequest struct { client requestgen.APIClient

    symbol string `param:"symbol,required"` } Equivalent to this command curl -X POST -F symbol=BTCUSDT https://…….
  6. // APIClient defines the request builder method and request method

    for the API service type APIClient interface { // NewRequest builds up the http request for public endpoints NewRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) // SendRequest sends the request object to the api gateway SendRequest(req *http.Request) (*Response, error) } Simply implement the following interface to support HTTP request:
  7. ctx := context.Background() client := NewClient() req := PlaceOrderRequest{client: client}

    req.Symbol(“BTCUSDT"). ClientOrderID(“0aa1a0123"). Side(SideTypeBuy). OrdType(OrderTypeLimit) Assign values
  8. ctx := context.Background() client := NewClient() req := PlaceOrderRequest{client: client}

    req.Symbol(“BTCUSDT"). ClientOrderID(“0aa1a0123"). Side(SideTypeBuy). OrdType(OrderTypeLimit) response, err := req.Do(ctx) if err != nil { // ... } Send request
  9. * Pointer fi eld = optional fi eld type PlaceOrderRequest

    struct { client requestgen.APIClient tag *string `param:"tag"` }
  10. type PlaceOrderRequest struct { client requestgen.APIClient // "buy" or "sell"

    side SideType `param:"side,required" validValues:"buy,sell"` } Simple Value Validator For String type and Integer type
  11. type PlaceOrderRequest struct { client requestgen.APIClient // "buy" or "sell"

    side SideType `param:"side,required" validValues:"buy,sell"` } User-de fi ned types are supported type SideType string const ( SideTypeBuy SideType = "buy" SideTypeSell SideType = "sell" )
  12. type PlaceOrderRequest struct { client requestgen.APIClient startTime *time.Time `param:"startTime,milliseconds" defaultValuer:"now()"`

    } Millisecond timestamp conversion curl -X POST -F startTime=1648467987762 https://…….
  13. type PlaceOrderRequest struct { client requestgen.APIClient // page defines the

    query parameters for something like '?page=123' page *int64 `param:"page,query"` } Query parameter in the POST request
  14. //go:generate requestgen -type PlaceOrderRequest -responseType .Response -responseDataField Data -responseDataType .Order

    type Response struct { Code string `json:"code"` Message string `json:"msg"` CurrentPage int `json:"currentPage"` PageSize int `json:"pageSize"` TotalNum int `json:"totalNum"` TotalPage int `json:"totalPage"` Data json.RawMessage `json:"data"` } Response Type
  15. //go:generate requestgen -type PlaceOrderRequest -responseType .Response -responseDataField Data -responseDataType .Order

    type Response struct { Code string `json:"code"` Message string `json:"msg"` CurrentPage int `json:"currentPage"` PageSize int `json:"pageSize"` TotalNum int `json:"totalNum"` TotalPage int `json:"totalPage"` Data json.RawMessage `json:"data"` } Response Data Field Response Data Field
  16. //go:generate requestgen -type PlaceOrderRequest -responseType .Response -responseDataField Data -responseDataType .Order

    type Response struct { Code string `json:"code"` Message string `json:"msg"` CurrentPage int `json:"currentPage"` PageSize int `json:"pageSize"` TotalNum int `json:"totalNum"` TotalPage int `json:"totalPage"` Data json.RawMessage `json:"data"` } Parsed into the Order struct
  17. var apiResponse APIResponse if err := response.DecodeJSON(&apiResponse); err != nil

    { return nil, err } var data OrderResponse if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } return &data, nil The generated code Response Wrapper Type
  18. var apiResponse APIResponse if err := response.DecodeJSON(&apiResponse); err != nil

    { return nil, err } var data OrderResponse if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } return &data, nil The generated code Data Type
  19. var apiResponse APIResponse if err := response.DecodeJSON(&apiResponse); err != nil

    { return nil, err } var data OrderResponse if err := json.Unmarshal(apiResponse.Data, &data); err != nil { return nil, err } return &data, nil The generated code Data Field
  20. go:generate requestgen -method POST -type PlaceOrderRequest go:generate requestgen -method GET

    -type PlaceOrderRequest go:generate requestgen -method PUT -type PlaceOrderRequest go:generate requestgen -method DELETE -type PlaceOrderRequest HTTP Method
  21. //go:generate -command GetRequest requestgen -method GET //go:generate -command PostRequest requestgen

    -method POST //go:generate GetRequest -type ListSymbolsRequest -url "/api/v1/symbols" -responseDataType []Symbol Go Generate Alias
  22. Step 1 parse package fi les package main func parsePackage(patterns

    []string, tags []string) { cfg := &packages.Config{ Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedTypes | packages.NeedTypesSizes | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedSyntax | packages.NeedTypesInfo, Tests: false, BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))}, } pkgs, err := packages.Load(cfg, patterns...) if err != nil { log.Fatal(err) } }
  23. Step 2 traverse the syntax tree for _, pkg :=

    range pkgs { for i, file := range pkg.Syntax { ast.Inspect(file.file, func(node ast.Node) bool { switch decl := node.(type) { case *ast.ImportSpec: case *ast.FuncDecl: // .... } return true } } } USVFJUFSBUFJOUPUIFOPEF CMPDL TUBUFNFOU GVODUJPOʜFUD UIFOPEFUZQFUIBUZPVXBOUUPJOTQFDU
  24. type ( // An ImportSpec node represents a single package

    import. ImportSpec struct { Doc *CommentGroup // associated documentation; or nil Name *Ident // local package name (including "."); or nil Path *BasicLit // import path Comment *CommentGroup // line comments; or nil EndPos token.Pos // end of spec (overrides Path.Pos if nonzero) } // A ValueSpec node represents a constant or variable declaration // (ConstSpec or VarSpec production). // ValueSpec struct { Doc *CommentGroup // associated documentation; or nil Names []*Ident // value names (len(Names) > 0) Type Expr // value type; or nil Values []Expr // initial values; or nil Comment *CommentGroup // line comments; or nil } ) import ( "golang.org/x/tools/go/packages" ) a := 1 + 2
  25. Step 3 Resolve the AST node to Type Info switch

    decl := node.(type) { case *ast.FuncDecl: // get the first receiver node receiver := decl.Recv.List[0] // pass the receiver type token to the type info map receiverTypeAndValue, ok := pkg.TypesInfo.Types[receiver.Type] if !ok { return true } // inspect the REAL type switch v := recvTV.Type.(type) { case *types.Named: // type A int case *types.Pointer: // *int } } 5ZQF"OE7BMVFDPOUBJOTUIFUZQFJOGPSNBUJPO VTFUIFUZQFJOGPSNBUJPOUPDIFDL
  26. Step 4 generate the code func renderCode() { var sliceCallbackTmpl

    = template.New("slice-callbacks").Funcs(funcMap) sliceCallbackTmpl = template.Must(sliceCallbackTmpl.Parse(` func ( {{- .RecvName }} *{{ .Field.StructName -}} ) On{{- .Field.EventName -}} (cb {{ .Field.CallbackTypeName .Qualifier -}} ) {{ .RecvName }}.{{ .Field.FieldName }} = append({{- .RecvName }}.{{ .Field.FieldName }}, cb) } func ( {{- .RecvName }} *{{ .Field.StructName -}} ) Emit{{- .Field.EventName -}} {{ .Field.CallbackParamsTuple | typeString }} for _, cb := range {{ .RecvName }}.{{ .Field.FieldName }} { cb({{ .Field.CallbackParamsVarNames | join ", " }}) } } `) sliceCallbackTmpl.Execute(&buf, TemplateArgs{ Field: field, RecvName: recvName, Qualifier: qf, GenerateRemoveMethod: *generateRemoveMethod, }) }
  27. Step 5 reformat the code import ( "go/format" ) src,

    err := format.Source(buf.Bytes()) if err != nil { // Should never happen, but can arise when developing this code. // The user can compile the output to see the error. log.Printf("warning: internal error: invalid Go generated: %s", err) log.Printf("warning: compile the package to analyze the error") return buf.Bytes() } return src