Packages
caffeine_lang
0.0.21
6.3.1
6.3.0
6.2.2
6.2.1
6.2.0
6.1.2
6.1.1
6.1.0
6.0.0
5.6.0
5.5.0
5.4.4
5.4.3
5.4.2
5.4.1
5.4.0
5.3.0
5.2.0
5.1.1
5.1.0
5.0.12
5.0.11
5.0.10
5.0.8
5.0.7
5.0.6
5.0.5
5.0.4
5.0.1
5.0.0
4.10.0
4.9.0
4.8.3
4.8.2
4.8.1
4.8.0
4.7.9
4.7.8
4.7.7
4.7.6
4.7.5
4.6.7
4.6.6
4.6.5
4.6.4
4.6.3
4.6.2
4.6.0
4.5.1
4.5.0
4.4.4
4.4.3
4.4.1
4.4.0
4.3.7
4.3.6
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.0.2
1.0.1
0.1.0
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.2
0.0.1
A compiler for generating reliability artifacts from service expectation definitions.
Current section
Files
Jump to
Current section
Files
src/caffeine_lang/phase_4/slo_resolver.gleam
import caffeine_lang/cql/parser
import caffeine_lang/types/ast/organization
import caffeine_lang/types/ast/sli_type
import caffeine_lang/types/ast/slo
import caffeine_lang/types/common/accepted_types
import caffeine_lang/types/common/generic_dictionary
import caffeine_lang/types/resolved/resolved_sli
import caffeine_lang/types/resolved/resolved_slo
import gleam/dict
import gleam/int
import gleam/list
import gleam/result
import gleam/string
pub fn resolve_slos(
organization: organization.Organization,
) -> Result(List(resolved_slo.Slo), String) {
let sli_types =
organization.service_definitions
|> list.flat_map(fn(service_definition) {
service_definition.supported_sli_types
})
|> list.unique
organization.teams
|> list.map(fn(team) {
team.slos
|> list.map(fn(slo) { resolve_slo(slo, team.name, sli_types) })
|> result.all
})
|> result.all
|> result.map(list.flatten)
}
pub fn resolve_slo(
slo: slo.Slo,
team_name: String,
sli_types: List(sli_type.SliType),
) -> Result(resolved_slo.Slo, String) {
let assert Ok(sli_type) =
sli_types
|> list.find(fn(sli_type) { sli_type.name == slo.sli_type })
// Convert typed instantiation to Dict(String, String)
let filters_dict =
generic_dictionary.to_string_dict(
slo.typed_instatiation_of_query_templatized_variables,
)
use resolve_sli <- result.try(resolve_sli(filters_dict, sli_type, slo.name))
Ok(resolved_slo.Slo(
window_in_days: slo.window_in_days,
threshold: slo.threshold,
service_name: slo.service_name,
team_name: team_name,
sli: resolve_sli,
))
}
pub fn resolve_sli(
filters: dict.Dict(String, String),
sli_type: sli_type.SliType,
slo_name: String,
) -> Result(resolved_sli.Sli, String) {
use resolved_queries <- result.try(
sli_type.typed_instatiation_of_query_templates
|> generic_dictionary.to_string_dict
|> dict.to_list
|> list.try_map(fn(pair) {
let #(metric_attribute, template) = pair
// Process all template variables in the template string
use processed <- result.try(process_template_string(
template,
filters,
sli_type,
))
Ok(#(metric_attribute, processed))
})
|> result.map(dict.from_list),
)
// Resolve the CQL query by substituting words with resolved query values
use resolved_query <- result.try(resolve_cql_query(
sli_type.query_template_type.query,
resolved_queries,
))
Ok(resolved_sli.Sli(
name: slo_name,
query_template_type: sli_type.query_template_type,
metric_attributes: resolved_queries,
resolved_query: resolved_query,
))
}
fn resolve_cql_query(
query: parser.ExpContainer,
resolved_queries: dict.Dict(String, String),
) -> Result(parser.ExpContainer, String) {
case query {
parser.ExpContainer(exp) -> {
use resolved_exp <- result.try(resolve_exp(exp, resolved_queries))
Ok(parser.ExpContainer(resolved_exp))
}
}
}
fn resolve_exp(
exp: parser.Exp,
resolved_queries: dict.Dict(String, String),
) -> Result(parser.Exp, String) {
case exp {
parser.OperatorExpr(left, right, op) -> {
use resolved_left <- result.try(resolve_exp(left, resolved_queries))
use resolved_right <- result.try(resolve_exp(right, resolved_queries))
Ok(parser.OperatorExpr(resolved_left, resolved_right, op))
}
parser.Primary(primary) -> {
use resolved_primary <- result.try(resolve_primary(
primary,
resolved_queries,
))
Ok(parser.Primary(resolved_primary))
}
}
}
fn resolve_primary(
primary: parser.Primary,
resolved_queries: dict.Dict(String, String),
) -> Result(parser.Primary, String) {
case primary {
parser.PrimaryWord(word) -> {
case word {
parser.Word(word_value) -> {
case dict.get(resolved_queries, word_value) {
Ok(resolved_value) -> {
// Parse the resolved value as a new expression
case parser.parse_expr(resolved_value) {
Ok(parsed_exp) -> {
case parsed_exp {
parser.ExpContainer(exp) -> Ok(parser.PrimaryExp(exp))
}
}
Error(_) -> {
// If parsing fails, treat as a literal word
Ok(parser.PrimaryWord(parser.Word(resolved_value)))
}
}
}
Error(_) -> {
// Word not found in resolved queries, keep as is
Ok(parser.PrimaryWord(word))
}
}
}
}
}
parser.PrimaryExp(exp) -> {
use resolved_exp <- result.try(resolve_exp(exp, resolved_queries))
Ok(parser.PrimaryExp(resolved_exp))
}
}
}
// Process a complete template string, finding and replacing all template variables
fn process_template_string(
template: String,
filters: dict.Dict(String, String),
sli_type: sli_type.SliType,
) -> Result(String, String) {
// Find all template variables by splitting on $$
let parts = string.split(template, "$$")
// Process parts: odd indices are template variables, even indices are literal text
use result <- result.try(process_template_parts(parts, [], filters, sli_type, False))
// Clean up any remaining comma issues and convert to AND syntax
let cleaned = cleanup_empty_optionals(result)
Ok(convert_commas_to_and(cleaned))
}
// Helper to process template parts alternating between literal text and variables
fn process_template_parts(
parts: List(String),
acc: List(String),
filters: dict.Dict(String, String),
sli_type: sli_type.SliType,
is_variable: Bool,
) -> Result(String, String) {
case parts {
[] -> Ok(string.join(list.reverse(acc), ""))
[part, ..rest] -> {
case is_variable {
False -> {
// This is literal text, keep as is
process_template_parts(rest, [part, ..acc], filters, sli_type, True)
}
True -> {
// This is a template variable, process it
use replacement <- result.try(process_template_variable(
part,
filters,
sli_type,
))
process_template_parts(rest, [replacement, ..acc], filters, sli_type, False)
}
}
}
}
}
// Clean up hanging commas and spaces from empty optional fields
// This handles edge cases where multiple consecutive optional fields are missing
fn cleanup_empty_optionals(query: String) -> String {
query
// Remove multiple consecutive ", , " patterns (handles 2+ missing optionals)
|> string.replace(", , , ", ", ")
|> string.replace(", , ", ", ")
// Remove leading comma after opening brace (all initial optionals missing)
|> string.replace("{, ", "{")
// Remove trailing comma before closing brace (all trailing optionals missing)
|> string.replace(", }", "}")
// Remove trailing comma before closing paren
|> string.replace(", )", ")")
// Handle case where only commas remain in braces
|> string.replace("{,}", "{}")
}
// Convert comma-separated tags to AND-separated tags for Datadog queries
// Datadog requires AND operators when there are OR expressions in the query
fn convert_commas_to_and(query: String) -> String {
// Only convert if the query contains OR expressions (parentheses with OR)
// Otherwise, Datadog accepts comma-separated tags
case string.contains(query, " OR ") {
True -> convert_commas_in_braces(query, "", False)
False -> query
}
}
// Helper function to replace commas with AND only inside curly braces
fn convert_commas_in_braces(
remaining: String,
acc: String,
inside_braces: Bool,
) -> String {
case string.pop_grapheme(remaining) {
Ok(#("{", rest)) -> {
// Entering braces
convert_commas_in_braces(rest, acc <> "{", True)
}
Ok(#("}", rest)) -> {
// Exiting braces
convert_commas_in_braces(rest, acc <> "}", False)
}
Ok(#(",", rest)) -> {
case inside_braces {
True -> {
// Inside braces, replace comma with " AND "
// Check if followed by space to avoid double spaces
case string.pop_grapheme(rest) {
Ok(#(" ", rest2)) -> {
// Replace ", " with " AND "
convert_commas_in_braces(rest2, acc <> " AND ", True)
}
_ -> {
// Replace "," with " AND " (no space after comma)
convert_commas_in_braces(rest, acc <> " AND ", True)
}
}
}
False -> {
// Outside braces, keep comma as is
convert_commas_in_braces(rest, acc <> ",", False)
}
}
}
Ok(#(char, rest)) -> {
// Any other character, keep it
convert_commas_in_braces(rest, acc <> char, inside_braces)
}
Error(_) -> {
// End of string
acc
}
}
}
// Parse and process a single template variable
fn process_template_variable(
template_var: String,
filters: dict.Dict(String, String),
sli_type: sli_type.SliType,
) -> Result(String, String) {
// Parse the template variable to extract components
use #(field_name, var_name, is_negated) <- result.try(parse_template_variable(
template_var,
))
// Check if this is an optional type
case find_filter_type(sli_type, var_name) {
Ok(accepted_types.Optional(_)) -> {
// For optional types, if not provided, return empty string
case dict.get(filters, var_name) {
Ok(value) -> {
use processed_value <- result.try(process_filter_value(
value,
sli_type,
var_name,
field_name,
))
// Apply negation if needed
case is_negated {
True -> Ok("NOT (" <> processed_value <> ")")
False -> Ok(processed_value)
}
}
Error(_) -> Ok("")
}
}
_ -> {
// For non-optional types, require the value
case dict.get(filters, var_name) {
Ok(value) -> {
// Process the value based on its type
use processed_value <- result.try(process_filter_value(
value,
sli_type,
var_name,
field_name,
))
// Apply negation if needed
case is_negated {
True -> Ok("NOT (" <> processed_value <> ")")
False -> Ok(processed_value)
}
}
Error(_) ->
Error(
"Template variable '" <> var_name <> "' not found in filters",
)
}
}
}
}
// Parse template variable string to extract field name, variable name, and negation flag
// Input: "$$field->var$$" or "$$NOT[field->var]$$"
// Output: #(field_name, var_name, is_negated)
fn parse_template_variable(
template_var: String,
) -> Result(#(String, String, Bool), String) {
// Remove $$ markers
let content =
template_var
|> string.replace("$$", "")
// Check for NOT prefix
let #(content, is_negated) = case string.starts_with(content, "NOT[") {
True -> {
let inner =
content
|> string.replace("NOT[", "")
|> string.replace("]", "")
#(inner, True)
}
False -> #(content, False)
}
// Split by -> to get field and variable name
case string.split(content, "->") {
[field_name, var_name] -> Ok(#(field_name, var_name, is_negated))
_ ->
Error(
"Invalid template variable format: " <> template_var <> ". Expected $$field->var$$ or $$NOT[field->var]$$",
)
}
}
fn process_filter_value(
value: String,
sli_type: sli_type.SliType,
filter_name: String,
field_name: String,
) -> Result(String, String) {
// Check the filter type
case find_filter_type(sli_type, filter_name) {
Ok(accepted_types.NonEmptyList(inner_type)) -> {
case inner_type {
accepted_types.String -> {
case parse_list_value(value, inner_parse_string) {
Ok(parsed_list) -> {
case parsed_list {
[] ->
Error("Empty list not allowed for NonEmptyList field '" <> filter_name <> "': must contain at least one value")
_ -> Ok(convert_list_to_or_expression(parsed_list, field_name))
}
}
Error(err) -> Error("Error parsing NonEmptyList field '" <> filter_name <> "': " <> err)
}
}
accepted_types.Integer -> {
case parse_list_value(value, inner_parse_int) {
Ok(parsed_list) -> {
case parsed_list {
[] ->
Error("Empty list not allowed for NonEmptyList field '" <> filter_name <> "': must contain at least one value")
_ ->
Ok(
convert_list_to_or_expression(
list.map(parsed_list, int.to_string),
field_name,
),
)
}
}
Error(err) -> Error("Error parsing NonEmptyList field '" <> filter_name <> "': " <> err)
}
}
_ -> Ok(field_name <> ":" <> value)
}
}
Ok(accepted_types.Optional(inner_type)) -> {
// For optional types, process the inner type
case inner_type {
accepted_types.NonEmptyList(list_inner_type) -> {
// Handle Optional(NonEmptyList(...))
case list_inner_type {
accepted_types.String -> {
case parse_list_value(value, inner_parse_string) {
Ok(parsed_list) -> {
case parsed_list {
[] ->
Error("Empty list not allowed for NonEmptyList field '" <> filter_name <> "': must contain at least one value")
_ -> Ok(convert_list_to_or_expression(parsed_list, field_name))
}
}
Error(err) -> Error("Error parsing NonEmptyList field '" <> filter_name <> "': " <> err)
}
}
accepted_types.Integer -> {
case parse_list_value(value, inner_parse_int) {
Ok(parsed_list) -> {
case parsed_list {
[] ->
Error("Empty list not allowed for NonEmptyList field '" <> filter_name <> "': must contain at least one value")
_ ->
Ok(
convert_list_to_or_expression(
list.map(parsed_list, int.to_string),
field_name,
),
)
}
}
Error(err) -> Error("Error parsing NonEmptyList field '" <> filter_name <> "': " <> err)
}
}
_ -> Ok(field_name <> ":" <> value)
}
}
accepted_types.String -> Ok(field_name <> ":" <> value)
accepted_types.Integer -> Ok(field_name <> ":" <> value)
accepted_types.Boolean -> Ok(field_name <> ":" <> value)
accepted_types.Decimal -> Ok(field_name <> ":" <> value)
_ -> Ok(field_name <> ":" <> value)
}
}
_ -> Ok(field_name <> ":" <> value)
}
}
pub fn convert_list_to_or_expression(items: List(String), field_name: String) -> String {
case items {
[] -> "[]"
[single] -> {
// Check if the value contains special characters that could be parsed as operators
// If so, wrap in parentheses to prevent CQL parsing issues
let value = field_name <> ":" <> single
case string.contains(single, "*") || string.contains(single, "+") || string.contains(single, "-") || string.contains(single, "/") {
True -> "(" <> value <> ")"
False -> value
}
}
_multiple -> {
let or_parts = list.map(items, fn(item) { field_name <> ":" <> item })
"(" <> string.join(or_parts, " OR ") <> ")"
}
}
}
pub fn parse_list_value(
value: String,
inner_parse: fn(String) -> Result(a, String),
) -> Result(List(a), String) {
let splitted = string.split(value, "]")
let splitted = string.split(string.join(splitted, ""), "[")
let content = string.join(splitted, "") |> string.trim
// Handle empty list case
case content {
"" -> Error("Empty list not allowed: list must contain at least one value")
_ -> {
let parse_result =
content
|> string.split(",")
|> list.map(inner_parse)
|> result.all
case parse_result {
Error(parse_error) -> Error("Failed to parse list values: " <> parse_error)
Ok(parsed_list) -> Ok(parsed_list)
}
}
}
}
pub fn inner_parse_string(value: String) -> Result(String, String) {
let splitted = string.split(value, "\"")
let result =
string.join(splitted, "")
|> string.trim
Ok(result)
}
pub fn inner_parse_int(value: String) -> Result(Int, String) {
case int.parse(string.trim(value)) {
Ok(int_value) -> Ok(int_value)
Error(_) -> Error("Invalid integer value: " <> value)
}
}
fn find_filter_type(
sli_type: sli_type.SliType,
filter_name: String,
) -> Result(accepted_types.AcceptedTypes, String) {
sli_type.specification_of_query_templatized_variables
|> list.find(fn(basic_type) { basic_type.attribute_name == filter_name })
|> result.map(fn(basic_type) { basic_type.attribute_type })
|> result.replace_error("Filter type not found")
}