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
IDisproviderName + ":" + sourceID. - Populate
SourceRef/SourceURLwhenever the tool has them — provenance is a core contract. - Map workflow statuses to a
StatusCategoryonly as reliably as the source allows; leaveCategoryunset 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.ErrUnsupportedOperationfor operations the tool can't back (and reflect that inCapabilities).
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.