package main import ( "bytes" "compress/gzip" "encoding/json" "fmt" "os" "sort" "strings" "time" "gopkg.in/yaml.v3" ) const ( catalogMapPath = "db/catalog.map.yml" catalogPath = "db/catalog.json" catalogVersion = 1 ) type catalogTag struct { Tag string Name string Icon string } func (t *catalogTag) UnmarshalYAML(node *yaml.Node) error { if node.Kind == yaml.ScalarNode { return node.Decode(&t.Tag) } var raw struct { Tag string `yaml:"tag"` Name string `yaml:"name"` Icon string `yaml:"icon"` } if err := node.Decode(&raw); err != nil { return err } t.Tag, t.Name, t.Icon = raw.Tag, raw.Name, raw.Icon return nil } type catalogCategory struct { ID string `yaml:"id" json:"id"` Name string `yaml:"name" json:"name"` Sensitive bool `yaml:"sensitive" json:"sensitive,omitempty"` Tags []catalogTag `yaml:"tags" json:"-"` } type catalogService struct { ID string `json:"id"` Name string `json:"name"` Cat string `json:"cat"` Icon string `json:"icon,omitempty"` Src string `json:"src"` } type catalogFile struct { Version int `json:"version"` Generated string `json:"generated"` Base string `json:"base"` Categories []catalogCategory `json:"categories"` Services []catalogService `json:"services"` } func buildCatalog(index map[string]string, now time.Time) (int, int) { mapData, err := os.ReadFile(catalogMapPath) if err != nil { fmt.Println(" ", err) return 0, 0 } var categories []catalogCategory if err := yaml.Unmarshal(mapData, &categories); err != nil { fmt.Println(" ", err) return 0, 0 } out := catalogFile{Version: catalogVersion, Generated: now.Format(time.RFC3339), Categories: categories} mapped := make(map[string]struct{}, len(index)) var lost []string for _, cat := range categories { for _, t := range cat.Tags { url, ok := index[t.Tag] if !ok { lost = append(lost, cat.ID+"/"+t.Tag) continue } cut := strings.Index(url, "/source") if cut < 0 { lost = append(lost, cat.ID+"/"+t.Tag) continue } base := url[:cut+1] if out.Base == "" { out.Base = base } else if out.Base != base { fmt.Printf(" расхождение base: %s против %s\n", out.Base, base) } mapped[t.Tag] = struct{}{} svc := catalogService{ID: t.Tag, Name: t.Name, Cat: cat.ID, Src: url[cut+1:]} if svc.Name == "" { svc.Name = t.Tag } if t.Icon != "" && t.Icon != t.Tag { svc.Icon = t.Icon } out.Services = append(out.Services, svc) } } data, err := json.MarshalIndent(out, "", " ") if err != nil { fmt.Println(" ", err) return 0, 0 } os.WriteFile(catalogPath, data, 0644) var gzBuf bytes.Buffer gz, _ := gzip.NewWriterLevel(&gzBuf, gzip.BestCompression) gz.Write(data) gz.Close() os.WriteFile(catalogPath+".gz", gzBuf.Bytes(), 0644) unmapped := make([]string, 0, len(index)-len(mapped)) for tag := range index { if _, ok := mapped[tag]; !ok { unmapped = append(unmapped, tag) } } sort.Strings(unmapped) sort.Strings(lost) fmt.Printf(" %d категорий, %d сервисов, вне каталога %d тегов, потеряно %d\n", len(out.Categories), len(out.Services), len(unmapped), len(lost)) writeCatalogSummary(unmapped, lost) return len(out.Categories), len(out.Services) } func writeCatalogSummary(unmapped, lost []string) { path := os.Getenv("GITHUB_STEP_SUMMARY") if path == "" { return } f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return } defer f.Close() fmt.Fprintf(f, "## Каталог\n\n") if len(lost) > 0 { fmt.Fprintf(f, "### Теги карты, которых нет в database.json (%d)\n\n%s\n\n", len(lost), strings.Join(lost, ", ")) } fmt.Fprintf(f, "### Теги вне каталога (%d)\n\n%s\n", len(unmapped), strings.Join(unmapped, ", ")) } func runCatalogOnly() { data, err := os.ReadFile("db/database.json") if err != nil { fmt.Println(" ", err) os.Exit(1) } var index map[string]string if err := json.Unmarshal(data, &index); err != nil { fmt.Println(" ", err) os.Exit(1) } fmt.Println("=== Генерация db/catalog.json ===") buildCatalog(index, time.Now().UTC()) }