api-tools-0.11.0.0: tests/Data/API/Test/UnionMigration.hs
{-# 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" "TypeSwapUnionAltMigration" "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 :: TypeSwapUnionAltMigration -> 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 TypeSwapUnionAltMigration TypeSwapFieldMigration
typeSwapMigration = CustomMigrations
{ databaseMigration = \ _ -> noDataChanges
, databaseMigrationSchema = \ _ -> noSchemaChanges
, typeMigration = \ _ -> noDataChanges
, typeMigrationSchema = \ _ -> noSchemaChanges
, unionAltMigration = migratePersonV1ToV2
, 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
]