tapes_client/core/models/
skill.rs1use serde::{Deserialize, Serialize};
10
11use super::ContractModel;
12
13#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
17#[serde(default)]
18#[non_exhaustive]
19pub struct SkillResponse {
20 #[serde(rename = "authorId")]
22 pub author_id: String,
23
24 pub content: String,
26
27 #[serde(rename = "createdAt")]
29 pub created_at: String,
30
31 pub description: String,
33
34 #[serde(rename = "downloadCount")]
36 pub download_count: i64,
37
38 pub id: String,
40
41 #[serde(rename = "isAiGenerated")]
43 pub is_ai_generated: bool,
44
45 pub name: String,
47
48 #[serde(
50 rename = "originatingSessionIds",
51 deserialize_with = "super::null_default"
52 )]
53 pub originating_session_ids: Vec<String>,
54
55 #[serde(rename = "parentId", default)]
60 pub parent_id: Option<String>,
61
62 pub slug: String,
64
65 #[serde(deserialize_with = "super::null_default")]
67 pub tags: Vec<String>,
68
69 #[serde(rename = "type")]
71 pub type_: String,
72
73 #[serde(rename = "updatedAt")]
75 pub updated_at: String,
76
77 pub version: String,
79
80 pub visibility: String,
82}
83
84impl ContractModel for SkillResponse {
85 const SCHEMA: &'static str = "skillResponse";
86}
87
88#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
94#[serde(default)]
95#[non_exhaustive]
96pub struct SkillsListResponse {
97 #[serde(deserialize_with = "super::null_default")]
99 pub counts: SkillCounts,
100
101 #[serde(deserialize_with = "super::null_default")]
103 pub items: Vec<SkillResponse>,
104
105 pub next_cursor: String,
107}
108
109impl ContractModel for SkillsListResponse {
110 const SCHEMA: &'static str = "skillsListResponse";
111}
112
113#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
118#[serde(default)]
119#[non_exhaustive]
120pub struct SkillCounts {
121 pub all: i64,
123
124 pub mine: i64,
126
127 pub team: i64,
129}
130
131impl ContractModel for SkillCounts {
132 const SCHEMA: &'static str = "skillCountsResp";
133}
134
135#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
139#[serde(default)]
140#[non_exhaustive]
141pub struct SkillVersionResponse {
142 #[serde(rename = "authorId")]
144 pub author_id: String,
145
146 pub changelog: String,
148
149 pub content: String,
151
152 pub id: String,
154
155 #[serde(rename = "publishedAt")]
157 pub published_at: String,
158
159 pub semver: String,
161
162 #[serde(rename = "skillId")]
164 pub skill_id: String,
165
166 #[serde(rename = "versionNumber")]
168 pub version_number: i32,
169}
170
171impl ContractModel for SkillVersionResponse {
172 const SCHEMA: &'static str = "skillVersionResponse";
173}
174
175#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
180#[serde(default)]
181#[non_exhaustive]
182pub struct SkillVersionsResponse {
183 #[serde(rename = "totalCount")]
185 pub total_count: i32,
186
187 #[serde(deserialize_with = "super::null_default")]
189 pub versions: Vec<SkillVersionResponse>,
190}
191
192impl ContractModel for SkillVersionsResponse {
193 const SCHEMA: &'static str = "skillVersionsResponse";
194}
195
196#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
201#[serde(default)]
202#[non_exhaustive]
203pub struct SessionSkillsResponse {
204 #[serde(deserialize_with = "super::null_default")]
206 pub items: Vec<SkillResponse>,
207}
208
209impl ContractModel for SessionSkillsResponse {
210 const SCHEMA: &'static str = "sessionSkillsResponse";
211}
212
213#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
219#[serde(default)]
220pub struct CreateSkillRequest {
221 pub content: String,
223
224 pub description: String,
226
227 pub name: String,
229
230 #[serde(deserialize_with = "super::null_default")]
232 pub tags: Vec<String>,
233
234 #[serde(rename = "type")]
236 pub type_: String,
237}
238
239impl ContractModel for CreateSkillRequest {
240 const SCHEMA: &'static str = "createSkillRequest";
241}
242
243#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
255#[serde(default)]
256pub struct UpdateSkillRequest {
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub content: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub description: Option<String>,
264
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub name: Option<String>,
268
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub tags: Option<Vec<String>>,
272
273 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
275 pub type_: Option<String>,
276
277 #[serde(skip_serializing_if = "Option::is_none")]
279 pub visibility: Option<String>,
280}
281
282impl ContractModel for UpdateSkillRequest {
283 const SCHEMA: &'static str = "updateSkillRequest";
284}
285
286#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
290#[serde(default)]
291pub struct PublishSkillRequest {
292 pub changelog: String,
294
295 pub content: String,
297}
298
299impl ContractModel for PublishSkillRequest {
300 const SCHEMA: &'static str = "publishSkillRequest";
301}
302
303#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
309#[serde(default)]
310pub struct GenerateSkillRequest {
311 #[serde(deserialize_with = "super::null_default")]
313 pub hint: GenerateSkillRequestHint,
314
315 #[serde(rename = "sessionIds", deserialize_with = "super::null_default")]
317 pub session_ids: Vec<String>,
318}
319
320impl ContractModel for GenerateSkillRequest {
321 const SCHEMA: &'static str = "generateSkillRequest";
322}
323
324#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
336#[serde(default)]
337pub struct GenerateSkillRequestHint {
338 pub description: String,
340
341 pub name: String,
343
344 #[serde(deserialize_with = "super::null_default")]
346 pub tags: Vec<String>,
347
348 #[serde(rename = "type")]
350 pub type_: String,
351}
352
353impl SkillsListResponse {
354 #[must_use]
359 pub fn into_page(self) -> crate::page::Page<SkillResponse> {
360 crate::page::Page {
361 items: self.items,
362 next_cursor: Some(self.next_cursor),
363 }
364 }
365}
366
367#[cfg(test)]
368#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
369mod parent_id_tests {
370 use super::SkillResponse;
371
372 #[test]
373 fn a_root_skill_with_null_parent_decodes() {
374 let row = serde_json::json!({
379 "id": "skl-1", "slug": "s", "name": "n", "description": "d",
380 "type": "workflow", "version": "0.1.0", "visibility": "private",
381 "tags": [], "content": "c", "isAiGenerated": false,
382 "originatingSessionIds": [], "authorId": "user_1",
383 "downloadCount": 0, "parentId": null,
384 "createdAt": "2026-01-01T00:00:00Z", "updatedAt": "2026-01-01T00:00:00Z"
385 });
386 let parsed: SkillResponse = serde_json::from_value(row)
387 .unwrap_or_else(|e| panic!("null parentId must decode: {e}"));
388 assert_eq!(parsed.parent_id, None);
389
390 let fork = serde_json::json!({
391 "id": "skl-2", "slug": "s2", "name": "n", "description": "d",
392 "type": "workflow", "version": "0.1.0", "visibility": "private",
393 "tags": [], "content": "c", "isAiGenerated": false,
394 "originatingSessionIds": [], "authorId": "user_1",
395 "downloadCount": 0, "parentId": "skl-1",
396 "createdAt": "2026-01-01T00:00:00Z", "updatedAt": "2026-01-01T00:00:00Z"
397 });
398 let parsed: SkillResponse =
399 serde_json::from_value(fork).unwrap_or_else(|e| panic!("fork must decode: {e}"));
400 assert_eq!(parsed.parent_id.as_deref(), Some("skl-1"));
401 }
402}