96 lines
3.4 KiB
Go
96 lines
3.4 KiB
Go
package profile
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/filecatalog"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/jsonvalue"
|
|
)
|
|
|
|
// LoadedProfileMetadata identifies one profile accepted by LoadFSRepository.
|
|
type LoadedProfileMetadata struct {
|
|
ID string
|
|
Path string
|
|
ExplicitFields []string
|
|
}
|
|
|
|
// LoadFSRepository eagerly validates every profile under root and returns an
|
|
// immutable raw repository and independently owned source metadata.
|
|
func LoadFSRepository(ctx context.Context, fsys fs.FS, root string) (Repository, []LoadedProfileMetadata, error) {
|
|
if fsys == nil {
|
|
return nil, nil, fmt.Errorf("failed to read profile directory: filesystem is nil")
|
|
}
|
|
paths, err := filecatalog.FindFSYAMLFiles(ctx, fsys, root)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to read profile directory: %w", err)
|
|
}
|
|
|
|
repository := &loadedRepository{profiles: make(map[string]domain.ExecutionProfile, len(paths))}
|
|
metadata := make([]LoadedProfileMetadata, 0, len(paths))
|
|
for _, path := range paths {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
data, err := fs.ReadFile(fsys, path)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed to read profile file %s: %w", filecatalog.DisplayPath(root, path), err)
|
|
}
|
|
fileMetadata, err := readProfileFileMetadata(data)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%w: %s: %v", ErrInvalidYAML, filecatalog.DisplayPath(root, path), err)
|
|
}
|
|
if fileMetadata.hasRawAPIKey {
|
|
return nil, nil, fmt.Errorf("%w: %s", ErrRawAPIKeyNotAllowed, filecatalog.DisplayPath(root, path))
|
|
}
|
|
definition, err := decodeProfile(data)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%w: %s: %v", ErrInvalidYAML, filecatalog.DisplayPath(root, path), err)
|
|
}
|
|
definition.ExtraParams, err = jsonvalue.CopyMap(definition.ExtraParams)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("%w: %s: %v", ErrInvalidProfile, filecatalog.DisplayPath(root, path), err)
|
|
}
|
|
if err := NormalizeAndValidateDefinition(definition); err != nil {
|
|
return nil, nil, fmt.Errorf("%w: %s: %v", ErrInvalidProfile, filecatalog.DisplayPath(root, path), err)
|
|
}
|
|
if _, exists := repository.profiles[definition.ID]; exists {
|
|
return nil, nil, fmt.Errorf("%w: duplicate execution profile id %q", ErrInvalidProfile, definition.ID)
|
|
}
|
|
repository.profiles[definition.ID] = *definition
|
|
fields := append([]string(nil), fileMetadata.explicitFields...)
|
|
sort.Strings(fields)
|
|
metadata = append(metadata, LoadedProfileMetadata{
|
|
ID: definition.ID,
|
|
Path: filecatalog.DisplayPath(root, path),
|
|
ExplicitFields: fields,
|
|
})
|
|
}
|
|
sort.Slice(metadata, func(left, right int) bool { return metadata[left].ID < metadata[right].ID })
|
|
return repository, metadata, nil
|
|
}
|
|
|
|
type loadedRepository struct {
|
|
profiles map[string]domain.ExecutionProfile
|
|
}
|
|
|
|
func (r *loadedRepository) GetProfile(ctx context.Context, id string) (*domain.ExecutionProfile, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
definition, found := r.profiles[strings.TrimSpace(id)]
|
|
if !found {
|
|
return nil, ErrProfileNotFound
|
|
}
|
|
extraParams, err := jsonvalue.CopyMap(definition.ExtraParams)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("copy loaded profile %q: %w", definition.ID, err)
|
|
}
|
|
definition.ExtraParams = extraParams
|
|
return &definition, nil
|
|
}
|