package caps import ( "context" "encoding/json" "fmt" abiv1 "git.dev.alexdunmow.com/block/pluginsdk/abi/v1" "git.dev.alexdunmow.com/block/pluginsdk/plugin" "github.com/google/uuid" ) // provisionerStub implements plugin.Provisioner over provisioner.* capability // calls (WO-WZ-019). This is a LOAD-TIME capability: call it from the Load // hook. RegisterWithProvisioner still receives a no-op at Register/DESCRIBE // time (host functions are stubbed to fail there), so seed logic must move to // Load — see core/docs/wasm-abi.md. // // Most Provisioner methods carry no context (the Go interface predates the // ABI); calls run under the ambient invoke deadline the host enforces. type provisionerStub struct{ base } var _ plugin.Provisioner = (*provisionerStub)(nil) func (s *provisionerStub) EnsureDataTable(config plugin.DataTableConfig) error { req := &abiv1.ProvisionerEnsureDataTableRequest{ Key: config.Key, Name: config.Name, Description: config.Description, SchemaJson: config.Schema, PrimaryKey: config.PrimaryKey, } return s.invoke(context.Background(), "ensure_data_table", req, &abiv1.ProvisionerEnsureDataTableResponse{}) } func (s *provisionerStub) MergeSiteSettings(defaults map[string]any) error { defaultsJSON, err := json.Marshal(defaults) if err != nil { return fmt.Errorf("provisioner.merge_site_settings: marshal defaults: %w", err) } req := &abiv1.ProvisionerMergeSiteSettingsRequest{DefaultsJson: defaultsJSON} return s.invoke(context.Background(), "merge_site_settings", req, &abiv1.ProvisionerMergeSiteSettingsResponse{}) } func (s *provisionerStub) EnsureSetting(key string, defaultValue any) error { valueJSON, err := json.Marshal(defaultValue) if err != nil { return fmt.Errorf("provisioner.ensure_setting: marshal value: %w", err) } req := &abiv1.ProvisionerEnsureSettingRequest{Key: key, DefaultValueJson: valueJSON} return s.invoke(context.Background(), "ensure_setting", req, &abiv1.ProvisionerEnsureSettingResponse{}) } func (s *provisionerStub) EnsurePage(config plugin.PageConfig) error { seed := &abiv1.PageSeed{ Slug: config.Slug, ParentSlug: config.ParentSlug, Title: config.Title, TemplateKey: config.TemplateKey, DetailSourceType: config.DetailSourceType, DetailSourceKey: config.DetailSourceKey, DetailSlugField: config.DetailSlugField, ReconcileBlocks: config.ReconcileBlocks, ReconcileTemplate: config.ReconcileTemplate, } for _, b := range config.Blocks { contentJSON, err := json.Marshal(b.Content) if err != nil { return fmt.Errorf("provisioner.ensure_page: block %q: marshal content: %w", b.BlockKey, err) } seed.Blocks = append(seed.Blocks, &abiv1.PageBlock{ BlockKey: b.BlockKey, Title: b.Title, ContentJson: contentJSON, HtmlContent: b.HtmlContent, Slot: b.Slot, SortOrder: b.SortOrder, }) } req := &abiv1.ProvisionerEnsurePageRequest{Page: seed} return s.invoke(context.Background(), "ensure_page", req, &abiv1.ProvisionerEnsurePageResponse{}) } func (s *provisionerStub) OverrideSiteSettings(overrides map[string]any) error { overridesJSON, err := json.Marshal(overrides) if err != nil { return fmt.Errorf("provisioner.override_site_settings: marshal overrides: %w", err) } req := &abiv1.ProvisionerOverrideSiteSettingsRequest{OverridesJson: overridesJSON} return s.invoke(context.Background(), "override_site_settings", req, &abiv1.ProvisionerOverrideSiteSettingsResponse{}) } func (s *provisionerStub) EnsureMenuItem(menuName string, config plugin.MenuItemConfig) error { req := &abiv1.ProvisionerEnsureMenuItemRequest{ MenuName: menuName, Label: config.Label, Url: config.URL, PageSlug: config.PageSlug, SortOrder: config.SortOrder, } return s.invoke(context.Background(), "ensure_menu_item", req, &abiv1.ProvisionerEnsureMenuItemResponse{}) } func (s *provisionerStub) RegisterEmbeddingConfig(config plugin.EmbeddingConfigDef) error { req := &abiv1.ProvisionerRegisterEmbeddingConfigRequest{ TableKey: config.TableKey, TextTemplate: config.TextTemplate, Enabled: config.Enabled, } return s.invoke(context.Background(), "register_embedding_config", req, &abiv1.ProvisionerRegisterEmbeddingConfigResponse{}) } // EnsureEmbed crosses the template-rendered surface only: EmbedConfig. // RenderFunc is a function value that cannot serialize, so an ABI-provisioned // embed must carry a Template (a RenderFunc-only embed returns an error // rather than silently dropping the renderer). func (s *provisionerStub) EnsureEmbed(config plugin.EmbedConfig) error { if config.RenderFunc != nil && config.Template == "" { return fmt.Errorf("provisioner.ensure_embed: embed %q uses RenderFunc, which cannot cross the wasm ABI — provide a Template instead", config.Key) } req := &abiv1.ProvisionerEnsureEmbedRequest{ Key: config.Key, Title: config.Title, Description: config.Description, Icon: config.Icon, LabelField: config.LabelField, Template: config.Template, DataSourceType: config.DataSource.Type, DataSourceTableKey: config.DataSource.TableKey, } return s.invoke(context.Background(), "ensure_embed", req, &abiv1.ProvisionerEnsureEmbedResponse{}) } func (s *provisionerStub) EnsureJobSchedule(config plugin.JobScheduleConfig) error { req := &abiv1.ProvisionerEnsureJobScheduleRequest{ JobType: config.JobType, CronExpression: config.CronExpression, ConfigJson: config.Config, } return s.invoke(context.Background(), "ensure_job_schedule", req, &abiv1.ProvisionerEnsureJobScheduleResponse{}) } func (s *provisionerStub) UpdateDataTableRowField(ctx context.Context, rowID uuid.UUID, fieldKey string, value any) error { valueJSON, err := json.Marshal(value) if err != nil { return fmt.Errorf("provisioner.update_data_table_row_field: marshal value: %w", err) } req := &abiv1.ProvisionerUpdateDataTableRowFieldRequest{ RowId: rowID.String(), FieldKey: fieldKey, ValueJson: valueJSON, } return s.invoke(ctx, "update_data_table_row_field", req, &abiv1.ProvisionerUpdateDataTableRowFieldResponse{}) } func (s *provisionerStub) DisableOrphanedJobSchedules(registeredTypes []string) error { req := &abiv1.ProvisionerDisableOrphanedJobSchedulesRequest{RegisteredTypes: registeredTypes} return s.invoke(context.Background(), "disable_orphaned_job_schedules", req, &abiv1.ProvisionerDisableOrphanedJobSchedulesResponse{}) } func (s *provisionerStub) EnsurePlugin(name string) error { req := &abiv1.ProvisionerEnsurePluginRequest{Name: name} return s.invoke(context.Background(), "ensure_plugin", req, &abiv1.ProvisionerEnsurePluginResponse{}) } func (s *provisionerStub) EnsureCustomColor(config plugin.CustomColorConfig) error { req := &abiv1.ProvisionerEnsureCustomColorRequest{ Name: config.Name, LightValue: config.LightValue, DarkValue: config.DarkValue, Source: config.Source, } return s.invoke(context.Background(), "ensure_custom_color", req, &abiv1.ProvisionerEnsureCustomColorResponse{}) } func (s *provisionerStub) EnsureMedia(deposit plugin.MediaDeposit) error { if deposit.ID == uuid.Nil { return fmt.Errorf("provisioner.ensure_media: MediaDeposit.ID is required (the deterministic, template-referable key)") } req := &abiv1.ProvisionerEnsureMediaRequest{ Id: deposit.ID.String(), Filename: deposit.Filename, Data: deposit.Data, AltText: deposit.AltText, Folder: deposit.Folder, Source: deposit.Source, } return s.invoke(context.Background(), "ensure_media", req, &abiv1.ProvisionerEnsureMediaResponse{}) }