froggy-docs 1.1.1 → 1.1.4
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.
- package/.claude/settings.local.json +21 -0
- package/.github/workflows/froggy-docs.yml +59 -0
- package/ABOUT.md +99 -0
- package/MILESTONE.md +29 -17
- package/bin/froggy_docs.dart +3 -45
- package/frontend/lib/app.dart +488 -280
- package/frontend/lib/components/file_field.dart +75 -0
- package/frontend/web/app.js +224 -67
- package/frontend/web/froggy_docs.json +193 -4
- package/lib/froggy_docs.dart +1 -0
- package/lib/src/cli_runner.dart +123 -0
- package/lib/src/parser_engine.dart +369 -23
- package/lib/src/watcher_engine.dart +46 -14
- package/lib/src/web_server.dart +40 -17
- package/package.js +0 -0
- package/package.json +4 -4
- package/bin/froggy-docs +0 -0
package/lib/src/cli_runner.dart
CHANGED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
import 'package:args/args.dart';
|
|
3
|
+
import 'package:froggy_docs/src/watcher_engine.dart';
|
|
4
|
+
import 'package:froggy_docs/src/web_server.dart';
|
|
5
|
+
import 'package:froggy_docs/src/parser_engine.dart';
|
|
6
|
+
|
|
7
|
+
class CliRunner {
|
|
8
|
+
void run(List<String> arguments) async {
|
|
9
|
+
final parser = ArgParser();
|
|
10
|
+
parser.addCommand('watch');
|
|
11
|
+
parser.addCommand('serve');
|
|
12
|
+
parser.addCommand('build');
|
|
13
|
+
parser.addOption(
|
|
14
|
+
'port',
|
|
15
|
+
abbr: 'p',
|
|
16
|
+
help: 'Port for server',
|
|
17
|
+
defaultsTo: '8080',
|
|
18
|
+
);
|
|
19
|
+
parser.addOption(
|
|
20
|
+
'proxy',
|
|
21
|
+
abbr: 'x',
|
|
22
|
+
help: 'Proxy API requests to this URL (e.g., http://localhost:3000)',
|
|
23
|
+
defaultsTo: '',
|
|
24
|
+
);
|
|
25
|
+
parser.addOption(
|
|
26
|
+
'output',
|
|
27
|
+
abbr: 'o',
|
|
28
|
+
help: 'Output path for generated JSON spec (default: frontend/web/froggy_docs.json)',
|
|
29
|
+
defaultsTo: 'frontend/web/froggy_docs.json',
|
|
30
|
+
);
|
|
31
|
+
parser.addOption(
|
|
32
|
+
'ignore',
|
|
33
|
+
help: 'Glob pattern for paths to exclude from watching (e.g., "**/*.g.dart")',
|
|
34
|
+
defaultsTo: '',
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
final results = parser.parse(arguments);
|
|
39
|
+
|
|
40
|
+
final outputPath = results['output'] as String;
|
|
41
|
+
final ignorePattern = results['ignore'] as String;
|
|
42
|
+
|
|
43
|
+
if (results.command?.name == 'watch') {
|
|
44
|
+
print('🐸 FroggyDocs is watching your API project...');
|
|
45
|
+
final watcher = WatcherEngine(outputPath: outputPath, ignorePattern: ignorePattern);
|
|
46
|
+
watcher.startWatching(Directory.current.path);
|
|
47
|
+
} else if (results.command?.name == 'serve') {
|
|
48
|
+
final port = int.parse(results['port'] as String);
|
|
49
|
+
final proxyUrl = results['proxy'] as String;
|
|
50
|
+
print('🐸 Starting FroggyDocs server with live reload...');
|
|
51
|
+
if (proxyUrl.isNotEmpty) {
|
|
52
|
+
print('🔄 Proxy enabled: API requests will be forwarded to $proxyUrl');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await startServer(port: port, proxyUrl: proxyUrl);
|
|
56
|
+
final watcher = WatcherEngine(outputPath: outputPath, ignorePattern: ignorePattern);
|
|
57
|
+
watcher.startWatching(Directory.current.path);
|
|
58
|
+
} else if (results.command?.name == 'build') {
|
|
59
|
+
print('🐸 Building static FroggyDocs site...');
|
|
60
|
+
final parser = ParserEngine();
|
|
61
|
+
parser.setOutputPath(outputPath);
|
|
62
|
+
_buildStaticSite(parser, Directory.current.path);
|
|
63
|
+
print('✅ Build complete. Output: $outputPath');
|
|
64
|
+
} else {
|
|
65
|
+
_printHelp();
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
print('Error: ${e.toString()}');
|
|
69
|
+
exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void _buildStaticSite(ParserEngine parser, String directoryPath) {
|
|
74
|
+
final dir = Directory(directoryPath);
|
|
75
|
+
try {
|
|
76
|
+
dir.listSync(recursive: true).forEach((entity) {
|
|
77
|
+
if (entity is File) {
|
|
78
|
+
final normalizedPath = entity.path.replaceAll('\\', '/');
|
|
79
|
+
if (!_shouldIgnore(normalizedPath)) {
|
|
80
|
+
parser.parseFile(entity.path);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
print('⚠️ Warning during build: $e');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
bool _shouldIgnore(String path) {
|
|
90
|
+
return path.contains('/.dart_tool/') ||
|
|
91
|
+
path.contains('/frontend/') ||
|
|
92
|
+
path.contains('/node_modules/') ||
|
|
93
|
+
path.contains('/.git/') ||
|
|
94
|
+
path.endsWith('.json') ||
|
|
95
|
+
path.endsWith('.md');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
void _printHelp() {
|
|
99
|
+
print('''
|
|
100
|
+
🐸 FroggyDocs v1.0.0
|
|
101
|
+
|
|
102
|
+
Usage:
|
|
103
|
+
froggy_docs serve Start server with live documentation
|
|
104
|
+
froggy_docs watch Watch for changes and regenerate docs
|
|
105
|
+
froggy_docs build Generate static documentation without starting server
|
|
106
|
+
|
|
107
|
+
Options:
|
|
108
|
+
-p, --port <port> Port number (default: 8080)
|
|
109
|
+
-x, --proxy <url> Proxy API requests to this URL
|
|
110
|
+
-o, --output <path> Output path for generated JSON spec
|
|
111
|
+
(default: frontend/web/froggy_docs.json)
|
|
112
|
+
--ignore <glob> Glob pattern for paths to exclude from watching
|
|
113
|
+
(e.g., "**/*.g.dart")
|
|
114
|
+
-h, --help Show this help message
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
froggy_docs serve --port 3000
|
|
118
|
+
froggy_docs serve --output docs/api.json
|
|
119
|
+
froggy_docs build --output docs/froggy_docs.json
|
|
120
|
+
froggy_docs watch --ignore "**/*.g.dart"
|
|
121
|
+
''');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -36,6 +36,14 @@ class ParserEngine {
|
|
|
36
36
|
'options',
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
String _outputPath = 'frontend/web/froggy_docs.json';
|
|
40
|
+
|
|
41
|
+
void setOutputPath(String path) {
|
|
42
|
+
_outputPath = path;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
String get outputPath => _outputPath;
|
|
46
|
+
|
|
39
47
|
final Map<String, dynamic> _baseSpec = {
|
|
40
48
|
"openapi": "3.0.0",
|
|
41
49
|
"info": {
|
|
@@ -104,6 +112,37 @@ class ParserEngine {
|
|
|
104
112
|
}
|
|
105
113
|
}
|
|
106
114
|
|
|
115
|
+
dynamic _parseDefaultValue(String value, String? type) {
|
|
116
|
+
switch (type?.toString().toLowerCase()) {
|
|
117
|
+
case 'number':
|
|
118
|
+
case 'integer':
|
|
119
|
+
case 'int':
|
|
120
|
+
return num.tryParse(value) ?? value;
|
|
121
|
+
case 'boolean':
|
|
122
|
+
return value.toLowerCase() == 'true';
|
|
123
|
+
default:
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
void _attachResponseExample(
|
|
129
|
+
String code,
|
|
130
|
+
dynamic jsonData,
|
|
131
|
+
Map<String, Map<String, dynamic>> responseSchemas,
|
|
132
|
+
) {
|
|
133
|
+
final response = responseSchemas[code]!;
|
|
134
|
+
if (!response.containsKey('content')) {
|
|
135
|
+
response['content'] = <String, dynamic>{
|
|
136
|
+
"application/json": <String, dynamic>{
|
|
137
|
+
"schema": <String, dynamic>{"type": "object"},
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
final contentMap = response['content'] as Map;
|
|
142
|
+
final appJsonMap = contentMap['application/json'] as Map;
|
|
143
|
+
appJsonMap['example'] = jsonData;
|
|
144
|
+
}
|
|
145
|
+
|
|
107
146
|
void parseFile(String filePath) {
|
|
108
147
|
_errors.clear();
|
|
109
148
|
|
|
@@ -132,8 +171,15 @@ class ParserEngine {
|
|
|
132
171
|
bool hasAuth = false;
|
|
133
172
|
List<String> currentTags = [];
|
|
134
173
|
List<Map<String, String>> requestBodyFields = [];
|
|
174
|
+
List<Map<String, String>> requestFileFields = [];
|
|
175
|
+
List<Map<String, dynamic>> queryParams = [];
|
|
176
|
+
List<Map<String, dynamic>> headerParams = [];
|
|
177
|
+
Map<String, Map<String, dynamic>> responseSchemas = {};
|
|
178
|
+
String? pendingResponseCode;
|
|
135
179
|
bool isReadingBodyJson = false;
|
|
180
|
+
bool isReadingResponseJson = false;
|
|
136
181
|
StringBuffer bodyJsonBuffer = StringBuffer();
|
|
182
|
+
StringBuffer responseJsonBuffer = StringBuffer();
|
|
137
183
|
int lineNumber = 0;
|
|
138
184
|
|
|
139
185
|
String inferType(dynamic value) {
|
|
@@ -146,6 +192,44 @@ class ParserEngine {
|
|
|
146
192
|
}
|
|
147
193
|
|
|
148
194
|
void flushEndpoint() {
|
|
195
|
+
// Defensively flush any pending JSON buffers (handles end-of-file cases).
|
|
196
|
+
if (isReadingBodyJson && bodyJsonBuffer.isNotEmpty) {
|
|
197
|
+
isReadingBodyJson = false;
|
|
198
|
+
try {
|
|
199
|
+
final jsonData = jsonDecode(bodyJsonBuffer.toString());
|
|
200
|
+
if (jsonData is Map) {
|
|
201
|
+
for (final entry in jsonData.entries) {
|
|
202
|
+
requestBodyFields.add({
|
|
203
|
+
'name': entry.key.toString(),
|
|
204
|
+
'type': inferType(entry.value),
|
|
205
|
+
'desc': 'Inferred from JSON',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
_errors.add('Invalid JSON in @body-json at line $lineNumber: $e');
|
|
211
|
+
}
|
|
212
|
+
bodyJsonBuffer.clear();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isReadingResponseJson &&
|
|
216
|
+
responseJsonBuffer.isNotEmpty &&
|
|
217
|
+
pendingResponseCode != null) {
|
|
218
|
+
isReadingResponseJson = false;
|
|
219
|
+
try {
|
|
220
|
+
final jsonData = jsonDecode(responseJsonBuffer.toString());
|
|
221
|
+
_attachResponseExample(
|
|
222
|
+
pendingResponseCode!,
|
|
223
|
+
jsonData,
|
|
224
|
+
responseSchemas,
|
|
225
|
+
);
|
|
226
|
+
} catch (e) {
|
|
227
|
+
_errors
|
|
228
|
+
.add('Invalid JSON in @response-json at line $lineNumber: $e');
|
|
229
|
+
}
|
|
230
|
+
responseJsonBuffer.clear();
|
|
231
|
+
}
|
|
232
|
+
|
|
149
233
|
if (currentMethod != null) {
|
|
150
234
|
if (currentPath == null) {
|
|
151
235
|
_errors.add('Endpoint defined without path at line $lineNumber');
|
|
@@ -153,8 +237,15 @@ class ParserEngine {
|
|
|
153
237
|
currentPath = null;
|
|
154
238
|
description = '';
|
|
155
239
|
hasAuth = false;
|
|
240
|
+
currentTags.clear();
|
|
156
241
|
requestBodyFields.clear();
|
|
242
|
+
requestFileFields.clear();
|
|
243
|
+
queryParams.clear();
|
|
244
|
+
headerParams.clear();
|
|
245
|
+
responseSchemas.clear();
|
|
246
|
+
pendingResponseCode = null;
|
|
157
247
|
bodyJsonBuffer.clear();
|
|
248
|
+
responseJsonBuffer.clear();
|
|
158
249
|
return;
|
|
159
250
|
}
|
|
160
251
|
|
|
@@ -166,8 +257,15 @@ class ParserEngine {
|
|
|
166
257
|
currentPath = null;
|
|
167
258
|
description = '';
|
|
168
259
|
hasAuth = false;
|
|
260
|
+
currentTags.clear();
|
|
169
261
|
requestBodyFields.clear();
|
|
262
|
+
requestFileFields.clear();
|
|
263
|
+
queryParams.clear();
|
|
264
|
+
headerParams.clear();
|
|
265
|
+
responseSchemas.clear();
|
|
266
|
+
pendingResponseCode = null;
|
|
170
267
|
bodyJsonBuffer.clear();
|
|
268
|
+
responseJsonBuffer.clear();
|
|
171
269
|
return;
|
|
172
270
|
}
|
|
173
271
|
|
|
@@ -178,11 +276,16 @@ class ParserEngine {
|
|
|
178
276
|
fileEndpoints[path] = {};
|
|
179
277
|
}
|
|
180
278
|
|
|
279
|
+
final responses = <String, dynamic>{};
|
|
280
|
+
if (responseSchemas.isEmpty) {
|
|
281
|
+
responses['200'] = {"description": "Successful response"};
|
|
282
|
+
} else {
|
|
283
|
+
responses.addAll(responseSchemas);
|
|
284
|
+
}
|
|
285
|
+
|
|
181
286
|
Map<String, dynamic> endpointData = {
|
|
182
287
|
"summary": description.isNotEmpty ? description : "No description",
|
|
183
|
-
"responses":
|
|
184
|
-
"200": {"description": "Successful response"},
|
|
185
|
-
},
|
|
288
|
+
"responses": responses,
|
|
186
289
|
};
|
|
187
290
|
|
|
188
291
|
if (currentTags.isNotEmpty) {
|
|
@@ -195,25 +298,93 @@ class ParserEngine {
|
|
|
195
298
|
];
|
|
196
299
|
}
|
|
197
300
|
|
|
198
|
-
|
|
199
|
-
|
|
301
|
+
final parameters = <Map<String, dynamic>>[];
|
|
302
|
+
for (var qp in queryParams) {
|
|
303
|
+
final param = <String, dynamic>{
|
|
304
|
+
"name": qp['name'],
|
|
305
|
+
"in": "query",
|
|
306
|
+
"description": qp['desc'] ?? '',
|
|
307
|
+
"schema": <String, dynamic>{
|
|
308
|
+
"type": (qp['type'] ?? 'string').toString().toLowerCase(),
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
if (qp['default'] != null && (qp['default'] as String).isNotEmpty) {
|
|
312
|
+
param['schema']['default'] = _parseDefaultValue(
|
|
313
|
+
qp['default'] as String,
|
|
314
|
+
qp['type'],
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
parameters.add(param);
|
|
318
|
+
}
|
|
319
|
+
for (var hp in headerParams) {
|
|
320
|
+
parameters.add({
|
|
321
|
+
"name": hp['name'],
|
|
322
|
+
"in": "header",
|
|
323
|
+
"description": hp['desc'] ?? '',
|
|
324
|
+
"schema": {
|
|
325
|
+
"type": (hp['type'] ?? 'string').toString().toLowerCase(),
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
final pathParamMatches = RegExp(r'\{(\w+)\}').allMatches(path);
|
|
330
|
+
for (final match in pathParamMatches) {
|
|
331
|
+
final paramName = match.group(1)!;
|
|
332
|
+
if (!parameters.any((p) => p['name'] == paramName && p['in'] == 'path')) {
|
|
333
|
+
parameters.insert(0, {
|
|
334
|
+
"name": paramName,
|
|
335
|
+
"in": "path",
|
|
336
|
+
"required": true,
|
|
337
|
+
"description": '',
|
|
338
|
+
"schema": {"type": "string"},
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (parameters.isNotEmpty) {
|
|
344
|
+
endpointData['parameters'] = parameters;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (requestBodyFields.isNotEmpty || requestFileFields.isNotEmpty) {
|
|
348
|
+
final properties = <String, dynamic>{};
|
|
200
349
|
for (var field in requestBodyFields) {
|
|
201
|
-
final fieldName = field['name']
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
350
|
+
final fieldName = field['name']!;
|
|
351
|
+
properties[fieldName] = {
|
|
352
|
+
"type": (field['type'] ?? 'string').toString().toLowerCase(),
|
|
353
|
+
"description": field['desc'] ?? '',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
for (var field in requestFileFields) {
|
|
357
|
+
final fieldName = field['name']!;
|
|
358
|
+
properties[fieldName] = {
|
|
359
|
+
"type": "string",
|
|
360
|
+
"format": "binary",
|
|
361
|
+
"description": field['desc'] ?? '',
|
|
362
|
+
};
|
|
208
363
|
}
|
|
209
364
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
"
|
|
213
|
-
"
|
|
365
|
+
if (requestFileFields.isNotEmpty) {
|
|
366
|
+
endpointData['requestBody'] = {
|
|
367
|
+
"content": <String, dynamic>{
|
|
368
|
+
"multipart/form-data": <String, dynamic>{
|
|
369
|
+
"schema": <String, dynamic>{
|
|
370
|
+
"type": "object",
|
|
371
|
+
"properties": properties,
|
|
372
|
+
},
|
|
373
|
+
},
|
|
214
374
|
},
|
|
215
|
-
}
|
|
216
|
-
}
|
|
375
|
+
};
|
|
376
|
+
} else {
|
|
377
|
+
endpointData['requestBody'] = {
|
|
378
|
+
"content": <String, dynamic>{
|
|
379
|
+
"application/json": <String, dynamic>{
|
|
380
|
+
"schema": <String, dynamic>{
|
|
381
|
+
"type": "object",
|
|
382
|
+
"properties": properties,
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
217
388
|
}
|
|
218
389
|
|
|
219
390
|
fileEndpoints[path][method] = endpointData;
|
|
@@ -225,7 +396,13 @@ class ParserEngine {
|
|
|
225
396
|
hasAuth = false;
|
|
226
397
|
currentTags.clear();
|
|
227
398
|
requestBodyFields.clear();
|
|
399
|
+
requestFileFields.clear();
|
|
400
|
+
queryParams.clear();
|
|
401
|
+
headerParams.clear();
|
|
402
|
+
responseSchemas.clear();
|
|
403
|
+
pendingResponseCode = null;
|
|
228
404
|
bodyJsonBuffer.clear();
|
|
405
|
+
responseJsonBuffer.clear();
|
|
229
406
|
}
|
|
230
407
|
|
|
231
408
|
for (final l in lines) {
|
|
@@ -259,6 +436,29 @@ class ParserEngine {
|
|
|
259
436
|
}
|
|
260
437
|
}
|
|
261
438
|
|
|
439
|
+
if (isReadingResponseJson) {
|
|
440
|
+
if (!_isCommentLine(trimmed, commentPrefix)) {
|
|
441
|
+
isReadingResponseJson = false;
|
|
442
|
+
if (responseJsonBuffer.isNotEmpty && pendingResponseCode != null) {
|
|
443
|
+
try {
|
|
444
|
+
final jsonData = jsonDecode(responseJsonBuffer.toString());
|
|
445
|
+
_attachResponseExample(
|
|
446
|
+
pendingResponseCode!,
|
|
447
|
+
jsonData,
|
|
448
|
+
responseSchemas,
|
|
449
|
+
);
|
|
450
|
+
} catch (e) {
|
|
451
|
+
_errors.add('Invalid response JSON at line $lineNumber: $e');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
responseJsonBuffer.clear();
|
|
455
|
+
} else {
|
|
456
|
+
final commentText = _extractCommentText(trimmed, commentPrefix);
|
|
457
|
+
responseJsonBuffer.write(commentText);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
262
462
|
if (!_isCommentLine(trimmed, commentPrefix)) {
|
|
263
463
|
if (trimmed.isNotEmpty) {
|
|
264
464
|
flushEndpoint();
|
|
@@ -280,7 +480,10 @@ class ParserEngine {
|
|
|
280
480
|
flushEndpoint();
|
|
281
481
|
if (parts.length >= 3) {
|
|
282
482
|
currentMethod = parts[1].toUpperCase();
|
|
283
|
-
currentPath = parts[2]
|
|
483
|
+
currentPath = parts[2].replaceAllMapped(
|
|
484
|
+
RegExp(r':(\w+)'),
|
|
485
|
+
(m) => '{${m.group(1)}}',
|
|
486
|
+
);
|
|
284
487
|
} else {
|
|
285
488
|
_errors.add('@api requires method and path at line $lineNumber');
|
|
286
489
|
}
|
|
@@ -309,7 +512,24 @@ class ParserEngine {
|
|
|
309
512
|
'desc': fieldDesc,
|
|
310
513
|
});
|
|
311
514
|
} else {
|
|
312
|
-
_errors.add(
|
|
515
|
+
_errors.add(
|
|
516
|
+
'@body requires field name and type at line $lineNumber',
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
} else if (tag == '@file') {
|
|
520
|
+
if (parts.length >= 3) {
|
|
521
|
+
final fieldName = parts[1];
|
|
522
|
+
final fileType = parts[2];
|
|
523
|
+
final fieldDesc = parts.length > 3 ? parts.sublist(3).join(' ') : '';
|
|
524
|
+
requestFileFields.add({
|
|
525
|
+
'name': fieldName,
|
|
526
|
+
'type': fileType,
|
|
527
|
+
'desc': fieldDesc,
|
|
528
|
+
});
|
|
529
|
+
} else {
|
|
530
|
+
_errors.add(
|
|
531
|
+
'@file requires field name and type at line $lineNumber',
|
|
532
|
+
);
|
|
313
533
|
}
|
|
314
534
|
} else if (tag == '@body-file') {
|
|
315
535
|
if (parts.length >= 2) {
|
|
@@ -334,6 +554,92 @@ class ParserEngine {
|
|
|
334
554
|
} else if (tag == '@body-json') {
|
|
335
555
|
isReadingBodyJson = true;
|
|
336
556
|
bodyJsonBuffer.clear();
|
|
557
|
+
} else if (tag == '@response') {
|
|
558
|
+
if (parts.length >= 3) {
|
|
559
|
+
pendingResponseCode = parts[1];
|
|
560
|
+
final responseType = parts[2];
|
|
561
|
+
final responseDesc =
|
|
562
|
+
parts.length > 3 ? parts.sublist(3).join(' ') : '';
|
|
563
|
+
final lowerType = responseType.toLowerCase();
|
|
564
|
+
final schemaType = (lowerType == 'array') ? 'array' : 'object';
|
|
565
|
+
responseSchemas[pendingResponseCode!] = {
|
|
566
|
+
"description": responseDesc,
|
|
567
|
+
"content": <String, dynamic>{
|
|
568
|
+
"application/json": <String, dynamic>{
|
|
569
|
+
"schema": <String, dynamic>{"type": schemaType},
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
} else {
|
|
574
|
+
_errors.add(
|
|
575
|
+
'@response requires status code and type at line $lineNumber',
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
} else if (tag == '@response-json') {
|
|
579
|
+
if (pendingResponseCode != null &&
|
|
580
|
+
responseSchemas.containsKey(pendingResponseCode)) {
|
|
581
|
+
final rest = commentText.replaceFirst('@response-json', '').trim();
|
|
582
|
+
if (rest.isNotEmpty) {
|
|
583
|
+
try {
|
|
584
|
+
final jsonData = jsonDecode(rest);
|
|
585
|
+
_attachResponseExample(
|
|
586
|
+
pendingResponseCode!,
|
|
587
|
+
jsonData,
|
|
588
|
+
responseSchemas,
|
|
589
|
+
);
|
|
590
|
+
} catch (_) {
|
|
591
|
+
isReadingResponseJson = true;
|
|
592
|
+
responseJsonBuffer.clear();
|
|
593
|
+
responseJsonBuffer.write(rest);
|
|
594
|
+
}
|
|
595
|
+
} else {
|
|
596
|
+
isReadingResponseJson = true;
|
|
597
|
+
responseJsonBuffer.clear();
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} else if (tag == '@query') {
|
|
601
|
+
if (parts.length >= 3) {
|
|
602
|
+
final name = parts[1];
|
|
603
|
+
final type = parts[2];
|
|
604
|
+
String desc = '';
|
|
605
|
+
String? defaultValue;
|
|
606
|
+
|
|
607
|
+
final rest = parts.length > 3 ? parts.sublist(3).join(' ') : '';
|
|
608
|
+
final defaultMatch =
|
|
609
|
+
RegExp(r'\(default:\s*(.+?)\)').firstMatch(rest);
|
|
610
|
+
if (defaultMatch != null) {
|
|
611
|
+
defaultValue = defaultMatch.group(1);
|
|
612
|
+
desc = rest.replaceFirst(defaultMatch.group(0)!, '').trim();
|
|
613
|
+
} else {
|
|
614
|
+
desc = rest;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
queryParams.add({
|
|
618
|
+
'name': name,
|
|
619
|
+
'type': type,
|
|
620
|
+
'desc': desc,
|
|
621
|
+
'default': defaultValue,
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
_errors.add(
|
|
625
|
+
'@query requires name and type at line $lineNumber',
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
} else if (tag == '@header') {
|
|
629
|
+
if (parts.length >= 3) {
|
|
630
|
+
final name = parts[1];
|
|
631
|
+
final type = parts[2];
|
|
632
|
+
final desc = parts.length > 3 ? parts.sublist(3).join(' ') : '';
|
|
633
|
+
headerParams.add({
|
|
634
|
+
'name': name,
|
|
635
|
+
'type': type,
|
|
636
|
+
'desc': desc,
|
|
637
|
+
});
|
|
638
|
+
} else {
|
|
639
|
+
_errors.add(
|
|
640
|
+
'@header requires name and type at line $lineNumber',
|
|
641
|
+
);
|
|
642
|
+
}
|
|
337
643
|
}
|
|
338
644
|
}
|
|
339
645
|
|
|
@@ -341,7 +647,7 @@ class ParserEngine {
|
|
|
341
647
|
|
|
342
648
|
if (_errors.isNotEmpty) {
|
|
343
649
|
for (final error in _errors) {
|
|
344
|
-
print('⚠️
|
|
650
|
+
print('⚠️ $error');
|
|
345
651
|
}
|
|
346
652
|
}
|
|
347
653
|
|
|
@@ -370,12 +676,11 @@ class ParserEngine {
|
|
|
370
676
|
|
|
371
677
|
_baseSpec['paths'] = fullPaths;
|
|
372
678
|
|
|
373
|
-
// Add tags if any
|
|
374
679
|
if (_tags.isNotEmpty) {
|
|
375
680
|
_baseSpec['tags'] = _tags.map((t) => {"name": t}).toList();
|
|
376
681
|
}
|
|
377
682
|
|
|
378
|
-
final swaggerFile = File(
|
|
683
|
+
final swaggerFile = File(_outputPath);
|
|
379
684
|
if (!swaggerFile.parent.existsSync()) {
|
|
380
685
|
swaggerFile.parent.createSync(recursive: true);
|
|
381
686
|
}
|
|
@@ -387,4 +692,45 @@ class ParserEngine {
|
|
|
387
692
|
'🐸 FroggyDocs: Updated documentation. Total paths: ${fullPaths.length}',
|
|
388
693
|
);
|
|
389
694
|
}
|
|
695
|
+
|
|
696
|
+
Map<String, dynamic> generateSpec() {
|
|
697
|
+
final Map<String, dynamic> fullPaths = {};
|
|
698
|
+
for (final fileMap in _fileRegistry.values) {
|
|
699
|
+
for (final entry in fileMap.entries) {
|
|
700
|
+
final path = entry.key;
|
|
701
|
+
final methods = entry.value;
|
|
702
|
+
if (fullPaths[path] == null) {
|
|
703
|
+
fullPaths[path] = {};
|
|
704
|
+
}
|
|
705
|
+
(fullPaths[path] as Map).addAll(methods);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
final spec = Map<String, dynamic>.from(_baseSpec);
|
|
709
|
+
spec['paths'] = fullPaths;
|
|
710
|
+
if (_tags.isNotEmpty) {
|
|
711
|
+
spec['tags'] = _tags.map((t) => {"name": t}).toList();
|
|
712
|
+
}
|
|
713
|
+
return spec;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
void clearRegistry() {
|
|
717
|
+
_fileRegistry.clear();
|
|
718
|
+
_tags.clear();
|
|
719
|
+
_errors.clear();
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
int get pathCount {
|
|
723
|
+
final Map<String, dynamic> fullPaths = {};
|
|
724
|
+
for (final fileMap in _fileRegistry.values) {
|
|
725
|
+
for (final entry in fileMap.entries) {
|
|
726
|
+
final path = entry.key;
|
|
727
|
+
final methods = entry.value;
|
|
728
|
+
if (fullPaths[path] == null) {
|
|
729
|
+
fullPaths[path] = {};
|
|
730
|
+
}
|
|
731
|
+
(fullPaths[path] as Map).addAll(methods);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return fullPaths.length;
|
|
735
|
+
}
|
|
390
736
|
}
|