Skip to content
Open
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
63 changes: 63 additions & 0 deletions changelog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package requestmigrations

import (
"sort"
)

// ChangelogDescriber is an interface that migrations must implement
// to be included in the changelog
type ChangelogDescriber interface {
// ChangeDescription returns a human-readable description of what the migration does
ChangeDescription() string
}

// ChangelogEntry represents changes in a specific API version
type ChangelogEntry struct {
Version string `json:"version"`
Changes []string `json:"changes"`
}

// GenerateChangelog generates a list of changes between versions
func (rm *RequestMigration) GenerateChangelog() ([]*ChangelogEntry, error) {
rm.mu.Lock()
defer rm.mu.Unlock()

// Sort versions to ensure chronological order
versions := make([]*Version, len(rm.versions))
copy(versions, rm.versions)

switch rm.opts.VersionFormat {
case SemverFormat:
sort.Slice(versions, semVerSorter(versions))
case DateFormat:
sort.Slice(versions, dateVersionSorter(versions))
default:
return nil, ErrInvalidVersionFormat
}

// Create changelog entries for each version
var changelog []*ChangelogEntry
for _, version := range versions {
migrations, ok := rm.migrations[version.String()]
if !ok {
continue
}

var changes []string
for _, migration := range migrations {
if describer, ok := migration.(ChangelogDescriber); ok {
changes = append(changes, describer.ChangeDescription())
}
}

if len(changes) > 0 {
entry := &ChangelogEntry{
Version: version.String(),
Changes: changes,
}
changelog = append(changelog, entry)
}
}

return changelog, nil
}
81 changes: 81 additions & 0 deletions changelog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package requestmigrations

import (
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Example migrations that implement ChangelogDescriber
type testMigrationOne struct{}

func (t *testMigrationOne) Migrate(data []byte, header http.Header) ([]byte, http.Header, error) {
return data, header, nil
}

func (t *testMigrationOne) ChangeDescription() string {
return "Split the name field into firstName and lastName"
}

type testMigrationTwo struct{}

func (t *testMigrationTwo) Migrate(data []byte, header http.Header) ([]byte, http.Header, error) {
return data, header, nil
}

func (t *testMigrationTwo) ChangeDescription() string {
return "Added email verification field"
}

// Migration that doesn't implement ChangelogDescriber
type testMigrationWithoutDescription struct{}

func (t *testMigrationWithoutDescription) Migrate(data []byte, header http.Header) ([]byte, http.Header, error) {
return data, header, nil
}

func Test_GenerateChangelog(t *testing.T) {
// Create a new RequestMigration instance
opts := &RequestMigrationOptions{
VersionHeader: "X-Test-Version",
CurrentVersion: "2023-03-01",
VersionFormat: DateFormat,
}

rm, err := NewRequestMigration(opts)
require.NoError(t, err)

// Register test migrations across different versions
migrations := &MigrationStore{
"2023-03-01": Migrations{
&testMigrationOne{},
&testMigrationWithoutDescription{}, // Should be ignored in changelog
},
"2023-04-01": Migrations{
&testMigrationTwo{},
},
}

err = rm.RegisterMigrations(*migrations)
require.NoError(t, err)

// Generate changelog
changelog, err := rm.GenerateChangelog()
require.NoError(t, err)
require.NotNil(t, changelog)

// We should have entries for both versions
assert.Equal(t, 2, len(changelog))

// Verify first version entry
assert.Equal(t, "2023-03-01", changelog[0].Version)
assert.Equal(t, 1, len(changelog[0].Changes))
assert.Equal(t, "Split the name field into firstName and lastName", changelog[0].Changes[0])

// Verify second version entry
assert.Equal(t, "2023-04-01", changelog[1].Version)
assert.Equal(t, 1, len(changelog[1].Changes))
assert.Equal(t, "Added email verification field", changelog[1].Changes[0])
}
5 changes: 5 additions & 0 deletions example/basic/call_api.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ helpFunc() {
echo -e "\t-r Specify request type, see options below:"
echo -e "\t - lu: list users without versioning"
echo -e "\t - lvu: list users with versioning"
echo -e "\t - cl: get changelog"
exit 1
}

Expand All @@ -32,6 +33,10 @@ while getopts ":n:r:" opt; do
-H "X-Example-Version: 2023-04-01" | jq
done

elif [[ "$req" == "cl" ]]; then
curl -s localhost:9000/changelog \
-H "Content-Type: application/json" | jq

else
helpFunc
fi
Expand Down
13 changes: 13 additions & 0 deletions example/basic/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"basicexample/helper"
v20230401 "basicexample/v20230401"
v20230501 "basicexample/v20230501"
"encoding/json"
"log"
"math/rand"
"net/http"
Expand Down Expand Up @@ -59,6 +60,7 @@ func buildMux(api *API) http.Handler {

m.HandleFunc("/users", api.ListUser).Methods("GET")
m.HandleFunc("/users/{id}", api.GetUser).Methods("GET")
m.HandleFunc("/changelog", api.handleChangelog).Methods(http.MethodGet)

reg := prometheus.NewRegistry()
api.rm.RegisterMetrics(reg)
Expand Down Expand Up @@ -121,3 +123,14 @@ func (a *API) GetUser(w http.ResponseWriter, r *http.Request) {

w.Write(res)
}

func (a *API) handleChangelog(w http.ResponseWriter, r *http.Request) {
changelog, err := a.rm.GenerateChangelog()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(changelog)
}
6 changes: 5 additions & 1 deletion example/basic/v20230401/requestmigrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"time"
)

// Migrations
// ListUserResponseMigration handles the response migration for the list users endpoint
type ListUserResponseMigration struct{}

func (c *ListUserResponseMigration) Migrate(
Expand Down Expand Up @@ -55,6 +55,10 @@ func (c *ListUserResponseMigration) Migrate(
return body, h, nil
}

func (c *ListUserResponseMigration) ChangeDescription() string {
return "Combined firstName and lastName fields into a single fullName field in user response"
}

type oldUser20230501 struct {
UID string `json:"uid"`
Email string `json:"email"`
Expand Down
26 changes: 15 additions & 11 deletions example/basic/v20230501/requestmigrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,7 @@ type profile struct {
TwitterURL string `json:"twitter_url"`
}

// Migrations
type oldUser20230501 struct {
UID string `json:"uid"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Profile string `json:"profile"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// ListUserResponseMigration handles the response migration for the list users endpoint
type ListUserResponseMigration struct{}

func (e *ListUserResponseMigration) Migrate(
Expand Down Expand Up @@ -70,3 +60,17 @@ func (e *ListUserResponseMigration) Migrate(

return body, h, nil
}

func (e *ListUserResponseMigration) ChangeDescription() string {
return "Expanded profile field to include GitHub and Twitter URLs"
}

type oldUser20230501 struct {
UID string `json:"uid"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Profile string `json:"profile"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
Loading