refactor: decompose Analyzer into recordPool and serviceAccumulator

Restructure the service analyzer architecture to improve maintainability:

- Extract recordPool (zone records + mark-delete claiming) and
  serviceAccumulator (service registry + domain normalization) as
  embedded structs in Analyzer
- Replace swap-delete with mark-delete to eliminate mutation-during-iteration
- Centralize domain normalization using helpers.DomainRelative
- Make Comment/NbResources lazy via Service.MarshalJSON instead of
  eager assignment at three separate call sites
- Extract SPF merging from usecase layer into services.CollectAndMergeSPF
- Add GetDefaultTTL accessor and comprehensive Analyzer doc comments
- Add round-trip test infrastructure covering MX, CNAME, CAA, TXT, SPF,
  DMARC, GSuite, Origin, Server and more
This commit is contained in:
nemunaire 2026-03-14 00:05:45 +07:00
commit f4bcb1c9cf
10 changed files with 797 additions and 167 deletions

View file

@ -23,7 +23,6 @@ package zone
import (
"fmt"
"strings"
"github.com/miekg/dns"
@ -62,13 +61,6 @@ func (uc *ListRecordsUsecase) ToZoneFile(domain *happydns.Domain, zone *happydns
func (uc *ListRecordsUsecase) List(domain *happydns.Domain, zone *happydns.Zone) (rrs []happydns.Record, err error) {
var svc_rrs []happydns.Record
// Collect SPF contributions keyed by absolute domain name.
type spfContrib struct {
directives [][]string
policies []string
}
spfContribs := map[string]*spfContrib{}
for _, services := range zone.Services {
for _, svc := range services {
svc_rrs, err = uc.serviceListRecordsUC.List(svc, domain.DomainName, zone.DefaultTTL)
@ -76,45 +68,6 @@ func (uc *ListRecordsUsecase) List(domain *happydns.Domain, zone *happydns.Zone)
return
}
// If the service is an SPF contributor, collect its directives
// and filter out any SPF TXT records it emits (they'll be
// replaced by the merged record).
if contributor, ok := svc.Service.(happydns.SPFContributor); ok {
directives := contributor.GetSPFDirectives()
policy := contributor.GetSPFAllPolicy()
// Compute the absolute domain for this service.
absDomain := svc.Domain
if domain.DomainName != "" {
if absDomain == "" {
absDomain = domain.DomainName
} else {
absDomain = absDomain + "." + domain.DomainName
}
}
if !strings.HasSuffix(absDomain, ".") {
absDomain += "."
}
if spfContribs[absDomain] == nil {
spfContribs[absDomain] = &spfContrib{}
}
spfContribs[absDomain].directives = append(spfContribs[absDomain].directives, directives)
if policy != "" {
spfContribs[absDomain].policies = append(spfContribs[absDomain].policies, policy)
}
// Drop SPF TXT records from this service's output.
filtered := svc_rrs[:0]
for _, rr := range svc_rrs {
if txt, ok := rr.(*happydns.TXT); ok && strings.HasPrefix(txt.Txt, "v=spf1") {
continue
}
filtered = append(filtered, rr)
}
svc_rrs = filtered
}
rrs = append(rrs, svc_rrs...)
}
@ -127,28 +80,8 @@ func (uc *ListRecordsUsecase) List(domain *happydns.Domain, zone *happydns.Zone)
}
}
// Emit one merged SPF TXT record per domain that has SPF contributions.
for absDomain, contrib := range spfContribs {
merged := svcs.MergeSPFDirectives(contrib.directives...)
policy := svcs.ResolveSPFAllPolicy(contrib.policies)
merged = append(merged, policy)
spfFields := svcs.SPFFields{
Version: 1,
Directives: merged,
}
rr := &happydns.TXT{
Hdr: dns.RR_Header{
Name: absDomain,
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: zone.DefaultTTL,
},
Txt: spfFields.String(),
}
rrs = append(rrs, rr)
}
// Collect SPF contributions and merge into single records per domain.
rrs = svcs.CollectAndMergeSPF(domain.DomainName, zone, rrs, zone.DefaultTTL)
return
}

View file

@ -52,8 +52,6 @@ func (uc *AddToZoneUsecase) AddService(zone *happydns.Zone, subdomain happydns.S
service.Id = hash
service.Domain = string(subdomain)
service.NbResources = service.Service.GetNbResources()
service.Comment = service.Service.GenComment()
zone.Services[subdomain] = append(zone.Services[subdomain], service)