1use 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
12pub use vissue_core::events::Event;
14
15pub const PROTOCOL_VERSION: u32 = 1;
17
18pub const PARSE_ERROR: i32 = -32700;
20pub const INVALID_REQUEST: i32 = -32600;
22pub const METHOD_NOT_FOUND: i32 = -32601;
24pub const INVALID_PARAMS: i32 = -32602;
26pub const INTERNAL_ERROR: i32 = -32603;
28pub const NOT_FOUND: i32 = -32004;
30pub const CONFLICT: i32 = -32009;
32pub const INVALID_STATE: i32 = -32010;
34pub const CYCLE: i32 = -32022;
36
37pub const NOTIFY_VAULT_CHANGED: &str = "vault/changed";
39pub const NOTIFY_ISSUE_SELECTED: &str = "issue/selected";
41pub const NOTIFY_SHUTTING_DOWN: &str = "serve/shutting_down";
43
44#[derive(Debug)]
46pub enum Error {
47 Io(std::io::Error),
49 Json(serde_json::Error),
51 Frame(FrameError),
53 Rpc(JsonRpcError),
55 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(untagged)]
109pub enum JsonRpcId {
110 Number(i64),
112 String(String),
114 Null,
116}
117
118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
120pub struct JsonRpcRequest {
121 pub jsonrpc: String,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub id: Option<JsonRpcId>,
126 pub method: String,
128 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub params: Option<Value>,
131}
132
133impl JsonRpcRequest {
134 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 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 pub fn is_notification(&self) -> bool {
156 self.id.is_none()
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162pub struct JsonRpcResponse {
163 pub jsonrpc: String,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub id: Option<JsonRpcId>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub result: Option<Value>,
171 #[serde(default, skip_serializing_if = "Option::is_none")]
173 pub error: Option<JsonRpcError>,
174}
175
176impl JsonRpcResponse {
177 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
200pub struct JsonRpcError {
201 pub code: i32,
203 pub message: String,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub data: Option<Value>,
208}
209
210pub fn parse_error() -> JsonRpcError {
212 JsonRpcError {
213 code: PARSE_ERROR,
214 message: "parse error".into(),
215 data: None,
216 }
217}
218
219pub fn invalid_request() -> JsonRpcError {
221 JsonRpcError {
222 code: INVALID_REQUEST,
223 message: "invalid request".into(),
224 data: None,
225 }
226}
227
228pub 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
237pub fn invalid_params(message: impl Into<String>) -> JsonRpcError {
239 JsonRpcError {
240 code: INVALID_PARAMS,
241 message: message.into(),
242 data: None,
243 }
244}
245
246pub fn internal_error(message: impl Into<String>) -> JsonRpcError {
248 JsonRpcError {
249 code: INTERNAL_ERROR,
250 message: message.into(),
251 data: None,
252 }
253}
254
255pub 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::ClaimConflict { id, holder, .. } => JsonRpcError {
264 code: CONFLICT,
265 message: err.to_string(),
266 data: Some(json!({ "code": "conflict", "id": id, "holder": holder })),
267 },
268 CoreError::BlockerCycle { blocker, issue } => JsonRpcError {
269 code: CYCLE,
270 message: err.to_string(),
271 data: Some(json!({ "code": "cycle", "id": issue, "block": blocker })),
272 },
273 CoreError::InvalidState { id, state } => JsonRpcError {
274 code: INVALID_STATE,
275 message: err.to_string(),
276 data: Some(json!({ "code": "invalid_state", "id": id, "state": state })),
277 },
278 CoreError::Other(_) => internal_error(err.to_string()),
279 }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
284pub enum Method {
285 Initialize,
287 IdentityGet,
289 IssueList,
291 IssueGet,
293 IssueReady,
295 IssueSearch,
297 IssueClaims,
299 IssueAgenda,
301 IssueShow,
303 IssueExcerpt,
305 IssueTree,
307 IssueRelated,
309 IssueChildren,
311 IssueAncestors,
313 IssueImpact,
315 IssueBacklinks,
317 IssueOpen,
319 IssueCreate,
321 IssueUpdate,
323 IssueClaim,
325 IssueNote,
327 IssueRefile,
329 ProjectList,
331 EventsSince,
333 EventsGen,
335}
336
337impl Method {
338 pub fn as_str(self) -> &'static str {
340 match self {
341 Self::Initialize => "initialize",
342 Self::IdentityGet => "identity/get",
343 Self::IssueList => "issue/list",
344 Self::IssueGet => "issue/get",
345 Self::IssueReady => "issue/ready",
346 Self::IssueSearch => "issue/search",
347 Self::IssueClaims => "issue/claims",
348 Self::IssueAgenda => "issue/agenda",
349 Self::IssueShow => "issue/show",
350 Self::IssueExcerpt => "issue/excerpt",
351 Self::IssueTree => "issue/tree",
352 Self::IssueRelated => "issue/related",
353 Self::IssueChildren => "issue/children",
354 Self::IssueAncestors => "issue/ancestors",
355 Self::IssueImpact => "issue/impact",
356 Self::IssueBacklinks => "issue/backlinks",
357 Self::IssueOpen => "issue/open",
358 Self::IssueCreate => "issue/create",
359 Self::IssueUpdate => "issue/update",
360 Self::IssueClaim => "issue/claim",
361 Self::IssueNote => "issue/note",
362 Self::IssueRefile => "issue/refile",
363 Self::ProjectList => "project/list",
364 Self::EventsSince => "events/since",
365 Self::EventsGen => "events/gen",
366 }
367 }
368
369 pub fn parse(name: &str) -> Result<Self, JsonRpcError> {
375 match name {
376 "initialize" => Ok(Self::Initialize),
377 "identity/get" => Ok(Self::IdentityGet),
378 "issue/list" => Ok(Self::IssueList),
379 "issue/get" => Ok(Self::IssueGet),
380 "issue/ready" => Ok(Self::IssueReady),
381 "issue/search" => Ok(Self::IssueSearch),
382 "issue/claims" => Ok(Self::IssueClaims),
383 "issue/agenda" => Ok(Self::IssueAgenda),
384 "issue/show" => Ok(Self::IssueShow),
385 "issue/excerpt" => Ok(Self::IssueExcerpt),
386 "issue/tree" => Ok(Self::IssueTree),
387 "issue/related" => Ok(Self::IssueRelated),
388 "issue/children" => Ok(Self::IssueChildren),
389 "issue/ancestors" => Ok(Self::IssueAncestors),
390 "issue/impact" => Ok(Self::IssueImpact),
391 "issue/backlinks" => Ok(Self::IssueBacklinks),
392 "issue/open" => Ok(Self::IssueOpen),
393 "issue/create" => Ok(Self::IssueCreate),
394 "issue/update" => Ok(Self::IssueUpdate),
395 "issue/claim" => Ok(Self::IssueClaim),
396 "issue/note" => Ok(Self::IssueNote),
397 "issue/refile" => Ok(Self::IssueRefile),
398 "project/list" => Ok(Self::ProjectList),
399 "events/since" => Ok(Self::EventsSince),
400 "events/gen" => Ok(Self::EventsGen),
401 other => Err(method_not_found(other)),
402 }
403 }
404}
405
406pub const V1_CAPABILITIES: &[&str] = &[
408 "issue/list",
409 "issue/get",
410 "issue/ready",
411 "issue/search",
412 "issue/claims",
413 "issue/agenda",
414 "issue/show",
415 "issue/excerpt",
416 "issue/tree",
417 "issue/related",
418 "issue/children",
419 "issue/ancestors",
420 "issue/impact",
421 "issue/backlinks",
422 "issue/open",
423 "issue/create",
424 "issue/update",
425 "issue/claim",
426 "issue/note",
427 "issue/refile",
428 "project/list",
429 "events/since",
430 "events/gen",
431 "identity/get",
432];
433
434#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436#[serde(rename_all = "camelCase")]
437pub struct InitializeParams {
438 pub protocol_version: u32,
440 #[serde(default)]
442 pub client: String,
443 pub agent: String,
445}
446
447#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449#[serde(rename_all = "camelCase")]
450pub struct InitializeResult {
451 pub protocol_version: u32,
453 pub capabilities: Vec<String>,
455 pub root: String,
457 pub prefix: String,
459 pub generation: u64,
461 pub revision: u64,
463 pub identity: String,
465}
466
467pub fn parse_initialize_params(value: &Value) -> Result<InitializeParams, JsonRpcError> {
474 let obj = value
475 .as_object()
476 .ok_or_else(|| invalid_params("params must be an object"))?;
477 let version = match obj.get("protocolVersion") {
478 Some(Value::Number(n)) => n
479 .as_u64()
480 .ok_or_else(|| invalid_params("protocolVersion must be a number"))?,
481 Some(_) => return Err(invalid_params("protocolVersion must be a number")),
482 None => return Err(invalid_params("protocolVersion is required")),
483 };
484 if version != u64::from(PROTOCOL_VERSION) {
485 return Err(JsonRpcError {
486 code: INVALID_PARAMS,
487 message: "unsupported protocol version".into(),
488 data: Some(json!({ "supported": PROTOCOL_VERSION })),
489 });
490 }
491 let agent = match obj.get("agent") {
492 Some(Value::String(s)) if !s.trim().is_empty() => s.clone(),
493 _ => return Err(invalid_params("agent is required")),
494 };
495 let client = obj
496 .get("client")
497 .and_then(Value::as_str)
498 .unwrap_or("")
499 .to_string();
500 Ok(InitializeParams {
501 protocol_version: PROTOCOL_VERSION,
502 client,
503 agent,
504 })
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
509pub struct IssueListParams {
510 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub project: Option<String>,
513 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub state: Option<String>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
518 pub ready: Option<bool>,
519 #[serde(default, skip_serializing_if = "Option::is_none")]
521 pub query: Option<String>,
522 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub limit: Option<usize>,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub offset: Option<usize>,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub since_revision: Option<u64>,
531}
532
533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
535pub struct IssueListResult {
536 #[serde(default)]
538 pub issues: Vec<IssueRow>,
539 #[serde(default)]
541 pub total: u64,
542 #[serde(default)]
544 pub matched: u64,
545 pub revision: u64,
547 #[serde(default)]
549 pub generation: u64,
550 #[serde(default)]
552 pub unchanged: bool,
553}
554
555#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
557pub struct IdParams {
558 pub id: String,
560}
561
562#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
564pub struct IssueGetResult {
565 #[serde(flatten)]
567 pub issue: IssueDetail,
568 pub revision: u64,
570}
571
572#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
574pub struct SearchParams {
575 pub query: String,
577 #[serde(default, skip_serializing_if = "Option::is_none")]
579 pub limit: Option<usize>,
580}
581
582#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
584pub struct ClaimsParams {
585 #[serde(default, skip_serializing_if = "Option::is_none")]
587 pub holder: Option<String>,
588 #[serde(default, skip_serializing_if = "Option::is_none")]
590 pub project: Option<String>,
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
595pub struct AgendaParams {
596 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub days: Option<i64>,
599 #[serde(default, skip_serializing_if = "Option::is_none")]
601 pub project: Option<String>,
602}
603
604#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
606pub struct TreeParams {
607 pub id: String,
609 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub format: Option<String>,
612}
613
614#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
616#[serde(untagged)]
617pub enum TreeResult {
618 Nodes(TreeNode),
620 Text {
622 text: String,
624 },
625}
626
627#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
629pub struct RelatedParams {
630 pub id: String,
632 #[serde(default, skip_serializing_if = "Option::is_none")]
634 pub depth: Option<usize>,
635 #[serde(default, skip_serializing_if = "Option::is_none")]
637 pub limit: Option<usize>,
638}
639
640#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
642pub struct WalkParams {
643 pub id: String,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub depth: Option<usize>,
648}
649
650#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
652pub struct ProjectListResult {
653 pub projects: Vec<String>,
655 pub revision: u64,
657}
658
659#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
661pub struct EventsSinceParams {
662 pub since: u64,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub limit: Option<usize>,
667}
668
669#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct EventsSinceResult {
672 pub events: Vec<Event>,
674 pub generation: u64,
676}
677
678#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
680pub struct EventsGenResult {
681 pub generation: u64,
683 pub revision: u64,
685}
686
687#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689pub struct IdentityResult {
690 pub identity: String,
692 pub root: String,
694 pub prefix: String,
696 pub version: String,
698}
699
700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
702pub struct CreateParams {
703 pub project: String,
705 pub title: String,
707 #[serde(default, skip_serializing_if = "Option::is_none")]
709 pub agent: Option<String>,
710 #[serde(default, skip_serializing_if = "Option::is_none")]
712 pub priority: Option<char>,
713 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub issue_type: Option<String>,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub deadline: Option<String>,
719 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub scheduled: Option<String>,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
724 pub tags: Option<String>,
725 #[serde(default, skip_serializing_if = "Option::is_none")]
727 pub parent: Option<String>,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub body: Option<String>,
731}
732
733#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
735pub struct UpdateParams {
736 pub id: String,
738 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub state: Option<String>,
741 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub priority: Option<String>,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
746 pub block: Option<String>,
747 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub unblock: Option<String>,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub agent: Option<String>,
753}
754
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
757pub struct ClaimParams {
758 pub id: String,
760 #[serde(default)]
762 pub force: bool,
763 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub agent: Option<String>,
766}
767
768#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
770pub struct NoteParams {
771 pub id: String,
773 pub text: String,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
779pub struct RefileParams {
780 pub id: String,
782 pub to: String,
784}
785
786#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
788pub struct MutResult {
789 pub ok: bool,
791 pub report: String,
793 #[serde(default)]
795 pub issue: Option<IssueDetail>,
796 pub revision: u64,
798 pub generation: u64,
800}
801
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
804pub struct VaultChanged {
805 pub generation: u64,
807 pub revision: u64,
809 #[serde(default)]
811 pub projects: Vec<String>,
812 #[serde(default, skip_serializing_if = "Option::is_none")]
814 pub ids: Option<Vec<String>>,
815}
816
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
819pub struct IssueSelected {
820 pub id: String,
822 pub project: String,
824}
825
826#[derive(Debug, Clone, PartialEq)]
828pub enum Notification {
829 VaultChanged(VaultChanged),
831 IssueSelected(IssueSelected),
833 ServeShuttingDown,
835 Unknown {
837 method: String,
839 params: Value,
841 },
842}
843
844impl Notification {
845 pub fn method(&self) -> &str {
847 match self {
848 Self::VaultChanged(_) => NOTIFY_VAULT_CHANGED,
849 Self::IssueSelected(_) => NOTIFY_ISSUE_SELECTED,
850 Self::ServeShuttingDown => NOTIFY_SHUTTING_DOWN,
851 Self::Unknown { method, .. } => method,
852 }
853 }
854
855 pub fn parse(method: &str, params: Value) -> Self {
857 match method {
858 NOTIFY_VAULT_CHANGED => match serde_json::from_value(params.clone()) {
859 Ok(body) => Self::VaultChanged(body),
860 Err(_) => Self::Unknown {
861 method: method.into(),
862 params,
863 },
864 },
865 NOTIFY_ISSUE_SELECTED => match serde_json::from_value(params.clone()) {
866 Ok(body) => Self::IssueSelected(body),
867 Err(_) => Self::Unknown {
868 method: method.into(),
869 params,
870 },
871 },
872 NOTIFY_SHUTTING_DOWN => Self::ServeShuttingDown,
873 other => Self::Unknown {
874 method: other.into(),
875 params,
876 },
877 }
878 }
879
880 pub fn to_params(&self) -> Value {
882 match self {
883 Self::VaultChanged(body) => serde_json::to_value(body).unwrap_or(Value::Null),
884 Self::IssueSelected(body) => serde_json::to_value(body).unwrap_or(Value::Null),
885 Self::ServeShuttingDown => json!({}),
886 Self::Unknown { params, .. } => params.clone(),
887 }
888 }
889}
890
891#[derive(Debug, Clone, PartialEq)]
893pub enum Request {
894 Initialize(InitializeParams),
896 IdentityGet,
898 IssueList(IssueListParams),
900 IssueGet(IdParams),
902 IssueReady(IssueListParams),
904 IssueSearch(SearchParams),
906 IssueClaims(ClaimsParams),
908 IssueAgenda(AgendaParams),
910 IssueShow(IdParams),
912 IssueExcerpt(IdParams),
914 IssueTree(TreeParams),
916 IssueRelated(RelatedParams),
918 IssueChildren(WalkParams),
920 IssueAncestors(WalkParams),
922 IssueImpact(WalkParams),
924 IssueBacklinks(WalkParams),
926 IssueOpen(IdParams),
928 IssueCreate(CreateParams),
930 IssueUpdate(UpdateParams),
932 IssueClaim(ClaimParams),
934 IssueNote(NoteParams),
936 IssueRefile(RefileParams),
938 ProjectList,
940 EventsSince(EventsSinceParams),
942 EventsGen,
944}
945
946impl Request {
947 pub fn method(&self) -> Method {
949 match self {
950 Self::Initialize(_) => Method::Initialize,
951 Self::IdentityGet => Method::IdentityGet,
952 Self::IssueList(_) => Method::IssueList,
953 Self::IssueGet(_) => Method::IssueGet,
954 Self::IssueReady(_) => Method::IssueReady,
955 Self::IssueSearch(_) => Method::IssueSearch,
956 Self::IssueClaims(_) => Method::IssueClaims,
957 Self::IssueAgenda(_) => Method::IssueAgenda,
958 Self::IssueShow(_) => Method::IssueShow,
959 Self::IssueExcerpt(_) => Method::IssueExcerpt,
960 Self::IssueTree(_) => Method::IssueTree,
961 Self::IssueRelated(_) => Method::IssueRelated,
962 Self::IssueChildren(_) => Method::IssueChildren,
963 Self::IssueAncestors(_) => Method::IssueAncestors,
964 Self::IssueImpact(_) => Method::IssueImpact,
965 Self::IssueBacklinks(_) => Method::IssueBacklinks,
966 Self::IssueOpen(_) => Method::IssueOpen,
967 Self::IssueCreate(_) => Method::IssueCreate,
968 Self::IssueUpdate(_) => Method::IssueUpdate,
969 Self::IssueClaim(_) => Method::IssueClaim,
970 Self::IssueNote(_) => Method::IssueNote,
971 Self::IssueRefile(_) => Method::IssueRefile,
972 Self::ProjectList => Method::ProjectList,
973 Self::EventsSince(_) => Method::EventsSince,
974 Self::EventsGen => Method::EventsGen,
975 }
976 }
977
978 pub fn parse(method: &str, params: Option<Value>) -> Result<Self, JsonRpcError> {
984 let method = Method::parse(method)?;
985 let params = match params {
986 None | Some(Value::Null) => Value::Object(Default::default()),
987 Some(v) => v,
988 };
989 match method {
990 Method::Initialize => Ok(Self::Initialize(parse_initialize_params(¶ms)?)),
991 Method::IdentityGet => Ok(Self::IdentityGet),
992 Method::IssueList => Ok(Self::IssueList(decode_params(params)?)),
993 Method::IssueGet => Ok(Self::IssueGet(decode_params(params)?)),
994 Method::IssueReady => Ok(Self::IssueReady(decode_params(params)?)),
995 Method::IssueSearch => Ok(Self::IssueSearch(decode_params(params)?)),
996 Method::IssueClaims => Ok(Self::IssueClaims(decode_params(params)?)),
997 Method::IssueAgenda => Ok(Self::IssueAgenda(decode_params(params)?)),
998 Method::IssueShow => Ok(Self::IssueShow(decode_params(params)?)),
999 Method::IssueExcerpt => Ok(Self::IssueExcerpt(decode_params(params)?)),
1000 Method::IssueTree => Ok(Self::IssueTree(decode_params(params)?)),
1001 Method::IssueRelated => Ok(Self::IssueRelated(decode_params(params)?)),
1002 Method::IssueChildren => Ok(Self::IssueChildren(decode_params(params)?)),
1003 Method::IssueAncestors => Ok(Self::IssueAncestors(decode_params(params)?)),
1004 Method::IssueImpact => Ok(Self::IssueImpact(decode_params(params)?)),
1005 Method::IssueBacklinks => Ok(Self::IssueBacklinks(decode_params(params)?)),
1006 Method::IssueOpen => Ok(Self::IssueOpen(decode_params(params)?)),
1007 Method::IssueCreate => Ok(Self::IssueCreate(decode_params(params)?)),
1008 Method::IssueUpdate => Ok(Self::IssueUpdate(decode_params(params)?)),
1009 Method::IssueClaim => Ok(Self::IssueClaim(decode_params(params)?)),
1010 Method::IssueNote => Ok(Self::IssueNote(decode_params(params)?)),
1011 Method::IssueRefile => Ok(Self::IssueRefile(decode_params(params)?)),
1012 Method::ProjectList => Ok(Self::ProjectList),
1013 Method::EventsSince => Ok(Self::EventsSince(decode_params(params)?)),
1014 Method::EventsGen => Ok(Self::EventsGen),
1015 }
1016 }
1017
1018 pub fn to_params(&self) -> Value {
1020 match self {
1021 Self::Initialize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1022 Self::IdentityGet | Self::ProjectList | Self::EventsGen => json!({}),
1023 Self::IssueList(p) | Self::IssueReady(p) => {
1024 serde_json::to_value(p).unwrap_or(Value::Null)
1025 }
1026 Self::IssueGet(p) | Self::IssueShow(p) | Self::IssueExcerpt(p) | Self::IssueOpen(p) => {
1027 serde_json::to_value(p).unwrap_or(Value::Null)
1028 }
1029 Self::IssueSearch(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1030 Self::IssueClaims(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1031 Self::IssueAgenda(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1032 Self::IssueTree(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1033 Self::IssueRelated(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1034 Self::IssueChildren(p)
1035 | Self::IssueAncestors(p)
1036 | Self::IssueImpact(p)
1037 | Self::IssueBacklinks(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1038 Self::IssueCreate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1039 Self::IssueUpdate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1040 Self::IssueClaim(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1041 Self::IssueNote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1042 Self::IssueRefile(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1043 Self::EventsSince(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1044 }
1045 }
1046}
1047
1048#[derive(Debug, Clone)]
1050pub enum Response {
1051 Initialize(InitializeResult),
1053 IdentityGet(IdentityResult),
1055 IssueList(IssueListResult),
1057 IssueGet(IssueGetResult),
1059 IssueReady(IssueListResult),
1061 IssueSearch(Vec<SearchHit>),
1063 IssueClaims(Vec<ClaimRow>),
1065 IssueAgenda(Vec<AgendaRow>),
1067 IssueShow(IssueGetResult),
1069 IssueExcerpt(Excerpt),
1071 IssueTree(TreeResult),
1073 IssueRelated(Vec<RelatedHit>),
1075 IssueChildren(Vec<WalkHit>),
1077 IssueAncestors(Vec<WalkHit>),
1079 IssueImpact(Vec<WalkHit>),
1081 IssueBacklinks(Vec<WalkHit>),
1083 IssueOpen(IssueGetResult),
1085 IssueCreate(MutResult),
1087 IssueUpdate(MutResult),
1089 IssueClaim(MutResult),
1091 IssueNote(MutResult),
1093 IssueRefile(MutResult),
1095 ProjectList(ProjectListResult),
1097 EventsSince(EventsSinceResult),
1099 EventsGen(EventsGenResult),
1101}
1102
1103impl Response {
1104 pub fn to_value(&self) -> Result<Value, serde_json::Error> {
1110 match self {
1111 Self::Initialize(v) => serde_json::to_value(v),
1112 Self::IdentityGet(v) => serde_json::to_value(v),
1113 Self::IssueList(v) | Self::IssueReady(v) => serde_json::to_value(v),
1114 Self::IssueGet(v) | Self::IssueShow(v) | Self::IssueOpen(v) => serde_json::to_value(v),
1115 Self::IssueSearch(v) => serde_json::to_value(v),
1116 Self::IssueClaims(v) => serde_json::to_value(v),
1117 Self::IssueAgenda(v) => serde_json::to_value(v),
1118 Self::IssueExcerpt(v) => serde_json::to_value(v),
1119 Self::IssueTree(v) => serde_json::to_value(v),
1120 Self::IssueRelated(v) => serde_json::to_value(v),
1121 Self::IssueChildren(v)
1122 | Self::IssueAncestors(v)
1123 | Self::IssueImpact(v)
1124 | Self::IssueBacklinks(v) => serde_json::to_value(v),
1125 Self::IssueCreate(v)
1126 | Self::IssueUpdate(v)
1127 | Self::IssueClaim(v)
1128 | Self::IssueNote(v)
1129 | Self::IssueRefile(v) => serde_json::to_value(v),
1130 Self::ProjectList(v) => serde_json::to_value(v),
1131 Self::EventsSince(v) => serde_json::to_value(v),
1132 Self::EventsGen(v) => serde_json::to_value(v),
1133 }
1134 }
1135}
1136
1137fn decode_params<T: DeserializeOwned>(value: Value) -> Result<T, JsonRpcError> {
1138 serde_json::from_value(value).map_err(|e| invalid_params(e.to_string()))
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144 use std::collections::BTreeMap;
1145
1146 #[test]
1147 fn initialize_missing_agent_is_invalid_params() {
1148 let err = parse_initialize_params(&json!({
1149 "protocolVersion": 1,
1150 "client": "vissue-tui"
1151 }))
1152 .unwrap_err();
1153 assert_eq!(err.code, INVALID_PARAMS);
1154 assert_eq!(err.message, "agent is required");
1155
1156 let err = parse_initialize_params(&json!({
1157 "protocolVersion": 1,
1158 "agent": ""
1159 }))
1160 .unwrap_err();
1161 assert_eq!(err.code, INVALID_PARAMS);
1162 assert_eq!(err.message, "agent is required");
1163
1164 let err = Request::parse(
1165 "initialize",
1166 Some(json!({"protocolVersion": 1, "agent": " "})),
1167 )
1168 .unwrap_err();
1169 assert_eq!(err.code, INVALID_PARAMS);
1170 }
1171
1172 #[test]
1173 fn protocol_version_2_is_rejected() {
1174 let err = parse_initialize_params(&json!({
1175 "protocolVersion": 2,
1176 "agent": "rg@host"
1177 }))
1178 .unwrap_err();
1179 assert_eq!(err.code, INVALID_PARAMS);
1180 assert_eq!(err.message, "unsupported protocol version");
1181 assert_eq!(err.data, Some(json!({"supported": 1})));
1182 }
1183
1184 #[test]
1185 fn initialize_version_1_is_accepted() {
1186 let params = parse_initialize_params(&json!({
1187 "protocolVersion": 1,
1188 "client": "vissue-tui",
1189 "agent": "rg@host"
1190 }))
1191 .unwrap();
1192 assert_eq!(params.protocol_version, 1);
1193 assert_eq!(params.agent, "rg@host");
1194 assert_eq!(params.client, "vissue-tui");
1195 }
1196
1197 #[test]
1198 fn handshake_fields_are_camel_case() {
1199 let params = InitializeParams {
1200 protocol_version: 1,
1201 client: "vissue-tui".into(),
1202 agent: "rg@host".into(),
1203 };
1204 let value = serde_json::to_value(¶ms).unwrap();
1205 assert_eq!(value["protocolVersion"], 1);
1206 assert!(value.get("protocol_version").is_none());
1207
1208 let result = InitializeResult {
1209 protocol_version: 1,
1210 capabilities: vec!["issue/list".into()],
1211 root: "/tmp/tracker".into(),
1212 prefix: "Software".into(),
1213 generation: 3,
1214 revision: 1,
1215 identity: "rg@host".into(),
1216 };
1217 let value = serde_json::to_value(&result).unwrap();
1218 assert_eq!(value["protocolVersion"], 1);
1219 assert_eq!(value["generation"], 3);
1220 }
1221
1222 #[test]
1223 fn issue_payloads_are_snake_case() {
1224 let params = IssueListParams {
1225 since_revision: Some(41),
1226 ..IssueListParams::default()
1227 };
1228 let value = serde_json::to_value(¶ms).unwrap();
1229 assert_eq!(value["since_revision"], 41);
1230 assert!(value.get("sinceRevision").is_none());
1231 }
1232
1233 #[test]
1234 fn unknown_method_is_not_found() {
1235 let err = Method::parse("issue/fold").unwrap_err();
1236 assert_eq!(err.code, METHOD_NOT_FOUND);
1237 assert_eq!(err.data, Some(json!({"method": "issue/fold"})));
1238 }
1239
1240 #[test]
1241 fn every_v1_capability_parses() {
1242 for name in V1_CAPABILITIES {
1243 assert!(Method::parse(name).is_ok(), "{name}");
1244 }
1245 assert_eq!(Method::Initialize.as_str(), "initialize");
1246 }
1247
1248 #[test]
1249 fn request_parse_roundtrips_issue_get() {
1250 let req = Request::parse("issue/get", Some(json!({"id": "atlas-1a2b"}))).unwrap();
1251 assert_eq!(req.method(), Method::IssueGet);
1252 assert_eq!(req.to_params()["id"], "atlas-1a2b");
1253 }
1254
1255 #[test]
1256 fn missing_id_on_issue_get_is_invalid_params() {
1257 let err = Request::parse("issue/get", Some(json!({}))).unwrap_err();
1258 assert_eq!(err.code, INVALID_PARAMS);
1259 }
1260
1261 #[test]
1262 fn core_errors_carry_data_code() {
1263 let err = error_from_core(&CoreError::IssueNotFound {
1264 id: "atlas-1a2b".into(),
1265 });
1266 assert_eq!(err.code, NOT_FOUND);
1267 assert_eq!(err.data.unwrap()["code"], "not_found");
1268
1269 let err = error_from_core(&CoreError::ClaimConflict {
1270 id: "atlas-1a2b".into(),
1271 holder: "other".into(),
1272 claimed_at: None,
1273 });
1274 assert_eq!(err.code, CONFLICT);
1275 let data = err.data.unwrap();
1276 assert_eq!(data["code"], "conflict");
1277 assert_eq!(data["holder"], "other");
1278
1279 let err = error_from_core(&CoreError::BlockerCycle {
1280 blocker: "a".into(),
1281 issue: "b".into(),
1282 });
1283 assert_eq!(err.code, CYCLE);
1284 let data = err.data.unwrap();
1285 assert_eq!(data["code"], "cycle");
1286 assert_eq!(data["id"], "b");
1287 assert_eq!(data["block"], "a");
1288
1289 let err = error_from_core(&CoreError::InvalidState {
1290 id: "atlas-4g5h".into(),
1291 state: "DONE".into(),
1292 });
1293 assert_eq!(err.code, INVALID_STATE);
1294 assert_eq!(err.data.unwrap()["code"], "invalid_state");
1295 }
1296
1297 #[test]
1298 fn notification_parse_known_methods() {
1299 let n = Notification::parse(
1300 NOTIFY_VAULT_CHANGED,
1301 json!({"generation": 1, "revision": 2, "projects": ["atlas"]}),
1302 );
1303 assert!(matches!(n, Notification::VaultChanged(_)));
1304 assert_eq!(n.method(), NOTIFY_VAULT_CHANGED);
1305
1306 let n = Notification::parse(
1307 NOTIFY_ISSUE_SELECTED,
1308 json!({"id": "atlas-1a2b", "project": "atlas"}),
1309 );
1310 assert!(matches!(n, Notification::IssueSelected(_)));
1311
1312 let n = Notification::parse(NOTIFY_SHUTTING_DOWN, json!({}));
1313 assert!(matches!(n, Notification::ServeShuttingDown));
1314 assert_eq!(n.to_params(), json!({}));
1315 }
1316
1317 #[test]
1318 fn list_unchanged_deserializes_without_rows() {
1319 let page: IssueListResult =
1320 serde_json::from_value(json!({"unchanged": true, "revision": 41})).unwrap();
1321 assert!(page.unchanged);
1322 assert!(page.issues.is_empty());
1323 assert_eq!(page.revision, 41);
1324 }
1325
1326 #[test]
1327 fn response_to_value_serializes_initialize() {
1328 let resp = Response::Initialize(InitializeResult {
1329 protocol_version: 1,
1330 capabilities: V1_CAPABILITIES.iter().map(|s| (*s).to_string()).collect(),
1331 root: "/tmp".into(),
1332 prefix: "Software".into(),
1333 generation: 1,
1334 revision: 1,
1335 identity: "agent".into(),
1336 });
1337 let value = resp.to_value().unwrap();
1338 assert_eq!(value["protocolVersion"], 1);
1339 assert!(
1340 value["capabilities"]
1341 .as_array()
1342 .unwrap()
1343 .contains(&json!("issue/list"))
1344 );
1345 }
1346
1347 #[test]
1348 fn envelope_helpers_roundtrip() {
1349 let req = JsonRpcRequest::call(JsonRpcId::Number(1), "identity/get", json!({}));
1350 let bytes = serde_json::to_vec(&req).unwrap();
1351 let back: JsonRpcRequest = serde_json::from_slice(&bytes).unwrap();
1352 assert_eq!(back.method, "identity/get");
1353 assert!(!back.is_notification());
1354
1355 let note = JsonRpcRequest::notification(NOTIFY_SHUTTING_DOWN, json!({}));
1356 assert!(note.is_notification());
1357
1358 let ok = JsonRpcResponse::ok(Some(JsonRpcId::Number(1)), json!({"ok": true}));
1359 assert_eq!(ok.result.unwrap()["ok"], true);
1360 let err = JsonRpcResponse::err(Some(JsonRpcId::Null), parse_error());
1361 assert_eq!(err.error.unwrap().code, PARSE_ERROR);
1362 }
1363
1364 #[test]
1365 fn mut_and_walk_params_decode() {
1366 let claim = Request::parse(
1367 "issue/claim",
1368 Some(json!({"id": "atlas-1a2b", "force": true})),
1369 )
1370 .unwrap();
1371 match claim {
1372 Request::IssueClaim(p) => {
1373 assert!(p.force);
1374 assert_eq!(p.id, "atlas-1a2b");
1375 }
1376 other => panic!("{other:?}"),
1377 }
1378 let create = Request::parse(
1379 "issue/create",
1380 Some(json!({"project": "atlas", "title": "x"})),
1381 )
1382 .unwrap();
1383 assert_eq!(create.method(), Method::IssueCreate);
1384 assert!(Request::parse("issue/create", Some(json!({"title": "x"}))).is_err());
1385 assert_eq!(
1386 Request::parse("events/gen", None).unwrap().method(),
1387 Method::EventsGen
1388 );
1389 let _ = Request::IssueNote(NoteParams {
1390 id: "a".into(),
1391 text: "n".into(),
1392 })
1393 .to_params();
1394 let _ = Request::IssueRefile(RefileParams {
1395 id: "a".into(),
1396 to: "b".into(),
1397 })
1398 .to_params();
1399 let _ = Request::IssueUpdate(UpdateParams {
1400 id: "a".into(),
1401 state: Some("STARTED".into()),
1402 priority: None,
1403 block: None,
1404 unblock: None,
1405 agent: None,
1406 })
1407 .to_params();
1408 let _ = Request::EventsSince(EventsSinceParams {
1409 since: 0,
1410 limit: Some(10),
1411 })
1412 .to_params();
1413 let _ = Request::IssueTree(TreeParams {
1414 id: "a".into(),
1415 format: Some("ascii".into()),
1416 })
1417 .to_params();
1418 let _ = Request::IssueRelated(RelatedParams {
1419 id: "a".into(),
1420 depth: Some(2),
1421 limit: Some(20),
1422 })
1423 .to_params();
1424 let _ = Request::IssueChildren(WalkParams {
1425 id: "a".into(),
1426 depth: None,
1427 })
1428 .to_params();
1429 let _ = Request::IssueSearch(SearchParams {
1430 query: "q".into(),
1431 limit: None,
1432 })
1433 .to_params();
1434 let _ = Request::IssueClaims(ClaimsParams::default()).to_params();
1435 let _ = Request::IssueAgenda(AgendaParams::default()).to_params();
1436 let _ = Request::IdentityGet.to_params();
1437 }
1438
1439 #[test]
1440 fn response_variants_serialize() {
1441 let detail = IssueDetail {
1442 id: "atlas-1a2b".into(),
1443 project: "atlas".into(),
1444 title: "t".into(),
1445 state: "TODO".into(),
1446 priority: "B".into(),
1447 properties: BTreeMap::new(),
1448 org_tags: vec![],
1449 tags: vec![],
1450 blocked_by: vec![],
1451 parent: None,
1452 claimed_by: None,
1453 claimed_at: None,
1454 file: "issues.org:1-2".into(),
1455 line_start: 1,
1456 line_end: 2,
1457 body: "what the issue asks for".into(),
1458 logbook: vec![],
1459 };
1460 let get = IssueGetResult {
1461 issue: detail.clone(),
1462 revision: 1,
1463 };
1464 assert!(Response::IssueGet(get.clone()).to_value().unwrap()["id"] == "atlas-1a2b");
1465 assert!(Response::IssueShow(get.clone()).to_value().is_ok());
1466 assert!(Response::IssueOpen(get).to_value().is_ok());
1467 assert!(
1468 Response::IssueExcerpt(Excerpt {
1469 id: "atlas-1a2b".into(),
1470 file: "issues.org".into(),
1471 line_start: 1,
1472 line_end: 2,
1473 text: "body".into(),
1474 suppressed: false,
1475 })
1476 .to_value()
1477 .is_ok()
1478 );
1479 assert!(Response::IssueSearch(vec![]).to_value().unwrap().is_array());
1480 assert!(Response::IssueClaims(vec![]).to_value().unwrap().is_array());
1481 assert!(Response::IssueAgenda(vec![]).to_value().unwrap().is_array());
1482 assert!(
1483 Response::IssueRelated(vec![])
1484 .to_value()
1485 .unwrap()
1486 .is_array()
1487 );
1488 assert!(
1489 Response::IssueChildren(vec![])
1490 .to_value()
1491 .unwrap()
1492 .is_array()
1493 );
1494 assert!(
1495 Response::IssueAncestors(vec![])
1496 .to_value()
1497 .unwrap()
1498 .is_array()
1499 );
1500 assert!(Response::IssueImpact(vec![]).to_value().unwrap().is_array());
1501 assert!(
1502 Response::IssueBacklinks(vec![])
1503 .to_value()
1504 .unwrap()
1505 .is_array()
1506 );
1507 assert!(
1508 Response::ProjectList(ProjectListResult {
1509 projects: vec!["atlas".into()],
1510 revision: 1,
1511 })
1512 .to_value()
1513 .is_ok()
1514 );
1515 assert!(
1516 Response::EventsGen(EventsGenResult {
1517 generation: 1,
1518 revision: 1,
1519 })
1520 .to_value()
1521 .is_ok()
1522 );
1523 assert!(
1524 Response::EventsSince(EventsSinceResult {
1525 events: vec![],
1526 generation: 1,
1527 })
1528 .to_value()
1529 .is_ok()
1530 );
1531 assert!(
1532 Response::IdentityGet(IdentityResult {
1533 identity: "a".into(),
1534 root: "/".into(),
1535 prefix: "Software".into(),
1536 version: "0.2.0".into(),
1537 })
1538 .to_value()
1539 .is_ok()
1540 );
1541 let mut_ok = MutResult {
1542 ok: true,
1543 report: "ok".into(),
1544 issue: Some(detail),
1545 revision: 2,
1546 generation: 3,
1547 };
1548 assert!(Response::IssueClaim(mut_ok.clone()).to_value().is_ok());
1549 assert!(Response::IssueCreate(mut_ok.clone()).to_value().is_ok());
1550 assert!(Response::IssueUpdate(mut_ok.clone()).to_value().is_ok());
1551 assert!(Response::IssueNote(mut_ok.clone()).to_value().is_ok());
1552 assert!(Response::IssueRefile(mut_ok).to_value().is_ok());
1553 assert!(
1554 Response::IssueTree(TreeResult::Text { text: "* a".into() })
1555 .to_value()
1556 .is_ok()
1557 );
1558 assert!(
1559 Response::IssueList(IssueListResult {
1560 revision: 1,
1561 ..IssueListResult::default()
1562 })
1563 .to_value()
1564 .is_ok()
1565 );
1566 assert!(
1567 Response::IssueReady(IssueListResult {
1568 revision: 1,
1569 ..IssueListResult::default()
1570 })
1571 .to_value()
1572 .is_ok()
1573 );
1574 }
1575
1576 #[test]
1577 fn parse_every_method_with_minimal_params() {
1578 let id = json!({"id": "atlas-1a2b"});
1579 for (method, params) in [
1580 ("identity/get", json!({})),
1581 ("issue/list", json!({})),
1582 ("issue/get", id.clone()),
1583 ("issue/ready", json!({})),
1584 ("issue/search", json!({"query": "q"})),
1585 ("issue/claims", json!({})),
1586 ("issue/agenda", json!({})),
1587 ("issue/show", id.clone()),
1588 ("issue/excerpt", id.clone()),
1589 ("issue/tree", id.clone()),
1590 ("issue/related", id.clone()),
1591 ("issue/children", id.clone()),
1592 ("issue/ancestors", id.clone()),
1593 ("issue/impact", id.clone()),
1594 ("issue/backlinks", id.clone()),
1595 ("issue/open", id.clone()),
1596 ("issue/create", json!({"project": "atlas", "title": "t"})),
1597 ("issue/update", id.clone()),
1598 ("issue/claim", id.clone()),
1599 ("issue/note", json!({"id": "atlas-1a2b", "text": "n"})),
1600 ("issue/refile", json!({"id": "atlas-1a2b", "to": "beacon"})),
1601 ("project/list", json!({})),
1602 ("events/since", json!({"since": 0})),
1603 ("events/gen", json!({})),
1604 ] {
1605 let req = Request::parse(method, Some(params)).expect(method);
1606 assert_eq!(req.method().as_str(), method);
1607 let _ = req.to_params();
1608 }
1609 }
1610
1611 #[test]
1612 fn helper_errors_have_stable_codes() {
1613 assert_eq!(invalid_request().code, INVALID_REQUEST);
1614 assert_eq!(internal_error("x").code, INTERNAL_ERROR);
1615 assert_eq!(parse_error().code, PARSE_ERROR);
1616 let err = Error::Rpc(invalid_params("agent is required"));
1617 assert_eq!(err.to_string(), "agent is required");
1618 let _ = Error::Unsupported("unix only");
1619 let _ = Notification::parse("vault/changed", json!(null));
1620 let _ = Notification::parse("issue/selected", json!(null));
1621 let _ = Notification::parse("other/x", json!({"a": 1}));
1622 let n = Notification::Unknown {
1623 method: "x".into(),
1624 params: json!({"a": 1}),
1625 };
1626 assert_eq!(n.to_params()["a"], 1);
1627 assert_eq!(n.method(), "x");
1628 }
1629}