Use indentation in for loop

Hello,

I’m trying to use indent function with for expression to generate a map with indent but looks like I’m missing something.

Required

  "foo      = bar",
  "foo_long = bar_long",

Code

output "indent" {
  value = indent(10, "[\n  foo,\n  bar,\n]\n")
}

output "indent_loop" {
  value = [for k, v in { foo = "bar", foo_long = "bar_long" } : "${k} ${indent(10, "=")} ${v}"]
}

Output

Outputs:

indent = <<EOT
[
            foo,
            bar,
          ]

EOT
indent_loop = [
  "foo = bar",
  "foo_long = bar_long",
]

As the documentations says “adds a given number of spaces to the beginnings of all but the first line”. Your first example contains embedded \n newline characters, so works as expected.

However your second example consists of two single line strings in a list. Therefore indent wouldn’t do anything - no extra lines exist to indent. What are you actually trying to achieve, while might help us suggest a better solution?

1 Like

It is just to prettify CloudFront Function code, for readability

function handler(event) {
  var response = event.response;
  var headers = response.headers;

  %{ for header, value in var ~}
  headers['${header}'] = { value: '${value}' };
  %{ endfor ~}

  // Return the response to viewers
  return response;
}
function handler(event) {
  var response = event.response;
  var headers = response.headers;

    // Terraform
    headers['1'] = { value: 'value 1' };
    headers['123'] = { value: 'value 2' };
    headers['1234'] = { value: 'value 3' };
    headers['12'] = { value: 'value 4' };
    headers['12345678'] = { value: 'value 5' };

    // Manually
    headers['1']        = { value: 'value 1' };
    headers['123']      = { value: 'value 2' };
    headers['1234']     = { value: 'value 3' };
    headers['12']       = { value: 'value 4' };
    headers['12345678'] = { value: 'value 5' };

When you have tens of the headers in the list it requires additional effort to read it easily.

When we do it manually it looks good, when we switching to the variable with the map it is slightly different.

indent() definitely wouldn’t help here, as you aren’t wanting to indent anything.

It is possible, but won’t be the neatest code.

It should be possible to do this using format(). You’d need to loop through your map and calculate the longest header string. Then within your loop You can then use format() to insert max header length - current header length number of spaces before the =. You wouldn’t be able to do all this in your template code, as you’d need to be using local variables as well.

1 Like

@stuart-c, thank you for the direction!

All prettifying was moved to the locals and in template we just use these values

locals {
  headers = {
    header-123    = "value-123"
    header-123456 = "value-123456"
    header-78     = "value-78"
  }
  max_length     = max([for k, v in local.headers : length(format("headers['%s'] =", k))]...)
  headers_indent = { for k, v in local.headers : format("headers['%s']%${local.max_length - length(format("headers['%s']", k))}s", k, " =") => format("{ value: '%s' };", v) }
}
function handler(event) {
  var response = event.response;
  var headers = response.headers;

  %{ for header, value in var ~}
  ${header} ${value}
  %{ endfor ~}

  // Return the response to viewers
  return response;
}

I typically try to avoid implementing custom serializers using string concatenation whenever possible, because it tends to be awkward to handle edge cases like characters that have meaning in the language being generated.

In yor case, for example, if any of those “headers” values had the character ' in them then the result would be invalid syntax.

If you know that you’ll never encounter such situations and you are happy with what you have here then I think that’s fine, but since it looks like you’re generating JavaScript code and JSON is (more or less) a subset of JavaScript I wanted to offer an alternative that should be robust for all input.

function handler(event) {
  var response = event.response;
  var headers = response.headers;
  var newHeaders = ${jsonencode(headers)};

  for (const name in newHeaders) {
    headers[name] = newHeaders[name];
  }

  return response;
}

jsonencode always generates “minified” JSON and so the result will admittedly not be very human-readable. Your solution wins in that regard. But this new approach will work with any value of headers as long as it’s something that Terraform will encode as a JSON object, and Terraform will automatically deal with any escaping required to pass the string values through verbatim.

1 Like

@apparentlymart , thank you for the details. I checked it and your assumption is correct.

Test function

The CloudFront function associated with the CloudFront distribution is invalid or could not run. SyntaxError: Unexpected token "' };
" in 5

Reply from CloudFront

x-cache: FunctionThrottledError from cloudfront

So, no headers returned from the CloudFront.

But using your solution got another error

The CloudFront function associated with the CloudFront distribution is invalid or could not run. SyntaxError: Token "const" not supported in this version in 6

JavaScript runtime features for CloudFront Functions

Ahh… I wrote some “modern” JavaScript there because I’m not familiar with the CloudFront functions execution environment.

I expect there’s a similar for loop syntax that is valid in older versions of JavaScript, but I don’t write JavaScript very often right now so I’m not 100% sure. Perhaps the following would work well enough as an alternative:

  for (var name in newHeaders) {

I think this should declare a variable called name instead of a constant named name but otherwise be equivalent. Or if that doesn’t work either, I might try separating the variable declaration into a separate statement:

  var name;
  for (name in newHeaders) {

A JavaScript reference guide can hopefully explain more confidently what is available in the version of JavaScript that CloudFront supports. I referred to the MDN documentation on for ... in for what I suggested here.