diff --git a/x/build/blob/ref.go b/x/model/path.go similarity index 72% rename from x/build/blob/ref.go rename to x/model/path.go index 6682bb29..197b74f3 100644 --- a/x/build/blob/ref.go +++ b/x/model/path.go @@ -1,4 +1,4 @@ -package blob +package model import ( "cmp" @@ -7,7 +7,7 @@ import ( "strings" ) -const MaxRefLength = 255 +const MaxPathLength = 255 type PartKind int @@ -30,12 +30,12 @@ var kindNames = map[PartKind]string{ Build: "Build", } -// Ref is an opaque reference to a blob. +// Path is an opaque reference to a model. // // It is comparable and can be used as a map key. // -// Users or Ref must check Valid before using it. -type Ref struct { +// Users or Path must check Valid before using it. +type Path struct { domain string namespace string name string @@ -46,7 +46,7 @@ type Ref struct { // Format returns a string representation of the ref with the given // concreteness. If a part is missing, it is replaced with a loud // placeholder. -func (r Ref) Full() string { +func (r Path) Full() string { r.domain = cmp.Or(r.domain, "!(MISSING DOMAIN)") r.namespace = cmp.Or(r.namespace, "!(MISSING NAMESPACE)") r.name = cmp.Or(r.name, "!(MISSING NAME)") @@ -55,21 +55,21 @@ func (r Ref) Full() string { return r.String() } -func (r Ref) NameAndTag() string { +func (r Path) NameAndTag() string { r.domain = "" r.namespace = "" r.build = "" return r.String() } -func (r Ref) NameTagAndBuild() string { +func (r Path) NameTagAndBuild() string { r.domain = "" r.namespace = "" return r.String() } // String returns the fully qualified ref string. -func (r Ref) String() string { +func (r Path) String() string { var b strings.Builder if r.domain != "" { b.WriteString(r.domain) @@ -93,19 +93,19 @@ func (r Ref) String() string { // Complete reports whether the ref is fully qualified. That is it has a // domain, namespace, name, tag, and build. -func (r Ref) Complete() bool { +func (r Path) Complete() bool { return r.Valid() && !slices.Contains(r.Parts(), "") } // CompleteWithoutBuild reports whether the ref would be complete if it had a // valid build. -func (r Ref) CompleteWithoutBuild() bool { +func (r Path) CompleteWithoutBuild() bool { r.build = "x" return r.Valid() && r.Complete() } // Less returns true if r is less concrete than o; false otherwise. -func (r Ref) Less(o Ref) bool { +func (r Path) Less(o Path) bool { rp := r.Parts() op := o.Parts() for i := range rp { @@ -119,7 +119,7 @@ func (r Ref) Less(o Ref) bool { // Parts returns the parts of the ref in order of concreteness. // // The length of the returned slice is always 5. -func (r Ref) Parts() []string { +func (r Path) Parts() []string { return []string{ r.domain, r.namespace, @@ -129,36 +129,30 @@ func (r Ref) Parts() []string { } } -func (r Ref) Domain() string { return r.namespace } -func (r Ref) Namespace() string { return r.namespace } -func (r Ref) Name() string { return r.name } -func (r Ref) Tag() string { return r.tag } -func (r Ref) Build() string { return r.build } +func (r Path) Domain() string { return r.namespace } +func (r Path) Namespace() string { return r.namespace } +func (r Path) Name() string { return r.name } +func (r Path) Tag() string { return r.tag } +func (r Path) Build() string { return r.build } -// ParseRef parses a ref string into a Ref. A ref string is a name, an -// optional tag, and an optional build, separated by colons and pluses. +// ParsePath parses a model path string into a Path. // -// The name must be valid ascii [a-zA-Z0-9_]. -// The tag must be valid ascii [a-zA-Z0-9_]. -// The build must be valid ascii [a-zA-Z0-9_]. +// Examples of valid paths: // -// It returns then zero value if the ref is invalid. +// "example.com/mistral:7b+x" +// "example.com/mistral:7b+Q4_0" +// "mistral:7b+x" +// "example.com/x/mistral:latest+Q4_0" +// "example.com/x/mistral:latest" // -// // Valid Examples: -// ParseRef("mistral:latest") returns ("mistral", "latest", "") -// ParseRef("mistral") returns ("mistral", "", "") -// ParseRef("mistral:30B") returns ("mistral", "30B", "") -// ParseRef("mistral:7b") returns ("mistral", "7b", "") -// ParseRef("mistral:7b+Q4_0") returns ("mistral", "7b", "Q4_0") -// ParseRef("mistral+KQED") returns ("mistral", "latest", "KQED") -// ParseRef(".x.:7b+Q4_0:latest") returns (".x.", "7b", "Q4_0") -// ParseRef("-grok-f.oo:7b+Q4_0") returns ("-grok-f.oo", "7b", "Q4_0") +// Examples of invalid paths: // -// // Invalid Examples: -// ParseRef("m stral") returns ("", "", "") // zero -// ParseRef("... 129 chars ...") returns ("", "", "") // zero -func ParseRef(s string) Ref { - var r Ref +// "example.com/mistral:7b+" +// "example.com/mistral:7b+Q4_0+" +// "x/y/z/z:8n+I" +// "" +func ParsePath(s string) Path { + var r Path for kind, part := range Parts(s) { switch kind { case Domain: @@ -172,11 +166,11 @@ func ParseRef(s string) Ref { case Build: r.build = strings.ToUpper(part) case Invalid: - return Ref{} + return Path{} } } if !r.Valid() { - return Ref{} + return Path{} } return r } @@ -185,8 +179,8 @@ func ParseRef(s string) Ref { // The name is left untouched. // // Use this for merging a ref with a default ref. -func Merge(a, b Ref) Ref { - return Ref{ +func Merge(a, b Path) Path { + return Path{ // name is left untouched name: a.name, @@ -211,7 +205,7 @@ func Parts(s string) iter.Seq2[PartKind, string] { s = s[len("https://"):] } - if len(s) > MaxRefLength || len(s) == 0 { + if len(s) > MaxPathLength || len(s) == 0 { return } @@ -282,7 +276,7 @@ func Parts(s string) iter.Seq2[PartKind, string] { // Valid returns true if the ref has a valid name. To know if a ref is // "complete", use Complete. -func (r Ref) Valid() bool { +func (r Path) Valid() bool { // Parts ensures we only have valid parts, so no need to validate // them here, only check if we have a name or not. return r.name != "" diff --git a/x/build/blob/ref_test.go b/x/model/path_test.go similarity index 75% rename from x/build/blob/ref_test.go rename to x/model/path_test.go index c162e91d..7878b13a 100644 --- a/x/build/blob/ref_test.go +++ b/x/model/path_test.go @@ -1,4 +1,4 @@ -package blob +package model import ( "fmt" @@ -6,7 +6,7 @@ import ( "testing" ) -var testRefs = map[string]Ref{ +var testPaths = map[string]Path{ "mistral:latest": {name: "mistral", tag: "latest"}, "mistral": {name: "mistral"}, "mistral:30B": {name: "mistral", tag: "30B"}, @@ -36,33 +36,33 @@ var testRefs = map[string]Ref{ "file:///etc/passwd:latest": {}, "file:///etc/passwd:latest+u": {}, - strings.Repeat("a", MaxRefLength): {name: strings.Repeat("a", MaxRefLength)}, - strings.Repeat("a", MaxRefLength+1): {}, + strings.Repeat("a", MaxPathLength): {name: strings.Repeat("a", MaxPathLength)}, + strings.Repeat("a", MaxPathLength+1): {}, } -func TestRefParts(t *testing.T) { +func TestPathParts(t *testing.T) { const wantNumParts = 5 - var ref Ref - if len(ref.Parts()) != wantNumParts { - t.Errorf("Parts() = %d; want %d", len(ref.Parts()), wantNumParts) + var p Path + if len(p.Parts()) != wantNumParts { + t.Errorf("Parts() = %d; want %d", len(p.Parts()), wantNumParts) } } -func TestParseRef(t *testing.T) { - for s, want := range testRefs { +func TestParsePath(t *testing.T) { + for s, want := range testPaths { for _, prefix := range []string{"", "https://", "http://"} { // We should get the same results with or without the // http(s) prefixes s := prefix + s t.Run(s, func(t *testing.T) { - got := ParseRef(s) + got := ParsePath(s) if got != want { - t.Errorf("ParseRef(%q) = %q; want %q", s, got, want) + t.Errorf("ParsePath(%q) = %q; want %q", s, got, want) } // test round-trip - if ParseRef(got.String()) != got { + if ParsePath(got.String()) != got { t.Errorf("String() = %s; want %s", got.String(), s) } @@ -76,7 +76,7 @@ func TestParseRef(t *testing.T) { } } -func TestRefComplete(t *testing.T) { +func TestPathComplete(t *testing.T) { cases := []struct { in string complete bool @@ -92,19 +92,19 @@ func TestRefComplete(t *testing.T) { for _, tt := range cases { t.Run(tt.in, func(t *testing.T) { - ref := ParseRef(tt.in) - t.Logf("ParseRef(%q) = %#v", tt.in, ref) - if g := ref.Complete(); g != tt.complete { + p := ParsePath(tt.in) + t.Logf("ParsePath(%q) = %#v", tt.in, p) + if g := p.Complete(); g != tt.complete { t.Errorf("Complete(%q) = %v; want %v", tt.in, g, tt.complete) } - if g := ref.CompleteWithoutBuild(); g != tt.completeWithoutBuild { + if g := p.CompleteWithoutBuild(); g != tt.completeWithoutBuild { t.Errorf("CompleteWithoutBuild(%q) = %v; want %v", tt.in, g, tt.completeWithoutBuild) } }) } } -func TestRefStringVariants(t *testing.T) { +func TestPathStringVariants(t *testing.T) { cases := []struct { in string nameAndTag string @@ -116,19 +116,19 @@ func TestRefStringVariants(t *testing.T) { for _, tt := range cases { t.Run(tt.in, func(t *testing.T) { - ref := ParseRef(tt.in) - t.Logf("ParseRef(%q) = %#v", tt.in, ref) - if g := ref.NameAndTag(); g != tt.nameAndTag { + p := ParsePath(tt.in) + t.Logf("ParsePath(%q) = %#v", tt.in, p) + if g := p.NameAndTag(); g != tt.nameAndTag { t.Errorf("NameAndTag(%q) = %q; want %q", tt.in, g, tt.nameAndTag) } - if g := ref.NameTagAndBuild(); g != tt.nameTagAndBuild { + if g := p.NameTagAndBuild(); g != tt.nameTagAndBuild { t.Errorf("NameTagAndBuild(%q) = %q; want %q", tt.in, g, tt.nameTagAndBuild) } }) } } -func TestRefFull(t *testing.T) { +func TestPathFull(t *testing.T) { const empty = "!(MISSING DOMAIN)/!(MISSING NAMESPACE)/!(MISSING NAME):!(MISSING TAG)+!(MISSING BUILD)" cases := []struct { @@ -151,53 +151,53 @@ func TestRefFull(t *testing.T) { for _, tt := range cases { t.Run(tt.in, func(t *testing.T) { - ref := ParseRef(tt.in) - t.Logf("ParseRef(%q) = %#v", tt.in, ref) - if g := ref.Full(); g != tt.wantFull { + p := ParsePath(tt.in) + t.Logf("ParsePath(%q) = %#v", tt.in, p) + if g := p.Full(); g != tt.wantFull { t.Errorf("Full(%q) = %q; want %q", tt.in, g, tt.wantFull) } }) } } -func TestParseRefAllocs(t *testing.T) { +func TestParsePathAllocs(t *testing.T) { // test allocations - var r Ref + var r Path allocs := testing.AllocsPerRun(1000, func() { - r = ParseRef("example.com/mistral:7b+Q4_0") + r = ParsePath("example.com/mistral:7b+Q4_0") }) _ = r if allocs > 0 { - t.Errorf("ParseRef allocs = %v; want 0", allocs) + t.Errorf("ParsePath allocs = %v; want 0", allocs) } } -func BenchmarkParseRef(b *testing.B) { +func BenchmarkParsePath(b *testing.B) { b.ReportAllocs() - var r Ref + var r Path for i := 0; i < b.N; i++ { - r = ParseRef("example.com/mistral:7b+Q4_0") + r = ParsePath("example.com/mistral:7b+Q4_0") } _ = r } -func FuzzParseRef(f *testing.F) { +func FuzzParsePath(f *testing.F) { f.Add("example.com/mistral:7b+Q4_0") f.Add("example.com/mistral:7b+q4_0") f.Add("example.com/mistral:7b+x") f.Add("x/y/z:8n+I") f.Fuzz(func(t *testing.T, s string) { - r0 := ParseRef(s) + r0 := ParsePath(s) if !r0.Valid() { - if r0 != (Ref{}) { - t.Errorf("expected invalid ref to be zero value; got %#v", r0) + if r0 != (Path{}) { + t.Errorf("expected invalid path to be zero value; got %#v", r0) } - t.Skipf("invalid ref: %q", s) + t.Skipf("invalid path: %q", s) } for _, p := range r0.Parts() { - if len(p) > MaxRefLength { + if len(p) > MaxPathLength { t.Errorf("part too long: %q", p) } } @@ -206,7 +206,7 @@ func FuzzParseRef(f *testing.F) { t.Errorf("String() did not round-trip with case insensitivity: %q\ngot = %q\nwant = %q", s, r0.String(), s) } - r1 := ParseRef(r0.String()) + r1 := ParsePath(r0.String()) if r0 != r1 { t.Errorf("round-trip mismatch: %+v != %+v", r0, r1) } @@ -216,8 +216,8 @@ func FuzzParseRef(f *testing.F) { func ExampleMerge() { r := Merge( - ParseRef("mistral"), - ParseRef("registry.ollama.com/XXXXX:latest+Q4_0"), + ParsePath("mistral"), + ParsePath("registry.ollama.com/XXXXX:latest+Q4_0"), ) fmt.Println(r) diff --git a/x/build/blob/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa b/x/model/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa similarity index 100% rename from x/build/blob/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa rename to x/model/testdata/fuzz/FuzzParseRef/1d43ee52085cb4aa diff --git a/x/build/blob/testdata/fuzz/FuzzParseRef/27fd759314f0e6d6 b/x/model/testdata/fuzz/FuzzParseRef/27fd759314f0e6d6 similarity index 100% rename from x/build/blob/testdata/fuzz/FuzzParseRef/27fd759314f0e6d6 rename to x/model/testdata/fuzz/FuzzParseRef/27fd759314f0e6d6 diff --git a/x/build/blob/testdata/fuzz/FuzzParseRef/3e3b70dba384074d b/x/model/testdata/fuzz/FuzzParseRef/3e3b70dba384074d similarity index 100% rename from x/build/blob/testdata/fuzz/FuzzParseRef/3e3b70dba384074d rename to x/model/testdata/fuzz/FuzzParseRef/3e3b70dba384074d diff --git a/x/build/blob/testdata/fuzz/FuzzParseRef/71f1fdff711b6dab b/x/model/testdata/fuzz/FuzzParseRef/71f1fdff711b6dab similarity index 100% rename from x/build/blob/testdata/fuzz/FuzzParseRef/71f1fdff711b6dab rename to x/model/testdata/fuzz/FuzzParseRef/71f1fdff711b6dab diff --git a/x/build/blob/testdata/fuzz/FuzzParseRef/82c2975c430ac608 b/x/model/testdata/fuzz/FuzzParseRef/82c2975c430ac608 similarity index 100% rename from x/build/blob/testdata/fuzz/FuzzParseRef/82c2975c430ac608 rename to x/model/testdata/fuzz/FuzzParseRef/82c2975c430ac608 diff --git a/x/build/blob/testdata/fuzz/FuzzParseRef/b51b1c875e61a948 b/x/model/testdata/fuzz/FuzzParseRef/b51b1c875e61a948 similarity index 100% rename from x/build/blob/testdata/fuzz/FuzzParseRef/b51b1c875e61a948 rename to x/model/testdata/fuzz/FuzzParseRef/b51b1c875e61a948 diff --git a/x/registry/apitype/apitype.go b/x/registry/apitype/apitype.go index 19602868..77b319fd 100644 --- a/x/registry/apitype/apitype.go +++ b/x/registry/apitype/apitype.go @@ -18,7 +18,7 @@ type Layer struct { } type PushRequest struct { - Ref string `json:"ref"` + Name string `json:"ref"` Manifest json.RawMessage `json:"manifest"` // Parts is a list of upload parts that the client upload in the previous diff --git a/x/registry/client.go b/x/registry/client.go index 2d77efe4..957b772f 100644 --- a/x/registry/client.go +++ b/x/registry/client.go @@ -32,7 +32,7 @@ func (c *Client) Push(ctx context.Context, ref string, manifest []byte, p *PushP p = cmp.Or(p, &PushParams{}) // TODO(bmizerany): backoff v, err := ollama.Do[apitype.PushResponse](ctx, c.oclient(), "POST", "/v1/push", &apitype.PushRequest{ - Ref: ref, + Name: ref, Manifest: manifest, CompleteParts: p.CompleteParts, }) diff --git a/x/registry/server.go b/x/registry/server.go index ed270702..abce82a7 100644 --- a/x/registry/server.go +++ b/x/registry/server.go @@ -14,8 +14,8 @@ import ( "strconv" "time" - "bllamo.com/build/blob" "bllamo.com/client/ollama" + "bllamo.com/model" "bllamo.com/oweb" "bllamo.com/registry/apitype" "bllamo.com/utils/upload" @@ -82,9 +82,9 @@ func (s *Server) handlePush(w http.ResponseWriter, r *http.Request) error { return err } - ref := blob.ParseRef(pr.Ref) - if !ref.Complete() { - return oweb.Invalid("name", pr.Ref, "must be complete") + mp := model.ParsePath(pr.Name) + if !mp.Complete() { + return oweb.Invalid("name", pr.Name, "must be complete") } m, err := oweb.DecodeUserJSON[apitype.Manifest]("manifest", bytes.NewReader(pr.Manifest)) @@ -205,7 +205,7 @@ func (s *Server) handlePush(w http.ResponseWriter, r *http.Request) error { if len(requirements) == 0 { // Commit the manifest body := bytes.NewReader(pr.Manifest) - path := path.Join("manifests", path.Join(ref.Parts()...)) + path := path.Join("manifests", path.Join(mp.Parts()...)) _, err := s.mc().PutObject(r.Context(), bucketTODO, path, body, int64(len(pr.Manifest)), minio.PutObjectOptions{}) if err != nil { return err