Skip to main content

wesley_core/domain/
ir.rs

1//! Wesley Intermediate Representation (IR).
2//!
3//! Pure semantic representation of GraphQL SDL, free from domain-specific concepts.
4
5use indexmap::IndexMap;
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8
9/// The root Wesley IR structure.
10#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
11pub struct WesleyIR {
12    /// IR Schema version (e.g. "1.0.0").
13    pub version: String,
14    /// Non-deterministic metadata (stripped during parity hashing).
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub metadata: Option<Metadata>,
17    /// Consolidated type definitions.
18    pub types: Vec<TypeDefinition>,
19}
20
21/// Metadata for the IR.
22#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
23#[serde(rename_all = "camelCase")]
24pub struct Metadata {
25    /// Source SDL hash.
26    pub source_hash: Option<String>,
27    /// Generation timestamp.
28    pub generated_at: Option<String>,
29    /// Compilation units involved.
30    #[serde(default, skip_serializing_if = "Vec::is_empty")]
31    pub units: Vec<UnitMeta>,
32}
33
34/// Compilation unit metadata.
35#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
36pub struct UnitMeta {
37    /// Unit ID (usually path).
38    pub id: String,
39    /// Package name.
40    pub package: String,
41    /// Unit hash.
42    pub hash: String,
43}
44
45/// A type definition (Object, Interface, etc.).
46#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
47#[serde(rename_all = "camelCase")]
48pub struct TypeDefinition {
49    /// Type name.
50    pub name: String,
51    /// Kind of type.
52    pub kind: TypeKind,
53    /// Optional description.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub description: Option<String>,
56    /// Generic map of directives.
57    pub directives: IndexMap<String, serde_json::Value>,
58    /// Interfaces this type implements (for Objects and Interfaces).
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub implements: Vec<String>,
61    /// Fields (for Objects, Interfaces, InputObjects).
62    #[serde(default, skip_serializing_if = "Vec::is_empty")]
63    pub fields: Vec<Field>,
64    /// Enum values (for Enums).
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    pub enum_values: Vec<String>,
67    /// Union member type names (for Unions).
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub union_members: Vec<String>,
70}
71
72/// Kinds of GraphQL types.
73#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
74#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
75pub enum TypeKind {
76    /// Object type.
77    Object,
78    /// Interface type.
79    Interface,
80    /// Union type.
81    Union,
82    /// Enum type.
83    Enum,
84    /// Scalar type.
85    Scalar,
86    /// Input object type.
87    InputObject,
88}
89
90/// A field definition.
91#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
92#[serde(rename_all = "camelCase")]
93pub struct Field {
94    /// Field name.
95    pub name: String,
96    /// Optional description.
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub description: Option<String>,
99    /// Field type reference.
100    pub r#type: TypeReference,
101    /// Field arguments, for object and interface fields.
102    #[serde(default, skip_serializing_if = "Vec::is_empty")]
103    pub arguments: Vec<FieldArgument>,
104    /// Generic map of directives.
105    pub directives: IndexMap<String, serde_json::Value>,
106}
107
108/// A field argument definition.
109#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
110#[serde(rename_all = "camelCase")]
111pub struct FieldArgument {
112    /// Argument name.
113    pub name: String,
114    /// Optional description.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub description: Option<String>,
117    /// Argument type reference.
118    pub r#type: TypeReference,
119    /// Default value, if the schema declares one.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub default_value: Option<serde_json::Value>,
122    /// Generic map of directives attached to the argument.
123    pub directives: IndexMap<String, serde_json::Value>,
124}
125
126/// Reference to a type with nullability and list information.
127#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
128#[serde(rename_all = "camelCase")]
129pub struct TypeReference {
130    /// Base type name.
131    pub base: String,
132    /// Whether the type is nullable.
133    pub nullable: bool,
134    /// Whether the type is a list.
135    pub is_list: bool,
136    /// Whether list items are nullable.
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub list_item_nullable: Option<bool>,
139    /// Nested list wrappers ordered from outermost to innermost.
140    #[serde(default, skip_serializing_if = "Vec::is_empty")]
141    pub list_wrappers: Vec<TypeListWrapper>,
142    /// Whether the named leaf value is nullable for nested list references.
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub leaf_nullable: Option<bool>,
145}
146
147/// One list wrapper inside a nested GraphQL type reference.
148#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
149#[serde(rename_all = "camelCase")]
150pub struct TypeListWrapper {
151    /// Whether this list wrapper is nullable.
152    pub nullable: bool,
153}
154
155/// Computes the canonical registry hash for the given IR.
156pub fn compute_registry_hash(ir: &WesleyIR) -> Result<String, serde_json::Error> {
157    let mut parity_ir = ir.clone();
158    parity_ir.metadata = None;
159
160    let json = to_canonical_json(&parity_ir)?;
161    Ok(compute_content_hash(&json))
162}
163
164/// Computes a stable SHA-256 hex hash for arbitrary UTF-8 content.
165pub fn compute_content_hash(content: &str) -> String {
166    let mut hasher = Sha256::new();
167    hasher.update(content.as_bytes());
168    let result = hasher.finalize();
169
170    hex::encode(result)
171}
172
173/// Serializes a value to a canonical JSON string (sorted keys, no whitespace).
174pub fn to_canonical_json<T: Serialize>(value: &T) -> Result<String, serde_json::Error> {
175    let val = serde_json::to_value(value)?;
176    let sorted_val = sort_json_value(val);
177    serde_json::to_string(&sorted_val)
178}
179
180fn sort_json_value(value: serde_json::Value) -> serde_json::Value {
181    match value {
182        serde_json::Value::Object(map) => {
183            let mut sorted_map = serde_json::Map::new();
184            let mut keys: Vec<String> = map.keys().cloned().collect();
185            keys.sort();
186            for key in keys {
187                if let Some(val) = map.get(&key) {
188                    sorted_map.insert(key, sort_json_value(val.clone()));
189                }
190            }
191            serde_json::Value::Object(sorted_map)
192        }
193        serde_json::Value::Array(arr) => {
194            serde_json::Value::Array(arr.into_iter().map(sort_json_value).collect())
195        }
196        _ => value,
197    }
198}