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}