relay_knowledge/domain/code/
framework.rs1use serde::{Deserialize, Serialize};
4
5use super::{
6 DomainError, FreshnessPolicy,
7 error::required_text,
8 repository::{CodeRepositorySelector, RepositoryCodeRange},
9};
10
11const MAX_FRAMEWORK_FILTERS: usize = 2;
12const MAX_FRAMEWORK_KIND_FILTERS: usize = 16;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum FrameworkKind {
18 Angular,
19 Vue,
20}
21
22impl FrameworkKind {
23 pub const fn as_str(self) -> &'static str {
25 match self {
26 Self::Angular => "angular",
27 Self::Vue => "vue",
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum FrameworkNodeKind {
36 Component,
37 Directive,
38 Pipe,
39 Template,
40 Input,
41 Output,
42 Prop,
43 Emit,
44 Model,
45 Slot,
46 TemplateVariable,
47 ControlFlow,
48}
49
50impl FrameworkNodeKind {
51 pub const fn as_str(self) -> &'static str {
53 match self {
54 Self::Component => "component",
55 Self::Directive => "directive",
56 Self::Pipe => "pipe",
57 Self::Template => "template",
58 Self::Input => "input",
59 Self::Output => "output",
60 Self::Prop => "prop",
61 Self::Emit => "emit",
62 Self::Model => "model",
63 Self::Slot => "slot",
64 Self::TemplateVariable => "template_variable",
65 Self::ControlFlow => "control_flow",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum FrameworkEdgeKind {
74 OwnsTemplate,
75 Declares,
76 Imports,
77 Renders,
78 BindsInput,
79 HandlesOutput,
80 Reads,
81 Writes,
82 UsesDirective,
83 ProvidesSlot,
84}
85
86impl FrameworkEdgeKind {
87 pub const fn as_str(self) -> &'static str {
89 match self {
90 Self::OwnsTemplate => "owns_template",
91 Self::Declares => "declares",
92 Self::Imports => "imports",
93 Self::Renders => "renders",
94 Self::BindsInput => "binds_input",
95 Self::HandlesOutput => "handles_output",
96 Self::Reads => "reads",
97 Self::Writes => "writes",
98 Self::UsesDirective => "uses_directive",
99 Self::ProvidesSlot => "provides_slot",
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct CodeFrameworkNodeRecord {
107 pub repository_id: String,
108 pub source_scope: String,
109 pub node_id: String,
110 pub file_id: String,
111 pub path: String,
112 pub framework: FrameworkKind,
113 pub kind: FrameworkNodeKind,
114 pub name: String,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub detail: Option<String>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub symbol_snapshot_id: Option<String>,
119 pub byte_range: RepositoryCodeRange,
120 pub line_range: RepositoryCodeRange,
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
125pub struct CodeFrameworkEdgeRecord {
126 pub repository_id: String,
127 pub source_scope: String,
128 pub edge_id: String,
129 pub file_id: String,
130 pub path: String,
131 pub framework: FrameworkKind,
132 pub kind: FrameworkEdgeKind,
133 pub source_node_id: String,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub target_node_id: Option<String>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub target_hint: Option<String>,
138 pub resolution_state: String,
139 pub confidence_basis_points: u16,
140 pub confidence_tier: String,
141 pub byte_range: RepositoryCodeRange,
142 pub line_range: RepositoryCodeRange,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
147pub struct FrameworkGraphRequest {
148 #[serde(skip_serializing_if = "Option::is_none")]
149 pub query: Option<String>,
150 pub repository: CodeRepositorySelector,
151 #[serde(default, skip_serializing_if = "Vec::is_empty")]
152 pub frameworks: Vec<FrameworkKind>,
153 #[serde(default, skip_serializing_if = "Vec::is_empty")]
154 pub kinds: Vec<FrameworkNodeKind>,
155 pub limit: usize,
156 pub freshness_policy: FreshnessPolicy,
157}
158
159impl FrameworkGraphRequest {
160 pub fn new(
162 query: Option<String>,
163 repository: CodeRepositorySelector,
164 frameworks: Vec<FrameworkKind>,
165 kinds: Vec<FrameworkNodeKind>,
166 limit: usize,
167 freshness_policy: FreshnessPolicy,
168 ) -> Result<Self, DomainError> {
169 if !(1..=100).contains(&limit) {
170 return Err(DomainError::invalid("limit", "must be between 1 and 100"));
171 }
172 if frameworks.len() > MAX_FRAMEWORK_FILTERS {
173 return Err(DomainError::invalid(
174 "frameworks",
175 "must contain 2 or fewer entries",
176 ));
177 }
178 if kinds.len() > MAX_FRAMEWORK_KIND_FILTERS {
179 return Err(DomainError::invalid(
180 "kinds",
181 "must contain 16 or fewer entries",
182 ));
183 }
184 let query = query
185 .map(|value| required_text("query", value))
186 .transpose()?;
187
188 Ok(Self {
189 query,
190 repository,
191 frameworks,
192 kinds,
193 limit,
194 freshness_policy,
195 })
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201pub struct FrameworkGraph {
202 pub nodes: Vec<CodeFrameworkNodeRecord>,
203 pub edges: Vec<CodeFrameworkEdgeRecord>,
204 pub truncated: bool,
205}
206
207#[cfg(test)]
208#[path = "framework_tests.rs"]
209mod tests;