Skip to main content

component_shape/
field_component.rs

1use crate::{
2    ComponentCapabilities, ComponentPrototyping, ComponentShapeFor, ComponentShapeMetadata,
3    McpInput, RustPath, RustType,
4};
5
6/// Typed borrowed source field name used in component shape metadata.
7#[derive(
8    Clone,
9    Copy,
10    Debug,
11    Eq,
12    Hash,
13    Ord,
14    PartialEq,
15    PartialOrd,
16    derive_more::AsRef,
17    derive_more::Display,
18    derive_more::From,
19    derive_more::Into,
20)]
21#[as_ref(forward)]
22#[display("{_0}")]
23pub struct ComponentFieldName<'a>(&'a str);
24
25impl<'a> ComponentFieldName<'a> {
26    /// Creates a typed field-name wrapper from borrowed identifier text.
27    pub const fn new(value: &'a str) -> Self {
28        Self(value)
29    }
30
31    /// Returns the wrapped source field name.
32    pub const fn as_str(self) -> &'a str {
33        self.0
34    }
35}
36
37/// Framework-neutral metadata describing a field's component shape use.
38///
39/// This records that a source field is associated with a component shape path,
40/// along with optional field type metadata and shape-owned capability or
41/// prototyping metadata. It is metadata only: runtime construction contracts,
42/// framework-specific render behavior, and storage semantics belong in the
43/// backend-specific crates that consume it.
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub struct ComponentShapeUse {
46    field_name: ComponentFieldName<'static>,
47    field_type: Option<RustType>,
48    shape_path: RustPath,
49    capabilities: ComponentCapabilities,
50    prototyping: ComponentPrototyping,
51    mcp_input: McpInput,
52}
53
54impl ComponentShapeUse {
55    /// Records that `field_name` uses the component shape at `shape_path`.
56    pub const fn new(field_name: ComponentFieldName<'static>, shape_path: RustPath) -> Self {
57        Self {
58            field_name,
59            field_type: None,
60            shape_path,
61            capabilities: ComponentCapabilities::new(),
62            prototyping: ComponentPrototyping::new(),
63            mcp_input: McpInput::unsupported(),
64        }
65    }
66
67    /// Records that a source field uses the component shape at `shape_path`.
68    pub const fn for_field(field_name: &'static str, shape_path: RustPath) -> Self {
69        Self::new(ComponentFieldName::new(field_name), shape_path)
70    }
71
72    /// Adds Rust type metadata for the source field.
73    pub const fn with_field_type(mut self, field_type: RustType) -> Self {
74        self.field_type = Some(field_type);
75        self
76    }
77
78    /// Replaces the shape capability metadata.
79    pub const fn with_capabilities(mut self, capabilities: ComponentCapabilities) -> Self {
80        self.capabilities = capabilities;
81        self
82    }
83
84    /// Replaces the shape prototyping metadata.
85    pub const fn with_prototyping(mut self, prototyping: ComponentPrototyping) -> Self {
86        self.prototyping = prototyping;
87        self
88    }
89
90    /// Replaces the shape's model-controlled MCP input metadata.
91    pub const fn with_mcp_input(mut self, mcp_input: McpInput) -> Self {
92        self.mcp_input = mcp_input;
93        self
94    }
95
96    /// Copies type-owned shape metadata from a concrete component shape.
97    pub const fn with_shape_metadata<Shape>(mut self) -> Self
98    where
99        Shape: ComponentShapeMetadata,
100    {
101        self.capabilities = Shape::CAPABILITIES;
102        self.prototyping = Shape::PROTOTYPING;
103        self.mcp_input = Shape::MCP_INPUT;
104        self
105    }
106
107    /// Copies value-specific MCP input metadata from a component shape/value pair.
108    pub const fn with_value_mcp_input<Shape, Value>(mut self) -> Self
109    where
110        Shape: ComponentShapeFor<Value>,
111    {
112        self.mcp_input = <Shape as ComponentShapeFor<Value>>::MCP_INPUT;
113        self
114    }
115
116    /// Source field name.
117    pub const fn field_name(self) -> ComponentFieldName<'static> {
118        self.field_name
119    }
120
121    /// Optional Rust type metadata for the source field.
122    pub const fn field_type(self) -> Option<RustType> {
123        self.field_type
124    }
125
126    /// Component shape path used by the source field.
127    pub const fn shape_path(self) -> RustPath {
128        self.shape_path
129    }
130
131    /// Shape capability metadata.
132    pub const fn capabilities(self) -> ComponentCapabilities {
133        self.capabilities
134    }
135
136    /// Shape prototyping metadata.
137    pub const fn prototyping(self) -> ComponentPrototyping {
138        self.prototyping
139    }
140
141    /// Shape metadata for model-controlled MCP input.
142    pub const fn mcp_input(self) -> McpInput {
143        self.mcp_input
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::{ComponentFieldName, ComponentShapeUse};
150    use crate::{
151        ComponentCapabilities, ComponentPrototyping, ComponentShapeFor, ComponentShapeMetadata,
152        McpInput, McpInputShape, McpPrimitiveKind, RenderCapability, RustPath, RustType,
153        ValueBindingCapability,
154    };
155
156    struct TextShape;
157
158    impl ComponentShapeMetadata for TextShape {
159        const CAPABILITIES: ComponentCapabilities =
160            ComponentCapabilities::new().with_render(RenderCapability::Component);
161        const PROTOTYPING: ComponentPrototyping = ComponentPrototyping::new().field_suffix("input");
162        const MCP_INPUT: McpInput = McpInput::string();
163    }
164
165    impl ComponentShapeFor<Vec<String>> for TextShape {
166        const MCP_INPUT: McpInput = McpInput::string_list();
167    }
168
169    #[test]
170    fn component_field_name_exposes_borrowed_identifier_text() {
171        let field_name = ComponentFieldName::new("title");
172        let field_name_ref: &str = field_name.as_ref();
173
174        assert_eq!(field_name.as_str(), "title");
175        assert_eq!(field_name_ref, "title");
176        assert_eq!(field_name.to_string(), "title");
177    }
178
179    #[test]
180    fn component_shape_use_defaults_to_no_capabilities_or_prototyping() {
181        let shape_use = ComponentShapeUse::new(
182            ComponentFieldName::new("title"),
183            RustPath::from_macro_tokens_unchecked("crate::fields::TitleInput"),
184        );
185
186        assert_eq!(shape_use.capabilities(), ComponentCapabilities::new());
187        assert_eq!(shape_use.prototyping(), ComponentPrototyping::new());
188        assert_eq!(shape_use.mcp_input(), McpInput::unsupported());
189    }
190
191    #[test]
192    fn component_shape_use_preserves_field_name_and_shape_path() {
193        let shape_use = ComponentShapeUse::for_field(
194            std::hint::black_box("title"),
195            RustPath::from_macro_tokens_unchecked("crate::fields::TitleInput"),
196        );
197
198        assert_eq!(shape_use.field_name().as_str(), "title");
199        assert_eq!(shape_use.shape_path().as_str(), "crate::fields::TitleInput");
200    }
201
202    #[test]
203    fn component_shape_use_records_optional_field_type() {
204        let shape_use = ComponentShapeUse::new(
205            ComponentFieldName::new("title"),
206            RustPath::from_macro_tokens_unchecked("crate::fields::TitleInput"),
207        )
208        .with_field_type(RustType::from_macro_tokens_unchecked("String"));
209
210        assert_eq!(shape_use.field_type().map(RustType::as_str), Some("String"));
211    }
212
213    #[test]
214    fn component_shape_use_preserves_explicit_capabilities_and_prototyping() {
215        let capabilities = ComponentCapabilities::new()
216            .with_render(RenderCapability::Component)
217            .with_value_binding(ValueBindingCapability::Inherited);
218        let prototyping = ComponentPrototyping::new().field_suffix("title_input");
219
220        let shape_use = ComponentShapeUse::new(
221            ComponentFieldName::new("title"),
222            RustPath::from_macro_tokens_unchecked("crate::fields::TitleInput"),
223        )
224        .with_capabilities(capabilities)
225        .with_prototyping(prototyping);
226
227        assert_eq!(shape_use.capabilities(), capabilities);
228        assert_eq!(
229            shape_use
230                .prototyping()
231                .field_suffix
232                .map(crate::ComponentSuffix::as_str),
233            Some("title_input")
234        );
235    }
236
237    #[test]
238    fn component_shape_use_preserves_mcp_input_metadata() {
239        let input = McpInput::string();
240
241        let shape_use = ComponentShapeUse::new(
242            ComponentFieldName::new("title"),
243            RustPath::from_macro_tokens_unchecked("crate::fields::TitleInput"),
244        )
245        .with_mcp_input(input);
246
247        assert_eq!(shape_use.mcp_input(), input);
248        assert_eq!(
249            shape_use.mcp_input().input_shape(),
250            McpInputShape::Scalar(McpPrimitiveKind::String)
251        );
252    }
253
254    #[test]
255    fn component_shape_use_can_copy_shape_metadata_from_type() {
256        let shape_use = ComponentShapeUse::new(
257            ComponentFieldName::new("title"),
258            RustPath::from_macro_tokens_unchecked("crate::fields::TitleInput"),
259        )
260        .with_shape_metadata::<TextShape>();
261
262        assert_eq!(
263            shape_use.capabilities().render(),
264            RenderCapability::Component
265        );
266        assert_eq!(
267            shape_use
268                .prototyping()
269                .field_suffix
270                .map(crate::ComponentSuffix::as_str),
271            Some("input")
272        );
273        assert_eq!(
274            shape_use.mcp_input().input_shape(),
275            McpInputShape::Scalar(McpPrimitiveKind::String)
276        );
277    }
278
279    #[test]
280    fn component_shape_use_can_copy_value_specific_mcp_input_from_type() {
281        let shape_use = ComponentShapeUse::new(
282            ComponentFieldName::new("tags"),
283            RustPath::from_macro_tokens_unchecked("crate::fields::TagsInput"),
284        )
285        .with_shape_metadata::<TextShape>()
286        .with_value_mcp_input::<TextShape, Vec<String>>();
287
288        assert_eq!(
289            shape_use.mcp_input().input_shape(),
290            McpInputShape::List(McpPrimitiveKind::String)
291        );
292    }
293}