1use crate::error::ServerError;
44use citum_engine::{
45 Bibliography, BibliographyBlockRequest, 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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub bibliography_blocks: Vec<BibliographyBlockRequest>,
172 pub document_options: Option<DocumentOptions>,
174 #[serde(default, skip_serializing_if = "Vec::is_empty")]
179 pub nocite: Vec<String>,
180}
181
182#[cfg(feature = "session")]
184#[derive(Debug, Deserialize)]
185#[cfg_attr(
186 any(feature = "schema", feature = "schema-types"),
187 derive(schemars::JsonSchema)
188)]
189pub struct OpenSessionParams {
190 pub style: StyleInput,
192 pub style_overrides: Option<String>,
196 pub locale: Option<String>,
198 pub output_format: Option<OutputFormatKind>,
200 pub document_options: Option<DocumentOptions>,
202}
203
204#[cfg(feature = "session")]
206#[derive(Debug, Deserialize)]
207#[cfg_attr(
208 any(feature = "schema", feature = "schema-types"),
209 derive(schemars::JsonSchema)
210)]
211pub struct PutReferencesParams {
212 pub session_id: Option<String>,
214 pub refs: RefsInput,
216}
217
218#[cfg(feature = "session")]
220#[derive(Debug, Deserialize)]
221#[cfg_attr(
222 any(feature = "schema", feature = "schema-types"),
223 derive(schemars::JsonSchema)
224)]
225pub struct SetNociteParams {
226 pub session_id: Option<String>,
228 #[serde(default)]
233 pub nocite: Vec<String>,
234}
235
236#[cfg(feature = "session")]
238#[derive(Debug, Deserialize)]
239#[cfg_attr(
240 any(feature = "schema", feature = "schema-types"),
241 derive(schemars::JsonSchema)
242)]
243pub struct InsertCitationsBatchParams {
244 pub session_id: Option<String>,
246 pub citations: Vec<CitationOccurrence>,
248}
249
250#[cfg(feature = "session")]
252#[derive(Debug, Deserialize)]
253#[cfg_attr(
254 any(feature = "schema", feature = "schema-types"),
255 derive(schemars::JsonSchema)
256)]
257pub struct InsertCitationParams {
258 pub session_id: Option<String>,
260 pub citation: CitationOccurrence,
262 pub position: Option<CitationInsertPosition>,
264}
265
266#[cfg(feature = "session")]
268#[derive(Debug, Deserialize)]
269#[cfg_attr(
270 any(feature = "schema", feature = "schema-types"),
271 derive(schemars::JsonSchema)
272)]
273pub struct UpdateCitationParams {
274 pub session_id: Option<String>,
276 pub citation_id: String,
278 pub citation: CitationOccurrence,
280 pub position: Option<CitationInsertPosition>,
282}
283
284#[cfg(feature = "session")]
286#[derive(Debug, Deserialize)]
287#[cfg_attr(
288 any(feature = "schema", feature = "schema-types"),
289 derive(schemars::JsonSchema)
290)]
291pub struct DeleteCitationParams {
292 pub session_id: Option<String>,
294 pub citation_id: String,
296}
297
298#[cfg(feature = "session")]
300#[derive(Debug, Deserialize)]
301#[cfg_attr(
302 any(feature = "schema", feature = "schema-types"),
303 derive(schemars::JsonSchema)
304)]
305pub struct PreviewCitationParams {
306 pub session_id: Option<String>,
308 pub items: Vec<CitationOccurrenceItem>,
310 pub mode: Option<citum_schema::data::citation::CitationMode>,
312 pub position: Option<CitationInsertPosition>,
314}
315
316#[cfg(feature = "session")]
318#[derive(Debug, Deserialize)]
319#[cfg_attr(
320 any(feature = "schema", feature = "schema-types"),
321 derive(schemars::JsonSchema)
322)]
323pub struct SessionIdParams {
324 pub session_id: Option<String>,
326}
327
328#[derive(Debug, Serialize)]
329struct BibliographyResult {
330 format: OutputFormat,
331 content: String,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 entries: Option<Vec<String>>,
334}
335
336#[derive(Debug)]
337#[cfg(all(feature = "session", feature = "http"))]
338struct StoredSession {
339 session: DocumentSession,
340 last_access: SystemTime,
341}
342
343#[cfg(all(feature = "session", feature = "http"))]
344const HTTP_SESSION_TTL_SECS: u64 = 30 * 60;
345
346#[cfg(feature = "session")]
347#[derive(Debug)]
348enum SessionMode {
349 Stdio {
350 session: Box<Option<DocumentSession>>,
351 },
352 #[cfg(all(feature = "session", feature = "http"))]
353 Http {
354 sessions: HashMap<String, StoredSession>,
355 next_session_id: AtomicU64,
356 ttl: Duration,
357 },
358}
359
360#[derive(Debug)]
362pub struct RpcDispatcher {
363 #[cfg(feature = "session")]
364 session_mode: SessionMode,
365}
366
367#[derive(Debug)]
369pub enum RpcDispatchError {
370 Message(String),
372 Response(Box<Value>),
374}
375
376impl RpcDispatcher {
377 pub fn new_stdio() -> Self {
379 Self {
380 #[cfg(feature = "session")]
381 session_mode: SessionMode::Stdio {
382 session: Box::new(None),
383 },
384 }
385 }
386
387 #[cfg(feature = "http")]
389 pub fn new_http() -> Self {
390 Self {
391 #[cfg(feature = "session")]
392 session_mode: SessionMode::Http {
393 sessions: HashMap::new(),
394 next_session_id: AtomicU64::new(1),
395 ttl: Duration::from_secs(HTTP_SESSION_TTL_SECS),
396 },
397 }
398 }
399
400 pub fn dispatch(
407 &mut self,
408 req: RpcRequest,
409 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
410 let id = req.id.clone();
411
412 match req.method.as_str() {
413 "render_citation" => render_citation(&req.params, id)
414 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
415 "render_bibliography" => render_bibliography(&req.params, id)
416 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
417 "validate_style" => validate_style(&req.params, id)
418 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
419 "format_document" => format_document(&req.params, id)
420 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
421 #[cfg(feature = "session")]
422 "open_session" => self
423 .open_session(&req.params, id)
424 .map_err(|e| (Some(req.id), RpcDispatchError::Message(e.to_string()))),
425 #[cfg(feature = "session")]
426 "put_references" => self.put_references(&req.params, id, req.id.clone()),
427 #[cfg(feature = "session")]
428 "set_nocite" => self.set_nocite(&req.params, id, req.id.clone()),
429 #[cfg(feature = "session")]
430 "insert_citations_batch" => {
431 self.insert_citations_batch(&req.params, id, req.id.clone())
432 }
433 #[cfg(feature = "session")]
434 "insert_citation" => self.insert_citation(&req.params, id, req.id.clone()),
435 #[cfg(feature = "session")]
436 "update_citation" => self.update_citation(&req.params, id, req.id.clone()),
437 #[cfg(feature = "session")]
438 "delete_citation" => self.delete_citation(&req.params, id, req.id.clone()),
439 #[cfg(feature = "session")]
440 "preview_citation" => self.preview_citation(&req.params, id, req.id.clone()),
441 #[cfg(feature = "session")]
442 "get_citations" => self.get_citations(&req.params, id, req.id.clone()),
443 #[cfg(feature = "session")]
444 "get_bibliography" => self.get_bibliography(&req.params, id, req.id.clone()),
445 #[cfg(feature = "session")]
446 "close_session" => self.close_session(&req.params, id, req.id.clone()),
447 _ => Err((
448 Some(req.id),
449 RpcDispatchError::Message(format!("unknown method: {}", req.method)),
450 )),
451 }
452 }
453
454 #[cfg(feature = "session")]
455 fn open_session(&mut self, params: &Value, id: Value) -> Result<Value, ServerError> {
456 let params: OpenSessionParams = serde_json::from_value(params.clone())
457 .map_err(|e| ServerError::CitationError(format!("Invalid request JSON: {e}")))?;
458 let mut style = resolve_style_input(¶ms.style)?;
459 if let Some(src) = ¶ms.style_overrides {
460 apply_style_overrides(&mut style, src)
461 .map_err(|e| ServerError::StyleValidation(e.to_string()))?;
462 }
463 let session = DocumentSession::new(
464 style,
465 params.style,
466 params.locale,
467 params.output_format.unwrap_or_default(),
468 params.document_options,
469 );
470 let session_id = match &mut self.session_mode {
471 SessionMode::Stdio { session: slot } => {
472 **slot = Some(session);
473 "default".to_string()
474 }
475 #[cfg(feature = "http")]
476 SessionMode::Http {
477 sessions,
478 next_session_id,
479 ..
480 } => {
481 let next = next_session_id.fetch_add(1, Ordering::Relaxed);
482 let session_id = format!("s-{next:016x}");
483 sessions.insert(
484 session_id.clone(),
485 StoredSession {
486 session,
487 last_access: SystemTime::now(),
488 },
489 );
490 session_id
491 }
492 };
493 let result = OpenSessionResult { session_id };
494 Ok(json!({ "id": id, "result": result }))
495 }
496
497 #[cfg(feature = "session")]
498 fn put_references(
499 &mut self,
500 params: &Value,
501 id: Value,
502 request_id: Value,
503 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
504 let params: PutReferencesParams = parse_session_params(params, &request_id)?;
505 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
506 session.put_references(params.refs);
507 Ok(json!({ "id": id, "result": {} }))
508 }
509
510 #[cfg(feature = "session")]
511 fn set_nocite(
512 &mut self,
513 params: &Value,
514 id: Value,
515 request_id: Value,
516 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
517 let params: SetNociteParams = parse_session_params(params, &request_id)?;
518 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
519 let result = session
520 .set_nocite(params.nocite)
521 .map_err(|e| (Some(request_id), RpcDispatchError::Message(e.to_string())))?;
522 Ok(json!({ "id": id, "result": result }))
523 }
524
525 #[cfg(feature = "session")]
526 fn insert_citations_batch(
527 &mut self,
528 params: &Value,
529 id: Value,
530 request_id: Value,
531 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
532 let params: InsertCitationsBatchParams = parse_session_params(params, &request_id)?;
533 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
534 let result = session
535 .insert_citations_batch(params.citations)
536 .map_err(session_method_error(&request_id))?;
537 Ok(json!({ "id": id, "result": result }))
538 }
539
540 #[cfg(feature = "session")]
541 fn insert_citation(
542 &mut self,
543 params: &Value,
544 id: Value,
545 request_id: Value,
546 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
547 let params: InsertCitationParams = parse_session_params(params, &request_id)?;
548 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
549 let result = session
550 .insert_citation(params.citation, params.position)
551 .map_err(session_method_error(&request_id))?;
552 Ok(json!({ "id": id, "result": result }))
553 }
554
555 #[cfg(feature = "session")]
556 fn update_citation(
557 &mut self,
558 params: &Value,
559 id: Value,
560 request_id: Value,
561 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
562 let params: UpdateCitationParams = parse_session_params(params, &request_id)?;
563 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
564 let result = session
565 .update_citation(¶ms.citation_id, params.citation, params.position)
566 .map_err(session_method_error(&request_id))?;
567 Ok(json!({ "id": id, "result": result }))
568 }
569
570 #[cfg(feature = "session")]
571 fn delete_citation(
572 &mut self,
573 params: &Value,
574 id: Value,
575 request_id: Value,
576 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
577 let params: DeleteCitationParams = parse_session_params(params, &request_id)?;
578 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
579 let result = session
580 .delete_citation(¶ms.citation_id)
581 .map_err(session_method_error(&request_id))?;
582 Ok(json!({ "id": id, "result": result }))
583 }
584
585 #[cfg(feature = "session")]
586 fn preview_citation(
587 &mut self,
588 params: &Value,
589 id: Value,
590 request_id: Value,
591 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
592 let params: PreviewCitationParams = parse_session_params(params, &request_id)?;
593 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
594 let result = session
595 .preview_citation(params.items, params.mode, params.position)
596 .map_err(session_method_error(&request_id))?;
597 Ok(json!({ "id": id, "result": result }))
598 }
599
600 #[cfg(feature = "session")]
601 fn get_citations(
602 &mut self,
603 params: &Value,
604 id: Value,
605 request_id: Value,
606 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
607 let params: SessionIdParams = parse_session_params(params, &request_id)?;
608 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
609 Ok(json!({
610 "id": id,
611 "result": { "formatted_citations": session.get_citations() }
612 }))
613 }
614
615 #[cfg(feature = "session")]
616 fn get_bibliography(
617 &mut self,
618 params: &Value,
619 id: Value,
620 request_id: Value,
621 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
622 let params: SessionIdParams = parse_session_params(params, &request_id)?;
623 let session = self.session_mut(params.session_id.as_deref(), &request_id)?;
624 Ok(json!({
625 "id": id,
626 "result": { "bibliography": session.get_bibliography() }
627 }))
628 }
629
630 #[cfg(feature = "session")]
631 fn close_session(
632 &mut self,
633 params: &Value,
634 id: Value,
635 request_id: Value,
636 ) -> Result<Value, (Option<Value>, RpcDispatchError)> {
637 let params: SessionIdParams = parse_session_params(params, &request_id)?;
638 #[cfg(not(feature = "http"))]
639 let _ = ¶ms;
640 match &mut self.session_mode {
641 SessionMode::Stdio { session } => {
642 **session = None;
643 }
644 #[cfg(feature = "http")]
645 SessionMode::Http { sessions, .. } => {
646 let session_id = params.session_id.as_deref().ok_or_else(|| {
647 (
648 Some(request_id.clone()),
649 RpcDispatchError::Message("missing required field: session_id".to_string()),
650 )
651 })?;
652 sessions.remove(session_id);
653 }
654 }
655 Ok(json!({ "id": id, "result": {} }))
656 }
657
658 #[cfg(feature = "session")]
659 fn session_mut(
660 &mut self,
661 session_id: Option<&str>,
662 request_id: &Value,
663 ) -> Result<&mut DocumentSession, (Option<Value>, RpcDispatchError)> {
664 #[cfg(not(feature = "http"))]
665 let _ = session_id;
666 match &mut self.session_mode {
667 SessionMode::Stdio { session } => session.as_mut().as_mut().ok_or_else(|| {
668 (
669 Some(request_id.clone()),
670 RpcDispatchError::Message("session not open".to_string()),
671 )
672 }),
673 #[cfg(feature = "http")]
674 SessionMode::Http { sessions, ttl, .. } => {
675 let session_id = session_id.ok_or_else(|| {
676 (
677 Some(request_id.clone()),
678 RpcDispatchError::Message("missing required field: session_id".to_string()),
679 )
680 })?;
681 if let Some(stored) = sessions.get(session_id)
682 && stored.last_access.elapsed().unwrap_or_default() > *ttl
683 {
684 let expired_at = format_system_time(stored.last_access + *ttl);
685 sessions.remove(session_id);
686 return Err((
687 Some(request_id.clone()),
688 RpcDispatchError::Response(Box::new(json!({
689 "id": request_id,
690 "error": "session_expired",
691 "session_id": session_id,
692 "expired_at": expired_at
693 }))),
694 ));
695 }
696 let stored = sessions.get_mut(session_id).ok_or_else(|| {
697 (
698 Some(request_id.clone()),
699 RpcDispatchError::Message(format!("session not found: {session_id}")),
700 )
701 })?;
702 stored.last_access = SystemTime::now();
703 Ok(&mut stored.session)
704 }
705 }
706 }
707}
708
709impl Default for RpcDispatcher {
710 fn default() -> Self {
711 Self::new_stdio()
712 }
713}
714
715fn require_field(params: &Value, field: &'static str) -> Result<(), ServerError> {
717 if params.get(field).is_none() {
718 return Err(ServerError::MissingField(field.into()));
719 }
720 Ok(())
721}
722
723fn validate_output_format(params: &Value) -> Result<(), ServerError> {
725 if let Some(v) = params.get("output_format") {
726 serde_json::from_value::<OutputFormat>(v.clone()).map_err(|_| {
727 let raw = v.as_str().unwrap_or("unknown").to_string();
728 ServerError::UnsupportedOutputFormat(raw.into())
729 })?;
730 }
731 Ok(())
732}
733
734pub fn dispatch(req: RpcRequest) -> Result<Value, (Option<Value>, String)> {
745 let mut dispatcher = RpcDispatcher::new_stdio();
746 dispatcher.dispatch(req).map_err(|(id, error)| match error {
747 RpcDispatchError::Message(message) => (id, message),
748 RpcDispatchError::Response(value) => (id, value.to_string()),
749 })
750}
751
752fn render_citation(params: &Value, id: Value) -> Result<Value, ServerError> {
754 require_field(params, "style_path")?;
755 require_field(params, "refs")?;
756 require_field(params, "citation")?;
757 validate_output_format(params)?;
758 let params: RenderCitationParams = serde_json::from_value(params.clone())
759 .map_err(|e| ServerError::CitationError(e.to_string()))?;
760
761 let style = load_style(¶ms.style_path)?;
763
764 let bibliography: Bibliography = serde_json::from_value(params.refs.clone())
766 .map_err(|e| ServerError::BibliographyError(e.to_string()))?;
767
768 let citation: Citation = serde_json::from_value(params.citation.clone())
769 .map_err(|e| ServerError::CitationError(e.to_string()))?;
770
771 let mut processor = Processor::new(style, bibliography);
773 let inject_ast_indices = params.inject_ast_indices.unwrap_or(false);
774 processor.set_inject_ast_indices(inject_ast_indices);
775
776 let output_format = params.output_format.unwrap_or_default();
777 let result = render_citation_with_format(&processor, &citation, output_format)
778 .map_err(|e| ServerError::CitationError(e.to_string()))?;
779
780 Ok(json!({
781 "id": id,
782 "result": result
783 }))
784}
785
786fn render_bibliography(params: &Value, id: Value) -> Result<Value, ServerError> {
788 require_field(params, "style_path")?;
789 require_field(params, "refs")?;
790 validate_output_format(params)?;
791 let params: RenderBibliographyParams = serde_json::from_value(params.clone())
792 .map_err(|e| ServerError::CitationError(e.to_string()))?;
793
794 let style = load_style(¶ms.style_path)?;
796
797 let bibliography: Bibliography = serde_json::from_value(params.refs.clone())
799 .map_err(|e| ServerError::BibliographyError(e.to_string()))?;
800
801 let mut processor = Processor::new(style, bibliography);
803 let inject_ast_indices = params.inject_ast_indices.unwrap_or(false);
804 processor.set_inject_ast_indices(inject_ast_indices);
805
806 let output_format = params.output_format.unwrap_or_default();
807 let content = render_bibliography_with_format(&processor, output_format)?;
808 let entries = matches!(output_format, OutputFormat::Plain).then(|| {
809 content
810 .lines()
811 .filter(|line| !line.is_empty())
812 .map(std::string::ToString::to_string)
813 .collect()
814 });
815 let result = BibliographyResult {
816 format: output_format,
817 content,
818 entries,
819 };
820
821 Ok(json!({
822 "id": id,
823 "result": result
824 }))
825}
826
827fn render_citation_with_format(
828 processor: &Processor,
829 citation: &Citation,
830 format: OutputFormat,
831) -> Result<String, ServerError> {
832 match format {
833 OutputFormat::Plain => Ok(processor.process_citation_with_format::<PlainText>(citation)?),
834 OutputFormat::Html => Ok(processor.process_citation_with_format::<Html>(citation)?),
835 OutputFormat::Djot => Ok(processor.process_citation_with_format::<Djot>(citation)?),
836 OutputFormat::Latex => Ok(processor.process_citation_with_format::<Latex>(citation)?),
837 OutputFormat::Typst => Ok(processor.process_citation_with_format::<Typst>(citation)?),
838 }
839}
840
841fn render_bibliography_with_format(
842 processor: &Processor,
843 format: OutputFormat,
844) -> Result<String, ServerError> {
845 match format {
846 OutputFormat::Plain => Ok(processor.render_bibliography_with_format::<PlainText>()),
847 OutputFormat::Html => Ok(processor.render_bibliography_with_format::<Html>()),
848 OutputFormat::Djot => Ok(processor.render_bibliography_with_format::<Djot>()),
849 OutputFormat::Latex => Ok(processor.render_bibliography_with_format::<Latex>()),
850 OutputFormat::Typst => Ok(processor.render_bibliography_with_format::<Typst>()),
851 }
852}
853
854fn validate_style(params: &Value, id: Value) -> Result<Value, ServerError> {
856 require_field(params, "style_path")?;
857 let params: ValidateStyleParams = serde_json::from_value(params.clone())
858 .map_err(|e| ServerError::CitationError(e.to_string()))?;
859
860 match load_style(¶ms.style_path) {
861 Ok(_) => Ok(json!({
862 "id": id,
863 "result": {
864 "valid": true,
865 "warnings": []
866 }
867 })),
868 Err(e) => Ok(json!({
869 "id": id,
870 "result": {
871 "valid": false,
872 "warnings": [e.to_string()]
873 }
874 })),
875 }
876}
877
878fn format_document(params: &Value, id: Value) -> Result<Value, ServerError> {
880 let request: citum_engine::FormatDocumentRequest = serde_json::from_value(params.clone())
881 .map_err(|e| ServerError::CitationError(format!("Invalid request JSON: {}", e)))?;
882
883 let result = match &request.style {
884 citum_engine::StyleInput::Yaml(_) => citum_engine::format_document(request)
885 .map_err(|e| ServerError::CitationError(e.to_string()))?,
886 citum_engine::StyleInput::Id(s)
887 | citum_engine::StyleInput::Uri(s)
888 | citum_engine::StyleInput::Path(s) => {
889 let style = load_style(s)?;
890 citum_engine::format_document_with_style(style, request)
891 .map_err(|e| ServerError::CitationError(e.to_string()))?
892 }
893 };
894
895 let result_json =
896 serde_json::to_value(&result).map_err(|e| ServerError::CitationError(e.to_string()))?;
897
898 Ok(json!({
899 "id": id,
900 "result": result_json
901 }))
902}
903
904#[cfg(feature = "session")]
905fn resolve_style_input(style_input: &StyleInput) -> Result<Style, ServerError> {
906 match style_input {
907 StyleInput::Yaml(_) => style_input
908 .resolve_local()
909 .map_err(|e| ServerError::CitationError(e.to_string())),
910 StyleInput::Id(s) | StyleInput::Uri(s) | StyleInput::Path(s) => load_style(s),
911 }
912}
913
914fn load_style(style_input: &str) -> Result<Style, ServerError> {
920 use citum_store::resolver::{ResolverError, StyleResolver};
921
922 let chain = citum_store::build_standard_chain()
923 .map_err(|e| ServerError::ResolverError(e.to_string()))?;
924
925 match chain.resolve_style(style_input) {
926 Ok(style) => {
927 let mut resolved = style
928 .try_into_resolved_with(Some(&chain))
929 .map_err(|e| ServerError::StyleResolution(e.to_string()))?;
930 resolved.extends = None;
931 Ok(resolved)
932 }
933 Err(ResolverError::StyleNotFound(_)) => {
934 Err(ServerError::StyleNotFound(style_input.to_string()))
935 }
936 Err(e) => Err(ServerError::ResolverError(e.to_string())),
937 }
938}
939
940#[cfg(feature = "session")]
941fn parse_session_params<T>(
942 params: &Value,
943 request_id: &Value,
944) -> Result<T, (Option<Value>, RpcDispatchError)>
945where
946 T: for<'de> Deserialize<'de>,
947{
948 serde_json::from_value(params.clone()).map_err(|e| {
949 (
950 Some(request_id.clone()),
951 RpcDispatchError::Message(format!("Invalid request JSON: {e}")),
952 )
953 })
954}
955
956#[cfg(feature = "session")]
957fn session_method_error(
958 request_id: &Value,
959) -> impl FnOnce(citum_engine::DocumentSessionError) -> (Option<Value>, RpcDispatchError) + '_ {
960 |err| {
961 (
962 Some(request_id.clone()),
963 RpcDispatchError::Message(err.to_string()),
964 )
965 }
966}
967
968#[cfg(all(feature = "session", feature = "http"))]
969fn format_system_time(time: SystemTime) -> String {
970 let seconds = time
971 .duration_since(UNIX_EPOCH)
972 .unwrap_or_default()
973 .as_secs();
974 if let Ok(seconds) = i64::try_from(seconds)
975 && let Ok(datetime) = time::OffsetDateTime::from_unix_timestamp(seconds)
976 && let Ok(formatted) = datetime.format(&time::format_description::well_known::Rfc3339)
977 {
978 return formatted;
979 }
980 format!("unix:{seconds}")
981}
982
983pub fn error_response(id: Option<Value>, error: RpcDispatchError) -> Value {
985 match error {
986 RpcDispatchError::Message(error) => json!({
987 "id": id,
988 "error": error
989 }),
990 RpcDispatchError::Response(response) => *response,
991 }
992}
993
994pub fn run_stdio() -> io::Result<()> {
1002 let stdin = io::stdin();
1003 let mut stdout = io::stdout();
1004 let mut dispatcher = RpcDispatcher::new_stdio();
1005
1006 let reader = stdin.lock();
1007 for line in reader.lines() {
1008 let line = line?;
1009
1010 if line.is_empty() {
1012 continue;
1013 }
1014
1015 let response = match serde_json::from_str::<RpcRequest>(&line) {
1017 Ok(req) => match dispatcher.dispatch(req.clone()) {
1018 Ok(result) => result,
1019 Err((id, error)) => error_response(id, error),
1020 },
1021 Err(e) => {
1022 json!({
1024 "id": Value::Null,
1025 "error": format!("invalid JSON: {}", e)
1026 })
1027 }
1028 };
1029
1030 writeln!(stdout, "{response}")?;
1032 stdout.flush()?;
1033 }
1034
1035 Ok(())
1036}
1037
1038impl Clone for RpcRequest {
1040 fn clone(&self) -> Self {
1041 RpcRequest {
1042 id: self.id.clone(),
1043 method: self.method.clone(),
1044 params: self.params.clone(),
1045 }
1046 }
1047}