Skip to main content

vissue_control/
rpc.rs

1//! JSON-RPC 2.0 types. Handshake is camelCase; issue payloads are snake_case.
2
3use serde::{Deserialize, Serialize, de::DeserializeOwned};
4use serde_json::{Value, json};
5
6use crate::frame::FrameError;
7use vissue_core::error::Error as CoreError;
8use vissue_core::views::{
9    AgendaRow, ClaimRow, Excerpt, IssueDetail, IssueRow, RelatedHit, SearchHit, TreeNode, WalkHit,
10};
11
12/// One entry in the on-disk change log.
13pub use vissue_core::events::Event;
14
15/// Protocol version accepted by `initialize`.
16pub const PROTOCOL_VERSION: u32 = 1;
17
18/// JSON-RPC parse error (`-32700`).
19pub const PARSE_ERROR: i32 = -32700;
20/// JSON-RPC invalid request (`-32600`).
21pub const INVALID_REQUEST: i32 = -32600;
22/// JSON-RPC method not found (`-32601`).
23pub const METHOD_NOT_FOUND: i32 = -32601;
24/// JSON-RPC invalid params (`-32602`).
25pub const INVALID_PARAMS: i32 = -32602;
26/// JSON-RPC internal error (`-32603`).
27pub const INTERNAL_ERROR: i32 = -32603;
28/// Issue not found (`-32004`).
29pub const NOT_FOUND: i32 = -32004;
30/// Claim conflict (`-32009`).
31pub const CONFLICT: i32 = -32009;
32/// Closed issue or invalid state (`-32010`).
33pub const INVALID_STATE: i32 = -32010;
34/// Blocker cycle (`-32022`).
35pub const CYCLE: i32 = -32022;
36
37/// Catalog rebuilt. Params: [`VaultChanged`].
38pub const NOTIFY_VAULT_CHANGED: &str = "vault/changed";
39/// Shared selection. Params: [`IssueSelected`].
40pub const NOTIFY_ISSUE_SELECTED: &str = "issue/selected";
41/// Owner is exiting. Params: `{}`.
42pub const NOTIFY_SHUTTING_DOWN: &str = "serve/shutting_down";
43
44/// Wire-level failure for a control client or dispatcher.
45#[derive(Debug)]
46pub enum Error {
47    /// Socket or file I/O.
48    Io(std::io::Error),
49    /// JSON encode or decode.
50    Json(serde_json::Error),
51    /// Frame read or write.
52    Frame(FrameError),
53    /// Server JSON-RPC error object.
54    Rpc(JsonRpcError),
55    /// Method or platform the client cannot handle.
56    Unsupported(&'static str),
57}
58
59impl std::fmt::Display for Error {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        match self {
62            Error::Io(err) => write!(f, "{err}"),
63            Error::Json(err) => write!(f, "{err}"),
64            Error::Frame(err) => write!(f, "{err}"),
65            Error::Rpc(err) => write!(f, "{}", err.message),
66            Error::Unsupported(msg) => write!(f, "{msg}"),
67        }
68    }
69}
70
71impl std::error::Error for Error {
72    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
73        match self {
74            Error::Io(err) => Some(err),
75            Error::Json(err) => Some(err),
76            Error::Frame(err) => Some(err),
77            _ => None,
78        }
79    }
80}
81
82impl From<std::io::Error> for Error {
83    fn from(err: std::io::Error) -> Self {
84        Error::Io(err)
85    }
86}
87
88impl From<serde_json::Error> for Error {
89    fn from(err: serde_json::Error) -> Self {
90        Error::Json(err)
91    }
92}
93
94impl From<FrameError> for Error {
95    fn from(err: FrameError) -> Self {
96        Error::Frame(err)
97    }
98}
99
100impl From<JsonRpcError> for Error {
101    fn from(err: JsonRpcError) -> Self {
102        Error::Rpc(err)
103    }
104}
105
106/// JSON-RPC request or notification id.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(untagged)]
109pub enum JsonRpcId {
110    /// Numeric id.
111    Number(i64),
112    /// String id.
113    String(String),
114    /// JSON `null`. A response, never a notification.
115    Null,
116}
117
118/// JSON-RPC 2.0 request or notification envelope.
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120pub struct JsonRpcRequest {
121    /// Always `"2.0"`.
122    pub jsonrpc: String,
123    /// Present on a call; absent on a notification.
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub id: Option<JsonRpcId>,
126    /// Method name.
127    pub method: String,
128    /// Params object, or omitted.
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub params: Option<Value>,
131}
132
133impl JsonRpcRequest {
134    /// Request with `id`.
135    pub fn call(id: JsonRpcId, method: impl Into<String>, params: Value) -> Self {
136        Self {
137            jsonrpc: "2.0".into(),
138            id: Some(id),
139            method: method.into(),
140            params: Some(params),
141        }
142    }
143
144    /// Notification (no `id`).
145    pub fn notification(method: impl Into<String>, params: Value) -> Self {
146        Self {
147            jsonrpc: "2.0".into(),
148            id: None,
149            method: method.into(),
150            params: Some(params),
151        }
152    }
153
154    /// True when `id` is absent.
155    pub fn is_notification(&self) -> bool {
156        self.id.is_none()
157    }
158}
159
160/// JSON-RPC 2.0 response envelope.
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162pub struct JsonRpcResponse {
163    /// Always `"2.0"`.
164    pub jsonrpc: String,
165    /// Request id echoed back. `None` or [`JsonRpcId::Null`] on some errors.
166    #[serde(default, skip_serializing_if = "Option::is_none")]
167    pub id: Option<JsonRpcId>,
168    /// Success body.
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub result: Option<Value>,
171    /// Failure body.
172    #[serde(default, skip_serializing_if = "Option::is_none")]
173    pub error: Option<JsonRpcError>,
174}
175
176impl JsonRpcResponse {
177    /// Success response.
178    pub fn ok(id: Option<JsonRpcId>, result: Value) -> Self {
179        Self {
180            jsonrpc: "2.0".into(),
181            id,
182            result: Some(result),
183            error: None,
184        }
185    }
186
187    /// Error response.
188    pub fn err(id: Option<JsonRpcId>, error: JsonRpcError) -> Self {
189        Self {
190            jsonrpc: "2.0".into(),
191            id,
192            result: None,
193            error: Some(error),
194        }
195    }
196}
197
198/// JSON-RPC error object. Application codes carry `data.code`.
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub struct JsonRpcError {
201    /// JSON-RPC or application numeric code.
202    pub code: i32,
203    /// Human-readable message.
204    pub message: String,
205    /// Optional payload. Application codes put `code` here as a string.
206    #[serde(default, skip_serializing_if = "Option::is_none")]
207    pub data: Option<Value>,
208}
209
210/// `-32700` parse error.
211pub fn parse_error() -> JsonRpcError {
212    JsonRpcError {
213        code: PARSE_ERROR,
214        message: "parse error".into(),
215        data: None,
216    }
217}
218
219/// `-32600` invalid request.
220pub fn invalid_request() -> JsonRpcError {
221    JsonRpcError {
222        code: INVALID_REQUEST,
223        message: "invalid request".into(),
224        data: None,
225    }
226}
227
228/// `-32601` method not found. `data.method` is `method`.
229pub fn method_not_found(method: &str) -> JsonRpcError {
230    JsonRpcError {
231        code: METHOD_NOT_FOUND,
232        message: "method not found".into(),
233        data: Some(json!({ "method": method })),
234    }
235}
236
237/// `-32602` invalid params with `message`.
238pub fn invalid_params(message: impl Into<String>) -> JsonRpcError {
239    JsonRpcError {
240        code: INVALID_PARAMS,
241        message: message.into(),
242        data: None,
243    }
244}
245
246/// `-32603` internal error with `message`.
247pub fn internal_error(message: impl Into<String>) -> JsonRpcError {
248    JsonRpcError {
249        code: INTERNAL_ERROR,
250        message: message.into(),
251        data: None,
252    }
253}
254
255/// Map a typed core error onto the control-plane codes.
256pub fn error_from_core(err: &CoreError) -> JsonRpcError {
257    match err {
258        CoreError::IssueNotFound { id } => JsonRpcError {
259            code: NOT_FOUND,
260            message: err.to_string(),
261            data: Some(json!({ "code": "not_found", "id": id })),
262        },
263        CoreError::DuplicateId { id, paths } => JsonRpcError {
264            code: CONFLICT,
265            message: err.to_string(),
266            data: Some(json!({
267                "code": "duplicate_id",
268                "id": id,
269                "paths": paths,
270            })),
271        },
272        CoreError::ClaimConflict { id, holder, .. } => JsonRpcError {
273            code: CONFLICT,
274            message: err.to_string(),
275            data: Some(json!({ "code": "conflict", "id": id, "holder": holder })),
276        },
277        CoreError::BlockerCycle { blocker, issue } => JsonRpcError {
278            code: CYCLE,
279            message: err.to_string(),
280            data: Some(json!({ "code": "cycle", "id": issue, "block": blocker })),
281        },
282        CoreError::InvalidState { id, state } => JsonRpcError {
283            code: INVALID_STATE,
284            message: err.to_string(),
285            data: Some(json!({ "code": "invalid_state", "id": id, "state": state })),
286        },
287        CoreError::StaleWrite {
288            id,
289            expected_state,
290            actual_state,
291            expected_gen,
292            actual_gen,
293        } => JsonRpcError {
294            code: INVALID_STATE,
295            message: err.to_string(),
296            data: Some(json!({
297                "code": "stale",
298                "id": id,
299                "expected_state": expected_state,
300                "actual_state": actual_state,
301                "expected_gen": expected_gen,
302                "actual_gen": actual_gen,
303            })),
304        },
305        CoreError::TerminalConflict {
306            id,
307            held,
308            attempted,
309        } => JsonRpcError {
310            code: CONFLICT,
311            message: err.to_string(),
312            data: Some(json!({
313                "code": "terminal_conflict",
314                "id": id,
315                "held": held,
316                "attempted": attempted,
317            })),
318        },
319        CoreError::Other(_) => internal_error(err.to_string()),
320    }
321}
322
323/// v1 methods the owner advertises on `initialize`.
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
325pub enum Method {
326    /// Handshake. Not listed in [`V1_CAPABILITIES`].
327    Initialize,
328    /// Process identity, root, prefix, and crate version.
329    IdentityGet,
330    /// Filtered issue rows.
331    IssueList,
332    /// One issue plus revision.
333    IssueGet,
334    /// Frontier: `issue/list` with `ready: true`.
335    IssueReady,
336    /// Substring search over id, title, properties, tags, and body.
337    IssueSearch,
338    /// Live claims.
339    IssueClaims,
340    /// Deadlines and scheduled starts.
341    IssueAgenda,
342    /// Alias of [`Self::IssueGet`].
343    IssueShow,
344    /// Secret-screened body range.
345    IssueExcerpt,
346    /// Children and blockers.
347    IssueTree,
348    /// Bounded neighborhood with evidence.
349    IssueRelated,
350    /// Direct children.
351    IssueChildren,
352    /// Walk up the blocker graph.
353    IssueAncestors,
354    /// Walk down the blocker graph.
355    IssueImpact,
356    /// Everything pointing at the id.
357    IssueBacklinks,
358    /// Shared selection; notifies `issue/selected`.
359    IssueOpen,
360    /// Create an issue.
361    IssueCreate,
362    /// State, priority, block, unblock.
363    IssueUpdate,
364    /// Take the issue.
365    IssueClaim,
366    /// Dated logbook entry.
367    IssueNote,
368    /// Move to another project.
369    IssueRefile,
370    /// Operation added after v1's first draft; see schema/vissue.capnp.
371    IssueAppend,
372    /// Operation added after v1's first draft; see schema/vissue.capnp.
373    IssueReject,
374    /// Operation added after v1's first draft; see schema/vissue.capnp.
375    IssueResolve,
376    /// Operation added after v1's first draft; see schema/vissue.capnp.
377    IssueVote,
378    /// Operation added after v1's first draft; see schema/vissue.capnp.
379    IssueFold,
380    /// Operation added after v1's first draft; see schema/vissue.capnp.
381    IssueNormalize,
382    /// Operation added after v1's first draft; see schema/vissue.capnp.
383    IssueCheck,
384    /// Operation added after v1's first draft; see schema/vissue.capnp.
385    IssueCount,
386    /// Operation added after v1's first draft; see schema/vissue.capnp.
387    IssueCycles,
388    /// Operation added after v1's first draft; see schema/vissue.capnp.
389    IssueDigest,
390    /// Operation added after v1's first draft; see schema/vissue.capnp.
391    IssueExport,
392    /// Operation added after v1's first draft; see schema/vissue.capnp.
393    IssueGraph,
394    /// Operation added after v1's first draft; see schema/vissue.capnp.
395    IssueRoadmap,
396    /// Operation added after v1's first draft; see schema/vissue.capnp.
397    IssueStale,
398    /// Operation added after v1's first draft; see schema/vissue.capnp.
399    IssueHygiene,
400    /// Operation added after v1's first draft; see schema/vissue.capnp.
401    IssueWaitingOn,
402    /// Operation added after v1's first draft; see schema/vissue.capnp.
403    IssueMirror,
404    /// Operation added after v1's first draft; see schema/vissue.capnp.
405    EventsPing,
406    /// Operation added after v1's first draft; see schema/vissue.capnp.
407    EventsWait,
408    /// Project names plus revision.
409    ProjectList,
410    /// Pull of the on-disk event log.
411    EventsSince,
412    /// Current generation and revision.
413    EventsGen,
414}
415
416impl Method {
417    /// Wire method name.
418    pub fn as_str(self) -> &'static str {
419        match self {
420            Self::Initialize => "initialize",
421            Self::IdentityGet => "identity/get",
422            Self::IssueList => "issue/list",
423            Self::IssueGet => "issue/get",
424            Self::IssueReady => "issue/ready",
425            Self::IssueSearch => "issue/search",
426            Self::IssueClaims => "issue/claims",
427            Self::IssueAgenda => "issue/agenda",
428            Self::IssueShow => "issue/show",
429            Self::IssueExcerpt => "issue/excerpt",
430            Self::IssueTree => "issue/tree",
431            Self::IssueRelated => "issue/related",
432            Self::IssueChildren => "issue/children",
433            Self::IssueAncestors => "issue/ancestors",
434            Self::IssueImpact => "issue/impact",
435            Self::IssueBacklinks => "issue/backlinks",
436            Self::IssueOpen => "issue/open",
437            Self::IssueCreate => "issue/create",
438            Self::IssueUpdate => "issue/update",
439            Self::IssueClaim => "issue/claim",
440            Self::IssueNote => "issue/note",
441            Self::IssueRefile => "issue/refile",
442            Self::ProjectList => "project/list",
443            Self::EventsSince => "events/since",
444            Self::EventsGen => "events/gen",
445            Self::IssueAppend => "issue/append",
446            Self::IssueReject => "issue/reject",
447            Self::IssueResolve => "issue/resolve",
448            Self::IssueVote => "issue/vote",
449            Self::IssueFold => "issue/fold",
450            Self::IssueNormalize => "issue/normalize",
451            Self::IssueCheck => "issue/check",
452            Self::IssueCount => "issue/count",
453            Self::IssueCycles => "issue/cycles",
454            Self::IssueDigest => "issue/digest",
455            Self::IssueExport => "issue/export",
456            Self::IssueGraph => "issue/graph",
457            Self::IssueRoadmap => "issue/roadmap",
458            Self::IssueStale => "issue/stale",
459            Self::IssueHygiene => "issue/hygiene",
460            Self::IssueWaitingOn => "issue/waiting_on",
461            Self::IssueMirror => "issue/mirror_check",
462            Self::EventsPing => "events/ping",
463            Self::EventsWait => "events/wait",
464        }
465    }
466
467    /// Parse a v1 wire name.
468    ///
469    /// # Errors
470    ///
471    /// Returns an error when `name` is not a v1 method.
472    pub fn parse(name: &str) -> Result<Self, JsonRpcError> {
473        match name {
474            "initialize" => Ok(Self::Initialize),
475            "identity/get" => Ok(Self::IdentityGet),
476            "issue/list" => Ok(Self::IssueList),
477            "issue/get" => Ok(Self::IssueGet),
478            "issue/ready" => Ok(Self::IssueReady),
479            "issue/search" => Ok(Self::IssueSearch),
480            "issue/claims" => Ok(Self::IssueClaims),
481            "issue/agenda" => Ok(Self::IssueAgenda),
482            "issue/show" => Ok(Self::IssueShow),
483            "issue/excerpt" => Ok(Self::IssueExcerpt),
484            "issue/tree" => Ok(Self::IssueTree),
485            "issue/related" => Ok(Self::IssueRelated),
486            "issue/children" => Ok(Self::IssueChildren),
487            "issue/ancestors" => Ok(Self::IssueAncestors),
488            "issue/impact" => Ok(Self::IssueImpact),
489            "issue/backlinks" => Ok(Self::IssueBacklinks),
490            "issue/open" => Ok(Self::IssueOpen),
491            "issue/create" => Ok(Self::IssueCreate),
492            "issue/update" => Ok(Self::IssueUpdate),
493            "issue/claim" => Ok(Self::IssueClaim),
494            "issue/note" => Ok(Self::IssueNote),
495            "issue/refile" => Ok(Self::IssueRefile),
496            "project/list" => Ok(Self::ProjectList),
497            "events/since" => Ok(Self::EventsSince),
498            "events/gen" => Ok(Self::EventsGen),
499            "issue/append" => Ok(Self::IssueAppend),
500            "issue/reject" => Ok(Self::IssueReject),
501            "issue/resolve" => Ok(Self::IssueResolve),
502            "issue/vote" => Ok(Self::IssueVote),
503            "issue/fold" => Ok(Self::IssueFold),
504            "issue/normalize" => Ok(Self::IssueNormalize),
505            "issue/check" => Ok(Self::IssueCheck),
506            "issue/count" => Ok(Self::IssueCount),
507            "issue/cycles" => Ok(Self::IssueCycles),
508            "issue/digest" => Ok(Self::IssueDigest),
509            "issue/export" => Ok(Self::IssueExport),
510            "issue/graph" => Ok(Self::IssueGraph),
511            "issue/roadmap" => Ok(Self::IssueRoadmap),
512            "issue/stale" => Ok(Self::IssueStale),
513            "issue/hygiene" => Ok(Self::IssueHygiene),
514            "issue/waiting_on" => Ok(Self::IssueWaitingOn),
515            "issue/mirror_check" => Ok(Self::IssueMirror),
516            "events/ping" => Ok(Self::EventsPing),
517            "events/wait" => Ok(Self::EventsWait),
518            other => Err(method_not_found(other)),
519        }
520    }
521}
522
523/// Capability strings returned by `initialize` (v1). `initialize` itself is omitted.
524///
525/// This is the fourth place the method set is written down, after the dispatch table,
526/// the schema and the reference, and it is the one a client reads to decide what it
527/// may call. It fell nineteen methods behind while the other three agreed with each
528/// other, so a client inspecting capabilities would have concluded that append,
529/// vote, fold and every read added beside them did not exist.
530///
531/// `capabilities_match_the_schema` in vissue-serve holds this to the schema now.
532pub const V1_CAPABILITIES: &[&str] = &[
533    "issue/list",
534    "issue/get",
535    "issue/ready",
536    "issue/search",
537    "issue/claims",
538    "issue/agenda",
539    "issue/show",
540    "issue/excerpt",
541    "issue/tree",
542    "issue/related",
543    "issue/children",
544    "issue/ancestors",
545    "issue/impact",
546    "issue/backlinks",
547    "issue/open",
548    "issue/create",
549    "issue/update",
550    "issue/claim",
551    "issue/note",
552    "issue/refile",
553    "issue/append",
554    "issue/reject",
555    "issue/resolve",
556    "issue/vote",
557    "issue/fold",
558    "issue/normalize",
559    "issue/check",
560    "issue/count",
561    "issue/cycles",
562    "issue/digest",
563    "issue/export",
564    "issue/graph",
565    "issue/roadmap",
566    "issue/stale",
567    "issue/hygiene",
568    "issue/waiting_on",
569    "issue/mirror_check",
570    "project/list",
571    "events/since",
572    "events/gen",
573    "events/ping",
574    "events/wait",
575    "identity/get",
576];
577
578/// `initialize` params. camelCase on the wire.
579#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
580#[serde(rename_all = "camelCase")]
581pub struct InitializeParams {
582    /// Must be [`PROTOCOL_VERSION`].
583    pub protocol_version: u32,
584    /// Client name, e.g. `vissue-tui`. Empty when omitted.
585    #[serde(default)]
586    pub client: String,
587    /// Connection identity. Required and non-empty.
588    pub agent: String,
589}
590
591/// `initialize` result. camelCase on the wire.
592#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
593#[serde(rename_all = "camelCase")]
594pub struct InitializeResult {
595    /// Echo of [`PROTOCOL_VERSION`].
596    pub protocol_version: u32,
597    /// Advertised methods. See [`V1_CAPABILITIES`].
598    pub capabilities: Vec<String>,
599    /// Tracker root the owner bound.
600    pub root: String,
601    /// Layout prefix the owner bound.
602    pub prefix: String,
603    /// On-disk generation counter.
604    pub generation: u64,
605    /// Serve-local catalog revision. Starts at 1.
606    pub revision: u64,
607    /// Owner identity.
608    pub identity: String,
609}
610
611/// Parse `initialize` params. Missing/empty `agent` and version != 1 are -32602.
612///
613/// # Errors
614///
615/// Returns an error when `value` is not an object, `protocolVersion` is
616/// missing, not a number, or not 1, or `agent` is missing or empty.
617pub fn parse_initialize_params(value: &Value) -> Result<InitializeParams, JsonRpcError> {
618    let obj = value
619        .as_object()
620        .ok_or_else(|| invalid_params("params must be an object"))?;
621    let version = match obj.get("protocolVersion") {
622        Some(Value::Number(n)) => n
623            .as_u64()
624            .ok_or_else(|| invalid_params("protocolVersion must be a number"))?,
625        Some(_) => return Err(invalid_params("protocolVersion must be a number")),
626        None => return Err(invalid_params("protocolVersion is required")),
627    };
628    if version != u64::from(PROTOCOL_VERSION) {
629        return Err(JsonRpcError {
630            code: INVALID_PARAMS,
631            message: "unsupported protocol version".into(),
632            data: Some(json!({ "supported": PROTOCOL_VERSION })),
633        });
634    }
635    let agent = match obj.get("agent") {
636        Some(Value::String(s)) if !s.trim().is_empty() => s.clone(),
637        _ => return Err(invalid_params("agent is required")),
638    };
639    let client = obj
640        .get("client")
641        .and_then(Value::as_str)
642        .unwrap_or("")
643        .to_string();
644    Ok(InitializeParams {
645        protocol_version: PROTOCOL_VERSION,
646        client,
647        agent,
648    })
649}
650
651/// Filters for `issue/list` and `issue/ready`. snake_case on the wire.
652#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
653pub struct IssueListParams {
654    /// Restrict to this project.
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub project: Option<String>,
657    /// Restrict to this TODO keyword.
658    #[serde(default, skip_serializing_if = "Option::is_none")]
659    pub state: Option<String>,
660    /// When `true`, only the frontier (no open blockers).
661    #[serde(default, skip_serializing_if = "Option::is_none")]
662    pub ready: Option<bool>,
663    /// Case-insensitive substring over id, title, tags, and properties.
664    #[serde(default, skip_serializing_if = "Option::is_none")]
665    pub query: Option<String>,
666    /// Max rows after offset.
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub limit: Option<usize>,
669    /// Skip this many matching rows.
670    #[serde(default, skip_serializing_if = "Option::is_none")]
671    pub offset: Option<usize>,
672    /// When this equals the current revision, the result is unchanged.
673    #[serde(default, skip_serializing_if = "Option::is_none")]
674    pub since_revision: Option<u64>,
675}
676
677/// Page of issue rows, or an unchanged marker.
678#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
679pub struct IssueListResult {
680    /// Matching rows. Empty when [`Self::unchanged`].
681    #[serde(default)]
682    pub issues: Vec<IssueRow>,
683    /// Issues in the selected project (or whole vault) before other filters.
684    #[serde(default)]
685    pub total: u64,
686    /// Rows matching state, ready, and query, before limit and offset.
687    #[serde(default)]
688    pub matched: u64,
689    /// Current serve revision.
690    pub revision: u64,
691    /// Current on-disk generation.
692    #[serde(default)]
693    pub generation: u64,
694    /// `since_revision` matched; `issues` is empty.
695    #[serde(default)]
696    pub unchanged: bool,
697}
698
699/// Single issue id.
700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
701pub struct IdParams {
702    /// Issue id.
703    pub id: String,
704}
705
706/// One issue plus the serve revision.
707#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708pub struct IssueGetResult {
709    /// Flattened detail fields on the wire.
710    #[serde(flatten)]
711    pub issue: IssueDetail,
712    /// Current serve revision.
713    pub revision: u64,
714}
715
716/// `issue/search` params. Default `limit` is 20.
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718pub struct SearchParams {
719    /// Substring over id, title, properties, tags, and body.
720    pub query: String,
721    /// Max hits.
722    #[serde(default, skip_serializing_if = "Option::is_none")]
723    pub limit: Option<usize>,
724}
725
726/// `issue/claims` params.
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
728pub struct ClaimsParams {
729    /// Restrict to this holder.
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub holder: Option<String>,
732    /// Restrict to this project.
733    #[serde(default, skip_serializing_if = "Option::is_none")]
734    pub project: Option<String>,
735}
736
737/// `issue/agenda` params. Default `days` is 14.
738#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
739pub struct AgendaParams {
740    /// Horizon in days.
741    #[serde(default, skip_serializing_if = "Option::is_none")]
742    pub days: Option<i64>,
743    /// Restrict to this project.
744    #[serde(default, skip_serializing_if = "Option::is_none")]
745    pub project: Option<String>,
746}
747
748/// `issue/tree` params. `format` is `nodes`, `ascii`, or `dot`.
749#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750pub struct TreeParams {
751    /// Root issue id.
752    pub id: String,
753    /// `nodes` (default), `ascii`, or `dot`.
754    #[serde(default, skip_serializing_if = "Option::is_none")]
755    pub format: Option<String>,
756}
757
758/// `issue/tree` result: a node graph or rendered text.
759#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
760#[serde(untagged)]
761pub enum TreeResult {
762    /// Structured tree (`format` omitted or `nodes`).
763    Nodes(TreeNode),
764    /// Rendered `ascii` or `dot`.
765    Text {
766        /// Graph text.
767        text: String,
768    },
769}
770
771/// `issue/related` params. Default `depth` is 2 and `limit` is 20.
772#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
773pub struct RelatedParams {
774    /// Center issue id.
775    pub id: String,
776    /// Graph walk depth.
777    #[serde(default, skip_serializing_if = "Option::is_none")]
778    pub depth: Option<usize>,
779    /// Max hits.
780    #[serde(default, skip_serializing_if = "Option::is_none")]
781    pub limit: Option<usize>,
782}
783
784/// Params for children, ancestors, impact, and backlinks.
785#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
786pub struct WalkParams {
787    /// Start issue id.
788    pub id: String,
789    /// Walk depth. Omitted means the method default.
790    #[serde(default, skip_serializing_if = "Option::is_none")]
791    pub depth: Option<usize>,
792}
793
794/// `project/list` result.
795#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
796pub struct ProjectListResult {
797    /// Project names under the prefix.
798    pub projects: Vec<String>,
799    /// Current serve revision.
800    pub revision: u64,
801}
802
803/// `events/since` params.
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
805pub struct EventsSinceParams {
806    /// Return events with sequence greater than this.
807    pub since: u64,
808    /// Max events.
809    #[serde(default, skip_serializing_if = "Option::is_none")]
810    pub limit: Option<usize>,
811}
812
813/// Pull of the on-disk event log.
814#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct EventsSinceResult {
816    /// Events after `since`.
817    pub events: Vec<Event>,
818    /// Current generation after the pull.
819    pub generation: u64,
820}
821
822/// `events/gen` result.
823#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
824pub struct EventsGenResult {
825    /// On-disk generation counter.
826    pub generation: u64,
827    /// Serve-local catalog revision.
828    pub revision: u64,
829}
830
831/// `identity/get` result.
832#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
833pub struct IdentityResult {
834    /// Connection or process identity.
835    pub identity: String,
836    /// Tracker root.
837    pub root: String,
838    /// Layout prefix.
839    pub prefix: String,
840    /// Crate version string.
841    pub version: String,
842}
843
844/// `issue/create` params. Fields match the CLI create verb.
845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
846pub struct CreateParams {
847    /// Target project.
848    pub project: String,
849    /// Heading title.
850    pub title: String,
851    /// Override the connection agent.
852    #[serde(default, skip_serializing_if = "Option::is_none")]
853    pub agent: Option<String>,
854    /// Priority letter.
855    #[serde(default, skip_serializing_if = "Option::is_none")]
856    pub priority: Option<char>,
857    /// `:TYPE:` property.
858    #[serde(default, skip_serializing_if = "Option::is_none")]
859    pub issue_type: Option<String>,
860    /// Org deadline stamp.
861    #[serde(default, skip_serializing_if = "Option::is_none")]
862    pub deadline: Option<String>,
863    /// Org scheduled stamp.
864    #[serde(default, skip_serializing_if = "Option::is_none")]
865    pub scheduled: Option<String>,
866    /// Space-separated tags.
867    #[serde(default, skip_serializing_if = "Option::is_none")]
868    pub tags: Option<String>,
869    /// Parent issue id.
870    #[serde(default, skip_serializing_if = "Option::is_none")]
871    pub parent: Option<String>,
872    /// Body prose written under the properties drawer.
873    #[serde(default, skip_serializing_if = "Option::is_none")]
874    pub body: Option<String>,
875}
876
877/// `issue/update` params.
878#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
879pub struct UpdateParams {
880    /// Issue id.
881    pub id: String,
882    /// New TODO keyword.
883    #[serde(default, skip_serializing_if = "Option::is_none")]
884    pub state: Option<String>,
885    /// New priority letter.
886    #[serde(default, skip_serializing_if = "Option::is_none")]
887    pub priority: Option<String>,
888    /// Add this blocker.
889    #[serde(default, skip_serializing_if = "Option::is_none")]
890    pub block: Option<String>,
891    /// Remove this blocker.
892    #[serde(default, skip_serializing_if = "Option::is_none")]
893    pub unblock: Option<String>,
894    /// Refuse unless the heading is still this state.
895    #[serde(default, skip_serializing_if = "Option::is_none")]
896    pub if_state: Option<String>,
897    /// Refuse unless the corpus generation is still this value.
898    #[serde(default, skip_serializing_if = "Option::is_none")]
899    pub if_gen: Option<u64>,
900    /// Override the connection agent.
901    #[serde(default, skip_serializing_if = "Option::is_none")]
902    pub agent: Option<String>,
903}
904
905/// `issue/claim` params. `force` defaults to false.
906#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
907pub struct ClaimParams {
908    /// Issue id.
909    pub id: String,
910    /// Take over an existing claim.
911    #[serde(default)]
912    pub force: bool,
913    /// Override the connection agent.
914    #[serde(default, skip_serializing_if = "Option::is_none")]
915    pub agent: Option<String>,
916}
917
918/// `issue/vote` params. `choice` absent reads the tally without casting.
919#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
920pub struct VoteParams {
921    /// Issue id.
922    pub id: String,
923    /// What to vote for, one line. Absent reads the tally.
924    #[serde(default, skip_serializing_if = "Option::is_none")]
925    pub choice: Option<String>,
926    /// Override the connection agent.
927    #[serde(default, skip_serializing_if = "Option::is_none")]
928    pub agent: Option<String>,
929}
930
931/// Params for the reads that take an optional project filter: `issue/export`,
932/// `issue/graph`, `issue/roadmap`, `issue/cycles`, `issue/check`.
933#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
934pub struct ProjectFilterParams {
935    /// Only this project; every project when absent.
936    #[serde(default, skip_serializing_if = "Option::is_none")]
937    pub project: Option<String>,
938}
939
940/// `issue/count` params.
941#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
942pub struct CountParams {
943    /// Only this project.
944    #[serde(default, skip_serializing_if = "Option::is_none")]
945    pub project: Option<String>,
946    /// Only this state.
947    #[serde(default, skip_serializing_if = "Option::is_none")]
948    pub state: Option<String>,
949    /// Only issues with no live blocker.
950    #[serde(default)]
951    pub ready_only: bool,
952}
953
954/// `issue/stale` params.
955#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
956pub struct StaleParams {
957    /// How many days without a change counts as stale.
958    pub days: i64,
959    /// Only this project.
960    #[serde(default, skip_serializing_if = "Option::is_none")]
961    pub project: Option<String>,
962}
963
964/// `issue/hygiene` params.
965#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
966pub struct HygieneParams {
967    /// Days before a claim counts as stalled; the default when absent.
968    #[serde(default, skip_serializing_if = "Option::is_none")]
969    pub stale_days: Option<i64>,
970}
971
972/// `events/ping` params.
973#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
974pub struct PingParams {
975    /// Which detail to report; the summary when absent.
976    #[serde(default, skip_serializing_if = "Option::is_none")]
977    pub detail: Option<String>,
978}
979
980/// `events/wait` params. Waits for the corpus generation to pass `last`, or for
981/// `id` to reach a terminal state when one is given.
982#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
983pub struct WaitParams {
984    /// Generation to wait past.
985    #[serde(default)]
986    pub last: u64,
987    /// Wait for this issue to reach a terminal state instead.
988    #[serde(default, skip_serializing_if = "Option::is_none")]
989    pub id: Option<String>,
990    /// Poll interval in milliseconds.
991    #[serde(default, skip_serializing_if = "Option::is_none")]
992    pub poll_ms: Option<u64>,
993    /// Give up after this many milliseconds.
994    #[serde(default, skip_serializing_if = "Option::is_none")]
995    pub timeout_ms: Option<u64>,
996}
997
998/// `issue/mirror` params. Checks a mirror file's stamp against the tracker.
999#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1000pub struct MirrorCheckParams {
1001    /// Mirror file whose SYNC stamp is compared against the corpus.
1002    pub path: String,
1003    /// Only these projects; the stamp's own list when empty, since the file records
1004    /// what it covered.
1005    #[serde(default)]
1006    pub projects: Vec<String>,
1007}
1008
1009/// `issue/mirror` reply.
1010#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1011pub struct MirrorCheckResult {
1012    /// Whether the stamp still matches the tracker.
1013    pub fresh: bool,
1014    /// The verdict, naming which projects moved when stale.
1015    pub report: String,
1016}
1017
1018/// `issue/digest` params.
1019#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1020pub struct DigestParams {
1021    /// Only these projects; every project when empty.
1022    #[serde(default)]
1023    pub projects: Vec<String>,
1024}
1025
1026/// A report-shaped reply: the same text the subcommand prints.
1027///
1028/// Shared by the reads that produce prose rather than structure. Giving each its own
1029/// type would be a contract per report to keep in step with the text, and the text is
1030/// the part anyone reads.
1031#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1032pub struct ReportResult {
1033    /// The report, as the subcommand would print it.
1034    pub report: String,
1035}
1036
1037/// `issue/check` reply. The counts travel beside the text because the subcommand
1038/// exits non-zero on an error count and a client needs the same signal.
1039#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1040pub struct CheckResult {
1041    /// Findings, ending in a summary line.
1042    pub report: String,
1043    /// Count of `[err]` findings.
1044    pub errors: usize,
1045    /// Count of `[warn]` findings.
1046    pub warnings: usize,
1047}
1048
1049/// One project's digest inside [`DigestResult`].
1050#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1051pub struct ProjectDigestResult {
1052    /// Project directory name.
1053    pub project: String,
1054    /// Hash over that project's export.
1055    pub digest: String,
1056    /// Issue count in that project.
1057    pub issues: usize,
1058}
1059
1060/// `issue/digest` reply.
1061#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1062pub struct DigestResult {
1063    /// Hash over the per-project digests.
1064    pub combined: String,
1065    /// Sum of the per-project issue counts.
1066    pub issues: usize,
1067    /// Event-log generation the digest was taken at, so two digests can be placed in
1068    /// time relative to each other.
1069    pub generation: u64,
1070    /// Per project, sorted by name.
1071    pub projects: Vec<ProjectDigestResult>,
1072}
1073
1074/// `events/wait` reply. Waiting on a generation fills `generation` only; waiting on
1075/// an issue fills `state` and says whether it gave up.
1076#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1077pub struct WaitResult {
1078    /// Generation at the moment the wait returned.
1079    pub generation: u64,
1080    /// Heading state, when the wait was for an issue.
1081    #[serde(default, skip_serializing_if = "Option::is_none")]
1082    pub state: Option<String>,
1083    /// True when the timeout expired before a terminal state.
1084    #[serde(default)]
1085    pub timed_out: bool,
1086}
1087
1088/// `issue/append` params.
1089#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1090pub struct AppendParams {
1091    /// Issue id.
1092    pub id: String,
1093    /// Report text, written under the heading with a dated stamp.
1094    pub text: String,
1095    /// Override the connection agent, which the stamp records.
1096    #[serde(default, skip_serializing_if = "Option::is_none")]
1097    pub agent: Option<String>,
1098}
1099
1100/// `issue/resolve` params.
1101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1102pub struct ResolveParams {
1103    /// Issue id whose sibling terminal is being picked.
1104    pub id: String,
1105    /// Terminal state to settle on.
1106    pub state: String,
1107}
1108
1109/// `issue/reject` params. Either `to` or `project` has to say where the
1110/// successor goes.
1111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1112pub struct RejectParams {
1113    /// Issue being cancelled.
1114    pub id: String,
1115    /// Existing issue to point at instead of creating a successor.
1116    #[serde(default, skip_serializing_if = "Option::is_none")]
1117    pub to: Option<String>,
1118    /// Project to create the successor in.
1119    #[serde(default, skip_serializing_if = "Option::is_none")]
1120    pub project: Option<String>,
1121    /// Successor title.
1122    #[serde(default, skip_serializing_if = "Option::is_none")]
1123    pub title: Option<String>,
1124    /// Why the original was rejected.
1125    #[serde(default, skip_serializing_if = "Option::is_none")]
1126    pub reason: Option<String>,
1127}
1128
1129/// `issue/fold` params.
1130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1131pub struct FoldParams {
1132    /// Inbox file whose unstamped `* TODO` headings become issues.
1133    pub file: String,
1134    /// Project the new issues land in.
1135    #[serde(default, skip_serializing_if = "Option::is_none")]
1136    pub project: Option<String>,
1137}
1138
1139/// `issue/normalize` params.
1140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1141pub struct NormalizeParams {
1142    /// Only this project; every project when absent.
1143    #[serde(default, skip_serializing_if = "Option::is_none")]
1144    pub project: Option<String>,
1145    /// Report what would change without writing it.
1146    #[serde(default)]
1147    pub dry_run: bool,
1148}
1149
1150/// `issue/note` params.
1151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1152pub struct NoteParams {
1153    /// Issue id.
1154    pub id: String,
1155    /// Logbook text.
1156    pub text: String,
1157}
1158
1159/// `issue/refile` params.
1160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1161pub struct RefileParams {
1162    /// Issue id.
1163    pub id: String,
1164    /// Destination project.
1165    pub to: String,
1166}
1167
1168/// Mutation result shared by create, update, claim, note, and refile.
1169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1170pub struct MutResult {
1171    /// True when the write succeeded.
1172    pub ok: bool,
1173    /// Same text the CLI would print.
1174    pub report: String,
1175    /// Post-write detail. Null on refile of a vanished source.
1176    #[serde(default)]
1177    pub issue: Option<IssueDetail>,
1178    /// Serve revision after the write.
1179    pub revision: u64,
1180    /// On-disk generation after the write.
1181    pub generation: u64,
1182}
1183
1184/// `vault/changed` params. Broadcast after a catalog rebuild.
1185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1186pub struct VaultChanged {
1187    /// On-disk generation.
1188    pub generation: u64,
1189    /// Serve-local revision.
1190    pub revision: u64,
1191    /// Dirty project names.
1192    #[serde(default)]
1193    pub projects: Vec<String>,
1194    /// Touched issue ids, when known.
1195    #[serde(default, skip_serializing_if = "Option::is_none")]
1196    pub ids: Option<Vec<String>>,
1197}
1198
1199/// `issue/selected` params. Broadcast after `issue/open`.
1200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1201pub struct IssueSelected {
1202    /// Selected issue id.
1203    pub id: String,
1204    /// Project of that issue.
1205    pub project: String,
1206}
1207
1208/// Push notifications. No `id` on the wire.
1209#[derive(Debug, Clone, PartialEq)]
1210pub enum Notification {
1211    /// [`NOTIFY_VAULT_CHANGED`].
1212    VaultChanged(VaultChanged),
1213    /// [`NOTIFY_ISSUE_SELECTED`].
1214    IssueSelected(IssueSelected),
1215    /// [`NOTIFY_SHUTTING_DOWN`].
1216    ServeShuttingDown,
1217    /// Method the client does not know, or params that failed to decode.
1218    Unknown {
1219        /// Wire method name.
1220        method: String,
1221        /// Raw params.
1222        params: Value,
1223    },
1224}
1225
1226impl Notification {
1227    /// Wire method name.
1228    pub fn method(&self) -> &str {
1229        match self {
1230            Self::VaultChanged(_) => NOTIFY_VAULT_CHANGED,
1231            Self::IssueSelected(_) => NOTIFY_ISSUE_SELECTED,
1232            Self::ServeShuttingDown => NOTIFY_SHUTTING_DOWN,
1233            Self::Unknown { method, .. } => method,
1234        }
1235    }
1236
1237    /// Parse a method/params pair. Unknown names stay [`Self::Unknown`].
1238    pub fn parse(method: &str, params: Value) -> Self {
1239        match method {
1240            NOTIFY_VAULT_CHANGED => match serde_json::from_value(params.clone()) {
1241                Ok(body) => Self::VaultChanged(body),
1242                Err(_) => Self::Unknown {
1243                    method: method.into(),
1244                    params,
1245                },
1246            },
1247            NOTIFY_ISSUE_SELECTED => match serde_json::from_value(params.clone()) {
1248                Ok(body) => Self::IssueSelected(body),
1249                Err(_) => Self::Unknown {
1250                    method: method.into(),
1251                    params,
1252                },
1253            },
1254            NOTIFY_SHUTTING_DOWN => Self::ServeShuttingDown,
1255            other => Self::Unknown {
1256                method: other.into(),
1257                params,
1258            },
1259        }
1260    }
1261
1262    /// Params object for the wire.
1263    pub fn to_params(&self) -> Value {
1264        match self {
1265            Self::VaultChanged(body) => serde_json::to_value(body).unwrap_or(Value::Null),
1266            Self::IssueSelected(body) => serde_json::to_value(body).unwrap_or(Value::Null),
1267            Self::ServeShuttingDown => json!({}),
1268            Self::Unknown { params, .. } => params.clone(),
1269        }
1270    }
1271}
1272
1273/// Typed v1 request.
1274#[derive(Debug, Clone, PartialEq)]
1275pub enum Request {
1276    /// Handshake.
1277    Initialize(InitializeParams),
1278    /// Process identity, root, prefix, and crate version.
1279    IdentityGet,
1280    /// Filtered issue rows.
1281    IssueList(IssueListParams),
1282    /// One issue plus revision.
1283    IssueGet(IdParams),
1284    /// Frontier: `issue/list` with `ready: true`.
1285    IssueReady(IssueListParams),
1286    /// Substring search over id, title, properties, tags, and body.
1287    IssueSearch(SearchParams),
1288    /// Live claims.
1289    IssueClaims(ClaimsParams),
1290    /// Deadlines and scheduled starts.
1291    IssueAgenda(AgendaParams),
1292    /// Alias of [`Self::IssueGet`].
1293    IssueShow(IdParams),
1294    /// Secret-screened body range.
1295    IssueExcerpt(IdParams),
1296    /// Children and blockers.
1297    IssueTree(TreeParams),
1298    /// Bounded neighborhood with evidence.
1299    IssueRelated(RelatedParams),
1300    /// Direct children.
1301    IssueChildren(WalkParams),
1302    /// Walk up the blocker graph.
1303    IssueAncestors(WalkParams),
1304    /// Walk down the blocker graph.
1305    IssueImpact(WalkParams),
1306    /// Everything pointing at the id.
1307    IssueBacklinks(WalkParams),
1308    /// Shared selection; notifies `issue/selected`.
1309    IssueOpen(IdParams),
1310    /// Create an issue.
1311    IssueCreate(CreateParams),
1312    /// State, priority, block, unblock.
1313    IssueUpdate(UpdateParams),
1314    /// Take the issue.
1315    IssueClaim(ClaimParams),
1316    /// Dated logbook entry.
1317    IssueNote(NoteParams),
1318    /// Move to another project.
1319    IssueRefile(RefileParams),
1320    /// Dated report under the heading.
1321    IssueAppend(AppendParams),
1322    /// Cancel and point at a successor.
1323    IssueReject(RejectParams),
1324    /// Settle on a sibling terminal state.
1325    IssueResolve(ResolveParams),
1326    /// Cast a ballot, or read the tally.
1327    IssueVote(VoteParams),
1328    /// Inbox headings become issues.
1329    IssueFold(FoldParams),
1330    /// Rewrite onto the property split.
1331    IssueNormalize(NormalizeParams),
1332    /// Validate the corpus.
1333    IssueCheck(ProjectFilterParams),
1334    /// Counts by project, state, readiness.
1335    IssueCount(CountParams),
1336    /// Blocker cycles, if any.
1337    IssueCycles(ProjectFilterParams),
1338    /// Corpus hash, combined and per project.
1339    IssueDigest(DigestParams),
1340    /// The corpus as text.
1341    IssueExport(ProjectFilterParams),
1342    /// One dot document.
1343    IssueGraph(ProjectFilterParams),
1344    /// One roadmap document.
1345    IssueRoadmap(ProjectFilterParams),
1346    /// Issues untouched for a number of days.
1347    IssueStale(StaleParams),
1348    /// Stalled claims plus validation.
1349    IssueHygiene(HygieneParams),
1350    /// What blocks one issue.
1351    IssueWaitingOn(IdParams),
1352    /// The mirror's stamp.
1353    IssueMirror(MirrorCheckParams),
1354    /// Liveness and detail.
1355    EventsPing(PingParams),
1356    /// Block until the generation moves, or an issue is terminal.
1357    EventsWait(WaitParams),
1358    /// Project names plus revision.
1359    ProjectList,
1360    /// Pull of the on-disk event log.
1361    EventsSince(EventsSinceParams),
1362    /// Current generation and revision.
1363    EventsGen,
1364}
1365
1366impl Request {
1367    /// Wire [`Method`] for this request.
1368    pub fn method(&self) -> Method {
1369        match self {
1370            Self::Initialize(_) => Method::Initialize,
1371            Self::IdentityGet => Method::IdentityGet,
1372            Self::IssueList(_) => Method::IssueList,
1373            Self::IssueGet(_) => Method::IssueGet,
1374            Self::IssueReady(_) => Method::IssueReady,
1375            Self::IssueSearch(_) => Method::IssueSearch,
1376            Self::IssueClaims(_) => Method::IssueClaims,
1377            Self::IssueAgenda(_) => Method::IssueAgenda,
1378            Self::IssueShow(_) => Method::IssueShow,
1379            Self::IssueExcerpt(_) => Method::IssueExcerpt,
1380            Self::IssueTree(_) => Method::IssueTree,
1381            Self::IssueRelated(_) => Method::IssueRelated,
1382            Self::IssueChildren(_) => Method::IssueChildren,
1383            Self::IssueAncestors(_) => Method::IssueAncestors,
1384            Self::IssueImpact(_) => Method::IssueImpact,
1385            Self::IssueBacklinks(_) => Method::IssueBacklinks,
1386            Self::IssueOpen(_) => Method::IssueOpen,
1387            Self::IssueCreate(_) => Method::IssueCreate,
1388            Self::IssueUpdate(_) => Method::IssueUpdate,
1389            Self::IssueClaim(_) => Method::IssueClaim,
1390            Self::IssueNote(_) => Method::IssueNote,
1391            Self::IssueRefile(_) => Method::IssueRefile,
1392            Self::IssueAppend(_) => Method::IssueAppend,
1393            Self::IssueReject(_) => Method::IssueReject,
1394            Self::IssueResolve(_) => Method::IssueResolve,
1395            Self::IssueVote(_) => Method::IssueVote,
1396            Self::IssueFold(_) => Method::IssueFold,
1397            Self::IssueNormalize(_) => Method::IssueNormalize,
1398            Self::IssueCheck(_) => Method::IssueCheck,
1399            Self::IssueCount(_) => Method::IssueCount,
1400            Self::IssueCycles(_) => Method::IssueCycles,
1401            Self::IssueDigest(_) => Method::IssueDigest,
1402            Self::IssueExport(_) => Method::IssueExport,
1403            Self::IssueGraph(_) => Method::IssueGraph,
1404            Self::IssueRoadmap(_) => Method::IssueRoadmap,
1405            Self::IssueStale(_) => Method::IssueStale,
1406            Self::IssueHygiene(_) => Method::IssueHygiene,
1407            Self::IssueWaitingOn(_) => Method::IssueWaitingOn,
1408            Self::IssueMirror(_) => Method::IssueMirror,
1409            Self::EventsPing(_) => Method::EventsPing,
1410            Self::EventsWait(_) => Method::EventsWait,
1411            Self::ProjectList => Method::ProjectList,
1412            Self::EventsSince(_) => Method::EventsSince,
1413            Self::EventsGen => Method::EventsGen,
1414        }
1415    }
1416
1417    /// Parse a method/params pair.
1418    ///
1419    /// # Errors
1420    ///
1421    /// Returns an error when `method` is unknown or `params` fail to decode.
1422    pub fn parse(method: &str, params: Option<Value>) -> Result<Self, JsonRpcError> {
1423        let method = Method::parse(method)?;
1424        let params = match params {
1425            None | Some(Value::Null) => Value::Object(Default::default()),
1426            Some(v) => v,
1427        };
1428        match method {
1429            Method::Initialize => Ok(Self::Initialize(parse_initialize_params(&params)?)),
1430            Method::IdentityGet => Ok(Self::IdentityGet),
1431            Method::IssueList => Ok(Self::IssueList(decode_params(params)?)),
1432            Method::IssueGet => Ok(Self::IssueGet(decode_params(params)?)),
1433            Method::IssueReady => Ok(Self::IssueReady(decode_params(params)?)),
1434            Method::IssueSearch => Ok(Self::IssueSearch(decode_params(params)?)),
1435            Method::IssueClaims => Ok(Self::IssueClaims(decode_params(params)?)),
1436            Method::IssueAgenda => Ok(Self::IssueAgenda(decode_params(params)?)),
1437            Method::IssueShow => Ok(Self::IssueShow(decode_params(params)?)),
1438            Method::IssueExcerpt => Ok(Self::IssueExcerpt(decode_params(params)?)),
1439            Method::IssueTree => Ok(Self::IssueTree(decode_params(params)?)),
1440            Method::IssueRelated => Ok(Self::IssueRelated(decode_params(params)?)),
1441            Method::IssueChildren => Ok(Self::IssueChildren(decode_params(params)?)),
1442            Method::IssueAncestors => Ok(Self::IssueAncestors(decode_params(params)?)),
1443            Method::IssueImpact => Ok(Self::IssueImpact(decode_params(params)?)),
1444            Method::IssueBacklinks => Ok(Self::IssueBacklinks(decode_params(params)?)),
1445            Method::IssueOpen => Ok(Self::IssueOpen(decode_params(params)?)),
1446            Method::IssueCreate => Ok(Self::IssueCreate(decode_params(params)?)),
1447            Method::IssueUpdate => Ok(Self::IssueUpdate(decode_params(params)?)),
1448            Method::IssueClaim => Ok(Self::IssueClaim(decode_params(params)?)),
1449            Method::IssueNote => Ok(Self::IssueNote(decode_params(params)?)),
1450            Method::IssueRefile => Ok(Self::IssueRefile(decode_params(params)?)),
1451            Method::ProjectList => Ok(Self::ProjectList),
1452            Method::EventsSince => Ok(Self::EventsSince(decode_params(params)?)),
1453            Method::EventsGen => Ok(Self::EventsGen),
1454            Method::IssueAppend => Ok(Self::IssueAppend(decode_params(params)?)),
1455            Method::IssueReject => Ok(Self::IssueReject(decode_params(params)?)),
1456            Method::IssueResolve => Ok(Self::IssueResolve(decode_params(params)?)),
1457            Method::IssueVote => Ok(Self::IssueVote(decode_params(params)?)),
1458            Method::IssueFold => Ok(Self::IssueFold(decode_params(params)?)),
1459            Method::IssueNormalize => Ok(Self::IssueNormalize(decode_params(params)?)),
1460            Method::IssueCheck => Ok(Self::IssueCheck(decode_params(params)?)),
1461            Method::IssueCount => Ok(Self::IssueCount(decode_params(params)?)),
1462            Method::IssueCycles => Ok(Self::IssueCycles(decode_params(params)?)),
1463            Method::IssueDigest => Ok(Self::IssueDigest(decode_params(params)?)),
1464            Method::IssueExport => Ok(Self::IssueExport(decode_params(params)?)),
1465            Method::IssueGraph => Ok(Self::IssueGraph(decode_params(params)?)),
1466            Method::IssueRoadmap => Ok(Self::IssueRoadmap(decode_params(params)?)),
1467            Method::IssueStale => Ok(Self::IssueStale(decode_params(params)?)),
1468            Method::IssueHygiene => Ok(Self::IssueHygiene(decode_params(params)?)),
1469            Method::IssueWaitingOn => Ok(Self::IssueWaitingOn(decode_params(params)?)),
1470            Method::IssueMirror => Ok(Self::IssueMirror(decode_params(params)?)),
1471            Method::EventsPing => Ok(Self::EventsPing(decode_params(params)?)),
1472            Method::EventsWait => Ok(Self::EventsWait(decode_params(params)?)),
1473        }
1474    }
1475
1476    /// Params object for the wire.
1477    pub fn to_params(&self) -> Value {
1478        match self {
1479            Self::Initialize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1480            Self::IssueAppend(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1481            Self::IssueReject(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1482            Self::IssueResolve(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1483            Self::IssueVote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1484            Self::IssueFold(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1485            Self::IssueNormalize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1486            Self::IssueCheck(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1487            Self::IssueCount(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1488            Self::IssueCycles(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1489            Self::IssueDigest(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1490            Self::IssueExport(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1491            Self::IssueGraph(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1492            Self::IssueRoadmap(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1493            Self::IssueStale(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1494            Self::IssueHygiene(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1495            Self::IssueWaitingOn(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1496            Self::IssueMirror(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1497            Self::EventsPing(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1498            Self::EventsWait(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1499            Self::IdentityGet | Self::ProjectList | Self::EventsGen => json!({}),
1500            Self::IssueList(p) | Self::IssueReady(p) => {
1501                serde_json::to_value(p).unwrap_or(Value::Null)
1502            }
1503            Self::IssueGet(p) | Self::IssueShow(p) | Self::IssueExcerpt(p) | Self::IssueOpen(p) => {
1504                serde_json::to_value(p).unwrap_or(Value::Null)
1505            }
1506            Self::IssueSearch(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1507            Self::IssueClaims(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1508            Self::IssueAgenda(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1509            Self::IssueTree(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1510            Self::IssueRelated(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1511            Self::IssueChildren(p)
1512            | Self::IssueAncestors(p)
1513            | Self::IssueImpact(p)
1514            | Self::IssueBacklinks(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1515            Self::IssueCreate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1516            Self::IssueUpdate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1517            Self::IssueClaim(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1518            Self::IssueNote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1519            Self::IssueRefile(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1520            Self::EventsSince(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1521        }
1522    }
1523}
1524
1525/// Typed v1 result body.
1526#[derive(Debug, Clone)]
1527pub enum Response {
1528    /// Handshake.
1529    Initialize(InitializeResult),
1530    /// Process identity, root, prefix, and crate version.
1531    IdentityGet(IdentityResult),
1532    /// Filtered issue rows.
1533    IssueList(IssueListResult),
1534    /// One issue plus revision.
1535    IssueGet(IssueGetResult),
1536    /// Frontier page.
1537    IssueReady(IssueListResult),
1538    /// Search hits.
1539    IssueSearch(Vec<SearchHit>),
1540    /// Live claims.
1541    IssueClaims(Vec<ClaimRow>),
1542    /// Deadlines and scheduled starts.
1543    IssueAgenda(Vec<AgendaRow>),
1544    /// Alias of [`Self::IssueGet`].
1545    IssueShow(IssueGetResult),
1546    /// Secret-screened body range.
1547    IssueExcerpt(Excerpt),
1548    /// Children and blockers.
1549    IssueTree(TreeResult),
1550    /// Bounded neighborhood with evidence.
1551    IssueRelated(Vec<RelatedHit>),
1552    /// Direct children.
1553    IssueChildren(Vec<WalkHit>),
1554    /// Walk up the blocker graph.
1555    IssueAncestors(Vec<WalkHit>),
1556    /// Walk down the blocker graph.
1557    IssueImpact(Vec<WalkHit>),
1558    /// Everything pointing at the id.
1559    IssueBacklinks(Vec<WalkHit>),
1560    /// Shared selection result.
1561    IssueOpen(IssueGetResult),
1562    /// Create result.
1563    IssueCreate(MutResult),
1564    /// Update result.
1565    IssueUpdate(MutResult),
1566    /// Claim result.
1567    IssueClaim(MutResult),
1568    /// Note result.
1569    IssueNote(MutResult),
1570    /// Refile result.
1571    IssueRefile(MutResult),
1572    /// Dated report under the heading.
1573    IssueAppend(MutResult),
1574    /// Cancel and point at a successor.
1575    IssueReject(MutResult),
1576    /// Settle on a sibling terminal state.
1577    IssueResolve(MutResult),
1578    /// Cast a ballot, or read the tally.
1579    IssueVote(MutResult),
1580    /// Inbox headings become issues.
1581    IssueFold(MutResult),
1582    /// Rewrite onto the property split.
1583    IssueNormalize(MutResult),
1584    /// Validation findings plus counts.
1585    IssueCheck(CheckResult),
1586    /// Counts by project, state, readiness.
1587    IssueCount(ReportResult),
1588    /// Blocker cycles, if any.
1589    IssueCycles(ReportResult),
1590    /// Corpus hash, combined and per project.
1591    IssueDigest(DigestResult),
1592    /// The corpus as text.
1593    IssueExport(ReportResult),
1594    /// One dot document.
1595    IssueGraph(ReportResult),
1596    /// One roadmap document.
1597    IssueRoadmap(ReportResult),
1598    /// Issues untouched for a number of days.
1599    IssueStale(ReportResult),
1600    /// Stalled claims plus validation.
1601    IssueHygiene(ReportResult),
1602    /// What blocks one issue.
1603    IssueWaitingOn(ReportResult),
1604    /// The mirror's stamp.
1605    IssueMirror(MirrorCheckResult),
1606    /// Liveness and detail.
1607    EventsPing(ReportResult),
1608    /// Generation reached, or the state waited for.
1609    EventsWait(WaitResult),
1610    /// Project names plus revision.
1611    ProjectList(ProjectListResult),
1612    /// Pull of the on-disk event log.
1613    EventsSince(EventsSinceResult),
1614    /// Current generation and revision.
1615    EventsGen(EventsGenResult),
1616}
1617
1618impl Response {
1619    /// Serialize the result body (not the envelope).
1620    ///
1621    /// # Errors
1622    ///
1623    /// Returns an error when the body cannot be encoded as JSON.
1624    pub fn to_value(&self) -> Result<Value, serde_json::Error> {
1625        match self {
1626            Self::Initialize(v) => serde_json::to_value(v),
1627            Self::IdentityGet(v) => serde_json::to_value(v),
1628            Self::IssueAppend(v) => serde_json::to_value(v),
1629            Self::IssueReject(v) => serde_json::to_value(v),
1630            Self::IssueResolve(v) => serde_json::to_value(v),
1631            Self::IssueVote(v) => serde_json::to_value(v),
1632            Self::IssueFold(v) => serde_json::to_value(v),
1633            Self::IssueNormalize(v) => serde_json::to_value(v),
1634            Self::IssueCheck(v) => serde_json::to_value(v),
1635            Self::IssueCount(v) => serde_json::to_value(v),
1636            Self::IssueCycles(v) => serde_json::to_value(v),
1637            Self::IssueDigest(v) => serde_json::to_value(v),
1638            Self::IssueExport(v) => serde_json::to_value(v),
1639            Self::IssueGraph(v) => serde_json::to_value(v),
1640            Self::IssueRoadmap(v) => serde_json::to_value(v),
1641            Self::IssueStale(v) => serde_json::to_value(v),
1642            Self::IssueHygiene(v) => serde_json::to_value(v),
1643            Self::IssueWaitingOn(v) => serde_json::to_value(v),
1644            Self::IssueMirror(v) => serde_json::to_value(v),
1645            Self::EventsPing(v) => serde_json::to_value(v),
1646            Self::EventsWait(v) => serde_json::to_value(v),
1647            Self::IssueList(v) | Self::IssueReady(v) => serde_json::to_value(v),
1648            Self::IssueGet(v) | Self::IssueShow(v) | Self::IssueOpen(v) => serde_json::to_value(v),
1649            Self::IssueSearch(v) => serde_json::to_value(v),
1650            Self::IssueClaims(v) => serde_json::to_value(v),
1651            Self::IssueAgenda(v) => serde_json::to_value(v),
1652            Self::IssueExcerpt(v) => serde_json::to_value(v),
1653            Self::IssueTree(v) => serde_json::to_value(v),
1654            Self::IssueRelated(v) => serde_json::to_value(v),
1655            Self::IssueChildren(v)
1656            | Self::IssueAncestors(v)
1657            | Self::IssueImpact(v)
1658            | Self::IssueBacklinks(v) => serde_json::to_value(v),
1659            Self::IssueCreate(v)
1660            | Self::IssueUpdate(v)
1661            | Self::IssueClaim(v)
1662            | Self::IssueNote(v)
1663            | Self::IssueRefile(v) => serde_json::to_value(v),
1664            Self::ProjectList(v) => serde_json::to_value(v),
1665            Self::EventsSince(v) => serde_json::to_value(v),
1666            Self::EventsGen(v) => serde_json::to_value(v),
1667        }
1668    }
1669}
1670
1671fn decode_params<T: DeserializeOwned>(value: Value) -> Result<T, JsonRpcError> {
1672    serde_json::from_value(value).map_err(|e| invalid_params(e.to_string()))
1673}
1674
1675#[cfg(test)]
1676mod tests {
1677    use super::*;
1678    use std::collections::BTreeMap;
1679
1680    #[test]
1681    fn initialize_missing_agent_is_invalid_params() {
1682        let err = parse_initialize_params(&json!({
1683            "protocolVersion": 1,
1684            "client": "vissue-tui"
1685        }))
1686        .unwrap_err();
1687        assert_eq!(err.code, INVALID_PARAMS);
1688        assert_eq!(err.message, "agent is required");
1689
1690        let err = parse_initialize_params(&json!({
1691            "protocolVersion": 1,
1692            "agent": ""
1693        }))
1694        .unwrap_err();
1695        assert_eq!(err.code, INVALID_PARAMS);
1696        assert_eq!(err.message, "agent is required");
1697
1698        let err = Request::parse(
1699            "initialize",
1700            Some(json!({"protocolVersion": 1, "agent": "   "})),
1701        )
1702        .unwrap_err();
1703        assert_eq!(err.code, INVALID_PARAMS);
1704    }
1705
1706    #[test]
1707    fn protocol_version_2_is_rejected() {
1708        let err = parse_initialize_params(&json!({
1709            "protocolVersion": 2,
1710            "agent": "rg@host"
1711        }))
1712        .unwrap_err();
1713        assert_eq!(err.code, INVALID_PARAMS);
1714        assert_eq!(err.message, "unsupported protocol version");
1715        assert_eq!(err.data, Some(json!({"supported": 1})));
1716    }
1717
1718    #[test]
1719    fn initialize_version_1_is_accepted() {
1720        let params = parse_initialize_params(&json!({
1721            "protocolVersion": 1,
1722            "client": "vissue-tui",
1723            "agent": "rg@host"
1724        }))
1725        .unwrap();
1726        assert_eq!(params.protocol_version, 1);
1727        assert_eq!(params.agent, "rg@host");
1728        assert_eq!(params.client, "vissue-tui");
1729    }
1730
1731    #[test]
1732    fn handshake_fields_are_camel_case() {
1733        let params = InitializeParams {
1734            protocol_version: 1,
1735            client: "vissue-tui".into(),
1736            agent: "rg@host".into(),
1737        };
1738        let value = serde_json::to_value(&params).unwrap();
1739        assert_eq!(value["protocolVersion"], 1);
1740        assert!(value.get("protocol_version").is_none());
1741
1742        let result = InitializeResult {
1743            protocol_version: 1,
1744            capabilities: vec!["issue/list".into()],
1745            root: "/tmp/tracker".into(),
1746            prefix: "Software".into(),
1747            generation: 3,
1748            revision: 1,
1749            identity: "rg@host".into(),
1750        };
1751        let value = serde_json::to_value(&result).unwrap();
1752        assert_eq!(value["protocolVersion"], 1);
1753        assert_eq!(value["generation"], 3);
1754    }
1755
1756    #[test]
1757    fn issue_payloads_are_snake_case() {
1758        let params = IssueListParams {
1759            since_revision: Some(41),
1760            ..IssueListParams::default()
1761        };
1762        let value = serde_json::to_value(&params).unwrap();
1763        assert_eq!(value["since_revision"], 41);
1764        assert!(value.get("sinceRevision").is_none());
1765    }
1766
1767    #[test]
1768    fn unknown_method_is_not_found() {
1769        // Deliberately a name no verb will ever take. This test used "issue/fold"
1770        // until fold became a method, and the same trap caught the owner's copy of
1771        // this test on the same day: an example chosen because it sounds plausible is
1772        // an example that will one day be real, and then the test asserts that a
1773        // working method is missing.
1774        const NEVER: &str = "issue/no-such-method";
1775        let err = Method::parse(NEVER).unwrap_err();
1776        assert_eq!(err.code, METHOD_NOT_FOUND);
1777        assert_eq!(err.data, Some(json!({"method": NEVER})));
1778    }
1779
1780    #[test]
1781    fn every_v1_capability_parses() {
1782        for name in V1_CAPABILITIES {
1783            assert!(Method::parse(name).is_ok(), "{name}");
1784        }
1785        assert_eq!(Method::Initialize.as_str(), "initialize");
1786    }
1787
1788    #[test]
1789    fn request_parse_roundtrips_issue_get() {
1790        let req = Request::parse("issue/get", Some(json!({"id": "atlas-1a2b"}))).unwrap();
1791        assert_eq!(req.method(), Method::IssueGet);
1792        assert_eq!(req.to_params()["id"], "atlas-1a2b");
1793    }
1794
1795    #[test]
1796    fn missing_id_on_issue_get_is_invalid_params() {
1797        let err = Request::parse("issue/get", Some(json!({}))).unwrap_err();
1798        assert_eq!(err.code, INVALID_PARAMS);
1799    }
1800
1801    #[test]
1802    fn core_errors_carry_data_code() {
1803        let err = error_from_core(&CoreError::IssueNotFound {
1804            id: "atlas-1a2b".into(),
1805        });
1806        assert_eq!(err.code, NOT_FOUND);
1807        assert_eq!(err.data.unwrap()["code"], "not_found");
1808
1809        let err = error_from_core(&CoreError::ClaimConflict {
1810            id: "atlas-1a2b".into(),
1811            holder: "other".into(),
1812            claimed_at: None,
1813        });
1814        assert_eq!(err.code, CONFLICT);
1815        let data = err.data.unwrap();
1816        assert_eq!(data["code"], "conflict");
1817        assert_eq!(data["holder"], "other");
1818
1819        let err = error_from_core(&CoreError::BlockerCycle {
1820            blocker: "a".into(),
1821            issue: "b".into(),
1822        });
1823        assert_eq!(err.code, CYCLE);
1824        let data = err.data.unwrap();
1825        assert_eq!(data["code"], "cycle");
1826        assert_eq!(data["id"], "b");
1827        assert_eq!(data["block"], "a");
1828
1829        let err = error_from_core(&CoreError::InvalidState {
1830            id: "atlas-4g5h".into(),
1831            state: "DONE".into(),
1832        });
1833        assert_eq!(err.code, INVALID_STATE);
1834        assert_eq!(err.data.unwrap()["code"], "invalid_state");
1835
1836        let err = error_from_core(&CoreError::DuplicateId {
1837            id: "atlas-1a2b".into(),
1838            paths: vec![
1839                std::path::PathBuf::from("/a/issues.org"),
1840                std::path::PathBuf::from("/b/issues.org"),
1841            ],
1842        });
1843        assert_eq!(err.code, CONFLICT);
1844        assert_eq!(err.data.unwrap()["code"], "duplicate_id");
1845    }
1846
1847    #[test]
1848    fn notification_parse_known_methods() {
1849        let n = Notification::parse(
1850            NOTIFY_VAULT_CHANGED,
1851            json!({"generation": 1, "revision": 2, "projects": ["atlas"]}),
1852        );
1853        assert!(matches!(n, Notification::VaultChanged(_)));
1854        assert_eq!(n.method(), NOTIFY_VAULT_CHANGED);
1855
1856        let n = Notification::parse(
1857            NOTIFY_ISSUE_SELECTED,
1858            json!({"id": "atlas-1a2b", "project": "atlas"}),
1859        );
1860        assert!(matches!(n, Notification::IssueSelected(_)));
1861
1862        let n = Notification::parse(NOTIFY_SHUTTING_DOWN, json!({}));
1863        assert!(matches!(n, Notification::ServeShuttingDown));
1864        assert_eq!(n.to_params(), json!({}));
1865    }
1866
1867    #[test]
1868    fn list_unchanged_deserializes_without_rows() {
1869        let page: IssueListResult =
1870            serde_json::from_value(json!({"unchanged": true, "revision": 41})).unwrap();
1871        assert!(page.unchanged);
1872        assert!(page.issues.is_empty());
1873        assert_eq!(page.revision, 41);
1874    }
1875
1876    #[test]
1877    fn response_to_value_serializes_initialize() {
1878        let resp = Response::Initialize(InitializeResult {
1879            protocol_version: 1,
1880            capabilities: V1_CAPABILITIES.iter().map(|s| (*s).to_string()).collect(),
1881            root: "/tmp".into(),
1882            prefix: "Software".into(),
1883            generation: 1,
1884            revision: 1,
1885            identity: "agent".into(),
1886        });
1887        let value = resp.to_value().unwrap();
1888        assert_eq!(value["protocolVersion"], 1);
1889        assert!(
1890            value["capabilities"]
1891                .as_array()
1892                .unwrap()
1893                .contains(&json!("issue/list"))
1894        );
1895    }
1896
1897    #[test]
1898    fn envelope_helpers_roundtrip() {
1899        let req = JsonRpcRequest::call(JsonRpcId::Number(1), "identity/get", json!({}));
1900        let bytes = serde_json::to_vec(&req).unwrap();
1901        let back: JsonRpcRequest = serde_json::from_slice(&bytes).unwrap();
1902        assert_eq!(back.method, "identity/get");
1903        assert!(!back.is_notification());
1904
1905        let note = JsonRpcRequest::notification(NOTIFY_SHUTTING_DOWN, json!({}));
1906        assert!(note.is_notification());
1907
1908        let ok = JsonRpcResponse::ok(Some(JsonRpcId::Number(1)), json!({"ok": true}));
1909        assert_eq!(ok.result.unwrap()["ok"], true);
1910        let err = JsonRpcResponse::err(Some(JsonRpcId::Null), parse_error());
1911        assert_eq!(err.error.unwrap().code, PARSE_ERROR);
1912    }
1913
1914    #[test]
1915    fn mut_and_walk_params_decode() {
1916        let claim = Request::parse(
1917            "issue/claim",
1918            Some(json!({"id": "atlas-1a2b", "force": true})),
1919        )
1920        .unwrap();
1921        match claim {
1922            Request::IssueClaim(p) => {
1923                assert!(p.force);
1924                assert_eq!(p.id, "atlas-1a2b");
1925            }
1926            other => panic!("{other:?}"),
1927        }
1928        let create = Request::parse(
1929            "issue/create",
1930            Some(json!({"project": "atlas", "title": "x"})),
1931        )
1932        .unwrap();
1933        assert_eq!(create.method(), Method::IssueCreate);
1934        assert!(Request::parse("issue/create", Some(json!({"title": "x"}))).is_err());
1935        assert_eq!(
1936            Request::parse("events/gen", None).unwrap().method(),
1937            Method::EventsGen
1938        );
1939        let _ = Request::IssueNote(NoteParams {
1940            id: "a".into(),
1941            text: "n".into(),
1942        })
1943        .to_params();
1944        let _ = Request::IssueRefile(RefileParams {
1945            id: "a".into(),
1946            to: "b".into(),
1947        })
1948        .to_params();
1949        let _ = Request::IssueUpdate(UpdateParams {
1950            id: "a".into(),
1951            state: Some("STARTED".into()),
1952            priority: None,
1953            block: None,
1954            unblock: None,
1955            if_state: None,
1956            if_gen: None,
1957            agent: None,
1958        })
1959        .to_params();
1960        let _ = Request::EventsSince(EventsSinceParams {
1961            since: 0,
1962            limit: Some(10),
1963        })
1964        .to_params();
1965        let _ = Request::IssueTree(TreeParams {
1966            id: "a".into(),
1967            format: Some("ascii".into()),
1968        })
1969        .to_params();
1970        let _ = Request::IssueRelated(RelatedParams {
1971            id: "a".into(),
1972            depth: Some(2),
1973            limit: Some(20),
1974        })
1975        .to_params();
1976        let _ = Request::IssueChildren(WalkParams {
1977            id: "a".into(),
1978            depth: None,
1979        })
1980        .to_params();
1981        let _ = Request::IssueSearch(SearchParams {
1982            query: "q".into(),
1983            limit: None,
1984        })
1985        .to_params();
1986        let _ = Request::IssueClaims(ClaimsParams::default()).to_params();
1987        let _ = Request::IssueAgenda(AgendaParams::default()).to_params();
1988        let _ = Request::IdentityGet.to_params();
1989    }
1990
1991    #[test]
1992    fn response_variants_serialize() {
1993        let detail = IssueDetail {
1994            id: "atlas-1a2b".into(),
1995            project: "atlas".into(),
1996            title: "t".into(),
1997            state: "TODO".into(),
1998            priority: "B".into(),
1999            properties: BTreeMap::new(),
2000            org_tags: vec![],
2001            tags: vec![],
2002            blocked_by: vec![],
2003            parent: None,
2004            claimed_by: None,
2005            claimed_at: None,
2006            file: "issues.org:1-2".into(),
2007            line_start: 1,
2008            line_end: 2,
2009            body: "what the issue asks for".into(),
2010            logbook: vec![],
2011        };
2012        let get = IssueGetResult {
2013            issue: detail.clone(),
2014            revision: 1,
2015        };
2016        assert!(Response::IssueGet(get.clone()).to_value().unwrap()["id"] == "atlas-1a2b");
2017        assert!(Response::IssueShow(get.clone()).to_value().is_ok());
2018        assert!(Response::IssueOpen(get).to_value().is_ok());
2019        assert!(
2020            Response::IssueExcerpt(Excerpt {
2021                id: "atlas-1a2b".into(),
2022                file: "issues.org".into(),
2023                line_start: 1,
2024                line_end: 2,
2025                text: "body".into(),
2026                suppressed: false,
2027            })
2028            .to_value()
2029            .is_ok()
2030        );
2031        assert!(Response::IssueSearch(vec![]).to_value().unwrap().is_array());
2032        assert!(Response::IssueClaims(vec![]).to_value().unwrap().is_array());
2033        assert!(Response::IssueAgenda(vec![]).to_value().unwrap().is_array());
2034        assert!(
2035            Response::IssueRelated(vec![])
2036                .to_value()
2037                .unwrap()
2038                .is_array()
2039        );
2040        assert!(
2041            Response::IssueChildren(vec![])
2042                .to_value()
2043                .unwrap()
2044                .is_array()
2045        );
2046        assert!(
2047            Response::IssueAncestors(vec![])
2048                .to_value()
2049                .unwrap()
2050                .is_array()
2051        );
2052        assert!(Response::IssueImpact(vec![]).to_value().unwrap().is_array());
2053        assert!(
2054            Response::IssueBacklinks(vec![])
2055                .to_value()
2056                .unwrap()
2057                .is_array()
2058        );
2059        assert!(
2060            Response::ProjectList(ProjectListResult {
2061                projects: vec!["atlas".into()],
2062                revision: 1,
2063            })
2064            .to_value()
2065            .is_ok()
2066        );
2067        assert!(
2068            Response::EventsGen(EventsGenResult {
2069                generation: 1,
2070                revision: 1,
2071            })
2072            .to_value()
2073            .is_ok()
2074        );
2075        assert!(
2076            Response::EventsSince(EventsSinceResult {
2077                events: vec![],
2078                generation: 1,
2079            })
2080            .to_value()
2081            .is_ok()
2082        );
2083        assert!(
2084            Response::IdentityGet(IdentityResult {
2085                identity: "a".into(),
2086                root: "/".into(),
2087                prefix: "Software".into(),
2088                version: "0.2.0".into(),
2089            })
2090            .to_value()
2091            .is_ok()
2092        );
2093        let mut_ok = MutResult {
2094            ok: true,
2095            report: "ok".into(),
2096            issue: Some(detail),
2097            revision: 2,
2098            generation: 3,
2099        };
2100        assert!(Response::IssueClaim(mut_ok.clone()).to_value().is_ok());
2101        assert!(Response::IssueCreate(mut_ok.clone()).to_value().is_ok());
2102        assert!(Response::IssueUpdate(mut_ok.clone()).to_value().is_ok());
2103        assert!(Response::IssueNote(mut_ok.clone()).to_value().is_ok());
2104        assert!(Response::IssueRefile(mut_ok).to_value().is_ok());
2105        assert!(
2106            Response::IssueTree(TreeResult::Text { text: "* a".into() })
2107                .to_value()
2108                .is_ok()
2109        );
2110        assert!(
2111            Response::IssueList(IssueListResult {
2112                revision: 1,
2113                ..IssueListResult::default()
2114            })
2115            .to_value()
2116            .is_ok()
2117        );
2118        assert!(
2119            Response::IssueReady(IssueListResult {
2120                revision: 1,
2121                ..IssueListResult::default()
2122            })
2123            .to_value()
2124            .is_ok()
2125        );
2126    }
2127
2128    #[test]
2129    fn parse_every_method_with_minimal_params() {
2130        let id = json!({"id": "atlas-1a2b"});
2131        for (method, params) in [
2132            ("identity/get", json!({})),
2133            ("issue/list", json!({})),
2134            ("issue/get", id.clone()),
2135            ("issue/ready", json!({})),
2136            ("issue/search", json!({"query": "q"})),
2137            ("issue/claims", json!({})),
2138            ("issue/agenda", json!({})),
2139            ("issue/show", id.clone()),
2140            ("issue/excerpt", id.clone()),
2141            ("issue/tree", id.clone()),
2142            ("issue/related", id.clone()),
2143            ("issue/children", id.clone()),
2144            ("issue/ancestors", id.clone()),
2145            ("issue/impact", id.clone()),
2146            ("issue/backlinks", id.clone()),
2147            ("issue/open", id.clone()),
2148            ("issue/create", json!({"project": "atlas", "title": "t"})),
2149            ("issue/update", id.clone()),
2150            ("issue/claim", id.clone()),
2151            ("issue/note", json!({"id": "atlas-1a2b", "text": "n"})),
2152            ("issue/refile", json!({"id": "atlas-1a2b", "to": "beacon"})),
2153            ("project/list", json!({})),
2154            ("events/since", json!({"since": 0})),
2155            ("events/gen", json!({})),
2156        ] {
2157            let req = Request::parse(method, Some(params)).expect(method);
2158            assert_eq!(req.method().as_str(), method);
2159            let _ = req.to_params();
2160        }
2161    }
2162
2163    #[test]
2164    fn helper_errors_have_stable_codes() {
2165        assert_eq!(invalid_request().code, INVALID_REQUEST);
2166        assert_eq!(internal_error("x").code, INTERNAL_ERROR);
2167        assert_eq!(parse_error().code, PARSE_ERROR);
2168        let err = Error::Rpc(invalid_params("agent is required"));
2169        assert_eq!(err.to_string(), "agent is required");
2170        let _ = Error::Unsupported("unix only");
2171        let _ = Notification::parse("vault/changed", json!(null));
2172        let _ = Notification::parse("issue/selected", json!(null));
2173        let _ = Notification::parse("other/x", json!({"a": 1}));
2174        let n = Notification::Unknown {
2175            method: "x".into(),
2176            params: json!({"a": 1}),
2177        };
2178        assert_eq!(n.to_params()["a"], 1);
2179        assert_eq!(n.method(), "x");
2180    }
2181    /// Every method has a typed request form, and it round-trips.
2182    ///
2183    /// Nineteen methods reached the wire with no typed form for a while, and the
2184    /// typed helpers answered "send it untyped" per method. That was honest and it
2185    /// was a hole: a client wanting typed access to `issue/check` could not have it,
2186    /// and the two enums drifted from the method list by exactly the amount nobody
2187    /// was checking.
2188    ///
2189    /// Driven from `V1_CAPABILITIES`, so a method added to the wire without a typed
2190    /// form fails here rather than being discovered by whoever wanted it.
2191    #[test]
2192    fn every_advertised_method_has_a_typed_request() {
2193        for name in V1_CAPABILITIES {
2194            let method = Method::parse(name).unwrap_or_else(|_| panic!("{name} does not parse"));
2195            assert_eq!(
2196                method.as_str(),
2197                *name,
2198                "{name} does not round-trip as a method"
2199            );
2200
2201            // Empty params: what matters here is that a typed form exists and that
2202            // its required fields are the reason a decode fails, not the absence of
2203            // any form at all.
2204            let parsed = Request::parse(name, Some(json!({})));
2205            if let Ok(req) = parsed {
2206                assert_eq!(
2207                    req.method().as_str(),
2208                    *name,
2209                    "{name} parsed into a request that reports a different method"
2210                );
2211                // And the params it holds serialize back to an object.
2212                assert!(
2213                    req.to_params().is_object(),
2214                    "{name} does not serialize its params to an object"
2215                );
2216            }
2217        }
2218    }
2219
2220    /// And every typed response encodes.
2221    #[test]
2222    fn the_new_typed_responses_encode() {
2223        let cases = vec![
2224            Response::IssueCheck(CheckResult {
2225                report: "ok".into(),
2226                errors: 0,
2227                warnings: 2,
2228            }),
2229            Response::IssueCount(ReportResult {
2230                report: "3 issues".into(),
2231            }),
2232            Response::IssueDigest(DigestResult {
2233                combined: "abcd".into(),
2234                issues: 3,
2235                generation: 4,
2236                projects: vec![ProjectDigestResult {
2237                    project: "atlas".into(),
2238                    digest: "beef".into(),
2239                    issues: 3,
2240                }],
2241            }),
2242            Response::EventsWait(WaitResult {
2243                generation: 7,
2244                state: Some("DONE".into()),
2245                timed_out: false,
2246            }),
2247        ];
2248        for case in cases {
2249            let value = case.to_value().expect("encode");
2250            assert!(value.is_object(), "{value} is not an object");
2251        }
2252
2253        // The check counts survive the trip, since a client acts on them.
2254        let encoded = Response::IssueCheck(CheckResult {
2255            report: "two warnings".into(),
2256            errors: 0,
2257            warnings: 2,
2258        })
2259        .to_value()
2260        .unwrap();
2261        assert_eq!(encoded["warnings"], 2);
2262        assert_eq!(encoded["errors"], 0);
2263    }
2264}