Files
lpm/state/repository.go
zeekayandHanzo Dev 934757a791 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers, and the repository lookup is now a generic method on
DiskRepository; omitzero is not applied, since it changes what the JSON
says.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-16 19:23:26 -07:00

114 lines
3.5 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package state
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
"github.com/luxfi/lpm/git"
"github.com/luxfi/lpm/types"
)
var (
vmDir = "vms"
chainDir = "chains"
extension = "yaml"
// Wrapper keys used by on-disk plugin manifests. By convention every
// definition file is a single-key mapping (`vm:` for VMs, `subnet:` for
// chains) whose value is the actual definition. The keys match the
// wrappers in plugins-core/examples/{vm,subnet}.yaml and every shipped
// registry entry. The chain wrapper key remains `subnet` on disk for
// backwards compatibility with shipped manifests (was upstream Lux v0.x
// terminology); renaming it would break every registry entry already
// published.
vmWrapperKey = "vm"
chainWrapperKey = "subnet"
)
// Repository wraps a plugin repository's VMs and Chains
type Repository interface {
GetPath() string
GetVM(name string) (Definition[types.VM], error)
GetChain(name string) (Definition[types.Chain], error)
}
type DiskRepository struct {
Git git.Factory
Path string
}
func (d DiskRepository) GetVM(name string) (Definition[types.VM], error) {
return d.get[types.VM](vmDir, name, vmWrapperKey)
}
func (d DiskRepository) GetChain(name string) (Definition[types.Chain], error) {
return d.get[types.Chain](chainDir, name, chainWrapperKey)
}
func (d DiskRepository) GetPath() string {
return d.Path
}
func (d DiskRepository) get[T types.Definition](dir string, file string, wrapperKey string) (Definition[T], error) {
relativePathWithExtension := filepath.Join(dir, fmt.Sprintf("%s.%s", file, extension))
absolutePathWithExtension := filepath.Join(d.Path, relativePathWithExtension)
bytes, err := os.ReadFile(absolutePathWithExtension)
if err != nil {
return Definition[T]{}, err
}
definition, err := decodeWrapped[T](bytes, wrapperKey)
if err != nil {
return Definition[T]{}, fmt.Errorf("decoding %s: %w", relativePathWithExtension, err)
}
commit, err := d.Git.GetLastModified(d.Path, relativePathWithExtension)
if err != nil {
return Definition[T]{}, err
}
return Definition[T]{
Definition: definition,
Commit: commit,
}, nil
}
// decodeWrapped unmarshals a definition YAML that wraps its payload under a
// single top-level key (e.g. `vm:` or `subnet:`). Empty or missing wrappers
// are reported as errors so callers do not silently get a zero-valued T and
// crash later (see CI-PREMORTEM.md §12a).
func decodeWrapped[T types.Definition](data []byte, wrapperKey string) (T, error) {
var zero T
var root yaml.Node
if err := yaml.Unmarshal(data, &root); err != nil {
return zero, err
}
// A document node has exactly one content child: the top-level mapping.
if root.Kind != yaml.DocumentNode || len(root.Content) != 1 {
return zero, fmt.Errorf("expected a single YAML document, got kind=%d content=%d", root.Kind, len(root.Content))
}
mapping := root.Content[0]
if mapping.Kind != yaml.MappingNode {
return zero, fmt.Errorf("expected top-level mapping, got kind=%d", mapping.Kind)
}
// MappingNode.Content is a flat [key0, value0, key1, value1, ...] list.
for i := 0; i+1 < len(mapping.Content); i += 2 {
key := mapping.Content[i]
if key.Kind == yaml.ScalarNode && key.Value == wrapperKey {
var out T
if err := mapping.Content[i+1].Decode(&out); err != nil {
return zero, err
}
return out, nil
}
}
return zero, fmt.Errorf("missing required top-level %q key", wrapperKey)
}