Skip to content

refactor(architecture): simplify config and ports - #66

Merged
ekalinin merged 1 commit into
masterfrom
refactor/config-ports
Aug 1, 2026
Merged

refactor(architecture): simplify config and ports#66
ekalinin merged 1 commit into
masterfrom
refactor/config-ports

Conversation

@ekalinin

@ekalinin ekalinin commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

This PR simplifies configuration flow and dependency boundaries without changing the CLI or generated TOC format.

  • groups application configuration by responsibility;
  • removes pass-through configuration copies from controller and use cases;
  • moves small interfaces next to the code that consumes them;
  • makes internal/app the composition root for use cases;
  • moves HTTP infrastructure from internal/utils to internal/adapters;
  • moves header and footer formatting to the application presentation layer;
  • removes the redundant RemotePoster wrapper layer;
  • removes an unused logger dependency from FileWriter.

Motivation

Before this change, app.Config was copied almost field-for-field into controller.Config, then copied again into usecase/config.Config. Most of those fields were unrelated to controller execution and were not used by the individual use cases:

  • RemoteMd stored the complete use case config but did not read any field from it;
  • LocalMd and RemoteHTML only needed the Debug value;
  • renderer options were copied through layers that did not render a TOC;
  • GitHub credentials and API settings were carried through controller and core configuration even though they were only needed while constructing adapters;
  • presentation settings were carried into core configuration even though only app.Run used them.

The central internal/core/ports package also made interfaces provider-oriented. Every consumer depended on the same shared package even when it needed only one or two methods. In addition, RemotePoster wrapped another RemotePoster implementation without adding behavior, and internal/utils mixed HTTP transport, presentation formatting, and heading cleanup.

Configuration changes

app.Config remains the top-level application configuration, but its fields are now grouped by responsibility:

Configuration Responsibility
Files, Serial, Debug Application execution options
PresentationConfig Header and footer visibility
GitHubConfig GitHub token, API URL, and regexp layout version
toc.Config Start depth, maximum depth, escaping, and indentation

The existing GitHub field names GHToken, GHUrl, and GHVersion are preserved inside GitHubConfig.

cmd/gh-md-toc now creates these groups directly while parsing CLI flags. Adding a presentation or renderer flag no longer requires adding the same field to controller and use case configuration types.

app.New now performs the dependency wiring directly:

  1. creates the shared HTTP client and concrete adapters;
  2. creates the regexp and JSON heading extractors;
  3. creates the shared TOC renderer and generators;
  4. constructs LocalMd, RemoteMd, and RemoteHTML with only their required settings and dependencies;
  5. passes the three use cases to the controller.

The absolute-link rule is preserved. app.New still enables toc.Config.AbsolutePaths when at least one document path was supplied through the CLI and leaves it disabled for stdin.

Controller and use case configuration

controller.Config now contains only:

  • Files, used to select file processing or stdin processing;
  • Serial, used to select the worker count.

The former controller.Config.ToUseCaseConfig conversion and the complete internal/core/usecase/config package are removed.

Use cases now receive only the settings they use:

  • LocalMd receives debug bool because it controls the optional HTML debug artifact;
  • RemoteHTML receives debug bool because it controls the optional JSON debug artifact;
  • RemoteMd receives no configuration value because it did not use one.

The former aggregate usecase.New constructor is removed. It existed mainly to forward the same broad config and dependency list to the concrete use cases. Construction now happens in app.New, which is the application composition root.

Consumer-owned interfaces

The central internal/core/ports package is removed. Small interfaces are declared next to their consumers instead:

  • controller owns its useCase and logger contracts;
  • LocalMd owns file checker, file writer, HTML converter, TOC grabber, and logger contracts;
  • RemoteMd owns remote getter, Markdown processor, temporary file, and logger contracts;
  • RemoteHTML owns remote getter, TOC grabber, temporary file, and logger contracts;
  • HTMLConverter owns the remote poster contract;
  • adapters share their local logger contract inside the adapters package.

All dependencies still satisfy these interfaces implicitly. The change removes the shared interface package without coupling core code to adapter implementations.

RemoteMd now depends on a small Markdown processor interface instead of the concrete *localmd.LocalMd type. This keeps its orchestration behavior unchanged while removing an unnecessary concrete dependency between use case packages.

HTTP infrastructure

HTTP helpers and HTTP-specific error types are moved from internal/utils to internal/adapters/http.go, next to RemoteGetter, RemotePoster, and HTMLConverter.

The existing helper names and behavior are preserved:

  • HttpGet;
  • HttpGetJson;
  • HttpPost;
  • HTTPStatusError;
  • ResponseBodyTooLargeError.

The moved implementation preserves:

  • request context propagation;
  • the configured shared http.Client;
  • the User-Agent, JSON, content type, and authorization headers;
  • acceptance of HTTP 2xx responses;
  • limited error-body reads;
  • the maximum successful response-body size;
  • malformed URL errors;
  • timeout and cancellation behavior;
  • request file closing.

The HTTP tests move with the implementation into the adapters package so they continue to exercise the same behavior at its new location.

RemotePoster simplification

Previously the flow was:

HTMLConverter -> RemotePoster wrapper -> realPoster -> utils.HttpPost

RemotePoster did not add behavior. It only delegated every call to the nested ports.RemotePoster.

The new flow is:

HTMLConverter -> RemotePoster -> adapters.HttpPost

RemotePoster now stores the injected *http.Client directly. realPoster and NewRemotePosterX are removed. The existing NewRemotePoster and NewRemotePosterWithClient constructors remain.

The test seam used by HTMLConverter remains available through NewHTMLConverterX, which now accepts the consumer-owned remotePoster interface.

Presentation formatting

ShowHeader and ShowFooter move from internal/utils to internal/app, where their output is controlled and written by App.Run.

Their names and exact output are preserved. The footer is still omitted after a processing error, and the header is still omitted for stdin and multi-file input according to the existing behavior.

internal/utils now contains only the shared heading text cleanup used by the two extractor adapters.

Removed pass-through code

The following files are removed because their responsibilities no longer exist:

  • internal/core/ports/ports.go;
  • internal/core/usecase/config/config.go;
  • internal/core/usecase/new.go;
  • internal/app/config_test.go, which only tested the deleted field-for-field conversion methods.

FileWriter also no longer stores or receives a logger because it never used that dependency.

Compatibility

This is an internal architecture refactoring. It does not change:

  • CLI flags, defaults, or environment variable precedence;
  • supported regexp versions;
  • GitHub token logging protections;
  • generated TOC formatting;
  • heading depth, escaping, indentation, or absolute-link rules;
  • serial and parallel processing behavior;
  • HTTP request and response handling;
  • debug artifact behavior;
  • header and footer text.

Tests

The following checks pass:

  • GOTOOLCHAIN=go1.21.13 go test ./...;
  • GOTOOLCHAIN=go1.26.5 go test -race ./...;
  • go vet ./...;
  • golangci-lint run ./...;
  • go mod tidy -diff;
  • go mod verify;
  • make e2e.

The live e2e suite covers all nine existing scenarios:

  • local Markdown with three option sets;
  • remote raw Markdown with three option sets;
  • GitHub blob HTML/JSON with three option sets.

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.17266% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.16%. Comparing base (1253d81) to head (a636dbc).

Files with missing lines Patch % Lines
internal/adapters/http.go 71.42% 13 Missing and 7 partials ⚠️
internal/adapters/remoteposter.go 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #66      +/-   ##
==========================================
+ Coverage   80.45%   82.16%   +1.71%     
==========================================
  Files          29       27       -2     
  Lines         711      673      -38     
==========================================
- Hits          572      553      -19     
+ Misses         96       77      -19     
  Partials       43       43              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ekalinin
ekalinin merged commit 112af89 into master Aug 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant