* add usage docs for context types and metadata, improve comments * changes after review
295 lines
9.0 KiB
Go
295 lines
9.0 KiB
Go
package metadata
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// In the metadata package, context and metadata are treated as immutable.
|
||
// Deep copies of metadata are made to keep things safe and correct.
|
||
// If a user takes a map and changes it across threads, it's their responsibility.
|
||
//
|
||
// 1. Incoming Context
|
||
//
|
||
// This context is provided by an external system and populated by the server or broker of the micro framework.
|
||
// It should not be modified. The idea is to extract all necessary data from it,
|
||
// validate the data, and transfer it into the current context.
|
||
// After that, only the current context should be used throughout the code.
|
||
//
|
||
// 2. Current Context
|
||
//
|
||
// This is the context used during the execution flow.
|
||
// You can add any needed metadata to it and pass it through your code.
|
||
//
|
||
// 3. Outgoing Context
|
||
//
|
||
// This context is for sending data to external systems.
|
||
// You can add what you need before sending it out.
|
||
// But it’s usually better to build and prepare this context right before making the external call,
|
||
// instead of changing it in many places.
|
||
//
|
||
// Execution Flow:
|
||
//
|
||
// [External System]
|
||
// ↓
|
||
// [Incoming Context]
|
||
// ↓
|
||
// [Extract & Validate Metadata from Incoming Context]
|
||
// ↓
|
||
// [Prepare Current Context]
|
||
// ↓
|
||
// [Enrich Current Context]
|
||
// ↓
|
||
// [Business Logic]
|
||
// ↓
|
||
// [Prepare Outgoing Context]
|
||
// ↓
|
||
// [External System Call]
|
||
|
||
type (
|
||
metadataCurrentKey struct{}
|
||
metadataIncomingKey struct{}
|
||
metadataOutgoingKey struct{}
|
||
|
||
rawMetadata struct {
|
||
md Metadata
|
||
added [][]string
|
||
}
|
||
)
|
||
|
||
// NewContext creates a new context with the provided Metadata attached.
|
||
// The Metadata must not be modified after calling this function.
|
||
func NewContext(ctx context.Context, md Metadata) context.Context {
|
||
return context.WithValue(ctx, metadataCurrentKey{}, rawMetadata{md: md})
|
||
}
|
||
|
||
// NewIncomingContext creates a new context with the provided incoming Metadata attached.
|
||
// The Metadata must not be modified after calling this function.
|
||
func NewIncomingContext(ctx context.Context, md Metadata) context.Context {
|
||
return context.WithValue(ctx, metadataIncomingKey{}, rawMetadata{md: md})
|
||
}
|
||
|
||
// NewOutgoingContext creates a new context with the provided outgoing Metadata attached.
|
||
// The Metadata must not be modified after calling this function.
|
||
func NewOutgoingContext(ctx context.Context, md Metadata) context.Context {
|
||
return context.WithValue(ctx, metadataOutgoingKey{}, rawMetadata{md: md})
|
||
}
|
||
|
||
// AppendContext returns a new context with the provided key-value pairs (kv)
|
||
// merged with any existing metadata in the context. For a description of kv,
|
||
// please refer to the Pairs documentation.
|
||
func AppendContext(ctx context.Context, kv ...string) context.Context {
|
||
if len(kv)%2 == 1 {
|
||
panic(fmt.Sprintf("metadata: AppendContext got an odd number of input pairs for metadata: %d", len(kv)))
|
||
}
|
||
md, _ := ctx.Value(metadataCurrentKey{}).(rawMetadata)
|
||
added := make([][]string, len(md.added)+1)
|
||
copy(added, md.added)
|
||
kvCopy := make([]string, 0, len(kv))
|
||
for i := 0; i < len(kv); i += 2 {
|
||
kvCopy = append(kvCopy, strings.ToLower(kv[i]), kv[i+1])
|
||
}
|
||
added[len(added)-1] = kvCopy
|
||
return context.WithValue(ctx, metadataCurrentKey{}, rawMetadata{md: md.md, added: added})
|
||
}
|
||
|
||
// AppendOutgoingContext returns a new context with the provided key-value pairs (kv)
|
||
// merged with any existing metadata in the context. For a description of kv,
|
||
// please refer to the Pairs documentation.
|
||
func AppendOutgoingContext(ctx context.Context, kv ...string) context.Context {
|
||
if len(kv)%2 == 1 {
|
||
panic(fmt.Sprintf("metadata: AppendOutgoingContext got an odd number of input pairs for metadata: %d", len(kv)))
|
||
}
|
||
md, _ := ctx.Value(metadataOutgoingKey{}).(rawMetadata)
|
||
added := make([][]string, len(md.added)+1)
|
||
copy(added, md.added)
|
||
kvCopy := make([]string, 0, len(kv))
|
||
for i := 0; i < len(kv); i += 2 {
|
||
kvCopy = append(kvCopy, strings.ToLower(kv[i]), kv[i+1])
|
||
}
|
||
added[len(added)-1] = kvCopy
|
||
return context.WithValue(ctx, metadataOutgoingKey{}, rawMetadata{md: md.md, added: added})
|
||
}
|
||
|
||
// FromContext retrieves a deep copy of the metadata from the context and returns it
|
||
// with a boolean indicating if it was found.
|
||
func FromContext(ctx context.Context) (Metadata, bool) {
|
||
raw, ok := ctx.Value(metadataCurrentKey{}).(rawMetadata)
|
||
if !ok {
|
||
return nil, false
|
||
}
|
||
metadataSize := len(raw.md)
|
||
for i := range raw.added {
|
||
metadataSize += len(raw.added[i]) / 2
|
||
}
|
||
|
||
out := make(Metadata, metadataSize)
|
||
for k, v := range raw.md {
|
||
out[k] = copyOf(v)
|
||
}
|
||
for _, added := range raw.added {
|
||
if len(added)%2 == 1 {
|
||
panic(fmt.Sprintf("metadata: FromContext got an odd number of input pairs for metadata: %d", len(added)))
|
||
}
|
||
|
||
for i := 0; i < len(added); i += 2 {
|
||
out[added[i]] = append(out[added[i]], added[i+1])
|
||
}
|
||
}
|
||
return out, true
|
||
}
|
||
|
||
// MustContext retrieves a deep copy of the metadata from the context and panics
|
||
// if the metadata is not found.
|
||
func MustContext(ctx context.Context) Metadata {
|
||
md, ok := FromContext(ctx)
|
||
if !ok {
|
||
panic("missing metadata")
|
||
}
|
||
return md
|
||
}
|
||
|
||
// FromIncomingContext retrieves a deep copy of the metadata from the context and returns it
|
||
// with a boolean indicating if it was found.
|
||
func FromIncomingContext(ctx context.Context) (Metadata, bool) {
|
||
raw, ok := ctx.Value(metadataIncomingKey{}).(rawMetadata)
|
||
if !ok {
|
||
return nil, false
|
||
}
|
||
metadataSize := len(raw.md)
|
||
for i := range raw.added {
|
||
metadataSize += len(raw.added[i]) / 2
|
||
}
|
||
|
||
out := make(Metadata, metadataSize)
|
||
for k, v := range raw.md {
|
||
out[k] = copyOf(v)
|
||
}
|
||
for _, added := range raw.added {
|
||
if len(added)%2 == 1 {
|
||
panic(fmt.Sprintf("metadata: FromIncomingContext got an odd number of input pairs for metadata: %d", len(added)))
|
||
}
|
||
|
||
for i := 0; i < len(added); i += 2 {
|
||
out[added[i]] = append(out[added[i]], added[i+1])
|
||
}
|
||
}
|
||
return out, true
|
||
}
|
||
|
||
// MustIncomingContext retrieves a deep copy of the metadata from the context and panics
|
||
// if the metadata is not found.
|
||
func MustIncomingContext(ctx context.Context) Metadata {
|
||
md, ok := FromIncomingContext(ctx)
|
||
if !ok {
|
||
panic("missing metadata")
|
||
}
|
||
return md
|
||
}
|
||
|
||
// FromOutgoingContext retrieves a deep copy of the metadata from the context and returns it
|
||
// with a boolean indicating if it was found.
|
||
func FromOutgoingContext(ctx context.Context) (Metadata, bool) {
|
||
raw, ok := ctx.Value(metadataOutgoingKey{}).(rawMetadata)
|
||
if !ok {
|
||
return nil, false
|
||
}
|
||
|
||
metadataSize := len(raw.md)
|
||
for i := range raw.added {
|
||
metadataSize += len(raw.added[i]) / 2
|
||
}
|
||
|
||
out := make(Metadata, metadataSize)
|
||
for k, v := range raw.md {
|
||
out[k] = copyOf(v)
|
||
}
|
||
for _, added := range raw.added {
|
||
if len(added)%2 == 1 {
|
||
panic(fmt.Sprintf("metadata: FromOutgoingContext got an odd number of input pairs for metadata: %d", len(added)))
|
||
}
|
||
|
||
for i := 0; i < len(added); i += 2 {
|
||
out[added[i]] = append(out[added[i]], added[i+1])
|
||
}
|
||
}
|
||
return out, ok
|
||
}
|
||
|
||
// MustOutgoingContext retrieves a deep copy of the metadata from the context and panics
|
||
// if the metadata is not found.
|
||
func MustOutgoingContext(ctx context.Context) Metadata {
|
||
md, ok := FromOutgoingContext(ctx)
|
||
if !ok {
|
||
panic("missing metadata")
|
||
}
|
||
return md
|
||
}
|
||
|
||
// ValueFromCurrentContext retrieves a deep copy of the metadata for the given key
|
||
// from the context, performing a case-insensitive search if needed. Returns nil if not found.
|
||
func ValueFromCurrentContext(ctx context.Context, key string) []string {
|
||
md, ok := ctx.Value(metadataCurrentKey{}).(rawMetadata)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
if v, ok := md.md[key]; ok {
|
||
return copyOf(v)
|
||
}
|
||
for k, v := range md.md {
|
||
// Case-insensitive comparison: Metadata is a map, and there's no guarantee
|
||
// that the Metadata attached to the context is created using our helper
|
||
// functions.
|
||
if strings.EqualFold(k, key) {
|
||
return copyOf(v)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValueFromIncomingContext retrieves a deep copy of the metadata for the given key
|
||
// from the context, performing a case-insensitive search if needed. Returns nil if not found.
|
||
func ValueFromIncomingContext(ctx context.Context, key string) []string {
|
||
raw, ok := ctx.Value(metadataIncomingKey{}).(rawMetadata)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
if v, ok := raw.md[key]; ok {
|
||
return copyOf(v)
|
||
}
|
||
for k, v := range raw.md {
|
||
// Case-insensitive comparison: Metadata is a map, and there's no guarantee
|
||
// that the Metadata attached to the context is created using our helper
|
||
// functions.
|
||
if strings.EqualFold(k, key) {
|
||
return copyOf(v)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ValueFromOutgoingContext retrieves a deep copy of the metadata for the given key
|
||
// from the context, performing a case-insensitive search if needed. Returns nil if not found.
|
||
func ValueFromOutgoingContext(ctx context.Context, key string) []string {
|
||
md, ok := ctx.Value(metadataOutgoingKey{}).(rawMetadata)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
|
||
if v, ok := md.md[key]; ok {
|
||
return copyOf(v)
|
||
}
|
||
for k, v := range md.md {
|
||
// Case-insensitive comparison: Metadata is a map, and there's no guarantee
|
||
// that the Metadata attached to the context is created using our helper
|
||
// functions.
|
||
if strings.EqualFold(k, key) {
|
||
return copyOf(v)
|
||
}
|
||
}
|
||
return nil
|
||
}
|