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
 | |
| }
 |