Skip to main content

sochdb_core/
soch.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! TOON (Tabular Object-Oriented Notation) - Native Data Format for SochDB
19//!
20//! TOON is a compact, schema-aware data format optimized for LLMs and databases.
21//! It's the native format for SochDB, like JSON is for MongoDB.
22//!
23//! Format: `name[count]{fields}:\nrow1\nrow2\n...`
24//!
25//! Example:
26//! ```text
27//! users[3]{id,name,email}:
28//! 1,Alice,alice@example.com
29//! 2,Bob,bob@example.com
30//! 3,Charlie,charlie@example.com
31//! ```
32
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::fmt;
36
37/// TOON Value types
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub enum SochValue {
40    Null,
41    Bool(bool),
42    Int(i64),
43    UInt(u64),
44    Float(f64),
45    Text(String),
46    Binary(Vec<u8>),
47    Array(Vec<SochValue>),
48    Object(HashMap<String, SochValue>),
49    /// Reference to another table row: ref(table_name, id)
50    Ref {
51        table: String,
52        id: u64,
53    },
54}
55
56impl SochValue {
57    pub fn is_null(&self) -> bool {
58        matches!(self, SochValue::Null)
59    }
60
61    pub fn as_int(&self) -> Option<i64> {
62        match self {
63            SochValue::Int(v) => Some(*v),
64            SochValue::UInt(v) => Some(*v as i64),
65            _ => None,
66        }
67    }
68
69    pub fn as_uint(&self) -> Option<u64> {
70        match self {
71            SochValue::UInt(v) => Some(*v),
72            SochValue::Int(v) if *v >= 0 => Some(*v as u64),
73            _ => None,
74        }
75    }
76
77    pub fn as_float(&self) -> Option<f64> {
78        match self {
79            SochValue::Float(v) => Some(*v),
80            SochValue::Int(v) => Some(*v as f64),
81            SochValue::UInt(v) => Some(*v as f64),
82            _ => None,
83        }
84    }
85
86    pub fn as_text(&self) -> Option<&str> {
87        match self {
88            SochValue::Text(s) => Some(s),
89            _ => None,
90        }
91    }
92
93    pub fn as_bool(&self) -> Option<bool> {
94        match self {
95            SochValue::Bool(b) => Some(*b),
96            _ => None,
97        }
98    }
99}
100
101fn needs_quoting(s: &str) -> bool {
102    if s.is_empty() {
103        return true;
104    }
105    if s.starts_with(' ') || s.ends_with(' ') {
106        return true;
107    }
108    if matches!(s, "true" | "false" | "null") {
109        return true;
110    }
111
112    // Check for number-like patterns
113    if s.parse::<f64>().is_ok() {
114        return true;
115    }
116    if s == "-" || s.starts_with('-') {
117        return true;
118    }
119    // Leading zeros check (e.g. 05 usually treated as number in some contexts or invalid)
120    if s.len() > 1
121        && s.starts_with('0')
122        && s.chars().nth(1).map_or(false, |c| c.is_ascii_digit())
123        && !s.contains('.')
124    {
125        return true;
126    }
127
128    // Check for special chars or delimiter (comma)
129    // Spec ยง7.3: :, ", \, [, ], {, }, newline, return, tab, delimiter
130    s.contains(|c| {
131        matches!(
132            c,
133            ':' | '"' | '\\' | '[' | ']' | '{' | '}' | '\n' | '\r' | '\t' | ','
134        )
135    })
136}
137
138impl fmt::Display for SochValue {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        match self {
141            SochValue::Null => write!(f, "null"),
142            SochValue::Bool(b) => write!(f, "{}", b),
143            SochValue::Int(i) => write!(f, "{}", i),
144            SochValue::UInt(u) => write!(f, "{}", u),
145            SochValue::Float(fl) => write!(f, "{}", fl),
146            SochValue::Text(s) => {
147                if needs_quoting(s) {
148                    write!(f, "\"")?;
149                    for c in s.chars() {
150                        match c {
151                            '"' => write!(f, "\\\"")?,
152                            '\\' => write!(f, "\\\\")?,
153                            '\n' => write!(f, "\\n")?,
154                            '\r' => write!(f, "\\r")?,
155                            '\t' => write!(f, "\\t")?,
156                            c => write!(f, "{}", c)?,
157                        }
158                    }
159                    write!(f, "\"")
160                } else {
161                    write!(f, "{}", s)
162                }
163            }
164            SochValue::Binary(b) => write!(f, "0x{}", hex::encode(b)),
165            SochValue::Array(arr) => {
166                write!(f, "[")?;
167                for (i, v) in arr.iter().enumerate() {
168                    if i > 0 {
169                        write!(f, ";")?;
170                    }
171                    write!(f, "{}", v)?;
172                }
173                write!(f, "]")
174            }
175            SochValue::Object(obj) => {
176                write!(f, "{{")?;
177                for (i, (k, v)) in obj.iter().enumerate() {
178                    if i > 0 {
179                        write!(f, ";")?;
180                    }
181                    write!(f, "{}:{}", k, v)?;
182                }
183                write!(f, "}}")
184            }
185            SochValue::Ref { table, id } => write!(f, "@{}:{}", table, id),
186        }
187    }
188}
189
190/// Field type in a TOON schema
191#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
192pub enum SochType {
193    Null,
194    Bool,
195    Int,
196    UInt,
197    Float,
198    Text,
199    Binary,
200    Array(Box<SochType>),
201    Object(Vec<(String, SochType)>),
202    Ref(String), // Reference to table name
203    /// Union of types (for nullable fields)
204    Optional(Box<SochType>),
205}
206
207impl SochType {
208    /// Check if a value matches this type
209    pub fn matches(&self, value: &SochValue) -> bool {
210        match (self, value) {
211            (SochType::Null, SochValue::Null) => true,
212            (SochType::Bool, SochValue::Bool(_)) => true,
213            (SochType::Int, SochValue::Int(_)) => true,
214            (SochType::UInt, SochValue::UInt(_)) => true,
215            (SochType::Float, SochValue::Float(_)) => true,
216            (SochType::Text, SochValue::Text(_)) => true,
217            (SochType::Binary, SochValue::Binary(_)) => true,
218            (SochType::Array(inner), SochValue::Array(arr)) => arr.iter().all(|v| inner.matches(v)),
219            (SochType::Ref(table), SochValue::Ref { table: t, .. }) => table == t,
220            (SochType::Optional(inner), value) => value.is_null() || inner.matches(value),
221            _ => false,
222        }
223    }
224
225    /// Parse type from string notation
226    pub fn parse(s: &str) -> Option<Self> {
227        let s = s.trim();
228        match s {
229            "null" => Some(SochType::Null),
230            "bool" => Some(SochType::Bool),
231            "int" | "i64" => Some(SochType::Int),
232            "uint" | "u64" => Some(SochType::UInt),
233            "float" | "f64" => Some(SochType::Float),
234            "text" | "string" => Some(SochType::Text),
235            "binary" | "bytes" => Some(SochType::Binary),
236            _ if s.starts_with("ref(") && s.ends_with(')') => {
237                let table = &s[4..s.len() - 1];
238                Some(SochType::Ref(table.to_string()))
239            }
240            _ if s.starts_with("array(") && s.ends_with(')') => {
241                let inner = &s[6..s.len() - 1];
242                SochType::parse(inner).map(|t| SochType::Array(Box::new(t)))
243            }
244            _ if s.ends_with('?') => {
245                let inner = &s[..s.len() - 1];
246                SochType::parse(inner).map(|t| SochType::Optional(Box::new(t)))
247            }
248            _ => None,
249        }
250    }
251}
252
253impl fmt::Display for SochType {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        match self {
256            SochType::Null => write!(f, "null"),
257            SochType::Bool => write!(f, "bool"),
258            SochType::Int => write!(f, "int"),
259            SochType::UInt => write!(f, "uint"),
260            SochType::Float => write!(f, "float"),
261            SochType::Text => write!(f, "text"),
262            SochType::Binary => write!(f, "binary"),
263            SochType::Array(inner) => write!(f, "array({})", inner),
264            SochType::Object(fields) => {
265                write!(f, "{{")?;
266                for (i, (name, ty)) in fields.iter().enumerate() {
267                    if i > 0 {
268                        write!(f, ",")?;
269                    }
270                    write!(f, "{}:{}", name, ty)?;
271                }
272                write!(f, "}}")
273            }
274            SochType::Ref(table) => write!(f, "ref({})", table),
275            SochType::Optional(inner) => write!(f, "{}?", inner),
276        }
277    }
278}
279
280/// A TOON schema definition
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282pub struct SochSchema {
283    /// Schema name (table name)
284    pub name: String,
285    /// Field definitions
286    pub fields: Vec<SochField>,
287    /// Primary key field name
288    pub primary_key: Option<String>,
289    /// Indexes on this schema
290    pub indexes: Vec<SochIndex>,
291}
292
293/// A field in a TOON schema
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
295pub struct SochField {
296    pub name: String,
297    pub field_type: SochType,
298    pub nullable: bool,
299    pub default: Option<String>, // Default value as TOON string
300}
301
302/// An index definition
303#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304pub struct SochIndex {
305    pub name: String,
306    pub fields: Vec<String>,
307    pub unique: bool,
308}
309
310impl SochSchema {
311    pub fn new(name: impl Into<String>) -> Self {
312        Self {
313            name: name.into(),
314            fields: Vec::new(),
315            primary_key: None,
316            indexes: Vec::new(),
317        }
318    }
319
320    pub fn field(mut self, name: impl Into<String>, field_type: SochType) -> Self {
321        self.fields.push(SochField {
322            name: name.into(),
323            field_type,
324            nullable: false,
325            default: None,
326        });
327        self
328    }
329
330    pub fn nullable_field(mut self, name: impl Into<String>, field_type: SochType) -> Self {
331        self.fields.push(SochField {
332            name: name.into(),
333            field_type,
334            nullable: true,
335            default: None,
336        });
337        self
338    }
339
340    pub fn primary_key(mut self, field: impl Into<String>) -> Self {
341        self.primary_key = Some(field.into());
342        self
343    }
344
345    pub fn index(mut self, name: impl Into<String>, fields: Vec<String>, unique: bool) -> Self {
346        self.indexes.push(SochIndex {
347            name: name.into(),
348            fields,
349            unique,
350        });
351        self
352    }
353
354    /// Get field names for header
355    pub fn field_names(&self) -> Vec<&str> {
356        self.fields.iter().map(|f| f.name.as_str()).collect()
357    }
358
359    /// Format schema header: name[0]{field1,field2,...}:
360    pub fn format_header(&self) -> String {
361        let fields: Vec<&str> = self.fields.iter().map(|f| f.name.as_str()).collect();
362        format!("{}[0]{{{}}}:", self.name, fields.join(","))
363    }
364}
365
366/// A TOON row - values for a single record
367#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
368pub struct SochRow {
369    pub values: Vec<SochValue>,
370}
371
372impl SochRow {
373    pub fn new(values: Vec<SochValue>) -> Self {
374        Self { values }
375    }
376
377    /// Get value by index
378    pub fn get(&self, index: usize) -> Option<&SochValue> {
379        self.values.get(index)
380    }
381
382    /// Format row as TOON line
383    pub fn format(&self) -> String {
384        self.values
385            .iter()
386            .map(|v| v.to_string())
387            .collect::<Vec<_>>()
388            .join(",")
389    }
390
391    /// Parse row from TOON line
392    pub fn parse(line: &str, schema: &SochSchema) -> Result<Self, String> {
393        let mut values = Vec::with_capacity(schema.fields.len());
394        let mut chars = line.chars().peekable();
395        let mut current = String::new();
396        let mut in_quotes = false;
397        let mut field_idx = 0;
398
399        while let Some(ch) = chars.next() {
400            match ch {
401                '"' if !in_quotes => {
402                    in_quotes = true;
403                }
404                '"' if in_quotes => {
405                    if chars.peek() == Some(&'"') {
406                        chars.next();
407                        current.push('"');
408                    } else {
409                        in_quotes = false;
410                    }
411                }
412                ',' if !in_quotes => {
413                    let value = Self::parse_value(&current, field_idx, schema)?;
414                    values.push(value);
415                    current.clear();
416                    field_idx += 1;
417                }
418                _ => {
419                    current.push(ch);
420                }
421            }
422        }
423
424        // Last field
425        if !current.is_empty() || field_idx < schema.fields.len() {
426            let value = Self::parse_value(&current, field_idx, schema)?;
427            values.push(value);
428        }
429
430        Ok(Self { values })
431    }
432
433    fn parse_value(s: &str, field_idx: usize, schema: &SochSchema) -> Result<SochValue, String> {
434        let s = s.trim();
435
436        if s.is_empty() || s == "null" {
437            return Ok(SochValue::Null);
438        }
439
440        let field = schema
441            .fields
442            .get(field_idx)
443            .ok_or_else(|| format!("Field index {} out of bounds", field_idx))?;
444
445        match &field.field_type {
446            SochType::Bool => match s.to_lowercase().as_str() {
447                "true" | "1" | "yes" => Ok(SochValue::Bool(true)),
448                "false" | "0" | "no" => Ok(SochValue::Bool(false)),
449                _ => Err(format!("Invalid bool: {}", s)),
450            },
451            SochType::Int => s
452                .parse::<i64>()
453                .map(SochValue::Int)
454                .map_err(|e| format!("Invalid int: {}", e)),
455            SochType::UInt => s
456                .parse::<u64>()
457                .map(SochValue::UInt)
458                .map_err(|e| format!("Invalid uint: {}", e)),
459            SochType::Float => s
460                .parse::<f64>()
461                .map(SochValue::Float)
462                .map_err(|e| format!("Invalid float: {}", e)),
463            SochType::Text => Ok(SochValue::Text(s.to_string())),
464            SochType::Binary => {
465                if let Some(hex_str) = s.strip_prefix("0x") {
466                    hex::decode(hex_str)
467                        .map(SochValue::Binary)
468                        .map_err(|e| format!("Invalid hex: {}", e))
469                } else {
470                    Err("Binary must start with 0x".to_string())
471                }
472            }
473            SochType::Ref(table) => {
474                // Format: @table:id or just id
475                if let Some(ref_str) = s.strip_prefix('@') {
476                    let parts: Vec<&str> = ref_str.split(':').collect();
477                    if parts.len() == 2 {
478                        let id = parts[1]
479                            .parse::<u64>()
480                            .map_err(|e| format!("Invalid ref id: {}", e))?;
481                        Ok(SochValue::Ref {
482                            table: parts[0].to_string(),
483                            id,
484                        })
485                    } else {
486                        Err(format!("Invalid ref format: {}", s))
487                    }
488                } else {
489                    let id = s
490                        .parse::<u64>()
491                        .map_err(|e| format!("Invalid ref id: {}", e))?;
492                    Ok(SochValue::Ref {
493                        table: table.clone(),
494                        id,
495                    })
496                }
497            }
498            SochType::Optional(inner) => {
499                // Try to parse as inner type
500                let temp_field = SochField {
501                    name: field.name.clone(),
502                    field_type: (**inner).clone(),
503                    nullable: true,
504                    default: None,
505                };
506                let temp_schema = SochSchema {
507                    name: schema.name.clone(),
508                    fields: vec![temp_field],
509                    primary_key: None,
510                    indexes: vec![],
511                };
512                Self::parse_value(s, 0, &temp_schema)
513            }
514            _ => Ok(SochValue::Text(s.to_string())),
515        }
516    }
517}
518
519/// A complete TOON table (header + rows)
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct SochTable {
522    pub schema: SochSchema,
523    pub rows: Vec<SochRow>,
524}
525
526impl SochTable {
527    pub fn new(schema: SochSchema) -> Self {
528        Self {
529            schema,
530            rows: Vec::new(),
531        }
532    }
533
534    pub fn with_rows(schema: SochSchema, rows: Vec<SochRow>) -> Self {
535        Self { schema, rows }
536    }
537
538    pub fn push(&mut self, row: SochRow) {
539        self.rows.push(row);
540    }
541
542    pub fn len(&self) -> usize {
543        self.rows.len()
544    }
545
546    pub fn is_empty(&self) -> bool {
547        self.rows.is_empty()
548    }
549
550    /// Format as TOON string
551    pub fn format(&self) -> String {
552        let fields: Vec<&str> = self.schema.fields.iter().map(|f| f.name.as_str()).collect();
553        let header = format!(
554            "{}[{}]{{{}}}:",
555            self.schema.name,
556            self.rows.len(),
557            fields.join(",")
558        );
559
560        let mut output = header;
561        for row in &self.rows {
562            output.push('\n');
563            output.push_str(&row.format());
564        }
565        output
566    }
567
568    /// Parse TOON string to table
569    pub fn parse(input: &str) -> Result<Self, String> {
570        let mut lines = input.lines();
571
572        // Parse header: name[count]{field1,field2,...}:
573        let header = lines.next().ok_or("Empty input")?;
574        let (schema, _count) = Self::parse_header(header)?;
575
576        // Parse rows
577        let mut rows = Vec::new();
578        for line in lines {
579            if line.trim().is_empty() {
580                continue;
581            }
582            let row = SochRow::parse(line, &schema)?;
583            rows.push(row);
584        }
585
586        Ok(Self { schema, rows })
587    }
588
589    fn parse_header(header: &str) -> Result<(SochSchema, usize), String> {
590        // name[count]{field1,field2,...}:
591        let header = header.trim_end_matches(':');
592
593        let bracket_start = header.find('[').ok_or("Missing [")?;
594        let bracket_end = header.find(']').ok_or("Missing ]")?;
595        let brace_start = header.find('{').ok_or("Missing {")?;
596        let brace_end = header.find('}').ok_or("Missing }")?;
597
598        let name = &header[..bracket_start];
599        let count_str = &header[bracket_start + 1..bracket_end];
600        let fields_str = &header[brace_start + 1..brace_end];
601
602        let count = count_str
603            .parse::<usize>()
604            .map_err(|e| format!("Invalid count: {}", e))?;
605
606        let field_names: Vec<&str> = fields_str.split(',').map(|s| s.trim()).collect();
607
608        let mut schema = SochSchema::new(name);
609        for field_name in field_names {
610            // Check if type is specified: field_name:type
611            if let Some(colon_pos) = field_name.find(':') {
612                let fname = &field_name[..colon_pos];
613                let ftype_str = &field_name[colon_pos + 1..];
614                let ftype = SochType::parse(ftype_str).unwrap_or(SochType::Text);
615                schema.fields.push(SochField {
616                    name: fname.to_string(),
617                    field_type: ftype,
618                    nullable: false,
619                    default: None,
620                });
621            } else {
622                // Default to text type
623                schema.fields.push(SochField {
624                    name: field_name.to_string(),
625                    field_type: SochType::Text,
626                    nullable: false,
627                    default: None,
628                });
629            }
630        }
631
632        Ok((schema, count))
633    }
634}
635
636impl fmt::Display for SochTable {
637    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638        write!(f, "{}", self.format())
639    }
640}
641
642/// Trait for accessing columnar data without allocation
643pub trait ColumnAccess {
644    fn row_count(&self) -> usize;
645    fn col_count(&self) -> usize;
646    fn field_names(&self) -> Vec<&str>;
647    fn write_value(
648        &self,
649        col_idx: usize,
650        row_idx: usize,
651        f: &mut dyn std::fmt::Write,
652    ) -> std::fmt::Result;
653}
654
655/// Cursor for iterating over columnar data and emitting TOON format
656pub struct SochCursor<'a, C: ColumnAccess> {
657    access: &'a C,
658    current_row: usize,
659    header_emitted: bool,
660    schema_name: String,
661}
662
663impl<'a, C: ColumnAccess> SochCursor<'a, C> {
664    pub fn new(access: &'a C, schema_name: String) -> Self {
665        Self {
666            access,
667            current_row: 0,
668            header_emitted: false,
669            schema_name,
670        }
671    }
672}
673
674impl<'a, C: ColumnAccess> Iterator for SochCursor<'a, C> {
675    type Item = String;
676
677    fn next(&mut self) -> Option<Self::Item> {
678        if !self.header_emitted {
679            self.header_emitted = true;
680            let fields = self.access.field_names().join(",");
681            return Some(format!(
682                "{}[{}]{{{}}}:",
683                self.schema_name,
684                self.access.row_count(),
685                fields
686            ));
687        }
688
689        if self.current_row >= self.access.row_count() {
690            return None;
691        }
692
693        let mut row_str = String::new();
694        for col_idx in 0..self.access.col_count() {
695            if col_idx > 0 {
696                row_str.push(',');
697            }
698            // We ignore write errors here as String write shouldn't fail
699            let _ = self
700                .access
701                .write_value(col_idx, self.current_row, &mut row_str);
702        }
703
704        self.current_row += 1;
705        Some(row_str)
706    }
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    #[test]
714    fn test_soch_value_display() {
715        assert_eq!(SochValue::Int(42).to_string(), "42");
716        assert_eq!(SochValue::Text("hello".into()).to_string(), "hello");
717        assert_eq!(
718            SochValue::Text("hello, world".into()).to_string(),
719            "\"hello, world\""
720        );
721        assert_eq!(SochValue::Bool(true).to_string(), "true");
722        assert_eq!(SochValue::Null.to_string(), "null");
723    }
724
725    #[test]
726    fn test_soch_schema() {
727        let schema = SochSchema::new("users")
728            .field("id", SochType::UInt)
729            .field("name", SochType::Text)
730            .field("email", SochType::Text)
731            .primary_key("id");
732
733        assert_eq!(schema.name, "users");
734        assert_eq!(schema.fields.len(), 3);
735        assert_eq!(schema.primary_key, Some("id".to_string()));
736    }
737
738    #[test]
739    fn test_soch_table_format() {
740        let schema = SochSchema::new("users")
741            .field("id", SochType::UInt)
742            .field("name", SochType::Text)
743            .field("email", SochType::Text);
744
745        let mut table = SochTable::new(schema);
746        table.push(SochRow::new(vec![
747            SochValue::UInt(1),
748            SochValue::Text("Alice".into()),
749            SochValue::Text("alice@example.com".into()),
750        ]));
751        table.push(SochRow::new(vec![
752            SochValue::UInt(2),
753            SochValue::Text("Bob".into()),
754            SochValue::Text("bob@example.com".into()),
755        ]));
756
757        let formatted = table.format();
758        assert!(formatted.contains("users[2]{id,name,email}:"));
759        assert!(formatted.contains("1,Alice,alice@example.com"));
760        assert!(formatted.contains("2,Bob,bob@example.com"));
761    }
762
763    #[test]
764    fn test_soch_table_parse() {
765        let input = r#"users[2]{id,name,email}:
7661,Alice,alice@example.com
7672,Bob,bob@example.com"#;
768
769        let table = SochTable::parse(input).unwrap();
770        assert_eq!(table.schema.name, "users");
771        assert_eq!(table.rows.len(), 2);
772    }
773
774    #[test]
775    fn test_soch_type_parse() {
776        assert_eq!(SochType::parse("int"), Some(SochType::Int));
777        assert_eq!(SochType::parse("text"), Some(SochType::Text));
778        assert_eq!(
779            SochType::parse("ref(users)"),
780            Some(SochType::Ref("users".into()))
781        );
782        assert_eq!(
783            SochType::parse("int?"),
784            Some(SochType::Optional(Box::new(SochType::Int)))
785        );
786    }
787}