Expressions
For loops, if/else conditionals, let bindings, pipe operator, compose operator, and function definitions in the Carina DSL.
Expressions in the Carina DSL produce values. They can appear as attribute values, function arguments, or in any context where a value is expected.
For Expression
The for expression iterates over a list or map to create multiple resources or values.
Iterating Over a List
let vpcs = for env in ['dev', 'stg'] {
awscc.ec2.Vpc {
cidr_block = '10.0.0.0/16'
tags = {
Name = "vpc-${env}"
Environment = env
}
}
}
Indexed Iteration
Use (index, value) to access both the index and the element:
let vpcs = for (i, env) in ['dev', 'stg'] {
awscc.ec2.Vpc {
cidr_block = cidr_subnet('10.0.0.0/8', 8, i)
tags = {
Name = "for-list-test-${env}"
Environment = env
}
}
}
Iterating Over a Map
Use key, value to iterate over map entries:
let cidrs = {
dev = '10.0.0.0/16'
stg = '10.1.0.0/16'
}
let vpcs = for name, cidr in cidrs {
awscc.ec2.Vpc {
cidr_block = cidr
tags = {
Name = "vpc-${name}"
Environment = name
}
}
}
Local Bindings in For Body
let bindings can be used inside a for body to compute intermediate values:
let networks = for name, cidr in cidrs {
let subnet_cidr = cidr_subnet(cidr, 8, 1)
network {
cidr_block = cidr
subnet_cidr = subnet_cidr
az = 'ap-northeast-1a'
}
}
For with Module Calls
for works with module calls to create multiple instances of a module. See Modules: Modules with For Expressions for a full example.
If Expression
The if expression conditionally produces a resource or value.
Conditional Resource Creation
let is_production = true
if is_production {
awscc.ec2.NatGateway {
allocation_id = eip.allocation_id
subnet_id = subnet.subnet_id
}
}
If/Else as a Value
if/else can be used inline to choose between values:
awscc.ec2.Vpc {
cidr_block = if is_production { '10.0.0.0/16' } else { '172.16.0.0/16' }
tags = {
Name = if is_production { 'prod-vpc' } else { 'dev-vpc' }
}
}
Local Bindings in If Body
let bindings can be used inside if and else blocks:
if is_production {
let cidr = '10.0.0.0/16'
awscc.ec2.Vpc {
cidr_block = cidr
}
}
Let Binding
let binds a name to a value. At the top level, it binds resources or values. Inside blocks, it creates scoped variables.
# Top-level value binding
let env = 'prod'
let zones = ['ap-northeast-1a', 'ap-northeast-1c']
# Top-level resource binding
let vpc = awscc.ec2.Vpc {
cidr_block = '10.0.0.0/16'
}
# Module use binding
let network = use { source = './modules/network' }
Upstream state is bound with let <binding> = upstream_state { source = "..." }. See Upstream State for details.
Use let _ = (the discard pattern) when you need to evaluate an expression but do not need to reference the result. See Syntax: Discard Pattern for details.
Pipe Operator (|>)
The pipe operator passes the result of the left expression as the last argument to the function on the right. This enables readable left-to-right data transformations.
# Without pipe: nested calls are hard to read
let result = join('-', concat(['vpc'], ['prod', 'web']))
# With pipe: reads left to right
let result = ['prod', 'web'] |> concat(['vpc']) |> join('-')
The pipe operator works with any built-in function. The piped value becomes the last argument:
# These are equivalent:
join('-', ['a', 'b', 'c'])
['a', 'b', 'c'] |> join('-')
# These are equivalent:
replace('-', '_', 'hello-world')
'hello-world' |> replace('-', '_')
# These are equivalent:
split(',', 'a,b,c')
'a,b,c' |> split(',')
Chaining Multiple Pipes
Multiple pipe operations can be chained for complex transformations:
let result = ['prod', 'web']
|> concat(['vpc'])
|> join('-')
|> upper()
Compose Operator (>>)
The compose operator (>>) combines two partially applied functions into a new function. The result of the first function is passed as input to the second. Both sides of >> must be closures (partially applied functions).
# Compose split and join into a single function
let transform = split(',') >> join('-')
# Apply the composed function via pipe
let result = 'a,b,c' |> transform()
# => 'a-b-c'
Compose works with any partially applied built-in function:
# Extract IDs from a list of maps, then join them
let pipeline = map('.id') >> join(', ')
let result = [{ id = '1' }, { id = '2' }] |> pipeline()
# => '1, 2'
Three or more functions can be composed:
let transform = split(',') >> join('-') >> split('-')
The compose operator binds tighter than the pipe operator, so f >> g |> h means (f >> g) |> h.
Function Calls
Functions are called with parentheses:
let len = length(zones)
let subnet = cidr_subnet('10.0.0.0/16', 8, 1)
let name = join('-', ['prod', 'web', 'vpc'])
Partial Application
When a built-in function is called with fewer arguments than it expects, it returns a closure that captures the provided arguments. The closure waits for the remaining arguments before executing.
# split expects 2 args: split(separator, string)
# Providing only 1 creates a closure
let split_by_comma = split(',')
# The closure can be used with pipe (parentheses are required)
let parts = 'a,b,c' |> split_by_comma()
This is particularly useful with the pipe operator:
let result = 'hello-world' |> replace('-', '_')
# replace(search, replacement, string) gets '-' and '_' captured,
# then 'hello-world' is supplied as the third argument via pipe
User-Defined Functions
Define functions with fn. Parameters can have optional type annotations and default values:
fn tag_name(env: String, service: String): String {
join('-', [env, service, 'vpc'])
}
Parameters
Function parameters support:
- Type annotations:
name: String - Default values:
name: String = "default" - No annotation:
name(any type accepted)
fn make_tags(env: String, service: String, team: String = 'platform') {
{
Environment = env
Service = service
Team = team
}
}
Local Bindings in Functions
Functions can contain let bindings before the return expression:
fn subnet_name(env: String, az: String): String {
let short_az = replace('ap-northeast-1', '', az)
"${env}-subnet-${short_az}"
}
Return Type
The optional return type annotation follows the parameter list with a colon:
fn cidr_for_env(env: String): String {
lookup({ dev = '10.0.0.0/16', stg = '10.1.0.0/16' }, env, '10.99.0.0/16')
}
Validate Expressions
Validate expressions are boolean expressions used in arguments blocks for input validation and in require statements. They support comparison and logical operators.
Comparison Operators
| Operator | Meaning |
|---|---|
== | Equal |
!= | Not equal |
> | Greater than |
< | Less than |
>= | Greater than or equal |
<= | Less than or equal |
Logical Operators
| Operator | Meaning |
|---|---|
&& | Logical AND |
|| | Logical OR |
! | Logical NOT |
Example: Argument Validation
arguments {
instance_count: Int {
description = 'Number of instances to create'
default = 1
validation {
condition = instance_count >= 1 && instance_count <= 10
error_message = 'Instance count must be between 1 and 10'
}
}
}
Function calls can be used in validate expressions:
arguments {
name: String {
validation {
condition = length(name) > 0
error_message = 'Name must not be empty'
}
}
}