|
| 1 | +# Plan 47: Enhanced Tera Template Context with Version Parsing |
| 2 | + |
| 3 | +**Status**: Completed |
| 4 | +**Priority**: Medium |
| 5 | +**Context**: Users need more granular access to version components in Tera templates, particularly for SemVer and PEP440 format parsing |
| 6 | +**Goals**: Provide structured access to version parts (base, pre-release, build) and add PEP440 label parsing |
| 7 | + |
| 8 | +## Implementation Plan |
| 9 | + |
| 10 | +### 1. Enhance Template Context Structures |
| 11 | + |
| 12 | +**File**: `src/cli/utils/template/context.rs` |
| 13 | + |
| 14 | +**Add import at top of file:** |
| 15 | + |
| 16 | +```rust |
| 17 | +use crate::version::pep440::PEP440; |
| 18 | +use crate::version::pep440::utils::pre_release_label_to_pep440_string; |
| 19 | +use crate::version::semver::SemVer; |
| 20 | +use crate::version::zerv::Zerv; |
| 21 | +``` |
| 22 | + |
| 23 | +**Update PreReleaseContext struct** - add PEP440 label using existing function: |
| 24 | + |
| 25 | +```rust |
| 26 | +#[derive(Debug, Clone, PartialEq, serde::Serialize)] |
| 27 | +pub struct PreReleaseContext { |
| 28 | + pub label: String, // Current: "alpha", "beta", etc. |
| 29 | + pub number: Option<u64>, |
| 30 | + pub pep440_label: Option<String>, // PEP440 format: "rc", "a", "b", etc. |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +**Add new fields to ZervTemplateContext struct**: |
| 35 | + |
| 36 | +```rust |
| 37 | +// SemVer parsed components |
| 38 | +pub semver_base_part: String, |
| 39 | +pub semver_pre_release_part: Option<String>, // None if no pre-release |
| 40 | +pub semver_build_part: Option<String>, // None if no build metadata |
| 41 | +pub semver_docker: String, |
| 42 | + |
| 43 | +// PEP440 parsed components |
| 44 | +pub pep440_base_part: String, |
| 45 | +pub pep440_pre_release_part: Option<String>, // None if no pre-release |
| 46 | +pub pep440_build_part: Option<String>, // None if no build metadata |
| 47 | +``` |
| 48 | + |
| 49 | +### 2. Add Conditional Prefix Function to Tera |
| 50 | + |
| 51 | +**Add new function to template functions** - File: `src/cli/utils/template/functions.rs` |
| 52 | + |
| 53 | +```rust |
| 54 | +/// Add conditional prefix to string (only if string is not empty) |
| 55 | +/// Usage: {{ prefix_if(value, prefix="+") }} |
| 56 | +fn prefix_if_function(args: &std::collections::HashMap<String, Value>) -> Result<Value, tera::Error> { |
| 57 | + let value = args |
| 58 | + .get("value") |
| 59 | + .and_then(|v| v.as_str()) |
| 60 | + .ok_or_else(|| tera::Error::msg("prefix_if function requires a 'value' parameter"))?; |
| 61 | + |
| 62 | + let prefix = args |
| 63 | + .get("prefix") |
| 64 | + .and_then(|v| v.as_str()) |
| 65 | + .ok_or_else(|| tera::Error::msg("prefix_if function requires a 'prefix' parameter"))?; |
| 66 | + |
| 67 | + if value.is_empty() { |
| 68 | + Ok(Value::String("".to_string())) |
| 69 | + } else { |
| 70 | + Ok(Value::String(format!("{}{}", prefix, value))) |
| 71 | + } |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +**Update register_functions() to include the new function:** |
| 76 | + |
| 77 | +```rust |
| 78 | +pub fn register_functions(tera: &mut Tera) -> Result<(), ZervError> { |
| 79 | + tera.register_function("sanitize", Box::new(sanitize_function)); |
| 80 | + tera.register_function("hash", Box::new(hash_function)); |
| 81 | + tera.register_function("hash_int", Box::new(hash_int_function)); |
| 82 | + tera.register_function("prefix", Box::new(prefix_function)); |
| 83 | + tera.register_function("prefix_if", Box::new(prefix_if_function)); // NEW |
| 84 | + tera.register_function("format_timestamp", Box::new(format_timestamp_function)); |
| 85 | + Ok(()) |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +### 3. Add Methods to Version Classes |
| 90 | + |
| 91 | +#### Add methods to SemVer class |
| 92 | + |
| 93 | +**File**: `src/version/semver.rs` |
| 94 | + |
| 95 | +```rust |
| 96 | +impl SemVer { |
| 97 | + pub fn to_base_part(&self) -> String { |
| 98 | + format!("{}.{}.{}", self.major, self.minor, self.patch) |
| 99 | + } |
| 100 | + |
| 101 | + pub fn to_pre_release_part(&self) -> Option<String> { |
| 102 | + self.pre_release.as_ref().map(|pr| pr.to_string()) |
| 103 | + } |
| 104 | + |
| 105 | + pub fn to_build_part(&self) -> Option<String> { |
| 106 | + self.build.as_ref().map(|b| b.to_string()) |
| 107 | + } |
| 108 | + |
| 109 | + pub fn to_docker_format(&self) -> String { |
| 110 | + let mut parts = vec![self.to_base_part()]; |
| 111 | + if let Some(pre) = self.to_pre_release_part() { |
| 112 | + parts.push(pre); |
| 113 | + } |
| 114 | + if let Some(build) = self.to_build_part() { |
| 115 | + parts.push(build); |
| 116 | + } |
| 117 | + parts.join("-") |
| 118 | + } |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +#### Add methods to PEP440 class |
| 123 | + |
| 124 | +**File**: `src/version/pep440/mod.rs` or appropriate file |
| 125 | + |
| 126 | +```rust |
| 127 | +impl PEP440 { |
| 128 | + pub fn to_base_part(&self) -> String { |
| 129 | + // Return base version including epoch if present |
| 130 | + // Implementation depends on PEP440 structure |
| 131 | + } |
| 132 | + |
| 133 | + pub fn to_pre_release_part(&self) -> Option<String> { |
| 134 | + // Return pre-release components |
| 135 | + // Implementation depends on PEP440 structure |
| 136 | + } |
| 137 | + |
| 138 | + pub fn to_build_part(&self) -> Option<String> { |
| 139 | + // Return build metadata |
| 140 | + // Implementation depends on PEP440 structure |
| 141 | + } |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +### 4. Update ZervTemplateContext::from_zerv() |
| 146 | + |
| 147 | +Modify the `from_zerv()` method to use the new methods: |
| 148 | + |
| 149 | +```rust |
| 150 | +impl ZervTemplateContext { |
| 151 | + pub fn from_zerv(zerv: &Zerv) -> Self { |
| 152 | + let vars = &zerv.vars; |
| 153 | + |
| 154 | + let semver = SemVer::from(zerv.clone()); |
| 155 | + let pep440 = PEP440::from(zerv.clone()); |
| 156 | + |
| 157 | + Self { |
| 158 | + // ... existing fields ... |
| 159 | + pre_release: vars.pre_release.as_ref().map(|pr| PreReleaseContext { |
| 160 | + label: pr.label.label_str().to_string(), |
| 161 | + number: pr.number, |
| 162 | + pep440_label: Some(pre_release_label_to_pep440_string(&pr.label).to_string()), |
| 163 | + }), |
| 164 | + semver_base_part: semver.to_base_part(), |
| 165 | + semver_pre_release_part: semver.to_pre_release_part(), |
| 166 | + semver_build_part: semver.to_build_part(), |
| 167 | + semver_docker: semver.to_docker_format(), |
| 168 | + pep440_base_part: pep440.to_base_part(), |
| 169 | + pep440_pre_release_part: pep440.to_pre_release_part(), |
| 170 | + pep440_build_part: pep440.to_build_part(), |
| 171 | + } |
| 172 | + } |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +### 5. Add Comprehensive Tests |
| 177 | + |
| 178 | +Add test cases covering: |
| 179 | + |
| 180 | +- Basic SemVer parsing: `1.2.3` |
| 181 | +- SemVer with pre-release: `1.2.3-alpha.1` |
| 182 | +- SemVer with build: `1.2.3+build.456` |
| 183 | +- SemVer with both: `1.2.3-alpha.1+build.456` |
| 184 | +- Basic PEP440: `1.2.3` |
| 185 | +- PEP440 with rc: `1.2.3rc1` |
| 186 | +- PEP440 with dev: `1.2.3dev1` |
| 187 | +- PEP440 with post: `1.2.3.post1` |
| 188 | +- Complex PEP440: `1.2.3rc1.post2.dev3+build.456` |
| 189 | + |
| 190 | +### 5. Integration with Existing Version Classes |
| 191 | + |
| 192 | +Ensure compatibility with: |
| 193 | + |
| 194 | +- `SemVer` class in `src/version/semver.rs` |
| 195 | +- `PEP440` class in `src/version/pep440.rs` |
| 196 | +- `Zerv` class in `src/version/zerv.rs` |
| 197 | + |
| 198 | +## Testing Strategy |
| 199 | + |
| 200 | +### Unit Tests |
| 201 | + |
| 202 | +- Test each parsing function with various version formats |
| 203 | +- Test edge cases (empty strings, malformed versions) |
| 204 | +- Test integration with existing version classes |
| 205 | + |
| 206 | +### Integration Tests |
| 207 | + |
| 208 | +- Test template rendering with new context fields |
| 209 | +- Verify Docker tag format generation |
| 210 | +- Test with real Zerv versions from fixtures |
| 211 | + |
| 212 | +## Success Criteria |
| 213 | + |
| 214 | +1. ✅ All new template context fields are properly populated |
| 215 | +2. ✅ SemVer parsing correctly separates base, pre-release, and build parts |
| 216 | +3. ✅ PEP440 parsing correctly extracts labels (rc, a, b) and components |
| 217 | +4. ✅ Docker format follows `base-pre-release-build` pattern |
| 218 | +5. ✅ All existing tests pass without modification |
| 219 | +6. ✅ New comprehensive test coverage for parsing functions |
| 220 | +7. ✅ Template examples work correctly with new fields |
| 221 | + |
| 222 | +## Documentation Updates |
| 223 | + |
| 224 | +- Update template documentation with new field examples |
| 225 | +- Add usage examples for Docker tag generation |
| 226 | +- Document PEP440 label mapping |
| 227 | + |
| 228 | +## Example Template Usage |
| 229 | + |
| 230 | +After implementation, templates will be able to use: |
| 231 | + |
| 232 | +```tera |
| 233 | +# SemVer components |
| 234 | +semver: {{ semver }} |
| 235 | +base: {{ semver_base_part }} |
| 236 | +pre: {{ semver_pre_release_part }} |
| 237 | +build: {{ semver_build_part }} |
| 238 | +docker: {{ semver_docker }} |
| 239 | +
|
| 240 | +# PEP440 components |
| 241 | +pep440: {{ pep440 }} |
| 242 | +base: {{ pep440_base_part }} |
| 243 | +pre: {{ pep440_pre_release_part }} |
| 244 | +build: {{ pep440_build_part }} |
| 245 | +
|
| 246 | +# Pre-release context (access both label formats) |
| 247 | +{% if pre_release %} |
| 248 | +label: {{ pre_release.label }} # "alpha", "beta", etc. |
| 249 | +pep440_label: {{ pre_release.pep440_label }} # "a", "b", "rc", etc. |
| 250 | +number: {{ pre_release.number }} |
| 251 | +{% endif %} |
| 252 | +
|
| 253 | +# Conditional prefix usage (NEW) |
| 254 | +# semver_build_part might be "build.456" or "" |
| 255 | +build_with_prefix: {{ prefix_if(semver_build_part, prefix="+") }} # "+build.456" or "" |
| 256 | +pre_with_prefix: {{ prefix_if(semver_pre_release_part, prefix="-") }} # "-alpha.1" or "" |
| 257 | +``` |
| 258 | + |
| 259 | +## Implementation Notes |
| 260 | + |
| 261 | +- **COMPLETED**: All features implemented successfully |
| 262 | +- **COMPLETED**: All tests passing (2,334 tests) |
| 263 | +- **COMPLETED**: Template context enhanced with new fields |
| 264 | +- **COMPLETED**: prefix_if function added and tested |
| 265 | +- **COMPLETED**: SemVer and PEP440 parsing methods implemented |
| 266 | +- **COMPLETED**: Comprehensive test coverage added |
| 267 | +- **COMPLETED**: Backward compatibility maintained |
0 commit comments