1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::fmt::Write as FmtWrite;
4
5use crate::and;
6use crate::blob::schemas::longstring::LongString;
7use crate::id::Id;
8use crate::metadata;
9use crate::metadata::ConstId;
10use crate::prelude::{find, pattern};
11use crate::query::TriblePattern;
12use crate::repo::BlobStoreGet;
13use crate::temp;
14use crate::trible::TribleSet;
15use crate::value::schemas::boolean::Boolean;
16use crate::value::schemas::f64::F64;
17use crate::value::schemas::genid::GenId;
18use crate::value::schemas::hash::{Blake3, Handle, Hash};
19use crate::value::schemas::UnknownValue;
20use crate::value::RawValue;
21use crate::value::ToValue;
22use crate::value::Value;
23use anybytes::View;
24use ryu::Buffer;
25
26#[derive(Debug)]
28pub enum ExportError {
29 MissingBlob {
31 hash: String,
33 },
34 BlobStore {
36 hash: String,
38 source: String,
40 },
41}
42
43impl fmt::Display for ExportError {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::MissingBlob { hash } => {
47 write!(f, "missing blob for handle hash {hash}")
48 }
49 Self::BlobStore { hash, source } => {
50 write!(f, "failed to load blob {hash}: {source}")
51 }
52 }
53 }
54}
55
56impl std::error::Error for ExportError {}
57
58pub fn export_to_json(
60 merged: &TribleSet,
61 root: Id,
62 store: &impl BlobStoreGet<Blake3>,
63 out: &mut impl FmtWrite,
64) -> Result<(), ExportError> {
65 let mut multi_flags = HashSet::new();
66 find!(
67 (name_handle: Value<Handle<Blake3, LongString>>),
68 temp!((field), pattern!(merged, [
69 { ?field @ metadata::name: ?name_handle },
70 { ?field @ metadata::tag: metadata::KIND_MULTI }
71 ]))
72 )
73 .for_each(|(name_handle,)| {
74 multi_flags.insert(name_handle.raw);
75 });
76
77 let mut ctx = ExportCtx {
78 store,
79 name_cache: HashMap::new(),
80 string_cache: HashMap::new(),
81 multi_flags,
82 };
83 let mut visited = HashSet::new();
84 write_entity(merged, root, &mut visited, &mut ctx, out)?;
85 Ok(())
86}
87
88fn write_entity(
89 merged: &TribleSet,
90 entity: Id,
91 visited: &mut HashSet<Id>,
92 ctx: &mut ExportCtx<'_, impl BlobStoreGet<Blake3>>,
93 out: &mut impl FmtWrite,
94) -> Result<(), ExportError> {
95 if !visited.insert(entity) {
96 let _ = out.write_str("{\"$ref\":\"");
97 let _ = write!(out, "{entity:x}");
98 let _ = out.write_str("\"}");
99 return Ok(());
100 }
101
102 let _ = out.write_char('{');
103
104 let mut field_values: Vec<(
105 RawValue,
106 Value<Handle<Blake3, LongString>>,
107 Id,
108 Value<UnknownValue>,
109 )> = Vec::new();
110 find!(
111 (name_handle: Value<Handle<Blake3, LongString>>, schema_value: Value<GenId>, value: Value<UnknownValue>),
112 temp!((e, attr), and!(
113 e.is(entity.to_value()),
114 merged.pattern(e, attr, value),
115 pattern!(merged, [
116 { ?attr @ metadata::name: ?name_handle },
117 { ?attr @ metadata::value_schema: ?schema_value }
118 ])
119 ))
120 )
121 .filter_map(|(name_handle, schema_value, value)| {
122 let schema: Id = schema_value.try_from_value().ok()?;
123 Some((name_handle.raw, name_handle, schema, value))
124 })
125 .for_each(|(raw, name_handle, schema, value)| {
126 field_values.push((raw, name_handle, schema, value));
127 });
128
129 field_values.sort_by(|(a, _, _, _), (b, _, _, _)| a.cmp(b));
130
131 let mut iter = field_values.into_iter().peekable();
132 let mut field_idx = 0usize;
133 while let Some((name_raw, name_handle, schema, value)) = iter.next() {
134 let mut values = vec![(schema, value)];
135 while let Some((next_raw, _, _, _)) = iter.peek() {
136 if *next_raw != name_raw {
137 break;
138 }
139 let (_, _, s, v) = iter.next().expect("peeked element exists");
140 values.push((s, v));
141 }
142
143 let name = resolve_name(ctx, name_handle)?;
144
145 if field_idx > 0 {
146 let _ = out.write_char(',');
147 }
148 write_escaped_str(&name, out);
149 let _ = out.write_char(':');
150
151 let card_multi = ctx.multi_flags.contains(&name_raw) || values.len() > 1;
152 if card_multi {
153 let _ = out.write_char('[');
154 for (i, (schema, value)) in values.into_iter().enumerate() {
155 if i > 0 {
156 let _ = out.write_char(',');
157 }
158 render_schema_value(merged, schema, value, visited, ctx, out)?;
159 }
160 let _ = out.write_char(']');
161 } else if let Some((schema, value)) = values.into_iter().next() {
162 render_schema_value(merged, schema, value, visited, ctx, out)?;
163 }
164 field_idx += 1;
165 }
166 let _ = out.write_char('}');
167 Ok(())
168}
169
170fn render_schema_value(
171 merged: &TribleSet,
172 schema: Id,
173 value: Value<UnknownValue>,
174 visited: &mut HashSet<Id>,
175 ctx: &mut ExportCtx<'_, impl BlobStoreGet<Blake3>>,
176 out: &mut impl FmtWrite,
177) -> Result<(), ExportError> {
178 if schema == Boolean::ID {
179 let value = value.transmute::<Boolean>();
180 if let Ok(b) = value.try_from_value::<bool>() {
181 let _ = out.write_str(if b { "true" } else { "false" });
182 } else {
183 let _ = out.write_str("null");
184 }
185 return Ok(());
186 }
187 if schema == F64::ID {
188 let value = value.transmute::<F64>();
189 let number = value.from_value::<f64>();
190 if !number.is_finite() {
191 let _ = out.write_str("null");
192 return Ok(());
193 }
194 if number.fract() == 0.0 {
195 let _ = write!(out, "{number:.0}");
196 } else {
197 let mut buf = Buffer::new();
198 let s = buf.format_finite(number);
199 let _ = out.write_str(s);
200 }
201 return Ok(());
202 }
203 if schema == GenId::ID {
204 if let Ok(child_id) = value.transmute::<GenId>().try_from_value::<Id>() {
205 return write_entity(merged, child_id, visited, ctx, out);
206 }
207 return Ok(());
208 }
209 if schema == Handle::<Blake3, LongString>::ID {
210 let handle = value.transmute::<Handle<Blake3, LongString>>();
211 let text = resolve_string(ctx, handle)?;
212 write_escaped_str(text.as_ref(), out);
213 return Ok(());
214 }
215
216 Ok(())
217}
218
219fn write_escaped_str(text: &str, out: &mut impl FmtWrite) {
220 let _ = out.write_char('"');
221 let bytes = text.as_bytes();
222 let mut idx = 0;
223 while idx < bytes.len() {
224 let b = bytes[idx];
225 if b >= 0x20 && b != b'\\' && b != b'"' {
226 let start = idx;
228 idx += 1;
229 while idx < bytes.len() {
230 let b2 = bytes[idx];
231 if b2 < 0x20 || b2 == b'\\' || b2 == b'"' {
232 break;
233 }
234 idx += 1;
235 }
236 let _ = out.write_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..idx]) });
237 continue;
238 }
239 match b {
240 b'"' => {
241 let _ = out.write_str("\\\"");
242 }
243 b'\\' => {
244 let _ = out.write_str("\\\\");
245 }
246 b'\n' => {
247 let _ = out.write_str("\\n");
248 }
249 b'\r' => {
250 let _ = out.write_str("\\r");
251 }
252 b'\t' => {
253 let _ = out.write_str("\\t");
254 }
255 0x08 => {
256 let _ = out.write_str("\\b");
257 }
258 0x0c => {
259 let _ = out.write_str("\\f");
260 }
261 _ if b < 0x20 => {
262 let _ = write!(out, "\\u{:04x}", b);
263 }
264 _ => {
265 let _ = out.write_char(b as char);
266 }
267 }
268 idx += 1;
269 }
270 let _ = out.write_char('"');
271}
272
273struct ExportCtx<'a, Store: BlobStoreGet<Blake3>> {
274 store: &'a Store,
275 name_cache: HashMap<RawValue, String>,
276 string_cache: HashMap<RawValue, View<str>>,
277 multi_flags: HashSet<RawValue>,
278}
279
280fn resolve_name(
281 ctx: &mut ExportCtx<'_, impl BlobStoreGet<Blake3>>,
282 handle: Value<Handle<Blake3, LongString>>,
283) -> Result<String, ExportError> {
284 if let Some(cached) = ctx.name_cache.get(&handle.raw) {
285 return Ok(cached.clone());
286 }
287
288 let hash: Value<Hash<Blake3>> = Handle::to_hash(handle);
289 let text = ctx
290 .store
291 .get::<View<str>, LongString>(handle)
292 .map_err(|err| ExportError::BlobStore {
293 hash: hex::encode(hash.raw),
294 source: err.to_string(),
295 })?
296 .to_string();
297 ctx.name_cache.insert(handle.raw, text.clone());
298 Ok(text)
299}
300
301fn resolve_string(
302 ctx: &mut ExportCtx<'_, impl BlobStoreGet<Blake3>>,
303 handle: Value<Handle<Blake3, LongString>>,
304) -> Result<View<str>, ExportError> {
305 if let Some(cached) = ctx.string_cache.get(&handle.raw) {
306 return Ok(cached.clone());
307 }
308
309 let hash: Value<Hash<Blake3>> = Handle::to_hash(handle);
310 let text: View<str> = ctx
311 .store
312 .get::<View<str>, LongString>(handle)
313 .map_err(|err| ExportError::BlobStore {
314 hash: hex::encode(hash.raw),
315 source: err.to_string(),
316 })?;
317 ctx.string_cache.insert(handle.raw, text.clone());
318 Ok(text)
319}