Packages
caffeine_lang
5.4.1
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/frontend/validator.gleam
/// Validation for Caffeine frontend AST.
/// Handles extendable-related validation that must occur before JSON generation.
import caffeine_lang/frontend/ast.{
type ExpectItem, type ExpectsFile, type Extendable, type Field,
type MeasurementItem, type MeasurementsFile, type Parsed, type TypeAlias,
type Validated,
}
import caffeine_lang/types.{
type ParsedType, type PrimitiveTypes, Boolean, Defaulted, Dict, InclusiveRange,
NumericType, OneOf, ParsedCollection, ParsedModifier, ParsedPrimitive,
ParsedRefinement, ParsedTypeAliasRef, Percentage, SemanticType,
String as StringType,
}
import gleam/bool
import gleam/dict
import gleam/float
import gleam/int
import gleam/list
import gleam/result
import gleam/set
import gleam/string
/// Errors that can occur during validation.
pub type ValidatorError {
DuplicateExtendable(name: String)
UndefinedExtendable(
name: String,
referenced_by: String,
candidates: List(String),
)
DuplicateExtendsReference(name: String, referenced_by: String)
InvalidExtendableKind(name: String, expected: String, got: String)
UndefinedTypeAlias(
name: String,
referenced_by: String,
candidates: List(String),
)
DuplicateTypeAlias(name: String)
CircularTypeAlias(name: String, cycle: List(String))
InvalidDictKeyTypeAlias(
alias_name: String,
resolved_to: String,
referenced_by: String,
)
ExtendableOvershadowing(
field_name: String,
item_name: String,
extendable_name: String,
)
ExtendableTypeAliasNameCollision(name: String)
InvalidRefinementValue(
value: String,
expected_type: String,
referenced_by: String,
)
InvalidPercentageBounds(value: String, referenced_by: String)
}
/// Validates a measurements file.
/// Checks for duplicate extendables, undefined extendable references,
/// duplicate type aliases, circular type aliases, undefined type alias references,
/// and that Dict key type aliases resolve to String-based types.
/// Returns all independent validation errors instead of stopping at the first.
@internal
pub fn validate_measurements_file(
file: MeasurementsFile(Parsed),
) -> Result(MeasurementsFile(Validated), List(ValidatorError)) {
let type_aliases = file.type_aliases
let extendables = file.extendables
let items = file.items
// Group A: type alias structural checks (sequential — circularity depends on no dupes)
let type_alias_errors =
validate_no_duplicates(type_aliases, fn(ta) { ta.name }, DuplicateTypeAlias)
|> result.try(fn(_) { validate_no_circular_type_aliases(type_aliases) })
|> errors_to_list
// Group C: extendable structural checks (independent of Group A)
let extendable_errors =
collect_errors([
validate_no_duplicates(extendables, fn(e) { e.name }, DuplicateExtendable),
validate_no_extendable_type_alias_collision(extendables, type_aliases),
])
// If structural checks failed, skip dependent checks and return all errors so far
let structural_errors = list.append(type_alias_errors, extendable_errors)
use <- guard_errors(structural_errors)
// Build lookup structures (safe because structural checks passed)
let type_alias_names =
type_aliases
|> list.map(fn(ta) { ta.name })
|> set.from_list
let type_alias_map = build_type_alias_map(type_aliases)
// Group B: type alias reference checks and refinement value checks
let type_ref_errors =
collect_errors([
validate_type_aliases_type_refs(
type_aliases,
type_alias_names,
type_alias_map,
),
validate_extendables_type_refs(
extendables,
type_alias_names,
type_alias_map,
),
validate_measurement_items_type_refs(
items,
type_alias_names,
type_alias_map,
),
])
// Group D: extends validation (depends on extendable structural checks passing)
let extends_errors =
validate_measurement_items_extends(items, extendables) |> errors_to_list
let dependent_errors = list.append(type_ref_errors, extends_errors)
use <- guard_errors(dependent_errors)
Ok(ast.promote_measurements_file(file))
}
/// Validates an expects file.
/// Checks for duplicate extendables, undefined extendable references,
/// and that all extendables are Provides kind.
/// Returns all independent validation errors instead of stopping at the first.
@internal
pub fn validate_expects_file(
file: ExpectsFile(Parsed),
) -> Result(ExpectsFile(Validated), List(ValidatorError)) {
let extendables = file.extendables
let items =
file.blocks
|> list.flat_map(fn(block) { block.items })
// Structural checks (independent of each other)
let structural_errors =
collect_errors([
validate_no_duplicates(extendables, fn(e) { e.name }, DuplicateExtendable),
validate_extendables_are_provides(extendables),
])
use <- guard_errors(structural_errors)
// Depends on structural checks passing
let extends_errors =
validate_expect_items_extends(items, extendables) |> errors_to_list
use <- guard_errors(extends_errors)
Ok(ast.promote_expects_file(file))
}
/// Validates that no two items in a list share the same name.
fn validate_no_duplicates(
items: List(a),
get_name: fn(a) -> String,
make_error: fn(String) -> ValidatorError,
) -> Result(Nil, ValidatorError) {
validate_no_duplicates_loop(items, get_name, make_error, set.new())
}
fn validate_no_duplicates_loop(
items: List(a),
get_name: fn(a) -> String,
make_error: fn(String) -> ValidatorError,
seen: set.Set(String),
) -> Result(Nil, ValidatorError) {
case items {
[] -> Ok(Nil)
[first, ..rest] -> {
let name = get_name(first)
use <- bool.guard(
when: set.contains(seen, name),
return: Error(make_error(name)),
)
validate_no_duplicates_loop(
rest,
get_name,
make_error,
set.insert(seen, name),
)
}
}
}
/// Validates that no extendable shares a name with a type alias.
fn validate_no_extendable_type_alias_collision(
extendables: List(Extendable),
type_aliases: List(TypeAlias),
) -> Result(Nil, ValidatorError) {
let type_alias_names =
type_aliases
|> list.map(fn(ta) { ta.name })
|> set.from_list
list.try_each(extendables, fn(ext) {
use <- bool.guard(
when: set.contains(type_alias_names, ext.name),
return: Error(ExtendableTypeAliasNameCollision(name: ext.name)),
)
Ok(Nil)
})
}
/// Validates that all extendables in an expects file are Provides kind.
fn validate_extendables_are_provides(
extendables: List(Extendable),
) -> Result(Nil, ValidatorError) {
case extendables {
[] -> Ok(Nil)
[first, ..rest] -> {
case first.kind {
ast.ExtendableProvides -> validate_extendables_are_provides(rest)
ast.ExtendableRequires ->
Error(InvalidExtendableKind(
name: first.name,
expected: "Provides",
got: "Requires",
))
}
}
}
}
/// Validates extends references for measurement items.
fn validate_measurement_items_extends(
items: List(MeasurementItem),
extendables: List(Extendable),
) -> Result(Nil, ValidatorError) {
let extendable_names =
extendables
|> list.map(fn(e) { e.name })
|> set.from_list
let extendable_map = build_extendable_field_map(extendables)
list.try_each(items, fn(item) {
use _ <- result.try(validate_extends_exist(
item.extends,
item.name,
extendable_names,
))
use _ <- result.try(validate_no_duplicate_extends(item.extends, item.name))
// Check that item's requires/provides don't overshadow extended fields
let requires_fields =
item.requires.fields |> list.map(fn(f) { f.name }) |> set.from_list
let provides_fields =
item.provides.fields |> list.map(fn(f) { f.name }) |> set.from_list
validate_no_overshadowing(
item.name,
item.extends,
[requires_fields, provides_fields],
extendable_map,
)
})
}
/// Validates extends references for expect items.
fn validate_expect_items_extends(
items: List(ExpectItem),
extendables: List(Extendable),
) -> Result(Nil, ValidatorError) {
let extendable_names =
extendables
|> list.map(fn(e) { e.name })
|> set.from_list
let extendable_map = build_extendable_field_map(extendables)
list.try_each(items, fn(item) {
use _ <- result.try(validate_extends_exist(
item.extends,
item.name,
extendable_names,
))
use _ <- result.try(validate_no_duplicate_extends(item.extends, item.name))
// Check that item's provides don't overshadow extended fields
let provides_fields =
item.provides.fields |> list.map(fn(f) { f.name }) |> set.from_list
validate_no_overshadowing(
item.name,
item.extends,
[provides_fields],
extendable_map,
)
})
}
/// Validates that all names in extends list exist as extendables.
fn validate_extends_exist(
extends: List(String),
item_name: String,
extendable_names: set.Set(String),
) -> Result(Nil, ValidatorError) {
case extends {
[] -> Ok(Nil)
[first, ..rest] -> {
use <- bool.guard(
when: !set.contains(extendable_names, first),
return: Error(UndefinedExtendable(
name: first,
referenced_by: item_name,
candidates: set.to_list(extendable_names),
)),
)
validate_extends_exist(rest, item_name, extendable_names)
}
}
}
/// Validates that no extendable is referenced twice in the same extends list.
fn validate_no_duplicate_extends(
extends: List(String),
item_name: String,
) -> Result(Nil, ValidatorError) {
validate_no_duplicate_extends_loop(extends, item_name, set.new())
}
fn validate_no_duplicate_extends_loop(
extends: List(String),
item_name: String,
seen: set.Set(String),
) -> Result(Nil, ValidatorError) {
case extends {
[] -> Ok(Nil)
[first, ..rest] -> {
use <- bool.guard(
when: set.contains(seen, first),
return: Error(DuplicateExtendsReference(
name: first,
referenced_by: item_name,
)),
)
validate_no_duplicate_extends_loop(
rest,
item_name,
set.insert(seen, first),
)
}
}
}
/// Builds a map of extendable name to its field names for overshadowing checks.
fn build_extendable_field_map(
extendables: List(Extendable),
) -> dict.Dict(String, set.Set(String)) {
extendables
|> list.map(fn(e) {
let field_names =
e.body.fields
|> list.map(fn(f) { f.name })
|> set.from_list
#(e.name, field_names)
})
|> dict.from_list
}
/// Validates that an item's field sets don't overshadow fields from its extended extendables.
fn validate_no_overshadowing(
item_name: String,
extends: List(String),
field_sets: List(set.Set(String)),
extendable_map: dict.Dict(String, set.Set(String)),
) -> Result(Nil, ValidatorError) {
list.try_each(extends, fn(ext_name) {
case dict.get(extendable_map, ext_name) {
Error(_) -> Ok(Nil)
Ok(ext_fields) ->
list.try_each(field_sets, fn(item_fields) {
case set.intersection(item_fields, ext_fields) |> set.to_list {
[] -> Ok(Nil)
[field, ..] ->
Error(ExtendableOvershadowing(
field_name: field,
item_name: item_name,
extendable_name: ext_name,
))
}
})
}
})
}
// =============================================================================
// TYPE ALIAS VALIDATION
// =============================================================================
/// Builds a map of type alias name to its type for validation.
fn build_type_alias_map(
type_aliases: List(TypeAlias),
) -> List(#(String, ParsedType)) {
ast.build_type_alias_pairs(type_aliases)
}
/// Validates that no type alias has circular references.
fn validate_no_circular_type_aliases(
type_aliases: List(TypeAlias),
) -> Result(Nil, ValidatorError) {
let type_alias_map = build_type_alias_map(type_aliases)
list.try_each(type_aliases, fn(ta) {
validate_type_alias_not_circular(
ta.name,
ta.type_,
type_alias_map,
[ta.name],
set.from_list([ta.name]),
)
})
}
/// Checks if a type contains a circular reference.
/// Uses try_each_inner_parsed to handle the structural decomposition of compound types,
/// with leaf-specific logic for ParsedPrimitive and ParsedTypeAliasRef.
fn validate_type_alias_not_circular(
original_name: String,
typ: ParsedType,
type_alias_map: List(#(String, ParsedType)),
visited_path: List(String),
visited_set: set.Set(String),
) -> Result(Nil, ValidatorError) {
case typ {
ParsedPrimitive(_) -> Ok(Nil)
ParsedTypeAliasRef(name) -> {
use <- bool.guard(
when: set.contains(visited_set, name),
return: Error(CircularTypeAlias(
name: original_name,
cycle: visited_path,
)),
)
case list.key_find(type_alias_map, name) {
Ok(resolved) ->
validate_type_alias_not_circular(
original_name,
resolved,
type_alias_map,
[name, ..visited_path],
set.insert(visited_set, name),
)
// Undefined ref is caught elsewhere
Error(_) -> Ok(Nil)
}
}
// For compound types, decompose and recurse via try_each_inner_parsed
_ ->
types.try_each_inner_parsed(typ, fn(inner) {
validate_type_alias_not_circular(
original_name,
inner,
type_alias_map,
visited_path,
visited_set,
)
})
}
}
/// Validates type alias definitions for type refs and refinement values.
fn validate_type_aliases_type_refs(
type_aliases: List(TypeAlias),
type_alias_names: set.Set(String),
type_alias_map: List(#(String, ParsedType)),
) -> Result(Nil, ValidatorError) {
list.try_each(type_aliases, fn(ta) {
validate_type_refs(ta.type_, ta.name, type_alias_names, type_alias_map)
})
}
/// Validates type alias references in extendables.
fn validate_extendables_type_refs(
extendables: List(Extendable),
type_alias_names: set.Set(String),
type_alias_map: List(#(String, ParsedType)),
) -> Result(Nil, ValidatorError) {
list.try_each(extendables, fn(ext) {
validate_fields_type_refs(
ext.body.fields,
ext.name,
type_alias_names,
type_alias_map,
)
})
}
/// Validates type alias references in measurement items.
fn validate_measurement_items_type_refs(
items: List(MeasurementItem),
type_alias_names: set.Set(String),
type_alias_map: List(#(String, ParsedType)),
) -> Result(Nil, ValidatorError) {
list.try_each(items, fn(item) {
validate_fields_type_refs(
item.requires.fields,
item.name,
type_alias_names,
type_alias_map,
)
})
}
/// Validates type alias references in a list of fields.
fn validate_fields_type_refs(
fields: List(Field),
context_name: String,
type_alias_names: set.Set(String),
type_alias_map: List(#(String, ParsedType)),
) -> Result(Nil, ValidatorError) {
list.try_each(fields, fn(field) {
case field.value {
ast.TypeValue(typ) ->
validate_type_refs(typ, context_name, type_alias_names, type_alias_map)
ast.LiteralValue(_) -> Ok(Nil)
}
})
}
/// Validates that all ParsedTypeAliasRef in a type are defined.
/// Uses try_each_inner_parsed to handle the structural decomposition of compound types.
/// Adds special Dict key validation and refinement value validation.
fn validate_type_refs(
typ: ParsedType,
context_name: String,
type_alias_names: set.Set(String),
type_alias_map: List(#(String, ParsedType)),
) -> Result(Nil, ValidatorError) {
case typ {
ParsedPrimitive(_) -> Ok(Nil)
ParsedTypeAliasRef(name) -> {
use <- bool.guard(
when: set.contains(type_alias_names, name),
return: Ok(Nil),
)
Error(UndefinedTypeAlias(
name: name,
referenced_by: context_name,
candidates: set.to_list(type_alias_names),
))
}
// For Dict, validate key type resolves to String-based before recursing
ParsedCollection(Dict(key, _)) -> {
use _ <- result.try(validate_dict_key_type(
key,
context_name,
type_alias_map,
))
types.try_each_inner_parsed(typ, fn(inner) {
validate_type_refs(
inner,
context_name,
type_alias_names,
type_alias_map,
)
})
}
// For refinements, validate values match the declared primitive type
ParsedRefinement(refinement) -> {
use _ <- result.try(validate_refinement_values(refinement, context_name))
types.try_each_inner_parsed(typ, fn(inner) {
validate_type_refs(
inner,
context_name,
type_alias_names,
type_alias_map,
)
})
}
// For all other compound types, recurse via try_each_inner_parsed
_ ->
types.try_each_inner_parsed(typ, fn(inner) {
validate_type_refs(
inner,
context_name,
type_alias_names,
type_alias_map,
)
})
}
}
/// Validates that a Dict key type resolves to a String-based type.
fn validate_dict_key_type(
key_type: ParsedType,
context_name: String,
type_alias_map: List(#(String, ParsedType)),
) -> Result(Nil, ValidatorError) {
case key_type {
// String primitive is always valid
ParsedPrimitive(StringType) -> Ok(Nil)
// ParsedTypeAliasRef must resolve to String-based type
ParsedTypeAliasRef(alias_name) -> {
case list.key_find(type_alias_map, alias_name) {
Ok(resolved) -> {
use <- bool.guard(
when: is_string_based_parsed_type(resolved),
return: Ok(Nil),
)
Error(InvalidDictKeyTypeAlias(
alias_name: alias_name,
resolved_to: types.parsed_type_to_string(resolved),
referenced_by: context_name,
))
}
Error(_) -> Ok(Nil)
// Undefined ref is caught elsewhere
}
}
// Refinement of String is valid
ParsedRefinement(OneOf(ParsedPrimitive(StringType), _)) -> Ok(Nil)
// Other types are not valid Dict keys
_ ->
Error(InvalidDictKeyTypeAlias(
alias_name: "inline",
resolved_to: types.parsed_type_to_string(key_type),
referenced_by: context_name,
))
}
}
/// Checks if a parsed type is String-based (String primitive or String refinement).
fn is_string_based_parsed_type(typ: ParsedType) -> Bool {
case typ {
ParsedPrimitive(StringType) -> True
ParsedRefinement(OneOf(ParsedPrimitive(StringType), _)) -> True
_ -> False
}
}
// =============================================================================
// REFINEMENT VALUE VALIDATION
// =============================================================================
/// Validates that refinement string values match the declared primitive type.
fn validate_refinement_values(
refinement: types.RefinementTypes(ParsedType),
context_name: String,
) -> Result(Nil, ValidatorError) {
case refinement {
OneOf(inner, values) -> {
case extract_primitive_from_parsed(inner) {
Ok(primitive) ->
values
|> set.to_list
|> list.try_each(fn(value) {
validate_string_matches_primitive(value, primitive, context_name)
})
Error(_) -> Ok(Nil)
}
}
InclusiveRange(inner, low, high) -> {
case extract_primitive_from_parsed(inner) {
Ok(primitive) -> {
use _ <- result.try(validate_string_matches_primitive(
low,
primitive,
context_name,
))
validate_string_matches_primitive(high, primitive, context_name)
}
Error(_) -> Ok(Nil)
}
}
}
}
/// Extracts the primitive type from a ParsedType, unwrapping Defaulted modifiers.
fn extract_primitive_from_parsed(
typ: ParsedType,
) -> Result(PrimitiveTypes, Nil) {
case typ {
ParsedPrimitive(primitive) -> Ok(primitive)
ParsedModifier(Defaulted(inner, _)) -> extract_primitive_from_parsed(inner)
_ -> Error(Nil)
}
}
/// Validates that a string value is valid for the given primitive type.
fn validate_string_matches_primitive(
value: String,
primitive: PrimitiveTypes,
context_name: String,
) -> Result(Nil, ValidatorError) {
let is_valid = case primitive {
types.String -> True
SemanticType(_) -> True
Boolean -> value == "true" || value == "false"
NumericType(types.Integer) -> result.is_ok(int.parse(value))
NumericType(types.Float) -> result.is_ok(float.parse(value))
NumericType(Percentage) -> {
// Strip trailing % if present for parsing
let raw = case string.ends_with(value, "%") {
True -> string.drop_end(value, 1)
False -> value
}
case float.parse(raw) {
Ok(f) -> {
use <- bool.guard(when: f <. 0.0 || f >. 100.0, return: {
// Value parses but is out of range — percentage bounds error
False
})
True
}
Error(_) -> False
}
}
}
case is_valid {
True -> Ok(Nil)
False ->
case primitive {
NumericType(Percentage) -> {
// Distinguish parse failure from out-of-range
let raw = case string.ends_with(value, "%") {
True -> string.drop_end(value, 1)
False -> value
}
case float.parse(raw) {
Ok(_) ->
Error(InvalidPercentageBounds(
value: value,
referenced_by: context_name,
))
Error(_) ->
Error(InvalidRefinementValue(
value: value,
expected_type: types.primitive_type_to_string(primitive),
referenced_by: context_name,
))
}
}
_ ->
Error(InvalidRefinementValue(
value: value,
expected_type: types.primitive_type_to_string(primitive),
referenced_by: context_name,
))
}
}
}
// =============================================================================
// ERROR ACCUMULATION HELPERS
// =============================================================================
/// Converts a single-error Result to a list of errors (empty on Ok).
fn errors_to_list(result: Result(Nil, ValidatorError)) -> List(ValidatorError) {
case result {
Ok(_) -> []
Error(err) -> [err]
}
}
/// Collects errors from a list of independent validation results.
fn collect_errors(
results: List(Result(Nil, ValidatorError)),
) -> List(ValidatorError) {
results |> list.flat_map(errors_to_list)
}
/// Guards against accumulated errors. If errors is non-empty, returns them;
/// otherwise continues with the provided callback.
fn guard_errors(
errors: List(ValidatorError),
otherwise: fn() -> Result(a, List(ValidatorError)),
) -> Result(a, List(ValidatorError)) {
case errors {
[] -> otherwise()
_ -> Error(errors)
}
}