diff --git a/helpers/tools/converter/bin/extract_contracts.dart b/helpers/tools/converter/bin/extract_contracts.dart new file mode 100644 index 0000000..70bcb40 --- /dev/null +++ b/helpers/tools/converter/bin/extract_contracts.dart @@ -0,0 +1,156 @@ +import 'dart:io'; +import 'package:args/args.dart'; +import 'package:path/path.dart' as path; +import 'package:converter/src/extractors/base_extractor.dart'; +import 'package:converter/src/extractors/php_extractor.dart'; + +/// Factory for creating language-specific extractors +class ExtractorFactory { + /// Create an appropriate extractor based on file extension + static LanguageExtractor? createExtractor(String extension) { + switch (extension.toLowerCase()) { + case '.php': + return PhpExtractor(); + // TODO: Add more extractors as they're implemented + // case '.py': + // return PythonExtractor(); + // case '.ts': + // case '.js': + // return TypeScriptExtractor(); + // case '.java': + // return JavaExtractor(); + default: + return null; + } + } +} + +/// Main contract extractor CLI +class ContractExtractorCLI { + final String sourcePath; + final String outputPath; + final bool verbose; + + ContractExtractorCLI({ + required this.sourcePath, + required this.outputPath, + this.verbose = false, + }); + + /// Run the extraction process + Future<void> run() async { + try { + if (await FileSystemEntity.isDirectory(sourcePath)) { + await _processDirectory(sourcePath); + } else if (await FileSystemEntity.isFile(sourcePath)) { + await _processFile(sourcePath); + } else { + throw Exception('Source path does not exist: $sourcePath'); + } + } catch (e) { + print('Error: $e'); + exit(1); + } + } + + /// Process a directory recursively + Future<void> _processDirectory(String dirPath) async { + final dir = Directory(dirPath); + await for (final entity in dir.list(recursive: true)) { + if (entity is File) { + await _processFile(entity.path); + } + } + } + + /// Process a single file + Future<void> _processFile(String filePath) async { + final extension = path.extension(filePath); + final extractor = ExtractorFactory.createExtractor(extension); + + if (extractor == null) { + if (verbose) { + print('Skipping unsupported file type: $filePath'); + } + return; + } + + try { + // Calculate relative path to maintain directory structure + final relativePath = path.relative(filePath, from: sourcePath); + final destDir = path.join(outputPath, path.dirname(relativePath)); + + // Create destination directory + await Directory(destDir).create(recursive: true); + + // Extract contract + final contract = await extractor.parseFile(filePath); + final yamlContent = extractor.convertToYaml(contract); + + // Write YAML contract + final yamlFile = File(path.join( + destDir, + '${path.basenameWithoutExtension(filePath)}.yaml', + )); + await yamlFile.writeAsString(yamlContent); + + if (verbose) { + print('Processed: $filePath'); + } + } catch (e) { + print('Error processing $filePath: $e'); + } + } +} + +void main(List<String> arguments) async { + final parser = ArgParser() + ..addOption( + 'source', + abbr: 's', + help: 'Source file or directory path', + mandatory: true, + ) + ..addOption( + 'output', + abbr: 'o', + help: 'Output directory for YAML contracts', + mandatory: true, + ) + ..addFlag( + 'verbose', + abbr: 'v', + help: 'Enable verbose output', + defaultsTo: false, + ) + ..addFlag( + 'help', + abbr: 'h', + help: 'Show this help message', + negatable: false, + ); + + try { + final results = parser.parse(arguments); + + if (results['help'] as bool) { + print('Usage: dart extract_contracts.dart [options]'); + print(parser.usage); + exit(0); + } + + final cli = ContractExtractorCLI( + sourcePath: results['source'] as String, + outputPath: results['output'] as String, + verbose: results['verbose'] as bool, + ); + + await cli.run(); + print('Contract extraction completed successfully.'); + } catch (e) { + print('Error: $e'); + print('\nUsage: dart extract_contracts.dart [options]'); + print(parser.usage); + exit(1); + } +} diff --git a/helpers/tools/converter/example/extract_contracts_example.dart b/helpers/tools/converter/example/extract_contracts_example.dart new file mode 100644 index 0000000..c404c80 --- /dev/null +++ b/helpers/tools/converter/example/extract_contracts_example.dart @@ -0,0 +1,108 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; + +void main() async { + // Create a sample PHP file + final samplePhp = ''' +<?php + +namespace App\\Models; + +use Illuminate\\Database\\Eloquent\\Model; +use Illuminate\\Database\\Eloquent\\Factories\\HasFactory; +use App\\Interfaces\\UserInterface; + +/** + * User model class. + * Represents a user in the system. + */ +class User extends Model implements UserInterface { + use HasFactory; + + /** + * The attributes that are mass assignable. + * + * @var array<string> + */ + protected array \$fillable = [ + 'name', + 'email', + 'password', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array<string> + */ + protected array \$hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the user's full name. + * + * @param string \$title Optional title prefix + * @return string + */ + public function getFullName(string \$title = ''): string { + return trim(\$title . ' ' . \$this->name); + } + + /** + * Set the user's password. + * + * @param string \$value + * @return void + */ + public function setPasswordAttribute(string \$value): void { + \$this->attributes['password'] = bcrypt(\$value); + } +} +'''; + + // Create temporary directories + final tempDir = Directory.systemTemp.createTempSync('contract_example'); + final sourceDir = Directory(path.join(tempDir.path, 'source'))..createSync(); + final outputDir = Directory(path.join(tempDir.path, 'output'))..createSync(); + + // Write sample PHP file + final phpFile = File(path.join(sourceDir.path, 'User.php')); + await phpFile.writeAsString(samplePhp); + + // Run the contract extractor + print('Extracting contracts from ${sourceDir.path}'); + print('Output directory: ${outputDir.path}'); + + final result = await Process.run( + 'dart', + [ + 'run', + 'bin/extract_contracts.dart', + '--source', + sourceDir.path, + '--output', + outputDir.path, + '--verbose', + ], + ); + + if (result.exitCode != 0) { + print('Error: ${result.stderr}'); + exit(1); + } + + // Read and display the generated YAML + final yamlFile = File(path.join(outputDir.path, 'User.yaml')); + if (await yamlFile.exists()) { + print('\nGenerated YAML contract:'); + print('------------------------'); + print(await yamlFile.readAsString()); + } else { + print('Error: YAML file was not generated'); + } + + // Cleanup + tempDir.deleteSync(recursive: true); +} diff --git a/helpers/tools/converter/lib/src/extractors/base_extractor.dart b/helpers/tools/converter/lib/src/extractors/base_extractor.dart new file mode 100644 index 0000000..00b79c5 --- /dev/null +++ b/helpers/tools/converter/lib/src/extractors/base_extractor.dart @@ -0,0 +1,122 @@ +import 'dart:io'; +import 'package:path/path.dart' as path; +import 'yaml_formatter.dart'; + +/// Base class for all language extractors +abstract class LanguageExtractor { + /// File extension this extractor handles (e.g., '.php', '.py') + String get fileExtension; + + /// Parse a source file and extract its components + Future<Map<String, dynamic>> parseFile(String filePath); + + /// Extract class-level documentation + String? extractClassComment(String content); + + /// Extract dependencies (imports, use statements, etc.) + List<Map<String, String>> extractDependencies(String content); + + /// Extract class properties/fields + List<Map<String, dynamic>> extractProperties(String content); + + /// Extract class methods + List<Map<String, dynamic>> extractMethods(String content); + + /// Extract implemented interfaces + List<String> extractInterfaces(String content); + + /// Extract used traits/mixins + List<String> extractTraits(String content); + + /// Convert extracted data to YAML format + String convertToYaml(Map<String, dynamic> data) { + return YamlFormatter.toYaml(data); + } + + /// Process a directory of source files + Future<void> processDirectory(String sourceDir, String destDir) async { + final sourceDirectory = Directory(sourceDir); + + await for (final entity in sourceDirectory.list(recursive: true)) { + if (entity is! File || !entity.path.endsWith(fileExtension)) continue; + + final relativePath = path.relative(entity.path, from: sourceDir); + final destPath = path.join(destDir, path.dirname(relativePath)); + + await Directory(destPath).create(recursive: true); + + final data = await parseFile(entity.path); + final yamlContent = convertToYaml(data); + + final yamlFile = File(path.join( + destPath, '${path.basenameWithoutExtension(entity.path)}.yaml')); + + await yamlFile.writeAsString(yamlContent); + } + } + + /// Parse method parameters from a parameter string + List<Map<String, String>> parseParameters(String paramsStr) { + final params = <Map<String, String>>[]; + if (paramsStr.trim().isEmpty) return params; + + for (final param in paramsStr.split(',')) { + final parts = param.trim().split('='); + final paramInfo = <String, String>{ + 'name': parts[0].trim(), + }; + + if (parts.length > 1) { + paramInfo['default'] = parts[1].trim(); + } + + params.add(paramInfo); + } + + return params; + } + + /// Format a comment by removing common comment markers and whitespace + String? formatComment(String? comment) { + if (comment == null || comment.isEmpty) return null; + + return comment + .split('\n') + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .map((line) { + // Remove common comment markers + line = line.replaceAll(RegExp(r'^/\*+|\*+/$'), ''); + line = line.replaceAll(RegExp(r'^\s*\*\s*'), ''); + line = line.replaceAll(RegExp(r'^//\s*'), ''); + return line.trim(); + }) + .where((line) => line.isNotEmpty) + .join('\n'); + } + + /// Extract type information from a type string + Map<String, dynamic> extractTypeInfo(String typeStr) { + // Handle nullable types + final isNullable = typeStr.endsWith('?'); + if (isNullable) { + typeStr = typeStr.substring(0, typeStr.length - 1); + } + + // Handle generics + final genericMatch = RegExp(r'^([\w\d_]+)<(.+)>$').firstMatch(typeStr); + if (genericMatch != null) { + return { + 'base_type': genericMatch.group(1), + 'generic_params': + genericMatch.group(2)!.split(',').map((t) => t.trim()).toList(), + 'nullable': isNullable, + }; + } + + return { + 'type': typeStr, + 'nullable': isNullable, + }; + } +} diff --git a/helpers/tools/converter/lib/src/extractors/php_extractor.dart b/helpers/tools/converter/lib/src/extractors/php_extractor.dart new file mode 100644 index 0000000..9508a37 --- /dev/null +++ b/helpers/tools/converter/lib/src/extractors/php_extractor.dart @@ -0,0 +1,157 @@ +import 'dart:io'; +import 'base_extractor.dart'; + +/// Extracts contract information from PHP source files +class PhpExtractor extends LanguageExtractor { + @override + String get fileExtension => '.php'; + + @override + Future<Map<String, dynamic>> parseFile(String filePath) async { + final file = File(filePath); + final content = await file.readAsString(); + + return { + 'name': filePath.split('/').last.split('.').first, + 'class_comment': extractClassComment(content), + 'dependencies': extractDependencies(content), + 'properties': extractProperties(content), + 'methods': extractMethods(content), + 'traits': extractTraits(content), + 'interfaces': extractInterfaces(content), + }; + } + + @override + String? extractClassComment(String content) { + final regex = + RegExp(r'/\*\*(.*?)\*/\s*class', multiLine: true, dotAll: true); + final match = regex.firstMatch(content); + return formatComment(match?.group(1)); + } + + @override + List<Map<String, String>> extractDependencies(String content) { + final regex = RegExp(r'use\s+([\w\\]+)(?:\s+as\s+(\w+))?;'); + final matches = regex.allMatches(content); + + return matches.map((match) { + final fullName = match.group(1)!; + final alias = match.group(2); + return { + 'name': alias ?? fullName.split('\\').last, + 'type': 'class', // Assuming class for now + 'source': fullName, + }; + }).toList(); + } + + @override + List<Map<String, dynamic>> extractProperties(String content) { + final regex = RegExp( + r'(?:/\*\*(.*?)\*/\s*)?(public|protected|private)\s+(?:readonly\s+)?(?:static\s+)?(?:[\w|]+\s+)?\$(\w+)(?:\s*=\s*[^;]+)?;', + multiLine: true, + dotAll: true, + ); + final matches = regex.allMatches(content); + + return matches.map((match) { + return { + 'name': match.group(3), // Property name without $ + 'visibility': match.group(2), + 'comment': formatComment(match.group(1)), + }; + }).toList(); + } + + @override + List<Map<String, dynamic>> extractMethods(String content) { + final regex = RegExp( + r'(?:/\*\*(.*?)\*/\s*)?(public|protected|private)\s+(?:static\s+)?function\s+(\w+)\s*\((.*?)\)(?:\s*:\s*(?:[\w|\\]+))?\s*{', + multiLine: true, + dotAll: true, + ); + final matches = regex.allMatches(content); + + return matches.map((match) { + return { + 'name': match.group(3), + 'visibility': match.group(2), + 'parameters': _parseMethodParameters(match.group(4) ?? ''), + 'comment': formatComment(match.group(1)), + }; + }).toList(); + } + + List<Map<String, String>> _parseMethodParameters(String params) { + if (params.trim().isEmpty) return []; + + final parameters = <Map<String, String>>[]; + final paramList = params.split(','); + + for (var param in paramList) { + param = param.trim(); + if (param.isEmpty) continue; + + final paramInfo = <String, String>{}; + + // Handle type declaration and parameter name + final typeAndName = param.split(RegExp(r'\$')); + if (typeAndName.length > 1) { + // Has type declaration + final type = typeAndName[0].trim(); + if (type.isNotEmpty) { + paramInfo['type'] = type; + } + + // Handle parameter name and default value + final nameAndDefault = typeAndName[1].split('='); + paramInfo['name'] = nameAndDefault[0].trim(); + + if (nameAndDefault.length > 1) { + paramInfo['default'] = nameAndDefault[1].trim(); + } + } else { + // No type declaration, just name and possibly default value + final nameAndDefault = param.replaceAll(r'$', '').split('='); + paramInfo['name'] = nameAndDefault[0].trim(); + + if (nameAndDefault.length > 1) { + paramInfo['default'] = nameAndDefault[1].trim(); + } + } + + parameters.add(paramInfo); + } + + return parameters; + } + + @override + List<String> extractTraits(String content) { + final regex = RegExp(r'use\s+([\w\\]+(?:\s*,\s*[\w\\]+)*)\s*;'); + final matches = regex.allMatches(content); + final traits = <String>[]; + + for (final match in matches) { + final traitList = match.group(1)!.split(','); + traits.addAll(traitList.map((t) => t.trim())); + } + + return traits; + } + + @override + List<String> extractInterfaces(String content) { + final regex = RegExp(r'implements\s+([\w\\]+(?:\s*,\s*[\w\\]+)*)'); + final matches = regex.allMatches(content); + final interfaces = <String>[]; + + for (final match in matches) { + final interfaceList = match.group(1)!.split(','); + interfaces.addAll(interfaceList.map((i) => i.trim())); + } + + return interfaces; + } +} diff --git a/helpers/tools/converter/lib/src/extractors/yaml_formatter.dart b/helpers/tools/converter/lib/src/extractors/yaml_formatter.dart new file mode 100644 index 0000000..d093a43 --- /dev/null +++ b/helpers/tools/converter/lib/src/extractors/yaml_formatter.dart @@ -0,0 +1,223 @@ +/// Handles YAML formatting with proper comment preservation +class YamlFormatter { + /// Format a value for YAML output + static String format(dynamic value, {int indent = 0}) { + if (value == null) return 'null'; + + final indentStr = ' ' * indent; + + if (value is String) { + if (value.startsWith('#')) { + // Handle comments - preserve only actual comment content + return value + .split('\n') + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .map((line) => '$indentStr$line') + .join('\n'); + } + // Escape special characters and handle multiline strings + if (value.contains('\n') || value.contains('"')) { + return '|\n${value.split('\n').map((line) => '$indentStr ${line.trim()}').join('\n')}'; + } + return value.contains(' ') ? '"$value"' : value; + } + + if (value is num || value is bool) { + return value.toString(); + } + + if (value is List) { + if (value.isEmpty) return '[]'; + final buffer = StringBuffer('\n'); + for (final item in value) { + buffer.writeln( + '$indentStr- ${format(item, indent: indent + 2).trimLeft()}'); + } + return buffer.toString().trimRight(); + } + + if (value is Map) { + if (value.isEmpty) return '{}'; + final buffer = StringBuffer('\n'); + value.forEach((key, val) { + if (val != null) { + final formattedValue = format(val, indent: indent + 2); + if (formattedValue.contains('\n')) { + buffer.writeln('$indentStr$key:$formattedValue'); + } else { + buffer.writeln('$indentStr$key: $formattedValue'); + } + // Add extra newline between top-level sections + if (indent == 0) { + buffer.writeln(); + } + } + }); + return buffer.toString().trimRight(); + } + + return value.toString(); + } + + /// Extract the actual documentation from a comment block + static String _extractDocumentation(String comment) { + return comment + .split('\n') + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .where( + (line) => !line.contains('class ') && !line.contains('function ')) + .where((line) => !line.startsWith('@')) + .where((line) => !line.contains('use ') && !line.contains('protected ')) + .where((line) => !line.contains('];') && !line.contains('[')) + .where((line) => !line.contains("'")) + .where( + (line) => !line.contains('private ') && !line.contains('public ')) + .where((line) => !line.contains('\$')) + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .join('\n'); + } + + /// Format method documentation + static String formatMethodDoc(Map<String, dynamic> method) { + final buffer = StringBuffer(); + + // Add main comment + if (method['comment'] != null) { + final mainComment = _extractDocumentation(method['comment'].toString()); + if (mainComment.isNotEmpty) { + buffer.writeln( + mainComment.split('\n').map((line) => '# $line').join('\n')); + } + } + + // Add parameter documentation + final params = method['parameters'] as List<Map<String, String>>?; + if (params != null && params.isNotEmpty) { + buffer.writeln('# Parameters:'); + for (final param in params) { + final name = param['name']; + final type = param['type'] ?? 'mixed'; + final defaultValue = param['default']; + if (defaultValue != null) { + buffer.writeln('# $name ($type = $defaultValue)'); + } else { + buffer.writeln('# $name ($type)'); + } + } + } + + return buffer.toString().trimRight(); + } + + /// Format property documentation + static String formatPropertyDoc(Map<String, dynamic> property) { + final buffer = StringBuffer(); + + // Add main comment + if (property['comment'] != null) { + final mainComment = _extractDocumentation(property['comment'].toString()); + if (mainComment.isNotEmpty) { + buffer.writeln( + mainComment.split('\n').map((line) => '# $line').join('\n')); + } + } + + // Add visibility + if (property['visibility'] != null) { + buffer.writeln('# Visibility: ${property["visibility"]}'); + } + + return buffer.toString().trimRight(); + } + + /// Convert a contract to YAML format + static String toYaml(Map<String, dynamic> contract) { + final formatted = <String, dynamic>{}; + + // Format class documentation + if (contract['class_comment'] != null) { + final doc = _extractDocumentation(contract['class_comment'] as String); + if (doc.isNotEmpty) { + formatted['documentation'] = + doc.split('\n').map((line) => '# $line').join('\n'); + } + } + + // Format dependencies (remove duplicates) + if (contract['dependencies'] != null) { + final deps = contract['dependencies'] as List; + final uniqueDeps = <String, Map<String, String>>{}; + for (final dep in deps) { + final source = dep['source'] as String; + if (!uniqueDeps.containsKey(source)) { + uniqueDeps[source] = dep as Map<String, String>; + } + } + formatted['dependencies'] = uniqueDeps.values.toList(); + } + + // Format properties with documentation + if (contract['properties'] != null) { + formatted['properties'] = (contract['properties'] as List).map((prop) { + final doc = formatPropertyDoc(prop as Map<String, dynamic>); + return { + 'name': prop['name'], + 'visibility': prop['visibility'], + 'documentation': doc, + }; + }).toList(); + } + + // Format methods with documentation + if (contract['methods'] != null) { + formatted['methods'] = (contract['methods'] as List).map((method) { + final doc = formatMethodDoc(method as Map<String, dynamic>); + return { + 'name': method['name'], + 'visibility': method['visibility'], + 'parameters': method['parameters'], + 'documentation': doc, + }; + }).toList(); + } + + // Format interfaces (remove duplicates) + if (contract['interfaces'] != null) { + formatted['interfaces'] = + (contract['interfaces'] as List).toSet().toList(); + } + + // Format traits (remove duplicates and filter out interfaces) + if (contract['traits'] != null) { + final traits = (contract['traits'] as List) + .where((t) { + // Filter out duplicates from dependencies + if (contract['dependencies'] != null) { + final deps = contract['dependencies'] as List; + if (deps.any((d) => d['source'] == t)) { + return false; + } + } + // Filter out interfaces + if (formatted['interfaces'] != null) { + final interfaces = formatted['interfaces'] as List; + if (interfaces.contains(t)) { + return false; + } + } + return true; + }) + .toSet() + .toList(); + + if (traits.isNotEmpty) { + formatted['traits'] = traits; + } + } + + return format(formatted); + } +} diff --git a/helpers/tools/converter/lib/src/utils/class_generator_utils.dart b/helpers/tools/converter/lib/src/utils/class_generator_utils.dart new file mode 100644 index 0000000..8c3911d --- /dev/null +++ b/helpers/tools/converter/lib/src/utils/class_generator_utils.dart @@ -0,0 +1,168 @@ +import 'name_utils.dart'; +import 'type_conversion_utils.dart'; + +/// Utility class for generating Dart class code +class ClassGeneratorUtils { + /// Generate a constructor for a class + static String generateConstructor( + String className, List<Map<String, dynamic>> properties) { + final buffer = StringBuffer(); + + // Constructor signature + buffer.writeln(' $className({'); + final params = <String>[]; + for (final prop in properties) { + final propName = NameUtils.toDartName(prop['name'] as String); + final propType = + TypeConversionUtils.pythonToDartType(prop['type'] as String); + final hasDefault = prop['has_default'] == true; + + if (hasDefault) { + params.add(' $propType? $propName,'); + } else { + params.add(' required $propType $propName,'); + } + } + buffer.writeln(params.join('\n')); + buffer.writeln(' }) {'); + + // Initialize properties in constructor body + for (final prop in properties) { + final propName = NameUtils.toDartName(prop['name'] as String); + final propType = + TypeConversionUtils.pythonToDartType(prop['type'] as String); + final hasDefault = prop['has_default'] == true; + + if (hasDefault) { + final defaultValue = TypeConversionUtils.getDefaultValue(propType); + buffer.writeln(' _$propName = $propName ?? $defaultValue;'); + } else { + buffer.writeln(' _$propName = $propName;'); + } + } + buffer.writeln(' }'); + buffer.writeln(); + + return buffer.toString(); + } + + /// Generate property declarations and accessors + static String generateProperties(List<Map<String, dynamic>> properties) { + final buffer = StringBuffer(); + + for (final prop in properties) { + final propName = NameUtils.toDartName(prop['name'] as String); + final propType = + TypeConversionUtils.pythonToDartType(prop['type'] as String); + buffer.writeln(' late $propType _$propName;'); + + // Generate getter + buffer.writeln(' $propType get $propName => _$propName;'); + + // Generate setter if not readonly + final isReadonly = prop['is_readonly']; + if (isReadonly != null && !isReadonly) { + buffer.writeln(' set $propName($propType value) {'); + buffer.writeln(' _$propName = value;'); + buffer.writeln(' }'); + } + buffer.writeln(); + } + + return buffer.toString(); + } + + /// Generate a method implementation + static String generateMethod(Map<String, dynamic> method) { + if (method['name'] == '__init__') return ''; + + final buffer = StringBuffer(); + final methodName = NameUtils.toDartName(method['name'] as String); + final returnType = + TypeConversionUtils.pythonToDartType(method['return_type'] as String); + final methodDoc = method['docstring'] as String?; + final isAsync = method['is_async'] == true; + + if (methodDoc != null) { + buffer.writeln(' /// ${methodDoc.replaceAll('\n', '\n /// ')}'); + } + + // Method signature + if (isAsync) { + buffer.write(' Future<$returnType> $methodName('); + } else { + buffer.write(' $returnType $methodName('); + } + + // Parameters + final params = method['arguments'] as List?; + if (params != null && params.isNotEmpty) { + final paramStrings = <String>[]; + + for (final param in params) { + final paramName = NameUtils.toDartName(param['name'] as String); + final paramType = + TypeConversionUtils.pythonToDartType(param['type'] as String); + final isOptional = param['is_optional'] == true; + + if (isOptional) { + paramStrings.add('[$paramType $paramName]'); + } else { + paramStrings.add('$paramType $paramName'); + } + } + + buffer.write(paramStrings.join(', ')); + } + + buffer.write(')'); + if (isAsync) buffer.write(' async'); + buffer.writeln(' {'); + buffer.writeln(' // TODO: Implement $methodName'); + if (returnType == 'void') { + buffer.writeln(' throw UnimplementedError();'); + } else { + buffer.writeln(' throw UnimplementedError();'); + } + buffer.writeln(' }'); + buffer.writeln(); + + return buffer.toString(); + } + + /// Generate required interface implementations + static String generateRequiredImplementations( + List<String> bases, Map<String, dynamic> classContract) { + final buffer = StringBuffer(); + + // Generate BaseChain implementations + if (bases.contains('BaseChain')) { + buffer.writeln(' late Map<String, dynamic>? _memory;'); + buffer.writeln(' Map<String, dynamic>? get memory => _memory;'); + buffer.writeln(); + buffer.writeln(' late bool _verbose;'); + buffer.writeln(' bool get verbose => _verbose;'); + buffer.writeln(); + + // Constructor with required properties + buffer.writeln(' ${classContract['name']}({'); + buffer.writeln(' Map<String, dynamic>? memory,'); + buffer.writeln(' bool? verbose,'); + buffer.writeln(' }) {'); + buffer.writeln(' _memory = memory ?? {};'); + buffer.writeln(' _verbose = verbose ?? false;'); + buffer.writeln(' }'); + buffer.writeln(); + + // Required methods + buffer.writeln(' @override'); + buffer.writeln(' void setMemory(Map<String, dynamic> memory) {'); + buffer.writeln(' // TODO: Implement setMemory'); + buffer.writeln(' throw UnimplementedError();'); + buffer.writeln(' }'); + buffer.writeln(); + } + + return buffer.toString(); + } +} diff --git a/helpers/tools/converter/lib/src/utils/constructor_utils.dart b/helpers/tools/converter/lib/src/utils/constructor_utils.dart new file mode 100644 index 0000000..ec225c5 --- /dev/null +++ b/helpers/tools/converter/lib/src/utils/constructor_utils.dart @@ -0,0 +1,40 @@ +/// Utility functions for constructor generation +class ConstructorUtils { + /// Generate constructor parameter declarations + static String generateParameters(List<Map<String, dynamic>> properties) { + final buffer = StringBuffer(); + for (final prop in properties) { + final propName = prop['name'] as String; + final propType = prop['type'] as String; + final hasDefault = prop['has_default'] == true; + + if (hasDefault) { + buffer.writeln(' $propType? $propName,'); + } else { + buffer.writeln(' required $propType $propName,'); + } + } + return buffer.toString(); + } + + /// Generate constructor initialization statements + static String generateInitializers(List<Map<String, dynamic>> properties) { + final buffer = StringBuffer(); + for (final prop in properties) { + final propName = prop['name'] as String; + final hasDefault = prop['has_default'] == true; + if (hasDefault) { + buffer.writeln( + ' _$propName = $propName ?? false;'); // TODO: Better default values + } else { + buffer.writeln(' _$propName = $propName;'); + } + } + return buffer.toString(); + } + + /// Check if a method is a constructor (__init__) + static bool isConstructor(Map<String, dynamic> method) { + return method['name'] == '__init__'; + } +} diff --git a/helpers/tools/converter/lib/src/utils/name_utils.dart b/helpers/tools/converter/lib/src/utils/name_utils.dart new file mode 100644 index 0000000..aeda7de --- /dev/null +++ b/helpers/tools/converter/lib/src/utils/name_utils.dart @@ -0,0 +1,23 @@ +/// Utility functions for name conversions +class NameUtils { + /// Convert Python method/property name to Dart style + static String toDartName(String pythonName) { + // Handle special Python method names + if (pythonName.startsWith('__') && pythonName.endsWith('__')) { + final name = pythonName.substring(2, pythonName.length - 2); + if (name == 'init') return 'new'; + return name; + } + + // Convert snake_case to camelCase + final parts = pythonName.split('_'); + if (parts.isEmpty) return pythonName; + + return parts.first + + parts + .skip(1) + .where((p) => p.isNotEmpty) + .map((p) => p[0].toUpperCase() + p.substring(1)) + .join(''); + } +} diff --git a/helpers/tools/converter/lib/src/utils/type_conversion_utils.dart b/helpers/tools/converter/lib/src/utils/type_conversion_utils.dart new file mode 100644 index 0000000..f43169a --- /dev/null +++ b/helpers/tools/converter/lib/src/utils/type_conversion_utils.dart @@ -0,0 +1,87 @@ +/// Utility class for converting Python types to Dart types +class TypeConversionUtils { + /// Convert a Python type string to its Dart equivalent + static String pythonToDartType(String pythonType) { + final typeMap = { + 'str': 'String', + 'int': 'int', + 'float': 'double', + 'bool': 'bool', + 'List': 'List', + 'Dict': 'Map', + 'dict': 'Map<String, dynamic>', + 'Any': 'dynamic', + 'None': 'void', + 'Optional': 'dynamic', + 'Union': 'dynamic', + 'Callable': 'Function', + }; + + // Handle generic types + if (pythonType.contains('[')) { + final match = RegExp(r'(\w+)\[(.*)\]').firstMatch(pythonType); + if (match != null) { + final baseType = match.group(1)!; + final genericType = match.group(2)!; + + if (baseType == 'List') { + return 'List<${pythonToDartType(genericType)}>'; + } else if (baseType == 'Dict' || baseType == 'dict') { + final types = genericType.split(','); + if (types.length == 2) { + return 'Map<${pythonToDartType(types[0].trim())}, ${pythonToDartType(types[1].trim())}>'; + } + return 'Map<String, dynamic>'; + } else if (baseType == 'Optional') { + final innerType = pythonToDartType(genericType); + if (innerType == 'Map<String, dynamic>') { + return 'Map<String, dynamic>?'; + } + return '${innerType}?'; + } + } + } + + // Handle raw types + if (pythonType == 'dict') { + return 'Map<String, dynamic>'; + } else if (pythonType == 'None') { + return 'void'; + } + + return typeMap[pythonType] ?? pythonType; + } + + /// Get a default value for a Dart type + static String getDefaultValue(String dartType) { + switch (dartType) { + case 'bool': + case 'bool?': + return 'false'; + case 'int': + case 'int?': + return '0'; + case 'double': + case 'double?': + return '0.0'; + case 'String': + case 'String?': + return "''"; + case 'Map<String, dynamic>': + case 'Map<String, dynamic>?': + return '{}'; + case 'List': + case 'List?': + return '[]'; + default: + if (dartType.startsWith('List<')) { + return '[]'; + } else if (dartType.startsWith('Map<')) { + return '{}'; + } else if (dartType.endsWith('?')) { + return 'null'; + } + return 'null'; + } + } +} diff --git a/helpers/tools/converter/lib/src/utils/type_utils.dart b/helpers/tools/converter/lib/src/utils/type_utils.dart new file mode 100644 index 0000000..7b33f63 --- /dev/null +++ b/helpers/tools/converter/lib/src/utils/type_utils.dart @@ -0,0 +1,20 @@ +/// Utility functions for type casting +class TypeUtils { + /// Cast a List<dynamic> to List<Map<String, dynamic>> + static List<Map<String, dynamic>> castToMapList(List<dynamic>? list) { + if (list == null) return []; + return list.map((item) => item as Map<String, dynamic>).toList(); + } + + /// Cast a dynamic value to Map<String, dynamic> + static Map<String, dynamic> castToMap(dynamic value) { + if (value == null) return {}; + return value as Map<String, dynamic>; + } + + /// Cast a List<dynamic> to List<String> + static List<String> castToStringList(List<dynamic>? list) { + if (list == null) return []; + return list.map((item) => item.toString()).toList(); + } +} diff --git a/helpers/tools/converter/lib/src/utils/yaml_utils.dart b/helpers/tools/converter/lib/src/utils/yaml_utils.dart new file mode 100644 index 0000000..eb60aed --- /dev/null +++ b/helpers/tools/converter/lib/src/utils/yaml_utils.dart @@ -0,0 +1,36 @@ +import 'package:yaml/yaml.dart'; + +/// Utility class for handling YAML conversions +class YamlUtils { + /// Convert YamlMap to regular Map recursively + static Map<String, dynamic> convertYamlToMap(YamlMap yamlMap) { + return Map<String, dynamic>.fromEntries( + yamlMap.entries.map((entry) { + if (entry.value is YamlMap) { + return MapEntry( + entry.key.toString(), + convertYamlToMap(entry.value as YamlMap), + ); + } else if (entry.value is YamlList) { + return MapEntry( + entry.key.toString(), + convertYamlList(entry.value as YamlList), + ); + } + return MapEntry(entry.key.toString(), entry.value); + }), + ); + } + + /// Convert YamlList to regular List recursively + static List<dynamic> convertYamlList(YamlList yamlList) { + return yamlList.map((item) { + if (item is YamlMap) { + return convertYamlToMap(item); + } else if (item is YamlList) { + return convertYamlList(item); + } + return item; + }).toList(); + } +} diff --git a/helpers/tools/converter/pubspec.lock b/helpers/tools/converter/pubspec.lock new file mode 100644 index 0000000..5b77b09 --- /dev/null +++ b/helpers/tools/converter/pubspec.lock @@ -0,0 +1,402 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77" + url: "https://pub.dev" + source: hosted + version: "73.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a" + url: "https://pub.dev" + source: hosted + version: "6.8.0" + args: + dependency: "direct main" + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "88b0fddbe4c92910fefc09cc0248f5e7f0cd23e450ded4c28f16ab8ee8f83268" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + lints: + dependency: "direct dev" + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" + url: "https://pub.dev" + source: hosted + version: "1.25.8" + test_api: + dependency: transitive + description: + name: test_api + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + url: "https://pub.dev" + source: hosted + version: "0.7.3" + test_core: + dependency: transitive + description: + name: test_core + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" + url: "https://pub.dev" + source: hosted + version: "0.6.5" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.5.0 <4.0.0" diff --git a/helpers/tools/converter/pubspec.yaml b/helpers/tools/converter/pubspec.yaml new file mode 100644 index 0000000..8e3f3e2 --- /dev/null +++ b/helpers/tools/converter/pubspec.yaml @@ -0,0 +1,15 @@ +name: converter +description: A Dart implementation of LangChain, providing tools and utilities for building applications powered by large language models (LLMs). +version: 0.1.0 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + yaml: ^3.1.2 + path: ^1.8.3 + args: ^2.4.2 + +dev_dependencies: + lints: ^2.1.1 + test: ^1.24.6 diff --git a/helpers/tools/converter/test/code_generator_class_test.dart b/helpers/tools/converter/test/code_generator_class_test.dart new file mode 100644 index 0000000..a97ffda --- /dev/null +++ b/helpers/tools/converter/test/code_generator_class_test.dart @@ -0,0 +1,95 @@ +import 'package:test/test.dart'; +import '../tools/generate_dart_code.dart'; + +void main() { + group('Class Generation', () { + test('generates class with interface implementations', () { + final classContract = { + 'name': 'SimpleChain', + 'docstring': 'A simple implementation of a chain.', + 'bases': ['BaseChain'], + 'methods': [ + { + 'name': 'run', + 'return_type': 'dict', + 'arguments': [ + { + 'name': 'inputs', + 'type': 'dict', + 'is_optional': false, + } + ], + 'docstring': 'Execute the chain logic.', + } + ], + }; + + final code = generateClass(classContract); + + // Should include BaseChain implementations + expect(code, contains('late Map<String, dynamic>? _memory;')); + expect(code, contains('Map<String, dynamic>? get memory => _memory;')); + expect(code, contains('late bool _verbose;')); + expect(code, contains('bool get verbose => _verbose;')); + + // Should include constructor with required properties + expect(code, contains('SimpleChain({')); + expect(code, contains('Map<String, dynamic>? memory,')); + expect(code, contains('bool? verbose,')); + expect(code, contains('_memory = memory ?? {};')); + expect(code, contains('_verbose = verbose ?? false;')); + + // Should include required method implementations + expect(code, contains('@override')); + expect(code, contains('void setMemory(Map<String, dynamic> memory)')); + + // Should include additional methods + expect(code, contains('Map<String, dynamic> run(')); + expect(code, contains('Map<String, dynamic> inputs')); + expect(code, contains('/// Execute the chain logic.')); + }); + + test('generates class with own properties and methods', () { + final classContract = { + 'name': 'CustomClass', + 'docstring': 'A custom class.', + 'properties': [ + { + 'name': 'model_name', + 'type': 'String', + 'has_default': false, + } + ], + 'methods': [ + { + 'name': 'process', + 'return_type': 'String', + 'arguments': [ + { + 'name': 'input', + 'type': 'String', + 'is_optional': false, + } + ], + 'docstring': 'Process input.', + } + ], + }; + + final code = generateClass(classContract); + + // Should include properties + expect(code, contains('late String _modelName;')); + expect(code, contains('String get modelName => _modelName;')); + + // Should include constructor + expect(code, contains('CustomClass({')); + expect(code, contains('required String modelName')); + expect(code, contains('_modelName = modelName;')); + + // Should include methods + expect(code, contains('String process(String input)')); + expect(code, contains('/// Process input.')); + }); + }); +} diff --git a/helpers/tools/converter/test/code_generator_integration_test.dart b/helpers/tools/converter/test/code_generator_integration_test.dart new file mode 100644 index 0000000..5ebdb20 --- /dev/null +++ b/helpers/tools/converter/test/code_generator_integration_test.dart @@ -0,0 +1,84 @@ +import 'package:test/test.dart'; +import '../tools/generate_dart_code.dart'; +import '../lib/src/utils/class_generator_utils.dart'; + +void main() { + group('Code Generator Integration', () { + test('generates complete class with properties and methods', () { + final classContract = { + 'name': 'TestModel', + 'docstring': 'A test model implementation.', + 'bases': ['BaseModel'], + 'properties': [ + { + 'name': 'model_name', + 'type': 'String', + 'has_default': false, + }, + { + 'name': 'is_loaded', + 'type': 'bool', + 'has_default': true, + } + ], + 'methods': [ + { + 'name': '__init__', + 'return_type': 'None', + 'arguments': [ + { + 'name': 'model_name', + 'type': 'String', + 'is_optional': false, + 'has_default': false, + } + ], + 'docstring': 'Initialize the model.', + }, + { + 'name': 'process_input', + 'return_type': 'String', + 'arguments': [ + { + 'name': 'input_text', + 'type': 'String', + 'is_optional': false, + 'has_default': false, + } + ], + 'docstring': 'Process input text.', + 'is_async': true, + } + ], + }; + + final code = generateClass(classContract); + + // Class definition + expect(code, contains('class TestModel implements BaseModel {')); + expect(code, contains('/// A test model implementation.')); + + // Properties + expect(code, contains('late String _modelName;')); + expect(code, contains('String get modelName => _modelName;')); + expect(code, contains('late bool _isLoaded;')); + expect(code, contains('bool get isLoaded => _isLoaded;')); + + // Constructor + expect(code, contains('TestModel({')); + expect(code, contains('required String modelName,')); + expect(code, contains('bool? isLoaded,')); + expect(code, contains('_modelName = modelName;')); + expect(code, contains('_isLoaded = isLoaded ?? false;')); + + // Methods + expect(code, + contains('Future<String> processInput(String inputText) async {')); + expect(code, contains('/// Process input text.')); + + // No __init__ method + expect(code, isNot(contains('__init__'))); + expect(code, isNot(contains('void new('))); + }); + }); +} diff --git a/helpers/tools/converter/test/code_generator_name_test.dart b/helpers/tools/converter/test/code_generator_name_test.dart new file mode 100644 index 0000000..ce35404 --- /dev/null +++ b/helpers/tools/converter/test/code_generator_name_test.dart @@ -0,0 +1,63 @@ +import 'package:test/test.dart'; +import '../tools/generate_dart_code.dart'; + +void main() { + group('Code Generator Name Handling', () { + test('converts Python method names to Dart style in interfaces', () { + final interface = { + 'name': 'TestInterface', + 'docstring': 'Test interface.', + 'methods': [ + { + 'name': 'get_model_name', + 'return_type': 'str', + 'arguments': [], + 'docstring': 'Get model name.', + }, + { + 'name': '__init__', + 'return_type': 'None', + 'arguments': [ + { + 'name': 'model_path', + 'type': 'str', + 'is_optional': false, + 'has_default': false, + } + ], + 'docstring': 'Initialize.', + } + ], + 'properties': [], + }; + + final code = generateInterface(interface); + expect(code, contains('String getModelName();')); + expect(code, contains('void new(String modelPath);')); + }); + + test('converts Python property names to Dart style in classes', () { + final classContract = { + 'name': 'TestClass', + 'docstring': 'Test class.', + 'properties': [ + { + 'name': 'model_name', + 'type': 'str', + 'has_default': false, + }, + { + 'name': 'is_initialized', + 'type': 'bool', + 'has_default': true, + } + ], + 'methods': [], + }; + + final code = generateClass(classContract); + expect(code, contains('String get modelName')); + expect(code, contains('bool get isInitialized')); + }); + }); +} diff --git a/helpers/tools/converter/test/code_generator_test.dart b/helpers/tools/converter/test/code_generator_test.dart new file mode 100644 index 0000000..800f0c7 --- /dev/null +++ b/helpers/tools/converter/test/code_generator_test.dart @@ -0,0 +1,83 @@ +import 'package:test/test.dart'; +import '../tools/generate_dart_code.dart'; + +void main() { + group('Code Generator', () { + test('generates constructor correctly', () { + final classContract = { + 'name': 'TestClass', + 'docstring': 'Test class.', + 'properties': [ + { + 'name': 'name', + 'type': 'String', + 'has_default': false, + }, + { + 'name': 'is_ready', + 'type': 'bool', + 'has_default': true, + } + ], + }; + + final code = generateClass(classContract); + expect(code, contains('TestClass({')); + expect(code, contains('required String name,')); + expect(code, contains('bool? isReady,')); + expect(code, contains('_name = name;')); + expect(code, contains('_isReady = isReady ?? false;')); + }); + + test('handles async methods correctly', () { + final classContract = { + 'name': 'TestClass', + 'docstring': 'Test class.', + 'methods': [ + { + 'name': 'process', + 'return_type': 'String', + 'arguments': [ + { + 'name': 'input', + 'type': 'String', + 'is_optional': false, + } + ], + 'docstring': 'Process input.', + 'is_async': true, + } + ], + }; + + final code = generateClass(classContract); + expect(code, contains('Future<String> process(String input) async {')); + expect(code, contains('/// Process input.')); + }); + + test('initializes properties in constructor', () { + final classContract = { + 'name': 'TestClass', + 'docstring': 'Test class.', + 'properties': [ + { + 'name': 'name', + 'type': 'String', + 'has_default': false, + }, + { + 'name': 'is_ready', + 'type': 'bool', + 'has_default': true, + } + ], + }; + + final code = generateClass(classContract); + expect(code, contains('late String _name;')); + expect(code, contains('late bool _isReady;')); + expect(code, contains('_name = name;')); + expect(code, contains('_isReady = isReady ?? false;')); + }); + }); +} diff --git a/helpers/tools/converter/test/extractors/php_extractor_test.dart b/helpers/tools/converter/test/extractors/php_extractor_test.dart new file mode 100644 index 0000000..9bc9d14 --- /dev/null +++ b/helpers/tools/converter/test/extractors/php_extractor_test.dart @@ -0,0 +1,181 @@ +import 'package:test/test.dart'; +import '../../lib/src/extractors/php_extractor.dart'; + +void main() { + group('PhpExtractor', () { + final extractor = PhpExtractor(); + + test('extracts class comment', () { + const phpCode = ''' +/** + * User entity class. + * Represents a user in the system. + */ +class User { +} +'''; + final comment = extractor.extractClassComment(phpCode); + expect(comment, contains('User entity class')); + expect(comment, contains('Represents a user in the system')); + }); + + test('extracts dependencies', () { + const phpCode = ''' +use App\\Models\\User; +use Illuminate\\Support\\Str as StringHelper; +use App\\Interfaces\\UserInterface; +'''; + final deps = extractor.extractDependencies(phpCode); + expect(deps, hasLength(3)); + expect(deps[0]['name'], equals('User')); + expect(deps[1]['name'], equals('StringHelper')); + expect(deps[2]['name'], equals('UserInterface')); + expect(deps[1]['source'], equals('Illuminate\\Support\\Str')); + }); + + test('extracts properties', () { + const phpCode = ''' +class User { + /** + * The user's name. + */ + private string \$name; + + /** + * The user's email. + */ + protected string \$email; + + /** + * Is the user active? + */ + public bool \$isActive = false; +} +'''; + final props = extractor.extractProperties(phpCode); + expect(props, hasLength(3)); + + expect(props[0]['name'], equals('name')); + expect(props[0]['visibility'], equals('private')); + expect(props[0]['comment'], contains("The user's name")); + + expect(props[1]['name'], equals('email')); + expect(props[1]['visibility'], equals('protected')); + + expect(props[2]['name'], equals('isActive')); + expect(props[2]['visibility'], equals('public')); + }); + + test('extracts methods', () { + const phpCode = ''' +class User { + /** + * Get the user's full name. + * @param string \$title Optional title + * @return string + */ + public function getFullName(string \$title = '') { + return \$title . ' ' . \$this->name; + } + + /** + * Set the user's email address. + */ + protected function setEmail(string \$email) { + \$this->email = \$email; + } +} +'''; + final methods = extractor.extractMethods(phpCode); + expect(methods, hasLength(2)); + + expect(methods[0]['name'], equals('getFullName')); + expect(methods[0]['visibility'], equals('public')); + expect(methods[0]['parameters'], hasLength(1)); + expect(methods[0]['parameters'][0]['name'], equals('title')); + expect(methods[0]['parameters'][0]['default'], equals("''")); + expect(methods[0]['comment'], contains("Get the user's full name")); + + expect(methods[1]['name'], equals('setEmail')); + expect(methods[1]['visibility'], equals('protected')); + expect(methods[1]['parameters'], hasLength(1)); + expect(methods[1]['parameters'][0]['name'], equals('email')); + }); + + test('extracts interfaces', () { + const phpCode = ''' +class User implements UserInterface, Authenticatable { +} +'''; + final interfaces = extractor.extractInterfaces(phpCode); + expect(interfaces, hasLength(2)); + expect(interfaces[0], equals('UserInterface')); + expect(interfaces[1], equals('Authenticatable')); + }); + + test('extracts traits', () { + const phpCode = ''' +class User { + use HasFactory, Notifiable; +} +'''; + final traits = extractor.extractTraits(phpCode); + expect(traits, hasLength(2)); + expect(traits[0], equals('HasFactory')); + expect(traits[1], equals('Notifiable')); + }); + + test('generates valid YAML output', () { + const phpCode = ''' +/** + * User entity class. + */ +class User implements UserInterface { + use HasFactory; + + /** + * The user's name. + */ + private string \$name; + + /** + * Get the user's name. + */ + public function getName(): string { + return \$this->name; + } +} +'''; + final contract = { + 'name': 'User', + 'class_comment': extractor.extractClassComment(phpCode), + 'dependencies': extractor.extractDependencies(phpCode), + 'properties': extractor.extractProperties(phpCode), + 'methods': extractor.extractMethods(phpCode), + 'traits': extractor.extractTraits(phpCode), + 'interfaces': extractor.extractInterfaces(phpCode), + }; + + final yaml = extractor.convertToYaml(contract); + + // Check required sections + expect(yaml, contains('documentation:')); + expect(yaml, contains('properties:')); + expect(yaml, contains('methods:')); + expect(yaml, contains('interfaces:')); + + // Check content + expect(yaml, contains('User entity class')); + expect(yaml, contains('name: name')); + expect(yaml, contains('visibility: private')); + expect(yaml, contains('name: getName')); + expect(yaml, contains('visibility: public')); + expect(yaml, contains('UserInterface')); + + // Verify formatting + expect(yaml, isNot(contains('class User'))); + expect(yaml, isNot(contains('function'))); + expect(yaml, isNot(contains('private string'))); + }); + }); +} diff --git a/helpers/tools/converter/test/fixtures/contracts.yaml b/helpers/tools/converter/test/fixtures/contracts.yaml new file mode 100644 index 0000000..43dc8ac --- /dev/null +++ b/helpers/tools/converter/test/fixtures/contracts.yaml @@ -0,0 +1,112 @@ +interfaces: +- + name: "LLMProtocol" + bases: + - Protocol + methods: + - + name: "generate" + arguments: + - + name: "prompts" + type: "List[str]" + is_optional: false + has_default: false + return_type: "List[str]" + docstring: "Generate completions for the prompts." + decorators: + - + name: "abstractmethod" + is_abstract: true + - + name: "model_name" + arguments: + return_type: "str" + docstring: "Get the model name." + decorators: + - + name: "property" + - + name: "abstractmethod" + is_abstract: true + properties: + docstring: "Protocol for language models." + decorators: + is_interface: true +classes: +- + name: "BaseChain" + bases: + - ABC + methods: + - + name: "run" + arguments: + - + name: "inputs" + type: "dict" + is_optional: false + has_default: false + return_type: "dict" + docstring: "Run the chain on the inputs." + decorators: + - + name: "abstractmethod" + is_abstract: true + - + name: "set_memory" + arguments: + - + name: "memory" + type: "dict" + is_optional: false + has_default: false + return_type: "None" + docstring: "Set the memory for the chain." + decorators: + is_abstract: false + properties: + - + name: "memory" + type: "Optional[dict]" + has_default: true + - + name: "verbose" + type: "bool" + has_default: true + docstring: "Base class for chains." + decorators: + is_interface: false +- + name: "SimpleChain" + bases: + - BaseChain + methods: + - + name: "__init__" + arguments: + - + name: "llm" + type: "LLMProtocol" + is_optional: false + has_default: false + return_type: "None" + docstring: "Initialize the chain." + decorators: + is_abstract: false + - + name: "run" + arguments: + - + name: "inputs" + type: "dict" + is_optional: false + has_default: false + return_type: "dict" + docstring: "Execute the chain logic." + decorators: + is_abstract: false + properties: + docstring: "A simple implementation of a chain." + decorators: + is_interface: false diff --git a/helpers/tools/converter/test/fixtures/sample.py b/helpers/tools/converter/test/fixtures/sample.py new file mode 100644 index 0000000..76eb13b --- /dev/null +++ b/helpers/tools/converter/test/fixtures/sample.py @@ -0,0 +1,46 @@ +from typing import List, Optional, Protocol +from abc import ABC, abstractmethod + +class LLMProtocol(Protocol): + """Protocol for language models.""" + + @abstractmethod + async def generate(self, prompts: List[str], **kwargs) -> List[str]: + """Generate completions for the prompts.""" + pass + + @property + @abstractmethod + def model_name(self) -> str: + """Get the model name.""" + pass + +class BaseChain(ABC): + """Base class for chains.""" + + memory: Optional[dict] = None + verbose: bool = False + + @abstractmethod + async def run(self, inputs: dict) -> dict: + """Run the chain on the inputs.""" + pass + + def set_memory(self, memory: dict) -> None: + """Set the memory for the chain.""" + self.memory = memory + +class SimpleChain(BaseChain): + """A simple implementation of a chain.""" + + def __init__(self, llm: LLMProtocol): + """Initialize the chain.""" + self.llm = llm + self.history: List[str] = [] + + async def run(self, inputs: dict) -> dict: + """Execute the chain logic.""" + prompt = inputs.get("prompt", "") + result = await self.llm.generate([prompt]) + self.history.append(result[0]) + return {"output": result[0]} diff --git a/helpers/tools/converter/test/python_parser_test.dart b/helpers/tools/converter/test/python_parser_test.dart new file mode 100644 index 0000000..72d0496 --- /dev/null +++ b/helpers/tools/converter/test/python_parser_test.dart @@ -0,0 +1,91 @@ +import 'dart:io'; +import 'package:test/test.dart'; +import '../tools/python_parser.dart'; + +void main() { + group('PythonParser', () { + test('parses interface correctly', () async { + final file = File('test/fixtures/sample.py'); + final classes = await PythonParser.parseFile(file); + + final interface = classes.firstWhere((c) => c.isInterface); + expect(interface.name, equals('LLMProtocol')); + expect(interface.docstring, equals('Protocol for language models.')); + + // Test generate method + final generateMethod = + interface.methods.firstWhere((m) => m.name == 'generate'); + expect(generateMethod.isAsync, isTrue); + expect(generateMethod.isAbstract, isTrue); + expect(generateMethod.docstring, + equals('Generate completions for the prompts.')); + expect(generateMethod.parameters.length, equals(1)); + expect(generateMethod.parameters.first.name, equals('prompts')); + expect(generateMethod.parameters.first.type, equals('List[str]')); + expect(generateMethod.returnType, equals('List[str]')); + + // Test model_name property + final modelNameMethod = + interface.methods.firstWhere((m) => m.name == 'model_name'); + expect(modelNameMethod.isProperty, isTrue); + expect(modelNameMethod.isAbstract, isTrue); + expect(modelNameMethod.docstring, equals('Get the model name.')); + expect(modelNameMethod.parameters.isEmpty, isTrue); + expect(modelNameMethod.returnType, equals('str')); + }); + + test('parses abstract class correctly', () async { + final file = File('test/fixtures/sample.py'); + final classes = await PythonParser.parseFile(file); + + final abstractClass = classes.firstWhere((c) => c.name == 'BaseChain'); + expect(abstractClass.docstring, equals('Base class for chains.')); + + // Test properties + expect(abstractClass.properties.length, equals(2)); + final memoryProp = + abstractClass.properties.firstWhere((p) => p.name == 'memory'); + expect(memoryProp.type, equals('Optional[dict]')); + expect(memoryProp.hasDefault, isTrue); + + // Test run method + final runMethod = + abstractClass.methods.firstWhere((m) => m.name == 'run'); + expect(runMethod.isAsync, isTrue); + expect(runMethod.isAbstract, isTrue); + expect(runMethod.docstring, equals('Run the chain on the inputs.')); + expect(runMethod.parameters.length, equals(1)); + expect(runMethod.parameters.first.name, equals('inputs')); + expect(runMethod.parameters.first.type, equals('dict')); + expect(runMethod.returnType, equals('dict')); + }); + + test('parses concrete class correctly', () async { + final file = File('test/fixtures/sample.py'); + final classes = await PythonParser.parseFile(file); + + final concreteClass = classes.firstWhere((c) => c.name == 'SimpleChain'); + expect(concreteClass.docstring, + equals('A simple implementation of a chain.')); + + // Test constructor + final constructor = + concreteClass.methods.firstWhere((m) => m.name == '__init__'); + expect(constructor.docstring, equals('Initialize the chain.')); + expect(constructor.parameters.length, equals(1)); + expect(constructor.parameters.first.name, equals('llm')); + expect(constructor.parameters.first.type, equals('LLMProtocol')); + + // Test run method + final runMethod = + concreteClass.methods.firstWhere((m) => m.name == 'run'); + expect(runMethod.isAsync, isTrue); + expect(runMethod.isAbstract, isFalse); + expect(runMethod.docstring, equals('Execute the chain logic.')); + expect(runMethod.parameters.length, equals(1)); + expect(runMethod.parameters.first.name, equals('inputs')); + expect(runMethod.parameters.first.type, equals('dict')); + expect(runMethod.returnType, equals('dict')); + }); + }); +} diff --git a/helpers/tools/converter/test/utils/class_generator_utils_test.dart b/helpers/tools/converter/test/utils/class_generator_utils_test.dart new file mode 100644 index 0000000..d92fca6 --- /dev/null +++ b/helpers/tools/converter/test/utils/class_generator_utils_test.dart @@ -0,0 +1,85 @@ +import 'package:test/test.dart'; +import '../../lib/src/utils/class_generator_utils.dart'; + +void main() { + group('ClassGeneratorUtils', () { + test('generates constructor with property initialization', () { + final properties = [ + { + 'name': 'model_name', + 'type': 'String', + 'has_default': false, + }, + { + 'name': 'is_ready', + 'type': 'bool', + 'has_default': true, + } + ]; + + final code = + ClassGeneratorUtils.generateConstructor('TestClass', properties); + expect(code, contains('TestClass({')); + expect(code, contains('required String modelName,')); + expect(code, contains('bool? isReady,')); + expect(code, contains('_modelName = modelName;')); + expect(code, contains('_isReady = isReady ?? false;')); + }); + + test('generates properties with getters and setters', () { + final properties = [ + { + 'name': 'model_name', + 'type': 'String', + 'is_readonly': true, + }, + { + 'name': 'is_ready', + 'type': 'bool', + 'is_readonly': false, + } + ]; + + final code = ClassGeneratorUtils.generateProperties(properties); + expect(code, contains('late String _modelName;')); + expect(code, contains('String get modelName => _modelName;')); + expect(code, contains('late bool _isReady;')); + expect(code, contains('bool get isReady => _isReady;')); + expect(code, contains('set isReady(bool value)')); + expect(code, isNot(contains('set modelName'))); + }); + + test('generates async method correctly', () { + final method = { + 'name': 'process_input', + 'return_type': 'String', + 'arguments': [ + { + 'name': 'input', + 'type': 'String', + 'is_optional': false, + } + ], + 'docstring': 'Process the input.', + 'is_async': true, + }; + + final code = ClassGeneratorUtils.generateMethod(method); + expect(code, contains('Future<String> processInput(')); + expect(code, contains('String input')); + expect(code, contains('async {')); + expect(code, contains('/// Process the input.')); + }); + + test('skips generating constructor method', () { + final method = { + 'name': '__init__', + 'return_type': 'void', + 'arguments': [], + }; + + final code = ClassGeneratorUtils.generateMethod(method); + expect(code, isEmpty); + }); + }); +} diff --git a/helpers/tools/converter/test/utils/constructor_utils_test.dart b/helpers/tools/converter/test/utils/constructor_utils_test.dart new file mode 100644 index 0000000..3c9b97f --- /dev/null +++ b/helpers/tools/converter/test/utils/constructor_utils_test.dart @@ -0,0 +1,68 @@ +import 'package:test/test.dart'; +import '../../lib/src/utils/constructor_utils.dart'; + +void main() { + group('ConstructorUtils', () { + test('generates required parameters for non-default properties', () { + final properties = [ + { + 'name': 'model_name', + 'type': 'String', + 'has_default': false, + } + ]; + + final params = ConstructorUtils.generateParameters(properties); + expect(params, contains('required String model_name')); + }); + + test('generates optional parameters for default properties', () { + final properties = [ + { + 'name': 'is_ready', + 'type': 'bool', + 'has_default': true, + } + ]; + + final params = ConstructorUtils.generateParameters(properties); + expect(params, contains('bool? is_ready')); + }); + + test('generates property initializers', () { + final properties = [ + { + 'name': 'model_name', + 'type': 'String', + 'has_default': false, + }, + { + 'name': 'is_ready', + 'type': 'bool', + 'has_default': true, + } + ]; + + final inits = ConstructorUtils.generateInitializers(properties); + expect(inits, contains('_model_name = model_name;')); + expect(inits, contains('_is_ready = is_ready ?? false;')); + }); + + test('identifies constructor methods', () { + final initMethod = { + 'name': '__init__', + 'return_type': 'None', + 'arguments': [], + }; + + final regularMethod = { + 'name': 'process', + 'return_type': 'String', + 'arguments': [], + }; + + expect(ConstructorUtils.isConstructor(initMethod), isTrue); + expect(ConstructorUtils.isConstructor(regularMethod), isFalse); + }); + }); +} diff --git a/helpers/tools/converter/test/utils/interface_implementation_test.dart b/helpers/tools/converter/test/utils/interface_implementation_test.dart new file mode 100644 index 0000000..82dbe0f --- /dev/null +++ b/helpers/tools/converter/test/utils/interface_implementation_test.dart @@ -0,0 +1,84 @@ +import 'package:test/test.dart'; +import '../../lib/src/utils/class_generator_utils.dart'; + +void main() { + group('Interface Implementation Generation', () { + test('generates required BaseChain implementations', () { + final bases = ['BaseChain']; + final classContract = { + 'name': 'SimpleChain', + 'docstring': 'A simple implementation of a chain.', + 'methods': [ + { + 'name': 'run', + 'return_type': 'dict', + 'arguments': [ + { + 'name': 'inputs', + 'type': 'dict', + 'is_optional': false, + } + ], + 'docstring': 'Execute the chain logic.', + } + ], + }; + + final code = ClassGeneratorUtils.generateRequiredImplementations( + bases, classContract); + + // Should include memory property + expect(code, contains('late Map<String, dynamic>? _memory;')); + expect(code, contains('Map<String, dynamic>? get memory => _memory;')); + + // Should include verbose property + expect(code, contains('late bool _verbose;')); + expect(code, contains('bool get verbose => _verbose;')); + + // Should include constructor with required properties + expect(code, contains('SimpleChain({')); + expect(code, contains('Map<String, dynamic>? memory,')); + expect(code, contains('bool? verbose,')); + expect(code, contains('_memory = memory ?? {};')); + expect(code, contains('_verbose = verbose ?? false;')); + + // Should include required method implementations + expect(code, contains('@override')); + expect(code, contains('void setMemory(Map<String, dynamic> memory)')); + }); + + test('handles multiple interface implementations', () { + final bases = ['BaseChain', 'Serializable']; + final classContract = { + 'name': 'SimpleChain', + 'docstring': 'A simple implementation of a chain.', + 'methods': [], + }; + + final code = ClassGeneratorUtils.generateRequiredImplementations( + bases, classContract); + + // Should include BaseChain implementations + expect(code, contains('Map<String, dynamic>? get memory')); + expect(code, contains('bool get verbose')); + + // Should include constructor with all required properties + expect(code, contains('SimpleChain({')); + expect(code, contains('Map<String, dynamic>? memory,')); + expect(code, contains('bool? verbose,')); + }); + + test('handles no interface implementations', () { + final bases = <String>[]; + final classContract = { + 'name': 'SimpleClass', + 'docstring': 'A simple class.', + 'methods': [], + }; + + final code = ClassGeneratorUtils.generateRequiredImplementations( + bases, classContract); + expect(code, isEmpty); + }); + }); +} diff --git a/helpers/tools/converter/test/utils/name_utils_test.dart b/helpers/tools/converter/test/utils/name_utils_test.dart new file mode 100644 index 0000000..392b5de --- /dev/null +++ b/helpers/tools/converter/test/utils/name_utils_test.dart @@ -0,0 +1,37 @@ +import 'package:test/test.dart'; +import '../../lib/src/utils/name_utils.dart'; + +void main() { + group('NameUtils', () { + test('converts snake_case to camelCase', () { + expect(NameUtils.toDartName('hello_world'), equals('helloWorld')); + expect(NameUtils.toDartName('get_model_name'), equals('getModelName')); + expect(NameUtils.toDartName('set_memory'), equals('setMemory')); + }); + + test('handles single word correctly', () { + expect(NameUtils.toDartName('hello'), equals('hello')); + expect(NameUtils.toDartName('test'), equals('test')); + }); + + test('preserves existing camelCase', () { + expect(NameUtils.toDartName('helloWorld'), equals('helloWorld')); + expect(NameUtils.toDartName('getModelName'), equals('getModelName')); + }); + + test('handles empty string', () { + expect(NameUtils.toDartName(''), equals('')); + }); + + test('handles special method names', () { + expect(NameUtils.toDartName('__init__'), equals('new')); + expect(NameUtils.toDartName('__str__'), equals('str')); + expect(NameUtils.toDartName('__repr__'), equals('repr')); + }); + + test('handles consecutive underscores', () { + expect(NameUtils.toDartName('hello__world'), equals('helloWorld')); + expect(NameUtils.toDartName('test___name'), equals('testName')); + }); + }); +} diff --git a/helpers/tools/converter/test/utils/type_conversion_utils_test.dart b/helpers/tools/converter/test/utils/type_conversion_utils_test.dart new file mode 100644 index 0000000..aefbee9 --- /dev/null +++ b/helpers/tools/converter/test/utils/type_conversion_utils_test.dart @@ -0,0 +1,90 @@ +import 'package:test/test.dart'; +import '../../lib/src/utils/type_conversion_utils.dart'; + +void main() { + group('TypeConversionUtils', () { + group('pythonToDartType', () { + test('converts basic Python types to Dart types', () { + expect(TypeConversionUtils.pythonToDartType('str'), equals('String')); + expect(TypeConversionUtils.pythonToDartType('int'), equals('int')); + expect(TypeConversionUtils.pythonToDartType('bool'), equals('bool')); + expect(TypeConversionUtils.pythonToDartType('None'), equals('void')); + expect(TypeConversionUtils.pythonToDartType('dict'), + equals('Map<String, dynamic>')); + }); + + test('converts List types correctly', () { + expect(TypeConversionUtils.pythonToDartType('List[str]'), + equals('List<String>')); + expect(TypeConversionUtils.pythonToDartType('List[int]'), + equals('List<int>')); + expect(TypeConversionUtils.pythonToDartType('List[dict]'), + equals('List<Map<String, dynamic>>')); + }); + + test('converts Dict types correctly', () { + expect(TypeConversionUtils.pythonToDartType('Dict[str, Any]'), + equals('Map<String, dynamic>')); + expect(TypeConversionUtils.pythonToDartType('Dict[str, int]'), + equals('Map<String, int>')); + expect(TypeConversionUtils.pythonToDartType('dict'), + equals('Map<String, dynamic>')); + }); + + test('converts Optional types correctly', () { + expect(TypeConversionUtils.pythonToDartType('Optional[str]'), + equals('String?')); + expect(TypeConversionUtils.pythonToDartType('Optional[int]'), + equals('int?')); + expect(TypeConversionUtils.pythonToDartType('Optional[dict]'), + equals('Map<String, dynamic>?')); + expect(TypeConversionUtils.pythonToDartType('Optional[List[str]]'), + equals('List<String>?')); + }); + + test('handles nested generic types', () { + expect(TypeConversionUtils.pythonToDartType('List[Optional[str]]'), + equals('List<String?>')); + expect(TypeConversionUtils.pythonToDartType('Dict[str, List[int]]'), + equals('Map<String, List<int>>')); + expect( + TypeConversionUtils.pythonToDartType( + 'Optional[Dict[str, List[int]]]'), + equals('Map<String, List<int>>?')); + }); + }); + + group('getDefaultValue', () { + test('returns correct default values for basic types', () { + expect(TypeConversionUtils.getDefaultValue('bool'), equals('false')); + expect(TypeConversionUtils.getDefaultValue('int'), equals('0')); + expect(TypeConversionUtils.getDefaultValue('double'), equals('0.0')); + expect(TypeConversionUtils.getDefaultValue('String'), equals("''")); + }); + + test('returns correct default values for nullable types', () { + expect(TypeConversionUtils.getDefaultValue('bool?'), equals('false')); + expect(TypeConversionUtils.getDefaultValue('int?'), equals('0')); + expect(TypeConversionUtils.getDefaultValue('double?'), equals('0.0')); + expect(TypeConversionUtils.getDefaultValue('String?'), equals("''")); + }); + + test('returns correct default values for collection types', () { + expect(TypeConversionUtils.getDefaultValue('List'), equals('[]')); + expect( + TypeConversionUtils.getDefaultValue('List<String>'), equals('[]')); + expect(TypeConversionUtils.getDefaultValue('Map<String, dynamic>'), + equals('{}')); + expect(TypeConversionUtils.getDefaultValue('Map<String, int>'), + equals('{}')); + }); + + test('returns null for unknown types', () { + expect( + TypeConversionUtils.getDefaultValue('CustomType'), equals('null')); + expect(TypeConversionUtils.getDefaultValue('UnknownType?'), + equals('null')); + }); + }); + }); +} diff --git a/helpers/tools/converter/test/utils/type_utils_test.dart b/helpers/tools/converter/test/utils/type_utils_test.dart new file mode 100644 index 0000000..d4d18e0 --- /dev/null +++ b/helpers/tools/converter/test/utils/type_utils_test.dart @@ -0,0 +1,45 @@ +import 'package:test/test.dart'; +import '../../lib/src/utils/type_utils.dart'; + +void main() { + group('TypeUtils', () { + test('castToMapList handles null input', () { + expect(TypeUtils.castToMapList(null), isEmpty); + }); + + test('castToMapList converts List<dynamic> to List<Map<String, dynamic>>', + () { + final input = [ + {'name': 'test', 'value': 1}, + {'type': 'string', 'optional': true} + ]; + final result = TypeUtils.castToMapList(input); + expect(result, isA<List<Map<String, dynamic>>>()); + expect(result.first['name'], equals('test')); + expect(result.last['type'], equals('string')); + }); + + test('castToMap handles null input', () { + expect(TypeUtils.castToMap(null), isEmpty); + }); + + test('castToMap converts dynamic to Map<String, dynamic>', () { + final input = {'name': 'test', 'value': 1}; + final result = TypeUtils.castToMap(input); + expect(result, isA<Map<String, dynamic>>()); + expect(result['name'], equals('test')); + expect(result['value'], equals(1)); + }); + + test('castToStringList handles null input', () { + expect(TypeUtils.castToStringList(null), isEmpty); + }); + + test('castToStringList converts List<dynamic> to List<String>', () { + final input = ['test', 1, true]; + final result = TypeUtils.castToStringList(input); + expect(result, isA<List<String>>()); + expect(result, equals(['test', '1', 'true'])); + }); + }); +} diff --git a/helpers/tools/converter/test/yaml_handling_test.dart b/helpers/tools/converter/test/yaml_handling_test.dart new file mode 100644 index 0000000..6b0c23b --- /dev/null +++ b/helpers/tools/converter/test/yaml_handling_test.dart @@ -0,0 +1,90 @@ +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; +import '../lib/src/utils/yaml_utils.dart'; + +void main() { + group('YamlUtils', () { + test('converts simple YAML to Map correctly', () { + final yamlStr = ''' +interfaces: + - name: TestInterface + methods: + - name: testMethod + arguments: + - name: arg1 + type: str + return_type: str +'''; + + final yaml = loadYaml(yamlStr) as YamlMap; + final map = YamlUtils.convertYamlToMap(yaml); + + expect(map, isA<Map<String, dynamic>>()); + expect(map['interfaces'], isA<List>()); + expect(map['interfaces'][0]['name'], equals('TestInterface')); + expect(map['interfaces'][0]['methods'][0]['arguments'][0]['type'], + equals('str')); + }); + + test('handles nested YAML structures', () { + final yamlStr = ''' +interfaces: + - name: TestInterface + properties: + - name: prop1 + type: List[str] + has_default: true + methods: + - name: testMethod + arguments: + - name: arg1 + type: Dict[str, Any] + is_optional: true + return_type: Optional[int] +'''; + + final yaml = loadYaml(yamlStr) as YamlMap; + final map = YamlUtils.convertYamlToMap(yaml); + + expect( + map['interfaces'][0]['properties'][0]['type'], equals('List[str]')); + expect(map['interfaces'][0]['methods'][0]['arguments'][0]['type'], + equals('Dict[str, Any]')); + }); + + test('converts actual contract YAML correctly', () { + final yamlStr = ''' +interfaces: + - name: LLMProtocol + bases: + - Protocol + methods: + - name: generate + arguments: + - name: prompts + type: List[str] + is_optional: false + has_default: false + return_type: List[str] + docstring: Generate completions for the prompts. + decorators: + - name: abstractmethod + is_abstract: true + properties: [] + docstring: Protocol for language models. + is_interface: true +'''; + + final yaml = loadYaml(yamlStr) as YamlMap; + final map = YamlUtils.convertYamlToMap(yaml); + + expect(map['interfaces'][0]['name'], equals('LLMProtocol')); + expect(map['interfaces'][0]['bases'][0], equals('Protocol')); + expect(map['interfaces'][0]['methods'][0]['name'], equals('generate')); + expect(map['interfaces'][0]['methods'][0]['arguments'][0]['type'], + equals('List[str]')); + expect(map['interfaces'][0]['docstring'], + equals('Protocol for language models.')); + }); + }); +} diff --git a/helpers/tools/converter/test/yaml_type_test.dart b/helpers/tools/converter/test/yaml_type_test.dart new file mode 100644 index 0000000..714e7fe --- /dev/null +++ b/helpers/tools/converter/test/yaml_type_test.dart @@ -0,0 +1,57 @@ +import 'package:test/test.dart'; +import 'package:yaml/yaml.dart'; +import '../lib/src/utils/yaml_utils.dart'; +import '../lib/src/utils/type_utils.dart'; + +void main() { + group('YAML Type Casting', () { + test('converts YAML data to properly typed maps and lists', () { + final yamlStr = ''' +classes: + - name: TestClass + properties: + - name: model_name + type: String + has_default: false + - name: is_ready + type: bool + has_default: true + methods: + - name: process + return_type: String + arguments: + - name: input + type: String + is_optional: false +'''; + + final yamlDoc = loadYaml(yamlStr) as YamlMap; + final data = YamlUtils.convertYamlToMap(yamlDoc); + + final classes = data['classes'] as List; + final firstClass = classes.first as Map<String, dynamic>; + + // Test properties casting + final properties = + TypeUtils.castToMapList(firstClass['properties'] as List?); + expect(properties, isA<List<Map<String, dynamic>>>()); + expect(properties.first['name'], equals('model_name')); + expect(properties.first['type'], equals('String')); + expect(properties.first['has_default'], isFalse); + + // Test methods casting + final methods = TypeUtils.castToMapList(firstClass['methods'] as List?); + expect(methods, isA<List<Map<String, dynamic>>>()); + expect(methods.first['name'], equals('process')); + expect(methods.first['return_type'], equals('String')); + + // Test nested arguments casting + final arguments = + TypeUtils.castToMapList(methods.first['arguments'] as List?); + expect(arguments, isA<List<Map<String, dynamic>>>()); + expect(arguments.first['name'], equals('input')); + expect(arguments.first['type'], equals('String')); + expect(arguments.first['is_optional'], isFalse); + }); + }); +} diff --git a/helpers/tools/converter/tools/extract_contracts.dart b/helpers/tools/converter/tools/extract_contracts.dart new file mode 100644 index 0000000..628070c --- /dev/null +++ b/helpers/tools/converter/tools/extract_contracts.dart @@ -0,0 +1,255 @@ +import 'dart:io'; +import 'package:args/args.dart'; +import 'python_parser.dart'; + +/// Represents a Python class or interface contract +class ContractDefinition { + final String name; + final List<String> bases; + final List<MethodDefinition> methods; + final List<PropertyDefinition> properties; + final String? docstring; + final List<Map<String, dynamic>> decorators; + final bool isInterface; + + ContractDefinition({ + required this.name, + required this.bases, + required this.methods, + required this.properties, + this.docstring, + required this.decorators, + required this.isInterface, + }); + + Map<String, dynamic> toJson() => { + 'name': name, + 'bases': bases, + 'methods': methods.map((m) => m.toJson()).toList(), + 'properties': properties.map((p) => p.toJson()).toList(), + if (docstring != null) 'docstring': docstring, + 'decorators': decorators, + 'is_interface': isInterface, + }; + + /// Create ContractDefinition from PythonClass + factory ContractDefinition.fromPythonClass(PythonClass pythonClass) { + return ContractDefinition( + name: pythonClass.name, + bases: pythonClass.bases, + methods: pythonClass.methods + .map((m) => MethodDefinition( + name: m.name, + arguments: m.parameters + .map((p) => ArgumentDefinition( + name: p.name, + type: p.type, + isOptional: p.isOptional, + hasDefault: p.hasDefault, + )) + .toList(), + returnType: m.returnType, + docstring: m.docstring, + decorators: m.decorators.map((d) => {'name': d}).toList(), + isAbstract: m.isAbstract, + )) + .toList(), + properties: pythonClass.properties + .map((p) => PropertyDefinition( + name: p.name, + type: p.type, + hasDefault: p.hasDefault, + )) + .toList(), + docstring: pythonClass.docstring, + decorators: pythonClass.decorators.map((d) => {'name': d}).toList(), + isInterface: pythonClass.isInterface, + ); + } +} + +/// Represents a method in a contract +class MethodDefinition { + final String name; + final List<ArgumentDefinition> arguments; + final String returnType; + final String? docstring; + final List<Map<String, dynamic>> decorators; + final bool isAbstract; + + MethodDefinition({ + required this.name, + required this.arguments, + required this.returnType, + this.docstring, + required this.decorators, + required this.isAbstract, + }); + + Map<String, dynamic> toJson() => { + 'name': name, + 'arguments': arguments.map((a) => a.toJson()).toList(), + 'return_type': returnType, + if (docstring != null) 'docstring': docstring, + 'decorators': decorators, + 'is_abstract': isAbstract, + }; +} + +/// Represents a method argument +class ArgumentDefinition { + final String name; + final String type; + final bool isOptional; + final bool hasDefault; + + ArgumentDefinition({ + required this.name, + required this.type, + required this.isOptional, + required this.hasDefault, + }); + + Map<String, dynamic> toJson() => { + 'name': name, + 'type': type, + 'is_optional': isOptional, + 'has_default': hasDefault, + }; +} + +/// Represents a class property +class PropertyDefinition { + final String name; + final String type; + final bool hasDefault; + + PropertyDefinition({ + required this.name, + required this.type, + required this.hasDefault, + }); + + Map<String, dynamic> toJson() => { + 'name': name, + 'type': type, + 'has_default': hasDefault, + }; +} + +/// Main contract extractor class +class ContractExtractor { + final List<ContractDefinition> interfaces = []; + final List<ContractDefinition> classes = []; + + /// Process a Python source file and extract contracts + Future<void> processFile(File file) async { + try { + final pythonClasses = await PythonParser.parseFile(file); + + for (final pythonClass in pythonClasses) { + final contract = ContractDefinition.fromPythonClass(pythonClass); + if (pythonClass.isInterface) { + interfaces.add(contract); + } else { + classes.add(contract); + } + } + } catch (e) { + print('Error processing file ${file.path}: $e'); + } + } + + /// Process all Python files in a directory recursively + Future<void> processDirectory(String dirPath) async { + final dir = Directory(dirPath); + await for (final entity in dir.list(recursive: true)) { + if (entity is File && entity.path.endsWith('.py')) { + await processFile(entity); + } + } + } + + /// Generate YAML output + Future<void> generateYaml(String outputPath) async { + final output = { + 'interfaces': interfaces.map((i) => i.toJson()).toList(), + 'classes': classes.map((c) => c.toJson()).toList(), + }; + + final yamlString = mapToYaml(output); + final file = File(outputPath); + await file.writeAsString(yamlString); + } +} + +/// Converts a Map to YAML string with proper formatting +String mapToYaml(Map<String, dynamic> map, {int indent = 0}) { + final buffer = StringBuffer(); + final indentStr = ' ' * indent; + + map.forEach((key, value) { + if (value is Map) { + buffer.writeln('$indentStr$key:'); + buffer + .write(mapToYaml(value as Map<String, dynamic>, indent: indent + 2)); + } else if (value is List) { + buffer.writeln('$indentStr$key:'); + for (var item in value) { + if (item is Map) { + buffer.writeln('$indentStr- '); + buffer.write( + mapToYaml(item as Map<String, dynamic>, indent: indent + 4)); + } else { + buffer.writeln('$indentStr- $item'); + } + } + } else { + if (value == null) { + buffer.writeln('$indentStr$key: null'); + } else if (value is String) { + // Handle multi-line strings + if (value.contains('\n')) { + buffer.writeln('$indentStr$key: |'); + value.split('\n').forEach((line) { + buffer.writeln('$indentStr $line'); + }); + } else { + buffer.writeln('$indentStr$key: "$value"'); + } + } else { + buffer.writeln('$indentStr$key: $value'); + } + } + }); + + return buffer.toString(); +} + +void main(List<String> arguments) async { + final parser = ArgParser() + ..addOption('source', + abbr: 's', + help: 'Source directory containing Python LangChain implementation', + mandatory: true) + ..addOption('output', + abbr: 'o', help: 'Output YAML file path', mandatory: true); + + try { + final results = parser.parse(arguments); + final sourceDir = results['source'] as String; + final outputFile = results['output'] as String; + + final extractor = ContractExtractor(); + await extractor.processDirectory(sourceDir); + await extractor.generateYaml(outputFile); + + print('Contract extraction completed successfully.'); + print('Interfaces found: ${extractor.interfaces.length}'); + print('Classes found: ${extractor.classes.length}'); + } catch (e) { + print('Error: $e'); + print('Usage: dart extract_contracts.dart --source <dir> --output <file>'); + exit(1); + } +} diff --git a/helpers/tools/converter/tools/generate_dart_code.dart b/helpers/tools/converter/tools/generate_dart_code.dart new file mode 100644 index 0000000..0a43f55 --- /dev/null +++ b/helpers/tools/converter/tools/generate_dart_code.dart @@ -0,0 +1,209 @@ +import 'dart:io'; +import 'package:yaml/yaml.dart'; +import 'package:path/path.dart' as path; +import 'package:args/args.dart'; +import 'package:converter/src/utils/yaml_utils.dart'; +import 'package:converter/src/utils/name_utils.dart'; +import 'package:converter/src/utils/class_generator_utils.dart'; +import 'package:converter/src/utils/type_utils.dart'; +import 'package:converter/src/utils/type_conversion_utils.dart'; + +/// Generates a Dart interface from a contract +String generateInterface(Map<String, dynamic> interface) { + final buffer = StringBuffer(); + final name = interface['name'] as String; + final docstring = interface['docstring'] as String?; + + // Add documentation + if (docstring != null) { + buffer.writeln('/// ${docstring.replaceAll('\n', '\n/// ')}'); + } + + // Begin interface definition + buffer.writeln('abstract class $name {'); + + // Generate properties + final properties = interface['properties'] as List?; + if (properties != null) { + for (final prop in properties) { + final propName = NameUtils.toDartName(prop['name'] as String); + final propType = + TypeConversionUtils.pythonToDartType(prop['type'] as String); + buffer.writeln(' $propType get $propName;'); + // Only generate setter if is_readonly is explicitly false + final isReadonly = prop['is_readonly']; + if (isReadonly != null && !isReadonly) { + buffer.writeln(' set $propName($propType value);'); + } + } + if (properties.isNotEmpty) buffer.writeln(); + } + + // Generate methods + final methods = interface['methods'] as List?; + if (methods != null) { + for (final method in methods) { + final methodName = NameUtils.toDartName(method['name'] as String); + final returnType = + TypeConversionUtils.pythonToDartType(method['return_type'] as String); + final methodDoc = method['docstring'] as String?; + final decorators = TypeUtils.castToMapList(method['decorators'] as List?); + final isProperty = decorators.any((d) => d['name'] == 'property'); + + if (methodDoc != null) { + buffer.writeln(' /// ${methodDoc.replaceAll('\n', '\n /// ')}'); + } + + if (isProperty) { + // Generate as a getter + buffer.writeln(' $returnType get $methodName;'); + } else { + // Generate as a method + final isAsync = method['is_async'] == true; + if (isAsync) { + buffer.write(' Future<$returnType> $methodName('); + } else { + buffer.write(' $returnType $methodName('); + } + + // Generate parameters + final params = method['arguments'] as List?; + if (params != null && params.isNotEmpty) { + final paramStrings = <String>[]; + + for (final param in params) { + final paramName = NameUtils.toDartName(param['name'] as String); + final paramType = + TypeConversionUtils.pythonToDartType(param['type'] as String); + final isOptional = param['is_optional'] == true; + + if (isOptional) { + paramStrings.add('[$paramType $paramName]'); + } else { + paramStrings.add('$paramType $paramName'); + } + } + + buffer.write(paramStrings.join(', ')); + } + + buffer.writeln(');'); + } + } + } + + buffer.writeln('}'); + return buffer.toString(); +} + +/// Generates a Dart class implementation from a contract +String generateClass(Map<String, dynamic> classContract) { + final buffer = StringBuffer(); + final name = classContract['name'] as String; + final bases = TypeUtils.castToStringList(classContract['bases'] as List?); + final docstring = classContract['docstring'] as String?; + + // Add documentation + if (docstring != null) { + buffer.writeln('/// ${docstring.replaceAll('\n', '\n/// ')}'); + } + + // Begin class definition + buffer.write('class $name'); + if (bases.isNotEmpty) { + final implementsStr = bases.join(', '); + buffer.write(' implements $implementsStr'); + } + buffer.writeln(' {'); + + // Generate required interface implementations first + if (bases.contains('BaseChain')) { + buffer.write(ClassGeneratorUtils.generateRequiredImplementations( + bases, classContract)); + } + + // Generate properties from contract properties + final properties = + TypeUtils.castToMapList(classContract['properties'] as List?); + if (properties.isNotEmpty) { + buffer.write(ClassGeneratorUtils.generateProperties(properties)); + } + + // Generate constructor + if (properties.isNotEmpty || bases.contains('BaseChain')) { + buffer.write(ClassGeneratorUtils.generateConstructor(name, properties)); + } + + // Generate additional methods + final methods = TypeUtils.castToMapList(classContract['methods'] as List?); + if (methods.isNotEmpty) { + for (final method in methods) { + if (method['name'] != '__init__') { + buffer.write(ClassGeneratorUtils.generateMethod(method)); + } + } + } + + buffer.writeln('}'); + return buffer.toString(); +} + +/// Main code generator class +class DartCodeGenerator { + final String outputDir; + + DartCodeGenerator(this.outputDir); + + Future<void> generateFromYaml(String yamlPath) async { + final file = File(yamlPath); + final content = await file.readAsString(); + final yamlDoc = loadYaml(content) as YamlMap; + final contracts = YamlUtils.convertYamlToMap(yamlDoc); + + // Generate interfaces + for (final interface in contracts['interfaces'] ?? []) { + final code = generateInterface(interface as Map<String, dynamic>); + final fileName = '${interface['name'].toString().toLowerCase()}.dart'; + final outputFile = + File(path.join(outputDir, 'lib', 'src', 'interfaces', fileName)); + await outputFile.create(recursive: true); + await outputFile.writeAsString(code); + } + + // Generate classes + for (final classContract in contracts['classes'] ?? []) { + final code = generateClass(classContract as Map<String, dynamic>); + final fileName = '${classContract['name'].toString().toLowerCase()}.dart'; + final outputFile = + File(path.join(outputDir, 'lib', 'src', 'implementations', fileName)); + await outputFile.create(recursive: true); + await outputFile.writeAsString(code); + } + } +} + +void main(List<String> arguments) async { + final parser = ArgParser() + ..addOption('contracts', + abbr: 'c', help: 'Path to the YAML contracts file', mandatory: true) + ..addOption('output', + abbr: 'o', + help: 'Output directory for generated Dart code', + mandatory: true); + + try { + final results = parser.parse(arguments); + final contractsFile = results['contracts'] as String; + final outputDir = results['output'] as String; + + final generator = DartCodeGenerator(outputDir); + await generator.generateFromYaml(contractsFile); + + print('Code generation completed successfully.'); + } catch (e) { + print('Error: $e'); + print( + 'Usage: dart generate_dart_code.dart --contracts <file> --output <dir>'); + exit(1); + } +} diff --git a/helpers/tools/converter/tools/python_parser.dart b/helpers/tools/converter/tools/python_parser.dart new file mode 100644 index 0000000..aec9c09 --- /dev/null +++ b/helpers/tools/converter/tools/python_parser.dart @@ -0,0 +1,443 @@ +import 'dart:io'; + +/// Represents a Python method parameter +class Parameter { + final String name; + final String type; + final bool isOptional; + final bool hasDefault; + + Parameter({ + required this.name, + required this.type, + required this.isOptional, + required this.hasDefault, + }); + + Map<String, dynamic> toJson() => { + 'name': name, + 'type': type, + 'is_optional': isOptional, + 'has_default': hasDefault, + }; +} + +/// Represents a Python method +class Method { + final String name; + final List<Parameter> parameters; + final String returnType; + final String? docstring; + final List<String> decorators; + final bool isAsync; + final bool isAbstract; + final bool isProperty; + + Method({ + required this.name, + required this.parameters, + required this.returnType, + this.docstring, + required this.decorators, + required this.isAsync, + required this.isAbstract, + required this.isProperty, + }); + + Map<String, dynamic> toJson() => { + 'name': name, + 'parameters': parameters.map((p) => p.toJson()).toList(), + 'return_type': returnType, + if (docstring != null) 'docstring': docstring, + 'decorators': decorators, + 'is_async': isAsync, + 'is_abstract': isAbstract, + 'is_property': isProperty, + }; +} + +/// Represents a Python class property +class Property { + final String name; + final String type; + final bool hasDefault; + final String? defaultValue; + + Property({ + required this.name, + required this.type, + required this.hasDefault, + this.defaultValue, + }); + + Map<String, dynamic> toJson() => { + 'name': name, + 'type': type, + 'has_default': hasDefault, + if (defaultValue != null) 'default_value': defaultValue, + }; +} + +/// Represents a Python class +class PythonClass { + final String name; + final List<String> bases; + final List<Method> methods; + final List<Property> properties; + final String? docstring; + final List<String> decorators; + final bool isInterface; + + PythonClass({ + required this.name, + required this.bases, + required this.methods, + required this.properties, + this.docstring, + required this.decorators, + required this.isInterface, + }); + + Map<String, dynamic> toJson() => { + 'name': name, + 'bases': bases, + 'methods': methods.map((m) => m.toJson()).toList(), + 'properties': properties.map((p) => p.toJson()).toList(), + if (docstring != null) 'docstring': docstring, + 'decorators': decorators, + 'is_interface': isInterface, + }; +} + +/// Parser for Python source code +class PythonParser { + /// Check if a line looks like code + static bool _isCodeLine(String line) { + return line.startsWith('def ') || + line.startsWith('@') || + line.startsWith('class ') || + line.contains(' = ') || + line.contains('self.') || + line.contains('return ') || + line.contains('pass') || + line.contains('raise ') || + line.contains('yield ') || + line.contains('async ') || + line.contains('await ') || + (line.contains(':') && !line.startsWith('Note:')) || + line.trim().startsWith('"""') || + line.trim().endsWith('"""'); + } + + /// Parse a docstring from Python code lines + static String? _parseDocstring( + List<String> lines, int startIndex, int baseIndent) { + if (startIndex >= lines.length) return null; + + final line = lines[startIndex].trim(); + if (!line.startsWith('"""')) return null; + + // Handle single-line docstring + if (line.endsWith('"""') && line.length > 6) { + return line.substring(3, line.length - 3).trim(); + } + + final docLines = <String>[]; + // Add first line content if it exists after the opening quotes + var firstLineContent = line.substring(3).trim(); + if (firstLineContent.isNotEmpty && !_isCodeLine(firstLineContent)) { + docLines.add(firstLineContent); + } + + var i = startIndex + 1; + while (i < lines.length) { + final currentLine = lines[i].trim(); + + // Stop at closing quotes + if (currentLine.endsWith('"""')) { + // Add the last line content if it exists before the closing quotes + var lastLineContent = + currentLine.substring(0, currentLine.length - 3).trim(); + if (lastLineContent.isNotEmpty && !_isCodeLine(lastLineContent)) { + docLines.add(lastLineContent); + } + break; + } + + // Only add non-code lines + if (currentLine.isNotEmpty && !_isCodeLine(currentLine)) { + docLines.add(currentLine); + } + + i++; + } + + return docLines.isEmpty ? null : docLines.join('\n').trim(); + } + + /// Get the indentation level of a line + static int _getIndentation(String line) { + return line.length - line.trimLeft().length; + } + + /// Parse method parameters from a parameter string + static List<Parameter> _parseParameters(String paramsStr) { + if (paramsStr.trim().isEmpty) return []; + + final params = <Parameter>[]; + var depth = 0; + var currentParam = StringBuffer(); + + // Handle nested brackets in parameter types + for (var i = 0; i < paramsStr.length; i++) { + final char = paramsStr[i]; + if (char == '[') depth++; + if (char == ']') depth--; + if (char == ',' && depth == 0) { + final param = currentParam.toString().trim(); + if (param.isNotEmpty && param != 'self' && !param.startsWith('**')) { + final paramObj = _parseParameter(param); + if (paramObj != null) { + params.add(paramObj); + } + } + currentParam.clear(); + } else { + currentParam.write(char); + } + } + + final lastParam = currentParam.toString().trim(); + if (lastParam.isNotEmpty && + lastParam != 'self' && + !lastParam.startsWith('**')) { + final paramObj = _parseParameter(lastParam); + if (paramObj != null) { + params.add(paramObj); + } + } + + return params; + } + + /// Parse a single parameter + static Parameter? _parseParameter(String param) { + if (param.isEmpty) return null; + + var name = param; + var type = 'Any'; + var hasDefault = false; + var isOptional = false; + + // Check for type annotation + if (param.contains(':')) { + final parts = param.split(':'); + name = parts[0].trim(); + var typeStr = parts[1]; + + // Handle default value + if (typeStr.contains('=')) { + final typeParts = typeStr.split('='); + typeStr = typeParts[0]; + hasDefault = true; + isOptional = true; + } + + type = typeStr.trim(); + + // Handle Optional type + if (type.startsWith('Optional[')) { + type = type.substring(9, type.length - 1); + isOptional = true; + } + } + + // Check for default value without type annotation + if (param.contains('=')) { + hasDefault = true; + isOptional = true; + if (!param.contains(':')) { + name = param.split('=')[0].trim(); + } + } + + return Parameter( + name: name, + type: type, + isOptional: isOptional, + hasDefault: hasDefault, + ); + } + + /// Parse a method definition + static Method? _parseMethod( + List<String> lines, int startIndex, List<String> decorators) { + final line = lines[startIndex].trim(); + if (!line.startsWith('def ') && !line.startsWith('async def ')) return null; + + final methodMatch = + RegExp(r'(?:async\s+)?def\s+(\w+)\s*\((.*?)\)(?:\s*->\s*(.+?))?\s*:') + .firstMatch(line); + if (methodMatch == null) return null; + + final name = methodMatch.group(1)!; + final paramsStr = methodMatch.group(2) ?? ''; + var returnType = methodMatch.group(3) ?? 'None'; + returnType = returnType.trim(); + + final isAsync = line.contains('async '); + final isAbstract = decorators.contains('abstractmethod'); + final isProperty = decorators.contains('property'); + + // Parse docstring if present + var i = startIndex + 1; + String? docstring; + if (i < lines.length) { + final nextLine = lines[i].trim(); + if (nextLine.startsWith('"""')) { + docstring = + _parseDocstring(lines, i, _getIndentation(lines[startIndex])); + } + } + + return Method( + name: name, + parameters: _parseParameters(paramsStr), + returnType: returnType, + docstring: docstring, + decorators: decorators, + isAsync: isAsync, + isAbstract: isAbstract, + isProperty: isProperty, + ); + } + + /// Parse a property definition + static Property? _parseProperty(String line) { + if (!line.contains(':') || line.contains('def ')) return null; + + final propertyMatch = + RegExp(r'(\w+)\s*:\s*(.+?)(?:\s*=\s*(.+))?$').firstMatch(line); + if (propertyMatch == null) return null; + + final name = propertyMatch.group(1)!; + final type = propertyMatch.group(2)!; + final defaultValue = propertyMatch.group(3); + + return Property( + name: name, + type: type.trim(), + hasDefault: defaultValue != null, + defaultValue: defaultValue?.trim(), + ); + } + + /// Parse Python source code into a list of classes + static Future<List<PythonClass>> parseFile(File file) async { + final content = await file.readAsString(); + final lines = content.split('\n'); + final classes = <PythonClass>[]; + + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; + final trimmedLine = line.trim(); + + if (trimmedLine.startsWith('class ')) { + final classMatch = + RegExp(r'class\s+(\w+)(?:\((.*?)\))?:').firstMatch(trimmedLine); + if (classMatch != null) { + final className = classMatch.group(1)!; + final basesStr = classMatch.group(2); + final bases = + basesStr?.split(',').map((b) => b.trim()).toList() ?? []; + final isInterface = bases.any((b) => b.contains('Protocol')); + + final classIndent = _getIndentation(line); + var currentDecorators = <String>[]; + final methods = <Method>[]; + final properties = <Property>[]; + + // Parse class docstring + var j = i + 1; + String? docstring; + if (j < lines.length && lines[j].trim().startsWith('"""')) { + docstring = _parseDocstring(lines, j, classIndent); + // Skip past docstring + while (j < lines.length && !lines[j].trim().endsWith('"""')) { + j++; + } + j++; + } + + // Parse class body + while (j < lines.length) { + final currentLine = lines[j]; + final currentIndent = _getIndentation(currentLine); + final trimmedCurrentLine = currentLine.trim(); + + // Check if we're still in the class + if (trimmedCurrentLine.isNotEmpty && currentIndent <= classIndent) { + break; + } + + // Skip empty lines + if (trimmedCurrentLine.isEmpty) { + j++; + continue; + } + + // Parse decorators + if (trimmedCurrentLine.startsWith('@')) { + currentDecorators + .add(trimmedCurrentLine.substring(1).split('(')[0].trim()); + j++; + continue; + } + + // Parse methods + if (trimmedCurrentLine.startsWith('def ') || + trimmedCurrentLine.startsWith('async def ')) { + final method = + _parseMethod(lines, j, List.from(currentDecorators)); + if (method != null) { + methods.add(method); + currentDecorators = []; + // Skip past method body + while (j < lines.length - 1) { + final nextLine = lines[j + 1]; + if (nextLine.trim().isEmpty || + _getIndentation(nextLine) <= currentIndent) { + break; + } + j++; + } + } + } + + // Parse properties + final property = _parseProperty(trimmedCurrentLine); + if (property != null) { + properties.add(property); + } + + j++; + } + + i = j - 1; + + classes.add(PythonClass( + name: className, + bases: bases, + methods: methods, + properties: properties, + docstring: docstring, + decorators: [], + isInterface: isInterface, + )); + } + } + } + + return classes; + } +} diff --git a/helpers/tools/converter/tools/setup_langchain.sh b/helpers/tools/converter/tools/setup_langchain.sh new file mode 100644 index 0000000..507bcdb --- /dev/null +++ b/helpers/tools/converter/tools/setup_langchain.sh @@ -0,0 +1,152 @@ +#!/bin/bash + +# Exit on error +set -e + +# Script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PACKAGE_DIR="$(dirname "$SCRIPT_DIR")" +TEMP_DIR="$PACKAGE_DIR/temp" +CONTRACTS_FILE="$TEMP_DIR/contracts.yaml" + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Print step information +print_step() { + echo -e "${BLUE}=== $1 ===${NC}" +} + +# Ensure required commands are available +check_requirements() { + print_step "Checking requirements" + + commands=("git" "dart" "pub") + for cmd in "${commands[@]}"; do + if ! command -v $cmd &> /dev/null; then + echo "Error: $cmd is required but not installed." + exit 1 + fi + done +} + +# Create necessary directories +setup_directories() { + print_step "Setting up directories" + + mkdir -p "$TEMP_DIR" + mkdir -p "$PACKAGE_DIR/lib/src/interfaces" + mkdir -p "$PACKAGE_DIR/lib/src/implementations" +} + +# Clone Python LangChain repository +clone_langchain() { + print_step "Cloning Python LangChain repository" + + if [ -d "$TEMP_DIR/langchain" ]; then + echo "Updating existing LangChain repository..." + cd "$TEMP_DIR/langchain" + git pull + else + echo "Cloning LangChain repository..." + git clone https://github.com/langchain-ai/langchain.git "$TEMP_DIR/langchain" + fi +} + +# Extract contracts from Python code +extract_contracts() { + print_step "Extracting contracts from Python code" + + cd "$PACKAGE_DIR" + dart run "$SCRIPT_DIR/extract_contracts.dart" \ + --source "$TEMP_DIR/langchain/langchain" \ + --output "$CONTRACTS_FILE" +} + +# Generate Dart code from contracts +generate_dart_code() { + print_step "Generating Dart code from contracts" + + cd "$PACKAGE_DIR" + dart run "$SCRIPT_DIR/generate_dart_code.dart" \ + --contracts "$CONTRACTS_FILE" \ + --output "$PACKAGE_DIR" +} + +# Update package dependencies +update_dependencies() { + print_step "Updating package dependencies" + + cd "$PACKAGE_DIR" + + # Ensure required dependencies are in pubspec.yaml + if ! grep -q "yaml:" pubspec.yaml; then + echo " +dependencies: + yaml: ^3.1.0 + path: ^1.8.0 + args: ^2.3.0" >> pubspec.yaml + fi + + dart pub get +} + +# Create package exports file +create_exports() { + print_step "Creating package exports" + + cat > "$PACKAGE_DIR/lib/langchain.dart" << EOL +/// LangChain for Dart +/// +/// This is a Dart implementation of LangChain, providing tools and utilities +/// for building applications powered by large language models (LLMs). +library langchain; + +// Export interfaces +export 'src/interfaces/llm.dart'; +export 'src/interfaces/chain.dart'; +export 'src/interfaces/prompt.dart'; +export 'src/interfaces/memory.dart'; +export 'src/interfaces/embeddings.dart'; +export 'src/interfaces/document.dart'; +export 'src/interfaces/vectorstore.dart'; +export 'src/interfaces/tool.dart'; +export 'src/interfaces/agent.dart'; + +// Export implementations +export 'src/implementations/llm.dart'; +export 'src/implementations/chain.dart'; +export 'src/implementations/prompt.dart'; +export 'src/implementations/memory.dart'; +export 'src/implementations/embeddings.dart'; +export 'src/implementations/document.dart'; +export 'src/implementations/vectorstore.dart'; +export 'src/implementations/tool.dart'; +export 'src/implementations/agent.dart'; +EOL +} + +# Main execution +main() { + print_step "Starting LangChain setup" + + check_requirements + setup_directories + clone_langchain + extract_contracts + generate_dart_code + update_dependencies + create_exports + + echo -e "${GREEN}Setup completed successfully!${NC}" + echo "Next steps:" + echo "1. Review generated code in lib/src/" + echo "2. Implement TODOs in the generated classes" + echo "3. Add tests for the implementations" + echo "4. Update the documentation" +} + +# Run main function +main