mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Move input scalar coercion to shared types
This commit is contained in:
parent
c673ef4fde
commit
e38f372eeb
3 changed files with 196 additions and 26 deletions
|
|
@ -1,32 +1,29 @@
|
|||
use fabro_types::JsonScalarToTomlError;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::common::{ToolError, ToolResult};
|
||||
|
||||
pub fn json_to_toml_value(key: &str, value: &Value) -> ToolResult<toml::Value> {
|
||||
match value {
|
||||
Value::Null => Err(ToolError::message(format!(
|
||||
"input `{key}` cannot be null; use a string, boolean, or number"
|
||||
))),
|
||||
Value::Bool(value) => Ok(toml::Value::Boolean(*value)),
|
||||
Value::Number(value) => {
|
||||
if let Some(integer) = value.as_i64() {
|
||||
Ok(toml::Value::Integer(integer))
|
||||
} else if let Some(float) = value.as_f64() {
|
||||
Ok(toml::Value::Float(float))
|
||||
} else {
|
||||
Err(ToolError::message(format!(
|
||||
"input `{key}` contains a number outside TOML's supported range"
|
||||
)))
|
||||
}
|
||||
fabro_types::json_scalar_to_toml_value(value)
|
||||
.map_err(|error| json_scalar_to_tool_error(key, error))
|
||||
}
|
||||
|
||||
fn json_scalar_to_tool_error(key: &str, error: JsonScalarToTomlError) -> ToolError {
|
||||
let message = match error {
|
||||
JsonScalarToTomlError::Null => {
|
||||
format!("input `{key}` cannot be null; use a string, boolean, or number")
|
||||
}
|
||||
Value::String(value) => Ok(toml::Value::String(value.clone())),
|
||||
Value::Array(_) => Err(ToolError::message(format!(
|
||||
"input `{key}` does not support array values; use a string, boolean, or number",
|
||||
))),
|
||||
Value::Object(_) => Err(ToolError::message(format!(
|
||||
JsonScalarToTomlError::Array => {
|
||||
format!("input `{key}` does not support array values; use a string, boolean, or number")
|
||||
}
|
||||
JsonScalarToTomlError::Object => format!(
|
||||
"input `{key}` does not support object values; use a string, boolean, or number",
|
||||
))),
|
||||
}
|
||||
),
|
||||
JsonScalarToTomlError::NumberOutOfRange => {
|
||||
format!("input `{key}` contains a number outside TOML's supported range")
|
||||
}
|
||||
};
|
||||
ToolError::message(message)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -45,19 +42,25 @@ mod tests {
|
|||
];
|
||||
|
||||
for (json, expected) in cases {
|
||||
assert_eq!(json_to_toml_value("input", &json).unwrap(), expected);
|
||||
assert_eq!(
|
||||
json_to_toml_value("input", &json)
|
||||
.expect("representative JSON scalar should convert"),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_arrays_and_objects_are_rejected() {
|
||||
let array_err = json_to_toml_value("matrix", &json!(["a", 1])).unwrap_err();
|
||||
let array_err = json_to_toml_value("matrix", &json!(["a", 1]))
|
||||
.expect_err("JSON arrays should be rejected");
|
||||
assert_eq!(
|
||||
array_err.as_str(),
|
||||
"input `matrix` does not support array values; use a string, boolean, or number",
|
||||
);
|
||||
|
||||
let object_err = json_to_toml_value("settings", &json!({ "enabled": true })).unwrap_err();
|
||||
let object_err = json_to_toml_value("settings", &json!({ "enabled": true }))
|
||||
.expect_err("JSON objects should be rejected");
|
||||
assert_eq!(
|
||||
object_err.as_str(),
|
||||
"input `settings` does not support object values; use a string, boolean, or number",
|
||||
|
|
@ -66,11 +69,22 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn json_input_null_is_rejected_with_key_name() {
|
||||
let err = json_to_toml_value("goal", &Value::Null).unwrap_err();
|
||||
let err =
|
||||
json_to_toml_value("goal", &Value::Null).expect_err("JSON null should be rejected");
|
||||
|
||||
assert_eq!(
|
||||
err.as_str(),
|
||||
"input `goal` cannot be null; use a string, boolean, or number",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_input_out_of_range_number_preserves_tool_message() {
|
||||
let err = json_scalar_to_tool_error("threshold", JsonScalarToTomlError::NumberOutOfRange);
|
||||
|
||||
assert_eq!(
|
||||
err.as_str(),
|
||||
"input `threshold` contains a number outside TOML's supported range",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
154
lib/foundation/fabro-types/src/input_scalar.rs
Normal file
154
lib/foundation/fabro-types/src/input_scalar.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use serde_json::Value;
|
||||
|
||||
/// The reason a parsed JSON value cannot be represented as a TOML scalar.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum JsonScalarToTomlError {
|
||||
/// JSON null has no TOML scalar representation.
|
||||
#[error("JSON null is not a TOML scalar")]
|
||||
Null,
|
||||
/// JSON arrays are outside the scalar-only conversion contract.
|
||||
#[error("JSON arrays are not TOML scalars")]
|
||||
Array,
|
||||
/// JSON objects are outside the scalar-only conversion contract.
|
||||
#[error("JSON objects are not TOML scalars")]
|
||||
Object,
|
||||
/// The JSON number is not representable by the supported TOML number types.
|
||||
#[error("JSON number is outside TOML's supported range")]
|
||||
NumberOutOfRange,
|
||||
}
|
||||
|
||||
/// Converts an already-parsed JSON scalar into a TOML value.
|
||||
///
|
||||
/// Numbers are converted to a signed integer first and then to a float. As a
|
||||
/// result, nonnegative integers above `i64::MAX` become TOML floats and may
|
||||
/// lose integer precision. JSON floats remain TOML floats.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`JsonScalarToTomlError`] for JSON null, arrays, objects, or a
|
||||
/// number that can be represented as neither an `i64` nor an `f64`.
|
||||
pub fn json_scalar_to_toml_value(value: &Value) -> Result<toml::Value, JsonScalarToTomlError> {
|
||||
match value {
|
||||
Value::Null => Err(JsonScalarToTomlError::Null),
|
||||
Value::Bool(value) => Ok(toml::Value::Boolean(*value)),
|
||||
Value::Number(value) => {
|
||||
if let Some(integer) = value.as_i64() {
|
||||
Ok(toml::Value::Integer(integer))
|
||||
} else if let Some(float) = value.as_f64() {
|
||||
Ok(toml::Value::Float(float))
|
||||
} else {
|
||||
Err(JsonScalarToTomlError::NumberOutOfRange)
|
||||
}
|
||||
}
|
||||
Value::String(value) => Ok(toml::Value::String(value.clone())),
|
||||
Value::Array(_) => Err(JsonScalarToTomlError::Array),
|
||||
Value::Object(_) => Err(JsonScalarToTomlError::Object),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{JsonScalarToTomlError, json_scalar_to_toml_value};
|
||||
|
||||
#[test]
|
||||
fn converts_strings_to_toml_strings() -> Result<(), JsonScalarToTomlError> {
|
||||
for input in ["", "hello", "Grüße 世界"] {
|
||||
let json = Value::String(input.to_string());
|
||||
|
||||
assert_eq!(
|
||||
json_scalar_to_toml_value(&json)?,
|
||||
toml::Value::String(input.to_string()),
|
||||
"{input:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_booleans_to_toml_booleans() -> Result<(), JsonScalarToTomlError> {
|
||||
for input in [false, true] {
|
||||
assert_eq!(
|
||||
json_scalar_to_toml_value(&Value::Bool(input))?,
|
||||
toml::Value::Boolean(input)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_signed_range_integers_to_toml_integers() -> Result<(), JsonScalarToTomlError> {
|
||||
for input in [i64::MIN, -1, 0, i64::MAX] {
|
||||
assert_eq!(
|
||||
json_scalar_to_toml_value(&Value::from(input))?,
|
||||
toml::Value::Integer(input)
|
||||
);
|
||||
}
|
||||
|
||||
let signed_max_as_u64 =
|
||||
u64::try_from(i64::MAX).expect("i64::MAX should be representable as u64");
|
||||
assert_eq!(
|
||||
json_scalar_to_toml_value(&Value::from(signed_max_as_u64))?,
|
||||
toml::Value::Integer(i64::MAX)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_large_unsigned_integers_to_toml_floats() -> Result<(), JsonScalarToTomlError> {
|
||||
let signed_max_as_u64 =
|
||||
u64::try_from(i64::MAX).expect("i64::MAX should be representable as u64");
|
||||
for input in [signed_max_as_u64 + 1, u64::MAX] {
|
||||
assert_eq!(
|
||||
json_scalar_to_toml_value(&Value::from(input))?,
|
||||
toml::Value::Float(input as f64)
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_finite_json_floats_as_toml_floats() -> Result<(), JsonScalarToTomlError> {
|
||||
for input in [-0.0, 0.5, 1.0, f64::MAX] {
|
||||
let converted = json_scalar_to_toml_value(&Value::from(input))?;
|
||||
let toml::Value::Float(output) = converted else {
|
||||
panic!("{input:?} should remain a TOML float");
|
||||
};
|
||||
|
||||
assert_eq!(output.to_bits(), input.to_bits(), "{input:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_scalar_json_values_with_typed_errors() {
|
||||
let cases = [
|
||||
(Value::Null, JsonScalarToTomlError::Null),
|
||||
(json!(["value"]), JsonScalarToTomlError::Array),
|
||||
(json!({ "key": "value" }), JsonScalarToTomlError::Object),
|
||||
];
|
||||
|
||||
for (input, expected) in cases {
|
||||
assert_eq!(json_scalar_to_toml_value(&input), Err(expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_non_finite_float_values_at_the_json_boundary() {
|
||||
for input in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
let json = Value::from(input);
|
||||
|
||||
assert_eq!(json, Value::Null);
|
||||
assert_eq!(
|
||||
json_scalar_to_toml_value(&json),
|
||||
Err(JsonScalarToTomlError::Null)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ pub mod event_envelope;
|
|||
pub mod failure_signature;
|
||||
pub mod graph;
|
||||
mod id;
|
||||
mod input_scalar;
|
||||
pub mod interview;
|
||||
pub mod llm_backend;
|
||||
pub mod manifest_path;
|
||||
|
|
@ -75,6 +76,7 @@ pub use graph::{
|
|||
AttrValue, ContextKeyAttr, Edge, Graph, KNOWN_HANDLER_TYPES, Node, is_known_handler_type,
|
||||
is_llm_handler_type, shape_to_handler_type,
|
||||
};
|
||||
pub use input_scalar::{JsonScalarToTomlError, json_scalar_to_toml_value};
|
||||
pub use interview::{
|
||||
InterviewQuestionRecord, QuestionType, ReviewTarget, ReviewTargetError, ReviewTargetKind,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue