froggy-docs 1.0.0

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.
@@ -0,0 +1,390 @@
1
+ import 'dart:io';
2
+ import 'dart:convert';
3
+ import 'package:path/path.dart' as p;
4
+
5
+ class ParserEngine {
6
+ static final Map<String, Map<String, dynamic>> _fileRegistry = {};
7
+
8
+ static const Set<String> supportedExtensions = {
9
+ '.dart',
10
+ '.js',
11
+ '.ts',
12
+ '.jsx',
13
+ '.tsx',
14
+ '.py',
15
+ '.go',
16
+ '.rs',
17
+ '.java',
18
+ '.kt',
19
+ '.scala',
20
+ '.php',
21
+ '.rb',
22
+ '.cs',
23
+ '.c',
24
+ '.cpp',
25
+ '.h',
26
+ '.hpp',
27
+ };
28
+
29
+ static const Set<String> validMethods = {
30
+ 'get',
31
+ 'post',
32
+ 'put',
33
+ 'patch',
34
+ 'delete',
35
+ 'head',
36
+ 'options',
37
+ };
38
+
39
+ final Map<String, dynamic> _baseSpec = {
40
+ "openapi": "3.0.0",
41
+ "info": {
42
+ "title": "FroggyDocs Generated API",
43
+ "version": "1.0.0",
44
+ "description": "Auto-generated by FroggyDocs",
45
+ },
46
+ "paths": {},
47
+ "components": {
48
+ "securitySchemes": {
49
+ "bearerAuth": {
50
+ "type": "http",
51
+ "scheme": "bearer",
52
+ "bearerFormat": "JWT",
53
+ },
54
+ },
55
+ },
56
+ };
57
+
58
+ final List<String> _errors = [];
59
+ final Set<String> _tags = {};
60
+
61
+ String _getCommentPrefix(String filePath) {
62
+ final ext = p.extension(filePath).toLowerCase();
63
+ return switch (ext) {
64
+ '.dart' => '//',
65
+ '.js' || '.jsx' || '.ts' || '.tsx' => '//',
66
+ '.py' || '.rb' => '#',
67
+ '.go' ||
68
+ '.rs' ||
69
+ '.c' ||
70
+ '.cpp' ||
71
+ '.h' ||
72
+ '.hpp' ||
73
+ '.java' ||
74
+ '.kt' ||
75
+ '.scala' ||
76
+ '.cs' => '//',
77
+ '.php' => '//',
78
+ _ => '//',
79
+ };
80
+ }
81
+
82
+ bool _isCommentLine(String line, String prefix) {
83
+ if (prefix == '//') {
84
+ return line.trimLeft().startsWith('//');
85
+ } else if (prefix == '#') {
86
+ return line.trimLeft().startsWith('#');
87
+ }
88
+ return line.trimLeft().startsWith(prefix);
89
+ }
90
+
91
+ String _extractCommentText(String line, String prefix) {
92
+ if (prefix == '//') {
93
+ return line.replaceFirst(RegExp(r'^//\s*'), '').trim();
94
+ } else if (prefix == '#') {
95
+ return line.replaceFirst(RegExp(r'^#\s*'), '').trim();
96
+ }
97
+ return line.replaceFirst(RegExp(r'^$prefix\s*'), '').trim();
98
+ }
99
+
100
+ void removeFile(String filePath) {
101
+ if (_fileRegistry.containsKey(filePath)) {
102
+ _fileRegistry.remove(filePath);
103
+ _generateSwaggerFile();
104
+ }
105
+ }
106
+
107
+ void parseFile(String filePath) {
108
+ _errors.clear();
109
+
110
+ if (filePath.contains('frontend/') || filePath.contains('.dart_tool')) {
111
+ return;
112
+ }
113
+
114
+ final file = File(filePath);
115
+ if (!file.existsSync()) {
116
+ removeFile(filePath);
117
+ return;
118
+ }
119
+
120
+ final ext = p.extension(filePath).toLowerCase();
121
+ if (!supportedExtensions.contains(ext)) {
122
+ return;
123
+ }
124
+
125
+ final lines = file.readAsLinesSync();
126
+ final commentPrefix = _getCommentPrefix(filePath);
127
+ Map<String, dynamic> fileEndpoints = {};
128
+
129
+ String? currentMethod;
130
+ String? currentPath;
131
+ String description = '';
132
+ bool hasAuth = false;
133
+ List<String> currentTags = [];
134
+ List<Map<String, String>> requestBodyFields = [];
135
+ bool isReadingBodyJson = false;
136
+ StringBuffer bodyJsonBuffer = StringBuffer();
137
+ int lineNumber = 0;
138
+
139
+ String inferType(dynamic value) {
140
+ if (value is String) return 'string';
141
+ if (value is num) return 'number';
142
+ if (value is bool) return 'boolean';
143
+ if (value is List) return 'array';
144
+ if (value is Map) return 'object';
145
+ return 'string';
146
+ }
147
+
148
+ void flushEndpoint() {
149
+ if (currentMethod != null) {
150
+ if (currentPath == null) {
151
+ _errors.add('Endpoint defined without path at line $lineNumber');
152
+ currentMethod = null;
153
+ currentPath = null;
154
+ description = '';
155
+ hasAuth = false;
156
+ requestBodyFields.clear();
157
+ bodyJsonBuffer.clear();
158
+ return;
159
+ }
160
+
161
+ if (!validMethods.contains(currentMethod!.toLowerCase())) {
162
+ _errors.add(
163
+ 'Invalid HTTP method "$currentMethod" at line $lineNumber',
164
+ );
165
+ currentMethod = null;
166
+ currentPath = null;
167
+ description = '';
168
+ hasAuth = false;
169
+ requestBodyFields.clear();
170
+ bodyJsonBuffer.clear();
171
+ return;
172
+ }
173
+
174
+ final path = currentPath!;
175
+ final method = currentMethod!.toLowerCase();
176
+
177
+ if (fileEndpoints[path] == null) {
178
+ fileEndpoints[path] = {};
179
+ }
180
+
181
+ Map<String, dynamic> endpointData = {
182
+ "summary": description.isNotEmpty ? description : "No description",
183
+ "responses": {
184
+ "200": {"description": "Successful response"},
185
+ },
186
+ };
187
+
188
+ if (currentTags.isNotEmpty) {
189
+ endpointData['tags'] = currentTags.toList();
190
+ }
191
+
192
+ if (hasAuth) {
193
+ endpointData['security'] = [
194
+ {"bearerAuth": []},
195
+ ];
196
+ }
197
+
198
+ if (requestBodyFields.isNotEmpty) {
199
+ Map<String, dynamic> properties = {};
200
+ for (var field in requestBodyFields) {
201
+ final fieldName = field['name'];
202
+ if (fieldName != null && fieldName.isNotEmpty) {
203
+ properties[fieldName] = {
204
+ "type": (field['type'] ?? 'string').toString().toLowerCase(),
205
+ "description": field['desc'] ?? '',
206
+ };
207
+ }
208
+ }
209
+
210
+ endpointData['requestBody'] = {
211
+ "content": {
212
+ "application/json": {
213
+ "schema": {"type": "object", "properties": properties},
214
+ },
215
+ },
216
+ };
217
+ }
218
+
219
+ fileEndpoints[path][method] = endpointData;
220
+ }
221
+
222
+ currentMethod = null;
223
+ currentPath = null;
224
+ description = '';
225
+ hasAuth = false;
226
+ currentTags.clear();
227
+ requestBodyFields.clear();
228
+ bodyJsonBuffer.clear();
229
+ }
230
+
231
+ for (final l in lines) {
232
+ lineNumber++;
233
+ final trimmed = l.trim();
234
+
235
+ if (isReadingBodyJson) {
236
+ if (!_isCommentLine(trimmed, commentPrefix)) {
237
+ isReadingBodyJson = false;
238
+ if (bodyJsonBuffer.isNotEmpty) {
239
+ try {
240
+ final jsonData = jsonDecode(bodyJsonBuffer.toString());
241
+ if (jsonData is Map) {
242
+ for (final entry in jsonData.entries) {
243
+ requestBodyFields.add({
244
+ 'name': entry.key.toString(),
245
+ 'type': inferType(entry.value),
246
+ 'desc': 'Inferred from JSON',
247
+ });
248
+ }
249
+ }
250
+ } catch (e) {
251
+ _errors.add('Invalid JSON at line $lineNumber: $e');
252
+ }
253
+ }
254
+ bodyJsonBuffer.clear();
255
+ } else {
256
+ final commentText = _extractCommentText(trimmed, commentPrefix);
257
+ bodyJsonBuffer.write(commentText);
258
+ continue;
259
+ }
260
+ }
261
+
262
+ if (!_isCommentLine(trimmed, commentPrefix)) {
263
+ if (trimmed.isNotEmpty) {
264
+ flushEndpoint();
265
+ }
266
+ continue;
267
+ }
268
+
269
+ final commentText = _extractCommentText(trimmed, commentPrefix);
270
+ if (!commentText.startsWith('@')) {
271
+ continue;
272
+ }
273
+
274
+ final parts = commentText.split(RegExp(r'\s+'));
275
+ if (parts.isEmpty) continue;
276
+
277
+ final tag = parts[0];
278
+
279
+ if (tag == '@api') {
280
+ flushEndpoint();
281
+ if (parts.length >= 3) {
282
+ currentMethod = parts[1].toUpperCase();
283
+ currentPath = parts[2];
284
+ } else {
285
+ _errors.add('@api requires method and path at line $lineNumber');
286
+ }
287
+ } else if (tag == '@desc') {
288
+ description = commentText.replaceFirst('@desc', '').trim();
289
+ } else if (tag == '@auth') {
290
+ hasAuth = true;
291
+ } else if (tag == '@tag') {
292
+ if (parts.length >= 2) {
293
+ final tagName = parts.sublist(1).join(' ');
294
+ if (!_tags.contains(tagName)) {
295
+ _tags.add(tagName);
296
+ }
297
+ if (!currentTags.contains(tagName)) {
298
+ currentTags.add(tagName);
299
+ }
300
+ }
301
+ } else if (tag == '@body') {
302
+ if (parts.length >= 3) {
303
+ final fieldName = parts[1];
304
+ final fieldType = parts[2];
305
+ final fieldDesc = parts.length > 3 ? parts.sublist(3).join(' ') : '';
306
+ requestBodyFields.add({
307
+ 'name': fieldName,
308
+ 'type': fieldType,
309
+ 'desc': fieldDesc,
310
+ });
311
+ } else {
312
+ _errors.add('@body requires field name and type at line $lineNumber');
313
+ }
314
+ } else if (tag == '@body-file') {
315
+ if (parts.length >= 2) {
316
+ final jsonFile = File(p.join(p.dirname(filePath), parts[1]));
317
+ if (jsonFile.existsSync()) {
318
+ try {
319
+ final jsonData = jsonDecode(jsonFile.readAsStringSync());
320
+ if (jsonData is Map) {
321
+ for (final entry in jsonData.entries) {
322
+ requestBodyFields.add({
323
+ 'name': entry.key.toString(),
324
+ 'type': inferType(entry.value),
325
+ 'desc': 'Inferred from file',
326
+ });
327
+ }
328
+ }
329
+ } catch (e) {
330
+ _errors.add('Invalid JSON file at line $lineNumber: $e');
331
+ }
332
+ }
333
+ }
334
+ } else if (tag == '@body-json') {
335
+ isReadingBodyJson = true;
336
+ bodyJsonBuffer.clear();
337
+ }
338
+ }
339
+
340
+ flushEndpoint();
341
+
342
+ if (_errors.isNotEmpty) {
343
+ for (final error in _errors) {
344
+ print('⚠️ $error');
345
+ }
346
+ }
347
+
348
+ if (fileEndpoints.isNotEmpty) {
349
+ _fileRegistry[filePath] = fileEndpoints;
350
+ _generateSwaggerFile();
351
+ } else if (_fileRegistry.containsKey(filePath)) {
352
+ _fileRegistry.remove(filePath);
353
+ _generateSwaggerFile();
354
+ }
355
+ }
356
+
357
+ void _generateSwaggerFile() {
358
+ final Map<String, dynamic> fullPaths = {};
359
+
360
+ for (final fileMap in _fileRegistry.values) {
361
+ for (final entry in fileMap.entries) {
362
+ final path = entry.key;
363
+ final methods = entry.value;
364
+ if (fullPaths[path] == null) {
365
+ fullPaths[path] = {};
366
+ }
367
+ (fullPaths[path] as Map).addAll(methods);
368
+ }
369
+ }
370
+
371
+ _baseSpec['paths'] = fullPaths;
372
+
373
+ // Add tags if any
374
+ if (_tags.isNotEmpty) {
375
+ _baseSpec['tags'] = _tags.map((t) => {"name": t}).toList();
376
+ }
377
+
378
+ final swaggerFile = File('frontend/web/froggy_docs.json');
379
+ if (!swaggerFile.parent.existsSync()) {
380
+ swaggerFile.parent.createSync(recursive: true);
381
+ }
382
+
383
+ swaggerFile.writeAsStringSync(
384
+ JsonEncoder.withIndent(' ').convert(_baseSpec),
385
+ );
386
+ print(
387
+ '🐸 FroggyDocs: Updated documentation. Total paths: ${fullPaths.length}',
388
+ );
389
+ }
390
+ }
@@ -0,0 +1,41 @@
1
+ // @api GET /api/simple
2
+ // @tag Users
3
+ // @desc Simple setup using method 1
4
+ // @body title String Post title
5
+ // @body views Number Total views count
6
+ app.post('/api/simple', (req, res) => {});
7
+
8
+ // @api GET /api/me
9
+ // @tag Users
10
+ // @desc Extracting from external file called mock.json using method 2
11
+ // @body-file ./mock.json
12
+ app.post('/api/me', (req, res) => {});
13
+
14
+ // @api PUT /api/inline-json
15
+ // @tag Users
16
+ // @desc Extracting from inline JSON block using method 3
17
+ app.put('/api/inline-json', (req, res) => {
18
+ res.json({
19
+ "message": "This is an inline JSON response",
20
+ })
21
+ });
22
+
23
+ //@api POST /api/login
24
+ //@tag Auth
25
+ //@desc User login endpoint with inline JSON body
26
+ app.post('/api/login', (req, res) => {
27
+ res.json({
28
+ "token": "abc123",
29
+ "expiresIn": 3600
30
+ });
31
+ });
32
+
33
+ //@api POST /api/register
34
+ //@tag Auth
35
+ //@desc User registration endpoint with inline JSON body
36
+ app.post('/api/register', (req, res) => {
37
+ res.json({
38
+ "message": "User registered successfully",
39
+ "userId": 12345
40
+ });
41
+ });
@@ -0,0 +1,58 @@
1
+ import 'dart:io';
2
+ import 'package:watcher/watcher.dart';
3
+ import 'package:path/path.dart' as p;
4
+ import 'parser_engine.dart';
5
+
6
+ class WatcherEngine {
7
+ final ParserEngine _parser = ParserEngine();
8
+
9
+ void startWatching(String directoryPath) {
10
+ print('🚀 Initializing FroggyDocs scan...');
11
+ _initialScan(directoryPath);
12
+
13
+ final watcher = DirectoryWatcher(directoryPath);
14
+ print('👀 Watching for changes in: ${p.absolute(directoryPath)}...\n');
15
+
16
+ watcher.events.listen((WatchEvent event) {
17
+ if (_shouldIgnore(event.path)) return;
18
+ _handleFileChange(event);
19
+ });
20
+ }
21
+
22
+ void _initialScan(String directoryPath) {
23
+ final dir = Directory(directoryPath);
24
+ try {
25
+ dir.listSync(recursive: true).forEach((entity) {
26
+ if (entity is File && !_shouldIgnore(entity.path)) {
27
+ _parser.parseFile(entity.path);
28
+ }
29
+ });
30
+ } catch (e) {
31
+ print('⚠️ Warning during initial scan: $e');
32
+ }
33
+ print('✅ Initial scan complete.\n');
34
+ }
35
+
36
+ bool _shouldIgnore(String path) {
37
+ final normalizedPath = path.replaceAll('\\', '/');
38
+ return normalizedPath.contains('/.dart_tool/') ||
39
+ normalizedPath.contains('/frontend/') ||
40
+ normalizedPath.contains('/node_modules/') ||
41
+ normalizedPath.contains('/.git/') ||
42
+ normalizedPath.endsWith('.json') ||
43
+ normalizedPath.endsWith('.md');
44
+ }
45
+
46
+ void _handleFileChange(WatchEvent event) {
47
+ switch (event.type) {
48
+ case ChangeType.ADD:
49
+ case ChangeType.MODIFY:
50
+ _parser.parseFile(event.path);
51
+ break;
52
+ case ChangeType.REMOVE:
53
+ print('🗑️ Removing documentation for: ${p.basename(event.path)}');
54
+ _parser.removeFile(event.path);
55
+ break;
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,164 @@
1
+ import 'dart:io';
2
+ import 'dart:async';
3
+ import 'dart:convert';
4
+ import 'package:shelf/shelf.dart';
5
+ import 'package:shelf/shelf_io.dart' as shelf_io;
6
+ import 'package:shelf_router/shelf_router.dart';
7
+ import 'package:path/path.dart' as p;
8
+
9
+ const defaultPort = 8080;
10
+
11
+ Future<void> startServer({int port = defaultPort}) async {
12
+ final handler = const Pipeline()
13
+ .addMiddleware(logRequests())
14
+ .addHandler(_router.call);
15
+
16
+ final server = await shelf_io.serve(handler, 'localhost', port);
17
+ print(
18
+ '🐸 FroggyDocs server running at http://${server.address.host}:${server.port}',
19
+ );
20
+ print('📖 Open http://${server.address.host}:${server.port} in your browser');
21
+ }
22
+
23
+ const _corsHeaders = {
24
+ 'Access-Control-Allow-Origin': '*',
25
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
26
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
27
+ };
28
+
29
+ const String webDir = 'frontend/web';
30
+ const String deployDir = 'frontend/deploy/web';
31
+
32
+ Router get _router {
33
+ final router = Router();
34
+
35
+ router.get('/froggy_docs.json', (Request request) async {
36
+ final file = File(
37
+ p.join(Directory.current.path, webDir, 'froggy_docs.json'),
38
+ );
39
+ if (await file.existsSync()) {
40
+ return Response.ok(
41
+ file.openRead(),
42
+ headers: {'Content-Type': 'application/json'},
43
+ );
44
+ }
45
+ return Response.notFound('{"error": "Not found"}');
46
+ });
47
+
48
+ router.get('/', (Request request) async {
49
+ final indexFile = File(
50
+ p.join(Directory.current.path, deployDir, 'index.html'),
51
+ );
52
+ if (await indexFile.existsSync()) {
53
+ return Response.ok(
54
+ indexFile.openRead(),
55
+ headers: {'Content-Type': 'text/html'},
56
+ );
57
+ }
58
+ return Response.notFound('index.html not found');
59
+ });
60
+
61
+ // ═════════════════════════════════════════════════════════════
62
+ // Demo/Mock API Endpoints
63
+ // These are included for testing "Try It Out" functionality
64
+ // Remove these routes in production - your real API will handle requests
65
+ // ═════════════════════════════════════════════════════════════
66
+ router.post('/api/simple', (Request request) async {
67
+ final body = await request.readAsString();
68
+ return Response.ok(
69
+ jsonEncode({
70
+ 'status': 'success',
71
+ 'received': body.isNotEmpty ? jsonDecode(body) : {},
72
+ }),
73
+ headers: {'Content-Type': 'application/json'},
74
+ );
75
+ });
76
+
77
+ router.post('/api/me', (Request request) async {
78
+ return Response.ok(
79
+ jsonEncode({
80
+ 'message': 'Logged in',
81
+ 'user': {'email': 'test@example.com', 'id': 1},
82
+ }),
83
+ headers: {'Content-Type': 'application/json'},
84
+ );
85
+ });
86
+
87
+ router.put('/api/inline-json', (Request request) async {
88
+ return Response.ok(
89
+ jsonEncode({
90
+ 'status': 'updated',
91
+ 'timestamp': DateTime.now().toIso8601String(),
92
+ }),
93
+ headers: {'Content-Type': 'application/json'},
94
+ );
95
+ });
96
+
97
+ router.get(
98
+ '/api/simple',
99
+ (Request request) => Response.ok(
100
+ '{"message": "GET works"}',
101
+ headers: {'Content-Type': 'application/json'},
102
+ ),
103
+ );
104
+ router.get(
105
+ '/api/me',
106
+ (Request request) => Response.ok(
107
+ '{"message": "GET /api/me"}',
108
+ headers: {'Content-Type': 'application/json'},
109
+ ),
110
+ );
111
+ router.get(
112
+ '/api/inline-json',
113
+ (Request request) => Response.ok(
114
+ '{"message": "GET /api/inline-json"}',
115
+ headers: {'Content-Type': 'application/json'},
116
+ ),
117
+ );
118
+ router.delete(
119
+ '/api/simple',
120
+ (Request request) => Response.ok(
121
+ '{"message": "Deleted"}',
122
+ headers: {'Content-Type': 'application/json'},
123
+ ),
124
+ );
125
+
126
+ // ═════════════════════════════════════════════════════════════
127
+ // End of Demo/Mock API
128
+ // In production: Remove lines 66-124 or connect to your real API
129
+ // The "Try It Out" button will call your actual API endpoints
130
+ // ═════════════════════════════════════════════════════════════
131
+
132
+ router.get('/<path|[^/]+>', (Request request, String path) async {
133
+ var file = File(p.join(Directory.current.path, deployDir, path));
134
+ if (await file.existsSync()) {
135
+ return Response.ok(
136
+ file.openRead(),
137
+ headers: {'Content-Type': _getContentType(path)},
138
+ );
139
+ }
140
+ file = File(p.join(Directory.current.path, webDir, path));
141
+ if (await file.existsSync()) {
142
+ return Response.ok(
143
+ file.openRead(),
144
+ headers: {'Content-Type': _getContentType(path)},
145
+ );
146
+ }
147
+ return Response.notFound('File not found: $path');
148
+ });
149
+
150
+ return router;
151
+ }
152
+
153
+ String _getContentType(String path) {
154
+ final ext = p.extension(path).toLowerCase();
155
+ return switch (ext) {
156
+ '.html' => 'text/html',
157
+ '.css' => 'text/css',
158
+ '.js' => 'application/javascript',
159
+ '.json' => 'application/json',
160
+ '.svg' => 'image/svg+xml',
161
+ '.ico' => 'image/x-icon',
162
+ _ => 'application/octet-stream',
163
+ };
164
+ }