feat: add TypeScript generation options to generate-types tool
Add configurable options for customizing TypeScript output: - enumAsUnion/enumAsEnum: control enum generation style - interfacePrefix/interfaceSuffix: naming conventions for interfaces - indentation: 2 spaces, 4 spaces, or tab Includes validation for mutually exclusive options and valid TypeScript identifier prefixes/suffixes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1feae7a91d
commit
a4fc2df4ea
6 changed files with 767 additions and 23 deletions
28
CLAUDE.md
28
CLAUDE.md
|
|
@ -35,7 +35,7 @@ Restart Claude Code after configuration.
|
||||||
| `validate-spec` | Validate against schema | `path` |
|
| `validate-spec` | Validate against schema | `path` |
|
||||||
| `query-endpoints` | Search/filter endpoints | `path`, `method?`, `pathPattern?`, `tag?` |
|
| `query-endpoints` | Search/filter endpoints | `path`, `method?`, `pathPattern?`, `tag?` |
|
||||||
| `get-schema` | Get schema details | `path`, `schemaName` |
|
| `get-schema` | Get schema details | `path`, `schemaName` |
|
||||||
| `generate-types` | Generate TypeScript | `path`, `schemas?` |
|
| `generate-types` | Generate TypeScript | `path`, `schemas?`, `options?` |
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
|
|
@ -70,6 +70,32 @@ Converts .NET-style schema names to valid TypeScript:
|
||||||
"Company.Api.V5.ViewModel" → "ViewModel"
|
"Company.Api.V5.ViewModel" → "ViewModel"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### TypeScript Generation Options
|
||||||
|
The `generate-types` tool supports customization via `options`:
|
||||||
|
|
||||||
|
| Option | Type | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `enumAsUnion` | boolean | Generate enums as union types (default) |
|
||||||
|
| `enumAsEnum` | boolean | Generate enums as TypeScript enums |
|
||||||
|
| `interfacePrefix` | string | Prefix for interface names (e.g., "I") |
|
||||||
|
| `interfaceSuffix` | string | Suffix for interface names (e.g., "Type") |
|
||||||
|
| `indentation` | '2'\|'4'\|'tab' | Indentation style (default: '2') |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "generate-types",
|
||||||
|
"arguments": {
|
||||||
|
"path": "api.yaml",
|
||||||
|
"options": {
|
||||||
|
"enumAsEnum": true,
|
||||||
|
"interfacePrefix": "I",
|
||||||
|
"indentation": "4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
470
docs/analysis/feature-enhancements-analysis.md
Normal file
470
docs/analysis/feature-enhancements-analysis.md
Normal file
|
|
@ -0,0 +1,470 @@
|
||||||
|
# Analysis: Feature Enhancements (Caching, OpenAPI 3.1, Batch, TypeScript Options)
|
||||||
|
|
||||||
|
## Scope Affected
|
||||||
|
- [x] Backend
|
||||||
|
- [ ] Frontend
|
||||||
|
- [ ] Database
|
||||||
|
- [ ] Infrastructure
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Analysis of four proposed feature enhancements for the swagger-tools MCP server: remote spec caching, OpenAPI 3.1 support improvements, batch operations, and TypeScript generation options. The current codebase is well-structured with clear separation of concerns, making these enhancements straightforward to implement.
|
||||||
|
|
||||||
|
## Files Involved
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/index.ts` | MCP server entry point - tool registration |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/lib/parser.ts` | Spec parsing with dereference fallback |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/lib/validator.ts` | Spec validation wrapper |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/lib/types.ts` | TypeScript interfaces for parsed data |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/tools/parse.ts` | parse-spec tool handler |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/tools/validate.ts` | validate-spec tool handler |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/tools/query.ts` | query-endpoints tool handler |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/tools/schema.ts` | get-schema tool handler |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/tools/generate.ts` | generate-types tool with sanitizeName() |
|
||||||
|
| `/Users/rimskij/projects/swagger-tools/src/utils/format.ts` | Human-readable output formatters |
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.ts # Entry: registers 5 tools, starts stdio server
|
||||||
|
├── lib/
|
||||||
|
│ ├── parser.ts # parseSpec() - dereference with fallback
|
||||||
|
│ ├── validator.ts # validateSpec(), validateWithWarnings()
|
||||||
|
│ └── types.ts # TypeScript interfaces
|
||||||
|
├── tools/
|
||||||
|
│ ├── parse.ts # Calls parseSpec(), returns metadata
|
||||||
|
│ ├── validate.ts # Calls validateWithWarnings()
|
||||||
|
│ ├── query.ts # Filters endpoints from parsed spec
|
||||||
|
│ ├── schema.ts # Looks up schema by name
|
||||||
|
│ └── generate.ts # Converts schemas to TypeScript
|
||||||
|
└── utils/
|
||||||
|
└── format.ts # Markdown formatters
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution Flow
|
||||||
|
|
||||||
|
1. **Entry**: MCP client sends JSON-RPC request via stdio
|
||||||
|
2. **Routing**: McpServer routes to registered tool handler
|
||||||
|
3. **Parsing**: Tool handler calls `parseSpec()` with spec path
|
||||||
|
4. **Processing**: Tool-specific logic (filter, validate, generate)
|
||||||
|
5. **Response**: Returns `{content: [...], structuredContent: {...}}`
|
||||||
|
|
||||||
|
### Key Dependencies
|
||||||
|
|
||||||
|
- `@apidevtools/swagger-parser` v10.1.1 - Supports OpenAPI 3.1 (with workarounds)
|
||||||
|
- `@modelcontextprotocol/sdk` v1.25.2 - MCP protocol
|
||||||
|
- `openapi-types` v12.1.3 - TypeScript types (OpenAPIV3_1 namespace)
|
||||||
|
- `zod` v3.23.0 - Schema validation for tool parameters
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 1: Caching for Remote Specs
|
||||||
|
|
||||||
|
### Current Behavior
|
||||||
|
|
||||||
|
Every tool call to `parseSpec()` fetches and parses the spec fresh:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/parser.ts:7-11
|
||||||
|
export async function parseSpec(specPath: string, options?: { dereference?: boolean }): Promise<{...}>
|
||||||
|
// Always fetches/reads from scratch
|
||||||
|
spec = await SwaggerParser.dereference(specPath);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proposed Solution
|
||||||
|
|
||||||
|
Add an in-memory LRU cache in `src/lib/parser.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// New file: src/lib/cache.ts
|
||||||
|
interface CacheEntry {
|
||||||
|
spec: OpenAPISpec;
|
||||||
|
metadata: ParsedSpec;
|
||||||
|
dereferenced: boolean;
|
||||||
|
timestamp: number;
|
||||||
|
etag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpecCache {
|
||||||
|
private cache: Map<string, CacheEntry>;
|
||||||
|
private maxSize: number;
|
||||||
|
private ttlMs: number;
|
||||||
|
|
||||||
|
constructor(maxSize = 10, ttlMinutes = 15) {...}
|
||||||
|
get(key: string): CacheEntry | undefined {...}
|
||||||
|
set(key: string, entry: CacheEntry): void {...}
|
||||||
|
invalidate(key?: string): void {...}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `src/lib/cache.ts` | NEW: LRU cache with TTL |
|
||||||
|
| `src/lib/parser.ts` | Import cache, wrap parseSpec |
|
||||||
|
| `src/tools/parse.ts` | Add optional `noCache` parameter |
|
||||||
|
| `src/tools/validate.ts` | Add optional `noCache` parameter |
|
||||||
|
| `src/tools/query.ts` | Add optional `noCache` parameter |
|
||||||
|
| `src/tools/schema.ts` | Add optional `noCache` parameter |
|
||||||
|
| `src/tools/generate.ts` | Add optional `noCache` parameter |
|
||||||
|
|
||||||
|
### Cache Key Strategy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// For remote URLs: normalize URL (remove trailing slash, sort query params)
|
||||||
|
// For local files: use absolute path + file mtime
|
||||||
|
function getCacheKey(specPath: string): string {
|
||||||
|
if (specPath.startsWith('http://') || specPath.startsWith('https://')) {
|
||||||
|
return normalizeUrl(specPath);
|
||||||
|
}
|
||||||
|
const resolved = path.resolve(specPath);
|
||||||
|
const stat = fs.statSync(resolved);
|
||||||
|
return `${resolved}:${stat.mtimeMs}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
| Risk | Impact | Mitigation |
|
||||||
|
|------|--------|------------|
|
||||||
|
| Stale cache for rapidly changing specs | MEDIUM | Add `noCache` param, short TTL (15 min) |
|
||||||
|
| Memory pressure with large specs | MEDIUM | LRU eviction, max 10 entries |
|
||||||
|
| Local file changes not detected | LOW | Include mtime in cache key |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 2: OpenAPI 3.1 Support
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
|
||||||
|
**Good news**: The underlying `@apidevtools/swagger-parser` v10.1.1 already supports OpenAPI 3.1:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// node_modules/@apidevtools/swagger-parser/lib/index.js
|
||||||
|
const supported31Versions = ["3.1.0", "3.1.1"];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gap**: The TypeScript generation in `generate.ts` doesn't handle 3.1-specific JSON Schema features.
|
||||||
|
|
||||||
|
### OpenAPI 3.1 Differences from 3.0
|
||||||
|
|
||||||
|
| Feature | 3.0 | 3.1 | Code Impact |
|
||||||
|
|---------|-----|-----|-------------|
|
||||||
|
| Schema dialect | OpenAPI Schema | JSON Schema 2020-12 | Type generation |
|
||||||
|
| `type` field | Single string | Array allowed | schemaToType() |
|
||||||
|
| `nullable` | `nullable: true` | `type: ['string', 'null']` | schemaToType() |
|
||||||
|
| `const` | Not supported | Supported | schemaToType() |
|
||||||
|
| `exclusiveMinimum` | boolean | number | Not TS-relevant |
|
||||||
|
| `webhooks` | Not supported | Top-level field | metadata extraction |
|
||||||
|
| `$ref` siblings | Not allowed | Allowed with summary/desc | Reference handling |
|
||||||
|
| `pathItems` | Not in components | In components | Schema lookup |
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
|
||||||
|
1. **Detect 3.1 version** in parser.ts metadata extraction
|
||||||
|
2. **Handle array types** in generate.ts:
|
||||||
|
```typescript
|
||||||
|
// type: ['string', 'null'] -> string | null
|
||||||
|
if (Array.isArray(s.type)) {
|
||||||
|
return s.type.map(t => t === 'null' ? 'null' : mapPrimitiveType(t)).join(' | ');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. **Handle `const`** in generate.ts:
|
||||||
|
```typescript
|
||||||
|
if (s.const !== undefined) {
|
||||||
|
return typeof s.const === 'string' ? `'${s.const}'` : String(s.const);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
4. **Extract webhooks** in parser.ts metadata
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `src/lib/parser.ts` | Add webhook count to metadata |
|
||||||
|
| `src/lib/types.ts` | Add `webhookCount?: number` to ParsedSpec |
|
||||||
|
| `src/tools/generate.ts` | Handle 3.1 type arrays, const |
|
||||||
|
| `src/utils/format.ts` | Display webhook count |
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
| Risk | Impact | Mitigation |
|
||||||
|
|------|--------|------------|
|
||||||
|
| Array type combinations complex | LOW | Map each type, join with `\|` |
|
||||||
|
| `const` type inference tricky | LOW | Use literal types |
|
||||||
|
| Existing 3.0 specs break | LOW | Changes are additive |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 3: Batch Operations
|
||||||
|
|
||||||
|
### Proposed Design
|
||||||
|
|
||||||
|
Add a new tool `batch-process` that accepts multiple specs:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/tools/batch.ts
|
||||||
|
export const batchToolSchema = {
|
||||||
|
paths: z.array(z.string()).describe('Paths to OpenAPI specs'),
|
||||||
|
operation: z.enum(['parse', 'validate']).describe('Operation to perform'),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Options
|
||||||
|
|
||||||
|
**Option A: Sequential Processing** (Simpler)
|
||||||
|
```typescript
|
||||||
|
const results = [];
|
||||||
|
for (const path of paths) {
|
||||||
|
results.push(await parseSpec(path));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option B: Parallel Processing** (Faster)
|
||||||
|
```typescript
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
paths.map(path => parseSpec(path))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Approach
|
||||||
|
|
||||||
|
Use **Option B** with concurrency limit:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/batch.ts
|
||||||
|
async function batchProcess<T>(
|
||||||
|
items: string[],
|
||||||
|
processor: (item: string) => Promise<T>,
|
||||||
|
concurrency = 5
|
||||||
|
): Promise<Array<{ path: string; result?: T; error?: string }>> {
|
||||||
|
const results: Array<...> = [];
|
||||||
|
const executing: Promise<void>[] = [];
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const p = processor(item)
|
||||||
|
.then(result => results.push({ path: item, result }))
|
||||||
|
.catch(error => results.push({ path: item, error: error.message }));
|
||||||
|
|
||||||
|
executing.push(p);
|
||||||
|
if (executing.length >= concurrency) {
|
||||||
|
await Promise.race(executing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(executing);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `src/lib/batch.ts` | NEW: Parallel processing utility |
|
||||||
|
| `src/tools/batch.ts` | NEW: batch-process tool |
|
||||||
|
| `src/index.ts` | Register batch-process tool |
|
||||||
|
| `src/utils/format.ts` | Add formatBatchResults() |
|
||||||
|
|
||||||
|
### Output Format
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface BatchResult {
|
||||||
|
success: boolean;
|
||||||
|
total: number;
|
||||||
|
succeeded: number;
|
||||||
|
failed: number;
|
||||||
|
results: Array<{
|
||||||
|
path: string;
|
||||||
|
success: boolean;
|
||||||
|
data?: ParsedSpec | ValidationResult;
|
||||||
|
error?: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
| Risk | Impact | Mitigation |
|
||||||
|
|------|--------|------------|
|
||||||
|
| Resource exhaustion with many specs | HIGH | Concurrency limit (5) |
|
||||||
|
| One failure stops all | LOW | Promise.allSettled continues |
|
||||||
|
| Output too large | MEDIUM | Summarize in text, full in structured |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature 4: TypeScript Generation Options
|
||||||
|
|
||||||
|
### Current Behavior
|
||||||
|
|
||||||
|
`generate-types` produces a fixed output format:
|
||||||
|
- `export interface` for objects
|
||||||
|
- `export type` for enums, arrays, primitives
|
||||||
|
- JSDoc comments for descriptions
|
||||||
|
- Properties marked optional with `?`
|
||||||
|
|
||||||
|
### Proposed Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export const generateToolSchema = {
|
||||||
|
path: z.string().describe('Path to the OpenAPI/Swagger spec file'),
|
||||||
|
schemas: z.array(z.string()).optional(),
|
||||||
|
options: z.object({
|
||||||
|
// Output style
|
||||||
|
interfacePrefix: z.string().optional().describe('Prefix for interfaces (e.g., "I")'),
|
||||||
|
interfaceSuffix: z.string().optional().describe('Suffix for interfaces (e.g., "Type")'),
|
||||||
|
enumAsUnion: z.boolean().optional().describe('Generate enums as union types (default: true)'),
|
||||||
|
enumAsEnum: z.boolean().optional().describe('Generate enums as TypeScript enums'),
|
||||||
|
|
||||||
|
// Formatting
|
||||||
|
indentation: z.enum(['2', '4', 'tab']).optional().describe('Indentation style'),
|
||||||
|
semicolons: z.boolean().optional().describe('Add trailing semicolons'),
|
||||||
|
|
||||||
|
// Optionality
|
||||||
|
allOptional: z.boolean().optional().describe('Make all properties optional'),
|
||||||
|
allRequired: z.boolean().optional().describe('Make all properties required'),
|
||||||
|
|
||||||
|
// Advanced
|
||||||
|
readonlyProperties: z.boolean().optional().describe('Mark properties as readonly'),
|
||||||
|
exportDefault: z.boolean().optional().describe('Use default exports'),
|
||||||
|
includeRefs: z.boolean().optional().describe('Include $ref comments'),
|
||||||
|
}).optional(),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Priority
|
||||||
|
|
||||||
|
**Phase 1** (High value, low effort):
|
||||||
|
- `enumAsUnion` vs `enumAsEnum`
|
||||||
|
- `interfacePrefix` / `interfaceSuffix`
|
||||||
|
|
||||||
|
**Phase 2** (Medium value):
|
||||||
|
- `indentation` options
|
||||||
|
- `allOptional` / `allRequired`
|
||||||
|
|
||||||
|
**Phase 3** (Lower priority):
|
||||||
|
- `readonlyProperties`
|
||||||
|
- `includeRefs`
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `src/tools/generate.ts` | Add options parameter, conditional generation |
|
||||||
|
| `src/lib/types.ts` | Add TypeScriptOptions interface |
|
||||||
|
|
||||||
|
### Example Output Variations
|
||||||
|
|
||||||
|
**Default (current)**:
|
||||||
|
```typescript
|
||||||
|
export type PetStatus = 'available' | 'pending' | 'sold';
|
||||||
|
export interface Pet {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status?: PetStatus;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**With `enumAsEnum: true, interfacePrefix: 'I'`**:
|
||||||
|
```typescript
|
||||||
|
export enum PetStatus {
|
||||||
|
Available = 'available',
|
||||||
|
Pending = 'pending',
|
||||||
|
Sold = 'sold'
|
||||||
|
}
|
||||||
|
export interface IPet {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
status?: PetStatus;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
| Risk | Impact | Mitigation |
|
||||||
|
|------|--------|------------|
|
||||||
|
| Option combinations create invalid TS | LOW | Validate combinations |
|
||||||
|
| Too many options confuse users | MEDIUM | Good defaults, clear docs |
|
||||||
|
| Breaking existing behavior | LOW | All options optional with current defaults |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Upstream (What calls this)
|
||||||
|
- MCP Clients (Claude Code, etc.) call tools via JSON-RPC
|
||||||
|
- All tools use `parseSpec()` from lib/parser.ts
|
||||||
|
|
||||||
|
### Downstream (What this calls)
|
||||||
|
- `@apidevtools/swagger-parser` for spec parsing/validation
|
||||||
|
- Node.js `fs` module for local file access
|
||||||
|
- Node.js `https`/`http` for remote fetch (via swagger-parser)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
### Caching
|
||||||
|
- Spec URL returns 304 Not Modified - should reuse cache
|
||||||
|
- Local file deleted between cache set and get - invalidate on error
|
||||||
|
- Concurrent requests for same URL - deduplicate in-flight requests
|
||||||
|
|
||||||
|
### OpenAPI 3.1
|
||||||
|
- Mixed array types: `type: ['string', 'integer', 'null']`
|
||||||
|
- `const` with complex objects: `const: {foo: 'bar'}`
|
||||||
|
- `$ref` with sibling `description` - merge into output
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
- Empty paths array - return immediately with success
|
||||||
|
- Mix of local and remote paths - process in parallel
|
||||||
|
- Single path failure mid-batch - continue, report in results
|
||||||
|
|
||||||
|
### TypeScript Options
|
||||||
|
- Invalid combination: `enumAsUnion` + `enumAsEnum` both true
|
||||||
|
- Schema name collision after prefix/suffix added
|
||||||
|
- Circular references with option changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
1. **Caching**
|
||||||
|
- Unit test cache hit/miss
|
||||||
|
- Test TTL expiration
|
||||||
|
- Test LRU eviction
|
||||||
|
- Test local file mtime detection
|
||||||
|
|
||||||
|
2. **OpenAPI 3.1**
|
||||||
|
- Add 3.1 test fixture with webhooks, array types, const
|
||||||
|
- Test type generation for `type: ['string', 'null']`
|
||||||
|
- Test backward compatibility with 3.0 spec
|
||||||
|
|
||||||
|
3. **Batch Operations**
|
||||||
|
- Test with 0, 1, many specs
|
||||||
|
- Test mix of success and failure
|
||||||
|
- Test concurrency limit respected
|
||||||
|
|
||||||
|
4. **TypeScript Options**
|
||||||
|
- Snapshot tests for each option combination
|
||||||
|
- Test prefix/suffix application
|
||||||
|
- Test enum generation modes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order (Recommended)
|
||||||
|
|
||||||
|
1. **TypeScript Options** - Most user-visible, low risk
|
||||||
|
2. **Caching** - Performance win, moderate complexity
|
||||||
|
3. **OpenAPI 3.1** - Already mostly working, polish needed
|
||||||
|
4. **Batch Operations** - New tool, can be done independently
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- `/implement` - Proceed with implementation (specify which feature)
|
||||||
|
- `/create work` - Create detailed task documentation for each feature
|
||||||
|
|
||||||
133
docs/tasks/typescript-options-task.md
Normal file
133
docs/tasks/typescript-options-task.md
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
# Task: TypeScript Generation Options
|
||||||
|
|
||||||
|
## Related Documents
|
||||||
|
- Analysis: `docs/analysis/feature-enhancements-analysis.md`
|
||||||
|
- Branch: `feature/typescript-options` (from `dev`)
|
||||||
|
|
||||||
|
## Priority
|
||||||
|
NORMAL
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Add configurable options to the `generate-types` tool for customizing TypeScript output format, including enum style, interface naming conventions, and indentation preferences.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
- [x] Code implemented per specification
|
||||||
|
- [x] TypeScript compilation CLEAN
|
||||||
|
- [x] Lint checks PASSED (no lint script in project)
|
||||||
|
- [x] ALL tests passing
|
||||||
|
- [x] Manual verification with petstore spec
|
||||||
|
- [x] PROOF PROVIDED (before/after output samples)
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
### IN SCOPE
|
||||||
|
- `enumAsUnion` option (default true) - generate enums as union types
|
||||||
|
- `enumAsEnum` option - generate enums as TypeScript enums
|
||||||
|
- `interfacePrefix` option - prefix for interface names (e.g., "I")
|
||||||
|
- `interfaceSuffix` option - suffix for interface names (e.g., "Type")
|
||||||
|
- `indentation` option - 2 spaces, 4 spaces, or tab
|
||||||
|
- Validation of mutually exclusive options
|
||||||
|
|
||||||
|
### OUT OF SCOPE
|
||||||
|
- `readonlyProperties` option (future enhancement)
|
||||||
|
- `exportDefault` option (future enhancement)
|
||||||
|
- `includeRefs` option (future enhancement)
|
||||||
|
- `allOptional` / `allRequired` options (future enhancement)
|
||||||
|
|
||||||
|
## Sub-Tasks
|
||||||
|
|
||||||
|
### Phase 1: Options Interface and Schema
|
||||||
|
#### 1.1 Add TypeScriptOptions interface to types.ts
|
||||||
|
- **Details**: Define interface with all option properties as optional
|
||||||
|
- **Files**: `src/lib/types.ts`
|
||||||
|
- **Testing**: TypeScript compilation
|
||||||
|
|
||||||
|
#### 1.2 Update generate tool schema with options parameter
|
||||||
|
- **Details**: Add nested options object to Zod schema
|
||||||
|
- **Files**: `src/tools/generate.ts`
|
||||||
|
- **Testing**: Parse spec with invalid options should fail
|
||||||
|
|
||||||
|
### Phase 2: Enum Generation Modes
|
||||||
|
#### 2.1 Implement enumAsEnum generation
|
||||||
|
- **Details**: Convert `type X = 'a' | 'b'` to `enum X { A = 'a', B = 'b' }`
|
||||||
|
- **Files**: `src/tools/generate.ts`
|
||||||
|
- **Testing**: Generate with enumAsEnum=true
|
||||||
|
|
||||||
|
#### 2.2 Add mutual exclusion validation
|
||||||
|
- **Details**: Error if both enumAsUnion and enumAsEnum are true
|
||||||
|
- **Files**: `src/tools/generate.ts`
|
||||||
|
- **Testing**: Should reject invalid combination
|
||||||
|
|
||||||
|
### Phase 3: Interface Naming
|
||||||
|
#### 3.1 Implement prefix/suffix application
|
||||||
|
- **Details**: Apply prefix/suffix to interface names during generation
|
||||||
|
- **Files**: `src/tools/generate.ts`
|
||||||
|
- **Testing**: interfacePrefix='I' produces `interface IPet`
|
||||||
|
|
||||||
|
#### 3.2 Handle sanitized names with prefix/suffix
|
||||||
|
- **Details**: Ensure prefix/suffix works with .NET sanitized names
|
||||||
|
- **Files**: `src/tools/generate.ts`
|
||||||
|
- **Testing**: `Namespace.Type` with prefix='I' produces `IType`
|
||||||
|
|
||||||
|
### Phase 4: Indentation Options
|
||||||
|
#### 4.1 Implement indentation configuration
|
||||||
|
- **Details**: Support '2', '4', 'tab' options for generated code
|
||||||
|
- **Files**: `src/tools/generate.ts`
|
||||||
|
- **Testing**: Generate with each option, verify output
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
- `src/lib/types.ts`: Add `TypeScriptOptions` interface
|
||||||
|
- `src/tools/generate.ts`: Update schema, implement option handling in schemaToType() and generateTypes()
|
||||||
|
|
||||||
|
## Risks & Mitigations
|
||||||
|
|
||||||
|
| Risk | Impact | Mitigation |
|
||||||
|
|------|--------|------------|
|
||||||
|
| Invalid option combinations | LOW | Validate at handler entry, return clear error |
|
||||||
|
| Breaking existing behavior | LOW | All options optional with current defaults |
|
||||||
|
| Enum PascalCase conversion edge cases | MEDIUM | Use robust capitalizeFirst function |
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- Build: `npm run build` - must pass
|
||||||
|
- Manual: Test with petstore spec using various option combinations
|
||||||
|
- Verification commands:
|
||||||
|
```bash
|
||||||
|
# Default (current behavior)
|
||||||
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"generate-types","arguments":{"path":"https://petstore.swagger.io/v2/swagger.json","schemas":["Pet"]}}}' | node dist/index.js
|
||||||
|
|
||||||
|
# With enumAsEnum
|
||||||
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"generate-types","arguments":{"path":"https://petstore.swagger.io/v2/swagger.json","schemas":["Pet"],"options":{"enumAsEnum":true}}}}' | node dist/index.js
|
||||||
|
|
||||||
|
# With prefix
|
||||||
|
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"generate-types","arguments":{"path":"https://petstore.swagger.io/v2/swagger.json","schemas":["Pet"],"options":{"interfacePrefix":"I"}}}}' | node dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
### Enum to TypeScript Enum Conversion
|
||||||
|
```typescript
|
||||||
|
// Input: enum values ['available', 'pending', 'sold']
|
||||||
|
// Output:
|
||||||
|
export enum PetStatus {
|
||||||
|
Available = 'available',
|
||||||
|
Pending = 'pending',
|
||||||
|
Sold = 'sold'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use PascalCase for enum member names:
|
||||||
|
```typescript
|
||||||
|
function toPascalCase(value: string): string {
|
||||||
|
return value.replace(/(?:^|[_-])(\w)/g, (_, c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Indentation Implementation
|
||||||
|
```typescript
|
||||||
|
const INDENT = {
|
||||||
|
'2': ' ',
|
||||||
|
'4': ' ',
|
||||||
|
'tab': '\t'
|
||||||
|
};
|
||||||
|
const indent = INDENT[options.indentation ?? '2'];
|
||||||
|
```
|
||||||
|
|
@ -73,3 +73,16 @@ export interface SchemaInfo {
|
||||||
required?: string[];
|
required?: string[];
|
||||||
schema: object;
|
schema: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TypeScriptOptions {
|
||||||
|
/** Generate enums as union types (default: true) */
|
||||||
|
enumAsUnion?: boolean;
|
||||||
|
/** Generate enums as TypeScript enums */
|
||||||
|
enumAsEnum?: boolean;
|
||||||
|
/** Prefix for interface names (e.g., "I") */
|
||||||
|
interfacePrefix?: string;
|
||||||
|
/** Suffix for interface names (e.g., "Type") */
|
||||||
|
interfaceSuffix?: string;
|
||||||
|
/** Indentation style: '2' spaces, '4' spaces, or 'tab' */
|
||||||
|
indentation?: '2' | '4' | 'tab';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { parseSpec } from '../lib/parser.js';
|
import { parseSpec } from '../lib/parser.js';
|
||||||
import { formatTypes } from '../utils/format.js';
|
import { formatTypes } from '../utils/format.js';
|
||||||
|
import type { TypeScriptOptions } from '../lib/types.js';
|
||||||
|
|
||||||
export const generateToolName = 'generate-types';
|
export const generateToolName = 'generate-types';
|
||||||
|
|
||||||
|
|
@ -9,16 +10,41 @@ export const generateToolDescription = 'Generate TypeScript interfaces from Open
|
||||||
export const generateToolSchema = {
|
export const generateToolSchema = {
|
||||||
path: z.string().describe('Path to the OpenAPI/Swagger spec file'),
|
path: z.string().describe('Path to the OpenAPI/Swagger spec file'),
|
||||||
schemas: z.array(z.string()).optional().describe('Specific schema names to generate (all if omitted)'),
|
schemas: z.array(z.string()).optional().describe('Specific schema names to generate (all if omitted)'),
|
||||||
|
options: z.object({
|
||||||
|
enumAsUnion: z.boolean().optional().describe('Generate enums as union types (default: true)'),
|
||||||
|
enumAsEnum: z.boolean().optional().describe('Generate enums as TypeScript enums'),
|
||||||
|
interfacePrefix: z.string()
|
||||||
|
.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/)
|
||||||
|
.optional()
|
||||||
|
.describe('Prefix for interface names (e.g., "I")'),
|
||||||
|
interfaceSuffix: z.string()
|
||||||
|
.regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/)
|
||||||
|
.optional()
|
||||||
|
.describe('Suffix for interface names (e.g., "Type")'),
|
||||||
|
indentation: z.enum(['2', '4', 'tab']).optional().describe('Indentation style'),
|
||||||
|
}).optional().describe('TypeScript generation options'),
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateToolHandler({ path, schemas }: {
|
export async function generateToolHandler({ path, schemas, options }: {
|
||||||
path: string;
|
path: string;
|
||||||
schemas?: string[];
|
schemas?: string[];
|
||||||
|
options?: TypeScriptOptions;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
content: Array<{ type: 'text'; text: string }>;
|
content: Array<{ type: 'text'; text: string }>;
|
||||||
structuredContent: Record<string, unknown>;
|
structuredContent: Record<string, unknown>;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
|
// Validate mutually exclusive options
|
||||||
|
if (options?.enumAsUnion && options?.enumAsEnum) {
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text', text: 'Error: enumAsUnion and enumAsEnum are mutually exclusive.' }],
|
||||||
|
structuredContent: {
|
||||||
|
success: false,
|
||||||
|
error: 'enumAsUnion and enumAsEnum cannot both be true',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { spec } = await parseSpec(path);
|
const { spec } = await parseSpec(path);
|
||||||
|
|
||||||
const allSchemas = getSchemas(spec);
|
const allSchemas = getSchemas(spec);
|
||||||
|
|
@ -40,7 +66,7 @@ export async function generateToolHandler({ path, schemas }: {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const types = generateTypeScript(schemasToGenerate);
|
const types = generateTypeScript(schemasToGenerate, options);
|
||||||
const text = formatTypes(types);
|
const text = formatTypes(types);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -49,6 +75,7 @@ export async function generateToolHandler({ path, schemas }: {
|
||||||
success: true,
|
success: true,
|
||||||
types,
|
types,
|
||||||
generatedCount: Object.keys(schemasToGenerate).length,
|
generatedCount: Object.keys(schemasToGenerate).length,
|
||||||
|
options: options || {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -79,11 +106,35 @@ function getSchemas(spec: object): Record<string, object> {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTypeScript(schemas: Record<string, object>): string {
|
/** Get indentation string based on options */
|
||||||
|
function getIndent(options?: TypeScriptOptions): string {
|
||||||
|
const style = options?.indentation ?? '2';
|
||||||
|
switch (style) {
|
||||||
|
case '4': return ' ';
|
||||||
|
case 'tab': return '\t';
|
||||||
|
default: return ' ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convert string to PascalCase for enum member names */
|
||||||
|
function toPascalCase(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '')
|
||||||
|
.replace(/^(.)/, (c) => c.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply prefix and suffix to a type name */
|
||||||
|
function applyNaming(name: string, options?: TypeScriptOptions): string {
|
||||||
|
const prefix = options?.interfacePrefix ?? '';
|
||||||
|
const suffix = options?.interfaceSuffix ?? '';
|
||||||
|
return `${prefix}${name}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTypeScript(schemas: Record<string, object>, options?: TypeScriptOptions): string {
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
for (const [name, schema] of Object.entries(schemas)) {
|
for (const [name, schema] of Object.entries(schemas)) {
|
||||||
const typeDef = schemaToTypeScript(name, schema);
|
const typeDef = schemaToTypeScript(name, schema, options);
|
||||||
lines.push(typeDef);
|
lines.push(typeDef);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
|
|
@ -126,8 +177,10 @@ function sanitizeName(name: string): string {
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function schemaToTypeScript(name: string, schema: object): string {
|
function schemaToTypeScript(name: string, schema: object, options?: TypeScriptOptions): string {
|
||||||
const safeName = sanitizeName(name);
|
const sanitized = sanitizeName(name);
|
||||||
|
const safeName = applyNaming(sanitized, options);
|
||||||
|
const indent = getIndent(options);
|
||||||
const s = schema as {
|
const s = schema as {
|
||||||
type?: string;
|
type?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
@ -146,14 +199,26 @@ function schemaToTypeScript(name: string, schema: object): string {
|
||||||
// Add JSDoc comment with original name if different
|
// Add JSDoc comment with original name if different
|
||||||
if (s.description) {
|
if (s.description) {
|
||||||
lines.push(`/** ${s.description} */`);
|
lines.push(`/** ${s.description} */`);
|
||||||
} else if (safeName !== name) {
|
} else if (sanitized !== name) {
|
||||||
lines.push(`/** Original: ${name} */`);
|
lines.push(`/** Original: ${name} */`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle enums
|
// Handle enums
|
||||||
if (s.enum) {
|
if (s.enum) {
|
||||||
const values = s.enum.map(v => typeof v === 'string' ? `'${v}'` : v).join(' | ');
|
// Generate TypeScript enum if enumAsEnum is true
|
||||||
lines.push(`export type ${safeName} = ${values};`);
|
if (options?.enumAsEnum) {
|
||||||
|
lines.push(`export enum ${safeName} {`);
|
||||||
|
for (const value of s.enum) {
|
||||||
|
const memberName = typeof value === 'string' ? toPascalCase(value) : `Value${value}`;
|
||||||
|
const memberValue = typeof value === 'string' ? `'${value}'` : value;
|
||||||
|
lines.push(`${indent}${memberName} = ${memberValue},`);
|
||||||
|
}
|
||||||
|
lines.push('}');
|
||||||
|
} else {
|
||||||
|
// Default: generate union type
|
||||||
|
const values = s.enum.map(v => typeof v === 'string' ? `'${v}'` : v).join(' | ');
|
||||||
|
lines.push(`export type ${safeName} = ${values};`);
|
||||||
|
}
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -166,14 +231,14 @@ function schemaToTypeScript(name: string, schema: object): string {
|
||||||
|
|
||||||
for (const [propName, propSchema] of Object.entries(s.properties)) {
|
for (const [propName, propSchema] of Object.entries(s.properties)) {
|
||||||
const optional = required.has(propName) ? '' : '?';
|
const optional = required.has(propName) ? '' : '?';
|
||||||
const propType = schemaToType(propSchema);
|
const propType = schemaToType(propSchema, options);
|
||||||
const propDesc = (propSchema as { description?: string }).description;
|
const propDesc = (propSchema as { description?: string }).description;
|
||||||
|
|
||||||
if (propDesc) {
|
if (propDesc) {
|
||||||
lines.push(` /** ${propDesc} */`);
|
lines.push(`${indent}/** ${propDesc} */`);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(` ${propName}${optional}: ${propType};`);
|
lines.push(`${indent}${propName}${optional}: ${propType};`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,18 +248,18 @@ function schemaToTypeScript(name: string, schema: object): string {
|
||||||
|
|
||||||
// Handle array types
|
// Handle array types
|
||||||
if (s.type === 'array' && s.items) {
|
if (s.type === 'array' && s.items) {
|
||||||
const itemType = schemaToType(s.items);
|
const itemType = schemaToType(s.items, options);
|
||||||
lines.push(`export type ${safeName} = ${itemType}[];`);
|
lines.push(`export type ${safeName} = ${itemType}[];`);
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle simple types
|
// Handle simple types
|
||||||
const simpleType = schemaToType(schema);
|
const simpleType = schemaToType(schema, options);
|
||||||
lines.push(`export type ${safeName} = ${simpleType};`);
|
lines.push(`export type ${safeName} = ${simpleType};`);
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function schemaToType(schema: object): string {
|
function schemaToType(schema: object, options?: TypeScriptOptions): string {
|
||||||
const s = schema as {
|
const s = schema as {
|
||||||
type?: string;
|
type?: string;
|
||||||
format?: string;
|
format?: string;
|
||||||
|
|
@ -212,35 +277,36 @@ function schemaToType(schema: object): string {
|
||||||
// Handle $ref (after dereferencing, these should be resolved, but handle just in case)
|
// Handle $ref (after dereferencing, these should be resolved, but handle just in case)
|
||||||
if (s.$ref) {
|
if (s.$ref) {
|
||||||
const refName = sanitizeName(s.$ref.split('/').pop() || 'unknown');
|
const refName = sanitizeName(s.$ref.split('/').pop() || 'unknown');
|
||||||
return refName;
|
// Apply naming to referenced types
|
||||||
|
return applyNaming(refName, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle enums
|
// Handle enums (inline)
|
||||||
if (s.enum) {
|
if (s.enum) {
|
||||||
return s.enum.map(v => typeof v === 'string' ? `'${v}'` : v).join(' | ');
|
return s.enum.map(v => typeof v === 'string' ? `'${v}'` : v).join(' | ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle allOf (intersection)
|
// Handle allOf (intersection)
|
||||||
if (s.allOf) {
|
if (s.allOf) {
|
||||||
return s.allOf.map(schemaToType).join(' & ');
|
return s.allOf.map(sub => schemaToType(sub, options)).join(' & ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle oneOf/anyOf (union)
|
// Handle oneOf/anyOf (union)
|
||||||
if (s.oneOf || s.anyOf) {
|
if (s.oneOf || s.anyOf) {
|
||||||
const schemas = s.oneOf || s.anyOf || [];
|
const schemas = s.oneOf || s.anyOf || [];
|
||||||
return schemas.map(schemaToType).join(' | ');
|
return schemas.map(sub => schemaToType(sub, options)).join(' | ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle arrays
|
// Handle arrays
|
||||||
if (s.type === 'array') {
|
if (s.type === 'array') {
|
||||||
const itemType = s.items ? schemaToType(s.items) : 'unknown';
|
const itemType = s.items ? schemaToType(s.items, options) : 'unknown';
|
||||||
return `${itemType}[]`;
|
return `${itemType}[]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle objects with additionalProperties
|
// Handle objects with additionalProperties
|
||||||
if (s.type === 'object' && s.additionalProperties) {
|
if (s.type === 'object' && s.additionalProperties) {
|
||||||
const valueType = typeof s.additionalProperties === 'object'
|
const valueType = typeof s.additionalProperties === 'object'
|
||||||
? schemaToType(s.additionalProperties)
|
? schemaToType(s.additionalProperties, options)
|
||||||
: 'unknown';
|
: 'unknown';
|
||||||
return `Record<string, ${valueType}>`;
|
return `Record<string, ${valueType}>`;
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +317,7 @@ function schemaToType(schema: object): string {
|
||||||
const props = Object.entries(s.properties)
|
const props = Object.entries(s.properties)
|
||||||
.map(([name, prop]) => {
|
.map(([name, prop]) => {
|
||||||
const optional = required.has(name) ? '' : '?';
|
const optional = required.has(name) ? '' : '?';
|
||||||
return `${name}${optional}: ${schemaToType(prop)}`;
|
return `${name}${optional}: ${schemaToType(prop, options)}`;
|
||||||
})
|
})
|
||||||
.join('; ');
|
.join('; ');
|
||||||
return `{ ${props} }`;
|
return `{ ${props} }`;
|
||||||
|
|
|
||||||
36
test/fixtures/enum-test.yaml
vendored
Normal file
36
test/fixtures/enum-test.yaml
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: Enum Test API
|
||||||
|
version: 1.0.0
|
||||||
|
paths: {}
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
Status:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- pending
|
||||||
|
- active
|
||||||
|
- completed
|
||||||
|
- cancelled
|
||||||
|
description: Task status
|
||||||
|
Priority:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- low
|
||||||
|
- normal
|
||||||
|
- high
|
||||||
|
- urgent
|
||||||
|
Task:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- status
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
$ref: '#/components/schemas/Status'
|
||||||
|
priority:
|
||||||
|
$ref: '#/components/schemas/Priority'
|
||||||
Loading…
Add table
Reference in a new issue