[bugs-central] Interface used by the different issue frameworks

Bug: skia:10783
Change-Id: I699d7008a60fcbacb500b8dd1bed82aed2328160
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/328986
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
diff --git a/bugs-central/go/bugs/bugs.go b/bugs-central/go/bugs/bugs.go
new file mode 100644
index 0000000..b2456e7
--- /dev/null
+++ b/bugs-central/go/bugs/bugs.go
@@ -0,0 +1,23 @@
+package bugs
+
+// Defines a generic interface used by the different issue frameworks.
+
+import (
+	"context"
+
+	"go.skia.org/infra/bugs-central/go/db"
+	"go.skia.org/infra/bugs-central/go/types"
+)
+
+type BugFramework interface {
+
+	// Search calls the bug framework and returns standardized issues.
+	Search(ctx context.Context) ([]*types.Issue, *types.IssueCountsData, error)
+
+	// SearchClientAndPersist queries issues and puts results into the DB and into the
+	// OpenIssues in-memory object.
+	SearchClientAndPersist(ctx context.Context, dbClient *db.FirestoreDB, runId string) error
+
+	// GetIssueLink returns a link to the specified issue ID.
+	GetIssueLink(project, id string) string
+}
diff --git a/bugs-central/go/bugs/open_issues.go b/bugs-central/go/bugs/open_issues.go
new file mode 100644
index 0000000..df5d6f3
--- /dev/null
+++ b/bugs-central/go/bugs/open_issues.go
@@ -0,0 +1,66 @@
+package bugs
+
+// Defines an interface used by the different issue frameworks to add open issues to an in-memory object.
+
+import (
+	"sync"
+
+	"go.skia.org/infra/bugs-central/go/types"
+	"go.skia.org/infra/go/sklog"
+)
+
+type OpenIssues struct {
+	// Mapping of client to source to query to issues. Mirrors the DB structure but stores real issues instead of counts.
+	// This will be used mainly by endpoints and emails that require the actual issue IDs.
+	openIssues map[types.RecognizedClient]map[types.IssueSource]map[string][]*types.Issue
+	// Mutex to access the above object.
+	mtx sync.RWMutex
+}
+
+func InitOpenIssues() *OpenIssues {
+	return &OpenIssues{
+		openIssues: map[types.RecognizedClient]map[types.IssueSource]map[string][]*types.Issue{},
+	}
+}
+
+// PrettyPrintOpenIssues pretty prints the open issues in-memory object.
+func (o *OpenIssues) PrettyPrintOpenIssues() {
+	o.mtx.RLock()
+	defer o.mtx.RUnlock()
+
+	sklog.Info("---- open issues ----")
+	for c, sourceToQueries := range o.openIssues {
+		sklog.Infof("%s", c)
+		for s, queriesToIssues := range sourceToQueries {
+			sklog.Infof("    %s", s)
+			for q, issues := range queriesToIssues {
+				sklog.Infof("        \"%s\"", q)
+				sklog.Infof("            Open Issues: %d", len(issues))
+			}
+		}
+	}
+	sklog.Info("---------------------")
+}
+
+// PutOpenIssues adds/removes data from the open issues in-memory object.
+func (o *OpenIssues) PutOpenIssues(client types.RecognizedClient, source types.IssueSource, query string, issues []*types.Issue) {
+	o.mtx.Lock()
+	defer o.mtx.Unlock()
+
+	if sourceToQueries, ok := o.openIssues[client]; ok {
+		if queryToIssues, ok := sourceToQueries[source]; ok {
+			// Replace existing slice with new issues.
+			queryToIssues[query] = issues
+		} else {
+			sourceToQueries[source] = map[string][]*types.Issue{
+				query: issues,
+			}
+		}
+	} else {
+		o.openIssues[client] = map[types.IssueSource]map[string][]*types.Issue{
+			source: {
+				query: issues,
+			},
+		}
+	}
+}
diff --git a/bugs-central/go/bugs/open_issues_test.go b/bugs-central/go/bugs/open_issues_test.go
new file mode 100644
index 0000000..09358e5
--- /dev/null
+++ b/bugs-central/go/bugs/open_issues_test.go
@@ -0,0 +1,59 @@
+package bugs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"go.skia.org/infra/bugs-central/go/types"
+	"go.skia.org/infra/go/testutils/unittest"
+)
+
+func TestPutOpenIssues(t *testing.T) {
+	unittest.SmallTest(t)
+
+	o := InitOpenIssues()
+	client1 := types.RecognizedClient("client1")
+	client2 := types.RecognizedClient("client2")
+	source1 := types.IssueSource("source1")
+	source2 := types.IssueSource("source2")
+	query1 := "query1"
+	query2 := "query2"
+	issues1 := []*types.Issue{
+		{Id: "id11"},
+		{Id: "id12"},
+	}
+	issues2 := []*types.Issue{
+		{Id: "id21"},
+		{Id: "id22"},
+		{Id: "id32"},
+	}
+	issues3 := []*types.Issue{
+		{Id: "id31"},
+		{Id: "id32"},
+		{Id: "id32"},
+	}
+
+	// Add 1 client+source+query entry with 2 issues.
+	o.PutOpenIssues(client1, source1, query1, issues1)
+	require.Equal(t, 1, len(o.openIssues))
+	require.Equal(t, 1, len(o.openIssues[client1]))
+	require.Equal(t, 1, len(o.openIssues[client1][source1]))
+	require.Equal(t, 2, len(o.openIssues[client1][source1][query1]))
+	require.Equal(t, "id11", o.openIssues[client1][source1][query1][0].Id)
+	require.Equal(t, "id12", o.openIssues[client1][source1][query1][1].Id)
+
+	// Add another client with 2 sources.
+	o.PutOpenIssues(client2, source1, query2, issues2)
+	o.PutOpenIssues(client2, source2, query2, issues3)
+	require.Equal(t, 2, len(o.openIssues))
+	require.Equal(t, 2, len(o.openIssues[client2]))
+	require.Equal(t, "id21", o.openIssues[client2][source1][query2][0].Id)
+	require.Equal(t, "id31", o.openIssues[client2][source2][query2][0].Id)
+
+	// Replace an existing entries with new set of issues.
+	o.PutOpenIssues(client2, source1, query2, issues1)
+	require.Equal(t, 2, len(o.openIssues[client2][source1][query2]))
+	require.Equal(t, "id11", o.openIssues[client2][source1][query2][0].Id)
+	require.Equal(t, "id12", o.openIssues[client2][source1][query2][1].Id)
+}