package managed import ( "errors" "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) var ErrServiceNotFound = errors.New("managed service not found") type Registry struct { Services []Service `yaml:"services"` } type Service struct { ID string `yaml:"id" json:"id"` DisplayName string `yaml:"display_name" json:"display_name"` ProjectType string `yaml:"project_type" json:"project_type"` ProjectRoot string `yaml:"project_root" json:"project_root"` ContainerName string `yaml:"container_name" json:"container_name"` APIBaseURL string `yaml:"api_base_url" json:"api_base_url"` ServiceName string `yaml:"service_name" json:"service_name"` ConfigPath string `yaml:"config_path" json:"config_path"` RTSPField string `yaml:"rtsp_field" json:"rtsp_field"` ResultType string `yaml:"result_type" json:"result_type"` ResultPaths map[string]string `yaml:"result_paths" json:"result_paths"` } func EmptyRegistry() *Registry { return &Registry{Services: []Service{}} } func LoadRegistry(path string) (*Registry, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read managed registry: %w", err) } var registry Registry if err := yaml.Unmarshal(data, ®istry); err != nil { return nil, fmt.Errorf("parse managed registry: %w", err) } baseDir := filepath.Dir(path) ids := make(map[string]struct{}, len(registry.Services)) for i := range registry.Services { svc := ®istry.Services[i] if err := normalizeService(baseDir, svc); err != nil { return nil, fmt.Errorf("service[%d]: %w", i, err) } if _, exists := ids[svc.ID]; exists { return nil, fmt.Errorf("duplicate service id %q", svc.ID) } ids[svc.ID] = struct{}{} } if registry.Services == nil { registry.Services = []Service{} } return ®istry, nil } func (r *Registry) Get(id string) (Service, bool) { for _, svc := range r.Services { if svc.ID == id { return svc, true } } return Service{}, false } func normalizeService(baseDir string, svc *Service) error { svc.ID = strings.TrimSpace(svc.ID) svc.DisplayName = strings.TrimSpace(svc.DisplayName) svc.ProjectType = strings.TrimSpace(svc.ProjectType) svc.ContainerName = strings.TrimSpace(svc.ContainerName) svc.APIBaseURL = strings.TrimSpace(svc.APIBaseURL) svc.ServiceName = strings.TrimSpace(svc.ServiceName) svc.ConfigPath = strings.TrimSpace(svc.ConfigPath) svc.RTSPField = strings.TrimSpace(svc.RTSPField) svc.ResultType = strings.TrimSpace(svc.ResultType) if svc.ID == "" { return errors.New("id is required") } if svc.DisplayName == "" { return errors.New("display_name is required") } if svc.ProjectType == "" { return errors.New("project_type is required") } if svc.ContainerName == "" { return errors.New("container_name is required") } if svc.APIBaseURL == "" { return errors.New("api_base_url is required") } if svc.ResultType == "" { return errors.New("result_type is required") } projectRoot := strings.TrimSpace(svc.ProjectRoot) if projectRoot == "" { return errors.New("project_root is required") } svc.ProjectRoot = resolvePath(baseDir, projectRoot) if svc.ServiceName == "" { svc.ServiceName = svc.ContainerName } if svc.ConfigPath != "" { svc.ConfigPath = resolvePath(baseDir, svc.ConfigPath) } if svc.ResultPaths == nil { svc.ResultPaths = map[string]string{} } for key, path := range svc.ResultPaths { svc.ResultPaths[key] = resolvePath(baseDir, strings.TrimSpace(path)) } return nil } func resolvePath(baseDir, path string) string { if filepath.IsAbs(path) { return filepath.Clean(path) } return filepath.Clean(filepath.Join(baseDir, path)) }