Skip to content

Commit

Permalink
feat: implement CLI command to prepare, dump and render diagrams
Browse files Browse the repository at this point in the history
  • Loading branch information
vst committed May 24, 2024
1 parent fb862f3 commit 41cac18
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 13 deletions.
6 changes: 6 additions & 0 deletions default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ let
## Import nixpkgs pinned by niv:
pkgs = import sources.nixpkgs { inherit system; };

## Import nixpkgs pinned by niv:
pkgs-unstable = import sources.nixpkgs-unstable { inherit system; };

##################
## LOAD HELPERS ##
##################
Expand Down Expand Up @@ -85,6 +88,9 @@ let
thisHaskell.hlint
thisHaskell.hpack

## Application dependencies:
pkgs-unstable.d2

## Other build inputs for various development requirements:
pkgs.docker-client
pkgs.git
Expand Down
12 changes: 12 additions & 0 deletions nix/sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,17 @@
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/40d989164088db79c1895f177ee67216d91ae8a5.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixpkgs-unstable": {
"branch": "nixpkgs-unstable",
"description": "Nix Packages collection & NixOS",
"homepage": "",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3f316d2a50699a78afe5e77ca486ad553169061e",
"sha256": "1gfnjl8zjai1cjqhx96jjnnq7zjdn0ajd14xmb09jrgnjs0dw1im",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/3f316d2a50699a78afe5e77ca486ad553169061e.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}
1 change: 1 addition & 0 deletions package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ library:
- template-haskell
- text
- time
- typed-process
- unordered-containers
- vector
- vty
Expand Down
54 changes: 41 additions & 13 deletions src/Postmap/Cli.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import qualified Hasql.Connection
import qualified Options.Applicative as OA
import qualified Postmap.Diagrams as Diagrams
import qualified Postmap.Introspect as Introspect
import qualified Postmap.Meta as Meta
import qualified Postmap.Spec as Spec
Expand Down Expand Up @@ -87,6 +88,7 @@ commandSchema = OA.hsubparser (OA.command "schema" (OA.info parser infomod) <> O
parser =
commandSchemaInit
<|> commandSchemaTui
<|> commandSchemaDiagrams


-- ** schema init
Expand Down Expand Up @@ -121,19 +123,6 @@ doSchemaInit (InitSourceDatabase u s) = do
pure ExitSuccess


-- ** version


-- | Definition for @version@ CLI command.
commandVersion :: OA.Parser (IO ExitCode)
commandVersion = OA.hsubparser (OA.command "version" (OA.info parser infomod) <> OA.metavar "version")
where
infomod = OA.fullDesc <> infoModHeader <> OA.progDesc "Show version and build information." <> OA.footer "This command shows version and build information."
parser =
doVersion
<$> OA.switch (OA.short 'j' <> OA.long "json" <> OA.help "Format output in JSON.")


-- ** schema tui


Expand All @@ -159,6 +148,45 @@ doSchemaTui fp = do
pure ExitSuccess


-- ** schema diagrams


-- | Definition for @schema diagrams@ CLI command.
commandSchemaDiagrams :: OA.Parser (IO ExitCode)
commandSchemaDiagrams = OA.hsubparser (OA.command "diagrams" (OA.info parser infomod) <> OA.metavar "diagrams")
where
infomod = OA.fullDesc <> infoModHeader <> OA.progDesc "Produce Diagrams." <> OA.footer "This command produces diagrams."
parser =
doSchemaDiagrams
<$> OA.strOption (OA.short 'f' <> OA.long "file" <> OA.help "Path to the schema file.")
<*> OA.strOption (OA.short 'o' <> OA.long "output-directory" <> OA.help "Path to output directory.")


doSchemaDiagrams :: FilePath -> FilePath -> IO ExitCode
doSchemaDiagrams fp dp = do
eSchema <- ADC.Yaml.eitherDecodeYamlViaCodec @Spec.Spec <$> B.readFile fp
case eSchema of
Left err -> do
TIO.putStrLn ("Error while parsing schema file: " <> Z.Text.tshow err)
pure (ExitFailure 1)
Right schema -> do
Diagrams.runDiagrams dp schema
pure ExitSuccess


-- ** version


-- | Definition for @version@ CLI command.
commandVersion :: OA.Parser (IO ExitCode)
commandVersion = OA.hsubparser (OA.command "version" (OA.info parser infomod) <> OA.metavar "version")
where
infomod = OA.fullDesc <> infoModHeader <> OA.progDesc "Show version and build information." <> OA.footer "This command shows version and build information."
parser =
doVersion
<$> OA.switch (OA.short 'j' <> OA.long "json" <> OA.help "Format output in JSON.")


-- | @version@ CLI command program.
doVersion :: Bool -> IO ExitCode
doVersion True = BLC.putStrLn (Aeson.encode Meta.buildInfo) >> pure ExitSuccess
Expand Down
160 changes: 160 additions & 0 deletions src/Postmap/Diagrams.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}

module Postmap.Diagrams where

import Data.Maybe (isJust, mapMaybe)
import Data.String.Interpolate (i)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Postmap.Spec (Field (..), FieldName (..), FieldReference (..), Record (..), RecordName (..), Spec (..))
import qualified System.Process.Typed as TP


runDiagrams :: FilePath -> Spec -> IO ()
runDiagrams path Spec {..} = do
runDiagramMaster path specRecords
mapM_ (runDiagramPerRecord path) specRecords


runDiagramMaster :: FilePath -> [Record] -> IO ()
runDiagramMaster path rs =
let d2Rec = fmap renderSqlRecord rs
d2Ref = fmap (\r@Record {..} -> T.intercalate "\n" $ mapMaybe (\f@Field {..} -> fmap (sqlRef False r f) fieldReference) recordFields) rs
d2Sch = T.intercalate "\n" d2Rec <> "\n" <> T.intercalate "\n" d2Ref
opDsl = path <> "/_schema.d2"
opSvg = path <> "/_schema.svg"
in do
TIO.writeFile opDsl d2Sch
TP.runProcess_ $ TP.proc "d2" ["--layout=elk", opDsl, opSvg]


runDiagramPerRecord :: FilePath -> Record -> IO ()
runDiagramPerRecord path record@Record {..} =
let d2 = renderRecordD2 record
opD2 = path <> "/" <> T.unpack (unRecordName recordName) <> ".d2"
opSvg = path <> "/" <> T.unpack (unRecordName recordName) <> ".svg"
in do
TIO.writeFile opD2 d2
TP.runProcess_ $ TP.proc "d2" ["--layout=elk", opD2, opSvg]


renderRecordD2 :: Record -> T.Text
renderRecordD2 record@Record {..} =
let d2Rec = renderSqlRecord record
d2Ref = T.intercalate "\n" $ mapMaybe (\f@Field {..} -> fmap (sqlRef True record f) fieldReference) recordFields
d2Ent =
T.intercalate "\n"
. fmap (<> ".class: record")
. filter (/= sqlRecordName recordName)
$ mapMaybe (\Field {..} -> sqlRecordName . fieldReferenceRecord <$> fieldReference) recordFields
in [i|
classes: {
record: {
style: {
border-radius: 8
font-size: 24
bold: false
}
}
}

#{d2Ent}

#{d2Rec}

#{d2Ref}
|]


-- * Sql Record


renderSqlRecord :: Record -> T.Text
renderSqlRecord Record {..} =
let fields = fmap renderSqlTableField recordFields
in [i|#{sqlRecordName recordName} {
shape: sql_table
#{T.intercalate "\n " fields}
}|]


-- * Sql Fields


renderSqlTableField :: Field -> T.Text
renderSqlTableField field@Field {..} =
let constraints = T.intercalate ";" (sqlFieldConstraints field)
in [i|#{sqlFieldName fieldName}: #{fieldType} {constraint: [#{constraints}]}|]


sqlFieldConstraints :: Field -> [T.Text]
sqlFieldConstraints Field {..} =
let pk = (["primary_key" | fieldIsPrimaryKey])
uq = (["unique" | fieldIsUnique])
fk = (["foreign_key" | isJust fieldReference])
in pk <> uq <> fk


-- * SQL References


sqlRef :: Bool -> Record -> Field -> FieldReference -> T.Text
sqlRef simple record field FieldReference {..} =
let fr = sqlRecordNameFromRecord record
ff = sqlFieldNameFromField field
tr = sqlRecordName fieldReferenceRecord
tf = sqlFieldName fieldReferenceField
in if simple
then [i|#{fr}.#{ff} -> #{tr}|]
else [i|#{fr}.#{ff} -> #{tr}.#{tf}|]


-- * Helpers


sqlFieldName :: FieldName -> T.Text
sqlFieldName f =
let fn = unFieldName f
in fn <> if fn `elem` _reserved then "_" else ""


sqlFieldNameFromField :: Field -> T.Text
sqlFieldNameFromField Field {..} =
sqlFieldName fieldName


sqlRecordName :: RecordName -> T.Text
sqlRecordName =
unRecordName


sqlRecordNameFromRecord :: Record -> T.Text
sqlRecordNameFromRecord Record {..} =
sqlRecordName recordName


_reserved :: [T.Text]
_reserved =
[ "class"
, "classes"
, "constraint"
, "direction"
, "grid-columns"
, "grid-gap"
, "grid-rows"
, "height"
, "horizontal-gap"
, "icon"
, "label"
, "left"
, "link"
, "near"
, "shape"
, "style"
, "tooltip"
, "top"
, "vertical-gap"
, "width"
]

0 comments on commit 41cac18

Please sign in to comment.