1use crate::error::ServerError;
44use citum_engine::{
45 Bibliography, Citation, DocumentOptions, Processor, StyleInput,
46 render::{djot::Djot, html::Html, latex::Latex, plain::PlainText, typst::Typst},
47};
48#[cfg(feature = "session")]
49use citum_engine::{
50 CitationInsertPosition, CitationOccurrence, CitationOccurrenceItem, DocumentSession,
51 OpenSessionResult, OutputFormatKind, RefsInput, apply_style_overrides,
52};
53use citum_schema::Style;
54use serde::{Deserialize, Serialize};
55use serde_json::{Value, json};
56#[cfg(all(feature = "session", feature = "http"))]
57use std::collections::HashMap;
58use std::io::{self, BufRead, Write};
59#[cfg(all(feature = "session", feature = "http"))]
60use std::sync::atomic::{AtomicU64, Ordering};
61#[cfg(all(feature = "session", feature = "http"))]
62use std::time::{Duration, SystemTime, UNIX_EPOCH};
63
64#[derive(Debug, Deserialize)]
66#[cfg_attr(
67 any(feature = "schema", feature = "schema-types"),
68 derive(schemars::JsonSchema)
69)]
70pub struct RpcRequest {
71 pub id: Value,
73 pub method: String,
75 pub params: Value,
77}
78
79#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
81#[serde(rename_all = "lowercase")]
82#[cfg_attr(
83 any(feature = "schema", feature = "schema-types"),
84 derive(schemars::JsonSchema)
85)]
86pub enum OutputFormat {
87 #[default]
89 Plain,
90 Html,
92 Djot,
94 Latex,
96 Typst,
98}
99
100#[derive(Debug, Deserialize)]
102#[cfg_attr(
103 any(feature = "schema", feature = "schema-types"),
104 derive(schemars::JsonSchema)
105)]
106pub struct RenderCitationParams {
107 pub style_path: String,
109 pub refs: serde_json::Value,
111 pub citation: serde_json::Value,
113 pub output_format: Option<OutputFormat>,
115 pub inject_ast_indices: Option<bool>,
117}
118
119#[derive(Debug, Deserialize)]
121#[cfg_attr(
122 any(feature = "schema", feature = "schema-types"),
123 derive(schemars::JsonSchema)
124)]
125pub struct RenderBibliographyParams {
126 pub style_path: String,
128 pub refs: serde_json::Value,
130 pub output_format: Option<OutputFormat>,
132 pub inject_ast_indices: Option<bool>,
134}
135
136#[derive(Debug, Deserialize)]
138#[cfg_attr(
139 any(feature = "schema", feature = "schema-types"),
140 derive(schemars::JsonSchema)
141)]
142pub struct ValidateStyleParams {
143 pub style_path: String,
145}
146
147#[derive(Debug, Deserialize)]
149#[cfg_attr(
150 any(feature = "schema", feature = "schema-types"),
151 derive(schemars::JsonSchema)
152)]
153pub struct FormatDocumentParams {
154 pub style: StyleInput,
156 pub style_overrides: Option<String>,
160 pub locale: Option<String>,
162 pub output_format: Option<OutputFormat>,
164 pub refs: serde_json::Value,
167 pub citations: serde_json::Value,
169 pub document_options: Option<DocumentOptions>,
171}
172
173#[cfg(feature = "session")]
175#[derive(Debug, Deserialize)]
176#[cfg_attr(
177 any(feature = "schema", feature = "schema-types"),
178 derive(schemars::JsonSchema)
179)]
180pub struct OpenSessionParams {
181 pub style: StyleInput,
183 pub style_overrides: Option<String>,
187 pub locale: Option<String>,
189 pub output_format: Option<OutputFormatKind>,
191 pub document_options: Option<DocumentOptions>,
193}
194
195#[cfg(feature = "session")]
197#[derive(Debug, Deserialize)]
198#[cfg_attr(
199 any(feature = "schema", feature = "schema-types"),
200 derive(schemars::JsonSchema)
201)]
202pub struct PutReferencesParams {
203 pub session_id: Option<String>,
205 pub refs: RefsInput,
207}
208
209#[cfg(feature = "session")]
211#[derive(Debug, Deserialize)]
212#[cfg_attr(
213 any(feature = "schema", feature = "schema-types"),
214 derive(schemars::JsonSchema)
215)]
216pub struct InsertCitationsBatchParams {
217 pub session_id: Option<String>,
219 pub citations: Vec<CitationOccurrence>,
221}
222
223#[cfg(feature = "session")]
225#[derive(Debug, Deserialize)]
226#[cfg_attr(
227 any(feature = "schema", feature = "schema-types"),
228 derive(schemars::JsonSchema)
229)]
230pub struct InsertCitationParams {
231 pub session_id: Option<String>,
233 pub citation: CitationOccurrence,
235 pub position: Option<CitationInsertPosition>,
237}
238
239#[cfg(feature = "session")]
241#[derive(Debug, Deserialize)]
242#[cfg_attr(
243 any(feature = "schema", feature = "schema-types"),
244 derive(schemars::JsonSchema)
245)]
246pub struct UpdateCitationParams {
247 pub session_id: Option<String>,
249 pub citation_id: String,
251 pub citation: CitationOccurrence,
253 pub position: Option<CitationInsertPosition>,
255}
256
257#[cfg(feature = "session")]
259#[derive(Debug, Deserialize)]
260#[cfg_attr(
261 any(feature = "schema", feature = "schema-types"),
262 derive(schemars::JsonSchema)
263)]
264pub struct DeleteCitationParams {
265 pub session_id: Option<String>,
267 pub citation_id: String,
269}
270
271#[cfg(feature = "session")]
273#[derive(Debug, Deserialize)]
274#[cfg_attr(
275 any(feature = "schema", feature = "schema-types"),
276 derive(schemars::JsonSchema)
277)]
278pub struct PreviewCitationParams {
279 pub session_id: Option<String>,
281 pub items: Vec<CitationOccurrenceItem>,
283 pub mode: Option<citum_schema::data::citation::CitationMode>,
285 pub position: Option<CitationInsertPosition>,
287}
288
289#[cfg(feature = "session")]
291#[derive(Debug, Deserialize)]
292#[cfg_attr(
293 any(feature = "schema", feature = "schema-types"),
294 derive(schemars::JsonSchema)
295)]
296pub struct SessionIdParams {
297 pub session_id: Option<String>,
299}
300
301#[derive(Debug, Serialize)]
302struct BibliographyResult {
303 format: OutputFormat,
304 content: String,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 entries: Option<Vec<String>>,
307}
308
309#[derive(Debug)]
310#[cfg(all(feature = "session", feature = "http"))]
311struct StoredSession {
312 session: DocumentSession,
313 last_access: SystemTime,
314}
315
316#[cfg(all(feature = "session", feature = "http"))]
317const HTTP_SESSION_TTL_SECS: u64 = 30 * 60;
318
319#[cfg(feature = "session")]
320#[derive(Debug)]
321enum SessionMode {
322 Stdio {
323 session: Box<Option<DocumentSession>>,
324 },
325 #[cfg(all(feature = "session", feature = "http"))]
326 Http {
327 sessions: HashMap<String, StoredSession>,
328 next_session_id: AtomicU64,
329 ttl: Duration,
330 },
331}
332
333#[derive(Debug)]
335pub struct RpcDispatcher {
336 #[cfg(feature = "session")]
337 session_mode: SessionMode,
338}
339
340#[derive(Debug)]
342pub enum RpcDispatchError {
343 Message(String),
345 Response(Box<Value>),
347}
348
349impl RpcDispatcher {
350 pub fn new_stdio() -> Self {
352 Self {
353 #[cfg(feature = "session")]
354 session_mode: SessionMode::Stdio {
355 session: Box::new(None),
356 },
357 }
358 }
359
360 #[cfg(feature = "http")]
362 pub fn new_http() -> Self {
363 Self {
364 #[cfg(feature = "session")]
365 session_mode: SessionMode::Http {
366 sessions: HashMap::new(),
367 next_session_id: AtomicU64::new(1),
368 ttl: Duration::from_secs(HTTP_SESSION_TTL_SECS),
369 },
370 }
371 }
372
373 pub fn dispatch(
380 &mut self,
381 req: RpcRequest,
382 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
383 let id = req.id.clone();
384
385 match req.method.as_str() {
386 "render_citation" => render_citation(&req.params, id)
387 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
388 "render_bibliography" => render_bibliography(&req.params, id)
389 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
390 "validate_style" => validate_style(&req.params, id)
391 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
392 "format_document" => format_document(&req.params, id)
393 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
394 #[cfg(feature = "session")]
395 "open_session" => self
396 .open_session(&req.params, id)
397 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
398 #[cfg(feature = "session")]
399 "put_references" => self.put_references(&req.params, id, req.id.clone()),
400 #[cfg(feature = "session")]
401 "insert_citations_batch" => {
402 self.insert_citations_batch(&req.params, id, req.id.clone())
403 }
404 #[cfg(feature = "session")]
405 "insert_citation" => self.insert_citation(&req.params, id, req.id.clone()),
406 #[cfg(feature = "session")]
407 "update_citation" => self.update_citation(&req.params, id, req.id.clone()),
408 #[cfg(feature = "session")]
409 "delete_citation" => self.delete_citation(&req.params, id, req.id.clone()),
410 #[cfg(feature = "session")]
411 "preview_citation" => self.preview_citation(&req.params, id, req.id.clone()),
412 #[cfg(feature = "session")]
413 "get_citations" => self.get_citations(&req.params, id, req.id.clone()),
414 #[cfg(feature = "session")]
415 "get_bibliography" => self.get_bibliography(&req.params, id, req.id.clone()),
416 #[cfg(feature = "session")]
417 "close_session" => self.close_session(&req.params, id, req.id.clone()),
418 _ => Err((
419 Some(req.id),
420 RpcDispatchError::Message(format!("unknown method: {}", req.method)),
421 )),
422 }
423 }
424
425 #[cfg(feature = "session")]
426 fn open_session(&mut self, params: &Value, id: Value) -> Result<Value, ServerError> {
427 let params: OpenSessionParams = serde_json::from_value(params.clone())
428 .map_err(|e| ServerError::CitationError(format!("Invalid request JSON: {e}")))?;
429 let mut style = resolve_style_input(¶ms.style)?;
430 if let Some(src) = ¶ms.style_overrides {
431 apply_style_overrides(&mut style, src)
432 .map_err(|e| ServerError::StyleValidation(e.to_string()))?;
433 }
434 let session = DocumentSession::new(
435 style,
436 params.style,
437 params.locale,
438 params.output_format.unwrap_or_default(),
439 params.document_options,
440 );
441 let session_id = match &mut self.session_mode {
442 SessionMode::Stdio { session: slot } => {
443 **slot = Some(session);
444 "default".to_string()
445 }
446 #[cfg(feature = "http")]
447 SessionMode::Http {
448 sessions,
449 next_session_id,
450 ..
451 } => {
452 let next = next_session_id.fetch_add(1, Ordering::Relaxed);
453 let session_id = format!("s-{next:016x}");
454 sessions.insert(
455 session_id.clone(),
456 StoredSession {
457 session,
458 last_access: SystemTime::now(),
459 },
460 );
461 session_id
462 }
463 };
464 let result = OpenSessionResult { session_id };
465 Ok(json!({ "id": id, "result": result }))
466 }
467
468 #[cfg(feature = "session")]
469 fn put_references(
470 &mut self,
471 params: &Value,
472 id: Value,
473 request_id: Value,
474 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
475 let params: PutReferencesParams = parse_session_params(params, &request_id)?;
476 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
477 session.put_references(params.refs);
478 Ok(json!({ "id": id, "result": {} }))
479 }
480
481 #[cfg(feature = "session")]
482 fn insert_citations_batch(
483 &mut self,
484 params: &Value,
485 id: Value,
486 request_id: Value,
487 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
488 let params: InsertCitationsBatchParams = parse_session_params(params, &request_id)?;
489 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
490 let result = session
491 .insert_citations_batch(params.citations)
492 .map_err(session_method_error(&request_id))?;
493 Ok(json!({ "id": id, "result": result }))
494 }
495
496 #[cfg(feature = "session")]
497 fn insert_citation(
498 &mut self,
499 params: &Value,
500 id: Value,
501 request_id: Value,
502 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
503 let params: InsertCitationParams = parse_session_params(params, &request_id)?;
504 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
505 let result = session
506 .insert_citation(params.citation, params.position)
507 .map_err(session_method_error(&request_id))?;
508 Ok(json!({ "id": id, "result": result }))
509 }
510
511 #[cfg(feature = "session")]
512 fn update_citation(
513 &mut self,
514 params: &Value,
515 id: Value,
516 request_id: Value,
517 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
518 let params: UpdateCitationParams = parse_session_params(params, &request_id)?;
519 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
520 let result = session
521 .update_citation(¶ms.citation_id, params.citation, params.position)
522 .map_err(session_method_error(&request_id))?;
523 Ok(json!({ "id": id, "result": result }))
524 }
525
526 #[cfg(feature = "session")]
527 fn delete_citation(
528 &mut self,
529 params: &Value,
530 id: Value,
531 request_id: Value,
532 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
533 let params: DeleteCitationParams = parse_session_params(params, &request_id)?;
534 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
535 let result = session
536 .delete_citation(¶ms.citation_id)
537 .map_err(session_method_error(&request_id))?;
538 Ok(json!({ "id": id, "result": result }))
539 }
540
541 #[cfg(feature = "session")]
542 fn preview_citation(
543 &mut self,
544 params: &Value,
545 id: Value,
546 request_id: Value,
547 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
548 let params: PreviewCitationParams = parse_session_params(params, &request_id)?;
549 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
550 let result = session
551 .preview_citation(params.items, params.mode, params.position)
552 .map_err(session_method_error(&request_id))?;
553 Ok(json!({ "id": id, "result": result }))
554 }
555
556 #[cfg(feature = "session")]
557 fn get_citations(
558 &mut self,
559 params: &Value,
560 id: Value,
561 request_id: Value,
562 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
563 let params: SessionIdParams = parse_session_params(params, &request_id)?;
564 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
565 Ok(json!({
566 "id": id,
567 "result": { "formatted_citations": session.get_citations() }
568 }))
569 }
570
571 #[cfg(feature = "session")]
572 fn get_bibliography(
573 &mut self,
574 params: &Value,
575 id: Value,
576 request_id: Value,
577 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
578 let params: SessionIdParams = parse_session_params(params, &request_id)?;
579 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
580 Ok(json!({
581 "id": id,
582 "result": { "bibliography": session.get_bibliography() }
583 }))
584 }
585
586 #[cfg(feature = "session")]
587 fn close_session(
588 &mut self,
589 params: &Value,
590 id: Value,
591 request_id: Value,
592 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
593 let params: SessionIdParams = parse_session_params(params, &request_id)?;
594 #[cfg(not(feature = "http"))]
595 let _ = ¶ms;
596 match &mut self.session_mode {
597 SessionMode::Stdio { session } => {
598 **session = None;
599 }
600 #[cfg(feature = "http")]
601 SessionMode::Http { sessions, .. } => {
602 let session_id = params.session_id.as_deref().ok_or_else(|| {
603 (
604 Some(request_id.clone()),
605 RpcDispatchError::Message("missing required field: session_id".to_string()),
606 )
607 })?;
608 sessions.remove(session_id);
609 }
610 }
611 Ok(json!({ "id": id, "result": {} }))
612 }
613
614 #[cfg(feature = "session")]
615 fn session_mut(
616 &mut self,
617 session_id: Option<&str>,
618 request_id: &Value,
619 ) -> Result<&mut DocumentSession, (Option<Value>, RpcDispatchError)> {
620 #[cfg(not(feature = "http"))]
621 let _ = session_id;
622 match &mut self.session_mode {
623 SessionMode::Stdio { session } => session.as_mut().as_mut().ok_or_else(|| {
624 (
625 Some(request_id.clone()),
626 RpcDispatchError::Message("session not open".to_string()),
627 )
628 }),
629 #[cfg(feature = "http")]
630 SessionMode::Http { sessions, ttl, .. } => {
631 let session_id = session_id.ok_or_else(|| {
632 (
633 Some(request_id.clone()),
634 RpcDispatchError::Message("missing required field: session_id".to_string()),
635 )
636 })?;
637 if let Some(stored) = sessions.get(session_id)
638 && stored.last_access.elapsed().unwrap_or_default() > *ttl
639 {
640 let expired_at = format_system_time(stored.last_access + *ttl);
641 sessions.remove(session_id);
642 return Err((
643 Some(request_id.clone()),
644 RpcDispatchError::Response(Box::new(json!({
645 "id": request_id,
646 "error": "session_expired",
647 "session_id": session_id,
648 "expired_at": expired_at
649 }))),
650 ));
651 }
652 let stored = sessions.get_mut(session_id).ok_or_else(|| {
653 (
654 Some(request_id.clone()),
655 RpcDispatchError::Message(format!("session not found: {session_id}")),
656 )
657 })?;
658 stored.last_access = SystemTime::now();
659 Ok(&mut stored.session)
660 }
661 }
662 }
663}
664
665impl Default for RpcDispatcher {
666 fn default() -> Self {
667 Self::new_stdio()
668 }
669}
670
671fn require_field(params: &Value, field: &'static str) -> Result<(), ServerError> {
673 if params.get(field).is_none() {
674 return Err(ServerError::MissingField(field.into()));
675 }
676 Ok(())
677}
678
679fn validate_output_format(params: &Value) -> Result<(), ServerError> {
681 if let Some(v) = params.get("output_format") {
682 serde_json::from_value::<OutputFormat>(v.clone()).map_err(|_| {
683 let raw = v.as_str().unwrap_or("unknown").to_string();
684 ServerError::UnsupportedOutputFormat(raw.into())
685 })?;
686 }
687 Ok(())
688}
689
690pub fn dispatch(req: RpcRequest) -> Result<Value, (Option<Value>, String)> {
701 let mut dispatcher = RpcDispatcher::new_stdio();
702 dispatcher.dispatch(req).map_err(|(id, error)| match error {
703 RpcDispatchError::Message(message) => (id, message),
704 RpcDispatchError::Response(value) => (id, value.to_string()),
705 })
706}
707
708fn render_citation(params: &Value, id: Value) -> Result<Value, ServerError> {
710 require_field(params, "style_path")?;
711 require_field(params, "refs")?;
712 require_field(params, "citation")?;
713 validate_output_format(params)?;
714 let params: RenderCitationParams = serde_json::from_value(params.clone())
715 .map_err(|e| ServerError::CitationError(e.to_string()))?;
716
717 let style = load_style(¶ms.style_path)?;
719
720 let bibliography: Bibliography = serde_json::from_value(params.refs.clone())
722 .map_err(|e| ServerError::BibliographyError(e.to_string()))?;
723
724 let citation: Citation = serde_json::from_value(params.citation.clone())
725 .map_err(|e| ServerError::CitationError(e.to_string()))?;
726
727 let mut processor = Processor::new(style, bibliography);
729 let inject_ast_indices = params.inject_ast_indices.unwrap_or(false);
730 processor.set_inject_ast_indices(inject_ast_indices);
731
732 let output_format = params.output_format.unwrap_or_default();
733 let result = render_citation_with_format(&processor, &citation, output_format)
734 .map_err(|e| ServerError::CitationError(e.to_string()))?;
735
736 Ok(json!({
737 "id": id,
738 "result": result
739 }))
740}
741
742fn render_bibliography(params: &Value, id: Value) -> Result<Value, ServerError> {
744 require_field(params, "style_path")?;
745 require_field(params, "refs")?;
746 validate_output_format(params)?;
747 let params: RenderBibliographyParams = serde_json::from_value(params.clone())
748 .map_err(|e| ServerError::CitationError(e.to_string()))?;
749
750 let style = load_style(¶ms.style_path)?;
752
753 let bibliography: Bibliography = serde_json::from_value(params.refs.clone())
755 .map_err(|e| ServerError::BibliographyError(e.to_string()))?;
756
757 let mut processor = Processor::new(style, bibliography);
759 let inject_ast_indices = params.inject_ast_indices.unwrap_or(false);
760 processor.set_inject_ast_indices(inject_ast_indices);
761
762 let output_format = params.output_format.unwrap_or_default();
763 let content = render_bibliography_with_format(&processor, output_format)?;
764 let entries = matches!(output_format, OutputFormat::Plain).then(|| {
765 content
766 .lines()
767 .filter(|line| !line.is_empty())
768 .map(std::string::ToString::to_string)
769 .collect()
770 });
771 let result = BibliographyResult {
772 format: output_format,
773 content,
774 entries,
775 };
776
777 Ok(json!({
778 "id": id,
779 "result": result
780 }))
781}
782
783fn render_citation_with_format(
784 processor: &Processor,
785 citation: &Citation,
786 format: OutputFormat,
787) -> Result<String, ServerError> {
788 match format {
789 OutputFormat::Plain => Ok(processor.process_citation_with_format::<PlainText>(citation)?),
790 OutputFormat::Html => Ok(processor.process_citation_with_format::<Html>(citation)?),
791 OutputFormat::Djot => Ok(processor.process_citation_with_format::<Djot>(citation)?),
792 OutputFormat::Latex => Ok(processor.process_citation_with_format::<Latex>(citation)?),
793 OutputFormat::Typst => Ok(processor.process_citation_with_format::<Typst>(citation)?),
794 }
795}
796
797fn render_bibliography_with_format(
798 processor: &Processor,
799 format: OutputFormat,
800) -> Result<String, ServerError> {
801 match format {
802 OutputFormat::Plain => Ok(processor.render_bibliography_with_format::<PlainText>()),
803 OutputFormat::Html => Ok(processor.render_bibliography_with_format::<Html>()),
804 OutputFormat::Djot => Ok(processor.render_bibliography_with_format::<Djot>()),
805 OutputFormat::Latex => Ok(processor.render_bibliography_with_format::<Latex>()),
806 OutputFormat::Typst => Ok(processor.render_bibliography_with_format::<Typst>()),
807 }
808}
809
810fn validate_style(params: &Value, id: Value) -> Result<Value, ServerError> {
812 require_field(params, "style_path")?;
813 let params: ValidateStyleParams = serde_json::from_value(params.clone())
814 .map_err(|e| ServerError::CitationError(e.to_string()))?;
815
816 match load_style(¶ms.style_path) {
817 Ok(_) => Ok(json!({
818 "id": id,
819 "result": {
820 "valid": true,
821 "warnings": []
822 }
823 })),
824 Err(e) => Ok(json!({
825 "id": id,
826 "result": {
827 "valid": false,
828 "warnings": [e.to_string()]
829 }
830 })),
831 }
832}
833
834fn format_document(params: &Value, id: Value) -> Result<Value, ServerError> {
836 let request: citum_engine::FormatDocumentRequest = serde_json::from_value(params.clone())
837 .map_err(|e| ServerError::CitationError(format!("Invalid request JSON: {}", e)))?;
838
839 let result = match &request.style {
840 citum_engine::StyleInput::Yaml(_) => citum_engine::format_document(request)
841 .map_err(|e| ServerError::CitationError(e.to_string()))?,
842 citum_engine::StyleInput::Id(s)
843 | citum_engine::StyleInput::Uri(s)
844 | citum_engine::StyleInput::Path(s) => {
845 let style = load_style(s)?;
846 citum_engine::format_document_with_style(style, request)
847 .map_err(|e| ServerError::CitationError(e.to_string()))?
848 }
849 };
850
851 let result_json =
852 serde_json::to_value(&result).map_err(|e| ServerError::CitationError(e.to_string()))?;
853
854 Ok(json!({
855 "id": id,
856 "result": result_json
857 }))
858}
859
860#[cfg(feature = "session")]
861fn resolve_style_input(style_input: &StyleInput) -> Result<Style, ServerError> {
862 match style_input {
863 StyleInput::Yaml(_) => style_input
864 .resolve_local()
865 .map_err(|e| ServerError::CitationError(e.to_string())),
866 StyleInput::Id(s) | StyleInput::Uri(s) | StyleInput::Path(s) => load_style(s),
867 }
868}
869
870fn load_style(style_input: &str) -> Result<Style, ServerError> {
876 use citum_store::resolver::{ResolverError, StyleResolver};
877
878 let chain = citum_store::build_standard_chain()
879 .map_err(|e| ServerError::ResolverError(e.to_string()))?;
880
881 match chain.resolve_style(style_input) {
882 Ok(style) => {
883 let mut resolved = style
884 .try_into_resolved_with(Some(&chain))
885 .map_err(|e| ServerError::StyleResolution(e.to_string()))?;
886 resolved.extends = None;
887 Ok(resolved)
888 }
889 Err(ResolverError::StyleNotFound(_)) => {
890 Err(ServerError::StyleNotFound(style_input.to_string()))
891 }
892 Err(e) => Err(ServerError::ResolverError(e.to_string())),
893 }
894}
895
896#[cfg(feature = "session")]
897fn parse_session_params<T>(
898 params: &Value,
899 request_id: &Value,
900) -> Result<T, (Option<Value>, RpcDispatchError)>
901where
902 T: for<'de> Deserialize<'de>,
903{
904 serde_json::from_value(params.clone()).map_err(|e| {
905 (
906 Some(request_id.clone()),
907 RpcDispatchError::Message(format!("Invalid request JSON: {e}")),
908 )
909 })
910}
911
912#[cfg(feature = "session")]
913fn session_method_error(
914 request_id: &Value,
915) -> impl FnOnce(citum_engine::DocumentSessionError) -> (Option<Value>, RpcDispatchError) + '_ {
916 |err| {
917 (
918 Some(request_id.clone()),
919 RpcDispatchError::Message(err.to_string()),
920 )
921 }
922}
923
924#[cfg(all(feature = "session", feature = "http"))]
925fn format_system_time(time: SystemTime) -> String {
926 let seconds = time
927 .duration_since(UNIX_EPOCH)
928 .unwrap_or_default()
929 .as_secs();
930 if let Ok(seconds) = i64::try_from(seconds)
931 && let Ok(datetime) = time::OffsetDateTime::from_unix_timestamp(seconds)
932 && let Ok(formatted) = datetime.format(&time::format_description::well_known::Rfc3339)
933 {
934 return formatted;
935 }
936 format!("unix:{seconds}")
937}
938
939pub fn error_response(id: Option<Value>, error: RpcDispatchError) -> Value {
941 match error {
942 RpcDispatchError::Message(error) => json!({
943 "id": id,
944 "error": error
945 }),
946 RpcDispatchError::Response(response) => *response,
947 }
948}
949
950pub fn run_stdio() -> io::Result<()> {
958 let stdin = io::stdin();
959 let mut stdout = io::stdout();
960 let mut dispatcher = RpcDispatcher::new_stdio();
961
962 let reader = stdin.lock();
963 for line in reader.lines() {
964 let line = line?;
965
966 if line.is_empty() {
968 continue;
969 }
970
971 let response = match serde_json::from_str::<RpcRequest>(&line) {
973 Ok(req) => match dispatcher.dispatch(req.clone()) {
974 Ok(result) => result,
975 Err((id, error)) => error_response(id, error),
976 },
977 Err(e) => {
978 json!({
980 "id": Value::Null,
981 "error": format!("invalid JSON: {}", e)
982 })
983 }
984 };
985
986 writeln!(stdout, "{response}")?;
988 stdout.flush()?;
989 }
990
991 Ok(())
992}
993
994impl Clone for RpcRequest {
996 fn clone(&self) -> Self {
997 RpcRequest {
998 id: self.id.clone(),
999 method: self.method.clone(),
1000 params: self.params.clone(),
1001 }
1002 }
1003}