// Package config handles micro.mu and micro.json configuration parsing package config import ( "bufio" "encoding/json" "fmt" "os" "path/filepath" "strconv" "strings" ) // Config represents the micro run configuration type Config struct { Services map[string]*Service `json:"services"` Envs map[string]map[string]string `json:"env"` Deploy map[string]*DeployTarget `json:"deploy"` } // DeployTarget represents a deployment target configuration type DeployTarget struct { Name string `json:"-"` SSH string `json:"ssh"` Path string `json:"path,omitempty"` } // Service represents a service configuration type Service struct { Name string `json:"-"` Path string `json:"path"` Port int `json:"port,omitempty"` Depends []string `json:"depends,omitempty"` } // Load attempts to load configuration from micro.mu or micro.json in the given directory func Load(dir string) (*Config, error) { // Try micro.mu first (preferred) muPath := filepath.Join(dir, "micro.mu") if _, err := os.Stat(muPath); err == nil { return ParseMu(muPath) } // Fall back to micro.json jsonPath := filepath.Join(dir, "micro.json") if _, err := os.Stat(jsonPath); err == nil { return ParseJSON(jsonPath) } return nil, nil // No config file, not an error } // ParseJSON parses a micro.json configuration file func ParseJSON(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read %s: %w", path, err) } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("failed to parse %s: %w", path, err) } // Set service names from map keys for name, svc := range cfg.Services { svc.Name = name } return &cfg, nil } // ParseMu parses a micro.mu DSL configuration file // // Format: // // service users // path ./users // port 8081 // // service posts // path ./posts // port 8082 // depends users // // env development // STORE_ADDRESS file://./data func ParseMu(path string) (*Config, error) { file, err := os.Open(path) if err != nil { return nil, fmt.Errorf("failed to open %s: %w", path, err) } defer file.Close() cfg := &Config{ Services: make(map[string]*Service), Envs: make(map[string]map[string]string), Deploy: make(map[string]*DeployTarget), } var currentService *Service var currentEnv string var currentEnvMap map[string]string var currentDeploy *DeployTarget scanner := bufio.NewScanner(file) lineNum := 0 for scanner.Scan() { lineNum++ line := scanner.Text() // Skip empty lines and comments trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "#") { continue } // Check indentation indented := strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") if !indented { // Top-level declaration parts := strings.Fields(trimmed) if len(parts) < 2 { return nil, fmt.Errorf("%s:%d: expected 'service ' or 'env '", path, lineNum) } keyword := parts[0] name := parts[1] switch keyword { case "service": // Save previous env if any if currentEnv != "" && currentEnvMap != nil { cfg.Envs[currentEnv] = currentEnvMap } currentEnv = "" currentEnvMap = nil currentService = &Service{Name: name} cfg.Services[name] = currentService case "env": // Save previous env if any if currentEnv != "" && currentEnvMap != nil { cfg.Envs[currentEnv] = currentEnvMap } currentService = nil currentDeploy = nil currentEnv = name currentEnvMap = make(map[string]string) case "deploy": // Save previous env if any if currentEnv != "" && currentEnvMap != nil { cfg.Envs[currentEnv] = currentEnvMap } currentService = nil currentEnv = "" currentEnvMap = nil currentDeploy = &DeployTarget{Name: name} cfg.Deploy[name] = currentDeploy default: return nil, fmt.Errorf("%s:%d: unknown keyword '%s'", path, lineNum, keyword) } } else { // Indented property parts := strings.Fields(trimmed) if len(parts) < 2 { return nil, fmt.Errorf("%s:%d: expected 'key value'", path, lineNum) } key := parts[0] value := strings.Join(parts[1:], " ") if currentService != nil { switch key { case "path": currentService.Path = value case "port": port, err := strconv.Atoi(value) if err != nil { return nil, fmt.Errorf("%s:%d: invalid port '%s'", path, lineNum, value) } currentService.Port = port case "depends": currentService.Depends = parts[1:] default: return nil, fmt.Errorf("%s:%d: unknown service property '%s'", path, lineNum, key) } } else if currentDeploy != nil { switch key { case "ssh": currentDeploy.SSH = value case "path": currentDeploy.Path = value default: return nil, fmt.Errorf("%s:%d: unknown deploy property '%s'", path, lineNum, key) } } else if currentEnvMap != nil { // Environment variable currentEnvMap[key] = value } else { return nil, fmt.Errorf("%s:%d: property outside of service, deploy, or env block", path, lineNum) } } } // Save final env if any if currentEnv != "" && currentEnvMap != nil { cfg.Envs[currentEnv] = currentEnvMap } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("error reading %s: %w", path, err) } return cfg, nil } // TopologicalSort returns services in dependency order func (c *Config) TopologicalSort() ([]*Service, error) { if c == nil || len(c.Services) == 0 { return nil, nil } // Build adjacency list and in-degree count inDegree := make(map[string]int) for name := range c.Services { inDegree[name] = 0 } for _, svc := range c.Services { for _, dep := range svc.Depends { if _, ok := c.Services[dep]; !ok { return nil, fmt.Errorf("service '%s' depends on unknown service '%s'", svc.Name, dep) } inDegree[svc.Name]++ } } // Kahn's algorithm var queue []string for name, degree := range inDegree { if degree == 0 { queue = append(queue, name) } } var result []*Service for len(queue) > 0 { name := queue[0] queue = queue[1:] result = append(result, c.Services[name]) // Reduce in-degree for dependents for _, svc := range c.Services { for _, dep := range svc.Depends { if dep == name { inDegree[svc.Name]-- if inDegree[svc.Name] == 0 { queue = append(queue, svc.Name) } } } } } if len(result) != len(c.Services) { return nil, fmt.Errorf("circular dependency detected") } return result, nil } // GetEnv returns environment variables for the given environment name func (c *Config) GetEnv(name string) map[string]string { if c == nil || c.Envs == nil { return nil } return c.Envs[name] }