|
1 | | -//! Python's `_validate_unique_project_resource_names`: one namespace across resource kinds. |
| 1 | +//! One namespace across resource kinds, orchestration declarations included. |
2 | 2 |
|
3 | 3 | use std::collections::{HashMap, HashSet}; |
4 | 4 |
|
5 | 5 | use crate::project_validation::_helpers::conflicts::{conflict, schema_seeds}; |
6 | 6 | use crate::project_validation::_helpers::identities::block_or_file_name; |
7 | | -use crate::project_validation::models::{NodeKind, ProjectFacts, ValidationFailure}; |
| 7 | +use crate::project_validation::constants::ORCHESTRATION_RENAME_ADVICE; |
| 8 | +use crate::project_validation::models::{ |
| 9 | + DeclaredName, NamedPath, NodeKind, ProjectFacts, ValidationFailure, |
| 10 | +}; |
8 | 11 |
|
9 | 12 | /// Reject the first resource name declared twice across models, sources, seeds and the rest. |
10 | 13 | pub(crate) fn validate_unique_project_resource_names( |
11 | 14 | facts: &ProjectFacts, |
12 | 15 | ) -> Result<(), ValidationFailure> { |
13 | | - let mut seen: HashMap<String, (String, String)> = HashMap::new(); |
14 | | - let mut declare = |name: &str, kind: &str, path: &str| -> Result<(), ValidationFailure> { |
15 | | - if let Some((existing_kind, existing_path)) = seen.get(name) { |
16 | | - return Err(conflict(format!( |
17 | | - "Project resource name '{name}' is declared as both {existing_kind} in \ |
18 | | - {existing_path} and {kind} in {path}; model, source, seed, function, loader, \ |
19 | | - task, asset, check, provider, hook, unit test, and scenario names must be \ |
20 | | - globally unique" |
21 | | - ))); |
| 16 | + let seeds: Vec<NamedPath> = schema_seeds(facts); |
| 17 | + let mut seen: HashMap<&str, DeclaredName<'_>> = HashMap::new(); |
| 18 | + for declared in declarations(facts, &seeds) { |
| 19 | + if let Some(first) = seen.get(declared.name) { |
| 20 | + return Err(name_conflict(first, &declared)); |
22 | 21 | } |
23 | | - seen.insert(name.to_owned(), (kind.to_owned(), path.to_owned())); |
24 | | - Ok(()) |
25 | | - }; |
| 22 | + seen.insert(declared.name, declared); |
| 23 | + } |
| 24 | + Ok(()) |
| 25 | +} |
| 26 | + |
| 27 | +/// Every declared name in the order conflicts are reported. |
| 28 | +fn declarations<'a>(facts: &'a ProjectFacts, seeds: &'a [NamedPath]) -> Vec<DeclaredName<'a>> { |
| 29 | + let mut declared: Vec<DeclaredName<'a>> = Vec::new(); |
26 | 30 | for model in &facts.models { |
27 | | - declare(&model.file.name, "model", &model.file.path)?; |
| 31 | + declared.push(DeclaredName { |
| 32 | + name: &model.file.name, |
| 33 | + kind: "model", |
| 34 | + path: &model.file.path, |
| 35 | + orchestration: false, |
| 36 | + }); |
28 | 37 | } |
29 | 38 | let mut managed_sources: HashSet<&str> = HashSet::new(); |
30 | 39 | for file in &facts.sources { |
31 | 40 | for entry in &file.entries { |
32 | 41 | if entry.managed { |
33 | 42 | managed_sources.insert(&entry.name); |
34 | 43 | } |
35 | | - declare(&entry.name, "source", &file.path)?; |
| 44 | + declared.push(DeclaredName { |
| 45 | + name: &entry.name, |
| 46 | + kind: "source", |
| 47 | + path: &file.path, |
| 48 | + orchestration: false, |
| 49 | + }); |
36 | 50 | } |
37 | 51 | } |
38 | | - for seed in schema_seeds(facts) { |
39 | | - declare(&seed.name, "seed", &seed.path)?; |
| 52 | + for seed in seeds { |
| 53 | + declared.push(DeclaredName { |
| 54 | + name: &seed.name, |
| 55 | + kind: "seed", |
| 56 | + path: &seed.path, |
| 57 | + orchestration: false, |
| 58 | + }); |
40 | 59 | } |
41 | 60 | for function in facts.sql_functions.iter().chain(&facts.python_functions) { |
42 | | - declare(&function.name, "function", &function.path)?; |
| 61 | + declared.push(DeclaredName { |
| 62 | + name: &function.name, |
| 63 | + kind: "function", |
| 64 | + path: &function.path, |
| 65 | + orchestration: false, |
| 66 | + }); |
43 | 67 | } |
44 | 68 | for node in &facts.nodes { |
45 | 69 | if node.kind == NodeKind::Loader && managed_sources.contains(node.name.as_str()) { |
46 | 70 | continue; |
47 | 71 | } |
48 | | - declare(&node.name, node.kind.label(), &node.path)?; |
| 72 | + declared.push(DeclaredName { |
| 73 | + name: &node.name, |
| 74 | + kind: node.kind.label(), |
| 75 | + path: &node.path, |
| 76 | + orchestration: false, |
| 77 | + }); |
49 | 78 | } |
50 | 79 | for provider in &facts.providers { |
51 | | - declare(&provider.name, "provider", &provider.path)?; |
| 80 | + declared.push(DeclaredName { |
| 81 | + name: &provider.name, |
| 82 | + kind: "provider", |
| 83 | + path: &provider.path, |
| 84 | + orchestration: false, |
| 85 | + }); |
52 | 86 | } |
53 | 87 | for hook in &facts.sql_hooks { |
54 | | - declare(&hook.name, "sql hook", &hook.path)?; |
| 88 | + declared.push(DeclaredName { |
| 89 | + name: &hook.name, |
| 90 | + kind: "sql hook", |
| 91 | + path: &hook.path, |
| 92 | + orchestration: false, |
| 93 | + }); |
55 | 94 | } |
56 | 95 | for hook in &facts.hook_functions { |
57 | | - declare(&hook.name, "python hook", &hook.path)?; |
| 96 | + declared.push(DeclaredName { |
| 97 | + name: &hook.name, |
| 98 | + kind: "python hook", |
| 99 | + path: &hook.path, |
| 100 | + orchestration: false, |
| 101 | + }); |
58 | 102 | } |
59 | 103 | for scenario in &facts.scenarios { |
60 | | - declare(&scenario.name, "scenario", &scenario.path)?; |
| 104 | + declared.push(DeclaredName { |
| 105 | + name: &scenario.name, |
| 106 | + kind: "scenario", |
| 107 | + path: &scenario.path, |
| 108 | + orchestration: false, |
| 109 | + }); |
61 | 110 | } |
62 | 111 | let mut test_names: HashSet<&str> = HashSet::new(); |
63 | 112 | for test in &facts.tests { |
64 | 113 | for block in &test.blocks { |
65 | 114 | let name: &str = block_or_file_name(block.name.as_deref(), &test.file.name); |
66 | | - if !test_names.insert(name) { |
67 | | - continue; |
| 115 | + if test_names.insert(name) { |
| 116 | + declared.push(DeclaredName { |
| 117 | + name, |
| 118 | + kind: "unit test", |
| 119 | + path: &test.file.path, |
| 120 | + orchestration: false, |
| 121 | + }); |
68 | 122 | } |
69 | | - declare(name, "unit test", &test.file.path)?; |
70 | 123 | } |
71 | 124 | } |
72 | | - Ok(()) |
| 125 | + for declaration in &facts.orchestration { |
| 126 | + declared.push(DeclaredName { |
| 127 | + name: &declaration.name, |
| 128 | + kind: declaration.kind, |
| 129 | + path: &declaration.path, |
| 130 | + orchestration: true, |
| 131 | + }); |
| 132 | + } |
| 133 | + declared |
| 134 | +} |
| 135 | + |
| 136 | +/// Name both declarations and say how to rename one of them. |
| 137 | +fn name_conflict(first: &DeclaredName<'_>, second: &DeclaredName<'_>) -> ValidationFailure { |
| 138 | + let advice: &str = if first.orchestration || second.orchestration { |
| 139 | + ORCHESTRATION_RENAME_ADVICE |
| 140 | + } else { |
| 141 | + "" |
| 142 | + }; |
| 143 | + conflict(format!( |
| 144 | + "Project resource name '{}' is declared as both {} in {} and {} in {}; model, source, seed, function, loader, task, asset, check, provider, \ |
| 145 | + hook, unit test, scenario, job, job group, sensor, and calendar schedule names must be \ |
| 146 | + globally unique; rename one of them{advice}", |
| 147 | + first.name, first.kind, first.path, second.kind, second.path |
| 148 | + )) |
73 | 149 | } |
0 commit comments