Add opt-in JSON chunk map export
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/chunkmap"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/contracts"
|
||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
|
||||
)
|
||||
@@ -17,18 +18,25 @@ const Key = "json"
|
||||
|
||||
const contentTypeJSON = "application/json"
|
||||
|
||||
const chunkMapFileName = "chunk-map.json"
|
||||
|
||||
var safeOutputFileChar = regexp.MustCompile(`[^A-Za-z0-9._-]`)
|
||||
|
||||
var _ contracts.OutputEncoder = (*Encoder)(nil)
|
||||
|
||||
type Options struct{}
|
||||
type Options struct {
|
||||
IncludeChunkMap bool
|
||||
}
|
||||
|
||||
type Encoder struct {
|
||||
options Options
|
||||
}
|
||||
|
||||
func New() *Encoder {
|
||||
options, _ := DecodeOptions(nil)
|
||||
return NewWithOptions(Options{})
|
||||
}
|
||||
|
||||
func NewWithOptions(options Options) *Encoder {
|
||||
return &Encoder{options: options}
|
||||
}
|
||||
|
||||
@@ -47,7 +55,7 @@ func (e *Encoder) Encode(ctx context.Context, req contracts.OutputRequest) (cont
|
||||
return contracts.OutputResult{}, encoderErrorf("context error before encoding: %w", err)
|
||||
}
|
||||
|
||||
files, err := logicalFiles(req)
|
||||
files, err := logicalFiles(req, e.options)
|
||||
if err != nil {
|
||||
return contracts.OutputResult{}, err
|
||||
}
|
||||
@@ -79,9 +87,16 @@ func validateOptions(options map[string]any) error {
|
||||
}
|
||||
|
||||
func DecodeOptions(options map[string]any) (Options, error) {
|
||||
if err := pipeline.RejectUnknownOptions(options); err != nil {
|
||||
if err := pipeline.RejectUnknownOptions(options, "include_chunk_map"); err != nil {
|
||||
return Options{}, encoderErrorf("%w", err)
|
||||
}
|
||||
if value, ok := options["include_chunk_map"]; ok {
|
||||
enabled, ok := value.(bool)
|
||||
if !ok {
|
||||
return Options{}, encoderErrorf("option %q must be a boolean", "include_chunk_map")
|
||||
}
|
||||
return Options{IncludeChunkMap: enabled}, nil
|
||||
}
|
||||
return Options{}, nil
|
||||
}
|
||||
|
||||
@@ -90,6 +105,16 @@ type indexFile struct {
|
||||
OutputFiles []outputFileIndex `json:"output_files"`
|
||||
RejectedFile string `json:"rejected_file"`
|
||||
WarningsFile string `json:"warnings_file"`
|
||||
ChunkMap *chunkMapIndex `json:"chunk_map,omitempty"`
|
||||
}
|
||||
|
||||
type chunkMapIndex struct {
|
||||
ArtifactKind contracts.ArtifactKind `json:"artifact_kind"`
|
||||
File string `json:"file"`
|
||||
MediaType string `json:"media_type"`
|
||||
SchemaID string `json:"schema_id"`
|
||||
SchemaName string `json:"schema_name"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
}
|
||||
|
||||
type outputFileIndex struct {
|
||||
@@ -110,7 +135,7 @@ type warningsFile struct {
|
||||
Warnings []contracts.Warning `json:"warnings"`
|
||||
}
|
||||
|
||||
func logicalFiles(req contracts.OutputRequest) ([]contracts.OutputFile, error) {
|
||||
func logicalFiles(req contracts.OutputRequest, options Options) ([]contracts.OutputFile, error) {
|
||||
outputs := cloneNormalizeOutputs(req.NormalizeOutputs)
|
||||
sort.SliceStable(outputs, func(i, j int) bool {
|
||||
return outputs[i].LaneID < outputs[j].LaneID
|
||||
@@ -156,6 +181,14 @@ func logicalFiles(req contracts.OutputRequest) ([]contracts.OutputFile, error) {
|
||||
RejectedFile: "rejected.json",
|
||||
WarningsFile: "warnings.json",
|
||||
}
|
||||
if options.IncludeChunkMap && req.ChunkMap != nil {
|
||||
chunkMapOutput, chunkMapDescriptor, err := serializedChunkMapFile(*req.ChunkMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, chunkMapOutput)
|
||||
index.ChunkMap = &chunkMapDescriptor
|
||||
}
|
||||
indexOutput, err := jsonFile("index.json", index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -175,6 +208,33 @@ func logicalFiles(req contracts.OutputRequest) ([]contracts.OutputFile, error) {
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func serializedChunkMapFile(artifact contracts.SerializedArtifact) (contracts.OutputFile, chunkMapIndex, error) {
|
||||
if artifact.Kind != chunkmap.ArtifactKind {
|
||||
return contracts.OutputFile{}, chunkMapIndex{}, encoderErrorf("chunk map has unexpected artifact kind %q", artifact.Kind)
|
||||
}
|
||||
if artifact.Schema.ID != chunkmap.SchemaID || artifact.Schema.Name != chunkmap.SchemaName || artifact.Schema.Version != chunkmap.SchemaVersion {
|
||||
return contracts.OutputFile{}, chunkMapIndex{}, encoderErrorf("chunk map has unexpected schema identity")
|
||||
}
|
||||
if strings.TrimSpace(artifact.MediaType) != chunkmap.MediaType {
|
||||
return contracts.OutputFile{}, chunkMapIndex{}, encoderErrorf("chunk map has unsupported media type %q", artifact.MediaType)
|
||||
}
|
||||
if _, err := chunkmap.New().Decode(artifact.Content); err != nil {
|
||||
return contracts.OutputFile{}, chunkMapIndex{}, encoderErrorf("decode chunk map: %w", err)
|
||||
}
|
||||
file, err := serializedOutputFile(chunkMapFileName, artifact)
|
||||
if err != nil {
|
||||
return contracts.OutputFile{}, chunkMapIndex{}, err
|
||||
}
|
||||
return file, chunkMapIndex{
|
||||
ArtifactKind: artifact.Kind,
|
||||
File: chunkMapFileName,
|
||||
MediaType: chunkmap.MediaType,
|
||||
SchemaID: artifact.Schema.ID,
|
||||
SchemaName: artifact.Schema.Name,
|
||||
SchemaVersion: artifact.Schema.Version,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func serializedOutputFile(name string, artifact contracts.SerializedArtifact) (contracts.OutputFile, error) {
|
||||
content := append([]byte(nil), artifact.Content...)
|
||||
if len(content) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user