Skip to main content

dynoxide/schema/
mod.rs

1//! Application-level data model definitions for MCP agent context.
2//!
3//! Parses schema files (e.g., OneTable) into a [`DataModel`] that can be
4//! exposed to MCP-connected agents via instructions and `get_database_info`.
5
6pub mod onetable;
7
8use serde::Serialize;
9
10/// Application-level data model parsed from a schema file (e.g., OneTable).
11/// Designed for agent consumption: serializes to JSON for MCP responses.
12#[derive(Debug, Clone, Serialize)]
13pub struct DataModel {
14    /// Schema format identifier, e.g. "onetable:1.1.0"
15    pub schema_format: String,
16    /// The attribute name used to discriminate entity types (e.g. "_type")
17    pub type_attribute: String,
18    /// Entity definitions with key templates and GSI mappings
19    pub entities: Vec<EntityDefinition>,
20}
21
22/// A single entity type within the data model.
23#[derive(Debug, Clone, Serialize)]
24pub struct EntityDefinition {
25    /// Entity name, e.g. "Account"
26    pub name: String,
27    /// Primary key partition template, e.g. "account#${id}"
28    pub pk_template: String,
29    /// Primary key sort template, e.g. "account#"
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub sk_template: Option<String>,
32    /// Entity-level type attribute override (usually same as DataModel.type_attribute)
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub type_attribute: Option<String>,
35    /// Which GSIs this entity participates in, with key templates
36    pub gsi_mappings: Vec<GsiMapping>,
37    /// Human-readable description from schema (if present)
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub description: Option<String>,
40}
41
42/// A GSI mapping for an entity, describing the key templates used.
43#[derive(Debug, Clone, Serialize)]
44pub struct GsiMapping {
45    /// DynamoDB index name resolved from the schema's index definitions.
46    ///
47    /// For OneTable schemas, this is resolved from the `name` field in the
48    /// index definition if present, otherwise falls back to the OneTable key
49    /// (e.g. "gs1"). Must match the name from CreateTable / describe_table
50    /// so agents can pass it directly to query's `index_name` parameter.
51    pub index_name: String,
52    /// GSI partition key template. Empty when the index hashes on a plain
53    /// attribute (a tenant id, say) and only its sort key is templated: the
54    /// index is still listed so the sort key template reaches every reader.
55    pub pk_template: String,
56    /// GSI sort key template (if the index has a sort key)
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub sk_template: Option<String>,
59}
60
61impl DataModel {
62    /// Generate a compact summary for MCP instructions.
63    ///
64    /// Returns entity names with their GSI participation, truncated to `limit`.
65    /// If `limit` is 0, returns `None` (summary suppressed).
66    pub fn instructions_summary(&self, limit: usize) -> Option<String> {
67        if limit == 0 {
68            return None;
69        }
70
71        let entity_count = self.entities.len();
72        let shown = self.entities.iter().take(limit);
73
74        let entity_parts: Vec<String> = shown
75            .map(|e| {
76                if e.gsi_mappings.is_empty() {
77                    e.name.clone()
78                } else {
79                    let gsis: Vec<&str> = e
80                        .gsi_mappings
81                        .iter()
82                        .map(|g| g.index_name.as_str())
83                        .collect();
84                    format!("{} ({})", e.name, gsis.join(", "))
85                }
86            })
87            .collect();
88
89        let mut summary = format!(
90            "## Data model\n\n\
91             Schema: {} ({} entities, type attribute: \"{}\")\n\
92             Entities: {}",
93            self.schema_format,
94            entity_count,
95            self.type_attribute,
96            entity_parts.join(", "),
97        );
98
99        if entity_count > limit {
100            summary.push_str(&format!("...and {} more", entity_count - limit));
101        }
102
103        summary.push_str(
104            "\n\nCall get_database_info for full entity definitions with key templates.\n\
105             Note: Data model definitions describe the intended schema but are not enforced. \
106             Actual database contents may differ.",
107        );
108
109        Some(summary)
110    }
111}