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 ErrImportNotSupported = errors.New("import is not supported")
 16var ErrExportNotSupported = errors.New("export is not supported")
 17
 18const bridgeConfigKeyPrefix = "git-bug.bridge"
 19
 20var bridgeImpl map[string]reflect.Type
 21
 22// Bridge is a wrapper around a BridgeImpl that will bind low-level
 23// implementation with utility code to provide high-level functions.
 24type Bridge struct {
 25	Name     string
 26	repo     *cache.RepoCache
 27	impl     BridgeImpl
 28	importer Importer
 29	exporter Exporter
 30	conf     Configuration
 31	initDone bool
 32}
 33
 34// Register will register a new BridgeImpl
 35func Register(impl BridgeImpl) {
 36	if bridgeImpl == nil {
 37		bridgeImpl = make(map[string]reflect.Type)
 38	}
 39	bridgeImpl[impl.Target()] = reflect.TypeOf(impl)
 40}
 41
 42// Targets return all known bridge implementation target
 43func Targets() []string {
 44	var result []string
 45
 46	for key := range bridgeImpl {
 47		result = append(result, key)
 48	}
 49
 50	return result
 51}
 52
 53// Instantiate a new Bridge for a repo, from the given target and name
 54func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
 55	implType, ok := bridgeImpl[target]
 56	if !ok {
 57		return nil, fmt.Errorf("unknown bridge target %v", target)
 58	}
 59
 60	impl := reflect.New(implType).Elem().Interface().(BridgeImpl)
 61
 62	bridge := &Bridge{
 63		Name: name,
 64		repo: repo,
 65		impl: impl,
 66	}
 67
 68	return bridge, nil
 69}
 70
 71// Instantiate a new bridge for a repo, from the combined target and name contained
 72// in the full name
 73func NewBridgeFromFullName(repo *cache.RepoCache, fullName string) (*Bridge, error) {
 74	target, name, err := splitFullName(fullName)
 75	if err != nil {
 76		return nil, err
 77	}
 78
 79	return NewBridge(repo, target, name)
 80}
 81
 82// Attempt to retrieve a default bridge for the given repo. If zero or multiple
 83// bridge exist, it fails.
 84func DefaultBridge(repo *cache.RepoCache) (*Bridge, error) {
 85	bridges, err := ConfiguredBridges(repo)
 86	if err != nil {
 87		return nil, err
 88	}
 89
 90	if len(bridges) == 0 {
 91		return nil, fmt.Errorf("no configured bridge")
 92	}
 93
 94	if len(bridges) > 1 {
 95		return nil, fmt.Errorf("multiple bridge are configured, you need to select one explicitely")
 96	}
 97
 98	target, name, err := splitFullName(bridges[0])
 99	if err != nil {
100		return nil, err
101	}
102
103	return NewBridge(repo, target, name)
104}
105
106func splitFullName(fullName string) (string, string, error) {
107	split := strings.Split(fullName, ".")
108
109	if len(split) != 2 {
110		return "", "", fmt.Errorf("bad bridge fullname: %s", fullName)
111	}
112
113	return split[0], split[1], nil
114}
115
116// ConfiguredBridges return the list of bridge that are configured for the given
117// repo
118func ConfiguredBridges(repo repository.RepoCommon) ([]string, error) {
119	configs, err := repo.ReadConfigs(bridgeConfigKeyPrefix + ".")
120	if err != nil {
121		return nil, errors.Wrap(err, "can't read configured bridges")
122	}
123
124	re, err := regexp.Compile(bridgeConfigKeyPrefix + `.([^.]+\.[^.]+)`)
125	if err != nil {
126		panic(err)
127	}
128
129	set := make(map[string]interface{})
130
131	for key := range configs {
132		res := re.FindStringSubmatch(key)
133
134		if res == nil {
135			continue
136		}
137
138		set[res[1]] = nil
139	}
140
141	result := make([]string, len(set))
142
143	i := 0
144	for key := range set {
145		result[i] = key
146		i++
147	}
148
149	return result, nil
150}
151
152// Remove a configured bridge
153func RemoveBridge(repo repository.RepoCommon, fullName string) error {
154	re, err := regexp.Compile(`^[^.]+\.[^.]+$`)
155	if err != nil {
156		panic(err)
157	}
158
159	if !re.MatchString(fullName) {
160		return fmt.Errorf("bad bridge fullname: %s", fullName)
161	}
162
163	keyPrefix := fmt.Sprintf("git-bug.bridge.%s", fullName)
164	return repo.RmConfigs(keyPrefix)
165}
166
167// Configure run the target specific configuration process
168func (b *Bridge) Configure() error {
169	conf, err := b.impl.Configure(b.repo)
170	if err != nil {
171		return err
172	}
173
174	b.conf = conf
175
176	return b.storeConfig(conf)
177}
178
179func (b *Bridge) storeConfig(conf Configuration) error {
180	for key, val := range conf {
181		storeKey := fmt.Sprintf("git-bug.bridge.%s.%s.%s", b.impl.Target(), b.Name, key)
182
183		err := b.repo.StoreConfig(storeKey, val)
184		if err != nil {
185			return errors.Wrap(err, "error while storing bridge configuration")
186		}
187	}
188
189	return nil
190}
191
192func (b *Bridge) ensureConfig() error {
193	if b.conf == nil {
194		conf, err := b.loadConfig()
195		if err != nil {
196			return err
197		}
198		b.conf = conf
199	}
200
201	return nil
202}
203
204func (b *Bridge) loadConfig() (Configuration, error) {
205	keyPrefix := fmt.Sprintf("git-bug.bridge.%s.%s.", b.impl.Target(), b.Name)
206
207	pairs, err := b.repo.ReadConfigs(keyPrefix)
208	if err != nil {
209		return nil, errors.Wrap(err, "error while reading bridge configuration")
210	}
211
212	result := make(Configuration, len(pairs))
213	for key, value := range pairs {
214		key := strings.TrimPrefix(key, keyPrefix)
215		result[key] = value
216	}
217
218	err = b.impl.ValidateConfig(result)
219	if err != nil {
220		return nil, errors.Wrap(err, "invalid configuration")
221	}
222
223	return result, nil
224}
225
226func (b *Bridge) getImporter() Importer {
227	if b.importer == nil {
228		b.importer = b.impl.NewImporter()
229	}
230
231	return b.importer
232}
233
234func (b *Bridge) getExporter() Exporter {
235	if b.exporter == nil {
236		b.exporter = b.impl.NewExporter()
237	}
238
239	return b.exporter
240}
241
242func (b *Bridge) ensureInit() error {
243	if b.initDone {
244		return nil
245	}
246
247	importer := b.getImporter()
248	if importer != nil {
249		err := importer.Init(b.conf)
250		if err != nil {
251			return err
252		}
253	}
254
255	exporter := b.getExporter()
256	if exporter != nil {
257		err := exporter.Init(b.conf)
258		if err != nil {
259			return err
260		}
261	}
262
263	b.initDone = true
264
265	return nil
266}
267
268func (b *Bridge) ImportAll() error {
269	importer := b.getImporter()
270	if importer == nil {
271		return ErrImportNotSupported
272	}
273
274	err := b.ensureConfig()
275	if err != nil {
276		return err
277	}
278
279	err = b.ensureInit()
280	if err != nil {
281		return err
282	}
283
284	return importer.ImportAll(b.repo)
285}
286
287func (b *Bridge) Import(id string) error {
288	importer := b.getImporter()
289	if importer == nil {
290		return ErrImportNotSupported
291	}
292
293	err := b.ensureConfig()
294	if err != nil {
295		return err
296	}
297
298	err = b.ensureInit()
299	if err != nil {
300		return err
301	}
302
303	return importer.Import(b.repo, id)
304}
305
306func (b *Bridge) ExportAll() error {
307	exporter := b.getExporter()
308	if exporter == nil {
309		return ErrExportNotSupported
310	}
311
312	err := b.ensureConfig()
313	if err != nil {
314		return err
315	}
316
317	err = b.ensureInit()
318	if err != nil {
319		return err
320	}
321
322	return exporter.ExportAll(b.repo)
323}
324
325func (b *Bridge) Export(id string) error {
326	exporter := b.getExporter()
327	if exporter == nil {
328		return ErrExportNotSupported
329	}
330
331	err := b.ensureConfig()
332	if err != nil {
333		return err
334	}
335
336	err = b.ensureInit()
337	if err != nil {
338		return err
339	}
340
341	return exporter.Export(b.repo, id)
342}