Skip to content

Commit 97c0a34

Browse files
committed
chore(utils): updated extension generation
1 parent 872e148 commit 97c0a34

File tree

8 files changed

+497
-10
lines changed

8 files changed

+497
-10
lines changed

package.json

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,81 @@
1919
}
2020
]
2121
},
22+
"configuration": {
23+
"title": "Neuma API Dart",
24+
"properties": {
25+
"neuma-api-dart.defaultBaseFolder": {
26+
"type": "string",
27+
"default": "lib/data/models",
28+
"description": "Default base folder for generated Dart models (e.g., 'lib/data/models', 'lib/models')"
29+
},
30+
"neuma-api-dart.generateSubfolders": {
31+
"type": "boolean",
32+
"default": true,
33+
"description": "Create subfolders based on class names (e.g., lib/data/models/user_profile/)"
34+
},
35+
"neuma-api-dart.nullSafety": {
36+
"type": "string",
37+
"enum": [
38+
"nullable",
39+
"non-nullable",
40+
"auto"
41+
],
42+
"default": "auto",
43+
"description": "Null safety mode: 'nullable' (String?), 'non-nullable' (String), or 'auto' (detect from JSON)"
44+
},
45+
"neuma-api-dart.generateJsonAnnotation": {
46+
"type": "boolean",
47+
"default": true,
48+
"description": "Add @JsonKey() annotations from json_annotation package"
49+
},
50+
"neuma-api-dart.generateFromJson": {
51+
"type": "boolean",
52+
"default": true,
53+
"description": "Generate fromJson() constructor"
54+
},
55+
"neuma-api-dart.generateToJson": {
56+
"type": "boolean",
57+
"default": true,
58+
"description": "Generate toJson() method"
59+
},
60+
"neuma-api-dart.generateCopyWith": {
61+
"type": "boolean",
62+
"default": false,
63+
"description": "Generate copyWith() method for immutable updates"
64+
},
65+
"neuma-api-dart.generateEquatable": {
66+
"type": "boolean",
67+
"default": false,
68+
"description": "Extend Equatable class for value equality (requires equatable package)"
69+
},
70+
"neuma-api-dart.generateToString": {
71+
"type": "boolean",
72+
"default": false,
73+
"description": "Override toString() method"
74+
},
75+
"neuma-api-dart.useFreezed": {
76+
"type": "boolean",
77+
"default": false,
78+
"description": "Generate Freezed data classes instead of regular classes (requires freezed package)"
79+
},
80+
"neuma-api-dart.fieldCase": {
81+
"type": "string",
82+
"enum": [
83+
"camelCase",
84+
"snake_case",
85+
"preserve"
86+
],
87+
"default": "camelCase",
88+
"description": "Field naming convention: camelCase (Dart standard), snake_case (API standard), or preserve original"
89+
},
90+
"neuma-api-dart.addPartStatement": {
91+
"type": "boolean",
92+
"default": true,
93+
"description": "Add part statement for code generation (e.g., part 'user.g.dart';)"
94+
}
95+
}
96+
},
2297
"scripts": {
2398
"vscode:prepublish": "npm run package",
2499
"compile": "npm run check-types && npm run lint && node esbuild.js",

src/commands/generateModel.ts

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from 'vscode';
2-
import { generateDartModel } from '../utils/jsonToDart';
2+
import { generateDartModel } from '../utils/dartGenUtils';
33

44
export function registerGenerateModelCommand(context: vscode.ExtensionContext) {
55
return vscode.commands.registerCommand('neuma-api-dart.generateModel', async () => {
@@ -25,17 +25,92 @@ export function registerGenerateModelCommand(context: vscode.ExtensionContext) {
2525
if (!modelType) return;
2626

2727
const classNameBase = await vscode.window.showInputBox({
28-
placeHolder: 'Enter base class name (e.g. Login)',
29-
prompt: 'The model will be named LoginRequest or LoginResponse'
28+
placeHolder: 'Enter base class name (e.g. UserProfile, LoginAuth, ProductDetails)',
29+
prompt: 'This will create the model class and organize it in a folder structure. For example:\n• "UserProfile" creates models/user_profile/user_profile_request.dart\n• "LoginAuth" creates models/login_auth/login_auth_response.dart\nUse PascalCase - it will be converted to snake_case for folders and files.'
3030
});
3131

3232
if (!classNameBase) return;
3333

3434
const finalClassName = `${classNameBase}${modelType}`;
35+
36+
// Get extension configuration
37+
const config = vscode.workspace.getConfiguration('neuma-api-dart');
38+
const baseFolder = config.get<string>('defaultBaseFolder', 'lib/models');
39+
const generateSubfolders = config.get<boolean>('generateSubfolders', true);
40+
41+
// Convert PascalCase to snake_case for folder and file names (Dart convention)
42+
const folderName = classNameBase.replace(/([A-Z])/g, (match, letter, index) => {
43+
return index === 0 ? letter.toLowerCase() : '_' + letter.toLowerCase();
44+
});
45+
46+
const fileName = `${folderName}_${modelType.toLowerCase()}.dart`;
47+
48+
// Build path based on subfolder setting
49+
const relativePath = generateSubfolders
50+
? `${baseFolder}/${folderName}/${fileName}`
51+
: `${baseFolder}/${fileName}`;
52+
3553
const json = JSON.parse(jsonInput);
36-
const dartCode = generateDartModel(json, finalClassName);
3754

38-
const doc = await vscode.workspace.openTextDocument({ content: dartCode, language: 'dart' });
39-
vscode.window.showTextDocument(doc);
55+
// Create model generation options from config
56+
const modelOptions = {
57+
nullSafety: config.get<string>('nullSafety', 'auto'),
58+
generateJsonAnnotation: config.get<boolean>('generateJsonAnnotation', true),
59+
generateFromJson: config.get<boolean>('generateFromJson', true),
60+
generateToJson: config.get<boolean>('generateToJson', true),
61+
generateCopyWith: config.get<boolean>('generateCopyWith', false),
62+
generateEquatable: config.get<boolean>('generateEquatable', false),
63+
generateToString: config.get<boolean>('generateToString', false),
64+
useFreezed: config.get<boolean>('useFreezed', false),
65+
fieldCase: config.get<string>('fieldCase', 'camelCase'),
66+
addPartStatement: config.get<boolean>('addPartStatement', true)
67+
};
68+
69+
const dartCode = generateDartModel(json, finalClassName, new Set(Object.keys(modelOptions)));
70+
71+
// Get the workspace folder
72+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
73+
74+
if (!workspaceFolder) {
75+
// No workspace - fallback to untitled document
76+
vscode.window.showWarningMessage('No workspace folder found. Opening as untitled document.');
77+
const doc = await vscode.workspace.openTextDocument({
78+
content: dartCode,
79+
language: 'dart'
80+
});
81+
await vscode.window.showTextDocument(doc);
82+
return;
83+
}
84+
85+
try {
86+
// Create the full file path
87+
const fullPath = vscode.Uri.joinPath(workspaceFolder.uri, relativePath);
88+
89+
// Create directories if they don't exist
90+
const dirPath = generateSubfolders
91+
? vscode.Uri.joinPath(workspaceFolder.uri, `${baseFolder}/${folderName}`)
92+
: vscode.Uri.joinPath(workspaceFolder.uri, baseFolder);
93+
await vscode.workspace.fs.createDirectory(dirPath);
94+
95+
// Write the file
96+
const encoder = new TextEncoder();
97+
await vscode.workspace.fs.writeFile(fullPath, encoder.encode(dartCode));
98+
99+
// Open the created file
100+
const doc = await vscode.workspace.openTextDocument(fullPath);
101+
await vscode.window.showTextDocument(doc);
102+
103+
vscode.window.showInformationMessage(`Model created at: ${relativePath}`);
104+
105+
} catch (error) {
106+
vscode.window.showErrorMessage(`Failed to create file: ${error}`);
107+
108+
// Fallback: open in untitled document
109+
const doc = await vscode.workspace.openTextDocument({
110+
content: dartCode,
111+
language: 'dart'
112+
});
113+
vscode.window.showTextDocument(doc);
114+
}
40115
});
41-
}
116+
}

src/configs/types.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Configuration types for the extension
2+
3+
export interface ModelGenerationOptions {
4+
nullSafety: 'nullable' | 'non-nullable' | 'auto';
5+
generateJsonAnnotation: boolean;
6+
generateFromJson: boolean;
7+
generateToJson: boolean;
8+
generateCopyWith: boolean;
9+
generateEquatable: boolean;
10+
generateToString: boolean;
11+
useFreezed: boolean;
12+
fieldCase: 'camelCase' | 'snake_case' | 'preserve';
13+
addPartStatement: boolean;
14+
}
15+
16+
export interface GenerationConfig {
17+
baseFolder: string;
18+
generateSubfolders: boolean;
19+
modelOptions: ModelGenerationOptions;
20+
}
21+
22+
export enum GenerationMode {
23+
SINGLE = 'single',
24+
COLLECTION = 'collection'
25+
}
26+
27+
export interface JsonCollectionInput {
28+
collectionName: string;
29+
jsonObjects: Array<{
30+
name: string;
31+
type: 'Request' | 'Response';
32+
json: any;
33+
}>;
34+
}

src/extension.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import * as vscode from 'vscode';
22
import { registerGenerateModelCommand } from './commands/generateModel';
33

44
export function activate(context: vscode.ExtensionContext) {
5-
const disposable = registerGenerateModelCommand(context)
6-
context.subscriptions.push(disposable);
7-
}
5+
console.log('Neuma API Dart extension is now active!');
6+
7+
const generateModelDisposable = registerGenerateModelCommand(context);
8+
// const generateCollectionDisposable = registerGenerateModelCommand(context);
9+
10+
context.subscriptions.push(generateModelDisposable);
11+
// context.subscriptions.push(generateCollectionDisposable);
12+
}
13+
14+
export function deactivate() { }

src/utils/collectionGenUtils.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import * as vscode from 'vscode';
2+
import { getExtensionConfig, buildFilePath, modelOptionsToSet } from './configUtils';
3+
import { JsonCollectionInput } from "../configs/types"
4+
import { generateDartModel } from './dartGenUtils';
5+
6+
export async function generateCollectionModels(): Promise<void> {
7+
// Get collection name first
8+
const collectionName = await vscode.window.showInputBox({
9+
placeHolder: 'Enter collection name (e.g. UserManagement, AuthSystem)',
10+
prompt: 'This will be used as the base folder name for all models in this collection.\nExample: "UserManagement" creates lib/data/models/user_management/ folder.'
11+
});
12+
13+
if (!collectionName) return;
14+
15+
// Get JSON collection input
16+
const jsonCollectionInput = await vscode.window.showInputBox({
17+
placeHolder: 'Paste your JSON collection here',
18+
prompt: 'Format: {"ModelName": {"type": "Request|Response", "data": {...}}, ...}\n\nExample:\n{\n "Login": {"type": "Request", "data": {"email": "", "password": ""}},\n "User": {"type": "Response", "data": {"id": 1, "name": "", "email": ""}}\n}',
19+
validateInput: text => {
20+
try {
21+
const parsed = JSON.parse(text);
22+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
23+
return 'Must be a JSON object with model definitions';
24+
}
25+
26+
// Validate structure
27+
for (const [key, value] of Object.entries(parsed)) {
28+
if (!value || typeof value !== 'object') {
29+
return `Invalid structure for "${key}": must be an object`;
30+
}
31+
const item = value as any;
32+
if (!item.type || !['Request', 'Response'].includes(item.type)) {
33+
return `Invalid type for "${key}": must be "Request" or "Response"`;
34+
}
35+
if (!item.data || typeof item.data !== 'object') {
36+
return `Invalid data for "${key}": must be a valid JSON object`;
37+
}
38+
}
39+
return null;
40+
} catch (e) {
41+
return 'Invalid JSON format';
42+
}
43+
}
44+
});
45+
46+
if (!jsonCollectionInput) return;
47+
48+
try {
49+
const collection = parseJsonCollection(jsonCollectionInput, collectionName);
50+
await generateAllModelsFromCollection(collection);
51+
} catch (error) {
52+
vscode.window.showErrorMessage(`Failed to parse collection: ${error}`);
53+
}
54+
}
55+
56+
function parseJsonCollection(input: string, collectionName: string): JsonCollectionInput {
57+
const parsed = JSON.parse(input);
58+
const jsonObjects = [];
59+
60+
for (const [modelName, config] of Object.entries(parsed)) {
61+
const item = config as any;
62+
jsonObjects.push({
63+
name: modelName,
64+
type: item.type,
65+
json: item.data
66+
});
67+
}
68+
69+
return {
70+
collectionName,
71+
jsonObjects
72+
};
73+
}
74+
75+
async function generateAllModelsFromCollection(collection: JsonCollectionInput): Promise<void> {
76+
const config = getExtensionConfig();
77+
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
78+
79+
if (!workspaceFolder) {
80+
vscode.window.showWarningMessage('No workspace folder found. Cannot create collection.');
81+
return;
82+
}
83+
84+
const results: string[] = [];
85+
const errors: string[] = [];
86+
87+
// Show progress
88+
await vscode.window.withProgress({
89+
location: vscode.ProgressLocation.Notification,
90+
title: `Generating ${collection.jsonObjects.length} models for ${collection.collectionName}`,
91+
cancellable: false
92+
}, async (progress) => {
93+
const increment = 100 / collection.jsonObjects.length;
94+
95+
for (let i = 0; i < collection.jsonObjects.length; i++) {
96+
const model = collection.jsonObjects[i];
97+
progress.report({
98+
increment,
99+
message: `Creating ${model.name}${model.type}...`
100+
});
101+
102+
try {
103+
const finalClassName = `${model.name}${model.type}`;
104+
const relativePath = buildFilePath(config, model.name, model.type);
105+
106+
// For collections, we might want to group them under the collection name
107+
const collectionPath = relativePath.replace(
108+
config.baseFolder,
109+
`${config.baseFolder}/${collection.collectionName.toLowerCase()}`
110+
);
111+
112+
const featuresSet = modelOptionsToSet(config.modelOptions);
113+
const dartCode = generateDartModel(model.json, finalClassName, featuresSet);
114+
115+
// Create directories
116+
const dirPath = vscode.Uri.joinPath(workspaceFolder.uri, collectionPath.substring(0, collectionPath.lastIndexOf('/')));
117+
await vscode.workspace.fs.createDirectory(dirPath);
118+
119+
// Write file
120+
const fullPath = vscode.Uri.joinPath(workspaceFolder.uri, collectionPath);
121+
const encoder = new TextEncoder();
122+
await vscode.workspace.fs.writeFile(fullPath, encoder.encode(dartCode));
123+
124+
results.push(collectionPath);
125+
126+
} catch (error) {
127+
errors.push(`${model.name}${model.type}: ${error}`);
128+
}
129+
}
130+
});
131+
132+
// Show results
133+
if (results.length > 0) {
134+
const message = `Successfully generated ${results.length} models in ${collection.collectionName}`;
135+
vscode.window.showInformationMessage(message);
136+
137+
// Optionally show all created files
138+
const showFiles = await vscode.window.showInformationMessage(
139+
message,
140+
'Show Files'
141+
);
142+
143+
if (showFiles) {
144+
const fileList = results.join('\n• ');
145+
vscode.window.showInformationMessage(`Created files:\n• ${fileList}`);
146+
}
147+
}
148+
149+
if (errors.length > 0) {
150+
vscode.window.showErrorMessage(`Failed to generate ${errors.length} models. Check output for details.`);
151+
console.error('Generation errors:', errors);
152+
}
153+
}

0 commit comments

Comments
 (0)