libasterix: Asterix data processing library
This library provides features to process asterix data format, including parsing, unparsing, constructing and manipulating asterix records.
Asterix data format is a set of specifications, defined by Eurocontrol. It is mostly used for exchanging surveillance related information in air traffic control applications.
For more details and tutorial see the readme.
[Skip to Readme]
Modules
[Index] [Quick Jump]
Downloads
- libasterix-0.11.0.tar.gz [browse] (Cabal source package)
- Package description (as included in the package)
Maintainer's Corner
For package maintainers and hackage trustees
Candidates
- No Candidates
| Versions [RSS] | 0.11.0 |
|---|---|
| Change log | CHANGELOG.md |
| Dependencies | base (<5), base16-bytestring (>=1.0.2.0 && <1.1), bytestring (>=0.12.2.0 && <0.13), containers (>=0.7 && <0.8), text (>=2.1.3 && <2.2), transformers (>=0.6.1.1 && <0.7) [details] |
| License | BSD-3-Clause |
| Author | Zoran Bošnjak |
| Maintainer | zoran.bosnjak@via.si |
| Uploaded | by zoranbosnjak at 2026-04-22T07:16:57Z |
| Category | Data |
| Home page | https://github.com/zoranbosnjak/asterix-libs/tree/main/libs/haskell |
| Bug tracker | https://github.com/zoranbosnjak/asterix-libs/issues |
| Source repo | head: git clone https://github.com/zoranbosnjak/asterix-libs |
| Distributions | |
| Downloads | 6 total (6 in the last 30 days) |
| Rating | (no votes yet) [estimated by Bayesian average] |
| Your Rating |
|
| Status | Docs uploaded by user Build status unknown [no reports yet] |
Readme for libasterix-0.11.0
[back to package description]Asterix data processing library for haskell
Features:
- pure haskell implementation
- asterix data parsing/decoding from bytes
- asterix data encoding/unparsing to bytes
- precise conversion functions for physical quantities
- support for many asterix categories and editions
- support for Reserved Expansion Fields (REF)
- support for Random Field Sequencing (RFS)
- support for categories with multiple UAPs, eg. cat001
- support for context dependent items, eg. I062/380/IAS
- support for strict or partial record parsing, to be used with so called blocking or non-blocking asterix categories
- support to encode zero, one or more records in a datablock
- type annotations for static type checking, including subitem access by name
Asterix encoding and decoding example
-- | file: readme-samples/example0.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Data.Maybe
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
-- Select particular asterix categories and editions
type Cat034 = Gen.Cat_034_1_29
type Cat048 = Gen.Cat_048_1_32
type TSacSic = SameType '[ Cat034 ~> "010", Cat048 ~> "010"]
-- Example messages for this application
data Token
= NorthMarker
| SectorCrossing Double -- Azimuth
| Plot Double Double String -- Rho, Theta, SSR
deriving (Eq, Show)
-- example message to be encoded
txMessage :: [Token]
txMessage =
[ NorthMarker
, SectorCrossing 0.0
, Plot 10.0 45.0 "7777"
, SectorCrossing 45.0
]
sacSic :: NonSpare TSacSic
sacSic = group (item @"SAC" 1 *: item @"SIC" 2 *: nil)
-- encode token to datablock
encode :: Token -> SBuilder
encode = \case
NorthMarker ->
let db :: Datablock (DatablockOf Cat034) = datablock (r *: nil)
r = record
( item @"000" 1 -- North marker message
*: item @"010" sacSic
*: nil )
in unparse db
SectorCrossing azimuth ->
let db :: Datablock (DatablockOf Cat034) = datablock (r *: nil)
r = record
( item @"000" 2 -- Sector crossing message
*: item @"010" sacSic
*: item @"020" (quantity @"°" (Quantity azimuth))
*: nil )
in unparse db
Plot rho theta ssr ->
let db :: Datablock (DatablockOf Cat048) = datablock (r *: nil)
r = record
( item @"010" sacSic
*: item @"040" ( group
( item @"RHO" ( quantity @"NM" (Quantity rho))
*: item @"THETA" ( quantity @"°" (Quantity theta))
*: nil ))
*: item @"070" ( group (0 *: 0 *: 0 *: 0
*: item @"MODE3A" (string ssr)
*: nil))
*: nil )
in unparse db
-- decode bytes to message list
decode :: ByteString -> [Token]
decode rxBytes = fromRight $ do
rawDatablocks <- parseRawDatablocks rxBytes
tokens <- mapM go rawDatablocks
pure $ mconcat tokens
where
fromRight = \case
Left (ParsingError e) -> error (show e)
Right val -> val
go :: RawDatablock -> Either ParsingError [Token]
go rawDb = case rawDatablockCategory rawDb of
34 -> do
let act = parseRecords (schema @(RecordOf Cat034) Proxy)
records <- fmap Record <$> parse @StrictParsing act (getRawRecords rawDb)
pure (mapMaybe handleCat034 records)
48 -> do
let act = parseRecords (schema @(RecordOf Cat048) Proxy)
records <- fmap Record <$> parse @StrictParsing act (getRawRecords rawDb)
pure (mapMaybe handleCat048 records)
_ -> pure []
handleCat034 :: Record (RecordOf Cat034) -> Maybe Token
handleCat034 rec = case asUint @Int i000 of
1 -> Just NorthMarker
2 -> Just $ SectorCrossing (unQuantity $ asQuantity @"°" i020)
_ -> Nothing
where
i000 = fromMaybe (error "missing item") (getItem @"000" rec)
i020 = fromMaybe (error "missing item") (getItem @"020" rec)
handleCat048 :: Record (RecordOf Cat048) -> Maybe Token
handleCat048 rec = Just $ Plot rho theta ssr where
i040 = fromMaybe (error "missing item") (getItem @"040" rec)
rho = unQuantity $ asQuantity @"NM" $ getItem @"RHO" i040
theta = unQuantity $ asQuantity @"°" $ getItem @"THETA" i040
i070 = fromMaybe (error "missing item") (getItem @"070" rec)
ssr = asString $ getItem @"MODE3A" i070
expected :: ByteString
expected = fromJust $ unhexlify "220007c0010201220008d00102020030000c9801020a0020000fff220008d001020220"
main :: IO ()
main = do
-- encode message to bytes
print ("sending message: " <> show txMessage)
let datablocks = fmap encode txMessage
tx = toByteString $ mconcat datablocks
putStrLn ("bytes on the wire: " <> hexlify tx)
assert (tx == expected)
-- decode bytes back to message, expect the same message
let rx = tx
rxMessage = decode rx
assert (rxMessage == txMessage)
Installation and library import
This tutorial assumes importing complete asterix module into the current
namespace. In practice however only the required objects could be imported
or the modules might be imported qualified.
import Asterix.Coding
import Asterix.Generated as Gen
Asterix object hierarchy and terminology
This library is built aroud the following concepts:
- Datagram is a raw binary data as received for example from UDP socket.
- RawDatablock is asterix datablock in the form
cat|length|datawith correct byte size. A datagram can contain multiple datablocks. In some cases it might be sufficient to work with raw datablocks, for example "asterix category filtering". In this case, it is not necessary to fully parse all asterix records, but is sufficient and faster to parse only up to theRawDatablocklevel. - Datablock/Record is a higher level construct, where we have a guarantee that all containing elements (records, subitems) are semantically correct (asterix is fully parsed or correctly constructed).
- Item is a union of regular item or spare item, where the spare item is a wrapper around raw bits (normally zero).
- Variation is a union of
[element, group, extended, repetitive compound]constructors.
Constructing, parsing/unparsing, encoding/decoding
This library uses term to construct asterix, when a record/datablock/datagram is "constructed" inside the application source code.
Once the datablock is constructed, it is unparsed to bytes, ready to be sent over the network. Similarly, the term parsing is used when we perform oposite transformation from unstructured bytes to structured Datablock/Record. This operation can obviously fail at runtime, so some form of error handling is required inside application.
The terms encoding/decoding are used to denote conversion between objects from this library to application specific objects, for example target reports or sector messages
application objects (e.g. Sector crossing message)
^ |
| | decoding / encoding
| v
asterix objects (e.g. Record)
^ |
| | parsing / unparsing
| v
(bytes)
Subitem and content access
A Record contains Items, which in turn contains subitems at various
nesting levels. To access a subitem, use getItem @"itemName" function.
The result (if not Nothing) can be in turn querried for nested subitems
or converted to required value.
This is a typical usage:
-- | file: readme-samples/subitems-get.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Data.Maybe
import Asterix.Coding
import Asterix.Generated as Gen
type Cat048 = Gen.Cat_048_1_32
-- test record
rec048 :: Record (RecordOf Cat048)
rec048 = record ( item @"040" 0 *: item @"070" 0 *: nil)
main :: IO ()
main = do
let i040 = fromJust $ getItem @"040" rec048
i070 = fromJust $ getItem @"070" rec048
-- access subitems
rho :: Double = unQuantity $ asQuantity @"NM" (getItem @"RHO" i040)
theta :: Double = unQuantity $ asQuantity @"°" (getItem @"THETA" i040)
ssr :: String = asString $ getItem @"MODE3A" i070
print (rho, theta, ssr)
Setting subitem
This library provides setItem @"itemName" subItem parentItem and
maybeSetItem @"itemName" subItem parentItem functions to manipulate asterix
constructs. For example:
-- | file: readme-samples/subitems-set.hs
{-# LANGUAGE DataKinds #-}
import Data.Function ((&))
import Asterix.Coding
import Asterix.Generated as Gen
type Cat048 = Gen.Cat_048_1_32
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
main :: IO ()
main = do
-- construct compound item
let i120a:: NonSpare (Cat048 ~> "120")
i120a = compound
( item @"CAL" (group
( item @"D" 0
*: spare
*: item @"CAL" 0
*: nil))
*: item @"RDS" (repetitive [1,2,3])
*: nil)
-- construct empty compound item and add items
let i120b :: NonSpare (Cat048 ~> "120")
i120b = compound nil
& setItem @"CAL" (group
( item @"D" 0
*: spare
*: item @"CAL" 0
*: nil))
& maybeSetItem @"RDS" (Just (repetitive [1,2,3]))
-- the result shall be the same
assert (unparse @Bits i120a == unparse i120b)
assert (isEmpty i120b == False)
-- same scenario is possible on 'Record' too
let recordA :: Record (RecordOf Cat048)
recordA = record
( item @"010" 0x0102
*: item @"120" i120a
*: nil)
let recordB :: Record (RecordOf Cat048)
recordB = record nil
& setItem @"010" 0x0102
& maybeSetItem @"020" Nothing
& setItem @"120" i120b
assert (unparse @Bits recordA == unparse recordB)
Modifying extended subitem
Extended item contains always-present primary part and optional extensions and so some subitems might not be present.
modifyExtendedSubitemIfPresent @subitemName f item function provides
necessary checking and if a subitem is present, it applies a modifier function
f to the subitem. Otherwises, if the subitem is not present, the function
returns complete item unchanged.
-- | file: readme-samples/modify-extended-subitem.hs
{-# LANGUAGE DataKinds #-}
import Asterix.Coding
import Asterix.Generated as Gen
type Cat048 = Gen.Cat_048_1_32
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
main :: IO ()
main = do
-- construct extended item with only the first group present
let i020 :: NonSpare (Cat048 ~> "020")
i020 = extendedGroups (0 *: nil)
-- "TYP" is part of the first group (present), we are changing it
result1 = modifyExtendedSubitemIfPresent @"TYP" (const 1) i020
-- "TST" is part of the second group (not present),
-- so the function call shall have no effect
result2 = modifyExtendedSubitemIfPresent @"TST" (const 1) i020
assert $ not (unparse @Bits i020 == unparse result1)
assert (unparse @Bits i020 == unparse result2)
Application examples
Category filter
Example: Category filter, drop datablocks if category == 1
-- | file: readme-samples/catflt.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Control.Monad
import Data.Maybe
import Data.ByteString (ByteString)
import Asterix.Coding
-- UDP rx test function
receiveFromUdp :: IO ByteString
receiveFromUdp = pure $ fromJust $ unhexlify $ join
[ "01000401" -- cat1 datablock
, "02000402" -- cat2 datablock
]
-- UDP tx test function
sendToUdp :: SBuilder -> IO ()
sendToUdp = putStrLn . hexlify . toByteString
main :: IO ()
main = do
inputData <- receiveFromUdp
let rawDatablocks = case (parseRawDatablocks inputData) of
Left _ -> error "unable to parse"
Right val -> val
validDatablocks = do
db <- rawDatablocks
guard $ rawDatablockCategory db /= 1
pure $ unparse @SBuilder db
outputData = mconcat validDatablocks
sendToUdp outputData
Rewrite SAC/SIC in item 010
Example: Asterix filter, rewrite SAC/SIC code.
-- | file: readme-samples/rewrite-sacsic.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MonoLocalBinds #-}
import GHC.TypeLits
import Data.Maybe
import Data.Either
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
-- categories/editions of interest
type Cat048 = Gen.Cat_048_1_31
type Cat062 = Gen.Cat_062_1_19
type Cat063 = Gen.Cat_063_1_6
-- All of the following types have the same item "010"
type TSacSic = SameType '[ Cat048 ~> "010", Cat062 ~> "010", Cat063 ~> "010"]
handleDatablock :: forall cat.
( Schema (RecordOf cat) VRecord
, SetItem "010" (Record (RecordOf cat)) (NonSpare TSacSic)
, KnownNat (CategoryOf cat)
) => Proxy cat -> NonSpare TSacSic -> ByteString -> SBuilder
handleDatablock _p sacSic bs =
let act = parseRecords (schema @(RecordOf cat) Proxy)
records :: [Record (RecordOf cat)]
records = case parse @StrictParsing act bs of
Left e -> error (show e)
Right lst -> (setItem @"010" sacSic) . Record <$> lst
urecords = fmap unRecord records
in datablockBuilder (natVal (Proxy @(CategoryOf cat))) urecords
handleRawDatablock :: NonSpare TSacSic -> RawDatablock -> SBuilder
handleRawDatablock sacSic rawDb = case rawDatablockCategory rawDb of
48 -> handleDatablock @Cat048 Proxy sacSic rawRecords
62 -> handleDatablock @Cat062 Proxy sacSic rawRecords
63 -> handleDatablock @Cat063 Proxy sacSic rawRecords
cat -> error ("unsupported category: " <> show cat)
where
rawRecords = getRawRecords rawDb
rewriteSacSic :: NonSpare TSacSic -> ByteString -> SBuilder
rewriteSacSic sacSic bs = output where
rawDatablocks = fromRight (error "unexpected") $ parseRawDatablocks bs
result = fmap (handleRawDatablock sacSic) rawDatablocks
output = mconcat result
-- Dummy rx function (generate valid asterix datagram).
readBytesFromTheNetwork :: IO ByteString
readBytesFromTheNetwork = do
let rec :: Record (RecordOf Cat048)
rec = record
( item @"010" 0
*: item @"040" 0
*: nil)
db1, db2 :: Datablock (DatablockOf Cat048)
db1 = datablock (rec *: rec *: nil)
db2 = datablock (rec *: nil)
pure $ toByteString (unparse @SBuilder db1 <> unparse @SBuilder db2)
-- Dummy tx function
txBytesToTheNetwork :: SBuilder -> IO ()
txBytesToTheNetwork = putStrLn . hexlify . toByteString
main :: IO ()
main = do
sInput <- readBytesFromTheNetwork
let newSacSic :: NonSpare TSacSic
newSacSic = group (1 *: 2 *: nil)
sOutput = rewriteSacSic newSacSic sInput
expected = fromJust $ unhexlify $ "300011900102000000009001020000000030000a90010200000000"
txBytesToTheNetwork sOutput
case expected == (toByteString sOutput) of
True -> print "OK"
False -> error "unexpected output"
Spare bits
Some bits are defined as Spare, which are normally set to 0.
With this library:
- A user is able set spare bits to any value, including abusing spare bits to contain non-zero value.
- When parsing data, tolerate spare bits to contain any value. It is up to the application to check the spare bits if desired.
Multiple spare bit groups can be defined on a single item.
getSpares function returns the actual values of all spare bit groups.
Example
-- | file: readme-samples/spares.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MonoLocalBinds #-}
import Data.Maybe
import Asterix.Coding
import Asterix.Generated as Gen
-- I062/120 contain single group of spare bits
type Spec = Gen.Cat_062_1_20
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
main :: IO ()
main = do
-- create regular record with spare bits set to '0'
let rec1 :: Record (RecordOf Spec)
rec1 = record (item @"120" (group (0 *: item @"MODE2" 0x1234 *: nil))
*: nil)
i120a = fromJust $ getItem @"120" rec1
assert ((bitsToNum <$> getSpares i120a) == [0::Int])
-- create record, abuse spare bits, set to '0xf'
let rec2 :: Record (RecordOf Spec)
rec2 = record (item @"120" (group (0xf *: item @"MODE2" 0x1234 *: nil))
*: nil)
i120b = fromJust $ getItem @"120" rec2
assert ((bitsToNum <$> getSpares i120b) == [0xf::Int])
Reserved expansion (RE) fields
This library supports working with expansion fields. From the Record
prespective, the RE item contains raw bytes, without any structure, similar
to how a datablock contains raw bytes without a structure. Parsing raw
datablocks and parsing records are 2 separate steps. Similarly, parsing RE
out of the record would be a third step. Once parsed, the RE item gets it's
structure, and it's possible to access it's subitems, similar to a regular
record/subitem situation.
When constructing a record with the RE item, a user must first
construct the RE item itself, unparse it to bytes and insert bytes
as a value of the RE item of a record.
A reason for this separate stage approach is that a category and expansion specification can remain separate to one another. In addition, a user has a possiblity to explicitly select both editions individually.
This example demonstrates required steps for constructing and parsing:
-- | file: readme-samples/ref.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Data.Either
import Data.Maybe
import Asterix.Coding
import Asterix.Generated as Gen
type Spec = Gen.Cat_062_1_20
type Ref = Gen.Ref_062_1_3
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
-- create 'RE' subitem
ref :: Expansion (ExpansionOf Ref)
ref = expansion
( item @"CST" (repetitive [0])
*: item @"CSN" (repetitive [1, 2])
*: item @"V3" (compound
( item @"PS3" 0
*: nil ))
*: nil )
-- create record, insert 'RE' subitem
rec :: Record (RecordOf Spec)
rec = record
( item @"010" (group (item @"SAC" 1 *: item @"SIC" 2 *: nil))
*: item @"RE" (explicit ref)
*: nil )
db :: Datablock (DatablockOf Spec)
db = datablock (rec *: nil)
main :: IO ()
main = do
let s = unparse @SBuilder db
bs = toByteString s
expected = fromJust $ unhexlify $ "3e001b8101010104010211c8010000000000020000010000028000"
assert (bs == expected)
-- first stage, parse to the record
let rawDatablocks = fromRight (error "unexpected") (parseRawDatablocks bs)
assert (length rawDatablocks == 1) -- expecting 1 datablock
let rawDatablock = rawDatablocks !! 0
act = parseRecords (schema @(RecordOf Spec) Proxy)
result1 = fromRight (error "unexpected")
(parse @StrictParsing act (getRawRecords rawDatablock))
assert (length result1 == 1) -- expecting one record
let rec2 :: Record (RecordOf Spec)
rec2 = Record (result1 !! 0)
-- get 'RE' subitem,
let reSubitem = getVariation $ fromJust $ getItem @"RE" rec2
reBytes = toByteString $ bitsToBuilder $ getExplicitData reSubitem
-- second stage: parse 'RE' structure
let act2 = parseExpansion (schema @(ExpansionOf Ref) Proxy)
refReadback :: Expansion (ExpansionOf Ref)
refReadback = Expansion (fromRight (error "unexpected")
(parse @StrictParsing act2 reBytes))
-- expecting the same 'ref' as the original
assert (unparse @Bits refReadback == unparse ref)
-- we have a structure back and we can extract the values
let iCsn = fromJust (getItem @"CSN" refReadback)
lst = getRepetitiveItems $ getVariation iCsn
assert (length lst == 2)
assert (asUint @Int (lst !! 0) == 1)
assert (asUint @Int (lst !! 1) == 2)
putStrLn "OK"
Generic asterix processing
Generic processing in this context means working with asterix data where the subitem names and types are determined at runtime. That is: the explicit subitem names are never mentioned in the application source code.
This is in contrast to application specific processing, where we are
explicit about subitems, for example ["010", "SAC"].
Example: Show raw content of all toplevel items of each record
-- | file: readme-samples/generic-names.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Control.Monad
import Data.Word
import Data.Maybe
import Data.Either
import Data.Map as Map
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
specs :: Map Word8 VRecord
specs = Map.fromList
[ (48, schema @(RecordOf Cat_048_1_31) Proxy)
, (62, schema @(RecordOf Cat_062_1_19) Proxy)
, (63, schema @(RecordOf Cat_063_1_6) Proxy)
-- , ...
]
-- some test input bytes
s :: ByteString
s = mconcat $ fmap (fromJust . unhexlify)
[ "3e00a5254327d835a95a0d0a2baf256af940e8a8d0caa1a594e1e525f2e32bc0448b"
, "0e34c0b6211b5847038319d1b88d714b990a6e061589a414209d2e1d00ba5602248e"
, "64092c2a0410138b2c030621c2043080fe06182ee40d2fa51078192cce70e9af5435"
, "aeb2e3c74efc7107052ce9a0a721290cb5b2b566137911b5315fa412250031b95579"
, "03ed2ef47142ed8a79165c82fb803c0e38c7f7d641c1a4a77740960737"
]
handleNonspare :: Word8 -> (GUapItem ValueLevel, Maybe (RecordItem UNonSpare))
-> IO ()
handleNonspare cat = \case
(GUapItem (GNonSpare name _title _rv), Just (RecordItem nsp)) -> do
print (cat, name, debugBits $ unparse @Bits nsp)
-- depending on the application, we might want to display
-- deep subitems, which is possible by examining 'nsp' object
_ -> pure ()
main :: IO ()
main = do
let rawDatablocks = fromRight (error "unexpected") $ parseRawDatablocks s
forM_ rawDatablocks $ \db -> do
let cat = rawDatablockCategory db
case Map.lookup cat specs of
Nothing -> print ("unsupported category", cat)
Just (GRecord sch) -> do
let act = parseRecords (GRecord sch)
records = fromRight (error "unexpected")
(parse @StrictParsing act (getRawRecords db))
forM_ records $ \rec -> do
mapM_ (handleNonspare cat) (zip sch $ uRecItems rec)
Example: Generate dummy single record datablock with all fixed items set to zero
-- | file: readme-samples/generic-zero.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Data.Maybe
import Asterix.Coding
import Asterix.Generated as Gen
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
-- we could even randomly select a category/edition from the 'manifest',
-- but for simplicity just use a particular spec's schema
schAsterix :: VAsterix
schAsterix = schema @Cat_062_1_20 Proxy
schCat :: Int
schCat = case schAsterix of
GAsterixBasic cat _ed _uap -> cat
GAsterixExpansion cat _ed _exp -> cat
schRecord :: VRecord
schRecord = case schAsterix of
GAsterixBasic _cat _ed (GUap r) -> r
_ -> error "unexpected"
schItems :: [VUapItem]
schItems =
let GRecord lst = schRecord
in lst
rec :: URecord
rec = URecord bld items
where
goVar :: VVariation -> Maybe UVariation
goVar = \case
GElement o n _cont -> Just $ UElement $ integerToBits o n 0
GGroup _o lst -> UGroup <$> mapM goItem lst
_ -> Nothing -- skip for this test
goItem :: VItem -> Maybe UItem
goItem = \case
GSpare o n -> Just $ USpare $ integerToBits o n 0
GItem nsp -> UItem <$> goNsp nsp
goRv :: VRule VVariation -> Maybe URuleVar
goRv = \case
GContextFree var -> URuleVar <$> goVar var
GDependent _lst1 var _lst2 -> URuleVar <$> goVar var
goNsp :: VNonSpare -> Maybe UNonSpare
goNsp (GNonSpare _name _title rv) = UNonSpare <$> goRv rv
f :: VUapItem -> Maybe (RecordItem UNonSpare)
f = \case
GUapItem nsp -> RecordItem <$> goNsp nsp
_ -> Nothing
items = fmap f schItems
bld = rebuildRecord items
db :: UDatablock
db = UDatablock bld records
where
records = [(Nothing, rec)]
bld = datablockBuilder schCat (fmap snd records)
main :: IO ()
main = do
let expected = fromJust $ unhexlify $ "3e0038bfe9bd5000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
result = toByteString $ unparse @SBuilder db
putStrLn $ hexlify result
assert (result == expected)
Library manifest
This library defines a manifest structure in the form:
manifest :: [GAsterix 'ValueLevel]
manifest =
[ schema @Cat_001_1_2 Proxy
, schema @Cat_001_1_3 Proxy
, schema @Cat_001_1_4 Proxy
-- ...
]
This structure can be used to extract latest editions for each defined category, for example:
-- | file: readme-samples/generic-latest.hs
import Control.Monad
import Data.List (sort)
import Data.Map as Map
import Data.Map.Merge.Lazy as Map
import Asterix.Schema
import Asterix.Generated as Gen
latest :: Map VInt VEdition
latest = Prelude.foldr f mempty Gen.manifest
where
f :: VAsterix -> Map VInt VEdition -> Map VInt VEdition
f sch acc = case sch of
GAsterixBasic cat ed _uap -> Map.merge
preserveMissing
preserveMissing
(zipWithMatched (\_key ed1 ed2 -> max ed1 ed2))
acc
(Map.singleton cat ed)
GAsterixExpansion _cat _ed _exp -> acc
main :: IO ()
main = forM_ (sort (Map.keys latest)) $ \cat -> do
print (cat, latest Map.! cat)
Alternatively, a prefered way is to be explicit about each edition, for example:
-- | file: readme-samples/generic-editon.hs
import Data.List (sort)
import Data.Map as Map
import Asterix.Schema
import Asterix.Generated as Gen
specs :: Map VInt VAsterix
specs = Map.fromList
[ (48, schema @Cat_048_1_31 Proxy)
, (62, schema @Cat_062_1_19 Proxy)
, (63, schema @Cat_063_1_6 Proxy)
-- , ...
]
main :: IO ()
main = mapM_ print (sort $ Map.keys specs)
Error handling
Some operation (eg. parsing) can fail on unexpected input. In such case,
this library returns ParsingError.
parseDataBlocks :: ByteString -> [RawDatablock]
parseDataBlocks s = case parseRawDatablocks s of
Left (ParsingError _err) -> [] -- decide what to do in case of error
Right val -> val
For clarity, the error handling part is skipped in some parts of this tutorial.
Miscellaneous project and source code remarks
A core part of this project is the Asterix.Generated module, where
all important aspect of asterix data format is captured as haskell types.
Having asterix specifications defined as types is important, to be able to
catch errors at compile time (e.g. compiler can detect access attempt
into unspecified item).
The specifications are available at runtime too. For example,
we want to be able to generate random record of some category/edition or
generically convert binary asterix data to json. So, each type level
specification is converted into a value level counterpart.
The Asterix.Schema provides the necessary conversion function schema
from types to term:
valueLevel = schema @typeLevel Proxy
Schema naming conventions
Naming conventions for types describing asterix schema:
GType u - generic data structure, parametrized over 'usecase',
to be used on a type and value level
TType - type TType = GType 'TypeLevel (all generated types)
VType - type VType = GType 'ValueLevel (TType converted to value level)
See Asterix.Schema module for details.
Data naming conventions
For types containing actual asterix data.
UType - Untyped value, for example UItem, UVariation, ...
Type t - Typed wrapper around Untyped value: (Item t), (Variation t), ...
Constructing
A simplified syntax is provided to construct asterix objects within the source
code. It's based on processing HList of subitems (list of different types).
'HList' constuction is performed with *: as HCons operator and nil as
list termination HNil. For example:
-- | file: readme-samples/construct1.hs
{-# LANGUAGE DataKinds #-}
import Data.Maybe
import Asterix.Coding
import Asterix.Generated as Gen
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
db062 :: Datablock (DatablockOf Cat_062_1_21)
db062 = datablock (rec1 *: nil)
where
rec1 = record
( item @"010" 0x0102
*: item @"120" ( group
( spare
*: item @"MODE2" (string "1234") -- octal string
*: nil ))
*: item @"380" ( compound
( item @"ID" (string "ICAO") -- icao string
*: nil ))
*: item @"070" (quantity @"s" 123.4 ) -- quantity with units
*: nil )
main :: IO ()
main = do
let sb :: SBuilder = unparse db062
result = toByteString sb
expected = fromJust $ unhexlify $ "3e0015911101100102003db34024304f820820029c"
assert (result == expected)
putStrLn $ hexlify result
Parsing
Regular asterix parsing with this library is performed in the following stages:
- Parsing datagram (
ByteStringas received on the network) toRawDatablocks. ARawDatablockrepresents input data which is correctly parsed, according to asterix datablock cat/length schema. - Depending on asterix category, each
RawDatablockcan be either skipped or parsed to the next level, resulting in list of records. - Once the records are parsed according to a particular UAP schema,
each record is normally wrapped inside typed
Record t, such that a content structure is statically known and the subitems can be accessed by name (at the type level), using type application.
Parsing is performed using parse function, for example:
-- | file: readme-samples/parsing-normal.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
type Cat034 = Gen.Cat_034_1_29
db :: Datablock (DatablockOf Cat034)
db = datablock (record (item @"000" 1 *: nil ) *: nil)
sample :: ByteString
sample = toByteString $ unparse @SBuilder db
main :: IO ()
main = do
let rawDatablocks :: [RawDatablock]
rawDatablocks = case parseRawDatablocks sample of
Left (ParsingError _) -> error "can not parse raw datablocks"
Right val -> val
rawDatablock :: RawDatablock = rawDatablocks !! 0
rawRecords = case rawDatablockCategory rawDatablock of
34 -> getRawRecords rawDatablock
_ -> error "unexpected category"
parsingAction = parseRecords (schema @(RecordOf Cat034) Proxy)
records :: [Record (RecordOf Cat034)]
records = case parse @StrictParsing parsingAction rawRecords of
Left (ParsingError _) -> error "can not parse records"
Right lst -> fmap Record lst
record0 = records !! 0
i000 = case getItem @"000" record0 of
Nothing -> error "Missing item 000"
Just val -> val
case asUint @Integer i000 of
1 -> print "OK"
_ -> error "unexpected result"
In some cases, the parsing result type is not 'a priori' known and the second stage of parsing becomes more complicated.
Unparsing
Asterix.Base module provides class Unparsing r t for types that can be
unparsed into target value, such as Bits or SBuilder. Unparsing into
regular ByteString is not efficient (a problem is slow ByteString
concatenation) and so the instances are not provided. It is however possible to
(inefficiently) convert from SBuilder to ByteString if necessary for debug
purposes.
The target type argument in a typeclass comes first, for simplified type application when necessary, for example:
instance Unparsing Bits UItem
instance Unparsing Bits (Item t)
instance Unparsing SBuilder (Record t)
let s1 = unparse @Bits item
s2 = unparse @SBuilder record
-- explicit type application is not required here
print $ debugBits $ unparse record
Rare asterix cases
Dependent specifications
In some rare cases, asterix definitions depend on a value of some other item(s). In such cases, the asterix processing is more involved. This dependency manifests itself in two ways:
- content dependency, where a content (interpretation of bits) of some
item depends on the value of some other item(s). For example:
I062/380/IAS/IAS, the structure is always 15 bits long, but the interpretation of bits could be either speed inNM/sorMach, with different scaling factors, depending on the values of a sibling item. - variation dependency, where not only the content, but also a complete
item stucture depends on some other item(s). For example, the structure
of item
I004/120/CC/CPCdepends on 2 other item values.
This library can handle all structure cases, however it does not automatically correlate to the values of items that a structure depends on. When creating records, it is a user responsibility to properly set "other item values" for a record to be valid. Similarly, after a record is parsed, a user shall cast a default structure to a target structure, depending on the other items values. Whenever there is a dependency, there is also a statically known default structure, which is used during automatic record parsing.
Handling content dependency
This example demonstrates how to work with content dependency,
such as I062/380/IAS.
-- | file: readme-samples/dep-content.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Control.Monad
import Data.Maybe
import Data.Either
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
type Spec = Gen.Cat_062_1_20
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
-- create records by different methods
-- set raw value
rec0 :: Record (RecordOf Spec)
rec0 = record
( item @"380" (compound
( item @"IAS" (group
( item @"IM" 0 -- set IM to 0
*: item @"IAS" 1 -- set IAS to raw value 1 (no unit conversion)
*: nil))
*: nil))
*: nil )
-- set raw value using default case
rec1 :: Record (RecordOf Spec)
rec1 = record
( item @"380" (compound
( item @"IAS" (group
( item @"IM" 0 -- set IM to 0
*: item @"IAS" 1 -- same as above
*: nil))
*: nil))
*: nil )
-- set IAS speed (NM/s)
rec2 :: Record (RecordOf Spec)
rec2 = record
( item @"380" (compound
( item @"IAS" (group
( item @"IM" 0 -- set IM to 0
-- use case with index 0 (IAS), set IAS to 1.2 NM/s
*: item @"IAS" (quantity @"NM/s" @('Just 0) 1.2)
*: nil))
*: nil))
*: nil )
-- set Mach speed
rec3 :: Record (RecordOf Spec)
rec3 = record
( item @"380" (compound
( item @"IAS" (group
( item @"IM" 1 -- set IM to 1 (Mach)
-- use case with index 1 (Mach), set IAS to 0.8 Mach
*: item @"IAS" (quantity @"Mach" @('Just 1) 0.8)
*: nil))
*: nil))
*: nil )
db0 :: Datablock (DatablockOf Spec)
db0 = datablock (rec0 *: rec1 *: rec2 *: rec3 *: nil)
expected :: ByteString
expected = fromJust $ unhexlify "3e0017011010000101101000010110104ccd0110108320"
main :: IO ()
main = do
assert ((toByteString $ unparse @SBuilder db0) == expected)
-- parse and interpret data from the example above
let rx = expected
rawDatablocks = fromRight (error "unexpected") (parseRawDatablocks rx)
forM_ rawDatablocks $ \db -> do
assert (rawDatablockCategory db == 62)
let act = parseRecords (schema @(RecordOf Spec) Proxy)
records :: [Record (RecordOf Spec)]
records = fromRight (error "unexpected")
(fmap Record <$> parse @StrictParsing act (getRawRecords db))
forM_ (zip [0::Int ..] records) $ \(cnt, rec) -> do
let i380 = fromJust $ getItem @"380" rec
iIAS1 = fromJust $ getItem @"IAS" $ getVariation i380
iIM = getItem @"IM" iIAS1
iIAS2 = getItem @"IAS" iIAS1
value :: Double
value = case asUint @Int iIM of
-- this is IAS, convert to 'NM/s', use case with index (0,)
0 -> unQuantity $ asQuantity @"NM/s" @('Just 0) iIAS2
-- this is Mach, convert to 'Mach', use case with index (1,)
1 -> unQuantity $ asQuantity @"Mach" @('Just 1) iIAS2
_ -> error "unexpected value"
print ("--- record", cnt, "---")
print ("I062/380/IAS/IM raw value:", asUint @Integer iIM)
print ("I062/380/IAS/IAS raw value:", asUint @Integer iIAS2)
print ("converted value", value)
Handling variation dependency
This example demonstrates how to work with variation dependency,
such as I004/120/CC/CPC.
-- | file: readme-samples/dep-variation.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
import Control.Monad
import Data.Maybe
import Data.Either
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
type Spec = Gen.Cat_004_1_13 -- Cat 004, edition 1.13
-- Item 'I004/120/CC/CPC' depends on I004/000 and I004/120/CC/TID values
-- Default case is: element3, raw, but there are many other cases.
-- See asterix specification for details.
-- This example handles the following cases:
-- case (5, 1): element 3, table
-- case (9, 2): group (('RAS', element1, table), spare 2)
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
-- case (0, 0) - invalid combination
rec0 :: Record (RecordOf Spec)
rec0 = record
( item @"000" 0 -- invalid value
*: item @"120" (compound
( item @"CC" (group
( item @"TID" 0
*: item @"CPC" 0 -- set to raw value 0
*: item @"CS" 0
*: nil))
*: nil))
*: nil)
-- case (5, 1)
rec1 :: Record (RecordOf Spec)
rec1 = record
( item @"000" 5 -- Area Proximity Warning (APW)
*: item @"120" (compound
( item @"CC" (group
( item @"TID" 1
*: item @"CPC" 0 -- set to raw value 0
*: item @"CS" 0
*: nil))
*: nil))
*: nil)
-- case (9, 2)
-- get variation structure of case (9, 2)
-- and create object of that structure ('RAS' + spare item)
obj :: Variation (DepRule (Spec ~> "120" ~> "CC" ~> "CPC") '[ 9, 2])
obj = group (item @"RAS" 1 *: spare *: nil)
rec2 :: Record (RecordOf Spec)
rec2 = record
( item @"000" 9 -- RIMCAS Arrival / Landing Monitor (ALM)
*: item @"120" (compound
( item @"CC" (group
( item @"TID" 2
*: item @"CPC" (fromInteger $ asUint obj)
*: item @"CS" 0
*: nil))
*: nil))
*: nil)
db0 :: Datablock (DatablockOf Spec)
db0 = datablock (rec0 *: rec1 *: rec2 *: nil)
expected :: ByteString
expected = fromJust $ unhexlify "040012412000400041200540104120094028"
main :: IO ()
main = do
assert ((toByteString $ unparse @SBuilder db0) == expected)
-- parse and interpret data from the example above
let rx = expected
rawDatablocks = fromRight (error "unexpected") (parseRawDatablocks rx)
forM_ rawDatablocks $ \db -> do
assert (rawDatablockCategory db == 4)
let act = parseRecords (schema @(RecordOf Spec) Proxy)
records :: [Record (RecordOf Spec)]
records = fromRight (error "unexpected")
(fmap Record <$> parse @StrictParsing act (getRawRecords db))
forM_ (zip [0::Int ..] records) $ \(cnt, rec) -> do
print ("--- record", cnt, "---")
let i000 = fromJust $ getItem @"000" rec
i120 = fromJust $ getItem @"120" rec
iCC = fromJust $ getItem @"CC" $ getVariation i120
iTID = asUint @Int (getItem @"TID" iCC)
iCPC = getItem @"CPC" iCC
_iCS = asUint @Int (getItem @"CS" iCC)
index = (asUint @Int i000, iTID)
value = case index of
(5, 1) ->
let x = asUint @Int iCPC
in Just ("case 5,1 raw " <> show x)
(9, 2) ->
let fromRight' = fromRight (error "unexpected")
varCPC = fromRight' $ getDepVariation @'[ 9, 2] iCPC
ras = asUint @Int (getItem @"RAS" varCPC)
spares = asUint @Int <$> getSpares varCPC
in Just ("case 9,2 RAS "
<> show ras <> ", " <> show spares)
_ -> Nothing :: Maybe String
print value
Multiple UAP categories
With multiple UAP categories, it is in general not possible to unambiguously determine parsing success or failure result. In this case, a user has the following options:
- try to parse all possible UAP combinations and post-process results
- enforce parsing according to particular UAP and recover unambiguously parsing result (success or failure)
Trying all possible combinations
This kind of parsing is provided by parseRecordsTry function.
In this case, the result is a 'list of possible parsing results', where
- An empty list represents parsing failure.
- Single element list is the actual result, which is normally expected. The (one) element of a list is itself a list of records.
- Multi element list are all valid parsing results, library user shall decide what to do with multiple results. Typically a user might want to check each record in turn if it actually represents a valid record, based on the content of particular subitems.
For example, cat001 defines plot and track UAPs. Parsing of a particular
input string might be (all valid at parsing stage):
Datagram of [plot, plot, plot]Datagram of [track, track, track]Datagram of [plot, track, track]- ... and so on
Library user might examine each record (after parsing stage), to determine if all records are actually valid, according to the subitem content. With this additional step, some parsing solutions might be rejected and with some luck, there is only one remaining result.
Example:
-- | file: readme-samples/parsing-cat001-try.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
import Control.Monad
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
type Cat001 = Gen.Cat_001_1_4
recPlot :: Record (RecordOfUap Cat001 "plot")
recPlot = record
( item @"020" (extended
( item @"TYP" 0 *: 0 *: 0 *: 0 *: 0 *: 0 *: fx *: nil))
*: nil )
recTrack :: Record (RecordOfUap Cat001 "track")
recTrack = record
( item @"020" (extended
( item @"TYP" 1 *: 0 *: 0 *: 0 *: 0 *: 0 *: fx *: nil))
*: nil )
db :: Datablock (DatablockOf Cat001)
db = datablock ( recPlot *: recTrack *: nil)
sample :: ByteString
sample = toByteString $ unparse @SBuilder db
handlePlot :: Record (RecordOfUap Cat001 "plot") -> IO ()
handlePlot _rec = putStrLn "got plot"
handleTrack :: Record (RecordOfUap Cat001 "track") -> IO ()
handleTrack _rec = putStrLn "got track"
main :: IO ()
main = do
let rawDatablocks :: [RawDatablock]
rawDatablocks = case parseRawDatablocks sample of
Left (ParsingError _) -> error "can not parse raw datablocks"
Right val -> val
rawDatablock :: RawDatablock = rawDatablocks !! 0
rawRecords = case rawDatablockCategory rawDatablock of
1 -> getRawRecords rawDatablock
_ -> error "unexpected category"
parsingAction = parseRecordsTry (Just 10) (schema @Cat001 Proxy)
results = case parse @StrictParsing parsingAction rawRecords of
Left (ParsingError _) -> error "unexpected parse failure"
Right val -> val
case length results of
4 -> pure ()
_ -> error "unexpected length of results"
forM_ results $ \result -> do
putStrLn "possible result"
forM_ result $ \(name, rec) -> case name of
"plot" -> handlePlot (Record rec)
"track" -> handleTrack (Record rec)
_ -> error "unexpected record type"
Enforce parsing according to a particular UAP*
When the input is 'known' to contain only 'tracks' for example, a user can enforce parsing to try only that UAP and avoid additional processing stage. In this case, the situation is similar to the regular single UAP parsing. Example:
-- | file: readme-samples/parsing-cat001-tracks.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
type Cat001 = Gen.Cat_001_1_4
recTrack :: Record (RecordOfUap Cat001 "track")
recTrack = record
( item @"020" (extended
( item @"TYP" 1 *: 0 *: 0 *: 0 *: 0 *: 0 *: fx *: nil))
*: nil )
db :: Datablock (DatablockOf Cat001)
db = datablock ( recTrack *: recTrack *: nil)
sample :: ByteString
sample = toByteString $ unparse @SBuilder db
handleTrack :: Record (RecordOfUap Cat001 "track") -> IO ()
handleTrack _rec = putStrLn "got track"
main :: IO ()
main = do
let rawDatablocks :: [RawDatablock]
rawDatablocks = case parseRawDatablocks sample of
Left (ParsingError _) -> error "can not parse raw datablocks"
Right val -> val
rawDatablock :: RawDatablock = rawDatablocks !! 0
rawRecords = case rawDatablockCategory rawDatablock of
1 -> getRawRecords rawDatablock
_ -> error "unexpected category"
parsingAction = parseRecords (schema @(RecordOfUap Cat001 "track") Proxy)
records = case parse @StrictParsing parsingAction rawRecords of
Left (ParsingError _) -> error "can not parse records"
Right lst -> fmap Record lst
mapM_ handleTrack records
RFS handling
This library supports RFS mechanism for categories that include RFS
indicators. For such cases, it is possible to sequence subitems in
any order. Once such record is created or parsed, a user can extract
subitems using getRfsItem function. The result in this case is
a list, since the item can be present in the record multiple times.
An empty list indicates that no such item is present in the RFS.
Example
-- | file: readme-samples/rfs.hs
{-# LANGUAGE DataKinds #-}
import Control.Monad
import Data.Maybe
import Asterix.Coding
import Asterix.Generated as Gen
-- cat008 contains RFS indicator, so we are able to add RFS items
type Spec = Gen.Cat_008_1_3
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
rec1 :: Record (RecordOf Spec)
rec1 = record
-- add some regular items
( item @"000" 1
*: item @"010" (group (item @"SAC" 1 *: item @"SIC" 2 *: nil))
-- add items as RFS (may repeat)
*: rfs
( item @"010" (group (item @"SAC" 1 *: item @"SIC" 2 *: nil))
*: item @"010" (group (item @"SAC" 1 *: item @"SIC" 2 *: nil))
*: nil)
*: nil )
main :: IO ()
main = do
-- extract regular item 010
let i010Regular = fromJust $ getItem @"010" rec1
putStrLn $ debugBits $ unparse @Bits i010Regular
-- extract RFS items 010, expecting 2 such items
let i010Rfs = getRfsItem @"010" rec1
assert (length i010Rfs == 2)
forM_ i010Rfs $ \i -> do
putStrLn $ debugBits $ unparse @Bits i
-- but item '000' is not present in RFS
assert (length (getRfsItem @"000" rec1) == 0)
Strict and partial record parsing modes
This library supports parsing records strictly or partially.
In a strict mode, we want to make sure that all data is parsed exactly as specified in the particular category/edition schema. The record parsing fails if the FSPEC parsing fails or if any subsequent item parsing fails.
In a partial mode, we don't require exact parsing match. If we know where in a bytestring a record starts, we can try to parse some information out of the data stream, even in the case if the editions of the transmitter and the receiver do not match exactly. In particular: if the transmitter sends some additional items, unknown to the receiver. In that case, the receiver can still parse up to some point in a datablock.
Partial record parsing means to parse the FSPEC (which might fail) followed by parsing subitems up to the point until items parsing is successful. The record parsing only fails if the FSPEC parsing itself fails.
This is useful in situations where a datablock contains only one record (known as non-blocking in Asterix Maintenance Group vocabulary) or if we are interested only in the first record (even if there are more). The idea is to regain some forward compatibility on the receiver side, such that the receiver does not need to upgrade edition immediately as the transmitter upgrades or even before that. Whether this is safe or not, depends on the application and the exact differences between transmitter and receiver asterix editions.
The following parsing methods exist:
data ParsingMode
= StrictParsing
| PartialParsing
This example demonstrates both parsing modes:
-- | file: readme-samples/parsing-partial-mode.hs
{-# LANGUAGE DataKinds #-}
import Data.Maybe
import Data.Either
import Data.ByteString (ByteString)
import Asterix.Coding
import Asterix.Generated as Gen
type SpecOld = Gen.Cat_063_1_6
type SpecNew = Gen.Cat_063_1_7
assert :: Bool -> IO ()
assert True = pure ()
assert False = error "Assertion error"
-- In the new spec, item 060 is extended to contain 3 groups,
-- while in the old spec it only contain2 groups.
-- Create record according to the new spec
rec0 :: Record (RecordOf SpecNew)
rec0 = record
( item @"010" (group (item @"SAC" 1 *: item @"SIC" 2 *: nil))
*: item @"015" 3
*: item @"060" (extendedGroups (1 *: 2 *: 3 *: nil))
*: nil )
-- This bytestring represents the record of SpecNew
bs :: ByteString
bs = toByteString $ unparse @SBuilder rec0
main :: IO ()
main = do
let expected = fromJust $ unhexlify "c8010203030506"
assert (bs == expected)
-- We should be able to parse the record, using the new spec
-- and get the same record back.
let act1 = parseRecord (schema @(RecordOf SpecNew) Proxy)
rec1 = fromRight (error "unexpected") (parse @StrictParsing act1 bs)
assert (unparse @Bits rec1 == unparse rec0)
-- Strict parsing with the old spec fails.
let act2 = parseRecord (schema @(RecordOf SpecOld) Proxy)
rec2 = parse @StrictParsing act2 bs
assert $ isLeft rec2
-- However, we can still try to parse using the PartialParsing mode.
let act3 = parseRecord (schema @(RecordOf SpecOld) Proxy)
rec3 :: Record (RecordOf SpecOld)
rec3 = Record $ fromRight (error "unexpected")
(parse @PartialParsing act3 bs)
-- We accept the fact that resulting record might not be complete, but
-- items "010" and "015" are valid, even if parsing using the old edition.
let i010 = fromJust $ getItem @"010" rec3
i015 = fromJust $ getItem @"015" rec3
assert (asUint @Int i010 == 0x0102)
assert (asUint @Int i015 == 3)
-- Note, that the result in this case in not equal to the original record.
assert (unparse @Bits rec3 /= unparse rec0)
Unit tests
For more examples using test specifications, see also project repository unit tests.