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::{de::DeserializeOwned, Deserialize, Serialize};
4use serde_json::{json, Value};
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
12pub use vissue_core::events::Event;
13
14/// Protocol version accepted by `initialize`.
15pub const PROTOCOL_VERSION: u32 = 1;
16
17pub const PARSE_ERROR: i32 = -32700;
18pub const INVALID_REQUEST: i32 = -32600;
19pub const METHOD_NOT_FOUND: i32 = -32601;
20pub const INVALID_PARAMS: i32 = -32602;
21pub const INTERNAL_ERROR: i32 = -32603;
22pub const NOT_FOUND: i32 = -32004;
23pub const CONFLICT: i32 = -32009;
24pub const INVALID_STATE: i32 = -32010;
25pub const CYCLE: i32 = -32022;
26
27pub const NOTIFY_VAULT_CHANGED: &str = "vault/changed";
28pub const NOTIFY_ISSUE_SELECTED: &str = "issue/selected";
29pub const NOTIFY_SHUTTING_DOWN: &str = "serve/shutting_down";
30
31/// Wire-level failure for a control client or dispatcher.
32#[derive(Debug)]
33pub enum Error {
34    Io(std::io::Error),
35    Json(serde_json::Error),
36    Frame(FrameError),
37    Rpc(JsonRpcError),
38    Unsupported(&'static str),
39}
40
41impl std::fmt::Display for Error {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            Error::Io(err) => write!(f, "{err}"),
45            Error::Json(err) => write!(f, "{err}"),
46            Error::Frame(err) => write!(f, "{err}"),
47            Error::Rpc(err) => write!(f, "{}", err.message),
48            Error::Unsupported(msg) => write!(f, "{msg}"),
49        }
50    }
51}
52
53impl std::error::Error for Error {
54    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
55        match self {
56            Error::Io(err) => Some(err),
57            Error::Json(err) => Some(err),
58            Error::Frame(err) => Some(err),
59            _ => None,
60        }
61    }
62}
63
64impl From<std::io::Error> for Error {
65    fn from(err: std::io::Error) -> Self {
66        Error::Io(err)
67    }
68}
69
70impl From<serde_json::Error> for Error {
71    fn from(err: serde_json::Error) -> Self {
72        Error::Json(err)
73    }
74}
75
76impl From<FrameError> for Error {
77    fn from(err: FrameError) -> Self {
78        Error::Frame(err)
79    }
80}
81
82impl From<JsonRpcError> for Error {
83    fn from(err: JsonRpcError) -> Self {
84        Error::Rpc(err)
85    }
86}
87
88/// JSON-RPC request or notification id.
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(untagged)]
91pub enum JsonRpcId {
92    Number(i64),
93    String(String),
94    Null,
95}
96
97/// JSON-RPC 2.0 request or notification envelope.
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99pub struct JsonRpcRequest {
100    pub jsonrpc: String,
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub id: Option<JsonRpcId>,
103    pub method: String,
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub params: Option<Value>,
106}
107
108impl JsonRpcRequest {
109    pub fn call(id: JsonRpcId, method: impl Into<String>, params: Value) -> Self {
110        Self {
111            jsonrpc: "2.0".into(),
112            id: Some(id),
113            method: method.into(),
114            params: Some(params),
115        }
116    }
117
118    pub fn notification(method: impl Into<String>, params: Value) -> Self {
119        Self {
120            jsonrpc: "2.0".into(),
121            id: None,
122            method: method.into(),
123            params: Some(params),
124        }
125    }
126
127    pub fn is_notification(&self) -> bool {
128        self.id.is_none()
129    }
130}
131
132/// JSON-RPC 2.0 response envelope.
133#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134pub struct JsonRpcResponse {
135    pub jsonrpc: String,
136    #[serde(default, skip_serializing_if = "Option::is_none")]
137    pub id: Option<JsonRpcId>,
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub result: Option<Value>,
140    #[serde(default, skip_serializing_if = "Option::is_none")]
141    pub error: Option<JsonRpcError>,
142}
143
144impl JsonRpcResponse {
145    pub fn ok(id: Option<JsonRpcId>, result: Value) -> Self {
146        Self {
147            jsonrpc: "2.0".into(),
148            id,
149            result: Some(result),
150            error: None,
151        }
152    }
153
154    pub fn err(id: Option<JsonRpcId>, error: JsonRpcError) -> Self {
155        Self {
156            jsonrpc: "2.0".into(),
157            id,
158            result: None,
159            error: Some(error),
160        }
161    }
162}
163
164/// JSON-RPC error object. Application codes carry `data.code`.
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166pub struct JsonRpcError {
167    pub code: i32,
168    pub message: String,
169    #[serde(default, skip_serializing_if = "Option::is_none")]
170    pub data: Option<Value>,
171}
172
173pub fn parse_error() -> JsonRpcError {
174    JsonRpcError {
175        code: PARSE_ERROR,
176        message: "parse error".into(),
177        data: None,
178    }
179}
180
181pub fn invalid_request() -> JsonRpcError {
182    JsonRpcError {
183        code: INVALID_REQUEST,
184        message: "invalid request".into(),
185        data: None,
186    }
187}
188
189pub fn method_not_found(method: &str) -> JsonRpcError {
190    JsonRpcError {
191        code: METHOD_NOT_FOUND,
192        message: "method not found".into(),
193        data: Some(json!({ "method": method })),
194    }
195}
196
197pub fn invalid_params(message: impl Into<String>) -> JsonRpcError {
198    JsonRpcError {
199        code: INVALID_PARAMS,
200        message: message.into(),
201        data: None,
202    }
203}
204
205pub fn internal_error(message: impl Into<String>) -> JsonRpcError {
206    JsonRpcError {
207        code: INTERNAL_ERROR,
208        message: message.into(),
209        data: None,
210    }
211}
212
213/// Map a typed core error onto the control-plane codes.
214pub fn error_from_core(err: &CoreError) -> JsonRpcError {
215    match err {
216        CoreError::IssueNotFound { id } => JsonRpcError {
217            code: NOT_FOUND,
218            message: err.to_string(),
219            data: Some(json!({ "code": "not_found", "id": id })),
220        },
221        CoreError::ClaimConflict { id, holder, .. } => JsonRpcError {
222            code: CONFLICT,
223            message: err.to_string(),
224            data: Some(json!({ "code": "conflict", "id": id, "holder": holder })),
225        },
226        CoreError::BlockerCycle { blocker, issue } => JsonRpcError {
227            code: CYCLE,
228            message: err.to_string(),
229            data: Some(json!({ "code": "cycle", "id": issue, "block": blocker })),
230        },
231        CoreError::InvalidState { id, state } => JsonRpcError {
232            code: INVALID_STATE,
233            message: err.to_string(),
234            data: Some(json!({ "code": "invalid_state", "id": id, "state": state })),
235        },
236        CoreError::Other(_) => internal_error(err.to_string()),
237    }
238}
239
240/// v1 methods the owner advertises on `initialize`.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
242pub enum Method {
243    Initialize,
244    IdentityGet,
245    IssueList,
246    IssueGet,
247    IssueReady,
248    IssueSearch,
249    IssueClaims,
250    IssueAgenda,
251    IssueShow,
252    IssueExcerpt,
253    IssueTree,
254    IssueRelated,
255    IssueChildren,
256    IssueAncestors,
257    IssueImpact,
258    IssueBacklinks,
259    IssueOpen,
260    IssueCreate,
261    IssueUpdate,
262    IssueClaim,
263    IssueNote,
264    IssueRefile,
265    ProjectList,
266    EventsSince,
267    EventsGen,
268}
269
270impl Method {
271    pub fn as_str(self) -> &'static str {
272        match self {
273            Self::Initialize => "initialize",
274            Self::IdentityGet => "identity/get",
275            Self::IssueList => "issue/list",
276            Self::IssueGet => "issue/get",
277            Self::IssueReady => "issue/ready",
278            Self::IssueSearch => "issue/search",
279            Self::IssueClaims => "issue/claims",
280            Self::IssueAgenda => "issue/agenda",
281            Self::IssueShow => "issue/show",
282            Self::IssueExcerpt => "issue/excerpt",
283            Self::IssueTree => "issue/tree",
284            Self::IssueRelated => "issue/related",
285            Self::IssueChildren => "issue/children",
286            Self::IssueAncestors => "issue/ancestors",
287            Self::IssueImpact => "issue/impact",
288            Self::IssueBacklinks => "issue/backlinks",
289            Self::IssueOpen => "issue/open",
290            Self::IssueCreate => "issue/create",
291            Self::IssueUpdate => "issue/update",
292            Self::IssueClaim => "issue/claim",
293            Self::IssueNote => "issue/note",
294            Self::IssueRefile => "issue/refile",
295            Self::ProjectList => "project/list",
296            Self::EventsSince => "events/since",
297            Self::EventsGen => "events/gen",
298        }
299    }
300
301    pub fn parse(name: &str) -> Result<Self, JsonRpcError> {
302        match name {
303            "initialize" => Ok(Self::Initialize),
304            "identity/get" => Ok(Self::IdentityGet),
305            "issue/list" => Ok(Self::IssueList),
306            "issue/get" => Ok(Self::IssueGet),
307            "issue/ready" => Ok(Self::IssueReady),
308            "issue/search" => Ok(Self::IssueSearch),
309            "issue/claims" => Ok(Self::IssueClaims),
310            "issue/agenda" => Ok(Self::IssueAgenda),
311            "issue/show" => Ok(Self::IssueShow),
312            "issue/excerpt" => Ok(Self::IssueExcerpt),
313            "issue/tree" => Ok(Self::IssueTree),
314            "issue/related" => Ok(Self::IssueRelated),
315            "issue/children" => Ok(Self::IssueChildren),
316            "issue/ancestors" => Ok(Self::IssueAncestors),
317            "issue/impact" => Ok(Self::IssueImpact),
318            "issue/backlinks" => Ok(Self::IssueBacklinks),
319            "issue/open" => Ok(Self::IssueOpen),
320            "issue/create" => Ok(Self::IssueCreate),
321            "issue/update" => Ok(Self::IssueUpdate),
322            "issue/claim" => Ok(Self::IssueClaim),
323            "issue/note" => Ok(Self::IssueNote),
324            "issue/refile" => Ok(Self::IssueRefile),
325            "project/list" => Ok(Self::ProjectList),
326            "events/since" => Ok(Self::EventsSince),
327            "events/gen" => Ok(Self::EventsGen),
328            other => Err(method_not_found(other)),
329        }
330    }
331}
332
333/// Capability strings returned by `initialize` (v1). `initialize` itself is omitted.
334pub const V1_CAPABILITIES: &[&str] = &[
335    "issue/list",
336    "issue/get",
337    "issue/ready",
338    "issue/search",
339    "issue/claims",
340    "issue/agenda",
341    "issue/show",
342    "issue/excerpt",
343    "issue/tree",
344    "issue/related",
345    "issue/children",
346    "issue/ancestors",
347    "issue/impact",
348    "issue/backlinks",
349    "issue/open",
350    "issue/create",
351    "issue/update",
352    "issue/claim",
353    "issue/note",
354    "issue/refile",
355    "project/list",
356    "events/since",
357    "events/gen",
358    "identity/get",
359];
360
361#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362#[serde(rename_all = "camelCase")]
363pub struct InitializeParams {
364    pub protocol_version: u32,
365    #[serde(default)]
366    pub client: String,
367    pub agent: String,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
371#[serde(rename_all = "camelCase")]
372pub struct InitializeResult {
373    pub protocol_version: u32,
374    pub capabilities: Vec<String>,
375    pub root: String,
376    pub prefix: String,
377    pub generation: u64,
378    pub revision: u64,
379    pub identity: String,
380}
381
382/// Parse `initialize` params. Missing/empty `agent` and version != 1 are -32602.
383pub fn parse_initialize_params(value: &Value) -> Result<InitializeParams, JsonRpcError> {
384    let obj = value
385        .as_object()
386        .ok_or_else(|| invalid_params("params must be an object"))?;
387    let version = match obj.get("protocolVersion") {
388        Some(Value::Number(n)) => n
389            .as_u64()
390            .ok_or_else(|| invalid_params("protocolVersion must be a number"))?,
391        Some(_) => return Err(invalid_params("protocolVersion must be a number")),
392        None => return Err(invalid_params("protocolVersion is required")),
393    };
394    if version != u64::from(PROTOCOL_VERSION) {
395        return Err(JsonRpcError {
396            code: INVALID_PARAMS,
397            message: "unsupported protocol version".into(),
398            data: Some(json!({ "supported": PROTOCOL_VERSION })),
399        });
400    }
401    let agent = match obj.get("agent") {
402        Some(Value::String(s)) if !s.trim().is_empty() => s.clone(),
403        _ => return Err(invalid_params("agent is required")),
404    };
405    let client = obj
406        .get("client")
407        .and_then(Value::as_str)
408        .unwrap_or("")
409        .to_string();
410    Ok(InitializeParams {
411        protocol_version: PROTOCOL_VERSION,
412        client,
413        agent,
414    })
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
418pub struct IssueListParams {
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub project: Option<String>,
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    pub state: Option<String>,
423    #[serde(default, skip_serializing_if = "Option::is_none")]
424    pub ready: Option<bool>,
425    #[serde(default, skip_serializing_if = "Option::is_none")]
426    pub query: Option<String>,
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    pub limit: Option<usize>,
429    #[serde(default, skip_serializing_if = "Option::is_none")]
430    pub offset: Option<usize>,
431    #[serde(default, skip_serializing_if = "Option::is_none")]
432    pub since_revision: Option<u64>,
433}
434
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
436pub struct IssueListResult {
437    #[serde(default)]
438    pub issues: Vec<IssueRow>,
439    #[serde(default)]
440    pub total: u64,
441    #[serde(default)]
442    pub matched: u64,
443    pub revision: u64,
444    #[serde(default)]
445    pub generation: u64,
446    #[serde(default)]
447    pub unchanged: bool,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451pub struct IdParams {
452    pub id: String,
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
456pub struct IssueGetResult {
457    #[serde(flatten)]
458    pub issue: IssueDetail,
459    pub revision: u64,
460}
461
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
463pub struct SearchParams {
464    pub query: String,
465    #[serde(default, skip_serializing_if = "Option::is_none")]
466    pub limit: Option<usize>,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
470pub struct ClaimsParams {
471    #[serde(default, skip_serializing_if = "Option::is_none")]
472    pub holder: Option<String>,
473    #[serde(default, skip_serializing_if = "Option::is_none")]
474    pub project: Option<String>,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
478pub struct AgendaParams {
479    #[serde(default, skip_serializing_if = "Option::is_none")]
480    pub days: Option<i64>,
481    #[serde(default, skip_serializing_if = "Option::is_none")]
482    pub project: Option<String>,
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
486pub struct TreeParams {
487    pub id: String,
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub format: Option<String>,
490}
491
492#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493#[serde(untagged)]
494pub enum TreeResult {
495    Nodes(TreeNode),
496    Text { text: String },
497}
498
499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
500pub struct RelatedParams {
501    pub id: String,
502    #[serde(default, skip_serializing_if = "Option::is_none")]
503    pub depth: Option<usize>,
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    pub limit: Option<usize>,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
509pub struct WalkParams {
510    pub id: String,
511    #[serde(default, skip_serializing_if = "Option::is_none")]
512    pub depth: Option<usize>,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
516pub struct ProjectListResult {
517    pub projects: Vec<String>,
518    pub revision: u64,
519}
520
521#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
522pub struct EventsSinceParams {
523    pub since: u64,
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub limit: Option<usize>,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct EventsSinceResult {
530    pub events: Vec<Event>,
531    pub generation: u64,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
535pub struct EventsGenResult {
536    pub generation: u64,
537    pub revision: u64,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
541pub struct IdentityResult {
542    pub identity: String,
543    pub root: String,
544    pub prefix: String,
545    pub version: String,
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
549pub struct CreateParams {
550    pub project: String,
551    pub title: String,
552    #[serde(default, skip_serializing_if = "Option::is_none")]
553    pub agent: Option<String>,
554    #[serde(default, skip_serializing_if = "Option::is_none")]
555    pub priority: Option<char>,
556    #[serde(default, skip_serializing_if = "Option::is_none")]
557    pub issue_type: Option<String>,
558    #[serde(default, skip_serializing_if = "Option::is_none")]
559    pub deadline: Option<String>,
560    #[serde(default, skip_serializing_if = "Option::is_none")]
561    pub scheduled: Option<String>,
562    #[serde(default, skip_serializing_if = "Option::is_none")]
563    pub tags: Option<String>,
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub parent: Option<String>,
566    #[serde(default, skip_serializing_if = "Option::is_none")]
567    pub body: Option<String>,
568}
569
570#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
571pub struct UpdateParams {
572    pub id: String,
573    #[serde(default, skip_serializing_if = "Option::is_none")]
574    pub state: Option<String>,
575    #[serde(default, skip_serializing_if = "Option::is_none")]
576    pub priority: Option<String>,
577    #[serde(default, skip_serializing_if = "Option::is_none")]
578    pub block: Option<String>,
579    #[serde(default, skip_serializing_if = "Option::is_none")]
580    pub unblock: Option<String>,
581    #[serde(default, skip_serializing_if = "Option::is_none")]
582    pub agent: Option<String>,
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
586pub struct ClaimParams {
587    pub id: String,
588    #[serde(default)]
589    pub force: bool,
590    #[serde(default, skip_serializing_if = "Option::is_none")]
591    pub agent: Option<String>,
592}
593
594#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
595pub struct NoteParams {
596    pub id: String,
597    pub text: String,
598}
599
600#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601pub struct RefileParams {
602    pub id: String,
603    pub to: String,
604}
605
606#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
607pub struct MutResult {
608    pub ok: bool,
609    pub report: String,
610    #[serde(default)]
611    pub issue: Option<IssueDetail>,
612    pub revision: u64,
613    pub generation: u64,
614}
615
616#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
617pub struct VaultChanged {
618    pub generation: u64,
619    pub revision: u64,
620    #[serde(default)]
621    pub projects: Vec<String>,
622    #[serde(default, skip_serializing_if = "Option::is_none")]
623    pub ids: Option<Vec<String>>,
624}
625
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
627pub struct IssueSelected {
628    pub id: String,
629    pub project: String,
630}
631
632/// Push notifications. No `id` on the wire.
633#[derive(Debug, Clone, PartialEq)]
634pub enum Notification {
635    VaultChanged(VaultChanged),
636    IssueSelected(IssueSelected),
637    ServeShuttingDown,
638    Unknown { method: String, params: Value },
639}
640
641impl Notification {
642    pub fn method(&self) -> &str {
643        match self {
644            Self::VaultChanged(_) => NOTIFY_VAULT_CHANGED,
645            Self::IssueSelected(_) => NOTIFY_ISSUE_SELECTED,
646            Self::ServeShuttingDown => NOTIFY_SHUTTING_DOWN,
647            Self::Unknown { method, .. } => method,
648        }
649    }
650
651    pub fn parse(method: &str, params: Value) -> Self {
652        match method {
653            NOTIFY_VAULT_CHANGED => match serde_json::from_value(params.clone()) {
654                Ok(body) => Self::VaultChanged(body),
655                Err(_) => Self::Unknown {
656                    method: method.into(),
657                    params,
658                },
659            },
660            NOTIFY_ISSUE_SELECTED => match serde_json::from_value(params.clone()) {
661                Ok(body) => Self::IssueSelected(body),
662                Err(_) => Self::Unknown {
663                    method: method.into(),
664                    params,
665                },
666            },
667            NOTIFY_SHUTTING_DOWN => Self::ServeShuttingDown,
668            other => Self::Unknown {
669                method: other.into(),
670                params,
671            },
672        }
673    }
674
675    pub fn to_params(&self) -> Value {
676        match self {
677            Self::VaultChanged(body) => serde_json::to_value(body).unwrap_or(Value::Null),
678            Self::IssueSelected(body) => serde_json::to_value(body).unwrap_or(Value::Null),
679            Self::ServeShuttingDown => json!({}),
680            Self::Unknown { params, .. } => params.clone(),
681        }
682    }
683}
684
685/// Typed v1 request.
686#[derive(Debug, Clone, PartialEq)]
687pub enum Request {
688    Initialize(InitializeParams),
689    IdentityGet,
690    IssueList(IssueListParams),
691    IssueGet(IdParams),
692    IssueReady(IssueListParams),
693    IssueSearch(SearchParams),
694    IssueClaims(ClaimsParams),
695    IssueAgenda(AgendaParams),
696    IssueShow(IdParams),
697    IssueExcerpt(IdParams),
698    IssueTree(TreeParams),
699    IssueRelated(RelatedParams),
700    IssueChildren(WalkParams),
701    IssueAncestors(WalkParams),
702    IssueImpact(WalkParams),
703    IssueBacklinks(WalkParams),
704    IssueOpen(IdParams),
705    IssueCreate(CreateParams),
706    IssueUpdate(UpdateParams),
707    IssueClaim(ClaimParams),
708    IssueNote(NoteParams),
709    IssueRefile(RefileParams),
710    ProjectList,
711    EventsSince(EventsSinceParams),
712    EventsGen,
713}
714
715impl Request {
716    pub fn method(&self) -> Method {
717        match self {
718            Self::Initialize(_) => Method::Initialize,
719            Self::IdentityGet => Method::IdentityGet,
720            Self::IssueList(_) => Method::IssueList,
721            Self::IssueGet(_) => Method::IssueGet,
722            Self::IssueReady(_) => Method::IssueReady,
723            Self::IssueSearch(_) => Method::IssueSearch,
724            Self::IssueClaims(_) => Method::IssueClaims,
725            Self::IssueAgenda(_) => Method::IssueAgenda,
726            Self::IssueShow(_) => Method::IssueShow,
727            Self::IssueExcerpt(_) => Method::IssueExcerpt,
728            Self::IssueTree(_) => Method::IssueTree,
729            Self::IssueRelated(_) => Method::IssueRelated,
730            Self::IssueChildren(_) => Method::IssueChildren,
731            Self::IssueAncestors(_) => Method::IssueAncestors,
732            Self::IssueImpact(_) => Method::IssueImpact,
733            Self::IssueBacklinks(_) => Method::IssueBacklinks,
734            Self::IssueOpen(_) => Method::IssueOpen,
735            Self::IssueCreate(_) => Method::IssueCreate,
736            Self::IssueUpdate(_) => Method::IssueUpdate,
737            Self::IssueClaim(_) => Method::IssueClaim,
738            Self::IssueNote(_) => Method::IssueNote,
739            Self::IssueRefile(_) => Method::IssueRefile,
740            Self::ProjectList => Method::ProjectList,
741            Self::EventsSince(_) => Method::EventsSince,
742            Self::EventsGen => Method::EventsGen,
743        }
744    }
745
746    pub fn parse(method: &str, params: Option<Value>) -> Result<Self, JsonRpcError> {
747        let method = Method::parse(method)?;
748        let params = match params {
749            None | Some(Value::Null) => Value::Object(Default::default()),
750            Some(v) => v,
751        };
752        match method {
753            Method::Initialize => Ok(Self::Initialize(parse_initialize_params(&params)?)),
754            Method::IdentityGet => Ok(Self::IdentityGet),
755            Method::IssueList => Ok(Self::IssueList(decode_params(params)?)),
756            Method::IssueGet => Ok(Self::IssueGet(decode_params(params)?)),
757            Method::IssueReady => Ok(Self::IssueReady(decode_params(params)?)),
758            Method::IssueSearch => Ok(Self::IssueSearch(decode_params(params)?)),
759            Method::IssueClaims => Ok(Self::IssueClaims(decode_params(params)?)),
760            Method::IssueAgenda => Ok(Self::IssueAgenda(decode_params(params)?)),
761            Method::IssueShow => Ok(Self::IssueShow(decode_params(params)?)),
762            Method::IssueExcerpt => Ok(Self::IssueExcerpt(decode_params(params)?)),
763            Method::IssueTree => Ok(Self::IssueTree(decode_params(params)?)),
764            Method::IssueRelated => Ok(Self::IssueRelated(decode_params(params)?)),
765            Method::IssueChildren => Ok(Self::IssueChildren(decode_params(params)?)),
766            Method::IssueAncestors => Ok(Self::IssueAncestors(decode_params(params)?)),
767            Method::IssueImpact => Ok(Self::IssueImpact(decode_params(params)?)),
768            Method::IssueBacklinks => Ok(Self::IssueBacklinks(decode_params(params)?)),
769            Method::IssueOpen => Ok(Self::IssueOpen(decode_params(params)?)),
770            Method::IssueCreate => Ok(Self::IssueCreate(decode_params(params)?)),
771            Method::IssueUpdate => Ok(Self::IssueUpdate(decode_params(params)?)),
772            Method::IssueClaim => Ok(Self::IssueClaim(decode_params(params)?)),
773            Method::IssueNote => Ok(Self::IssueNote(decode_params(params)?)),
774            Method::IssueRefile => Ok(Self::IssueRefile(decode_params(params)?)),
775            Method::ProjectList => Ok(Self::ProjectList),
776            Method::EventsSince => Ok(Self::EventsSince(decode_params(params)?)),
777            Method::EventsGen => Ok(Self::EventsGen),
778        }
779    }
780
781    pub fn to_params(&self) -> Value {
782        match self {
783            Self::Initialize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
784            Self::IdentityGet | Self::ProjectList | Self::EventsGen => json!({}),
785            Self::IssueList(p) | Self::IssueReady(p) => {
786                serde_json::to_value(p).unwrap_or(Value::Null)
787            }
788            Self::IssueGet(p) | Self::IssueShow(p) | Self::IssueExcerpt(p) | Self::IssueOpen(p) => {
789                serde_json::to_value(p).unwrap_or(Value::Null)
790            }
791            Self::IssueSearch(p) => serde_json::to_value(p).unwrap_or(Value::Null),
792            Self::IssueClaims(p) => serde_json::to_value(p).unwrap_or(Value::Null),
793            Self::IssueAgenda(p) => serde_json::to_value(p).unwrap_or(Value::Null),
794            Self::IssueTree(p) => serde_json::to_value(p).unwrap_or(Value::Null),
795            Self::IssueRelated(p) => serde_json::to_value(p).unwrap_or(Value::Null),
796            Self::IssueChildren(p)
797            | Self::IssueAncestors(p)
798            | Self::IssueImpact(p)
799            | Self::IssueBacklinks(p) => serde_json::to_value(p).unwrap_or(Value::Null),
800            Self::IssueCreate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
801            Self::IssueUpdate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
802            Self::IssueClaim(p) => serde_json::to_value(p).unwrap_or(Value::Null),
803            Self::IssueNote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
804            Self::IssueRefile(p) => serde_json::to_value(p).unwrap_or(Value::Null),
805            Self::EventsSince(p) => serde_json::to_value(p).unwrap_or(Value::Null),
806        }
807    }
808}
809
810/// Typed v1 result body.
811#[derive(Debug, Clone)]
812pub enum Response {
813    Initialize(InitializeResult),
814    IdentityGet(IdentityResult),
815    IssueList(IssueListResult),
816    IssueGet(IssueGetResult),
817    IssueReady(IssueListResult),
818    IssueSearch(Vec<SearchHit>),
819    IssueClaims(Vec<ClaimRow>),
820    IssueAgenda(Vec<AgendaRow>),
821    IssueShow(IssueGetResult),
822    IssueExcerpt(Excerpt),
823    IssueTree(TreeResult),
824    IssueRelated(Vec<RelatedHit>),
825    IssueChildren(Vec<WalkHit>),
826    IssueAncestors(Vec<WalkHit>),
827    IssueImpact(Vec<WalkHit>),
828    IssueBacklinks(Vec<WalkHit>),
829    IssueOpen(IssueGetResult),
830    IssueCreate(MutResult),
831    IssueUpdate(MutResult),
832    IssueClaim(MutResult),
833    IssueNote(MutResult),
834    IssueRefile(MutResult),
835    ProjectList(ProjectListResult),
836    EventsSince(EventsSinceResult),
837    EventsGen(EventsGenResult),
838}
839
840impl Response {
841    pub fn to_value(&self) -> Result<Value, serde_json::Error> {
842        match self {
843            Self::Initialize(v) => serde_json::to_value(v),
844            Self::IdentityGet(v) => serde_json::to_value(v),
845            Self::IssueList(v) | Self::IssueReady(v) => serde_json::to_value(v),
846            Self::IssueGet(v) | Self::IssueShow(v) | Self::IssueOpen(v) => serde_json::to_value(v),
847            Self::IssueSearch(v) => serde_json::to_value(v),
848            Self::IssueClaims(v) => serde_json::to_value(v),
849            Self::IssueAgenda(v) => serde_json::to_value(v),
850            Self::IssueExcerpt(v) => serde_json::to_value(v),
851            Self::IssueTree(v) => serde_json::to_value(v),
852            Self::IssueRelated(v) => serde_json::to_value(v),
853            Self::IssueChildren(v)
854            | Self::IssueAncestors(v)
855            | Self::IssueImpact(v)
856            | Self::IssueBacklinks(v) => serde_json::to_value(v),
857            Self::IssueCreate(v)
858            | Self::IssueUpdate(v)
859            | Self::IssueClaim(v)
860            | Self::IssueNote(v)
861            | Self::IssueRefile(v) => serde_json::to_value(v),
862            Self::ProjectList(v) => serde_json::to_value(v),
863            Self::EventsSince(v) => serde_json::to_value(v),
864            Self::EventsGen(v) => serde_json::to_value(v),
865        }
866    }
867}
868
869fn decode_params<T: DeserializeOwned>(value: Value) -> Result<T, JsonRpcError> {
870    serde_json::from_value(value).map_err(|e| invalid_params(e.to_string()))
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876    use std::collections::BTreeMap;
877
878    #[test]
879    fn initialize_missing_agent_is_invalid_params() {
880        let err = parse_initialize_params(&json!({
881            "protocolVersion": 1,
882            "client": "vissue-tui"
883        }))
884        .unwrap_err();
885        assert_eq!(err.code, INVALID_PARAMS);
886        assert_eq!(err.message, "agent is required");
887
888        let err = parse_initialize_params(&json!({
889            "protocolVersion": 1,
890            "agent": ""
891        }))
892        .unwrap_err();
893        assert_eq!(err.code, INVALID_PARAMS);
894        assert_eq!(err.message, "agent is required");
895
896        let err = Request::parse(
897            "initialize",
898            Some(json!({"protocolVersion": 1, "agent": "   "})),
899        )
900        .unwrap_err();
901        assert_eq!(err.code, INVALID_PARAMS);
902    }
903
904    #[test]
905    fn protocol_version_2_is_rejected() {
906        let err = parse_initialize_params(&json!({
907            "protocolVersion": 2,
908            "agent": "rg@host"
909        }))
910        .unwrap_err();
911        assert_eq!(err.code, INVALID_PARAMS);
912        assert_eq!(err.message, "unsupported protocol version");
913        assert_eq!(err.data, Some(json!({"supported": 1})));
914    }
915
916    #[test]
917    fn initialize_version_1_is_accepted() {
918        let params = parse_initialize_params(&json!({
919            "protocolVersion": 1,
920            "client": "vissue-tui",
921            "agent": "rg@host"
922        }))
923        .unwrap();
924        assert_eq!(params.protocol_version, 1);
925        assert_eq!(params.agent, "rg@host");
926        assert_eq!(params.client, "vissue-tui");
927    }
928
929    #[test]
930    fn handshake_fields_are_camel_case() {
931        let params = InitializeParams {
932            protocol_version: 1,
933            client: "vissue-tui".into(),
934            agent: "rg@host".into(),
935        };
936        let value = serde_json::to_value(&params).unwrap();
937        assert_eq!(value["protocolVersion"], 1);
938        assert!(value.get("protocol_version").is_none());
939
940        let result = InitializeResult {
941            protocol_version: 1,
942            capabilities: vec!["issue/list".into()],
943            root: "/tmp/tracker".into(),
944            prefix: "Software".into(),
945            generation: 3,
946            revision: 1,
947            identity: "rg@host".into(),
948        };
949        let value = serde_json::to_value(&result).unwrap();
950        assert_eq!(value["protocolVersion"], 1);
951        assert_eq!(value["generation"], 3);
952    }
953
954    #[test]
955    fn issue_payloads_are_snake_case() {
956        let params = IssueListParams {
957            since_revision: Some(41),
958            ..IssueListParams::default()
959        };
960        let value = serde_json::to_value(&params).unwrap();
961        assert_eq!(value["since_revision"], 41);
962        assert!(value.get("sinceRevision").is_none());
963    }
964
965    #[test]
966    fn unknown_method_is_not_found() {
967        let err = Method::parse("issue/fold").unwrap_err();
968        assert_eq!(err.code, METHOD_NOT_FOUND);
969        assert_eq!(err.data, Some(json!({"method": "issue/fold"})));
970    }
971
972    #[test]
973    fn every_v1_capability_parses() {
974        for name in V1_CAPABILITIES {
975            assert!(Method::parse(name).is_ok(), "{name}");
976        }
977        assert_eq!(Method::Initialize.as_str(), "initialize");
978    }
979
980    #[test]
981    fn request_parse_roundtrips_issue_get() {
982        let req = Request::parse("issue/get", Some(json!({"id": "atlas-1a2b"}))).unwrap();
983        assert_eq!(req.method(), Method::IssueGet);
984        assert_eq!(req.to_params()["id"], "atlas-1a2b");
985    }
986
987    #[test]
988    fn missing_id_on_issue_get_is_invalid_params() {
989        let err = Request::parse("issue/get", Some(json!({}))).unwrap_err();
990        assert_eq!(err.code, INVALID_PARAMS);
991    }
992
993    #[test]
994    fn core_errors_carry_data_code() {
995        let err = error_from_core(&CoreError::IssueNotFound {
996            id: "atlas-1a2b".into(),
997        });
998        assert_eq!(err.code, NOT_FOUND);
999        assert_eq!(err.data.unwrap()["code"], "not_found");
1000
1001        let err = error_from_core(&CoreError::ClaimConflict {
1002            id: "atlas-1a2b".into(),
1003            holder: "other".into(),
1004            claimed_at: None,
1005        });
1006        assert_eq!(err.code, CONFLICT);
1007        let data = err.data.unwrap();
1008        assert_eq!(data["code"], "conflict");
1009        assert_eq!(data["holder"], "other");
1010
1011        let err = error_from_core(&CoreError::BlockerCycle {
1012            blocker: "a".into(),
1013            issue: "b".into(),
1014        });
1015        assert_eq!(err.code, CYCLE);
1016        let data = err.data.unwrap();
1017        assert_eq!(data["code"], "cycle");
1018        assert_eq!(data["id"], "b");
1019        assert_eq!(data["block"], "a");
1020
1021        let err = error_from_core(&CoreError::InvalidState {
1022            id: "atlas-4g5h".into(),
1023            state: "DONE".into(),
1024        });
1025        assert_eq!(err.code, INVALID_STATE);
1026        assert_eq!(err.data.unwrap()["code"], "invalid_state");
1027    }
1028
1029    #[test]
1030    fn notification_parse_known_methods() {
1031        let n = Notification::parse(
1032            NOTIFY_VAULT_CHANGED,
1033            json!({"generation": 1, "revision": 2, "projects": ["atlas"]}),
1034        );
1035        assert!(matches!(n, Notification::VaultChanged(_)));
1036        assert_eq!(n.method(), NOTIFY_VAULT_CHANGED);
1037
1038        let n = Notification::parse(
1039            NOTIFY_ISSUE_SELECTED,
1040            json!({"id": "atlas-1a2b", "project": "atlas"}),
1041        );
1042        assert!(matches!(n, Notification::IssueSelected(_)));
1043
1044        let n = Notification::parse(NOTIFY_SHUTTING_DOWN, json!({}));
1045        assert!(matches!(n, Notification::ServeShuttingDown));
1046        assert_eq!(n.to_params(), json!({}));
1047    }
1048
1049    #[test]
1050    fn list_unchanged_deserializes_without_rows() {
1051        let page: IssueListResult =
1052            serde_json::from_value(json!({"unchanged": true, "revision": 41})).unwrap();
1053        assert!(page.unchanged);
1054        assert!(page.issues.is_empty());
1055        assert_eq!(page.revision, 41);
1056    }
1057
1058    #[test]
1059    fn response_to_value_serializes_initialize() {
1060        let resp = Response::Initialize(InitializeResult {
1061            protocol_version: 1,
1062            capabilities: V1_CAPABILITIES.iter().map(|s| (*s).to_string()).collect(),
1063            root: "/tmp".into(),
1064            prefix: "Software".into(),
1065            generation: 1,
1066            revision: 1,
1067            identity: "agent".into(),
1068        });
1069        let value = resp.to_value().unwrap();
1070        assert_eq!(value["protocolVersion"], 1);
1071        assert!(value["capabilities"]
1072            .as_array()
1073            .unwrap()
1074            .contains(&json!("issue/list")));
1075    }
1076
1077    #[test]
1078    fn envelope_helpers_roundtrip() {
1079        let req = JsonRpcRequest::call(JsonRpcId::Number(1), "identity/get", json!({}));
1080        let bytes = serde_json::to_vec(&req).unwrap();
1081        let back: JsonRpcRequest = serde_json::from_slice(&bytes).unwrap();
1082        assert_eq!(back.method, "identity/get");
1083        assert!(!back.is_notification());
1084
1085        let note = JsonRpcRequest::notification(NOTIFY_SHUTTING_DOWN, json!({}));
1086        assert!(note.is_notification());
1087
1088        let ok = JsonRpcResponse::ok(Some(JsonRpcId::Number(1)), json!({"ok": true}));
1089        assert_eq!(ok.result.unwrap()["ok"], true);
1090        let err = JsonRpcResponse::err(Some(JsonRpcId::Null), parse_error());
1091        assert_eq!(err.error.unwrap().code, PARSE_ERROR);
1092    }
1093
1094    #[test]
1095    fn mut_and_walk_params_decode() {
1096        let claim = Request::parse(
1097            "issue/claim",
1098            Some(json!({"id": "atlas-1a2b", "force": true})),
1099        )
1100        .unwrap();
1101        match claim {
1102            Request::IssueClaim(p) => {
1103                assert!(p.force);
1104                assert_eq!(p.id, "atlas-1a2b");
1105            }
1106            other => panic!("{other:?}"),
1107        }
1108        let create = Request::parse(
1109            "issue/create",
1110            Some(json!({"project": "atlas", "title": "x"})),
1111        )
1112        .unwrap();
1113        assert_eq!(create.method(), Method::IssueCreate);
1114        assert!(Request::parse("issue/create", Some(json!({"title": "x"}))).is_err());
1115        assert_eq!(
1116            Request::parse("events/gen", None).unwrap().method(),
1117            Method::EventsGen
1118        );
1119        let _ = Request::IssueNote(NoteParams {
1120            id: "a".into(),
1121            text: "n".into(),
1122        })
1123        .to_params();
1124        let _ = Request::IssueRefile(RefileParams {
1125            id: "a".into(),
1126            to: "b".into(),
1127        })
1128        .to_params();
1129        let _ = Request::IssueUpdate(UpdateParams {
1130            id: "a".into(),
1131            state: Some("STARTED".into()),
1132            priority: None,
1133            block: None,
1134            unblock: None,
1135            agent: None,
1136        })
1137        .to_params();
1138        let _ = Request::EventsSince(EventsSinceParams {
1139            since: 0,
1140            limit: Some(10),
1141        })
1142        .to_params();
1143        let _ = Request::IssueTree(TreeParams {
1144            id: "a".into(),
1145            format: Some("ascii".into()),
1146        })
1147        .to_params();
1148        let _ = Request::IssueRelated(RelatedParams {
1149            id: "a".into(),
1150            depth: Some(2),
1151            limit: Some(20),
1152        })
1153        .to_params();
1154        let _ = Request::IssueChildren(WalkParams {
1155            id: "a".into(),
1156            depth: None,
1157        })
1158        .to_params();
1159        let _ = Request::IssueSearch(SearchParams {
1160            query: "q".into(),
1161            limit: None,
1162        })
1163        .to_params();
1164        let _ = Request::IssueClaims(ClaimsParams::default()).to_params();
1165        let _ = Request::IssueAgenda(AgendaParams::default()).to_params();
1166        let _ = Request::IdentityGet.to_params();
1167    }
1168
1169    #[test]
1170    fn response_variants_serialize() {
1171        let detail = IssueDetail {
1172            id: "atlas-1a2b".into(),
1173            project: "atlas".into(),
1174            title: "t".into(),
1175            state: "TODO".into(),
1176            priority: "B".into(),
1177            properties: BTreeMap::new(),
1178            org_tags: vec![],
1179            tags: vec![],
1180            blocked_by: vec![],
1181            parent: None,
1182            claimed_by: None,
1183            claimed_at: None,
1184            file: "issues.org:1-2".into(),
1185            line_start: 1,
1186            line_end: 2,
1187            body: "what the issue asks for".into(),
1188            logbook: vec![],
1189        };
1190        let get = IssueGetResult {
1191            issue: detail.clone(),
1192            revision: 1,
1193        };
1194        assert!(Response::IssueGet(get.clone()).to_value().unwrap()["id"] == "atlas-1a2b");
1195        assert!(Response::IssueShow(get.clone()).to_value().is_ok());
1196        assert!(Response::IssueOpen(get).to_value().is_ok());
1197        assert!(Response::IssueExcerpt(Excerpt {
1198            id: "atlas-1a2b".into(),
1199            file: "issues.org".into(),
1200            line_start: 1,
1201            line_end: 2,
1202            text: "body".into(),
1203            suppressed: false,
1204        })
1205        .to_value()
1206        .is_ok());
1207        assert!(Response::IssueSearch(vec![]).to_value().unwrap().is_array());
1208        assert!(Response::IssueClaims(vec![]).to_value().unwrap().is_array());
1209        assert!(Response::IssueAgenda(vec![]).to_value().unwrap().is_array());
1210        assert!(Response::IssueRelated(vec![])
1211            .to_value()
1212            .unwrap()
1213            .is_array());
1214        assert!(Response::IssueChildren(vec![])
1215            .to_value()
1216            .unwrap()
1217            .is_array());
1218        assert!(Response::IssueAncestors(vec![])
1219            .to_value()
1220            .unwrap()
1221            .is_array());
1222        assert!(Response::IssueImpact(vec![]).to_value().unwrap().is_array());
1223        assert!(Response::IssueBacklinks(vec![])
1224            .to_value()
1225            .unwrap()
1226            .is_array());
1227        assert!(Response::ProjectList(ProjectListResult {
1228            projects: vec!["atlas".into()],
1229            revision: 1,
1230        })
1231        .to_value()
1232        .is_ok());
1233        assert!(Response::EventsGen(EventsGenResult {
1234            generation: 1,
1235            revision: 1,
1236        })
1237        .to_value()
1238        .is_ok());
1239        assert!(Response::EventsSince(EventsSinceResult {
1240            events: vec![],
1241            generation: 1,
1242        })
1243        .to_value()
1244        .is_ok());
1245        assert!(Response::IdentityGet(IdentityResult {
1246            identity: "a".into(),
1247            root: "/".into(),
1248            prefix: "Software".into(),
1249            version: "0.2.0".into(),
1250        })
1251        .to_value()
1252        .is_ok());
1253        let mut_ok = MutResult {
1254            ok: true,
1255            report: "ok".into(),
1256            issue: Some(detail),
1257            revision: 2,
1258            generation: 3,
1259        };
1260        assert!(Response::IssueClaim(mut_ok.clone()).to_value().is_ok());
1261        assert!(Response::IssueCreate(mut_ok.clone()).to_value().is_ok());
1262        assert!(Response::IssueUpdate(mut_ok.clone()).to_value().is_ok());
1263        assert!(Response::IssueNote(mut_ok.clone()).to_value().is_ok());
1264        assert!(Response::IssueRefile(mut_ok).to_value().is_ok());
1265        assert!(Response::IssueTree(TreeResult::Text { text: "* a".into() })
1266            .to_value()
1267            .is_ok());
1268        assert!(Response::IssueList(IssueListResult {
1269            revision: 1,
1270            ..IssueListResult::default()
1271        })
1272        .to_value()
1273        .is_ok());
1274        assert!(Response::IssueReady(IssueListResult {
1275            revision: 1,
1276            ..IssueListResult::default()
1277        })
1278        .to_value()
1279        .is_ok());
1280    }
1281
1282    #[test]
1283    fn parse_every_method_with_minimal_params() {
1284        let id = json!({"id": "atlas-1a2b"});
1285        for (method, params) in [
1286            ("identity/get", json!({})),
1287            ("issue/list", json!({})),
1288            ("issue/get", id.clone()),
1289            ("issue/ready", json!({})),
1290            ("issue/search", json!({"query": "q"})),
1291            ("issue/claims", json!({})),
1292            ("issue/agenda", json!({})),
1293            ("issue/show", id.clone()),
1294            ("issue/excerpt", id.clone()),
1295            ("issue/tree", id.clone()),
1296            ("issue/related", id.clone()),
1297            ("issue/children", id.clone()),
1298            ("issue/ancestors", id.clone()),
1299            ("issue/impact", id.clone()),
1300            ("issue/backlinks", id.clone()),
1301            ("issue/open", id.clone()),
1302            ("issue/create", json!({"project": "atlas", "title": "t"})),
1303            ("issue/update", id.clone()),
1304            ("issue/claim", id.clone()),
1305            ("issue/note", json!({"id": "atlas-1a2b", "text": "n"})),
1306            ("issue/refile", json!({"id": "atlas-1a2b", "to": "beacon"})),
1307            ("project/list", json!({})),
1308            ("events/since", json!({"since": 0})),
1309            ("events/gen", json!({})),
1310        ] {
1311            let req = Request::parse(method, Some(params)).expect(method);
1312            assert_eq!(req.method().as_str(), method);
1313            let _ = req.to_params();
1314        }
1315    }
1316
1317    #[test]
1318    fn helper_errors_have_stable_codes() {
1319        assert_eq!(invalid_request().code, INVALID_REQUEST);
1320        assert_eq!(internal_error("x").code, INTERNAL_ERROR);
1321        assert_eq!(parse_error().code, PARSE_ERROR);
1322        let err = Error::Rpc(invalid_params("agent is required"));
1323        assert_eq!(err.to_string(), "agent is required");
1324        let _ = Error::Unsupported("unix only");
1325        let _ = Notification::parse("vault/changed", json!(null));
1326        let _ = Notification::parse("issue/selected", json!(null));
1327        let _ = Notification::parse("other/x", json!({"a": 1}));
1328        let n = Notification::Unknown {
1329            method: "x".into(),
1330            params: json!({"a": 1}),
1331        };
1332        assert_eq!(n.to_params()["a"], 1);
1333        assert_eq!(n.method(), "x");
1334    }
1335}