1use std::collections::{HashMap, HashSet};
2use std::fmt;
3use std::fmt::Write as FmtWrite;
4
5use crate::and;
6use crate::blob::encodings::longstring::LongString;
7use crate::id::Id;
8use crate::metadata;
9use crate::metadata::MetaDescribe;
10use crate::prelude::{find, pattern};
11use crate::query::TriblePattern;
12use crate::repo::BlobStoreGet;
13use crate::temp;
14use crate::trible::TribleSet;
15use crate::inline::encodings::boolean::Boolean;
16use crate::inline::encodings::f64::F64;
17use crate::inline::encodings::genid::GenId;
18use crate::inline::encodings::hash::{Blake3, Handle, Hash};
19use crate::inline::encodings::UnknownInline;
20use crate::inline::RawInline;
21use crate::inline::IntoInline;
22use crate::inline::Inline;
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,
63 out: &mut impl FmtWrite,
64) -> Result<(), ExportError> {
65 let mut multi_flags = HashSet::new();
66 find!(
67 (name_handle: Inline<Handle<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>,
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 RawInline,
106 Inline<Handle<LongString>>,
107 Id,
108 Inline<UnknownInline>,
109 )> = Vec::new();
110 find!(
111 (name_handle: Inline<Handle<LongString>>, schema_value: Inline<GenId>, value: Inline<UnknownInline>),
112 temp!((e, attr), and!(
113 e.is(entity.to_inline()),
114 merged.pattern(e, attr, value),
115 pattern!(merged, [
116 { ?attr @ metadata::name: ?name_handle },
117 { ?attr @ metadata::value_encoding: ?schema_value }
118 ])
119 ))
120 )
121 .filter_map(|(name_handle, schema_value, value)| {
122 let schema: Id = schema_value.try_from_inline().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: Inline<UnknownInline>,
174 visited: &mut HashSet<Id>,
175 ctx: &mut ExportCtx<'_, impl BlobStoreGet>,
176 out: &mut impl FmtWrite,
177) -> Result<(), ExportError> {
178 use std::sync::LazyLock;
181 static BOOLEAN_ID: LazyLock<Id> = LazyLock::new(Boolean::id);
182 static F64_ID: LazyLock<Id> = LazyLock::new(F64::id);
183 static GENID_ID: LazyLock<Id> = LazyLock::new(GenId::id);
184 static HANDLE_BLAKE3_LONGSTRING_ID: LazyLock<Id> =
185 LazyLock::new(Handle::<LongString>::id);
186
187 if schema == *BOOLEAN_ID {
188 let value = value.transmute::<Boolean>();
189 if let Ok(b) = value.try_from_inline::<bool>() {
190 let _ = out.write_str(if b { "true" } else { "false" });
191 } else {
192 let _ = out.write_str("null");
193 }
194 return Ok(());
195 }
196 if schema == *F64_ID {
197 let value = value.transmute::<F64>();
198 let number = value.from_inline::<f64>();
199 if !number.is_finite() {
200 let _ = out.write_str("null");
201 return Ok(());
202 }
203 if number.fract() == 0.0 {
204 let _ = write!(out, "{number:.0}");
205 } else {
206 let mut buf = Buffer::new();
207 let s = buf.format_finite(number);
208 let _ = out.write_str(s);
209 }
210 return Ok(());
211 }
212 if schema == *GENID_ID {
213 if let Ok(child_id) = value.transmute::<GenId>().try_from_inline::<Id>() {
214 return write_entity(merged, child_id, visited, ctx, out);
215 }
216 return Ok(());
217 }
218 if schema == *HANDLE_BLAKE3_LONGSTRING_ID {
219 let handle = value.transmute::<Handle<LongString>>();
220 let text = resolve_string(ctx, handle)?;
221 write_escaped_str(text.as_ref(), out);
222 return Ok(());
223 }
224
225 Ok(())
226}
227
228fn write_escaped_str(text: &str, out: &mut impl FmtWrite) {
229 let _ = out.write_char('"');
230 let bytes = text.as_bytes();
231 let mut idx = 0;
232 while idx < bytes.len() {
233 let b = bytes[idx];
234 if b >= 0x20 && b != b'\\' && b != b'"' {
235 let start = idx;
237 idx += 1;
238 while idx < bytes.len() {
239 let b2 = bytes[idx];
240 if b2 < 0x20 || b2 == b'\\' || b2 == b'"' {
241 break;
242 }
243 idx += 1;
244 }
245 let _ = out.write_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..idx]) });
246 continue;
247 }
248 match b {
249 b'"' => {
250 let _ = out.write_str("\\\"");
251 }
252 b'\\' => {
253 let _ = out.write_str("\\\\");
254 }
255 b'\n' => {
256 let _ = out.write_str("\\n");
257 }
258 b'\r' => {
259 let _ = out.write_str("\\r");
260 }
261 b'\t' => {
262 let _ = out.write_str("\\t");
263 }
264 0x08 => {
265 let _ = out.write_str("\\b");
266 }
267 0x0c => {
268 let _ = out.write_str("\\f");
269 }
270 _ if b < 0x20 => {
271 let _ = write!(out, "\\u{:04x}", b);
272 }
273 _ => {
274 let _ = out.write_char(b as char);
275 }
276 }
277 idx += 1;
278 }
279 let _ = out.write_char('"');
280}
281
282struct ExportCtx<'a, Store: BlobStoreGet> {
283 store: &'a Store,
284 name_cache: HashMap<RawInline, String>,
285 string_cache: HashMap<RawInline, View<str>>,
286 multi_flags: HashSet<RawInline>,
287}
288
289fn resolve_name(
290 ctx: &mut ExportCtx<'_, impl BlobStoreGet>,
291 handle: Inline<Handle<LongString>>,
292) -> Result<String, ExportError> {
293 if let Some(cached) = ctx.name_cache.get(&handle.raw) {
294 return Ok(cached.clone());
295 }
296
297 let hash: Inline<Hash<Blake3>> = Handle::to_hash(handle);
298 let text = ctx
299 .store
300 .get::<View<str>, LongString>(handle)
301 .map_err(|err| ExportError::BlobStore {
302 hash: hex::encode(hash.raw),
303 source: err.to_string(),
304 })?
305 .to_string();
306 ctx.name_cache.insert(handle.raw, text.clone());
307 Ok(text)
308}
309
310fn resolve_string(
311 ctx: &mut ExportCtx<'_, impl BlobStoreGet>,
312 handle: Inline<Handle<LongString>>,
313) -> Result<View<str>, ExportError> {
314 if let Some(cached) = ctx.string_cache.get(&handle.raw) {
315 return Ok(cached.clone());
316 }
317
318 let hash: Inline<Hash<Blake3>> = Handle::to_hash(handle);
319 let text: View<str> = ctx
320 .store
321 .get::<View<str>, LongString>(handle)
322 .map_err(|err| ExportError::BlobStore {
323 hash: hex::encode(hash.raw),
324 source: err.to_string(),
325 })?;
326 ctx.string_cache.insert(handle.raw, text.clone());
327 Ok(text)
328}