package eventcatalog import ( "context" "errors" "fmt" "strings" ) var ( ErrCatalogNotFound = errors.New("event catalog: version not found") ErrCatalogConflict = errors.New("event catalog: bootstrap conflict") ) type CatalogStatus string const ( CatalogStatusDraft CatalogStatus = "DRAFT" CatalogStatusApproved CatalogStatus = "APPROVED" CatalogStatusActive CatalogStatus = "ACTIVE" CatalogStatusRetired CatalogStatus = "RETIRED" ) type CatalogState struct { Version string Status CatalogStatus CoverageStartJalaliYear int CoverageEndJalaliYear int SourceURI string SourceVersion string SourceChecksum string ImportedBy string } type BootstrapResult struct { Version string Status CatalogStatus Transitions []string NoOp bool CreatedJobs int } type bootstrapStore interface { CatalogState(context.Context, string) (CatalogState, error) ActiveCatalogState(context.Context) (CatalogState, error) ImportDraft(context.Context, Import) error Approve(context.Context, string, string) error ActivateInitial(context.Context, string, string) error } type BootstrapScheduler func(context.Context) (int, error) func BootstrapCatalog(ctx context.Context, store bootstrapStore, input Import, currentJalaliYear int, reviewer, operator string, schedule BootstrapScheduler) (BootstrapResult, error) { reviewer = strings.TrimSpace(reviewer) operator = strings.TrimSpace(operator) if err := ValidateImport(input, currentJalaliYear); err != nil { return BootstrapResult{}, err } if err := ValidateReviewableChecksum(input.SourceChecksum); err != nil { return BootstrapResult{}, err } if err := ValidateApproval(input.ImportedBy, reviewer); err != nil { return BootstrapResult{}, err } if operator == "" { return BootstrapResult{}, errors.New("event catalog: activation operator is required") } if schedule == nil { return BootstrapResult{}, errors.New("event catalog: bootstrap scheduler is required") } active, err := store.ActiveCatalogState(ctx) if err != nil && !errors.Is(err, ErrCatalogNotFound) { return BootstrapResult{}, fmt.Errorf("loading active calendar catalog: %w", err) } if active.Version != "" && active.Version != input.Version { return BootstrapResult{}, fmt.Errorf("%w: active version %q differs from requested version %q", ErrCatalogConflict, active.Version, input.Version) } imported := false state, err := store.CatalogState(ctx, input.Version) if errors.Is(err, ErrCatalogNotFound) { if err := store.ImportDraft(ctx, input); err != nil { return BootstrapResult{}, fmt.Errorf("importing calendar draft: %w", err) } state = stateFromImport(input, CatalogStatusDraft) imported = true } else if err != nil { return BootstrapResult{}, fmt.Errorf("loading calendar catalog version: %w", err) } else if !state.matches(input) { return BootstrapResult{}, fmt.Errorf("%w: version %q metadata does not match the reviewed file", ErrCatalogConflict, input.Version) } result := BootstrapResult{Version: input.Version} if imported { result.Transitions = append(result.Transitions, "IMPORTED") } if state.Status == CatalogStatusDraft && active.Version == "" { if err := store.Approve(ctx, input.Version, reviewer); err != nil { return BootstrapResult{}, fmt.Errorf("approving calendar catalog: %w", err) } result.Transitions = append(result.Transitions, "APPROVED") state.Status = CatalogStatusApproved } if state.Status == CatalogStatusApproved && active.Version == "" { if err := store.ActivateInitial(ctx, input.Version, operator); err != nil { return BootstrapResult{}, fmt.Errorf("activating calendar catalog: %w", err) } result.Transitions = append(result.Transitions, "ACTIVATED") state.Status = CatalogStatusActive } if state.Status != CatalogStatusActive { return BootstrapResult{}, fmt.Errorf("%w: version %q has status %q", ErrCatalogConflict, input.Version, state.Status) } created, err := schedule(ctx) if err != nil { return BootstrapResult{}, fmt.Errorf("scheduling calendar refresh: %w", err) } result.Status = CatalogStatusActive result.CreatedJobs = created result.NoOp = len(result.Transitions) == 0 && created == 0 return result, nil } func stateFromImport(input Import, status CatalogStatus) CatalogState { return CatalogState{ Version: input.Version, Status: status, CoverageStartJalaliYear: input.CoverageStartJalaliYear, CoverageEndJalaliYear: input.CoverageEndJalaliYear, SourceURI: input.SourceURI, SourceVersion: input.SourceVersion, SourceChecksum: input.SourceChecksum, ImportedBy: input.ImportedBy, } } func (state CatalogState) matches(input Import) bool { return state == stateFromImport(input, state.Status) }