Add transform resolver wiring

This commit is contained in:
2026-05-31 03:30:08 +00:00
parent be4a61fbe6
commit f2ec7bd11e
7 changed files with 113 additions and 8 deletions

View File

@@ -6,7 +6,7 @@
## Inputs and outputs
Inputs are a source bundle, source backend, destination backend, pipeline id, destination id, publish policy, transfer policy, destination bundle path, and existing destination state.
Inputs are a source bundle, source backend, destination backend, pipeline id, destination id, publish policy, transform policy, transformer resolver, transfer policy, destination bundle path, and existing destination state.
Output is a plan with an action, reason, and selected source or generated outputs. Execution writes selected source files, generated files, and `.distributor.json` for publish or replacement actions.
@@ -18,7 +18,7 @@ Supported actions are publish new, replace older destination, skip same source,
The current implementation publishes source files and Markdown-to-HTML sidecar outputs. Remote backend execution is not implemented.
The package uses `internal/state` for destination comparison, `internal/storage` for IO, and the shared `internal/config` publish/transform policy helper for request validation. It does not parse CLI flags or load config files.
The package uses `internal/state` for destination comparison, `internal/storage` for IO, and the shared `internal/config` publish/transform policy helper for request validation. It resolves transforms through a narrow resolver supplied by the caller; concrete transform registration is owned by the app layer. It does not parse CLI flags or load config files.
## Safety

View File

@@ -18,6 +18,8 @@ Generated HTML is deterministic for the same source content and transform config
Transforms do not publish files, mutate source bundles, or write destination state. Publish planning selects and writes transform outputs.
The app layer owns default transform registration. The transform package does not import concrete transform implementations.
## Tests
Before changing transform behavior, inspect tests under `internal/transform`.

View File

@@ -44,6 +44,7 @@ func runConfig(ctx context.Context, cfg config.Config, options RunOptions) error
summary := runSummary{dryRun: options.DryRun}
var failures runFailures
backends := newBackendFactory()
transforms := newTransformRegistry()
if options.Stdout != nil {
if _, err := fmt.Fprintf(options.Stdout, "Configured pipelines: %d\n", len(cfg.Pipelines)); err != nil {
return err
@@ -83,6 +84,7 @@ func runConfig(ctx context.Context, cfg config.Config, options RunOptions) error
DestinationBundlePath: sourceBundle.RootRelativePath,
Publish: *destination.Publish,
Transform: destination.Transform,
Transformers: transforms,
Transfer: destination.Transfer,
DistributorVersion: Version,
}

View File

@@ -0,0 +1,12 @@
package app
import (
"gitea.maximumdirect.net/eric/distributor/internal/transform"
"gitea.maximumdirect.net/eric/distributor/internal/transform/markdown"
)
func newTransformRegistry() *transform.Registry {
registry := transform.NewRegistry()
_ = registry.Register(transform.MarkdownToHTML, markdown.New())
return registry
}

View File

@@ -19,7 +19,11 @@ func PlanOutputs(ctx context.Context, req Request) ([]Output, error) {
outputs = append(outputs, sourceOutputs...)
}
if req.Publish.HTML {
generatedOutputs, err := markdownTransformer().Generate(ctx, transform.Request{
transformer, err := resolveTransformer(req.Transformers, transform.MarkdownToHTML)
if err != nil {
return nil, err
}
generatedOutputs, err := transformer.Generate(ctx, transform.Request{
SourceBundle: req.SourceBundle,
SourceBackend: req.SourceBackend,
})
@@ -47,6 +51,17 @@ func PlanOutputs(ctx context.Context, req Request) ([]Output, error) {
return outputs, nil
}
func resolveTransformer(resolver TransformerResolver, name string) (transform.Transformer, error) {
if resolver == nil {
return nil, fmt.Errorf("transformer resolver is required for %s", name)
}
transformer, ok := resolver.Get(name)
if !ok {
return nil, fmt.Errorf("transformer %s is not registered", name)
}
return transformer, nil
}
func PlanSourceOutputs(req Request) ([]Output, error) {
outputs := make([]Output, 0, len(req.SourceBundle.Manifest.Files))
for _, file := range req.SourceBundle.Manifest.Files {

View File

@@ -9,6 +9,7 @@ import (
"gitea.maximumdirect.net/eric/distributor/internal/config"
"gitea.maximumdirect.net/eric/distributor/internal/storage"
"gitea.maximumdirect.net/eric/distributor/internal/storage/fake"
"gitea.maximumdirect.net/eric/distributor/internal/transform"
)
func TestPlanOutputsRejectsCollision(t *testing.T) {
@@ -38,6 +39,14 @@ func TestPlanOutputsRejectsCollision(t *testing.T) {
},
Publish: config.PublishPolicy{Source: true, HTML: true},
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}},
Transformers: testResolver{transform.MarkdownToHTML: testTransformer{outputs: []transform.Output{{
Path: "report.html",
SourcePath: "report.md",
Transform: transform.MarkdownToHTML,
Data: []byte("<p>Report</p>\n"),
SHA256: bundle.FileDigest([]byte("<p>Report</p>\n")),
Size: int64(len("<p>Report</p>\n")),
}}}},
})
if err == nil {
t.Fatal("PlanSourceOutputs() error = nil, want collision")
@@ -61,12 +70,61 @@ func TestPlanOutputsRejectsHTMLWithoutMarkdown(t *testing.T) {
}},
Publish: config.PublishPolicy{HTML: true},
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}},
Transformers: testResolver{
transform.MarkdownToHTML: testTransformer{},
},
})
if err == nil {
t.Fatal("PlanOutputs() error = nil, want no markdown failure")
}
}
func TestPlanOutputsRejectsHTMLWithoutTransformerResolver(t *testing.T) {
_, err := PlanOutputs(context.Background(), Request{
Publish: config.PublishPolicy{HTML: true},
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}},
})
if err == nil {
t.Fatal("PlanOutputs() error = nil, want resolver failure")
}
}
func TestPlanOutputsRejectsMissingMarkdownTransformer(t *testing.T) {
_, err := PlanOutputs(context.Background(), Request{
Publish: config.PublishPolicy{HTML: true},
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}},
Transformers: testResolver{},
})
if err == nil {
t.Fatal("PlanOutputs() error = nil, want missing transformer failure")
}
}
func TestPlanOutputsUsesRegisteredTransformer(t *testing.T) {
data := []byte("<p>Generated</p>\n")
outputs, err := PlanOutputs(context.Background(), Request{
Publish: config.PublishPolicy{HTML: true},
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}},
Transformers: testResolver{transform.MarkdownToHTML: testTransformer{outputs: []transform.Output{{
Path: "report.html",
SourcePath: "report.md",
Transform: transform.MarkdownToHTML,
Data: data,
SHA256: bundle.FileDigest(data),
Size: int64(len(data)),
}}}},
})
if err != nil {
t.Fatalf("PlanOutputs() error = %v", err)
}
if got, want := len(outputs), 1; got != want {
t.Fatalf("output count = %d, want %d", got, want)
}
if outputs[0].DestinationPath != "report.html" || string(outputs[0].Data) != string(data) {
t.Fatalf("output = %#v", outputs[0])
}
}
func TestBuildRejectsHTMLWithoutTransform(t *testing.T) {
sourceBackend := fake.New()
destinationBackend := fake.New()
@@ -208,3 +266,19 @@ func TestValidateRequestChecksPublishTransformPolicy(t *testing.T) {
})
}
}
type testResolver map[string]transform.Transformer
func (r testResolver) Get(name string) (transform.Transformer, bool) {
transformer, ok := r[name]
return transformer, ok
}
type testTransformer struct {
outputs []transform.Output
err error
}
func (t testTransformer) Generate(context.Context, transform.Request) ([]transform.Output, error) {
return t.outputs, t.err
}

View File

@@ -9,7 +9,6 @@ import (
"gitea.maximumdirect.net/eric/distributor/internal/state"
"gitea.maximumdirect.net/eric/distributor/internal/storage"
"gitea.maximumdirect.net/eric/distributor/internal/transform"
markdowntransform "gitea.maximumdirect.net/eric/distributor/internal/transform/markdown"
)
type Action string
@@ -32,10 +31,15 @@ type Request struct {
DestinationBundlePath string
Publish config.PublishPolicy
Transform config.Transform
Transformers TransformerResolver
Transfer config.TransferPolicy
DistributorVersion string
}
type TransformerResolver interface {
Get(name string) (transform.Transformer, bool)
}
type Plan struct {
PipelineID string
DestinationID string
@@ -108,10 +112,6 @@ func validateRequest(req Request) error {
return nil
}
func markdownTransformer() transform.Transformer {
return markdowntransform.New()
}
func actionForComparison(comparison state.Comparison, transfer config.TransferPolicy) (Action, string) {
switch comparison.Outcome {
case state.OutcomeDestinationAbsent: