326 lines
7.8 KiB
Go
326 lines
7.8 KiB
Go
package protocol
|
|
|
|
import (
|
|
"fmt"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// BzzzURI represents a parsed CHORUS:// URI with semantic addressing
|
|
// Grammar: CHORUS://[agent]:[role]@[project]:[task]/[path][?query][#fragment]
|
|
type BzzzURI struct {
|
|
// Core addressing components
|
|
Agent string // Agent identifier (e.g., "claude", "any", "*")
|
|
Role string // Agent role (e.g., "frontend", "backend", "architect")
|
|
Project string // Project context (e.g., "chorus", "CHORUS")
|
|
Task string // Task identifier (e.g., "implement", "review", "test", "*")
|
|
|
|
// Resource path
|
|
Path string // Resource path (e.g., "/src/main.go", "/docs/api.md")
|
|
|
|
// Standard URI components
|
|
Query string // Query parameters
|
|
Fragment string // Fragment identifier
|
|
|
|
// Original raw URI string
|
|
Raw string
|
|
}
|
|
|
|
// URI grammar constants
|
|
const (
|
|
BzzzScheme = "CHORUS"
|
|
|
|
// Special identifiers
|
|
AnyAgent = "any"
|
|
AnyRole = "any"
|
|
AnyProject = "any"
|
|
AnyTask = "any"
|
|
Wildcard = "*"
|
|
)
|
|
|
|
// Validation patterns
|
|
var (
|
|
// Component validation patterns
|
|
agentPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
|
|
rolePattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
|
|
projectPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
|
|
taskPattern = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$|^\*$|^any$`)
|
|
pathPattern = regexp.MustCompile(`^/[a-zA-Z0-9\-_/\.]*$|^$`)
|
|
|
|
// Full URI pattern for validation
|
|
chorusURIPattern = regexp.MustCompile(`^CHORUS://([a-zA-Z0-9\-_*]|any):([a-zA-Z0-9\-_*]|any)@([a-zA-Z0-9\-_*]|any):([a-zA-Z0-9\-_*]|any)(/[a-zA-Z0-9\-_/\.]*)?(\?[^#]*)?(\#.*)?$`)
|
|
)
|
|
|
|
// ParseBzzzURI parses a CHORUS:// URI string into a BzzzURI struct
|
|
func ParseBzzzURI(uri string) (*BzzzURI, error) {
|
|
if uri == "" {
|
|
return nil, fmt.Errorf("empty URI")
|
|
}
|
|
|
|
// Basic scheme validation
|
|
if !strings.HasPrefix(uri, BzzzScheme+"://") {
|
|
return nil, fmt.Errorf("invalid scheme: expected '%s'", BzzzScheme)
|
|
}
|
|
|
|
// Use Go's standard URL parser for basic parsing
|
|
parsedURL, err := url.Parse(uri)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse URI: %w", err)
|
|
}
|
|
|
|
if parsedURL.Scheme != BzzzScheme {
|
|
return nil, fmt.Errorf("invalid scheme: expected '%s', got '%s'", BzzzScheme, parsedURL.Scheme)
|
|
}
|
|
|
|
// Parse the authority part (user:pass@host:port becomes agent:role@project:task)
|
|
userInfo := parsedURL.User
|
|
if userInfo == nil {
|
|
return nil, fmt.Errorf("missing agent:role information")
|
|
}
|
|
|
|
username := userInfo.Username()
|
|
password, hasPassword := userInfo.Password()
|
|
if !hasPassword {
|
|
return nil, fmt.Errorf("missing role information")
|
|
}
|
|
|
|
agent := username
|
|
role := password
|
|
|
|
// Parse host:port as project:task
|
|
hostPort := parsedURL.Host
|
|
if hostPort == "" {
|
|
return nil, fmt.Errorf("missing project:task information")
|
|
}
|
|
|
|
// Split host:port to get project:task
|
|
parts := strings.Split(hostPort, ":")
|
|
if len(parts) != 2 {
|
|
return nil, fmt.Errorf("invalid project:task format: expected 'project:task'")
|
|
}
|
|
|
|
project := parts[0]
|
|
task := parts[1]
|
|
|
|
// Create BzzzURI instance
|
|
chorusURI := &BzzzURI{
|
|
Agent: agent,
|
|
Role: role,
|
|
Project: project,
|
|
Task: task,
|
|
Path: parsedURL.Path,
|
|
Query: parsedURL.RawQuery,
|
|
Fragment: parsedURL.Fragment,
|
|
Raw: uri,
|
|
}
|
|
|
|
// Validate components
|
|
if err := chorusURI.Validate(); err != nil {
|
|
return nil, fmt.Errorf("validation failed: %w", err)
|
|
}
|
|
|
|
return chorusURI, nil
|
|
}
|
|
|
|
// Validate validates all components of the BzzzURI
|
|
func (u *BzzzURI) Validate() error {
|
|
// Validate agent
|
|
if u.Agent == "" {
|
|
return fmt.Errorf("agent cannot be empty")
|
|
}
|
|
if !agentPattern.MatchString(u.Agent) {
|
|
return fmt.Errorf("invalid agent format: '%s'", u.Agent)
|
|
}
|
|
|
|
// Validate role
|
|
if u.Role == "" {
|
|
return fmt.Errorf("role cannot be empty")
|
|
}
|
|
if !rolePattern.MatchString(u.Role) {
|
|
return fmt.Errorf("invalid role format: '%s'", u.Role)
|
|
}
|
|
|
|
// Validate project
|
|
if u.Project == "" {
|
|
return fmt.Errorf("project cannot be empty")
|
|
}
|
|
if !projectPattern.MatchString(u.Project) {
|
|
return fmt.Errorf("invalid project format: '%s'", u.Project)
|
|
}
|
|
|
|
// Validate task
|
|
if u.Task == "" {
|
|
return fmt.Errorf("task cannot be empty")
|
|
}
|
|
if !taskPattern.MatchString(u.Task) {
|
|
return fmt.Errorf("invalid task format: '%s'", u.Task)
|
|
}
|
|
|
|
// Validate path (optional)
|
|
if u.Path != "" && !pathPattern.MatchString(u.Path) {
|
|
return fmt.Errorf("invalid path format: '%s'", u.Path)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// String returns the canonical string representation of the BzzzURI
|
|
func (u *BzzzURI) String() string {
|
|
uri := fmt.Sprintf("%s://%s:%s@%s:%s", BzzzScheme, u.Agent, u.Role, u.Project, u.Task)
|
|
|
|
if u.Path != "" {
|
|
uri += u.Path
|
|
}
|
|
|
|
if u.Query != "" {
|
|
uri += "?" + u.Query
|
|
}
|
|
|
|
if u.Fragment != "" {
|
|
uri += "#" + u.Fragment
|
|
}
|
|
|
|
return uri
|
|
}
|
|
|
|
// Normalize normalizes the URI components for consistent addressing
|
|
func (u *BzzzURI) Normalize() {
|
|
// Convert empty wildcards to standard wildcard
|
|
if u.Agent == "" {
|
|
u.Agent = Wildcard
|
|
}
|
|
if u.Role == "" {
|
|
u.Role = Wildcard
|
|
}
|
|
if u.Project == "" {
|
|
u.Project = Wildcard
|
|
}
|
|
if u.Task == "" {
|
|
u.Task = Wildcard
|
|
}
|
|
|
|
// Normalize to lowercase for consistency
|
|
u.Agent = strings.ToLower(u.Agent)
|
|
u.Role = strings.ToLower(u.Role)
|
|
u.Project = strings.ToLower(u.Project)
|
|
u.Task = strings.ToLower(u.Task)
|
|
|
|
// Clean path
|
|
if u.Path != "" && !strings.HasPrefix(u.Path, "/") {
|
|
u.Path = "/" + u.Path
|
|
}
|
|
}
|
|
|
|
// IsWildcard checks if a component is a wildcard or "any"
|
|
func IsWildcard(component string) bool {
|
|
return component == Wildcard || component == AnyAgent || component == AnyRole ||
|
|
component == AnyProject || component == AnyTask
|
|
}
|
|
|
|
// Matches checks if this URI matches another URI (with wildcard support)
|
|
func (u *BzzzURI) Matches(other *BzzzURI) bool {
|
|
if other == nil {
|
|
return false
|
|
}
|
|
|
|
// Check each component with wildcard support
|
|
if !componentMatches(u.Agent, other.Agent) {
|
|
return false
|
|
}
|
|
if !componentMatches(u.Role, other.Role) {
|
|
return false
|
|
}
|
|
if !componentMatches(u.Project, other.Project) {
|
|
return false
|
|
}
|
|
if !componentMatches(u.Task, other.Task) {
|
|
return false
|
|
}
|
|
|
|
// Path matching (exact or wildcard)
|
|
if u.Path != "" && other.Path != "" && u.Path != other.Path {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// componentMatches checks if two components match (with wildcard support)
|
|
func componentMatches(a, b string) bool {
|
|
// Exact match
|
|
if a == b {
|
|
return true
|
|
}
|
|
|
|
// Wildcard matching
|
|
if IsWildcard(a) || IsWildcard(b) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// GetSelectorPriority returns a priority score for URI matching (higher = more specific)
|
|
func (u *BzzzURI) GetSelectorPriority() int {
|
|
priority := 0
|
|
|
|
// More specific components get higher priority
|
|
if !IsWildcard(u.Agent) {
|
|
priority += 8
|
|
}
|
|
if !IsWildcard(u.Role) {
|
|
priority += 4
|
|
}
|
|
if !IsWildcard(u.Project) {
|
|
priority += 2
|
|
}
|
|
if !IsWildcard(u.Task) {
|
|
priority += 1
|
|
}
|
|
|
|
// Path specificity adds priority
|
|
if u.Path != "" && u.Path != "/" {
|
|
priority += 1
|
|
}
|
|
|
|
return priority
|
|
}
|
|
|
|
// ToAddress returns a simplified address representation for P2P routing
|
|
func (u *BzzzURI) ToAddress() string {
|
|
return fmt.Sprintf("%s:%s@%s:%s", u.Agent, u.Role, u.Project, u.Task)
|
|
}
|
|
|
|
// ValidateBzzzURIString validates a CHORUS:// URI string without parsing
|
|
func ValidateBzzzURIString(uri string) error {
|
|
if uri == "" {
|
|
return fmt.Errorf("empty URI")
|
|
}
|
|
|
|
if !chorusURIPattern.MatchString(uri) {
|
|
return fmt.Errorf("invalid CHORUS:// URI format")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NewBzzzURI creates a new BzzzURI with the given components
|
|
func NewBzzzURI(agent, role, project, task, path string) *BzzzURI {
|
|
uri := &BzzzURI{
|
|
Agent: agent,
|
|
Role: role,
|
|
Project: project,
|
|
Task: task,
|
|
Path: path,
|
|
}
|
|
uri.Normalize()
|
|
return uri
|
|
}
|
|
|
|
// ParseAddress parses a simplified address format (agent:role@project:task)
|
|
func ParseAddress(addr string) (*BzzzURI, error) {
|
|
// Convert simplified address to full URI
|
|
fullURI := BzzzScheme + "://" + addr
|
|
return ParseBzzzURI(fullURI)
|
|
} |