Packages
caffeine_lang
1.0.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/parser/expectations.gleam
import caffeine_lang/common/decoders
import caffeine_lang/common/errors.{type CompilationError, ParserDuplicateError}
import caffeine_lang/common/helpers
import caffeine_lang/common/validations
import caffeine_lang/middle_end/semantic_analyzer.{
type IntermediateRepresentation,
}
import caffeine_lang/parser/blueprints.{type Blueprint}
import gleam/dict
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/json
import gleam/list
import gleam/option
import gleam/result
import gleam/string
pub type Expectation {
Expectation(
name: String,
blueprint_ref: String,
inputs: dict.Dict(String, Dynamic),
)
}
/// Parse expectations from a file, leveraging the given file path for metadata extraction.
pub fn parse_from_file(
file_path: String,
blueprints: List(Blueprint),
) -> Result(List(IntermediateRepresentation), CompilationError) {
use json_string <- result.try(helpers.json_from_file(file_path))
parse_from_string(json_string, file_path, blueprints)
}
/// Parse expectations from a JSON string with a given path for metadata extraction.
/// This is public so it can be used by browser.gleam for in-browser compilation.
/// Furthermore, internally we use this as the base from which parse_from_file also
/// uses to parse.
pub fn parse_from_string(
json_string: String,
file_path: String,
blueprints: List(Blueprint),
) -> Result(List(IntermediateRepresentation), CompilationError) {
// actually parse
use expectations <- result.try(
case expectations_from_json(json_string, blueprints) {
Ok(expectations) -> Ok(expectations)
Error(err) -> Error(errors.format_json_decode_error(err))
},
)
validate_and_build_irs(expectations, blueprints, file_path)
}
/// Validate expectations and build intermediate representations.
fn validate_and_build_irs(
expectations: List(Expectation),
blueprints: List(Blueprint),
file_path: String,
) -> Result(List(IntermediateRepresentation), CompilationError) {
// map expectations to blueprints since we'll reuse that numerous times
// and we've already validated all blueprint_refs
let expectations_blueprint_collection =
helpers.map_reference_to_referrer_over_collection(
references: blueprints,
referrers: expectations,
reference_name: fn(b) { b.name },
referrer_reference: fn(e) { e.blueprint_ref },
)
// validate that expectation inputs don't overshadow blueprint inputs
use _ <- result.try(check_input_overshadowing(
expectations_blueprint_collection,
))
// validate that expectation.inputs provides params NOT already provided by blueprint.inputs
use _ <- result.try(
validations.validate_inputs_for_collection(
expectations_blueprint_collection,
fn(expectation) { expectation.inputs },
fn(blueprint) {
let blueprint_input_keys = blueprint.inputs |> dict.keys
blueprint.params
|> dict.filter(fn(key, _) { !list.contains(blueprint_input_keys, key) })
},
),
)
// validate unique names within a file
use _ <- result.try(validations.validate_relevant_uniqueness(
expectations,
fn(e) { e.name },
"expectation names",
))
// at this point we're completely validated, now build IR
build_ir(expectations_blueprint_collection, file_path)
}
@internal
pub fn build_ir(
expectations_blueprint_collection: List(#(Expectation, Blueprint)),
file_path: String,
) {
expectations_blueprint_collection
|> list.map(fn(expectation_and_blueprint_pair) {
let #(expectation, blueprint) = expectation_and_blueprint_pair
// merge blueprint inputs with expectation inputs
// Expectation inputs override blueprint inputs for the same key
let merged_inputs = dict.merge(blueprint.inputs, expectation.inputs)
// Build value tuples from provided inputs
let provided_value_tuples =
merged_inputs
|> dict.keys
|> list.map(fn(label) {
// safe assertions since we're already validated everything
let assert Ok(value) = merged_inputs |> dict.get(label)
let assert Ok(typ) = blueprint.params |> dict.get(label)
helpers.ValueTuple(label:, typ:, value:)
})
// Also include Optional and Defaulted params that weren't provided
// These need to be in value_tuples so the templatizer can resolve them
let unprovided_optional_value_tuples =
blueprint.params
|> dict.to_list
|> list.filter_map(fn(param) {
let #(label, typ) = param
case dict.has_key(merged_inputs, label) {
True -> Error(Nil)
False ->
case typ {
helpers.Optional(_) | helpers.Defaulted(_, _) ->
Ok(helpers.ValueTuple(label:, typ:, value: dynamic.nil()))
_ -> Error(Nil)
}
}
})
let value_tuples =
list.append(provided_value_tuples, unprovided_optional_value_tuples)
// build unique expectation name by combining path prefix with name
let #(org, team, service) = extract_path_prefix(file_path)
let service_name = service
let unique_name = org <> "_" <> service_name <> "_" <> expectation.name
semantic_analyzer.IntermediateRepresentation(
metadata: semantic_analyzer.IntermediateRepresentationMetaData(
friendly_label: expectation.name,
org_name: org,
service_name: service_name,
blueprint_name: blueprint.name,
team_name: team,
),
unique_identifier: unique_name,
artifact_ref: blueprint.artifact_ref,
values: value_tuples,
vendor: option.None,
)
})
|> Ok
}
/// Extract a meaningful prefix from the source path
/// e.g., "examples/org/platform_team/authentication.json" -> #("org", "platform_team", "authentication")
@internal
pub fn extract_path_prefix(path: String) -> #(String, String, String) {
case
path
|> string.split("/")
|> list.reverse
|> list.take(3)
|> list.reverse
|> list.map(fn(segment) {
// Remove .json extension if present
case string.ends_with(segment, ".json") {
True -> string.drop_end(segment, 5)
False -> segment
}
})
{
[org, team, service] -> #(org, team, service)
// this is not actually a possible state, however for pattern matching completeness we
// inlcude it here
_ -> #("unknown", "unknown", "unknown")
}
}
fn check_input_overshadowing(
expectations_blueprint_collection: List(#(Expectation, Blueprint)),
) -> Result(Bool, CompilationError) {
let overshadow_errors =
expectations_blueprint_collection
|> list.filter_map(fn(pair) {
let #(expectation, blueprint) = pair
case
validations.check_collection_key_overshadowing(
expectation.inputs,
blueprint.inputs,
"Expectation '"
<> expectation.name
<> "' overshadowing inputs from blueprint: ",
)
{
Ok(_) -> Error(Nil)
Error(msg) -> Ok(msg)
}
})
|> string.join(", ")
case overshadow_errors {
"" -> Ok(True)
_ -> Error(ParserDuplicateError(msg: overshadow_errors))
}
}
/// Parse expectations from a JSON string.
/// This is public so it can be used by browser.gleam for in-browser compilation.
pub fn expectations_from_json(
json_string: String,
blueprints: List(Blueprint),
) -> Result(List(Expectation), json.DecodeError) {
let expectation_decoded = {
use name <- decode.field("name", decoders.non_empty_string_decoder())
use blueprint_ref <- decode.field(
"blueprint_ref",
decoders.named_reference_decoder(blueprints, fn(b) { b.name }),
)
use inputs <- decode.field(
"inputs",
decode.dict(decode.string, decode.dynamic),
)
decode.success(Expectation(name:, blueprint_ref:, inputs:))
}
let expectations_decoded = {
use expectations <- decode.field(
"expectations",
decode.list(expectation_decoded),
)
decode.success(expectations)
}
json.parse(from: json_string, using: expectations_decoded)
}