Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
976 changes: 670 additions & 306 deletions api/openapi.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ func main() {
apiRepo := repositories.NewApiRepository(db)
APIsAPIService := services.NewAPIsAPIService(apiRepo)
APIsAPIController := handler.NewAPIsAPIController(APIsAPIService)

adoptionRepo := repositories.NewAdoptionRepository(db)
adoptionService := services.NewAdoptionService(adoptionRepo)
statsController := handler.NewStatisticsController(adoptionService)
if err := APIsAPIService.PublishAllApisToTypesense(context.Background()); err != nil {
log.Fatalf("[typesense-sync] bulk publish failed: %v", err)
}
Expand All @@ -145,7 +149,7 @@ func main() {
}()

// Start server
router := api.NewRouter(version, APIsAPIController)
router := api.NewRouter(version, APIsAPIController, statsController)

log.Println("Server is running on port 1337")
log.Fatal(http.ListenAndServe(":1337", router))
Expand Down
5 changes: 0 additions & 5 deletions pkg/api_client/handler/api_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ func (c *APIsAPIController) RetrieveApi(ctx *gin.Context, params *models.ApiPara
return api, nil
}

// ListLintResults handles GET /lint-results
func (c *APIsAPIController) ListLintResults(ctx *gin.Context) ([]models.LintResult, error) {
return c.Service.ListLintResults(ctx.Request.Context())
}

// CreateApiFromOas handles POST /apis
func (c *APIsAPIController) CreateApiFromOas(ctx *gin.Context, body *models.ApiPost) (*models.ApiSummary, error) {
created, err := c.Service.CreateApiFromOas(*body)
Expand Down
7 changes: 0 additions & 7 deletions pkg/api_client/handler/api_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ type stubRepo struct {
searchFunc func(ctx context.Context, page, perPage int, organisation *string, query string) ([]models.Api, models.Pagination, error)
retrFunc func(ctx context.Context, id string) (*models.Api, error)
lintResFunc func(ctx context.Context, apiID string) ([]models.LintResult, error)
listLint func(ctx context.Context) ([]models.LintResult, error)
findOasFunc func(ctx context.Context, oasUrl string) (*models.Api, error)
getOrgs func(ctx context.Context) ([]models.Organisation, int, error)
findOrg func(ctx context.Context, uri string) (*models.Organisation, error)
Expand All @@ -46,12 +45,6 @@ func (s *stubRepo) GetApiByID(ctx context.Context, id string) (*models.Api, erro
func (s *stubRepo) GetLintResults(ctx context.Context, apiID string) ([]models.LintResult, error) {
return s.lintResFunc(ctx, apiID)
}
func (s *stubRepo) ListLintResults(ctx context.Context) ([]models.LintResult, error) {
if s.listLint != nil {
return s.listLint(ctx)
}
return []models.LintResult{}, nil
}
func (s *stubRepo) FindByOasUrl(ctx context.Context, oasUrl string) (*models.Api, error) {
return s.findOasFunc(ctx, oasUrl)
}
Expand Down
37 changes: 37 additions & 0 deletions pkg/api_client/handler/statistics_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package handler

import (
"github.com/developer-overheid-nl/don-api-register/pkg/api_client/helpers/util"
"github.com/developer-overheid-nl/don-api-register/pkg/api_client/models"
"github.com/developer-overheid-nl/don-api-register/pkg/api_client/services"
"github.com/gin-gonic/gin"
)

type StatisticsController struct {
Service *services.AdoptionService
}

func NewStatisticsController(s *services.AdoptionService) *StatisticsController {
return &StatisticsController{Service: s}
}

func (c *StatisticsController) GetSummary(ctx *gin.Context, p *models.AdoptionBaseParams) (*models.AdoptionSummary, error) {
return c.Service.GetSummary(ctx.Request.Context(), p)
}

func (c *StatisticsController) GetRules(ctx *gin.Context, p *models.AdoptionRulesParams) (*models.AdoptionRules, error) {
return c.Service.GetRules(ctx.Request.Context(), p)
}

func (c *StatisticsController) GetTimeline(ctx *gin.Context, p *models.AdoptionTimelineParams) (*models.AdoptionTimeline, error) {
return c.Service.GetTimeline(ctx.Request.Context(), p)
}

func (c *StatisticsController) GetApis(ctx *gin.Context, p *models.AdoptionApisParams) (*models.AdoptionApis, error) {
result, pagination, err := c.Service.GetApis(ctx.Request.Context(), p)
if err != nil {
return nil, err
}
util.SetPaginationHeaders(ctx.Request, ctx.Header, *pagination)
return result, nil
}
143 changes: 143 additions & 0 deletions pkg/api_client/handler/statistics_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package handler

import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/developer-overheid-nl/don-api-register/pkg/api_client/models"
"github.com/developer-overheid-nl/don-api-register/pkg/api_client/repositories"
"github.com/developer-overheid-nl/don-api-register/pkg/api_client/services"
"github.com/gin-gonic/gin"
"github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type statisticsAdoptionRepoStub struct {
getSummaryFunc func(ctx context.Context, params repositories.AdoptionQueryParams) (repositories.SummaryResult, int, error)
getApisFunc func(ctx context.Context, params repositories.ApisQueryParams) ([]repositories.ApiRow, int, error)
}

func (s *statisticsAdoptionRepoStub) GetSummary(ctx context.Context, params repositories.AdoptionQueryParams) (repositories.SummaryResult, int, error) {
if s.getSummaryFunc != nil {
return s.getSummaryFunc(ctx, params)
}
return repositories.SummaryResult{}, 0, nil
}

func (s *statisticsAdoptionRepoStub) GetRules(ctx context.Context, params repositories.AdoptionQueryParams) ([]repositories.RuleRow, int, error) {
return nil, 0, nil
}

func (s *statisticsAdoptionRepoStub) GetTimeline(ctx context.Context, params repositories.TimelineQueryParams) ([]repositories.TimelineRow, error) {
return nil, nil
}

func (s *statisticsAdoptionRepoStub) GetApis(ctx context.Context, params repositories.ApisQueryParams) ([]repositories.ApiRow, int, error) {
if s.getApisFunc != nil {
return s.getApisFunc(ctx, params)
}
return nil, 0, nil
}

func TestStatisticsControllerGetSummary_Success(t *testing.T) {
gin.SetMode(gin.TestMode)
repo := &statisticsAdoptionRepoStub{
getSummaryFunc: func(ctx context.Context, params repositories.AdoptionQueryParams) (repositories.SummaryResult, int, error) {
return repositories.SummaryResult{
TotalApis: 5,
CompliantApis: 4,
AdoptionRate: 80,
}, 11, nil
},
}
ctrl := NewStatisticsController(services.NewAdoptionService(repo))

w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = httptest.NewRequest(http.MethodGet, "/v1/statistics/summary", nil)

out, err := ctrl.GetSummary(ctx, &models.AdoptionBaseParams{
AdrVersion: "ADR-2.0",
StartDate: "2026-01-01",
EndDate: "2026-01-31",
})
require.NoError(t, err)
require.NotNil(t, out)
assert.Equal(t, 5, out.TotalApis)
assert.Equal(t, 4, out.CompliantApis)
assert.Equal(t, 80.0, out.OverallAdoptionRate)
assert.Equal(t, 11, out.TotalLintRuns)
}

func TestStatisticsControllerGetApis_SetsPaginationHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
repo := &statisticsAdoptionRepoStub{
getApisFunc: func(ctx context.Context, params repositories.ApisQueryParams) ([]repositories.ApiRow, int, error) {
return []repositories.ApiRow{
{
ApiId: "api-1",
ApiTitle: "API 1",
Organisation: "Org",
IsCompliant: true,
TotalFailures: 0,
TotalWarnings: 1,
ViolatedRules: pq.StringArray{},
LastLintDate: time.Date(2026, 1, 10, 9, 0, 0, 0, time.UTC),
},
}, 3, nil
},
}
ctrl := NewStatisticsController(services.NewAdoptionService(repo))

w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
req := httptest.NewRequest(http.MethodGet, "http://api.example.test/v1/statistics/apis?page=2&perPage=1", nil)
req.Host = "api.example.test"
ctx.Request = req

out, err := ctrl.GetApis(ctx, &models.AdoptionApisParams{
AdoptionBaseParams: models.AdoptionBaseParams{AdrVersion: "ADR-2.0", StartDate: "2026-01-01", EndDate: "2026-01-31"},
Page: 2,
PerPage: 1,
})
require.NoError(t, err)
require.NotNil(t, out)
require.Len(t, out.Apis, 1)

assert.Equal(t, "3", w.Header().Get("Total-Count"))
assert.Equal(t, "3", w.Header().Get("Total-Pages"))
assert.Equal(t, "1", w.Header().Get("Per-Page"))
assert.Equal(t, "2", w.Header().Get("Current-Page"))
assert.Contains(t, w.Header().Get("Link"), `rel="prev"`)
assert.Contains(t, w.Header().Get("Link"), `rel="next"`)
assert.Contains(t, w.Header().Get("Link"), `page=2`)
}

func TestStatisticsControllerGetApis_PropagatesErrorWithoutHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
repo := &statisticsAdoptionRepoStub{
getApisFunc: func(ctx context.Context, params repositories.ApisQueryParams) ([]repositories.ApiRow, int, error) {
return nil, 0, errors.New("query failed")
},
}
ctrl := NewStatisticsController(services.NewAdoptionService(repo))

w := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(w)
ctx.Request = httptest.NewRequest(http.MethodGet, "/v1/statistics/apis", nil)

out, err := ctrl.GetApis(ctx, &models.AdoptionApisParams{
AdoptionBaseParams: models.AdoptionBaseParams{AdrVersion: "ADR-2.0", StartDate: "2026-01-01", EndDate: "2026-01-31"},
})

require.Error(t, err)
assert.Nil(t, out)
assert.Contains(t, err.Error(), "query failed")
assert.Empty(t, w.Header().Get("Total-Count"))
assert.Empty(t, w.Header().Get("Link"))
}
5 changes: 4 additions & 1 deletion pkg/api_client/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,10 @@ func newIntegrationEnv(t *testing.T) *integrationEnv {
repo := repositories.NewApiRepository(db)
svc := services.NewAPIsAPIService(repo)
controller := handler.NewAPIsAPIController(svc)
router := api_client.NewRouter("test-version", controller)
adoptionRepo := repositories.NewAdoptionRepository(db)
adoptionService := services.NewAdoptionService(adoptionRepo)
statsController := handler.NewStatisticsController(adoptionService)
router := api_client.NewRouter("test-version", controller, statsController)

server := httptest.NewServer(router)
t.Cleanup(func() { server.Close() })
Expand Down
106 changes: 106 additions & 0 deletions pkg/api_client/models/adoption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package models

import "time"

type Period struct {
Start string `json:"start"`
End string `json:"end"`
}

// Summary endpoint

type AdoptionSummary struct {
AdrVersion string `json:"adrVersion"`
Period Period `json:"period"`
TotalApis int `json:"totalApis"`
CompliantApis int `json:"compliantApis"`
OverallAdoptionRate float64 `json:"overallAdoptionRate"`
TotalLintRuns int `json:"totalLintRuns"`
}

// Rules endpoint

type AdoptionRules struct {
AdrVersion string `json:"adrVersion"`
Period Period `json:"period"`
TotalApis int `json:"totalApis"`
Rules []RuleAdoption `json:"rules"`
}

type RuleAdoption struct {
Code string `json:"code"`
Severity string `json:"severity"`
ViolatingApis int `json:"violatingApis"`
CompliantApis int `json:"compliantApis"`
AdoptionRate float64 `json:"adoptionRate"`
}

// Timeline endpoint

type AdoptionTimeline struct {
AdrVersion string `json:"adrVersion"`
Granularity string `json:"granularity"`
Series []TimelineSeries `json:"series"`
}

type TimelineSeries struct {
Type string `json:"type"`
RuleCode string `json:"ruleCode,omitempty"`
DataPoints []TimelinePoint `json:"dataPoints"`
}

type TimelinePoint struct {
Period string `json:"period"`
TotalApis int `json:"totalApis"`
CompliantApis int `json:"compliantApis"`
AdoptionRate float64 `json:"adoptionRate"`
}

// APIs endpoint

type AdoptionApis struct {
AdrVersion string `json:"adrVersion"`
Period Period `json:"period"`
Apis []ApiAdoption `json:"apis"`
}

type ApiAdoption struct {
ApiId string `json:"apiId"`
ApiTitle string `json:"apiTitle"`
Organisation string `json:"organisation"`
IsCompliant bool `json:"isCompliant"`
TotalViolations int `json:"totalViolations"`
TotalWarnings int `json:"totalWarnings"`
ViolatedRules []string `json:"violatedRules"`
LastLintDate time.Time `json:"lastLintDate"`
}

// Query parameter structs

type AdoptionBaseParams struct {
AdrVersion string `query:"adrVersion" binding:"required"`
StartDate string `query:"startDate" binding:"required"`
EndDate string `query:"endDate" binding:"required"`
ApiIds *string `query:"apiIds"`
Organisation *string `query:"organisation"`
}

type AdoptionRulesParams struct {
AdoptionBaseParams
RuleCodes *string `query:"ruleCodes"`
Severity *string `query:"severity"`
}

type AdoptionTimelineParams struct {
AdoptionBaseParams
Granularity string `query:"granularity"`
RuleCodes *string `query:"ruleCodes"`
}

type AdoptionApisParams struct {
AdoptionBaseParams
Compliant *bool `query:"compliant"`
RuleCodes *string `query:"ruleCodes"`
Page int `query:"page"`
PerPage int `query:"perPage"`
}
Loading
Loading