Repository: aws-cloudformation/rain (go)
Issue: Outputs aren't carried over when formatting with rain from json to pkl
I'm converting some existing templates from json to pkl. The templates seem to be losing the Outputs configuration though.
I believe this should work, unless there's another approach I should be taking?
Repo:
```
 more outputsrepo.json 
{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "Generated by rain",
    "Resources": {
        "T100Subnet": {
            "Type": "AWS::EC2::Subnet",
            "Properties": {
                "CidrBlock": "11.0.0.0/16",
                "AvailabilityZone" : "us-west-2a"
}
        }},
    "Outputs": {
      "T100SubnetId" : {
          "Description": "Subnet Id",
          "Value" : { "Ref" : "T100Subnet" },
          "Export": { "Name": {"Fn::Sub":"${AWS::StackName}-T100SubnetId"}}
      },
   }

}
```



```
~/rain fmt -p outputsrepo.json
amends "@cfn/template.pkl"
import "@cfn/cloudformation.pkl" as cfn
import "@cfn/aws/ec2/subnet.pkl"

AWSTemplateFormatVersion = "2010-09-09"

Description = "Generated by rain"

Resources {
    ["T100Subnet"] = new subnet.Subnet {
        Type = "AWS::EC2::Subnet"
        CidrBlock = "11.0.0.0/16"
        AvailabilityZone = "us-west-2a"
    }

}
```

```
~/rain --version
Rain v1.12.0 linux/amd64
```

```
more PklProject
amends "pkl:Project"

dependencies {
    ["cfn"] {
        uri = "package://github.com/aws-cloudformation/cloudformation-pkl/releases/download/cloudformation@0.1.3/cloudformation@0.1.3"
    }
}
```
Pull request description: Add Outputs to Pkl serialization, and fix formatting Pkl to Yaml with Pkl modules
Fixes #524

Patch:
diff --git a/cft/format/pkl.go b/cft/format/pkl.go
index 7e7edeb0..af4ef63c 100644
--- a/cft/format/pkl.go
+++ b/cft/format/pkl.go
@@ -10,6 +10,7 @@ import (
 	"github.com/aws-cloudformation/rain/cft"
 	"github.com/aws-cloudformation/rain/internal/config"
 	"github.com/aws-cloudformation/rain/internal/node"
+	"github.com/aws-cloudformation/rain/internal/s11n"
 	"gopkg.in/yaml.v3"
 )
 
@@ -134,6 +135,56 @@ func writeResource(sb *strings.Builder, name string, resource *yaml.Node, basic
 	return nil
 }
 
+func writeOutput(sb *strings.Builder, name string, output *yaml.Node, basic bool) error {
+	w(sb, "    [\"%s\"] = new cfn.Output {\n", name)
+
+	for i := 0; i < len(output.Content); i += 2 {
+
+		/*
+			/// A stack Output exported value
+			open class Export {
+				Name: RefString
+			}
+
+			/// A stack output value
+			open class Output {
+				Description: RefString?
+				Value: RefString
+				Export: Export?
+			}
+		*/
+
+		attrName := output.Content[i].Value
+		attrValue := output.Content[i+1]
+
+		switch attrName {
+		case "Description":
+			w(sb, "        Description = %s\n", attrValue.Value)
+		case "Value":
+			if attrValue.Kind == yaml.MappingNode {
+				w(sb, "        Value = ")
+				writeMap(sb, attrValue, "    ", basic)
+			} else {
+				w(sb, "        Value = %s\n", attrValue.Value)
+			}
+		case "Export":
+			w(sb, "        Export = new cfn.Export {\n")
+			_, nameNode, _ := s11n.GetMapValue(attrValue, "Name")
+			if nameNode == nil {
+				return errors.New("expected Export to have Name")
+			}
+			exportName := nameNode.Value
+			w(sb, "            Name = %s\n", exportName)
+			w(sb, "        }\n")
+		}
+
+	}
+
+	w(sb, "    }\n")
+
+	return nil
+}
+
 func writeParameter(sb *strings.Builder, name string, param *yaml.Node) error {
 	w(sb, "    [\"%s\"] {\n", name)
 	for j := 0; j < len(param.Content); j += 2 {
@@ -375,13 +426,18 @@ func addSection(section cft.Section, n *yaml.Node, sb *strings.Builder, basic bo
 		sb.WriteString("}\n")
 	case cft.Transform:
 	case cft.Outputs:
+		w(sb, "%s {\n", section)
+		for i := 0; i < len(n.Content); i += 2 {
+			writeOutput(sb, n.Content[i].Value, n.Content[i+1], basic)
+		}
+		sb.WriteString("}\n")
 	}
 
 	return nil
 }
 
 // CftToPkl serializes the template as pkl.
-// It assumes that the user is import the cloudformation package
+// It assumes that the user is importing the cloudformation package
 func CftToPkl(t cft.Template, basic bool, pklPackageAlias string) (string, error) {
 	if t.Node == nil || len(t.Node.Content) != 1 {
 		return "", errors.New("expected t.Node.Content[0]")
diff --git a/pkl/pkl.go b/pkl/pkl.go
index 8c23841e..f2ad8ffb 100644
--- a/pkl/pkl.go
+++ b/pkl/pkl.go
@@ -2,6 +2,7 @@ package pkl
 
 import (
 	"context"
+	"path/filepath"
 
 	"github.com/apple/pkl-go/pkl"
 	"github.com/aws-cloudformation/rain/internal/config"
@@ -18,7 +19,8 @@ var EvaluatorOptionsFunc = func(opts *pkl.EvaluatorOptions) {
 
 func Yaml(filename string) (string, error) {
 	// Convert the template to YAML
-	evaluator, err := pkl.NewEvaluator(context.Background(), EvaluatorOptionsFunc)
+	evaluator, err := pkl.NewProjectEvaluator(
+		context.Background(), filepath.Dir(filename), EvaluatorOptionsFunc)
 	if err != nil {
 		return "", err
 	}


Test patch:
diff --git a/cft/format/format_test.go b/cft/format/format_test.go
index 13e7a5d9..d02bfe73 100644
--- a/cft/format/format_test.go
+++ b/cft/format/format_test.go
@@ -760,3 +760,44 @@ Resources:
 	}
 
 }
+
+func TestPkl(t *testing.T) {
+	source := `
+Resources:
+  Bucket:
+    Type: AWS::S3::Bucket
+Outputs:
+  BucketName:
+    Value: !Ref Bucket
+`
+	template, err := parse.String(source)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	output, err := format.CftToPkl(template, false, "@cfn")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	expected := `amends "@cfn/template.pkl"
+import "@cfn/cloudformation.pkl" as cfn
+import "@cfn/aws/s3/bucket.pkl"
+
+Resources {
+    ["Bucket"] = new bucket.Bucket {
+        Type = "AWS::S3::Bucket"
+    }
+
+}
+
+Outputs {
+    ["BucketName"] = new cfn.Output {
+        Value = cfn.Ref("Bucket")
+    }
+}
+`
+	if output != expected {
+		t.Fatalf("Got:\n[%s]\nExpected:\n[%s]\n", output, expected)
+	}
+}
diff --git a/cft/format/pkl_test.go b/cft/format/pkl_test.go
index 52c2fcd7..aff0f3bd 100644
--- a/cft/format/pkl_test.go
+++ b/cft/format/pkl_test.go
@@ -18,6 +18,12 @@ Resources:
     Properties:
         BucketName: 
           Ref: Name
+Outputs:
+  BucketName:
+    Value: !Ref BucketName
+    Description: The bucket name
+    Export:
+      Name: ExportedBucketName
 `
 
 	expected := `amends "@cfn/template.pkl"
@@ -38,6 +44,16 @@ Resources {
     }
 
 }
+
+Outputs {
+    ["BucketName"] = new cfn.Output {
+        Value = cfn.Ref("BucketName")
+        Description = The bucket name
+        Export = new cfn.Export {
+            Name = ExportedBucketName
+        }
+    }
+}
 `
 
 	template, err := parse.String(input)
diff --git a/internal/cmd/fmt/fmt_test.go b/internal/cmd/fmt/fmt_test.go
deleted file mode 100644
index fb8f2fb4..00000000
--- a/internal/cmd/fmt/fmt_test.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package fmt_test
-
-import (
-	"os"
-
-	"github.com/aws-cloudformation/rain/internal/cmd/fmt"
-)
-
-func Example_fmt_help() {
-	os.Args = []string{
-		os.Args[0],
-		"../../../test/templates/success.template",
-	}
-
-	fmt.Cmd.Execute()
-	// Output:
-	// Description: This template succeeds
-	//
-	// Parameters:
-	//   BucketName:
-	//     Type: String
-	//
-	// Resources:
-	//   Bucket1:
-	//     Type: AWS::S3::Bucket
-	//     Properties:
-	//       BucketName: !Ref BucketName
-}
diff --git a/test/pkl/pkl-out.pkl b/test/pkl/pkl-out.pkl
new file mode 100644
index 00000000..2649ad9c
--- /dev/null
+++ b/test/pkl/pkl-out.pkl
@@ -0,0 +1,16 @@
+amends "@cfn/template.pkl"
+import "@cfn/cloudformation.pkl" as cfn
+import "@cfn/aws/s3/bucket.pkl"
+
+Resources {
+    ["Bucket"] = new bucket.Bucket {
+        Type = "AWS::S3::Bucket"
+    }
+
+}
+
+Outputs {
+	["BucketName"] = new cfn.Output {
+		Value = cfn.Ref("Bucket")
+	}
+}
diff --git a/test/pkl/pkl-out.yaml b/test/pkl/pkl-out.yaml
new file mode 100644
index 00000000..93f5312a
--- /dev/null
+++ b/test/pkl/pkl-out.yaml
@@ -0,0 +1,9 @@
+Resources:
+  Bucket:
+    Type: AWS::S3::Bucket
+Outputs:
+  BucketName:
+    Value: !Ref Bucket
+    Export:
+      Name: foo
+