1use crate::{
2 ComponentCapabilities, ComponentPrototyping, ComponentShapeFor, ComponentShapeMetadata,
3 McpInput, RustPath, RustType,
4};
5
6#[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 pub const fn new(value: &'a str) -> Self {
28 Self(value)
29 }
30
31 pub const fn as_str(self) -> &'a str {
33 self.0
34 }
35}
36
37#[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 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 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 pub const fn with_field_type(mut self, field_type: RustType) -> Self {
74 self.field_type = Some(field_type);
75 self
76 }
77
78 pub const fn with_capabilities(mut self, capabilities: ComponentCapabilities) -> Self {
80 self.capabilities = capabilities;
81 self
82 }
83
84 pub const fn with_prototyping(mut self, prototyping: ComponentPrototyping) -> Self {
86 self.prototyping = prototyping;
87 self
88 }
89
90 pub const fn with_mcp_input(mut self, mcp_input: McpInput) -> Self {
92 self.mcp_input = mcp_input;
93 self
94 }
95
96 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 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 pub const fn field_name(self) -> ComponentFieldName<'static> {
118 self.field_name
119 }
120
121 pub const fn field_type(self) -> Option<RustType> {
123 self.field_type
124 }
125
126 pub const fn shape_path(self) -> RustPath {
128 self.shape_path
129 }
130
131 pub const fn capabilities(self) -> ComponentCapabilities {
133 self.capabilities
134 }
135
136 pub const fn prototyping(self) -> ComponentPrototyping {
138 self.prototyping
139 }
140
141 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}