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 (
|
const (
|
||||||
kindHost partKind = iota
|
kindHost partKind = iota
|
||||||
kindNamespace
|
kindNamespace
|
||||||
|
kindKind
|
||||||
kindModel
|
kindModel
|
||||||
kindTag
|
kindTag
|
||||||
kindDigest
|
kindDigest
|
||||||
|
|
@ -70,6 +71,8 @@ func (k partKind) String() string {
|
||||||
return "host"
|
return "host"
|
||||||
case kindNamespace:
|
case kindNamespace:
|
||||||
return "namespace"
|
return "namespace"
|
||||||
|
case kindKind:
|
||||||
|
return "kind"
|
||||||
case kindModel:
|
case kindModel:
|
||||||
return "model"
|
return "model"
|
||||||
case kindTag:
|
case kindTag:
|
||||||
|
|
@ -89,6 +92,7 @@ func (k partKind) String() string {
|
||||||
type Name struct {
|
type Name struct {
|
||||||
Host string
|
Host string
|
||||||
Namespace string
|
Namespace string
|
||||||
|
Kind string // Optional: "skill", "agent", or empty for models
|
||||||
Model string
|
Model string
|
||||||
Tag string
|
Tag string
|
||||||
}
|
}
|
||||||
|
|
@ -97,34 +101,27 @@ type Name struct {
|
||||||
// format of a valid name string is:
|
// format of a valid name string is:
|
||||||
//
|
//
|
||||||
// s:
|
// s:
|
||||||
// { host } "/" { namespace } "/" { model } ":" { tag } "@" { digest }
|
// { host } "/" { namespace } "/" { kind } "/" { model } ":" { tag }
|
||||||
// { host } "/" { namespace } "/" { model } ":" { tag }
|
// { host } "/" { namespace } "/" { model } ":" { tag }
|
||||||
// { host } "/" { namespace } "/" { model } "@" { digest }
|
// { namespace } "/" { kind } "/" { model } ":" { tag }
|
||||||
// { host } "/" { namespace } "/" { model }
|
|
||||||
// { namespace } "/" { model } ":" { tag } "@" { digest }
|
|
||||||
// { namespace } "/" { model } ":" { tag }
|
// { namespace } "/" { model } ":" { tag }
|
||||||
// { namespace } "/" { model } "@" { digest }
|
|
||||||
// { namespace } "/" { model }
|
|
||||||
// { model } ":" { tag } "@" { digest }
|
|
||||||
// { model } ":" { tag }
|
// { model } ":" { tag }
|
||||||
// { model } "@" { digest }
|
|
||||||
// { model }
|
// { model }
|
||||||
// "@" { digest }
|
|
||||||
// host:
|
// host:
|
||||||
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." | ":" }*
|
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." | ":" }*
|
||||||
// length: [1, 350]
|
// length: [1, 350]
|
||||||
// namespace:
|
// namespace:
|
||||||
// pattern: { alphanum | "_" } { alphanum | "-" | "_" }*
|
// pattern: { alphanum | "_" } { alphanum | "-" | "_" }*
|
||||||
// length: [1, 80]
|
// length: [1, 80]
|
||||||
|
// kind:
|
||||||
|
// pattern: "skill" | "agent" | "" (empty for models)
|
||||||
|
// length: [0, 80]
|
||||||
// model:
|
// model:
|
||||||
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
|
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
|
||||||
// length: [1, 80]
|
// length: [1, 80]
|
||||||
// tag:
|
// tag:
|
||||||
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
|
// pattern: { alphanum | "_" } { alphanum | "-" | "_" | "." }*
|
||||||
// length: [1, 80]
|
// length: [1, 80]
|
||||||
// digest:
|
|
||||||
// pattern: { alphanum | "_" } { alphanum | "-" | ":" }*
|
|
||||||
// length: [1, 80]
|
|
||||||
//
|
//
|
||||||
// Most users should use [ParseName] instead, unless need to support
|
// Most users should use [ParseName] instead, unless need to support
|
||||||
// different defaults than DefaultName.
|
// different defaults than DefaultName.
|
||||||
|
|
@ -136,6 +133,12 @@ func ParseName(s string) Name {
|
||||||
return Merge(ParseNameBare(s), DefaultName())
|
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
|
// ParseNameBare parses s as a name string and returns a Name. No merge with
|
||||||
// [DefaultName] is performed.
|
// [DefaultName] is performed.
|
||||||
func ParseNameBare(s string) Name {
|
func ParseNameBare(s string) Name {
|
||||||
|
|
@ -153,6 +156,30 @@ func ParseNameBare(s string) Name {
|
||||||
return n
|
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, "/")
|
s, n.Namespace, promised = cutPromised(s, "/")
|
||||||
if !promised {
|
if !promised {
|
||||||
n.Namespace = s
|
n.Namespace = s
|
||||||
|
|
@ -168,20 +195,32 @@ func ParseNameBare(s string) Name {
|
||||||
return n
|
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:
|
// expected to be in the form:
|
||||||
//
|
//
|
||||||
// { host } "/" { namespace } "/" { model } "/" { tag }
|
// { host } "/" { namespace } "/" { model } "/" { tag }
|
||||||
|
// { host } "/" { namespace } "/" { kind } "/" { model } "/" { tag }
|
||||||
func ParseNameFromFilepath(s string) (n Name) {
|
func ParseNameFromFilepath(s string) (n Name) {
|
||||||
parts := strings.Split(s, string(filepath.Separator))
|
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.Host = parts[0]
|
||||||
n.Namespace = parts[1]
|
n.Namespace = parts[1]
|
||||||
n.Model = parts[2]
|
n.Model = parts[2]
|
||||||
n.Tag = parts[3]
|
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() {
|
if !n.IsFullyQualified() {
|
||||||
return Name{}
|
return Name{}
|
||||||
}
|
}
|
||||||
|
|
@ -189,11 +228,12 @@ func ParseNameFromFilepath(s string) (n Name) {
|
||||||
return n
|
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.
|
// preferring the non-empty parts of a.
|
||||||
func Merge(a, b Name) Name {
|
func Merge(a, b Name) Name {
|
||||||
a.Host = cmp.Or(a.Host, b.Host)
|
a.Host = cmp.Or(a.Host, b.Host)
|
||||||
a.Namespace = cmp.Or(a.Namespace, b.Namespace)
|
a.Namespace = cmp.Or(a.Namespace, b.Namespace)
|
||||||
|
a.Kind = cmp.Or(a.Kind, b.Kind)
|
||||||
a.Tag = cmp.Or(a.Tag, b.Tag)
|
a.Tag = cmp.Or(a.Tag, b.Tag)
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
@ -211,6 +251,10 @@ func (n Name) String() string {
|
||||||
b.WriteString(n.Namespace)
|
b.WriteString(n.Namespace)
|
||||||
b.WriteByte('/')
|
b.WriteByte('/')
|
||||||
}
|
}
|
||||||
|
if n.Kind != "" {
|
||||||
|
b.WriteString(n.Kind)
|
||||||
|
b.WriteByte('/')
|
||||||
|
}
|
||||||
b.WriteString(n.Model)
|
b.WriteString(n.Model)
|
||||||
if n.Tag != "" {
|
if n.Tag != "" {
|
||||||
b.WriteByte(':')
|
b.WriteByte(':')
|
||||||
|
|
@ -233,6 +277,12 @@ func (n Name) DisplayShortest() string {
|
||||||
sb.WriteByte('/')
|
sb.WriteByte('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// include kind if present
|
||||||
|
if n.Kind != "" {
|
||||||
|
sb.WriteString(n.Kind)
|
||||||
|
sb.WriteByte('/')
|
||||||
|
}
|
||||||
|
|
||||||
// always include model and tag
|
// always include model and tag
|
||||||
sb.WriteString(n.Model)
|
sb.WriteString(n.Model)
|
||||||
sb.WriteString(":")
|
sb.WriteString(":")
|
||||||
|
|
@ -256,18 +306,23 @@ func (n Name) IsValid() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFullyQualified returns true if all parts of the name are present and
|
// 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 {
|
func (n Name) IsFullyQualified() bool {
|
||||||
parts := []string{
|
if !isValidPart(kindHost, n.Host) {
|
||||||
n.Host,
|
|
||||||
n.Namespace,
|
|
||||||
n.Model,
|
|
||||||
n.Tag,
|
|
||||||
}
|
|
||||||
for i, part := range parts {
|
|
||||||
if !isValidPart(partKind(i), part) {
|
|
||||||
return false
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -276,6 +331,7 @@ func (n Name) IsFullyQualified() bool {
|
||||||
// host to tag as a directory in the form:
|
// host to tag as a directory in the form:
|
||||||
//
|
//
|
||||||
// {host}/{namespace}/{model}/{tag}
|
// {host}/{namespace}/{model}/{tag}
|
||||||
|
// {host}/{namespace}/{kind}/{model}/{tag}
|
||||||
//
|
//
|
||||||
// It uses the system's filepath separator and ensures the path is clean.
|
// 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() {
|
if !n.IsFullyQualified() {
|
||||||
panic("illegal attempt to get filepath of invalid name")
|
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(
|
return filepath.Join(
|
||||||
n.Host,
|
n.Host,
|
||||||
n.Namespace,
|
n.Namespace,
|
||||||
|
|
@ -301,6 +366,7 @@ func (n Name) LogValue() slog.Value {
|
||||||
func (n Name) EqualFold(o Name) bool {
|
func (n Name) EqualFold(o Name) bool {
|
||||||
return strings.EqualFold(n.Host, o.Host) &&
|
return strings.EqualFold(n.Host, o.Host) &&
|
||||||
strings.EqualFold(n.Namespace, o.Namespace) &&
|
strings.EqualFold(n.Namespace, o.Namespace) &&
|
||||||
|
strings.EqualFold(n.Kind, o.Kind) &&
|
||||||
strings.EqualFold(n.Model, o.Model) &&
|
strings.EqualFold(n.Model, o.Model) &&
|
||||||
strings.EqualFold(n.Tag, o.Tag)
|
strings.EqualFold(n.Tag, o.Tag)
|
||||||
}
|
}
|
||||||
|
|
@ -317,6 +383,11 @@ func isValidLen(kind partKind, s string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidPart(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) {
|
if !isValidLen(kind, s) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue