yerba 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/exe/yerba +2 -2
- data/ext/yerba/extconf.rb +54 -0
- data/lib/yerba/version.rb +1 -1
- data/lib/yerba.rb +99 -0
- data/rust/Cargo.lock +429 -0
- data/rust/Cargo.toml +32 -0
- data/rust/rustfmt.toml +2 -0
- data/rust/src/document.rs +1779 -0
- data/rust/src/error.rs +50 -0
- data/rust/src/json.rs +169 -0
- data/rust/src/lib.rs +22 -0
- data/rust/src/main.rs +856 -0
- data/rust/src/quote_style.rs +42 -0
- data/rust/src/syntax.rs +186 -0
- data/rust/src/yerbafile.rs +563 -0
- data/yerba.gemspec +8 -3
- metadata +15 -3
- data/Rakefile +0 -12
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
use rayon::prelude::*;
|
|
2
|
+
use serde::Deserialize;
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
use std::fs;
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
|
|
7
|
+
use crate::{Document, QuoteStyle, YerbaError};
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Deserialize)]
|
|
10
|
+
pub struct Yerbafile {
|
|
11
|
+
#[serde(default)]
|
|
12
|
+
pub rules: Vec<Rule>,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
16
|
+
pub struct Rule {
|
|
17
|
+
pub files: String,
|
|
18
|
+
#[serde(default)]
|
|
19
|
+
pub path: Option<String>,
|
|
20
|
+
pub pipeline: Vec<PipelineStep>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[derive(Debug, Clone)]
|
|
24
|
+
pub enum PipelineStep {
|
|
25
|
+
Get(GetConfig),
|
|
26
|
+
SortKeys(SortKeysConfig),
|
|
27
|
+
QuoteStyle(QuoteStyleConfig),
|
|
28
|
+
Set(SetConfig),
|
|
29
|
+
Insert(InsertConfig),
|
|
30
|
+
Delete(DeleteConfig),
|
|
31
|
+
Rename(RenameConfig),
|
|
32
|
+
Remove(RemoveConfig),
|
|
33
|
+
BlankLines(BlankLinesConfig),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
37
|
+
pub struct BlankLinesConfig {
|
|
38
|
+
#[serde(default)]
|
|
39
|
+
pub path: Option<String>,
|
|
40
|
+
pub count: usize,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
44
|
+
pub struct GetConfig {
|
|
45
|
+
pub path: String,
|
|
46
|
+
#[serde(rename = "as")]
|
|
47
|
+
pub as_name: String,
|
|
48
|
+
#[serde(default)]
|
|
49
|
+
pub file: Option<String>,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[derive(Debug, Clone)]
|
|
53
|
+
pub enum Variable {
|
|
54
|
+
Single(String),
|
|
55
|
+
List(Vec<String>),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
59
|
+
pub struct RenameConfig {
|
|
60
|
+
pub from: String,
|
|
61
|
+
pub to: String,
|
|
62
|
+
#[serde(default)]
|
|
63
|
+
pub condition: Option<String>,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
67
|
+
pub struct RemoveConfig {
|
|
68
|
+
pub path: String,
|
|
69
|
+
pub value: String,
|
|
70
|
+
#[serde(default)]
|
|
71
|
+
pub condition: Option<String>,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl<'de> Deserialize<'de> for PipelineStep {
|
|
75
|
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
76
|
+
where
|
|
77
|
+
D: serde::Deserializer<'de>,
|
|
78
|
+
{
|
|
79
|
+
let mapping = serde_yaml::Mapping::deserialize(deserializer)?;
|
|
80
|
+
|
|
81
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("get".to_string())) {
|
|
82
|
+
let config: GetConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
83
|
+
return Ok(PipelineStep::Get(config));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("sort_keys".to_string())) {
|
|
87
|
+
let config: SortKeysConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
88
|
+
return Ok(PipelineStep::SortKeys(config));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("quote_style".to_string())) {
|
|
92
|
+
let config: QuoteStyleConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
93
|
+
return Ok(PipelineStep::QuoteStyle(config));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("set".to_string())) {
|
|
97
|
+
let config: SetConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
98
|
+
return Ok(PipelineStep::Set(config));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("insert".to_string())) {
|
|
102
|
+
let config: InsertConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
103
|
+
return Ok(PipelineStep::Insert(config));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("delete".to_string())) {
|
|
107
|
+
let config: DeleteConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
108
|
+
return Ok(PipelineStep::Delete(config));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("rename".to_string())) {
|
|
112
|
+
let config: RenameConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
113
|
+
return Ok(PipelineStep::Rename(config));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("remove".to_string())) {
|
|
117
|
+
let config: RemoveConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
118
|
+
return Ok(PipelineStep::Remove(config));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if let Some(value) = mapping.get(serde_yaml::Value::String("blank_lines".to_string())) {
|
|
122
|
+
let config: BlankLinesConfig = serde_yaml::from_value(value.clone()).map_err(serde::de::Error::custom)?;
|
|
123
|
+
return Ok(PipelineStep::BlankLines(config));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Err(serde::de::Error::custom(
|
|
127
|
+
"unknown pipeline step: expected get, sort_keys, quote_style, set, insert, delete, rename, remove, or blank_lines",
|
|
128
|
+
))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
133
|
+
pub struct SortKeysConfig {
|
|
134
|
+
#[serde(default)]
|
|
135
|
+
pub path: Option<String>,
|
|
136
|
+
pub order: Vec<String>,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
140
|
+
pub struct QuoteStyleConfig {
|
|
141
|
+
#[serde(default = "default_key_style")]
|
|
142
|
+
pub key_style: String,
|
|
143
|
+
pub value_style: String,
|
|
144
|
+
#[serde(default)]
|
|
145
|
+
pub path: Option<String>,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
149
|
+
pub struct SetConfig {
|
|
150
|
+
pub path: String,
|
|
151
|
+
pub value: String,
|
|
152
|
+
#[serde(default)]
|
|
153
|
+
pub condition: Option<String>,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
157
|
+
pub struct InsertConfig {
|
|
158
|
+
pub path: String,
|
|
159
|
+
pub value: String,
|
|
160
|
+
#[serde(default)]
|
|
161
|
+
pub condition: Option<String>,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
165
|
+
pub struct DeleteConfig {
|
|
166
|
+
pub path: String,
|
|
167
|
+
#[serde(default)]
|
|
168
|
+
pub condition: Option<String>,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn default_key_style() -> String {
|
|
172
|
+
"plain".to_string()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#[derive(Debug)]
|
|
176
|
+
pub struct RuleResult {
|
|
177
|
+
pub file: String,
|
|
178
|
+
pub changed: bool,
|
|
179
|
+
pub error: Option<String>,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
impl Yerbafile {
|
|
183
|
+
pub fn load(path: impl AsRef<Path>) -> Result<Self, YerbaError> {
|
|
184
|
+
let content = fs::read_to_string(path.as_ref())?;
|
|
185
|
+
let yerbafile: Yerbafile =
|
|
186
|
+
serde_yaml::from_str(&content).map_err(|error| YerbaError::ParseError(format!("{}", error)))?;
|
|
187
|
+
Ok(yerbafile)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
pub fn find() -> Option<PathBuf> {
|
|
191
|
+
let candidates = ["Yerbafile", "Yerbafile.yml", "Yerbafile.yaml", ".yerbafile"];
|
|
192
|
+
|
|
193
|
+
let mut directory = std::env::current_dir().ok()?;
|
|
194
|
+
|
|
195
|
+
loop {
|
|
196
|
+
for candidate in &candidates {
|
|
197
|
+
let path = directory.join(candidate);
|
|
198
|
+
|
|
199
|
+
if path.exists() {
|
|
200
|
+
return Some(path);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if !directory.pop() {
|
|
205
|
+
return None;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
pub fn sort_order_for(&self, file_path: &str, dot_path: &str) -> Option<Vec<String>> {
|
|
211
|
+
for rule in &self.rules {
|
|
212
|
+
let sort_keys_config = rule.pipeline.iter().find_map(|step| match step {
|
|
213
|
+
PipelineStep::SortKeys(config) => Some(config),
|
|
214
|
+
_ => None,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if let Some(config) = sort_keys_config {
|
|
218
|
+
let full_path = resolve_step_path(rule.path.as_deref(), config.path.as_deref());
|
|
219
|
+
let normalized = full_path.trim_end_matches("[]").trim_end_matches('.');
|
|
220
|
+
|
|
221
|
+
if normalized != dot_path && !normalized.is_empty() && !dot_path.is_empty() {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if let Ok(pattern) = glob::Pattern::new(&rule.files) {
|
|
226
|
+
if pattern.matches(file_path) || pattern.matches_path(Path::new(file_path)) {
|
|
227
|
+
return Some(config.order.clone());
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
None
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
pub fn apply(&self, write: bool) -> Vec<RuleResult> {
|
|
237
|
+
let mut results = Vec::new();
|
|
238
|
+
|
|
239
|
+
for rule in &self.rules {
|
|
240
|
+
let files = match glob::glob(&rule.files) {
|
|
241
|
+
Ok(paths) => paths.filter_map(|entry| entry.ok()).collect::<Vec<_>>(),
|
|
242
|
+
|
|
243
|
+
Err(error) => {
|
|
244
|
+
results.push(RuleResult {
|
|
245
|
+
file: rule.files.clone(),
|
|
246
|
+
changed: false,
|
|
247
|
+
error: Some(format!("invalid glob: {}", error)),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
let file_strings: Vec<String> = files.iter().map(|path| path.to_string_lossy().to_string()).collect();
|
|
255
|
+
|
|
256
|
+
let mut has_validation_error = false;
|
|
257
|
+
|
|
258
|
+
for step in &rule.pipeline {
|
|
259
|
+
if let PipelineStep::SortKeys(config) = step {
|
|
260
|
+
let full_path = resolve_step_path(rule.path.as_deref(), config.path.as_deref());
|
|
261
|
+
let key_order: Vec<&str> = config.order.iter().map(|key| key.as_str()).collect();
|
|
262
|
+
|
|
263
|
+
let validation_results: Vec<RuleResult> = file_strings
|
|
264
|
+
.par_iter()
|
|
265
|
+
.filter_map(|file| {
|
|
266
|
+
let document = match Document::parse_file(file) {
|
|
267
|
+
Ok(document) => document,
|
|
268
|
+
Err(error) => {
|
|
269
|
+
return Some(RuleResult {
|
|
270
|
+
file: file.clone(),
|
|
271
|
+
changed: false,
|
|
272
|
+
error: Some(format!("{}", error)),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if let Err(error) = document.validate_sort_keys(&full_path, &key_order) {
|
|
278
|
+
Some(RuleResult {
|
|
279
|
+
file: file.clone(),
|
|
280
|
+
changed: false,
|
|
281
|
+
error: Some(format!("{}", error)),
|
|
282
|
+
})
|
|
283
|
+
} else {
|
|
284
|
+
None
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
.collect();
|
|
288
|
+
|
|
289
|
+
if !validation_results.is_empty() {
|
|
290
|
+
has_validation_error = true;
|
|
291
|
+
results.extend(validation_results);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if has_validation_error {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
let file_results: Vec<RuleResult> = file_strings
|
|
301
|
+
.par_iter()
|
|
302
|
+
.map(|file| self.apply_pipeline_to_file(rule, file, write))
|
|
303
|
+
.collect();
|
|
304
|
+
|
|
305
|
+
results.extend(file_results);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
results
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
fn apply_pipeline_to_file(&self, rule: &Rule, file: &str, write: bool) -> RuleResult {
|
|
312
|
+
let mut document = match Document::parse_file(file) {
|
|
313
|
+
Ok(document) => document,
|
|
314
|
+
Err(error) => {
|
|
315
|
+
return RuleResult {
|
|
316
|
+
file: file.to_string(),
|
|
317
|
+
changed: false,
|
|
318
|
+
error: Some(format!("{}", error)),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
let original = document.to_string();
|
|
324
|
+
let base_path = rule.path.as_deref();
|
|
325
|
+
let mut variables: HashMap<String, Variable> = HashMap::new();
|
|
326
|
+
|
|
327
|
+
for step in &rule.pipeline {
|
|
328
|
+
if let Err(error) = execute_step(&mut document, step, base_path, &mut variables) {
|
|
329
|
+
return RuleResult {
|
|
330
|
+
file: file.to_string(),
|
|
331
|
+
changed: false,
|
|
332
|
+
error: Some(format!("{}", error)),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let new_content = document.to_string();
|
|
338
|
+
let changed = new_content != original;
|
|
339
|
+
|
|
340
|
+
if changed && write {
|
|
341
|
+
if let Err(error) = fs::write(file, &new_content) {
|
|
342
|
+
return RuleResult {
|
|
343
|
+
file: file.to_string(),
|
|
344
|
+
changed,
|
|
345
|
+
error: Some(format!("{}", error)),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
RuleResult {
|
|
351
|
+
file: file.to_string(),
|
|
352
|
+
changed,
|
|
353
|
+
error: None,
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
fn execute_step(
|
|
359
|
+
document: &mut Document,
|
|
360
|
+
step: &PipelineStep,
|
|
361
|
+
base_path: Option<&str>,
|
|
362
|
+
variables: &mut HashMap<String, Variable>,
|
|
363
|
+
) -> Result<(), YerbaError> {
|
|
364
|
+
match step {
|
|
365
|
+
PipelineStep::Get(config) => {
|
|
366
|
+
let full_path = resolve_step_path(base_path, Some(&config.path));
|
|
367
|
+
|
|
368
|
+
if let Some(file_pattern) = &config.file {
|
|
369
|
+
let mut all_values = Vec::new();
|
|
370
|
+
|
|
371
|
+
let files =
|
|
372
|
+
glob::glob(file_pattern).map_err(|error| YerbaError::ParseError(format!("invalid glob: {}", error)))?;
|
|
373
|
+
|
|
374
|
+
for entry in files.flatten() {
|
|
375
|
+
let external_document = Document::parse_file(&entry)?;
|
|
376
|
+
all_values.extend(external_document.get_all(&config.path));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if all_values.len() == 1 && !config.path.contains('[') {
|
|
380
|
+
variables.insert(config.as_name.clone(), Variable::Single(all_values.remove(0)));
|
|
381
|
+
} else {
|
|
382
|
+
variables.insert(config.as_name.clone(), Variable::List(all_values));
|
|
383
|
+
}
|
|
384
|
+
} else if config.path.contains('[') {
|
|
385
|
+
let values = document.get_all(&full_path);
|
|
386
|
+
|
|
387
|
+
variables.insert(config.as_name.clone(), Variable::List(values));
|
|
388
|
+
} else {
|
|
389
|
+
let value = document
|
|
390
|
+
.get(&full_path)
|
|
391
|
+
.ok_or_else(|| YerbaError::PathNotFound(full_path.clone()))?;
|
|
392
|
+
variables.insert(config.as_name.clone(), Variable::Single(value));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
Ok(())
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
PipelineStep::QuoteStyle(config) => {
|
|
399
|
+
let dot_path = config.path.as_deref();
|
|
400
|
+
|
|
401
|
+
let key_style = config.key_style.parse::<QuoteStyle>().map_err(YerbaError::ParseError)?;
|
|
402
|
+
|
|
403
|
+
let value_style = config
|
|
404
|
+
.value_style
|
|
405
|
+
.parse::<QuoteStyle>()
|
|
406
|
+
.map_err(YerbaError::ParseError)?;
|
|
407
|
+
|
|
408
|
+
document.enforce_key_style(&key_style, dot_path)?;
|
|
409
|
+
document.enforce_quotes_at(&value_style, dot_path)?;
|
|
410
|
+
|
|
411
|
+
Ok(())
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
PipelineStep::SortKeys(config) => {
|
|
415
|
+
let full_path = resolve_step_path(base_path, config.path.as_deref());
|
|
416
|
+
let key_order: Vec<&str> = config.order.iter().map(|key| key.as_str()).collect();
|
|
417
|
+
|
|
418
|
+
document.sort_keys(&full_path, &key_order)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
PipelineStep::Set(config) => {
|
|
422
|
+
let full_path = resolve_step_path(base_path, Some(&config.path));
|
|
423
|
+
let resolved_value = resolve_template(&config.value, document, base_path, variables)?;
|
|
424
|
+
|
|
425
|
+
if let Some(condition) = &config.condition {
|
|
426
|
+
let resolved_condition = resolve_template(condition, document, base_path, variables)?;
|
|
427
|
+
let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
428
|
+
|
|
429
|
+
if !document.evaluate_condition(parent_path, &resolved_condition) {
|
|
430
|
+
return Ok(());
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
document.set(&full_path, &resolved_value)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
PipelineStep::Insert(config) => {
|
|
438
|
+
let full_path = resolve_step_path(base_path, Some(&config.path));
|
|
439
|
+
let resolved_value = resolve_template(&config.value, document, base_path, variables)?;
|
|
440
|
+
|
|
441
|
+
if let Some(condition) = &config.condition {
|
|
442
|
+
let resolved_condition = resolve_template(condition, document, base_path, variables)?;
|
|
443
|
+
let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
444
|
+
|
|
445
|
+
if !document.evaluate_condition(parent_path, &resolved_condition) {
|
|
446
|
+
return Ok(());
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
document.insert_into(&full_path, &resolved_value, crate::InsertPosition::Last)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
PipelineStep::Delete(config) => {
|
|
454
|
+
let full_path = resolve_step_path(base_path, Some(&config.path));
|
|
455
|
+
|
|
456
|
+
if let Some(condition) = &config.condition {
|
|
457
|
+
let resolved_condition = resolve_template(condition, document, base_path, variables)?;
|
|
458
|
+
let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
459
|
+
|
|
460
|
+
if !document.evaluate_condition(parent_path, &resolved_condition) {
|
|
461
|
+
return Ok(());
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
document.delete(&full_path)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
PipelineStep::Rename(config) => {
|
|
469
|
+
let full_path = resolve_step_path(base_path, Some(&config.from));
|
|
470
|
+
|
|
471
|
+
if let Some(condition) = &config.condition {
|
|
472
|
+
let resolved_condition = resolve_template(condition, document, base_path, variables)?;
|
|
473
|
+
let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
474
|
+
|
|
475
|
+
if !document.evaluate_condition(parent_path, &resolved_condition) {
|
|
476
|
+
return Ok(());
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
document.rename(&full_path, &config.to)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
PipelineStep::Remove(config) => {
|
|
484
|
+
let full_path = resolve_step_path(base_path, Some(&config.path));
|
|
485
|
+
let resolved_value = resolve_template(&config.value, document, base_path, variables)?;
|
|
486
|
+
|
|
487
|
+
if let Some(condition) = &config.condition {
|
|
488
|
+
let resolved_condition = resolve_template(condition, document, base_path, variables)?;
|
|
489
|
+
let parent_path = full_path.rsplit_once('.').map(|(parent, _)| parent).unwrap_or("");
|
|
490
|
+
|
|
491
|
+
if !document.evaluate_condition(parent_path, &resolved_condition) {
|
|
492
|
+
return Ok(());
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
document.remove(&full_path, &resolved_value)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
PipelineStep::BlankLines(config) => {
|
|
500
|
+
let full_path = resolve_step_path(base_path, config.path.as_deref());
|
|
501
|
+
|
|
502
|
+
document.enforce_blank_lines(&full_path, config.count)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
pub fn resolve_template(
|
|
508
|
+
template: &str,
|
|
509
|
+
document: &Document,
|
|
510
|
+
base_path: Option<&str>,
|
|
511
|
+
variables: &HashMap<String, Variable>,
|
|
512
|
+
) -> Result<String, YerbaError> {
|
|
513
|
+
if !template.contains("${") {
|
|
514
|
+
return Ok(template.to_string());
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
let mut result = String::new();
|
|
518
|
+
let mut rest = template;
|
|
519
|
+
|
|
520
|
+
while let Some(start) = rest.find("${") {
|
|
521
|
+
result.push_str(&rest[..start]);
|
|
522
|
+
|
|
523
|
+
let after_dollar = &rest[start + 2..];
|
|
524
|
+
|
|
525
|
+
let end = after_dollar
|
|
526
|
+
.find('}')
|
|
527
|
+
.ok_or_else(|| YerbaError::ParseError("unclosed ${ in template".to_string()))?;
|
|
528
|
+
|
|
529
|
+
let reference = &after_dollar[..end];
|
|
530
|
+
|
|
531
|
+
let resolved = if let Some(variable) = variables.get(reference) {
|
|
532
|
+
match variable {
|
|
533
|
+
Variable::Single(value) => value.clone(),
|
|
534
|
+
Variable::List(values) => values.join(", "),
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
let full_path = resolve_step_path(base_path, Some(reference));
|
|
538
|
+
|
|
539
|
+
document
|
|
540
|
+
.get(&full_path)
|
|
541
|
+
.ok_or_else(|| YerbaError::ReferenceNotFound(reference.to_string()))?
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
result.push_str(&resolved);
|
|
545
|
+
rest = &after_dollar[end + 1..];
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
result.push_str(rest);
|
|
549
|
+
|
|
550
|
+
Ok(result)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
fn resolve_step_path(base_path: Option<&str>, step_path: Option<&str>) -> String {
|
|
554
|
+
let base = base_path.unwrap_or("");
|
|
555
|
+
let step = step_path.unwrap_or("");
|
|
556
|
+
|
|
557
|
+
match (base.is_empty(), step.is_empty()) {
|
|
558
|
+
(true, true) => String::new(),
|
|
559
|
+
(true, false) => step.to_string(),
|
|
560
|
+
(false, true) => base.to_string(),
|
|
561
|
+
(false, false) => format!("{}.{}", base, step),
|
|
562
|
+
}
|
|
563
|
+
}
|
data/yerba.gemspec
CHANGED
|
@@ -23,15 +23,20 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
spec.files = Dir[
|
|
24
24
|
"yerba.gemspec",
|
|
25
25
|
"LICENSE.txt",
|
|
26
|
-
"Rakefile",
|
|
27
26
|
"README.md",
|
|
28
27
|
"lib/**/*.rb",
|
|
29
28
|
"sig/**/*.rbs",
|
|
30
|
-
"exe
|
|
29
|
+
"exe/yerba",
|
|
30
|
+
"ext/yerba/extconf.rb",
|
|
31
|
+
"rust/Cargo.toml",
|
|
32
|
+
"rust/Cargo.lock",
|
|
33
|
+
"rust/src/**/*.rs",
|
|
34
|
+
"rust/rustfmt.toml"
|
|
31
35
|
]
|
|
32
36
|
|
|
33
37
|
spec.bindir = "exe"
|
|
34
|
-
spec.executables =
|
|
38
|
+
spec.executables = ["yerba"]
|
|
39
|
+
spec.extensions = ["ext/yerba/extconf.rb"]
|
|
35
40
|
|
|
36
41
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
37
42
|
spec.metadata["rubygems_mfa_required"] = "true"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yerba
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Marco Roth
|
|
@@ -15,15 +15,27 @@ email:
|
|
|
15
15
|
- marco.roth@intergga.ch
|
|
16
16
|
executables:
|
|
17
17
|
- yerba
|
|
18
|
-
extensions:
|
|
18
|
+
extensions:
|
|
19
|
+
- ext/yerba/extconf.rb
|
|
19
20
|
extra_rdoc_files: []
|
|
20
21
|
files:
|
|
21
22
|
- LICENSE.txt
|
|
22
23
|
- README.md
|
|
23
|
-
- Rakefile
|
|
24
24
|
- exe/yerba
|
|
25
|
+
- ext/yerba/extconf.rb
|
|
25
26
|
- lib/yerba.rb
|
|
26
27
|
- lib/yerba/version.rb
|
|
28
|
+
- rust/Cargo.lock
|
|
29
|
+
- rust/Cargo.toml
|
|
30
|
+
- rust/rustfmt.toml
|
|
31
|
+
- rust/src/document.rs
|
|
32
|
+
- rust/src/error.rs
|
|
33
|
+
- rust/src/json.rs
|
|
34
|
+
- rust/src/lib.rs
|
|
35
|
+
- rust/src/main.rs
|
|
36
|
+
- rust/src/quote_style.rs
|
|
37
|
+
- rust/src/syntax.rs
|
|
38
|
+
- rust/src/yerbafile.rs
|
|
27
39
|
- sig/yerba.rbs
|
|
28
40
|
- yerba.gemspec
|
|
29
41
|
homepage: https://github.com/marcoroth/yerba
|
data/Rakefile
DELETED