Your First Resolver
Create a resolver that merges JSON files from layered templates
Your First Resolver
Create a resolver that merges JSON files from layered templates. This tutorial builds a JSON merger that combines multiple versions of the same file.
Prerequisites
- Docker installed and running
- Basic knowledge of your chosen language (TypeScript, Python, or C#)
How Resolvers Work
Resolvers receive multiple versions of the same file and must return a single merged output:
graph LRA[Template Layer 1<br/>config.json] --> D[Resolver]B[Template Layer 2<br/>config.json] --> DC[Template Layer 3<br/>config.json] --> DD --> E[Merged config.json]
Create Project
Initialize Project
mkdir my-resolvercd my-resolverbun init -ybun add @atomicloud/cyan-sdk
mkdir my-resolvercd my-resolver# Create virtual environmentpython -m venv .venvsource .venv/bin/activate # On Windows: .venv\Scripts\activatepip install cyanprintsdk
mkdir my-resolvercd my-resolverdotnet new console -n MyResolvercd MyResolverdotnet add package Sulfone.Helium
Create Resolver Logic
Create the main resolver file:
Create index.ts:
import { ResolverOutput, StartResolverWithLambda } from '@atomicloud/cyan-sdk';StartResolverWithLambda(async (input): Promise<ResolverOutput> => {// Get all file versions (from different template layers)const files = input.files;// Validate: all files should have the same pathconst paths = files.map(f => f.path);const uniquePaths = new Set(paths);if (uniquePaths.size !== 1) {throw new Error(`Expected all files to have the same path, got: ${[...uniquePaths].join(', ')}`);}// Sort by layer (lower layer = higher priority)const sorted = [...files].sort((a, b) => a.origin.layer - b.origin.layer);// Merge JSON contentconst merged = sorted.reduce((acc, file) => {const content = JSON.parse(file.content);return { ...acc, ...content };}, {});// Return merged outputreturn {path: paths[0],content: JSON.stringify(merged, null, 2),};});
Create main.py:
import jsonfrom cyanprintsdk.domain.resolver.input import ResolverInputfrom cyanprintsdk.domain.resolver.output import ResolverOutputfrom cyanprintsdk.main import start_resolver_with_fnasync def resolver(i: ResolverInput) -> ResolverOutput:# Get all file versions (from different template layers)files = i.files# Validate: all files should have the same pathpaths = [f.path for f in files]unique_paths = set(paths)if len(unique_paths) != 1:raise ValueError(f"Expected all files to have the same path, got: {', '.join(unique_paths)}")# Sort by layer (lower layer = higher priority)sorted_files = sorted(files, key=lambda f: f.origin.layer)# Merge JSON contentmerged = {}for file in sorted_files:content = json.loads(file.content)merged = {**merged, **content}# Return merged outputreturn ResolverOutput(path=paths[0],content=json.dumps(merged, indent=2))if __name__ == "__main__":start_resolver_with_fn(resolver)
Create Program.cs:
using System.Text.Json;using sulfone_helium;using sulfone_helium.Domain.Resolver;CyanEngine.StartResolver(args,async (ResolverInput input) =>{// Get all file versions (from different template layers)var files = input.Files.ToList();// Validate: all files should have the same pathvar paths = files.Select(f => f.Path).Distinct().ToList();if (paths.Count != 1){throw new InvalidOperationException($"Expected all files to have the same path, got: {string.Join(", ", paths)}");}// Sort by layer (lower layer = higher priority)var sorted = files.OrderBy(f => f.Origin.Layer).ToList();// Merge JSON contentvar merged = new Dictionary<string, object>();foreach (var file in sorted){var content = JsonSerializer.Deserialize<Dictionary<string, object>>(file.Content)?? new Dictionary<string, object>();foreach (var kvp in content){merged[kvp.Key] = kvp.Value;}}// Return merged outputreturn new ResolverOutput(paths[0],JsonSerializer.Serialize(merged, new JsonSerializerOptions { WriteIndented = true }));});
Create Dockerfile
Create Dockerfile:
FROM oven/bun:1.1.30WORKDIR /app# Mark as CyanPrint resolverLABEL cyanprint.dev=true# Install dependenciesCOPY package.json .COPY bun.lock .RUN bun install# Copy resolver codeCOPY . .# Run resolverCMD ["bun", "run", "index.ts"]
Create Dockerfile:
FROM python:3.11-slimWORKDIR /app# Mark as CyanPrint resolverLABEL cyanprint.dev=true# Install dependenciesCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt# Copy resolver codeCOPY . .# Run resolverCMD ["python", "-u", "main.py"]
Create requirements.txt:
cyanprintsdk>=1.0.0
Create Dockerfile:
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS baseWORKDIR /appENV ASPNETCORE_URLS=http://+:5553FROM mcr.microsoft.com/dotnet/sdk:8.0 AS buildWORKDIR /srcCOPY ["MyResolver.csproj", "./"]RUN dotnet restore "MyResolver.csproj"COPY . .RUN dotnet build "MyResolver.csproj" -c Release -o /app/buildFROM build AS publishRUN dotnet publish "MyResolver.csproj" -c Release -o /app/publish /p:UseAppHost=falseFROM base AS finalLABEL cyanprint.dev=trueWORKDIR /appCOPY --from=publish /app/publish .ENTRYPOINT ["dotnet", "MyResolver.dll"]
Understanding the Code
ResolverInput
The input contains configuration and all file versions:
Prop
Type
ResolvedFile
Each file version includes its origin information:
Prop
Type
FileOrigin
Identifies the source of each file version:
Prop
Type
ResolverOutput
Return a single merged file:
Prop
Type
Build and Test
Build Docker Image
docker build -t my-resolver:dev .
docker build -t my-resolver:dev .
docker build -t my-resolver:dev .
Use in Template
Configure your template's cyan.yaml to use the resolver:
resolvers:- resolver: my-org/my-resolver:1config:mergeStrategy: deepfiles:- config.json- package.json
During development, use local Docker images with the :dev tag. Push to a registry when ready to share.
Test Locally
Create a test template that uses your resolver, then run:
cyanprint try template ./test-template ./output
Automated Testing
Set up snapshot tests to verify your resolver produces consistent output:
cyanprint test resolver .
Create a test.cyan.yaml with multi-origin input fixtures and expected snapshots. See Automated Testing for the full guide.
What You Learned
- How to create a resolver project
- Understanding resolver input/output types
- Accessing file versions and their origins
- Merging content from multiple layers
- Returning a single merged file
- Setting up automated snapshot tests
Next Steps
Learn more about resolver capabilities: