Skip to main content

graphix_package_toml/
lib.rs

1#![doc(
2    html_logo_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg",
3    html_favicon_url = "https://graphix-lang.github.io/graphix/graphix-icon.svg"
4)]
5use anyhow::{bail, Result};
6use arcstr::ArcStr;
7use bytes::Bytes;
8use chrono::Utc;
9use graphix_compiler::{
10    errf, typ::FnType, typ::Type, ExecCtx, Node, Rt, Scope, TypecheckPhase, UserEvent,
11};
12use graphix_package_core::{
13    extract_cast_type, is_struct, CachedArgs, CachedArgsAsync, CachedVals, EvalCached,
14    EvalCachedAsync,
15};
16use graphix_package_sys::{get_stream, StreamKind};
17use netidx_value::{PBytes, ValArray, Value};
18use poolshark::local::LPooled;
19use std::sync::Arc;
20use tokio::{io::AsyncReadExt, io::AsyncWriteExt, sync::Mutex};
21use triomphe::Arc as TArc;
22
23// ── TOML ↔ Value conversion ──────────────────────────────────────
24
25fn toml_to_value(v: toml::Value) -> Value {
26    match v {
27        toml::Value::String(s) => Value::String(ArcStr::from(s.as_str())),
28        toml::Value::Integer(i) => Value::I64(i),
29        toml::Value::Float(f) => Value::F64(f),
30        toml::Value::Boolean(b) => Value::Bool(b),
31        toml::Value::Datetime(dt) => {
32            let s = dt.to_string();
33            match chrono::DateTime::parse_from_rfc3339(&s) {
34                Ok(parsed) => Value::DateTime(TArc::new(parsed.with_timezone(&Utc))),
35                Err(_) => Value::String(ArcStr::from(s.as_str())),
36            }
37        }
38        toml::Value::Array(arr) => {
39            let mut vals: LPooled<Vec<Value>> =
40                arr.into_iter().map(toml_to_value).collect();
41            Value::Array(ValArray::from_iter_exact(vals.drain(..)))
42        }
43        toml::Value::Table(table) => {
44            let mut pairs: LPooled<Vec<(String, Value)>> =
45                table.into_iter().map(|(k, v)| (k, toml_to_value(v))).collect();
46            pairs.sort_by(|a, b| a.0.cmp(&b.0));
47            let mut vals: LPooled<Vec<Value>> = pairs
48                .drain(..)
49                .map(|(k, v)| {
50                    Value::Array(ValArray::from([
51                        Value::String(ArcStr::from(k.as_str())),
52                        v,
53                    ]))
54                })
55                .collect();
56            Value::Array(ValArray::from_iter_exact(vals.drain(..)))
57        }
58    }
59}
60
61fn value_to_toml(value: &Value) -> Result<toml::Value, String> {
62    match value {
63        Value::Null => Err("cannot represent null in TOML".into()),
64        Value::Bool(b) => Ok(toml::Value::Boolean(*b)),
65        Value::I8(n) => Ok(toml::Value::Integer(i64::from(*n))),
66        Value::I16(n) => Ok(toml::Value::Integer(i64::from(*n))),
67        Value::I32(n) => Ok(toml::Value::Integer(i64::from(*n))),
68        Value::I64(n) => Ok(toml::Value::Integer(*n)),
69        Value::U8(n) => Ok(toml::Value::Integer(i64::from(*n))),
70        Value::U16(n) => Ok(toml::Value::Integer(i64::from(*n))),
71        Value::U32(n) => Ok(toml::Value::Integer(i64::from(*n))),
72        Value::U64(n) => i64::try_from(*n)
73            .map(toml::Value::Integer)
74            .map_err(|_| format!("u64 value {n} exceeds TOML i64 range")),
75        Value::V32(n) => Ok(toml::Value::Integer(i64::from(*n))),
76        Value::V64(n) => i64::try_from(*n)
77            .map(toml::Value::Integer)
78            .map_err(|_| format!("v64 value {n} exceeds TOML i64 range")),
79        Value::Z32(n) => Ok(toml::Value::Integer(i64::from(*n))),
80        Value::Z64(n) => Ok(toml::Value::Integer(*n)),
81        Value::F32(n) => Ok(toml::Value::Float(*n as f64)),
82        Value::F64(n) => Ok(toml::Value::Float(*n)),
83        Value::String(s) => Ok(toml::Value::String(s.to_string())),
84        Value::DateTime(dt) => {
85            let s = dt.to_rfc3339();
86            s.parse()
87                .map(toml::Value::Datetime)
88                .map_err(|e| format!("cannot convert datetime to TOML: {e}"))
89        }
90        Value::Array(arr) => {
91            if is_struct(arr) {
92                let mut table = toml::map::Map::new();
93                for v in arr.iter() {
94                    if let Value::Array(pair) = v {
95                        if let Value::String(k) = &pair[0] {
96                            table.insert(k.to_string(), value_to_toml(&pair[1])?);
97                        }
98                    }
99                }
100                Ok(toml::Value::Table(table))
101            } else {
102                let mut vals: LPooled<Vec<toml::Value>> =
103                    arr.iter().map(value_to_toml).collect::<Result<_, _>>()?;
104                Ok(toml::Value::Array(vals.drain(..).collect()))
105            }
106        }
107        Value::Bytes(_) => Err("cannot represent bytes in TOML".into()),
108        Value::Duration(_) => Err("cannot represent duration in TOML".into()),
109        Value::Decimal(_) => Err("cannot represent decimal in TOML".into()),
110        Value::Map(_) => Err("cannot represent map in TOML".into()),
111        Value::Error(_) => Err("cannot serialize Error to TOML".into()),
112        Value::Abstract(_) => Err("cannot serialize abstract type to TOML".into()),
113    }
114}
115
116// ── TomlRead (async — handles string, bytes, and stream) ────────
117
118#[derive(Debug)]
119enum ReadInput {
120    Str(ArcStr),
121    Bytes(Bytes),
122    Stream(Arc<Mutex<Option<StreamKind>>>),
123}
124
125#[derive(Debug, Default)]
126struct TomlReadEv {
127    cast_typ: Option<Type>,
128}
129
130impl EvalCachedAsync for TomlReadEv {
131    const NAME: &str = "toml_read";
132    const NEEDS_CALLSITE: bool = true;
133    type Args = ReadInput;
134
135    fn init<R: Rt, E: UserEvent>(
136        _ctx: &mut ExecCtx<R, E>,
137        _typ: &FnType,
138        resolved: Option<&FnType>,
139        _scope: &Scope,
140        _from: &[Node<R, E>],
141        _top_id: graphix_compiler::expr::ExprId,
142    ) -> Self {
143        Self { cast_typ: extract_cast_type(resolved) }
144    }
145
146    fn typecheck<R: Rt, E: UserEvent>(
147        &mut self,
148        _ctx: &mut ExecCtx<R, E>,
149        _from: &mut [Node<R, E>],
150        phase: TypecheckPhase<'_>,
151    ) -> Result<()> {
152        match phase {
153            TypecheckPhase::Lambda => Ok(()),
154            TypecheckPhase::CallSite(resolved) => {
155                self.cast_typ = extract_cast_type(Some(resolved));
156                if self.cast_typ.is_none() {
157                    bail!("toml::read requires a concrete return type")
158                }
159                Ok(())
160            }
161        }
162    }
163
164    fn map_value<R: Rt, E: UserEvent>(
165        &mut self,
166        ctx: &mut ExecCtx<R, E>,
167        v: Value,
168    ) -> Option<Value> {
169        match &self.cast_typ {
170            Some(typ) => Some(typ.cast_value(&ctx.env, v)),
171            None => Some(errf!("TomlErr", "no concrete return type found")),
172        }
173    }
174
175    fn prepare_args(&mut self, cached: &CachedVals) -> Option<Self::Args> {
176        let v = cached.0.first()?.as_ref()?;
177        match v {
178            Value::String(s) => Some(ReadInput::Str(s.clone())),
179            Value::Bytes(b) => Some(ReadInput::Bytes((**b).clone())),
180            Value::Abstract(_) => Some(ReadInput::Stream(get_stream(cached, 0)?)),
181            _ => None,
182        }
183    }
184
185    fn eval(input: Self::Args) -> impl Future<Output = Value> + Send {
186        async move {
187            match input {
188                ReadInput::Str(s) => match toml::from_str::<toml::Value>(&s) {
189                    Ok(t) => toml_to_value(t),
190                    Err(e) => errf!("TomlErr", "{e}"),
191                },
192                ReadInput::Bytes(b) => {
193                    let s = match std::str::from_utf8(&b) {
194                        Ok(s) => s,
195                        Err(e) => return errf!("TomlErr", "invalid UTF-8: {e}"),
196                    };
197                    match toml::from_str::<toml::Value>(s) {
198                        Ok(t) => toml_to_value(t),
199                        Err(e) => errf!("TomlErr", "{e}"),
200                    }
201                }
202                ReadInput::Stream(stream) => {
203                    let mut guard = stream.lock().await;
204                    let s = match guard.as_mut() {
205                        Some(s) => s,
206                        None => return errf!("IOErr", "stream unavailable"),
207                    };
208                    let mut buf: LPooled<Vec<u8>> = LPooled::take();
209                    if let Err(e) = s.read_to_end(&mut buf).await {
210                        return errf!("IOErr", "read failed: {e}");
211                    }
212                    let text = match std::str::from_utf8(&buf) {
213                        Ok(s) => s,
214                        Err(e) => return errf!("TomlErr", "invalid UTF-8: {e}"),
215                    };
216                    match toml::from_str::<toml::Value>(text) {
217                        Ok(t) => toml_to_value(t),
218                        Err(e) => errf!("TomlErr", "{e}"),
219                    }
220                }
221            }
222        }
223    }
224}
225
226type TomlRead = CachedArgsAsync<TomlReadEv>;
227
228// ── TomlWriteStr (sync) ──────────────────────────────────────────
229
230#[derive(Debug, Default)]
231struct TomlWriteStrEv;
232
233impl<R: Rt, E: UserEvent> EvalCached<R, E> for TomlWriteStrEv {
234    const NAME: &str = "toml_write_str";
235    const NEEDS_CALLSITE: bool = false;
236
237    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, cached: &CachedVals) -> Option<Value> {
238        let pretty = cached.get::<bool>(0)?;
239        let v = cached.0.get(1)?.as_ref()?;
240        let toml_val = match value_to_toml(v) {
241            Ok(t) => t,
242            Err(e) => return Some(errf!("TomlErr", "{e}")),
243        };
244        let res = if pretty {
245            toml::to_string_pretty(&toml_val)
246        } else {
247            toml::to_string(&toml_val)
248        };
249        Some(match res {
250            Ok(s) => Value::String(ArcStr::from(s.as_str())),
251            Err(e) => errf!("TomlErr", "{e}"),
252        })
253    }
254}
255
256type TomlWriteStr = CachedArgs<TomlWriteStrEv>;
257
258// ── TomlWriteBytes (sync) ────────────────────────────────────────
259
260#[derive(Debug, Default)]
261struct TomlWriteBytesEv;
262
263impl<R: Rt, E: UserEvent> EvalCached<R, E> for TomlWriteBytesEv {
264    const NAME: &str = "toml_write_bytes";
265    const NEEDS_CALLSITE: bool = false;
266
267    fn eval(&mut self, _ctx: &mut ExecCtx<R, E>, cached: &CachedVals) -> Option<Value> {
268        let pretty = cached.get::<bool>(0)?;
269        let v = cached.0.get(1)?.as_ref()?;
270        let toml_val = match value_to_toml(v) {
271            Ok(t) => t,
272            Err(e) => return Some(errf!("TomlErr", "{e}")),
273        };
274        let res = if pretty {
275            toml::to_string_pretty(&toml_val)
276        } else {
277            toml::to_string(&toml_val)
278        };
279        Some(match res {
280            Ok(s) => Value::Bytes(PBytes::new(Bytes::from(s.into_bytes()))),
281            Err(e) => errf!("TomlErr", "{e}"),
282        })
283    }
284}
285
286type TomlWriteBytes = CachedArgs<TomlWriteBytesEv>;
287
288// ── TomlWriteStream (async) ──────────────────────────────────────
289
290#[derive(Debug, Default)]
291struct TomlWriteStreamEv;
292
293impl EvalCachedAsync for TomlWriteStreamEv {
294    const NAME: &str = "toml_write_stream";
295    const NEEDS_CALLSITE: bool = false;
296    type Args = (bool, Arc<Mutex<Option<StreamKind>>>, toml::Value);
297
298    fn prepare_args(&mut self, cached: &CachedVals) -> Option<Self::Args> {
299        let pretty = cached.get::<bool>(0)?;
300        let stream = get_stream(cached, 1)?;
301        let v = cached.0.get(2)?.as_ref()?;
302        let toml_val = value_to_toml(v).ok()?;
303        Some((pretty, stream, toml_val))
304    }
305
306    fn eval(
307        (pretty, stream, toml_val): Self::Args,
308    ) -> impl Future<Output = Value> + Send {
309        async move {
310            let s = if pretty {
311                toml::to_string_pretty(&toml_val)
312            } else {
313                toml::to_string(&toml_val)
314            };
315            let s = match s {
316                Ok(s) => s,
317                Err(e) => return errf!("TomlErr", "{e}"),
318            };
319            let mut guard = stream.lock().await;
320            let st = match guard.as_mut() {
321                Some(s) => s,
322                None => return errf!("IOErr", "stream unavailable"),
323            };
324            match st.write_all(s.as_bytes()).await {
325                Ok(()) => Value::Null,
326                Err(e) => errf!("IOErr", "write failed: {e}"),
327            }
328        }
329    }
330}
331
332type TomlWriteStream = CachedArgsAsync<TomlWriteStreamEv>;
333
334// ── Package registration ─────────────────────────────────────────
335
336graphix_derive::defpackage! {
337    builtins => [
338        TomlRead,
339        TomlWriteStr,
340        TomlWriteBytes,
341        TomlWriteStream,
342    ],
343}