1package entity
2
3import (
4 "fmt"
5 "io"
6 "strings"
7
8 "github.com/pkg/errors"
9
10 bootstrap "github.com/MichaelMure/git-bug/entity/boostrap"
11)
12
13const UnsetCombinedId = CombinedId("unset")
14
15// CombinedId is an Id holding information from both a primary Id and a secondary Id.
16// While it looks like a regular Id, do not just cast from one to another.
17// Instead, use CombineIds and SeparateIds to create it and split it.
18type CombinedId string
19
20// String return the identifier as a string
21func (ci CombinedId) String() string {
22 return string(ci)
23}
24
25// Human return the identifier, shortened for human consumption
26func (ci CombinedId) Human() string {
27 format := fmt.Sprintf("%%.%ds", HumanIdLength)
28 return fmt.Sprintf(format, ci)
29}
30
31func (ci CombinedId) HasPrefix(prefix string) bool {
32 return strings.HasPrefix(string(ci), prefix)
33}
34
35// UnmarshalGQL implement the Unmarshaler interface for gqlgen
36func (ci *CombinedId) UnmarshalGQL(v interface{}) error {
37 _, ok := v.(string)
38 if !ok {
39 return fmt.Errorf("CombinedIds must be strings")
40 }
41
42 *ci = v.(CombinedId)
43
44 if err := ci.Validate(); err != nil {
45 return errors.Wrap(err, "invalid CombinedId")
46 }
47
48 return nil
49}
50
51// MarshalGQL implement the Marshaler interface for gqlgen
52func (ci CombinedId) MarshalGQL(w io.Writer) {
53 _, _ = w.Write([]byte(`"` + ci.String() + `"`))
54}
55
56// Validate tell if the Id is valid
57func (ci CombinedId) Validate() error {
58 // Special case to detect outdated repo
59 if len(ci) == 40 {
60 return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade")
61 }
62 if len(ci) != bootstrap.IdLength {
63 return fmt.Errorf("invalid length")
64 }
65 for _, r := range ci {
66 if (r < 'a' || r > 'z') && (r < '0' || r > '9') {
67 return fmt.Errorf("invalid character")
68 }
69 }
70 return nil
71}
72
73// PrimaryPrefix is a helper to extract the primary prefix.
74// If practical, use SeparateIds instead.
75func (ci CombinedId) PrimaryPrefix() string {
76 primaryPrefix, _ := SeparateIds(string(ci))
77 return primaryPrefix
78}
79
80// SecondaryPrefix is a helper to extract the secondary prefix.
81// If practical, use SeparateIds instead.
82func (ci CombinedId) SecondaryPrefix() string {
83 _, secondaryPrefix := SeparateIds(string(ci))
84 return secondaryPrefix
85}
86
87// CombineIds compute a merged Id holding information from both the primary Id
88// and the secondary Id.
89//
90// This allows to later find efficiently a secondary element because we can access
91// the primary one directly instead of searching for a primary that has a
92// secondary matching the Id.
93//
94// An example usage is Comment in a Bug. The interleaved Id will hold part of the
95// Bug Id and part of the Comment Id.
96//
97// To allow the use of an arbitrary length prefix of this Id, Ids from primary
98// and secondary are interleaved with this irregular pattern to give the
99// best chance to find the secondary even with a 7 character prefix.
100//
101// Format is: PSPSPSPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPP
102//
103// A complete interleaved Id hold 50 characters for the primary and 14 for the
104// secondary, which give a key space of 36^50 for the primary (~6 * 10^77) and
105// 36^14 for the secondary (~6 * 10^21). This asymmetry assumes a reasonable number
106// of secondary within a primary Entity, while still allowing for a vast key space
107// for the primary (that is, a globally merged database) with a low risk of collision.
108//
109// Here is the breakdown of several common prefix length:
110//
111// 5: 3P, 2S
112// 7: 4P, 3S
113// 10: 6P, 4S
114// 16: 11P, 5S
115func CombineIds(primary Id, secondary Id) CombinedId {
116 var id strings.Builder
117
118 for i := 0; i < bootstrap.IdLength; i++ {
119 switch {
120 default:
121 id.WriteByte(primary[0])
122 primary = primary[1:]
123 case i == 1, i == 3, i == 5, i == 9, i >= 10 && i%5 == 4:
124 id.WriteByte(secondary[0])
125 secondary = secondary[1:]
126 }
127 }
128
129 return CombinedId(id.String())
130}
131
132// SeparateIds extract primary and secondary prefix from an arbitrary length prefix
133// of an Id created with CombineIds.
134func SeparateIds(prefix string) (primaryPrefix string, secondaryPrefix string) {
135 var primary strings.Builder
136 var secondary strings.Builder
137
138 for i, r := range prefix {
139 switch {
140 default:
141 primary.WriteRune(r)
142 case i == 1, i == 3, i == 5, i == 9, i >= 10 && i%5 == 4:
143 secondary.WriteRune(r)
144 }
145 }
146
147 return primary.String(), secondary.String()
148}