Skip to main content

tapes_client/core/models/
skill.rs

1//! Skill shapes.
2//!
3//! These are the one corner of the contract that speaks camelCase — the
4//! console's skills schemas predate the snake_case convention the rest of tapes
5//! uses. The models carry snake_case field names with the wire spelling
6//! attached, so a Rust call site reads like Rust and the bytes stay the
7//! document's.
8
9use serde::{Deserialize, Serialize};
10
11use super::ContractModel;
12
13/// The unified Skill shape the console expects (camelCase).
14///
15/// Models the contract's `skillResponse` schema.
16#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
17#[serde(default)]
18#[non_exhaustive]
19pub struct SkillResponse {
20    /// The contract's `authorId`.
21    #[serde(rename = "authorId")]
22    pub author_id: String,
23
24    /// The contract's `content`.
25    pub content: String,
26
27    /// The contract's `createdAt`.
28    #[serde(rename = "createdAt")]
29    pub created_at: String,
30
31    /// The contract's `description`.
32    pub description: String,
33
34    /// The contract's `downloadCount`.
35    #[serde(rename = "downloadCount")]
36    pub download_count: i64,
37
38    /// The contract's `id`.
39    pub id: String,
40
41    /// The contract's `isAiGenerated`.
42    #[serde(rename = "isAiGenerated")]
43    pub is_ai_generated: bool,
44
45    /// The contract's `name`.
46    pub name: String,
47
48    /// The contract's `originatingSessionIds`.
49    #[serde(
50        rename = "originatingSessionIds",
51        deserialize_with = "super::null_default"
52    )]
53    pub originating_session_ids: Vec<String>,
54
55    /// The contract's `parentId`.
56    #[serde(rename = "parentId")]
57    pub parent_id: String,
58
59    /// The contract's `slug`.
60    pub slug: String,
61
62    /// The contract's `tags`.
63    #[serde(deserialize_with = "super::null_default")]
64    pub tags: Vec<String>,
65
66    /// The contract's `type`.
67    #[serde(rename = "type")]
68    pub type_: String,
69
70    /// The contract's `updatedAt`.
71    #[serde(rename = "updatedAt")]
72    pub updated_at: String,
73
74    /// The contract's `version`.
75    pub version: String,
76
77    /// The contract's `visibility`.
78    pub visibility: String,
79}
80
81impl ContractModel for SkillResponse {
82    const SCHEMA: &'static str = "skillResponse";
83}
84
85/// The paginated list envelope: one keyset page plus
86/// the opaque next_cursor (mirroring /v1/sessions) and the per-tab counts for
87/// the active search.
88///
89/// Models the contract's `skillsListResponse` schema.
90#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
91#[serde(default)]
92#[non_exhaustive]
93pub struct SkillsListResponse {
94    /// The contract's `counts`.
95    #[serde(deserialize_with = "super::null_default")]
96    pub counts: SkillCounts,
97
98    /// The contract's `items`.
99    #[serde(deserialize_with = "super::null_default")]
100    pub items: Vec<SkillResponse>,
101
102    /// The contract's `next_cursor`.
103    pub next_cursor: String,
104}
105
106impl ContractModel for SkillsListResponse {
107    const SCHEMA: &'static str = "skillsListResponse";
108}
109
110/// The tab counts for the current search: all matching,
111/// authored by the caller (mine), and everyone else's (team = all - mine).
112///
113/// Models the contract's `skillCountsResp` schema.
114#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
115#[serde(default)]
116#[non_exhaustive]
117pub struct SkillCounts {
118    /// The contract's `all`.
119    pub all: i64,
120
121    /// The contract's `mine`.
122    pub mine: i64,
123
124    /// The contract's `team`.
125    pub team: i64,
126}
127
128impl ContractModel for SkillCounts {
129    const SCHEMA: &'static str = "skillCountsResp";
130}
131
132/// One immutable published snapshot.
133///
134/// Models the contract's `skillVersionResponse` schema.
135#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
136#[serde(default)]
137#[non_exhaustive]
138pub struct SkillVersionResponse {
139    /// The contract's `authorId`.
140    #[serde(rename = "authorId")]
141    pub author_id: String,
142
143    /// The contract's `changelog`.
144    pub changelog: String,
145
146    /// The contract's `content`.
147    pub content: String,
148
149    /// The contract's `id`.
150    pub id: String,
151
152    /// The contract's `publishedAt`.
153    #[serde(rename = "publishedAt")]
154    pub published_at: String,
155
156    /// The contract's `semver`.
157    pub semver: String,
158
159    /// The contract's `skillId`.
160    #[serde(rename = "skillId")]
161    pub skill_id: String,
162
163    /// The contract's `versionNumber`.
164    #[serde(rename = "versionNumber")]
165    pub version_number: i32,
166}
167
168impl ContractModel for SkillVersionResponse {
169    const SCHEMA: &'static str = "skillVersionResponse";
170}
171
172/// The full version history for one skill, newest
173/// first.
174///
175/// Models the contract's `skillVersionsResponse` schema.
176#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
177#[serde(default)]
178#[non_exhaustive]
179pub struct SkillVersionsResponse {
180    /// The contract's `totalCount`.
181    #[serde(rename = "totalCount")]
182    pub total_count: i32,
183
184    /// The contract's `versions`.
185    #[serde(deserialize_with = "super::null_default")]
186    pub versions: Vec<SkillVersionResponse>,
187}
188
189impl ContractModel for SkillVersionsResponse {
190    const SCHEMA: &'static str = "skillVersionsResponse";
191}
192
193/// The envelope for the skills attributed to one
194/// session.
195///
196/// Models the contract's `sessionSkillsResponse` schema.
197#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
198#[serde(default)]
199#[non_exhaustive]
200pub struct SessionSkillsResponse {
201    /// The contract's `items`.
202    #[serde(deserialize_with = "super::null_default")]
203    pub items: Vec<SkillResponse>,
204}
205
206impl ContractModel for SessionSkillsResponse {
207    const SCHEMA: &'static str = "sessionSkillsResponse";
208}
209
210/// The POST /v1/skills body for an authored-from-
211/// scratch skill — only a name is required; the rest default to an empty
212/// private draft.
213///
214/// Models the contract's `createSkillRequest` schema.
215#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
216#[serde(default)]
217pub struct CreateSkillRequest {
218    /// The contract's `content`.
219    pub content: String,
220
221    /// The contract's `description`.
222    pub description: String,
223
224    /// The contract's `name`.
225    pub name: String,
226
227    /// The contract's `tags`.
228    #[serde(deserialize_with = "super::null_default")]
229    pub tags: Vec<String>,
230
231    /// The contract's `type`.
232    #[serde(rename = "type")]
233    pub type_: String,
234}
235
236impl ContractModel for CreateSkillRequest {
237    const SCHEMA: &'static str = "createSkillRequest";
238}
239
240/// The PUT /v1/skills/:slug body — all fields optional;
241/// only present fields are applied onto the existing record.
242///
243/// Every field is an [`Option`] that is omitted from the wire when unset,
244/// because "only present fields are applied" makes an absent field the *only*
245/// way to say "leave this one alone". A body that spelled all six every time
246/// would turn a one-field rename into a five-field erasure: the server would
247/// dutifully apply the empty content, the empty description, and the empty tag
248/// list it was sent.
249///
250/// Models the contract's `updateSkillRequest` schema.
251#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
252#[serde(default)]
253pub struct UpdateSkillRequest {
254    /// The contract's `content`.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub content: Option<String>,
257
258    /// The contract's `description`.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub description: Option<String>,
261
262    /// The contract's `name`.
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub name: Option<String>,
265
266    /// The contract's `tags`.
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub tags: Option<Vec<String>>,
269
270    /// The contract's `type`.
271    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
272    pub type_: Option<String>,
273
274    /// The contract's `visibility`.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub visibility: Option<String>,
277}
278
279impl ContractModel for UpdateSkillRequest {
280    const SCHEMA: &'static str = "updateSkillRequest";
281}
282
283/// The POST /v1/skills/:slug/versions body.
284///
285/// Models the contract's `publishSkillRequest` schema.
286#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
287#[serde(default)]
288pub struct PublishSkillRequest {
289    /// The contract's `changelog`.
290    pub changelog: String,
291
292    /// The contract's `content`.
293    pub content: String,
294}
295
296impl ContractModel for PublishSkillRequest {
297    const SCHEMA: &'static str = "publishSkillRequest";
298}
299
300/// The POST /v1/skills/generate body. It mirrors the
301/// console's GenerateSkillInput: the client nominates source sessions plus
302/// optional hints, and the server is authoritative on the skill body.
303///
304/// Models the contract's `generateSkillRequest` schema.
305#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
306#[serde(default)]
307pub struct GenerateSkillRequest {
308    /// The contract's `hint`.
309    #[serde(deserialize_with = "super::null_default")]
310    pub hint: GenerateSkillRequestHint,
311
312    /// The contract's `sessionIds`.
313    #[serde(rename = "sessionIds", deserialize_with = "super::null_default")]
314    pub session_ids: Vec<String>,
315}
316
317impl ContractModel for GenerateSkillRequest {
318    const SCHEMA: &'static str = "generateSkillRequest";
319}
320
321/// The optional authoring hints on a generate request.
322///
323/// Part of a request body, so it is constructible: not `non_exhaustive`, and
324/// every field public. The whole point of the type is that a caller fills one
325/// in — a hint nobody outside this crate could set would leave
326/// [`GenerateSkillRequest::hint`] permanently at its default.
327///
328/// Models the inline `hint` object of the contract's `generateSkillRequest`
329/// schema, which the document declares in place rather than as a schema of its
330/// own — so the [`super::coverage`] gate holds these fields through
331/// [`GenerateSkillRequest`] rather than registering them separately.
332#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
333#[serde(default)]
334pub struct GenerateSkillRequestHint {
335    /// The contract's `description`.
336    pub description: String,
337
338    /// The contract's `name`.
339    pub name: String,
340
341    /// The contract's `tags`.
342    #[serde(deserialize_with = "super::null_default")]
343    pub tags: Vec<String>,
344
345    /// The contract's `type`.
346    #[serde(rename = "type")]
347    pub type_: String,
348}
349
350impl SkillsListResponse {
351    /// This listing as one page of the crate's pagination convention.
352    ///
353    /// See [`crate::core::models::SessionListResponse::into_page`]: the skills
354    /// envelope pages the same way, so it walks through the same loop.
355    #[must_use]
356    pub fn into_page(self) -> crate::page::Page<SkillResponse> {
357        crate::page::Page {
358            items: self.items,
359            next_cursor: Some(self.next_cursor),
360        }
361    }
362}