mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
refactor(rust): derive string enum serde through strum and serde_with (#44675)
* docs(rust): document string enum serde conversions Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * refactor(rust): derive string enum serde through strum and serde_with Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Yujong Lee <yujong@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
8b7b42f3aa
commit
b9251dafad
12 changed files with 273 additions and 85 deletions
66
litellm-rust/.agents/skills/rust-string-enums/SKILL.md
Normal file
66
litellm-rust/.agents/skills/rust-string-enums/SKILL.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
name: rust-string-enums
|
||||
description: Define or refactor Rust string-valued enums and their Serde adapters in litellm-rust, using Strum and serde_with while preserving parsing, wire values, and schemas
|
||||
---
|
||||
|
||||
# Rust string enums
|
||||
|
||||
Use this skill when adding or changing enums represented by a single string, or surveying handwritten string conversions
|
||||
|
||||
## Choose the representation
|
||||
|
||||
For an enum with fixed spellings and an unknown-string fallback, prefer `strum::EnumString` and `strum::Display` together with `serde_with::DeserializeFromStr` and `serde_with::SerializeDisplay`. Keep each wire spelling in the Strum attributes instead of repeating it in a handwritten Serde match
|
||||
|
||||
Strum implements string conversion traits, not Serde traits. `EnumString` implements `FromStr`; `Display` formats the wire string. The two `serde_with` derives connect those traits to Serde. `AsRefStr` provides a borrowed string accessor and is optional
|
||||
|
||||
```rust
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum::AsRefStr,
|
||||
strum::EnumString,
|
||||
strum::Display,
|
||||
serde_with::DeserializeFromStr,
|
||||
serde_with::SerializeDisplay,
|
||||
)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schema", schemars(from = "String", into = "String"))]
|
||||
pub enum EventType {
|
||||
#[strum(serialize = "event.created")]
|
||||
Created,
|
||||
#[strum(default, transparent)]
|
||||
Other(String),
|
||||
}
|
||||
```
|
||||
|
||||
Use `#[strum(serialize_all = "snake_case")]` or another supported case style when it exactly matches the contract. Use explicit variant spellings otherwise. With multiple accepted aliases, set `to_string` to the existing canonical output: Strum Display otherwise selects the longest `serialize` spelling
|
||||
|
||||
Use only the derives the contract needs. A deserialize-only type should remain deserialize-only. Do not add an unknown variant to a closed enum, or derive Serde for a type that currently has no serialization contract
|
||||
|
||||
Plain Serde derives with `rename` or `rename_all` remain appropriate for closed unit enums. Adding Strum and serde_with solely to replace working Serde derives adds little value. When a type has both Serde and Strum parsing, compare their accepted inputs before sharing the parser: case-insensitive Strum parsing must not silently make strict JSON parsing case-insensitive
|
||||
|
||||
## Preserve behavior during migration
|
||||
|
||||
Read the type, its callers, and existing tests before changing it. Preserve canonical output, accepted aliases, case sensitivity, whitespace handling, unknown values, malformed-input rejection, and existing public conversion APIs. Keep conversions needed by callers or compatibility even when Serde no longer uses them
|
||||
|
||||
Use the workspace dependencies and enable `serde_with.workspace = true` in a crate only when needed. Check the versions and enabled features in `Cargo.toml` and `Cargo.lock` rather than upgrading dependencies for this refactor
|
||||
|
||||
Remove replaced manual Serde implementations and obsolete Serde conversion attributes. Do not combine the new derives with `wire_type`, `request_type`, or `response_type` aliases that already derive the same Serde traits. Expand the necessary non-Serde derives and schema attributes locally rather than changing shared aliases for unrelated types
|
||||
|
||||
Preserve the generated schema, including titles and definition names. Open string enums need a string schema, including unknown values. When replacing Serde `from`/`into` attributes that previously supplied that schema, retain their schema behavior with `#[cfg_attr(feature = "schema", schemars(from = "String", into = "String"))]` and compare the full generated result. `schemars(with = "String")` also makes a string schema, but changes the schema name and title, so keep it only where it already matches the contract
|
||||
|
||||
Keep custom `FromStr` and `Display` implementations for structured strings or validation that Strum does not express faithfully. Their Serde adapters can still use `DeserializeFromStr` and `SerializeDisplay`. Do not replace JSON visitors, tagged payload enums, permissive value wrappers, or domain transformations with string parsing
|
||||
|
||||
For a requested survey, document candidates and exceptions without migrating source. If the user asks to approve a bulk migration, present the concrete scope and wait for that approval
|
||||
|
||||
## Verify the contract
|
||||
|
||||
Extend existing mapped tests with named `rstest` cases. Assert both parsing into the expected variant and serialization to the expected wire string. Include unknown and empty strings for open enums, accepted aliases when present, and rejection of non-string JSON. A decode-only assertion does not prove a round trip
|
||||
|
||||
Test structured parsers with valid and invalid payloads, including their existing error behavior. For types with schema support, check the string schema and run the affected crate tests with the schema feature enabled. Run affected downstream checks when conversion APIs or derive aliases change
|
||||
|
||||
## Upstream references
|
||||
|
||||
The workspace used Strum 0.28.0 and serde_with 3.16.1 when this guidance was written. Consult the matching version of the [EnumString docs](https://docs.rs/strum/0.28.0/strum/derive.EnumString.html), [Display docs](https://docs.rs/strum_macros/0.28.0/strum_macros/derive.Display.html), [DeserializeFromStr docs](https://docs.rs/serde_with/3.16.1/serde_with/derive.DeserializeFromStr.html), and [SerializeDisplay docs](https://docs.rs/serde_with/3.16.1/serde_with/derive.SerializeDisplay.html). Schemars documents [schema overrides and Serde conversion attributes](https://docs.rs/schemars/1.2.2/schemars/derive.JsonSchema.html)
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
For diagnostic tracing changes, follow [.agents/skills/rust-tracing/SKILL.md](.agents/skills/rust-tracing/SKILL.md)
|
||||
|
||||
For string-valued enums and their Serde conversions, follow [.agents/skills/rust-string-enums/SKILL.md](.agents/skills/rust-string-enums/SKILL.md)
|
||||
|
||||
## Test placement
|
||||
|
||||
- Never create a `tests.rs` (or `test.rs`) file under `src/`, and never `#[path = "tests.rs"] mod tests;`
|
||||
|
|
|
|||
1
litellm-rust/Cargo.lock
generated
1
litellm-rust/Cargo.lock
generated
|
|
@ -4450,6 +4450,7 @@ dependencies = [
|
|||
"schemars 1.2.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"strum",
|
||||
"thiserror 2.0.19",
|
||||
"time",
|
||||
|
|
|
|||
|
|
@ -18,9 +18,18 @@ pub enum MessageContent {
|
|||
Blocks(Vec<ContentBlock>),
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
#[derive(Eq, strum::Display, strum::EnumString)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum::Display,
|
||||
strum::EnumString,
|
||||
serde_with::DeserializeFromStr,
|
||||
serde_with::SerializeDisplay,
|
||||
)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schema", schemars(from = "String", into = "String"))]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum ContentBlockType {
|
||||
Text,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, strum::AsRefStr)]
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
strum::AsRefStr,
|
||||
strum::EnumString,
|
||||
strum::Display,
|
||||
serde_with::DeserializeFromStr,
|
||||
serde_with::SerializeDisplay,
|
||||
)]
|
||||
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
|
||||
#[cfg_attr(feature = "schema", schemars(with = "String"))]
|
||||
pub enum ResponsesWsEventType {
|
||||
|
|
@ -27,33 +36,6 @@ impl ResponsesWsEventType {
|
|||
}
|
||||
}
|
||||
|
||||
impl Serialize for ResponsesWsEventType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ResponsesWsEventType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Ok(match value.as_str() {
|
||||
"response.create" => Self::ResponseCreate,
|
||||
"response.created" => Self::ResponseCreated,
|
||||
"response.completed" => Self::ResponseCompleted,
|
||||
"response.failed" => Self::ResponseFailed,
|
||||
"response.incomplete" => Self::ResponseIncomplete,
|
||||
"error" => Self::Error,
|
||||
_ => Self::Other(value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_rules_attribute::apply(wire_type)]
|
||||
pub struct ResponsesWsEvent {
|
||||
#[serde(rename = "type")]
|
||||
|
|
@ -114,21 +96,6 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::known("response.completed", ResponsesWsEventType::ResponseCompleted)]
|
||||
#[case::unknown(
|
||||
"response.output_text.delta",
|
||||
ResponsesWsEventType::Other("response.output_text.delta".to_string())
|
||||
)]
|
||||
fn event_type_round_trips_known_and_unknown_values(
|
||||
#[case] value: &str,
|
||||
#[case] expected: ResponsesWsEventType,
|
||||
) {
|
||||
let actual: ResponsesWsEventType =
|
||||
serde_json::from_str(&serde_json::to_string(value).unwrap()).expect("valid event type");
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_frame_matches_proxy_shape() {
|
||||
let frame = ResponsesErrorFrame::invalid_request("missing model");
|
||||
|
|
|
|||
|
|
@ -2,6 +2,27 @@ use litellm_llms_types::formats::messages::{ContentBlock, ContentBlockType};
|
|||
use rstest::rstest;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[rstest]
|
||||
#[case::null(json!(null))]
|
||||
#[case::number(json!(1))]
|
||||
#[case::boolean(json!(true))]
|
||||
#[case::array(json!(["tool_use"]))]
|
||||
#[case::object(json!({"type": "tool_use"}))]
|
||||
fn content_block_type_rejects_non_string_json(#[case] value: Value) {
|
||||
assert!(serde_json::from_value::<ContentBlockType>(value).is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
#[rstest]
|
||||
fn content_block_type_schema_remains_a_string() {
|
||||
let schema = schemars::schema_for!(ContentBlockType).to_value();
|
||||
assert_eq!(schema.get("type"), Some(&json!("string")));
|
||||
assert_eq!(
|
||||
schema.get("title"),
|
||||
Some(&json!(stringify!(ContentBlockType)))
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::text("text", ContentBlockType::Text)]
|
||||
#[case::thinking("thinking", ContentBlockType::Thinking)]
|
||||
|
|
|
|||
48
litellm-rust/crates/llms-types/tests/responses.rs
Normal file
48
litellm-rust/crates/llms-types/tests/responses.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use litellm_llms_types::formats::responses::streaming_websocket::ResponsesWsEventType;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case::create("response.create", ResponsesWsEventType::ResponseCreate)]
|
||||
#[case::created("response.created", ResponsesWsEventType::ResponseCreated)]
|
||||
#[case::completed("response.completed", ResponsesWsEventType::ResponseCompleted)]
|
||||
#[case::failed("response.failed", ResponsesWsEventType::ResponseFailed)]
|
||||
#[case::incomplete("response.incomplete", ResponsesWsEventType::ResponseIncomplete)]
|
||||
#[case::error("error", ResponsesWsEventType::Error)]
|
||||
#[case::unknown(
|
||||
"response.output_text.delta",
|
||||
ResponsesWsEventType::Other("response.output_text.delta".to_string())
|
||||
)]
|
||||
#[case::empty("", ResponsesWsEventType::Other(String::new()))]
|
||||
#[case::case_sensitive(
|
||||
"Response.Completed",
|
||||
ResponsesWsEventType::Other("Response.Completed".into())
|
||||
)]
|
||||
#[case::escaped("future\"\\\n", ResponsesWsEventType::Other("future\"\\\n".into()))]
|
||||
fn websocket_event_type_round_trips(#[case] wire: &str, #[case] expected: ResponsesWsEventType) {
|
||||
let serialized = serde_json::to_string(&expected).unwrap();
|
||||
assert_eq!(serialized, serde_json::to_string(wire).unwrap());
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ResponsesWsEventType>(&serialized).unwrap(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::number("17")]
|
||||
#[case::boolean("true")]
|
||||
#[case::null("null")]
|
||||
#[case::array("[]")]
|
||||
#[case::object("{}")]
|
||||
fn websocket_event_type_rejects_non_strings(#[case] wire: &str) {
|
||||
assert!(serde_json::from_str::<ResponsesWsEventType>(wire).is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "schema")]
|
||||
#[rstest]
|
||||
fn websocket_event_type_schema_is_open_string() {
|
||||
let schema = schemars::schema_for!(ResponsesWsEventType);
|
||||
assert_eq!(
|
||||
schema.to_value().get("type"),
|
||||
Some(&serde_json::json!("string"))
|
||||
);
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ use litellm_auth_azure::{AzureAuthInputs, SECRET_NAMES as AZURE_AUTH_SECRET_NAME
|
|||
use litellm_core_utils::{call_arguments::CallArguments, url_utils::ApiUrl};
|
||||
use litellm_llms_types::serde_compat::{FiniteF64, LaxI64};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Map, Value};
|
||||
use serde_with::serde_as;
|
||||
use tokio::time::Instant;
|
||||
|
|
@ -54,39 +54,22 @@ pub enum DocumentIntelligenceRequest {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, strum::EnumString, strum::Display, serde_with::DeserializeFromStr,
|
||||
)]
|
||||
enum OperationStatus {
|
||||
#[strum(serialize = "succeeded")]
|
||||
Succeeded,
|
||||
#[strum(serialize = "running")]
|
||||
Running,
|
||||
#[strum(serialize = "notStarted")]
|
||||
NotStarted,
|
||||
#[strum(serialize = "failed")]
|
||||
Failed,
|
||||
#[strum(default, transparent)]
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for OperationStatus {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
Ok(match String::deserialize(deserializer)?.as_str() {
|
||||
"succeeded" => Self::Succeeded,
|
||||
"running" => Self::Running,
|
||||
"notStarted" => Self::NotStarted,
|
||||
"failed" => Self::Failed,
|
||||
value => Self::Unknown(value.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for OperationStatus {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str(match self {
|
||||
Self::Succeeded => "succeeded",
|
||||
Self::Running => "running",
|
||||
Self::NotStarted => "notStarted",
|
||||
Self::Failed => "failed",
|
||||
Self::Unknown(value) => value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct AzureDocumentIntelligenceOperation {
|
||||
status: Option<OperationStatus>,
|
||||
|
|
@ -671,6 +654,43 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::succeeded("succeeded", OperationStatus::Succeeded)]
|
||||
#[case::running("running", OperationStatus::Running)]
|
||||
#[case::not_started("notStarted", OperationStatus::NotStarted)]
|
||||
#[case::failed("failed", OperationStatus::Failed)]
|
||||
fn operation_status_parses_known_values(
|
||||
#[case] input: &str,
|
||||
#[case] expected: OperationStatus,
|
||||
) {
|
||||
let parsed = serde_json::from_value::<OperationStatus>(json!(input)).unwrap();
|
||||
|
||||
assert_eq!(parsed, expected);
|
||||
assert_eq!(parsed.to_string(), input);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::unknown("queued")]
|
||||
#[case::case_sensitive("NotStarted")]
|
||||
#[case::escaped("future\"\\\n")]
|
||||
#[case::empty("")]
|
||||
fn operation_status_preserves_unknown_values(#[case] input: &str) {
|
||||
let parsed = serde_json::from_value::<OperationStatus>(json!(input)).unwrap();
|
||||
|
||||
assert_eq!(parsed, OperationStatus::Unknown(input.into()));
|
||||
assert_eq!(parsed.to_string(), input);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::number(json!(1))]
|
||||
#[case::boolean(json!(true))]
|
||||
#[case::array(json!([]))]
|
||||
#[case::null(Value::Null)]
|
||||
#[case::object(json!({"status": "succeeded"}))]
|
||||
fn operation_status_rejects_non_string_json(#[case] input: Value) {
|
||||
assert!(serde_json::from_value::<OperationStatus>(input).is_err());
|
||||
}
|
||||
|
||||
fn map(value: Value) -> Result<DocumentIntelligenceParams, Error> {
|
||||
let arguments = serde_json::from_value(value).unwrap();
|
||||
AzureDocumentIntelligenceOcrConfig.map_ocr_params(&arguments, "model")
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ opentelemetry-proto = { workspace = true, features = ["gen-tonic-messages", "tra
|
|||
prost.workspace = true
|
||||
serde = { workspace = true, features = ["rc"] }
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
serde_with.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
|
|
|
|||
|
|
@ -15,9 +15,15 @@ pub enum AgentType {
|
|||
}
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Eq, PartialEq, Serialize, Deserialize, strum::EnumString, strum::Display,
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
PartialEq,
|
||||
strum::EnumString,
|
||||
strum::Display,
|
||||
serde_with::DeserializeFromStr,
|
||||
serde_with::SerializeDisplay,
|
||||
)]
|
||||
#[serde(from = "String", into = "String")]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum Integration {
|
||||
ClaudeCode,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{Error, otlp::DecodedEvent};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod format;
|
||||
mod instrumentation;
|
||||
|
|
@ -44,8 +44,16 @@ pub enum ObservationType {
|
|||
}
|
||||
|
||||
/// A model request a span stands for, by the identifier its instrumentation recorded.
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[serde(try_from = "String")]
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
Eq,
|
||||
Ord,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
serde_with::DeserializeFromStr,
|
||||
serde_with::SerializeDisplay,
|
||||
)]
|
||||
pub enum CallKey {
|
||||
/// LiteLLM's gateway call id, with a fallback to legacy spend request ids.
|
||||
LiteLlmRequest(String),
|
||||
|
|
@ -101,12 +109,6 @@ pub enum CallEvidenceKind {
|
|||
Complete,
|
||||
}
|
||||
|
||||
impl Serialize for CallKey {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Which model requests a span accounts for. `Complete` comes only from an instrumentation's known
|
||||
/// contract (one chat span is one response), never from how many ids happened to be found.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use litellm_traces::{DecodedSpan, ObservationType, decode_otlp};
|
||||
use litellm_traces::{DecodedSpan, Integration, ObservationType, decode_otlp};
|
||||
use rstest::rstest;
|
||||
use serde_json::Value;
|
||||
|
||||
|
|
@ -101,6 +101,38 @@ fn assert_invariants(span: &DecodedSpan) {
|
|||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::known("claude-code", Integration::ClaudeCode)]
|
||||
#[case::unknown("future-agent", Integration::Other("future-agent".to_owned()))]
|
||||
#[case::case_sensitive("Claude-Code", Integration::Other("Claude-Code".to_owned()))]
|
||||
#[case::empty("", Integration::Other(String::new()))]
|
||||
#[case::escaped_unknown(
|
||||
"future\"agent\\path\nnext",
|
||||
Integration::Other("future\"agent\\path\nnext".to_owned())
|
||||
)]
|
||||
fn integration_string_round_trips(#[case] input: &str, #[case] expected: Integration) {
|
||||
assert_eq!(
|
||||
serde_json::from_value::<Integration>(serde_json::json!(input)).unwrap(),
|
||||
expected
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&expected).unwrap(),
|
||||
serde_json::json!(input)
|
||||
);
|
||||
assert_eq!(Integration::from(input.to_owned()), expected);
|
||||
assert_eq!(String::from(expected), input);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::null("null")]
|
||||
#[case::number("42")]
|
||||
#[case::boolean("true")]
|
||||
#[case::array("[]")]
|
||||
#[case::object("{}")]
|
||||
fn integration_rejects_non_string_json(#[case] input: &str) {
|
||||
assert!(serde_json::from_str::<Integration>(input).is_err());
|
||||
}
|
||||
|
||||
fn array<'a>(value: &'a Value, key: &str) -> &'a [Value] {
|
||||
value
|
||||
.get(key)
|
||||
|
|
@ -260,6 +292,10 @@ fn call_keys_round_trip_through_storage(#[case] key: litellm_traces::CallKey) {
|
|||
key
|
||||
);
|
||||
let encoded = serde_json::to_string(&key).unwrap();
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&encoded).unwrap(),
|
||||
serde_json::json!(key.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<litellm_traces::CallKey>(&encoded).unwrap(),
|
||||
key
|
||||
|
|
@ -278,3 +314,12 @@ fn malformed_call_keys_are_rejected_at_the_boundary(#[case] encoded: &str) {
|
|||
assert!(encoded.parse::<litellm_traces::CallKey>().is_err());
|
||||
assert!(serde_json::from_value::<litellm_traces::CallKey>(serde_json::json!(encoded)).is_err());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::null(serde_json::Value::Null)]
|
||||
#[case::number(serde_json::json!(42))]
|
||||
#[case::object(serde_json::json!({}))]
|
||||
#[case::array(serde_json::json!([]))]
|
||||
fn call_keys_reject_non_string_json(#[case] value: Value) {
|
||||
assert!(serde_json::from_value::<litellm_traces::CallKey>(value).is_err());
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue