1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain
4
5import (
6 "fmt"
7 "slices"
8 "strings"
9 "time"
10)
11
12// Visibility is the access level of a repository.
13type Visibility string
14
15// Visibility values.
16const (
17 // VisibilityPrivate hides the repository from anonymous callers.
18 VisibilityPrivate Visibility = "private"
19 // VisibilityPublic lets anonymous callers read the repository.
20 VisibilityPublic Visibility = "public"
21)
22
23// ParseVisibility parses "public" or "private".
24func ParseVisibility(s string) (Visibility, error) {
25 switch v := Visibility(s); v {
26 case VisibilityPublic, VisibilityPrivate:
27 return v, nil
28 }
29 return "", fmt.Errorf("%w: %q (want public or private)", ErrInvalidVisibility, s)
30}
31
32// String returns the visibility as text.
33func (v Visibility) String() string { return string(v) }
34
35// Repo is the metadata ermite owns about a repository. Git-owned state (refs,
36// HEAD) stays in Git.
37type Repo struct {
38 Name RepoName
39 Description string
40 // Topics are the normalized topics (see NormalizeTopics); nil for none.
41 Topics []string
42 Visibility Visibility
43 CreatedAt time.Time
44 PushedAt time.Time // zero until the first push
45 // Settings are the merge settings; the stores always fill them in, with
46 // the defaults for a repository that never stored any.
47 Settings RepoSettings
48}
49
50// Limits of the topics of a repository.
51const (
52 // MaxTopics is the largest number of topics of a repository.
53 MaxTopics = 20
54 // MaxTopicLen is the longest topic, in characters.
55 MaxTopicLen = 50
56)
57
58// NormalizeTopics lowercases, validates, deduplicates and sorts topics. A
59// topic is 1 to MaxTopicLen characters of a-z, 0-9 and "-", starting with a
60// letter or a digit; there are at most MaxTopics of them once deduplicated.
61// No topic gives nil. A broken rule gives an error matching ErrInvalid.
62func NormalizeTopics(topics []string) ([]string, error) {
63 var out []string
64 for _, t := range topics {
65 t = strings.ToLower(t)
66 if !validTopic(t) {
67 return nil, fmt.Errorf("%w: topic %q must be 1 to %d characters of a-z, 0-9 and -, starting with a letter or a digit", ErrInvalid, t, MaxTopicLen)
68 }
69 out = append(out, t)
70 }
71 slices.Sort(out)
72 out = slices.Compact(out)
73 if len(out) > MaxTopics {
74 return nil, fmt.Errorf("%w: %d topics, at most %d", ErrInvalid, len(out), MaxTopics)
75 }
76 return out, nil
77}
78
79func validTopic(t string) bool {
80 if t == "" || len(t) > MaxTopicLen {
81 return false
82 }
83 for i := range len(t) {
84 c := t[i]
85 switch {
86 case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
87 case c == '-' && i > 0:
88 default:
89 return false
90 }
91 }
92 return true
93}