mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
132 lines
3.8 KiB
Go
132 lines
3.8 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package skillref
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
)
|
|
|
|
// ErrInvalidRemap identifies malformed, conflicting, or dangling explicit
|
|
// reference mappings. Callers classify it as an invalid SkillsOverlay.
|
|
var ErrInvalidRemap = errors.New("invalid skill reference remap")
|
|
|
|
// Mapping is one parsed remap. A source without Path remaps the whole skill
|
|
// name; a source with Path is an exact-reference override.
|
|
type Mapping struct {
|
|
From Ref
|
|
To Ref
|
|
}
|
|
|
|
// Resolver is an immutable, build-local projection from canonical references
|
|
// to the composed runtime skill tree. Underlying skill files remain live, in
|
|
// line with the skill manifest contract, so Resolve verifies availability at
|
|
// read/help-render time as well as validating explicit targets at construction.
|
|
type Resolver struct {
|
|
content fs.FS
|
|
skills map[string]Ref
|
|
exact map[string]Ref
|
|
}
|
|
|
|
// New validates mappings against content and returns an immutable resolver.
|
|
//
|
|
// Explicit targets are build-integrity declarations and must exist. In
|
|
// contrast, an unmapped canonical reference may be absent: presenters then
|
|
// omit the complete guidance fragment that owns it.
|
|
func New(content fs.FS, mappings []Mapping) (*Resolver, error) {
|
|
r := &Resolver{
|
|
content: content,
|
|
skills: make(map[string]Ref),
|
|
exact: make(map[string]Ref),
|
|
}
|
|
seen := make(map[string]bool, len(mappings))
|
|
for _, mapping := range mappings {
|
|
from, to := mapping.From, mapping.To
|
|
if _, err := Parse(from.String()); err != nil {
|
|
return nil, fmt.Errorf("%w: source %q: %w", ErrInvalidRemap, from.String(), err)
|
|
}
|
|
if _, err := Parse(to.String()); err != nil {
|
|
return nil, fmt.Errorf("%w: target %q: %w", ErrInvalidRemap, to.String(), err)
|
|
}
|
|
key := from.String()
|
|
if seen[key] {
|
|
return nil, fmt.Errorf("%w: source %q is mapped more than once", ErrInvalidRemap, key)
|
|
}
|
|
seen[key] = true
|
|
|
|
if from.Path == "" {
|
|
if to.Path != "" {
|
|
return nil, fmt.Errorf(
|
|
"%w: whole-skill source %q requires a bare target skill, got %q",
|
|
ErrInvalidRemap, key, to.String())
|
|
}
|
|
r.skills[from.Skill] = to
|
|
} else {
|
|
r.exact[key] = to
|
|
}
|
|
exists, err := probe(content, to)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: cannot inspect target %q: %w",
|
|
ErrInvalidRemap, to.String(), err)
|
|
}
|
|
if !exists {
|
|
return nil, fmt.Errorf("%w: target %q does not exist in the composed skill tree",
|
|
ErrInvalidRemap, to.String())
|
|
}
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
// Resolve projects canonical through the exact-reference map first, then the
|
|
// whole-skill map, and finally identity. It returns ok=false when the projected
|
|
// target is not currently readable.
|
|
func (r *Resolver) Resolve(canonical Ref) (target Ref, ok bool) {
|
|
if r == nil {
|
|
return Ref{}, false
|
|
}
|
|
if exact, found := r.exact[canonical.String()]; found {
|
|
target = exact
|
|
} else if renamed, found := r.skills[canonical.Skill]; found {
|
|
target = Ref{Skill: renamed.Skill, Path: canonical.Path}
|
|
} else {
|
|
target = canonical
|
|
}
|
|
return target, readable(r.content, target)
|
|
}
|
|
|
|
// ResolveString is a convenience boundary for metadata that stores references
|
|
// in their canonical string form.
|
|
func (r *Resolver) ResolveString(canonical string) (string, bool) {
|
|
ref, err := Parse(canonical)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
target, ok := r.Resolve(ref)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
return target.String(), true
|
|
}
|
|
|
|
func readable(content fs.FS, ref Ref) bool {
|
|
ok, err := probe(content, ref)
|
|
return err == nil && ok
|
|
}
|
|
|
|
func probe(content fs.FS, ref Ref) (bool, error) {
|
|
if content == nil || !ValidSkillName(ref.Skill) {
|
|
return false, nil
|
|
}
|
|
info, err := fs.Stat(content, ref.StatPath())
|
|
switch {
|
|
case err == nil:
|
|
return !info.IsDir(), nil
|
|
case errors.Is(err, fs.ErrNotExist) || errors.Is(err, fs.ErrInvalid):
|
|
return false, nil
|
|
default:
|
|
return false, err
|
|
}
|
|
}
|