`Reference<T>`: Type safe "deferred" references
One common pattern in infrastructure-as-code systems like Terraform and Pulumi is "references". A reference provides a way to refer to a resource (or a property thereof) that exists at a time after Pkl evaluation completes.
Example: The pulumi aws-yaml-static-website example creates a aws:s3:BucketV2 and creates a aws:s3:BucketPolicy that both directly references the bucket and a property of the bucket (its arn). In the case of the Pulumi YAML runtime, references are rendered as String sequences beginning with ${ and ending with }.
It would be useful for a Pkl module representing the aws:s3:BucketPolicy resource to define its bucket like so:
bucket: String | // accept a bucket name directly
Reference<String> | // accept a reference to a string, possibly from another resource or function's output
Reference<Resource> // accept a reference to a aws:s3:BucketV2 or another resource, using its ID (a String)
There should be an API within the standard library for representing and producing such references. How a reference is rendered or otherwise consumed would be module-dependent. At minimum, the Reference API should look something like this:
// members must be functions to avoid ambiguities between reading reference info and construction property "sub-references"
class Reference<T> {
/// The Class that this underlying Reference points to.
function getRootClass(): Class<T>
/// The concrete value of the "root" of the reference.
function getRootData(): Any
/// The "path" of properties/keys referenced from the "root" value.
function getPath(): List<PropertyReference|SubscriptReference>
}
class PropertyReference {
property: String
}
class SubscriptReference {
key: Any
}
function Reference<T>(root: T): Reference<T> = new {
// clazz = root.getClass()
// data = root
}
Reference should also exhibit covariance: given a class T and a subclass class U extends T, Reference<U> should be a subtype of Reference<T>.
It may also be beneficial to add syntax sugar for producing references, so these two expressions would be equivalent:
Reference(someValue).propA["key1"].propB[0]
&someValue.propA["key1"].propB[0]
This requires a SPICE to be written to iterate on the design.
Thanks for getting the ball rolling.
The basic idea here is that a Reference<T> would have the same property accessors as T, except all accessors continue to give you a Reference.
class Person { pet: Pet }
class Pet { name: String }
hidden bob: Reference<Person> = makeRef(Person, "bob")
petName = bob.pet.name // `petName is a Reference<String>`
Users would use output converters when rendering this. For example, maybe something like:
output {
renderer = new YamlRenderer {
converters {
[Reference] = (it) -> "%{" + it.getPath().join(".") + "}"
}
}
}
Produces:
petName: %{bob.pet.name}