NameParts -> Parts

This commit is contained in:
Blake Mizerany 2024-04-06 23:19:55 -07:00
parent bd446a72cc
commit 6ba495d4a3
2 changed files with 42 additions and 16 deletions

View File

@ -43,7 +43,7 @@ const (
// It should be kept as the last part in the list. // It should be kept as the last part in the list.
Invalid Invalid
NumParts = Invalid - 1 NumParts = Invalid
) )
var kindNames = map[NamePart]string{ var kindNames = map[NamePart]string{
@ -134,7 +134,7 @@ type Name struct {
// [Name.String] will not print a "+" if the build is empty. // [Name.String] will not print a "+" if the build is empty.
func ParseName(s string) Name { func ParseName(s string) Name {
var r Name var r Name
for kind, part := range NameParts(s) { for kind, part := range Parts(s) {
if kind == Invalid { if kind == Invalid {
return Name{} return Name{}
} }
@ -230,7 +230,8 @@ var seps = [...]string{
Namespace: "/", Namespace: "/",
Model: ":", Model: ":",
Tag: "+", Tag: "+",
Build: "", Build: "@",
Digest: "",
} }
// WriteTo implements io.WriterTo. It writes the fullest possible display // WriteTo implements io.WriterTo. It writes the fullest possible display
@ -345,13 +346,13 @@ func unsafeString(b []byte) string {
// Complete reports whether the Name is fully qualified. That is it has a // Complete reports whether the Name is fully qualified. That is it has a
// domain, namespace, name, tag, and build. // domain, namespace, name, tag, and build.
func (r Name) Complete() bool { func (r Name) Complete() bool {
return !slices.Contains(r.parts[:], "") return !slices.Contains(r.parts[:Digest], "")
} }
// CompleteNoBuild is like [Name.Complete] but it does not require the // CompleteNoBuild is like [Name.Complete] but it does not require the
// build part to be present. // build part to be present.
func (r Name) CompleteNoBuild() bool { func (r Name) CompleteNoBuild() bool {
return !slices.Contains(r.parts[:Build-1], "") return !slices.Contains(r.parts[:Build], "")
} }
// EqualFold reports whether r and o are equivalent model names, ignoring // EqualFold reports whether r and o are equivalent model names, ignoring
@ -396,7 +397,7 @@ func (r Name) Parts() []string {
// //
// It normalizes the input string by removing "http://" and "https://" only. // It normalizes the input string by removing "http://" and "https://" only.
// No other normalization is done. // No other normalization is done.
func NameParts(s string) iter.Seq2[NamePart, string] { func Parts(s string) iter.Seq2[NamePart, string] {
return func(yield func(NamePart, string) bool) { return func(yield func(NamePart, string) bool) {
if strings.HasPrefix(s, "http://") { if strings.HasPrefix(s, "http://") {
s = s[len("http://"):] s = s[len("http://"):]
@ -418,16 +419,27 @@ func NameParts(s string) iter.Seq2[NamePart, string] {
} }
partLen := 0 partLen := 0
state, j := Build, len(s) state, j := Digest, len(s)
for i := len(s) - 1; i >= 0; i-- { for i := len(s) - 1; i >= 0; i-- {
if partLen++; partLen > MaxNamePartLen { if partLen++; partLen > MaxNamePartLen {
yield(Invalid, "") yield(Invalid, "")
return return
} }
switch s[i] { switch s[i] {
case '@':
switch state {
case Digest:
if !yieldValid(Digest, s[i+1:j]) {
return
}
state, j, partLen = Build, i, 0
default:
yield(Invalid, "")
return
}
case '+': case '+':
switch state { switch state {
case Build: case Build, Digest:
if !yieldValid(Build, s[i+1:j]) { if !yieldValid(Build, s[i+1:j]) {
return return
} }
@ -438,7 +450,7 @@ func NameParts(s string) iter.Seq2[NamePart, string] {
} }
case ':': case ':':
switch state { switch state {
case Build, Tag: case Tag, Build, Digest:
if !yieldValid(Tag, s[i+1:j]) { if !yieldValid(Tag, s[i+1:j]) {
return return
} }
@ -449,7 +461,7 @@ func NameParts(s string) iter.Seq2[NamePart, string] {
} }
case '/': case '/':
switch state { switch state {
case Model, Tag, Build: case Model, Tag, Build, Digest:
if !yieldValid(Model, s[i+1:j]) { if !yieldValid(Model, s[i+1:j]) {
return return
} }

View File

@ -13,6 +13,7 @@ import (
type fields struct { type fields struct {
host, namespace, model, tag, build string host, namespace, model, tag, build string
digest string
} }
func fieldsFromName(p Name) fields { func fieldsFromName(p Name) fields {
@ -22,6 +23,7 @@ func fieldsFromName(p Name) fields {
model: p.parts[Model], model: p.parts[Model],
tag: p.parts[Tag], tag: p.parts[Tag],
build: p.parts[Build], build: p.parts[Build],
digest: p.parts[Digest],
} }
} }
@ -47,6 +49,9 @@ var testNames = map[string]fields{
"example.com/ns/mistral:7b+Q4_0": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "Q4_0"}, "example.com/ns/mistral:7b+Q4_0": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "Q4_0"},
"example.com/ns/mistral:7b+X": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "X"}, "example.com/ns/mistral:7b+X": {host: "example.com", namespace: "ns", model: "mistral", tag: "7b", build: "X"},
// resolved
"x@123": {model: "x", digest: "123"},
// preserves case for build // preserves case for build
"x+b": {model: "x", build: "b"}, "x+b": {model: "x", build: "b"},
@ -87,10 +92,9 @@ var testNames = map[string]fields{
} }
func TestNameParts(t *testing.T) { func TestNameParts(t *testing.T) {
const wantNumParts = 5
var p Name var p Name
if len(p.Parts()) != wantNumParts { if len(p.Parts()) != int(NumParts) {
t.Errorf("Parts() = %d; want %d", len(p.Parts()), wantNumParts) t.Errorf("Parts() = %d; want %d", len(p.Parts()), NumParts)
} }
} }
@ -211,6 +215,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "library/mistral:latest", wantLong: "library/mistral:latest",
wantComplete: "example.com/library/mistral:latest", wantComplete: "example.com/library/mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "example.com/library/mistral:latest+Q4_0@?",
}, },
{ {
name: "Short Name", name: "Short Name",
@ -219,7 +224,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "mistral:latest", wantLong: "mistral:latest",
wantComplete: "mistral:latest", wantComplete: "mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "?/?/mistral:latest+?", wantGoString: "?/?/mistral:latest+?@?",
}, },
{ {
name: "Long Name", name: "Long Name",
@ -228,7 +233,7 @@ func TestNameDisplay(t *testing.T) {
wantLong: "library/mistral:latest", wantLong: "library/mistral:latest",
wantComplete: "library/mistral:latest", wantComplete: "library/mistral:latest",
wantModel: "mistral", wantModel: "mistral",
wantGoString: "?/library/mistral:latest+?", wantGoString: "?/library/mistral:latest+?@?",
}, },
{ {
name: "Case Preserved", name: "Case Preserved",
@ -237,7 +242,16 @@ func TestNameDisplay(t *testing.T) {
wantLong: "Library/Mistral:Latest", wantLong: "Library/Mistral:Latest",
wantComplete: "Library/Mistral:Latest", wantComplete: "Library/Mistral:Latest",
wantModel: "Mistral", wantModel: "Mistral",
wantGoString: "?/Library/Mistral:Latest+?", wantGoString: "?/Library/Mistral:Latest+?@?",
},
{
name: "With digest",
in: "Library/Mistral:Latest@sha256-123456",
wantShort: "Mistral:Latest",
wantLong: "Library/Mistral:Latest",
wantComplete: "Library/Mistral:Latest",
wantModel: "Mistral",
wantGoString: "?/Library/Mistral:Latest+?@sha256-123456",
}, },
} }