package catalogue import ( "bytes" "encoding/json" "fmt" ) // What a module may call its logs (novox/hq ADR 0297). // // A log is a module's record of operations: entries appended under a key, each kept as long as the // log is, in the order they came. A module names each log it owns **locally** — `changes`, never a // stream or a subject (ADR 0201 §4) — and the mesh derives the stream from the module and the local // name, as it derives a bucket. So the rule for a log's name is a state name's: one plain token, and // a module that keeps a log has a name that is one plain token too. // The mesh's caps on a log, in MiB: what a module may ask, and what it gets when it asks nothing. const ( LogLeastMiB = 1 LogMostMiB = 8192 LogDefaultMiB = 1024 ) // LogDeclaration is one log a module owns: its local name, and how large it may grow. type LogDeclaration struct { Name string `json:"name"` // MaxMiB is the log's cap in MiB; zero is LogDefaultMiB. When full, the log refuses new entries // and never drops old ones. MaxMiB int `json:"max-mib,omitempty"` // MaxAgeDays is how long an entry is kept, in days; zero is as long as the log (novox/hq ADR 0297). The one way an // entry leaves a log: the server ages it out, never a delete or a purge (novox/hq ADR 0565 §10: the hand-act log // keeps every act 400 days; issue 1953 S-age). MaxAgeDays int `json:"max-age-days,omitempty"` } // LogMostAgeDays is the longest a log may keep an entry by age: past it, a log keeps its entries as long as it is. const LogMostAgeDays = 3650 // Cap is the log's cap in MiB, the default where none is said. func (l LogDeclaration) Cap() int { if l.MaxMiB == 0 { return LogDefaultMiB } return l.MaxMiB } // UnmarshalJSON reads a log as its bare name, or as {name, max-mib, max-age-days}. func (l *LogDeclaration) UnmarshalJSON(raw []byte) error { trimmed := bytes.TrimSpace(raw) if len(trimmed) > 0 && trimmed[0] == '"' { return json.Unmarshal(trimmed, &l.Name) } var full struct { Name string `json:"name"` MaxMiB *int `json:"max-mib"` MaxAgeDays *int `json:"max-age-days"` } dec := json.NewDecoder(bytes.NewReader(trimmed)) dec.DisallowUnknownFields() if err := dec.Decode(&full); err != nil { return fmt.Errorf("a log is either a name or {name, max-mib, max-age-days}: %w", typedUnknown(err)) } l.Name = full.Name l.MaxMiB = 0 if full.MaxMiB != nil { // Said, and said as nothing: refused rather than read as the default, which it did not say. if *full.MaxMiB == 0 { return fmt.Errorf("log %q: max-mib is between %d and %d, not 0", full.Name, LogLeastMiB, LogMostMiB) } l.MaxMiB = *full.MaxMiB } l.MaxAgeDays = 0 if full.MaxAgeDays != nil { if *full.MaxAgeDays < 1 || *full.MaxAgeDays > LogMostAgeDays { return fmt.Errorf("log %q: max-age-days is between 1 and %d, not %d", full.Name, LogMostAgeDays, *full.MaxAgeDays) } l.MaxAgeDays = *full.MaxAgeDays } return nil } // MarshalJSON writes back the short form when there is nothing else to say. func (l LogDeclaration) MarshalJSON() ([]byte, error) { if l.MaxMiB == 0 && l.MaxAgeDays == 0 { return json.Marshal(l.Name) } type plain LogDeclaration return json.Marshal(plain(l)) }