mu-avro-0.2.0.0: src/Mu/Quasi/Avro.hs
{-# language DataKinds #-}
{-# language LambdaCase #-}
{-# language NamedFieldPuns #-}
{-# language OverloadedStrings #-}
{-# language TemplateHaskell #-}
{-# language ViewPatterns #-}
{-|
Description : Quasi-quoters for Avro IDL format
This module turns schema definitions written in
<https://avro.apache.org/docs/current/idl.html Avro IDL>
into Mu 'Schema's. We provide versions for writing
the IDL inline ('avro') and import it from a file
('avroFile').
/Note/: as of now, only the JSON-based IDL format
is supported, not the Java-like one.
-}
module Mu.Quasi.Avro (
-- * Service generation from @.avdl@ files
avdl
-- * Quasi-quoters for @.avsc@ files
, avro
, avroFile
-- * Only for internal use
, schemaFromAvroType
) where
import Control.Monad.IO.Class
import Data.Aeson (decode)
import qualified Data.Avro.Schema as A
import Data.Avro.Types.Decimal as D
import qualified Data.ByteString as B
import Data.ByteString.Lazy.Char8 (pack)
import Data.Int
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Time
import Data.Time.Millis
import Data.UUID
import qualified Data.Vector as V
import Language.Avro.Parser
import qualified Language.Avro.Types as A
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Mu.Rpc
import Mu.Schema.Definition
-- | Imports an avro definition written in-line as a 'Schema'.
avro :: QuasiQuoter
avro =
QuasiQuoter
(const $ fail "cannot use as expression")
(const $ fail "cannot use as pattern")
schemaFromAvroString
(const $ fail "cannot use as declaration")
where
schemaFromAvroString :: String -> Q Type
schemaFromAvroString s =
case decode (pack s) of
Nothing -> fail "could not parse avro spec!"
Just (A.Union us) -> schemaFromAvro (V.toList us)
Just t -> schemaFromAvro [t]
-- | Imports an avro definition from a file as a 'Schema'.
avroFile :: QuasiQuoter
avroFile = quoteFile avro
-- | Reads a @.proto@ file and generates:
-- * A 'Mu.Schema.Definition.Schema' with all the message
-- types, using the name given as first argument.
-- * A 'Service' declaration for each service in the file,
-- where the name is obtained by applying the function
-- given as second argument to the name in the file.
avdl :: String -> String -> FilePath -> FilePath -> Q [Dec]
avdl schemaName serviceName baseDir initialFile
= do r <- liftIO $ readWithImports baseDir initialFile
case r of
Left e
-> fail ("could not parse protocol buffers spec: " ++ show e)
Right p
-> avdlToDecls schemaName serviceName p
avdlToDecls :: String -> String -> A.Protocol -> Q [Dec]
avdlToDecls schemaName serviceName protocol
= do let schemaName' = mkName schemaName
serviceName' = mkName serviceName
schemaDec <- tySynD schemaName' [] (schemaFromAvro $ S.toList (A.types protocol))
serviceDec <- tySynD serviceName' []
[t| 'Service $(textToStrLit (A.pname protocol)) $(pkgType (A.ns protocol))
$(typesToList <$> mapM (avroMethodToType schemaName') (S.toList $ A.messages protocol)) |]
return [schemaDec, serviceDec]
where
pkgType Nothing = [t| '[] |]
pkgType (Just (A.Namespace p))
= [t| '[ Package $(textToStrLit (T.intercalate "." p)) ] |]
schemaFromAvro :: [A.Schema] -> Q Type
schemaFromAvro =
(typesToList <$>) . mapM schemaDecFromAvroType . flattenAvroDecls
schemaDecFromAvroType :: A.Schema -> Q Type
schemaDecFromAvroType (A.Record name _ _ _ fields) =
[t|'DRecord $(textToStrLit $ A.baseName name)
$(typesToList <$> mapM avroFieldToType fields)|]
where
avroFieldToType :: A.Field -> Q Type
avroFieldToType field =
[t|'FieldDef $(textToStrLit $ A.fldName field)
$(schemaFromAvroType $ A.fldType field)|]
schemaDecFromAvroType (A.Enum name _ _ symbols) =
[t|'DEnum $(textToStrLit $ A.baseName name)
$(typesToList <$> mapM avChoiceToType (V.toList symbols))|]
where
avChoiceToType :: T.Text -> Q Type
avChoiceToType c = [t|'ChoiceDef $(textToStrLit c)|]
schemaDecFromAvroType t = [t|'DSimple $(schemaFromAvroType t)|]
-- | Turns a schema from Avro into a Template Haskell 'Type'.
schemaFromAvroType :: A.Schema -> Q Type
schemaFromAvroType =
\case
A.Null -> [t|'TPrimitive 'TNull|]
A.Boolean -> [t|'TPrimitive Bool|]
A.Int (Just A.Date) -> [t|'TPrimitive Day|]
A.Int (Just A.TimeMillis) -> [t|'TPrimitive DiffTimeMs|]
A.Int _ -> [t|'TPrimitive Int32|]
A.Long (Just (A.DecimalL (A.Decimal p s)))
-> [t|'TPrimitive (D.Decimal $(litT $ numTyLit p) $(litT $ numTyLit s)) |]
A.Long (Just A.TimeMicros) -> [t|'TPrimitive DiffTime|]
A.Long _ -> [t|'TPrimitive Int64|]
A.Float -> [t|'TPrimitive Float|]
A.Double -> [t|'TPrimitive Double|]
A.Bytes _ -> [t|'TPrimitive B.ByteString|]
A.String (Just A.UUID) -> [t|'TPrimitive UUID|]
A.String _ -> [t|'TPrimitive T.Text|]
A.Array item -> [t|'TList $(schemaFromAvroType item)|]
A.Map values -> [t|'TMap T.Text $(schemaFromAvroType values)|]
A.NamedType typeName ->
[t|'TSchematic $(textToStrLit (A.baseName typeName))|]
A.Enum {} -> fail "should never happen, please, file an issue"
A.Record {} -> fail "should never happen, please, file an issue"
A.Union options ->
case V.toList options of
[A.Null, x] -> toOption x
[x, A.Null] -> toOption x
_ ->
[t|'TUnion $(typesToList <$> mapM schemaFromAvroType (V.toList options))|]
where toOption x = [t|'TOption $(schemaFromAvroType x)|]
A.Fixed {} -> fail "fixed integers are not currently supported"
flattenAvroDecls :: [A.Schema] -> [A.Schema]
flattenAvroDecls = concatMap (uncurry (:) . flattenDecl)
where
flattenDecl :: A.Schema -> (A.Schema, [A.Schema])
flattenDecl (A.Record name a d o fields) =
let (flds, tts) = unzip (flattenAvroField <$> fields)
in (A.Record name a d o flds, concat tts)
flattenDecl (A.Union _) = error "should never happen, please, file an issue"
flattenDecl t = (t, [])
flattenAvroType :: A.Schema -> (A.Schema, [A.Schema])
flattenAvroType (A.Record name a d o fields) =
let (flds, tts) = unzip (flattenAvroField <$> fields)
in (A.NamedType name, A.Record name a d o flds : concat tts)
flattenAvroType (A.Union (V.toList -> ts)) =
let (us, tts) = unzip (map flattenAvroType ts)
in (A.Union $ V.fromList us, concat tts)
flattenAvroType e@A.Enum {A.name} = (A.NamedType name, [e])
flattenAvroType t = (t, [])
flattenAvroField :: A.Field -> (A.Field, [A.Schema])
flattenAvroField f =
let (t, decs) = flattenAvroType (A.fldType f)
in (f {A.fldType = t}, decs)
avroMethodToType :: Name -> A.Method -> Q Type
avroMethodToType schemaName m
= [t| 'Method $(textToStrLit (A.mname m)) '[]
$(typesToList <$> mapM argToType (A.args m))
$(retToType (A.result m)) |]
where
argToType :: A.Argument -> Q Type
argToType (A.Argument (A.NamedType a) _)
= [t| 'ArgSingle ('ViaSchema $(conT schemaName) $(textToStrLit (A.baseName a))) |]
argToType (A.Argument _ _)
= fail "only named types may be used as arguments"
retToType :: A.Schema -> Q Type
retToType A.Null
= [t| 'RetNothing |]
retToType (A.NamedType a)
= [t| 'RetSingle ('ViaSchema $(conT schemaName) $(textToStrLit (A.baseName a))) |]
retToType _
= fail "only named types may be used as results"
typesToList :: [Type] -> Type
typesToList = foldr (\y ys -> AppT (AppT PromotedConsT y) ys) PromotedNilT
textToStrLit :: T.Text -> Q Type
textToStrLit s = return $ LitT $ StrTyLit $ T.unpack s