Skip to main content

gobby_code/
models.rs

1use postgres::Row;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4use uuid::Uuid;
5
6use crate::utils::i64_to_usize;
7
8/// Stable namespace for deterministic symbol UUIDs.
9/// Must match Python: uuid.UUID("c0de1de0-0000-4000-8000-000000000000")
10pub const CODE_INDEX_UUID_NAMESPACE: Uuid = Uuid::from_bytes([
11    0xc0, 0xde, 0x1d, 0xe0, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
12]);
13
14pub const SOURCE_SYSTEM_GCODE: &str = "gcode";
15
16/// Producer confidence classification for graph and vector projection facts.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
19pub enum ProjectionProvenance {
20    #[default]
21    Extracted,
22    Inferred,
23    Ambiguous,
24}
25
26impl ProjectionProvenance {
27    pub fn as_str(self) -> &'static str {
28        match self {
29            Self::Extracted => "EXTRACTED",
30            Self::Inferred => "INFERRED",
31            Self::Ambiguous => "AMBIGUOUS",
32        }
33    }
34
35    pub fn from_wire_value(value: &str) -> Option<Self> {
36        match value {
37            "EXTRACTED" | "extracted" => Some(Self::Extracted),
38            "INFERRED" | "inferred" => Some(Self::Inferred),
39            "AMBIGUOUS" | "ambiguous" => Some(Self::Ambiguous),
40            _ => None,
41        }
42    }
43}
44
45impl fmt::Display for ProjectionProvenance {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.write_str(self.as_str())
48    }
49}
50
51/// Optional provenance attached to graph results and projection payloads.
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct ProjectionMetadata {
54    pub provenance: ProjectionProvenance,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub confidence: Option<f64>,
57    pub source_system: String,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub source_file_path: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub source_line: Option<usize>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub source_symbol_id: Option<String>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub matching_method: Option<String>,
66}
67
68impl ProjectionMetadata {
69    pub fn new(provenance: ProjectionProvenance, source_system: impl Into<String>) -> Self {
70        Self {
71            provenance,
72            confidence: None,
73            source_system: source_system.into(),
74            source_file_path: None,
75            source_line: None,
76            source_symbol_id: None,
77            matching_method: None,
78        }
79    }
80
81    pub fn gcode_extracted() -> Self {
82        Self::new(ProjectionProvenance::Extracted, SOURCE_SYSTEM_GCODE).with_confidence(Some(1.0))
83    }
84
85    pub fn inferred(source_system: impl Into<String>, confidence: Option<f64>) -> Self {
86        Self::new(ProjectionProvenance::Inferred, source_system).with_confidence(confidence)
87    }
88
89    pub fn ambiguous(source_system: impl Into<String>, confidence: Option<f64>) -> Self {
90        Self::new(ProjectionProvenance::Ambiguous, source_system).with_confidence(confidence)
91    }
92
93    pub fn with_confidence(mut self, confidence: Option<f64>) -> Self {
94        self.confidence = confidence;
95        self
96    }
97
98    pub fn with_source_file_path(mut self, file_path: impl Into<String>) -> Self {
99        self.source_file_path = Some(file_path.into());
100        self
101    }
102
103    pub fn with_source_line(mut self, line: usize) -> Self {
104        self.source_line = Some(line);
105        self
106    }
107
108    pub fn with_source_symbol_id(mut self, symbol_id: impl Into<String>) -> Self {
109        self.source_symbol_id = Some(symbol_id.into());
110        self
111    }
112
113    pub fn with_matching_method(mut self, matching_method: impl Into<String>) -> Self {
114        self.matching_method = Some(matching_method.into());
115        self
116    }
117
118    pub fn is_hypothesis(&self) -> bool {
119        matches!(
120            self.provenance,
121            ProjectionProvenance::Inferred | ProjectionProvenance::Ambiguous
122        )
123    }
124}
125
126/// A code symbol extracted from AST parsing.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct Symbol {
129    pub id: String,
130    pub project_id: String,
131    pub file_path: String,
132    pub name: String,
133    pub qualified_name: String,
134    pub kind: String,
135    pub language: String,
136    pub byte_start: usize,
137    pub byte_end: usize,
138    pub line_start: usize,
139    pub line_end: usize,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub signature: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub docstring: Option<String>,
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub parent_symbol_id: Option<String>,
146    #[serde(default)]
147    pub content_hash: String,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub summary: Option<String>,
150    #[serde(default)]
151    pub created_at: String,
152    #[serde(default)]
153    pub updated_at: String,
154}
155
156impl Symbol {
157    /// Generate deterministic UUID5 for a symbol.
158    /// Must produce identical IDs to Python Symbol.make_id().
159    pub fn make_id(
160        project_id: &str,
161        file_path: &str,
162        name: &str,
163        kind: &str,
164        byte_start: usize,
165    ) -> String {
166        let key = format!("{project_id}:{file_path}:{name}:{kind}:{byte_start}");
167        Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
168    }
169
170    /// Read a Symbol from a PostgreSQL row.
171    ///
172    /// Callers should select via `crate::db::symbol_select_columns()` so integer
173    /// and timestamp fields are cast to stable Rust-readable types.
174    pub fn from_row(row: &Row) -> anyhow::Result<Self> {
175        Ok(Self {
176            id: row.try_get("id")?,
177            project_id: row.try_get("project_id")?,
178            file_path: row.try_get("file_path")?,
179            name: row.try_get("name")?,
180            qualified_name: row.try_get("qualified_name")?,
181            kind: row.try_get("kind")?,
182            language: row.try_get("language")?,
183            byte_start: i64_to_usize(row.try_get("byte_start")?, "byte_start")?,
184            byte_end: i64_to_usize(row.try_get("byte_end")?, "byte_end")?,
185            line_start: i64_to_usize(row.try_get("line_start")?, "line_start")?,
186            line_end: i64_to_usize(row.try_get("line_end")?, "line_end")?,
187            signature: row.try_get("signature")?,
188            docstring: row.try_get("docstring")?,
189            parent_symbol_id: row.try_get("parent_symbol_id")?,
190            content_hash: row
191                .try_get::<_, Option<String>>("content_hash")?
192                .unwrap_or_default(),
193            summary: row.try_get("summary")?,
194            created_at: row
195                .try_get::<_, Option<String>>("created_at")?
196                .unwrap_or_default(),
197            updated_at: row
198                .try_get::<_, Option<String>>("updated_at")?
199                .unwrap_or_default(),
200        })
201    }
202
203    /// Slim representation for outline output.
204    pub fn to_outline(&self) -> OutlineSymbol {
205        OutlineSymbol {
206            id: self.id.clone(),
207            name: self.name.clone(),
208            kind: self.kind.clone(),
209            line_start: self.line_start,
210            line_end: self.line_end,
211            signature: self.signature.clone(),
212        }
213    }
214
215    /// Brief dict-like representation for search results.
216    pub fn to_brief(&self) -> SearchResult {
217        SearchResult {
218            id: self.id.clone(),
219            name: self.name.clone(),
220            qualified_name: self.qualified_name.clone(),
221            kind: self.kind.clone(),
222            language: self.language.clone(),
223            file_path: self.file_path.clone(),
224            line_start: self.line_start,
225            line_end: self.line_end,
226            score: 0.0,
227            rrf_score: None,
228            summary: self.summary.clone(),
229            signature: self.signature.clone(),
230            sources: None,
231        }
232    }
233}
234
235pub fn make_unresolved_callee_id(project_id: &str, callee_name: &str) -> String {
236    let key = format!("unresolved:{project_id}:{callee_name}");
237    Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
238}
239
240pub fn make_external_symbol_id(
241    project_id: &str,
242    callee_name: &str,
243    module: Option<&str>,
244) -> String {
245    let module_key = module.unwrap_or_default();
246    let key = format!("external:{project_id}:{module_key}:{callee_name}");
247    Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
248}
249
250/// Metadata for an indexed file.
251#[derive(Debug, Clone, Serialize, Deserialize)]
252pub struct IndexedFile {
253    pub id: String,
254    pub project_id: String,
255    pub file_path: String,
256    pub language: String,
257    pub content_hash: String,
258    pub symbol_count: usize,
259    pub byte_size: usize,
260    pub indexed_at: String,
261}
262
263impl IndexedFile {
264    pub fn make_id(project_id: &str, file_path: &str) -> String {
265        let key = format!("{project_id}:{file_path}");
266        Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
267    }
268}
269
270/// A chunk of file content for FTS search.
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ContentChunk {
273    pub id: String,
274    pub project_id: String,
275    pub file_path: String,
276    pub chunk_index: usize,
277    pub line_start: usize,
278    pub line_end: usize,
279    pub content: String,
280    pub language: String,
281    pub created_at: String,
282}
283
284impl ContentChunk {
285    pub fn make_id(project_id: &str, file_path: &str, chunk_index: usize) -> String {
286        let key = format!("{project_id}:{file_path}:chunk:{chunk_index}");
287        Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
288    }
289}
290
291/// Import relationship extracted from AST.
292#[derive(Debug, Clone)]
293pub struct ImportRelation {
294    pub file_path: String,
295    pub module_name: String,
296}
297
298/// Call relationship extracted from AST.
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum CallTargetKind {
301    Symbol,
302    Unresolved,
303    External,
304    /// Transient marker for a cross-file local import whose canonical target is
305    /// resolved against `code_symbols` in a post-write pass (see
306    /// `index::indexer::local_imports`). After resolution a row is rewritten to
307    /// `Symbol` (hit) or `Unresolved` (miss), so this kind never persists past a
308    /// completed index run.
309    LocalImport,
310}
311
312impl CallTargetKind {
313    pub fn as_str(self) -> &'static str {
314        match self {
315            Self::Symbol => "symbol",
316            Self::Unresolved => "unresolved",
317            Self::External => "external",
318            Self::LocalImport => "local_import",
319        }
320    }
321}
322
323/// Call relationship extracted from AST.
324#[derive(Debug, Clone)]
325pub struct CallRelation {
326    pub caller_symbol_id: String,
327    pub callee_symbol_id: Option<String>,
328    pub callee_name: String,
329    pub callee_target_kind: CallTargetKind,
330    pub callee_external_module: Option<String>,
331    pub file_path: String,
332    pub line: usize,
333}
334
335impl CallRelation {
336    pub fn new(
337        caller_symbol_id: String,
338        callee_name: String,
339        file_path: String,
340        line: usize,
341    ) -> Self {
342        Self {
343            caller_symbol_id,
344            callee_symbol_id: None,
345            callee_name,
346            callee_target_kind: CallTargetKind::Unresolved,
347            callee_external_module: None,
348            file_path,
349            line,
350        }
351    }
352
353    pub fn with_symbol_target(mut self, callee_symbol_id: String) -> Self {
354        self.callee_symbol_id = Some(callee_symbol_id);
355        self.callee_target_kind = CallTargetKind::Symbol;
356        self
357    }
358
359    pub fn with_external_target(
360        mut self,
361        callee_name: String,
362        callee_external_module: String,
363    ) -> Self {
364        self.callee_name = callee_name;
365        self.callee_target_kind = CallTargetKind::External;
366        self.callee_external_module = Some(callee_external_module);
367        self
368    }
369
370    /// Mark this call as a pending cross-file local import. `callee_name` is the
371    /// originally imported name (not the local alias) and `candidate_files` are
372    /// the project-relative files the target symbol might live in, derived from
373    /// the import by pure path logic (no file reads). The post-write resolution
374    /// pass (`index::indexer::local_imports`) looks the target up in
375    /// `code_symbols` and rewrites this row to `Symbol` or `Unresolved`.
376    ///
377    /// Candidate files ride in `callee_external_module` joined by `\n`; the
378    /// column is unused for local imports otherwise and is cleared on resolution.
379    /// JavaScript default imports prefix the list with an internal marker so
380    /// post-write resolution can use a unique top-level-symbol fallback while
381    /// unresolved calls still keep their source alias in `callee_name`.
382    pub fn with_local_import_target(
383        mut self,
384        callee_name: String,
385        candidate_files: Vec<String>,
386    ) -> Self {
387        self.callee_name = callee_name;
388        self.callee_target_kind = CallTargetKind::LocalImport;
389        self.callee_symbol_id = None;
390        self.callee_external_module = Some(candidate_files.join(LOCAL_IMPORT_CANDIDATE_SEP));
391        self
392    }
393
394    pub fn with_local_default_import_target(
395        mut self,
396        callee_name: String,
397        candidate_files: Vec<String>,
398    ) -> Self {
399        self.callee_name = callee_name;
400        self.callee_target_kind = CallTargetKind::LocalImport;
401        self.callee_symbol_id = None;
402        let encoded = std::iter::once(LOCAL_IMPORT_DEFAULT_EXPORT_MARKER.to_string())
403            .chain(candidate_files)
404            .collect::<Vec<_>>()
405            .join(LOCAL_IMPORT_CANDIDATE_SEP);
406        self.callee_external_module = Some(encoded);
407        self
408    }
409
410    pub fn local_import_uses_default_export_fallback(&self) -> bool {
411        self.callee_target_kind == CallTargetKind::LocalImport
412            && self
413                .callee_external_module
414                .as_deref()
415                .and_then(|joined| joined.split(LOCAL_IMPORT_CANDIDATE_SEP).next())
416                == Some(LOCAL_IMPORT_DEFAULT_EXPORT_MARKER)
417    }
418
419    /// Candidate target files carried by a `LocalImport` call, parsed back out of
420    /// `callee_external_module`. Empty for any other kind.
421    pub fn local_import_candidate_files(&self) -> Vec<String> {
422        if self.callee_target_kind != CallTargetKind::LocalImport {
423            return Vec::new();
424        }
425        self.callee_external_module
426            .as_deref()
427            .map(|joined| {
428                joined
429                    .split(LOCAL_IMPORT_CANDIDATE_SEP)
430                    .filter(|part| !part.is_empty() && *part != LOCAL_IMPORT_DEFAULT_EXPORT_MARKER)
431                    .map(ToOwned::to_owned)
432                    .collect()
433            })
434            .unwrap_or_default()
435    }
436}
437
438/// Separator for the candidate-file list carried in `callee_external_module`
439/// while a call is a pending `LocalImport`. Newlines never appear in project
440/// file paths, so the join/split round-trips losslessly.
441pub const LOCAL_IMPORT_CANDIDATE_SEP: &str = "\n";
442const LOCAL_IMPORT_DEFAULT_EXPORT_MARKER: &str = "__gcode_local_import_default_export__";
443
444/// Project index statistics.
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct IndexedProject {
447    pub id: String,
448    pub root_path: String,
449    pub total_files: usize,
450    pub total_symbols: usize,
451    pub last_indexed_at: String,
452    pub index_duration_ms: u64,
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub total_eligible_files: Option<usize>,
455}
456
457/// Search result with score.
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct SearchResult {
460    pub id: String,
461    pub name: String,
462    pub qualified_name: String,
463    pub kind: String,
464    pub language: String,
465    pub file_path: String,
466    pub line_start: usize,
467    pub line_end: usize,
468    pub score: f64,
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub rrf_score: Option<f64>,
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub summary: Option<String>,
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub signature: Option<String>,
475    #[serde(skip_serializing_if = "Option::is_none")]
476    pub sources: Option<Vec<String>>,
477}
478
479/// Graph query result (callers, usages).
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct GraphResult {
482    pub id: String,
483    pub name: String,
484    pub file_path: String,
485    pub line: usize,
486    /// Edge confidence/provenance label; numeric scores live in `metadata.confidence`.
487    #[serde(default)]
488    pub confidence: ProjectionProvenance,
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub relation: Option<String>,
491    #[serde(skip_serializing_if = "Option::is_none")]
492    pub distance: Option<usize>,
493    #[serde(default, skip_serializing_if = "Option::is_none")]
494    pub metadata: Option<ProjectionMetadata>,
495}
496
497#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
498pub struct GraphPathStep {
499    pub position: usize,
500    pub id: String,
501    pub name: String,
502    pub file_path: String,
503    pub line: usize,
504}
505
506/// Result of parsing a single file.
507pub struct ParseResult {
508    pub symbols: Vec<Symbol>,
509    pub imports: Vec<ImportRelation>,
510    pub calls: Vec<CallRelation>,
511    /// Raw file bytes — carried through for body snippet extraction at embedding time.
512    pub source: Vec<u8>,
513}
514
515/// Aggregate result of indexing a directory.
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct IndexResult {
518    pub project_id: String,
519    pub files_indexed: usize,
520    pub files_skipped: usize,
521    pub symbols_found: usize,
522    pub errors: Vec<String>,
523    pub duration_ms: u64,
524}
525
526/// Paginated response envelope for JSON output.
527/// Hoists `project_id` to avoid repeating it on every result.
528#[derive(Debug, Clone, Serialize)]
529pub struct PagedResponse<T: Serialize> {
530    pub project_id: String,
531    pub total: usize,
532    pub offset: usize,
533    pub limit: usize,
534    pub results: Vec<T>,
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub hint: Option<String>,
537}
538
539/// Slim symbol for outline output — only what agents need.
540#[derive(Debug, Clone, Serialize)]
541pub struct OutlineSymbol {
542    pub id: String,
543    pub name: String,
544    pub kind: String,
545    pub line_start: usize,
546    pub line_end: usize,
547    #[serde(skip_serializing_if = "Option::is_none")]
548    pub signature: Option<String>,
549}
550
551/// Content search hit from FTS.
552#[derive(Debug, Clone, Serialize, Deserialize)]
553pub struct ContentSearchHit {
554    pub file_path: String,
555    pub line_start: usize,
556    pub line_end: usize,
557    pub snippet: String,
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub language: Option<String>,
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565
566    #[test]
567    fn symbol_make_id_matches_python_uuid5_golden_vectors() {
568        assert_eq!(
569            CODE_INDEX_UUID_NAMESPACE.to_string(),
570            "c0de1de0-0000-4000-8000-000000000000"
571        );
572
573        let cases = [
574            (
575                "proj1",
576                "src/main.py",
577                "foo",
578                "function",
579                42,
580                "403e2117-92e7-5390-ad83-226629486481",
581            ),
582            (
583                "3bf57fe7-2a0c-4074-8912-a83d9cd4df01",
584                "crates/gcode/src/models.rs",
585                "Symbol",
586                "struct",
587                111,
588                "d28e80d3-a95e-5c2a-91c3-92551f75a2b1",
589            ),
590            (
591                "proj-with-dashes",
592                "src/lib.rs",
593                "Widget::render",
594                "method",
595                0,
596                "44da4f31-7218-5b3b-97c4-5a5eca9f0451",
597            ),
598            (
599                "overlay:child",
600                "nested/path/file.ts",
601                "HTTPClient.new",
602                "class",
603                987654321,
604                "f9531553-f2a7-5425-b487-6fb5b31d57bb",
605            ),
606        ];
607
608        for (project_id, file_path, name, kind, byte_start, expected) in cases {
609            assert_eq!(
610                Symbol::make_id(project_id, file_path, name, kind, byte_start),
611                expected,
612                "Python UUID5 parity failed for {project_id}:{file_path}:{name}:{kind}:{byte_start}"
613            );
614        }
615    }
616
617    #[test]
618    fn unresolved_and_external_ids_match_python_uuid5_golden_vectors() {
619        assert_eq!(
620            make_unresolved_callee_id("proj1", "missing_func"),
621            "42693df1-99e6-5daa-be29-3535096cd2b5"
622        );
623        assert_eq!(
624            make_external_symbol_id("proj1", "get", Some("requests")),
625            "7c7e6ebe-47c6-5a3d-a83d-d5160f10cb74"
626        );
627        assert_eq!(
628            make_external_symbol_id("proj1", "println", None),
629            "c6b97498-448e-5ef1-9cb5-ab1cf37b6596"
630        );
631    }
632    #[test]
633    fn test_call_relation_promotes_symbol_targets() {
634        let call = CallRelation::new(
635            "caller-id".to_string(),
636            "foo".to_string(),
637            "src/main.py".to_string(),
638            12,
639        )
640        .with_symbol_target("callee-id".to_string());
641
642        assert_eq!(call.callee_symbol_id.as_deref(), Some("callee-id"));
643        assert_eq!(call.callee_target_kind, CallTargetKind::Symbol);
644    }
645
646    #[test]
647    fn graph_result_metadata_remains_optional_in_json_contract() {
648        let json = serde_json::json!({
649            "id": "sym-1",
650            "name": "foo",
651            "file_path": "src/main.rs",
652            "line": 10
653        });
654
655        let parsed: GraphResult =
656            serde_json::from_value(json).expect("graph result JSON parses without metadata");
657        assert_eq!(parsed.confidence, ProjectionProvenance::Extracted);
658        assert!(parsed.metadata.is_none());
659
660        let serialized = serde_json::to_value(&parsed).expect("graph result serializes");
661        assert_eq!(serialized["confidence"], "EXTRACTED");
662        assert!(serialized.get("metadata").is_none());
663    }
664
665    #[test]
666    fn graph_result_without_metadata_omits_metadata_when_serialized() {
667        let strategy = (
668            proptest::string::string_regex("[ -~]{0,32}").expect("valid id regex"),
669            proptest::string::string_regex("[ -~]{0,32}").expect("valid name regex"),
670            proptest::string::string_regex("[ -~]{0,64}").expect("valid path regex"),
671            0usize..1_000_000,
672            proptest::option::of(
673                proptest::string::string_regex("[ -~]{0,32}").expect("valid relation regex"),
674            ),
675            proptest::option::of(0usize..1_000),
676        );
677
678        proptest::test_runner::TestRunner::default()
679            .run(
680                &strategy,
681                |(id, name, file_path, line, relation, distance)| {
682                    let result = GraphResult {
683                        id,
684                        name,
685                        file_path,
686                        line,
687                        confidence: ProjectionProvenance::Extracted,
688                        relation,
689                        distance,
690                        metadata: None,
691                    };
692
693                    let serialized =
694                        serde_json::to_value(&result).expect("graph result serializes");
695                    assert_eq!(serialized["confidence"], "EXTRACTED");
696                    assert_eq!(serialized.get("metadata"), None);
697
698                    Ok(())
699                },
700            )
701            .expect("metadata omission property holds");
702    }
703}