formal_ai/seed/
projects.rs1use super::parser::{parse_lino, split_pipe_list, LinoNode};
27use super::PROJECTS_LINO;
28
29#[derive(Debug, Clone, Default)]
31pub struct ProjectStatement {
32 pub text: String,
33 pub kind: String,
34 pub weight: u8,
35}
36
37impl ProjectStatement {
38 fn parse(node: &LinoNode) -> Option<Self> {
39 if node.name != "statement" {
40 return None;
41 }
42 let text = node.id.trim().to_string();
43 if text.is_empty() {
44 return None;
45 }
46 let kind = node.find_child_value("kind").trim().to_string();
47 let weight = node
48 .find_child_value("weight")
49 .trim()
50 .parse::<u8>()
51 .unwrap_or(50);
52 Some(Self { text, kind, weight })
53 }
54}
55
56#[derive(Debug, Clone, Default)]
58pub struct LocalizedProject {
59 pub language: String,
60 pub display_name: String,
61 pub statements: Vec<ProjectStatement>,
62}
63
64#[derive(Debug, Clone)]
66pub struct ProjectRecord {
67 pub slug: String,
68 pub org: String,
69 pub name: String,
70 pub display_name: String,
71 pub url: String,
72 pub language: String,
73 pub category: String,
74 pub aliases: Vec<String>,
75 pub topic: String,
76 pub statements: Vec<ProjectStatement>,
77 pub localized: Vec<LocalizedProject>,
78}
79
80impl ProjectRecord {
81 #[must_use]
83 pub fn repo_slug(&self) -> String {
84 format!("{}/{}", self.org, self.name)
85 }
86
87 #[must_use]
90 pub fn localized_for(&self, language: &str) -> Option<&LocalizedProject> {
91 self.localized
92 .iter()
93 .find(|loc| loc.language == language)
94 .or_else(|| self.localized.iter().find(|loc| loc.language == "en"))
95 }
96
97 #[must_use]
100 pub fn statements_for(&self, language: &str) -> &[ProjectStatement] {
101 self.localized_for(language)
102 .map(|loc| loc.statements.as_slice())
103 .filter(|stmts| !stmts.is_empty())
104 .unwrap_or(self.statements.as_slice())
105 }
106
107 #[must_use]
109 pub fn display_name_for(&self, language: &str) -> &str {
110 self.localized_for(language)
111 .map(|loc| loc.display_name.as_str())
112 .filter(|name| !name.is_empty())
113 .unwrap_or(self.display_name.as_str())
114 }
115
116 #[must_use]
118 pub const fn topic_for(&self, language: &str) -> &str {
119 let _ = language;
122 if self.topic.is_empty() {
123 self.display_name.as_str()
124 } else {
125 self.topic.as_str()
126 }
127 }
128
129 #[must_use]
133 pub fn matches_alias(&self, term: &str) -> bool {
134 let normalized = normalize_alias(term);
135 if normalized.is_empty() {
136 return false;
137 }
138 self.aliases.iter().any(|alias| alias == &normalized)
139 }
140}
141
142#[derive(Debug, Clone, Default)]
144pub struct ProjectsRegistry {
145 pub projects: Vec<ProjectRecord>,
146}
147
148impl ProjectsRegistry {
149 #[must_use]
151 pub fn by_slug(&self, slug: &str) -> Option<&ProjectRecord> {
152 self.projects.iter().find(|p| p.slug == slug)
153 }
154
155 #[must_use]
157 pub fn by_alias(&self, term: &str) -> Option<&ProjectRecord> {
158 if term.is_empty() {
159 return None;
160 }
161 let normalized = normalize_alias(term);
162 if normalized.is_empty() {
163 return None;
164 }
165 self.projects.iter().find(|p| {
166 p.aliases.iter().any(|alias| alias == &normalized)
167 || normalize_alias(&p.display_name) == normalized
168 || normalize_alias(&p.name) == normalized
169 })
170 }
171
172 #[must_use]
174 pub fn by_org<'a>(&'a self, org: &str) -> Vec<&'a ProjectRecord> {
175 self.projects.iter().filter(|p| p.org == org).collect()
176 }
177}
178
179#[must_use]
181pub fn projects_registry() -> ProjectsRegistry {
182 let tree = parse_lino(PROJECTS_LINO);
183 let entries: &[LinoNode] = if tree.name.is_empty() {
184 tree.children.as_slice()
185 } else {
186 std::slice::from_ref(&tree)
187 };
188 let mut out = Vec::new();
189 for entry in entries {
190 if !entry.name.starts_with("project_") {
191 continue;
192 }
193 let aliases = split_pipe_list(entry.find_child_value("aliases"))
194 .into_iter()
195 .map(|s| normalize_alias(&s))
196 .filter(|s| !s.is_empty())
197 .collect();
198 let statements: Vec<ProjectStatement> = entry
199 .children
200 .iter()
201 .filter_map(ProjectStatement::parse)
202 .collect();
203 let localized = entry
204 .children
205 .iter()
206 .filter(|c| c.name == "localized")
207 .map(|child| {
208 let language = child.id.clone();
209 let display_name = child.find_child_value("display_name").to_string();
210 let stmts: Vec<ProjectStatement> = child
211 .children
212 .iter()
213 .filter_map(ProjectStatement::parse)
214 .collect();
215 LocalizedProject {
216 language,
217 display_name,
218 statements: stmts,
219 }
220 })
221 .filter(|loc| !loc.language.is_empty())
222 .collect();
223 out.push(ProjectRecord {
224 slug: entry.name.clone(),
225 org: entry.find_child_value("org").to_string(),
226 name: entry.find_child_value("name").to_string(),
227 display_name: entry.find_child_value("display_name").to_string(),
228 url: entry.find_child_value("url").to_string(),
229 language: entry.find_child_value("language").to_string(),
230 category: entry.find_child_value("category").to_string(),
231 aliases,
232 topic: entry.find_child_value("topic").to_string(),
233 statements,
234 localized,
235 });
236 }
237 ProjectsRegistry { projects: out }
238}
239
240fn normalize_alias(value: &str) -> String {
243 value
244 .to_lowercase()
245 .replace(['-', '_'], " ")
246 .split_whitespace()
247 .collect::<Vec<_>>()
248 .join(" ")
249}