Packages
caffeine_lang
1.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/common/decoders.gleam
import caffeine_lang/common/accepted_types.{
type AcceptedTypes, Boolean, Defaulted, Dict, Float, Integer, List, Modifier,
Optional, String,
}
import gleam/bool
import gleam/dynamic/decode
import gleam/float
import gleam/int
import gleam/list
import gleam/option
import gleam/string
/// Creates a decoder that validates a string is a valid reference to an item in a collection.
/// Returns the string if it matches a name in the collection, otherwise fails decoding.
pub fn named_reference_decoder(
collection: List(a),
name_extraction: fn(a) -> String,
) -> decode.Decoder(String) {
let names = collection |> list.map(name_extraction)
let default = Error("")
decode.new_primitive_decoder("NamedReference", fn(dyn) {
case decode.run(dyn, decode.string) {
Ok(x) -> {
case names |> list.contains(x) {
True -> Ok(x)
False -> default
}
}
_ -> default
}
})
}
/// Decoder for non-empty strings. Fails if the string is empty.
pub fn non_empty_string_decoder() -> decode.Decoder(String) {
let default = Error("")
decode.new_primitive_decoder("NonEmptyString", fn(dyn) {
case decode.run(dyn, decode.string) {
Ok("") -> default
Ok(s) -> Ok(s)
_ -> default
}
})
}
/// Decoder for AcceptedTypes from a raw string representation.
/// i.e. for "Dict(String, String)" will return a decoder for
/// Ok(helpers.Dict(helpers.String, helpers.String))).
///
/// Note that the default for the error is decode.failure(Boolean, "AcceptedType")
/// which is a bit odd, but nonetheless should do the job for now.
pub fn accepted_types_decoder() -> decode.Decoder(AcceptedTypes) {
use raw_string <- decode.then(decode.string)
case parse_accepted_type(raw_string) {
Ok(t) -> decode.success(t)
Error(Nil) -> decode.failure(Boolean, "AcceptedType")
}
}
/// Decoder that decodes a dynamic value based on an AcceptedTypes and returns its String representation.
pub fn decode_value_to_string(typ: AcceptedTypes) -> decode.Decoder(String) {
case typ {
Boolean -> {
use val <- decode.then(decode.bool)
decode.success(bool.to_string(val))
}
Float -> {
use val <- decode.then(decode.float)
decode.success(float.to_string(val))
}
Integer -> {
use val <- decode.then(decode.int)
decode.success(int.to_string(val))
}
String -> decode.string
Dict(_, _) -> decode.failure("", "Dict")
List(_) -> decode.failure("", "List")
Modifier(modifier_type) ->
case modifier_type {
Optional(inner_type) -> {
use maybe_val <- decode.then(
decode.optional(decode_value_to_string(inner_type)),
)
case maybe_val {
option.Some(val) -> decode.success(val)
option.None -> decode.success("")
}
}
Defaulted(inner_type, default_val) -> {
use maybe_val <- decode.then(
decode.optional(decode_value_to_string(inner_type)),
)
case maybe_val {
option.Some(val) -> decode.success(val)
option.None -> decode.success(default_val)
}
}
}
}
}
/// Decoder that decodes a list of dynamic values and returns a List(String).
pub fn decode_list_values_to_strings(
inner_type: AcceptedTypes,
) -> decode.Decoder(List(String)) {
decode.list(decode_value_to_string(inner_type))
}
/// Parses a raw string into an AcceptedType.
fn parse_accepted_type(raw_accepted_type) -> Result(AcceptedTypes, Nil) {
case raw_accepted_type {
// Basic types
"Boolean" -> Ok(Boolean)
"Float" -> Ok(Float)
"Integer" -> Ok(Integer)
"String" -> Ok(String)
// Dict types
"Dict(String, String)" -> Ok(Dict(String, String))
"Dict(String, Integer)" -> Ok(Dict(String, Integer))
"Dict(String, Float)" -> Ok(Dict(String, Float))
"Dict(String, Boolean)" -> Ok(Dict(String, Boolean))
// List types
"List(String)" -> Ok(List(String))
"List(Integer)" -> Ok(List(Integer))
"List(Boolean)" -> Ok(List(Boolean))
"List(Float)" -> Ok(List(Float))
// Optional types
"Optional(String)" -> Ok(Modifier(Optional(String)))
"Optional(Integer)" -> Ok(Modifier(Optional(Integer)))
"Optional(Float)" -> Ok(Modifier(Optional(Float)))
"Optional(Boolean)" -> Ok(Modifier(Optional(Boolean)))
// Optional List types
"Optional(List(String))" -> Ok(Modifier(Optional(List(String))))
"Optional(List(Integer))" -> Ok(Modifier(Optional(List(Integer))))
"Optional(List(Float))" -> Ok(Modifier(Optional(List(Float))))
"Optional(List(Boolean))" -> Ok(Modifier(Optional(List(Boolean))))
// Optional Dict types
"Optional(Dict(String, String))" ->
Ok(Modifier(Optional(Dict(String, String))))
"Optional(Dict(String, Integer))" ->
Ok(Modifier(Optional(Dict(String, Integer))))
"Optional(Dict(String, Float))" ->
Ok(Modifier(Optional(Dict(String, Float))))
"Optional(Dict(String, Boolean))" ->
Ok(Modifier(Optional(Dict(String, Boolean))))
// Try to parse Defaulted types with default value
_ -> parse_defaulted_type(raw_accepted_type)
}
}
/// Parses a Defaulted type string like "Defaulted(Integer, 10)" into Defaulted(Integer, "10")
/// Also validates that the default value is compatible with the inner type.
fn parse_defaulted_type(raw: String) -> Result(AcceptedTypes, Nil) {
case string.starts_with(raw, "Defaulted(") && string.ends_with(raw, ")") {
False -> Error(Nil)
True -> {
// Remove "Defaulted(" prefix and ")" suffix
let inner =
raw
|> string.drop_start(10)
|> string.drop_end(1)
// Find the last comma that separates the type from the default value
// We need to handle nested types like "List(String), default"
case find_type_default_split(inner) {
Error(Nil) -> Error(Nil)
Ok(#(type_str, default_val)) -> {
let trimmed_type = string.trim(type_str)
let trimmed_default = string.trim(default_val)
case parse_accepted_type(trimmed_type) {
Ok(inner_type) -> {
// Validate the default value is compatible with the inner type
case validate_default_value(inner_type, trimmed_default) {
True -> Ok(Modifier(Defaulted(inner_type, trimmed_default)))
False -> Error(Nil)
}
}
Error(Nil) -> Error(Nil)
}
}
}
}
}
}
/// Validates that a default value string is compatible with the given type.
/// For basic types, checks that the string can be parsed as that type.
/// For complex types (List, Dict), we accept any string since the default
/// is used as a literal string value in templates.
fn validate_default_value(typ: AcceptedTypes, default_val: String) -> Bool {
case typ {
Boolean -> default_val == "True" || default_val == "False"
Integer ->
case int.parse(default_val) {
Ok(_) -> True
Error(_) -> False
}
Float ->
case float.parse(default_val) {
Ok(_) -> True
Error(_) -> False
}
String -> True
// For complex types, we accept any default since it's used as a literal
// string value in template substitution
List(_) -> True
Dict(_, _) -> True
// Nested Optional/Defaulted doesn't make sense
Modifier(_) -> False
}
}
/// Finds the split point between the type and default value in a Defaulted inner string.
/// For "Integer, 10" returns Ok(#("Integer", "10"))
/// For "List(String), hello" returns Ok(#("List(String)", "hello"))
/// Handles nested parentheses correctly.
fn find_type_default_split(inner: String) -> Result(#(String, String), Nil) {
find_type_default_split_helper(inner, inner, 0, 0)
}
fn find_type_default_split_helper(
original: String,
s: String,
index: Int,
paren_depth: Int,
) -> Result(#(String, String), Nil) {
case string.pop_grapheme(s) {
Error(Nil) -> Error(Nil)
Ok(#(char, rest)) -> {
case char, paren_depth {
"(", _ ->
find_type_default_split_helper(
original,
rest,
index + 1,
paren_depth + 1,
)
")", _ ->
find_type_default_split_helper(
original,
rest,
index + 1,
paren_depth - 1,
)
",", 0 -> {
// Found the split point at depth 0
let type_str = string.slice(original, 0, index)
let default_val = rest
Ok(#(type_str, default_val))
}
_, _ ->
find_type_default_split_helper(original, rest, index + 1, paren_depth)
}
}
}
}