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, Recall, RelatedHit, SearchHit, TreeNode,
10 WalkHit,
11};
12
13pub use vissue_core::events::Event;
15
16pub const PROTOCOL_VERSION: u32 = 1;
18
19pub const PARSE_ERROR: i32 = -32700;
21pub const INVALID_REQUEST: i32 = -32600;
23pub const METHOD_NOT_FOUND: i32 = -32601;
25pub const INVALID_PARAMS: i32 = -32602;
27pub const INTERNAL_ERROR: i32 = -32603;
29pub const NOT_FOUND: i32 = -32004;
31pub const CONFLICT: i32 = -32009;
33pub const INVALID_STATE: i32 = -32010;
35pub const CYCLE: i32 = -32022;
37
38pub const NOTIFY_VAULT_CHANGED: &str = "vault/changed";
40pub const NOTIFY_ISSUE_SELECTED: &str = "issue/selected";
42pub const NOTIFY_SHUTTING_DOWN: &str = "serve/shutting_down";
44
45#[derive(Debug)]
47pub enum Error {
48 Io(std::io::Error),
50 Json(serde_json::Error),
52 Frame(FrameError),
54 Rpc(JsonRpcError),
56 Unsupported(&'static str),
58}
59
60impl std::fmt::Display for Error {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 match self {
63 Error::Io(err) => write!(f, "{err}"),
64 Error::Json(err) => write!(f, "{err}"),
65 Error::Frame(err) => write!(f, "{err}"),
66 Error::Rpc(err) => write!(f, "{}", err.message),
67 Error::Unsupported(msg) => write!(f, "{msg}"),
68 }
69 }
70}
71
72impl std::error::Error for Error {
73 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
74 match self {
75 Error::Io(err) => Some(err),
76 Error::Json(err) => Some(err),
77 Error::Frame(err) => Some(err),
78 _ => None,
79 }
80 }
81}
82
83impl From<std::io::Error> for Error {
84 fn from(err: std::io::Error) -> Self {
85 Error::Io(err)
86 }
87}
88
89impl From<serde_json::Error> for Error {
90 fn from(err: serde_json::Error) -> Self {
91 Error::Json(err)
92 }
93}
94
95impl From<FrameError> for Error {
96 fn from(err: FrameError) -> Self {
97 Error::Frame(err)
98 }
99}
100
101impl From<JsonRpcError> for Error {
102 fn from(err: JsonRpcError) -> Self {
103 Error::Rpc(err)
104 }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(untagged)]
110pub enum JsonRpcId {
111 Number(i64),
113 String(String),
115 Null,
117}
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121pub struct JsonRpcRequest {
122 pub jsonrpc: String,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub id: Option<JsonRpcId>,
127 pub method: String,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub params: Option<Value>,
132}
133
134impl JsonRpcRequest {
135 pub fn call(id: JsonRpcId, method: impl Into<String>, params: Value) -> Self {
137 Self {
138 jsonrpc: "2.0".into(),
139 id: Some(id),
140 method: method.into(),
141 params: Some(params),
142 }
143 }
144
145 pub fn notification(method: impl Into<String>, params: Value) -> Self {
147 Self {
148 jsonrpc: "2.0".into(),
149 id: None,
150 method: method.into(),
151 params: Some(params),
152 }
153 }
154
155 pub fn is_notification(&self) -> bool {
157 self.id.is_none()
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct JsonRpcResponse {
164 pub jsonrpc: String,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub id: Option<JsonRpcId>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub result: Option<Value>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub error: Option<JsonRpcError>,
175}
176
177impl JsonRpcResponse {
178 pub fn ok(id: Option<JsonRpcId>, result: Value) -> Self {
180 Self {
181 jsonrpc: "2.0".into(),
182 id,
183 result: Some(result),
184 error: None,
185 }
186 }
187
188 pub fn err(id: Option<JsonRpcId>, error: JsonRpcError) -> Self {
190 Self {
191 jsonrpc: "2.0".into(),
192 id,
193 result: None,
194 error: Some(error),
195 }
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct JsonRpcError {
202 pub code: i32,
204 pub message: String,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub data: Option<Value>,
209}
210
211pub fn parse_error() -> JsonRpcError {
213 JsonRpcError {
214 code: PARSE_ERROR,
215 message: "parse error".into(),
216 data: None,
217 }
218}
219
220pub fn invalid_request() -> JsonRpcError {
222 JsonRpcError {
223 code: INVALID_REQUEST,
224 message: "invalid request".into(),
225 data: None,
226 }
227}
228
229pub fn method_not_found(method: &str) -> JsonRpcError {
231 JsonRpcError {
232 code: METHOD_NOT_FOUND,
233 message: "method not found".into(),
234 data: Some(json!({ "method": method })),
235 }
236}
237
238pub fn invalid_params(message: impl Into<String>) -> JsonRpcError {
240 JsonRpcError {
241 code: INVALID_PARAMS,
242 message: message.into(),
243 data: None,
244 }
245}
246
247pub fn internal_error(message: impl Into<String>) -> JsonRpcError {
249 JsonRpcError {
250 code: INTERNAL_ERROR,
251 message: message.into(),
252 data: None,
253 }
254}
255
256pub fn error_from_core(err: &CoreError) -> JsonRpcError {
258 match err {
259 CoreError::NotATracker { root, prefix } => JsonRpcError {
263 code: NOT_FOUND,
264 message: err.to_string(),
265 data: Some(json!({
266 "code": "not_a_tracker",
267 "root": root,
268 "prefix": prefix,
269 })),
270 },
271 CoreError::IssueNotFound { id } => JsonRpcError {
272 code: NOT_FOUND,
273 message: err.to_string(),
274 data: Some(json!({ "code": "not_found", "id": id })),
275 },
276 CoreError::DuplicateId { id, paths } => JsonRpcError {
277 code: CONFLICT,
278 message: err.to_string(),
279 data: Some(json!({
280 "code": "duplicate_id",
281 "id": id,
282 "paths": paths,
283 })),
284 },
285 CoreError::ClaimConflict { id, holder, .. } => JsonRpcError {
286 code: CONFLICT,
287 message: err.to_string(),
288 data: Some(json!({ "code": "conflict", "id": id, "holder": holder })),
289 },
290 CoreError::BlockerCycle { blocker, issue } => JsonRpcError {
291 code: CYCLE,
292 message: err.to_string(),
293 data: Some(json!({ "code": "cycle", "id": issue, "block": blocker })),
294 },
295 CoreError::InvalidState { id, state } => JsonRpcError {
296 code: INVALID_STATE,
297 message: err.to_string(),
298 data: Some(json!({ "code": "invalid_state", "id": id, "state": state })),
299 },
300 CoreError::StaleWrite {
301 id,
302 expected_state,
303 actual_state,
304 expected_gen,
305 actual_gen,
306 } => JsonRpcError {
307 code: INVALID_STATE,
308 message: err.to_string(),
309 data: Some(json!({
310 "code": "stale",
311 "id": id,
312 "expected_state": expected_state,
313 "actual_state": actual_state,
314 "expected_gen": expected_gen,
315 "actual_gen": actual_gen,
316 })),
317 },
318 CoreError::TerminalConflict {
319 id,
320 held,
321 attempted,
322 } => JsonRpcError {
323 code: CONFLICT,
324 message: err.to_string(),
325 data: Some(json!({
326 "code": "terminal_conflict",
327 "id": id,
328 "held": held,
329 "attempted": attempted,
330 })),
331 },
332 CoreError::Other(_) => internal_error(err.to_string()),
333 }
334}
335
336#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
338pub enum Method {
339 Initialize,
341 IdentityGet,
343 IssueList,
345 IssueGet,
347 IssueReady,
349 IssueSearch,
351 IssueClaims,
353 IssueAgenda,
355 IssueShow,
357 IssueExcerpt,
359 IssueTree,
361 IssueRelated,
363 IssueChildren,
365 IssueAncestors,
367 IssueImpact,
369 IssueBacklinks,
371 IssueOpen,
373 IssueCreate,
375 IssueUpdate,
377 IssueClaim,
379 IssueNote,
381 IssueRefile,
383 IssueAppend,
385 IssueReject,
387 IssueResolve,
389 IssueVote,
391 IssueDeed,
393 IssueRecall,
395 IssueConsensus,
397 IssueFold,
399 IssueNormalize,
401 IssueCheck,
403 IssueCount,
405 IssueCycles,
407 IssueDigest,
409 IssueExport,
411 IssueGraph,
413 IssueRoadmap,
415 IssueStale,
417 IssueHygiene,
419 IssueWaitingOn,
421 IssueMirror,
423 EventsPing,
425 EventsWait,
427 ProjectList,
429 EventsSince,
431 EventsGen,
433}
434
435impl Method {
436 pub fn as_str(self) -> &'static str {
438 match self {
439 Self::Initialize => "initialize",
440 Self::IdentityGet => "identity/get",
441 Self::IssueList => "issue/list",
442 Self::IssueGet => "issue/get",
443 Self::IssueReady => "issue/ready",
444 Self::IssueSearch => "issue/search",
445 Self::IssueClaims => "issue/claims",
446 Self::IssueAgenda => "issue/agenda",
447 Self::IssueShow => "issue/show",
448 Self::IssueExcerpt => "issue/excerpt",
449 Self::IssueTree => "issue/tree",
450 Self::IssueRelated => "issue/related",
451 Self::IssueChildren => "issue/children",
452 Self::IssueAncestors => "issue/ancestors",
453 Self::IssueImpact => "issue/impact",
454 Self::IssueBacklinks => "issue/backlinks",
455 Self::IssueOpen => "issue/open",
456 Self::IssueCreate => "issue/create",
457 Self::IssueUpdate => "issue/update",
458 Self::IssueClaim => "issue/claim",
459 Self::IssueNote => "issue/note",
460 Self::IssueRefile => "issue/refile",
461 Self::ProjectList => "project/list",
462 Self::EventsSince => "events/since",
463 Self::EventsGen => "events/gen",
464 Self::IssueAppend => "issue/append",
465 Self::IssueReject => "issue/reject",
466 Self::IssueResolve => "issue/resolve",
467 Self::IssueVote => "issue/vote",
468 Self::IssueDeed => "issue/deed",
469 Self::IssueRecall => "issue/recall",
470 Self::IssueConsensus => "issue/consensus",
471 Self::IssueFold => "issue/fold",
472 Self::IssueNormalize => "issue/normalize",
473 Self::IssueCheck => "issue/check",
474 Self::IssueCount => "issue/count",
475 Self::IssueCycles => "issue/cycles",
476 Self::IssueDigest => "issue/digest",
477 Self::IssueExport => "issue/export",
478 Self::IssueGraph => "issue/graph",
479 Self::IssueRoadmap => "issue/roadmap",
480 Self::IssueStale => "issue/stale",
481 Self::IssueHygiene => "issue/hygiene",
482 Self::IssueWaitingOn => "issue/waiting_on",
483 Self::IssueMirror => "issue/mirror_check",
484 Self::EventsPing => "events/ping",
485 Self::EventsWait => "events/wait",
486 }
487 }
488
489 pub fn parse(name: &str) -> Result<Self, JsonRpcError> {
495 match name {
496 "initialize" => Ok(Self::Initialize),
497 "identity/get" => Ok(Self::IdentityGet),
498 "issue/list" => Ok(Self::IssueList),
499 "issue/get" => Ok(Self::IssueGet),
500 "issue/ready" => Ok(Self::IssueReady),
501 "issue/search" => Ok(Self::IssueSearch),
502 "issue/claims" => Ok(Self::IssueClaims),
503 "issue/agenda" => Ok(Self::IssueAgenda),
504 "issue/show" => Ok(Self::IssueShow),
505 "issue/excerpt" => Ok(Self::IssueExcerpt),
506 "issue/tree" => Ok(Self::IssueTree),
507 "issue/related" => Ok(Self::IssueRelated),
508 "issue/children" => Ok(Self::IssueChildren),
509 "issue/ancestors" => Ok(Self::IssueAncestors),
510 "issue/impact" => Ok(Self::IssueImpact),
511 "issue/backlinks" => Ok(Self::IssueBacklinks),
512 "issue/open" => Ok(Self::IssueOpen),
513 "issue/create" => Ok(Self::IssueCreate),
514 "issue/update" => Ok(Self::IssueUpdate),
515 "issue/claim" => Ok(Self::IssueClaim),
516 "issue/note" => Ok(Self::IssueNote),
517 "issue/refile" => Ok(Self::IssueRefile),
518 "project/list" => Ok(Self::ProjectList),
519 "events/since" => Ok(Self::EventsSince),
520 "events/gen" => Ok(Self::EventsGen),
521 "issue/append" => Ok(Self::IssueAppend),
522 "issue/reject" => Ok(Self::IssueReject),
523 "issue/resolve" => Ok(Self::IssueResolve),
524 "issue/vote" => Ok(Self::IssueVote),
525 "issue/deed" => Ok(Self::IssueDeed),
526 "issue/recall" => Ok(Self::IssueRecall),
527 "issue/consensus" => Ok(Self::IssueConsensus),
528 "issue/fold" => Ok(Self::IssueFold),
529 "issue/normalize" => Ok(Self::IssueNormalize),
530 "issue/check" => Ok(Self::IssueCheck),
531 "issue/count" => Ok(Self::IssueCount),
532 "issue/cycles" => Ok(Self::IssueCycles),
533 "issue/digest" => Ok(Self::IssueDigest),
534 "issue/export" => Ok(Self::IssueExport),
535 "issue/graph" => Ok(Self::IssueGraph),
536 "issue/roadmap" => Ok(Self::IssueRoadmap),
537 "issue/stale" => Ok(Self::IssueStale),
538 "issue/hygiene" => Ok(Self::IssueHygiene),
539 "issue/waiting_on" => Ok(Self::IssueWaitingOn),
540 "issue/mirror_check" => Ok(Self::IssueMirror),
541 "events/ping" => Ok(Self::EventsPing),
542 "events/wait" => Ok(Self::EventsWait),
543 other => Err(method_not_found(other)),
544 }
545 }
546}
547
548pub const V1_CAPABILITIES: &[&str] = &[
558 "issue/list",
559 "issue/get",
560 "issue/ready",
561 "issue/search",
562 "issue/claims",
563 "issue/agenda",
564 "issue/show",
565 "issue/excerpt",
566 "issue/tree",
567 "issue/related",
568 "issue/children",
569 "issue/ancestors",
570 "issue/impact",
571 "issue/backlinks",
572 "issue/open",
573 "issue/create",
574 "issue/update",
575 "issue/claim",
576 "issue/note",
577 "issue/refile",
578 "issue/append",
579 "issue/reject",
580 "issue/resolve",
581 "issue/vote",
582 "issue/deed",
583 "issue/recall",
584 "issue/consensus",
585 "issue/fold",
586 "issue/normalize",
587 "issue/check",
588 "issue/count",
589 "issue/cycles",
590 "issue/digest",
591 "issue/export",
592 "issue/graph",
593 "issue/roadmap",
594 "issue/stale",
595 "issue/hygiene",
596 "issue/waiting_on",
597 "issue/mirror_check",
598 "project/list",
599 "events/since",
600 "events/gen",
601 "events/ping",
602 "events/wait",
603 "identity/get",
604];
605
606#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct InitializeParams {
610 pub protocol_version: u32,
612 #[serde(default)]
614 pub client: String,
615 pub agent: String,
617}
618
619#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621#[serde(rename_all = "camelCase")]
622pub struct InitializeResult {
623 pub protocol_version: u32,
625 pub capabilities: Vec<String>,
627 pub root: String,
629 pub prefix: String,
631 pub generation: u64,
633 pub revision: u64,
635 pub identity: String,
637}
638
639pub fn parse_initialize_params(value: &Value) -> Result<InitializeParams, JsonRpcError> {
646 let obj = value
647 .as_object()
648 .ok_or_else(|| invalid_params("params must be an object"))?;
649 let version = match obj.get("protocolVersion") {
650 Some(Value::Number(n)) => n
651 .as_u64()
652 .ok_or_else(|| invalid_params("protocolVersion must be a number"))?,
653 Some(_) => return Err(invalid_params("protocolVersion must be a number")),
654 None => return Err(invalid_params("protocolVersion is required")),
655 };
656 if version != u64::from(PROTOCOL_VERSION) {
657 return Err(JsonRpcError {
658 code: INVALID_PARAMS,
659 message: "unsupported protocol version".into(),
660 data: Some(json!({ "supported": PROTOCOL_VERSION })),
661 });
662 }
663 let agent = match obj.get("agent") {
664 Some(Value::String(s)) if !s.trim().is_empty() => s.clone(),
665 _ => return Err(invalid_params("agent is required")),
666 };
667 let client = obj
668 .get("client")
669 .and_then(Value::as_str)
670 .unwrap_or("")
671 .to_string();
672 Ok(InitializeParams {
673 protocol_version: PROTOCOL_VERSION,
674 client,
675 agent,
676 })
677}
678
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
681pub struct IssueListParams {
682 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub project: Option<String>,
685 #[serde(default, skip_serializing_if = "Option::is_none")]
687 pub state: Option<String>,
688 #[serde(default, skip_serializing_if = "Option::is_none")]
690 pub ready: Option<bool>,
691 #[serde(default, skip_serializing_if = "Option::is_none")]
693 pub query: Option<String>,
694 #[serde(default, skip_serializing_if = "Option::is_none")]
696 pub limit: Option<usize>,
697 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub offset: Option<usize>,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
702 pub since_revision: Option<u64>,
703}
704
705#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
707pub struct IssueListResult {
708 #[serde(default)]
710 pub issues: Vec<IssueRow>,
711 #[serde(default)]
713 pub total: u64,
714 #[serde(default)]
716 pub matched: u64,
717 pub revision: u64,
719 #[serde(default)]
721 pub generation: u64,
722 #[serde(default)]
724 pub unchanged: bool,
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
729pub struct IdParams {
730 pub id: String,
732}
733
734#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
736pub struct IssueGetResult {
737 #[serde(flatten)]
739 pub issue: IssueDetail,
740 pub revision: u64,
742}
743
744#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
746pub struct SearchParams {
747 pub query: String,
749 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub limit: Option<usize>,
752}
753
754#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
756pub struct ClaimsParams {
757 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub holder: Option<String>,
760 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub project: Option<String>,
763}
764
765#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
767pub struct AgendaParams {
768 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub days: Option<i64>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub project: Option<String>,
774}
775
776#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
778pub struct TreeParams {
779 pub id: String,
781 #[serde(default, skip_serializing_if = "Option::is_none")]
783 pub format: Option<String>,
784}
785
786#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
788#[serde(untagged)]
789pub enum TreeResult {
790 Nodes(TreeNode),
792 Text {
794 text: String,
796 },
797}
798
799#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
801pub struct RelatedParams {
802 pub id: String,
804 #[serde(default, skip_serializing_if = "Option::is_none")]
806 pub depth: Option<usize>,
807 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub limit: Option<usize>,
810}
811
812#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
814pub struct WalkParams {
815 pub id: String,
817 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub depth: Option<usize>,
820}
821
822#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
824pub struct ProjectListResult {
825 pub projects: Vec<String>,
827 pub revision: u64,
829}
830
831#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
833pub struct EventsSinceParams {
834 pub since: u64,
836 #[serde(default, skip_serializing_if = "Option::is_none")]
838 pub limit: Option<usize>,
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize)]
843pub struct EventsSinceResult {
844 pub events: Vec<Event>,
846 pub generation: u64,
848}
849
850#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
852pub struct EventsGenResult {
853 pub generation: u64,
855 pub revision: u64,
857}
858
859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
861pub struct IdentityResult {
862 pub identity: String,
864 pub root: String,
866 pub prefix: String,
868 pub version: String,
870}
871
872#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
874pub struct CreateParams {
875 pub project: String,
877 pub title: String,
879 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub agent: Option<String>,
882 #[serde(default, skip_serializing_if = "Option::is_none")]
884 pub priority: Option<char>,
885 #[serde(default, skip_serializing_if = "Option::is_none")]
887 pub issue_type: Option<String>,
888 #[serde(default, skip_serializing_if = "Option::is_none")]
890 pub deadline: Option<String>,
891 #[serde(default, skip_serializing_if = "Option::is_none")]
893 pub scheduled: Option<String>,
894 #[serde(default, skip_serializing_if = "Option::is_none")]
896 pub tags: Option<String>,
897 #[serde(default, skip_serializing_if = "Option::is_none")]
899 pub parent: Option<String>,
900 #[serde(default, skip_serializing_if = "Option::is_none")]
902 pub body: Option<String>,
903}
904
905#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
907pub struct UpdateParams {
908 pub id: String,
910 #[serde(default, skip_serializing_if = "Option::is_none")]
912 pub state: Option<String>,
913 #[serde(default, skip_serializing_if = "Option::is_none")]
915 pub priority: Option<String>,
916 #[serde(default, skip_serializing_if = "Option::is_none")]
918 pub block: Option<String>,
919 #[serde(default, skip_serializing_if = "Option::is_none")]
921 pub unblock: Option<String>,
922 #[serde(default, skip_serializing_if = "Option::is_none")]
924 pub if_state: Option<String>,
925 #[serde(default, skip_serializing_if = "Option::is_none")]
927 pub if_gen: Option<u64>,
928 #[serde(default, skip_serializing_if = "Option::is_none")]
930 pub agent: Option<String>,
931}
932
933#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
935pub struct ClaimParams {
936 pub id: String,
938 #[serde(default)]
940 pub force: bool,
941 #[serde(default, skip_serializing_if = "Option::is_none")]
943 pub agent: Option<String>,
944}
945
946#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
948pub struct VoteParams {
949 pub id: String,
951 #[serde(default, skip_serializing_if = "Option::is_none")]
953 pub choice: Option<String>,
954 #[serde(default, skip_serializing_if = "Option::is_none")]
956 pub agent: Option<String>,
957}
958
959#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
961pub struct DeedParams {
962 pub id: String,
964 #[serde(default)]
966 pub add: Vec<String>,
967 #[serde(default)]
969 pub remove: Vec<String>,
970}
971
972#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
974pub struct RecallParams {
975 pub id: String,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub depth: Option<usize>,
980 #[serde(default)]
982 pub excerpts: bool,
983}
984
985#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
987pub struct ConsensusParams {
988 pub id: String,
990 #[serde(default)]
992 pub children: bool,
993}
994
995#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
998pub struct ProjectFilterParams {
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1001 pub project: Option<String>,
1002}
1003
1004#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1006pub struct CountParams {
1007 #[serde(default, skip_serializing_if = "Option::is_none")]
1009 pub project: Option<String>,
1010 #[serde(default, skip_serializing_if = "Option::is_none")]
1012 pub state: Option<String>,
1013 #[serde(default)]
1015 pub ready_only: bool,
1016}
1017
1018#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1020pub struct StaleParams {
1021 pub days: i64,
1023 #[serde(default, skip_serializing_if = "Option::is_none")]
1025 pub project: Option<String>,
1026}
1027
1028#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1030pub struct HygieneParams {
1031 #[serde(default, skip_serializing_if = "Option::is_none")]
1033 pub stale_days: Option<i64>,
1034}
1035
1036#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1038pub struct PingParams {
1039 #[serde(default, skip_serializing_if = "Option::is_none")]
1041 pub detail: Option<String>,
1042}
1043
1044#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1047pub struct WaitParams {
1048 #[serde(default)]
1050 pub last: u64,
1051 #[serde(default, skip_serializing_if = "Option::is_none")]
1053 pub id: Option<String>,
1054 #[serde(default, skip_serializing_if = "Option::is_none")]
1056 pub poll_ms: Option<u64>,
1057 #[serde(default, skip_serializing_if = "Option::is_none")]
1059 pub timeout_ms: Option<u64>,
1060}
1061
1062#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1064pub struct MirrorCheckParams {
1065 pub path: String,
1067 #[serde(default)]
1070 pub projects: Vec<String>,
1071}
1072
1073#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1075pub struct MirrorCheckResult {
1076 pub fresh: bool,
1078 pub report: String,
1080}
1081
1082#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1084pub struct DigestParams {
1085 #[serde(default)]
1087 pub projects: Vec<String>,
1088}
1089
1090#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1096pub struct ReportResult {
1097 pub report: String,
1099}
1100
1101#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1104pub struct CheckResult {
1105 pub report: String,
1107 pub errors: usize,
1109 pub warnings: usize,
1111}
1112
1113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1115pub struct ProjectDigestResult {
1116 pub project: String,
1118 pub digest: String,
1120 pub issues: usize,
1122}
1123
1124#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1126pub struct DigestResult {
1127 pub combined: String,
1129 pub issues: usize,
1131 pub generation: u64,
1134 pub projects: Vec<ProjectDigestResult>,
1136}
1137
1138#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
1141pub struct WaitResult {
1142 pub generation: u64,
1144 #[serde(default, skip_serializing_if = "Option::is_none")]
1146 pub state: Option<String>,
1147 #[serde(default)]
1149 pub timed_out: bool,
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1154pub struct AppendParams {
1155 pub id: String,
1157 pub text: String,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1161 pub agent: Option<String>,
1162}
1163
1164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1166pub struct ResolveParams {
1167 pub id: String,
1169 pub state: String,
1171}
1172
1173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1176pub struct RejectParams {
1177 pub id: String,
1179 #[serde(default, skip_serializing_if = "Option::is_none")]
1181 pub to: Option<String>,
1182 #[serde(default, skip_serializing_if = "Option::is_none")]
1184 pub project: Option<String>,
1185 #[serde(default, skip_serializing_if = "Option::is_none")]
1187 pub title: Option<String>,
1188 #[serde(default, skip_serializing_if = "Option::is_none")]
1190 pub reason: Option<String>,
1191}
1192
1193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1195pub struct FoldParams {
1196 pub file: String,
1198 #[serde(default, skip_serializing_if = "Option::is_none")]
1200 pub project: Option<String>,
1201}
1202
1203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1205pub struct NormalizeParams {
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1208 pub project: Option<String>,
1209 #[serde(default)]
1211 pub dry_run: bool,
1212}
1213
1214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1216pub struct NoteParams {
1217 pub id: String,
1219 pub text: String,
1221}
1222
1223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1225pub struct RefileParams {
1226 pub id: String,
1228 pub to: String,
1230}
1231
1232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1234pub struct MutResult {
1235 pub ok: bool,
1237 pub report: String,
1239 #[serde(default)]
1241 pub issue: Option<IssueDetail>,
1242 pub revision: u64,
1244 pub generation: u64,
1246}
1247
1248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1250pub struct VaultChanged {
1251 pub generation: u64,
1253 pub revision: u64,
1255 #[serde(default)]
1257 pub projects: Vec<String>,
1258 #[serde(default, skip_serializing_if = "Option::is_none")]
1260 pub ids: Option<Vec<String>>,
1261}
1262
1263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1265pub struct IssueSelected {
1266 pub id: String,
1268 pub project: String,
1270}
1271
1272#[derive(Debug, Clone, PartialEq)]
1274pub enum Notification {
1275 VaultChanged(VaultChanged),
1277 IssueSelected(IssueSelected),
1279 ServeShuttingDown,
1281 Unknown {
1283 method: String,
1285 params: Value,
1287 },
1288}
1289
1290impl Notification {
1291 pub fn method(&self) -> &str {
1293 match self {
1294 Self::VaultChanged(_) => NOTIFY_VAULT_CHANGED,
1295 Self::IssueSelected(_) => NOTIFY_ISSUE_SELECTED,
1296 Self::ServeShuttingDown => NOTIFY_SHUTTING_DOWN,
1297 Self::Unknown { method, .. } => method,
1298 }
1299 }
1300
1301 pub fn parse(method: &str, params: Value) -> Self {
1303 match method {
1304 NOTIFY_VAULT_CHANGED => match serde_json::from_value(params.clone()) {
1305 Ok(body) => Self::VaultChanged(body),
1306 Err(_) => Self::Unknown {
1307 method: method.into(),
1308 params,
1309 },
1310 },
1311 NOTIFY_ISSUE_SELECTED => match serde_json::from_value(params.clone()) {
1312 Ok(body) => Self::IssueSelected(body),
1313 Err(_) => Self::Unknown {
1314 method: method.into(),
1315 params,
1316 },
1317 },
1318 NOTIFY_SHUTTING_DOWN => Self::ServeShuttingDown,
1319 other => Self::Unknown {
1320 method: other.into(),
1321 params,
1322 },
1323 }
1324 }
1325
1326 pub fn to_params(&self) -> Value {
1328 match self {
1329 Self::VaultChanged(body) => serde_json::to_value(body).unwrap_or(Value::Null),
1330 Self::IssueSelected(body) => serde_json::to_value(body).unwrap_or(Value::Null),
1331 Self::ServeShuttingDown => json!({}),
1332 Self::Unknown { params, .. } => params.clone(),
1333 }
1334 }
1335}
1336
1337#[derive(Debug, Clone, PartialEq)]
1339pub enum Request {
1340 Initialize(InitializeParams),
1342 IdentityGet,
1344 IssueList(IssueListParams),
1346 IssueGet(IdParams),
1348 IssueReady(IssueListParams),
1350 IssueSearch(SearchParams),
1352 IssueClaims(ClaimsParams),
1354 IssueAgenda(AgendaParams),
1356 IssueShow(IdParams),
1358 IssueExcerpt(IdParams),
1360 IssueTree(TreeParams),
1362 IssueRelated(RelatedParams),
1364 IssueChildren(WalkParams),
1366 IssueAncestors(WalkParams),
1368 IssueImpact(WalkParams),
1370 IssueBacklinks(WalkParams),
1372 IssueOpen(IdParams),
1374 IssueCreate(CreateParams),
1376 IssueUpdate(UpdateParams),
1378 IssueClaim(ClaimParams),
1380 IssueNote(NoteParams),
1382 IssueRefile(RefileParams),
1384 IssueAppend(AppendParams),
1386 IssueReject(RejectParams),
1388 IssueResolve(ResolveParams),
1390 IssueVote(VoteParams),
1392 IssueDeed(DeedParams),
1394 IssueRecall(RecallParams),
1396 IssueConsensus(ConsensusParams),
1398 IssueFold(FoldParams),
1400 IssueNormalize(NormalizeParams),
1402 IssueCheck(ProjectFilterParams),
1404 IssueCount(CountParams),
1406 IssueCycles(ProjectFilterParams),
1408 IssueDigest(DigestParams),
1410 IssueExport(ProjectFilterParams),
1412 IssueGraph(ProjectFilterParams),
1414 IssueRoadmap(ProjectFilterParams),
1416 IssueStale(StaleParams),
1418 IssueHygiene(HygieneParams),
1420 IssueWaitingOn(IdParams),
1422 IssueMirror(MirrorCheckParams),
1424 EventsPing(PingParams),
1426 EventsWait(WaitParams),
1428 ProjectList,
1430 EventsSince(EventsSinceParams),
1432 EventsGen,
1434}
1435
1436impl Request {
1437 pub fn method(&self) -> Method {
1439 match self {
1440 Self::Initialize(_) => Method::Initialize,
1441 Self::IdentityGet => Method::IdentityGet,
1442 Self::IssueList(_) => Method::IssueList,
1443 Self::IssueGet(_) => Method::IssueGet,
1444 Self::IssueReady(_) => Method::IssueReady,
1445 Self::IssueSearch(_) => Method::IssueSearch,
1446 Self::IssueClaims(_) => Method::IssueClaims,
1447 Self::IssueAgenda(_) => Method::IssueAgenda,
1448 Self::IssueShow(_) => Method::IssueShow,
1449 Self::IssueExcerpt(_) => Method::IssueExcerpt,
1450 Self::IssueTree(_) => Method::IssueTree,
1451 Self::IssueRelated(_) => Method::IssueRelated,
1452 Self::IssueChildren(_) => Method::IssueChildren,
1453 Self::IssueAncestors(_) => Method::IssueAncestors,
1454 Self::IssueImpact(_) => Method::IssueImpact,
1455 Self::IssueBacklinks(_) => Method::IssueBacklinks,
1456 Self::IssueOpen(_) => Method::IssueOpen,
1457 Self::IssueCreate(_) => Method::IssueCreate,
1458 Self::IssueUpdate(_) => Method::IssueUpdate,
1459 Self::IssueClaim(_) => Method::IssueClaim,
1460 Self::IssueNote(_) => Method::IssueNote,
1461 Self::IssueRefile(_) => Method::IssueRefile,
1462 Self::IssueAppend(_) => Method::IssueAppend,
1463 Self::IssueReject(_) => Method::IssueReject,
1464 Self::IssueResolve(_) => Method::IssueResolve,
1465 Self::IssueVote(_) => Method::IssueVote,
1466 Self::IssueDeed(_) => Method::IssueDeed,
1467 Self::IssueRecall(_) => Method::IssueRecall,
1468 Self::IssueConsensus(_) => Method::IssueConsensus,
1469 Self::IssueFold(_) => Method::IssueFold,
1470 Self::IssueNormalize(_) => Method::IssueNormalize,
1471 Self::IssueCheck(_) => Method::IssueCheck,
1472 Self::IssueCount(_) => Method::IssueCount,
1473 Self::IssueCycles(_) => Method::IssueCycles,
1474 Self::IssueDigest(_) => Method::IssueDigest,
1475 Self::IssueExport(_) => Method::IssueExport,
1476 Self::IssueGraph(_) => Method::IssueGraph,
1477 Self::IssueRoadmap(_) => Method::IssueRoadmap,
1478 Self::IssueStale(_) => Method::IssueStale,
1479 Self::IssueHygiene(_) => Method::IssueHygiene,
1480 Self::IssueWaitingOn(_) => Method::IssueWaitingOn,
1481 Self::IssueMirror(_) => Method::IssueMirror,
1482 Self::EventsPing(_) => Method::EventsPing,
1483 Self::EventsWait(_) => Method::EventsWait,
1484 Self::ProjectList => Method::ProjectList,
1485 Self::EventsSince(_) => Method::EventsSince,
1486 Self::EventsGen => Method::EventsGen,
1487 }
1488 }
1489
1490 pub fn parse(method: &str, params: Option<Value>) -> Result<Self, JsonRpcError> {
1496 let method = Method::parse(method)?;
1497 let params = match params {
1498 None | Some(Value::Null) => Value::Object(Default::default()),
1499 Some(v) => v,
1500 };
1501 match method {
1502 Method::Initialize => Ok(Self::Initialize(parse_initialize_params(¶ms)?)),
1503 Method::IdentityGet => Ok(Self::IdentityGet),
1504 Method::IssueList => Ok(Self::IssueList(decode_params(params)?)),
1505 Method::IssueGet => Ok(Self::IssueGet(decode_params(params)?)),
1506 Method::IssueReady => Ok(Self::IssueReady(decode_params(params)?)),
1507 Method::IssueSearch => Ok(Self::IssueSearch(decode_params(params)?)),
1508 Method::IssueClaims => Ok(Self::IssueClaims(decode_params(params)?)),
1509 Method::IssueAgenda => Ok(Self::IssueAgenda(decode_params(params)?)),
1510 Method::IssueShow => Ok(Self::IssueShow(decode_params(params)?)),
1511 Method::IssueExcerpt => Ok(Self::IssueExcerpt(decode_params(params)?)),
1512 Method::IssueTree => Ok(Self::IssueTree(decode_params(params)?)),
1513 Method::IssueRelated => Ok(Self::IssueRelated(decode_params(params)?)),
1514 Method::IssueChildren => Ok(Self::IssueChildren(decode_params(params)?)),
1515 Method::IssueAncestors => Ok(Self::IssueAncestors(decode_params(params)?)),
1516 Method::IssueImpact => Ok(Self::IssueImpact(decode_params(params)?)),
1517 Method::IssueBacklinks => Ok(Self::IssueBacklinks(decode_params(params)?)),
1518 Method::IssueOpen => Ok(Self::IssueOpen(decode_params(params)?)),
1519 Method::IssueCreate => Ok(Self::IssueCreate(decode_params(params)?)),
1520 Method::IssueUpdate => Ok(Self::IssueUpdate(decode_params(params)?)),
1521 Method::IssueClaim => Ok(Self::IssueClaim(decode_params(params)?)),
1522 Method::IssueNote => Ok(Self::IssueNote(decode_params(params)?)),
1523 Method::IssueRefile => Ok(Self::IssueRefile(decode_params(params)?)),
1524 Method::ProjectList => Ok(Self::ProjectList),
1525 Method::EventsSince => Ok(Self::EventsSince(decode_params(params)?)),
1526 Method::EventsGen => Ok(Self::EventsGen),
1527 Method::IssueAppend => Ok(Self::IssueAppend(decode_params(params)?)),
1528 Method::IssueReject => Ok(Self::IssueReject(decode_params(params)?)),
1529 Method::IssueResolve => Ok(Self::IssueResolve(decode_params(params)?)),
1530 Method::IssueVote => Ok(Self::IssueVote(decode_params(params)?)),
1531 Method::IssueDeed => Ok(Self::IssueDeed(decode_params(params)?)),
1532 Method::IssueRecall => Ok(Self::IssueRecall(decode_params(params)?)),
1533 Method::IssueConsensus => Ok(Self::IssueConsensus(decode_params(params)?)),
1534 Method::IssueFold => Ok(Self::IssueFold(decode_params(params)?)),
1535 Method::IssueNormalize => Ok(Self::IssueNormalize(decode_params(params)?)),
1536 Method::IssueCheck => Ok(Self::IssueCheck(decode_params(params)?)),
1537 Method::IssueCount => Ok(Self::IssueCount(decode_params(params)?)),
1538 Method::IssueCycles => Ok(Self::IssueCycles(decode_params(params)?)),
1539 Method::IssueDigest => Ok(Self::IssueDigest(decode_params(params)?)),
1540 Method::IssueExport => Ok(Self::IssueExport(decode_params(params)?)),
1541 Method::IssueGraph => Ok(Self::IssueGraph(decode_params(params)?)),
1542 Method::IssueRoadmap => Ok(Self::IssueRoadmap(decode_params(params)?)),
1543 Method::IssueStale => Ok(Self::IssueStale(decode_params(params)?)),
1544 Method::IssueHygiene => Ok(Self::IssueHygiene(decode_params(params)?)),
1545 Method::IssueWaitingOn => Ok(Self::IssueWaitingOn(decode_params(params)?)),
1546 Method::IssueMirror => Ok(Self::IssueMirror(decode_params(params)?)),
1547 Method::EventsPing => Ok(Self::EventsPing(decode_params(params)?)),
1548 Method::EventsWait => Ok(Self::EventsWait(decode_params(params)?)),
1549 }
1550 }
1551
1552 pub fn to_params(&self) -> Value {
1554 match self {
1555 Self::Initialize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1556 Self::IssueAppend(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1557 Self::IssueReject(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1558 Self::IssueResolve(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1559 Self::IssueVote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1560 Self::IssueDeed(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1561 Self::IssueRecall(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1562 Self::IssueConsensus(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1563 Self::IssueFold(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1564 Self::IssueNormalize(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1565 Self::IssueCheck(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1566 Self::IssueCount(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1567 Self::IssueCycles(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1568 Self::IssueDigest(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1569 Self::IssueExport(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1570 Self::IssueGraph(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1571 Self::IssueRoadmap(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1572 Self::IssueStale(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1573 Self::IssueHygiene(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1574 Self::IssueWaitingOn(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1575 Self::IssueMirror(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1576 Self::EventsPing(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1577 Self::EventsWait(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1578 Self::IdentityGet | Self::ProjectList | Self::EventsGen => json!({}),
1579 Self::IssueList(p) | Self::IssueReady(p) => {
1580 serde_json::to_value(p).unwrap_or(Value::Null)
1581 }
1582 Self::IssueGet(p) | Self::IssueShow(p) | Self::IssueExcerpt(p) | Self::IssueOpen(p) => {
1583 serde_json::to_value(p).unwrap_or(Value::Null)
1584 }
1585 Self::IssueSearch(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1586 Self::IssueClaims(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1587 Self::IssueAgenda(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1588 Self::IssueTree(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1589 Self::IssueRelated(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1590 Self::IssueChildren(p)
1591 | Self::IssueAncestors(p)
1592 | Self::IssueImpact(p)
1593 | Self::IssueBacklinks(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1594 Self::IssueCreate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1595 Self::IssueUpdate(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1596 Self::IssueClaim(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1597 Self::IssueNote(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1598 Self::IssueRefile(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1599 Self::EventsSince(p) => serde_json::to_value(p).unwrap_or(Value::Null),
1600 }
1601 }
1602}
1603
1604#[derive(Debug, Clone)]
1606pub enum Response {
1607 Initialize(InitializeResult),
1609 IdentityGet(IdentityResult),
1611 IssueList(IssueListResult),
1613 IssueGet(IssueGetResult),
1615 IssueReady(IssueListResult),
1617 IssueSearch(Vec<SearchHit>),
1619 IssueClaims(Vec<ClaimRow>),
1621 IssueAgenda(Vec<AgendaRow>),
1623 IssueShow(IssueGetResult),
1625 IssueExcerpt(Excerpt),
1627 IssueTree(TreeResult),
1629 IssueRelated(Vec<RelatedHit>),
1631 IssueChildren(Vec<WalkHit>),
1633 IssueAncestors(Vec<WalkHit>),
1635 IssueImpact(Vec<WalkHit>),
1637 IssueBacklinks(Vec<WalkHit>),
1639 IssueOpen(IssueGetResult),
1641 IssueCreate(MutResult),
1643 IssueUpdate(MutResult),
1645 IssueClaim(MutResult),
1647 IssueNote(MutResult),
1649 IssueRefile(MutResult),
1651 IssueAppend(MutResult),
1653 IssueReject(MutResult),
1655 IssueResolve(MutResult),
1657 IssueVote(MutResult),
1659 IssueDeed(MutResult),
1661 IssueRecall(Recall),
1663 IssueConsensus(Value),
1665 IssueFold(MutResult),
1667 IssueNormalize(MutResult),
1669 IssueCheck(CheckResult),
1671 IssueCount(ReportResult),
1673 IssueCycles(ReportResult),
1675 IssueDigest(DigestResult),
1677 IssueExport(ReportResult),
1679 IssueGraph(ReportResult),
1681 IssueRoadmap(ReportResult),
1683 IssueStale(ReportResult),
1685 IssueHygiene(ReportResult),
1687 IssueWaitingOn(ReportResult),
1689 IssueMirror(MirrorCheckResult),
1691 EventsPing(ReportResult),
1693 EventsWait(WaitResult),
1695 ProjectList(ProjectListResult),
1697 EventsSince(EventsSinceResult),
1699 EventsGen(EventsGenResult),
1701}
1702
1703impl Response {
1704 pub fn to_value(&self) -> Result<Value, serde_json::Error> {
1710 match self {
1711 Self::Initialize(v) => serde_json::to_value(v),
1712 Self::IdentityGet(v) => serde_json::to_value(v),
1713 Self::IssueAppend(v) => serde_json::to_value(v),
1714 Self::IssueReject(v) => serde_json::to_value(v),
1715 Self::IssueResolve(v) => serde_json::to_value(v),
1716 Self::IssueVote(v) => serde_json::to_value(v),
1717 Self::IssueDeed(v) => serde_json::to_value(v),
1718 Self::IssueRecall(v) => serde_json::to_value(v),
1719 Self::IssueConsensus(v) => serde_json::to_value(v),
1720 Self::IssueFold(v) => serde_json::to_value(v),
1721 Self::IssueNormalize(v) => serde_json::to_value(v),
1722 Self::IssueCheck(v) => serde_json::to_value(v),
1723 Self::IssueCount(v) => serde_json::to_value(v),
1724 Self::IssueCycles(v) => serde_json::to_value(v),
1725 Self::IssueDigest(v) => serde_json::to_value(v),
1726 Self::IssueExport(v) => serde_json::to_value(v),
1727 Self::IssueGraph(v) => serde_json::to_value(v),
1728 Self::IssueRoadmap(v) => serde_json::to_value(v),
1729 Self::IssueStale(v) => serde_json::to_value(v),
1730 Self::IssueHygiene(v) => serde_json::to_value(v),
1731 Self::IssueWaitingOn(v) => serde_json::to_value(v),
1732 Self::IssueMirror(v) => serde_json::to_value(v),
1733 Self::EventsPing(v) => serde_json::to_value(v),
1734 Self::EventsWait(v) => serde_json::to_value(v),
1735 Self::IssueList(v) | Self::IssueReady(v) => serde_json::to_value(v),
1736 Self::IssueGet(v) | Self::IssueShow(v) | Self::IssueOpen(v) => serde_json::to_value(v),
1737 Self::IssueSearch(v) => serde_json::to_value(v),
1738 Self::IssueClaims(v) => serde_json::to_value(v),
1739 Self::IssueAgenda(v) => serde_json::to_value(v),
1740 Self::IssueExcerpt(v) => serde_json::to_value(v),
1741 Self::IssueTree(v) => serde_json::to_value(v),
1742 Self::IssueRelated(v) => serde_json::to_value(v),
1743 Self::IssueChildren(v)
1744 | Self::IssueAncestors(v)
1745 | Self::IssueImpact(v)
1746 | Self::IssueBacklinks(v) => serde_json::to_value(v),
1747 Self::IssueCreate(v)
1748 | Self::IssueUpdate(v)
1749 | Self::IssueClaim(v)
1750 | Self::IssueNote(v)
1751 | Self::IssueRefile(v) => serde_json::to_value(v),
1752 Self::ProjectList(v) => serde_json::to_value(v),
1753 Self::EventsSince(v) => serde_json::to_value(v),
1754 Self::EventsGen(v) => serde_json::to_value(v),
1755 }
1756 }
1757}
1758
1759fn decode_params<T: DeserializeOwned>(value: Value) -> Result<T, JsonRpcError> {
1760 serde_json::from_value(value).map_err(|e| invalid_params(e.to_string()))
1761}
1762
1763#[cfg(test)]
1764mod tests {
1765 use super::*;
1766 use std::collections::BTreeMap;
1767
1768 #[test]
1769 fn initialize_missing_agent_is_invalid_params() {
1770 let err = parse_initialize_params(&json!({
1771 "protocolVersion": 1,
1772 "client": "vissue-tui"
1773 }))
1774 .unwrap_err();
1775 assert_eq!(err.code, INVALID_PARAMS);
1776 assert_eq!(err.message, "agent is required");
1777
1778 let err = parse_initialize_params(&json!({
1779 "protocolVersion": 1,
1780 "agent": ""
1781 }))
1782 .unwrap_err();
1783 assert_eq!(err.code, INVALID_PARAMS);
1784 assert_eq!(err.message, "agent is required");
1785
1786 let err = Request::parse(
1787 "initialize",
1788 Some(json!({"protocolVersion": 1, "agent": " "})),
1789 )
1790 .unwrap_err();
1791 assert_eq!(err.code, INVALID_PARAMS);
1792 }
1793
1794 #[test]
1795 fn protocol_version_2_is_rejected() {
1796 let err = parse_initialize_params(&json!({
1797 "protocolVersion": 2,
1798 "agent": "rg@host"
1799 }))
1800 .unwrap_err();
1801 assert_eq!(err.code, INVALID_PARAMS);
1802 assert_eq!(err.message, "unsupported protocol version");
1803 assert_eq!(err.data, Some(json!({"supported": 1})));
1804 }
1805
1806 #[test]
1807 fn initialize_version_1_is_accepted() {
1808 let params = parse_initialize_params(&json!({
1809 "protocolVersion": 1,
1810 "client": "vissue-tui",
1811 "agent": "rg@host"
1812 }))
1813 .unwrap();
1814 assert_eq!(params.protocol_version, 1);
1815 assert_eq!(params.agent, "rg@host");
1816 assert_eq!(params.client, "vissue-tui");
1817 }
1818
1819 #[test]
1820 fn handshake_fields_are_camel_case() {
1821 let params = InitializeParams {
1822 protocol_version: 1,
1823 client: "vissue-tui".into(),
1824 agent: "rg@host".into(),
1825 };
1826 let value = serde_json::to_value(¶ms).unwrap();
1827 assert_eq!(value["protocolVersion"], 1);
1828 assert!(value.get("protocol_version").is_none());
1829
1830 let result = InitializeResult {
1831 protocol_version: 1,
1832 capabilities: vec!["issue/list".into()],
1833 root: "/tmp/tracker".into(),
1834 prefix: "Software".into(),
1835 generation: 3,
1836 revision: 1,
1837 identity: "rg@host".into(),
1838 };
1839 let value = serde_json::to_value(&result).unwrap();
1840 assert_eq!(value["protocolVersion"], 1);
1841 assert_eq!(value["generation"], 3);
1842 }
1843
1844 #[test]
1845 fn issue_payloads_are_snake_case() {
1846 let params = IssueListParams {
1847 since_revision: Some(41),
1848 ..IssueListParams::default()
1849 };
1850 let value = serde_json::to_value(¶ms).unwrap();
1851 assert_eq!(value["since_revision"], 41);
1852 assert!(value.get("sinceRevision").is_none());
1853 }
1854
1855 #[test]
1856 fn unknown_method_is_not_found() {
1857 const NEVER: &str = "issue/no-such-method";
1863 let err = Method::parse(NEVER).unwrap_err();
1864 assert_eq!(err.code, METHOD_NOT_FOUND);
1865 assert_eq!(err.data, Some(json!({"method": NEVER})));
1866 }
1867
1868 #[test]
1869 fn every_v1_capability_parses() {
1870 for name in V1_CAPABILITIES {
1871 assert!(Method::parse(name).is_ok(), "{name}");
1872 }
1873 assert_eq!(Method::Initialize.as_str(), "initialize");
1874 }
1875
1876 #[test]
1877 fn request_parse_roundtrips_issue_get() {
1878 let req = Request::parse("issue/get", Some(json!({"id": "atlas-1a2b"}))).unwrap();
1879 assert_eq!(req.method(), Method::IssueGet);
1880 assert_eq!(req.to_params()["id"], "atlas-1a2b");
1881 }
1882
1883 #[test]
1884 fn missing_id_on_issue_get_is_invalid_params() {
1885 let err = Request::parse("issue/get", Some(json!({}))).unwrap_err();
1886 assert_eq!(err.code, INVALID_PARAMS);
1887 }
1888
1889 #[test]
1890 fn core_errors_carry_data_code() {
1891 let err = error_from_core(&CoreError::IssueNotFound {
1892 id: "atlas-1a2b".into(),
1893 });
1894 assert_eq!(err.code, NOT_FOUND);
1895 assert_eq!(err.data.unwrap()["code"], "not_found");
1896
1897 let err = error_from_core(&CoreError::ClaimConflict {
1898 id: "atlas-1a2b".into(),
1899 holder: "other".into(),
1900 claimed_at: None,
1901 });
1902 assert_eq!(err.code, CONFLICT);
1903 let data = err.data.unwrap();
1904 assert_eq!(data["code"], "conflict");
1905 assert_eq!(data["holder"], "other");
1906
1907 let err = error_from_core(&CoreError::BlockerCycle {
1908 blocker: "a".into(),
1909 issue: "b".into(),
1910 });
1911 assert_eq!(err.code, CYCLE);
1912 let data = err.data.unwrap();
1913 assert_eq!(data["code"], "cycle");
1914 assert_eq!(data["id"], "b");
1915 assert_eq!(data["block"], "a");
1916
1917 let err = error_from_core(&CoreError::InvalidState {
1918 id: "atlas-4g5h".into(),
1919 state: "DONE".into(),
1920 });
1921 assert_eq!(err.code, INVALID_STATE);
1922 assert_eq!(err.data.unwrap()["code"], "invalid_state");
1923
1924 let err = error_from_core(&CoreError::DuplicateId {
1925 id: "atlas-1a2b".into(),
1926 paths: vec![
1927 std::path::PathBuf::from("/a/issues.org"),
1928 std::path::PathBuf::from("/b/issues.org"),
1929 ],
1930 });
1931 assert_eq!(err.code, CONFLICT);
1932 assert_eq!(err.data.unwrap()["code"], "duplicate_id");
1933 }
1934
1935 #[test]
1936 fn notification_parse_known_methods() {
1937 let n = Notification::parse(
1938 NOTIFY_VAULT_CHANGED,
1939 json!({"generation": 1, "revision": 2, "projects": ["atlas"]}),
1940 );
1941 assert!(matches!(n, Notification::VaultChanged(_)));
1942 assert_eq!(n.method(), NOTIFY_VAULT_CHANGED);
1943
1944 let n = Notification::parse(
1945 NOTIFY_ISSUE_SELECTED,
1946 json!({"id": "atlas-1a2b", "project": "atlas"}),
1947 );
1948 assert!(matches!(n, Notification::IssueSelected(_)));
1949
1950 let n = Notification::parse(NOTIFY_SHUTTING_DOWN, json!({}));
1951 assert!(matches!(n, Notification::ServeShuttingDown));
1952 assert_eq!(n.to_params(), json!({}));
1953 }
1954
1955 #[test]
1956 fn list_unchanged_deserializes_without_rows() {
1957 let page: IssueListResult =
1958 serde_json::from_value(json!({"unchanged": true, "revision": 41})).unwrap();
1959 assert!(page.unchanged);
1960 assert!(page.issues.is_empty());
1961 assert_eq!(page.revision, 41);
1962 }
1963
1964 #[test]
1965 fn response_to_value_serializes_initialize() {
1966 let resp = Response::Initialize(InitializeResult {
1967 protocol_version: 1,
1968 capabilities: V1_CAPABILITIES.iter().map(|s| (*s).to_string()).collect(),
1969 root: "/tmp".into(),
1970 prefix: "Software".into(),
1971 generation: 1,
1972 revision: 1,
1973 identity: "agent".into(),
1974 });
1975 let value = resp.to_value().unwrap();
1976 assert_eq!(value["protocolVersion"], 1);
1977 assert!(
1978 value["capabilities"]
1979 .as_array()
1980 .unwrap()
1981 .contains(&json!("issue/list"))
1982 );
1983 }
1984
1985 #[test]
1986 fn envelope_helpers_roundtrip() {
1987 let req = JsonRpcRequest::call(JsonRpcId::Number(1), "identity/get", json!({}));
1988 let bytes = serde_json::to_vec(&req).unwrap();
1989 let back: JsonRpcRequest = serde_json::from_slice(&bytes).unwrap();
1990 assert_eq!(back.method, "identity/get");
1991 assert!(!back.is_notification());
1992
1993 let note = JsonRpcRequest::notification(NOTIFY_SHUTTING_DOWN, json!({}));
1994 assert!(note.is_notification());
1995
1996 let ok = JsonRpcResponse::ok(Some(JsonRpcId::Number(1)), json!({"ok": true}));
1997 assert_eq!(ok.result.unwrap()["ok"], true);
1998 let err = JsonRpcResponse::err(Some(JsonRpcId::Null), parse_error());
1999 assert_eq!(err.error.unwrap().code, PARSE_ERROR);
2000 }
2001
2002 #[test]
2003 fn mut_and_walk_params_decode() {
2004 let claim = Request::parse(
2005 "issue/claim",
2006 Some(json!({"id": "atlas-1a2b", "force": true})),
2007 )
2008 .unwrap();
2009 match claim {
2010 Request::IssueClaim(p) => {
2011 assert!(p.force);
2012 assert_eq!(p.id, "atlas-1a2b");
2013 }
2014 other => panic!("{other:?}"),
2015 }
2016 let create = Request::parse(
2017 "issue/create",
2018 Some(json!({"project": "atlas", "title": "x"})),
2019 )
2020 .unwrap();
2021 assert_eq!(create.method(), Method::IssueCreate);
2022 assert!(Request::parse("issue/create", Some(json!({"title": "x"}))).is_err());
2023 assert_eq!(
2024 Request::parse("events/gen", None).unwrap().method(),
2025 Method::EventsGen
2026 );
2027 let _ = Request::IssueNote(NoteParams {
2028 id: "a".into(),
2029 text: "n".into(),
2030 })
2031 .to_params();
2032 let _ = Request::IssueRefile(RefileParams {
2033 id: "a".into(),
2034 to: "b".into(),
2035 })
2036 .to_params();
2037 let _ = Request::IssueUpdate(UpdateParams {
2038 id: "a".into(),
2039 state: Some("STARTED".into()),
2040 priority: None,
2041 block: None,
2042 unblock: None,
2043 if_state: None,
2044 if_gen: None,
2045 agent: None,
2046 })
2047 .to_params();
2048 let _ = Request::EventsSince(EventsSinceParams {
2049 since: 0,
2050 limit: Some(10),
2051 })
2052 .to_params();
2053 let _ = Request::IssueTree(TreeParams {
2054 id: "a".into(),
2055 format: Some("ascii".into()),
2056 })
2057 .to_params();
2058 let _ = Request::IssueRelated(RelatedParams {
2059 id: "a".into(),
2060 depth: Some(2),
2061 limit: Some(20),
2062 })
2063 .to_params();
2064 let _ = Request::IssueChildren(WalkParams {
2065 id: "a".into(),
2066 depth: None,
2067 })
2068 .to_params();
2069 let _ = Request::IssueSearch(SearchParams {
2070 query: "q".into(),
2071 limit: None,
2072 })
2073 .to_params();
2074 let _ = Request::IssueClaims(ClaimsParams::default()).to_params();
2075 let _ = Request::IssueAgenda(AgendaParams::default()).to_params();
2076 let _ = Request::IdentityGet.to_params();
2077 }
2078
2079 #[test]
2080 fn response_variants_serialize() {
2081 let detail = IssueDetail {
2082 id: "atlas-1a2b".into(),
2083 project: "atlas".into(),
2084 title: "t".into(),
2085 state: "TODO".into(),
2086 priority: "B".into(),
2087 properties: BTreeMap::new(),
2088 org_tags: vec![],
2089 tags: vec![],
2090 blocked_by: vec![],
2091 deeds: vec![],
2092 parent: None,
2093 claimed_by: None,
2094 claimed_at: None,
2095 file: "issues.org:1-2".into(),
2096 line_start: 1,
2097 line_end: 2,
2098 body: "what the issue asks for".into(),
2099 logbook: vec![],
2100 };
2101 let get = IssueGetResult {
2102 issue: detail.clone(),
2103 revision: 1,
2104 };
2105 assert!(Response::IssueGet(get.clone()).to_value().unwrap()["id"] == "atlas-1a2b");
2106 assert!(Response::IssueShow(get.clone()).to_value().is_ok());
2107 assert!(Response::IssueOpen(get).to_value().is_ok());
2108 assert!(
2109 Response::IssueExcerpt(Excerpt {
2110 id: "atlas-1a2b".into(),
2111 file: "issues.org".into(),
2112 line_start: 1,
2113 line_end: 2,
2114 text: "body".into(),
2115 suppressed: false,
2116 })
2117 .to_value()
2118 .is_ok()
2119 );
2120 assert!(Response::IssueSearch(vec![]).to_value().unwrap().is_array());
2121 assert!(Response::IssueClaims(vec![]).to_value().unwrap().is_array());
2122 assert!(Response::IssueAgenda(vec![]).to_value().unwrap().is_array());
2123 assert!(
2124 Response::IssueRelated(vec![])
2125 .to_value()
2126 .unwrap()
2127 .is_array()
2128 );
2129 assert!(
2130 Response::IssueChildren(vec![])
2131 .to_value()
2132 .unwrap()
2133 .is_array()
2134 );
2135 assert!(
2136 Response::IssueAncestors(vec![])
2137 .to_value()
2138 .unwrap()
2139 .is_array()
2140 );
2141 assert!(Response::IssueImpact(vec![]).to_value().unwrap().is_array());
2142 assert!(
2143 Response::IssueBacklinks(vec![])
2144 .to_value()
2145 .unwrap()
2146 .is_array()
2147 );
2148 assert!(
2149 Response::ProjectList(ProjectListResult {
2150 projects: vec!["atlas".into()],
2151 revision: 1,
2152 })
2153 .to_value()
2154 .is_ok()
2155 );
2156 assert!(
2157 Response::EventsGen(EventsGenResult {
2158 generation: 1,
2159 revision: 1,
2160 })
2161 .to_value()
2162 .is_ok()
2163 );
2164 assert!(
2165 Response::EventsSince(EventsSinceResult {
2166 events: vec![],
2167 generation: 1,
2168 })
2169 .to_value()
2170 .is_ok()
2171 );
2172 assert!(
2173 Response::IdentityGet(IdentityResult {
2174 identity: "a".into(),
2175 root: "/".into(),
2176 prefix: "Software".into(),
2177 version: "0.2.0".into(),
2178 })
2179 .to_value()
2180 .is_ok()
2181 );
2182 let mut_ok = MutResult {
2183 ok: true,
2184 report: "ok".into(),
2185 issue: Some(detail),
2186 revision: 2,
2187 generation: 3,
2188 };
2189 assert!(Response::IssueClaim(mut_ok.clone()).to_value().is_ok());
2190 assert!(Response::IssueCreate(mut_ok.clone()).to_value().is_ok());
2191 assert!(Response::IssueUpdate(mut_ok.clone()).to_value().is_ok());
2192 assert!(Response::IssueNote(mut_ok.clone()).to_value().is_ok());
2193 assert!(Response::IssueRefile(mut_ok).to_value().is_ok());
2194 assert!(
2195 Response::IssueTree(TreeResult::Text { text: "* a".into() })
2196 .to_value()
2197 .is_ok()
2198 );
2199 assert!(
2200 Response::IssueList(IssueListResult {
2201 revision: 1,
2202 ..IssueListResult::default()
2203 })
2204 .to_value()
2205 .is_ok()
2206 );
2207 assert!(
2208 Response::IssueReady(IssueListResult {
2209 revision: 1,
2210 ..IssueListResult::default()
2211 })
2212 .to_value()
2213 .is_ok()
2214 );
2215 }
2216
2217 #[test]
2218 fn parse_every_method_with_minimal_params() {
2219 let id = json!({"id": "atlas-1a2b"});
2220 for (method, params) in [
2221 ("identity/get", json!({})),
2222 ("issue/list", json!({})),
2223 ("issue/get", id.clone()),
2224 ("issue/ready", json!({})),
2225 ("issue/search", json!({"query": "q"})),
2226 ("issue/claims", json!({})),
2227 ("issue/agenda", json!({})),
2228 ("issue/show", id.clone()),
2229 ("issue/excerpt", id.clone()),
2230 ("issue/tree", id.clone()),
2231 ("issue/related", id.clone()),
2232 ("issue/children", id.clone()),
2233 ("issue/ancestors", id.clone()),
2234 ("issue/impact", id.clone()),
2235 ("issue/backlinks", id.clone()),
2236 ("issue/open", id.clone()),
2237 ("issue/create", json!({"project": "atlas", "title": "t"})),
2238 ("issue/update", id.clone()),
2239 ("issue/claim", id.clone()),
2240 ("issue/note", json!({"id": "atlas-1a2b", "text": "n"})),
2241 ("issue/refile", json!({"id": "atlas-1a2b", "to": "beacon"})),
2242 ("project/list", json!({})),
2243 ("events/since", json!({"since": 0})),
2244 ("events/gen", json!({})),
2245 ] {
2246 let req = Request::parse(method, Some(params)).expect(method);
2247 assert_eq!(req.method().as_str(), method);
2248 let _ = req.to_params();
2249 }
2250 }
2251
2252 #[test]
2253 fn helper_errors_have_stable_codes() {
2254 assert_eq!(invalid_request().code, INVALID_REQUEST);
2255 assert_eq!(internal_error("x").code, INTERNAL_ERROR);
2256 assert_eq!(parse_error().code, PARSE_ERROR);
2257 let err = Error::Rpc(invalid_params("agent is required"));
2258 assert_eq!(err.to_string(), "agent is required");
2259 let _ = Error::Unsupported("unix only");
2260 let _ = Notification::parse("vault/changed", json!(null));
2261 let _ = Notification::parse("issue/selected", json!(null));
2262 let _ = Notification::parse("other/x", json!({"a": 1}));
2263 let n = Notification::Unknown {
2264 method: "x".into(),
2265 params: json!({"a": 1}),
2266 };
2267 assert_eq!(n.to_params()["a"], 1);
2268 assert_eq!(n.method(), "x");
2269 }
2270 #[test]
2281 fn every_advertised_method_has_a_typed_request() {
2282 for name in V1_CAPABILITIES {
2283 let method = Method::parse(name).unwrap_or_else(|_| panic!("{name} does not parse"));
2284 assert_eq!(
2285 method.as_str(),
2286 *name,
2287 "{name} does not round-trip as a method"
2288 );
2289
2290 let parsed = Request::parse(name, Some(json!({})));
2294 if let Ok(req) = parsed {
2295 assert_eq!(
2296 req.method().as_str(),
2297 *name,
2298 "{name} parsed into a request that reports a different method"
2299 );
2300 assert!(
2302 req.to_params().is_object(),
2303 "{name} does not serialize its params to an object"
2304 );
2305 }
2306 }
2307 }
2308
2309 #[test]
2311 fn the_new_typed_responses_encode() {
2312 let cases = vec![
2313 Response::IssueCheck(CheckResult {
2314 report: "ok".into(),
2315 errors: 0,
2316 warnings: 2,
2317 }),
2318 Response::IssueCount(ReportResult {
2319 report: "3 issues".into(),
2320 }),
2321 Response::IssueDigest(DigestResult {
2322 combined: "abcd".into(),
2323 issues: 3,
2324 generation: 4,
2325 projects: vec![ProjectDigestResult {
2326 project: "atlas".into(),
2327 digest: "beef".into(),
2328 issues: 3,
2329 }],
2330 }),
2331 Response::EventsWait(WaitResult {
2332 generation: 7,
2333 state: Some("DONE".into()),
2334 timed_out: false,
2335 }),
2336 ];
2337 for case in cases {
2338 let value = case.to_value().expect("encode");
2339 assert!(value.is_object(), "{value} is not an object");
2340 }
2341
2342 let encoded = Response::IssueCheck(CheckResult {
2344 report: "two warnings".into(),
2345 errors: 0,
2346 warnings: 2,
2347 })
2348 .to_value()
2349 .unwrap();
2350 assert_eq!(encoded["warnings"], 2);
2351 assert_eq!(encoded["errors"], 0);
2352 }
2353}