From adf51e4462b948672a684f98fc15b7566c12bcc8 Mon Sep 17 00:00:00 2001 From: Sebastian Lewis Date: Thu, 12 Dec 2024 23:25:21 -0800 Subject: [PATCH 1/2] Add changelog generation functionality - Add ChangelogDescriber interface for migrations - Add ChangelogEntry struct to represent version changes - Implement GenerateChangelog method to create changelog entries - Sort versions chronologically based on version format --- changelog.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 changelog.go diff --git a/changelog.go b/changelog.go new file mode 100644 index 0000000..e8ddaa7 --- /dev/null +++ b/changelog.go @@ -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 +} \ No newline at end of file From bba0221d271090420f8821a3826e5641b8c70105 Mon Sep 17 00:00:00 2001 From: Sebastian Lewis Date: Thu, 12 Dec 2024 23:31:50 -0800 Subject: [PATCH 2/2] Add changelog generation functionality --- changelog_test.go | 81 ++++++++++++++++++++ example/basic/call_api.sh | 5 ++ example/basic/main.go | 13 ++++ example/basic/v20230401/requestmigrations.go | 6 +- example/basic/v20230501/requestmigrations.go | 26 ++++--- 5 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 changelog_test.go diff --git a/changelog_test.go b/changelog_test.go new file mode 100644 index 0000000..928423d --- /dev/null +++ b/changelog_test.go @@ -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]) +} \ No newline at end of file diff --git a/example/basic/call_api.sh b/example/basic/call_api.sh index 476ddcb..1ed4eb9 100755 --- a/example/basic/call_api.sh +++ b/example/basic/call_api.sh @@ -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 } @@ -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 diff --git a/example/basic/main.go b/example/basic/main.go index 6a4dc09..42f0d53 100644 --- a/example/basic/main.go +++ b/example/basic/main.go @@ -4,6 +4,7 @@ import ( "basicexample/helper" v20230401 "basicexample/v20230401" v20230501 "basicexample/v20230501" + "encoding/json" "log" "math/rand" "net/http" @@ -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) @@ -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) +} diff --git a/example/basic/v20230401/requestmigrations.go b/example/basic/v20230401/requestmigrations.go index 29066b2..171cfab 100644 --- a/example/basic/v20230401/requestmigrations.go +++ b/example/basic/v20230401/requestmigrations.go @@ -8,7 +8,7 @@ import ( "time" ) -// Migrations +// ListUserResponseMigration handles the response migration for the list users endpoint type ListUserResponseMigration struct{} func (c *ListUserResponseMigration) Migrate( @@ -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"` diff --git a/example/basic/v20230501/requestmigrations.go b/example/basic/v20230501/requestmigrations.go index 75be5c2..abf2d2b 100644 --- a/example/basic/v20230501/requestmigrations.go +++ b/example/basic/v20230501/requestmigrations.go @@ -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( @@ -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"` +}