invar-tools 1.8.0__py3-none-any.whl → 1.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- invar/__init__.py +8 -0
- invar/core/doc_edit.py +187 -0
- invar/core/doc_parser.py +563 -0
- invar/core/language.py +88 -0
- invar/core/models.py +106 -0
- invar/core/patterns/detector.py +6 -1
- invar/core/patterns/p0_exhaustive.py +15 -3
- invar/core/patterns/p0_literal.py +15 -3
- invar/core/patterns/p0_newtype.py +15 -3
- invar/core/patterns/p0_nonempty.py +15 -3
- invar/core/patterns/p0_validation.py +15 -3
- invar/core/patterns/registry.py +5 -1
- invar/core/patterns/types.py +5 -1
- invar/core/property_gen.py +4 -0
- invar/core/rules.py +84 -18
- invar/core/sync_helpers.py +27 -1
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +310 -0
- invar/mcp/handlers.py +408 -0
- invar/mcp/server.py +288 -143
- invar/node_tools/MANIFEST +7 -0
- invar/node_tools/__init__.py +51 -0
- invar/node_tools/fc-runner/cli.js +77 -0
- invar/node_tools/quick-check/cli.js +28 -0
- invar/node_tools/ts-analyzer/cli.js +480 -0
- invar/shell/claude_hooks.py +35 -12
- invar/shell/commands/doc.py +409 -0
- invar/shell/commands/guard.py +41 -1
- invar/shell/commands/init.py +154 -16
- invar/shell/commands/perception.py +157 -33
- invar/shell/commands/skill.py +187 -0
- invar/shell/commands/template_sync.py +65 -13
- invar/shell/commands/uninstall.py +60 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/doc_tools.py +459 -0
- invar/shell/fs.py +67 -13
- invar/shell/pi_hooks.py +6 -0
- invar/shell/prove/crosshair.py +3 -0
- invar/shell/prove/guard_ts.py +902 -0
- invar/shell/skill_manager.py +355 -0
- invar/shell/template_engine.py +28 -4
- invar/shell/templates.py +4 -4
- invar/templates/claude-md/python/critical-rules.md +33 -0
- invar/templates/claude-md/python/quick-reference.md +24 -0
- invar/templates/claude-md/typescript/critical-rules.md +40 -0
- invar/templates/claude-md/typescript/quick-reference.md +24 -0
- invar/templates/claude-md/universal/check-in.md +25 -0
- invar/templates/claude-md/universal/skills.md +73 -0
- invar/templates/claude-md/universal/workflow.md +55 -0
- invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
- invar/templates/config/AGENT.md.jinja +58 -0
- invar/templates/config/CLAUDE.md.jinja +16 -209
- invar/templates/config/context.md.jinja +19 -0
- invar/templates/examples/{README.md → python/README.md} +2 -0
- invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
- invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
- invar/templates/examples/python/core_shell.py +227 -0
- invar/templates/examples/python/functional.py +613 -0
- invar/templates/examples/typescript/README.md +31 -0
- invar/templates/examples/typescript/contracts.ts +163 -0
- invar/templates/examples/typescript/core_shell.ts +374 -0
- invar/templates/examples/typescript/functional.ts +601 -0
- invar/templates/examples/typescript/workflow.md +95 -0
- invar/templates/hooks/PostToolUse.sh.jinja +10 -1
- invar/templates/hooks/PreToolUse.sh.jinja +38 -0
- invar/templates/hooks/Stop.sh.jinja +1 -1
- invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
- invar/templates/hooks/pi/invar.ts.jinja +9 -0
- invar/templates/manifest.toml +7 -6
- invar/templates/onboard/assessment.md.jinja +214 -0
- invar/templates/onboard/patterns/python.md +347 -0
- invar/templates/onboard/patterns/typescript.md +452 -0
- invar/templates/onboard/roadmap.md.jinja +168 -0
- invar/templates/protocol/INVAR.md.jinja +51 -0
- invar/templates/protocol/python/architecture-examples.md +41 -0
- invar/templates/protocol/python/contracts-syntax.md +56 -0
- invar/templates/protocol/python/markers.md +44 -0
- invar/templates/protocol/python/tools.md +24 -0
- invar/templates/protocol/python/troubleshooting.md +38 -0
- invar/templates/protocol/typescript/architecture-examples.md +52 -0
- invar/templates/protocol/typescript/contracts-syntax.md +73 -0
- invar/templates/protocol/typescript/markers.md +48 -0
- invar/templates/protocol/typescript/tools.md +65 -0
- invar/templates/protocol/typescript/troubleshooting.md +104 -0
- invar/templates/protocol/universal/architecture.md +36 -0
- invar/templates/protocol/universal/completion.md +14 -0
- invar/templates/protocol/universal/contracts-concept.md +37 -0
- invar/templates/protocol/universal/header.md +17 -0
- invar/templates/protocol/universal/session.md +17 -0
- invar/templates/protocol/universal/six-laws.md +10 -0
- invar/templates/protocol/universal/usbv.md +14 -0
- invar/templates/protocol/universal/visible-workflow.md +25 -0
- invar/templates/skills/develop/SKILL.md.jinja +85 -3
- invar/templates/skills/extensions/_registry.yaml +93 -0
- invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
- invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
- invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
- invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
- invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
- invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
- invar/templates/skills/extensions/security/SKILL.md +382 -0
- invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
- invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
- invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
- invar/templates/skills/review/SKILL.md.jinja +220 -248
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/METADATA +336 -12
- invar_tools-1.11.0.dist-info/RECORD +178 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.8.0.dist-info/RECORD +0 -116
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.11.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invar Contract Examples (TypeScript)
|
|
3
|
+
*
|
|
4
|
+
* Reference patterns for Zod schemas as contracts.
|
|
5
|
+
* Managed by Invar - do not edit directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// GOOD: Complete Contract with Zod Schema
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Precondition: price > 0, 0 <= discount <= 1
|
|
16
|
+
* Postcondition: result >= 0
|
|
17
|
+
*/
|
|
18
|
+
const DiscountedPriceInput = z.object({
|
|
19
|
+
price: z.number().positive(),
|
|
20
|
+
discount: z.number().min(0).max(1),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const DiscountedPriceOutput = z.number().nonnegative();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Apply discount to price.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* discountedPrice({ price: 100.0, discount: 0.2 }) // => 80.0
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* discountedPrice({ price: 100.0, discount: 0 }) // => 100.0 (no discount)
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* discountedPrice({ price: 100.0, discount: 1 }) // => 0.0 (full discount)
|
|
36
|
+
*/
|
|
37
|
+
export function discountedPrice(
|
|
38
|
+
input: z.infer<typeof DiscountedPriceInput>
|
|
39
|
+
): number {
|
|
40
|
+
const { price, discount } = DiscountedPriceInput.parse(input);
|
|
41
|
+
const result = price * (1 - discount);
|
|
42
|
+
return DiscountedPriceOutput.parse(result);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// GOOD: List Processing with Length Constraint
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Precondition: items.length > 0
|
|
51
|
+
* Postcondition: result is finite (no NaN/Infinity)
|
|
52
|
+
*/
|
|
53
|
+
const AverageInput = z.array(z.number()).nonempty();
|
|
54
|
+
const AverageOutput = z.number().finite();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Calculate average of non-empty array.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* average([1, 2, 3]) // => 2.0
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* average([5]) // => 5.0 (single element)
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* average([0, 0, 0]) // => 0.0 (all zeros)
|
|
67
|
+
*/
|
|
68
|
+
export function average(items: number[]): number {
|
|
69
|
+
const validated = AverageInput.parse(items);
|
|
70
|
+
const sum = validated.reduce((a, b) => a + b, 0);
|
|
71
|
+
const result = sum / validated.length;
|
|
72
|
+
return AverageOutput.parse(result);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// GOOD: Object Transformation
|
|
77
|
+
// =============================================================================
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Precondition: Object.keys(data).length > 0
|
|
81
|
+
* Postcondition: Object.keys(result).length > 0
|
|
82
|
+
*/
|
|
83
|
+
const NormalizeKeysInput = z.record(z.string(), z.number()).refine(
|
|
84
|
+
(obj) => Object.keys(obj).length > 0,
|
|
85
|
+
{ message: 'Object must have at least one key' }
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const NormalizeKeysOutput = z.record(z.string(), z.number()).refine(
|
|
89
|
+
(obj) => Object.keys(obj).length > 0,
|
|
90
|
+
{ message: 'Result must have at least one key' }
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Lowercase all keys.
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* normalizeKeys({ A: 1, B: 2 }) // => { a: 1, b: 2 }
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* normalizeKeys({ X: 10 }) // => { x: 10 }
|
|
101
|
+
*/
|
|
102
|
+
export function normalizeKeys(
|
|
103
|
+
data: Record<string, number>
|
|
104
|
+
): Record<string, number> {
|
|
105
|
+
const validated = NormalizeKeysInput.parse(data);
|
|
106
|
+
const result = Object.fromEntries(
|
|
107
|
+
Object.entries(validated).map(([k, v]) => [k.toLowerCase(), v])
|
|
108
|
+
);
|
|
109
|
+
return NormalizeKeysOutput.parse(result);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// BAD: Incomplete Contract (anti-pattern)
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
// DON'T: Schema that accepts anything
|
|
117
|
+
// const BadInput = z.any();
|
|
118
|
+
// function process(x: unknown) { ... }
|
|
119
|
+
|
|
120
|
+
// DON'T: Missing edge cases in examples
|
|
121
|
+
// function divide(a: number, b: number) {
|
|
122
|
+
// // @example divide(10, 2) // => 5.0
|
|
123
|
+
// // Missing: what about b=0?
|
|
124
|
+
// }
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// GOOD: Multiple Preconditions
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Precondition: start >= 0
|
|
132
|
+
* Precondition: end >= start
|
|
133
|
+
* Postcondition: result >= 0
|
|
134
|
+
*/
|
|
135
|
+
const RangeSizeInput = z.object({
|
|
136
|
+
start: z.number().nonnegative(),
|
|
137
|
+
end: z.number(),
|
|
138
|
+
}).refine(
|
|
139
|
+
(data) => data.end >= data.start,
|
|
140
|
+
{ message: 'end must be >= start' }
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const RangeSizeOutput = z.number().nonnegative();
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Calculate size of range [start, end).
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* rangeSize({ start: 0, end: 10 }) // => 10
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* rangeSize({ start: 5, end: 5 }) // => 0 (empty range)
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* rangeSize({ start: 0, end: 1 }) // => 1 (single element)
|
|
156
|
+
*/
|
|
157
|
+
export function rangeSize(
|
|
158
|
+
input: z.infer<typeof RangeSizeInput>
|
|
159
|
+
): number {
|
|
160
|
+
const { start, end } = RangeSizeInput.parse(input);
|
|
161
|
+
const result = end - start;
|
|
162
|
+
return RangeSizeOutput.parse(result);
|
|
163
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invar Core/Shell Separation Examples (TypeScript)
|
|
3
|
+
*
|
|
4
|
+
* Reference patterns for Core vs Shell architecture.
|
|
5
|
+
* Managed by Invar - do not edit directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { Result, ok, err } from 'neverthrow';
|
|
10
|
+
import * as fs from 'fs/promises';
|
|
11
|
+
|
|
12
|
+
// =============================================================================
|
|
13
|
+
// CORE: Pure Logic (no I/O)
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Location: src/*/core/
|
|
16
|
+
// Requirements: Zod schemas, pure functions, no I/O imports
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Precondition: content is defined (can be empty string)
|
|
21
|
+
* Postcondition: all lines are trimmed and non-empty
|
|
22
|
+
*/
|
|
23
|
+
const ParseLinesInput = z.string();
|
|
24
|
+
const ParseLinesOutput = z.array(z.string().min(1));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse content into non-empty lines.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* parseLines("a\nb\nc") // => ["a", "b", "c"]
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* parseLines("") // => []
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* parseLines(" \n ") // => [] (whitespace only)
|
|
37
|
+
*/
|
|
38
|
+
export function parseLines(content: string): string[] {
|
|
39
|
+
const validated = ParseLinesInput.parse(content);
|
|
40
|
+
const result = validated
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map(line => line.trim())
|
|
43
|
+
.filter(line => line.length > 0);
|
|
44
|
+
return ParseLinesOutput.parse(result);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Precondition: all items are strings
|
|
49
|
+
* Postcondition: all counts are positive
|
|
50
|
+
*/
|
|
51
|
+
const CountItemsInput = z.array(z.string());
|
|
52
|
+
const CountItemsOutput = z.record(z.string(), z.number().positive());
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Count occurrences of each item.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* countItems(["a", "b", "a"]) // => { a: 2, b: 1 }
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* countItems([]) // => {}
|
|
62
|
+
*/
|
|
63
|
+
export function countItems(items: string[]): Record<string, number> {
|
|
64
|
+
const validated = CountItemsInput.parse(items);
|
|
65
|
+
const counts: Record<string, number> = {};
|
|
66
|
+
for (const item of validated) {
|
|
67
|
+
counts[item] = (counts[item] ?? 0) + 1;
|
|
68
|
+
}
|
|
69
|
+
// Note: Output validation skipped for empty result (no positive numbers)
|
|
70
|
+
if (Object.keys(counts).length === 0) {
|
|
71
|
+
return counts;
|
|
72
|
+
}
|
|
73
|
+
return CountItemsOutput.parse(counts);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// =============================================================================
|
|
77
|
+
// SHELL: I/O Operations
|
|
78
|
+
// =============================================================================
|
|
79
|
+
// Location: src/*/shell/
|
|
80
|
+
// Requirements: Result<T, E> return type, calls Core for logic
|
|
81
|
+
// =============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Error types for file operations.
|
|
85
|
+
*/
|
|
86
|
+
export class FileNotFoundError extends Error {
|
|
87
|
+
constructor(path: string) {
|
|
88
|
+
super(`File not found: ${path}`);
|
|
89
|
+
this.name = 'FileNotFoundError';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class PermissionError extends Error {
|
|
94
|
+
constructor(path: string) {
|
|
95
|
+
super(`Permission denied: ${path}`);
|
|
96
|
+
this.name = 'PermissionError';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type FileError = FileNotFoundError | PermissionError | Error;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Read file content.
|
|
104
|
+
*
|
|
105
|
+
* Shell handles I/O, returns Result for error handling.
|
|
106
|
+
*/
|
|
107
|
+
export async function readFile(
|
|
108
|
+
path: string
|
|
109
|
+
): Promise<Result<string, FileError>> {
|
|
110
|
+
try {
|
|
111
|
+
const content = await fs.readFile(path, 'utf-8');
|
|
112
|
+
return ok(content);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof Error) {
|
|
115
|
+
if ('code' in error && error.code === 'ENOENT') {
|
|
116
|
+
return err(new FileNotFoundError(path));
|
|
117
|
+
}
|
|
118
|
+
if ('code' in error && error.code === 'EACCES') {
|
|
119
|
+
return err(new PermissionError(path));
|
|
120
|
+
}
|
|
121
|
+
return err(error);
|
|
122
|
+
}
|
|
123
|
+
return err(new Error(String(error)));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Count lines in file - demonstrates Core/Shell integration.
|
|
129
|
+
*
|
|
130
|
+
* Shell reads file -> Core parses content -> Shell returns result.
|
|
131
|
+
*/
|
|
132
|
+
export async function countLinesInFile(
|
|
133
|
+
path: string
|
|
134
|
+
): Promise<Result<Record<string, number>, FileError>> {
|
|
135
|
+
// Shell: I/O operation
|
|
136
|
+
const contentResult = await readFile(path);
|
|
137
|
+
|
|
138
|
+
if (contentResult.isErr()) {
|
|
139
|
+
return err(contentResult.error);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const content = contentResult.value;
|
|
143
|
+
|
|
144
|
+
// Core: Pure logic (no I/O)
|
|
145
|
+
const lines = parseLines(content);
|
|
146
|
+
const counts = countItems(lines);
|
|
147
|
+
|
|
148
|
+
// Shell: Return result
|
|
149
|
+
return ok(counts);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// =============================================================================
|
|
153
|
+
// ANTI-PATTERNS
|
|
154
|
+
// =============================================================================
|
|
155
|
+
|
|
156
|
+
// DON'T: I/O in Core
|
|
157
|
+
// function parseFile(path: string) { // BAD: path in Core
|
|
158
|
+
// const content = fs.readFileSync(path); // BAD: I/O in Core
|
|
159
|
+
// return parseLines(content);
|
|
160
|
+
// }
|
|
161
|
+
|
|
162
|
+
// DO: Core receives content, not paths
|
|
163
|
+
// function parseContent(content: string) { // GOOD: receives data
|
|
164
|
+
// return parseLines(content);
|
|
165
|
+
// }
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
// DON'T: Throw exceptions in Shell
|
|
169
|
+
// async function loadConfig(path: string): Promise<Config> { // BAD: no Result
|
|
170
|
+
// return JSON.parse(await fs.readFile(path)); // Exceptions not handled
|
|
171
|
+
// }
|
|
172
|
+
|
|
173
|
+
// DO: Return Result<T, E>
|
|
174
|
+
// async function loadConfig(path: string): Promise<Result<Config, Error>> {
|
|
175
|
+
// try {
|
|
176
|
+
// const content = await fs.readFile(path, 'utf-8');
|
|
177
|
+
// return ok(JSON.parse(content));
|
|
178
|
+
// } catch (error) {
|
|
179
|
+
// return err(error instanceof Error ? error : new Error(String(error)));
|
|
180
|
+
// }
|
|
181
|
+
// }
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// Next.js Integration Pattern
|
|
186
|
+
// =============================================================================
|
|
187
|
+
// Demonstrates how to use Result with Next.js API routes and Server Components.
|
|
188
|
+
// Shell (API handler) → Core (business logic) → Shell (HTTP response)
|
|
189
|
+
|
|
190
|
+
// NOTE: This is pseudocode - requires Next.js to be installed
|
|
191
|
+
// import { NextRequest, NextResponse } from 'next/server';
|
|
192
|
+
// import { ResultAsync } from 'neverthrow';
|
|
193
|
+
|
|
194
|
+
// -----------------------------------------------------------------------------
|
|
195
|
+
// Error Types
|
|
196
|
+
// -----------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
// interface ApiError {
|
|
199
|
+
// readonly code: string;
|
|
200
|
+
// readonly message: string;
|
|
201
|
+
// readonly status: number;
|
|
202
|
+
// }
|
|
203
|
+
//
|
|
204
|
+
// const NotFoundError = (id: string): ApiError => ({
|
|
205
|
+
// code: 'not_found',
|
|
206
|
+
// message: `Resource ${id} not found`,
|
|
207
|
+
// status: 404,
|
|
208
|
+
// });
|
|
209
|
+
//
|
|
210
|
+
// const ValidationError = (message: string): ApiError => ({
|
|
211
|
+
// code: 'validation_error',
|
|
212
|
+
// message,
|
|
213
|
+
// status: 400,
|
|
214
|
+
// });
|
|
215
|
+
//
|
|
216
|
+
// const InternalError = (message: string): ApiError => ({
|
|
217
|
+
// code: 'internal_error',
|
|
218
|
+
// message,
|
|
219
|
+
// status: 500,
|
|
220
|
+
// });
|
|
221
|
+
|
|
222
|
+
// -----------------------------------------------------------------------------
|
|
223
|
+
// CORE: Pure business logic (no Next.js imports)
|
|
224
|
+
// -----------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
// const UserSchema = z.object({
|
|
227
|
+
// id: z.string().min(1),
|
|
228
|
+
// name: z.string().min(1),
|
|
229
|
+
// email: z.string().email(),
|
|
230
|
+
// });
|
|
231
|
+
//
|
|
232
|
+
// type User = z.infer<typeof UserSchema>;
|
|
233
|
+
//
|
|
234
|
+
// /**
|
|
235
|
+
// * Core: Validate user data (pure, no I/O).
|
|
236
|
+
// */
|
|
237
|
+
// function validateUserData(data: unknown): Result<User, string> {
|
|
238
|
+
// const result = UserSchema.safeParse(data);
|
|
239
|
+
// if (!result.success) {
|
|
240
|
+
// return err(result.error.errors.map(e => e.message).join(', '));
|
|
241
|
+
// }
|
|
242
|
+
// return ok(result.data);
|
|
243
|
+
// }
|
|
244
|
+
|
|
245
|
+
// -----------------------------------------------------------------------------
|
|
246
|
+
// SHELL: Database I/O layer
|
|
247
|
+
// -----------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
// /**
|
|
250
|
+
// * Shell: Fetch user from database.
|
|
251
|
+
// */
|
|
252
|
+
// function fetchUserFromDb(id: string): ResultAsync<User, ApiError> {
|
|
253
|
+
// return ResultAsync.fromPromise(
|
|
254
|
+
// prisma.user.findUnique({ where: { id } }).then(user => {
|
|
255
|
+
// if (!user) throw new Error('not_found');
|
|
256
|
+
// return user;
|
|
257
|
+
// }),
|
|
258
|
+
// (error): ApiError => {
|
|
259
|
+
// if (error instanceof Error && error.message === 'not_found') {
|
|
260
|
+
// return NotFoundError(id);
|
|
261
|
+
// }
|
|
262
|
+
// return InternalError('Database error');
|
|
263
|
+
// }
|
|
264
|
+
// );
|
|
265
|
+
// }
|
|
266
|
+
|
|
267
|
+
// -----------------------------------------------------------------------------
|
|
268
|
+
// SHELL: Next.js API Route (App Router)
|
|
269
|
+
// -----------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
// /**
|
|
272
|
+
// * Shell: API route handler.
|
|
273
|
+
// *
|
|
274
|
+
// * Pattern: Shell → Core → Shell
|
|
275
|
+
// * 1. Shell receives HTTP request
|
|
276
|
+
// * 2. Core validates/processes
|
|
277
|
+
// * 3. Shell converts Result to HTTP response
|
|
278
|
+
// */
|
|
279
|
+
// export async function GET(
|
|
280
|
+
// request: NextRequest,
|
|
281
|
+
// { params }: { params: { id: string } }
|
|
282
|
+
// ) {
|
|
283
|
+
// const result = await fetchUserFromDb(params.id);
|
|
284
|
+
//
|
|
285
|
+
// // Convert Result to NextResponse
|
|
286
|
+
// return result.match(
|
|
287
|
+
// (user) => NextResponse.json(user),
|
|
288
|
+
// (error) => NextResponse.json(
|
|
289
|
+
// { error: error.message },
|
|
290
|
+
// { status: error.status }
|
|
291
|
+
// )
|
|
292
|
+
// );
|
|
293
|
+
// }
|
|
294
|
+
//
|
|
295
|
+
// export async function POST(request: NextRequest) {
|
|
296
|
+
// const body = await request.json();
|
|
297
|
+
//
|
|
298
|
+
// // Core: validate
|
|
299
|
+
// const validationResult = validateUserData(body);
|
|
300
|
+
// if (validationResult.isErr()) {
|
|
301
|
+
// return NextResponse.json(
|
|
302
|
+
// { error: validationResult.error },
|
|
303
|
+
// { status: 400 }
|
|
304
|
+
// );
|
|
305
|
+
// }
|
|
306
|
+
//
|
|
307
|
+
// // Shell: save to DB
|
|
308
|
+
// const saveResult = await saveUserToDb(validationResult.value);
|
|
309
|
+
//
|
|
310
|
+
// return saveResult.match(
|
|
311
|
+
// (user) => NextResponse.json(user, { status: 201 }),
|
|
312
|
+
// (error) => NextResponse.json(
|
|
313
|
+
// { error: error.message },
|
|
314
|
+
// { status: error.status }
|
|
315
|
+
// )
|
|
316
|
+
// );
|
|
317
|
+
// }
|
|
318
|
+
|
|
319
|
+
// -----------------------------------------------------------------------------
|
|
320
|
+
// SHELL: React Server Component
|
|
321
|
+
// -----------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
// /**
|
|
324
|
+
// * Shell: Server Component with Result handling.
|
|
325
|
+
// *
|
|
326
|
+
// * Server Components can call Shell functions directly.
|
|
327
|
+
// * Use .match() to handle success/error rendering.
|
|
328
|
+
// */
|
|
329
|
+
// export async function UserProfile({ userId }: { userId: string }) {
|
|
330
|
+
// const result = await fetchUserFromDb(userId);
|
|
331
|
+
//
|
|
332
|
+
// return result.match(
|
|
333
|
+
// (user) => (
|
|
334
|
+
// <div>
|
|
335
|
+
// <h1>{user.name}</h1>
|
|
336
|
+
// <p>{user.email}</p>
|
|
337
|
+
// </div>
|
|
338
|
+
// ),
|
|
339
|
+
// (error) => (
|
|
340
|
+
// <div className="error">
|
|
341
|
+
// {error.code === 'not_found'
|
|
342
|
+
// ? <p>User not found</p>
|
|
343
|
+
// : <p>Something went wrong</p>
|
|
344
|
+
// }
|
|
345
|
+
// </div>
|
|
346
|
+
// )
|
|
347
|
+
// );
|
|
348
|
+
// }
|
|
349
|
+
|
|
350
|
+
// =============================================================================
|
|
351
|
+
// Result → HTTP Response Mapping
|
|
352
|
+
// =============================================================================
|
|
353
|
+
// Common pattern for converting Result errors to HTTP status codes:
|
|
354
|
+
//
|
|
355
|
+
// | Error Type | HTTP Status | When to Use |
|
|
356
|
+
// |-----------------|-------------|--------------------------------|
|
|
357
|
+
// | NotFoundError | 404 | Resource doesn't exist |
|
|
358
|
+
// | ValidationError | 400 | Invalid input from client |
|
|
359
|
+
// | AuthError | 401/403 | Authentication/authorization |
|
|
360
|
+
// | ConflictError | 409 | Resource state conflict |
|
|
361
|
+
// | InternalError | 500 | Unexpected server error |
|
|
362
|
+
//
|
|
363
|
+
// Helper function:
|
|
364
|
+
// function resultToResponse<T>(
|
|
365
|
+
// result: Result<T, ApiError>
|
|
366
|
+
// ): NextResponse {
|
|
367
|
+
// return result.match(
|
|
368
|
+
// (value) => NextResponse.json(value),
|
|
369
|
+
// (error) => NextResponse.json(
|
|
370
|
+
// { code: error.code, message: error.message },
|
|
371
|
+
// { status: error.status }
|
|
372
|
+
// )
|
|
373
|
+
// );
|
|
374
|
+
// }
|