Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/implement command translations and command definition #173

Merged
merged 10 commits into from
Jul 25, 2024
53 changes: 35 additions & 18 deletions lib/api/common/commands/builder/command_builder.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import 'package:mineral/api/common/commands/builder/command_group_builder.dart';
import 'package:mineral/api/common/commands/builder/sub_command_builder.dart';
import 'package:mineral/api/common/commands/builder/translation.dart';
import 'package:mineral/api/common/commands/command_context_type.dart';
import 'package:mineral/api/common/commands/command_helper.dart';
import 'package:mineral/api/common/commands/command_option.dart';
import 'package:mineral/api/common/commands/command_type.dart';

final class CommandBuilder {
final CommandHelper _helper = CommandHelper();

String? _name;
Map<String, String>? _nameLocalizations;
String? _description;
Map<String, String>? _descriptionLocalizations;
CommandContextType context = CommandContextType.guild;
final List<CommandOption> _options = [];
final List<SubCommandBuilder> _subCommands = [];
final List<CommandGroupBuilder> _groups = [];
final List<CommandOption> options = [];
final List<SubCommandBuilder> subCommands = [];
final List<CommandGroupBuilder> groups = [];
Function? _handle;

CommandBuilder setName(String name) {
CommandBuilder setName(String name, {Translation? translation}) {
_name = name;
if (translation != null) {
_nameLocalizations = _helper.extractTranslations('name', translation);
}

return this;
}

Expand All @@ -23,17 +33,20 @@ final class CommandBuilder {
return this;
}

CommandBuilder setDescription(String description) {
CommandBuilder setDescription(String description, {Translation? translation}) {
_description = description;
if (translation != null) {
_descriptionLocalizations = _helper.extractTranslations('description', translation);
}
return this;
}

CommandBuilder addOption<T extends CommandOption>(T option) {
_options.add(option);
options.add(option);
return this;
}

CommandBuilder handle(Function fn) {
CommandBuilder setHandle(Function fn) {
final firstArg = fn.toString().split('(')[1].split(')')[0].split(' ')[0];

if (!firstArg.contains('CommandContext')) {
Expand All @@ -44,47 +57,51 @@ final class CommandBuilder {
return this;
}

CommandBuilder addSubCommand(SubCommandBuilder Function(SubCommandBuilder) command) {
CommandBuilder addSubCommand(Function(SubCommandBuilder) command) {
final builder = SubCommandBuilder();
command(builder);
_subCommands.add(builder);

subCommands.add(builder);
return this;
}

CommandBuilder createGroup(CommandGroupBuilder Function(CommandGroupBuilder) group) {
final builder = CommandGroupBuilder();
group(builder);
_groups.add(builder);

groups.add(builder);
return this;
}

Map<String, dynamic> toJson() {
final List<Map<String, dynamic>> options = [
for (final option in _options) option.toJson(),
for (final subCommand in _subCommands) subCommand.toJson(),
for (final group in _groups) group.toJson(),
for (final option in this.options) option.toJson(),
for (final subCommand in subCommands) subCommand.toJson(),
for (final group in groups) group.toJson(),
];

return {
'name': _name,
'name_localizations': _nameLocalizations,
'description': _description,
if (_subCommands.isEmpty && _groups.isEmpty) 'type': CommandType.subCommand.value,
'description_localizations': _descriptionLocalizations,
if (subCommands.isEmpty && groups.isEmpty) 'type': CommandType.subCommand.value,
'options': options,
};
}

List<(String, Function handler)> reduceHandlers() {
if (_subCommands.isEmpty && _groups.isEmpty) {
return [('$_name', _handle!)];
if (subCommands.isEmpty && groups.isEmpty) {
return [('$_name', _handle!)];
}

final List<(String, Function handler)> handlers = [];

for (final subCommand in _subCommands) {
for (final subCommand in subCommands) {
handlers.add(('$_name.${subCommand.name}', subCommand.handle!));
}

for (final group in _groups) {
for (final group in groups) {
for (final subCommand in group.commands) {
handlers.add(('$_name.${group.name}.${subCommand.name}', subCommand.handle!));
}
Expand Down
218 changes: 218 additions & 0 deletions lib/api/common/commands/builder/command_definition.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import 'dart:convert';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:mineral/api/common/commands/builder/command_builder.dart';
import 'package:mineral/api/common/commands/builder/sub_command_builder.dart';
import 'package:mineral/api/common/commands/builder/translation.dart';
import 'package:mineral/api/common/commands/command_choice_option.dart';
import 'package:mineral/api/common/commands/command_option.dart';
import 'package:mineral/api/common/lang.dart';
import 'package:yaml/yaml.dart';

final class CommandDefinition {
final String _defaultIdentifier = '_default';

final Map<String, dynamic Function()> _commandMapper = {};
final CommandBuilder command = CommandBuilder();

String _extractDefaultValue(String commandKey, String key, Map<String, dynamic> payload) {
final Map<String, dynamic>? elements = payload[key];
if (elements == null) {
throw Exception('Missing "$key" key under $commandKey');
}

if (elements[_defaultIdentifier] case final String value) {
return value;
}

throw Exception('Missing "$key.$_defaultIdentifier" key under $commandKey struct');
}

Map<Lang, String> _extractTranslations(String key, Map<String, dynamic> payload) {
final Map<String, dynamic>? elements = payload[key];
if (elements == null) {
throw Exception('Missing "$key" key');
}

return elements.entries.whereNot((element) => element.key == _defaultIdentifier).fold({},
(acc, element) {
final lang = Lang.values.firstWhere((lang) => lang.uid == element.key);
return {...acc, lang: element.value};
});
}

void _declareGroups(Map<String, dynamic> content) {
final Map<String, dynamic> groupList = content['groups'] ?? {};

for (final element in groupList.entries) {
final String defaultName = _extractDefaultValue(element.key, 'name', element.value);
final String defaultDescription =
_extractDefaultValue(element.key, 'description', element.value);

final nameTranslations = _extractTranslations('name', element.value);
final descriptionTranslations = _extractTranslations('description', element.value);

command.createGroup((group) {
return group
..setName(defaultName, translation: Translation({'name': nameTranslations}))
..setDescription(defaultDescription,
translation: Translation({'name': descriptionTranslations}));
});
}
}

void _declareOptions(SubCommandBuilder command, MapEntry<String, dynamic> element) {
final options = element.value['options'] ?? [];

for (final Map<String, dynamic> element in options) {
final String name = _extractDefaultValue('option', 'name', element);
final String description = _extractDefaultValue('option', 'description', element);
final bool required = element['required'] ?? false;

final option = switch (element['type']) {
final String value when value == 'string' =>
Option.string(name: name, description: description, required: required),
final String value when value == 'integer' =>
Option.integer(name: name, description: description, required: required),
final String value when value == 'double' =>
Option.double(name: name, description: description, required: required),
final String value when value == 'string' =>
Option.boolean(name: name, description: description, required: required),
final String value when value == 'user' =>
Option.user(name: name, description: description, required: required),
final String value when value == 'channel' =>
Option.channel(name: name, description: description, required: required),
final String value when value == 'role' =>
Option.role(name: name, description: description, required: required),
final String value when value == 'mention' =>
Option.mentionable(name: name, description: description, required: required),
final String value when value == 'choice.string' => ChoiceOption.string(
name: name,
description: description,
required: required,
choices: List.from(element['choices'] ?? [])
.map((element) => Choice<String>(element['name'], element['value']))
.toList()),
final String value when value == 'choice.integer' => ChoiceOption.integer(
name: name,
description: description,
required: required,
choices: List.from(element['choices'] ?? [])
.map((element) => Choice(element['name'], int.parse(element['value'])))
.toList()),
final String value when value == 'choice.double' => ChoiceOption.double(
name: name,
description: description,
required: required,
choices: List.from(element['choices'] ?? [])
.map((element) => Choice(element['name'], double.parse(element['value'])))
.toList()),
_ => throw Exception('Unknown option type')
};

command.addOption(option);
}
}

void _declareSubCommands(Map<String, dynamic> content) {
final Map<String, dynamic> commandList = content['commands'] ?? {};

for (final element in commandList.entries) {
if (element.key.contains('.')) {
final String defaultName = _extractDefaultValue(element.key, 'name', element.value);
final String defaultDescription =
_extractDefaultValue(element.key, 'description', element.value);

final nameTranslations = _extractTranslations('name', element.value);
final descriptionTranslations = _extractTranslations('description', element.value);

if (element.value['group'] case final String group) {
final currentGroup = command.groups.firstWhere((element) => element.name == group)
..addSubCommand((command) {
command
..setName(defaultName, translation: Translation({'name': nameTranslations}))
..setDescription(defaultDescription,
translation: Translation({'description': descriptionTranslations}));

_declareOptions(command, element);
});

final int currentGroupIndex = command.groups.indexOf(currentGroup);
final int currentSubCommandIndex =
currentGroup.commands.indexOf(currentGroup.commands.last);
_commandMapper[element.key] =
() => command.groups[currentGroupIndex].commands[currentSubCommandIndex];
} else {
command.addSubCommand((command) {
command
..setName(defaultName, translation: Translation({'name': nameTranslations}))
..setDescription(defaultDescription,
translation: Translation({'description': descriptionTranslations}));

_declareOptions(command, element);
});
final currentSubCommandIndex = command.subCommands.indexOf(command.subCommands.last);
_commandMapper[element.key] = () => command.subCommands[currentSubCommandIndex];
}
}
}
}

void _declareCommand(Map<String, dynamic> content) {
final Map<String, dynamic> commandList = content['commands'] ?? {};

for (final element in commandList.entries) {
if (!element.key.contains('.')) {
final String defaultName = _extractDefaultValue(element.key, 'name', element.value);
final String defaultDescription =
_extractDefaultValue(element.key, 'description', element.value);

final nameTranslations = _extractTranslations('name', element.value);
final descriptionTranslations = _extractTranslations('description', element.value);

command
..setName(defaultName, translation: Translation({'name': nameTranslations}))
..setDescription(defaultDescription,
translation: Translation({'description': descriptionTranslations}));

_commandMapper[element.key] = () => command;
}
}
}

CommandDefinition context<T>(String key, Function(T) fn) {
final command = _commandMapper.entries.firstWhere((element) => element.key == key);
fn(command.value());

return this;
}

CommandDefinition setHandler(String key, Function fn) {
final command = _commandMapper.entries.firstWhere((element) => element.key == key);

switch (command.value()) {
case final SubCommandBuilder command:
command.setHandle(fn);
case final CommandBuilder command:
command.setHandle(fn);
}

return this;
}

CommandDefinition using(File file) {
final String stringContent = file.readAsStringSync();
final content = switch(file.path) {
final String path when path.contains('.json') => jsonDecode(stringContent),
final String path when path.contains('.yaml') => (loadYaml(stringContent) as YamlMap).toMap(),
_ => throw Exception('File type not supported')
};

_declareCommand(content);
_declareGroups(content);
_declareSubCommands(content);

return this;
}
}
25 changes: 20 additions & 5 deletions lib/api/common/commands/builder/command_group_builder.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import 'package:mineral/api/common/commands/builder/sub_command_builder.dart';
import 'package:mineral/api/common/commands/builder/translation.dart';
import 'package:mineral/api/common/commands/command_helper.dart';
import 'package:mineral/api/common/commands/command_type.dart';

final class CommandGroupBuilder {
final CommandHelper _helper = CommandHelper();

String? name;
String? description;
Map<String, String>? _nameLocalizations;
String? _description;
Map<String, String>? _descriptionLocalizations;
final List<SubCommandBuilder> commands = [];

CommandGroupBuilder();

CommandGroupBuilder setName(String name) {
CommandGroupBuilder setName(String name, {Translation? translation}) {
this.name = name;
if (translation != null) {
_nameLocalizations = _helper.extractTranslations('name', translation);
}

return this;
}

CommandGroupBuilder setDescription(String description) {
this.description = description;
CommandGroupBuilder setDescription(String description, {Translation? translation}) {
_description = description;
if (translation != null) {
_descriptionLocalizations = _helper.extractTranslations('description', translation);
}
return this;
}

Expand All @@ -28,7 +41,9 @@ final class CommandGroupBuilder {
Map<String, dynamic> toJson() {
return {
'name': name,
'description': description,
'name_localizations': _nameLocalizations,
'description': _description,
'description_localizations': _descriptionLocalizations,
'type': CommandType.subCommandGroup.value,
'options': commands.map((e) => e.toJson()).toList(),
};
Expand Down
Loading