Expressions
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.nat_gateway { 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 bindinglet env = 'prod'let zones = ['ap-northeast-1a', 'ap-northeast-1c']
# Top-level resource bindinglet vpc = awscc.ec2.vpc { cidr_block = '10.0.0.0/16'}
# Module import bindinglet network = import './modules/network'
# Remote state bindinglet shared = remote_state { path = 'shared.state.json'}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 readlet result = join('-', concat(['vpc'], ['prod', 'web']))
# With pipe: reads left to rightlet 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 functionlet transform = split(',') >> join('-')
# Apply the composed function via pipelet 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 themlet 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 closurelet 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 pipeUser-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' } }}