package catalogue import ( "bytes" "encoding/json" "fmt" "regexp" "strings" ) // What a module may call its state, and whose state it may ask to read (novox/hq ADR 0201). // // A module names its state **locally** — `servers`, never a bucket or a subject — and another // module's as `.`, the way a consumed event names its emitter (design 32 §1). The // mesh derives the bucket from the two names, so the module and the local name must each be one // token: the bucket joins them with an underscore, which neither may contain, so two modules can // never derive one bucket. // stateName is one local name of a module's state: lower-case, no dot, no underscore. var stateName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) // The mesh's caps on what a module may ask of a bucket's history. const ( // StateMostHistory is the most past values a key may keep. The server's own limit. StateMostHistory = 64 ) // StateDeclaration is one bucket a module owns: its local name, and the options that are the // owner's to choose, as a seat chooses how long its backlog survives (design 32 §3). type StateDeclaration struct { Name string `json:"name"` // History is how many values a key keeps, the current one included; zero is one. History int `json:"history,omitempty"` // TTLSeconds is how long a value lives once written; zero is until it is replaced or deleted. TTLSeconds int `json:"ttl-seconds,omitempty"` } // UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds}. func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { trimmed := bytes.TrimSpace(raw) if len(trimmed) > 0 && trimmed[0] == '"' { return json.Unmarshal(trimmed, &s.Name) } type plain StateDeclaration var full plain dec := json.NewDecoder(bytes.NewReader(trimmed)) dec.DisallowUnknownFields() if err := dec.Decode(&full); err != nil { return fmt.Errorf("a state is either a name or {name, history, ttl-seconds}: %w", err) } *s = StateDeclaration(full) return nil } // MarshalJSON writes back the short form when there is nothing else to say. func (s StateDeclaration) MarshalJSON() ([]byte, error) { if s.History == 0 && s.TTLSeconds == 0 { return json.Marshal(s.Name) } type plain StateDeclaration return json.Marshal(plain(s)) } // ReadState splits a read into the owning module and the local name, or says why it is not one. func ReadState(read string) (module, local string, err error) { at := strings.LastIndex(read, ".") if at <= 0 || at == len(read)-1 { return "", "", fmt.Errorf("%q does not name a module and its state: a read is .", read) } module, local = read[:at], read[at+1:] if !stateName.MatchString(module) { return "", "", fmt.Errorf("%q cannot own state: a module whose state is read is one plain name", module) } if !stateName.MatchString(local) { return "", "", fmt.Errorf("%q is not a state name: lower-case letters, digits and hyphens", local) } return module, local, nil } // StateProblems is what is wrong with a manifest's state and reads. // // Refused at registration, because a bucket name the bus cannot hold is a module that installs, // starts, and is refused on its first write with a reason about a bucket nobody named. func StateProblems(m Manifest) []string { var problems []string if len(m.State) > 0 && !stateName.MatchString(m.Module) { problems = append(problems, fmt.Sprintf( "%s keeps state, and a module's name is part of its buckets' names, which take one plain "+ "name — no dot (novox/hq ADR 0201)", m.Module)) } seen := map[string]bool{} for _, s := range m.State { switch { case !stateName.MatchString(s.Name): problems = append(problems, fmt.Sprintf( "%s keeps state %q: a state is named locally — lower-case letters, digits and hyphens, "+ "no dot and no underscore; the mesh derives the bucket (novox/hq ADR 0201)", m.Module, s.Name)) case seen[s.Name]: problems = append(problems, fmt.Sprintf("%s keeps state %q twice", m.Module, s.Name)) } seen[s.Name] = true if s.History < 0 || s.History > StateMostHistory { problems = append(problems, fmt.Sprintf( "%s keeps %d values of %q; a key keeps between 1 and %d", m.Module, s.History, s.Name, StateMostHistory)) } if s.TTLSeconds < 0 { problems = append(problems, fmt.Sprintf("%s gives %q a negative lifetime", m.Module, s.Name)) } } for _, r := range m.Reads { module, _, err := ReadState(r) if err != nil { problems = append(problems, fmt.Sprintf("%s reads %v", m.Module, err)) continue } if module == m.Module { problems = append(problems, fmt.Sprintf( "%s reads %q, which is its own state: a module reads and writes what it keeps already", m.Module, r)) } } return problems } // StateReadsNothingDeclares is every read across a catalogue whose owner is present and declares no // such state. An absent owner says nothing — a module may be installed long before the one whose // state it reads, as a consumer may before its emitter (design 32 §1). func StateReadsNothingDeclares(manifests []Manifest) []string { declared := map[string]map[string]bool{} for _, m := range manifests { own := map[string]bool{} for _, s := range m.State { own[s.Name] = true } declared[m.Module] = own } var problems []string for _, m := range manifests { for _, r := range m.Reads { module, local, err := ReadState(r) if err != nil { continue } if own, present := declared[module]; present && !own[local] { problems = append(problems, fmt.Sprintf( "%s reads %q, and %s keeps no state called %q", m.Module, r, module, local)) } } } return problems }