dhall-openapi-1.0.0: openapi-to-dhall/Main.hs
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RecordWildCards #-}
module Main (main) where
import Control.Applicative.Combinators (option, sepBy1)
import Data.Aeson (decodeFileStrict, eitherDecodeFileStrict)
import Data.Bifunctor (bimap)
import Data.Foldable (for_)
import Data.Text (Text, pack, unpack)
import Data.Void (Void)
import Dhall.Core (Expr (..))
import Dhall.Format (Format (..))
import Numeric.Natural (Natural)
import Text.Megaparsec
( Parsec
, errorBundlePretty
, parse
, some
, optional
, (<|>)
, eof
)
import Text.Megaparsec.Char (alphaNumChar, char)
import Dhall.Kubernetes.Data (patchCyclicImports)
import Dhall.Kubernetes.Types
( DuplicateHandler
, ModelName (..)
, ModelHierarchy
, Prefix
, Swagger (..)
)
import System.FilePath ((</>))
import qualified Data.List as List
import qualified Data.Map.Strict as Data.Map
import qualified Data.Ord as Ord
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import qualified Data.Text.Prettyprint.Doc as Pretty
import qualified Data.Text.Prettyprint.Doc.Render.Text as PrettyText
import qualified Dhall.Core as Dhall
import qualified Dhall.Format
import qualified Dhall.Kubernetes.Convert as Convert
import qualified Dhall.Kubernetes.Types as Types
import qualified Dhall.Map
import qualified Dhall.Parser
import qualified Dhall.Util
import qualified GHC.IO.Encoding
import qualified Options.Applicative
import qualified System.IO
import qualified System.Directory as Directory
import qualified Text.Megaparsec as Megaparsec
import qualified Text.Megaparsec.Char.Lexer as Megaparsec.Lexer
-- | Top-level program options
data Options = Options
{ skipDuplicates :: Bool
, prefixMap :: Data.Map.Map Prefix Dhall.Import
, splits :: Data.Map.Map ModelHierarchy (Maybe ModelName)
, filename :: String
, crd :: Bool
}
-- | Write and format a Dhall expression to a file
writeDhall :: FilePath -> Types.Expr -> IO ()
writeDhall path expr = do
putStrLn $ "Writing file '" <> path <> "'"
Text.writeFile path $ pretty expr <> "\n"
let chosenCharacterSet = Nothing -- Infer from input
let censor = Dhall.Util.NoCensor
let outputMode = Dhall.Util.Write
let input =
Dhall.Util.PossiblyTransitiveInputFile
path
Dhall.Util.NonTransitive
let formatOptions = Dhall.Format.Format{..}
Dhall.Format.format formatOptions
-- | Pretty print things
pretty :: Pretty.Pretty a => a -> Text
pretty = PrettyText.renderStrict
. Pretty.layoutPretty Pretty.defaultLayoutOptions
. Pretty.pretty
data Stability = Alpha Natural | Beta Natural | Production deriving (Eq, Ord)
data Version = Version
{ stability :: Stability
, version :: Natural
} deriving (Eq, Ord)
parseStability :: Parsec Void Text Stability
parseStability = parseAlpha <|> parseBeta <|> parseProduction
where
parseAlpha = do
_ <- "alpha"
n <- Megaparsec.Lexer.decimal
return (Alpha n)
parseBeta = do
_ <- "beta"
n <- Megaparsec.Lexer.decimal
return (Beta n)
parseProduction = do
return Production
parseVersion :: Parsec Void Text Version
parseVersion = Megaparsec.try parseSuffix <|> parsePrefix
where
parseComponent = do
Megaparsec.takeWhile1P (Just "not a period") (/= '.')
parseSuffix = do
_ <- "v"
version <- Megaparsec.Lexer.decimal
stability <- parseStability
_ <- "."
_ <- parseComponent
Megaparsec.eof
return Version{..}
parsePrefix = do
_ <- parseComponent
_ <- "."
parseVersion
getVersion :: ModelName -> Maybe Version
getVersion ModelName{..} =
case Megaparsec.parse parseVersion "" unModelName of
Left _ -> Nothing
Right version -> Just version
-- https://github.com/dhall-lang/dhall-kubernetes/issues/112
data Autoscaling = AutoscalingV1 | AutoscalingV2beta1 | AutoscalingV2beta2
deriving (Eq, Ord)
getAutoscaling :: ModelName -> Maybe Autoscaling
getAutoscaling ModelName{..}
| Text.isPrefixOf "io.k8s.api.autoscaling.v1" unModelName =
Just AutoscalingV1
| Text.isPrefixOf "io.k8s.api.autoscaling.v2beta1" unModelName =
Just AutoscalingV2beta1
| Text.isPrefixOf "io.k8s.api.autoscaling.v2beta2" unModelName =
Just AutoscalingV2beta2
| otherwise =
Nothing
isK8sNative :: ModelName -> Bool
isK8sNative ModelName{..} = Text.isPrefixOf "io.k8s." unModelName
preferStableResource :: DuplicateHandler
preferStableResource (_, names) = do
let issue112 = Ord.comparing getAutoscaling
let k8sOverCrd = Ord.comparing isK8sNative
let defaultComparison = Ord.comparing getVersion
let comparison = issue112 <> k8sOverCrd <> defaultComparison
return (List.maximumBy comparison names)
skipDuplicatesHandler :: DuplicateHandler
skipDuplicatesHandler = const Nothing
parseImport :: String -> Types.Expr -> Dhall.Parser.Parser Dhall.Import
parseImport _ (Dhall.Note _ (Dhall.Embed l)) = pure l
parseImport prefix e = fail $ "Expected a Dhall import for " <> prefix <> " not:\n" <> show e
parsePrefixMap :: Options.Applicative.ReadM (Data.Map.Map Prefix Dhall.Import)
parsePrefixMap =
Options.Applicative.eitherReader $ \s ->
bimap errorBundlePretty Data.Map.fromList $ result (pack s)
where
parser = do
prefix <- some (alphaNumChar <|> char '.')
char '='
e <- Dhall.Parser.expr
imp <- parseImport prefix e
return (pack prefix, imp)
result = parse ((Dhall.Parser.unParser parser `sepBy1` char ',') <* eof) "MAPPING"
parseSplits :: Options.Applicative.ReadM (Data.Map.Map ModelHierarchy (Maybe ModelName))
parseSplits =
Options.Applicative.eitherReader $ \s ->
bimap errorBundlePretty Data.Map.fromList $ result (pack s)
where
parseModelInner = some (alphaNumChar <|> char '-' <|> char '.')
parseModel = (ModelName . pack) <$> (((char '(') *> parseModelInner <* (char ')')) <|> parseModelInner)
parser = do
path <- parseModel `sepBy1` char '.'
model <- optional $ do
char '='
mo <- parseModel
return mo
return (path, model)
result = parse ((Dhall.Parser.unParser parser `sepBy1` char ',') <* eof) "MAPPING"
parseOptions :: Options.Applicative.Parser Options
parseOptions = Options <$> parseSkip <*> parsePrefixMap' <*> parseSplits' <*> fileArg <*> crdArg
where
parseSkip =
Options.Applicative.switch
( Options.Applicative.long "skipDuplicates"
<> Options.Applicative.help "Skip types with the same name when aggregating types"
)
parsePrefixMap' =
option Data.Map.empty $ Options.Applicative.option parsePrefixMap
( Options.Applicative.long "prefixMap"
<> Options.Applicative.help "Specify prefix mappings as 'prefix1=importBase1,prefix2=importBase2,...'"
<> Options.Applicative.metavar "MAPPING"
)
parseSplits' =
option Data.Map.empty $ Options.Applicative.option parseSplits
( Options.Applicative.long "splitPaths"
<> Options.Applicative.help
"Specifiy path and model name pairs with paths being delimited by '.' and pairs separated by '=' for which \
\definitions should be aritifically split with a ref: \n\
\'(com.example.v1.Certificate).spec=com.example.v1.CertificateSpec'\n\
\When the model name is omitted, a guess will be made based on the first word of the definition's \
\description. Also note that top level model names in a path must use () when the name contains '.'"
<> Options.Applicative.metavar "SPLITS"
)
fileArg = Options.Applicative.strArgument
( Options.Applicative.help "The input file to read"
<> Options.Applicative.metavar "FILE"
<> Options.Applicative.action "file"
)
crdArg = Options.Applicative.switch
( Options.Applicative.long "crd"
<> Options.Applicative.help "The input file is a custom resource definition"
)
-- | `ParserInfo` for the `Options` type
parserInfoOptions :: Options.Applicative.ParserInfo Options
parserInfoOptions =
Options.Applicative.info
(Options.Applicative.helper <*> parseOptions)
( Options.Applicative.progDesc "Swagger to Dhall generator"
<> Options.Applicative.fullDesc
)
main :: IO ()
main = do
GHC.IO.Encoding.setLocaleEncoding System.IO.utf8
Options{..} <- Options.Applicative.execParser parserInfoOptions
let duplicateHandler =
if skipDuplicates
then skipDuplicatesHandler
else preferStableResource
-- Get the Definitions
defs <-
if crd then do
crdFile <- eitherDecodeFileStrict filename
case crdFile of
Left e -> do
fail $ "Unable to decode the CRD file. " <> show e
Right s -> do
case Convert.toDefinition s of
Left text -> do
fail (Text.unpack text)
Right result -> do
return (Data.Map.fromList [result])
else do
swaggerFile <- decodeFileStrict filename
case swaggerFile of
Nothing -> fail "Unable to decode the Swagger file"
Just (Swagger{..}) -> pure definitions
let fix m = Data.Map.adjust patchCyclicImports (ModelName m)
-- Convert to Dhall types in a Map
let types = Convert.toTypes prefixMap (Convert.pathSplitter splits)
-- TODO: find a better way to deal with this cyclic import
$ fix "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1beta1.JSONSchemaProps"
$ fix "io.k8s.apiextensions-apiserver.pkg.apis.apiextensions.v1.JSONSchemaProps"
defs
-- Output to types
Directory.createDirectoryIfMissing True "types"
for_ (Data.Map.toList types) $ \(ModelName name, expr) -> do
let path = "./types" </> unpack name <> ".dhall"
writeDhall path expr
-- Convert from Dhall types to defaults
let defaults = Data.Map.mapMaybeWithKey (Convert.toDefault prefixMap defs) types
-- Output to defaults
Directory.createDirectoryIfMissing True "defaults"
for_ (Data.Map.toList defaults) $ \(ModelName name, expr) -> do
let path = "./defaults" </> unpack name <> ".dhall"
writeDhall path expr
let mkEmbedField = Dhall.makeRecordField . Dhall.Embed
let toSchema (ModelName key) _ _ =
Dhall.RecordLit
[ ("Type", mkEmbedField (Convert.mkImport prefixMap ["types", ".."] (key <> ".dhall")))
, ("default", mkEmbedField (Convert.mkImport prefixMap ["defaults", ".."] (key <> ".dhall")))
]
let schemas = Data.Map.intersectionWithKey toSchema types defaults
let package =
Combine
mempty
Nothing
(Embed (Convert.mkImport prefixMap [ ] "schemas.dhall"))
(RecordLit
[ ( "IntOrString"
, Dhall.makeRecordField $ Field (Embed (Convert.mkImport prefixMap [ ] "types.dhall")) $ Dhall.makeFieldSelection "IntOrString"
)
, ( "Resource", mkEmbedField (Convert.mkImport prefixMap [ ] "typesUnion.dhall"))
]
)
-- Output schemas that combine both the types and defaults
Directory.createDirectoryIfMissing True "schemas"
for_ (Data.Map.toList schemas) $ \(ModelName name, expr) -> do
let path = "./schemas" </> unpack name <> ".dhall"
writeDhall path expr
-- Output the types record, the defaults record, and the giant union type
let getImportsMap = Convert.getImportsMap prefixMap duplicateHandler objectNames
makeRecordMap = Dhall.Map.mapMaybe (Just . Dhall.makeRecordField)
objectNames = Data.Map.keys types
typesMap = getImportsMap "types" $ Data.Map.keys types
defaultsMap = getImportsMap "defaults" $ Data.Map.keys defaults
schemasMap = getImportsMap "schemas" $ Data.Map.keys schemas
typesRecordPath = "./types.dhall"
typesUnionPath = "./typesUnion.dhall"
defaultsRecordPath = "./defaults.dhall"
schemasRecordPath = "./schemas.dhall"
packageRecordPath = "./package.dhall"
writeDhall typesUnionPath (Dhall.Union $ fmap Just typesMap)
writeDhall typesRecordPath (Dhall.RecordLit $ makeRecordMap typesMap)
writeDhall defaultsRecordPath (Dhall.RecordLit $ makeRecordMap defaultsMap)
writeDhall schemasRecordPath (Dhall.RecordLit $ makeRecordMap schemasMap)
writeDhall packageRecordPath package