lean_ctx/core/
rules_validation.rs1use crate::core::config::CompressionLevel;
3
4use super::rules_canonical::{END_MARK, RULES_VERSION, START_MARK, Wrapper, render};
5
6#[derive(Debug)]
11pub struct RulesFile<'a> {
12 content: &'a str,
13 start: Option<usize>,
15 end: Option<usize>,
17 version: usize,
19}
20
21fn parse_version_number(s: &str) -> Option<usize> {
23 let prefix = "<!-- version: ";
24 let vs = s.find(prefix)?;
25 let num_start = vs + prefix.len();
26 let end = s[num_start..].find(" -->")?;
27 s[num_start..num_start + end].parse().ok()
28}
29
30impl<'a> RulesFile<'a> {
31 pub fn parse(content: &'a str) -> Self {
33 let start = content.find(START_MARK);
34 let version = start
35 .and_then(|s| parse_version_number(&content[s + START_MARK.len()..]))
36 .unwrap_or(0);
37 let end = content.find(END_MARK);
38 RulesFile {
39 content,
40 start,
41 end,
42 version,
43 }
44 }
45
46 pub fn has_content(&self) -> bool {
48 self.start.is_some()
49 }
50
51 pub fn version(&self) -> usize {
53 self.version
54 }
55
56 pub fn is_current(&self) -> bool {
58 self.version >= RULES_VERSION
59 }
60
61 pub fn prefix(&self) -> &'a str {
63 self.start.map_or("", |s| self.content[..s].trim())
64 }
65
66 pub fn suffix(&self) -> &'a str {
68 self.end
69 .map_or("", |e| self.content[e + END_MARK.len()..].trim())
70 }
71
72 fn block(&self) -> Option<&'a str> {
74 match (self.start, self.end) {
75 (Some(s), Some(e)) if e >= s => Some(&self.content[s..e + END_MARK.len()]),
76 _ => None,
77 }
78 }
79
80 pub fn block_matches_render(
82 &self,
83 shadow: bool,
84 wrapper: Wrapper,
85 level: CompressionLevel,
86 tool_profile: &super::tool_profiles::ToolProfile,
87 ) -> bool {
88 match self.block() {
89 Some(block) => block.trim() == render(shadow, wrapper, level, tool_profile).trim(),
90 None => false,
91 }
92 }
93
94 pub fn merged(
96 &self,
97 shadow: bool,
98 wrapper: Wrapper,
99 level: CompressionLevel,
100 tool_profile: &super::tool_profiles::ToolProfile,
101 ) -> String {
102 let fresh = render(shadow, wrapper, level, tool_profile);
103 if self.start.is_some() {
104 let before = self.prefix();
105 let after = self.suffix();
106 let mut out = String::new();
107 if !before.is_empty() {
108 out.push_str(before);
109 out.push('\n');
110 out.push('\n');
111 }
112 out.push_str(&fresh);
113 if !after.is_empty() {
114 out.push('\n');
115 out.push('\n');
116 out.push_str(after);
117 }
118 if !out.ends_with('\n') {
119 out.push('\n');
120 }
121 out
122 } else {
123 let trimmed = self.content.trim_end();
124 let mut out = trimmed.to_string();
125 if !out.is_empty() {
126 out.push('\n');
127 out.push('\n');
128 }
129 out.push_str(&fresh);
130 out
131 }
132 }
133
134 pub fn initial(
136 shadow: bool,
137 wrapper: Wrapper,
138 level: CompressionLevel,
139 tool_profile: &super::tool_profiles::ToolProfile,
140 ) -> String {
141 render(shadow, wrapper, level, tool_profile)
142 }
143
144 pub fn without_section(&self) -> String {
146 if let Some(start_pos) = self.start {
147 let before = self.content[..start_pos].trim();
148 let after = self.suffix();
149 let mut out = String::new();
150 if !before.is_empty() {
151 out.push_str(before);
152 out.push('\n');
153 }
154 if !after.is_empty() {
155 out.push('\n');
156 out.push_str(after);
157 }
158 out
159 } else {
160 self.content.to_string()
161 }
162 }
163}