diff --git a/README.md b/README.md index 5a4a1f8..17257a2 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ go get github.com/astronomer/epoch package main import ( + "log" + "github.com/astronomer/epoch/epoch" "github.com/gin-gonic/gin" ) @@ -52,7 +54,7 @@ func main() { v1, _ := epoch.NewSemverVersion("1.0.0") v2, _ := epoch.NewSemverVersion("2.0.0") - migration := epoch.NewVersionChangeBuilder(v1, v2). + migration, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Add email to User"). ForType(User{}). RequestToNextVersion(). @@ -60,6 +62,9 @@ func main() { ResponseToPreviousVersion(). RemoveField("email"). Build() + if err != nil { + log.Fatal(err) + } // Setup Epoch epochInstance, err := epoch.NewEpoch(). @@ -69,7 +74,7 @@ func main() { Build() if err != nil { - panic(err) + log.Fatal(err) } // Add to Gin @@ -122,13 +127,16 @@ The new framework uses **flow-based operations** that match the actual migration When a v1 client sends a request, it needs to be migrated TO the HEAD version: ```go -migration := epoch.NewVersionChangeBuilder(v1, v2). +migration, err := epoch.NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). AddField("email", "default@example.com"). // Add field for old clients RemoveField("deprecated_field"). // Remove deprecated field RenameField("name", "full_name"). // Rename old field to new Build() +if err != nil { + // handle err +} ``` ### Response Operations (HEAD → Client) @@ -136,13 +144,16 @@ migration := epoch.NewVersionChangeBuilder(v1, v2). When returning to a v1 client, response needs to be migrated FROM HEAD to v1: ```go -migration := epoch.NewVersionChangeBuilder(v1, v2). +migration, err := epoch.NewVersionChangeBuilder(v1, v2). ForType(User{}). ResponseToPreviousVersion(). RemoveField("email"). // Remove new fields AddField("old_field", "default"). // Restore old fields RenameField("full_name", "name"). // Rename back to old name Build() +if err != nil { + // handle err +} ``` ### Available Operations @@ -191,7 +202,7 @@ r.GET("/users", You can migrate multiple types together: ```go -migration := epoch.NewVersionChangeBuilder(v2, v3). +migration, err := epoch.NewVersionChangeBuilder(v2, v3). Description("Update User and Product"). ForType(User{}). ResponseToPreviousVersion(). @@ -200,6 +211,9 @@ migration := epoch.NewVersionChangeBuilder(v2, v3). ResponseToPreviousVersion(). RemoveField("currency"). Build() +if err != nil { + // handle err +} ``` ## Custom Transformations @@ -207,7 +221,7 @@ migration := epoch.NewVersionChangeBuilder(v2, v3). Mix declarative operations with custom logic: ```go -migration := epoch.NewVersionChangeBuilder(v1, v2). +migration, err := epoch.NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). AddField("email", "default@example.com"). @@ -219,6 +233,9 @@ migration := epoch.NewVersionChangeBuilder(v1, v2). return nil }). Build() +if err != nil { + // handle err +} ``` ## Global Transformers @@ -226,7 +243,7 @@ migration := epoch.NewVersionChangeBuilder(v1, v2). Apply transformations to all types: ```go -migration := epoch.NewVersionChangeBuilder(v1, v2). +migration, err := epoch.NewVersionChangeBuilder(v1, v2). CustomRequest(func(req *epoch.RequestInfo) error { // Applies to ALL request types return nil @@ -239,6 +256,9 @@ migration := epoch.NewVersionChangeBuilder(v1, v2). ResponseToPreviousVersion(). RemoveField("email"). Build() +if err != nil { + // handle err +} ``` ## Helper Methods @@ -385,15 +405,21 @@ Keep migrations focused on single types: ```go // ✅ Good - separate migrations per type -userChange := epoch.NewVersionChangeBuilder(v1, v2). +userChange, err := epoch.NewVersionChangeBuilder(v1, v2). ForType(User{}). ResponseToPreviousVersion().RemoveField("email"). Build() +if err != nil { + // handle err +} -productChange := epoch.NewVersionChangeBuilder(v1, v2). +productChange, err := epoch.NewVersionChangeBuilder(v1, v2). ForType(Product{}). ResponseToPreviousVersion().RemoveField("sku"). Build() +if err != nil { + // handle err +} // ❌ Avoid - mixing types in operations can be confusing ``` @@ -404,13 +430,16 @@ Use operations that match the actual migration direction: ```go // ✅ Good - clear flow direction -migration := epoch.NewVersionChangeBuilder(v1, v2). +migration, err := epoch.NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). // Client → HEAD AddField("email", "default"). ResponseToPreviousVersion(). // HEAD → Client RemoveField("email"). Build() +if err != nil { + // handle err +} ``` ## Testing diff --git a/epoch/integration_test.go b/epoch/integration_test.go index 52ba69f..85ecc16 100644 --- a/epoch/integration_test.go +++ b/epoch/integration_test.go @@ -103,7 +103,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") // V1→V2: Add email field - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Add email field to User"). ForType(User{}). RequestToNextVersion(). @@ -111,6 +111,7 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -167,7 +168,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Rename name to full_name"). ForType(User{}). RequestToNextVersion(). @@ -175,6 +176,7 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). RenameField("full_name", "name"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -220,13 +222,14 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewSemverVersion("1.0.0") v2, _ := NewSemverVersion("2.0.0") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). AddField("email", "default@example.com"). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := NewEpoch(). WithVersions(v1, v2). @@ -265,13 +268,14 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). AddField("email", "unknown@example.com"). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -307,13 +311,14 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). AddField("email", "unknown@example.com"). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -365,22 +370,24 @@ var _ = Describe("End-to-End Integration Tests", func() { v3, _ := NewDateVersion("2025-01-01") // V1->V2: Add currency - change1 := NewVersionChangeBuilder(v1, v2). + change1, err := NewVersionChangeBuilder(v1, v2). ForType(Product{}). RequestToNextVersion(). AddField("currency", "USD"). ResponseToPreviousVersion(). RemoveField("currency"). Build() + Expect(err).NotTo(HaveOccurred()) // V2->V3: Add description - change2 := NewVersionChangeBuilder(v2, v3). + change2, err := NewVersionChangeBuilder(v2, v3). ForType(Product{}). RequestToNextVersion(). AddField("description", ""). ResponseToPreviousVersion(). RemoveField("description"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2, v3}, []*VersionChange{change1, change2}) Expect(err).NotTo(HaveOccurred()) @@ -428,21 +435,23 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") v3, _ := NewDateVersion("2025-01-01") - change1 := NewVersionChangeBuilder(v1, v2). + change1, err := NewVersionChangeBuilder(v1, v2). ForType(Product{}). RequestToNextVersion(). AddField("currency", "USD"). ResponseToPreviousVersion(). RemoveField("currency"). Build() + Expect(err).NotTo(HaveOccurred()) - change2 := NewVersionChangeBuilder(v2, v3). + change2, err := NewVersionChangeBuilder(v2, v3). ForType(Product{}). RequestToNextVersion(). AddField("description", ""). ResponseToPreviousVersion(). RemoveField("description"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2, v3}, []*VersionChange{change1, change2}) Expect(err).NotTo(HaveOccurred()) @@ -480,17 +489,19 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - reqChange := NewVersionChangeBuilder(v1, v2). + reqChange, err := NewVersionChangeBuilder(v1, v2). ForType(CreateUserRequest{}). RequestToNextVersion(). AddField("email", "unknown@example.com"). Build() + Expect(err).NotTo(HaveOccurred()) - respChange := NewVersionChangeBuilder(v1, v2). + respChange, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). ResponseToPreviousVersion(). RemoveField("phone"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{reqChange, respChange}) Expect(err).NotTo(HaveOccurred()) @@ -552,7 +563,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewSemverVersion("2.0.0") v3, _ := NewSemverVersion("3.0.0") - v1ToV2 := NewVersionChangeBuilder(v1, v2). + v1ToV2, err := NewVersionChangeBuilder(v1, v2). Description("Rename name to newName"). ForType(ErrorTestRequest{}, ErrorTestResponse{}). RequestToNextVersion(). @@ -560,8 +571,9 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). RenameField("new_name", "name"). Build() + Expect(err).NotTo(HaveOccurred()) - v2ToV3 := NewVersionChangeBuilder(v2, v3). + v2ToV3, err := NewVersionChangeBuilder(v2, v3). Description("Rename newName to betterNewName"). ForType(ErrorTestRequest{}, ErrorTestResponse{}). RequestToNextVersion(). @@ -569,15 +581,16 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). RenameField("better_new_name", "new_name"). Build() + Expect(err).NotTo(HaveOccurred()) - var err error - e, err = NewEpoch(). + var epochErr error + e, epochErr = NewEpoch(). WithVersions(v1, v2, v3). WithHeadVersion(). WithChanges(v1ToV2, v2ToV3). WithVersionParameter("X-API-Version"). Build() - Expect(err).NotTo(HaveOccurred()) + Expect(epochErr).NotTo(HaveOccurred()) gin.SetMode(gin.TestMode) router = gin.New() @@ -884,11 +897,12 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -924,17 +938,19 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - userChange := NewVersionChangeBuilder(v1, v2). + userChange, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) - createChange := NewVersionChangeBuilder(v1, v2). + createChange, err := NewVersionChangeBuilder(v1, v2). ForType(CreateUserRequest{}). RequestToNextVersion(). AddField("email", "default@example.com"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{userChange, createChange}) Expect(err).NotTo(HaveOccurred()) @@ -1079,19 +1095,21 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change1 := NewVersionChangeBuilder(v1, v2). + change1, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). AddField("email", "test@example.com"). Build() + Expect(err).NotTo(HaveOccurred()) - change2 := NewVersionChangeBuilder(v2, v1). + change2, err := NewVersionChangeBuilder(v2, v1). ForType(User{}). RequestToNextVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) - _, err := NewEpoch(). + _, err = NewEpoch(). WithVersions(v1, v2). WithChanges(change1, change2). Build() @@ -1105,17 +1123,19 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") v3, _ := NewDateVersion("2025-01-01") - change1 := NewVersionChangeBuilder(v1, v2). + change1, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). AddField("email", "test@example.com"). Build() + Expect(err).NotTo(HaveOccurred()) - change2 := NewVersionChangeBuilder(v2, v3). + change2, err := NewVersionChangeBuilder(v2, v3). ForType(User{}). RequestToNextVersion(). RenameField("name", "full_name"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := NewEpoch(). WithVersions(v1, v2, v3). @@ -1133,15 +1153,16 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") v3, _ := NewDateVersion("2025-01-01") - userChange1 := NewVersionChangeBuilder(v1, v2). + userChange1, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). AddField("email", "unknown@example.com"). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) - userChange2 := NewVersionChangeBuilder(v2, v3). + userChange2, err := NewVersionChangeBuilder(v2, v3). ForType(User{}). RequestToNextVersion(). RenameField("name", "full_name"). @@ -1150,8 +1171,9 @@ var _ = Describe("End-to-End Integration Tests", func() { RenameField("full_name", "name"). RemoveField("phone"). Build() + Expect(err).NotTo(HaveOccurred()) - productChange := NewVersionChangeBuilder(v2, v3). + productChange, err := NewVersionChangeBuilder(v2, v3). ForType(Product{}). RequestToNextVersion(). AddField("currency", "USD"). @@ -1160,6 +1182,7 @@ var _ = Describe("End-to-End Integration Tests", func() { RemoveField("currency"). RemoveField("description"). Build() + Expect(err).NotTo(HaveOccurred()) instance, err := NewEpoch(). WithVersions(v1, v2, v3). @@ -1246,7 +1269,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewSemverVersion("1.0") v2, _ := NewSemverVersion("2.0") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Add phone field to User"). ForType(User{}). RequestToNextVersion(). @@ -1254,6 +1277,7 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). RemoveField("phone"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := NewEpoch(). WithVersions(v1, v2). @@ -1331,7 +1355,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewSemverVersion("1.0") v2, _ := NewSemverVersion("2.0") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Add email field to User"). ForType(User{}). RequestToNextVersion(). @@ -1339,6 +1363,7 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := NewEpoch(). WithVersions(v1, v2). @@ -1400,13 +1425,14 @@ var _ = Describe("End-to-End Integration Tests", func() { Name string `json:"name"` // Renamed from "user_name" in older version } - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(SimpleUser{}). RequestToNextVersion(). RenameField("user_name", "name"). ResponseToPreviousVersion(). RenameField("name", "user_name"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -1453,18 +1479,20 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") // Define change for CreateUserRequest (top-level) and Profile (nested object) - requestChange := NewVersionChangeBuilder(v1, v2). + requestChange, err := NewVersionChangeBuilder(v1, v2). ForType(CreateUserRequest{}). RequestToNextVersion(). RenameField("full_name", "display_name"). // Top-level rename Build() + Expect(err).NotTo(HaveOccurred()) // Separate change for nested Profile type - profileChange := NewVersionChangeBuilder(v1, v2). + profileChange, err := NewVersionChangeBuilder(v1, v2). ForType(Profile{}). RequestToNextVersion(). RenameField("biography", "bio"). // Nested object rename Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{requestChange, profileChange}) Expect(err).NotTo(HaveOccurred()) @@ -1516,19 +1544,21 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") // Define change for User (used as request type) and Role (nested array item) - userChange := NewVersionChangeBuilder(v1, v2). + userChange, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). RequestToNextVersion(). RenameField("name", "full_name"). // Top-level rename Build() + Expect(err).NotTo(HaveOccurred()) // Separate change for nested Role type in roles[] array - roleChange := NewVersionChangeBuilder(v1, v2). + roleChange, err := NewVersionChangeBuilder(v1, v2). ForType(Role{}). RequestToNextVersion(). RenameField("role_name", "name"). // Nested array item rename AddField("priority", 0). // Add default priority Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{userChange, roleChange}) Expect(err).NotTo(HaveOccurred()) @@ -1592,11 +1622,12 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). ResponseToPreviousVersion(). RenameField("full_name", "name"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -1647,12 +1678,13 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") // Migration for Role type in nested array - roleChange := NewVersionChangeBuilder(v1, v2). + roleChange, err := NewVersionChangeBuilder(v1, v2). ForType(Role{}). ResponseToPreviousVersion(). RenameField("name", "role_name"). RemoveField("priority"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{roleChange}) Expect(err).NotTo(HaveOccurred()) @@ -1702,18 +1734,20 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") // Migration for ListMetadata type - metadataChange := NewVersionChangeBuilder(v1, v2). + metadataChange, err := NewVersionChangeBuilder(v1, v2). ForType(ListMetadata{}). ResponseToPreviousVersion(). RenameField("updated_by", "author"). Build() + Expect(err).NotTo(HaveOccurred()) // Migration for User in array - userChange := NewVersionChangeBuilder(v1, v2). + userChange, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). ResponseToPreviousVersion(). RemoveField("phone"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{metadataChange, userChange}) Expect(err).NotTo(HaveOccurred()) @@ -1762,19 +1796,21 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") // Migration for User in array - userChange := NewVersionChangeBuilder(v1, v2). + userChange, err := NewVersionChangeBuilder(v1, v2). ForType(User{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) // Migration for Role type in deeply nested array (users[].roles[]) - roleChange := NewVersionChangeBuilder(v1, v2). + roleChange, err := NewVersionChangeBuilder(v1, v2). ForType(Role{}). ResponseToPreviousVersion(). RenameField("name", "role_name"). RemoveField("priority"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{userChange, roleChange}) Expect(err).NotTo(HaveOccurred()) @@ -1865,12 +1901,13 @@ var _ = Describe("End-to-End Integration Tests", func() { v2, _ := NewDateVersion("2024-06-01") // Migration for Profile type nested in users[] - profileChange := NewVersionChangeBuilder(v1, v2). + profileChange, err := NewVersionChangeBuilder(v1, v2). ForType(Profile{}). ResponseToPreviousVersion(). RenameField("bio", "biography"). RemoveField("avatar"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{profileChange}) Expect(err).NotTo(HaveOccurred()) @@ -1960,7 +1997,7 @@ var _ = Describe("End-to-End Integration Tests", func() { // V1→V2: Remove deprecated fields from request, add them back in response // The auto-capture feature should preserve the original values - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Remove description and metadata fields"). ForType(AutoCaptureRequest{}). RequestToNextVersion(). @@ -1971,6 +2008,7 @@ var _ = Describe("End-to-End Integration Tests", func() { AddField("description", "default description"). // Uses captured value instead AddField("metadata", "default metadata"). // Uses captured value instead Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -2036,7 +2074,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Handle optional field"). ForType(AutoCaptureRequest{}). RequestToNextVersion(). @@ -2045,6 +2083,7 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). AddField("description", "default description"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -2085,7 +2124,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Handler override test"). ForType(AutoCaptureRequest{}). RequestToNextVersion(). @@ -2094,6 +2133,7 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). AddField("description", "default description"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -2150,7 +2190,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Complex field preservation"). ForType(ComplexRequest{}). RequestToNextVersion(). @@ -2159,6 +2199,7 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). AddField("settings", map[string]interface{}{"default": true}). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -2224,7 +2265,7 @@ var _ = Describe("End-to-End Integration Tests", func() { v1, _ := NewDateVersion("2024-01-01") v2, _ := NewDateVersion("2024-06-01") - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). Description("Concurrent test"). ForType(ConcurrentRequest{}). RequestToNextVersion(). @@ -2233,6 +2274,7 @@ var _ = Describe("End-to-End Integration Tests", func() { ResponseToPreviousVersion(). AddField("description", "default"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := setupBasicEpoch([]*Version{v1, v2}, []*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) diff --git a/epoch/openapi/generator_test.go b/epoch/openapi/generator_test.go index c5d143d..12dac4f 100644 --- a/epoch/openapi/generator_test.go +++ b/epoch/openapi/generator_test.go @@ -186,12 +186,13 @@ var _ = Describe("SchemaGenerator", func() { v2, _ := epoch.NewDateVersion("2024-06-01") // Create version change - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Add email field"). ForType(TestUserResponse{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) // Create version bundle versionBundle, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) @@ -266,12 +267,13 @@ var _ = Describe("SchemaGenerator", func() { versionBundle, _ := epoch.NewVersionBundle([]*epoch.Version{v1}) // Create version change: remove 'email' field in v1 - change := epoch.NewVersionChangeBuilder(v1, headVersion). + change, err := epoch.NewVersionChangeBuilder(v1, headVersion). Description("Add email field"). ForType(TestUserResponse{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) v1.Changes = []epoch.VersionChangeInterface{change} @@ -394,7 +396,7 @@ var _ = Describe("SchemaGenerator", func() { headVersion := epoch.NewHeadVersion() // Migration from v1 to HEAD - change := epoch.NewVersionChangeBuilder(v1, headVersion). + change, err := epoch.NewVersionChangeBuilder(v1, headVersion). Description("Add betterNewName and timezone fields"). ForType(UpdateExampleRequest{}). ResponseToPreviousVersion(). @@ -404,6 +406,7 @@ var _ = Describe("SchemaGenerator", func() { RenameField("name", "betterNewName"). AddField("timezone", ""). Build() + Expect(err).NotTo(HaveOccurred()) vb, err := epoch.NewVersionBundle([]*epoch.Version{headVersion, v1}) Expect(err).NotTo(HaveOccurred()) diff --git a/epoch/openapi/swag_integration_test.go b/epoch/openapi/swag_integration_test.go index ec9b06c..0a6c435 100644 --- a/epoch/openapi/swag_integration_test.go +++ b/epoch/openapi/swag_integration_test.go @@ -51,7 +51,7 @@ func createTestVersions() (*epoch.Version, *epoch.Version, *epoch.Version) { // createTestMigrations creates the standard v1→v2 and v2→v3 migrations func createTestMigrations(v1, v2, v3 *epoch.Version) (*epoch.VersionChange, *epoch.VersionChange) { // v1→v2: Add email and status fields - v1ToV2 := epoch.NewVersionChangeBuilder(v1, v2). + v1ToV2, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Add email and status fields to User"). ForType(swagTestCreateUserRequest{}). RequestToNextVersion(). @@ -62,9 +62,12 @@ func createTestMigrations(v1, v2, v3 *epoch.Version) (*epoch.VersionChange, *epo RemoveField("email"). RemoveField("status"). Build() + if err != nil { + panic(err) + } // v2→v3: Rename name to full_name, add phone - v2ToV3 := epoch.NewVersionChangeBuilder(v2, v3). + v2ToV3, err := epoch.NewVersionChangeBuilder(v2, v3). Description("Rename name to full_name, add phone"). ForType(swagTestCreateUserRequest{}). RequestToNextVersion(). @@ -75,6 +78,9 @@ func createTestMigrations(v1, v2, v3 *epoch.Version) (*epoch.VersionChange, *epo RenameField("full_name", "name"). RemoveField("phone"). Build() + if err != nil { + panic(err) + } return v1ToV2, v2ToV3 } @@ -453,12 +459,13 @@ var _ = Describe("Swag Integration", func() { headVersion := epoch.NewHeadVersion() // Simple migration that removes one field - change := epoch.NewVersionChangeBuilder(v1, headVersion). + change, err := epoch.NewVersionChangeBuilder(v1, headVersion). Description("Add email field"). ForType(swagTestUserResponse{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) epochInstance, err := epoch.NewEpoch(). WithHeadVersion(). diff --git a/epoch/openapi/version_transformer_test.go b/epoch/openapi/version_transformer_test.go index 9e72f3e..b25ae24 100644 --- a/epoch/openapi/version_transformer_test.go +++ b/epoch/openapi/version_transformer_test.go @@ -66,12 +66,13 @@ var _ = Describe("VersionTransformer", func() { v2, _ := epoch.NewDateVersion("2024-06-01") // Create version change: v1 -> v2 adds email - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Add email field"). ForType(TestUser{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) v2.Changes = []epoch.VersionChangeInterface{change} @@ -106,12 +107,13 @@ var _ = Describe("VersionTransformer", func() { v2, _ := epoch.NewDateVersion("2024-06-01") // v1 -> v2 renames "full_name" to "name" - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Rename name to full_name"). ForType(TestUser{}). ResponseToPreviousVersion(). RenameField("name", "full_name"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) v2.Changes = []epoch.VersionChangeInterface{change} @@ -143,12 +145,13 @@ var _ = Describe("VersionTransformer", func() { v1, _ := epoch.NewDateVersion("2024-01-01") v2, _ := epoch.NewDateVersion("2024-06-01") - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Add status field"). ForType(TestUser{}). ResponseToPreviousVersion(). RemoveField("status"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) v2.Changes = []epoch.VersionChangeInterface{change} @@ -180,7 +183,7 @@ var _ = Describe("VersionTransformer", func() { v2, _ := epoch.NewDateVersion("2024-06-01") // v1 -> v2: adds email, phone, and status - change := epoch.NewVersionChangeBuilder(v1, v2). + change, buildErr := epoch.NewVersionChangeBuilder(v1, v2). Description("Add multiple fields"). ForType(TestUser{}). ResponseToPreviousVersion(). @@ -188,6 +191,7 @@ var _ = Describe("VersionTransformer", func() { RemoveField("phone"). RemoveField("status"). Build() + Expect(buildErr).NotTo(HaveOccurred()) v2.Changes = []epoch.VersionChangeInterface{change} @@ -229,12 +233,13 @@ var _ = Describe("VersionTransformer", func() { // v1 -> v2: add "email" field (v2/HEAD has it, v1 doesn't) // When generating v1 request schema, we start from v2 and remove email - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Add email field"). ForType(TestUser{}). RequestToNextVersion(). AddField("email", "unknown@example.com"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) v2.Changes = []epoch.VersionChangeInterface{change} @@ -272,12 +277,13 @@ var _ = Describe("VersionTransformer", func() { // v1 -> v2: rename "name" to "full_name" // HEAD has "full_name", v1 has "name" - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Rename name to full_name"). ForType(TestUser{}). RequestToNextVersion(). RenameField("name", "full_name"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) v2.Changes = []epoch.VersionChangeInterface{change} @@ -313,12 +319,13 @@ var _ = Describe("VersionTransformer", func() { // v1 -> v2: remove deprecated field (v1 has it, v2 doesn't) // This means RequestToNextVersion removes it when going from v1 to v2 - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Remove deprecated field"). ForType(TestUser{}). RequestToNextVersion(). RemoveField("deprecated_field"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) v2.Changes = []epoch.VersionChangeInterface{change} @@ -357,7 +364,7 @@ var _ = Describe("VersionTransformer", func() { // - v1 has "old_status", v2 has "status" (rename) // - v1 doesn't have "email", v2 has "email" (add) // - v1 doesn't have "phone", v2 has "phone" (add) - change := epoch.NewVersionChangeBuilder(v1, v2). + change, buildErr := epoch.NewVersionChangeBuilder(v1, v2). Description("Multiple request changes"). ForType(TestUser{}). RequestToNextVersion(). @@ -365,6 +372,7 @@ var _ = Describe("VersionTransformer", func() { AddField("phone", ""). RenameField("old_status", "status"). Build() + Expect(buildErr).NotTo(HaveOccurred()) v2.Changes = []epoch.VersionChangeInterface{change} @@ -411,12 +419,13 @@ var _ = Describe("VersionTransformer", func() { Field string } - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Change for different type"). ForType(DifferentType{}). ResponseToPreviousVersion(). RemoveField("field"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) v2.Changes = []epoch.VersionChangeInterface{change} @@ -566,12 +575,13 @@ var _ = Describe("VersionTransformer", func() { v1, _ := epoch.NewDateVersion("2024-01-01") v2, _ := epoch.NewDateVersion("2024-06-01") - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Test change"). ForType(TestUser{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) transformer := NewVersionTransformer(vb) @@ -584,12 +594,13 @@ var _ = Describe("VersionTransformer", func() { v1, _ := epoch.NewDateVersion("2024-01-01") v2, _ := epoch.NewDateVersion("2024-06-01") - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Test change"). ForType(TestUser{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) transformer := NewVersionTransformer(vb) @@ -605,12 +616,13 @@ var _ = Describe("VersionTransformer", func() { v1, _ := epoch.NewDateVersion("2024-01-01") v2, _ := epoch.NewDateVersion("2024-06-01") - change := epoch.NewVersionChangeBuilder(v1, v2). + change, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Test change"). ForType(TestUser{}). ResponseToPreviousVersion(). RemoveField("email"). Build() + Expect(err).NotTo(HaveOccurred()) vb, _ := epoch.NewVersionBundle([]*epoch.Version{v1, v2}) transformer := NewVersionTransformer(vb) diff --git a/epoch/version_change_builder.go b/epoch/version_change_builder.go index aed734b..cd504f5 100644 --- a/epoch/version_change_builder.go +++ b/epoch/version_change_builder.go @@ -1,6 +1,7 @@ package epoch import ( + "fmt" "reflect" "strings" @@ -81,14 +82,14 @@ func (b *versionChangeBuilder) CustomResponse(fn func(*ResponseInfo) error) *ver } // Build compiles all operations into a VersionChange -func (b *versionChangeBuilder) Build() *VersionChange { +func (b *versionChangeBuilder) Build() (*VersionChange, error) { if b.description == "" { b.description = "Migration from " + b.fromVersion.String() + " to " + b.toVersion.String() } // Validate: require at least one type or custom transformer if len(b.typeOps) == 0 && b.customRequest == nil && b.customResponse == nil { - panic("epoch: VersionChange must specify at least one type using ForType() or custom transformers") + return nil, fmt.Errorf("epoch: VersionChange must specify at least one type using ForType() or custom transformers") } var instructions []interface{} @@ -233,7 +234,7 @@ func (b *versionChangeBuilder) Build() *VersionChange { } } - return vc + return vc, nil } // typeBuilder builds operations for specific types @@ -262,7 +263,7 @@ func (tb *typeBuilder) ForType(types ...interface{}) *typeBuilder { } // Build is a convenience method that calls the parent's Build() -func (tb *typeBuilder) Build() *VersionChange { +func (tb *typeBuilder) Build() (*VersionChange, error) { return tb.parent.Build() } @@ -334,7 +335,7 @@ func (b *requestToNextVersionBuilder) ForType(types ...interface{}) *typeBuilder } // Build completes the builder chain -func (b *requestToNextVersionBuilder) Build() *VersionChange { +func (b *requestToNextVersionBuilder) Build() (*VersionChange, error) { return b.parent.Build() } @@ -406,7 +407,7 @@ func (b *responseToPreviousVersionBuilder) ForType(types ...interface{}) *typeBu } // Build completes the builder chain -func (b *responseToPreviousVersionBuilder) Build() *VersionChange { +func (b *responseToPreviousVersionBuilder) Build() (*VersionChange, error) { return b.parent.Build() } diff --git a/epoch/version_change_builder_test.go b/epoch/version_change_builder_test.go index b2dc62b..ad87563 100644 --- a/epoch/version_change_builder_test.go +++ b/epoch/version_change_builder_test.go @@ -45,7 +45,7 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { Describe("Cadwyn-Style API", func() { It("should create migration with clear direction semantics", func() { - migration := NewVersionChangeBuilder(v1, v2). + migration, err := NewVersionChangeBuilder(v1, v2). Description("Add email field to User"). ForType(BuilderTestUser{}). RequestToNextVersion(). @@ -54,6 +54,7 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { RemoveField("email"). // Remove email from responses for v1 clients Build() + Expect(err).NotTo(HaveOccurred()) Expect(migration).NotTo(BeNil()) Expect(migration.Description()).To(Equal("Add email field to User")) Expect(migration.FromVersion()).To(Equal(v1)) @@ -61,7 +62,7 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { }) It("should support multiple schemas in one migration", func() { - migration := NewVersionChangeBuilder(v1, v2). + migration, err := NewVersionChangeBuilder(v1, v2). Description("Update User and Product schemas"). ForType(BuilderTestUser{}). ResponseToPreviousVersion(). @@ -71,12 +72,13 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { RemoveField("currency"). // Remove new field for v1 clients Build() + Expect(err).NotTo(HaveOccurred()) Expect(migration).NotTo(BeNil()) Expect(migration.Description()).To(Equal("Update User and Product schemas")) }) It("should support global custom transformers", func() { - migration := NewVersionChangeBuilder(v1, v2). + migration, err := NewVersionChangeBuilder(v1, v2). Description("Global custom operations"). CustomRequest(func(req *RequestInfo) error { // Custom logic for all requests @@ -88,24 +90,26 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { }). Build() + Expect(err).NotTo(HaveOccurred()) Expect(migration).NotTo(BeNil()) }) It("should require at least one schema or custom transformer", func() { - Expect(func() { - NewVersionChangeBuilder(v1, v2). - Description("Empty migration"). - Build() - }).To(Panic()) + _, err := NewVersionChangeBuilder(v1, v2). + Description("Empty migration"). + Build() + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("at least one type")) }) It("should generate default description if none provided", func() { - migration := NewVersionChangeBuilder(v1, v2). + migration, err := NewVersionChangeBuilder(v1, v2). ForType(BuilderTestUser{}). RequestToNextVersion(). AddField("email", "test@example.com"). Build() + Expect(err).NotTo(HaveOccurred()) Expect(migration.Description()).To(Equal("Migration from 2024-01-01 to 2024-06-01")) }) }) @@ -130,12 +134,13 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { }) It("should apply RequestToNextVersion operations correctly", func() { - migration := NewVersionChangeBuilder(v1, v2). // v1→v2 migration - ForType(BuilderTestUser{}). - RequestToNextVersion(). - AddField("created_at", "2024-01-01"). - RenameField("name", "full_name"). - Build() + migration, err := NewVersionChangeBuilder(v1, v2). // v1→v2 migration + ForType(BuilderTestUser{}). + RequestToNextVersion(). + AddField("created_at", "2024-01-01"). + RenameField("name", "full_name"). + Build() + Expect(err).NotTo(HaveOccurred()) // Create a mock RequestInfo requestInfo := &RequestInfo{Body: testNode} @@ -161,12 +166,13 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { }) It("should apply ResponseToPreviousVersion operations correctly", func() { - migration := NewVersionChangeBuilder(v2, v1). // v2→v1 migration - ForType(BuilderTestUser{}). - ResponseToPreviousVersion(). - RemoveField("email"). - AddField("legacy_field", "legacy_value"). - Build() + migration, err := NewVersionChangeBuilder(v2, v1). // v2→v1 migration + ForType(BuilderTestUser{}). + ResponseToPreviousVersion(). + RemoveField("email"). + AddField("legacy_field", "legacy_value"). + Build() + Expect(err).NotTo(HaveOccurred()) // Create a mock ResponseInfo responseInfo := &ResponseInfo{Body: testNode, StatusCode: 200} @@ -191,7 +197,7 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { Describe("Builder Fluency", func() { It("should allow chaining between different direction builders", func() { - migration := NewVersionChangeBuilder(v1, v2). + migration, err := NewVersionChangeBuilder(v1, v2). Description("Complex chaining example"). ForType(BuilderTestUser{}). RequestToNextVersion(). @@ -203,17 +209,19 @@ var _ = Describe("SchemaVersionChangeBuilder", func() { RemoveField("currency"). Build() + Expect(err).NotTo(HaveOccurred()) Expect(migration).NotTo(BeNil()) Expect(migration.Description()).To(Equal("Complex chaining example")) }) It("should allow returning to schema builder from direction builders", func() { - migration := NewVersionChangeBuilder(v1, v2). + migration, err := NewVersionChangeBuilder(v1, v2). ForType(BuilderTestProduct{}). // Should return to schema builder ResponseToPreviousVersion(). RemoveField("description"). Build() + Expect(err).NotTo(HaveOccurred()) Expect(migration).NotTo(BeNil()) }) }) diff --git a/epoch/version_change_test.go b/epoch/version_change_test.go index 08c4493..1084624 100644 --- a/epoch/version_change_test.go +++ b/epoch/version_change_test.go @@ -402,21 +402,23 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { It("should apply transformations step-by-step (V3→V2→V1)", func() { // V1→V2 migration: title → name, remove category // (When going backward V2→V1) - change1 := NewVersionChangeBuilder(v1, v2). + change1, err := NewVersionChangeBuilder(v1, v2). ForType(Item{}). ResponseToPreviousVersion(). RenameField("title", "name"). RemoveField("category"). Build() + Expect(err).NotTo(HaveOccurred()) // V2→V3 migration: display_name → title, remove priority // (When going backward V3→V2) - change2 := NewVersionChangeBuilder(v2, v3). + change2, err := NewVersionChangeBuilder(v2, v3). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "title"). RemoveField("priority"). Build() + Expect(err).NotTo(HaveOccurred()) // Build chain chain, err := NewMigrationChain([]*VersionChange{change1, change2}) @@ -469,12 +471,13 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { It("should handle V3→V2 migrations correctly", func() { // V2→V3 migration - change := NewVersionChangeBuilder(v2, v3). + change, err := NewVersionChangeBuilder(v2, v3). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "title"). RemoveField("priority"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -511,19 +514,21 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { }) It("should handle multiple items in nested arrays", func() { - change1 := NewVersionChangeBuilder(v1, v2). + change1, err := NewVersionChangeBuilder(v1, v2). ForType(Item{}). ResponseToPreviousVersion(). RenameField("title", "name"). RemoveField("category"). Build() + Expect(err).NotTo(HaveOccurred()) - change2 := NewVersionChangeBuilder(v2, v3). + change2, err := NewVersionChangeBuilder(v2, v3). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "title"). RemoveField("priority"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change1, change2}) Expect(err).NotTo(HaveOccurred()) @@ -566,11 +571,12 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { }) It("should handle empty nested arrays gracefully", func() { - change := NewVersionChangeBuilder(v2, v3). + change, err := NewVersionChangeBuilder(v2, v3). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "title"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -595,11 +601,12 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { }) It("should preserve nestedArrayTypes through migration chain", func() { - change := NewVersionChangeBuilder(v2, v3). + change, err := NewVersionChangeBuilder(v2, v3). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "title"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -790,11 +797,12 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { Describe("Nested object transformations", func() { It("should demonstrate that standard RenameField does NOT work on nested fields", func() { // This test documents the LIMITATION: standard operations only work on top-level - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(Container{}). ResponseToPreviousVersion(). RenameField("created_by", "author"). // This targets top-level, not nested metadata! Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -823,11 +831,12 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { It("should transform nested objects using MigrateResponseForTypeWithNestedObjects", func() { // Migration for Metadata type - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(Container{}, Metadata{}). ResponseToPreviousVersion(). RenameField("created_by", "author"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -866,17 +875,19 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { // Each item has a details object (nested object inside array item) // With recursive transformation, the details object WILL be transformed - detailsChange := NewVersionChangeBuilder(v1, v2). + detailsChange, err := NewVersionChangeBuilder(v1, v2). ForType(Details{}). ResponseToPreviousVersion(). RenameField("last_updated", "updated_at"). Build() + Expect(err).NotTo(HaveOccurred()) - itemChange := NewVersionChangeBuilder(v1, v2). + itemChange, err := NewVersionChangeBuilder(v1, v2). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "name"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{detailsChange, itemChange}) Expect(err).NotTo(HaveOccurred()) @@ -925,17 +936,19 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { It("should recursively transform both first-level and second-level array items", func() { // Test items[].subitems[] - two-level nesting // With recursive transformation, both levels should be transformed - subItemChange := NewVersionChangeBuilder(v1, v2). + subItemChange, err := NewVersionChangeBuilder(v1, v2). ForType(SubItem{}). ResponseToPreviousVersion(). RenameField("label", "name"). Build() + Expect(err).NotTo(HaveOccurred()) - itemChange := NewVersionChangeBuilder(v1, v2). + itemChange, err := NewVersionChangeBuilder(v1, v2). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "title"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{subItemChange, itemChange}) Expect(err).NotTo(HaveOccurred()) @@ -1006,17 +1019,19 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { } It("should handle multiple arrays registered at same level", func() { - userChange := NewVersionChangeBuilder(v1, v2). + userChange, err := NewVersionChangeBuilder(v1, v2). ForType(UserItem{}). ResponseToPreviousVersion(). RenameField("name", "user_name"). Build() + Expect(err).NotTo(HaveOccurred()) - productChange := NewVersionChangeBuilder(v1, v2). + productChange, err := NewVersionChangeBuilder(v1, v2). ForType(ProductItem{}). ResponseToPreviousVersion(). RenameField("title", "product_title"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{userChange, productChange}) Expect(err).NotTo(HaveOccurred()) @@ -1055,11 +1070,12 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { Describe("Edge cases and error handling", func() { It("should handle null field values gracefully", func() { - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "name"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -1089,11 +1105,12 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { }) It("should handle type mismatch gracefully (expected array got object)", func() { - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "name"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -1119,11 +1136,12 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { }) It("should handle missing field to rename gracefully", func() { - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(Item{}). ResponseToPreviousVersion(). RenameField("nonexistent_field", "new_name"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -1153,11 +1171,12 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { }) It("should handle large payloads with many nested items", func() { - change := NewVersionChangeBuilder(v1, v2). + change, err := NewVersionChangeBuilder(v1, v2). ForType(Item{}). ResponseToPreviousVersion(). RenameField("display_name", "name"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{change}) Expect(err).NotTo(HaveOccurred()) @@ -1226,23 +1245,26 @@ var _ = Describe("Nested Array Multi-Step Migrations", func() { Items []ItemWithDeepNesting `json:"items"` } - subSubItemChange := NewVersionChangeBuilder(v1, v2). + subSubItemChange, err := NewVersionChangeBuilder(v1, v2). ForType(SubSubItem{}). ResponseToPreviousVersion(). RenameField("code", "item_code"). Build() + Expect(err).NotTo(HaveOccurred()) - subItemChange := NewVersionChangeBuilder(v1, v2). + subItemChange, err := NewVersionChangeBuilder(v1, v2). ForType(SubItemWithNesting{}). ResponseToPreviousVersion(). RenameField("label", "sub_label"). Build() + Expect(err).NotTo(HaveOccurred()) - itemChange := NewVersionChangeBuilder(v1, v2). + itemChange, err := NewVersionChangeBuilder(v1, v2). ForType(ItemWithDeepNesting{}). ResponseToPreviousVersion(). RenameField("name", "item_name"). Build() + Expect(err).NotTo(HaveOccurred()) chain, err := NewMigrationChain([]*VersionChange{subSubItemChange, subItemChange, itemChange}) Expect(err).NotTo(HaveOccurred()) diff --git a/examples/advanced/main.go b/examples/advanced/main.go index 73ecf83..d98c75e 100644 --- a/examples/advanced/main.go +++ b/examples/advanced/main.go @@ -470,22 +470,55 @@ func main() { v3, _ := epoch.NewDateVersion("2025-01-01") // Build Epoch instance + userV1ToV2, err := createUserV1ToV2Migration(v1, v2) + if err != nil { + panic(fmt.Sprintf("Failed to build migration: %v", err)) + } + profileV1ToV2, err := createProfileV1ToV2Migration(v1, v2) + if err != nil { + panic(fmt.Sprintf("Failed to build migration: %v", err)) + } + skillV1ToV2, err := createSkillV1ToV2Migration(v1, v2) + if err != nil { + panic(fmt.Sprintf("Failed to build migration: %v", err)) + } + profileSettingsV1ToV2, err := createProfileSettingsV1ToV2Migration(v1, v2) + if err != nil { + panic(fmt.Sprintf("Failed to build migration: %v", err)) + } + userV2ToV3, err := createUserV2ToV3Migration(v2, v3) + if err != nil { + panic(fmt.Sprintf("Failed to build migration: %v", err)) + } + productV2ToV3, err := createProductV2ToV3Migration(v2, v3) + if err != nil { + panic(fmt.Sprintf("Failed to build migration: %v", err)) + } + exampleV1ToV2, err := createExampleV1ToV2Migration(v1, v2) + if err != nil { + panic(fmt.Sprintf("Failed to build migration: %v", err)) + } + exampleV2ToV3, err := createExampleV2ToV3Migration(v2, v3) + if err != nil { + panic(fmt.Sprintf("Failed to build migration: %v", err)) + } + epochInstance, err := epoch.NewEpoch(). WithVersions(v1, v2, v3). WithHeadVersion(). WithChanges( // User v1->v2: Top-level fields + nested type transformations (separate for each type) - createUserV1ToV2Migration(v1, v2), - createProfileV1ToV2Migration(v1, v2), - createSkillV1ToV2Migration(v1, v2), - createProfileSettingsV1ToV2Migration(v1, v2), + userV1ToV2, + profileV1ToV2, + skillV1ToV2, + profileSettingsV1ToV2, // User v2->v3 - createUserV2ToV3Migration(v2, v3), + userV2ToV3, // Product v2->v3 - createProductV2ToV3Migration(v2, v3), + productV2ToV3, // Example migrations - createExampleV1ToV2Migration(v1, v2), - createExampleV2ToV3Migration(v2, v3), + exampleV1ToV2, + exampleV2ToV3, ). WithTypes( // User types (including nested types for profile, skills, settings) @@ -815,12 +848,7 @@ func main() { // createUserV1ToV2Migration defines migrations for TOP-LEVEL user fields only // Nested types (Profile, Skill, Settings) have their own separate migrations -// -// IMPORTANT: This migration demonstrates the AUTO-CAPTURE feature: -// - RemoveField("legacy_notes") on request CAPTURES the value before removing -// - AddField("legacy_notes", "") on response USES the captured value instead of the default -// This enables seamless round-trip preservation of deprecated fields! -func createUserV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange { +func createUserV1ToV2Migration(from, to *epoch.Version) (*epoch.VersionChange, error) { return epoch.NewVersionChangeBuilder(from, to). Description("Add email and status fields, deprecate legacy_notes (auto-captured)"). // Only target top-level user types (NOT nested types - they have separate migrations) @@ -839,7 +867,7 @@ func createUserV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange { } // createProfileV1ToV2Migration handles nested Profile object transformations -func createProfileV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange { +func createProfileV1ToV2Migration(from, to *epoch.Version) (*epoch.VersionChange, error) { return epoch.NewVersionChangeBuilder(from, to). Description("Transform profile.biography -> profile.bio"). // Target ONLY Profile types (response and request) @@ -854,7 +882,7 @@ func createProfileV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange } // createSkillV1ToV2Migration handles nested Skill array item transformations -func createSkillV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange { +func createSkillV1ToV2Migration(from, to *epoch.Version) (*epoch.VersionChange, error) { return epoch.NewVersionChangeBuilder(from, to). Description("Transform skills[].skill_name -> skills[].name, add level"). // Target ONLY Skill types (response and request) @@ -871,7 +899,7 @@ func createSkillV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange { } // createProfileSettingsV1ToV2Migration handles deeply nested Settings transformations -func createProfileSettingsV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange { +func createProfileSettingsV1ToV2Migration(from, to *epoch.Version) (*epoch.VersionChange, error) { return epoch.NewVersionChangeBuilder(from, to). Description("Transform settings.color_theme -> settings.theme"). // Target ONLY ProfileSettings types (response and request) @@ -886,7 +914,7 @@ func createProfileSettingsV1ToV2Migration(from, to *epoch.Version) *epoch.Versio } // createUserV2ToV3Migration defines the migration from v2 to v3 -func createUserV2ToV3Migration(from, to *epoch.Version) *epoch.VersionChange { +func createUserV2ToV3Migration(from, to *epoch.Version) (*epoch.VersionChange, error) { return epoch.NewVersionChangeBuilder(from, to). Description("Rename name to full_name, add phone"). // Only target top-level user types @@ -905,7 +933,7 @@ func createUserV2ToV3Migration(from, to *epoch.Version) *epoch.VersionChange { // createProductV2ToV3Migration defines the migration from v2 to v3 for products // This uses the NEW flow-based API with only 2 directions -func createProductV2ToV3Migration(from, to *epoch.Version) *epoch.VersionChange { +func createProductV2ToV3Migration(from, to *epoch.Version) (*epoch.VersionChange, error) { return epoch.NewVersionChangeBuilder(from, to). Description("Add description and currency to Product"). // TYPE-BASED ROUTING: Target all Product-related request/response types @@ -924,7 +952,7 @@ func createProductV2ToV3Migration(from, to *epoch.Version) *epoch.VersionChange // createExampleV1ToV2Migration // This migration affects the ExampleItem structs inside the Examples array // Also demonstrates 2-level nested array transformation (examples[].sub_items[]) -func createExampleV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange { +func createExampleV1ToV2Migration(from, to *epoch.Version) (*epoch.VersionChange, error) { return epoch.NewVersionChangeBuilder(from, to). Description("Rename name to title in nested array items, add category field, transform sub_items"). // TYPE-BASED ROUTING: Target the container, nested item types, and sub-item types @@ -938,7 +966,7 @@ func createExampleV1ToV2Migration(from, to *epoch.Version) *epoch.VersionChange } // createExampleV2ToV3Migration -func createExampleV2ToV3Migration(from, to *epoch.Version) *epoch.VersionChange { +func createExampleV2ToV3Migration(from, to *epoch.Version) (*epoch.VersionChange, error) { return epoch.NewVersionChangeBuilder(from, to). Description("Rename title to display_name in nested array items, add priority field, rename updated_at to last_updated in metadata"). // TYPE-BASED ROUTING: Target the container, nested item response, metadata response, and sub-item types diff --git a/examples/basic/main.go b/examples/basic/main.go index c551816..0edce7c 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -30,10 +30,15 @@ func main() { fmt.Println("") // Build Epoch instance with automatic cycle detection + v1ToV2Change, err := createV1ToV2Change() + if err != nil { + panic(fmt.Sprintf("Failed to build version change: %v", err)) + } + epochInstance, err := epoch.NewEpoch(). WithSemverVersions("1.0.0", "2.0.0"). WithHeadVersion(). - WithChanges(createV1ToV2Change()). + WithChanges(v1ToV2Change). Build() if err != nil { @@ -79,7 +84,7 @@ func main() { // This uses the NEW flow-based API with only 2 directions (matching actual flow): // 1. RequestToNextVersion: Client→HEAD (ONLY direction requests flow) // 2. ResponseToPreviousVersion: HEAD→Client (ONLY direction responses flow) -func createV1ToV2Change() *epoch.VersionChange { +func createV1ToV2Change() (*epoch.VersionChange, error) { v1, _ := epoch.NewSemverVersion("1.0.0") v2, _ := epoch.NewSemverVersion("2.0.0") diff --git a/examples/schema-generation/main.go b/examples/schema-generation/main.go index 35194c9..e34fddf 100644 --- a/examples/schema-generation/main.go +++ b/examples/schema-generation/main.go @@ -466,7 +466,7 @@ func main() { // HEAD has these fields, v1 doesn't, so: // - Response: Remove them when sending to v1 (ResponseToPreviousVersion) // - Request: Add them when v1 client sends request (RequestToNextVersion) - v1ToV2 := epoch.NewVersionChangeBuilder(v1, v2). + v1ToV2, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Add email and status fields to User"). ForType(CreateUserRequest{}). RequestToNextVersion(). @@ -477,12 +477,15 @@ func main() { RemoveField("email"). RemoveField("status"). Build() + if err != nil { + log.Fatalf("Failed to build v1ToV2 migration: %v", err) + } // v2→v3: Rename name to full_name, add phone // HEAD has full_name and phone, v2 has name (no phone), so: // - Response: Rename full_name→name, remove phone when sending to v2 // - Request: Rename name→full_name, add phone when v2 client sends request - v2ToV3 := epoch.NewVersionChangeBuilder(v2, v3). + v2ToV3, err := epoch.NewVersionChangeBuilder(v2, v3). Description("Rename name to full_name, add phone"). ForType(CreateUserRequest{}). RequestToNextVersion(). @@ -493,13 +496,16 @@ func main() { RenameField("full_name", "name"). RemoveField("phone"). Build() + if err != nil { + log.Fatalf("Failed to build v2ToV3 migration: %v", err) + } // ============================================================================ // NESTED TYPE MIGRATIONS (separate migrations for each nested type) // ============================================================================ // Profile migration: bio ↔ biography - profileV1ToV2 := epoch.NewVersionChangeBuilder(v1, v2). + profileV1ToV2, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Transform profile.biography -> profile.bio"). ForType(UserProfile{}, ProfileRequest{}). RequestToNextVersion(). @@ -507,9 +513,12 @@ func main() { ResponseToPreviousVersion(). RenameField("bio", "biography"). Build() + if err != nil { + log.Fatalf("Failed to build profileV1ToV2 migration: %v", err) + } // Skill migration: name ↔ skill_name, add/remove level - skillV1ToV2 := epoch.NewVersionChangeBuilder(v1, v2). + skillV1ToV2, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Transform skills[].skill_name -> skills[].name, add level"). ForType(Skill{}, SkillRequest{}). RequestToNextVersion(). @@ -519,9 +528,12 @@ func main() { RenameField("name", "skill_name"). RemoveField("level"). Build() + if err != nil { + log.Fatalf("Failed to build skillV1ToV2 migration: %v", err) + } // ProfileSettings migration: theme ↔ color_theme - settingsV1ToV2 := epoch.NewVersionChangeBuilder(v1, v2). + settingsV1ToV2, err := epoch.NewVersionChangeBuilder(v1, v2). Description("Transform settings.color_theme -> settings.theme"). ForType(ProfileSettings{}, ProfileSettingsRequest{}). RequestToNextVersion(). @@ -529,6 +541,9 @@ func main() { ResponseToPreviousVersion(). RenameField("theme", "color_theme"). Build() + if err != nil { + log.Fatalf("Failed to build settingsV1ToV2 migration: %v", err) + } // Create Epoch instance with all types and migrations epochInstance, err := epoch.NewEpoch().