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::DuplicateId { id, paths } => JsonRpcError {
264 code: CONFLICT,
265 message: err.to_string(),
266 data: Some(json!({
267 "code": "duplicate_id",
268 "id": id,
269 "paths": paths,
270 })),
271 },
272 CoreError::ClaimConflict { id, holder, .. } => JsonRpcError {
273 code: CONFLICT,
274 message: err.to_string(),
275 data: Some(json!({ "code": "conflict", "id": id, "holder": holder })),
276 },
277 CoreError::BlockerCycle { blocker, issue } => JsonRpcError {
278 code: CYCLE,
279 message: err.to_string(),
280 data: Some(json!({ "code": "cycle", "id": issue, "block": blocker })),
281 },
282 CoreError::InvalidState { id, state } => JsonRpcError {
283 code: INVALID_STATE,
284 message: err.to_string(),
285 data: Some(json!({ "code": "invalid_state", "id": id, "state": state })),
286 },
287 CoreError::StaleWrite {
288 id,
289 expected_state,
290 actual_state,
291 expected_gen,
292 actual_gen,
293 } => JsonRpcError {
294 code: INVALID_STATE,
295 message: err.to_string(),
296 data: Some(json!({
297 "code": "stale",
298 "id": id,
299 "expected_state": expected_state,
300 "actual_state": actual_state,
301 "expected_gen": expected_gen,
302 "actual_gen": actual_gen,
303 })),
304 },
305 CoreError::TerminalConflict {
306 id,
307 held,
308 attempted,
309 } => JsonRpcError {
310 code: CONFLICT,
311 message: err.to_string(),
312 data: Some(json!({
313 "code": "terminal_conflict",
314 "id": id,
315 "held": held,
316 "attempted": attempted,
317 })),
318 },
319 CoreError::Other(_) => internal_error(err.to_string()),
320 }
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
325pub enum Method {
326 Initialize,
328 IdentityGet,
330 IssueList,
332 IssueGet,
334 IssueReady,
336 IssueSearch,
338 IssueClaims,
340 IssueAgenda,
342 IssueShow,
344 IssueExcerpt,
346 IssueTree,
348 IssueRelated,
350 IssueChildren,
352 IssueAncestors,
354 IssueImpact,
356 IssueBacklinks,
358 IssueOpen,
360 IssueCreate,
362 IssueUpdate,
364 IssueClaim,
366 IssueNote,
368 IssueRefile,
370 ProjectList,
372 EventsSince,
374 EventsGen,
376}
377
378impl Method {
379 pub fn as_str(self) -> &'static str {
381 match self {
382 Self::Initialize => "initialize",
383 Self::IdentityGet => "identity/get",
384 Self::IssueList => "issue/list",
385 Self::IssueGet => "issue/get",
386 Self::IssueReady => "issue/ready",
387 Self::IssueSearch => "issue/search",
388 Self::IssueClaims => "issue/claims",
389 Self::IssueAgenda => "issue/agenda",
390 Self::IssueShow => "issue/show",
391 Self::IssueExcerpt => "issue/excerpt",
392 Self::IssueTree => "issue/tree",
393 Self::IssueRelated => "issue/related",
394 Self::IssueChildren => "issue/children",
395 Self::IssueAncestors => "issue/ancestors",
396 Self::IssueImpact => "issue/impact",
397 Self::IssueBacklinks => "issue/backlinks",
398 Self::IssueOpen => "issue/open",
399 Self::IssueCreate => "issue/create",
400 Self::IssueUpdate => "issue/update",
401 Self::IssueClaim => "issue/claim",
402 Self::IssueNote => "issue/note",
403 Self::IssueRefile => "issue/refile",
404 Self::ProjectList => "project/list",
405 Self::EventsSince => "events/since",
406 Self::EventsGen => "events/gen",
407 }
408 }
409
410 pub fn parse(name: &str) -> Result<Self, JsonRpcError> {
416 match name {
417 "initialize" => Ok(Self::Initialize),
418 "identity/get" => Ok(Self::IdentityGet),
419 "issue/list" => Ok(Self::IssueList),
420 "issue/get" => Ok(Self::IssueGet),
421 "issue/ready" => Ok(Self::IssueReady),
422 "issue/search" => Ok(Self::IssueSearch),
423 "issue/claims" => Ok(Self::IssueClaims),
424 "issue/agenda" => Ok(Self::IssueAgenda),
425 "issue/show" => Ok(Self::IssueShow),
426 "issue/excerpt" => Ok(Self::IssueExcerpt),
427 "issue/tree" => Ok(Self::IssueTree),
428 "issue/related" => Ok(Self::IssueRelated),
429 "issue/children" => Ok(Self::IssueChildren),
430 "issue/ancestors" => Ok(Self::IssueAncestors),
431 "issue/impact" => Ok(Self::IssueImpact),
432 "issue/backlinks" => Ok(Self::IssueBacklinks),
433 "issue/open" => Ok(Self::IssueOpen),
434 "issue/create" => Ok(Self::IssueCreate),
435 "issue/update" => Ok(Self::IssueUpdate),
436 "issue/claim" => Ok(Self::IssueClaim),
437 "issue/note" => Ok(Self::IssueNote),
438 "issue/refile" => Ok(Self::IssueRefile),
439 "project/list" => Ok(Self::ProjectList),
440 "events/since" => Ok(Self::EventsSince),
441 "events/gen" => Ok(Self::EventsGen),
442 other => Err(method_not_found(other)),
443 }
444 }
445}
446
447pub const V1_CAPABILITIES: &[&str] = &[
449 "issue/list",
450 "issue/get",
451 "issue/ready",
452 "issue/search",
453 "issue/claims",
454 "issue/agenda",
455 "issue/show",
456 "issue/excerpt",
457 "issue/tree",
458 "issue/related",
459 "issue/children",
460 "issue/ancestors",
461 "issue/impact",
462 "issue/backlinks",
463 "issue/open",
464 "issue/create",
465 "issue/update",
466 "issue/claim",
467 "issue/note",
468 "issue/refile",
469 "project/list",
470 "events/since",
471 "events/gen",
472 "identity/get",
473];
474
475#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
477#[serde(rename_all = "camelCase")]
478pub struct InitializeParams {
479 pub protocol_version: u32,
481 #[serde(default)]
483 pub client: String,
484 pub agent: String,
486}
487
488#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
490#[serde(rename_all = "camelCase")]
491pub struct InitializeResult {
492 pub protocol_version: u32,
494 pub capabilities: Vec<String>,
496 pub root: String,
498 pub prefix: String,
500 pub generation: u64,
502 pub revision: u64,
504 pub identity: String,
506}
507
508pub fn parse_initialize_params(value: &Value) -> Result<InitializeParams, JsonRpcError> {
515 let obj = value
516 .as_object()
517 .ok_or_else(|| invalid_params("params must be an object"))?;
518 let version = match obj.get("protocolVersion") {
519 Some(Value::Number(n)) => n
520 .as_u64()
521 .ok_or_else(|| invalid_params("protocolVersion must be a number"))?,
522 Some(_) => return Err(invalid_params("protocolVersion must be a number")),
523 None => return Err(invalid_params("protocolVersion is required")),
524 };
525 if version != u64::from(PROTOCOL_VERSION) {
526 return Err(JsonRpcError {
527 code: INVALID_PARAMS,
528 message: "unsupported protocol version".into(),
529 data: Some(json!({ "supported": PROTOCOL_VERSION })),
530 });
531 }
532 let agent = match obj.get("agent") {
533 Some(Value::String(s)) if !s.trim().is_empty() => s.clone(),
534 _ => return Err(invalid_params("agent is required")),
535 };
536 let client = obj
537 .get("client")
538 .and_then(Value::as_str)
539 .unwrap_or("")
540 .to_string();
541 Ok(InitializeParams {
542 protocol_version: PROTOCOL_VERSION,
543 client,
544 agent,
545 })
546}
547
548#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
550pub struct IssueListParams {
551 #[serde(default, skip_serializing_if = "Option::is_none")]
553 pub project: Option<String>,
554 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub state: Option<String>,
557 #[serde(default, skip_serializing_if = "Option::is_none")]
559 pub ready: Option<bool>,
560 #[serde(default, skip_serializing_if = "Option::is_none")]
562 pub query: Option<String>,
563 #[serde(default, skip_serializing_if = "Option::is_none")]
565 pub limit: Option<usize>,
566 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub offset: Option<usize>,
569 #[serde(default, skip_serializing_if = "Option::is_none")]
571 pub since_revision: Option<u64>,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
576pub struct IssueListResult {
577 #[serde(default)]
579 pub issues: Vec<IssueRow>,
580 #[serde(default)]
582 pub total: u64,
583 #[serde(default)]
585 pub matched: u64,
586 pub revision: u64,
588 #[serde(default)]
590 pub generation: u64,
591 #[serde(default)]
593 pub unchanged: bool,
594}
595
596#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
598pub struct IdParams {
599 pub id: String,
601}
602
603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
605pub struct IssueGetResult {
606 #[serde(flatten)]
608 pub issue: IssueDetail,
609 pub revision: u64,
611}
612
613#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
615pub struct SearchParams {
616 pub query: String,
618 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub limit: Option<usize>,
621}
622
623#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
625pub struct ClaimsParams {
626 #[serde(default, skip_serializing_if = "Option::is_none")]
628 pub holder: Option<String>,
629 #[serde(default, skip_serializing_if = "Option::is_none")]
631 pub project: Option<String>,
632}
633
634#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
636pub struct AgendaParams {
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub days: Option<i64>,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub project: Option<String>,
643}
644
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647pub struct TreeParams {
648 pub id: String,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub format: Option<String>,
653}
654
655#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
657#[serde(untagged)]
658pub enum TreeResult {
659 Nodes(TreeNode),
661 Text {
663 text: String,
665 },
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
670pub struct RelatedParams {
671 pub id: String,
673 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub depth: Option<usize>,
676 #[serde(default, skip_serializing_if = "Option::is_none")]
678 pub limit: Option<usize>,
679}
680
681#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
683pub struct WalkParams {
684 pub id: String,
686 #[serde(default, skip_serializing_if = "Option::is_none")]
688 pub depth: Option<usize>,
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
693pub struct ProjectListResult {
694 pub projects: Vec<String>,
696 pub revision: u64,
698}
699
700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
702pub struct EventsSinceParams {
703 pub since: u64,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub limit: Option<usize>,
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
712pub struct EventsSinceResult {
713 pub events: Vec<Event>,
715 pub generation: u64,
717}
718
719#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
721pub struct EventsGenResult {
722 pub generation: u64,
724 pub revision: u64,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
730pub struct IdentityResult {
731 pub identity: String,
733 pub root: String,
735 pub prefix: String,
737 pub version: String,
739}
740
741#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
743pub struct CreateParams {
744 pub project: String,
746 pub title: String,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
750 pub agent: Option<String>,
751 #[serde(default, skip_serializing_if = "Option::is_none")]
753 pub priority: Option<char>,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub issue_type: Option<String>,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub deadline: Option<String>,
760 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub scheduled: Option<String>,
763 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub tags: Option<String>,
766 #[serde(default, skip_serializing_if = "Option::is_none")]
768 pub parent: Option<String>,
769 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub body: Option<String>,
772}
773
774#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
776pub struct UpdateParams {
777 pub id: String,
779 #[serde(default, skip_serializing_if = "Option::is_none")]
781 pub state: Option<String>,
782 #[serde(default, skip_serializing_if = "Option::is_none")]
784 pub priority: Option<String>,
785 #[serde(default, skip_serializing_if = "Option::is_none")]
787 pub block: Option<String>,
788 #[serde(default, skip_serializing_if = "Option::is_none")]
790 pub unblock: Option<String>,
791 #[serde(default, skip_serializing_if = "Option::is_none")]
793 pub if_state: Option<String>,
794 #[serde(default, skip_serializing_if = "Option::is_none")]
796 pub if_gen: Option<u64>,
797 #[serde(default, skip_serializing_if = "Option::is_none")]
799 pub agent: Option<String>,
800}
801
802#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
804pub struct ClaimParams {
805 pub id: String,
807 #[serde(default)]
809 pub force: bool,
810 #[serde(default, skip_serializing_if = "Option::is_none")]
812 pub agent: Option<String>,
813}
814
815#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
817pub struct NoteParams {
818 pub id: String,
820 pub text: String,
822}
823
824#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
826pub struct RefileParams {
827 pub id: String,
829 pub to: String,
831}
832
833#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
835pub struct MutResult {
836 pub ok: bool,
838 pub report: String,
840 #[serde(default)]
842 pub issue: Option<IssueDetail>,
843 pub revision: u64,
845 pub generation: u64,
847}
848
849#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
851pub struct VaultChanged {
852 pub generation: u64,
854 pub revision: u64,
856 #[serde(default)]
858 pub projects: Vec<String>,
859 #[serde(default, skip_serializing_if = "Option::is_none")]
861 pub ids: Option<Vec<String>>,
862}
863
864#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
866pub struct IssueSelected {
867 pub id: String,
869 pub project: String,
871}
872
873#[derive(Debug, Clone, PartialEq)]
875pub enum Notification {
876 VaultChanged(VaultChanged),
878 IssueSelected(IssueSelected),
880 ServeShuttingDown,
882 Unknown {
884 method: String,
886 params: Value,
888 },
889}
890
891impl Notification {
892 pub fn method(&self) -> &str {
894 match self {
895 Self::VaultChanged(_) => NOTIFY_VAULT_CHANGED,
896 Self::IssueSelected(_) => NOTIFY_ISSUE_SELECTED,
897 Self::ServeShuttingDown => NOTIFY_SHUTTING_DOWN,
898 Self::Unknown { method, .. } => method,
899 }
900 }
901
902 pub fn parse(method: &str, params: Value) -> Self {
904 match method {
905 NOTIFY_VAULT_CHANGED => match serde_json::from_value(params.clone()) {
906 Ok(body) => Self::VaultChanged(body),
907 Err(_) => Self::Unknown {
908 method: method.into(),
909 params,
910 },
911 },
912 NOTIFY_ISSUE_SELECTED => match serde_json::from_value(params.clone()) {
913 Ok(body) => Self::IssueSelected(body),
914 Err(_) => Self::Unknown {
915 method: method.into(),
916 params,
917 },
918 },
919 NOTIFY_SHUTTING_DOWN => Self::ServeShuttingDown,
920 other => Self::Unknown {
921 method: other.into(),
922 params,
923 },
924 }
925 }
926
927 pub fn to_params(&self) -> Value {
929 match self {
930 Self::VaultChanged(body) => serde_json::to_value(body).unwrap_or(Value::Null),
931 Self::IssueSelected(body) => serde_json::to_value(body).unwrap_or(Value::Null),
932 Self::ServeShuttingDown => json!({}),
933 Self::Unknown { params, .. } => params.clone(),
934 }
935 }
936}
937
938#[derive(Debug, Clone, PartialEq)]
940pub enum Request {
941 Initialize(InitializeParams),
943 IdentityGet,
945 IssueList(IssueListParams),
947 IssueGet(IdParams),
949 IssueReady(IssueListParams),
951 IssueSearch(SearchParams),
953 IssueClaims(ClaimsParams),
955 IssueAgenda(AgendaParams),
957 IssueShow(IdParams),
959 IssueExcerpt(IdParams),
961 IssueTree(TreeParams),
963 IssueRelated(RelatedParams),
965 IssueChildren(WalkParams),
967 IssueAncestors(WalkParams),
969 IssueImpact(WalkParams),
971 IssueBacklinks(WalkParams),
973 IssueOpen(IdParams),
975 IssueCreate(CreateParams),
977 IssueUpdate(UpdateParams),
979 IssueClaim(ClaimParams),
981 IssueNote(NoteParams),
983 IssueRefile(RefileParams),
985 ProjectList,
987 EventsSince(EventsSinceParams),
989 EventsGen,
991}
992
993impl Request {
994 pub fn method(&self) -> Method {
996 match self {
997 Self::Initialize(_) => Method::Initialize,
998 Self::IdentityGet => Method::IdentityGet,
999 Self::IssueList(_) => Method::IssueList,
1000 Self::IssueGet(_) => Method::IssueGet,
1001 Self::IssueReady(_) => Method::IssueReady,
1002 Self::IssueSearch(_) => Method::IssueSearch,
1003 Self::IssueClaims(_) => Method::IssueClaims,
1004 Self::IssueAgenda(_) => Method::IssueAgenda,
1005 Self::IssueShow(_) => Method::IssueShow,
1006 Self::IssueExcerpt(_) => Method::IssueExcerpt,
1007 Self::IssueTree(_) => Method::IssueTree,
1008 Self::IssueRelated(_) => Method::IssueRelated,
1009 Self::IssueChildren(_) => Method::IssueChildren,
1010 Self::IssueAncestors(_) => Method::IssueAncestors,
1011 Self::IssueImpact(_) => Method::IssueImpact,
1012 Self::IssueBacklinks(_) => Method::IssueBacklinks,
1013 Self::IssueOpen(_) => Method::IssueOpen,
1014 Self::IssueCreate(_) => Method::IssueCreate,
1015 Self::IssueUpdate(_) => Method::IssueUpdate,
1016 Self::IssueClaim(_) => Method::IssueClaim,
1017 Self::IssueNote(_) => Method::IssueNote,
1018 Self::IssueRefile(_) => Method::IssueRefile,
1019 Self::ProjectList => Method::ProjectList,
1020 Self::EventsSince(_) => Method::EventsSince,
1021 Self::EventsGen => Method::EventsGen,
1022 }
1023 }
1024
1025 pub fn parse(method: &str, params: Option<Value>) -> Result<Self, JsonRpcError> {
1031 let method = Method::parse(method)?;
1032 let params = match params {
1033 None | Some(Value::Null) => Value::Object(Default::default()),
1034 Some(v) => v,
1035 };
1036 match method {
1037 Method::Initialize => Ok(Self::Initialize(parse_initialize_params(¶ms)?)),
1038 Method::IdentityGet => Ok(Self::IdentityGet),
1039 Method::IssueList => Ok(Self::IssueList(decode_params(params)?)),
1040 Method::IssueGet => Ok(Self::IssueGet(decode_params(params)?)),
1041 Method::IssueReady => Ok(Self::IssueReady(decode_params(params)?)),
1042 Method::IssueSearch => Ok(Self::IssueSearch(decode_params(params)?)),
1043 Method::IssueClaims => Ok(Self::IssueClaims(decode_params(params)?)),
1044 Method::IssueAgenda => Ok(Self::IssueAgenda(decode_params(params)?)),
1045 Method::IssueShow => Ok(Self::IssueShow(decode_params(params)?)),
1046 Method::IssueExcerpt => Ok(Self::IssueExcerpt(decode_params(params)?)),
1047 Method::IssueTree => Ok(Self::IssueTree(decode_params(params)?)),
1048 Method::IssueRelated => Ok(Self::IssueRelated(decode_params(params)?)),
1049 Method::IssueChildren => Ok(Self::IssueChildren(decode_params(params)?)),
1050 Method::IssueAncestors => Ok(Self::IssueAncestors(decode_params(params)?)),
1051 Method::IssueImpact => Ok(Self::IssueImpact(decode_params(params)?)),
1052 Method::IssueBacklinks => Ok(Self::IssueBacklinks(decode_params(params)?)),
1053 Method::IssueOpen => Ok(Self::IssueOpen(decode_params(params)?)),
1054 Method::IssueCreate => Ok(Self::IssueCreate(decode_params(params)?)),
1055 Method::IssueUpdate => Ok(Self::IssueUpdate(decode_params(params)?)),
1056 Method::IssueClaim => Ok(Self::IssueClaim(decode_params(params)?)),
1057 Method::IssueNote => Ok(Self::IssueNote(decode_params(params)?)),
1058 Method::IssueRefile => Ok(Self::IssueRefile(decode_params(params)?)),
1059 Method::ProjectList => Ok(Self::ProjectList),
1060 Method::EventsSince => Ok(Self::EventsSince(decode_params(params)?)),
1061 Method::EventsGen => Ok(Self::EventsGen),
1062 }
1063 }
1064
1065 pub fn to_params(&self) -> Value {
1067 match self {
1068 Self::Initialize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1069 Self::IdentityGet | Self::ProjectList | Self::EventsGen => json!({}),
1070 Self::IssueList(p) | Self::IssueReady(p) => {
1071 serde_json::to_value(p).unwrap_or(Value::Null)
1072 }
1073 Self::IssueGet(p) | Self::IssueShow(p) | Self::IssueExcerpt(p) | Self::IssueOpen(p) => {
1074 serde_json::to_value(p).unwrap_or(Value::Null)
1075 }
1076 Self::IssueSearch(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1077 Self::IssueClaims(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1078 Self::IssueAgenda(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1079 Self::IssueTree(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1080 Self::IssueRelated(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1081 Self::IssueChildren(p)
1082 | Self::IssueAncestors(p)
1083 | Self::IssueImpact(p)
1084 | Self::IssueBacklinks(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1085 Self::IssueCreate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1086 Self::IssueUpdate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1087 Self::IssueClaim(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1088 Self::IssueNote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1089 Self::IssueRefile(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1090 Self::EventsSince(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1091 }
1092 }
1093}
1094
1095#[derive(Debug, Clone)]
1097pub enum Response {
1098 Initialize(InitializeResult),
1100 IdentityGet(IdentityResult),
1102 IssueList(IssueListResult),
1104 IssueGet(IssueGetResult),
1106 IssueReady(IssueListResult),
1108 IssueSearch(Vec<SearchHit>),
1110 IssueClaims(Vec<ClaimRow>),
1112 IssueAgenda(Vec<AgendaRow>),
1114 IssueShow(IssueGetResult),
1116 IssueExcerpt(Excerpt),
1118 IssueTree(TreeResult),
1120 IssueRelated(Vec<RelatedHit>),
1122 IssueChildren(Vec<WalkHit>),
1124 IssueAncestors(Vec<WalkHit>),
1126 IssueImpact(Vec<WalkHit>),
1128 IssueBacklinks(Vec<WalkHit>),
1130 IssueOpen(IssueGetResult),
1132 IssueCreate(MutResult),
1134 IssueUpdate(MutResult),
1136 IssueClaim(MutResult),
1138 IssueNote(MutResult),
1140 IssueRefile(MutResult),
1142 ProjectList(ProjectListResult),
1144 EventsSince(EventsSinceResult),
1146 EventsGen(EventsGenResult),
1148}
1149
1150impl Response {
1151 pub fn to_value(&self) -> Result<Value, serde_json::Error> {
1157 match self {
1158 Self::Initialize(v) => serde_json::to_value(v),
1159 Self::IdentityGet(v) => serde_json::to_value(v),
1160 Self::IssueList(v) | Self::IssueReady(v) => serde_json::to_value(v),
1161 Self::IssueGet(v) | Self::IssueShow(v) | Self::IssueOpen(v) => serde_json::to_value(v),
1162 Self::IssueSearch(v) => serde_json::to_value(v),
1163 Self::IssueClaims(v) => serde_json::to_value(v),
1164 Self::IssueAgenda(v) => serde_json::to_value(v),
1165 Self::IssueExcerpt(v) => serde_json::to_value(v),
1166 Self::IssueTree(v) => serde_json::to_value(v),
1167 Self::IssueRelated(v) => serde_json::to_value(v),
1168 Self::IssueChildren(v)
1169 | Self::IssueAncestors(v)
1170 | Self::IssueImpact(v)
1171 | Self::IssueBacklinks(v) => serde_json::to_value(v),
1172 Self::IssueCreate(v)
1173 | Self::IssueUpdate(v)
1174 | Self::IssueClaim(v)
1175 | Self::IssueNote(v)
1176 | Self::IssueRefile(v) => serde_json::to_value(v),
1177 Self::ProjectList(v) => serde_json::to_value(v),
1178 Self::EventsSince(v) => serde_json::to_value(v),
1179 Self::EventsGen(v) => serde_json::to_value(v),
1180 }
1181 }
1182}
1183
1184fn decode_params<T: DeserializeOwned>(value: Value) -> Result<T, JsonRpcError> {
1185 serde_json::from_value(value).map_err(|e| invalid_params(e.to_string()))
1186}
1187
1188#[cfg(test)]
1189mod tests {
1190 use super::*;
1191 use std::collections::BTreeMap;
1192
1193 #[test]
1194 fn initialize_missing_agent_is_invalid_params() {
1195 let err = parse_initialize_params(&json!({
1196 "protocolVersion": 1,
1197 "client": "vissue-tui"
1198 }))
1199 .unwrap_err();
1200 assert_eq!(err.code, INVALID_PARAMS);
1201 assert_eq!(err.message, "agent is required");
1202
1203 let err = parse_initialize_params(&json!({
1204 "protocolVersion": 1,
1205 "agent": ""
1206 }))
1207 .unwrap_err();
1208 assert_eq!(err.code, INVALID_PARAMS);
1209 assert_eq!(err.message, "agent is required");
1210
1211 let err = Request::parse(
1212 "initialize",
1213 Some(json!({"protocolVersion": 1, "agent": " "})),
1214 )
1215 .unwrap_err();
1216 assert_eq!(err.code, INVALID_PARAMS);
1217 }
1218
1219 #[test]
1220 fn protocol_version_2_is_rejected() {
1221 let err = parse_initialize_params(&json!({
1222 "protocolVersion": 2,
1223 "agent": "rg@host"
1224 }))
1225 .unwrap_err();
1226 assert_eq!(err.code, INVALID_PARAMS);
1227 assert_eq!(err.message, "unsupported protocol version");
1228 assert_eq!(err.data, Some(json!({"supported": 1})));
1229 }
1230
1231 #[test]
1232 fn initialize_version_1_is_accepted() {
1233 let params = parse_initialize_params(&json!({
1234 "protocolVersion": 1,
1235 "client": "vissue-tui",
1236 "agent": "rg@host"
1237 }))
1238 .unwrap();
1239 assert_eq!(params.protocol_version, 1);
1240 assert_eq!(params.agent, "rg@host");
1241 assert_eq!(params.client, "vissue-tui");
1242 }
1243
1244 #[test]
1245 fn handshake_fields_are_camel_case() {
1246 let params = InitializeParams {
1247 protocol_version: 1,
1248 client: "vissue-tui".into(),
1249 agent: "rg@host".into(),
1250 };
1251 let value = serde_json::to_value(¶ms).unwrap();
1252 assert_eq!(value["protocolVersion"], 1);
1253 assert!(value.get("protocol_version").is_none());
1254
1255 let result = InitializeResult {
1256 protocol_version: 1,
1257 capabilities: vec!["issue/list".into()],
1258 root: "/tmp/tracker".into(),
1259 prefix: "Software".into(),
1260 generation: 3,
1261 revision: 1,
1262 identity: "rg@host".into(),
1263 };
1264 let value = serde_json::to_value(&result).unwrap();
1265 assert_eq!(value["protocolVersion"], 1);
1266 assert_eq!(value["generation"], 3);
1267 }
1268
1269 #[test]
1270 fn issue_payloads_are_snake_case() {
1271 let params = IssueListParams {
1272 since_revision: Some(41),
1273 ..IssueListParams::default()
1274 };
1275 let value = serde_json::to_value(¶ms).unwrap();
1276 assert_eq!(value["since_revision"], 41);
1277 assert!(value.get("sinceRevision").is_none());
1278 }
1279
1280 #[test]
1281 fn unknown_method_is_not_found() {
1282 let err = Method::parse("issue/fold").unwrap_err();
1283 assert_eq!(err.code, METHOD_NOT_FOUND);
1284 assert_eq!(err.data, Some(json!({"method": "issue/fold"})));
1285 }
1286
1287 #[test]
1288 fn every_v1_capability_parses() {
1289 for name in V1_CAPABILITIES {
1290 assert!(Method::parse(name).is_ok(), "{name}");
1291 }
1292 assert_eq!(Method::Initialize.as_str(), "initialize");
1293 }
1294
1295 #[test]
1296 fn request_parse_roundtrips_issue_get() {
1297 let req = Request::parse("issue/get", Some(json!({"id": "atlas-1a2b"}))).unwrap();
1298 assert_eq!(req.method(), Method::IssueGet);
1299 assert_eq!(req.to_params()["id"], "atlas-1a2b");
1300 }
1301
1302 #[test]
1303 fn missing_id_on_issue_get_is_invalid_params() {
1304 let err = Request::parse("issue/get", Some(json!({}))).unwrap_err();
1305 assert_eq!(err.code, INVALID_PARAMS);
1306 }
1307
1308 #[test]
1309 fn core_errors_carry_data_code() {
1310 let err = error_from_core(&CoreError::IssueNotFound {
1311 id: "atlas-1a2b".into(),
1312 });
1313 assert_eq!(err.code, NOT_FOUND);
1314 assert_eq!(err.data.unwrap()["code"], "not_found");
1315
1316 let err = error_from_core(&CoreError::ClaimConflict {
1317 id: "atlas-1a2b".into(),
1318 holder: "other".into(),
1319 claimed_at: None,
1320 });
1321 assert_eq!(err.code, CONFLICT);
1322 let data = err.data.unwrap();
1323 assert_eq!(data["code"], "conflict");
1324 assert_eq!(data["holder"], "other");
1325
1326 let err = error_from_core(&CoreError::BlockerCycle {
1327 blocker: "a".into(),
1328 issue: "b".into(),
1329 });
1330 assert_eq!(err.code, CYCLE);
1331 let data = err.data.unwrap();
1332 assert_eq!(data["code"], "cycle");
1333 assert_eq!(data["id"], "b");
1334 assert_eq!(data["block"], "a");
1335
1336 let err = error_from_core(&CoreError::InvalidState {
1337 id: "atlas-4g5h".into(),
1338 state: "DONE".into(),
1339 });
1340 assert_eq!(err.code, INVALID_STATE);
1341 assert_eq!(err.data.unwrap()["code"], "invalid_state");
1342
1343 let err = error_from_core(&CoreError::DuplicateId {
1344 id: "atlas-1a2b".into(),
1345 paths: vec![
1346 std::path::PathBuf::from("/a/issues.org"),
1347 std::path::PathBuf::from("/b/issues.org"),
1348 ],
1349 });
1350 assert_eq!(err.code, CONFLICT);
1351 assert_eq!(err.data.unwrap()["code"], "duplicate_id");
1352 }
1353
1354 #[test]
1355 fn notification_parse_known_methods() {
1356 let n = Notification::parse(
1357 NOTIFY_VAULT_CHANGED,
1358 json!({"generation": 1, "revision": 2, "projects": ["atlas"]}),
1359 );
1360 assert!(matches!(n, Notification::VaultChanged(_)));
1361 assert_eq!(n.method(), NOTIFY_VAULT_CHANGED);
1362
1363 let n = Notification::parse(
1364 NOTIFY_ISSUE_SELECTED,
1365 json!({"id": "atlas-1a2b", "project": "atlas"}),
1366 );
1367 assert!(matches!(n, Notification::IssueSelected(_)));
1368
1369 let n = Notification::parse(NOTIFY_SHUTTING_DOWN, json!({}));
1370 assert!(matches!(n, Notification::ServeShuttingDown));
1371 assert_eq!(n.to_params(), json!({}));
1372 }
1373
1374 #[test]
1375 fn list_unchanged_deserializes_without_rows() {
1376 let page: IssueListResult =
1377 serde_json::from_value(json!({"unchanged": true, "revision": 41})).unwrap();
1378 assert!(page.unchanged);
1379 assert!(page.issues.is_empty());
1380 assert_eq!(page.revision, 41);
1381 }
1382
1383 #[test]
1384 fn response_to_value_serializes_initialize() {
1385 let resp = Response::Initialize(InitializeResult {
1386 protocol_version: 1,
1387 capabilities: V1_CAPABILITIES.iter().map(|s| (*s).to_string()).collect(),
1388 root: "/tmp".into(),
1389 prefix: "Software".into(),
1390 generation: 1,
1391 revision: 1,
1392 identity: "agent".into(),
1393 });
1394 let value = resp.to_value().unwrap();
1395 assert_eq!(value["protocolVersion"], 1);
1396 assert!(
1397 value["capabilities"]
1398 .as_array()
1399 .unwrap()
1400 .contains(&json!("issue/list"))
1401 );
1402 }
1403
1404 #[test]
1405 fn envelope_helpers_roundtrip() {
1406 let req = JsonRpcRequest::call(JsonRpcId::Number(1), "identity/get", json!({}));
1407 let bytes = serde_json::to_vec(&req).unwrap();
1408 let back: JsonRpcRequest = serde_json::from_slice(&bytes).unwrap();
1409 assert_eq!(back.method, "identity/get");
1410 assert!(!back.is_notification());
1411
1412 let note = JsonRpcRequest::notification(NOTIFY_SHUTTING_DOWN, json!({}));
1413 assert!(note.is_notification());
1414
1415 let ok = JsonRpcResponse::ok(Some(JsonRpcId::Number(1)), json!({"ok": true}));
1416 assert_eq!(ok.result.unwrap()["ok"], true);
1417 let err = JsonRpcResponse::err(Some(JsonRpcId::Null), parse_error());
1418 assert_eq!(err.error.unwrap().code, PARSE_ERROR);
1419 }
1420
1421 #[test]
1422 fn mut_and_walk_params_decode() {
1423 let claim = Request::parse(
1424 "issue/claim",
1425 Some(json!({"id": "atlas-1a2b", "force": true})),
1426 )
1427 .unwrap();
1428 match claim {
1429 Request::IssueClaim(p) => {
1430 assert!(p.force);
1431 assert_eq!(p.id, "atlas-1a2b");
1432 }
1433 other => panic!("{other:?}"),
1434 }
1435 let create = Request::parse(
1436 "issue/create",
1437 Some(json!({"project": "atlas", "title": "x"})),
1438 )
1439 .unwrap();
1440 assert_eq!(create.method(), Method::IssueCreate);
1441 assert!(Request::parse("issue/create", Some(json!({"title": "x"}))).is_err());
1442 assert_eq!(
1443 Request::parse("events/gen", None).unwrap().method(),
1444 Method::EventsGen
1445 );
1446 let _ = Request::IssueNote(NoteParams {
1447 id: "a".into(),
1448 text: "n".into(),
1449 })
1450 .to_params();
1451 let _ = Request::IssueRefile(RefileParams {
1452 id: "a".into(),
1453 to: "b".into(),
1454 })
1455 .to_params();
1456 let _ = Request::IssueUpdate(UpdateParams {
1457 id: "a".into(),
1458 state: Some("STARTED".into()),
1459 priority: None,
1460 block: None,
1461 unblock: None,
1462 if_state: None,
1463 if_gen: None,
1464 agent: None,
1465 })
1466 .to_params();
1467 let _ = Request::EventsSince(EventsSinceParams {
1468 since: 0,
1469 limit: Some(10),
1470 })
1471 .to_params();
1472 let _ = Request::IssueTree(TreeParams {
1473 id: "a".into(),
1474 format: Some("ascii".into()),
1475 })
1476 .to_params();
1477 let _ = Request::IssueRelated(RelatedParams {
1478 id: "a".into(),
1479 depth: Some(2),
1480 limit: Some(20),
1481 })
1482 .to_params();
1483 let _ = Request::IssueChildren(WalkParams {
1484 id: "a".into(),
1485 depth: None,
1486 })
1487 .to_params();
1488 let _ = Request::IssueSearch(SearchParams {
1489 query: "q".into(),
1490 limit: None,
1491 })
1492 .to_params();
1493 let _ = Request::IssueClaims(ClaimsParams::default()).to_params();
1494 let _ = Request::IssueAgenda(AgendaParams::default()).to_params();
1495 let _ = Request::IdentityGet.to_params();
1496 }
1497
1498 #[test]
1499 fn response_variants_serialize() {
1500 let detail = IssueDetail {
1501 id: "atlas-1a2b".into(),
1502 project: "atlas".into(),
1503 title: "t".into(),
1504 state: "TODO".into(),
1505 priority: "B".into(),
1506 properties: BTreeMap::new(),
1507 org_tags: vec![],
1508 tags: vec![],
1509 blocked_by: vec![],
1510 parent: None,
1511 claimed_by: None,
1512 claimed_at: None,
1513 file: "issues.org:1-2".into(),
1514 line_start: 1,
1515 line_end: 2,
1516 body: "what the issue asks for".into(),
1517 logbook: vec![],
1518 };
1519 let get = IssueGetResult {
1520 issue: detail.clone(),
1521 revision: 1,
1522 };
1523 assert!(Response::IssueGet(get.clone()).to_value().unwrap()["id"] == "atlas-1a2b");
1524 assert!(Response::IssueShow(get.clone()).to_value().is_ok());
1525 assert!(Response::IssueOpen(get).to_value().is_ok());
1526 assert!(
1527 Response::IssueExcerpt(Excerpt {
1528 id: "atlas-1a2b".into(),
1529 file: "issues.org".into(),
1530 line_start: 1,
1531 line_end: 2,
1532 text: "body".into(),
1533 suppressed: false,
1534 })
1535 .to_value()
1536 .is_ok()
1537 );
1538 assert!(Response::IssueSearch(vec![]).to_value().unwrap().is_array());
1539 assert!(Response::IssueClaims(vec![]).to_value().unwrap().is_array());
1540 assert!(Response::IssueAgenda(vec![]).to_value().unwrap().is_array());
1541 assert!(
1542 Response::IssueRelated(vec![])
1543 .to_value()
1544 .unwrap()
1545 .is_array()
1546 );
1547 assert!(
1548 Response::IssueChildren(vec![])
1549 .to_value()
1550 .unwrap()
1551 .is_array()
1552 );
1553 assert!(
1554 Response::IssueAncestors(vec![])
1555 .to_value()
1556 .unwrap()
1557 .is_array()
1558 );
1559 assert!(Response::IssueImpact(vec![]).to_value().unwrap().is_array());
1560 assert!(
1561 Response::IssueBacklinks(vec![])
1562 .to_value()
1563 .unwrap()
1564 .is_array()
1565 );
1566 assert!(
1567 Response::ProjectList(ProjectListResult {
1568 projects: vec!["atlas".into()],
1569 revision: 1,
1570 })
1571 .to_value()
1572 .is_ok()
1573 );
1574 assert!(
1575 Response::EventsGen(EventsGenResult {
1576 generation: 1,
1577 revision: 1,
1578 })
1579 .to_value()
1580 .is_ok()
1581 );
1582 assert!(
1583 Response::EventsSince(EventsSinceResult {
1584 events: vec![],
1585 generation: 1,
1586 })
1587 .to_value()
1588 .is_ok()
1589 );
1590 assert!(
1591 Response::IdentityGet(IdentityResult {
1592 identity: "a".into(),
1593 root: "/".into(),
1594 prefix: "Software".into(),
1595 version: "0.2.0".into(),
1596 })
1597 .to_value()
1598 .is_ok()
1599 );
1600 let mut_ok = MutResult {
1601 ok: true,
1602 report: "ok".into(),
1603 issue: Some(detail),
1604 revision: 2,
1605 generation: 3,
1606 };
1607 assert!(Response::IssueClaim(mut_ok.clone()).to_value().is_ok());
1608 assert!(Response::IssueCreate(mut_ok.clone()).to_value().is_ok());
1609 assert!(Response::IssueUpdate(mut_ok.clone()).to_value().is_ok());
1610 assert!(Response::IssueNote(mut_ok.clone()).to_value().is_ok());
1611 assert!(Response::IssueRefile(mut_ok).to_value().is_ok());
1612 assert!(
1613 Response::IssueTree(TreeResult::Text { text: "* a".into() })
1614 .to_value()
1615 .is_ok()
1616 );
1617 assert!(
1618 Response::IssueList(IssueListResult {
1619 revision: 1,
1620 ..IssueListResult::default()
1621 })
1622 .to_value()
1623 .is_ok()
1624 );
1625 assert!(
1626 Response::IssueReady(IssueListResult {
1627 revision: 1,
1628 ..IssueListResult::default()
1629 })
1630 .to_value()
1631 .is_ok()
1632 );
1633 }
1634
1635 #[test]
1636 fn parse_every_method_with_minimal_params() {
1637 let id = json!({"id": "atlas-1a2b"});
1638 for (method, params) in [
1639 ("identity/get", json!({})),
1640 ("issue/list", json!({})),
1641 ("issue/get", id.clone()),
1642 ("issue/ready", json!({})),
1643 ("issue/search", json!({"query": "q"})),
1644 ("issue/claims", json!({})),
1645 ("issue/agenda", json!({})),
1646 ("issue/show", id.clone()),
1647 ("issue/excerpt", id.clone()),
1648 ("issue/tree", id.clone()),
1649 ("issue/related", id.clone()),
1650 ("issue/children", id.clone()),
1651 ("issue/ancestors", id.clone()),
1652 ("issue/impact", id.clone()),
1653 ("issue/backlinks", id.clone()),
1654 ("issue/open", id.clone()),
1655 ("issue/create", json!({"project": "atlas", "title": "t"})),
1656 ("issue/update", id.clone()),
1657 ("issue/claim", id.clone()),
1658 ("issue/note", json!({"id": "atlas-1a2b", "text": "n"})),
1659 ("issue/refile", json!({"id": "atlas-1a2b", "to": "beacon"})),
1660 ("project/list", json!({})),
1661 ("events/since", json!({"since": 0})),
1662 ("events/gen", json!({})),
1663 ] {
1664 let req = Request::parse(method, Some(params)).expect(method);
1665 assert_eq!(req.method().as_str(), method);
1666 let _ = req.to_params();
1667 }
1668 }
1669
1670 #[test]
1671 fn helper_errors_have_stable_codes() {
1672 assert_eq!(invalid_request().code, INVALID_REQUEST);
1673 assert_eq!(internal_error("x").code, INTERNAL_ERROR);
1674 assert_eq!(parse_error().code, PARSE_ERROR);
1675 let err = Error::Rpc(invalid_params("agent is required"));
1676 assert_eq!(err.to_string(), "agent is required");
1677 let _ = Error::Unsupported("unix only");
1678 let _ = Notification::parse("vault/changed", json!(null));
1679 let _ = Notification::parse("issue/selected", json!(null));
1680 let _ = Notification::parse("other/x", json!({"a": 1}));
1681 let n = Notification::Unknown {
1682 method: "x".into(),
1683 params: json!({"a": 1}),
1684 };
1685 assert_eq!(n.to_params()["a"], 1);
1686 assert_eq!(n.method(), "x");
1687 }
1688}