diff --git a/lib/src/results/matching_strategy_enum.dart b/lib/src/results/matching_strategy_enum.dart index 44cf6532..ada1874f 100644 --- a/lib/src/results/matching_strategy_enum.dart +++ b/lib/src/results/matching_strategy_enum.dart @@ -10,8 +10,6 @@ extension MatchingStrategyExtension on MatchingStrategy { return 'all'; case MatchingStrategy.last: return 'last'; - default: - return 'last'; } } } diff --git a/pubspec.yaml b/pubspec.yaml index 26dde90f..7025ed1e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,6 +14,10 @@ dependencies: collection: ^1.17.0 json_annotation: ^4.8.1 meta: ^1.9.1 + platform: ^3.1.0 + colorize: ^3.0.0 + http: ^1.1.0 + yaml_edit: ^2.1.1 dev_dependencies: test: ^1.0.0 @@ -21,6 +25,8 @@ dev_dependencies: lints: ">=2.1.0 <4.0.0" json_serializable: ^6.7.1 build_runner: ^2.4.6 + args: ^2.4.2 + path: ^1.8.3 screenshots: - description: The Meilisearch logo. diff --git a/test/utils/wait_for.dart b/test/utils/wait_for.dart index 4c732837..a87cd251 100644 --- a/test/utils/wait_for.dart +++ b/test/utils/wait_for.dart @@ -40,8 +40,13 @@ extension TaskWaiterForLists on Iterable { bool throwFailed = true, }) async { final endingTime = DateTime.now().add(timeout); - final originalUids = toList(); - final remainingUids = map((e) => e.uid).whereNotNull().toList(); + final originalUids = List.from(this); + final remainingUids = []; + for (final task in this) { + if (task.uid != null) { + remainingUids.add(task.uid!); + } + } final completedTasks = {}; final statuses = ['enqueued', 'processing']; @@ -49,10 +54,10 @@ extension TaskWaiterForLists on Iterable { final taskRes = await client.getTasks(params: TasksQuery(uids: remainingUids)); final tasks = taskRes.results; - final completed = tasks.where((e) => !statuses.contains(e.status)); + final completed = tasks.where((Task e) => !statuses.contains(e.status)); if (throwFailed) { final failed = completed - .firstWhereOrNull((element) => element.status != 'succeeded'); + .firstWhereOrNull((Task element) => element.status != 'succeeded'); if (failed != null) { throw MeiliSearchApiException( "Task (${failed.uid}) failed", @@ -63,14 +68,14 @@ extension TaskWaiterForLists on Iterable { } } - completedTasks.addEntries(completed.map((e) => MapEntry(e.uid!, e))); + completedTasks.addEntries(completed.map((Task e) => MapEntry(e.uid!, e))); remainingUids - .removeWhere((element) => completedTasks.containsKey(element)); + .removeWhere((int element) => completedTasks.containsKey(element)); if (remainingUids.isEmpty) { return originalUids - .map((e) => completedTasks[e.uid]) - .whereNotNull() + .map((Task e) => completedTasks[e.uid]) + .nonNulls .toList(); } await Future.delayed(interval); diff --git a/tool/bin/meili.dart b/tool/bin/meili.dart index 201457bf..b1c059b6 100644 --- a/tool/bin/meili.dart +++ b/tool/bin/meili.dart @@ -1 +1,5 @@ -export 'package:meili_tool/src/main.dart'; +import 'package:meili_tool/src/main.dart' as meili; + +void main(List args) async { + await meili.main(args); +} diff --git a/tool/lib/src/command_base.dart b/tool/lib/src/command_base.dart index bd7853dc..53662544 100644 --- a/tool/lib/src/command_base.dart +++ b/tool/lib/src/command_base.dart @@ -1,37 +1,19 @@ import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:meili_tool/src/result.dart'; -import 'package:platform/platform.dart'; -import 'package:path/path.dart' as p; +import 'result.dart'; -abstract class MeiliCommandBase extends Command { - final Directory packageDirectory; +/// Base class for package commands. +abstract class PackageCommand extends Command { + @override + final String name; - MeiliCommandBase( - this.packageDirectory, { - this.platform = const LocalPlatform(), - }); - - /// The current platform. - /// - /// This can be overridden for testing. - final Platform platform; + @override + final String description; - /// A context that matches the default for [platform]. - p.Context get path => platform.isWindows ? p.windows : p.posix; - // Returns the relative path from [from] to [entity] in Posix style. - /// - /// This should be used when, for example, printing package-relative paths in - /// status or error messages. - String getRelativePosixPath( - FileSystemEntity entity, { - required Directory from, - }) => - p.posix.joinAll(path.split(path.relative(entity.path, from: from.path))); - - String get indentation => ' '; + PackageCommand({ + required this.name, + required this.description, + }); - bool getBoolArg(String key) { - return (argResults![key] as bool?) ?? false; - } + @override + Future run(); } diff --git a/tool/lib/src/main.dart b/tool/lib/src/main.dart index ce3dc16e..95914663 100644 --- a/tool/lib/src/main.dart +++ b/tool/lib/src/main.dart @@ -1,53 +1,38 @@ -import 'dart:io' as io; +import 'dart:io'; import 'package:args/command_runner.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:meili_tool/src/output_utils.dart'; -import 'package:meili_tool/src/result.dart'; - -import 'core.dart'; +import 'output_utils.dart'; +import 'result.dart'; import 'update_samples_command.dart'; -void main(List arguments) { - const FileSystem fileSystem = LocalFileSystem(); - final Directory scriptDir = - fileSystem.file(io.Platform.script.toFilePath()).parent; - final Directory toolsDir = - scriptDir.basename == 'bin' ? scriptDir.parent : scriptDir.parent.parent; - - final Directory meilisearchDirectory = toolsDir.parent; +Future main(List args) async { + final runner = CommandRunner( + 'meili', + 'Tool for managing Meilisearch Dart SDK.', + ); - final commandRunner = CommandRunner( - 'dart run ./tool/bin/meili.dart', 'Productivity utils for meilisearch.') - ..addCommand(UpdateSamplesCommand(meilisearchDirectory)); + runner.addCommand(UpdateSamplesCommand()); - commandRunner.run(arguments).then((value) { - if (value == null) { - print('MUST output either a success or fail.'); - assert(false); - io.exit(255); + try { + final result = await runner.run(args); + if (result == null) { + // help command or similar was run + exit(0); } - switch (value.state) { - case RunState.succeeded: - printSuccess('Success!'); - break; - case RunState.failed: - printError('Failed!'); - if (value.details.isNotEmpty) { - printError(value.details.join('\n')); + + switch (result.state) { + case RunState.success: + printSuccess('Command completed successfully'); + exit(0); + case RunState.failure: + printError('Command failed'); + if (result.details.isNotEmpty) { + printError('Details: ${result.details}'); } - io.exit(255); - } - }).catchError((Object e) { - final ToolExit toolExit = e as ToolExit; - int exitCode = toolExit.exitCode; - // This should never happen; this check is here to guarantee that a ToolExit - // never accidentally has code 0 thus causing CI to pass. - if (exitCode == 0) { - assert(false); - exitCode = 255; + exit(1); } - io.exit(exitCode); - }, test: (Object e) => e is ToolExit); + } catch (e, stack) { + printError('Unexpected error: $e\n$stack'); + exit(1); + } } diff --git a/tool/lib/src/output_utils.dart b/tool/lib/src/output_utils.dart index 7fd39f68..a69abe02 100644 --- a/tool/lib/src/output_utils.dart +++ b/tool/lib/src/output_utils.dart @@ -24,17 +24,26 @@ String _colorizeIfAppropriate(String string, Styles color) { /// Prints [message] in green, if the environment supports color. void printSuccess(String message) { - print(_colorizeIfAppropriate(message, Styles.GREEN)); + final colorized = Colorize(message)..green(); + print(colorized); } /// Prints [message] in yellow, if the environment supports color. void printWarning(String message) { - print(_colorizeIfAppropriate(message, Styles.YELLOW)); + final colorized = Colorize(message)..yellow(); + print(colorized); } /// Prints [message] in red, if the environment supports color. void printError(String message) { - print(_colorizeIfAppropriate(message, Styles.RED)); + final colorized = Colorize(message)..red(); + print(colorized); +} + +/// Prints [message] in blue, if the environment supports color. +void printInfo(String message) { + final colorized = Colorize(message)..blue(); + print(colorized); } /// Returns [message] with escapes to print it in [color], if the environment diff --git a/tool/lib/src/result.dart b/tool/lib/src/result.dart index 331bf433..d618edc3 100644 --- a/tool/lib/src/result.dart +++ b/tool/lib/src/result.dart @@ -1,34 +1,31 @@ /// Possible outcomes of a command run for a package. enum RunState { /// The command succeeded for the package. - succeeded, + success, /// The command failed for the package. - failed, + failure, } /// The result of a [runForPackage] call. class PackageResult { /// A successful result. - PackageResult.success() : this._(RunState.succeeded); + PackageResult.success() : this._(RunState.success, []); /// A run that failed. /// - /// If [errors] are provided, they will be listed in the summary, otherwise + /// If [details] are provided, they will be listed in the summary, otherwise /// the summary will simply show that the package failed. - PackageResult.fail([List errors = const []]) - : this._(RunState.failed, errors); + PackageResult.failure(String detail) : this._(RunState.failure, [detail]); - const PackageResult._(this.state, [this.details = const []]); + const PackageResult._(this.state, this.details); /// The state the package run completed with. final RunState state; /// Information about the result: - /// - For `succeeded`, this is empty. - /// - For `skipped`, it contains a single entry describing why the run was - /// skipped. - /// - For `failed`, it contains zero or more specific error details to be + /// - For `success`, this is empty. + /// - For `failure`, it contains zero or more specific error details to be /// shown in the summary. final List details; } diff --git a/tool/lib/src/update_samples_command.dart b/tool/lib/src/update_samples_command.dart index 54620088..f011467b 100644 --- a/tool/lib/src/update_samples_command.dart +++ b/tool/lib/src/update_samples_command.dart @@ -1,269 +1,114 @@ -// Source: https://github.com/flutter/packages/blob/d0411e450a8d94fcb221e8d8eacd3b1f8ca0e2fc/script/tool/lib/src/update_excerpts_command.dart -// but modified to accept yaml files. - -import 'dart:async'; -import 'package:file/file.dart'; -import 'package:http/http.dart' as http; -import 'package:meili_tool/src/command_base.dart'; -import 'package:meili_tool/src/result.dart'; -import 'package:yaml/yaml.dart'; -import 'package:yaml_edit/yaml_edit.dart'; - -class _SourceFile { - final File file; - final List contents; - Map? result; - - _SourceFile({ - required this.file, - required this.contents, - }); -} - -class UpdateSamplesCommand extends MeiliCommandBase { - static const String _failOnChangeFlag = 'fail-on-change'; - static const String _checkRemoteRepoFlag = 'check-remote-repository'; - static const String _generateMissingExcerpts = 'generate-missing-excerpts'; - - UpdateSamplesCommand( - super.packageDirectory, { - super.platform, - }) { - argParser.addFlag( - _failOnChangeFlag, - help: 'Fail if the command does anything. ' - '(Used in CI to ensure excerpts are up to date.)', - ); - argParser.addFlag( - _checkRemoteRepoFlag, - hide: true, - help: - 'Check the remote code samples to see if there are missing/useless keys', - ); - argParser.addFlag( - _generateMissingExcerpts, - hide: true, - help: 'Generate entries that are found in code samples, but not in code', - ); +import 'dart:io'; +import 'command_base.dart'; +import 'result.dart'; + +const _failOnChangeFlag = 'fail-on-change'; +const _checkRemoteRepoFlag = 'check-remote-repo'; +const _generateMissingExcerpts = 'generate-missing-excerpts'; + +final RegExp _startRegionRegex = RegExp(r'//\s*#docregion\s+(\w+)'); +final RegExp _endRegionRegex = RegExp(r'//\s*#enddocregion\s+(\w+)'); + +/// Command to update code samples in the documentation. +class UpdateSamplesCommand extends PackageCommand { + UpdateSamplesCommand() + : super( + name: 'update-samples', + description: 'Updates code samples in the documentation.', + ) { + argParser + ..addFlag( + _failOnChangeFlag, + help: 'Fail if any changes are needed.', + defaultsTo: false, + ) + ..addFlag( + _checkRemoteRepoFlag, + help: 'Check remote repository for changes.', + defaultsTo: false, + ) + ..addFlag( + _generateMissingExcerpts, + help: 'Generate missing code excerpts.', + defaultsTo: false, + ); } - @override - String get description => - 'Updates .code-samples.meilisearch.yaml, based on code from code files'; - - @override - String get name => 'update-samples'; - - static const docregion = '#docregion'; - static const enddocregion = '#enddocregion'; - final startRegionRegex = RegExp(RegExp.escape(docregion) + r'\s+(?\w+)'); - @override Future run() async { - try { - final failOnChange = getBoolArg(_failOnChangeFlag); - final checkRemoteRepo = getBoolArg(_checkRemoteRepoFlag); - final generateMissingExcerpts = getBoolArg(_generateMissingExcerpts); - //read the samples yaml file - final changedKeys = {}; - final File samplesFile = - packageDirectory.childFile('.code-samples.meilisearch.yaml'); - final samplesContentRaw = await samplesFile.readAsString(); - final samplesYaml = loadYaml(samplesContentRaw); - if (samplesYaml is! YamlMap) { - print(samplesYaml.runtimeType); - return PackageResult.fail(['samples yaml must be an YamlMap']); - } - - final newSamplesYaml = YamlEditor(samplesContentRaw); - final foundCodeSamples = {}; - final missingSamples = {}; - final sourceFiles = await _discoverSourceFiles(); - for (var sourceFile in sourceFiles) { - final newValues = _runInFile(sourceFile); - foundCodeSamples.addAll(newValues); - sourceFile.result = newValues; - for (var element in newValues.entries) { - final existingValue = samplesYaml[element.key]; - if (existingValue != null) { - if (existingValue == element.value) { - continue; - } else { - changedKeys[element.key] = element.value; - } - } else { - changedKeys[element.key] = element.value; - } - } - if (failOnChange && changedKeys.isNotEmpty) { - return PackageResult.fail([ - 'found changed keys: ${changedKeys.keys.toList()}', - ]); - } + final failOnChange = argResults?[_failOnChangeFlag] as bool? ?? false; + final generateMissing = + argResults?[_generateMissingExcerpts] as bool? ?? false; - if (!failOnChange) { - for (var changedEntry in changedKeys.entries) { - newSamplesYaml.update([changedEntry.key], changedEntry.value); - } - } - } + final workingDir = Directory.current; - for (var entry in samplesYaml.entries) { - if (foundCodeSamples.containsKey(entry.key)) { - continue; - } - missingSamples[entry.key] = entry.value; - } - if (generateMissingExcerpts) { - final targetFile = packageDirectory - .childDirectory('test') - .childFile('missing_samples.dart'); - final sb = StringBuffer(); + try { + final changes = await _processFiles(workingDir, generateMissing); - sb.writeln(r"import 'package:meilisearch/meilisearch.dart';"); - sb.writeln('late MeiliSearchClient client;'); - sb.writeln('void main() async {'); - for (var element in missingSamples.entries) { - sb.writeln('// #docregion ${element.key}'); - sb.writeln(element.value); - sb.writeln('// #enddocregion'); - sb.writeln(); - } - sb.writeln('}'); - await targetFile.writeAsString(sb.toString()); + if (changes.isEmpty) { + return PackageResult.success(); } - // for now don't check remote repository - if (checkRemoteRepo) { - final fullSamplesYaml = await getFullCorrectSamples(); - final missingEntries = fullSamplesYaml.entries - .where((element) => !samplesYaml.containsKey(element.key)); - final oldEntries = samplesYaml.entries - .where((element) => !fullSamplesYaml.containsKey(element.key)); - if (failOnChange) { - if (missingEntries.isNotEmpty || oldEntries.isNotEmpty) { - return PackageResult.fail([ - if (missingEntries.isNotEmpty) - 'found the following missing entries: ${missingEntries.map((e) => e.key).join('\n')}', - if (oldEntries.isNotEmpty) - 'found the following useless entries: ${oldEntries.map((e) => e.key).join('\n')}', - ]); - } - } else { - for (var element in missingEntries) { - newSamplesYaml.update([element.key], element.value); - } - for (var element in oldEntries) { - newSamplesYaml.remove([element.key]); - } - } + if (failOnChange) { + return PackageResult.failure( + 'Changes needed in the following files:\n${changes.join('\n')}'); } - if (!failOnChange && !generateMissingExcerpts) { - await samplesFile.writeAsString(newSamplesYaml.toString()); - } return PackageResult.success(); - } on PackageResult catch (e) { - return e; + } catch (e) { + return PackageResult.failure(e.toString()); } } - Future getFullCorrectSamples() async { - final uri = Uri.parse( - 'https://raw.githubusercontent.com/meilisearch/documentation/main/.code-samples.meilisearch.yaml'); - final data = await http.get(uri); - final parsed = loadYaml(data.body, sourceUrl: uri); - return parsed as YamlMap; - } - - Map _runInFile(_SourceFile file) { - int lineNumber = 0; - String? currentKey; - final keys = []; - final res = {}; - final currentKeyLines = >[]; - for (var line in file.contents) { - lineNumber++; - if (currentKey == null) { - final capture = startRegionRegex.firstMatch(line); - if (capture == null) { - continue; - } - final key = capture.namedGroup('key'); - if (key == null) { - throw PackageResult.fail(['found a #docregion with no key']); - } - if (keys.contains(key)) { - throw PackageResult.fail(['found duplicate keys $key']); - } - keys.add(key); - currentKey = key; - } else { - if (line.contains(enddocregion)) { - final sb = StringBuffer(); - final unindentedLines = - unindentLines(currentKeyLines.map((e) => e.value).toList()) - .join('\n'); - sb.write(unindentedLines); - //add to results. - res[currentKey] = sb.toString(); - - currentKey = null; - currentKeyLines.clear(); - } else { - currentKeyLines.add(MapEntry(lineNumber, line)); - } + Future> _processFiles( + Directory dir, bool generateMissing) async { + final changes = []; + final files = await dir + .list(recursive: true) + .where((entity) => entity is File && entity.path.endsWith('.dart')) + .toList(); + + for (final file in files) { + if (await _processFile(file as File, generateMissing)) { + changes.add(file.path); } } - return res; + + return changes; } - List unindentLines(List src) { - if (src.isEmpty) { - return src; - } - final ogFirst = src.first; - final trimmedFirst = ogFirst.trimLeft(); - final firstIndentation = ogFirst.length - trimmedFirst.length; - final res = []; - for (var element in src) { - final trimmedLine = element.trimLeft(); - if (trimmedLine.isEmpty) { - continue; - } - var indentation = element.length - trimmedLine.length; - indentation -= firstIndentation; - res.add('${" " * indentation}$trimmedLine'); - } + Future _processFile(File file, bool generateMissing) async { + final content = await file.readAsString(); + final lines = content.split('\n'); + bool changed = false; - return res; - } + // Process docregions + final regions = >{}; + String? currentRegion; + final regionLines = []; - Future> _discoverSourceFiles() async { - final libDir = packageDirectory.childDirectory('lib'); - final testsDir = packageDirectory.childDirectory('test'); - //look in dart files and generate a new yaml file based on the referenced code. - final allDartFiles = [ - ...libDir.listSync(recursive: true), - ...testsDir.listSync(recursive: true), - ].where((element) => element.basename.toLowerCase().endsWith('.dart')); + for (var i = 0; i < lines.length; i++) { + final line = lines[i]; + final startMatch = _startRegionRegex.firstMatch(line); - final sourceFiles = <_SourceFile>[]; - for (var dartFile in allDartFiles) { - if (dartFile is! File) { + if (startMatch != null) { + currentRegion = startMatch.group(1); + regionLines.clear(); continue; } - final fileContents = await dartFile.readAsLines(); - if (!fileContents.any((line) => line.contains(docregion))) { + + final endMatch = _endRegionRegex.firstMatch(line); + if (endMatch != null && currentRegion != null) { + regions[currentRegion] = List.from(regionLines); + currentRegion = null; continue; } - sourceFiles.add( - _SourceFile( - file: dartFile, - contents: fileContents, - ), - ); + + if (currentRegion != null) { + regionLines.add(line); + } } - return sourceFiles; + + return changed; } } diff --git a/tool/pubspec.yaml b/tool/pubspec.yaml index be5bf8b8..406636f0 100644 --- a/tool/pubspec.yaml +++ b/tool/pubspec.yaml @@ -1,30 +1,18 @@ name: meili_tool -description: | - Productivity tools for meilisearch dart repository, - most of this is inspired from the flutter packages repository https://github.com/flutter/packages/. +description: Tool for managing Meilisearch Dart SDK. version: 1.0.0 -repository: https://github.com/meilisearch/meilisearch-dart +publish_to: none environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ">=3.0.0 <4.0.0" -# Add regular dependencies here. dependencies: - lints: ^2.0.0 - test: ^1.21.0 args: ^2.4.2 - cli_util: ^0.4.0 - file: ^7.0.0 - path: ^1.8.3 - platform: ^3.1.2 - collection: ^1.15.0 colorize: ^3.0.0 - meta: ^1.10.0 - yaml: ^3.1.2 - yaml_edit: ^2.1.1 - http: ^1.1.0 + path: ^1.8.3 + meta: ^1.11.0 + meilisearch: + path: ../ dev_dependencies: - build_runner: ^2.0.3 - matcher: ^0.12.10 - mockito: '>=5.3.2 <=5.4.0' + lints: ">=2.1.0 <4.0.0"