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