Skip to content

Writing an Adapter

An adapter implements provider.Provider for one PM tool. If the tool's SDK is one of this ecosystem's own from-scratch *-go SDKs, the adapter lives as an omniroadmap/ subpackage inside that repo (the embedded pattern); an adapter wrapping a third-party official SDK would get its own omni-<provider> repo instead.

1. Implement the interface

package omniroadmap // inside yourtool-go/omniroadmap/

const providerName = "yourtool"

type Provider struct {
    client *yourtool.Client
}

var _ provider.Provider = (*Provider)(nil) // compile-time check

func NewProvider(client *yourtool.Client) *Provider {
    return &Provider{client: client}
}

func (p *Provider) Name() string { return providerName }
func (p *Provider) Close() error { return nil }

func (p *Provider) Capabilities() provider.Capabilities {
    return provider.Capabilities{
        Kinds:            []provider.ItemKind{provider.ItemKindFeature},
        SupportsReleases: true,
    }
}

Conversion guidelines:

  • Canonical ID is providerName + ":" + sourceID.
  • Populate SourceRef/SourceURL whenever the tool has them — provenance is a core contract.
  • Map workflow statuses to a StatusCategory only as reliably as the source allows; leave Category unset rather than guessing badly.
  • Everything without a canonical field goes into Metadata, namespaced: "yourtool.some_field".
  • Never populate MoSCoW/RICE — that's the downstream fieldmap layer's job.
  • Return omniroadmap.ErrUnsupportedOperation for operations the tool can't back (and reflect that in Capabilities).

2. Register

func init() {
    _ = omniroadmap.RegisterProvider(providerName, func(config any) (provider.Provider, error) {
        client, ok := config.(*yourtool.Client)
        if !ok {
            return nil, omniroadmap.NewAPIError(providerName, 0, "invalid_config",
                "omniroadmap/yourtool: expected *yourtool.Client config")
        }
        return NewProvider(client), nil
    })
}

Registration is name-unique — duplicate names error (there's exactly one adapter per provider; a cache-backed variant registers under its own name, like "aha-studio" alongside "aha").

3. Run the conformance suite

Every adapter's test file calls the shared providertest harness:

func TestConformance(t *testing.T) {
    p := NewProvider(testClient(t)) // httptest-backed or seeded-cache client

    providertest.RunAll(t, providertest.Config{
        Provider:        p,
        SkipIntegration: !hasLiveCredentials(),
        TestItemID:      "KNOWN-1",
        TestItemKind:    provider.ItemKindFeature,
    })
}

RunAll covers interface basics (Name/Capabilities well-formed), behavior (context cancellation), and — unless skipped — integration calls (ListItems/GetItem/ListReleases against real data). Add adapter-specific conversion unit tests alongside it; the existing adapters (aha-go/omniroadmap, productboard-go/omniroadmap, go-atlassian/omniroadmap, aha-studio/omniroadmap) are the reference implementations.