314 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			314 lines
		
	
	
		
			8.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package builder
 | 
						|
 | 
						|
import (
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"net/url"
 | 
						|
	"strings"
 | 
						|
 | 
						|
	"google.golang.org/protobuf/proto"
 | 
						|
	"google.golang.org/protobuf/reflect/protoreflect"
 | 
						|
)
 | 
						|
 | 
						|
// -------------------------- Path template representation -------------------------
 | 
						|
 | 
						|
// pathSegment is a helper interface for elements of a path.
 | 
						|
type pathSegment interface {
 | 
						|
	isSegment() bool
 | 
						|
}
 | 
						|
 | 
						|
// pathTemplate represents a parsed URL path template.
 | 
						|
type pathTemplate struct {
 | 
						|
	// literalPrefix is the fixed part of the path before the first {var},
 | 
						|
	// e.g. "/v1/users/" for "/v1/users/{user_id}/orders:get".
 | 
						|
	// It is removed from segments, so segments contain only the remaining path literals and variables.
 | 
						|
	literalPrefix string
 | 
						|
 | 
						|
	// segments is a sequence of pathLiteral or pathVar representing the rest of the path after literalPrefix.
 | 
						|
	segments []pathSegment
 | 
						|
 | 
						|
	// customVerb is an optional ":verb" suffix, e.g. ":get".
 | 
						|
	customVerb string
 | 
						|
}
 | 
						|
 | 
						|
// pathLiteral represents a fixed literal segment in a path template, e.g., "/v1/users/".
 | 
						|
type pathLiteral struct {
 | 
						|
	text string
 | 
						|
}
 | 
						|
 | 
						|
func (p pathLiteral) isSegment() bool { return true }
 | 
						|
 | 
						|
// pathVar represents a variable segment in a path template, e.g., "{user.id}".
 | 
						|
type pathVar struct {
 | 
						|
	// fieldPath is the dotted path to the field in the struct, e.g., "user.id".
 | 
						|
	fieldPath string
 | 
						|
 | 
						|
	// pattern is the optional pattern after '=', e.g., "*" or "**/orders".
 | 
						|
	// It specifies how the variable can match parts of the URL path.
 | 
						|
	pattern string
 | 
						|
 | 
						|
	// multiSegment is true if the pattern can match multiple path segments
 | 
						|
	// (contains '/' or "**").
 | 
						|
	multiSegment bool
 | 
						|
}
 | 
						|
 | 
						|
func (p pathVar) isSegment() bool { return true }
 | 
						|
 | 
						|
// ----------------------------- Path template parsing -----------------------------
 | 
						|
 | 
						|
// parsePathTemplate parses a URL path template into a pathTemplate.
 | 
						|
// It extracts:
 | 
						|
//  1. literalPrefix — fixed part before the first variable,
 | 
						|
//  2. segments — sequence of pathLiteral and pathVar,
 | 
						|
//  3. customVerb — optional ":verb" suffix.
 | 
						|
//
 | 
						|
// Complexity: time O(n), memory O(n).
 | 
						|
//
 | 
						|
// Example:
 | 
						|
//
 | 
						|
//	input: "/v1/users/{user_id}/orders:get"
 | 
						|
//	output: pathTemplate{
 | 
						|
//	  literalPrefix: "/v1/users/",
 | 
						|
//	  segments:      [{user_id}, "/orders"],
 | 
						|
//	  customVerb:    ":get",
 | 
						|
//	}
 | 
						|
func parsePathTemplate(input string) (*pathTemplate, error) {
 | 
						|
	// Step 1: extract custom verb after the last colon, e.g. ":get"
 | 
						|
	var customVerb string
 | 
						|
	if i := strings.LastIndex(input, ":"); i >= 0 && i > strings.LastIndex(input, "/") {
 | 
						|
		customVerb = input[i:]
 | 
						|
		input = input[:i]
 | 
						|
	}
 | 
						|
 | 
						|
	var (
 | 
						|
		segments []pathSegment
 | 
						|
		buf      strings.Builder
 | 
						|
	)
 | 
						|
 | 
						|
	// Step 2: iterate over the input and split into segments
 | 
						|
	for i := 0; i < len(input); {
 | 
						|
		if input[i] != '{' {
 | 
						|
			buf.WriteByte(input[i])
 | 
						|
			i++
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// Add literal before '{' if any
 | 
						|
		if buf.Len() > 0 {
 | 
						|
			segments = append(segments, pathLiteral{text: buf.String()})
 | 
						|
			buf.Reset()
 | 
						|
		}
 | 
						|
 | 
						|
		// Find closing '}'
 | 
						|
		start := i + 1
 | 
						|
		offset := strings.IndexByte(input[start:], '}') // relative offset from start
 | 
						|
		if offset < 0 {
 | 
						|
			return nil, fmt.Errorf("unclosed '{' in path: %s", input)
 | 
						|
		}
 | 
						|
		end := start + offset
 | 
						|
 | 
						|
		token := input[start:end]
 | 
						|
		i = end + 1 // jump past '}'
 | 
						|
 | 
						|
		// Split field path and optional pattern
 | 
						|
		var fieldPath, pattern string
 | 
						|
		if k := strings.IndexByte(token, '='); k >= 0 {
 | 
						|
			fieldPath = strings.TrimSpace(token[:k])
 | 
						|
			pattern = strings.TrimSpace(token[k+1:])
 | 
						|
		} else {
 | 
						|
			fieldPath = strings.TrimSpace(token)
 | 
						|
		}
 | 
						|
 | 
						|
		if fieldPath == "" {
 | 
						|
			return nil, fmt.Errorf("empty variable in path: %s", input)
 | 
						|
		}
 | 
						|
 | 
						|
		pv := pathVar{
 | 
						|
			fieldPath:    fieldPath,
 | 
						|
			pattern:      pattern,
 | 
						|
			multiSegment: isMultiSegmentPattern(pattern),
 | 
						|
		}
 | 
						|
		segments = append(segments, pv)
 | 
						|
	}
 | 
						|
 | 
						|
	// Step 3: add any trailing literal after last '}'
 | 
						|
	if buf.Len() > 0 {
 | 
						|
		segments = append(segments, pathLiteral{text: buf.String()})
 | 
						|
	}
 | 
						|
 | 
						|
	// Step 4: extract literalPrefix if the first segment is a literal
 | 
						|
	var literalPrefix string
 | 
						|
	if len(segments) > 0 {
 | 
						|
		if pl, ok := segments[0].(pathLiteral); ok {
 | 
						|
			literalPrefix = pl.text
 | 
						|
			segments = segments[1:] // remove from segments to avoid duplication
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Step 5: return fully parsed pathTemplate
 | 
						|
	return &pathTemplate{
 | 
						|
		literalPrefix: literalPrefix,
 | 
						|
		segments:      segments,
 | 
						|
		customVerb:    customVerb,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// isMultiSegmentPattern returns true if pattern can match multiple path segments (contains '/' or '**').
 | 
						|
// Examples:
 | 
						|
// | 	Pattern     | Result | 					Usecase					|
 | 
						|
// |----------------|--------|------------------------------------------|
 | 
						|
// | ""             | false  | {var} 			=> single segment		|
 | 
						|
// | "*"            | false  | {var=*} 			=> single segment		|
 | 
						|
// | "**"           | true   | {var=**} 		=> multiple segments	|
 | 
						|
// | "foo/*"        | true   | {var=foo/*} 		=> multiple segments	|
 | 
						|
// | "foo/**"       | true   | {var=foo/**} 	=> multiple segments	|
 | 
						|
// | "users/*/orders"| true  | {users/*/orders} => multiple segments	|
 | 
						|
func isMultiSegmentPattern(pattern string) bool {
 | 
						|
	if pattern == "" {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	if pattern == singleWildcard {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	return strings.Contains(pattern, "/") || strings.Contains(pattern, doubleWildcard)
 | 
						|
}
 | 
						|
 | 
						|
// ----------------------------- Path template resolving -----------------------------
 | 
						|
 | 
						|
// resolvePathPlaceholders expands placeholders in a path template using values from proto.Message.
 | 
						|
// Placeholders must be bound to non-repeated scalar fields (not lists, maps, or messages).
 | 
						|
//
 | 
						|
// Example:
 | 
						|
//
 | 
						|
//	tmpl:       "/v1/users/{user_id}/orders:get"
 | 
						|
//	msg:        &pb.Message{UserId: 12345}
 | 
						|
//
 | 
						|
//	path:       "/v1/users/12345/orders:get"
 | 
						|
//	usedFields: {"user_id"}
 | 
						|
func resolvePathPlaceholders(tmpl *pathTemplate, msg proto.Message) (path string, usedFields *usedFields, err error) {
 | 
						|
	usedFields = newUsedFields()
 | 
						|
 | 
						|
	var sb strings.Builder
 | 
						|
	sb.WriteString(tmpl.literalPrefix)
 | 
						|
 | 
						|
	msgReflect := msg.ProtoReflect()
 | 
						|
 | 
						|
	for _, segment := range tmpl.segments {
 | 
						|
		switch s := segment.(type) {
 | 
						|
		case pathLiteral:
 | 
						|
			sb.WriteString(s.text)
 | 
						|
 | 
						|
		case pathVar:
 | 
						|
			val, fd, ok := findFieldByPath(msgReflect, s.fieldPath)
 | 
						|
			if !ok {
 | 
						|
				return "", nil, fmt.Errorf("path placeholder %s not found", s.fieldPath)
 | 
						|
			}
 | 
						|
			if isZeroValue(val, fd) {
 | 
						|
				// it's the only case that allows zero-value matches.
 | 
						|
				if s.pattern == doubleWildcard {
 | 
						|
					usedFields.add(s.fieldPath)
 | 
						|
					continue
 | 
						|
				}
 | 
						|
				return "", nil, fmt.Errorf("path placeholder %s has zero value", s.fieldPath)
 | 
						|
			}
 | 
						|
 | 
						|
			// must be scalar (non-repeated, non-map, non-message)
 | 
						|
			if fd.IsList() || fd.IsMap() || fd.Kind() == protoreflect.MessageKind {
 | 
						|
				return "", nil, fmt.Errorf("path placeholder %s must be scalar", s.fieldPath)
 | 
						|
			}
 | 
						|
 | 
						|
			usedFields.add(s.fieldPath)
 | 
						|
 | 
						|
			var strVal string
 | 
						|
			strVal, err = stringifyValue(val, fd)
 | 
						|
			if err != nil {
 | 
						|
				return "", nil, fmt.Errorf("stringify placeholder %s: %w", s.fieldPath, err)
 | 
						|
			}
 | 
						|
 | 
						|
			if err = validatePattern(s.pattern, strVal); err != nil {
 | 
						|
				return "", nil, fmt.Errorf("validate pattern, %s:%s: %w", s.fieldPath, strVal, err)
 | 
						|
			}
 | 
						|
 | 
						|
			parts := strings.Split(strVal, "/")
 | 
						|
			for i := range parts {
 | 
						|
				parts[i] = url.PathEscape(parts[i])
 | 
						|
			}
 | 
						|
			sb.WriteString(strings.Join(parts, "/"))
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	sb.WriteString(tmpl.customVerb)
 | 
						|
	return sb.String(), usedFields, nil
 | 
						|
}
 | 
						|
 | 
						|
// validatePattern checks whether input matches the given path pattern.
 | 
						|
//
 | 
						|
// Rules:
 | 
						|
//   - "" or "*" => exactly one segment, no "/" allowed
 | 
						|
//   - "**"      => zero or more segments (may include "/")
 | 
						|
//   - composite patterns like "*/orders/*" must match literally
 | 
						|
//
 | 
						|
// Example for composite pattern case:
 | 
						|
//
 | 
						|
//	pattern: "*/orders/*"
 | 
						|
//	input:   "42/orders/123"
 | 
						|
//
 | 
						|
//	patternSegments = ["*", "orders", "*"]
 | 
						|
//	valueParts      = ["42", "orders", "123"]
 | 
						|
//
 | 
						|
//	Match:
 | 
						|
//	  "*"      -> "42"
 | 
						|
//	  "orders" -> "orders"
 | 
						|
//	  "*"      -> "123"
 | 
						|
func validatePattern(pattern, input string) error {
 | 
						|
	var (
 | 
						|
		parts    = strings.Split(input, "/")
 | 
						|
		lenParts = len(parts)
 | 
						|
	)
 | 
						|
 | 
						|
	if pattern == "" || pattern == singleWildcard {
 | 
						|
		if lenParts != 1 {
 | 
						|
			return errors.New("must be a single path segment")
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	if pattern == doubleWildcard {
 | 
						|
		if lenParts < 1 {
 | 
						|
			return errors.New("must contain at least one segment")
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
 | 
						|
	var (
 | 
						|
		patternSegments = strings.Split(pattern, "/")
 | 
						|
		patternIndex    int
 | 
						|
	)
 | 
						|
 | 
						|
	for i := 0; i < len(patternSegments); i++ {
 | 
						|
		switch patternSegments[i] {
 | 
						|
		case singleWildcard:
 | 
						|
			if patternIndex >= lenParts || parts[patternIndex] == "" {
 | 
						|
				return fmt.Errorf("segment %d must not be empty", patternIndex)
 | 
						|
			}
 | 
						|
			patternIndex++
 | 
						|
		case doubleWildcard:
 | 
						|
			if patternIndex >= lenParts {
 | 
						|
				return fmt.Errorf("must contain at least one segment at position %d", patternIndex)
 | 
						|
			}
 | 
						|
			return nil
 | 
						|
		default:
 | 
						|
			if patternIndex >= lenParts || parts[patternIndex] != patternSegments[i] {
 | 
						|
				return fmt.Errorf("expected literal %s at position %d", patternSegments[i], patternIndex)
 | 
						|
			}
 | 
						|
			patternIndex++
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if patternIndex != lenParts {
 | 
						|
		return errors.New("extra segments in value")
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 |