Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 61 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,74 @@ 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>
/// Parses JSON string into AppConfig.
/// Includes console logging side effects on parse errors.
/// </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>
/// Generates content sections based on configuration (performs I/O via scanner).
/// Uses declarative approach with LINQ and immutable collections.
/// </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