ProcessorsExplanation
Input Config
Understanding how configuration flows from template to processor
Input Config
Processors receive configuration from templates through input.config. This enables templates to customize processor behavior for each use case.
Configuration Flow
Template Side
Templates define processor configuration in the return value:
template/index.ts
// Template's index.tsStartTemplateWithLambda(async (i, d) => {// Collect user inputconst projectName = await i.text('Project name?', 'project.name', 'Enter name');const features = await i.checkbox('Features?', ['auth', 'api'], 'project.features', 'Select');const format = await i.select('Format?', ['json', 'yaml'], 'config.format', 'Choose');return {processors: [{name: 'myorg/my-processor',files: [{ root: 'templates', glob: '**/*', exclude: [], type: GlobType.Template }],config: {// This becomes input.config in processorprojectName,features,outputFormat: format,additionalSettings: {strict: true,minify: false}}}]};});
template/index.py
# Template's index.pydef start_template_with_lambda(i, d):# Collect user inputproject_name = i.text('Project name?', 'project.name', 'Enter name')features = i.checkbox('Features?', ['auth', 'api'], 'project.features', 'Select')format = i.select('Format?', ['json', 'yaml'], 'config.format', 'Choose')return {'processors': [{'name': 'myorg/my-processor','files': [{'root': 'templates', 'glob': '**/*', 'exclude': [], 'type': GlobType.Template}],'config': {# This becomes input.config in processor'projectName': project_name,'features': features,'outputFormat': format,'additionalSettings': {'strict': True,'minify': False}}}]}
Template/Index.cs
// Template's Index.cs[TemplateMain]public async Task<TemplateResult> RunAsync(IInquirer i, TemplateContext d){// Collect user inputvar projectName = await i.TextAsync("Project name?", "project.name", "Enter name");var features = await i.CheckboxAsync("Features?", ["auth", "api"], "project.features", "Select");var format = await i.SelectAsync("Format?", ["json", "yaml"], "config.format", "Choose");return new TemplateResult{Processors = new[]{new ProcessorConfig{Name = "myorg/my-processor",Files = new[] { new FileGlob { Root = "templates", Glob = "**/*", Exclude = Array.Empty<string>(), Type = GlobType.Template } },Config = new{// This becomes input.config in processorProjectName = projectName,Features = features,OutputFormat = format,AdditionalSettings = new{Strict = true,Minify = false}}}}};}
Processor Side
Processors receive config through input.config:
processor/index.ts
// Processor's index.ts// Define expected config shapeinterface MyProcessorConfig {projectName: string;features: string[];outputFormat: 'json' | 'yaml';additionalSettings?: {strict?: boolean;minify?: boolean;};}StartProcessorWithLambda(async (input, fileHelper) => {// Cast to your typeconst config = input.config as MyProcessorConfig;// Use config valuesconsole.log('Project:', config.projectName);console.log('Features:', config.features);const files = fileHelper.resolveAll();files.forEach(file => {if (config.features.includes('auth')) {file.content = addAuthCode(file.content);}file.writeFile();});return { directory: input.writeDir };});
processor/index.py
# Processor's index.pyfrom dataclasses import dataclassfrom typing import Optional, List@dataclassclass AdditionalSettings:strict: Optional[bool] = Noneminify: Optional[bool] = None@dataclassclass MyProcessorConfig:project_name: strfeatures: List[str]output_format: str # 'json' | 'yaml'additional_settings: Optional[AdditionalSettings] = Nonedef start_processor_with_fn(input, file_helper):# Cast to your typeconfig = MyProcessorConfig(**input.config)# Use config valuesprint('Project:', config.project_name)print('Features:', config.features)files = file_helper.resolve_all()for file in files:if 'auth' in config.features:file.content = add_auth_code(file.content)file.write_file()return {'directory': input.write_dir}
Processor/Index.cs
// Processor's Index.cs// Define expected config shapepublic class MyProcessorConfig{public string ProjectName { get; set; }public List<string> Features { get; set; }public string OutputFormat { get; set; } // "json" | "yaml"public AdditionalSettings? AdditionalSettings { get; set; }}public class AdditionalSettings{public bool? Strict { get; set; }public bool? Minify { get; set; }}[ProcessorMain]public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper){// Cast to your typevar config = input.Config as MyProcessorConfig;// Use config valuesConsole.WriteLine($"Project: {config.ProjectName}");Console.WriteLine($"Features: {string.Join(", ", config.Features)}");var files = fileHelper.ResolveAll();foreach (var file in files){if (config.Features.Contains("auth")){file.Content = AddAuthCode(file.Content);}file.WriteFile();}return new ProcessorResult { Directory = input.WriteDir };}
Config Structure
Simple Config
// Templateconfig: {prefix: 'MyApp',suffix: '.generated'}// Processorinterface SimpleConfig {prefix: string;suffix: string;}
Complex Config
// Templateconfig: {transformations: [{ type: 'replace', from: 'old', to: 'new' },{ type: 'append', value: '// Generated' }],output: {directory: 'dist',extension: '.gen.ts'},options: {verbose: true,dryRun: false}}// Processorinterface TransformRule {type: 'replace' | 'append' | 'prepend';from?: string;to?: string;value?: string;}interface ComplexConfig {transformations: TransformRule[];output: {directory: string;extension: string;};options?: {verbose?: boolean;dryRun?: boolean;};}
Default Values
Always handle missing config gracefully:
interface Config {required: string;optional?: boolean;withDefault?: number;}StartProcessorWithLambda(async (input, fileHelper) => {// Provide defaults for optional fieldsconst config: Config = {optional: true,withDefault: 10,...(input.config as Partial<Config>)};// Now config.optional and config.withDefault are guaranteedif (config.optional) {// ...}return { directory: input.writeDir };});
from dataclasses import dataclass, fieldfrom typing import Optional@dataclassclass Config:required: stroptional: Optional[bool] = Truewith_default: Optional[int] = 10def start_processor_with_fn(input, file_helper):# Provide defaults for optional fieldsconfig_data = {'optional': True, 'with_default': 10, **input.config}config = Config(**config_data)# Now config.optional and config.with_default are guaranteedif config.optional:# ...return {'directory': input.write_dir}
public class Config{public string Required { get; set; }public bool? Optional { get; set; } = true;public int? WithDefault { get; set; } = 10;}[ProcessorMain]public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper){// Provide defaults for optional fieldsvar config = input.Config != null? System.Text.Json.JsonSerializer.Deserialize<Config>(input.Config.ToString()): new Config();config ??= new Config();config.Optional ??= true;config.WithDefault ??= 10;// Now config.Optional and config.WithDefault are guaranteedif (config.Optional == true){// ...}return new ProcessorResult { Directory = input.WriteDir };}
Type Safety
input.config is typed as unknown. Always cast to your expected type and provide defaults for optional fields.
Config Validation
Validate config at processor start:
interface Config {mode: 'dev' | 'prod';maxFiles: number;}function validateConfig(config: unknown): Config {if (typeof config !== 'object' || config === null) {throw new Error('Config must be an object');}const c = config as Record<string, unknown>;if (c.mode !== 'dev' && c.mode !== 'prod') {throw new Error('mode must be "dev" or "prod"');}if (typeof c.maxFiles !== 'number' || c.maxFiles < 1) {throw new Error('maxFiles must be a positive number');}return c as Config;}StartProcessorWithLambda(async (input, fileHelper) => {const config = validateConfig(input.config);// Config is now validated// ...});
from dataclasses import dataclassfrom typing import Literal@dataclassclass Config:mode: Literal['dev', 'prod']max_files: intdef validate_config(config: dict) -> Config:if not isinstance(config, dict):raise ValueError('Config must be an object')if config.get('mode') not in ('dev', 'prod'):raise ValueError('mode must be "dev" or "prod"')if not isinstance(config.get('maxFiles'), int) or config['maxFiles'] < 1:raise ValueError('maxFiles must be a positive number')return Config(mode=config['mode'], max_files=config['maxFiles'])def start_processor_with_fn(input, file_helper):config = validate_config(input.config)# Config is now validated# ...
public class Config{public string Mode { get; set; } // "dev" | "prod"public int MaxFiles { get; set; }}public Config ValidateConfig(object config){if (config == null)throw new ArgumentException("Config must be an object");var json = System.Text.Json.JsonSerializer.Serialize(config);var c = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(json);if (c["Mode"]?.ToString() != "dev" && c["Mode"]?.ToString() != "prod")throw new ArgumentException("mode must be \"dev\" or \"prod\"");var maxFiles = Convert.ToInt32(c["MaxFiles"]);if (maxFiles < 1)throw new ArgumentException("maxFiles must be a positive number");return new Config { Mode = c["Mode"].ToString(), MaxFiles = maxFiles };}[ProcessorMain]public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper){var config = ValidateConfig(input.Config);// Config is now validated// ...}
Common Patterns
Variables Config
Like the default processor:
config: {vars: {name: 'my-project',version: '1.0.0',author: 'Developer'}}// Processor applies variablesfiles.forEach(file => {for (const [key, value] of Object.entries(config.vars)) {file.content = file.content.replaceAll(`{{${key}}}`, String(value));}});
Feature Flags
Enable/disable processor features:
config: {features: {authentication: true,logging: false,caching: true}}// Processor checks featuresif (config.features.authentication) {addAuthFiles();}
Output Customization
Control output format:
config: {output: {format: 'typescript',directory: 'src/generated',naming: 'camelCase'}}
Documentation
Document your config schema in cyan.yaml:
processor:inputs:- name: modetype: stringrequired: trueenum: [dev, prod]description: Processing mode- name: maxFilestype: numberrequired: falsedefault: 100description: Maximum files to process- name: featurestype: objectrequired: falsedescription: Feature flagsproperties:authentication:type: booleandefault: truelogging:type: booleandefault: false