import 'package:analyzer/dart/ast/ast.dart'; import 'package:sally_generator/src/errors.dart'; import 'package:sally_generator/src/model/specified_column.dart'; import 'package:sally_generator/src/parser/parser.dart'; import 'package:sally_generator/src/sally_generator.dart'; import 'package:recase/recase.dart'; const String startInt = 'integer'; const String startString = 'text'; const String startBool = 'boolean'; // todo replace with set literal once dart supports it final Set starters = [startInt, startString, startBool].toSet(); const String functionNamed = 'named'; const String functionPrimaryKey = 'primaryKey'; const String functionReferences = 'references'; const String functionAutoIncrement = 'autoIncrement'; const String functionWithLength = 'withLength'; const String functionNullable = 'nullable'; const String errorMessage = 'This getter does not create a valid column that ' 'can be parsed by sally. Please refer to the readme from sally to see how ' 'columns are formed. If you have any questions, feel free to raise an issue.'; class ColumnParser extends ParserBase { ColumnParser(SallyGenerator generator) : super(generator); SpecifiedColumn parse(MethodDeclaration getter) { /* These getters look like this: ... get id => integer().autoIncrement()(); The last () is a FunctionExpressionInvocation, the entries before that (here autoIncrement and integer) are MethodInvocations. We go through each of the method invocations until we hit one that starts the chain (integer, text, boolean, etc.). From each method in the chain, we can extract what it means for the column (name, auto increment, PK, constraints...). */ final expr = returnExpressionOfMethod(getter); if (!(expr is FunctionExpressionInvocation)) { generator.errors.add(SallyError( affectedElement: getter.declaredElement, message: errorMessage, critical: true, )); return null; } var remainingExpr = (expr as FunctionExpressionInvocation).function as MethodInvocation; String foundStartMethod; String foundExplicitName; var wasDeclaredAsPrimaryKey = false; var nullable = false; // todo parse reference final foundFeatures = []; while (true) { final methodName = remainingExpr.methodName.name; if (starters.contains(methodName)) { foundStartMethod = methodName; break; } switch (methodName) { case functionNamed: if (foundExplicitName != null) { generator.errors.add(SallyError( critical: false, affectedElement: getter.declaredElement, message: "You're setting more than one name here, the first will " 'be used')); } foundExplicitName = readStringLiteral(remainingExpr.argumentList.arguments.first, () { generator.errors.add(SallyError( critical: false, affectedElement: getter.declaredElement, message: 'This table name is cannot be resolved! Please only use ' 'a constant string as parameter for .named().')); }); break; case functionPrimaryKey: wasDeclaredAsPrimaryKey = true; break; case functionReferences: break; case functionWithLength: final args = remainingExpr.argumentList; final minArg = findNamedArgument(args, 'min'); final maxArg = findNamedArgument(args, 'max'); foundFeatures.add(LimitingTextLength.withLength( min: readIntLiteral(minArg, () {}), max: readIntLiteral(maxArg, () {}), )); break; case functionAutoIncrement: wasDeclaredAsPrimaryKey = true; foundFeatures.add(AutoIncrement()); break; case functionNullable: nullable = true; } // We're not at a starting method yet, so we need to go deeper! final inner = (remainingExpr.target) as MethodInvocation; remainingExpr = inner; } ColumnName name; if (foundExplicitName != null) { name = ColumnName.explicitly(foundExplicitName); } else { name = ColumnName.implicitly(ReCase(getter.name.name).snakeCase); } return SpecifiedColumn( type: _startMethodToColumnType(foundStartMethod), dartGetterName: getter.name.name, name: name.escapeIfSqlKeyword(), declaredAsPrimaryKey: wasDeclaredAsPrimaryKey, nullable: nullable, features: foundFeatures); } ColumnType _startMethodToColumnType(String startMethod) { return const { startBool: ColumnType.boolean, startString: ColumnType.text, startInt: ColumnType.integer, }[startMethod]; } }