* 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
180 lines
5.3 KiB
Go
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)
|
|
}
|