181 lines
4.5 KiB
Go
181 lines
4.5 KiB
Go
// Package builder implements google.api.http-style request building (gRPC JSON transcoding)
|
|
// for HTTP requests, closely following the google.api.http spec.
|
|
// See full spec for details: https://github.com/googleapis/googleapis/blob/master/google/api/http.proto
|
|
package builder
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/reflect/protoreflect"
|
|
)
|
|
|
|
type RequestBuilder struct {
|
|
path string // e.g. "/v1/{name=projects/*/topics/*}:publish" or "/users/{user.id}"
|
|
method string // GET, POST, PATCH, etc. (not used in mapping rules, but convenient for callers)
|
|
bodyOption bodyOption // "", "*", or top-level field name
|
|
msg proto.Message // request struct
|
|
}
|
|
|
|
func NewRequestBuilder(
|
|
path string,
|
|
method string,
|
|
bodyOpt string,
|
|
msg proto.Message,
|
|
) (
|
|
*RequestBuilder,
|
|
error,
|
|
) {
|
|
rb := &RequestBuilder{
|
|
path: path,
|
|
method: method,
|
|
bodyOption: bodyOption(bodyOpt),
|
|
msg: msg,
|
|
}
|
|
|
|
if err := rb.validate(); err != nil {
|
|
return nil, fmt.Errorf("validate: %w", err)
|
|
}
|
|
|
|
return rb, nil
|
|
}
|
|
|
|
// Build applies mapping rules and returns:
|
|
//
|
|
// resolvedPath — path with placeholders substituted and query appended
|
|
// newMsg — same concrete type as input, filtered to contain only the body fields
|
|
// err — if mapping/validation failed
|
|
func (b *RequestBuilder) Build() (resolvedPath string, newMsg proto.Message, err error) {
|
|
tmpl, isCached := getCachedPathTemplate(b.path)
|
|
if !isCached {
|
|
tmpl, err = parsePathTemplate(b.path)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("parse path template: %w", err)
|
|
}
|
|
setPathTemplateCache(b.path, tmpl)
|
|
}
|
|
|
|
var usedFieldsPath *usedFields
|
|
resolvedPath, usedFieldsPath, err = resolvePathPlaceholders(tmpl, b.msg)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("resolve path placeholders: %w", err)
|
|
}
|
|
|
|
// if all set fields are already used in path, no need to process query/body
|
|
if allFieldsUsed(b.msg, usedFieldsPath) {
|
|
return resolvedPath, initZeroMsg(b.msg), nil
|
|
}
|
|
|
|
switch {
|
|
case b.bodyOption.isWithoutBody():
|
|
var query url.Values
|
|
query, err = buildQuery(b.msg, usedFieldsPath, "")
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("build query: %w", err)
|
|
}
|
|
|
|
return resolvedPath + encodeQuery(query), initZeroMsg(b.msg), nil
|
|
|
|
case b.bodyOption.isSingleField():
|
|
fieldBody := b.bodyOption.String()
|
|
|
|
newMsg, err = buildSingleFieldBody(b.msg, fieldBody)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("build single field body: %w", err)
|
|
}
|
|
|
|
var query url.Values
|
|
query, err = buildQuery(b.msg, usedFieldsPath, fieldBody)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("build query: %w", err)
|
|
}
|
|
|
|
return resolvedPath + encodeQuery(query), newMsg, nil
|
|
|
|
case b.bodyOption.isFullBody():
|
|
newMsg, err = buildFullBody(b.msg, usedFieldsPath)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("build full body: %w", err)
|
|
}
|
|
|
|
return resolvedPath, newMsg, nil
|
|
|
|
default:
|
|
return "", nil, fmt.Errorf("unsupported body option %s", b.bodyOption.String())
|
|
}
|
|
}
|
|
|
|
func (b *RequestBuilder) validate() error {
|
|
if b.path == "" {
|
|
return errors.New("path is empty")
|
|
}
|
|
if err := validateHTTPMethod(b.method); err != nil {
|
|
return fmt.Errorf("validate http method: %w", err)
|
|
}
|
|
if err := validateHTTPMethodAndBody(b.method, b.bodyOption); err != nil {
|
|
return fmt.Errorf("validate http method and body: %w", err)
|
|
}
|
|
if b.msg == nil {
|
|
return errors.New("msg is nil")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateHTTPMethod(method string) error {
|
|
switch strings.ToUpper(method) {
|
|
case http.MethodGet,
|
|
http.MethodHead,
|
|
http.MethodPost,
|
|
http.MethodPut,
|
|
http.MethodPatch,
|
|
http.MethodDelete,
|
|
http.MethodConnect,
|
|
http.MethodOptions,
|
|
http.MethodTrace:
|
|
return nil
|
|
default:
|
|
return errors.New("invalid http method")
|
|
}
|
|
}
|
|
|
|
func validateHTTPMethodAndBody(method string, bodyOpt bodyOption) error {
|
|
switch method {
|
|
case http.MethodGet, http.MethodDelete, http.MethodHead, http.MethodOptions:
|
|
if !bodyOpt.isWithoutBody() {
|
|
return fmt.Errorf("%s method must not have a body", method)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func allFieldsUsed(msg proto.Message, used *usedFields) bool {
|
|
if used.len() == 0 {
|
|
return false
|
|
}
|
|
count := 0
|
|
msg.ProtoReflect().Range(func(protoreflect.FieldDescriptor, protoreflect.Value) bool {
|
|
count++
|
|
return true
|
|
})
|
|
return used.len() == count
|
|
}
|
|
|
|
func encodeQuery(query url.Values) string {
|
|
if len(query) == 0 {
|
|
return ""
|
|
}
|
|
enc := query.Encode()
|
|
if enc == "" {
|
|
return ""
|
|
}
|
|
return "?" + enc
|
|
}
|
|
|
|
func initZeroMsg(msg proto.Message) proto.Message {
|
|
return msg.ProtoReflect().New().Interface()
|
|
}
|