1package namespace
2
3import (
4	"context"
5	"errors"
6	"strings"
7)
8
9type contextValues struct{}
10
11type Namespace struct {
12	ID   string `json:"id"`
13	Path string `json:"path"`
14}
15
16const (
17	RootNamespaceID = "root"
18)
19
20var (
21	contextNamespace contextValues = struct{}{}
22	ErrNoNamespace   error         = errors.New("no namespace")
23	RootNamespace    *Namespace    = &Namespace{
24		ID:   RootNamespaceID,
25		Path: "",
26	}
27)
28
29func (n *Namespace) HasParent(possibleParent *Namespace) bool {
30	switch {
31	case n.Path == "":
32		return false
33	case possibleParent.Path == "":
34		return true
35	default:
36		return strings.HasPrefix(n.Path, possibleParent.Path)
37	}
38}
39
40func (n *Namespace) TrimmedPath(path string) string {
41	return strings.TrimPrefix(path, n.Path)
42}
43
44func ContextWithNamespace(ctx context.Context, ns *Namespace) context.Context {
45	return context.WithValue(ctx, contextNamespace, ns)
46}
47
48func RootContext(ctx context.Context) context.Context {
49	if ctx == nil {
50		return ContextWithNamespace(context.Background(), RootNamespace)
51	}
52	return ContextWithNamespace(ctx, RootNamespace)
53}
54
55// This function caches the ns to avoid doing a .Value lookup over and over,
56// because it's called a *lot* in the request critical path. .Value is
57// concurrency-safe so uses some kind of locking/atomicity, but it should never
58// be read before first write, plus we don't believe this will be called from
59// different goroutines, so it should be safe.
60func FromContext(ctx context.Context) (*Namespace, error) {
61	if ctx == nil {
62		return nil, errors.New("context was nil")
63	}
64
65	nsRaw := ctx.Value(contextNamespace)
66	if nsRaw == nil {
67		return nil, ErrNoNamespace
68	}
69
70	ns := nsRaw.(*Namespace)
71	if ns == nil {
72		return nil, ErrNoNamespace
73	}
74
75	return ns, nil
76}
77
78// Canonicalize trims any prefix '/' and adds a trailing '/' to the
79// provided string
80func Canonicalize(nsPath string) string {
81	if nsPath == "" {
82		return ""
83	}
84
85	// Canonicalize the path to not have a '/' prefix
86	nsPath = strings.TrimPrefix(nsPath, "/")
87
88	// Canonicalize the path to always having a '/' suffix
89	if !strings.HasSuffix(nsPath, "/") {
90		nsPath += "/"
91	}
92
93	return nsPath
94}
95
96func SplitIDFromString(input string) (string, string) {
97	prefix := ""
98	slashIdx := strings.LastIndex(input, "/")
99
100	switch {
101	case strings.HasPrefix(input, "b."):
102		prefix = "b."
103		input = input[2:]
104
105	case strings.HasPrefix(input, "s."):
106		prefix = "s."
107		input = input[2:]
108
109	case slashIdx > 0:
110		// Leases will never have a b./s. to start
111		if slashIdx == len(input)-1 {
112			return input, ""
113		}
114		prefix = input[:slashIdx+1]
115		input = input[slashIdx+1:]
116	}
117
118	idx := strings.LastIndex(input, ".")
119	if idx == -1 {
120		return prefix + input, ""
121	}
122	if idx == len(input)-1 {
123		return prefix + input, ""
124	}
125
126	return prefix + input[:idx], input[idx+1:]
127}
128