add: adding extract/converter tool (wip)

This commit is contained in:
Patrick Stewart 2024-11-11 12:40:51 -07:00
parent a4beca5a4c
commit df19fbea36
33 changed files with 3927 additions and 0 deletions

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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,
};
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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__';
}
}

View file

@ -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('');
}
}

View file

@ -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';
}
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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"

View file

@ -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

View file

@ -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.'));
});
});
}

View file

@ -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(')));
});
});
}

View file

@ -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'));
});
});
}

View file

@ -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;'));
});
});
}

View file

@ -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')));
});
});
}

View file

@ -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

View file

@ -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]}

View file

@ -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'));
});
});
}

View file

@ -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);
});
});
}

View file

@ -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);
});
});
}

View file

@ -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);
});
});
}

View file

@ -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'));
});
});
}

View file

@ -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'));
});
});
});
}

View file

@ -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']));
});
});
}

View file

@ -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.'));
});
});
}

View file

@ -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);
});
});
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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