warlock-0.1.0.0: src/Warlock/Tutorial.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE OverloadedLabels #-}
-- |
-- Module : Warlock.Tutorial
-- Description : Comprehensive tutorial for the Warlock library
-- License : MIT
-- Maintainer : ian@iankduncan.com
-- Stability : experimental
--
-- = Welcome to Warlock!
--
-- Warlock is a Template Haskell library for automatic type-safe mapping between
-- Haskell data types. It builds on the @witch@ library to provide compile-time
-- verified conversions with flexible configuration options.
--
-- This tutorial will guide you from basic usage to advanced techniques, showing
-- you how to leverage Warlock's features for real-world scenarios.
--
-- == Table of Contents
--
-- 1. "Warlock.Tutorial#g:intro" - Introduction & Core Concepts
-- 2. "Warlock.Tutorial#g:basics" - Getting Started (Basic Examples)
-- 3. "Warlock.Tutorial#g:naming" - Naming Conventions
-- 4. "Warlock.Tutorial#g:fields" - Advanced Field Manipulation
-- 5. "Warlock.Tutorial#g:constructors" - Constructor Mapping
-- 6. "Warlock.Tutorial#g:tweak" - Type Generation with Warlock.Tweak
-- 7. "Warlock.Tutorial#g:hkd" - Higher-Kinded Data with Warlock.HKD
-- 8. "Warlock.Tutorial#g:realworld" - Real-World Patterns
-- 9. "Warlock.Tutorial#g:advanced" - Advanced Techniques
-- 10. "Warlock.Tutorial#g:troubleshooting" - Troubleshooting Guide
--
-- = Introduction & Core Concepts #intro#
--
-- == What is Warlock?
--
-- Warlock generates 'Witch.From' and 'Witch.TryFrom' instances between data types,
-- eliminating boilerplate conversion code while maintaining type safety. Unlike
-- manual instances or generic deriving, Warlock gives you:
--
-- * __Compile-time field name checking__: Typos caught at compile time
-- * __Flexible mapping strategies__: Structural or semantic matching
-- * __Powerful field rules__: Computed fields, virtual fields, defaults
-- * __Type generation__: Create DTOs and HKD types automatically
--
-- == When to Use Warlock
--
-- Warlock excels at:
--
-- * API versioning (V1 → V2 → V3 migrations)
-- * Database to domain model conversions
-- * Creating DTOs for web APIs
-- * Form validation with HKD patterns
-- * Protocol buffer conversions
-- * Multi-layer architecture with distinct types per layer
--
-- == The witch Library
--
-- Warlock builds on @witch@, which provides two type classes:
--
-- * 'Witch.From' - Total conversions that always succeed
-- * 'Witch.TryFrom' - Partial conversions that may fail
--
-- Once Warlock generates instances, you use @witch@'s conversion functions:
--
-- @
-- -- Total conversion
-- employee = 'Witch.from' person :: Employee
--
-- -- Fallible conversion
-- result = 'Witch.tryFrom' text :: Either (TryFromException Text Int) Int
-- @
--
-- == ByPosition vs ByName: Decision Tree
--
-- Warlock offers two matching strategies:
--
-- +------------------+------------------------------------------------+
-- | ByPosition | Structural matching by declaration order |
-- +------------------+------------------------------------------------+
-- | Use when: | - Types have same shape, different names |
-- | | - Converting between library types (Either, |
-- | | Maybe, tuples) |
-- | | - No field naming conventions |
-- +------------------+------------------------------------------------+
-- | ByName Config | Semantic matching with configurable rules |
-- +------------------+------------------------------------------------+
-- | Use when: | - Field names follow conventions |
-- | | - Need field evolution (add/remove fields) |
-- | | - Require computed or virtual fields |
-- | | - Multi-constructor ADTs with naming patterns |
-- +------------------+------------------------------------------------+
--
-- __Rule of thumb:__ Start with 'ByName' for record types, use 'ByPosition'
-- for positional constructors or when converting standard library types.
--
-- = Getting Started (Basic Examples) #basics#
--
-- == Simple Record Mapping
--
-- The most common case: two records with the same field names.
--
-- @
-- {-\# LANGUAGE TemplateHaskell \#-}
-- {-\# LANGUAGE MultiParamTypeClasses \#-}
--
-- import Warlock
-- import qualified Witch as W
--
-- data Person = Person { name :: String, age :: Int }
-- data Employee = Employee { name :: String, age :: Int }
--
-- -- Generate From instance: Person → Employee
-- 'deriveAutomap' ('ByName' 'defaultConfig') ''Person ''Employee
--
-- example :: Employee
-- example = W.from (Person \"Alice\" 30)
-- @
--
-- The generated instance matches fields by name (exact match required).
--
-- == Positional Constructor Mapping
--
-- For types without field names, use 'ByPosition':
--
-- @
-- data Point2D = Point2D Int Int
-- data Vector2D = Vector2D Int Int
--
-- -- Match by position: 1st → 1st, 2nd → 2nd
-- 'deriveAutomap' 'ByPosition' ''Point2D ''Vector2D
--
-- example :: Vector2D
-- example = W.from (Point2D 3 4) -- Vector2D 3 4
-- @
--
-- == Adding Default Values
--
-- When the destination type has fields not present in the source:
--
-- @
-- data PersonV1 = PersonV1 { personName :: String }
-- data PersonV2 = PersonV2
-- { personName :: String
-- , personAge :: Int -- New field!
-- , personRegion :: String -- Another new field!
-- }
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'datatypePrefixConfig'
-- \`'withDefaults'\`
-- [ ('personAge, [| 0 :: Int |])
-- , ('personRegion, [| \"US\" :: String |])
-- ])
-- ''PersonV1
-- ''PersonV2
--
-- example :: PersonV2
-- example = W.from (PersonV1 \"Bob\")
-- -- PersonV2 { personName = \"Bob\", personAge = 0, personRegion = \"US\" }
-- @
--
-- == Basic Field Renaming
--
-- When field names differ between types:
--
-- @
-- data Source = Source
-- { oldName :: String
-- , oldBalance :: Double
-- }
--
-- data Dest = Dest
-- { newName :: String
-- , newBalance :: Double
-- }
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withRenames'\`
-- [ ('newName, 'oldName)
-- , ('newBalance, 'oldBalance)
-- ])
-- ''Source
-- ''Dest
-- @
--
-- = Naming Conventions #naming#
--
-- Haskell code often follows naming conventions. Warlock provides pre-built
-- configurations for common patterns.
--
-- == Datatype Prefix Convention
--
-- The most common pattern: fields prefixed with the type name.
--
-- @
-- data Person = Person
-- { personName :: String
-- , personAge :: Int
-- }
--
-- data Employee = Employee
-- { employeeName :: String
-- , employeeAge :: Int
-- }
--
-- -- Automatically strips \"person\" and adds \"employee\"
-- 'deriveAutomap' ('ByName' 'datatypePrefixConfig') ''Person ''Employee
-- @
--
-- How it works:
--
-- 1. Strip source type prefix: @personName@ → @Name@
-- 2. Add destination type prefix: @Name@ → @employeeName@
-- 3. Case handled automatically: @Person@ → @person@, @Employee@ → @employee@
--
-- == Constructor Prefix Convention
--
-- Less common but still used: fields prefixed with constructor name.
--
-- @
-- data Result
-- = Success { successValue :: String, successTime :: Int }
-- | Failure { failureError :: String, failureTime :: Int }
--
-- data Response
-- = Ok { okValue :: String, okTime :: Int }
-- | Err { errError :: String, errTime :: Int }
--
-- 'deriveAutomap'
-- ('ByName' $ 'constructorPrefixConfig' \`'withConstructorMap'\`
-- [ ('Success, 'Ok)
-- , ('Failure, 'Err)
-- ])
-- ''Result
-- ''Response
-- @
--
-- == Snake Case ↔ Camel Case
--
-- For interfacing with databases or JSON APIs:
--
-- @
-- data DbUser = DbUser
-- { user_name :: String
-- , user_email :: String
-- , created_at :: String
-- }
--
-- data User = User
-- { userName :: String
-- , userEmail :: String
-- , createdAt :: String
-- }
--
-- 'deriveAutomap' ('ByName' 'snakeToCamelConfig') ''DbUser ''User
-- @
--
-- == Custom Normalization
--
-- Build your own normalization function:
--
-- @
-- import Data.Char (toUpper)
--
-- -- Remove \"internal\" prefix and uppercase first letter
-- customNormalize :: String -> String
-- customNormalize s = case 'Warlock.stripPrefix' \"internal\" s of
-- Just rest -> case rest of
-- (c:cs) -> toUpper c : cs
-- [] -> []
-- Nothing -> s
--
-- data Internal = Internal { internalData :: String }
-- data Public = Public { data_ :: String }
--
-- 'deriveAutomap'
-- ('ByName' $ 'defaultConfig'
-- \`'withNormalize'\` customNormalize
-- \`'withRenames'\` [('data_, 'internalData)])
-- ''Internal
-- ''Public
-- @
--
-- = Advanced Field Manipulation #fields#
--
-- Warlock provides three powerful mechanisms for complex field transformations:
--
-- * __Virtual fields__: Runtime computation via 'GHC.Records.HasField'
-- * __Computed fields__: Compile-time combination of multiple sources
-- * __Disassembled fields__: Splitting one source into multiple destinations
--
-- == Virtual Fields with HasField
--
-- Virtual fields use GHC's @HasField@ mechanism to access computed properties
-- that don't exist as actual fields.
--
-- @
-- {-\# LANGUAGE DataKinds \#-}
-- {-\# LANGUAGE OverloadedLabels \#-}
--
-- import GHC.Records (HasField(..))
--
-- data Person = Person
-- { firstName :: String
-- , lastName :: String
-- , age :: Int
-- }
--
-- -- Define virtual field at runtime
-- instance HasField \"fullName\" Person String where
-- getField (Person first last _) = first ++ \" \" ++ last
--
-- instance HasField \"ageGroup\" Person String where
-- getField (Person _ _ a) = if a < 18 then \"minor\" else \"adult\"
--
-- data Employee = Employee
-- { empName :: String
-- , empAge :: Int
-- , category :: String
-- }
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withRules'\`
-- [ 'virtualField' 'empName #fullName -- OverloadedLabels syntax
-- , 'rename' 'empAge 'age
-- , 'virtualField' 'category \"ageGroup\" -- String syntax
-- ])
-- ''Person
-- ''Employee
-- @
--
-- __When to use virtual fields:__
--
-- * Source field is computed at runtime (not a real field)
-- * Single source, single destination
-- * Logic lives in @HasField@ instance
--
-- == Computed Fields (Combining Multiple Fields)
--
-- Combine multiple source fields at compile time using Template Haskell:
--
-- @
-- data Address = Address
-- { street :: String
-- , city :: String
-- , country :: String
-- }
--
-- data ShortAddress = ShortAddress
-- { location :: String
-- }
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withRules'\`
-- [ 'combineFields' 'location $ do
-- street <- 'Warlock.get' 'street
-- city <- 'Warlock.get' 'city
-- country <- 'Warlock.get' 'country
-- pure [| \$street ++ \", \" ++ \$city ++ \", \" ++ \$country |]
-- ])
-- ''Address
-- ''ShortAddress
-- @
--
-- The 'Warlock.FieldGetter' monad provides:
--
-- * 'Warlock.get' - Extract field as Template Haskell expression
-- * 'Warlock.getName' - Extract field as 'Language.Haskell.TH.Name' (advanced)
-- * Compile-time checking: references to missing fields are caught
--
-- __When to use computed fields:__
--
-- * Multiple sources, single destination
-- * Compile-time transformation
-- * Need to combine or calculate from multiple fields
--
-- == Disassembled Fields (Splitting One Into Many)
--
-- Split one source field into multiple destination fields:
--
-- @
-- data FullName = FullName { fullName :: String }
-- data SplitName = SplitName
-- { firstName :: String
-- , lastName :: String
-- }
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withRules'\`
-- ('disassembleFields' 'fullName
-- [ 'firstName 'Warlock..=' do
-- src <- 'Warlock.getSource'
-- pure [| case words \$src of (f:_) -> f; _ -> \"\" |]
-- , 'lastName 'Warlock..=' do
-- src <- 'Warlock.getSource'
-- pure [| case words \$src of (_:l:_) -> l; _ -> \"\" |]
-- ]))
-- ''FullName
-- ''SplitName
-- @
--
-- The 'Warlock.DisassembleGetter' monad provides:
--
-- * 'Warlock.getSource' - Get source field as TH expression
-- * 'Warlock.getSourceName' - Get source field as 'Language.Haskell.TH.Name'
--
-- __When to use disassembled fields:__
--
-- * Single source, multiple destinations
-- * Parsing or splitting structured data
-- * Extracting components from composite values
--
-- == Field Rule Composition
--
-- Rules can be composed using the builder pattern:
--
-- @
-- import Data.Function ((&))
--
-- -- Explicit builder style
-- rule1 = 'Warlock.field' 'balance & 'Warlock.fromField' 'balance_cents
-- rule2 = 'Warlock.field' 'region & 'Warlock.withDefault' [| \"US\" |]
--
-- -- Pre-composed helpers (more common)
-- rule3 = 'rename' 'newName 'oldName
-- rule4 = 'defaultTo' 'region [| \"US\" |]
-- @
--
-- = Constructor Mapping #constructors#
--
-- For multi-constructor ADTs (sum types), Warlock can map constructor names
-- with compile-time safety.
--
-- == Direct Constructor Mapping
--
-- Explicit constructor name mappings with compile-time checking:
--
-- @
-- data ShapeV1
-- = CircleV1 { radius :: Double }
-- | RectangleV1 { width :: Double, height :: Double }
--
-- data ShapeV2
-- = Circle { radius :: Double }
-- | Rectangle { width :: Double, height :: Double }
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withConstructorMap'\`
-- [ ('CircleV1, 'Circle)
-- , ('RectangleV1, 'Rectangle)
-- ])
-- ''ShapeV1
-- ''ShapeV2
-- @
--
-- The Template Haskell names @'CircleV1@ and @'Circle@ are checked at compile
-- time, so typos are caught immediately.
--
-- == Suffix Transformations
--
-- Common pattern: add or remove version suffixes.
--
-- @
-- data PaymentV1
-- = CardPaymentV1 { cardNumber :: String }
-- | CashPaymentV1 { amount :: Double }
--
-- data PaymentV2
-- = CardPaymentV2 { cardNumber :: String, cvv :: String }
-- | CashPaymentV2 { amount :: Double, currency :: String }
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withConstructorMap'\` 'replaceSuffix' \"V1\" \"V2\"
-- \`'withDefaults'\`
-- [ ('cvv, [| \"000\" |])
-- , ('currency, [| \"USD\" |])
-- ])
-- ''PaymentV1
-- ''PaymentV2
-- @
--
-- Available helpers:
--
-- * 'stripSuffix' @\"V1\"@ - Remove suffix: @FooV1@ → @Foo@
-- * 'addSuffix' @\"V2\"@ - Add suffix: @Foo@ → @FooV2@
-- * 'replaceSuffix' @\"V1\" \"V2\"@ - Replace: @FooV1@ → @FooV2@
--
-- == Custom Constructor Transformations
--
-- For complex patterns, provide a transformation function:
--
-- @
-- import Data.Char (toUpper)
--
-- uppercaseFirst :: String -> String
-- uppercaseFirst [] = []
-- uppercaseFirst (c:cs) = toUpper c : cs
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withConstructorMap'\` 'Warlock.Transform' uppercaseFirst)
-- ''Source
-- ''Dest
-- @
--
-- Or use a lambda directly (wrapped in 'Warlock.Transform'):
--
-- @
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withConstructorMap'\` 'Warlock.Transform' (++ \"_New\"))
-- ''Old
-- ''New
-- @
--
-- == Multi-Constructor ADTs with Field Evolution
--
-- Combining constructor mapping with field defaults:
--
-- @
-- data EventV1
-- = UserLoginV1 { userId :: Int, loginTime :: String }
-- | UserLogoutV1 { userId :: Int }
-- | ErrorOccurredV1 { errorMsg :: String }
--
-- data EventV2
-- = UserLoginV2 { userId :: Int, loginTime :: String, ipAddress :: String }
-- | UserLogoutV2 { userId :: Int, sessionDuration :: Int }
-- | ErrorOccurredV2 { errorMsg :: String, errorCode :: Int }
--
-- 'deriveAutomap'
-- ('ByName' $
-- 'defaultConfig'
-- \`'withConstructorMap'\` 'replaceSuffix' \"V1\" \"V2\"
-- \`'withDefaults'\`
-- [ ('ipAddress, [| \"unknown\" |])
-- , ('sessionDuration, [| 0 |])
-- , ('errorCode, [| 500 |])
-- ])
-- ''EventV1
-- ''EventV2
-- @
--
-- = Type Generation with Warlock.Tweak #tweak#
--
-- @Warlock.Tweak@ generates new data types from existing ones, inspired by
-- TypeScript's utility types like @Pick\<T, K\>@ and @Omit\<T, K\>@.
--
-- == Pick: Selecting Specific Fields
--
-- Create a type with only selected fields:
--
-- @
-- import qualified Warlock.Tweak as Tweak
-- import Warlock.Tweak (pick)
--
-- data User = User
-- { userId :: Int
-- , userName :: String
-- , userEmail :: String
-- , userPassword :: String
-- , userCreatedAt :: String
-- }
--
-- -- Pick only public fields (TypeScript: Pick\<User, \"id\" | \"name\"\>)
-- pick ''User \"UserSummary\" ['userId, 'userName]
--
-- -- Generates:
-- -- data UserSummary = UserSummary
-- -- { userId :: Int
-- -- , userName :: String
-- -- }
-- -- instance From User UserSummary
-- @
--
-- == Omit: Excluding Specific Fields
--
-- Create a type excluding sensitive fields:
--
-- @
-- import Warlock.Tweak (omit)
--
-- -- Omit sensitive fields (TypeScript: Omit\<User, \"password\"\>)
-- omit ''User \"UserResponse\" ['userPassword]
--
-- -- Generates:
-- -- data UserResponse = UserResponse
-- -- { userId :: Int
-- -- , userName :: String
-- -- , userEmail :: String
-- -- , userCreatedAt :: String
-- -- }
-- -- instance From User UserResponse
-- @
--
-- == Prefix Operations with Tweak
--
-- Combine field selection with prefix transformations:
--
-- @
-- -- Strip \"user\" prefix from field names
-- pick ''User \"UserClean\" ['userId, 'userName, 'userEmail]
-- \`Tweak.stripPrefix\` \"user\"
--
-- -- Generates:
-- -- data UserClean = UserClean
-- -- { id :: Int
-- -- , name :: String
-- -- , email :: String
-- -- }
-- @
--
-- Available prefix operations:
--
-- * @\`Tweak.stripPrefix\` \"user\"@ - Remove prefix
-- * @\`Tweak.addPrefix\` \"dto\"@ - Add prefix
-- * @\`Tweak.replacePrefix\` (\"user\", \"api\")@ - Replace prefix
--
-- == Combining with Field Rules
--
-- @Warlock.Tweak@ supports all Warlock field rules:
--
-- @
-- import Warlock.Tweak (withRules)
--
-- pick ''Person \"PersonDTO\" ['fullName, 'age]
-- \`withRules\`
-- [ combineFields 'fullName $ do
-- first <- Warlock.get 'firstName
-- last <- Warlock.get 'lastName
-- pure [| \$first ++ \" \" ++ \$last |]
-- , rename 'age 'personAge
-- ]
-- @
--
-- == API Response Types
--
-- Common pattern: create response DTOs for web APIs:
--
-- @
-- data DbUser = DbUser
-- { dbUserId :: Int
-- , dbUserName :: String
-- , dbUserEmail :: String
-- , dbUserPasswordHash :: ByteString
-- , dbUserSalt :: ByteString
-- , dbUserCreatedAt :: UTCTime
-- , dbUserUpdatedAt :: UTCTime
-- }
--
-- -- Public API response (omit internal fields, strip prefix)
-- omit ''DbUser \"UserResponse\"
-- [ 'dbUserPasswordHash
-- , 'dbUserSalt
-- ]
-- \`Tweak.stripPrefix\` \"dbUser\"
--
-- -- Generates:
-- -- data UserResponse = UserResponse
-- -- { id :: Int
-- -- , name :: String
-- -- , email :: String
-- -- , createdAt :: UTCTime
-- -- , updatedAt :: UTCTime
-- -- }
-- @
--
-- = Higher-Kinded Data with Warlock.HKD #hkd#
--
-- @Warlock.HKD@ generates Higher-Kinded Data types where each field is wrapped
-- in a type constructor @f@. This enables powerful patterns for validation,
-- partial construction, and generic programming.
--
-- == Basic HKD Generation
--
-- @
-- {-\# LANGUAGE TemplateHaskell \#-}
-- {-\# LANGUAGE TypeFamilies \#-}
--
-- import Warlock.HKD
-- import Data.Functor.Identity
-- import Witch (from)
--
-- data Person = Person
-- { name :: String
-- , age :: Int
-- , email :: String
-- }
--
-- 'Warlock.HKD.deriveHKD'' ''Person
--
-- -- Generates:
-- -- data Person' f = Person'
-- -- { name :: f String
-- -- , age :: f Int
-- -- , email :: f String
-- -- }
-- -- type instance HKD Person f = Person' f
-- -- instance From (Person' Identity) Person
-- -- instance From Person (Person' Identity)
-- @
--
-- == Identity Wrapper Conversion
--
-- The primary use case: wrap and unwrap values in 'Data.Functor.Identity':
--
-- @
-- -- Wrap in Identity
-- let person = Person \"Alice\" 30 \"alice\@example.com\"
-- let hkdPerson = from person :: HKD Person Identity
--
-- -- Unwrap from Identity
-- let unwrapped = from hkdPerson :: Person
-- @
--
-- == Form Validation with Maybe
--
-- Build up values incrementally with 'Maybe', then validate:
--
-- @
-- -- Form data with optional fields
-- let formData = Person' (Just \"Bob\") Nothing (Just \"bob\@example.com\")
-- :: Person' Maybe
--
-- -- Validate: all fields must be present
-- validatePerson :: Person' Maybe -> Maybe Person
-- validatePerson (Person' (Just n) (Just a) (Just e)) = Just (Person n a e)
-- validatePerson _ = Nothing
--
-- case validatePerson formData of
-- Just person -> putStrLn \"Valid!\"
-- Nothing -> putStrLn \"Missing fields\"
-- @
--
-- == Integration with Barbies
--
-- Warlock.HKD generates types compatible with the @barbies@ library for
-- generic traversals:
--
-- @
-- {-\# LANGUAGE DeriveGeneric \#-}
-- {-\# LANGUAGE StandaloneDeriving \#-}
--
-- import qualified Barbies as B
-- import GHC.Generics (Generic)
--
-- data Person = Person
-- { name :: String
-- , age :: Int
-- }
--
-- deriveHKD' ''Person
--
-- deriving instance Generic (Person' f)
-- instance B.FunctorB Person'
-- instance B.TraversableB Person'
-- instance B.ApplicativeB Person'
--
-- -- Now use barbies operations
-- validateForm :: Person' Maybe -> Either [String] Person
-- validateForm p = case B.btraverse (maybe (Left [\"missing\"]) Right) p of
-- Left errs -> Left errs
-- Right identityPerson -> Right (from identityPerson)
-- @
--
-- == Custom HKD Configuration
--
-- Customize field and constructor naming:
--
-- @
-- -- Add \"hkd\" prefix to fields
-- deriveHKD (defaultHKDConfig \`withFieldPrefix\` \"hkd\") ''User
--
-- -- Use \"HKD\" suffix for constructor
-- deriveHKD (defaultHKDConfig \`withConstructorSuffix\` \"HKD\") ''Person
--
-- -- Generates: PersonHKD instead of Person'
-- @
--
-- = Real-World Patterns #realworld#
--
-- == API Versioning (V1 → V2 → V3)
--
-- Gracefully evolve your API over multiple versions:
--
-- @
-- -- Version 1: Initial release
-- data UserV1 = UserV1
-- { userV1Name :: String
-- , userV1Email :: String
-- }
--
-- -- Version 2: Added age field
-- data UserV2 = UserV2
-- { userV2Name :: String
-- , userV2Email :: String
-- , userV2Age :: Int
-- }
--
-- -- Version 3: Split name into first/last
-- data UserV3 = UserV3
-- { userV3FirstName :: String
-- , userV3LastName :: String
-- , userV3Email :: String
-- , userV3Age :: Int
-- }
--
-- -- V1 → V2: Add default age
-- deriveAutomap
-- (ByName $ datatypePrefixConfig
-- \`withDefaults\` [('userV2Age, [| 0 |])])
-- ''UserV1 ''UserV2
--
-- -- V2 → V3: Split name, keep other fields
-- deriveAutomap
-- (ByName $ datatypePrefixConfig
-- \`withRules\`
-- (disassembleFields 'userV2Name
-- [ 'userV3FirstName .= do
-- src <- getSource
-- pure [| case words \$src of (f:_) -> f; _ -> \"\" |]
-- , 'userV3LastName .= do
-- src <- getSource
-- pure [| case words \$src of (_:l:_) -> l; _ -> \"\" |]
-- ]))
-- ''UserV2 ''UserV3
--
-- -- Now you can chain conversions
-- upgradeToLatest :: UserV1 -> UserV3
-- upgradeToLatest v1 = from (from v1 :: UserV2)
-- @
--
-- == Database to Domain Model Conversion
--
-- Separate persistence layer from domain logic:
--
-- @
-- -- Persistent-generated database entity
-- data DbProduct = DbProduct
-- { dbProductId :: Int
-- , dbProductName :: String
-- , dbProductPriceCents :: Int
-- , dbProductCreatedAt :: UTCTime
-- }
--
-- -- Clean domain model
-- data Product = Product
-- { productId :: Int
-- , productName :: String
-- , productPrice :: Double -- Different type!
-- }
--
-- deriveAutomap
-- (ByName $ customPrefixRules \"product\" \"dbProduct\"
-- \`withRules\`
-- [ mapField 'productPrice $ \\src _dst ->
-- [| fromIntegral (dbProductPriceCents \$(varE src)) / 100.0 |]
-- ])
-- ''DbProduct ''Product
-- @
--
-- == Request/Response DTOs
--
-- Create separate types for API requests and responses:
--
-- @
-- -- Internal domain model
-- data User = User
-- { userId :: Int
-- , userName :: String
-- , userEmail :: String
-- , userPasswordHash :: ByteString
-- , userCreatedAt :: UTCTime
-- }
--
-- -- API request (no ID, no timestamps)
-- pick ''User \"CreateUserRequest\" ['userName, 'userEmail, 'userPasswordHash]
-- \`Tweak.stripPrefix\` \"user\"
--
-- -- API response (no password)
-- omit ''User \"UserResponse\" ['userPasswordHash]
-- \`Tweak.stripPrefix\` \"user\"
--
-- -- Generates:
-- -- data CreateUserRequest = CreateUserRequest
-- -- { name :: String
-- -- , email :: String
-- -- , passwordHash :: ByteString
-- -- }
-- --
-- -- data UserResponse = UserResponse
-- -- { id :: Int
-- -- , name :: String
-- -- , email :: String
-- -- , createdAt :: UTCTime
-- -- }
-- @
--
-- == Nested Type Conversions
--
-- Warlock automatically handles nested conversions using @witch@'s @into@:
--
-- @
-- data PersonV1 = PersonV1 { age :: Int }
-- data PersonV2 = PersonV2 { age :: Integer } -- Different type
--
-- deriveAutomap (ByName defaultConfig) ''PersonV1 ''PersonV2
--
-- -- Requires: instance From Int Integer (provided by witch)
--
-- -- For lists and other containers:
-- data TeamV1 = TeamV1 { members :: [PersonV1] }
-- data TeamV2 = TeamV2 { members :: [PersonV2] }
--
-- deriveAutomap (ByName defaultConfig) ''TeamV1 ''TeamV2
--
-- -- Requires: instance From PersonV1 PersonV2 (derived above)
-- -- Warlock uses witch's into to convert the list elements
-- @
--
-- == Multi-Step Transformations
--
-- Chain multiple transformations for complex scenarios:
--
-- @
-- -- Step 1: Generate DTO with Tweak
-- omit ''User \"UserDTO\" ['userPassword]
-- \`Tweak.stripPrefix\` \"user\"
--
-- -- Step 2: Generate HKD for validation
-- deriveHKD' ''UserDTO
--
-- -- Step 3: Use in form validation
-- validateUserForm :: UserDTO' Maybe -> Either [String] User
-- validateUserForm formData = do
-- -- Validate with barbies
-- dto <- btraverse (maybe (Left [\"missing\"]) Right) formData
-- -- Add back password (from separate secure input)
-- password <- getPasswordSecurely
-- pure $ User
-- { userId = id (from dto)
-- , userName = name (from dto)
-- , userEmail = email (from dto)
-- , userPassword = password
-- , userCreatedAt = now
-- }
-- @
--
-- = Advanced Techniques #advanced#
--
-- == Combining Tweak + HKD
--
-- Generate DTOs and their HKD versions for form handling:
--
-- @
-- data User = User
-- { userId :: Int
-- , userName :: String
-- , userEmail :: String
-- , userPassword :: String
-- }
--
-- -- Step 1: Create DTO (omit ID)
-- omit ''User \"UserForm\" ['userId]
-- \`Tweak.stripPrefix\` \"user\"
--
-- -- Step 2: Generate HKD for partial form data
-- deriveHKD' ''UserForm
--
-- -- Usage:
-- emptyForm :: UserForm' Maybe
-- emptyForm = UserForm' Nothing Nothing Nothing
--
-- fillName :: String -> UserForm' Maybe -> UserForm' Maybe
-- fillName n form = form { name = Just n }
-- @
--
-- == Custom Rule Generators
--
-- Create reusable field rule generators for your domain:
--
-- @
-- -- Rule generator for monetary fields (cents to dollars)
-- moneyFieldRules :: RuleGenContext -> String -> Maybe FieldRule
-- moneyFieldRules _ctx dstField
-- | \"price\" \`isPrefixOf\` dstField = Just $ mapField (mkName dstField) $
-- \\src _dst -> [| fromIntegral \$(varE src) / 100.0 |]
-- | \"cost\" \`isPrefixOf\` dstField = Just $ mapField (mkName dstField) $
-- \\src _dst -> [| fromIntegral \$(varE src) / 100.0 |]
-- | otherwise = Nothing
--
-- deriveAutomap
-- (ByName $ defaultConfig
-- \`withRuleGens\` [datatypePrefixRules, moneyFieldRules])
-- ''DbProduct ''Product
-- @
--
-- == Parameterized Types
--
-- Handle types with type parameters:
--
-- @
-- data Container a = Container { value :: a }
-- data Box a = Box { contents :: a }
--
-- -- Use deriveAutomapWith with concrete types
-- deriveAutomapWith (ByName defaultConfig)
-- [t| Container Int |]
-- [t| Box Int |]
--
-- -- Or derive for polymorphic case (if structures match)
-- deriveAutomap (ByName defaultConfig)
-- ''Container ''Box
-- @
--
-- == Performance Considerations
--
-- Warlock generates code at compile time, so there's zero runtime overhead
-- beyond the conversion logic itself. However, keep these in mind:
--
-- * __Compile times__: Large types or many derivations increase TH compile time
-- * __Code size__: Each derivation generates instance code (negligible impact)
-- * __Field rules__: Computed fields add expression complexity
-- * __Nested conversions__: Deep nesting may impact conversion performance
--
-- Tips for large codebases:
--
-- * Group related derivations in separate modules
-- * Use @ByPosition@ for simple positional types (simpler generated code)
-- * Profile if performance is a concern (usually not an issue)
--
-- = Troubleshooting Guide #troubleshooting#
--
-- == Common Compile Errors
--
-- === \"No instance for From ...\"
--
-- __Problem:__ Warlock uses @witch@'s @into@ for nested conversions.
--
-- @
-- -- Error: No instance for From Int String
-- data A = A { field :: Int }
-- data B = B { field :: String }
-- deriveAutomap (ByName defaultConfig) ''A ''B
-- @
--
-- __Solution:__ Provide the missing From instance or use a field rule:
--
-- @
-- deriveAutomap
-- (ByName $ defaultConfig
-- \`withRules\`
-- [ mapField 'field $ \\src _dst -> [| show \$(varE src) |]
-- ])
-- ''A ''B
-- @
--
-- === \"Field not found in source type\"
--
-- __Problem:__ Destination field has no corresponding source field.
--
-- __Solution:__ Add a default value or compute it:
--
-- @
-- deriveAutomap
-- (ByName $ defaultConfig \`withDefaults\` [('newField, [| defaultValue |])])
-- ''Source ''Dest
-- @
--
-- === \"Constructor count mismatch\"
--
-- __Problem:__ ByPosition requires same number of constructors.
--
-- __Solution:__ Use ByName instead, or ensure constructor counts match.
--
-- === \"Cannot mix record and positional constructors\"
--
-- __Problem:__ One type uses records, the other uses positional constructors.
--
-- __Solution:__ Both types must use the same constructor style (or use ByPosition
-- for both if they're positional).
--
-- == Debugging Generated Code
--
-- To see what code Warlock generates, use GHC's @-ddump-splices@ flag:
--
-- @
-- stack build --ghc-options=-ddump-splices
-- cabal build --ghc-options=-ddump-splices
-- @
--
-- This shows the full generated instance code, helpful for understanding
-- compilation errors.
--
-- == Type Inference Issues
--
-- Sometimes GHC needs type annotations when using @from@:
--
-- @
-- -- Ambiguous:
-- let x = from myValue
--
-- -- Fixed with type annotation:
-- let x = from myValue :: TargetType
--
-- -- Or using TypeApplications:
-- let x = from \@SourceType \@TargetType myValue
-- @
--
-- == When to Use Qualified Imports
--
-- Some Warlock names might conflict with your code:
--
-- @
-- -- Recommended for Tweak (many common names)
-- import qualified Warlock.Tweak as Tweak
--
-- -- Use: Tweak.pick, Tweak.omit, Tweak.stripPrefix
--
-- -- Main Warlock module usually doesn't need qualification
-- import Warlock
-- @
--
-- = Conclusion
--
-- You now have a comprehensive understanding of Warlock's capabilities!
--
-- __Key Takeaways:__
--
-- * Use 'ByName' for records, 'ByPosition' for positional types
-- * Leverage pre-built configs: 'datatypePrefixConfig', 'snakeToCamelConfig'
-- * Virtual fields for runtime computation, computed fields for compile-time
-- * @Warlock.Tweak@ for generating DTOs (@pick@/@omit@)
-- * @Warlock.HKD@ for validation and partial construction
-- * Combine features for powerful real-world patterns
--
-- __Next Steps:__
--
-- * Explore the main "Warlock" module documentation
-- * Check out "Warlock.Tweak" for DTO generation
-- * Learn about "Warlock.HKD" for HKD patterns
-- * Read the README for quick reference
--
-- Happy mapping! 🧙
module Warlock.Tutorial () where
-- This module is documentation-only. All exports are from Warlock, Warlock.Tweak, and Warlock.HKD.