types: add Kind field to model.Name for 5-part naming
Extends the model name structure from 4-part to 5-part: host/namespace/kind/model:tag The Kind field is optional and supports: - "skill" for skill packages - "agent" for agent packages (future) - empty for regular models Parser detects valid kinds to distinguish between old format (host/namespace/model) and new format (host/namespace/kind/model).
This commit is contained in:
parent
18fdcc94e5
commit
d4f9bd5fe5
|
|
@ -59,6 +59,7 @@ type partKind int
|
|||
const (
|
||||
kindHost partKind = iota
|
||||
kindNamespace
|
||||
kindKind
|
||||
kindModel
|
||||
kindTag
|
||||
kindDigest
|
||||
|
|
@ -70,6 +71,8 @@ func (k partKind) String() string {
|
|||
return "host"
|
||||
case kindNamespace:
|
||||
return "namespace"
|
||||
case kindKind:
|
||||
return "kind"
|
||||
case kindModel:
|
||||
return "model"
|
||||
case kindTag:
|
||||
|
|
@ -89,6 +92,7 @@ func (k partKind) String() string {
|
|||
type Name struct {
|
||||
Host string
|
||||
Namespace string
|
||||
Kind string // Optional: "skill", "agent", or empty for models
|
||||
Model string
|
||||
Tag string
|
||||
}
|
||||
|
|
@ -97,34 +101,27 @@ type Name struct {
|
|||
// format of a valid name string is:
|
||||
//
|
||||
// s:
|
||||
// { host } "/" { namespace } "/" { model } ":" { tag } "@" { digest }
|
||||
// { host } "/" { namespace } "/" { kind } "/" { model } ":" { tag }
|
||||
// { host } "/" { namespace } "/" { model } ":" { tag }
|
||||
// { host } "/" { namespace } "/" { model } "@" { digest }
|
||||
// { host } "/" { namespace } "/" { model }
|
||||
// { namespace } "/" { model } ":" { tag } "@" { digest }
|
||||
// { namespace } "/" { kind } "/" { model } ":" { tag }
|
||||
// { namespace } "/" { model } ":" { tag }
|
||||
// { namespace } "/" { model } "@" { digest }
|
||||
// { namespace } "/" { model }
|
||||
// { model } ":" { tag } "@" { digest }
|
||||
// { model } ":" { tag }
|
||||
// { model } "@" { digest }
|
||||
// { model }
|
||||
// "@" { digest }
|
||||
// host:
|
||||
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." | ":" }*
|
||||
// length: [1, 350]
|
||||
// namespace:
|
||||
// pattern: { alphanum | "_" } { alphanum | "-" | "_" }*
|
||||
// length: [1, 80]
|
||||
// kind:
|
||||
// pattern: "skill" | "agent" | "" (empty for models)
|
||||
// length: [0, 80]
|
||||
// model:
|
||||
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
|
||||
// length: [1, 80]
|
||||
// tag:
|
||||
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
|
||||
// length: [1, 80]
|
||||
// digest:
|
||||
// pattern: { alphanum | "_" } { alphanum | "-" | ":" }*
|
||||
// length: [1, 80]
|
||||
//
|
||||
// Most users should use [ParseName] instead, unless need to support
|
||||
// different defaults than DefaultName.
|
||||
|
|
@ -136,6 +133,12 @@ func ParseName(s string) Name {
|
|||
return Merge(ParseNameBare(s), DefaultName())
|
||||
}
|
||||
|
||||
// ValidKinds are the allowed values for the Kind field
|
||||
var ValidKinds = map[string]bool{
|
||||
"skill": true,
|
||||
"agent": true,
|
||||
}
|
||||
|
||||
// ParseNameBare parses s as a name string and returns a Name. No merge with
|
||||
// [DefaultName] is performed.
|
||||
func ParseNameBare(s string) Name {
|
||||
|
|
@ -153,6 +156,30 @@ func ParseNameBare(s string) Name {
|
|||
return n
|
||||
}
|
||||
|
||||
s, n.Kind, promised = cutPromised(s, "/")
|
||||
if !promised {
|
||||
// Only 2 parts: namespace/model - what we parsed as Kind is actually Namespace
|
||||
n.Namespace = n.Kind
|
||||
n.Kind = ""
|
||||
return n
|
||||
}
|
||||
|
||||
// Check if what we parsed as Kind is actually a valid kind value
|
||||
if !ValidKinds[n.Kind] {
|
||||
// Not a valid kind - this is the old 3-part format: host/namespace/model
|
||||
// Shift: Kind -> Namespace, s -> Host
|
||||
n.Namespace = n.Kind
|
||||
n.Kind = ""
|
||||
|
||||
scheme, host, ok := strings.Cut(s, "://")
|
||||
if !ok {
|
||||
host = scheme
|
||||
}
|
||||
n.Host = host
|
||||
return n
|
||||
}
|
||||
|
||||
// Valid kind found - continue parsing for namespace and optional host
|
||||
s, n.Namespace, promised = cutPromised(s, "/")
|
||||
if !promised {
|
||||
n.Namespace = s
|
||||
|
|
@ -168,20 +195,32 @@ func ParseNameBare(s string) Name {
|
|||
return n
|
||||
}
|
||||
|
||||
// ParseNameFromFilepath parses a 4-part filepath as a Name. The parts are
|
||||
// ParseNameFromFilepath parses a 4 or 5-part filepath as a Name. The parts are
|
||||
// expected to be in the form:
|
||||
//
|
||||
// { host } "/" { namespace } "/" { model } "/" { tag }
|
||||
// { host } "/" { namespace } "/" { kind } "/" { model } "/" { tag }
|
||||
func ParseNameFromFilepath(s string) (n Name) {
|
||||
parts := strings.Split(s, string(filepath.Separator))
|
||||
if len(parts) != 4 {
|
||||
return Name{}
|
||||
}
|
||||
|
||||
switch len(parts) {
|
||||
case 4:
|
||||
// Old format: host/namespace/model/tag
|
||||
n.Host = parts[0]
|
||||
n.Namespace = parts[1]
|
||||
n.Model = parts[2]
|
||||
n.Tag = parts[3]
|
||||
case 5:
|
||||
// New format: host/namespace/kind/model/tag
|
||||
n.Host = parts[0]
|
||||
n.Namespace = parts[1]
|
||||
n.Kind = parts[2]
|
||||
n.Model = parts[3]
|
||||
n.Tag = parts[4]
|
||||
default:
|
||||
return Name{}
|
||||
}
|
||||
|
||||
if !n.IsFullyQualified() {
|
||||
return Name{}
|
||||
}
|
||||
|
|
@ -189,11 +228,12 @@ func ParseNameFromFilepath(s string) (n Name) {
|
|||
return n
|
||||
}
|
||||
|
||||
// Merge merges the host, namespace, and tag parts of the two names,
|
||||
// Merge merges the host, namespace, kind, and tag parts of the two names,
|
||||
// preferring the non-empty parts of a.
|
||||
func Merge(a, b Name) Name {
|
||||
a.Host = cmp.Or(a.Host, b.Host)
|
||||
a.Namespace = cmp.Or(a.Namespace, b.Namespace)
|
||||
a.Kind = cmp.Or(a.Kind, b.Kind)
|
||||
a.Tag = cmp.Or(a.Tag, b.Tag)
|
||||
return a
|
||||
}
|
||||
|
|
@ -211,6 +251,10 @@ func (n Name) String() string {
|
|||
b.WriteString(n.Namespace)
|
||||
b.WriteByte('/')
|
||||
}
|
||||
if n.Kind != "" {
|
||||
b.WriteString(n.Kind)
|
||||
b.WriteByte('/')
|
||||
}
|
||||
b.WriteString(n.Model)
|
||||
if n.Tag != "" {
|
||||
b.WriteByte(':')
|
||||
|
|
@ -233,6 +277,12 @@ func (n Name) DisplayShortest() string {
|
|||
sb.WriteByte('/')
|
||||
}
|
||||
|
||||
// include kind if present
|
||||
if n.Kind != "" {
|
||||
sb.WriteString(n.Kind)
|
||||
sb.WriteByte('/')
|
||||
}
|
||||
|
||||
// always include model and tag
|
||||
sb.WriteString(n.Model)
|
||||
sb.WriteString(":")
|
||||
|
|
@ -256,18 +306,23 @@ func (n Name) IsValid() bool {
|
|||
}
|
||||
|
||||
// IsFullyQualified returns true if all parts of the name are present and
|
||||
// valid without the digest.
|
||||
// valid without the digest. Kind is optional and only validated if non-empty.
|
||||
func (n Name) IsFullyQualified() bool {
|
||||
parts := []string{
|
||||
n.Host,
|
||||
n.Namespace,
|
||||
n.Model,
|
||||
n.Tag,
|
||||
}
|
||||
for i, part := range parts {
|
||||
if !isValidPart(partKind(i), part) {
|
||||
if !isValidPart(kindHost, n.Host) {
|
||||
return false
|
||||
}
|
||||
if !isValidPart(kindNamespace, n.Namespace) {
|
||||
return false
|
||||
}
|
||||
// Kind is optional - only validate if present
|
||||
if n.Kind != "" && !isValidPart(kindKind, n.Kind) {
|
||||
return false
|
||||
}
|
||||
if !isValidPart(kindModel, n.Model) {
|
||||
return false
|
||||
}
|
||||
if !isValidPart(kindTag, n.Tag) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
@ -276,6 +331,7 @@ func (n Name) IsFullyQualified() bool {
|
|||
// host to tag as a directory in the form:
|
||||
//
|
||||
// {host}/{namespace}/{model}/{tag}
|
||||
// {host}/{namespace}/{kind}/{model}/{tag}
|
||||
//
|
||||
// It uses the system's filepath separator and ensures the path is clean.
|
||||
//
|
||||
|
|
@ -285,6 +341,15 @@ func (n Name) Filepath() string {
|
|||
if !n.IsFullyQualified() {
|
||||
panic("illegal attempt to get filepath of invalid name")
|
||||
}
|
||||
if n.Kind != "" {
|
||||
return filepath.Join(
|
||||
n.Host,
|
||||
n.Namespace,
|
||||
n.Kind,
|
||||
n.Model,
|
||||
n.Tag,
|
||||
)
|
||||
}
|
||||
return filepath.Join(
|
||||
n.Host,
|
||||
n.Namespace,
|
||||
|
|
@ -301,6 +366,7 @@ func (n Name) LogValue() slog.Value {
|
|||
func (n Name) EqualFold(o Name) bool {
|
||||
return strings.EqualFold(n.Host, o.Host) &&
|
||||
strings.EqualFold(n.Namespace, o.Namespace) &&
|
||||
strings.EqualFold(n.Kind, o.Kind) &&
|
||||
strings.EqualFold(n.Model, o.Model) &&
|
||||
strings.EqualFold(n.Tag, o.Tag)
|
||||
}
|
||||
|
|
@ -317,6 +383,11 @@ func isValidLen(kind partKind, s string) bool {
|
|||
}
|
||||
|
||||
func isValidPart(kind partKind, s string) bool {
|
||||
// Kind must be one of the valid values
|
||||
if kind == kindKind {
|
||||
return ValidKinds[s]
|
||||
}
|
||||
|
||||
if !isValidLen(kind, s) {
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue