Current section

Files

Jump to
caffeine_lang src caffeine_query_language generator.gleam
Raw

src/caffeine_query_language/generator.gleam

import caffeine_query_language/parser.{
type Exp, type Operator, type Primary, PrimaryExp, PrimaryWord, Word,
}
import caffeine_query_language/resolver
import gleam/dict
import gleam/float
import gleam/int
import gleam/list
import gleam/option.{type Option}
import gleam/result
import gleam/string
import terra_madre/hcl
/// Converts an expression AST node to its string representation.
@internal
pub fn exp_to_string(exp: Exp) -> String {
case exp {
parser.Primary(primary:) -> primary_to_string(primary, option.None)
parser.TimeSliceExpr(spec) ->
"time_slice("
<> spec.query
<> " "
<> comparator_to_string(spec.comparator)
<> " "
<> float_to_string(spec.threshold)
<> " per "
<> float_to_string(spec.interval_seconds)
<> "s)"
parser.OperatorExpr(numerator:, denominator:, operator:) -> {
// Check if this entire expression tree is a path (all divisions with path-like components)
case operator, is_path_expression(exp) {
parser.Div, True -> {
// This is a path, render without spaces
exp_to_string_no_spaces(exp)
}
_, _ -> {
// Normal expression with spaces
let left =
exp_to_string_with_context(numerator, option.Some(operator), True)
let right =
exp_to_string_with_context(denominator, option.Some(operator), False)
let op = operator_to_datadog_query(operator)
left <> " " <> op <> " " <> right
}
}
}
}
}
fn comparator_to_string(comparator: parser.Comparator) -> String {
case comparator {
parser.LessThan -> "<"
parser.LessThanOrEqualTo -> "<="
parser.GreaterThan -> ">"
parser.GreaterThanOrEqualTo -> ">="
}
}
fn float_to_string(f: Float) -> String {
// Check if it's a whole number (no fractional part)
let truncated = float.truncate(f)
let is_whole = int.to_float(truncated) == f
case is_whole {
True -> int.to_string(truncated)
False -> float.to_string(f)
}
}
// Check if an expression is a path (all divisions with simple word components)
// A path expression starts with a field name ending in colon (like http.url_details.path:)
fn is_path_expression(exp: Exp) -> Bool {
// First check if the leftmost component is a field name (ends with :)
case get_leftmost_word(exp) {
option.Some(w) -> {
case string.ends_with(w, ":") {
True -> all_divisions(exp)
False -> False
}
}
option.None -> False
}
}
// Get the leftmost word in an expression tree
fn get_leftmost_word(exp: Exp) -> Option(String) {
case exp {
parser.Primary(parser.PrimaryWord(parser.Word(w))) -> option.Some(w)
parser.Primary(parser.PrimaryExp(inner_exp)) -> get_leftmost_word(inner_exp)
parser.TimeSliceExpr(_) -> option.None
parser.OperatorExpr(left, _, _) -> get_leftmost_word(left)
}
}
// Check if an expression is all divisions (no other operators)
fn all_divisions(exp: Exp) -> Bool {
case exp {
parser.Primary(_) -> True
parser.OperatorExpr(left, right, parser.Div) ->
all_divisions(left) && all_divisions(right)
_ -> False
}
}
// Convert expression to string without spaces (for paths)
fn exp_to_string_no_spaces(exp: Exp) -> String {
case exp {
parser.Primary(parser.PrimaryWord(parser.Word(w))) -> w
parser.OperatorExpr(left, right, parser.Div) ->
exp_to_string_no_spaces(left) <> "/" <> exp_to_string_no_spaces(right)
_ -> exp_to_string(exp)
}
}
fn exp_to_string_with_context(
exp: Exp,
parent_op: Option(Operator),
_is_left: Bool,
) -> String {
case exp {
parser.Primary(primary:) -> primary_to_string(primary, parent_op)
parser.TimeSliceExpr(_) -> exp_to_string(exp)
parser.OperatorExpr(numerator:, denominator:, operator:) -> {
// Check if this is a path expression to avoid adding spaces
// This is important when the division is part of a larger expression (e.g., with AND)
case operator, is_path_expression(exp) {
parser.Div, True -> exp_to_string_no_spaces(exp)
_, _ -> {
// Not a path, render with spaces
let left =
exp_to_string_with_context(numerator, option.Some(operator), True)
let right =
exp_to_string_with_context(denominator, option.Some(operator), False)
let op = operator_to_datadog_query(operator)
left <> " " <> op <> " " <> right
}
}
}
}
}
fn primary_to_string(primary: Primary, _parent_op: Option(Operator)) -> String {
case primary {
PrimaryWord(word:) -> word.value
PrimaryExp(exp:) -> {
// Always preserve explicit parentheses from the original input
"(" <> exp_to_string(exp) <> ")"
}
}
}
/// Converts a CQL operator to its Datadog query string representation.
@internal
pub fn operator_to_datadog_query(operator: parser.Operator) -> String {
case operator {
parser.Add -> "+"
parser.Sub -> "-"
parser.Mul -> "*"
parser.Div -> "/"
}
}
/// Transform an expression tree by substituting word values using a dictionary.
/// Words found in the dictionary are replaced with their corresponding values.
/// Words not found in the dictionary are left unchanged.
@internal
pub fn substitute_words(
exp: Exp,
substitutions: dict.Dict(String, String),
) -> Exp {
case exp {
parser.Primary(PrimaryWord(Word(name))) -> {
let value = dict.get(substitutions, name) |> result.unwrap(name)
parser.Primary(PrimaryWord(Word(value)))
}
parser.Primary(PrimaryExp(inner)) ->
parser.Primary(PrimaryExp(substitute_words(inner, substitutions)))
parser.TimeSliceExpr(spec) -> {
// Substitute in the query string if it matches a key
let query =
dict.get(substitutions, spec.query) |> result.unwrap(spec.query)
parser.TimeSliceExpr(parser.TimeSliceExp(..spec, query: query))
}
parser.OperatorExpr(left, right, op) ->
parser.OperatorExpr(
substitute_words(left, substitutions),
substitute_words(right, substitutions),
op,
)
}
}
/// Extracts all word names from an expression AST.
/// Returns a list of unique word strings found in the expression.
@internal
pub fn extract_words(exp: Exp) -> List(String) {
case exp {
parser.Primary(PrimaryWord(Word(name))) -> [name]
parser.Primary(PrimaryExp(inner)) -> extract_words(inner)
parser.TimeSliceExpr(_) -> []
parser.OperatorExpr(left, right, _) ->
list.append(extract_words(left), extract_words(right))
|> list.unique
}
}
/// Strips outer parentheses from a string if they wrap the entire expression.
/// E.g., "(a + b)" -> "a + b", but "(a + b) * c" stays unchanged.
fn strip_outer_parens(s: String) -> String {
let trimmed = string.trim(s)
case string.starts_with(trimmed, "(") && string.ends_with(trimmed, ")") {
True -> {
// Check if these parens actually wrap the whole expression
let inner = string.slice(trimmed, 1, string.length(trimmed) - 2)
// Verify parens are balanced in the inner part
case is_balanced(inner, 0) {
True -> inner
False -> trimmed
}
}
False -> trimmed
}
}
/// Check if parentheses are balanced in a string.
fn is_balanced(s: String, depth: Int) -> Bool {
case string.pop_grapheme(s) {
Error(_) -> depth == 0
Ok(#("(", rest)) -> is_balanced(rest, depth + 1)
Ok(#(")", rest)) ->
case depth {
0 -> False
// Closing without matching open
_ -> is_balanced(rest, depth - 1)
}
Ok(#(_, rest)) -> is_balanced(rest, depth)
}
}
/// Represents a single named query for TimeSlice formulas.
pub type NamedQuery {
NamedQuery(name: String, query: String)
}
/// Represents a resolved SLO query, either GoodOverTotal or TimeSlice.
pub type ResolvedSloQuery {
ResolvedGoodOverTotal(numerator: String, denominator: String)
ResolvedTimeSlice(
comparator: String,
interval_seconds: Int,
threshold: Float,
/// The formula expression (e.g., "build_time + deploy_time")
formula_expression: String,
/// List of named queries referenced by the formula
queries: List(NamedQuery),
)
}
/// Represents the SLO type for Datadog terraform generation.
pub type SloType {
MetricSlo
TimeSliceSlo
}
/// Resolved SLO with HCL blocks ready for terraform generation.
pub type ResolvedSloHcl {
ResolvedSloHcl(slo_type: SloType, blocks: List(hcl.Block))
}
/// Parse a value expression, resolve to primitive, substitute words,
/// and return the resolved SLO query type.
@internal
pub fn resolve_slo_query_typed(
value_expr: String,
substitutions: dict.Dict(String, String),
) -> Result(ResolvedSloQuery, String) {
case parser.parse_expr(value_expr) {
Error(err) -> Error("Parse error: " <> err)
Ok(exp_container) ->
case resolver.resolve_primitives(exp_container) {
Ok(resolver.GoodOverTotal(numerator_exp, denominator_exp)) -> {
let numerator_str =
substitute_words(numerator_exp, substitutions) |> exp_to_string
let denominator_str =
substitute_words(denominator_exp, substitutions) |> exp_to_string
Ok(ResolvedGoodOverTotal(numerator_str, denominator_str))
}
Ok(resolver.TimeSlice(comparator, interval_seconds, threshold, query)) -> {
let comparator_str = case comparator {
resolver.LessThan -> "<"
resolver.LessThanOrEqualTo -> "<="
resolver.GreaterThan -> ">"
resolver.GreaterThanOrEqualTo -> ">="
}
// Parse the query as an expression to extract word references
// The query could be a single word like "query1" or a formula like "(a + b)"
case parser.parse_expr(query) {
Ok(query_exp_container) -> {
let query_exp = query_exp_container.exp
let words = extract_words(query_exp)
// Build named queries by looking up each word in substitutions
let named_queries =
words
|> list.filter_map(fn(word) {
case dict.get(substitutions, word) {
Ok(resolved) -> Ok(NamedQuery(word, resolved))
Error(_) -> Error(Nil)
}
})
// If no substitutions were found, the query is likely a literal metric query
// In that case, use "query1" as the formula expression and the query as-is
case named_queries {
[] ->
Ok(
ResolvedTimeSlice(
comparator_str,
interval_seconds,
threshold,
"query1",
[NamedQuery("query1", query)],
),
)
_ -> {
// Use the original query string as the formula expression
// Strip outer parentheses if they wrap the entire expression
let formula_expr = strip_outer_parens(query)
Ok(ResolvedTimeSlice(
comparator_str,
interval_seconds,
threshold,
formula_expr,
named_queries,
))
}
}
}
Error(_) -> {
// If parsing fails, treat as a single literal query (backwards compat)
let resolved_query =
dict.get(substitutions, query) |> result.unwrap(query)
Ok(
ResolvedTimeSlice(
comparator_str,
interval_seconds,
threshold,
"query1",
[NamedQuery("query1", resolved_query)],
),
)
}
}
}
Error(err) -> Error("Resolution error: " <> err.msg)
}
}
}
/// Parse a value expression, resolve to GoodOverTotal primitive, substitute words,
/// and return the numerator and denominator as strings.
/// Panics if parsing or resolution fails.
@internal
pub fn resolve_slo_query(
value_expr: String,
substitutions: dict.Dict(String, String),
) -> #(String, String) {
case resolve_slo_query_typed(value_expr, substitutions) {
Ok(ResolvedGoodOverTotal(numerator, denominator)) -> #(
numerator,
denominator,
)
_ -> #("", "")
}
}
/// Parse a value expression, resolve to primitive, substitute words,
/// and return HCL blocks ready for Datadog terraform generation.
@internal
pub fn resolve_slo_to_hcl(
value_expr: String,
substitutions: dict.Dict(String, String),
) -> Result(ResolvedSloHcl, String) {
case resolve_slo_query_typed(value_expr, substitutions) {
Ok(ResolvedGoodOverTotal(numerator, denominator)) -> {
let query_block =
hcl.simple_block("query", [
#("numerator", hcl.StringLiteral(numerator)),
#("denominator", hcl.StringLiteral(denominator)),
])
Ok(ResolvedSloHcl(MetricSlo, [query_block]))
}
Ok(ResolvedTimeSlice(
comparator,
interval_seconds,
threshold,
formula_expression,
named_queries,
)) -> {
// Generate a metric_query block for each named query
let inner_query_blocks =
named_queries
|> list.map(fn(nq) {
let metric_query_block =
hcl.Block(
type_: "metric_query",
labels: [],
attributes: dict.from_list([
#("data_source", hcl.StringLiteral("metrics")),
#("name", hcl.StringLiteral(nq.name)),
#("query", hcl.StringLiteral(nq.query)),
]),
blocks: [],
)
hcl.Block(type_: "query", labels: [], attributes: dict.new(), blocks: [
metric_query_block,
])
})
let formula_block =
hcl.Block(
type_: "formula",
labels: [],
attributes: dict.from_list([
#("formula_expression", hcl.StringLiteral(formula_expression)),
]),
blocks: [],
)
let outer_query_block =
hcl.Block(type_: "query", labels: [], attributes: dict.new(), blocks: [
formula_block,
..inner_query_blocks
])
let time_slice_block =
hcl.Block(
type_: "time_slice",
labels: [],
attributes: dict.from_list([
#("comparator", hcl.StringLiteral(comparator)),
#("query_interval_seconds", hcl.IntLiteral(interval_seconds)),
#("threshold", hcl.FloatLiteral(threshold)),
]),
blocks: [outer_query_block],
)
let sli_specification_block =
hcl.Block(
type_: "sli_specification",
labels: [],
attributes: dict.new(),
blocks: [time_slice_block],
)
Ok(ResolvedSloHcl(TimeSliceSlo, [sli_specification_block]))
}
Error(err) -> Error(err)
}
}