component-shape-mcp 0.3.0

Shared MCP protocol and schema helpers for component-shape integrations.
Documentation
use super::*;

#[test]
fn schema_description_helpers_only_mutate_object_schemas() {
    let described = schema(json!({ "type": "string" })).with_description("Search text");

    assert_eq!(described["description"], "Search text");

    let boolean_schema = schema(json!(true)).with_description("Ignored");

    assert_eq!(boolean_schema.as_value(), &json!(true));
}

#[test]
fn schema_builders_emit_common_json_schema_keywords() {
    let schema = crate::McpSchema::object()
        .with_properties(crate::McpSchemaProperties::from([
            (
                "state".to_string(),
                crate::McpSchema::string().with_enum_values(["open", "closed"]),
            ),
            (
                "limit".to_string(),
                crate::McpSchema::integer()
                    .with_minimum(0_u64)
                    .with_default(25_u64),
            ),
            (
                "tags".to_string(),
                crate::McpSchema::array(crate::McpSchema::string()).with_unique_items(true),
            ),
        ]))
        .with_required(["state"])
        .with_additional_properties(false);

    assert_eq!(schema["type"], "object");
    assert_eq!(
        schema["properties"]["state"]["enum"],
        json!(["open", "closed"])
    );
    assert_eq!(schema["properties"]["limit"]["minimum"], 0);
    assert_eq!(schema["properties"]["limit"]["default"], 25);
    assert_eq!(schema["properties"]["tags"]["items"]["type"], "string");
    assert_eq!(schema["properties"]["tags"]["uniqueItems"], true);
    assert_eq!(schema["required"], json!(["state"]));
    assert_eq!(schema["additionalProperties"], false);
}

#[test]
fn schema_for_input_maps_range_dates() {
    let schema = crate::schema_for_input(McpInput::date_range());

    assert_eq!(schema["properties"]["min"]["anyOf"][0]["format"], "date");
    assert_eq!(schema["properties"]["max"]["anyOf"][1]["type"], "null");
}

#[test]
fn schema_for_input_distinguishes_any_from_unsupported() {
    assert_eq!(
        crate::schema_for_input(McpInput::any()).as_value(),
        &json!({})
    );
    assert_eq!(
        crate::schema_for_input(McpInput::unsupported()).as_value(),
        &json!({ "not": {} })
    );
}

#[test]
fn schema_for_input_distinguishes_lists_from_sets() {
    let list_schema = crate::schema_for_input(McpInput::string_list());
    let set_schema = crate::schema_for_input(McpInput::string_set());

    assert_eq!(list_schema["type"], "array");
    assert!(list_schema["uniqueItems"].is_null());
    assert_eq!(set_schema["type"], "array");
    assert_eq!(set_schema["uniqueItems"], true);
}

#[test]
fn mcp_input_descriptor_value_describes_supported_shapes() {
    assert_eq!(
        crate::mcp_input_descriptor_value(McpInput::unsupported()),
        json!({
            "supported": false,
            "shape": "unsupported",
        })
    );
    assert_eq!(
        crate::mcp_input_descriptor_value(McpInput::string_set()),
        json!({
            "supported": true,
            "shape": "set",
            "items": "string",
        })
    );
    assert_eq!(
        crate::mcp_input_descriptor_value(McpInput::date_range()),
        json!({
            "supported": true,
            "shape": "range",
            "bound": "date",
        })
    );
}

#[test]
fn json_schema_trait_supports_aliases_and_containers() {
    type UserId = u64;

    assert_eq!(
        <UserId as crate::McpJsonSchema>::json_schema()["type"],
        "integer"
    );
    assert_eq!(
        <Option<Vec<String>> as crate::McpJsonSchema>::json_schema()["anyOf"][0]["items"]["type"],
        "string"
    );
    assert_eq!(
        <std::collections::HashSet<String> as crate::McpJsonSchema>::json_schema()["uniqueItems"],
        true
    );
    assert_eq!(
        <std::collections::BTreeMap<String, u32> as crate::McpJsonSchema>::json_schema()["additionalProperties"]
            ["type"],
        "integer"
    );
    assert_eq!(
        <serde_json::Map<String, u32> as crate::McpJsonSchema>::json_schema()["additionalProperties"]
            ["type"],
        "integer"
    );
    assert_eq!(
        <McpAny as crate::McpJsonSchema>::json_schema().as_value(),
        &json!({})
    );
    assert_eq!(
        <serde_json::Value as crate::McpJsonSchema>::json_schema().as_value(),
        &json!({})
    );
    assert_eq!(
        <&str as crate::McpJsonSchema>::json_schema()["type"],
        "string"
    );
    assert_eq!(
        <std::borrow::Cow<'static, str> as crate::McpJsonSchema>::json_schema()["type"],
        "string"
    );
    assert_eq!(
        <[String] as crate::McpJsonSchema>::json_schema()["items"]["type"],
        "string"
    );
    assert_eq!(
        <McpRange<u32> as crate::McpJsonSchema>::json_schema()["properties"]["min"]["anyOf"][0]["type"],
        "integer"
    );
    assert_eq!(
        <(u32, String) as crate::McpJsonSchema>::json_schema()["prefixItems"][0]["type"],
        "integer"
    );
    assert_eq!(
        <(u32, String) as crate::McpJsonSchema>::json_schema()["prefixItems"][1]["type"],
        "string"
    );
    assert_eq!(
        <(u32, String) as crate::McpJsonSchema>::json_schema()["minItems"],
        2
    );
    assert_eq!(
        serde_json::from_value::<McpRange<u32>>(json!({
            "min": 1,
            "max": null
        }))
        .expect("range object should decode"),
        McpRange::new(Some(1), None)
    );
    assert!(
        serde_json::from_value::<McpRange<u32>>(json!({
            "min": 1,
            "step": 2
        }))
        .is_err()
    );
}

#[test]
fn schema_allows_null_matches_common_schema_forms() {
    assert!(crate::schema_allows_null(&schema(json!({}))));
    assert!(crate::schema_allows_null(&schema(json!({
        "anyOf": [
            { "type": "string" },
            { "type": "null" }
        ]
    }))));
    assert!(crate::schema_allows_null(&schema(json!({
        "type": ["string", "null"]
    }))));
    assert!(!crate::schema_allows_null(&schema(json!({
        "type": "string"
    }))));
}

#[test]
fn schema_object_rejects_non_object_values() {
    let object = crate::schema_object("input_schema", schema(json!({ "type": "object" })))
        .expect("schema should be accepted");
    assert_eq!(object["type"], "object");

    let error = crate::schema_object("input_schema", schema(json!(null)))
        .expect_err("schema should be rejected");
    assert!(error.to_string().contains("input_schema"));
}

#[test]
fn schema_conversions_and_remaining_json_schema_impls_are_exercised() {
    use std::{
        borrow::Cow,
        collections::{BTreeMap, BTreeSet, HashMap},
        path::{Path, PathBuf},
    };

    assert_eq!(crate::McpSchemaType::Number.as_str(), "number");
    assert_eq!(crate::McpStringFormat::DateTime.as_str(), "date-time");
    assert_eq!(
        crate::McpSchemaNumber::from_f64(1.5)
            .expect("finite number should convert")
            .into_value(),
        json!(1.5)
    );
    assert!(crate::McpSchemaNumber::from_f64(f64::NAN).is_none());
    assert_eq!(
        crate::McpSchemaNumber::from(-2_isize).into_value(),
        json!(-2)
    );
    assert_eq!(crate::McpSchemaNumber::from(2_usize).into_value(), json!(2));

    let schema = crate::McpSchema::one_of([crate::McpSchema::string(), crate::McpSchema::number()])
        .with_extension("x-test", json!(true))
        .with_const("fixed");
    assert_eq!(schema["oneOf"].as_array().map(Vec::len), Some(2));
    assert_eq!(schema["x-test"], true);
    assert_eq!(schema["const"], "fixed");

    let any = crate::McpAny::from(json!({ "value": 1 }));
    assert_eq!(any.as_value()["value"], 1);
    assert_eq!((*any)["value"], 1);
    let value: Value = any.into();
    assert_eq!(value["value"], 1);

    let range = crate::McpRange::from((Some(1_i32), Some(3_i32)));
    let tuple: (Option<i32>, Option<i32>) = range.into();
    assert_eq!(tuple, (Some(1), Some(3)));

    let schemas = [
        <f32 as crate::McpJsonSchema>::json_schema(),
        <f64 as crate::McpJsonSchema>::json_schema(),
        <rust_decimal::Decimal as crate::McpJsonSchema>::json_schema(),
        <char as crate::McpJsonSchema>::json_schema(),
        <PathBuf as crate::McpJsonSchema>::json_schema(),
        <Path as crate::McpJsonSchema>::json_schema(),
        <chrono::NaiveDate as crate::McpJsonSchema>::json_schema(),
        <chrono::NaiveDateTime as crate::McpJsonSchema>::json_schema(),
        <chrono::DateTime<chrono::Utc> as crate::McpJsonSchema>::json_schema(),
        <[String; 2] as crate::McpJsonSchema>::json_schema(),
        <BTreeSet<String> as crate::McpJsonSchema>::json_schema(),
        <HashMap<String, i32> as crate::McpJsonSchema>::json_schema(),
        <BTreeMap<String, i32> as crate::McpJsonSchema>::json_schema(),
        <Box<String> as crate::McpJsonSchema>::json_schema(),
        <() as crate::McpJsonSchema>::json_schema(),
        <&str as crate::McpJsonSchema>::json_schema(),
        <Cow<'static, str> as crate::McpJsonSchema>::json_schema(),
    ];
    assert_eq!(schemas[0]["type"], "number");
    assert_eq!(schemas[3]["type"], "string");
    assert_eq!(schemas[8]["format"], "date-time");
    assert_eq!(schemas[9]["minItems"], 2);
    assert_eq!(schemas[10]["uniqueItems"], true);
    assert_eq!(schemas[11]["additionalProperties"]["type"], "integer");
    assert_eq!(schemas[14]["type"], "null");
}

#[test]
fn schema_normalization_covers_composites_aliases_and_collection_items() {
    let enum_alias = json!({
        "type": "string",
        "enum": ["ready"],
        "x-mcpEnumDecodeAliases": { "prepared": "ready" }
    });
    let any_of = json!({
        "anyOf": [
            { "type": "integer" },
            enum_alias
        ]
    });
    assert_eq!(
        crate::normalize_value_against_schema(&any_of, json!("prepared")),
        json!("ready")
    );
    assert_eq!(
        crate::normalize_value_against_schema(
            &json!({ "oneOf": [false, enum_alias] }),
            json!("prepared")
        ),
        json!("ready")
    );

    let object_schema = json!({
        "type": "object",
        "properties": {
            "wire": {
                "type": "string",
                "x-mcpAliases": ["alias"],
                "x-mcpDecodeName": "rust_name",
                "x-mcpEnumDecodeAliases": { "prepared": "ready" }
            }
        },
        "additionalProperties": enum_alias
    });
    assert_eq!(
        crate::normalize_value_against_schema(
            &object_schema,
            json!({ "alias": "prepared", "extra": "prepared" })
        ),
        json!({ "rust_name": "ready", "extra": "ready" })
    );

    let all_of = json!({ "allOf": [true, object_schema] });
    assert_eq!(
        crate::normalize_value_against_schema(&all_of, json!({ "wire": "prepared" })),
        json!({ "rust_name": "ready" })
    );
    assert_eq!(
        crate::normalize_value_against_schema(&json!({ "items": enum_alias }), json!(["prepared"])),
        json!(["ready"])
    );
    assert_eq!(
        crate::normalize_value_against_schema(
            &json!({ "prefixItems": [enum_alias] }),
            json!(["prepared", "unchanged"])
        ),
        json!(["ready", "unchanged"])
    );

    for value in [
        json!(null),
        json!(true),
        json!(1),
        json!(1.5),
        json!("ready"),
        json!([]),
    ] {
        let schema = json!({ "anyOf": [true] });
        assert_eq!(
            crate::normalize_value_against_schema(&schema, value.clone()),
            value
        );
    }
}