aboutsummaryrefslogtreecommitdiffstats
path: root/entity/id_interleaved.go
blob: 7ae6d72e94cd623fe0970b0f09c1817630ce782c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package entity

import (
	"fmt"
	"io"
	"strings"

	"github.com/pkg/errors"
)

const UnsetCombinedId = CombinedId("unset")

// CombinedId is an Id holding information from both a primary Id and a secondary Id.
// While it looks like a regular Id, do not just cast from one to another.
// Instead, use CombineIds and SeparateIds to create it and split it.
type CombinedId string

// String return the identifier as a string
func (ci CombinedId) String() string {
	return string(ci)
}

// Human return the identifier, shortened for human consumption
func (ci CombinedId) Human() string {
	format := fmt.Sprintf("%%.%ds", HumanIdLength)
	return fmt.Sprintf(format, ci)
}

func (ci CombinedId) HasPrefix(prefix string) bool {
	return strings.HasPrefix(string(ci), prefix)
}

// UnmarshalGQL implement the Unmarshaler interface for gqlgen
func (ci *CombinedId) UnmarshalGQL(v interface{}) error {
	_, ok := v.(string)
	if !ok {
		return fmt.Errorf("CombinedIds must be strings")
	}

	*ci = v.(CombinedId)

	if err := ci.Validate(); err != nil {
		return errors.Wrap(err, "invalid CombinedId")
	}

	return nil
}

// MarshalGQL implement the Marshaler interface for gqlgen
func (ci CombinedId) MarshalGQL(w io.Writer) {
	_, _ = w.Write([]byte(`"` + ci.String() + `"`))
}

// Validate tell if the Id is valid
func (ci CombinedId) Validate() error {
	// Special case to detect outdated repo
	if len(ci) == 40 {
		return fmt.Errorf("outdated repository format, please use https://github.com/MichaelMure/git-bug-migration to upgrade")
	}
	if len(ci) != idLength {
		return fmt.Errorf("invalid length")
	}
	for _, r := range ci {
		if (r < 'a' || r > 'z') && (r < '0' || r > '9') {
			return fmt.Errorf("invalid character")
		}
	}
	return nil
}

// PrimaryPrefix is a helper to extract the primary prefix.
// If practical, use SeparateIds instead.
func (ci CombinedId) PrimaryPrefix() string {
	primaryPrefix, _ := SeparateIds(string(ci))
	return primaryPrefix
}

// SecondaryPrefix is a helper to extract the secondary prefix.
// If practical, use SeparateIds instead.
func (ci CombinedId) SecondaryPrefix() string {
	_, secondaryPrefix := SeparateIds(string(ci))
	return secondaryPrefix
}

// CombineIds compute a merged Id holding information from both the primary Id
// and the secondary Id.
//
// This allows to later find efficiently a secondary element because we can access
// the primary one directly instead of searching for a primary that has a
// secondary matching the Id.
//
// An example usage is Comment in a Bug. The interleaved Id will hold part of the
// Bug Id and part of the Comment Id.
//
// To allow the use of an arbitrary length prefix of this Id, Ids from primary
// and secondary are interleaved with this irregular pattern to give the
// best chance to find the secondary even with a 7 character prefix.
//
// Format is: PSPSPSPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPPSPPPP
//
// A complete interleaved Id hold 50 characters for the primary and 14 for the
// secondary, which give a key space of 36^50 for the primary (~6 * 10^77) and
// 36^14 for the secondary (~6 * 10^21). This asymmetry assumes a reasonable number
// of secondary within a primary Entity, while still allowing for a vast key space
// for the primary (that is, a globally merged database) with a low risk of collision.
//
// Here is the breakdown of several common prefix length:
//
// 5:    3P, 2S
// 7:    4P, 3S
// 10:   6P, 4S
// 16:  11P, 5S
func CombineIds(primary Id, secondary Id) CombinedId {
	var id strings.Builder

	for i := 0; i < idLength; i++ {
		switch {
		default:
			id.WriteByte(primary[0])
			primary = primary[1:]
		case i == 1, i == 3, i == 5, i == 9, i >= 10 && i%5 == 4:
			id.WriteByte(secondary[0])
			secondary = secondary[1:]
		}
	}

	return CombinedId(id.String())
}

// SeparateIds extract primary and secondary prefix from an arbitrary length prefix
// of an Id created with CombineIds.
func SeparateIds(prefix string) (primaryPrefix string, secondaryPrefix string) {
	var primary strings.Builder
	var secondary strings.Builder

	for i, r := range prefix {
		switch {
		default:
			primary.WriteRune(r)
		case i == 1, i == 3, i == 5, i == 9, i >= 10 && i%5 == 4:
			secondary.WriteRune(r)
		}
	}

	return primary.String(), secondary.String()
}