Add transform resolver wiring
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## Inputs and outputs
|
## 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.
|
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 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
|
## Safety
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
## Tests
|
||||||
|
|
||||||
Before changing transform behavior, inspect tests under `internal/transform`.
|
Before changing transform behavior, inspect tests under `internal/transform`.
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ func runConfig(ctx context.Context, cfg config.Config, options RunOptions) error
|
|||||||
summary := runSummary{dryRun: options.DryRun}
|
summary := runSummary{dryRun: options.DryRun}
|
||||||
var failures runFailures
|
var failures runFailures
|
||||||
backends := newBackendFactory()
|
backends := newBackendFactory()
|
||||||
|
transforms := newTransformRegistry()
|
||||||
if options.Stdout != nil {
|
if options.Stdout != nil {
|
||||||
if _, err := fmt.Fprintf(options.Stdout, "Configured pipelines: %d\n", len(cfg.Pipelines)); err != nil {
|
if _, err := fmt.Fprintf(options.Stdout, "Configured pipelines: %d\n", len(cfg.Pipelines)); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -83,6 +84,7 @@ func runConfig(ctx context.Context, cfg config.Config, options RunOptions) error
|
|||||||
DestinationBundlePath: sourceBundle.RootRelativePath,
|
DestinationBundlePath: sourceBundle.RootRelativePath,
|
||||||
Publish: *destination.Publish,
|
Publish: *destination.Publish,
|
||||||
Transform: destination.Transform,
|
Transform: destination.Transform,
|
||||||
|
Transformers: transforms,
|
||||||
Transfer: destination.Transfer,
|
Transfer: destination.Transfer,
|
||||||
DistributorVersion: Version,
|
DistributorVersion: Version,
|
||||||
}
|
}
|
||||||
|
|||||||
12
internal/app/transforms.go
Normal file
12
internal/app/transforms.go
Normal 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
|
||||||
|
}
|
||||||
@@ -19,7 +19,11 @@ func PlanOutputs(ctx context.Context, req Request) ([]Output, error) {
|
|||||||
outputs = append(outputs, sourceOutputs...)
|
outputs = append(outputs, sourceOutputs...)
|
||||||
}
|
}
|
||||||
if req.Publish.HTML {
|
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,
|
SourceBundle: req.SourceBundle,
|
||||||
SourceBackend: req.SourceBackend,
|
SourceBackend: req.SourceBackend,
|
||||||
})
|
})
|
||||||
@@ -47,6 +51,17 @@ func PlanOutputs(ctx context.Context, req Request) ([]Output, error) {
|
|||||||
return outputs, nil
|
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) {
|
func PlanSourceOutputs(req Request) ([]Output, error) {
|
||||||
outputs := make([]Output, 0, len(req.SourceBundle.Manifest.Files))
|
outputs := make([]Output, 0, len(req.SourceBundle.Manifest.Files))
|
||||||
for _, file := range req.SourceBundle.Manifest.Files {
|
for _, file := range req.SourceBundle.Manifest.Files {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/distributor/internal/config"
|
"gitea.maximumdirect.net/eric/distributor/internal/config"
|
||||||
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||||
"gitea.maximumdirect.net/eric/distributor/internal/storage/fake"
|
"gitea.maximumdirect.net/eric/distributor/internal/storage/fake"
|
||||||
|
"gitea.maximumdirect.net/eric/distributor/internal/transform"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPlanOutputsRejectsCollision(t *testing.T) {
|
func TestPlanOutputsRejectsCollision(t *testing.T) {
|
||||||
@@ -38,6 +39,14 @@ func TestPlanOutputsRejectsCollision(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Publish: config.PublishPolicy{Source: true, HTML: true},
|
Publish: config.PublishPolicy{Source: true, HTML: true},
|
||||||
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}},
|
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 {
|
if err == nil {
|
||||||
t.Fatal("PlanSourceOutputs() error = nil, want collision")
|
t.Fatal("PlanSourceOutputs() error = nil, want collision")
|
||||||
@@ -61,12 +70,61 @@ func TestPlanOutputsRejectsHTMLWithoutMarkdown(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
Publish: config.PublishPolicy{HTML: true},
|
Publish: config.PublishPolicy{HTML: true},
|
||||||
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}},
|
Transform: config.Transform{MarkdownToHTML: &config.MarkdownToHTML{Enabled: true, Mode: config.TransformModeSidecar}},
|
||||||
|
Transformers: testResolver{
|
||||||
|
transform.MarkdownToHTML: testTransformer{},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("PlanOutputs() error = nil, want no markdown failure")
|
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) {
|
func TestBuildRejectsHTMLWithoutTransform(t *testing.T) {
|
||||||
sourceBackend := fake.New()
|
sourceBackend := fake.New()
|
||||||
destinationBackend := 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/distributor/internal/state"
|
"gitea.maximumdirect.net/eric/distributor/internal/state"
|
||||||
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
"gitea.maximumdirect.net/eric/distributor/internal/storage"
|
||||||
"gitea.maximumdirect.net/eric/distributor/internal/transform"
|
"gitea.maximumdirect.net/eric/distributor/internal/transform"
|
||||||
markdowntransform "gitea.maximumdirect.net/eric/distributor/internal/transform/markdown"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Action string
|
type Action string
|
||||||
@@ -32,10 +31,15 @@ type Request struct {
|
|||||||
DestinationBundlePath string
|
DestinationBundlePath string
|
||||||
Publish config.PublishPolicy
|
Publish config.PublishPolicy
|
||||||
Transform config.Transform
|
Transform config.Transform
|
||||||
|
Transformers TransformerResolver
|
||||||
Transfer config.TransferPolicy
|
Transfer config.TransferPolicy
|
||||||
DistributorVersion string
|
DistributorVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TransformerResolver interface {
|
||||||
|
Get(name string) (transform.Transformer, bool)
|
||||||
|
}
|
||||||
|
|
||||||
type Plan struct {
|
type Plan struct {
|
||||||
PipelineID string
|
PipelineID string
|
||||||
DestinationID string
|
DestinationID string
|
||||||
@@ -108,10 +112,6 @@ func validateRequest(req Request) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func markdownTransformer() transform.Transformer {
|
|
||||||
return markdowntransform.New()
|
|
||||||
}
|
|
||||||
|
|
||||||
func actionForComparison(comparison state.Comparison, transfer config.TransferPolicy) (Action, string) {
|
func actionForComparison(comparison state.Comparison, transfer config.TransferPolicy) (Action, string) {
|
||||||
switch comparison.Outcome {
|
switch comparison.Outcome {
|
||||||
case state.OutcomeDestinationAbsent:
|
case state.OutcomeDestinationAbsent:
|
||||||
|
|||||||
Reference in New Issue
Block a user