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 IssueAppend,
372 IssueReject,
374 IssueResolve,
376 IssueVote,
378 IssueFold,
380 IssueNormalize,
382 IssueCheck,
384 IssueCount,
386 IssueCycles,
388 IssueDigest,
390 IssueExport,
392 IssueGraph,
394 IssueRoadmap,
396 IssueStale,
398 IssueHygiene,
400 IssueWaitingOn,
402 IssueMirror,
404 EventsPing,
406 EventsWait,
408 ProjectList,
410 EventsSince,
412 EventsGen,
414}
415
416impl Method {
417 pub fn as_str(self) -> &'static str {
419 match self {
420 Self::Initialize => "initialize",
421 Self::IdentityGet => "identity/get",
422 Self::IssueList => "issue/list",
423 Self::IssueGet => "issue/get",
424 Self::IssueReady => "issue/ready",
425 Self::IssueSearch => "issue/search",
426 Self::IssueClaims => "issue/claims",
427 Self::IssueAgenda => "issue/agenda",
428 Self::IssueShow => "issue/show",
429 Self::IssueExcerpt => "issue/excerpt",
430 Self::IssueTree => "issue/tree",
431 Self::IssueRelated => "issue/related",
432 Self::IssueChildren => "issue/children",
433 Self::IssueAncestors => "issue/ancestors",
434 Self::IssueImpact => "issue/impact",
435 Self::IssueBacklinks => "issue/backlinks",
436 Self::IssueOpen => "issue/open",
437 Self::IssueCreate => "issue/create",
438 Self::IssueUpdate => "issue/update",
439 Self::IssueClaim => "issue/claim",
440 Self::IssueNote => "issue/note",
441 Self::IssueRefile => "issue/refile",
442 Self::ProjectList => "project/list",
443 Self::EventsSince => "events/since",
444 Self::EventsGen => "events/gen",
445 Self::IssueAppend => "issue/append",
446 Self::IssueReject => "issue/reject",
447 Self::IssueResolve => "issue/resolve",
448 Self::IssueVote => "issue/vote",
449 Self::IssueFold => "issue/fold",
450 Self::IssueNormalize => "issue/normalize",
451 Self::IssueCheck => "issue/check",
452 Self::IssueCount => "issue/count",
453 Self::IssueCycles => "issue/cycles",
454 Self::IssueDigest => "issue/digest",
455 Self::IssueExport => "issue/export",
456 Self::IssueGraph => "issue/graph",
457 Self::IssueRoadmap => "issue/roadmap",
458 Self::IssueStale => "issue/stale",
459 Self::IssueHygiene => "issue/hygiene",
460 Self::IssueWaitingOn => "issue/waiting_on",
461 Self::IssueMirror => "issue/mirror_check",
462 Self::EventsPing => "events/ping",
463 Self::EventsWait => "events/wait",
464 }
465 }
466
467 pub fn parse(name: &str) -> Result<Self, JsonRpcError> {
473 match name {
474 "initialize" => Ok(Self::Initialize),
475 "identity/get" => Ok(Self::IdentityGet),
476 "issue/list" => Ok(Self::IssueList),
477 "issue/get" => Ok(Self::IssueGet),
478 "issue/ready" => Ok(Self::IssueReady),
479 "issue/search" => Ok(Self::IssueSearch),
480 "issue/claims" => Ok(Self::IssueClaims),
481 "issue/agenda" => Ok(Self::IssueAgenda),
482 "issue/show" => Ok(Self::IssueShow),
483 "issue/excerpt" => Ok(Self::IssueExcerpt),
484 "issue/tree" => Ok(Self::IssueTree),
485 "issue/related" => Ok(Self::IssueRelated),
486 "issue/children" => Ok(Self::IssueChildren),
487 "issue/ancestors" => Ok(Self::IssueAncestors),
488 "issue/impact" => Ok(Self::IssueImpact),
489 "issue/backlinks" => Ok(Self::IssueBacklinks),
490 "issue/open" => Ok(Self::IssueOpen),
491 "issue/create" => Ok(Self::IssueCreate),
492 "issue/update" => Ok(Self::IssueUpdate),
493 "issue/claim" => Ok(Self::IssueClaim),
494 "issue/note" => Ok(Self::IssueNote),
495 "issue/refile" => Ok(Self::IssueRefile),
496 "project/list" => Ok(Self::ProjectList),
497 "events/since" => Ok(Self::EventsSince),
498 "events/gen" => Ok(Self::EventsGen),
499 "issue/append" => Ok(Self::IssueAppend),
500 "issue/reject" => Ok(Self::IssueReject),
501 "issue/resolve" => Ok(Self::IssueResolve),
502 "issue/vote" => Ok(Self::IssueVote),
503 "issue/fold" => Ok(Self::IssueFold),
504 "issue/normalize" => Ok(Self::IssueNormalize),
505 "issue/check" => Ok(Self::IssueCheck),
506 "issue/count" => Ok(Self::IssueCount),
507 "issue/cycles" => Ok(Self::IssueCycles),
508 "issue/digest" => Ok(Self::IssueDigest),
509 "issue/export" => Ok(Self::IssueExport),
510 "issue/graph" => Ok(Self::IssueGraph),
511 "issue/roadmap" => Ok(Self::IssueRoadmap),
512 "issue/stale" => Ok(Self::IssueStale),
513 "issue/hygiene" => Ok(Self::IssueHygiene),
514 "issue/waiting_on" => Ok(Self::IssueWaitingOn),
515 "issue/mirror_check" => Ok(Self::IssueMirror),
516 "events/ping" => Ok(Self::EventsPing),
517 "events/wait" => Ok(Self::EventsWait),
518 other => Err(method_not_found(other)),
519 }
520 }
521}
522
523pub const V1_CAPABILITIES: &[&str] = &[
533 "issue/list",
534 "issue/get",
535 "issue/ready",
536 "issue/search",
537 "issue/claims",
538 "issue/agenda",
539 "issue/show",
540 "issue/excerpt",
541 "issue/tree",
542 "issue/related",
543 "issue/children",
544 "issue/ancestors",
545 "issue/impact",
546 "issue/backlinks",
547 "issue/open",
548 "issue/create",
549 "issue/update",
550 "issue/claim",
551 "issue/note",
552 "issue/refile",
553 "issue/append",
554 "issue/reject",
555 "issue/resolve",
556 "issue/vote",
557 "issue/fold",
558 "issue/normalize",
559 "issue/check",
560 "issue/count",
561 "issue/cycles",
562 "issue/digest",
563 "issue/export",
564 "issue/graph",
565 "issue/roadmap",
566 "issue/stale",
567 "issue/hygiene",
568 "issue/waiting_on",
569 "issue/mirror_check",
570 "project/list",
571 "events/since",
572 "events/gen",
573 "events/ping",
574 "events/wait",
575 "identity/get",
576];
577
578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
580#[serde(rename_all = "camelCase")]
581pub struct InitializeParams {
582 pub protocol_version: u32,
584 #[serde(default)]
586 pub client: String,
587 pub agent: String,
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
593#[serde(rename_all = "camelCase")]
594pub struct InitializeResult {
595 pub protocol_version: u32,
597 pub capabilities: Vec<String>,
599 pub root: String,
601 pub prefix: String,
603 pub generation: u64,
605 pub revision: u64,
607 pub identity: String,
609}
610
611pub fn parse_initialize_params(value: &Value) -> Result<InitializeParams, JsonRpcError> {
618 let obj = value
619 .as_object()
620 .ok_or_else(|| invalid_params("params must be an object"))?;
621 let version = match obj.get("protocolVersion") {
622 Some(Value::Number(n)) => n
623 .as_u64()
624 .ok_or_else(|| invalid_params("protocolVersion must be a number"))?,
625 Some(_) => return Err(invalid_params("protocolVersion must be a number")),
626 None => return Err(invalid_params("protocolVersion is required")),
627 };
628 if version != u64::from(PROTOCOL_VERSION) {
629 return Err(JsonRpcError {
630 code: INVALID_PARAMS,
631 message: "unsupported protocol version".into(),
632 data: Some(json!({ "supported": PROTOCOL_VERSION })),
633 });
634 }
635 let agent = match obj.get("agent") {
636 Some(Value::String(s)) if !s.trim().is_empty() => s.clone(),
637 _ => return Err(invalid_params("agent is required")),
638 };
639 let client = obj
640 .get("client")
641 .and_then(Value::as_str)
642 .unwrap_or("")
643 .to_string();
644 Ok(InitializeParams {
645 protocol_version: PROTOCOL_VERSION,
646 client,
647 agent,
648 })
649}
650
651#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
653pub struct IssueListParams {
654 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub project: Option<String>,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
659 pub state: Option<String>,
660 #[serde(default, skip_serializing_if = "Option::is_none")]
662 pub ready: Option<bool>,
663 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub query: Option<String>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
668 pub limit: Option<usize>,
669 #[serde(default, skip_serializing_if = "Option::is_none")]
671 pub offset: Option<usize>,
672 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub since_revision: Option<u64>,
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
679pub struct IssueListResult {
680 #[serde(default)]
682 pub issues: Vec<IssueRow>,
683 #[serde(default)]
685 pub total: u64,
686 #[serde(default)]
688 pub matched: u64,
689 pub revision: u64,
691 #[serde(default)]
693 pub generation: u64,
694 #[serde(default)]
696 pub unchanged: bool,
697}
698
699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
701pub struct IdParams {
702 pub id: String,
704}
705
706#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708pub struct IssueGetResult {
709 #[serde(flatten)]
711 pub issue: IssueDetail,
712 pub revision: u64,
714}
715
716#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
718pub struct SearchParams {
719 pub query: String,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub limit: Option<usize>,
724}
725
726#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
728pub struct ClaimsParams {
729 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub holder: Option<String>,
732 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub project: Option<String>,
735}
736
737#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
739pub struct AgendaParams {
740 #[serde(default, skip_serializing_if = "Option::is_none")]
742 pub days: Option<i64>,
743 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub project: Option<String>,
746}
747
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
750pub struct TreeParams {
751 pub id: String,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub format: Option<String>,
756}
757
758#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
760#[serde(untagged)]
761pub enum TreeResult {
762 Nodes(TreeNode),
764 Text {
766 text: String,
768 },
769}
770
771#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
773pub struct RelatedParams {
774 pub id: String,
776 #[serde(default, skip_serializing_if = "Option::is_none")]
778 pub depth: Option<usize>,
779 #[serde(default, skip_serializing_if = "Option::is_none")]
781 pub limit: Option<usize>,
782}
783
784#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
786pub struct WalkParams {
787 pub id: String,
789 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub depth: Option<usize>,
792}
793
794#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
796pub struct ProjectListResult {
797 pub projects: Vec<String>,
799 pub revision: u64,
801}
802
803#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
805pub struct EventsSinceParams {
806 pub since: u64,
808 #[serde(default, skip_serializing_if = "Option::is_none")]
810 pub limit: Option<usize>,
811}
812
813#[derive(Debug, Clone, Serialize, Deserialize)]
815pub struct EventsSinceResult {
816 pub events: Vec<Event>,
818 pub generation: u64,
820}
821
822#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
824pub struct EventsGenResult {
825 pub generation: u64,
827 pub revision: u64,
829}
830
831#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
833pub struct IdentityResult {
834 pub identity: String,
836 pub root: String,
838 pub prefix: String,
840 pub version: String,
842}
843
844#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
846pub struct CreateParams {
847 pub project: String,
849 pub title: String,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
853 pub agent: Option<String>,
854 #[serde(default, skip_serializing_if = "Option::is_none")]
856 pub priority: Option<char>,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
859 pub issue_type: Option<String>,
860 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub deadline: Option<String>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
865 pub scheduled: Option<String>,
866 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub tags: Option<String>,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub parent: Option<String>,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub body: Option<String>,
875}
876
877#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
879pub struct UpdateParams {
880 pub id: String,
882 #[serde(default, skip_serializing_if = "Option::is_none")]
884 pub state: Option<String>,
885 #[serde(default, skip_serializing_if = "Option::is_none")]
887 pub priority: Option<String>,
888 #[serde(default, skip_serializing_if = "Option::is_none")]
890 pub block: Option<String>,
891 #[serde(default, skip_serializing_if = "Option::is_none")]
893 pub unblock: Option<String>,
894 #[serde(default, skip_serializing_if = "Option::is_none")]
896 pub if_state: Option<String>,
897 #[serde(default, skip_serializing_if = "Option::is_none")]
899 pub if_gen: Option<u64>,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
902 pub agent: Option<String>,
903}
904
905#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
907pub struct ClaimParams {
908 pub id: String,
910 #[serde(default)]
912 pub force: bool,
913 #[serde(default, skip_serializing_if = "Option::is_none")]
915 pub agent: Option<String>,
916}
917
918#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
920pub struct VoteParams {
921 pub id: String,
923 #[serde(default, skip_serializing_if = "Option::is_none")]
925 pub choice: Option<String>,
926 #[serde(default, skip_serializing_if = "Option::is_none")]
928 pub agent: Option<String>,
929}
930
931#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
934pub struct ProjectFilterParams {
935 #[serde(default, skip_serializing_if = "Option::is_none")]
937 pub project: Option<String>,
938}
939
940#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
942pub struct CountParams {
943 #[serde(default, skip_serializing_if = "Option::is_none")]
945 pub project: Option<String>,
946 #[serde(default, skip_serializing_if = "Option::is_none")]
948 pub state: Option<String>,
949 #[serde(default)]
951 pub ready_only: bool,
952}
953
954#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
956pub struct StaleParams {
957 pub days: i64,
959 #[serde(default, skip_serializing_if = "Option::is_none")]
961 pub project: Option<String>,
962}
963
964#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
966pub struct HygieneParams {
967 #[serde(default, skip_serializing_if = "Option::is_none")]
969 pub stale_days: Option<i64>,
970}
971
972#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
974pub struct PingParams {
975 #[serde(default, skip_serializing_if = "Option::is_none")]
977 pub detail: Option<String>,
978}
979
980#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
983pub struct WaitParams {
984 #[serde(default)]
986 pub last: u64,
987 #[serde(default, skip_serializing_if = "Option::is_none")]
989 pub id: Option<String>,
990 #[serde(default, skip_serializing_if = "Option::is_none")]
992 pub poll_ms: Option<u64>,
993 #[serde(default, skip_serializing_if = "Option::is_none")]
995 pub timeout_ms: Option<u64>,
996}
997
998#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1000pub struct MirrorCheckParams {
1001 pub path: String,
1003 #[serde(default)]
1006 pub projects: Vec<String>,
1007}
1008
1009#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1011pub struct MirrorCheckResult {
1012 pub fresh: bool,
1014 pub report: String,
1016}
1017
1018#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1020pub struct DigestParams {
1021 #[serde(default)]
1023 pub projects: Vec<String>,
1024}
1025
1026#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1032pub struct ReportResult {
1033 pub report: String,
1035}
1036
1037#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1040pub struct CheckResult {
1041 pub report: String,
1043 pub errors: usize,
1045 pub warnings: usize,
1047}
1048
1049#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1051pub struct ProjectDigestResult {
1052 pub project: String,
1054 pub digest: String,
1056 pub issues: usize,
1058}
1059
1060#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1062pub struct DigestResult {
1063 pub combined: String,
1065 pub issues: usize,
1067 pub generation: u64,
1070 pub projects: Vec<ProjectDigestResult>,
1072}
1073
1074#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1077pub struct WaitResult {
1078 pub generation: u64,
1080 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub state: Option<String>,
1083 #[serde(default)]
1085 pub timed_out: bool,
1086}
1087
1088#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1090pub struct AppendParams {
1091 pub id: String,
1093 pub text: String,
1095 #[serde(default, skip_serializing_if = "Option::is_none")]
1097 pub agent: Option<String>,
1098}
1099
1100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1102pub struct ResolveParams {
1103 pub id: String,
1105 pub state: String,
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1112pub struct RejectParams {
1113 pub id: String,
1115 #[serde(default, skip_serializing_if = "Option::is_none")]
1117 pub to: Option<String>,
1118 #[serde(default, skip_serializing_if = "Option::is_none")]
1120 pub project: Option<String>,
1121 #[serde(default, skip_serializing_if = "Option::is_none")]
1123 pub title: Option<String>,
1124 #[serde(default, skip_serializing_if = "Option::is_none")]
1126 pub reason: Option<String>,
1127}
1128
1129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1131pub struct FoldParams {
1132 pub file: String,
1134 #[serde(default, skip_serializing_if = "Option::is_none")]
1136 pub project: Option<String>,
1137}
1138
1139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1141pub struct NormalizeParams {
1142 #[serde(default, skip_serializing_if = "Option::is_none")]
1144 pub project: Option<String>,
1145 #[serde(default)]
1147 pub dry_run: bool,
1148}
1149
1150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1152pub struct NoteParams {
1153 pub id: String,
1155 pub text: String,
1157}
1158
1159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1161pub struct RefileParams {
1162 pub id: String,
1164 pub to: String,
1166}
1167
1168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1170pub struct MutResult {
1171 pub ok: bool,
1173 pub report: String,
1175 #[serde(default)]
1177 pub issue: Option<IssueDetail>,
1178 pub revision: u64,
1180 pub generation: u64,
1182}
1183
1184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1186pub struct VaultChanged {
1187 pub generation: u64,
1189 pub revision: u64,
1191 #[serde(default)]
1193 pub projects: Vec<String>,
1194 #[serde(default, skip_serializing_if = "Option::is_none")]
1196 pub ids: Option<Vec<String>>,
1197}
1198
1199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1201pub struct IssueSelected {
1202 pub id: String,
1204 pub project: String,
1206}
1207
1208#[derive(Debug, Clone, PartialEq)]
1210pub enum Notification {
1211 VaultChanged(VaultChanged),
1213 IssueSelected(IssueSelected),
1215 ServeShuttingDown,
1217 Unknown {
1219 method: String,
1221 params: Value,
1223 },
1224}
1225
1226impl Notification {
1227 pub fn method(&self) -> &str {
1229 match self {
1230 Self::VaultChanged(_) => NOTIFY_VAULT_CHANGED,
1231 Self::IssueSelected(_) => NOTIFY_ISSUE_SELECTED,
1232 Self::ServeShuttingDown => NOTIFY_SHUTTING_DOWN,
1233 Self::Unknown { method, .. } => method,
1234 }
1235 }
1236
1237 pub fn parse(method: &str, params: Value) -> Self {
1239 match method {
1240 NOTIFY_VAULT_CHANGED => match serde_json::from_value(params.clone()) {
1241 Ok(body) => Self::VaultChanged(body),
1242 Err(_) => Self::Unknown {
1243 method: method.into(),
1244 params,
1245 },
1246 },
1247 NOTIFY_ISSUE_SELECTED => match serde_json::from_value(params.clone()) {
1248 Ok(body) => Self::IssueSelected(body),
1249 Err(_) => Self::Unknown {
1250 method: method.into(),
1251 params,
1252 },
1253 },
1254 NOTIFY_SHUTTING_DOWN => Self::ServeShuttingDown,
1255 other => Self::Unknown {
1256 method: other.into(),
1257 params,
1258 },
1259 }
1260 }
1261
1262 pub fn to_params(&self) -> Value {
1264 match self {
1265 Self::VaultChanged(body) => serde_json::to_value(body).unwrap_or(Value::Null),
1266 Self::IssueSelected(body) => serde_json::to_value(body).unwrap_or(Value::Null),
1267 Self::ServeShuttingDown => json!({}),
1268 Self::Unknown { params, .. } => params.clone(),
1269 }
1270 }
1271}
1272
1273#[derive(Debug, Clone, PartialEq)]
1275pub enum Request {
1276 Initialize(InitializeParams),
1278 IdentityGet,
1280 IssueList(IssueListParams),
1282 IssueGet(IdParams),
1284 IssueReady(IssueListParams),
1286 IssueSearch(SearchParams),
1288 IssueClaims(ClaimsParams),
1290 IssueAgenda(AgendaParams),
1292 IssueShow(IdParams),
1294 IssueExcerpt(IdParams),
1296 IssueTree(TreeParams),
1298 IssueRelated(RelatedParams),
1300 IssueChildren(WalkParams),
1302 IssueAncestors(WalkParams),
1304 IssueImpact(WalkParams),
1306 IssueBacklinks(WalkParams),
1308 IssueOpen(IdParams),
1310 IssueCreate(CreateParams),
1312 IssueUpdate(UpdateParams),
1314 IssueClaim(ClaimParams),
1316 IssueNote(NoteParams),
1318 IssueRefile(RefileParams),
1320 IssueAppend(AppendParams),
1322 IssueReject(RejectParams),
1324 IssueResolve(ResolveParams),
1326 IssueVote(VoteParams),
1328 IssueFold(FoldParams),
1330 IssueNormalize(NormalizeParams),
1332 IssueCheck(ProjectFilterParams),
1334 IssueCount(CountParams),
1336 IssueCycles(ProjectFilterParams),
1338 IssueDigest(DigestParams),
1340 IssueExport(ProjectFilterParams),
1342 IssueGraph(ProjectFilterParams),
1344 IssueRoadmap(ProjectFilterParams),
1346 IssueStale(StaleParams),
1348 IssueHygiene(HygieneParams),
1350 IssueWaitingOn(IdParams),
1352 IssueMirror(MirrorCheckParams),
1354 EventsPing(PingParams),
1356 EventsWait(WaitParams),
1358 ProjectList,
1360 EventsSince(EventsSinceParams),
1362 EventsGen,
1364}
1365
1366impl Request {
1367 pub fn method(&self) -> Method {
1369 match self {
1370 Self::Initialize(_) => Method::Initialize,
1371 Self::IdentityGet => Method::IdentityGet,
1372 Self::IssueList(_) => Method::IssueList,
1373 Self::IssueGet(_) => Method::IssueGet,
1374 Self::IssueReady(_) => Method::IssueReady,
1375 Self::IssueSearch(_) => Method::IssueSearch,
1376 Self::IssueClaims(_) => Method::IssueClaims,
1377 Self::IssueAgenda(_) => Method::IssueAgenda,
1378 Self::IssueShow(_) => Method::IssueShow,
1379 Self::IssueExcerpt(_) => Method::IssueExcerpt,
1380 Self::IssueTree(_) => Method::IssueTree,
1381 Self::IssueRelated(_) => Method::IssueRelated,
1382 Self::IssueChildren(_) => Method::IssueChildren,
1383 Self::IssueAncestors(_) => Method::IssueAncestors,
1384 Self::IssueImpact(_) => Method::IssueImpact,
1385 Self::IssueBacklinks(_) => Method::IssueBacklinks,
1386 Self::IssueOpen(_) => Method::IssueOpen,
1387 Self::IssueCreate(_) => Method::IssueCreate,
1388 Self::IssueUpdate(_) => Method::IssueUpdate,
1389 Self::IssueClaim(_) => Method::IssueClaim,
1390 Self::IssueNote(_) => Method::IssueNote,
1391 Self::IssueRefile(_) => Method::IssueRefile,
1392 Self::IssueAppend(_) => Method::IssueAppend,
1393 Self::IssueReject(_) => Method::IssueReject,
1394 Self::IssueResolve(_) => Method::IssueResolve,
1395 Self::IssueVote(_) => Method::IssueVote,
1396 Self::IssueFold(_) => Method::IssueFold,
1397 Self::IssueNormalize(_) => Method::IssueNormalize,
1398 Self::IssueCheck(_) => Method::IssueCheck,
1399 Self::IssueCount(_) => Method::IssueCount,
1400 Self::IssueCycles(_) => Method::IssueCycles,
1401 Self::IssueDigest(_) => Method::IssueDigest,
1402 Self::IssueExport(_) => Method::IssueExport,
1403 Self::IssueGraph(_) => Method::IssueGraph,
1404 Self::IssueRoadmap(_) => Method::IssueRoadmap,
1405 Self::IssueStale(_) => Method::IssueStale,
1406 Self::IssueHygiene(_) => Method::IssueHygiene,
1407 Self::IssueWaitingOn(_) => Method::IssueWaitingOn,
1408 Self::IssueMirror(_) => Method::IssueMirror,
1409 Self::EventsPing(_) => Method::EventsPing,
1410 Self::EventsWait(_) => Method::EventsWait,
1411 Self::ProjectList => Method::ProjectList,
1412 Self::EventsSince(_) => Method::EventsSince,
1413 Self::EventsGen => Method::EventsGen,
1414 }
1415 }
1416
1417 pub fn parse(method: &str, params: Option<Value>) -> Result<Self, JsonRpcError> {
1423 let method = Method::parse(method)?;
1424 let params = match params {
1425 None | Some(Value::Null) => Value::Object(Default::default()),
1426 Some(v) => v,
1427 };
1428 match method {
1429 Method::Initialize => Ok(Self::Initialize(parse_initialize_params(¶ms)?)),
1430 Method::IdentityGet => Ok(Self::IdentityGet),
1431 Method::IssueList => Ok(Self::IssueList(decode_params(params)?)),
1432 Method::IssueGet => Ok(Self::IssueGet(decode_params(params)?)),
1433 Method::IssueReady => Ok(Self::IssueReady(decode_params(params)?)),
1434 Method::IssueSearch => Ok(Self::IssueSearch(decode_params(params)?)),
1435 Method::IssueClaims => Ok(Self::IssueClaims(decode_params(params)?)),
1436 Method::IssueAgenda => Ok(Self::IssueAgenda(decode_params(params)?)),
1437 Method::IssueShow => Ok(Self::IssueShow(decode_params(params)?)),
1438 Method::IssueExcerpt => Ok(Self::IssueExcerpt(decode_params(params)?)),
1439 Method::IssueTree => Ok(Self::IssueTree(decode_params(params)?)),
1440 Method::IssueRelated => Ok(Self::IssueRelated(decode_params(params)?)),
1441 Method::IssueChildren => Ok(Self::IssueChildren(decode_params(params)?)),
1442 Method::IssueAncestors => Ok(Self::IssueAncestors(decode_params(params)?)),
1443 Method::IssueImpact => Ok(Self::IssueImpact(decode_params(params)?)),
1444 Method::IssueBacklinks => Ok(Self::IssueBacklinks(decode_params(params)?)),
1445 Method::IssueOpen => Ok(Self::IssueOpen(decode_params(params)?)),
1446 Method::IssueCreate => Ok(Self::IssueCreate(decode_params(params)?)),
1447 Method::IssueUpdate => Ok(Self::IssueUpdate(decode_params(params)?)),
1448 Method::IssueClaim => Ok(Self::IssueClaim(decode_params(params)?)),
1449 Method::IssueNote => Ok(Self::IssueNote(decode_params(params)?)),
1450 Method::IssueRefile => Ok(Self::IssueRefile(decode_params(params)?)),
1451 Method::ProjectList => Ok(Self::ProjectList),
1452 Method::EventsSince => Ok(Self::EventsSince(decode_params(params)?)),
1453 Method::EventsGen => Ok(Self::EventsGen),
1454 Method::IssueAppend => Ok(Self::IssueAppend(decode_params(params)?)),
1455 Method::IssueReject => Ok(Self::IssueReject(decode_params(params)?)),
1456 Method::IssueResolve => Ok(Self::IssueResolve(decode_params(params)?)),
1457 Method::IssueVote => Ok(Self::IssueVote(decode_params(params)?)),
1458 Method::IssueFold => Ok(Self::IssueFold(decode_params(params)?)),
1459 Method::IssueNormalize => Ok(Self::IssueNormalize(decode_params(params)?)),
1460 Method::IssueCheck => Ok(Self::IssueCheck(decode_params(params)?)),
1461 Method::IssueCount => Ok(Self::IssueCount(decode_params(params)?)),
1462 Method::IssueCycles => Ok(Self::IssueCycles(decode_params(params)?)),
1463 Method::IssueDigest => Ok(Self::IssueDigest(decode_params(params)?)),
1464 Method::IssueExport => Ok(Self::IssueExport(decode_params(params)?)),
1465 Method::IssueGraph => Ok(Self::IssueGraph(decode_params(params)?)),
1466 Method::IssueRoadmap => Ok(Self::IssueRoadmap(decode_params(params)?)),
1467 Method::IssueStale => Ok(Self::IssueStale(decode_params(params)?)),
1468 Method::IssueHygiene => Ok(Self::IssueHygiene(decode_params(params)?)),
1469 Method::IssueWaitingOn => Ok(Self::IssueWaitingOn(decode_params(params)?)),
1470 Method::IssueMirror => Ok(Self::IssueMirror(decode_params(params)?)),
1471 Method::EventsPing => Ok(Self::EventsPing(decode_params(params)?)),
1472 Method::EventsWait => Ok(Self::EventsWait(decode_params(params)?)),
1473 }
1474 }
1475
1476 pub fn to_params(&self) -> Value {
1478 match self {
1479 Self::Initialize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1480 Self::IssueAppend(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1481 Self::IssueReject(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1482 Self::IssueResolve(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1483 Self::IssueVote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1484 Self::IssueFold(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1485 Self::IssueNormalize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1486 Self::IssueCheck(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1487 Self::IssueCount(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1488 Self::IssueCycles(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1489 Self::IssueDigest(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1490 Self::IssueExport(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1491 Self::IssueGraph(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1492 Self::IssueRoadmap(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1493 Self::IssueStale(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1494 Self::IssueHygiene(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1495 Self::IssueWaitingOn(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1496 Self::IssueMirror(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1497 Self::EventsPing(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1498 Self::EventsWait(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1499 Self::IdentityGet | Self::ProjectList | Self::EventsGen => json!({}),
1500 Self::IssueList(p) | Self::IssueReady(p) => {
1501 serde_json::to_value(p).unwrap_or(Value::Null)
1502 }
1503 Self::IssueGet(p) | Self::IssueShow(p) | Self::IssueExcerpt(p) | Self::IssueOpen(p) => {
1504 serde_json::to_value(p).unwrap_or(Value::Null)
1505 }
1506 Self::IssueSearch(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1507 Self::IssueClaims(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1508 Self::IssueAgenda(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1509 Self::IssueTree(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1510 Self::IssueRelated(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1511 Self::IssueChildren(p)
1512 | Self::IssueAncestors(p)
1513 | Self::IssueImpact(p)
1514 | Self::IssueBacklinks(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1515 Self::IssueCreate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1516 Self::IssueUpdate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1517 Self::IssueClaim(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1518 Self::IssueNote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1519 Self::IssueRefile(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1520 Self::EventsSince(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1521 }
1522 }
1523}
1524
1525#[derive(Debug, Clone)]
1527pub enum Response {
1528 Initialize(InitializeResult),
1530 IdentityGet(IdentityResult),
1532 IssueList(IssueListResult),
1534 IssueGet(IssueGetResult),
1536 IssueReady(IssueListResult),
1538 IssueSearch(Vec<SearchHit>),
1540 IssueClaims(Vec<ClaimRow>),
1542 IssueAgenda(Vec<AgendaRow>),
1544 IssueShow(IssueGetResult),
1546 IssueExcerpt(Excerpt),
1548 IssueTree(TreeResult),
1550 IssueRelated(Vec<RelatedHit>),
1552 IssueChildren(Vec<WalkHit>),
1554 IssueAncestors(Vec<WalkHit>),
1556 IssueImpact(Vec<WalkHit>),
1558 IssueBacklinks(Vec<WalkHit>),
1560 IssueOpen(IssueGetResult),
1562 IssueCreate(MutResult),
1564 IssueUpdate(MutResult),
1566 IssueClaim(MutResult),
1568 IssueNote(MutResult),
1570 IssueRefile(MutResult),
1572 IssueAppend(MutResult),
1574 IssueReject(MutResult),
1576 IssueResolve(MutResult),
1578 IssueVote(MutResult),
1580 IssueFold(MutResult),
1582 IssueNormalize(MutResult),
1584 IssueCheck(CheckResult),
1586 IssueCount(ReportResult),
1588 IssueCycles(ReportResult),
1590 IssueDigest(DigestResult),
1592 IssueExport(ReportResult),
1594 IssueGraph(ReportResult),
1596 IssueRoadmap(ReportResult),
1598 IssueStale(ReportResult),
1600 IssueHygiene(ReportResult),
1602 IssueWaitingOn(ReportResult),
1604 IssueMirror(MirrorCheckResult),
1606 EventsPing(ReportResult),
1608 EventsWait(WaitResult),
1610 ProjectList(ProjectListResult),
1612 EventsSince(EventsSinceResult),
1614 EventsGen(EventsGenResult),
1616}
1617
1618impl Response {
1619 pub fn to_value(&self) -> Result<Value, serde_json::Error> {
1625 match self {
1626 Self::Initialize(v) => serde_json::to_value(v),
1627 Self::IdentityGet(v) => serde_json::to_value(v),
1628 Self::IssueAppend(v) => serde_json::to_value(v),
1629 Self::IssueReject(v) => serde_json::to_value(v),
1630 Self::IssueResolve(v) => serde_json::to_value(v),
1631 Self::IssueVote(v) => serde_json::to_value(v),
1632 Self::IssueFold(v) => serde_json::to_value(v),
1633 Self::IssueNormalize(v) => serde_json::to_value(v),
1634 Self::IssueCheck(v) => serde_json::to_value(v),
1635 Self::IssueCount(v) => serde_json::to_value(v),
1636 Self::IssueCycles(v) => serde_json::to_value(v),
1637 Self::IssueDigest(v) => serde_json::to_value(v),
1638 Self::IssueExport(v) => serde_json::to_value(v),
1639 Self::IssueGraph(v) => serde_json::to_value(v),
1640 Self::IssueRoadmap(v) => serde_json::to_value(v),
1641 Self::IssueStale(v) => serde_json::to_value(v),
1642 Self::IssueHygiene(v) => serde_json::to_value(v),
1643 Self::IssueWaitingOn(v) => serde_json::to_value(v),
1644 Self::IssueMirror(v) => serde_json::to_value(v),
1645 Self::EventsPing(v) => serde_json::to_value(v),
1646 Self::EventsWait(v) => serde_json::to_value(v),
1647 Self::IssueList(v) | Self::IssueReady(v) => serde_json::to_value(v),
1648 Self::IssueGet(v) | Self::IssueShow(v) | Self::IssueOpen(v) => serde_json::to_value(v),
1649 Self::IssueSearch(v) => serde_json::to_value(v),
1650 Self::IssueClaims(v) => serde_json::to_value(v),
1651 Self::IssueAgenda(v) => serde_json::to_value(v),
1652 Self::IssueExcerpt(v) => serde_json::to_value(v),
1653 Self::IssueTree(v) => serde_json::to_value(v),
1654 Self::IssueRelated(v) => serde_json::to_value(v),
1655 Self::IssueChildren(v)
1656 | Self::IssueAncestors(v)
1657 | Self::IssueImpact(v)
1658 | Self::IssueBacklinks(v) => serde_json::to_value(v),
1659 Self::IssueCreate(v)
1660 | Self::IssueUpdate(v)
1661 | Self::IssueClaim(v)
1662 | Self::IssueNote(v)
1663 | Self::IssueRefile(v) => serde_json::to_value(v),
1664 Self::ProjectList(v) => serde_json::to_value(v),
1665 Self::EventsSince(v) => serde_json::to_value(v),
1666 Self::EventsGen(v) => serde_json::to_value(v),
1667 }
1668 }
1669}
1670
1671fn decode_params<T: DeserializeOwned>(value: Value) -> Result<T, JsonRpcError> {
1672 serde_json::from_value(value).map_err(|e| invalid_params(e.to_string()))
1673}
1674
1675#[cfg(test)]
1676mod tests {
1677 use super::*;
1678 use std::collections::BTreeMap;
1679
1680 #[test]
1681 fn initialize_missing_agent_is_invalid_params() {
1682 let err = parse_initialize_params(&json!({
1683 "protocolVersion": 1,
1684 "client": "vissue-tui"
1685 }))
1686 .unwrap_err();
1687 assert_eq!(err.code, INVALID_PARAMS);
1688 assert_eq!(err.message, "agent is required");
1689
1690 let err = parse_initialize_params(&json!({
1691 "protocolVersion": 1,
1692 "agent": ""
1693 }))
1694 .unwrap_err();
1695 assert_eq!(err.code, INVALID_PARAMS);
1696 assert_eq!(err.message, "agent is required");
1697
1698 let err = Request::parse(
1699 "initialize",
1700 Some(json!({"protocolVersion": 1, "agent": " "})),
1701 )
1702 .unwrap_err();
1703 assert_eq!(err.code, INVALID_PARAMS);
1704 }
1705
1706 #[test]
1707 fn protocol_version_2_is_rejected() {
1708 let err = parse_initialize_params(&json!({
1709 "protocolVersion": 2,
1710 "agent": "rg@host"
1711 }))
1712 .unwrap_err();
1713 assert_eq!(err.code, INVALID_PARAMS);
1714 assert_eq!(err.message, "unsupported protocol version");
1715 assert_eq!(err.data, Some(json!({"supported": 1})));
1716 }
1717
1718 #[test]
1719 fn initialize_version_1_is_accepted() {
1720 let params = parse_initialize_params(&json!({
1721 "protocolVersion": 1,
1722 "client": "vissue-tui",
1723 "agent": "rg@host"
1724 }))
1725 .unwrap();
1726 assert_eq!(params.protocol_version, 1);
1727 assert_eq!(params.agent, "rg@host");
1728 assert_eq!(params.client, "vissue-tui");
1729 }
1730
1731 #[test]
1732 fn handshake_fields_are_camel_case() {
1733 let params = InitializeParams {
1734 protocol_version: 1,
1735 client: "vissue-tui".into(),
1736 agent: "rg@host".into(),
1737 };
1738 let value = serde_json::to_value(¶ms).unwrap();
1739 assert_eq!(value["protocolVersion"], 1);
1740 assert!(value.get("protocol_version").is_none());
1741
1742 let result = InitializeResult {
1743 protocol_version: 1,
1744 capabilities: vec!["issue/list".into()],
1745 root: "/tmp/tracker".into(),
1746 prefix: "Software".into(),
1747 generation: 3,
1748 revision: 1,
1749 identity: "rg@host".into(),
1750 };
1751 let value = serde_json::to_value(&result).unwrap();
1752 assert_eq!(value["protocolVersion"], 1);
1753 assert_eq!(value["generation"], 3);
1754 }
1755
1756 #[test]
1757 fn issue_payloads_are_snake_case() {
1758 let params = IssueListParams {
1759 since_revision: Some(41),
1760 ..IssueListParams::default()
1761 };
1762 let value = serde_json::to_value(¶ms).unwrap();
1763 assert_eq!(value["since_revision"], 41);
1764 assert!(value.get("sinceRevision").is_none());
1765 }
1766
1767 #[test]
1768 fn unknown_method_is_not_found() {
1769 const NEVER: &str = "issue/no-such-method";
1775 let err = Method::parse(NEVER).unwrap_err();
1776 assert_eq!(err.code, METHOD_NOT_FOUND);
1777 assert_eq!(err.data, Some(json!({"method": NEVER})));
1778 }
1779
1780 #[test]
1781 fn every_v1_capability_parses() {
1782 for name in V1_CAPABILITIES {
1783 assert!(Method::parse(name).is_ok(), "{name}");
1784 }
1785 assert_eq!(Method::Initialize.as_str(), "initialize");
1786 }
1787
1788 #[test]
1789 fn request_parse_roundtrips_issue_get() {
1790 let req = Request::parse("issue/get", Some(json!({"id": "atlas-1a2b"}))).unwrap();
1791 assert_eq!(req.method(), Method::IssueGet);
1792 assert_eq!(req.to_params()["id"], "atlas-1a2b");
1793 }
1794
1795 #[test]
1796 fn missing_id_on_issue_get_is_invalid_params() {
1797 let err = Request::parse("issue/get", Some(json!({}))).unwrap_err();
1798 assert_eq!(err.code, INVALID_PARAMS);
1799 }
1800
1801 #[test]
1802 fn core_errors_carry_data_code() {
1803 let err = error_from_core(&CoreError::IssueNotFound {
1804 id: "atlas-1a2b".into(),
1805 });
1806 assert_eq!(err.code, NOT_FOUND);
1807 assert_eq!(err.data.unwrap()["code"], "not_found");
1808
1809 let err = error_from_core(&CoreError::ClaimConflict {
1810 id: "atlas-1a2b".into(),
1811 holder: "other".into(),
1812 claimed_at: None,
1813 });
1814 assert_eq!(err.code, CONFLICT);
1815 let data = err.data.unwrap();
1816 assert_eq!(data["code"], "conflict");
1817 assert_eq!(data["holder"], "other");
1818
1819 let err = error_from_core(&CoreError::BlockerCycle {
1820 blocker: "a".into(),
1821 issue: "b".into(),
1822 });
1823 assert_eq!(err.code, CYCLE);
1824 let data = err.data.unwrap();
1825 assert_eq!(data["code"], "cycle");
1826 assert_eq!(data["id"], "b");
1827 assert_eq!(data["block"], "a");
1828
1829 let err = error_from_core(&CoreError::InvalidState {
1830 id: "atlas-4g5h".into(),
1831 state: "DONE".into(),
1832 });
1833 assert_eq!(err.code, INVALID_STATE);
1834 assert_eq!(err.data.unwrap()["code"], "invalid_state");
1835
1836 let err = error_from_core(&CoreError::DuplicateId {
1837 id: "atlas-1a2b".into(),
1838 paths: vec![
1839 std::path::PathBuf::from("/a/issues.org"),
1840 std::path::PathBuf::from("/b/issues.org"),
1841 ],
1842 });
1843 assert_eq!(err.code, CONFLICT);
1844 assert_eq!(err.data.unwrap()["code"], "duplicate_id");
1845 }
1846
1847 #[test]
1848 fn notification_parse_known_methods() {
1849 let n = Notification::parse(
1850 NOTIFY_VAULT_CHANGED,
1851 json!({"generation": 1, "revision": 2, "projects": ["atlas"]}),
1852 );
1853 assert!(matches!(n, Notification::VaultChanged(_)));
1854 assert_eq!(n.method(), NOTIFY_VAULT_CHANGED);
1855
1856 let n = Notification::parse(
1857 NOTIFY_ISSUE_SELECTED,
1858 json!({"id": "atlas-1a2b", "project": "atlas"}),
1859 );
1860 assert!(matches!(n, Notification::IssueSelected(_)));
1861
1862 let n = Notification::parse(NOTIFY_SHUTTING_DOWN, json!({}));
1863 assert!(matches!(n, Notification::ServeShuttingDown));
1864 assert_eq!(n.to_params(), json!({}));
1865 }
1866
1867 #[test]
1868 fn list_unchanged_deserializes_without_rows() {
1869 let page: IssueListResult =
1870 serde_json::from_value(json!({"unchanged": true, "revision": 41})).unwrap();
1871 assert!(page.unchanged);
1872 assert!(page.issues.is_empty());
1873 assert_eq!(page.revision, 41);
1874 }
1875
1876 #[test]
1877 fn response_to_value_serializes_initialize() {
1878 let resp = Response::Initialize(InitializeResult {
1879 protocol_version: 1,
1880 capabilities: V1_CAPABILITIES.iter().map(|s| (*s).to_string()).collect(),
1881 root: "/tmp".into(),
1882 prefix: "Software".into(),
1883 generation: 1,
1884 revision: 1,
1885 identity: "agent".into(),
1886 });
1887 let value = resp.to_value().unwrap();
1888 assert_eq!(value["protocolVersion"], 1);
1889 assert!(
1890 value["capabilities"]
1891 .as_array()
1892 .unwrap()
1893 .contains(&json!("issue/list"))
1894 );
1895 }
1896
1897 #[test]
1898 fn envelope_helpers_roundtrip() {
1899 let req = JsonRpcRequest::call(JsonRpcId::Number(1), "identity/get", json!({}));
1900 let bytes = serde_json::to_vec(&req).unwrap();
1901 let back: JsonRpcRequest = serde_json::from_slice(&bytes).unwrap();
1902 assert_eq!(back.method, "identity/get");
1903 assert!(!back.is_notification());
1904
1905 let note = JsonRpcRequest::notification(NOTIFY_SHUTTING_DOWN, json!({}));
1906 assert!(note.is_notification());
1907
1908 let ok = JsonRpcResponse::ok(Some(JsonRpcId::Number(1)), json!({"ok": true}));
1909 assert_eq!(ok.result.unwrap()["ok"], true);
1910 let err = JsonRpcResponse::err(Some(JsonRpcId::Null), parse_error());
1911 assert_eq!(err.error.unwrap().code, PARSE_ERROR);
1912 }
1913
1914 #[test]
1915 fn mut_and_walk_params_decode() {
1916 let claim = Request::parse(
1917 "issue/claim",
1918 Some(json!({"id": "atlas-1a2b", "force": true})),
1919 )
1920 .unwrap();
1921 match claim {
1922 Request::IssueClaim(p) => {
1923 assert!(p.force);
1924 assert_eq!(p.id, "atlas-1a2b");
1925 }
1926 other => panic!("{other:?}"),
1927 }
1928 let create = Request::parse(
1929 "issue/create",
1930 Some(json!({"project": "atlas", "title": "x"})),
1931 )
1932 .unwrap();
1933 assert_eq!(create.method(), Method::IssueCreate);
1934 assert!(Request::parse("issue/create", Some(json!({"title": "x"}))).is_err());
1935 assert_eq!(
1936 Request::parse("events/gen", None).unwrap().method(),
1937 Method::EventsGen
1938 );
1939 let _ = Request::IssueNote(NoteParams {
1940 id: "a".into(),
1941 text: "n".into(),
1942 })
1943 .to_params();
1944 let _ = Request::IssueRefile(RefileParams {
1945 id: "a".into(),
1946 to: "b".into(),
1947 })
1948 .to_params();
1949 let _ = Request::IssueUpdate(UpdateParams {
1950 id: "a".into(),
1951 state: Some("STARTED".into()),
1952 priority: None,
1953 block: None,
1954 unblock: None,
1955 if_state: None,
1956 if_gen: None,
1957 agent: None,
1958 })
1959 .to_params();
1960 let _ = Request::EventsSince(EventsSinceParams {
1961 since: 0,
1962 limit: Some(10),
1963 })
1964 .to_params();
1965 let _ = Request::IssueTree(TreeParams {
1966 id: "a".into(),
1967 format: Some("ascii".into()),
1968 })
1969 .to_params();
1970 let _ = Request::IssueRelated(RelatedParams {
1971 id: "a".into(),
1972 depth: Some(2),
1973 limit: Some(20),
1974 })
1975 .to_params();
1976 let _ = Request::IssueChildren(WalkParams {
1977 id: "a".into(),
1978 depth: None,
1979 })
1980 .to_params();
1981 let _ = Request::IssueSearch(SearchParams {
1982 query: "q".into(),
1983 limit: None,
1984 })
1985 .to_params();
1986 let _ = Request::IssueClaims(ClaimsParams::default()).to_params();
1987 let _ = Request::IssueAgenda(AgendaParams::default()).to_params();
1988 let _ = Request::IdentityGet.to_params();
1989 }
1990
1991 #[test]
1992 fn response_variants_serialize() {
1993 let detail = IssueDetail {
1994 id: "atlas-1a2b".into(),
1995 project: "atlas".into(),
1996 title: "t".into(),
1997 state: "TODO".into(),
1998 priority: "B".into(),
1999 properties: BTreeMap::new(),
2000 org_tags: vec![],
2001 tags: vec![],
2002 blocked_by: vec![],
2003 parent: None,
2004 claimed_by: None,
2005 claimed_at: None,
2006 file: "issues.org:1-2".into(),
2007 line_start: 1,
2008 line_end: 2,
2009 body: "what the issue asks for".into(),
2010 logbook: vec![],
2011 };
2012 let get = IssueGetResult {
2013 issue: detail.clone(),
2014 revision: 1,
2015 };
2016 assert!(Response::IssueGet(get.clone()).to_value().unwrap()["id"] == "atlas-1a2b");
2017 assert!(Response::IssueShow(get.clone()).to_value().is_ok());
2018 assert!(Response::IssueOpen(get).to_value().is_ok());
2019 assert!(
2020 Response::IssueExcerpt(Excerpt {
2021 id: "atlas-1a2b".into(),
2022 file: "issues.org".into(),
2023 line_start: 1,
2024 line_end: 2,
2025 text: "body".into(),
2026 suppressed: false,
2027 })
2028 .to_value()
2029 .is_ok()
2030 );
2031 assert!(Response::IssueSearch(vec![]).to_value().unwrap().is_array());
2032 assert!(Response::IssueClaims(vec![]).to_value().unwrap().is_array());
2033 assert!(Response::IssueAgenda(vec![]).to_value().unwrap().is_array());
2034 assert!(
2035 Response::IssueRelated(vec![])
2036 .to_value()
2037 .unwrap()
2038 .is_array()
2039 );
2040 assert!(
2041 Response::IssueChildren(vec![])
2042 .to_value()
2043 .unwrap()
2044 .is_array()
2045 );
2046 assert!(
2047 Response::IssueAncestors(vec![])
2048 .to_value()
2049 .unwrap()
2050 .is_array()
2051 );
2052 assert!(Response::IssueImpact(vec![]).to_value().unwrap().is_array());
2053 assert!(
2054 Response::IssueBacklinks(vec![])
2055 .to_value()
2056 .unwrap()
2057 .is_array()
2058 );
2059 assert!(
2060 Response::ProjectList(ProjectListResult {
2061 projects: vec!["atlas".into()],
2062 revision: 1,
2063 })
2064 .to_value()
2065 .is_ok()
2066 );
2067 assert!(
2068 Response::EventsGen(EventsGenResult {
2069 generation: 1,
2070 revision: 1,
2071 })
2072 .to_value()
2073 .is_ok()
2074 );
2075 assert!(
2076 Response::EventsSince(EventsSinceResult {
2077 events: vec![],
2078 generation: 1,
2079 })
2080 .to_value()
2081 .is_ok()
2082 );
2083 assert!(
2084 Response::IdentityGet(IdentityResult {
2085 identity: "a".into(),
2086 root: "/".into(),
2087 prefix: "Software".into(),
2088 version: "0.2.0".into(),
2089 })
2090 .to_value()
2091 .is_ok()
2092 );
2093 let mut_ok = MutResult {
2094 ok: true,
2095 report: "ok".into(),
2096 issue: Some(detail),
2097 revision: 2,
2098 generation: 3,
2099 };
2100 assert!(Response::IssueClaim(mut_ok.clone()).to_value().is_ok());
2101 assert!(Response::IssueCreate(mut_ok.clone()).to_value().is_ok());
2102 assert!(Response::IssueUpdate(mut_ok.clone()).to_value().is_ok());
2103 assert!(Response::IssueNote(mut_ok.clone()).to_value().is_ok());
2104 assert!(Response::IssueRefile(mut_ok).to_value().is_ok());
2105 assert!(
2106 Response::IssueTree(TreeResult::Text { text: "* a".into() })
2107 .to_value()
2108 .is_ok()
2109 );
2110 assert!(
2111 Response::IssueList(IssueListResult {
2112 revision: 1,
2113 ..IssueListResult::default()
2114 })
2115 .to_value()
2116 .is_ok()
2117 );
2118 assert!(
2119 Response::IssueReady(IssueListResult {
2120 revision: 1,
2121 ..IssueListResult::default()
2122 })
2123 .to_value()
2124 .is_ok()
2125 );
2126 }
2127
2128 #[test]
2129 fn parse_every_method_with_minimal_params() {
2130 let id = json!({"id": "atlas-1a2b"});
2131 for (method, params) in [
2132 ("identity/get", json!({})),
2133 ("issue/list", json!({})),
2134 ("issue/get", id.clone()),
2135 ("issue/ready", json!({})),
2136 ("issue/search", json!({"query": "q"})),
2137 ("issue/claims", json!({})),
2138 ("issue/agenda", json!({})),
2139 ("issue/show", id.clone()),
2140 ("issue/excerpt", id.clone()),
2141 ("issue/tree", id.clone()),
2142 ("issue/related", id.clone()),
2143 ("issue/children", id.clone()),
2144 ("issue/ancestors", id.clone()),
2145 ("issue/impact", id.clone()),
2146 ("issue/backlinks", id.clone()),
2147 ("issue/open", id.clone()),
2148 ("issue/create", json!({"project": "atlas", "title": "t"})),
2149 ("issue/update", id.clone()),
2150 ("issue/claim", id.clone()),
2151 ("issue/note", json!({"id": "atlas-1a2b", "text": "n"})),
2152 ("issue/refile", json!({"id": "atlas-1a2b", "to": "beacon"})),
2153 ("project/list", json!({})),
2154 ("events/since", json!({"since": 0})),
2155 ("events/gen", json!({})),
2156 ] {
2157 let req = Request::parse(method, Some(params)).expect(method);
2158 assert_eq!(req.method().as_str(), method);
2159 let _ = req.to_params();
2160 }
2161 }
2162
2163 #[test]
2164 fn helper_errors_have_stable_codes() {
2165 assert_eq!(invalid_request().code, INVALID_REQUEST);
2166 assert_eq!(internal_error("x").code, INTERNAL_ERROR);
2167 assert_eq!(parse_error().code, PARSE_ERROR);
2168 let err = Error::Rpc(invalid_params("agent is required"));
2169 assert_eq!(err.to_string(), "agent is required");
2170 let _ = Error::Unsupported("unix only");
2171 let _ = Notification::parse("vault/changed", json!(null));
2172 let _ = Notification::parse("issue/selected", json!(null));
2173 let _ = Notification::parse("other/x", json!({"a": 1}));
2174 let n = Notification::Unknown {
2175 method: "x".into(),
2176 params: json!({"a": 1}),
2177 };
2178 assert_eq!(n.to_params()["a"], 1);
2179 assert_eq!(n.method(), "x");
2180 }
2181 #[test]
2192 fn every_advertised_method_has_a_typed_request() {
2193 for name in V1_CAPABILITIES {
2194 let method = Method::parse(name).unwrap_or_else(|_| panic!("{name} does not parse"));
2195 assert_eq!(
2196 method.as_str(),
2197 *name,
2198 "{name} does not round-trip as a method"
2199 );
2200
2201 let parsed = Request::parse(name, Some(json!({})));
2205 if let Ok(req) = parsed {
2206 assert_eq!(
2207 req.method().as_str(),
2208 *name,
2209 "{name} parsed into a request that reports a different method"
2210 );
2211 assert!(
2213 req.to_params().is_object(),
2214 "{name} does not serialize its params to an object"
2215 );
2216 }
2217 }
2218 }
2219
2220 #[test]
2222 fn the_new_typed_responses_encode() {
2223 let cases = vec![
2224 Response::IssueCheck(CheckResult {
2225 report: "ok".into(),
2226 errors: 0,
2227 warnings: 2,
2228 }),
2229 Response::IssueCount(ReportResult {
2230 report: "3 issues".into(),
2231 }),
2232 Response::IssueDigest(DigestResult {
2233 combined: "abcd".into(),
2234 issues: 3,
2235 generation: 4,
2236 projects: vec![ProjectDigestResult {
2237 project: "atlas".into(),
2238 digest: "beef".into(),
2239 issues: 3,
2240 }],
2241 }),
2242 Response::EventsWait(WaitResult {
2243 generation: 7,
2244 state: Some("DONE".into()),
2245 timed_out: false,
2246 }),
2247 ];
2248 for case in cases {
2249 let value = case.to_value().expect("encode");
2250 assert!(value.is_object(), "{value} is not an object");
2251 }
2252
2253 let encoded = Response::IssueCheck(CheckResult {
2255 report: "two warnings".into(),
2256 errors: 0,
2257 warnings: 2,
2258 })
2259 .to_value()
2260 .unwrap();
2261 assert_eq!(encoded["warnings"], 2);
2262 assert_eq!(encoded["errors"], 0);
2263 }
2264}