ollama_sdk/parser.rs
1//! Provides a generic parser for parsing and handling streaming responses
2//! from the Ollama API.
3//!
4//! This module contains parsers for different types of streaming events,
5//! such as chat completions and text generation.
6
7use std::marker::PhantomData;
8use std::pin::Pin;
9use std::task::{Context, Poll};
10
11use crate::types::OllamaError;
12use crate::Result;
13use bytes::Bytes;
14use futures::Stream;
15use serde::de::DeserializeOwned;
16
17/// Small conversion trait so endpoint-specific event enums can be constructed
18/// from a successful message `M`, an error string, or a partial payload.
19pub trait StreamEventExt<M>: Sized {
20 /// Create an event from a successfully deserialized message.
21 fn from_message(msg: M) -> Self;
22
23 /// Create an event from an error string.
24 fn from_error(err: String) -> Self;
25
26 /// Create a partial event (with optional error text).
27 fn partial(partial: String, error: Option<String>) -> Self;
28}
29
30/// Generic newline-delimited JSON streaming parser.
31///
32/// - `S` is the underlying stream that yields `Result<Bytes>`
33/// - `M` is the concrete message struct you expect per line (DeserializeOwned)
34/// - `E` is the endpoint event enum type that implements `StreamEventExt<M>`
35pub struct GenericStreamParser<S, M, E>
36where
37 S: Stream<Item = Result<Bytes>> + Send + Unpin,
38 M: DeserializeOwned,
39 E: StreamEventExt<M>,
40{
41 inner: S,
42 buffer: Vec<u8>,
43 _marker: PhantomData<(M, E)>,
44}
45
46impl<S, M, E> GenericStreamParser<S, M, E>
47where
48 S: Stream<Item = Result<Bytes>> + Send + Unpin,
49 M: DeserializeOwned,
50 E: StreamEventExt<M>,
51{
52 pub fn new(stream: S) -> Self {
53 Self {
54 inner: stream,
55 buffer: Vec::new(),
56 _marker: PhantomData,
57 }
58 }
59
60 /// Try to parse one complete newline-terminated line from the buffer.
61 /// Returns `Some(Ok(E))` when we parsed one event; `Some(Err(e))` for a transport/error;
62 /// `None` when no full line is available yet.
63 fn parse_lines(&mut self) -> Option<Result<E>> {
64 loop {
65 // find newline
66 let newline_pos = self.buffer.iter().position(|&b| b == b'\n')?;
67 // take inclusive newline bytes
68 let line_bytes = self.buffer.drain(..=newline_pos).collect::<Vec<u8>>();
69 let line_str = String::from_utf8_lossy(&line_bytes);
70 let line_str = line_str.trim();
71
72 if line_str.is_empty() {
73 continue; // skip blank lines
74 }
75
76 // First try to parse the expected message type M
77 match serde_json::from_str::<M>(line_str) {
78 Ok(msg) => return Some(Ok(E::from_message(msg))),
79 Err(e_msg) => {
80 // If not M, try to parse an OllamaError
81 match serde_json::from_str::<OllamaError>(line_str) {
82 Ok(err) => return Some(Ok(E::from_error(err.error))),
83 Err(_) => {
84 // fallback: treat as partial with parse error string
85 return Some(Ok(E::partial(
86 line_str.to_string(),
87 Some(e_msg.to_string()),
88 )));
89 }
90 }
91 }
92 }
93 }
94 }
95}
96
97impl<S, M, E> Stream for GenericStreamParser<S, M, E>
98where
99 S: Stream<Item = Result<Bytes>> + Send + Unpin,
100 M: DeserializeOwned + Unpin,
101 E: StreamEventExt<M> + Unpin,
102{
103 type Item = Result<E>;
104
105 // remove `mut` from the `self` binding; we'll call `get_mut()` to get &mut Self.
106 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
107 // SAFETY / rationale:
108 // We have exclusive access to the pinned reference in this poll method,
109 // so calling `get_mut()` to obtain `&mut Self` for internal field mutation
110 // is correct here.
111 let this = self.get_mut();
112
113 loop {
114 // 1. Try to parse any complete lines in buffer
115 if let Some(event) = this.parse_lines() {
116 return Poll::Ready(Some(event));
117 }
118
119 // 2. If no complete line, check if stream is done
120 if this.buffer.is_empty() {
121 // Only poll inner if buffer is empty
122 match Pin::new(&mut this.inner).poll_next(cx) {
123 Poll::Ready(Some(Ok(bytes))) => {
124 this.buffer.extend_from_slice(&bytes);
125 continue; // loop: try parse again
126 }
127 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
128 Poll::Ready(None) => return Poll::Ready(None), // stream ended, buffer empty
129 Poll::Pending => return Poll::Pending,
130 }
131 } else {
132 // Buffer has data, but no newline → need more
133 // Poll inner stream
134 match Pin::new(&mut this.inner).poll_next(cx) {
135 Poll::Ready(Some(Ok(bytes))) => {
136 this.buffer.extend_from_slice(&bytes);
137 continue;
138 }
139 Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))),
140 Poll::Ready(None) => {
141 // Stream ended with partial data
142 let content = String::from_utf8_lossy(&this.buffer).to_string();
143 this.buffer.clear();
144 if !content.trim().is_empty() {
145 return Poll::Ready(Some(Ok(E::partial(content, None))));
146 }
147 return Poll::Ready(None);
148 }
149 Poll::Pending => return Poll::Pending,
150 }
151 }
152 }
153 }
154}