Packages
caffeine_lang
3.0.2
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/common/accepted_types.{type AcceptedTypes}
import caffeine_lang/common/collection_types
import caffeine_lang/common/modifier_types
import caffeine_lang/common/primitive_types
import caffeine_lang/common/refinement_types
import caffeine_lang/frontend/ast.{
type BlueprintItem, type BlueprintsFile, type ExpectItem, type ExpectsFile,
type Extendable, type Field, type TypeAlias,
}
import gleam/list
import gleam/result
import gleam/set
/// Errors that can occur during validation.
pub type ValidatorError {
DuplicateExtendable(name: String)
UndefinedExtendable(name: String, referenced_by: String)
DuplicateExtendsReference(name: String, referenced_by: String)
InvalidExtendableKind(name: String, expected: String, got: String)
UndefinedTypeAlias(name: String, referenced_by: String)
DuplicateTypeAlias(name: String)
CircularTypeAlias(name: String, cycle: List(String))
InvalidDictKeyTypeAlias(
alias_name: String,
resolved_to: String,
referenced_by: String,
)
}
/// Validates a blueprints 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.
@internal
pub fn validate_blueprints_file(
file: BlueprintsFile,
) -> Result(BlueprintsFile, ValidatorError) {
let type_aliases = file.type_aliases
let extendables = file.extendables
let items =
file.blocks
|> list.flat_map(fn(block) { block.items })
// Validate type aliases first
use _ <- result.try(validate_no_duplicate_type_aliases(type_aliases))
use _ <- result.try(validate_no_circular_type_aliases(type_aliases))
// Build set of defined type alias names for reference validation
let type_alias_names =
type_aliases
|> list.map(fn(ta) { ta.name })
|> set.from_list
// Build map for Dict key validation
let type_alias_map = build_type_alias_map(type_aliases)
// Validate type alias references in extendables
use _ <- result.try(validate_extendables_type_refs(
extendables,
type_alias_names,
type_alias_map,
))
// Validate type alias references in blueprint items
use _ <- result.try(validate_blueprint_items_type_refs(
items,
type_alias_names,
type_alias_map,
))
use _ <- result.try(validate_no_duplicate_extendables(extendables))
use _ <- result.try(validate_blueprint_items_extends(items, extendables))
Ok(file)
}
/// Validates an expects file.
/// Checks for duplicate extendables, undefined extendable references,
/// and that all extendables are Provides kind.
@internal
pub fn validate_expects_file(
file: ExpectsFile,
) -> Result(ExpectsFile, ValidatorError) {
let extendables = file.extendables
let items =
file.blocks
|> list.flat_map(fn(block) { block.items })
use _ <- result.try(validate_no_duplicate_extendables(extendables))
use _ <- result.try(validate_extendables_are_provides(extendables))
use _ <- result.try(validate_expect_items_extends(items, extendables))
Ok(file)
}
/// Validates that no two extendables have the same name.
fn validate_no_duplicate_extendables(
extendables: List(Extendable),
) -> Result(Nil, ValidatorError) {
validate_no_duplicate_extendables_loop(extendables, set.new())
}
fn validate_no_duplicate_extendables_loop(
extendables: List(Extendable),
seen: set.Set(String),
) -> Result(Nil, ValidatorError) {
case extendables {
[] -> Ok(Nil)
[first, ..rest] -> {
case set.contains(seen, first.name) {
True -> Error(DuplicateExtendable(name: first.name))
False ->
validate_no_duplicate_extendables_loop(
rest,
set.insert(seen, first.name),
)
}
}
}
}
/// 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 blueprint items.
fn validate_blueprint_items_extends(
items: List(BlueprintItem),
extendables: List(Extendable),
) -> Result(Nil, ValidatorError) {
let extendable_names =
extendables
|> list.map(fn(e) { e.name })
|> set.from_list
list.try_each(items, fn(item) {
use _ <- result.try(validate_extends_exist(
item.extends,
item.name,
extendable_names,
))
validate_no_duplicate_extends(item.extends, item.name)
})
}
/// 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
list.try_each(items, fn(item) {
use _ <- result.try(validate_extends_exist(
item.extends,
item.name,
extendable_names,
))
validate_no_duplicate_extends(item.extends, item.name)
})
}
/// 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] -> {
case set.contains(extendable_names, first) {
True -> validate_extends_exist(rest, item_name, extendable_names)
False ->
Error(UndefinedExtendable(name: first, referenced_by: item_name))
}
}
}
}
/// 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] -> {
case set.contains(seen, first) {
True ->
Error(DuplicateExtendsReference(name: first, referenced_by: item_name))
False ->
validate_no_duplicate_extends_loop(
rest,
item_name,
set.insert(seen, first),
)
}
}
}
}
// =============================================================================
// 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, AcceptedTypes)) {
list.map(type_aliases, fn(ta) { #(ta.name, ta.type_) })
}
/// Looks up a type alias by name in the map.
fn lookup_type_alias(
name: String,
type_alias_map: List(#(String, AcceptedTypes)),
) -> Result(AcceptedTypes, Nil) {
case type_alias_map {
[] -> Error(Nil)
[#(n, t), ..rest] ->
case n == name {
True -> Ok(t)
False -> lookup_type_alias(name, rest)
}
}
}
/// Validates that no two type aliases have the same name.
fn validate_no_duplicate_type_aliases(
type_aliases: List(TypeAlias),
) -> Result(Nil, ValidatorError) {
validate_no_duplicate_type_aliases_loop(type_aliases, set.new())
}
fn validate_no_duplicate_type_aliases_loop(
type_aliases: List(TypeAlias),
seen: set.Set(String),
) -> Result(Nil, ValidatorError) {
case type_aliases {
[] -> Ok(Nil)
[first, ..rest] -> {
case set.contains(seen, first.name) {
True -> Error(DuplicateTypeAlias(name: first.name))
False ->
validate_no_duplicate_type_aliases_loop(
rest,
set.insert(seen, first.name),
)
}
}
}
}
/// 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,
])
})
}
/// Checks if a type contains a circular reference.
fn validate_type_alias_not_circular(
original_name: String,
typ: AcceptedTypes,
type_alias_map: List(#(String, AcceptedTypes)),
visited: List(String),
) -> Result(Nil, ValidatorError) {
case typ {
accepted_types.PrimitiveType(_) -> Ok(Nil)
accepted_types.TypeAliasRef(name) -> {
case list.contains(visited, name) {
True -> Error(CircularTypeAlias(name: original_name, cycle: visited))
False -> {
case lookup_type_alias(name, type_alias_map) {
Ok(resolved) ->
validate_type_alias_not_circular(
original_name,
resolved,
type_alias_map,
[name, ..visited],
)
Error(_) -> Ok(Nil)
// Undefined ref is caught elsewhere
}
}
}
}
accepted_types.CollectionType(collection) ->
validate_collection_not_circular(
original_name,
collection,
type_alias_map,
visited,
)
accepted_types.ModifierType(modifier) ->
validate_modifier_not_circular(
original_name,
modifier,
type_alias_map,
visited,
)
accepted_types.RefinementType(refinement) ->
validate_refinement_not_circular(
original_name,
refinement,
type_alias_map,
visited,
)
}
}
fn validate_collection_not_circular(
original_name: String,
collection: collection_types.CollectionTypes(AcceptedTypes),
type_alias_map: List(#(String, AcceptedTypes)),
visited: List(String),
) -> Result(Nil, ValidatorError) {
case collection {
collection_types.List(inner) ->
validate_type_alias_not_circular(
original_name,
inner,
type_alias_map,
visited,
)
collection_types.Dict(key, value) -> {
use _ <- result.try(validate_type_alias_not_circular(
original_name,
key,
type_alias_map,
visited,
))
validate_type_alias_not_circular(
original_name,
value,
type_alias_map,
visited,
)
}
}
}
fn validate_modifier_not_circular(
original_name: String,
modifier: modifier_types.ModifierTypes(AcceptedTypes),
type_alias_map: List(#(String, AcceptedTypes)),
visited: List(String),
) -> Result(Nil, ValidatorError) {
case modifier {
modifier_types.Optional(inner) ->
validate_type_alias_not_circular(
original_name,
inner,
type_alias_map,
visited,
)
modifier_types.Defaulted(inner, _) ->
validate_type_alias_not_circular(
original_name,
inner,
type_alias_map,
visited,
)
}
}
fn validate_refinement_not_circular(
original_name: String,
refinement: refinement_types.RefinementTypes(AcceptedTypes),
type_alias_map: List(#(String, AcceptedTypes)),
visited: List(String),
) -> Result(Nil, ValidatorError) {
case refinement {
refinement_types.OneOf(inner, _) ->
validate_type_alias_not_circular(
original_name,
inner,
type_alias_map,
visited,
)
refinement_types.InclusiveRange(inner, _, _) ->
validate_type_alias_not_circular(
original_name,
inner,
type_alias_map,
visited,
)
}
}
/// 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, AcceptedTypes)),
) -> 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 blueprint items.
fn validate_blueprint_items_type_refs(
items: List(BlueprintItem),
type_alias_names: set.Set(String),
type_alias_map: List(#(String, AcceptedTypes)),
) -> 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, AcceptedTypes)),
) -> 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 TypeAliasRef in a type are defined.
fn validate_type_refs(
typ: AcceptedTypes,
context_name: String,
type_alias_names: set.Set(String),
type_alias_map: List(#(String, AcceptedTypes)),
) -> Result(Nil, ValidatorError) {
case typ {
accepted_types.PrimitiveType(_) -> Ok(Nil)
accepted_types.TypeAliasRef(name) -> {
case set.contains(type_alias_names, name) {
True -> Ok(Nil)
False ->
Error(UndefinedTypeAlias(name: name, referenced_by: context_name))
}
}
accepted_types.CollectionType(collection) ->
validate_collection_type_refs(
collection,
context_name,
type_alias_names,
type_alias_map,
)
accepted_types.ModifierType(modifier) ->
validate_modifier_type_refs(
modifier,
context_name,
type_alias_names,
type_alias_map,
)
accepted_types.RefinementType(refinement) ->
validate_refinement_type_refs(
refinement,
context_name,
type_alias_names,
type_alias_map,
)
}
}
fn validate_collection_type_refs(
collection: collection_types.CollectionTypes(AcceptedTypes),
context_name: String,
type_alias_names: set.Set(String),
type_alias_map: List(#(String, AcceptedTypes)),
) -> Result(Nil, ValidatorError) {
case collection {
collection_types.List(inner) ->
validate_type_refs(inner, context_name, type_alias_names, type_alias_map)
collection_types.Dict(key, value) -> {
// Validate key type alias exists
use _ <- result.try(validate_type_refs(
key,
context_name,
type_alias_names,
type_alias_map,
))
// Validate Dict key resolves to String-based type
use _ <- result.try(validate_dict_key_type(
key,
context_name,
type_alias_map,
))
// Validate value type alias exists
validate_type_refs(value, context_name, type_alias_names, type_alias_map)
}
}
}
fn validate_modifier_type_refs(
modifier: modifier_types.ModifierTypes(AcceptedTypes),
context_name: String,
type_alias_names: set.Set(String),
type_alias_map: List(#(String, AcceptedTypes)),
) -> Result(Nil, ValidatorError) {
case modifier {
modifier_types.Optional(inner) ->
validate_type_refs(inner, context_name, type_alias_names, type_alias_map)
modifier_types.Defaulted(inner, _) ->
validate_type_refs(inner, context_name, type_alias_names, type_alias_map)
}
}
fn validate_refinement_type_refs(
refinement: refinement_types.RefinementTypes(AcceptedTypes),
context_name: String,
type_alias_names: set.Set(String),
type_alias_map: List(#(String, AcceptedTypes)),
) -> Result(Nil, ValidatorError) {
case refinement {
refinement_types.OneOf(inner, _) ->
validate_type_refs(inner, context_name, type_alias_names, type_alias_map)
refinement_types.InclusiveRange(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: AcceptedTypes,
context_name: String,
type_alias_map: List(#(String, AcceptedTypes)),
) -> Result(Nil, ValidatorError) {
case key_type {
// String primitive is always valid
accepted_types.PrimitiveType(primitive_types.String) -> Ok(Nil)
// TypeAliasRef must resolve to String-based type
accepted_types.TypeAliasRef(alias_name) -> {
case lookup_type_alias(alias_name, type_alias_map) {
Ok(resolved) ->
case is_string_based_type(resolved) {
True -> Ok(Nil)
False ->
Error(InvalidDictKeyTypeAlias(
alias_name: alias_name,
resolved_to: accepted_types.accepted_type_to_string(resolved),
referenced_by: context_name,
))
}
Error(_) -> Ok(Nil)
// Undefined ref is caught elsewhere
}
}
// Refinement of String is valid
accepted_types.RefinementType(refinement) ->
case refinement {
refinement_types.OneOf(inner, _) ->
case inner {
accepted_types.PrimitiveType(primitive_types.String) -> Ok(Nil)
_ ->
Error(InvalidDictKeyTypeAlias(
alias_name: "inline",
resolved_to: accepted_types.accepted_type_to_string(key_type),
referenced_by: context_name,
))
}
refinement_types.InclusiveRange(_, _, _) ->
Error(InvalidDictKeyTypeAlias(
alias_name: "inline",
resolved_to: accepted_types.accepted_type_to_string(key_type),
referenced_by: context_name,
))
}
// Other types are not valid Dict keys
_ ->
Error(InvalidDictKeyTypeAlias(
alias_name: "inline",
resolved_to: accepted_types.accepted_type_to_string(key_type),
referenced_by: context_name,
))
}
}
/// Checks if a type is String-based (String primitive or String refinement).
fn is_string_based_type(typ: AcceptedTypes) -> Bool {
case typ {
accepted_types.PrimitiveType(primitive_types.String) -> True
accepted_types.RefinementType(refinement) ->
case refinement {
refinement_types.OneOf(inner, _) ->
case inner {
accepted_types.PrimitiveType(primitive_types.String) -> True
_ -> False
}
refinement_types.InclusiveRange(_, _, _) -> False
}
_ -> False
}
}