- Add complete UCXL address parser with BNF grammar validation - Implement temporal navigation system with bounds checking - Create UCXI HTTP server with REST-like operations - Add comprehensive test suite with 87 passing tests - Integrate with existing BZZZ architecture (opt-in via config) - Support semantic addressing with wildcards and version control Core Features: - UCXL address format: ucxl://agent:role@project:task/temporal/path - Temporal segments: *^, ~~N, ^^N, *~, *~N with navigation logic - UCXI endpoints: GET/PUT/POST/DELETE/ANNOUNCE operations - Production-ready with error handling and graceful shutdown 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
369 lines
10 KiB
Go
369 lines
10 KiB
Go
package ucxl
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Address represents a parsed UCXL address
|
|
// Format: ucxl://agent:role@project:task/temporal_segment/path
|
|
type Address struct {
|
|
// Core components
|
|
Agent string `json:"agent"`
|
|
Role string `json:"role"`
|
|
Project string `json:"project"`
|
|
Task string `json:"task"`
|
|
|
|
// Temporal component
|
|
TemporalSegment TemporalSegment `json:"temporal_segment"`
|
|
|
|
// Path component
|
|
Path string `json:"path"`
|
|
|
|
// Original raw address for reference
|
|
Raw string `json:"raw"`
|
|
}
|
|
|
|
// TemporalSegment represents temporal navigation information
|
|
type TemporalSegment struct {
|
|
Type TemporalType `json:"type"`
|
|
Direction Direction `json:"direction,omitempty"`
|
|
Count int `json:"count,omitempty"`
|
|
}
|
|
|
|
// TemporalType defines the type of temporal navigation
|
|
type TemporalType string
|
|
|
|
const (
|
|
TemporalLatest TemporalType = "latest" // *^
|
|
TemporalAny TemporalType = "any" // *~
|
|
TemporalSpecific TemporalType = "specific" // *~N
|
|
TemporalRelative TemporalType = "relative" // ~~N, ^^N
|
|
)
|
|
|
|
// Direction defines temporal navigation direction
|
|
type Direction string
|
|
|
|
const (
|
|
DirectionBackward Direction = "backward" // ~~
|
|
DirectionForward Direction = "forward" // ^^
|
|
)
|
|
|
|
// ValidationError represents an address validation error
|
|
type ValidationError struct {
|
|
Field string
|
|
Message string
|
|
Raw string
|
|
}
|
|
|
|
func (e ValidationError) Error() string {
|
|
return fmt.Sprintf("UCXL address validation error in %s: %s (address: %s)", e.Field, e.Message, e.Raw)
|
|
}
|
|
|
|
// Regular expressions for validation
|
|
var (
|
|
// Component validation patterns
|
|
componentPattern = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$|^any$`)
|
|
pathPattern = regexp.MustCompile(`^[a-zA-Z0-9_\-/\.]*$`)
|
|
|
|
// Temporal segment patterns
|
|
temporalLatestPattern = regexp.MustCompile(`^\*\^$`) // *^
|
|
temporalAnyPattern = regexp.MustCompile(`^\*~$`) // *~
|
|
temporalSpecificPattern = regexp.MustCompile(`^\*~(\d+)$`) // *~N
|
|
temporalBackwardPattern = regexp.MustCompile(`^~~(\d+)$`) // ~~N
|
|
temporalForwardPattern = regexp.MustCompile(`^\^\^(\d+)$`) // ^^N
|
|
|
|
// Full address pattern for initial validation
|
|
ucxlAddressPattern = regexp.MustCompile(`^ucxl://([^:]+):([^@]+)@([^:]+):([^/]+)/([^/]+)/?(.*)$`)
|
|
)
|
|
|
|
// Parse parses a UCXL address string into an Address struct
|
|
func Parse(address string) (*Address, error) {
|
|
if address == "" {
|
|
return nil, &ValidationError{
|
|
Field: "address",
|
|
Message: "address cannot be empty",
|
|
Raw: address,
|
|
}
|
|
}
|
|
|
|
// Normalize the address (trim whitespace, convert to lowercase for scheme)
|
|
normalized := strings.TrimSpace(address)
|
|
if !strings.HasPrefix(strings.ToLower(normalized), "ucxl://") {
|
|
return nil, &ValidationError{
|
|
Field: "scheme",
|
|
Message: "address must start with 'ucxl://'",
|
|
Raw: address,
|
|
}
|
|
}
|
|
|
|
// Check scheme manually since our format doesn't follow standard URL format
|
|
if !strings.HasPrefix(strings.ToLower(normalized), "ucxl://") {
|
|
return nil, &ValidationError{
|
|
Field: "scheme",
|
|
Message: "scheme must be 'ucxl'",
|
|
Raw: address,
|
|
}
|
|
}
|
|
|
|
// Use regex for detailed component extraction
|
|
// Convert to lowercase for scheme but keep original for case-sensitive parts
|
|
normalizedForPattern := strings.ToLower(normalized[:7]) + normalized[7:] // normalize "ucxl://" part
|
|
matches := ucxlAddressPattern.FindStringSubmatch(normalizedForPattern)
|
|
if matches == nil || len(matches) != 7 {
|
|
return nil, &ValidationError{
|
|
Field: "format",
|
|
Message: "address format must be 'ucxl://agent:role@project:task/temporal_segment/path'",
|
|
Raw: address,
|
|
}
|
|
}
|
|
|
|
addr := &Address{
|
|
Agent: normalizeComponent(matches[1]),
|
|
Role: normalizeComponent(matches[2]),
|
|
Project: normalizeComponent(matches[3]),
|
|
Task: normalizeComponent(matches[4]),
|
|
Path: matches[6], // Path can be empty
|
|
Raw: address,
|
|
}
|
|
|
|
// Parse temporal segment
|
|
temporalSegment, err := parseTemporalSegment(matches[5])
|
|
if err != nil {
|
|
return nil, &ValidationError{
|
|
Field: "temporal_segment",
|
|
Message: err.Error(),
|
|
Raw: address,
|
|
}
|
|
}
|
|
addr.TemporalSegment = *temporalSegment
|
|
|
|
// Validate all components
|
|
if err := addr.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return addr, nil
|
|
}
|
|
|
|
// parseTemporalSegment parses the temporal segment component
|
|
func parseTemporalSegment(segment string) (*TemporalSegment, error) {
|
|
if segment == "" {
|
|
return nil, fmt.Errorf("temporal segment cannot be empty")
|
|
}
|
|
|
|
// Check for latest (*^)
|
|
if temporalLatestPattern.MatchString(segment) {
|
|
return &TemporalSegment{Type: TemporalLatest}, nil
|
|
}
|
|
|
|
// Check for any (*~)
|
|
if temporalAnyPattern.MatchString(segment) {
|
|
return &TemporalSegment{Type: TemporalAny}, nil
|
|
}
|
|
|
|
// Check for specific version (*~N)
|
|
if matches := temporalSpecificPattern.FindStringSubmatch(segment); matches != nil {
|
|
count, err := strconv.Atoi(matches[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid version number in specific temporal segment: %s", matches[1])
|
|
}
|
|
if count < 0 {
|
|
return nil, fmt.Errorf("version number cannot be negative: %d", count)
|
|
}
|
|
return &TemporalSegment{
|
|
Type: TemporalSpecific,
|
|
Count: count,
|
|
}, nil
|
|
}
|
|
|
|
// Check for backward navigation (~~N)
|
|
if matches := temporalBackwardPattern.FindStringSubmatch(segment); matches != nil {
|
|
count, err := strconv.Atoi(matches[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid count in backward temporal segment: %s", matches[1])
|
|
}
|
|
if count < 0 {
|
|
return nil, fmt.Errorf("backward count cannot be negative: %d", count)
|
|
}
|
|
return &TemporalSegment{
|
|
Type: TemporalRelative,
|
|
Direction: DirectionBackward,
|
|
Count: count,
|
|
}, nil
|
|
}
|
|
|
|
// Check for forward navigation (^^N)
|
|
if matches := temporalForwardPattern.FindStringSubmatch(segment); matches != nil {
|
|
count, err := strconv.Atoi(matches[1])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid count in forward temporal segment: %s", matches[1])
|
|
}
|
|
if count < 0 {
|
|
return nil, fmt.Errorf("forward count cannot be negative: %d", count)
|
|
}
|
|
return &TemporalSegment{
|
|
Type: TemporalRelative,
|
|
Direction: DirectionForward,
|
|
Count: count,
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("invalid temporal segment format: %s", segment)
|
|
}
|
|
|
|
// normalizeComponent normalizes address components (case-insensitive)
|
|
func normalizeComponent(component string) string {
|
|
return strings.ToLower(strings.TrimSpace(component))
|
|
}
|
|
|
|
// Validate validates the Address components according to BNF grammar rules
|
|
func (a *Address) Validate() error {
|
|
// Validate agent component
|
|
if err := validateComponent("agent", a.Agent); err != nil {
|
|
return &ValidationError{
|
|
Field: "agent",
|
|
Message: err.Error(),
|
|
Raw: a.Raw,
|
|
}
|
|
}
|
|
|
|
// Validate role component
|
|
if err := validateComponent("role", a.Role); err != nil {
|
|
return &ValidationError{
|
|
Field: "role",
|
|
Message: err.Error(),
|
|
Raw: a.Raw,
|
|
}
|
|
}
|
|
|
|
// Validate project component
|
|
if err := validateComponent("project", a.Project); err != nil {
|
|
return &ValidationError{
|
|
Field: "project",
|
|
Message: err.Error(),
|
|
Raw: a.Raw,
|
|
}
|
|
}
|
|
|
|
// Validate task component
|
|
if err := validateComponent("task", a.Task); err != nil {
|
|
return &ValidationError{
|
|
Field: "task",
|
|
Message: err.Error(),
|
|
Raw: a.Raw,
|
|
}
|
|
}
|
|
|
|
// Validate path component (can be empty)
|
|
if a.Path != "" && !pathPattern.MatchString(a.Path) {
|
|
return &ValidationError{
|
|
Field: "path",
|
|
Message: "path can only contain alphanumeric characters, underscores, hyphens, forward slashes, and dots",
|
|
Raw: a.Raw,
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateComponent validates individual address components
|
|
func validateComponent(name, component string) error {
|
|
if component == "" {
|
|
return fmt.Errorf("%s cannot be empty", name)
|
|
}
|
|
|
|
if !componentPattern.MatchString(component) {
|
|
return fmt.Errorf("%s can only contain alphanumeric characters, underscores, hyphens, or be 'any'", name)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// String returns the canonical string representation of the address
|
|
func (a *Address) String() string {
|
|
temporalStr := a.TemporalSegment.String()
|
|
if a.Path != "" {
|
|
return fmt.Sprintf("ucxl://%s:%s@%s:%s/%s/%s", a.Agent, a.Role, a.Project, a.Task, temporalStr, a.Path)
|
|
}
|
|
return fmt.Sprintf("ucxl://%s:%s@%s:%s/%s", a.Agent, a.Role, a.Project, a.Task, temporalStr)
|
|
}
|
|
|
|
// String returns the string representation of the temporal segment
|
|
func (ts *TemporalSegment) String() string {
|
|
switch ts.Type {
|
|
case TemporalLatest:
|
|
return "*^"
|
|
case TemporalAny:
|
|
return "*~"
|
|
case TemporalSpecific:
|
|
return fmt.Sprintf("*~%d", ts.Count)
|
|
case TemporalRelative:
|
|
if ts.Direction == DirectionBackward {
|
|
return fmt.Sprintf("~~%d", ts.Count)
|
|
}
|
|
return fmt.Sprintf("^^%d", ts.Count)
|
|
default:
|
|
return "*^" // Default to latest
|
|
}
|
|
}
|
|
|
|
// IsWildcard returns true if the address uses wildcard patterns
|
|
func (a *Address) IsWildcard() bool {
|
|
return a.Agent == "any" || a.Role == "any" || a.Project == "any" || a.Task == "any"
|
|
}
|
|
|
|
// Matches returns true if this address matches the pattern address
|
|
// Supports wildcard matching where "any" matches any value
|
|
func (a *Address) Matches(pattern *Address) bool {
|
|
if pattern == nil {
|
|
return false
|
|
}
|
|
|
|
// Check each component for wildcard or exact match
|
|
if pattern.Agent != "any" && a.Agent != pattern.Agent {
|
|
return false
|
|
}
|
|
if pattern.Role != "any" && a.Role != pattern.Role {
|
|
return false
|
|
}
|
|
if pattern.Project != "any" && a.Project != pattern.Project {
|
|
return false
|
|
}
|
|
if pattern.Task != "any" && a.Task != pattern.Task {
|
|
return false
|
|
}
|
|
|
|
// Path matching (if pattern has path, address must match or be subset)
|
|
if pattern.Path != "" {
|
|
if a.Path == "" {
|
|
return false
|
|
}
|
|
// Simple prefix matching for paths
|
|
if !strings.HasPrefix(a.Path, pattern.Path) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Clone creates a deep copy of the address
|
|
func (a *Address) Clone() *Address {
|
|
return &Address{
|
|
Agent: a.Agent,
|
|
Role: a.Role,
|
|
Project: a.Project,
|
|
Task: a.Task,
|
|
TemporalSegment: a.TemporalSegment, // TemporalSegment is a value type, safe to copy
|
|
Path: a.Path,
|
|
Raw: a.Raw,
|
|
}
|
|
}
|
|
|
|
// IsValid performs comprehensive validation and returns true if the address is valid
|
|
func (a *Address) IsValid() bool {
|
|
return a.Validate() == nil
|
|
} |