Skip to main content

gqls/
model.rs

1//! The language-agnostic record model: every type, field, arg, enum value,
2//! and directive in a schema flattens to one [`SchemaRecord`]. Search and
3//! output operate only on these — they never touch a parser or a transport.
4
5use std::str::FromStr;
6
7use serde::Serialize;
8
9/// What a [`SchemaRecord`] describes. Root operation fields get their own
10/// kinds (`Query`/`Mutation`/`Subscription`) so ranking can float them to
11/// the top the way `rq` floats top-level definitions.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum Kind {
15    Object,
16    Interface,
17    Union,
18    Enum,
19    InputObject,
20    Scalar,
21    Directive,
22    Field,
23    InputField,
24    EnumValue,
25    Query,
26    Mutation,
27    Subscription,
28}
29
30impl Kind {
31    pub fn as_str(self) -> &'static str {
32        match self {
33            Kind::Object => "object",
34            Kind::Interface => "interface",
35            Kind::Union => "union",
36            Kind::Enum => "enum",
37            Kind::InputObject => "input_object",
38            Kind::Scalar => "scalar",
39            Kind::Directive => "directive",
40            Kind::Field => "field",
41            Kind::InputField => "input_field",
42            Kind::EnumValue => "enum_value",
43            Kind::Query => "query",
44            Kind::Mutation => "mutation",
45            Kind::Subscription => "subscription",
46        }
47    }
48
49    /// Static ranking bias by kind — roots and named types outrank leaf
50    /// args. (In `rq` this is the `kind`-weight table in `score.rs`.)
51    pub fn weight(self) -> i64 {
52        match self {
53            Kind::Query | Kind::Mutation | Kind::Subscription => 60,
54            Kind::Object
55            | Kind::Interface
56            | Kind::Union
57            | Kind::Enum
58            | Kind::InputObject
59            | Kind::Scalar => 40,
60            Kind::Directive => 30,
61            Kind::Field | Kind::InputField => 20,
62            Kind::EnumValue => 10,
63        }
64    }
65}
66
67impl FromStr for Kind {
68    type Err = anyhow::Error;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        Ok(match s.to_lowercase().as_str() {
72            "object" | "objects" | "type" | "types" => Kind::Object,
73            "interface" | "interfaces" => Kind::Interface,
74            "union" | "unions" => Kind::Union,
75            "enum" | "enums" => Kind::Enum,
76            "input" | "input_object" | "input_objects" | "inputobject" => Kind::InputObject,
77            "scalar" | "scalars" => Kind::Scalar,
78            "directive" | "directives" => Kind::Directive,
79            "field" | "fields" => Kind::Field,
80            "input_field" | "input_fields" | "inputfield" => Kind::InputField,
81            "enum_value" | "enum_values" | "enumvalue" => Kind::EnumValue,
82            "query" | "queries" => Kind::Query,
83            "mutation" | "mutations" => Kind::Mutation,
84            "subscription" | "subscriptions" => Kind::Subscription,
85            other => anyhow::bail!(
86                "unknown kind {other:?} — valid: object, interface, union, enum, \
87                 input_object, scalar, directive, field, input_field, enum_value, \
88                 query, mutation, subscription (plurals ok)"
89            ),
90        })
91    }
92}
93
94/// One searchable schema entity.
95#[derive(Debug, Clone, Serialize)]
96pub struct SchemaRecord {
97    /// Fully-qualified path, e.g. `User.email`, `Query.user`, `@deprecated`.
98    pub path: String,
99    /// The leaf name matched against — `email`, `user`, `User`.
100    pub name: String,
101    pub kind: Kind,
102    /// Enclosing type name for fields/args/enum values.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub parent: Option<String>,
105    /// Return/field/arg type, rendered like `[User!]!`.
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub type_ref: Option<String>,
108    /// Argument signatures for a field, e.g. `["id: ID!", "first: Int"]`.
109    #[serde(skip_serializing_if = "Vec::is_empty")]
110    pub args: Vec<String>,
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub description: Option<String>,
113    /// Deprecation reason, if the entity is `@deprecated`.
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub deprecated: Option<String>,
116    /// Applied directives, rendered like `@auth`. Populated from SDL; always
117    /// empty when the schema is loaded by introspection, since applied
118    /// directives aren't exposed by the standard introspection query — so the
119    /// same schema can differ in this field between its SDL and its endpoint.
120    #[serde(skip_serializing_if = "Vec::is_empty")]
121    pub directives: Vec<String>,
122}
123
124/// The root operation type names (Query / Mutation / Subscription), used to
125/// classify a type's fields as root operations vs. plain object fields. Shared
126/// by the SDL and introspection loaders so that one rule lives in one place;
127/// each loader fills in whichever roots its source declares.
128#[derive(Default)]
129pub struct Roots {
130    pub query: Option<String>,
131    pub mutation: Option<String>,
132    pub subscription: Option<String>,
133}
134
135impl Roots {
136    /// The [`Kind`] for a field defined on `type_name`: a root-operation kind if
137    /// `type_name` is a root type, otherwise a plain object [`Kind::Field`].
138    pub fn field_kind(&self, type_name: &str) -> Kind {
139        if self.query.as_deref() == Some(type_name) {
140            Kind::Query
141        } else if self.mutation.as_deref() == Some(type_name) {
142            Kind::Mutation
143        } else if self.subscription.as_deref() == Some(type_name) {
144            Kind::Subscription
145        } else {
146            Kind::Field
147        }
148    }
149}