1use crate::llm::actions::protocol_trait::{ActionResult, Protocol, Server};
6use crate::llm::actions::{ActionDefinition, Parameter};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::Result;
11use serde_json::{json, Value};
12use std::sync::LazyLock;
13
14pub struct CouchDbProtocol {
16 }
18
19impl CouchDbProtocol {
20 pub fn new() -> Self {
21 Self {}
22 }
23}
24
25pub static COUCHDB_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
27 EventType::new(
28 "couchdb_request",
29 "CouchDB API request received",
30 json!({
31 "type": "send_server_info",
32 "version": "3.5.1"
33 }),
34 )
35 .with_parameters(vec![
36 Parameter {
37 name: "method".to_string(),
38 type_hint: "string".to_string(),
39 description: "HTTP method (GET, POST, PUT, DELETE, HEAD)".to_string(),
40 required: true,
41 },
42 Parameter {
43 name: "path".to_string(),
44 type_hint: "string".to_string(),
45 description: "Request path (e.g., /, /{db}, /{db}/{docid})".to_string(),
46 required: true,
47 },
48 Parameter {
49 name: "operation".to_string(),
50 type_hint: "string".to_string(),
51 description: "Detected operation type (server_info, db_create, doc_get, etc.)"
52 .to_string(),
53 required: true,
54 },
55 Parameter {
56 name: "database".to_string(),
57 type_hint: "string".to_string(),
58 description: "Database name (if applicable)".to_string(),
59 required: false,
60 },
61 Parameter {
62 name: "doc_id".to_string(),
63 type_hint: "string".to_string(),
64 description: "Document ID (if applicable)".to_string(),
65 required: false,
66 },
67 Parameter {
68 name: "query_params".to_string(),
69 type_hint: "object".to_string(),
70 description: "URL query parameters".to_string(),
71 required: false,
72 },
73 Parameter {
74 name: "request_body".to_string(),
75 type_hint: "string".to_string(),
76 description: "JSON request body".to_string(),
77 required: false,
78 },
79 Parameter {
80 name: "authorization".to_string(),
81 type_hint: "string".to_string(),
82 description: "Authorization header (for basic auth)".to_string(),
83 required: false,
84 },
85 ])
86 .with_actions(vec![
87 send_couchdb_response_action(),
88 send_server_info_action(),
89 send_db_info_action(),
90 send_doc_response_action(),
91 send_all_dbs_action(),
92 send_all_docs_action(),
93 send_bulk_docs_response_action(),
94 send_view_response_action(),
95 send_changes_response_action(),
96 send_replication_response_action(),
97 send_auth_required_action(),
98 ])
99 .with_log_template(
100 LogTemplate::new()
101 .with_info("{client_ip} CouchDB {method} {path} -> {status} ({duration_ms}ms)")
102 .with_debug("CouchDB {operation}: {method} {path} db={database} doc={doc_id}")
103 .with_trace("CouchDB request: {json_pretty(.)}"),
104 )
105});
106
107fn send_couchdb_response_action() -> ActionDefinition {
108 ActionDefinition {
109 name: "send_couchdb_response".to_string(),
110 description: "Send generic CouchDB JSON response with HTTP status code".to_string(),
111 parameters: vec![
112 Parameter {
113 name: "status_code".to_string(),
114 type_hint: "number".to_string(),
115 description: "HTTP status code (200, 201, 400, 401, 404, 409, 500, etc.)"
116 .to_string(),
117 required: true,
118 },
119 Parameter {
120 name: "body".to_string(),
121 type_hint: "string".to_string(),
122 description: "JSON response body".to_string(),
123 required: true,
124 },
125 Parameter {
126 name: "etag".to_string(),
127 type_hint: "string".to_string(),
128 description: "ETag header value (for document revisions)".to_string(),
129 required: false,
130 },
131 ],
132 example: serde_json::json!({
133 "type": "send_couchdb_response",
134 "status_code": 200,
135 "body": "{\"ok\": true}",
136 "etag": "\"1-abc123\""
137 }),
138 log_template: Some(
139 LogTemplate::new()
140 .with_info("-> {status_code} ({body_len}B)")
141 .with_debug("CouchDB response: {status_code}")
142 .with_trace("CouchDB response: {body}"),
143 ),
144 }
145}
146
147fn send_server_info_action() -> ActionDefinition {
148 ActionDefinition {
149 name: "send_server_info".to_string(),
150 description: "Send CouchDB server welcome/info response (GET /)".to_string(),
151 parameters: vec![Parameter {
152 name: "version".to_string(),
153 type_hint: "string".to_string(),
154 description: "CouchDB version number".to_string(),
155 required: false,
156 }],
157 example: serde_json::json!({
158 "type": "send_server_info",
159 "version": "3.5.1"
160 }),
161 log_template: Some(
162 LogTemplate::new()
163 .with_info("-> CouchDB {version}")
164 .with_debug("CouchDB server info: version={version}"),
165 ),
166 }
167}
168
169fn send_db_info_action() -> ActionDefinition {
170 ActionDefinition {
171 name: "send_db_info".to_string(),
172 description: "Send database information response".to_string(),
173 parameters: vec![
174 Parameter {
175 name: "db_name".to_string(),
176 type_hint: "string".to_string(),
177 description: "Database name".to_string(),
178 required: true,
179 },
180 Parameter {
181 name: "doc_count".to_string(),
182 type_hint: "number".to_string(),
183 description: "Number of documents in database".to_string(),
184 required: false,
185 },
186 Parameter {
187 name: "update_seq".to_string(),
188 type_hint: "string".to_string(),
189 description: "Update sequence number".to_string(),
190 required: false,
191 },
192 ],
193 example: serde_json::json!({
194 "type": "send_db_info",
195 "db_name": "mydb",
196 "doc_count": 42,
197 "update_seq": "42-abc"
198 }),
199 log_template: Some(
200 LogTemplate::new()
201 .with_info("-> db {db_name}: {doc_count} docs")
202 .with_debug("CouchDB db info: {db_name}, docs={doc_count}"),
203 ),
204 }
205}
206
207fn send_doc_response_action() -> ActionDefinition {
208 ActionDefinition {
209 name: "send_doc_response".to_string(),
210 description: "Send document response (GET, PUT, POST, DELETE)".to_string(),
211 parameters: vec![
212 Parameter {
213 name: "success".to_string(),
214 type_hint: "boolean".to_string(),
215 description: "Whether the operation succeeded".to_string(),
216 required: true,
217 },
218 Parameter {
219 name: "doc_id".to_string(),
220 type_hint: "string".to_string(),
221 description: "Document ID".to_string(),
222 required: true,
223 },
224 Parameter {
225 name: "rev".to_string(),
226 type_hint: "string".to_string(),
227 description: "Document revision (format: seq-hash, e.g., 1-abc123)".to_string(),
228 required: true,
229 },
230 Parameter {
231 name: "document".to_string(),
232 type_hint: "object".to_string(),
233 description: "Document data (for GET requests)".to_string(),
234 required: false,
235 },
236 Parameter {
237 name: "created".to_string(),
238 type_hint: "boolean".to_string(),
239 description: "Set true when this request stored a document - CouchDB answers \
240 PUT /{db}/{docid} and POST /{db} with 201 Created, not 200. Leave \
241 it out (or false) for GET and DELETE, which answer 200"
242 .to_string(),
243 required: false,
244 },
245 Parameter {
246 name: "error".to_string(),
247 type_hint: "string".to_string(),
248 description: "Error type (conflict, not_found, etc.)".to_string(),
249 required: false,
250 },
251 Parameter {
252 name: "reason".to_string(),
253 type_hint: "string".to_string(),
254 description: "Human-readable error reason".to_string(),
255 required: false,
256 },
257 ],
258 example: serde_json::json!({
259 "type": "send_doc_response",
260 "success": true,
261 "doc_id": "user1",
262 "rev": "1-abc123",
263 "document": {"name": "Alice", "age": 30},
264 "created": false
265 }),
266 log_template: Some(
267 LogTemplate::new()
268 .with_info("-> doc {doc_id} rev={rev}")
269 .with_debug("CouchDB doc: {doc_id} success={success} rev={rev}"),
270 ),
271 }
272}
273
274fn send_all_dbs_action() -> ActionDefinition {
275 ActionDefinition {
276 name: "send_all_dbs".to_string(),
277 description: "Send list of all databases (GET /_all_dbs)".to_string(),
278 parameters: vec![Parameter {
279 name: "databases".to_string(),
280 type_hint: "array".to_string(),
281 description: "Array of database names".to_string(),
282 required: true,
283 }],
284 example: serde_json::json!({
285 "type": "send_all_dbs",
286 "databases": ["_replicator", "_users", "mydb", "testdb"]
287 }),
288 log_template: Some(
289 LogTemplate::new()
290 .with_info("-> {databases_len} databases")
291 .with_debug("CouchDB all_dbs: {databases_len} databases"),
292 ),
293 }
294}
295
296fn send_all_docs_action() -> ActionDefinition {
297 ActionDefinition {
298 name: "send_all_docs".to_string(),
299 description: "Send list of all documents in database (GET /{db}/_all_docs)".to_string(),
300 parameters: vec![
301 Parameter {
302 name: "total_rows".to_string(),
303 type_hint: "number".to_string(),
304 description: "Total number of documents".to_string(),
305 required: true,
306 },
307 Parameter {
308 name: "rows".to_string(),
309 type_hint: "array".to_string(),
310 description: "Array of document rows with id, key, value (rev)".to_string(),
311 required: true,
312 },
313 ],
314 example: serde_json::json!({
315 "type": "send_all_docs",
316 "total_rows": 2,
317 "rows": [
318 {"id": "doc1", "key": "doc1", "value": {"rev": "1-abc"}},
319 {"id": "doc2", "key": "doc2", "value": {"rev": "1-def"}}
320 ]
321 }),
322 log_template: Some(
323 LogTemplate::new()
324 .with_info("-> {total_rows} documents")
325 .with_debug("CouchDB all_docs: {total_rows} rows"),
326 ),
327 }
328}
329
330fn send_bulk_docs_response_action() -> ActionDefinition {
331 ActionDefinition {
332 name: "send_bulk_docs_response".to_string(),
333 description: "Send bulk documents operation results (POST /{db}/_bulk_docs)".to_string(),
334 parameters: vec![Parameter {
335 name: "results".to_string(),
336 type_hint: "array".to_string(),
337 description: "Array of results with ok, id, rev for each document".to_string(),
338 required: true,
339 }],
340 example: serde_json::json!({
341 "type": "send_bulk_docs_response",
342 "results": [
343 {"ok": true, "id": "doc1", "rev": "1-abc"},
344 {"ok": true, "id": "doc2", "rev": "1-def"}
345 ]
346 }),
347 log_template: Some(
348 LogTemplate::new()
349 .with_info("-> bulk: {results_len} docs")
350 .with_debug("CouchDB bulk_docs: {results_len} results"),
351 ),
352 }
353}
354
355fn send_view_response_action() -> ActionDefinition {
356 ActionDefinition {
357 name: "send_view_response".to_string(),
358 description: "Send view query results (MapReduce)".to_string(),
359 parameters: vec![
360 Parameter {
361 name: "total_rows".to_string(),
362 type_hint: "number".to_string(),
363 description: "Total number of rows".to_string(),
364 required: true,
365 },
366 Parameter {
367 name: "offset".to_string(),
368 type_hint: "number".to_string(),
369 description: "Offset in result set".to_string(),
370 required: false,
371 },
372 Parameter {
373 name: "rows".to_string(),
374 type_hint: "array".to_string(),
375 description: "Array of view rows with id, key, value".to_string(),
376 required: true,
377 },
378 ],
379 example: serde_json::json!({
380 "type": "send_view_response",
381 "total_rows": 2,
382 "offset": 0,
383 "rows": [
384 {"id": "user1", "key": 25, "value": "Alice"},
385 {"id": "user2", "key": 30, "value": "Bob"}
386 ]
387 }),
388 log_template: Some(
389 LogTemplate::new()
390 .with_info("-> view: {total_rows} rows")
391 .with_debug("CouchDB view: {total_rows} total, offset={offset}"),
392 ),
393 }
394}
395
396fn send_changes_response_action() -> ActionDefinition {
397 ActionDefinition {
398 name: "send_changes_response".to_string(),
399 description: "Send changes feed response (GET /{db}/_changes)".to_string(),
400 parameters: vec![
401 Parameter {
402 name: "results".to_string(),
403 type_hint: "array".to_string(),
404 description: "Array of change events with seq, id, changes".to_string(),
405 required: true,
406 },
407 Parameter {
408 name: "last_seq".to_string(),
409 type_hint: "string".to_string(),
410 description: "Last sequence number".to_string(),
411 required: true,
412 },
413 Parameter {
414 name: "pending".to_string(),
415 type_hint: "number".to_string(),
416 description: "Number of pending changes".to_string(),
417 required: false,
418 },
419 ],
420 example: serde_json::json!({
421 "type": "send_changes_response",
422 "results": [
423 {"seq": "1-abc", "id": "doc1", "changes": [{"rev": "1-xyz"}]},
424 {"seq": "2-def", "id": "doc2", "changes": [{"rev": "1-uvw"}]}
425 ],
426 "last_seq": "2-def",
427 "pending": 0
428 }),
429 log_template: Some(
430 LogTemplate::new()
431 .with_info("-> changes: {results_len} seq={last_seq}")
432 .with_debug("CouchDB changes: {results_len} results, last_seq={last_seq}"),
433 ),
434 }
435}
436
437fn send_replication_response_action() -> ActionDefinition {
438 ActionDefinition {
439 name: "send_replication_response".to_string(),
440 description: "Send replication protocol response".to_string(),
441 parameters: vec![
442 Parameter {
443 name: "history".to_string(),
444 type_hint: "array".to_string(),
445 description: "Replication history".to_string(),
446 required: false,
447 },
448 Parameter {
449 name: "session_id".to_string(),
450 type_hint: "string".to_string(),
451 description: "Replication session ID. Required: this response asserts the \
452 replication ran, and a placeholder session id makes that \
453 assertion on the handler's behalf"
454 .to_string(),
455 required: true,
456 },
457 Parameter {
458 name: "source_last_seq".to_string(),
459 type_hint: "string".to_string(),
460 description: "Sequence the source has been replicated up to. Required: the \
461 peer stores it as a checkpoint and resumes from it"
462 .to_string(),
463 required: true,
464 },
465 ],
466 example: serde_json::json!({
467 "type": "send_replication_response",
468 "session_id": "abc123",
469 "source_last_seq": "10-xyz"
470 }),
471 log_template: Some(
472 LogTemplate::new()
473 .with_info("-> replication session={session_id}")
474 .with_debug("CouchDB replication: session={session_id}, seq={source_last_seq}"),
475 ),
476 }
477}
478
479fn send_auth_required_action() -> ActionDefinition {
480 ActionDefinition {
481 name: "send_auth_required".to_string(),
482 description: "Send 401 Unauthorized response (basic auth required)".to_string(),
483 parameters: vec![Parameter {
484 name: "realm".to_string(),
485 type_hint: "string".to_string(),
486 description: "Authentication realm".to_string(),
487 required: false,
488 }],
489 example: serde_json::json!({
490 "type": "send_auth_required",
491 "realm": "CouchDB"
492 }),
493 log_template: Some(
494 LogTemplate::new()
495 .with_info("-> 401 Unauthorized")
496 .with_debug("CouchDB auth required: realm={realm}"),
497 ),
498 }
499}
500
501fn parse_status_code(action: &Value) -> Result<u16> {
507 let raw = action
508 .get("status_code")
509 .and_then(|v| v.as_u64())
510 .ok_or_else(|| anyhow::anyhow!("Missing or invalid status_code: expected a number"))?;
511
512 if !(100..=599).contains(&raw) {
513 return Err(anyhow::anyhow!(
514 "Invalid status_code {raw}: must be an HTTP status between 100 and 599. CouchDB \
515 uses 200 for success, 201 when a database or document is created, 404 when it \
516 does not exist and 409 when a document _rev does not match."
517 ));
518 }
519
520 Ok(raw as u16)
521}
522
523fn required_count(action: &Value, act: &str, field: &str) -> Result<u64> {
529 action.get(field).and_then(|v| v.as_u64()).ok_or_else(|| {
530 anyhow::anyhow!(
531 "{act} requires `{field}` (a non-negative integer). It is a statement about the \
532 database that cannot be defaulted: 0 alongside a non-empty result set is a \
533 response no client can reconcile."
534 )
535 })
536}
537
538fn required_str<'a>(action: &'a Value, act: &str, field: &str) -> Result<&'a str> {
540 action
541 .get(field)
542 .and_then(|v| v.as_str())
543 .filter(|s| !s.trim().is_empty())
544 .ok_or_else(|| {
545 anyhow::anyhow!(
546 "{act} requires a non-empty `{field}`. It is checkpoint state a peer stores \
547 and resumes from, so a placeholder is worse than a refusal."
548 )
549 })
550}
551
552fn body_of(value: &Value) -> String {
555 serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
556}
557
558pub fn get_couchdb_event_types() -> Vec<EventType> {
559 vec![COUCHDB_REQUEST_EVENT.clone()]
560}
561
562impl Protocol for CouchDbProtocol {
564 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
565 vec![
566 crate::llm::actions::ParameterDefinition {
567 name: "enable_auth".to_string(),
568 type_hint: "boolean".to_string(),
569 description: "Enable HTTP basic authentication (default: false)".to_string(),
570 required: false,
571 example: serde_json::json!(false),
572 default: None,
573 },
574 crate::llm::actions::ParameterDefinition {
575 name: "admin_username".to_string(),
576 type_hint: "string".to_string(),
577 description: "Admin username (if auth enabled)".to_string(),
578 required: false,
579 example: serde_json::json!("admin"),
580 default: None,
581 },
582 crate::llm::actions::ParameterDefinition {
583 name: "admin_password".to_string(),
584 type_hint: "string".to_string(),
585 description: "Admin password (if auth enabled)".to_string(),
586 required: false,
587 example: serde_json::json!("password"),
588 default: None,
589 },
590 ]
591 }
592
593 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
594 vec![]
596 }
597
598 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
599 vec![
600 send_couchdb_response_action(),
601 send_server_info_action(),
602 send_db_info_action(),
603 send_doc_response_action(),
604 send_all_dbs_action(),
605 send_all_docs_action(),
606 send_bulk_docs_response_action(),
607 send_view_response_action(),
608 send_changes_response_action(),
609 send_replication_response_action(),
610 send_auth_required_action(),
611 ]
612 }
613
614 fn protocol_name(&self) -> &'static str {
615 "CouchDB"
616 }
617
618 fn get_event_types(&self) -> Vec<EventType> {
619 get_couchdb_event_types()
620 }
621
622 fn stack_name(&self) -> &'static str {
623 "ETH>IP>TCP>HTTP>COUCHDB"
624 }
625
626 fn keywords(&self) -> Vec<&'static str> {
627 vec!["couchdb", "nosql", "document-database"]
628 }
629
630 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
631 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
632
633 ProtocolMetadataV2::builder()
634 .state(DevelopmentState::Experimental)
635 .well_known_port(5984)
636 .implementation("hyper v1.5 HTTP server with CouchDB REST API")
637 .llm_control("Database CRUD, document CRUD, views, changes, replication")
638 .e2e_testing("reqwest (a generic HTTP client); no CouchDB client has been used")
644 .notes(
645 "Virtual data (no persistence), LLM-controlled revisions and views. Request \
646 bodies are bounded at http_common::MAX_REQUEST_BODY_BYTES and a larger one \
647 is refused with 413. No attachment content action and no Mango queries",
648 )
649 .max_inbound_bytes(crate::server::couchdb::MAX_REQUEST_BODY_BYTES)
650 .request_only(
651 "CouchDB is HTTP request/response; a server cannot send a peer anything unprompted",
652 )
653 .build()
654 }
655
656 fn description(&self) -> &'static str {
657 "CouchDB document database"
658 }
659
660 fn example_prompt(&self) -> &'static str {
661 "Start a CouchDB server on port 5984"
662 }
663
664 fn group_name(&self) -> &'static str {
665 "Database"
666 }
667
668 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
669 use crate::llm::actions::StartupExamples;
670 use serde_json::json;
671
672 let script = r#"import json, sys
675data = json.load(sys.stdin)
676event = data["event"]
677if data["event_type_id"] == "couchdb_request":
678 if event.get("path", "/") == "/":
679 actions = [{"type": "send_server_info", "version": "3.5.1"}]
680 else:
681 actions = [{"type": "send_couchdb_response", "status_code": 200,
682 "body": '{"ok": true}'}]
683else:
684 actions = []
685print(json.dumps({"actions": actions}))"#;
686
687 StartupExamples::new(
688 json!({
690 "type": "open_server",
691 "port": 5984,
692 "base_stack": "couchdb",
693 "instruction": "CouchDB document database handling REST API requests"
694 }),
695 json!({
697 "type": "open_server",
698 "port": 5984,
699 "base_stack": "couchdb",
700 "event_handlers": [{
701 "event_pattern": "couchdb_request",
702 "handler": {
703 "type": "script",
704 "language": "python",
705 "code": script
706 }
707 }]
708 }),
709 json!({
711 "type": "open_server",
712 "port": 5984,
713 "base_stack": "couchdb",
714 "event_handlers": [{
715 "event_pattern": "couchdb_request",
716 "handler": {
717 "type": "static",
718 "actions": [{
719 "type": "send_server_info",
720 "version": "3.5.1"
721 }]
722 }
723 }]
724 }),
725 )
726 }
727}
728
729impl Server for CouchDbProtocol {
731 fn spawn(
732 &self,
733 ctx: crate::protocol::SpawnContext,
734 ) -> std::pin::Pin<
735 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
736 > {
737 Box::pin(async move {
738 use crate::server::couchdb::CouchDbServer;
739
740 let enable_auth = ctx
741 .startup_params
742 .as_ref()
743 .map(|p| p.get_optional_bool("enable_auth"))
744 .transpose()?
745 .flatten()
746 .unwrap_or(false);
747
748 let admin_username = ctx
749 .startup_params
750 .as_ref()
751 .map(|p| p.get_optional_string("admin_username"))
752 .transpose()?
753 .flatten()
754 .unwrap_or_else(|| "admin".to_string());
755
756 let admin_password = ctx
757 .startup_params
758 .as_ref()
759 .map(|p| p.get_optional_string("admin_password"))
760 .transpose()?
761 .flatten()
762 .unwrap_or_else(|| "password".to_string());
763
764 CouchDbServer::spawn_with_llm_actions(
765 ctx.legacy_listen_addr(),
766 ctx.llm_client,
767 ctx.state,
768 ctx.status_tx,
769 ctx.server_id,
770 enable_auth,
771 admin_username,
772 admin_password,
773 )
774 .await
775 })
776 }
777
778 fn execute_action(&self, action: Value) -> Result<ActionResult> {
779 let action_type = action
780 .get("type")
781 .and_then(|v| v.as_str())
782 .ok_or_else(|| anyhow::anyhow!("Missing action type"))?;
783
784 match action_type {
785 "send_couchdb_response" => {
786 let status_code = parse_status_code(&action)?;
787
788 let body = action
789 .get("body")
790 .and_then(|v| v.as_str())
791 .ok_or_else(|| anyhow::anyhow!("Missing body"))?
792 .to_string();
793
794 let etag = action
795 .get("etag")
796 .and_then(|v| v.as_str())
797 .map(|s| s.to_string());
798
799 Ok(ActionResult::Custom {
800 name: "couchdb_response".to_string(),
801 data: json!({
802 "status": status_code,
803 "body": body,
804 "etag": etag
805 }),
806 })
807 }
808 "send_server_info" => {
809 let version = action
810 .get("version")
811 .and_then(|v| v.as_str())
812 .unwrap_or("3.5.1");
813
814 let response = serde_json::json!({
815 "couchdb": "Welcome",
816 "version": version,
817 "git_sha": "netget-llm",
818 "uuid": "netget-couchdb-uuid",
819 "features": [
820 "access-ready",
821 "partitioned",
822 "pluggable-storage-engines",
823 "reshard",
824 "scheduler"
825 ],
826 "vendor": {
827 "name": "NetGet LLM CouchDB"
828 }
829 });
830
831 Ok(ActionResult::Custom {
832 name: "couchdb_response".to_string(),
833 data: json!({
834 "status": 200,
835 "body": body_of(&response)
836 }),
837 })
838 }
839 "send_db_info" => {
840 let db_name = action
841 .get("db_name")
842 .and_then(|v| v.as_str())
843 .ok_or_else(|| anyhow::anyhow!("Missing db_name"))?;
844
845 let doc_count = action
846 .get("doc_count")
847 .and_then(|v| v.as_u64())
848 .unwrap_or(0);
849
850 let update_seq = action
851 .get("update_seq")
852 .and_then(|v| v.as_str())
853 .unwrap_or("0");
854
855 let response = serde_json::json!({
856 "db_name": db_name,
857 "update_seq": update_seq,
858 "sizes": {
859 "file": 0,
860 "external": 0,
861 "active": 0
862 },
863 "purge_seq": 0,
864 "doc_del_count": 0,
865 "doc_count": doc_count,
866 "disk_format_version": 8,
867 "compact_running": false,
868 "cluster": {
869 "q": 2,
870 "n": 1,
871 "w": 1,
872 "r": 1
873 },
874 "instance_start_time": "0"
875 });
876
877 Ok(ActionResult::Custom {
878 name: "couchdb_response".to_string(),
879 data: json!({
880 "status": 200,
881 "body": body_of(&response)
882 }),
883 })
884 }
885 "send_doc_response" => {
886 let success = action
891 .get("success")
892 .and_then(|v| v.as_bool())
893 .ok_or_else(|| {
894 anyhow::anyhow!(
895 "send_doc_response requires `success` (a JSON boolean, not a \
896 string); it decides whether the document write is reported as \
897 applied and must be stated explicitly"
898 )
899 })?;
900
901 let doc_id = action
902 .get("doc_id")
903 .and_then(|v| v.as_str())
904 .ok_or_else(|| anyhow::anyhow!("Missing doc_id"))?;
905
906 let rev = action
907 .get("rev")
908 .and_then(|v| v.as_str())
909 .ok_or_else(|| anyhow::anyhow!("Missing rev"))?;
910
911 if success {
912 let document = action.get("document").cloned();
913
914 let response = if let Some(doc) = document {
915 let mut doc_obj = doc.clone();
917 if let Some(obj) = doc_obj.as_object_mut() {
918 obj.insert("_id".to_string(), json!(doc_id));
919 obj.insert("_rev".to_string(), json!(rev));
920 }
921 doc_obj
922 } else {
923 json!({
925 "ok": true,
926 "id": doc_id,
927 "rev": rev
928 })
929 };
930
931 let created = action
935 .get("created")
936 .and_then(|v| v.as_bool())
937 .unwrap_or(false);
938
939 Ok(ActionResult::Custom {
940 name: "couchdb_response".to_string(),
941 data: json!({
942 "status": if created { 201 } else { 200 },
943 "body": body_of(&response),
944 "etag": format!("\"{}\"", rev)
945 }),
946 })
947 } else {
948 let error = action
950 .get("error")
951 .and_then(|v| v.as_str())
952 .unwrap_or("unknown_error");
953
954 let reason = action
955 .get("reason")
956 .and_then(|v| v.as_str())
957 .unwrap_or("Unknown error");
958
959 let status = match error {
960 "not_found" => 404,
961 "conflict" => 409,
962 "unauthorized" => 401,
963 "forbidden" => 403,
964 _ => 400,
965 };
966
967 let response = json!({
968 "error": error,
969 "reason": reason
970 });
971
972 Ok(ActionResult::Custom {
973 name: "couchdb_response".to_string(),
974 data: json!({
975 "status": status,
976 "body": body_of(&response)
977 }),
978 })
979 }
980 }
981 "send_all_dbs" => {
982 let databases = action
983 .get("databases")
984 .ok_or_else(|| anyhow::anyhow!("Missing databases"))?;
985
986 Ok(ActionResult::Custom {
987 name: "couchdb_response".to_string(),
988 data: json!({
989 "status": 200,
990 "body": body_of(databases)
991 }),
992 })
993 }
994 "send_all_docs" => {
995 let rows = action
996 .get("rows")
997 .ok_or_else(|| anyhow::anyhow!("Missing rows"))?;
998 let total_rows = required_count(&action, "send_all_docs", "total_rows")?;
999
1000 let response = json!({
1001 "total_rows": total_rows,
1002 "offset": 0,
1003 "rows": rows
1004 });
1005
1006 Ok(ActionResult::Custom {
1007 name: "couchdb_response".to_string(),
1008 data: json!({
1009 "status": 200,
1010 "body": body_of(&response)
1011 }),
1012 })
1013 }
1014 "send_bulk_docs_response" => {
1015 let results = action
1016 .get("results")
1017 .ok_or_else(|| anyhow::anyhow!("Missing results"))?;
1018
1019 Ok(ActionResult::Custom {
1020 name: "couchdb_response".to_string(),
1021 data: json!({
1022 "status": 201,
1023 "body": body_of(results)
1024 }),
1025 })
1026 }
1027 "send_view_response" => {
1028 let offset = action.get("offset").and_then(|v| v.as_u64()).unwrap_or(0);
1029
1030 let rows = action
1031 .get("rows")
1032 .ok_or_else(|| anyhow::anyhow!("Missing rows"))?;
1033 let total_rows = required_count(&action, "send_view_response", "total_rows")?;
1034
1035 let response = json!({
1036 "total_rows": total_rows,
1037 "offset": offset,
1038 "rows": rows
1039 });
1040
1041 Ok(ActionResult::Custom {
1042 name: "couchdb_response".to_string(),
1043 data: json!({
1044 "status": 200,
1045 "body": body_of(&response)
1046 }),
1047 })
1048 }
1049 "send_changes_response" => {
1050 let results = action
1051 .get("results")
1052 .ok_or_else(|| anyhow::anyhow!("Missing results"))?;
1053
1054 let last_seq = required_str(&action, "send_changes_response", "last_seq")?;
1059
1060 let pending = action.get("pending").and_then(|v| v.as_u64()).unwrap_or(0);
1061
1062 let response = json!({
1063 "results": results,
1064 "last_seq": last_seq,
1065 "pending": pending
1066 });
1067
1068 Ok(ActionResult::Custom {
1069 name: "couchdb_response".to_string(),
1070 data: json!({
1071 "status": 200,
1072 "body": body_of(&response)
1073 }),
1074 })
1075 }
1076 "send_replication_response" => {
1077 let history = action.get("history").cloned().unwrap_or_else(|| json!([]));
1086 let session_id = required_str(&action, "send_replication_response", "session_id")?;
1087 let source_last_seq =
1088 required_str(&action, "send_replication_response", "source_last_seq")?;
1089
1090 let response = json!({
1091 "ok": true,
1092 "session_id": session_id,
1093 "source_last_seq": source_last_seq,
1094 "history": history
1095 });
1096
1097 Ok(ActionResult::Custom {
1098 name: "couchdb_response".to_string(),
1099 data: json!({
1100 "status": 200,
1101 "body": body_of(&response)
1102 }),
1103 })
1104 }
1105 "send_auth_required" => {
1106 let realm = action
1107 .get("realm")
1108 .and_then(|v| v.as_str())
1109 .unwrap_or("CouchDB");
1110
1111 let response = json!({
1112 "error": "unauthorized",
1113 "reason": "Authentication required"
1114 });
1115
1116 Ok(ActionResult::Custom {
1117 name: "couchdb_response".to_string(),
1118 data: json!({
1119 "status": 401,
1120 "body": body_of(&response),
1121 "www_authenticate": format!("Basic realm=\"{}\"", realm)
1122 }),
1123 })
1124 }
1125 _ => Err(anyhow::anyhow!("Unknown action type: {}", action_type)),
1126 }
1127 }
1128}