mirror of
https://github.com/hanzoai/namespace.git
synced 2026-09-30 22:57:59 +00:00
Naming the database and rendering that name as a place were one primitive living in two repositories: hanzoai/cloud held the slugger, the layout and the resolver, and this package held only the value they produced. A fact with two homes is a fact that can disagree with itself, and these particular strings are directory names on live volumes and keys in live buckets — a disagreement does not fail, it opens an empty database beside a real one. So the whole primitive moves here and cloud imports it: Sanitize one injective slug for a name a person chose OrgProject the door those names walk through to become a namespace MustOrgProject Key a namespace rendered as the place one subsystem's file lives Path Key resolved against a data directory Sanitize refuses a name carrying an invisible rune (nothing downstream can be trusted not to trim it) and disambiguates every other fold with a hash of the raw bytes, so two spellings are two files. OrgProject is named for what it takes; Of already means something else here — the namespace a billing subject owns — and clobbering it would have been the same collapse in miniature. Every golden value in the tests was captured from the implementation that wrote the files already on disk, before the code moved, so the rendering is byte-identical and no live data is orphaned. Co-authored-by: Hanzo Dev <[email protected]>
74 lines
2.7 KiB
Go
74 lines
2.7 KiB
Go
// Copyright (C) 2026, Hanzo AI. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package namespace
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
)
|
|
|
|
// orgsRoot is the single directory every namespace's files live under, and
|
|
// platformSlug is how the system namespace renders inside it — a leading "_",
|
|
// which Sanitize never emits, so the deployment's own partition cannot collide
|
|
// with an entity's however the slugger changes.
|
|
//
|
|
// Both are spelled once, here, because these strings are on disk and in object
|
|
// storage: a second spelling is a second answer to "where is this store", and
|
|
// the files that already exist only have one place.
|
|
const (
|
|
orgsRoot = "orgs"
|
|
platformSlug = "_platform"
|
|
)
|
|
|
|
// Key renders a namespace as the place one subsystem's file for it lives:
|
|
// relative to a data directory on disk, and byte-identically the object key
|
|
// durability ships it to. One rendering, two consumers, so the local file and
|
|
// the remote slot are the same name by construction.
|
|
//
|
|
// org/{id} → orgs/{id}/{subsystem}.db
|
|
// org/{id}/{group} → orgs/{id}/projects/{group}/{subsystem}.db
|
|
// system → orgs/_platform/{subsystem}.db
|
|
//
|
|
// The subsystem is a separate argument rather than part of the namespace
|
|
// because one store is one subsystem: which subsystem is a property of the
|
|
// registry that opens the file, not of the entity the file belongs to.
|
|
//
|
|
// Every other kind is an error. User and repo namespaces are real namespaces,
|
|
// but this layout has no place for them, and inventing one silently is how a
|
|
// second convention starts.
|
|
func Key(ns Namespace, subsystem string) (string, error) {
|
|
if subsystem == "" {
|
|
return "", fmt.Errorf("namespace: empty subsystem")
|
|
}
|
|
switch ns.Kind() {
|
|
case KindSystem:
|
|
return path.Join(orgsRoot, platformSlug, subsystem+".db"), nil
|
|
case KindOrg:
|
|
if g := ns.Group().String(); g != "" {
|
|
return path.Join(orgsRoot, ns.ID(), "projects", g, subsystem+".db"), nil
|
|
}
|
|
return path.Join(orgsRoot, ns.ID(), subsystem+".db"), nil
|
|
case "":
|
|
return "", fmt.Errorf("namespace: the zero namespace names no database")
|
|
default:
|
|
return "", fmt.Errorf("namespace: no database layout for a %s namespace", ns.Kind())
|
|
}
|
|
}
|
|
|
|
// Path is Key resolved against a deployment's data directory, in the host's own
|
|
// separator. An empty dir is an error rather than a relative path, because a
|
|
// store that opened relative to whatever the working directory happened to be
|
|
// would be a different file per invocation.
|
|
func Path(dir string, ns Namespace, subsystem string) (string, error) {
|
|
if dir == "" {
|
|
return "", fmt.Errorf("namespace: empty dir")
|
|
}
|
|
key, err := Key(ns, subsystem)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(dir, filepath.FromSlash(key)), nil
|
|
}
|