Skip to content

Commit 4aaabb4

Browse files
authored
Merge pull request #1 from git-stunts/v2.1.0-rc
V2.1.0 rc
2 parents 2c4a40d + 68a4dd2 commit 4aaabb4

46 files changed

Lines changed: 5611 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Use Node.js
15+
uses: actions/setup-node@v4
16+
with:
17+
node-version: '20'
18+
cache: 'npm'
19+
- run: npm install
20+
- run: npm run lint
21+
22+
test:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
- name: Use Node.js
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: '20'
30+
cache: 'npm'
31+
- run: npm install
32+
- run: npm test

.gitignore

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Dependency directories
2+
node_modules/
3+
jspm_packages/
4+
bun.lockb
5+
node_modules.bun/
6+
7+
# Deno
8+
.deno/
9+
deno.lock
10+
11+
# Build outputs
12+
dist/
13+
build/
14+
.next/
15+
.nuxt/
16+
.output/
17+
.cache/
18+
out/
19+
20+
# Logs
21+
logs
22+
*.log
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
lerna-debug.log*
27+
.pnpm-debug.log*
28+
29+
# Diagnostic reports (https://nodejs.org/api/report.html)
30+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
31+
32+
# Runtime data
33+
pids
34+
*.pid
35+
*.seed
36+
*.pid.lock
37+
38+
# Directory for instrumented libs generated by jscoverage/JSCover
39+
lib-cov
40+
41+
# Coverage directory used by tools like istanbul
42+
coverage
43+
*.lcov
44+
45+
# nyc documentation
46+
.nyc_output
47+
48+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
49+
.grunt
50+
51+
# Bower dependency directory (https://bower.io/)
52+
bower_components
53+
54+
# node-waf configuration
55+
.lock-wscript
56+
57+
# Compiled binary addons (https://nodejs.org/api/addons.html)
58+
build/Release
59+
60+
# Dependency directories
61+
jspm_packages/
62+
63+
# TypeScript v1 declaration files
64+
typings/
65+
66+
# TypeScript cache
67+
*.tsbuildinfo
68+
69+
# Optional npm cache directory
70+
.npm
71+
72+
# Optional eslint cache
73+
.eslintcache
74+
75+
# Optional stylelint cache
76+
.stylelintcache
77+
78+
# Optional REPL history
79+
.node_repl_history
80+
81+
# Output of 'npm pack'
82+
*.tgz
83+
84+
# Yarn Integrity file
85+
.yarn-integrity
86+
87+
# dotenv environment variable files
88+
.env
89+
.env.development.local
90+
.env.test.local
91+
.env.production.local
92+
.env.local
93+
94+
# parcel-bundler cache (https://parceljs.org/)
95+
.cache
96+
.parcel-cache
97+
98+
# Next.js build output
99+
.next
100+
out
101+
102+
# Nuxt.js build / generate output
103+
.nuxt
104+
dist
105+
106+
# Gatsby files
107+
.cache/
108+
public/
109+
110+
# vue-cli dist
111+
dist/
112+
113+
# Serverless directories
114+
.serverless/
115+
116+
# FuseBox cache
117+
.fusebox/
118+
119+
# DynamoDB Local files
120+
.dynamodb/
121+
122+
# Tern JS port file
123+
.tern-port
124+
125+
# Stores VS Code state
126+
.vscode/*
127+
!.vscode/settings.json
128+
!.vscode/tasks.json
129+
!.vscode/launch.json
130+
!.vscode/extensions.json
131+
!.vscode/code-actions.json
132+
133+
# IntelliJ IDEA
134+
.idea/
135+
136+
.crush
137+
.obsidian
138+
139+
# macOS
140+
.DS_Store
141+
.AppleDouble
142+
.LSOverride
143+
Icon
144+
145+
146+
# Thumbnails
147+
._*
148+
149+
# Files that might appear in the root of a volume
150+
.DocumentRevisions-V100
151+
.fseventsd
152+
.Spotlight-V100
153+
.TemporaryItems
154+
.Trashes
155+
.VolumeIcon.icns
156+
.com.apple.timemachine.donotpresent
157+
158+
# Directories potentially created by macOS
159+
.AppleDB
160+
.AppleDesktop
161+
Network Trash Folder
162+
Temporary Items
163+
.apdisk
164+
165+
# Other Editors
166+
*.swp
167+
*.swo
168+
*~

API_REFERENCE.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# API Reference
2+
3+
This file catalogs every public export from `@git-stunts/trailer-codec` so you can confidently import, configure, and extend the codec.
4+
5+
## Encoding & decoding helpers
6+
7+
### `decodeMessage(message: string)`
8+
- Deprecated convenience wrapper around `new TrailerCodec().decode(message)`. Use `TrailerCodec` instances instead. (to be removed in v3.0)
9+
- Input: a raw commit payload (title, optional body, trailers) as a string.
10+
- Output: `{ title: string, body: string, trailers: Record<string, string> }` where `body` is trimmed via `formatBodySegment` (see below) and trailer keys are normalized to lowercase.
11+
- Throws `TrailerCodecError` subclasses (e.g., `TrailerNoSeparatorError`, `TrailerValueInvalidError`, or `CommitMessageInvalidError`) for invalid titles, missing blank lines, oversized messages, or malformed trailers.
12+
13+
### `encodeMessage({ title: string, body?: string, trailers?: Record<string, string> })`
14+
-- Deprecated convenience wrapper around `new TrailerCodec().encode(payload)`. Use `TrailerCodec` instances instead. (to be removed in v3.0)
15+
- Builds a `GitCommitMessage` under the hood and returns the canonical string. Trailers are converted from plain objects to `GitTrailer` instances via the default factory (see `GitTrailer` below).
16+
17+
### `formatBodySegment(body?: string, { keepTrailingNewline = false })`
18+
- Shared helper for `decodeMessage` to trim whitespace while optionally keeping a trailing newline when you plan to write the body back into a template.
19+
20+
### `createMessageHelpers({ service, bodyFormatOptions } = {})`
21+
- Returns `{ decodeMessage, encodeMessage }` bound to the injected `TrailerCodecService` instance; a new service is created when none is provided.
22+
- Supports `bodyFormatOptions` (forwarded to `formatBodySegment`) and is useful for advanced/test wiring.
23+
24+
### `createDefaultTrailerCodec({ bodyFormatOptions } = {})`
25+
- Creates a new `TrailerCodecService`, builds a `TrailerCodec`, and returns it so you can call `encodeMessage()`/`decodeMessage()` without manually wiring services.
26+
27+
### `TrailerCodec`
28+
- Constructor opts: `{ service = new TrailerCodecService(), bodyFormatOptions }`.
29+
- Exposes `decodeMessage(input)`/`decode(input)` and `encodeMessage(payload)`/`encode(payload)` methods that delegate to `createMessageHelpers()`.
30+
- The `decode()` and `encode()` methods are convenience aliases added in v2.1.0.
31+
32+
### `createConfiguredCodec({ keyPattern, keyMaxLength, parserOptions, formatters, bodyFormatOptions } = {})`
33+
- Creates a schema bundle via `createGitTrailerSchemaBundle`, a `TrailerParser`, and a `TrailerCodecService`, then exposes `{ service, helpers, decodeMessage, encodeMessage }`.
34+
- Use this when you need custom trailer patterns, parser tweaks, formatter hooks, or tightly controlled key lengths.
35+
36+
## Domain model exports
37+
38+
### `GitCommitMessage`
39+
```ts
40+
new GitCommitMessage(
41+
payload: { title: string; body?: string; trailers?: GitTrailerInput[] },
42+
options?: { trailerSchema?: ReturnType<typeof getDefaultTrailerSchemaBundle>['schema']; formatters?: { titleFormatter?: (value: string) => string; bodyFormatter?: (value: string) => string } }
43+
);
44+
```
45+
- Validates via `GitCommitMessageSchema`, normalizes title/body with optional formatters, and converts `trailers` to `GitTrailer` instances.
46+
- `toString()` returns a Git-style commit string (title, optional body, blank line, trailers).
47+
48+
### `GitTrailer`
49+
- Accepts `(key: string, value: string, schema = GitTrailerSchema)`.
50+
- Validates using `GitTrailerSchema` and normalizes the key to lowercase and the value to a trimmed string.
51+
- Throws `TrailerInvalidError` or `TrailerValueInvalidError` when the provided key/value fail schema validation.
52+
53+
## Services & parsers
54+
55+
### `TrailerCodecService`
56+
- Core decode/encode logic; see `docs/SERVICE.md` for how the pipeline is wired.
57+
- Constructor options:
58+
- `schemaBundle`: result of `createGitTrailerSchemaBundle({ keyPattern, keyMaxLength })` (defaults: `keyPattern` = `[A-Za-z0-9_-]+`, `keyMaxLength` = `100`, see the schema section below).
59+
- `trailerFactory`: function that instantiates trailers (defaults to `GitTrailer`).
60+
- `parser`: instance of `TrailerParser`.
61+
- `messageNormalizer`, `titleExtractor`, `bodyComposer`: helper classes that normalize lines, extract the title, and compose the body.
62+
- `formatters`: optional `{ titleFormatter, bodyFormatter }` that run before serialization.
63+
- `decode(message)` enforces message size, normalizes lines, extracts the title, splits body/trailers with `TrailerParser`, composes the body, builds trailers with `trailerFactory`, and constructs a `GitCommitMessage`.
64+
- `encode(messageEntity)` accepts either a `GitCommitMessage` instance or a plain payload object, validates it against `GitCommitMessageSchema`, and returns the canonical commit string produced by the entity.
65+
66+
### `TrailerParser`
67+
- Constructor takes `{ keyPattern = TRAILER_KEY_RAW_PATTERN_STRING }`.
68+
- `split(lines)` finds where the trailer block begins (walks backward, validates the blank-line separator) and returns `{ bodyLines, trailerLines }`.
69+
- Used internally by `TrailerCodecService` and is injectable for custom parsing strategies.
70+
71+
## Schemas & constants
72+
73+
### `createGitTrailerSchemaBundle({ keyPattern, keyMaxLength } = {})`
74+
- Returns `{ schema, keyPattern, keyRegex }` with the schema used by `GitTrailer` and `GitCommitMessage`.
75+
- Default `keyPattern` is `[A-Za-z0-9_-]+` and `keyMaxLength` defaults to `100`.
76+
77+
### `TRAILER_KEY_RAW_PATTERN_STRING` / `TRAILER_KEY_REGEX`
78+
- Exported from the default schema bundle; use them to keep custom parsers aligned with validation rules.
79+
80+
## Errors
81+
82+
### `TrailerCodecError`
83+
- Base error type used throughout the codec (`index.js` re-exports it).
84+
- Signature: `(message: string, meta: Record<string, unknown> = {})`.
85+
86+
### Validation error subclasses
87+
88+
| Error | Thrown by | Meaning |
89+
| --- | --- | --- |
90+
| `TrailerTooLargeError` | `MessageNormalizer.guardMessageSize` (called by `TrailerCodecService.decode` and the exported `decodeMessage`) | Message exceeds the 5 MB guard in `MessageNormalizer`. |
91+
| `TrailerNoSeparatorError` | `TrailerParser.split` / `TrailerCodecService.decode` when the blank-line guard fails | A trailer block was found without a blank line separating it from the body (see `TrailerParser`). |
92+
| `TrailerValueInvalidError` | `GitTrailer` via `GitTrailerSchema.parse` when constructing trailers | A trailer value violated the `GitTrailerSchema` (e.g., contained `\n`). |
93+
| `TrailerInvalidError` | `GitTrailer` via `GitTrailerSchema.parse` when constructing trailers | Trailer key or value failed validation (`GitTrailerSchema`). |
94+
| `CommitMessageInvalidError` | `GitCommitMessage` via `GitCommitMessageSchema.parse` (triggered by `TrailerCodecService.decode` or `encode`) | The `GitCommitMessageSchema` rejected the title/body/trailers combination. |

ARCHITECTURE.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Architecture: @git-stunts/trailer-codec
2+
3+
This project adheres to **Hexagonal Architecture** (Ports and Adapters) and **Domain-Driven Design (DDD)** principles to ensure robustness, testability, and separation of concerns.
4+
5+
## 🧱 Core Concepts
6+
7+
### Domain Layer (`src/domain/`)
8+
The core business logic, isolated from external frameworks or I/O.
9+
10+
- **Entities**: Mutable objects with identity and lifecycle (e.g., `GitCommitMessage`).
11+
- **Value Objects**: Immutable objects defined by their attributes (e.g., `GitTrailer`).
12+
- **Services**: Domain logic that doesn't fit naturally into an entity (e.g., `TrailerCodecService` for parsing/serializing).
13+
- **Errors**: Domain-specific error hierarchy (`TrailerCodecError` plus concrete subclasses such as `TrailerNoSeparatorError` and `TrailerValueInvalidError`).
14+
- **Schemas**: Zod schemas for validation of domain objects.
15+
16+
### Ports Layer (`src/ports/`)
17+
*Currently implicit.* The public API (exported via `index.js`) serves as the primary input port/facade for consumers. Since this library is primarily a data transformation tool (codec), it does not currently have complex output ports for I/O.
18+
19+
## 📂 Directory Structure
20+
21+
```
22+
src/
23+
├── domain/
24+
│ ├── entities/ # GitCommitMessage
25+
│ ├── errors/ # TrailerCodecError and validation subclasses
26+
│ ├── schemas/ # Zod schemas
27+
│ ├── services/ # TrailerCodecService
28+
│ └── value-objects/ # GitTrailer
29+
```
30+
31+
## 🧪 Testing Strategy
32+
33+
- **Unit Tests** (`test/unit/`): Comprehensive tests for entities, value objects, and services.
34+
- **Test Doubles**: The architecture supports easy mocking of dependencies if the system grows.
35+
36+
## 🛠️ Design Decisions
37+
38+
1. **Zod for Validation**: We use Zod for runtime schema validation but wrap it in domain-specific `TrailerCodecError` subclasses to avoid leaking implementation details.
39+
2. **Case Normalization**: Git trailer keys are case-insensitive. We normalize them to lowercase in the `GitTrailer` Value Object to ensure consistency.
40+
3. **Facade Pattern**: `index.js` acts as a facade, providing a simple, backward-compatible API while exposing the rich domain model for advanced users.

0 commit comments

Comments
 (0)