addy-0.1.0.0: src/Addy/Internal/Types.hs
-- |
--
-- Copyright:
-- This file is part of the package addy. It is subject to the license
-- terms in the LICENSE file found in the top-level directory of this
-- distribution and at:
--
-- https://code.devalot.com/open/addy
--
-- No part of this package, including this file, may be copied,
-- modified, propagated, or distributed except according to the terms
-- contained in the LICENSE file.
--
-- License: BSD-2-Clause
--
-- The module exports internal types along with their constructors.
--
-- The rendering code relies on the newtype wrappers around 'Text' to
-- keep out invalid characters. Prefer to use the official interface
-- if possible.
module Addy.Internal.Types
( Error (..),
EmailAddr (..),
displayName,
localPart,
domain,
comments,
DisplayName (..),
LocalPart (..),
Domain (..),
_Domain,
_DomainLiteral,
DomainName (..),
HostName (..),
_HostNames,
AddressLiteral (..),
_IpAddressLiteral,
_TaggedAddressLiteral,
_AddressLiteral,
AddressTag (..),
Literal (..),
Comment (..),
_Comment,
commentLoc,
commentContent,
CommentLoc (..),
CommentContent (..),
)
where
import Control.Lens (Iso', Lens', Prism', iso, lens, prism')
import qualified Data.Text as Text
import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import Net.IP (IP)
import Text.Show (Show (..), showParen, showString)
-- | Potential validation errors.
--
-- @since 0.1.0.0
data Error
= -- | A component of an email address may not start with the
-- recorded prefix text.
InvalidPrefixError Text
| -- | A component of an email address may not end with the recorded
-- suffix text.
InvalidSuffixError Text
| -- | A component of an email address contains invalid characters.
InvalidCharactersError Text
| -- | A component of an email address does not meet the set length
-- requirements. The values in this constructor are @min@, @max@,
-- and @actual@.
InvalidLengthError Int Int Int
| -- | The input to the address decoder was not a valid email
-- address and produced the recorded error message.
ParserFailedError Text
deriving (Show, Eq)
-- | The representation of a complete email address.
--
-- The parser preserves optional components such as the display name
-- and comments. The rendering code can optionally include these
-- optional elements when turning the address back into 'Text'.
--
-- @since 0.1.0.0
data EmailAddr = EmailAddr
{ _displayName :: Maybe DisplayName,
_localPart :: LocalPart,
_domain :: Domain,
_comments :: [Comment]
}
-- | Optional display name. Addresses in the @name-addr@ format
-- from RFC 5322 allow descriptive text to precede the address.
-- This is commonly used in email messages to list the name of the
-- address' owner.
--
-- @since 0.1.0.0
displayName :: Lens' EmailAddr (Maybe DisplayName)
displayName = lens _displayName (\e d -> e {_displayName = d})
-- | The 'LocalPart' of an email address usually references the
-- destination mailbox on the 'Domain' server. However, the
-- content of the 'LocalPart' can only be understood by the
-- receiving 'Domain'.
--
-- @since 0.1.0.0
localPart :: Lens' EmailAddr LocalPart
localPart = lens _localPart (\e l -> e {_localPart = l})
-- | The 'Domain' refers to the fully-qualified domain name that
-- accepts mail for the associated 'LocalPart'. See the
-- documentation for the 'Domain' type for more details.
--
-- @since 0.1.0.0
domain :: Lens' EmailAddr Domain
domain = lens _domain (\e d -> e {_domain = d})
-- | Addresses in both the @name-addr@ and @addr-spec@ formats
-- support comments.
--
-- @since 0.1.0.0
comments :: Lens' EmailAddr [Comment]
comments = lens _comments (\e cs -> e {_comments = cs})
-- | Optional display name. Usually this is the name of the person
-- who receives email at the associated address.
--
-- > Display Name <example@example.com>
--
-- @since 0.1.0.0
newtype DisplayName = DP
{ displayNameText :: Text
}
deriving newtype (Eq, Semigroup)
deriving (Show) via RenamedShow "DisplayName" Text
-- | The name of the mailbox on the associated 'Domain'.
--
-- @since 0.1.0.0
newtype LocalPart = LP
{ localPartText :: Text
}
deriving newtype (Eq, Semigroup)
deriving (Show) via RenamedShow "LocalPart" Text
-- | A fully-qualified domain name /or/ an address literal.
--
-- Most email addresses use a domain name. However, it's perfectly
-- legal to use an 'AddressLiteral' instead.
--
-- @since 0.1.0.0
data Domain
= Domain DomainName
| DomainLiteral AddressLiteral
deriving (Show, Eq)
-- | Prism for working with domain names.
--
-- @since 0.1.0.0
_Domain :: Prism' Domain DomainName
_Domain =
prism'
Domain
( \case
Domain dn -> Just dn
DomainLiteral {} -> Nothing
)
-- | Prism for working with domain literals.
--
-- @since 0.1.0.0
_DomainLiteral :: Prism' Domain AddressLiteral
_DomainLiteral =
prism'
DomainLiteral
( \case
Domain {} -> Nothing
DomainLiteral dl -> Just dl
)
-- | A fully-qualified domain name which is made up of a list of
-- host names (labels) separated by dots.
--
-- @since 0.1.0.0
newtype DomainName = DN
{ domainNameText :: Text
}
deriving newtype (Eq, Semigroup)
deriving (Show) via RenamedShow "DomainName" Text
-- | The name of one host component of a domain name.
--
-- @since 0.1.0.0
newtype HostName = HN
{ hostNameText :: Text
}
deriving newtype (Eq, Semigroup)
deriving (Show) via RenamedShow "HostName" Text
-- | Iso for converting between domain names and a list of host names.
--
-- >>> "gmail.uk.co" ^. _DomainName._HostNames & map (review _HostName)
-- ["gmail","uk","co"]
--
-- @since 0.1.0.0
_HostNames :: Iso' DomainName [HostName]
_HostNames =
iso
(domainNameText >>> Text.splitOn "." >>> map HN)
(map hostNameText >>> Text.intercalate "." >>> DN)
-- | Address literals can be used instead of a domain name to direct
-- mail to a specific IP address or other tagged address type.
--
-- Example email addresses with address literals:
--
-- > example@[127.0.0.1]
-- > example@[IPv6:1111:2222:3333:4444:5555:6666:7777]
-- > example@[Just-some-text]
--
-- @since 0.1.0.0
data AddressLiteral
= -- | A literal IP address as defined in RFC 5321 §4.1.3. The
-- address can be in many formats so it is presented here in its
-- parsed form.
IpAddressLiteral IP
| -- | RFC 5321 also defines a /general address literal/ where a
-- /standardized tag/ precedes the address itself. The only
-- information provided about the standardized tag is:
--
-- > Standardized-tag MUST be specified in a
-- > Standards-Track RFC and registered with IANA
TaggedAddressLiteral AddressTag Literal
| -- | RFC 5322 defines a @domain-literal@ as (roughly) a span of
-- characters that are allowed in a domain name. The
-- interpretation of those characters is left to \"separate
-- documents\" such as RFC 5321.
--
-- If an address literal cannot be parsed in one of the proceeding
-- formats it is encoded as a 'Literal' value.
AddressLiteral Literal
deriving (Show, Eq)
-- | Prism for working with IP address literals.
--
-- @since 0.1.0.0
_IpAddressLiteral :: Prism' AddressLiteral IP
_IpAddressLiteral =
prism'
IpAddressLiteral
( \case
IpAddressLiteral ip -> Just ip
TaggedAddressLiteral {} -> Nothing
AddressLiteral {} -> Nothing
)
-- | Prism for working with tagged address literals.
--
-- @since 0.1.0.0
_TaggedAddressLiteral :: Prism' AddressLiteral (AddressTag, Literal)
_TaggedAddressLiteral =
prism'
(uncurry TaggedAddressLiteral)
( \case
IpAddressLiteral {} -> Nothing
TaggedAddressLiteral tag body -> Just (tag, body)
AddressLiteral {} -> Nothing
)
-- | Prism for working with address literals.
--
-- @since 0.1.0.0
_AddressLiteral :: Prism' AddressLiteral Literal
_AddressLiteral =
prism'
AddressLiteral
( \case
IpAddressLiteral {} -> Nothing
TaggedAddressLiteral {} -> Nothing
AddressLiteral lit -> Just lit
)
-- | A tag that can be used with a 'TaggedAddressLiteral'.
--
-- @since 0.1.0.0
newtype AddressTag = AT
{ addressTagText :: Text
}
deriving newtype (Eq, Semigroup)
deriving (Show) via RenamedShow "AddressTag" Text
-- | A literal address that can be used with a 'TaggedAddressLiteral'
-- or 'AddressLiteral'.
--
-- @since 0.1.0.0
newtype Literal = Lit
{ literalText :: Text
}
deriving newtype (Eq, Semigroup)
deriving (Show) via RenamedShow "Literal" Text
-- | A comment which may appear in an email address in a specific
-- location.
--
-- @since 0.1.0.0
data Comment = Comment CommentLoc CommentContent
deriving (Show, Eq)
-- | Prism for working with a 'Comment'.
--
-- @since 0.1.0.0
_Comment :: Prism' Comment (CommentLoc, CommentContent)
_Comment =
prism'
(uncurry Comment)
(\(Comment loc cc) -> Just (loc, cc))
-- | Lens for working with comment locations.
--
-- @since 0.1.0.0
commentLoc :: Lens' Comment CommentLoc
commentLoc =
lens
(\(Comment loc _) -> loc)
(\(Comment _ cc) loc -> Comment loc cc)
-- | Lens for working with comment contents.
--
-- @since 0.1.0.0
commentContent :: Lens' Comment CommentContent
commentContent =
lens
(\(Comment _ cc) -> cc)
(\(Comment loc _) cc -> Comment loc cc)
-- | The location where a comment was parsed or where it should be
-- rendered.
--
-- @since 0.1.0.0
data CommentLoc
= -- | Just before the 'DisplayName'.
BeforeDisplayName
| -- | Just after the 'DisplayName' but before the address.
AfterDisplayName
| -- | Before the 'LocalPart' of the address.
BeforeLocalPart
| -- | After the 'Domain'.
AfterDomain
| -- | After the complete address.
AfterAddress
deriving (Show, Eq)
-- | Text that can appear in a comment.
--
-- @since 0.1.0.0
newtype CommentContent = CC
{ commentContentText :: Text
}
deriving newtype (Eq, Semigroup)
deriving (Show) via RenamedShow "CommentContent" Text
-- | Newtype wrapper for deriving 'Show' instances that lie about the
-- name of the constructor.
newtype RenamedShow (n :: Symbol) a = RS a
instance (Show a, KnownSymbol n) => Show (RenamedShow n a) where
showsPrec d (RS x) =
showParen (d > 10) $
showString (symbolVal (Proxy :: Proxy n) <> " ")
. showsPrec d x