Current section

Files

Jump to
caffeine_lang src caffeine_lang parser expectations.gleam
Raw

src/caffeine_lang/parser/expectations.gleam

import caffeine_lang/common/decoders
import caffeine_lang/common/errors.{type CompilationError}
import caffeine_lang/common/helpers
import caffeine_lang/common/validations
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/result
import gleam/string
/// An Expectation is a concrete implementation of an Artifact + Blueprint.
pub type Expectation {
Expectation(
name: String,
blueprint_ref: String,
inputs: dict.Dict(String, Dynamic),
)
}
/// Parse expectations from a file.
@internal
pub fn parse_from_json_file(
file_path: String,
blueprints: List(Blueprint),
) -> Result(List(#(Expectation, Blueprint)), CompilationError) {
use json_string <- result.try(helpers.json_from_file(file_path))
parse_from_json_string(json_string, blueprints)
}
/// Parse expectations from a JSON string.
@internal
pub fn parse_from_json_string(
json_string: String,
blueprints: List(Blueprint),
) -> Result(List(#(Expectation, Blueprint)), CompilationError) {
// Parse the JSON string.
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_expectations(expectations, blueprints)
}
/// Parse expectations from a JSON string.
@internal
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)
}
/// Validate expectations and return paired with their blueprints.
/// TODO: This provides massive duplication and is an area of low hanging fruit for optimization.
fn validate_expectations(
expectations: List(Expectation),
blueprints: List(Blueprint),
) -> Result(List(#(Expectation, Blueprint)), 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(
input_param_collections: expectations_blueprint_collection,
get_inputs: fn(expectation) { expectation.inputs },
get_params: fn(blueprint) {
let blueprint_input_keys = blueprint.inputs |> dict.keys
blueprint.params
|> dict.filter(fn(key, _) { !list.contains(blueprint_input_keys, key) })
},
missing_inputs_ok: False,
))
// Validate unique names within a file.
use _ <- result.try(validations.validate_relevant_uniqueness(
expectations,
fn(e) { e.name },
"expectation names",
))
Ok(expectations_blueprint_collection)
}
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(errors.ParserDuplicateError(msg: overshadow_errors))
}
}