hydra-0.15.0: src/main/haskell/Hydra/Sources/Yaml/Serde.hs
module Hydra.Sources.Yaml.Serde where
-- Standard imports for term-level sources outside of the kernel
import Hydra.Kernel
import Hydra.Sources.Libraries
import Hydra.Dsl.Meta.Lib.Strings as Strings
import Hydra.Dsl.Meta.Phantoms as Phantoms
import qualified Hydra.Dsl.Meta.Lib.Chars as Chars
import qualified Hydra.Dsl.Meta.Lib.Equality as Equality
import qualified Hydra.Dsl.Meta.Lib.Lists as Lists
import qualified Hydra.Dsl.Meta.Lib.Logic as Logic
import qualified Hydra.Dsl.Meta.Lib.Literals as Literals
import qualified Hydra.Dsl.Meta.Lib.Maps as Maps
import qualified Hydra.Dsl.Meta.Lib.Maybes as Maybes
import qualified Hydra.Dsl.Meta.Lib.Pairs as Pairs
import qualified Hydra.Dsl.Terms as Terms
import qualified Hydra.Dsl.Types as Types
import qualified Hydra.Sources.Kernel.Types.All as KernelTypes
import Prelude hiding ((++))
import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Hydra.Yaml.Model as YM
ns :: Namespace
ns = Namespace "hydra.yaml.serde"
define :: String -> TTerm a -> TTermDefinition a
define = definitionInNamespace ns
module_ :: Module
module_ = Module {
moduleNamespace = ns,
moduleDefinitions = definitions,
moduleTermDependencies = [],
moduleTypeDependencies = (KernelTypes.kernelTypesNamespaces L.++ [Namespace "hydra.yaml.model"]),
moduleDescription = Just "Native YAML serialization: YAML Node to String"}
where
definitions = [
toDefinition escapeSingleQuotes,
toDefinition hasLeadingTrailingSpace,
toDefinition hydraYamlToString,
toDefinition indentString,
toDefinition isDecimalString,
toDefinition looksLikeNumber,
toDefinition needsQuoting,
toDefinition writeMappingEntry,
toDefinition writeMappingEntryInline,
toDefinition writeNode,
toDefinition writeNodeInline,
toDefinition writeScalar,
toDefinition writeSequenceItem,
toDefinition writeString,
toDefinition yamlReservedWords,
toDefinition yamlSpecialChars]
-- | Escape single quotes by doubling them
escapeSingleQuotes :: TTermDefinition (String -> String)
escapeSingleQuotes = define "escapeSingleQuotes" $
doc "Escape single quotes by doubling them" $
"s" ~>
"squote" <~ int32 39 $ -- '\''
Strings.fromList $ Lists.bind (Strings.toList $ var "s")
("c" ~> Logic.ifElse (Equality.equal (var "c") (var "squote"))
(list [var "squote", var "squote"])
(list [var "c"]))
-- | Check if a string has leading or trailing whitespace
hasLeadingTrailingSpace :: TTermDefinition (String -> Bool)
hasLeadingTrailingSpace = define "hasLeadingTrailingSpace" $
doc "Check if a string has leading or trailing whitespace" $
"s" ~>
"chars" <~ Strings.toList (var "s") $
Logic.or
(Maybes.fromMaybe false (Maybes.map (lambda "c" $ Chars.isSpace (var "c")) (Lists.maybeHead (var "chars"))))
(Maybes.fromMaybe false (Maybes.map (lambda "c" $ Chars.isSpace (var "c")) (Lists.maybeLast (var "chars"))))
-- | Serialize a YAML node to a string
hydraYamlToString :: TTermDefinition (YM.Node -> String)
hydraYamlToString = define "hydraYamlToString" $
doc "Serialize a YAML node to a string" $
lambda "node" $ writeNode @@ var "node"
-- | Indent all lines of a string by 2 spaces
indentString :: TTermDefinition (String -> String)
indentString = define "indentString" $
doc "Indent all lines of a string by 2 spaces" $
"s" ~>
Strings.cat $ Lists.map
("line" ~> Logic.ifElse (Strings.null $ var "line")
(string "")
(Strings.cat $ list [string " ", var "line", string "\n"]))
(Strings.lines $ var "s")
-- | Check if a list of character codes represents a decimal number (digits.digits)
isDecimalString :: TTermDefinition ([Int] -> Bool)
isDecimalString = define "isDecimalString" $
doc "Check if character codes represent a decimal number" $
"chars" ~>
"dotCode" <~ int32 46 $ -- '.'
"parts" <~ Lists.span ("c" ~> Logic.not (Equality.equal (var "c") (var "dotCode"))) (var "chars") $
"before" <~ Pairs.first (var "parts") $
"afterWithDot" <~ Pairs.second (var "parts") $
-- Must have something before the dot
Logic.ifElse (Lists.null $ var "before") false $
-- Must have the dot
Logic.ifElse (Lists.null $ var "afterWithDot") false $
-- Drop the dot
"after" <~ Lists.drop (int32 1) (var "afterWithDot") $
-- Must have something after the dot
Logic.ifElse (Lists.null $ var "after") false $
-- Both parts must be all digits
"isDigitFn" <~ ("c" ~> Logic.and
(Equality.gte (var "c") (int32 48))
(Equality.lte (var "c") (int32 57))) $
Logic.and
(Lists.null (Lists.filter ("c" ~> Logic.not (var "isDigitFn" @@ var "c")) (var "before")))
(Lists.null (Lists.filter ("c" ~> Logic.not (var "isDigitFn" @@ var "c")) (var "after")))
-- | Check if a string looks like a number
looksLikeNumber :: TTermDefinition (String -> Bool)
looksLikeNumber = define "looksLikeNumber" $
doc "Check if a string looks like a number" $
"s" ~>
"chars" <~ Strings.toList (var "s") $
Maybes.fromMaybe false $ Maybes.map
(lambda "p" $ lets [
"firstCh">: Pairs.first (var "p"),
"tailCh">: Pairs.second (var "p"),
-- Handle leading minus
"rest">: Logic.ifElse (Equality.equal (var "firstCh") (int32 45)) -- '-'
(var "tailCh")
(var "chars"),
"isDigitFn">: "c" ~> Logic.and
(Equality.gte (var "c") (int32 48)) -- '0'
(Equality.lte (var "c") (int32 57)), -- '9'
"allDigits">: Logic.and
(Logic.not (Lists.null (var "rest")))
(Lists.null (Lists.filter
("c" ~> Logic.not (var "isDigitFn" @@ var "c"))
(var "rest")))] $
Logic.ifElse (var "allDigits") true $
-- Decimal?
isDecimalString @@ var "rest")
(Lists.uncons (var "chars"))
-- | Check if a string needs quoting in YAML
needsQuoting :: TTermDefinition (String -> Bool)
needsQuoting = define "needsQuoting" $
doc "Check if a string needs quoting in YAML" $
"s" ~>
-- Empty string needs quoting
Logic.ifElse (Strings.null $ var "s") true $
-- Reserved words need quoting
Logic.ifElse (Lists.elem (var "s") (var "hydra.yaml.serde.yamlReservedWords" :: TTerm [String])) true $
-- Looks like a number needs quoting
Logic.ifElse (looksLikeNumber @@ var "s") true $
-- Contains special characters needs quoting
"chars" <~ Strings.toList (var "s") $
"specials" <~ Strings.toList (var "hydra.yaml.serde.yamlSpecialChars" :: TTerm String) $
"hasSpecial" <~ Logic.not (Lists.null (Lists.filter
("c" ~> Lists.elem (var "c" :: TTerm Int) (var "specials"))
(var "chars"))) $
Logic.ifElse (var "hasSpecial") true $
-- Leading or trailing space needs quoting
hasLeadingTrailingSpace @@ var "s"
-- | Write a mapping entry in block style (key: value\n)
writeMappingEntry :: TTermDefinition ((YM.Node, YM.Node) -> String)
writeMappingEntry = define "writeMappingEntry" $
doc "Write a mapping entry in block style" $
"entry" ~>
"key" <~ Pairs.first (var "entry") $
"value" <~ Pairs.second (var "entry") $
cases YM._Node (var "value") Nothing [
YM._Node_scalar>>: "s" ~> Strings.cat $ list [writeNodeInline @@ var "key", string ": ", writeScalar @@ var "s", string "\n"],
YM._Node_sequence>>: "items" ~>
Logic.ifElse (Lists.null $ var "items")
(Strings.cat $ list [writeNodeInline @@ var "key", string ": []\n"])
(Strings.cat $ list [writeNodeInline @@ var "key", string ":\n", indentString @@ (writeNode @@ var "value")]),
YM._Node_mapping>>: "m" ~>
Logic.ifElse (Equality.equal (Maps.size (var "m")) (int32 0))
(Strings.cat $ list [writeNodeInline @@ var "key", string ": {}\n"])
(Strings.cat $ list [writeNodeInline @@ var "key", string ":\n", indentString @@ (writeNode @@ var "value")])]
-- | Write a mapping entry for the first item of a sequence element
writeMappingEntryInline :: TTermDefinition ((YM.Node, YM.Node) -> String)
writeMappingEntryInline = define "writeMappingEntryInline" $
doc "Write a mapping entry for the first item of a sequence element" $
"entry" ~>
"key" <~ Pairs.first (var "entry") $
"value" <~ Pairs.second (var "entry") $
cases YM._Node (var "value") Nothing [
YM._Node_scalar>>: "s" ~> Strings.cat $ list [writeNodeInline @@ var "key", string ": ", writeScalar @@ var "s", string "\n"],
YM._Node_sequence>>: "items" ~>
Logic.ifElse (Lists.null $ var "items")
(Strings.cat $ list [writeNodeInline @@ var "key", string ": []\n"])
(Strings.cat $ list [writeNodeInline @@ var "key", string ":\n", indentString @@ (writeNode @@ var "value")]),
YM._Node_mapping>>: "m" ~>
Logic.ifElse (Equality.equal (Maps.size (var "m")) (int32 0))
(Strings.cat $ list [writeNodeInline @@ var "key", string ": {}\n"])
(Strings.cat $ list [writeNodeInline @@ var "key", string ":\n", indentString @@ (writeNode @@ var "value")])]
-- | Write a YAML node as a top-level value (block style)
writeNode :: TTermDefinition (YM.Node -> String)
writeNode = define "writeNode" $
doc "Write a YAML node as a top-level value in block style" $
"node" ~> cases YM._Node (var "node") Nothing [
YM._Node_scalar>>: "s" ~> Strings.cat2 (writeScalar @@ var "s") (string "\n"),
YM._Node_sequence>>: "items" ~>
Logic.ifElse (Lists.null $ var "items")
(string "[]\n")
(Strings.cat $ Lists.map (lambda "item" $ writeSequenceItem @@ var "item") (var "items")),
YM._Node_mapping>>: "m" ~>
Logic.ifElse (Equality.equal (Maps.size (var "m")) (int32 0))
(string "{}\n")
(Strings.cat $ Lists.map (lambda "e" $ writeMappingEntry @@ var "e") (Maps.toList $ var "m"))]
-- | Write a node inline (for use as a mapping key)
writeNodeInline :: TTermDefinition (YM.Node -> String)
writeNodeInline = define "writeNodeInline" $
doc "Write a node inline (for use as a mapping key)" $
"node" ~> cases YM._Node (var "node") Nothing [
YM._Node_scalar>>: "s" ~> writeScalar @@ var "s",
YM._Node_sequence>>: "items" ~>
Strings.cat $ list [
string "[",
Strings.intercalate (string ", ") (Lists.map (lambda "item" $ writeNodeInline @@ var "item") (var "items")),
string "]"],
YM._Node_mapping>>: "m" ~>
"writeFlowEntry" <~ ("e" ~>
Strings.cat $ list [
writeNodeInline @@ (Pairs.first $ var "e"),
string ": ",
writeNodeInline @@ (Pairs.second $ var "e")]) $
Strings.cat $ list [
string "{",
Strings.intercalate (string ", ") (Lists.map (var "writeFlowEntry") (Maps.toList $ var "m")),
string "}"]]
-- | Write a scalar value
writeScalar :: TTermDefinition (YM.Scalar -> String)
writeScalar = define "writeScalar" $
doc "Write a scalar value" $
"s" ~> cases YM._Scalar (var "s") Nothing [
YM._Scalar_bool>>: "b" ~> Logic.ifElse (var "b") (string "true") (string "false"),
YM._Scalar_decimal>>: "d" ~> Literals.showDecimal (var "d"),
YM._Scalar_float>>: "f" ~> Literals.showBigfloat (var "f"),
YM._Scalar_int>>: "i" ~> Literals.showBigint (var "i"),
YM._Scalar_null>>: constant (string "null"),
YM._Scalar_str>>: "str" ~> writeString @@ var "str"]
-- | Write a sequence item in block style
writeSequenceItem :: TTermDefinition (YM.Node -> String)
writeSequenceItem = define "writeSequenceItem" $
doc "Write a sequence item in block style" $
"node" ~> cases YM._Node (var "node") Nothing [
YM._Node_scalar>>: "s" ~> Strings.cat $ list [string "- ", writeScalar @@ var "s", string "\n"],
YM._Node_sequence>>: "items" ~>
Logic.ifElse (Lists.null $ var "items")
(string "- []\n")
(Strings.cat2 (string "-\n") (indentString @@ (writeNode @@ var "node"))),
YM._Node_mapping>>: "m" ~>
Logic.ifElse (Equality.equal (Maps.size (var "m")) (int32 0))
(string "- {}\n")
("entries" <~ Maps.toList (var "m") $
Maybes.fromMaybe (string "") $ Maybes.map
(lambda "p" $ lets [
"firstEntry">: Pairs.first (var "p"),
"restEntries">: Pairs.second (var "p"),
"firstStr">: writeMappingEntryInline @@ var "firstEntry",
"restStr">: Strings.cat $ Lists.map (lambda "e" $ writeMappingEntry @@ var "e") (var "restEntries")] $
Strings.cat $ list [string "- ", var "firstStr", indentString @@ var "restStr"])
(Lists.uncons (var "entries")))]
-- | Write a string value, quoting if necessary
writeString :: TTermDefinition (String -> String)
writeString = define "writeString" $
doc "Write a string value, quoting if necessary" $
"s" ~>
Logic.ifElse (needsQuoting @@ var "s")
(Strings.cat $ list [string "'", escapeSingleQuotes @@ var "s", string "'"])
(var "s")
-- | YAML reserved words that need quoting
yamlReservedWords :: TTermDefinition [String]
yamlReservedWords = define "yamlReservedWords" $
doc "YAML reserved words that need quoting" $
list [
string "true", string "false", string "null", string "~",
string "yes", string "no", string "on", string "off",
string "True", string "False", string "Null",
string "Yes", string "No", string "On", string "Off",
string "TRUE", string "FALSE", string "NULL",
string "YES", string "NO", string "ON", string "OFF"]
-- | YAML special characters that trigger quoting
yamlSpecialChars :: TTermDefinition String
yamlSpecialChars = define "yamlSpecialChars" $
doc "YAML special characters that trigger quoting" $
string ": {}[]#,&*!|>'\"%@`"