1use 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
14pub 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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(untagged)]
91pub enum JsonRpcId {
92 Number(i64),
93 String(String),
94 Null,
95}
96
97#[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#[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#[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
213pub 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#[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
333pub 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
382pub 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#[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#[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(¶ms)?)),
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#[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(¶ms).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(¶ms).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}