{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.GS1.EPC
(URI
, ParseFailure(..)
, XMLSnippet(..)
, MissingTag(..)
, EventIdStr(..)
, GS1CompanyPrefix(..)
, ItemReference(..)
, ExtensionDigit(..)
, SerialReference(..)
, CheckDigit(..)
, Lot(..)
, IndividualAssetReference(..)
, SerialNumber(..)
, SGLNExtension(..)
, Uom(..)
, Amount(..)
, AssetType(..)
, Lng(..)
, Lat(..)
, LocationReference(..)
, DocumentType(..)
, ServiceReference(..)
, BizTransactionId(..)
, TransformationId(..)
, EPCISTime(..)
, SGTINFilterValue(..)
, Quantity(..)
, InstanceLabelEPC(..)
, ClassLabelEPC(..)
, LocationEPC(..)
, SourceDestType(..)
, BusinessTransactionEPC(..)
, LocationError(..)
, BizStep(..)
, BizTransactionType(..)
, BizTransaction(..)
, Action(..)
, Disposition(..)
, readURI
, renderURL
, readURIClassLabelEPC
, readURIInstanceLabelEPC
, mkAction
)
where
import Control.Lens
import Data.Aeson
import Data.Aeson.TH
import Data.Swagger
import qualified Data.Text as T
import GHC.Generics (Generic)
import Web.HttpApiData
import Data.Bifunctor (first)
import Data.GS1.Utils
import Data.Time
import Data.UUID (UUID)
import Data.Semigroup
import Data.Hashable (Hashable (..))
newtype XMLSnippet = XMLSnippet T.Text deriving (Show, Eq, Read, Generic)
newtype MissingTag = MissingTag T.Text deriving (Show, Eq, Read, Generic)
newtype EventIdStr = EventIdStr T.Text deriving (Show, Eq, Read, Generic)
data ParseFailure
= InvalidLength XMLSnippet
| InvalidFormat XMLSnippet
| InvalidAction XMLSnippet
| InvalidBizTransaction XMLSnippet
| InvalidEventId EventIdStr
| TimeZoneError XMLSnippet
| TagNotFound MissingTag
| MultipleTags T.Text
| InvalidDispBizCombination XMLSnippet
| ChildFailure [ParseFailure]
deriving (Show, Eq, Read, Generic)
instance Semigroup ParseFailure where
ChildFailure xs <> ChildFailure ys = ChildFailure (xs++ys)
ChildFailure [] <> y = y
x <> ChildFailure [] = x
ChildFailure xs <> y = ChildFailure (xs++[y])
x <> ChildFailure ys = ChildFailure (x:ys)
x <> y = ChildFailure [x,y]
instance Monoid ParseFailure where
mempty = ChildFailure []
mappend = (<>)
class URI a where
{-# MINIMAL uriPrefix, uriSuffix, readURI #-}
uriPrefix :: a -> T.Text
uriSuffix :: a -> Either T.Text [T.Text]
readURI :: T.Text -> Either ParseFailure a
renderURL :: a -> T.Text
renderURL a = uriPrefix a <> either id dots (uriSuffix a)
dots :: [T.Text] -> T.Text
dots = T.intercalate "."
makeErrorType :: (XMLSnippet -> ParseFailure) -> [T.Text] -> Either ParseFailure b
makeErrorType e snippets = Left $ e (XMLSnippet $ dots snippets)
newtype GS1CompanyPrefix = GS1CompanyPrefix {unGS1CompanyPrefix :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype ItemReference = ItemReference {unItemReference :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype ExtensionDigit = ExtensionDigit {unExtensionDigit :: Int}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype SerialReference = SerialReference {unSerialReference :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype CheckDigit = CheckDigit {unCheckDigit :: Int}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype Lot = Lot {unLot :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype IndividualAssetReference =
IndividualAssetReference {unIndividualAssetReference :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype SerialNumber = SerialNumber {unSerialNumber :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype SGLNExtension = SGLNExtension {unSGLNExtension :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
instance ToSchema GS1CompanyPrefix
instance ToParamSchema GS1CompanyPrefix
instance ToSchema ItemReference
instance ToSchema ExtensionDigit
instance ToSchema SerialReference
instance ToSchema CheckDigit
instance ToSchema Lot
instance ToSchema IndividualAssetReference
instance ToSchema SerialNumber
instance ToSchema SGLNExtension
data SGTINFilterValue
= AllOthers
| POSTradeItem
| FullCaseForTransport
| Reserved1
| InnerPackTradeItemGroupingForHandling
| Reserved2
| UnitLoad
| UnitInsideTradeItemOrComponentInsideAProductNotIntendedForIndividualSale
deriving (Eq, Generic, Read, Enum, Show)
$(deriveJSON defaultOptions ''SGTINFilterValue)
instance ToSchema SGTINFilterValue
newtype Uom = Uom {unUom :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype Amount = Amount {unAmount :: Double}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
newtype AssetType = AssetType {unAssetType :: T.Text}
deriving (Show, Read, Eq, Generic, FromJSON, ToJSON)
instance ToSchema Amount
instance ToSchema Uom
instance ToSchema AssetType
data Quantity
= MeasuredQuantity
{
_quantityAmount :: Amount
, _quantityUom :: Uom
}
| ItemCount
{
_quantityCount :: Integer
}
deriving (Show, Read, Eq, Generic)
$(deriveJSON defaultOptions ''Quantity)
instance ToSchema Quantity
getSuffixTokens :: [T.Text] -> [T.Text]
getSuffixTokens suffix = T.splitOn "." $ T.concat suffix
data ClassLabelEPC
= LGTIN
{ _lgtinCompanyPrefix :: GS1CompanyPrefix
, _lgtinItemReference :: ItemReference
, _lgtinLot :: Lot
}
| CSGTIN
{ _csgtinCompanyPrefix :: GS1CompanyPrefix
, _csgtinSgtinFilterValue :: Maybe SGTINFilterValue
, _csgtinItemReference :: ItemReference
}
deriving (Show, Read, Eq, Generic)
instance URI ClassLabelEPC where
uriPrefix LGTIN{} = "urn:epc:class:lgtin:"
uriPrefix CSGTIN{} = "urn:epc:idpat:sgtin:"
uriSuffix
(LGTIN (GS1CompanyPrefix pfix) (ItemReference itemReference) (Lot lot)) =
Right [pfix, itemReference, lot]
uriSuffix
(CSGTIN (GS1CompanyPrefix pfix) _ (ItemReference itemReference)) =
Right [pfix, itemReference]
readURI epcStr = readURIClassLabelEPC $ T.splitOn ":" epcStr
readURIClassLabelEPC :: [T.Text] -> Either ParseFailure ClassLabelEPC
readURIClassLabelEPC ("urn" : "epc" : "class" : "lgtin" : rest) =
Right $ LGTIN (GS1CompanyPrefix pfix) (ItemReference itemReference) (Lot lot)
where [pfix, itemReference, lot] = getSuffixTokens rest
readURIClassLabelEPC ("urn" : "epc" : "idpat" : "sgtin" : rest) =
Right $ CSGTIN (GS1CompanyPrefix pfix) Nothing (ItemReference itemReference)
where (pfix:itemReference:_) = getSuffixTokens rest
readURIClassLabelEPC xSnippet = makeErrorType InvalidFormat xSnippet
$(deriveJSON defaultOptions ''ClassLabelEPC)
instance ToSchema ClassLabelEPC
data InstanceLabelEPC
= GIAI
{
_giaiCompanyPrefix :: GS1CompanyPrefix
, _giaiSerialNum :: SerialNumber
}
| SSCC
{
_ssccCompanyPrefix :: GS1CompanyPrefix
, _ssccSerialNum :: SerialNumber
}
| SGTIN
{
_sgtinCompanyPrefix :: GS1CompanyPrefix
, _sgtinSgtinFilterValue :: Maybe SGTINFilterValue
, _sgtinItemReference :: ItemReference
, _sgtinSerialNum :: SerialNumber
}
| GRAI
{
_graiCompanyPrefix :: GS1CompanyPrefix
, _graiAssetType :: AssetType
, _graiSerialNum :: SerialNumber
}
deriving (Show, Read, Eq, Generic)
instance URI InstanceLabelEPC where
uriPrefix GIAI{} = "urn:epc:id:giai:"
uriPrefix SSCC{} = "urn:epc:id:sscc:"
uriPrefix SGTIN{} = "urn:epc:id:sgtin:"
uriPrefix GRAI{} = "urn:epc:id:grai:"
uriSuffix (GIAI (GS1CompanyPrefix pfix) (SerialNumber sn)) = Right [pfix, sn]
uriSuffix (SSCC (GS1CompanyPrefix pfix) (SerialNumber sn)) = Right [pfix, sn]
uriSuffix (SGTIN (GS1CompanyPrefix pfix) _ (ItemReference ir) (SerialNumber sn)) =
Right [pfix, ir, sn]
uriSuffix (GRAI (GS1CompanyPrefix pfix) (AssetType aType) (SerialNumber sn)) =
Right [pfix, aType, sn]
readURI epcStr = readURIInstanceLabelEPC $ T.splitOn ":" epcStr
sgtinPadLen :: Int
sgtinPadLen = 13
ssccPadLen :: Int
ssccPadLen = 17
readURIInstanceLabelEPC :: [T.Text] -> Either ParseFailure InstanceLabelEPC
readURIInstanceLabelEPC ("urn" : "epc" : "id" : "giai" : rest) =
Right $ GIAI (GS1CompanyPrefix pfix) (SerialNumber sn)
where [pfix, sn] = getSuffixTokens rest
readURIInstanceLabelEPC xSnippet@("urn" : "epc" : "id" : "sscc" : rest)
| isCorrectLen = Right $ SSCC (GS1CompanyPrefix pfix) (SerialNumber sn)
| otherwise = makeErrorType InvalidLength xSnippet
where
[pfix, sn] = getSuffixTokens rest
isCorrectLen =
getTotalLength [pfix, sn] == ssccPadLen
readURIInstanceLabelEPC ("urn" : "epc" : "id" : "grai" : rest) =
Right $ GRAI (GS1CompanyPrefix pfix) (AssetType assetType) (SerialNumber sn)
where [pfix, assetType, sn] = getSuffixTokens rest
readURIInstanceLabelEPC xSnippet@("urn" : "epc" : "id" : "sgtin" : rest)
| isCorrectLen =
Right $ SGTIN (GS1CompanyPrefix pfix) Nothing (ItemReference ir) (SerialNumber sn)
| otherwise = makeErrorType InvalidLength xSnippet
where
[pfix, ir, sn] = getSuffixTokens rest
isCorrectLen =
getTotalLength [pfix, ir] == sgtinPadLen
readURIInstanceLabelEPC xSnippet = makeErrorType InvalidFormat xSnippet
$(deriveJSON defaultOptions ''InstanceLabelEPC)
instance ToSchema InstanceLabelEPC
newtype Lng = Lng {unLng :: Double}
deriving (Show, Read, Eq, Generic, ToJSON, FromJSON)
newtype Lat = Lat {unLat :: Double}
deriving (Show, Read, Eq, Generic, ToJSON, FromJSON)
newtype LocationReference
= LocationReference
{
_locationRefVal :: T.Text
}
deriving (Read, Eq, Generic, Show)
$(deriveJSON defaultOptions ''LocationReference)
data LocationEPC = SGLN {
_sglnCompanyPrefix :: GS1CompanyPrefix
, _locationRef :: LocationReference
, _sglnExt :: Maybe SGLNExtension
}
deriving (Show, Read, Eq, Generic)
$(deriveJSON defaultOptions ''LocationEPC)
instance ToSchema LocationReference
instance Hashable LocationEPC where
hashWithSalt salt (SGLN pfx _ _) = hashWithSalt salt $ unGS1CompanyPrefix pfx
instance URI LocationEPC where
uriPrefix SGLN{} = "urn:epc:id:sgln:"
uriSuffix (SGLN (GS1CompanyPrefix pfix) (LocationReference loc) (Just (SGLNExtension ext))) =
Right [pfix, loc, ext]
uriSuffix (SGLN (GS1CompanyPrefix pfix) (LocationReference loc) Nothing) = Right [pfix, loc]
readURI epcStr
| isLocationEPC (T.splitOn ":" epcStr) =
readURILocationEPC $ T.splitOn "." $ last $ T.splitOn ":" epcStr
| otherwise = Left $ InvalidFormat (XMLSnippet epcStr)
isLocationEPC :: [T.Text] -> Bool
isLocationEPC ("urn" : "epc" : "id" : "sgln" : _) = True
isLocationEPC _ = False
sglnPadLen :: Int
sglnPadLen = 12
getExt :: T.Text -> Maybe SGLNExtension
getExt "0" = Nothing
getExt s = Just (SGLNExtension s)
readURILocationEPC :: [T.Text] -> Either ParseFailure LocationEPC
readURILocationEPC xSnippet@[pfix, loc]
| isCorrectLen =
Right $ SGLN (GS1CompanyPrefix pfix) (LocationReference loc) Nothing
| otherwise = makeErrorType InvalidLength xSnippet
where
isCorrectLen = getTotalLength [pfix, loc] == sglnPadLen
readURILocationEPC xSnippet@([pfix, loc, extNum])
| isCorrectLen =
Right $
SGLN (GS1CompanyPrefix pfix) (LocationReference loc) (getExt extNum)
| otherwise = makeErrorType InvalidLength xSnippet
where
isCorrectLen = getTotalLength [pfix, loc] == sglnPadLen
readURILocationEPC xSnippet = makeErrorType InvalidFormat xSnippet
instance ToSchema LocationEPC
data SourceDestType
= SDOwningParty
| SDPossessingParty
| SDLocation
deriving (Show, Eq, Generic, Read)
$(deriveJSON defaultOptions ''SourceDestType)
instance ToSchema SourceDestType
instance URI SourceDestType where
uriPrefix _ = "urn:epcglobal:cbv:sdt:"
uriSuffix SDOwningParty = Left "owning_party"
uriSuffix SDPossessingParty = Left "possessing_party"
uriSuffix SDLocation = Left "location"
readURI epc = readSrcDestURI $ last $ T.splitOn ":" epc
readSrcDestURI :: T.Text -> Either ParseFailure SourceDestType
readSrcDestURI "owning_party" = Right SDOwningParty
readSrcDestURI "possessing_party" = Right SDPossessingParty
readSrcDestURI "location" = Right SDLocation
readSrcDestURI errTxt = Left $ InvalidFormat (XMLSnippet errTxt)
newtype DocumentType = DocumentType {unDocumentType :: T.Text}
deriving (Show, Read, Eq, Generic, ToJSON, FromJSON)
newtype ServiceReference = ServiceReference {unServiceReference :: T.Text}
deriving (Show, Read, Eq, Generic, ToJSON, FromJSON)
instance ToSchema DocumentType
data BusinessTransactionEPC
= GDTI {
_gdtiCompanyPrefix :: GS1CompanyPrefix
, _gdtiDocType :: DocumentType
, _gdtiSerialNum :: SerialNumber }
| GSRN {
_gsrnCompanyPrefix :: GS1CompanyPrefix
, _gsrnSerialRef :: SerialReference }
deriving (Show, Read, Eq, Generic)
instance URI BusinessTransactionEPC where
uriPrefix GDTI{} = "urn:epc:id:gsrn:"
uriPrefix GSRN{} = "urn:epc:id:gsrn:"
uriSuffix (GDTI (GS1CompanyPrefix pfix) (DocumentType documentType) (SerialNumber sn)) =
Right [pfix, documentType, sn]
uriSuffix (GSRN (GS1CompanyPrefix pfix) (SerialReference sr)) = Right [pfix, sr]
readURI epcStr = readURIBusinessTransactionEPC $
getSuffixTokens [last $ T.splitOn ":" epcStr]
gsrnPadLen :: Int
gsrnPadLen = 17
gdtiPadLen :: Int
gdtiPadLen = 12
readURIBusinessTransactionEPC :: [T.Text] ->
Either ParseFailure BusinessTransactionEPC
readURIBusinessTransactionEPC xSnippet@([pfix, sref])
| isCorrectLen = Right $ GSRN (GS1CompanyPrefix pfix) (SerialReference sref)
| otherwise = makeErrorType InvalidLength xSnippet
where
isCorrectLen =
getTotalLength [pfix, sref] == gsrnPadLen
readURIBusinessTransactionEPC xSnippet@([pfix, docType, sn])
| isCorrectLen = Right $ GDTI (GS1CompanyPrefix pfix) (DocumentType docType) (SerialNumber sn)
| otherwise = makeErrorType InvalidLength xSnippet
where
isCorrectLen = getTotalLength [pfix, docType, sn] == gdtiPadLen
readURIBusinessTransactionEPC xSnippet = makeErrorType InvalidFormat xSnippet
$(deriveJSON defaultOptions ''BusinessTransactionEPC)
instance ToSchema BusinessTransactionEPC
data LocationError
= IllegalGLNFormat
| InvalidChecksum
deriving (Show, Eq, Generic)
data BizStep
= Accepting
| Arriving
| Assembling
| Collecting
| Commissioning
| Consigning
| CreatingClassInstance
| CycleCounting
| Decommissioning
| Departing
| Destroying
| Disassembling
| Dispensing
| Encoding
| EnteringExiting
| Holding
| Inspecting
| Installing
| Killing
| Loading
| Other
| Packing
| Picking
| Receiving
| Removing
| Repackaging
| Repairing
| Replacing
| Reserving
| RetailSelling
| Shipping
| StagingOutbound
| StockTaking
| Stocking
| Storing
| Transporting
| Unloading
| VoidShipping
deriving (Show, Eq, Generic, Read)
$(deriveJSON defaultOptions ''BizStep)
instance ToSchema BizStep
ppBizStep :: BizStep -> T.Text
ppBizStep = revertCamelCase . T.pack . show
readURIBizStep :: Maybe BizStep -> T.Text -> Either ParseFailure BizStep
readURIBizStep Nothing s = Left $ InvalidFormat (XMLSnippet s)
readURIBizStep (Just bizstep) _ = Right bizstep
instance URI BizStep where
uriPrefix _ = "urn:epcglobal:cbv:bizstep:"
uriSuffix = Left . ppBizStep
readURI s = let pURI = parseURI s "urn:epcglobal:cbv:bizstep" :: Maybe BizStep
in readURIBizStep pURI s
newtype BizTransactionId = BizTransactionId {unBizTransactionId :: T.Text}
deriving (Show, Read, Eq, Generic, ToJSON, FromJSON)
instance ToSchema BizTransactionId
data BizTransactionType
= Bol
| Desadv
| Inv
| Pedigree
| Po
| Poc
| Prodorder
| Recadv
| Rma
deriving (Show, Eq, Generic, Read)
$(deriveJSON defaultOptions ''BizTransactionType)
instance ToSchema BizTransactionType
ppBizTransactionType :: BizTransactionType -> T.Text
ppBizTransactionType = revertCamelCase . T.pack . show
readURIBizTransactionType :: Maybe BizTransactionType
-> T.Text
-> Either ParseFailure BizTransactionType
readURIBizTransactionType Nothing s = Left $ InvalidFormat (XMLSnippet s)
readURIBizTransactionType (Just btt) _ = Right btt
instance URI BizTransactionType where
uriPrefix _ = "urn:epcglobal:cbv:btt:"
uriSuffix = Left . ppBizTransactionType
readURI s = let pURI = parseURI s "urn:epcglobal:cbv:btt" :: Maybe BizTransactionType
in readURIBizTransactionType pURI s
data BizTransaction = BizTransaction
{
_btid :: BizTransactionId
, _bt :: BizTransactionType
}
deriving (Show, Eq, Generic)
$(deriveJSON defaultOptions ''BizTransaction)
instance ToSchema BizTransaction
newtype TransformationId = TransformationId {unTransformationId :: UUID}
deriving (Show, Read, Eq, Generic, ToJSON, FromJSON)
instance ToSchema TransformationId
data Action
= Add
| Observe
| Delete
deriving (Show, Eq, Generic, Read)
$(deriveJSON defaultOptions ''Action)
instance ToSchema Action
instance ToParamSchema Action where
toParamSchema _ = mempty
& type_ .~ SwaggerString
instance FromHttpApiData Action where
parseQueryParam t = first (T.pack . show) (mkAction t)
mkAction :: T.Text -> Either ParseFailure Action
mkAction t =
case mkByName . camelCase $ T.toLower t of
Nothing -> Left $ InvalidAction (XMLSnippet t)
Just x -> Right x
data Disposition
= Active
| ContainerClosed
| Damaged
| Destroyed
| Dispensed
| Disposed
| Encoded
| Expired
| InProgress
| InTransit
| Inactive
| NoPedigreeMatch
| NonSellableOther
| PartiallyDispensed
| Recalled
| Reserved
| RetailSold
| Returned
| SellableAccessible
| SellableNotAccessible
| Stolen
| Unknown
deriving (Show, Eq, Generic, Read)
$(deriveJSON defaultOptions ''Disposition)
instance ToSchema Disposition
ppDisposition :: Disposition -> T.Text
ppDisposition = revertCamelCase . T.pack . show
readURIDisposition :: Maybe Disposition -> T.Text -> Either ParseFailure Disposition
readURIDisposition Nothing s = Left $ InvalidFormat (XMLSnippet s)
readURIDisposition (Just disp) _ = Right disp
instance URI Disposition where
uriPrefix _ = "urn:epcglobal:cbv:disp:"
uriSuffix = Left . ppDisposition
readURI s = let pURI = parseURI s "urn:epcglobal:cbv:disp" :: Maybe Disposition
in readURIDisposition pURI s
newtype EPCISTime = EPCISTime {unEPCISTime :: UTCTime}
deriving (Show, Read, Eq, Generic, Ord, ToJSON, FromJSON)
instance ToSchema EPCISTime
data EPCISTimeError = IllegalTimeFormat deriving (Show, Eq, Generic)
$(deriveJSON defaultOptions ''EPCISTimeError)
instance ToSchema EPCISTimeError
instance Eq ZonedTime where
x == y = show x == show y
$(deriveJSON defaultOptions ''TimeZone)
instance ToParamSchema TimeZone where
toParamSchema _ = mempty
& type_ .~ SwaggerString
named :: T.Text -> Schema -> NamedSchema
named n = NamedSchema (Just n)
timeSchema :: T.Text -> Schema
timeSchema fmt = mempty
& type_ .~ SwaggerString
& format ?~ fmt
instance ToSchema TimeZone where
declareNamedSchema _ = pure $ named (T.pack "TimeZone") $ timeSchema (T.pack "date-time")