6.0 KiB
6.0 KiB
AppData Refactoring Plan
Overview
Refactor the AppData class to improve elegance, maintainability, and extensibility while maintaining 100% backward compatibility.
Current Issues
- 100+ lines of nested switch statements in processEnvMapping
- Static helpers recreate Qenv instances on every call
- Complex boolean conversion logic scattered across multiple places
- Typo: "ephermal" should be "ephemeral"
- Difficult to test and extend with new transformations
Architecture Improvements
1. Singleton Qenv Provider
Create a shared Qenv instance to avoid repeated instantiation:
let sharedQenv: plugins.qenv.Qenv | undefined;
function getQenv(): plugins.qenv.Qenv {
if (!sharedQenv) {
sharedQenv = new plugins.qenv.Qenv(
process.cwd(),
plugins.path.join(process.cwd(), '.nogit')
);
}
return sharedQenv;
}
2. Centralized Type Converters
Extract all conversion logic into pure utility functions:
function toBoolean(value: unknown): boolean {
if (typeof value === 'boolean') return value;
if (value == null) return false;
const s = String(value).toLowerCase();
return s === 'true';
}
function toJson<T>(value: unknown): T | undefined {
if (typeof value === 'string') {
try {
return JSON.parse(value);
} catch {
return undefined;
}
}
return value as T;
}
function fromBase64(value: unknown): string {
if (value == null) return '';
return Buffer.from(String(value), 'base64').toString('utf8');
}
function toNumber(value: unknown): number | undefined {
if (value == null) return undefined;
const num = Number(value);
return Number.isNaN(num) ? undefined : num;
}
function toString(value: unknown): string | undefined {
if (value == null) return undefined;
return String(value);
}
3. Declarative Pipeline Architecture
Replace the giant switch statement with a composable pipeline:
Data Structures
type MappingSpec = {
source:
| { type: 'env', key: string }
| { type: 'hard', value: string };
transforms: Transform[];
}
type Transform = 'boolean' | 'json' | 'base64' | 'number';
Pipeline Functions
// Parse mapping string into spec
function parseMappingSpec(input: string): MappingSpec
// Resolve the source value
async function resolveSource(source: MappingSpec['source']): Promise<unknown>
// Apply transformations
function applyTransforms(value: unknown, transforms: Transform[]): unknown
// Complete pipeline
async function processMappingValue(mappingString: string): Promise<unknown>
4. Transform Registry
Enable easy extension with new transforms:
const transformRegistry: Record<string, (v: unknown) => unknown> = {
boolean: toBoolean,
json: toJson,
base64: fromBase64,
number: toNumber,
};
5. Simplified processEnvMapping
Build pure object tree first, then write to kvStore:
async function evaluateMappingValue(mappingValue: any): Promise<any> {
if (typeof mappingValue === 'string') {
return processMappingValue(mappingValue);
}
if (mappingValue && typeof mappingValue === 'object') {
const out: any = {};
for (const [k, v] of Object.entries(mappingValue)) {
out[k] = await evaluateMappingValue(v);
}
return out;
}
return undefined;
}
// Main loop becomes:
for (const key in this.options.envMapping) {
const evaluated = await evaluateMappingValue(this.options.envMapping[key]);
if (evaluated !== undefined) {
await this.kvStore.writeKey(key as keyof T, evaluated);
}
}
Backward Compatibility
Supported Prefixes (Maintained)
hard:
- Hardcoded valuehard_boolean:
- Hardcoded booleanhard_json:
- Hardcoded JSONhard_base64:
- Hardcoded base64boolean:
- Environment variable as booleanjson:
- Environment variable as JSONbase64:
- Environment variable as base64
Supported Suffixes (Maintained)
_JSON
- Auto-parse as JSON_BASE64
- Auto-decode from base64
Typo Fix Strategy
- Add
ephemeral
option to interface - Keep reading
ephermal
for backward compatibility - Log deprecation warning when old spelling is used
Implementation Steps
- Add utility functions at the top of the file
- Implement pipeline functions (parseMappingSpec, resolveSource, applyTransforms)
- Refactor processEnvMapping to use the pipeline
- Update static helpers to use shared utilities
- Fix typo with compatibility shim
- Add error boundaries for better error reporting
- Test to ensure backward compatibility
Benefits
Code Quality
- 70% reduction in processEnvMapping complexity
- Better separation of concerns
- Easier testing - each function is pure and testable
- Cleaner error handling with boundaries
Performance
- Shared Qenv instance reduces allocations
- Optional parallelization with Promise.all
- Fewer repeated operations
Maintainability
- Extensible - Easy to add new transforms
- Readable - Clear pipeline flow
- Debuggable - Each step can be logged
- Type-safe - Better TypeScript support
Testing Strategy
- Unit tests for each utility function
- Integration tests for the full pipeline
- Backward compatibility tests for all existing prefixes/suffixes
- Edge case tests for error conditions
Future Extensions
With the transform registry, adding new features becomes trivial:
// Add YAML support
transformRegistry['yaml'] = (v) => YAML.parse(String(v));
// Add integer parsing
transformRegistry['int'] = (v) => parseInt(String(v), 10);
// Add custom transformers
transformRegistry['uppercase'] = (v) => String(v).toUpperCase();
Migration Path
- Implement new architecture alongside existing code
- Gradually migrate internal usage
- Mark old patterns as deprecated (with warnings)
- Remove deprecated code in next major version
Success Metrics
- All existing tests pass
- No breaking changes for users
- Reduced code complexity (measurable via cyclomatic complexity)
- Improved test coverage
- Better performance (fewer allocations, optional parallelization)