id_interleaved.go

  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}