- Add MaxMind GeoLite2-City mmdb auto-download and IP-based geo lookup (covers all 249 countries globally, replacing limited 8-country regex) - Add RenameOptions with 5 toggleable fields (flag/alias/country/city/index) stored as rename_options_json on collections - Collection rename pipeline uses DetectGeoWithServer(name, server) which tries GeoIP lookup first, falls back to name-based regex matching - Chinese country/city names + emoji flag prefix - Country-city dedup: skip city when it equals country (e.g. 香港/香港) - URI fragment encoding: url.QueryEscape -> url.PathEscape (%20 not +) - Egress cache key excludes name field so renamed nodes still match cache - Frontend: 5 toggle buttons in collection edit form, default all on - Migrations: 0004 (source alias + collection rename_enabled), 0005 (collection rename_options_json)
705 lines
18 KiB
Go
705 lines
18 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
|
|
"github.com/peterqiu0516/sub-store/internal/model"
|
|
"github.com/peterqiu0516/sub-store/internal/template"
|
|
"github.com/peterqiu0516/sub-store/internal/util"
|
|
)
|
|
|
|
func getBuiltinTemplateIDs() map[string]bool {
|
|
return template.BuiltinTemplateIDs
|
|
}
|
|
|
|
// --- Source handlers ---
|
|
|
|
func (d *Deps) HandleListSources(c fiber.Ctx) error {
|
|
sources, err := d.SourceRepo.List()
|
|
if err != nil {
|
|
return failed(c, "Failed to list sources", 500)
|
|
}
|
|
return success(c, sources)
|
|
}
|
|
|
|
func (d *Deps) HandleCreateSource(c fiber.Ctx) error {
|
|
var input map[string]any
|
|
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
|
return failed(c, "Invalid JSON", 400)
|
|
}
|
|
if err := validateSourcePayload(input); err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
id := getStringValue(input["id"])
|
|
if id == "" {
|
|
id = getStringValue(input["name"])
|
|
}
|
|
if err := validateRecordId(id, "Source"); err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
existing, _ := d.SourceRepo.Get(id)
|
|
if existing != nil {
|
|
return failed(c, "Source id already exists", 409)
|
|
}
|
|
rec := mapToSourceRecord(input)
|
|
if err := validateSource(rec); err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
result, err := d.SourceRepo.Upsert(rec)
|
|
if err != nil {
|
|
return failed(c, "Failed to create source", 500)
|
|
}
|
|
return success(c, result)
|
|
}
|
|
|
|
func (d *Deps) HandleGetSource(c fiber.Ctx) error {
|
|
rec, err := d.SourceRepo.Get(c.Params("name"))
|
|
if err != nil || rec == nil {
|
|
return failed(c, "Source not found", 404)
|
|
}
|
|
return success(c, rec)
|
|
}
|
|
|
|
func (d *Deps) HandleUpdateSource(c fiber.Ctx) error {
|
|
name := c.Params("name")
|
|
existing, err := d.SourceRepo.Get(name)
|
|
if err != nil || existing == nil {
|
|
return failed(c, "Source not found", 404)
|
|
}
|
|
var input map[string]any
|
|
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
|
return failed(c, "Invalid JSON", 400)
|
|
}
|
|
merged := mergeSource(*existing, mapToSourceRecord(input))
|
|
if err := validateSource(merged); err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
result, err := d.SourceRepo.Upsert(merged)
|
|
if err != nil {
|
|
return failed(c, "Failed to update source", 500)
|
|
}
|
|
return success(c, result)
|
|
}
|
|
|
|
func (d *Deps) HandleDeleteSource(c fiber.Ctx) error {
|
|
name := c.Params("name")
|
|
existing, err := d.SourceRepo.Get(name)
|
|
if err != nil || existing == nil {
|
|
return failed(c, "Source not found", 404)
|
|
}
|
|
// Check references
|
|
collections, _ := d.CollectionRepo.List()
|
|
var references []string
|
|
for _, col := range collections {
|
|
for _, sid := range col.SourceIds {
|
|
if sid == existing.ID {
|
|
references = append(references, col.ID)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(references) > 0 {
|
|
return failed(c, "Source is used by collections: "+strings.Join(references, ", "), 409)
|
|
}
|
|
// Archive and delete — review-resolution #29
|
|
snapshot := sourceToMap(*existing)
|
|
_, err = d.RecycleRepo.ArchiveAndDelete("source", existing.ID, snapshot, "DELETE FROM sources WHERE id = ?", existing.ID)
|
|
if err != nil {
|
|
return failed(c, "Failed to delete source", 500)
|
|
}
|
|
return success(c, fiber.Map{"deleted": true, "references": []string{}})
|
|
}
|
|
|
|
func (d *Deps) HandleSortSources(c fiber.Ctx) error {
|
|
var input []map[string]any
|
|
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
|
return failed(c, "Invalid JSON", 400)
|
|
}
|
|
ids := extractIds(input)
|
|
if err := d.SourceRepo.Sort(ids); err != nil {
|
|
return failed(c, "Failed to sort sources", 500)
|
|
}
|
|
sources, _ := d.SourceRepo.List()
|
|
return success(c, sources)
|
|
}
|
|
|
|
func (d *Deps) HandleSortSourcesLegacy(c fiber.Ctx) error {
|
|
return d.HandleSortSources(c)
|
|
}
|
|
|
|
// --- Collection handlers ---
|
|
|
|
func (d *Deps) HandleListCollections(c fiber.Ctx) error {
|
|
collections, err := d.CollectionRepo.List()
|
|
if err != nil {
|
|
return failed(c, "Failed to list collections", 500)
|
|
}
|
|
return success(c, collections)
|
|
}
|
|
|
|
func (d *Deps) HandleCreateCollection(c fiber.Ctx) error {
|
|
var input map[string]any
|
|
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
|
return failed(c, "Invalid JSON", 400)
|
|
}
|
|
id := getStringValue(input["id"])
|
|
if id == "" {
|
|
id = getStringValue(input["name"])
|
|
}
|
|
if err := validateRecordId(id, "Collection"); err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
existing, _ := d.CollectionRepo.Get(id)
|
|
if existing != nil {
|
|
return failed(c, "Collection id already exists", 409)
|
|
}
|
|
rec := mapToCollectionRecord(input)
|
|
if err := d.validateCollection(rec); err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
result, err := d.CollectionRepo.Upsert(rec)
|
|
if err != nil {
|
|
return failed(c, "Failed to create collection", 500)
|
|
}
|
|
return success(c, result)
|
|
}
|
|
|
|
func (d *Deps) HandleGetCollection(c fiber.Ctx) error {
|
|
rec, err := d.CollectionRepo.Get(c.Params("name"))
|
|
if err != nil || rec == nil {
|
|
return failed(c, "Collection not found", 404)
|
|
}
|
|
return success(c, rec)
|
|
}
|
|
|
|
func (d *Deps) HandleUpdateCollection(c fiber.Ctx) error {
|
|
name := c.Params("name")
|
|
existing, err := d.CollectionRepo.Get(name)
|
|
if err != nil || existing == nil {
|
|
return failed(c, "Collection not found", 404)
|
|
}
|
|
var input map[string]any
|
|
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
|
return failed(c, "Invalid JSON", 400)
|
|
}
|
|
merged := mergeCollection(*existing, mapToCollectionRecord(input))
|
|
if err := d.validateCollection(merged); err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
result, err := d.CollectionRepo.Upsert(merged)
|
|
if err != nil {
|
|
return failed(c, "Failed to update collection", 500)
|
|
}
|
|
return success(c, result)
|
|
}
|
|
|
|
func (d *Deps) HandleDeleteCollection(c fiber.Ctx) error {
|
|
name := c.Params("name")
|
|
existing, err := d.CollectionRepo.Get(name)
|
|
if err != nil || existing == nil {
|
|
return failed(c, "Collection not found", 404)
|
|
}
|
|
snapshot := collectionToMap(*existing)
|
|
_, err = d.RecycleRepo.ArchiveAndDelete("collection", existing.ID, snapshot, "DELETE FROM collections WHERE id = ?", existing.ID)
|
|
if err != nil {
|
|
return failed(c, "Failed to delete collection", 500)
|
|
}
|
|
return success(c, fiber.Map{"deleted": true})
|
|
}
|
|
|
|
func (d *Deps) HandleSortCollections(c fiber.Ctx) error {
|
|
var input []map[string]any
|
|
if err := json.Unmarshal(c.Body(), &input); err != nil {
|
|
return failed(c, "Invalid JSON", 400)
|
|
}
|
|
ids := extractIds(input)
|
|
if err := d.CollectionRepo.Sort(ids); err != nil {
|
|
return failed(c, "Failed to sort collections", 500)
|
|
}
|
|
collections, _ := d.CollectionRepo.List()
|
|
return success(c, collections)
|
|
}
|
|
|
|
func (d *Deps) HandleSortCollectionsLegacy(c fiber.Ctx) error {
|
|
return d.HandleSortCollections(c)
|
|
}
|
|
|
|
// --- Template handlers ---
|
|
|
|
func (d *Deps) HandleListTemplates(c fiber.Ctx) error {
|
|
// Merge built-in + DB templates
|
|
dbTemplates, err := d.TemplateRepo.List()
|
|
if err != nil {
|
|
dbTemplates = []model.TemplateRecord{}
|
|
}
|
|
result := make([]fiber.Map, 0, len(template.BuiltinTemplates)+len(dbTemplates))
|
|
for _, bt := range template.BuiltinTemplates {
|
|
result = append(result, fiber.Map{
|
|
"id": bt.ID,
|
|
"name": bt.Name,
|
|
"target": bt.Target,
|
|
"config": bt.Config,
|
|
"readonly": true,
|
|
})
|
|
}
|
|
for _, t := range dbTemplates {
|
|
if !template.BuiltinTemplateIDs[t.ID] {
|
|
result = append(result, fiber.Map{
|
|
"id": t.ID,
|
|
"name": t.Name,
|
|
"target": t.Target,
|
|
"config": t.Config,
|
|
"readonly": false,
|
|
})
|
|
}
|
|
}
|
|
return success(c, result)
|
|
}
|
|
|
|
func (d *Deps) HandleGetTemplate(c fiber.Ctx) error {
|
|
name := c.Params("name")
|
|
// Check built-in first
|
|
for _, bt := range template.BuiltinTemplates {
|
|
if bt.ID == name {
|
|
return success(c, fiber.Map{
|
|
"id": bt.ID, "name": bt.Name, "target": bt.Target,
|
|
"config": bt.Config, "readonly": true,
|
|
})
|
|
}
|
|
}
|
|
rec, err := d.TemplateRepo.Get(name)
|
|
if err != nil || rec == nil {
|
|
return failed(c, "Template not found", 404)
|
|
}
|
|
return success(c, fiber.Map{
|
|
"id": rec.ID, "name": rec.Name, "target": rec.Target,
|
|
"config": rec.Config, "readonly": false,
|
|
})
|
|
}
|
|
|
|
func (d *Deps) HandleCreateTemplate(c fiber.Ctx) error {
|
|
input, _ := parseJsonOrText(c)
|
|
id := getStringValue(input["id"])
|
|
if id == "" {
|
|
id = getStringValue(input["name"])
|
|
}
|
|
if id == "" {
|
|
return failed(c, "Template name is required", 400)
|
|
}
|
|
if template.BuiltinTemplateIDs[id] {
|
|
return failed(c, "Built-in templates cannot be modified", 400)
|
|
}
|
|
rec := mapToTemplateRecord(input)
|
|
result, err := d.TemplateRepo.Upsert(rec)
|
|
if err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
return success(c, fiber.Map{
|
|
"id": result.ID, "name": result.Name, "target": result.Target,
|
|
"config": result.Config, "readonly": false,
|
|
})
|
|
}
|
|
|
|
func (d *Deps) HandleUpdateTemplate(c fiber.Ctx) error {
|
|
name := c.Params("name")
|
|
if template.BuiltinTemplateIDs[name] {
|
|
return failed(c, "Built-in templates cannot be modified", 400)
|
|
}
|
|
existing, err := d.TemplateRepo.Get(name)
|
|
if err != nil || existing == nil {
|
|
return failed(c, "Template not found", 404)
|
|
}
|
|
input, _ := parseJsonOrText(c)
|
|
input["id"] = name
|
|
rec := mapToTemplateRecord(input)
|
|
result, err := d.TemplateRepo.Upsert(rec)
|
|
if err != nil {
|
|
return failed(c, err.Error(), 400)
|
|
}
|
|
return success(c, fiber.Map{
|
|
"id": result.ID, "name": result.Name, "target": result.Target,
|
|
"config": result.Config, "readonly": false,
|
|
})
|
|
}
|
|
|
|
func (d *Deps) HandleDeleteTemplate(c fiber.Ctx) error {
|
|
name := c.Params("name")
|
|
if template.BuiltinTemplateIDs[name] {
|
|
return failed(c, "Built-in templates cannot be deleted", 400)
|
|
}
|
|
existing, err := d.TemplateRepo.Get(name)
|
|
if err != nil || existing == nil {
|
|
return failed(c, "Template not found", 404)
|
|
}
|
|
snapshot := templateToMap(*existing)
|
|
_, err = d.RecycleRepo.ArchiveAndDelete("template", existing.ID, snapshot, "DELETE FROM templates WHERE id = ?", existing.ID)
|
|
if err != nil {
|
|
return failed(c, "Failed to delete template", 500)
|
|
}
|
|
return success(c, fiber.Map{"deleted": true})
|
|
}
|
|
|
|
// --- Validation helpers ---
|
|
|
|
func validateRecordId(id, label string) error {
|
|
if id == "" {
|
|
return errWrap(label + " id is required")
|
|
}
|
|
for _, c := range id {
|
|
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' || c == '-') {
|
|
return errWrap(label + " id must use 1-64 lowercase letters, numbers, underscores, or hyphens")
|
|
}
|
|
}
|
|
if len(id) > 64 {
|
|
return errWrap(label + " id must use 1-64 lowercase letters, numbers, underscores, or hyphens")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSourcePayload(input map[string]any) error {
|
|
if t, ok := input["type"]; ok {
|
|
if t != "remote" && t != "local" {
|
|
return errWrap("Source type must be remote or local")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSource(rec model.SourceRecord) error {
|
|
if err := validateRecordId(rec.ID, "Source"); err != nil {
|
|
return err
|
|
}
|
|
if rec.Type == "local" {
|
|
if rec.Content == "" {
|
|
return errWrap("Local source content is required")
|
|
}
|
|
return nil
|
|
}
|
|
urls := splitLines(rec.URL)
|
|
if len(urls) == 0 {
|
|
return errWrap("Remote source URL is required")
|
|
}
|
|
for _, u := range urls {
|
|
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
|
|
return errWrap("Remote source URLs must use http or https")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *Deps) validateCollection(rec model.CollectionRecord) error {
|
|
if err := validateRecordId(rec.ID, "Collection"); err != nil {
|
|
return err
|
|
}
|
|
if len(rec.SourceIds) > 0 {
|
|
sources, _ := d.SourceRepo.List()
|
|
sourceIdSet := make(map[string]bool, len(sources))
|
|
for _, s := range sources {
|
|
sourceIdSet[s.ID] = true
|
|
}
|
|
for _, sid := range rec.SourceIds {
|
|
if !sourceIdSet[sid] {
|
|
return errWrap("Collection references missing sources: " + sid)
|
|
}
|
|
}
|
|
}
|
|
if rec.TemplateId != "" {
|
|
// Check built-in
|
|
if !template.BuiltinTemplateIDs[rec.TemplateId] {
|
|
t, _ := d.TemplateRepo.Get(rec.TemplateId)
|
|
if t == nil {
|
|
return errWrap("Collection references missing template: " + rec.TemplateId)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- Mapping helpers ---
|
|
|
|
func mapToSourceRecord(input map[string]any) model.SourceRecord {
|
|
id := getStringValue(input["id"])
|
|
if id == "" {
|
|
id = getStringValue(input["name"])
|
|
}
|
|
id = util.ToId(id)
|
|
sourceType := "remote"
|
|
if getStringValue(input["type"]) == "local" {
|
|
sourceType = "local"
|
|
}
|
|
enabled := true
|
|
if e, ok := input["enabled"].(bool); ok && !e {
|
|
enabled = false
|
|
}
|
|
filters := parseFilters(input["filters"])
|
|
meta := getMapValue(input["meta"])
|
|
alias := getStringValue(input["alias"])
|
|
return model.SourceRecord{
|
|
ID: id,
|
|
Name: orDefault(getStringValue(input["name"]), id),
|
|
Alias: alias,
|
|
Type: sourceType,
|
|
URL: getStringValue(input["url"]),
|
|
Content: getStringValue(input["content"]),
|
|
Enabled: enabled,
|
|
Filters: filters,
|
|
Meta: meta,
|
|
}
|
|
}
|
|
|
|
func mapToCollectionRecord(input map[string]any) model.CollectionRecord {
|
|
id := getStringValue(input["id"])
|
|
if id == "" {
|
|
id = getStringValue(input["name"])
|
|
}
|
|
id = util.ToId(id)
|
|
sourceIds := getStringArray(input["sourceIds"])
|
|
renameEnabled := false
|
|
if v, ok := input["renameEnabled"].(bool); ok {
|
|
renameEnabled = v
|
|
}
|
|
|
|
// Parse rename options (all default to true when renameEnabled is on)
|
|
var renameOptions *model.RenameOptions
|
|
if opts, ok := input["renameOptions"].(map[string]any); ok {
|
|
ro := model.RenameOptions{
|
|
Flag: true,
|
|
Alias: true,
|
|
Country: true,
|
|
City: true,
|
|
Index: true,
|
|
}
|
|
if v, ok := opts["flag"].(bool); ok {
|
|
ro.Flag = v
|
|
}
|
|
if v, ok := opts["alias"].(bool); ok {
|
|
ro.Alias = v
|
|
}
|
|
if v, ok := opts["country"].(bool); ok {
|
|
ro.Country = v
|
|
}
|
|
if v, ok := opts["city"].(bool); ok {
|
|
ro.City = v
|
|
}
|
|
if v, ok := opts["index"].(bool); ok {
|
|
ro.Index = v
|
|
}
|
|
renameOptions = &ro
|
|
}
|
|
|
|
templateId := getStringValue(input["templateId"])
|
|
if templateId == "" || templateId == "default" {
|
|
templateId = template.DefaultTemplateID
|
|
}
|
|
ignoreFailed := true
|
|
if v, ok := input["ignoreFailed"].(bool); ok {
|
|
ignoreFailed = v
|
|
}
|
|
enabled := true
|
|
if e, ok := input["enabled"].(bool); ok && !e {
|
|
enabled = false
|
|
}
|
|
meta := getMapValue(input["meta"])
|
|
return model.CollectionRecord{
|
|
ID: id,
|
|
Name: orDefault(getStringValue(input["name"]), id),
|
|
SourceIds: sourceIds,
|
|
RenameEnabled: renameEnabled,
|
|
RenameOptions: renameOptions,
|
|
TemplateId: templateId,
|
|
IgnoreFailed: ignoreFailed,
|
|
Enabled: enabled,
|
|
Meta: meta,
|
|
}
|
|
}
|
|
|
|
func mapToTemplateRecord(input map[string]any) model.TemplateRecord {
|
|
id := getStringValue(input["id"])
|
|
if id == "" {
|
|
id = getStringValue(input["name"])
|
|
}
|
|
id = util.ToId(id)
|
|
target := model.NormalizeTargetAlias(input["target"])
|
|
if target == "" {
|
|
target = "mihomo"
|
|
}
|
|
config := getMapValue(input["config"])
|
|
if len(config) == 0 {
|
|
if content, ok := input["content"].(string); ok && content != "" {
|
|
// Try to parse content as YAML or JSON
|
|
config = parseTemplateConfig(content)
|
|
}
|
|
}
|
|
config = template.NormalizeMihomoTemplateConfig(config)
|
|
return model.TemplateRecord{
|
|
ID: id,
|
|
Name: orDefault(getStringValue(input["name"]), id),
|
|
Target: target,
|
|
Config: config,
|
|
}
|
|
}
|
|
|
|
func mergeSource(existing model.SourceRecord, next model.SourceRecord) model.SourceRecord {
|
|
merged := existing
|
|
if next.Name != "" {
|
|
merged.Name = next.Name
|
|
}
|
|
if next.Alias != "" {
|
|
merged.Alias = next.Alias
|
|
}
|
|
if next.Type != "" {
|
|
merged.Type = next.Type
|
|
}
|
|
if next.URL != "" {
|
|
merged.URL = next.URL
|
|
}
|
|
if next.Content != "" {
|
|
merged.Content = next.Content
|
|
}
|
|
merged.Enabled = next.Enabled
|
|
if next.Filters != nil {
|
|
merged.Filters = next.Filters
|
|
}
|
|
if next.Meta != nil {
|
|
merged.Meta = util.MergeDeep(existing.Meta, next.Meta)
|
|
}
|
|
return merged
|
|
}
|
|
|
|
func mergeCollection(existing model.CollectionRecord, next model.CollectionRecord) model.CollectionRecord {
|
|
merged := existing
|
|
if next.Name != "" {
|
|
merged.Name = next.Name
|
|
}
|
|
if next.SourceIds != nil {
|
|
merged.SourceIds = next.SourceIds
|
|
}
|
|
merged.RenameEnabled = next.RenameEnabled
|
|
if next.RenameOptions != nil {
|
|
merged.RenameOptions = next.RenameOptions
|
|
}
|
|
if next.TemplateId != "" {
|
|
merged.TemplateId = next.TemplateId
|
|
}
|
|
merged.IgnoreFailed = next.IgnoreFailed
|
|
merged.Enabled = next.Enabled
|
|
if next.Meta != nil {
|
|
merged.Meta = util.MergeDeep(existing.Meta, next.Meta)
|
|
}
|
|
return merged
|
|
}
|
|
|
|
// --- Utility helpers ---
|
|
|
|
type appError struct{ msg string }
|
|
|
|
func (e *appError) Error() string { return e.msg }
|
|
func errWrap(msg string) error { return &appError{msg: msg} }
|
|
|
|
func getStringValue(v any) string {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func orDefault(s, def string) string {
|
|
if s != "" {
|
|
return s
|
|
}
|
|
return def
|
|
}
|
|
|
|
func getMapValue(v any) map[string]any {
|
|
if m, ok := v.(map[string]any); ok {
|
|
return m
|
|
}
|
|
return map[string]any{}
|
|
}
|
|
|
|
func getStringArray(v any) []string {
|
|
if arr, ok := v.([]any); ok {
|
|
result := make([]string, 0, len(arr))
|
|
for _, item := range arr {
|
|
if s, ok := item.(string); ok && s != "" {
|
|
result = append(result, s)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
return []string{}
|
|
}
|
|
|
|
func parseFilters(v any) []model.FilterRule {
|
|
if arr, ok := v.([]any); ok {
|
|
data, _ := json.Marshal(arr)
|
|
var rules []model.FilterRule
|
|
json.Unmarshal(data, &rules)
|
|
if rules == nil {
|
|
rules = []model.FilterRule{}
|
|
}
|
|
return rules
|
|
}
|
|
return []model.FilterRule{}
|
|
}
|
|
|
|
func extractIds(input []map[string]any) []string {
|
|
ids := make([]string, 0, len(input))
|
|
for _, item := range input {
|
|
id := getStringValue(item["id"])
|
|
if id == "" {
|
|
id = getStringValue(item["name"])
|
|
}
|
|
if id != "" {
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
var result []string
|
|
for _, line := range strings.Split(s, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
result = append(result, line)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func sourceToMap(s model.SourceRecord) map[string]any {
|
|
data, _ := json.Marshal(s)
|
|
var m map[string]any
|
|
json.Unmarshal(data, &m)
|
|
return m
|
|
}
|
|
|
|
func collectionToMap(c model.CollectionRecord) map[string]any {
|
|
data, _ := json.Marshal(c)
|
|
var m map[string]any
|
|
json.Unmarshal(data, &m)
|
|
return m
|
|
}
|
|
|
|
func templateToMap(t model.TemplateRecord) map[string]any {
|
|
data, _ := json.Marshal(t)
|
|
var m map[string]any
|
|
json.Unmarshal(data, &m)
|
|
return m
|
|
}
|
|
|
|
func parseTemplateConfig(content string) map[string]any {
|
|
// Try JSON first
|
|
var m map[string]any
|
|
if err := json.Unmarshal([]byte(content), &m); err == nil {
|
|
return m
|
|
}
|
|
// Try YAML
|
|
// We'll import yaml.v3 inline
|
|
return parseYamlConfig(content)
|
|
}
|