ooni-probe-cli/internal/engine/ooapi/internal/generator/apis.go
Simone Basso 28ce79eff1
feat(ooapi): add toplevel client and simplify API (#248)
* feat(ooapi): add toplevel client and simplify API

This diff should simplify using ooapi from other packages by
adding more abstraction that wraps the existing code.

Part of https://github.com/ooni/probe/issues/1355.

* fix(ooapi): use correct comment for cloners

See https://github.com/ooni/probe-cli/pull/248#discussion_r590663843

* fix(ooapi): make sure the documentation is current

See https://github.com/ooni/probe-cli/pull/248#discussion_r590665773

* fix(ooapi): automate copying APIs

See https://github.com/ooni/probe-cli/pull/248#discussion_r590665837

* feat(ooapi): add unit tests for clientcall.go

See https://github.com/ooni/probe-cli/pull/248#discussion_r590666297

* fix(ooapi): rewrite integration tests to use toplevel API

See https://github.com/ooni/probe-cli/pull/248#discussion_r590665084
2021-03-19 09:30:42 +01:00

180 lines
5.3 KiB
Go

package main
import (
"fmt"
"strings"
"time"
)
// apiField contains the fields of an API data structure
type apiField struct {
// name is the field name
name string
// kind is the filed type
kind string
// comment is a brief comment to document the field
comment string
// ifLogin indicates whether this field should only be
// emitted when the API requires login
ifLogin bool
// ifTemplate indicates whether this field should only be
// emitted when the URL path is a template
ifTemplate bool
// noClone is true when this field should not be copied
// from the parent data structure when cloning
noClone bool
}
var apiFields = []apiField{{
name: "BaseURL",
kind: "string",
comment: "optional",
}, {
name: "HTTPClient",
kind: "HTTPClient",
comment: "optional",
}, {
name: "JSONCodec",
kind: "JSONCodec",
comment: "optional",
}, {
name: "Token",
kind: "string",
comment: "mandatory",
ifLogin: true,
noClone: true,
}, {
name: "RequestMaker",
kind: "RequestMaker",
comment: "optional",
}, {
name: "TemplateExecutor",
kind: "templateExecutor",
comment: "optional",
ifTemplate: true,
}, {
name: "UserAgent",
kind: "string",
comment: "optional",
}}
func (d *Descriptor) genNewAPI(sb *strings.Builder) {
fmt.Fprintf(sb, "// %s implements the %s API.\n", d.APIStructName(), d.Name)
fmt.Fprintf(sb, "type %s struct {\n", d.APIStructName())
for _, f := range apiFields {
if !d.RequiresLogin && f.ifLogin {
continue
}
if !d.URLPath.IsTemplate && f.ifTemplate {
continue
}
fmt.Fprintf(sb, "\t%s %s // %s\n", f.name, f.kind, f.comment)
}
fmt.Fprint(sb, "}\n\n")
if d.RequiresLogin {
fmt.Fprintf(sb, "// WithToken returns a copy of the API where the\n")
fmt.Fprintf(sb, "// value of the Token field is replaced with token.\n")
fmt.Fprintf(sb, "func (api *%s) WithToken(token string) %s {\n",
d.APIStructName(), d.CallerInterfaceName())
fmt.Fprintf(sb, "out := &%s{}\n", d.APIStructName())
for _, f := range apiFields {
if !d.URLPath.IsTemplate && f.ifTemplate {
continue
}
if f.noClone == true {
continue
}
fmt.Fprintf(sb, "out.%s = api.%s\n", f.name, f.name)
}
fmt.Fprint(sb, "out.Token = token\n")
fmt.Fprint(sb, "return out\n")
fmt.Fprint(sb, "}\n\n")
}
fmt.Fprintf(sb, "func (api *%s) baseURL() string {\n", d.APIStructName())
fmt.Fprint(sb, "\tif api.BaseURL != \"\" {\n")
fmt.Fprint(sb, "\t\treturn api.BaseURL\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn \"https://ps1.ooni.io\"\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) requestMaker() RequestMaker {\n", d.APIStructName())
fmt.Fprint(sb, "\tif api.RequestMaker != nil {\n")
fmt.Fprint(sb, "\t\treturn api.RequestMaker\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &defaultRequestMaker{}\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) jsonCodec() JSONCodec {\n", d.APIStructName())
fmt.Fprint(sb, "\tif api.JSONCodec != nil {\n")
fmt.Fprint(sb, "\t\treturn api.JSONCodec\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &defaultJSONCodec{}\n")
fmt.Fprint(sb, "}\n\n")
if d.URLPath.IsTemplate {
fmt.Fprintf(
sb, "func (api *%s) templateExecutor() templateExecutor {\n",
d.APIStructName())
fmt.Fprint(sb, "\tif api.TemplateExecutor != nil {\n")
fmt.Fprint(sb, "\t\treturn api.TemplateExecutor\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &defaultTemplateExecutor{}\n")
fmt.Fprint(sb, "}\n\n")
}
fmt.Fprintf(
sb, "func (api *%s) httpClient() HTTPClient {\n",
d.APIStructName())
fmt.Fprint(sb, "\tif api.HTTPClient != nil {\n")
fmt.Fprint(sb, "\t\treturn api.HTTPClient\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn http.DefaultClient\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "// Call calls the %s API.\n", d.Name)
fmt.Fprintf(
sb, "func (api *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
d.APIStructName(), d.RequestTypeName(), d.ResponseTypeName())
fmt.Fprint(sb, "\thttpReq, err := api.newRequest(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\thttpReq.Header.Add(\"Accept\", \"application/json\")\n")
if d.RequiresLogin {
fmt.Fprint(sb, "\tif api.Token == \"\" {\n")
fmt.Fprint(sb, "\t\treturn nil, ErrMissingToken\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\thttpReq.Header.Add(\"Authorization\", newAuthorizationHeader(api.Token))\n")
}
fmt.Fprint(sb, "\tif api.UserAgent != \"\" {\n")
fmt.Fprint(sb, "\t\thttpReq.Header.Add(\"User-Agent\", api.UserAgent)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn api.newResponse(api.httpClient().Do(httpReq))\n")
fmt.Fprint(sb, "}\n\n")
}
// GenAPIsGo generates apis.go.
func GenAPIsGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"net/http\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/engine/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
desc.genNewAPI(&sb)
}
writefile(file, &sb)
}