bridge.go

  1// Package core contains the target-agnostic code to define and run a bridge
  2package core
  3
  4import (
  5	"fmt"
  6	"reflect"
  7	"regexp"
  8	"strings"
  9
 10	"github.com/MichaelMure/git-bug/cache"
 11	"github.com/MichaelMure/git-bug/repository"
 12	"github.com/pkg/errors"
 13)
 14
 15var ErrImportNorSupported = errors.New("import is not supported")
 16var ErrExportNorSupported = errors.New("export is not supported")
 17
 18var bridgeImpl map[string]reflect.Type
 19
 20// Bridge is a wrapper around a BridgeImpl that will bind low-level
 21// implementation with utility code to provide high-level functions.
 22type Bridge struct {
 23	Name string
 24	repo *cache.RepoCache
 25	impl BridgeImpl
 26	conf Configuration
 27}
 28
 29// Register will register a new BridgeImpl
 30func Register(impl BridgeImpl) {
 31	if bridgeImpl == nil {
 32		bridgeImpl = make(map[string]reflect.Type)
 33	}
 34	bridgeImpl[impl.Target()] = reflect.TypeOf(impl)
 35}
 36
 37// Targets return all known bridge implementation target
 38func Targets() []string {
 39	var result []string
 40
 41	for key := range bridgeImpl {
 42		result = append(result, key)
 43	}
 44
 45	return result
 46}
 47
 48// Instantiate a new Bridge for a repo, from the given target and name
 49func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
 50	implType, ok := bridgeImpl[target]
 51	if !ok {
 52		return nil, fmt.Errorf("unknown bridge target %v", target)
 53	}
 54
 55	impl := reflect.New(implType).Elem().Interface().(BridgeImpl)
 56
 57	bridge := &Bridge{
 58		Name: name,
 59		repo: repo,
 60		impl: impl,
 61	}
 62
 63	return bridge, nil
 64}
 65
 66// Instantiate a new bridge for a repo, from the combined target and name contained
 67// in the full name
 68func NewBridgeFullName(repo *cache.RepoCache, fullName string) (*Bridge, error) {
 69	target, name, err := splitFullName(fullName)
 70	if err != nil {
 71		return nil, err
 72	}
 73
 74	return NewBridge(repo, target, name)
 75}
 76
 77// Attempt to retrieve a default bridge for the given repo. If zero or multiple
 78// bridge exist, it fails.
 79func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
 80	bridges, err := ConfiguredBridges(repo)
 81	if err != nil {
 82		return nil, err
 83	}
 84
 85	if len(bridges) == 0 {
 86		return nil, fmt.Errorf("no configured bridge")
 87	}
 88
 89	if len(bridges) > 1 {
 90		return nil, fmt.Errorf("multiple bridge configured")
 91	}
 92
 93	target, name, err := splitFullName(bridges[0])
 94	if err != nil {
 95		return nil, err
 96	}
 97
 98	return NewBridge(repo, target, name)
 99}
100
101func splitFullName(fullName string) (string, string, error) {
102	split := strings.Split(fullName, ".")
103
104	if len(split) != 2 {
105		return "", "", fmt.Errorf("bad bridge fullname: %s", fullName)
106	}
107
108	return split[0], split[1], nil
109}
110
111// ConfiguredBridges return the list of bridge that are configured for the given
112// repo
113func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
114	configs, err := repo.ReadConfigs("git-bug.bridge.")
115	if err != nil {
116		return nil, errors.Wrap(err, "can't read configured bridges")
117	}
118
119	re, err := regexp.Compile(`git-bug.bridge.([^\.]+\.[^\.]+)`)
120	if err != nil {
121		panic(err)
122	}
123
124	set := make(map[string]interface{})
125
126	for key := range configs {
127		res := re.FindStringSubmatch(key)
128
129		if res == nil {
130			continue
131		}
132
133		set[res[1]] = nil
134	}
135
136	result := make([]string, len(set))
137
138	i := 0
139	for key := range set {
140		result[i] = key
141		i++
142	}
143
144	return result, nil
145}
146
147// Remove a configured bridge
148func RemoveBridge(repo repository.RepoCommon, fullName string) error {
149	re, err := regexp.Compile(`^[^\.]+\.[^\.]+$`)
150	if err != nil {
151		panic(err)
152	}
153
154	if !re.MatchString(fullName) {
155		return fmt.Errorf("bad bridge fullname: %s", fullName)
156	}
157
158	keyPrefix := fmt.Sprintf("git-bug.bridge.%s", fullName)
159	return repo.RmConfigs(keyPrefix)
160}
161
162// Configure run the target specific configuration process
163func (b *Bridge) Configure() error {
164	conf, err := b.impl.Configure(b.repo)
165	if err != nil {
166		return err
167	}
168
169	b.conf = conf
170
171	return b.storeConfig(conf)
172}
173
174func (b *Bridge) storeConfig(conf Configuration) error {
175	for key, val := range conf {
176		storeKey := fmt.Sprintf("git-bug.bridge.%s.%s.%s", b.impl.Target(), b.Name, key)
177
178		err := b.repo.StoreConfig(storeKey, val)
179		if err != nil {
180			return errors.Wrap(err, "error while storing bridge configuration")
181		}
182	}
183
184	return nil
185}
186
187func (b Bridge) getConfig() (Configuration, error) {
188	var err error
189	if b.conf == nil {
190		b.conf, err = b.loadConfig()
191		if err != nil {
192			return nil, err
193		}
194	}
195
196	return b.conf, nil
197}
198
199func (b Bridge) loadConfig() (Configuration, error) {
200	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.%s.", b.impl.Target(), b.Name)
201
202	pairs, err := b.repo.ReadConfigs(keyPrefix)
203	if err != nil {
204		return nil, errors.Wrap(err, "error while reading bridge configuration")
205	}
206
207	result := make(Configuration, len(pairs))
208	for key, value := range pairs {
209		key := strings.TrimPrefix(key, keyPrefix)
210		result[key] = value
211	}
212
213	err = b.impl.ValidateConfig(result)
214	if err != nil {
215		return nil, errors.Wrap(err, "invalid configuration")
216	}
217
218	return result, nil
219}
220
221func (b Bridge) ImportAll() error {
222	importer := b.impl.Importer()
223	if importer == nil {
224		return ErrImportNorSupported
225	}
226
227	conf, err := b.getConfig()
228	if err != nil {
229		return err
230	}
231
232	return b.impl.Importer().ImportAll(b.repo, conf)
233}
234
235func (b Bridge) Import(id string) error {
236	importer := b.impl.Importer()
237	if importer == nil {
238		return ErrImportNorSupported
239	}
240
241	conf, err := b.getConfig()
242	if err != nil {
243		return err
244	}
245
246	return b.impl.Importer().Import(b.repo, conf, id)
247}
248
249func (b Bridge) ExportAll() error {
250	exporter := b.impl.Exporter()
251	if exporter == nil {
252		return ErrExportNorSupported
253	}
254
255	conf, err := b.getConfig()
256	if err != nil {
257		return err
258	}
259
260	return b.impl.Exporter().ExportAll(b.repo, conf)
261}
262
263func (b Bridge) Export(id string) error {
264	exporter := b.impl.Exporter()
265	if exporter == nil {
266		return ErrExportNorSupported
267	}
268
269	conf, err := b.getConfig()
270	if err != nil {
271		return err
272	}
273
274	return b.impl.Exporter().Export(b.repo, conf, id)
275}