-
Notifications
You must be signed in to change notification settings - Fork 2
Support migrating individual alternatives in unions #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| {-# LANGUAGE OverloadedStrings #-} | ||
| {-# LANGUAGE TemplateHaskell #-} | ||
|
|
||
| -- | Tests for union alternative migration with type changes | ||
| -- | ||
| -- This module tests the 'alternative changed' changelog feature, which allows | ||
| -- changing the type of a union alternative with a custom migration function. | ||
| module Data.API.Test.UnionMigration | ||
| ( unionMigrationTests | ||
| ) where | ||
|
|
||
| import Data.API.Changes | ||
| import Data.API.JSON | ||
| import Data.API.JSON.Compat | ||
| import Data.API.Types | ||
| import Data.API.Utils | ||
|
|
||
| import qualified Data.Aeson as JS | ||
| import qualified Data.Aeson.Encode.Pretty as JS | ||
| import qualified Data.ByteString.Lazy.Char8 as BL | ||
| import qualified Data.Text as T | ||
| import Data.Version | ||
| import Test.Tasty | ||
| import Test.Tasty.HUnit | ||
|
|
||
| import Data.API.Test.UnionMigrationData | ||
|
|
||
|
|
||
| -- Generate migration enums from changelog | ||
| $(generateMigrationKinds typeSwapChangelog "TypeSwapDbMigration" "TypeSwapRecordMigration" "TypeSwapFieldMigration") | ||
|
|
||
|
|
||
| -- ----------------------------------------------------------------------------- | ||
| -- Type Swap Migration (PersonV1 -> PersonV2) | ||
| -- ----------------------------------------------------------------------------- | ||
|
|
||
| -- | Migrate PersonV1 to PersonV2 | ||
| -- | ||
| -- PersonV1: { "name": "John" } | ||
| -- PersonV2: { "fullName": "John", "age": 0 } | ||
| -- | ||
| -- This is a type migration because we're transforming the entire inner value | ||
| -- of the union alternative from one type to another. | ||
| migratePersonV1ToV2 :: TypeSwapRecordMigration -> JS.Value -> Either ValueError JS.Value | ||
| migratePersonV1ToV2 MigratePersonV1ToV2 (JS.Object obj) = do | ||
| nameVal <- lookupKey "name" obj ?! CustomMigrationError "missing 'name' field" (JS.Object obj) | ||
| case nameVal of | ||
| JS.String name -> return $ JS.Object $ | ||
| insertKey "fullName" (JS.String name) $ | ||
| singletonObject "age" (JS.Number 0) | ||
| _ -> Left $ CustomMigrationError "expected string for 'name'" (JS.Object obj) | ||
| migratePersonV1ToV2 MigratePersonV1ToV2 v = | ||
| Left $ CustomMigrationError "expected object for PersonV1" v | ||
|
|
||
|
|
||
| typeSwapMigration :: CustomMigrations JS.Object JS.Value TypeSwapDbMigration TypeSwapRecordMigration TypeSwapFieldMigration | ||
| typeSwapMigration = CustomMigrations | ||
| { databaseMigration = \ _ -> noDataChanges | ||
| , databaseMigrationSchema = \ _ -> noSchemaChanges | ||
| , typeMigration = migratePersonV1ToV2 | ||
| , typeMigrationSchema = \ _ -> noSchemaChanges | ||
| , fieldMigration = \ _ -> noDataChanges | ||
| } | ||
|
|
||
|
|
||
| -- Test data for type swap | ||
| -- | ||
| -- Start: Container with MyUnion containing PersonV1 | ||
| -- End: Container with MyUnion containing PersonV2 | ||
|
|
||
| -- | Start data: { "person": { "person": { "name": "Alice" } } } | ||
| startTypeSwapData :: JS.Value | ||
| Just startTypeSwapData = JS.decode "{ \"person\": { \"person\": { \"name\": \"Alice\" } } }" | ||
|
|
||
| -- | Expected end data: { "person": { "person": { "fullName": "Alice", "age": 0 } } } | ||
| expectedTypeSwapData :: JS.Value | ||
| Just expectedTypeSwapData = JS.decode "{ \"person\": { \"person\": { \"fullName\": \"Alice\", \"age\": 0 } } }" | ||
|
|
||
| -- | Start data with "other" alternative (should pass through unchanged) | ||
| startOtherAltData :: JS.Value | ||
| Just startOtherAltData = JS.decode "{ \"person\": { \"other\": 42 } }" | ||
|
|
||
| -- | Expected end data for "other" alternative (unchanged) | ||
| expectedOtherAltData :: JS.Value | ||
| Just expectedOtherAltData = JS.decode "{ \"person\": { \"other\": 42 } }" | ||
|
|
||
|
|
||
| -- | Test migrating PersonV1 to PersonV2 within a union | ||
| typeSwapMigrationTest :: Assertion | ||
| typeSwapMigrationTest = do | ||
| -- Verify start data matches start schema | ||
| case dataMatchesAPI rootName startTypeSwapSchema startTypeSwapData of | ||
| Right () -> return () | ||
| Left err -> assertFailure $ "Start data does not match start API: " | ||
| ++ prettyValueErrorPosition err | ||
|
|
||
| -- Verify expected end data matches end schema | ||
| case dataMatchesAPI rootName endTypeSwapSchema expectedTypeSwapData of | ||
| Right () -> return () | ||
| Left err -> assertFailure $ "Expected end data does not match end API: " | ||
| ++ prettyValueErrorPosition err | ||
|
|
||
| -- Run migration | ||
| case migrateDataDump (startTypeSwapSchema, parseVer "0") | ||
| (endTypeSwapSchema, Release (parseVer "1.0")) | ||
| typeSwapChangelog typeSwapMigration rootName CheckAll | ||
| startTypeSwapData of | ||
| Right (v, []) | ||
| | expectedTypeSwapData == v -> return () | ||
| | otherwise -> assertFailure $ unlines | ||
| [ "Type swap migration produced wrong result" | ||
| , "Expected:" | ||
| , BL.unpack (JS.encodePretty expectedTypeSwapData) | ||
| , "but got:" | ||
| , BL.unpack (JS.encodePretty v) | ||
| ] | ||
| Right (_, ws) -> assertFailure $ "Unexpected warnings: " ++ show ws | ||
| Left err -> assertFailure $ "Migration failed: " ++ prettyMigrateFailure err | ||
|
|
||
|
|
||
| -- | Test that non-matching alternatives pass through unchanged | ||
| otherAlternativeUnchangedTest :: Assertion | ||
| otherAlternativeUnchangedTest = do | ||
| -- Verify start data matches start schema | ||
| case dataMatchesAPI rootName startTypeSwapSchema startOtherAltData of | ||
| Right () -> return () | ||
| Left err -> assertFailure $ "Start data does not match start API: " | ||
| ++ prettyValueErrorPosition err | ||
|
|
||
| -- Verify expected end data matches end schema | ||
| case dataMatchesAPI rootName endTypeSwapSchema expectedOtherAltData of | ||
| Right () -> return () | ||
| Left err -> assertFailure $ "Expected end data does not match end API: " | ||
| ++ prettyValueErrorPosition err | ||
|
|
||
| -- Run migration - "other" alternative should pass through unchanged | ||
| case migrateDataDump (startTypeSwapSchema, parseVer "0") | ||
| (endTypeSwapSchema, Release (parseVer "1.0")) | ||
| typeSwapChangelog typeSwapMigration rootName CheckAll | ||
| startOtherAltData of | ||
| Right (v, []) | ||
| | expectedOtherAltData == v -> return () | ||
| | otherwise -> assertFailure $ unlines | ||
| [ "Other alternative was incorrectly modified" | ||
| , "Expected:" | ||
| , BL.unpack (JS.encodePretty expectedOtherAltData) | ||
| , "but got:" | ||
| , BL.unpack (JS.encodePretty v) | ||
| ] | ||
| Right (_, ws) -> assertFailure $ "Unexpected warnings: " ++ show ws | ||
| Left err -> assertFailure $ "Migration failed: " ++ prettyMigrateFailure err | ||
|
|
||
|
|
||
| rootName :: TypeName | ||
| rootName = TypeName "Container" | ||
|
|
||
| parseVer :: String -> Version | ||
| parseVer s = case simpleParseVersion s of | ||
| Just v -> v | ||
| Nothing -> error $ "Invalid version: " ++ s | ||
|
|
||
|
|
||
| -- | All union migration tests | ||
| unionMigrationTests :: TestTree | ||
| unionMigrationTests = testGroup "Union Alternative Migration" | ||
| [ testCase "Type swap: PersonV1 -> PersonV2" typeSwapMigrationTest | ||
| , testCase "Other alternatives pass through unchanged" otherAlternativeUnchangedTest | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| {-# LANGUAGE QuasiQuotes #-} | ||
|
|
||
| -- | Data for union alternative migration tests | ||
| -- | ||
| -- This module tests the 'alternative changed' changelog feature, which allows | ||
| -- swapping the type of a union alternative from one type to a completely | ||
| -- different type, with a custom migration function to transform the data. | ||
| module Data.API.Test.UnionMigrationData | ||
| ( -- * Type swap scenario (PersonV1 -> PersonV2) | ||
| startTypeSwapSchema | ||
| , endTypeSwapSchema | ||
| , typeSwapChangelog | ||
| ) where | ||
|
|
||
| import Data.API.Changes | ||
| import Data.API.Parse | ||
| import Data.API.Types | ||
|
|
||
|
|
||
| -- ----------------------------------------------------------------------------- | ||
| -- Type Swap Scenario | ||
| -- | ||
| -- This tests the primary use case: migrating a union alternative from one | ||
| -- type (PersonV1) to a completely different type (PersonV2). | ||
| -- | ||
| -- PersonV1 has: name :: string | ||
| -- PersonV2 has: fullName :: string, age :: integer | ||
| -- | ||
| -- The migration function transforms PersonV1 data to PersonV2 data. | ||
| -- ----------------------------------------------------------------------------- | ||
|
|
||
| -- | Initial schema with PersonV1 | ||
| startTypeSwapSchema :: API | ||
| startTypeSwapSchema = [api| | ||
|
|
||
| personV1Prefix :: PersonV1 | ||
| = record | ||
| name :: string | ||
|
|
||
| containerPrefix :: Container | ||
| = record | ||
| person :: MyUnion | ||
|
|
||
| myUnionPrefix :: MyUnion | ||
| = union | ||
| | person :: PersonV1 | ||
| | other :: integer | ||
| |] | ||
|
|
||
|
|
||
| -- | Final schema with PersonV2 and changelog | ||
| endTypeSwapSchema :: API | ||
| typeSwapChangelog :: APIChangelog | ||
| (endTypeSwapSchema, typeSwapChangelog) = [apiWithChangelog| | ||
|
|
||
| personV1Prefix :: PersonV1 | ||
| = record | ||
| name :: string | ||
|
|
||
| personV2Prefix :: PersonV2 | ||
| = record | ||
| fullName :: string | ||
| age :: integer | ||
|
|
||
| containerPrefix :: Container | ||
| = record | ||
| person :: MyUnion | ||
|
|
||
| myUnionPrefix :: MyUnion | ||
| = union | ||
| | person :: PersonV2 | ||
| | other :: integer | ||
|
|
||
| changes | ||
|
|
||
| version "1.0" | ||
| // Note: changes are processed bottom-up, so we must list the union change | ||
| // before adding the new type it references | ||
| changed union MyUnion | ||
| alternative changed person :: PersonV2 migration MigratePersonV1ToV2 | ||
| added PersonV2 record | ||
| fullName :: string | ||
| age :: integer | ||
|
|
||
| version "0" | ||
| |] |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.