mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
The address is a URL path, so a value substituted into it has to be escaped as a single segment. A trace id of t1/2 went in raw and read as two, so a service that forwards what AddressOf answers asked its runtime for a route nobody registered: the request arrived at the right operation and the forward went somewhere else. A space did the same thing more quietly. Co-authored-by: Hanzo Dev <[email protected]>
151 lines
5.7 KiB
Go
151 lines
5.7 KiB
Go
package zip
|
|
|
|
import (
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// A ROUTE'S ADDRESS IS A VALUE, AND ITS INPUT ALREADY CARRIES IT.
|
|
//
|
|
// [registerTyped] binds the segments the router matched onto the op's In —
|
|
// /traces/:traceId puts the matched segment in the field that names itself
|
|
// traceId (see bindURL, urlFieldName). So by the time a handler runs, the In IS
|
|
// the address's parameters, decoded and named.
|
|
//
|
|
// [Address] is the inverse of that binding, and it exists because the caller
|
|
// that has to reconstruct the concrete path — a relay that hands the call on to
|
|
// something that speaks HTTP — otherwise spells the address a SECOND time:
|
|
//
|
|
// opGet(g, "/traces/:traceId", traceSpans) // once, at the route
|
|
// relay(ctx, "GET", root+"/traces/"+in.TraceID, …) // and again, by hand
|
|
//
|
|
// Two spellings of one address drift, and the drift is invisible: the hand-built
|
|
// path is what gets requested, so a route pattern that no longer agrees with it
|
|
// is never consulted and never contradicted. Reading the parameters back out of
|
|
// the In through the SAME rule that put them there closes that by construction —
|
|
// there is one spelling, the one the route was registered with.
|
|
//
|
|
// It is deliberately the inverse of bindURL and not a new rule: same field walk
|
|
// (wireFields, so promoted embedded fields count), same naming (urlFieldName, so
|
|
// `url:` beats `json:`), same case-insensitive match. A parameter the input does
|
|
// not name renders empty rather than panicking, which is what bindURL does with
|
|
// the mirror-image miss.
|
|
//
|
|
// pattern is spelled the way the ROUTER spells it — ":name", optionally
|
|
// constrained (":project<guid>") or optional (":id?") — because the router's
|
|
// spelling is what a registration states.
|
|
func Address(pattern string, in any) string {
|
|
if !strings.Contains(pattern, ":") {
|
|
return pattern
|
|
}
|
|
values := addressValues(in)
|
|
segments := strings.Split(pattern, "/")
|
|
for i, segment := range segments {
|
|
if len(segment) < 2 || segment[0] != ':' {
|
|
continue
|
|
}
|
|
// ESCAPED, because the result is a URL PATH and a parameter's value is
|
|
// ONE segment of it. A trace id of "t1/2" substituted raw read as two
|
|
// segments, so a relay forwarding this address asked its runtime for a
|
|
// route nobody registered — the request arrived correctly and the
|
|
// forward went elsewhere.
|
|
segments[i] = url.PathEscape(values[strings.ToLower(paramName(segment))])
|
|
}
|
|
return strings.Join(segments, "/")
|
|
}
|
|
|
|
// Template is pattern in the spelling a DOCUMENT writes — ":traceId" becomes
|
|
// "{traceId}" — which is the address's public name: the OpenAPI path, the string
|
|
// a reference page prints and the key a caller matches an address on.
|
|
//
|
|
// A constraint and an optional marker are the ROUTER's business (":project<guid>",
|
|
// ":id?") and do not survive: they say how a segment is matched, not what it is
|
|
// called, and a document that published them would name a parameter no client
|
|
// ever writes.
|
|
//
|
|
// A WILDCARD IS A SEGMENT TOO, and it becomes "{wildcardN}", numbered in path
|
|
// order. The router's "*" and "+" match greedily where a document's parameter
|
|
// matches one segment, so the name is the closest thing a document can say — but
|
|
// it is not a new judgement here: [ID] already spells a wildcard "wildcardN" and
|
|
// numbers it the same way, so the two agree by construction and an operation id
|
|
// derived from the router's spelling equals one derived from the document's.
|
|
//
|
|
// Returning "*" verbatim, as this did, published a path no document template can
|
|
// mean, and left a caller that reads a route table and a caller that reads the
|
|
// registry disagreeing about the SAME address — which resolves as "no such
|
|
// operation" rather than as a difference of spelling.
|
|
func Template(pattern string) string {
|
|
if !strings.ContainsAny(pattern, ":*+") {
|
|
return pattern
|
|
}
|
|
segments := strings.Split(pattern, "/")
|
|
stars := 0
|
|
for i, segment := range segments {
|
|
switch {
|
|
case segment == "*" || segment == "+":
|
|
stars++
|
|
segments[i] = "{wildcard" + strconv.Itoa(stars) + "}"
|
|
case len(segment) >= 2 && segment[0] == ':':
|
|
segments[i] = "{" + paramName(segment) + "}"
|
|
}
|
|
}
|
|
return strings.Join(segments, "/")
|
|
}
|
|
|
|
// paramName is the name a router segment declares, without the constraint or the
|
|
// optional marker the router reads it with.
|
|
func paramName(segment string) string {
|
|
name := segment[1:]
|
|
if cut := strings.IndexAny(name, "<?"); cut >= 0 {
|
|
name = name[:cut]
|
|
}
|
|
return name
|
|
}
|
|
|
|
// addressValues renders every URL-carried field of in as the string the URL
|
|
// would carry, keyed by its lowercased URL name so the lookup folds case the way
|
|
// bindURL's match does.
|
|
func addressValues(in any) map[string]string {
|
|
v := reflect.ValueOf(in)
|
|
for v.Kind() == reflect.Pointer {
|
|
if v.IsNil() {
|
|
return nil
|
|
}
|
|
v = v.Elem()
|
|
}
|
|
if v.Kind() != reflect.Struct {
|
|
return nil
|
|
}
|
|
out := map[string]string{}
|
|
for _, f := range wireFields(v.Type()) {
|
|
name := urlFieldName(f)
|
|
if name == "" || name == "-" {
|
|
continue
|
|
}
|
|
out[strings.ToLower(name)] = scalarString(v.FieldByIndex(f.Index))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// scalarString is setScalar read backwards: the wire spelling of one value. A
|
|
// kind a URL cannot carry renders empty, which is the same silence setScalar
|
|
// keeps when it is handed one.
|
|
func scalarString(fv reflect.Value) string {
|
|
switch fv.Kind() {
|
|
case reflect.String:
|
|
return fv.String()
|
|
case reflect.Bool:
|
|
return strconv.FormatBool(fv.Bool())
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return strconv.FormatInt(fv.Int(), 10)
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
return strconv.FormatUint(fv.Uint(), 10)
|
|
case reflect.Float32, reflect.Float64:
|
|
return strconv.FormatFloat(fv.Float(), 'g', -1, fv.Type().Bits())
|
|
default:
|
|
return ""
|
|
}
|
|
}
|