package graders import ( "errors" "fmt" "regexp" "sync" ) // ErrDuplicateCode reports two graders published with the same code. var ErrDuplicateCode = errors.New("quizzes: duplicate grader code") // ErrReservedCode reports a grader that claims the built-in Manual code. var ErrReservedCode = errors.New("quizzes: grader code manual is built in") // codePattern is the shape of a grader code: it is stored in the quiz's // grader column (VARCHAR(64)). var codePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{0,63}$`) // ValidCode reports whether code has the shape of a grader code: lowercase // letters, digits and _, starting with a letter, at most 64 characters. // It says nothing about whether a plugin registered it. func ValidCode(code string) bool { return codePattern.MatchString(code) } // Option is one choice of the admin grader dropdown. type Option struct { Code string Label string } // Registry is the ordered set of graders collected at boot: Manual first, // then every activated plugin's graders in activation order. golem15.quizzes // publishes it on the app as *graders.Registry. Every method is safe on a // nil *Registry, which knows only Manual. type Registry struct { mu sync.RWMutex order []string byCode map[string]entry } type entry struct { grader Grader pluginID string } // NewRegistry returns a registry that knows only Manual. func NewRegistry() *Registry { return &Registry{byCode: map[string]entry{}} } // Add registers g as published by pluginID. A nil grader, an empty or // malformed code, Manual (ErrReservedCode) and a code already registered // (ErrDuplicateCode, naming both plugins) are errors. func (r *Registry) Add(pluginID string, g Grader) error { if r == nil { return fmt.Errorf("quizzes: grader registry is nil") } if g == nil { return fmt.Errorf("quizzes: plugin %s published a nil grader", pluginID) } code := g.Code() switch { case code == "": return fmt.Errorf("quizzes: plugin %s published a grader without a code", pluginID) case code == Manual: return fmt.Errorf("%w: published by %s", ErrReservedCode, pluginID) case !codePattern.MatchString(code): return fmt.Errorf("quizzes: plugin %s published grader code %q; want lowercase letters, digits and _ (at most 64)", pluginID, code) } r.mu.Lock() defer r.mu.Unlock() if r.byCode == nil { r.byCode = map[string]entry{} } if existing, ok := r.byCode[code]; ok { return fmt.Errorf("%w: %q is published by %s and %s", ErrDuplicateCode, code, existing.pluginID, pluginID) } r.byCode[code] = entry{grader: g, pluginID: pluginID} r.order = append(r.order, code) return nil } // Get returns the grader with code. Manual and unknown codes are false: // there is nothing to call, the owner grades. func (r *Registry) Get(code string) (Grader, bool) { if r == nil || code == Manual { return nil, false } r.mu.RLock() defer r.mu.RUnlock() e, ok := r.byCode[code] if !ok { return nil, false } return e.grader, true } // Codes lists Manual and then every registered code in registration order. func (r *Registry) Codes() []string { out := []string{Manual} if r == nil { return out } r.mu.RLock() defer r.mu.RUnlock() return append(out, r.order...) } // Options lists the admin dropdown choices: Manual with ManualLabel, then // every registered grader with its Label. func (r *Registry) Options() []Option { out := []Option{{Code: Manual, Label: ManualLabel}} if r == nil { return out } r.mu.RLock() defer r.mu.RUnlock() for _, code := range r.order { out = append(out, Option{Code: code, Label: r.byCode[code].grader.Label()}) } return out } // PluginID returns the id of the plugin that published code. Manual is // golem15.quizzes' own. func (r *Registry) PluginID(code string) (string, bool) { if code == Manual { return "golem15.quizzes", true } if r == nil { return "", false } r.mu.RLock() defer r.mu.RUnlock() e, ok := r.byCode[code] return e.pluginID, ok }