Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@
using System.Text;
using CodeContext.Configuration;
using CodeContext.Services;
using CodeContext.Utils;

Console.OutputEncoding = Encoding.UTF8;

try
{
// Initialize dependencies
// Initialize dependencies using functional composition
var console = new ConsoleWriter();
var configLoader = new ConfigLoader(console);
var pathResolver = new PathResolver(console);
var filterConfig = new FilterConfiguration();
var fileChecker = new FileFilterService(filterConfig);
var scanner = new ProjectScanner(fileChecker, console);
var contentBuilder = new ContentBuilder(scanner);
var outputFormatter = new OutputFormatter(console);
var statsCalculator = new StatsCalculator();

// Load configuration
Expand All @@ -26,6 +23,19 @@
console.Write($"Enter the path to index (default: {defaultInputPath}): ");
var projectPath = pathResolver.GetInputPath(defaultInputPath);

// Load GitIgnore patterns for the project (I/O boundary clearly defined)
var gitIgnoreParser = GitHelper.FindRepositoryRoot(projectPath) switch
{
null => GitIgnoreParser.Empty,
var gitRoot => GitIgnoreParser.FromFile(Path.Combine(gitRoot, ".gitignore"))
};

// Initialize file checker with immutable GitIgnore parser
var fileChecker = new FileFilterService(filterConfig, gitIgnoreParser);
var scanner = new ProjectScanner(fileChecker, console);
var contentBuilder = new ContentBuilder(scanner);
var outputFormatter = new OutputFormatter(console);

// Determine output path
var folderName = PathResolver.GetFolderName(projectPath);
var defaultFileName = $"{folderName}_{config.DefaultOutputFileName}";
Expand Down
64 changes: 60 additions & 4 deletions Services/ConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
namespace CodeContext.Services;

/// <summary>
/// Loads application configuration from config.json.
/// Functional configuration loader with separated I/O and parsing logic.
/// </summary>
public class ConfigLoader
{
private const string ConfigFileName = "config.json";
private readonly IConsoleWriter _console;

public ConfigLoader(IConsoleWriter console)
Expand All @@ -18,18 +19,73 @@ public ConfigLoader(IConsoleWriter console)

/// <summary>
/// Loads configuration from config.json if it exists, otherwise returns default configuration.
/// I/O operation with functional error handling.
/// </summary>
public AppConfig Load()
public AppConfig Load() =>
ReadConfigFile(ConfigFileName)
.Match(
onSuccess: ParseConfig,
onError: HandleParseError);

/// <summary>
/// I/O operation: reads config file or returns empty JSON.
/// </summary>
private static Result<string> ReadConfigFile(string fileName)
{
try
{
var json = File.Exists(fileName) ? File.ReadAllText(fileName) : "{}";
return new Result<string>.Success(json);
}
catch (Exception ex)
{
return new Result<string>.Error(ex.Message);
}
}

/// <summary>
/// Pure function: parses JSON string into AppConfig.

Copilot AI Nov 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment claims ParseConfig is a "pure function", but it can throw exceptions and has different behavior based on the input JSON structure. Additionally, catching JsonException and returning a default config means it has side effects (writing to console via the caller). Consider updating the documentation to reflect that this function has side effects or error-handling behavior.

Suggested change
/// Pure function: parses JSON string into AppConfig.
/// Parses JSON string into AppConfig, handling errors and logging warnings to the console.
/// This function has side effects (console output) and returns a default config on error.

Copilot uses AI. Check for mistakes.
/// </summary>
private AppConfig ParseConfig(string json)
{
try
{
var configJson = File.Exists("config.json") ? File.ReadAllText("config.json") : "{}";
return JsonSerializer.Deserialize<AppConfig>(configJson) ?? new AppConfig();
return JsonSerializer.Deserialize<AppConfig>(json) ?? new AppConfig();
}
catch (JsonException ex)
{
_console.WriteLine($"⚠️ Warning: Invalid config.json format ({ex.Message}). Using defaults.");
return new AppConfig();
}
}

/// <summary>
/// Error handler: returns default config and logs error.
/// </summary>
private AppConfig HandleParseError(string error)
{
_console.WriteLine($"⚠️ Warning: Could not read config.json ({error}). Using defaults.");
return new AppConfig();
}

/// <summary>
/// Simple Result type for functional error handling.
/// </summary>
private abstract record Result<T>
{
private Result() { }

public sealed record Success(T Value) : Result<T>;
public sealed record Error(string Message) : Result<T>;

public TResult Match<TResult>(
Func<T, TResult> onSuccess,
Func<string, TResult> onError) =>
this switch
{
Success s => onSuccess(s.Value),
Error e => onError(e.Message),
_ => throw new InvalidOperationException()
};
}
}
44 changes: 35 additions & 9 deletions Services/ContentBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.Text;
using System.Collections.Immutable;
using CodeContext.Configuration;

namespace CodeContext.Services;

/// <summary>
/// Builds project context content based on configuration.
/// Builds project context content using functional composition.
/// Separates content generation from assembly.
/// </summary>
public class ContentBuilder
{
Expand All @@ -17,26 +18,51 @@ public ContentBuilder(ProjectScanner scanner)

/// <summary>
/// Builds the complete content output including structure and file contents.
/// Uses functional composition to build content sections.
/// </summary>
/// <param name="projectPath">The directory path to process.</param>
/// <param name="config">The configuration specifying what to include.</param>
/// <returns>The complete output content.</returns>
public string Build(string projectPath, AppConfig config)
public string Build(string projectPath, AppConfig config) =>
string.Join("\n", BuildContentSections(projectPath, config));

/// <summary>
/// Pure function that generates content sections based on configuration.
/// Uses declarative approach with LINQ and immutable collections.

Copilot AI Nov 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment claims BuildContentSections is a "pure function", but it doesn't actually execute the content generation - it defers it by storing Func<string> delegates. The functions referenced (_scanner.GetProjectStructure and _scanner.GetFileContents) perform I/O operations. Consider updating the documentation to clarify that this function builds a lazy evaluation structure rather than being truly pure, since the deferred operations will perform I/O when executed.

Suggested change
/// Pure function that generates content sections based on configuration.
/// Uses declarative approach with LINQ and immutable collections.
/// Generates content sections based on configuration using a declarative approach.
/// Builds a lazy evaluation structure: the returned delegates will perform I/O when evaluated.
/// Uses LINQ and immutable collections to assemble content sections.

Copilot uses AI. Check for mistakes.
/// </summary>
private IEnumerable<string> BuildContentSections(string projectPath, AppConfig config)
{
var content = new StringBuilder();
var sections = ImmutableArray.CreateBuilder<ContentSection>();

if (config.IncludeStructure)
{
content.AppendLine("Project Structure:")
.AppendLine(_scanner.GetProjectStructure(projectPath));
sections.Add(new ContentSection(
"Project Structure:",
() => _scanner.GetProjectStructure(projectPath)));
}

if (config.IncludeContents)
{
content.AppendLine("\nFile Contents:")
.AppendLine(_scanner.GetFileContents(projectPath));
sections.Add(new ContentSection(
"\nFile Contents:",
() => _scanner.GetFileContents(projectPath)));
}

return content.ToString();
return sections.ToImmutable().SelectMany(section => section.Render());
}

/// <summary>
/// Immutable record representing a content section with lazy evaluation.
/// </summary>
private sealed record ContentSection(string Header, Func<string> ContentGenerator)
{
/// <summary>
/// Renders the section by evaluating the content generator.
/// </summary>
public IEnumerable<string> Render()
{
yield return Header;
yield return ContentGenerator();
}
}
}
Loading
Loading