@enetsee: You will probably want to do something similar to what dhall-kubernetes
does and auto-generate Dhall types from a CloudFormation Resources specification. I’m not aware of any project that has done this, yet.
One of the issues mentioned in that blog can be addressed by the fact that dhall-to-{yaml,json}
support the JSON
type from the Prelude, meaning that you can stick the JSON
type anywhere in your configuration where you don’t know what the schema for the JSON will be (i.e. if you want to be able to store arbitrary JSON). This allows you to have a “gradually typed” schema where you stick JSON
anywhere that you don’t have a more precise type.
One tricky bit here is support CloudFormation functions. I’m not an expert on CloudFormation, but my understanding is that anywhere CloudFormation
expects a string it could be an ordinary JSON string or it could be a CloudFormation “expression” that returns a string, such as:
{ "Fn::Base64" : "foo" }
… and I believe you can chain those functions, too:
{ "Fn::Base64" : { "Fn::Base64" : "foo" } }
However, this is possible to model in Dhall, especially once we support forward declarations. You can either wait for that feature to be standardized or you can get started with the equivalent feature using lambdas, which I will outline briefly here.
The idea is that each CloudFormation template would begin by “declaring” a dependence on outside types and builtins, like this:
\(CloudFormationText : Type) -- `Text` that could be produced by a CloudFormation function
-> \(PlainText : Text -> CloudFormationText) -- Plain `Text`
-> \(Base64 : CloudFormationText -> CloudFormationText) -- Fn::Base64
-> \(Join : CloudFormationText → List CloudFormationText → CloudFormationText) -- Fn::Join
-> …
… and you can add additional CloudFormation “types” and “functions” in this way. Using the forward declarations feature I linked to above, that would become:
let CloudFormationText : Type
let PlainText : Text -> CloudFormationText
let Base64 : CloudFormationText -> CloudFormationText
let Join : CloudFormationText → List CloudFormationText → CloudFormationText
…
Then anywhere in the CloudFormation schema where it expects a string that could be generated by functions, you can have it expect a CloudFormationText
instead. Then you could write:
{ foo = Base64 (PlainText "foo")
}
Then I believe you could convert such a strongly-typed schema that the user authors to the JSON
type. In other words, there could be a render : CloudFormationSchema → JSON
function implemented in Dhall that performs this conversion. Then it would take care of supplying the implementations of the CloudFormationText
/PlainText
/Base64
/Join
/… builtins that the CloudFormationSchema
expects. In other words, the CloudFormationSchema
type would be something like:
forall (CloudFormationText : Type)
-> forall (PlainText : Text -> CloudFormationText)
-> forall (Base64 : CloudFormationText -> CloudFormationText)
-> forall (Join : CloudFormationText -> List CloudFormationText -> CloudFormationText)
-> ... -- Then the strongly-typed schema of the CloudFormation resource type
… and the render
function would look something like this:
let CloudFormationSchema = ./CloudFormationSchema.dhall
let JSON = https://prelude.dhall-lang.org/JSON/package.dhall
let CloudFormationText = JSON.Type
let PlainText = JSON.string
let Base64 =
\(input : CloudFormationText)
-> JSON.object (toMap { `Fn::Base64` = input })
let Join =
\(delimiter : CloudFormationText)
-> \(values : List CloudFormationText)
-> JSON.object (toMap { `Fn::Join` = JSON.array [ JSON.string "delimiter", JSON.array values ] })
let render : CloudFormationSchema -> JSON.Type =
\(config : CloudFormationSchema)
-> let applied = config CloudFormationText PlainText Base64 Join ...
in ... -- Further conversion logic from the strongly typed schema to the weakly typed schema
Note that the trick I’ve described is essentially the same principle as the trick illustrated in How to translate recursive code to Dhall