From: Hamatoma Date: Sun, 8 Aug 2021 12:45:08 +0000 (+0200) Subject: Generator, i18n_text_parser, i18n, yaml_merger, rest_server X-Git-Url: https://gitweb.hamatoma.de/?a=commitdiff_plain;h=4a7ca743070c31b3cfe2719d61aec01155df2d10;p=exhibition.git Generator, i18n_text_parser, i18n, yaml_merger, rest_server * new: Generator contains all meta data creation parts. * new: i18n_text_parser parses Dart files for I18N texts. * new: i18n allows multi language support. * new: yaml_merger merges SQL statement definitions, normally generated files and hand made files. * rest_server: scripts for simple installing on the backend. --- diff --git a/.gitignore b/.gitignore index f8ae0fd..e75c7bd 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ ios/ web/ linux/ pubspec.lock +tools/meta_tool +tools/yaml_merger diff --git a/Meta b/Meta new file mode 100755 index 0000000..6668928 --- /dev/null +++ b/Meta @@ -0,0 +1,5 @@ +#! /bin/sh +EXE=tools/meta_tool +test ! -x $EXE && ./ReCreateMetaTool +$EXE $* + diff --git a/bin/generator.dart b/bin/generator.dart new file mode 100644 index 0000000..4cfb235 --- /dev/null +++ b/bin/generator.dart @@ -0,0 +1,339 @@ +import 'dart:io'; + +import 'package:exhibition/meta/module_meta_data.dart'; +import 'package:exhibition/meta/modules.dart'; +import 'package:path/path.dart'; + +/// Converts a [className] into a file name using Dart conventions. +/// Example: "UserData" is converted to "user_data" +String classToFilename(String className) { + String upperCase = className.toUpperCase(); + String rc = ''; + for (var ix = 0; ix < className.length; ix++) { + if (className[ix] == upperCase[ix] && ix > 0) { + rc += '_'; + } + rc = className[ix]; + } + return rc.toLowerCase(); +} + +/// Converts a [filename] into a class name using Dart conventions. +/// Example: "user_data.dart" is converted to "UserData" +String filenameToClass(String filename) { + final parts = basenameWithoutExtension(filename).split('_'); + String rc = ''; + for (var part in parts) { + if (part.isNotEmpty) { + rc += part[0].toUpperCase() + part.substring(1); + } + } + return rc; +} + +class Generator { + /// Appends a [string] at the end of a [buffer]. + /// If [buffer] is null a new instance is created. + /// If the last line in [buffer] has a length greater than [maxLength] + /// a newline is inserted and [indent] spaces. + /// If [separator] is not null that is inserted above the [string]. + /// Returns the buffer (for chaining). + StringBuffer addToBuffer(String string, + {required int maxLength, + String? separator, + int indent = 0, + StringBuffer? buffer}) { + buffer ??= StringBuffer(); + if (separator != null) { + buffer.write(separator); + } + final last = buffer.toString().lastIndexOf('\n'); + if (buffer.length - (last < 0 ? 0 : last) + string.length > maxLength) { + buffer.write('\n' + (' ' * indent)); + } + buffer.write(string); + return buffer; + } + + ModuleMetaData? checkModule(List args, int index) { + ModuleMetaData? rc; + if (index >= args.length) { + out('+++ too few arguments. Missing MODULE.'); + } else if ((rc = moduleByName(args[index])) == null) { + out('+++ unknown module: ${args[index]}'); + rc = null; + } + return rc; + } + + /// Returns a DDL statement creating the database table of the [module]. + String createDbTable(ModuleMetaData module) { + final tableName = module.tableName; + final list = module.list; + final buffer = StringBuffer(); + buffer.write('-- DO NOT CHANGE. This file is created by the meta_tool\n'); + buffer.write('CREATE TABLE $tableName (\n'); + String? primary; + for (var item in list) { + if (item.options.contains('primary')) { + primary = item.columnName; + } + buffer.write(' ${item.columnName}'); + buffer.write(' ' + module.mySqlType(item.dataType, item.size)); + buffer.write(module.dbOptions(item) + ',\n'); + } + buffer.write(' PRIMARY KEY ($primary)\n'); + buffer.write(');\n'); + return buffer.toString(); + } + + /// Returns a Dart class definition of the [module]. + String createModuleData(ModuleMetaData module) { + final buffer = StringBuffer(); + buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n'); + buffer.write("import '../../base/defines.dart';\n"); + buffer.write("import '../../base/helper.dart';\n"); + buffer.write('class ${module.moduleNameSingular}Data{\n'); + for (var item in module.list) { + buffer.write(' ' + module.dartType(item.dataType) + '? ${item.name};\n'); + } + buffer.write(' ${module.moduleNameSingular}Data({'); + String separator = ''; + for (var item in module.list) { + buffer.write('$separator this.${item.name}'); + separator = ','; + } + buffer.write('});\n'); + buffer + .write(' ${module.moduleNameSingular}Data.fromYaml(YamlMap data) {\n'); + for (var item in module.list) { + final name = item.columnName; + final type = item.dataType.toString(); + //id = data.containsKey('id') ? int.tryParse(data['id']) : null; + buffer.write( + " ${item.name} = data.containsKey('$name') ? valueOf($type, data['$name']) : null;\n"); + } + buffer.write(' }\n'); + buffer.write(' static DataType? dataTypeOf(String name) {\n'); + buffer.write(' DataType? rc;\n'); + buffer.write(' switch(name){\n'); + for (var item in module.list) { + buffer.write(" case '${item.name}':\n"); + buffer.write(' rc = ${item.dataType};\n'); + buffer.write(' break;\n'); + } + buffer.write(''' default: + break; + } + return rc; + } +'''); + buffer.write('}\n'); + + return buffer.toString(); + } + + /// Creates the file modules.dart. + String createModules() { + final buffer = StringBuffer(); + buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n'); + buffer.write("import 'dart:io';\n"); + buffer.write("import 'module_meta_data.dart';\n"); + final modules = []; + final files = []; + final fileOfModule = {}; + + for (var item in Directory('lib/meta').listSync()) { + final name = basename(item.path); + if (name.endsWith('_meta.dart')) { + final moduleName = filenameToClass(name.replaceFirst('_meta.dart', '')); + fileOfModule[moduleName] = name; + modules.add(moduleName); + files.add(name); + } + } + modules.sort(); + files.sort(); + for (var file in files) { + buffer.write("import '$file';\n"); + } + buffer.write(''' +/// Returns the meta data of the module given by [name]. +/// Returns null if not found. +ModuleMetaData? moduleByName(String name) { + ModuleMetaData? rc; + switch (name) { +'''); + for (var module in modules) { + buffer.write(" case '$module':\n"); + final className = findClass(fileOfModule[module]!); + buffer.write(" rc = $className();\n"); + buffer.write(" break;\n"); + } + buffer.write(''' default: + break; + } + return rc; +} +/// Returns the module names as string list. +List moduleNames(){ + return [ +'''); + for (var module in modules) { + buffer.write(" '$module',\n"); + } + buffer.write(''' ]; +} +'''); + return buffer.toString(); + } + + /// Returns the SQL statements for insert, update, delete... + /// for a given [module]. + /// This yaml file is a configuration for the rest_server. + String createSqlStatements(ModuleMetaData module) { + final moduleName = module.moduleName; + final tableName = module.tableName; + final list = module.list; + final buffer = StringBuffer(); + buffer.write('---\n'); + buffer.write('# DO NOT CHANGE. This file is created by the meta_tool\n'); + buffer.write('''# SQL statements of the module "$moduleName":\n +module: $moduleName +list: + type: list + parameters: [] + sql: "select * from $tableName;" +byId: + type: record + parameters: [ "${list[0].name}" ] + sql: "select * from $tableName where ${list[0].columnName}=:${list[0].name};" +delete: + type: delete + parameters: [ "${list[0].name}" ] + sql: "delete * from $tableName where ${list[0].columnName}=:${list[0].name};" +update: + type: update +'''); + var items = module.standardColumns('changedBy'); + var parameters = addToBuffer(' parameters: [', maxLength: 80); + var assignments = addToBuffer(' ', maxLength: 80); + var first = true; + for (var item in items) { + addToBuffer('":${item.name}"', + maxLength: 80, + separator: first ? null : ',', + indent: 4, + buffer: parameters); + addToBuffer('${item.columnName}=:${item.name}', + maxLength: 80, + separator: first ? null : ',', + indent: 4, + buffer: assignments); + first = false; + } + parameters.write(']'); + var item = module.properties['changed']!; + addToBuffer('${item.columnName}=NOW()', + maxLength: 80, separator: ',', indent: 4, buffer: assignments); + // parameters: [":id", ":name", ":displayname", ":email", ":changedby"] + buffer.writeln(parameters); + buffer.write(' sql: "UPDATE $tableName SET\n'); + buffer.writeln(assignments); + //user_name=:name, user_displayname=:displayname, user_email=:email, user_changed=NOW(), user_changedby=:changedby + buffer.write(' WHERE ${list[0].columnName}=:${list[0].name};"\n'); + + items = module.standardColumns('createdBy'); + parameters = addToBuffer(' parameters: [', maxLength: 80); + final sql1 = addToBuffer(' sql: "INSERT INTO $tableName(', maxLength: 80); + final sql2 = addToBuffer(' VALUES(', maxLength: 80); + first = true; + for (var item in items) { + addToBuffer('":${item.name}"', + maxLength: 80, + separator: first ? null : ',', + indent: 4, + buffer: parameters); + addToBuffer('${item.columnName}', + maxLength: 80, + separator: first ? null : ',', + indent: 6, + buffer: sql1); + addToBuffer(':${item.name}', + maxLength: 80, + separator: first ? null : ',', + indent: 6, + buffer: sql2); + first = false; + } + parameters.write(']'); + item = module.properties['created']!; + addToBuffer('${item.columnName})', + maxLength: 80, separator: ',', indent: 4, buffer: sql1); + addToBuffer('NOW());"', + maxLength: 80, separator: ',', indent: 4, buffer: sql2); + // parameters: [":id", ":name", ":displayname", ":email", ":changedby"] + buffer.write('''insert: + type: insert +'''); + buffer.writeln(parameters); + buffer.writeln(sql1); + buffer.writeln(sql2); + return buffer.toString(); + } + + /// Finds the class name of a module meta class given by the file [node]. + String findClass(String node) { + final contents = File('lib/meta/$node').readAsStringSync(); + RegExpMatch? match; + String rc; + if ((match = RegExp(r'class\s+(\w+)\s').firstMatch(contents)) != null) { + rc = match!.group(1)!; + } else { + print('+++ missing class in $node'); + rc = filenameToClass(node.replaceFirst('.dart', '')); + } + return rc; + } + + /// Replaces print(). + void out(String line) { + stdout.write(line + '\n'); + } + /// Generates all modules defined in the meta data. + void updateModules(Generator generator) { + writeFile('lib/meta/modules.dart', createModules()); + final modules = moduleNames(); + for (var name in modules) { + ModuleMetaData? module = moduleByName(name); + if (module == null) { + print('+++ unknown module: $name'); + } else { + String filename = + classToFilename(module.moduleNameSingular) + '_data.dart'; + final directory = name.toLowerCase(); + writeFile('lib/page/$directory/$filename', + generator.createModuleData(module)); + } + } + } + /// Generates the Sql statement file for each module defined in the meta data. + void updateSql(Generator generator) { + final modules = moduleNames(); + for (var name in modules) { + ModuleMetaData? module = moduleByName(name); + if (module == null) { + print('+++ unknown module: $name'); + } else { + String filename = 'rest_server/data/sql/${name.toLowerCase()}.sql.yaml'; + writeFile(filename, generator.createSqlStatements(module)); + } + } + } + + void writeFile(String filename, String contents) { + out('creating $filename ...'); + final file = File(filename); + file.writeAsStringSync(contents); + } +} diff --git a/bin/i18n_text_parser.dart b/bin/i18n_text_parser.dart new file mode 100644 index 0000000..bbf660d --- /dev/null +++ b/bin/i18n_text_parser.dart @@ -0,0 +1,194 @@ +import "dart:io"; + +import 'package:dart_bones/dart_bones.dart'; +import "package:path/path.dart"; + +void main(List args) {} + +class I18nTextParser { + final BaseLogger logger; + final modules = >{}; + final foundFiles = {}; + var regExpFiles = RegExp(r'.dart$'); + String currentModule = ''; + final moduleVariables = {}; + String currentFile = ''; + List lines = []; + int currentLineNo = 0; + // ...........................1.............1.........2 + final regExpModule = + RegExp(r'''(\w+) = (I18N\(\)|i18N)\.module\(["'](.*?)['"]\);'''); + final regExpDelimiter = RegExp(r'''["']'''); + // ..........1.............1..2..............................2 + final regExpText = + RegExp(r'(i18N|I18N\(\))\.(tr|trPlural|trMulti|trWithArgs)\('); + final regExpStringConstant = RegExp(r'''^\s*(r?)(["'])(.*?)\2'''); + final regExpVariable = RegExp(r'^\w+'); + final regExpEmptyString = RegExp(r'^\s*$'); + + I18nTextParser(this.logger); + + /// Gets the text (multiple lines) from the [arguments]. + /// [arguments] is the string behind the 'trMulti('. + void handleMultiText(String arguments) { + logger.error('not implemented: trMulti()'); + } + + /// Gets the text (multiple lines) from the [arguments] . + /// [arguments] is the string behind the 'trPlural('. + void handlePluralText(String arguments) { + logger.error('not implemented: trPlural()'); + } + + /// Gets the text (single line) from the [arguments] and store it. + /// [arguments] is the string behind the 'tr('. + void handleSimpleText(String arguments) { + RegExpMatch? match; + String module = '!global'; + int lineNo = currentLineNo; + if (regExpEmptyString.firstMatch(arguments) != null) { + arguments = lineNo >= lines.length ? '' : lines[lineNo++].trimLeft(); + } + String? key; + if ((match = regExpStringConstant.firstMatch(arguments)) != null) { + key = match?.group(3); + arguments = arguments.substring(match!.end).trimLeft(); + if (arguments.startsWith(',')) { + arguments = arguments.substring(1).trimLeft(); + if (arguments.isEmpty) { + arguments = lineNo >= lines.length ? '' : lines[lineNo++].trimLeft(); + } + if ((match = regExpStringConstant.firstMatch(arguments)) != null) { + module = match!.group(3)!; + } else if ((match = regExpVariable.firstMatch(arguments)) != null) { + final variable = match!.group(0); + if (moduleVariables.containsKey(variable)) { + module = moduleVariables[variable]!; + } else { + logger.error( + '$currentFile-$currentLineNo: unknown module variable: $variable'); + } + } else { + module = currentModule; + } + } + } + if (key == null) { + logger.error( + '$currentFile-$currentLineNo: cannot analyse parameters of tr()'); + } else { + putText(key, currentFile, currentLineNo, module); + } + } + + /// Gets the text (with placeholders) from the [arguments] and store it. + /// [arguments] is the string behind the 'trArgs('. + void handleTextWithArgs(String? arguments) { + logger.error('not implemented: trArgs()'); + } + + /// Stores the [key] in the [module] map with location info [filename] and + /// [lineNo]. + void putText(String key, String filename, int lineNo, [String module = '']) { + if (module.isEmpty) { + module = currentModule; + } + if (!modules.containsKey(module)) { + modules[module] = {}; + } + if (modules[module]!.containsKey(key)) { + modules[module]![key] = modules[module]![key]! + '\n#: $filename:$lineNo'; + } else { + modules[module]![key] = '#: $filename:$lineNo'; + } + } + + /// Scans recursively all dart files of the [directory]. + void scanDirectory( + String directory, + ) { + final base = Directory(directory); + final subDirs = []; + try { + for (var file in base.listSync()) { + if (file is Directory) { + subDirs.add(file.path); + continue; + } + if (regExpFiles.firstMatch(file.path) == null) { + continue; + } + if (file is File) { + scanFile(file, directory); + } else { + logger.error('ignored: $directory/${file.path}'); + } + } + } on FileSystemException catch (exc) { + logger.error('ignored: $directory: $exc'); + } + for (var node in subDirs) { + scanDirectory(join(directory, node)); + } + } + + /// Scans the [file] for I18N texts. + /// [directory]: used for logging. + void scanFile(File file, String directory) { + try { + currentFile = join(directory, file.path); + lines = file.readAsLinesSync(); + moduleVariables.clear(); + currentLineNo = 0; + for (var line in lines) { + ++currentLineNo; + RegExpMatch? match; + if ((match = regExpModule.firstMatch(line)) != null) { + currentModule = match!.group(3)!; + moduleVariables[match.group(1)!] = currentModule; + } else { + for (match in regExpText.allMatches(line)) { + final restLine = line.substring(match.end); + switch (match.group(2)!) { + case 'tr': + handleSimpleText(restLine); + break; + case 'trMulti': + handleMultiText(restLine); + break; + case 'trPlural': + handlePluralText(restLine); + break; + case 'trArgs': + handleTextWithArgs(restLine); + break; + } + } + } + } + } on FileSystemException catch (exc) { + logger.error('ignored: $directory: $exc'); + } + } + + /// Writes the *.pot files to the [directory]. + void writePot(String directory) { + FileSync().ensureDirectory(directory); + for (var module in modules.keys) { + final map = modules[module]!; + final keys = map.keys.toList(); + keys.sort(); + final buffer = StringBuffer('# Texts of module $module, parsed by i18n_text_parser\n'); + for (var key in keys) { + buffer.writeln(); + buffer.writeln(map[key]); + final key2 = key.replaceAll('"', r'\"'); + buffer.writeln('msgid "$key2"'); + buffer.writeln('msgstr ""'); + } + final fn = join(directory, '$module.pot'); + logger.log('writing $fn', LEVEL_DETAIL); + File(fn).writeAsStringSync(buffer.toString()); + } + } +} diff --git a/bin/meta_tool.dart b/bin/meta_tool.dart index 5f0f97c..13af6c5 100644 --- a/bin/meta_tool.dart +++ b/bin/meta_tool.dart @@ -1,162 +1,74 @@ -import 'dart:io'; - +import 'package:exhibition/base/i18n.dart'; import 'package:exhibition/meta/module_meta_data.dart'; import 'package:exhibition/meta/modules.dart'; -import 'package:path/path.dart'; -void usage(){ - out('''Usage: meta_tool MODE MODULE - Generate code specified by MODE for the module MODULE. -MODE: - all-modules - Prints the module names. - print-modules - Prints the file lib/meta/modules.dart. - print-table MODULE - Prints the SQL table definition of MODULE. - print-data MODULE - Creates the data storage class of MODULE - update-modules - Generates all module files depending on the meta data. -MODULE: - The name of the module, e.g. "Users" -Example: -meta_tool print-table Users -'''); -} +import 'generator.dart'; + void main(List args) { + final generator = Generator(); + I18N.internal('data/i18n'); if (args.isEmpty) { - usage(); - out('+++ missing arguments.'); + usage(generator); + generator.out('+++ missing arguments.'); } else { ModuleMetaData? metaData; switch (args[0]) { case 'all-modules': - out(moduleNames().join('\n')); + generator.out(moduleNames().join('\n')); break; case 'print-modules': - out(createModules()); + generator.out(generator.createModules()); break; case 'print-table': - if ((metaData = checkModule(args, 1)) != null) { - out(metaData!.createDbTable()); + if ((metaData = generator.checkModule(args, 1)) != null) { + generator.out(generator.createDbTable(metaData!)); + } + break; + case 'print-sql': + if ((metaData = generator.checkModule(args, 1)) != null) { + generator.out(generator.createSqlStatements(metaData!)); } break; case 'print-data': - if ((metaData = checkModule(args, 1)) != null) { - out(metaData!.createModuleData()); + if ((metaData = generator.checkModule(args, 1)) != null) { + generator.out(generator.createModuleData(metaData!)); } break; case 'update-modules': - updateModules(); + generator.updateModules(generator); + break; + case 'update-sql': + generator.updateSql(generator); break; default: - usage(); - out('+++ unknown MODE: ${args[0]}.'); + usage(generator); + generator.out('+++ unknown MODE: ${args[0]}.'); break; } } } -ModuleMetaData? checkModule(List args, int index) { - ModuleMetaData? rc; - if (index >= args.length) { - out('+++ too few arguments. Missing MODULE.'); - } else { - rc = moduleByName(args[index]); - } - return rc; -} - -String createModules() { - final buffer = StringBuffer(); - buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n'); - buffer.write("import 'dart:io';\n"); - buffer.write("import 'module_meta_data.dart';\n"); - final modules = []; - final files = []; - final fileOfModule = {}; - - for (var item in Directory('lib/meta').listSync()) { - final name = basename(item.path); - if (name.endsWith('_meta.dart')) { - final moduleName = filenameToClass(name.replaceFirst('_meta.dart', '')); - fileOfModule[moduleName] = name; - modules.add(moduleName); - files.add(name); - } - } - modules.sort(); - files.sort(); - for (var file in files) { - buffer.write("import '$file';\n"); - } - buffer.write(''' -/// Returns the meta data of the module given by [name]. -ModuleMetaData moduleByName(String name) { - ModuleMetaData rc; - switch (name) { -'''); - String? firstClass; - for (var module in modules) { - buffer.write(" case 'Users':\n"); - final className = findClass(fileOfModule[module]!); - firstClass ??= className; - buffer.write(" rc = $className();\n"); - buffer.write(" break;\n"); - } - firstClass ??= 'MissingFirstClass'; - buffer.write(''' default: - stdout.write('+++ unknown module: \$name. Using "$firstClass".'); - rc = $firstClass(); - break; - } - return rc; -} -/// Returns the module names as string list. -List moduleNames(){ - return [ -'''); - for (var module in modules) { - buffer.write(" '$module',\n"); - } - buffer.write(''' ]; -} +void usage(Generator generator) { + generator.out('''Usage: meta_tool MODE MODULE + Generate code specified by MODE for the module MODULE. +MODE: + all-modules + Prints the module names. + print-modules + Prints the file lib/meta/modules.dart. + print-table MODULE + Prints the SQL table definition of MODULE. + print-data MODULE + Creates the data storage class of MODULE + print-sql MODULE + Creates the SQL statements (for the rest_server) of MODULE + update-modules + Generates all module files depending on the meta data. + update-sql + Generates all Sql statement files depending on the meta data. +MODULE: + The name of the module, e.g. "Users" +Example: +meta_tool print-table Users '''); - return buffer.toString(); } - -/// Finds the class name of a module meta class given by the file [node]. -String findClass(String node) { - final contents = File('lib/meta/$node').readAsStringSync(); - RegExpMatch? match; - String rc; - if ((match = RegExp(r'class\s+(\w+)\s').firstMatch(contents)) != null) { - rc = match!.group(1)!; - } else { - out('+++ missing class in $node'); - rc = filenameToClass(node.replaceFirst('.dart', '')); - } - return rc; -} - -/// Replaces print(). -void out(String line) { - stdout.write(line + '\n'); -} - -void writeFile(String filename, String contents){ - out('creating $filename ...'); - final file = File(filename); - file.writeAsStringSync(contents); -} -void updateModules(){ - writeFile('lib/meta/modules.dart', createModules()); - final modules = moduleNames(); - for (var name in modules){ - ModuleMetaData module = moduleByName(name); - String filename = classToFilename(module.moduleNameSingular) + '_data.dart'; - final directory = name.toLowerCase(); - writeFile('lib/page/$directory/$filename', module.createModuleData()); - } -} \ No newline at end of file diff --git a/bin/yaml_merger.dart b/bin/yaml_merger.dart new file mode 100644 index 0000000..95f56e8 --- /dev/null +++ b/bin/yaml_merger.dart @@ -0,0 +1,163 @@ +import 'dart:io'; + +import 'package:path/path.dart'; +import 'package:dart_bones/dart_bones.dart'; + +/// Implements a merge tool for yaml files. +/// @see merge() for description of merge. +class Merger { + RegExp regExpKey = RegExp(r'^([a-zA-Z]\w+):\s*$'); + RegExp regExpKeyValue = RegExp(r'^ '); + final BaseLogger logger; + final precedenceKeys = {}; + Merger(this.logger); + + /// Parses the keys of a given [file] into [this.precedenceKeys]. + void parseKeys(File file){ + precedenceKeys.clear(); + final lines = file.readAsLinesSync(); + var state = 'undef'; + var lastKey = ''; + final keyValue = []; + for (var line in lines){ + final match = regExpKey.firstMatch(line); + if (match != null){ + if (keyValue.isNotEmpty){ + precedenceKeys[lastKey] = keyValue.join('\n'); + keyValue.clear(); + } + state = 'inKey'; + lastKey = match.group(1)!; + } else { + if (regExpKeyValue.firstMatch(line) != null){ + if (state == 'inKey'){ + keyValue.add(line); + } + } + } + } + if (keyValue.isNotEmpty){ + precedenceKeys[lastKey] = keyValue.join('\n'); + } + } + /// Merges the [fileSource] and [filePreference] into the [target] file. + void mergeOneFile(File fileSource, File filePreference, String target) { + parseKeys(filePreference); + final output = []; + final foundKeys = {}; + final lines = fileSource.readAsLinesSync(); + var state = 'undef'; + var lastKey = ''; + final keyValue = []; + for (var line in lines){ + final match = regExpKey.firstMatch(line); + if (match != null) { + if (keyValue.isNotEmpty) { + output.add(lastKey + ':'); + output.add(precedenceKeys.containsKey(lastKey) + ? precedenceKeys[lastKey]! + : keyValue.join('\n')); + } + keyValue.clear(); + state = 'inKey'; + lastKey = match.group(1)!; + if (foundKeys.contains(lastKey)){ + logger.error('key $lastKey found multiple times'); + } + foundKeys.add(lastKey); + } else { + if (regExpKeyValue.firstMatch(line) != null){ + if (state == 'inKey'){ + keyValue.add(line); + } + } else { + output.add(line); + } + } + } + if (keyValue.isNotEmpty){ + output.add(lastKey + ':'); + if (precedenceKeys.containsKey(lastKey)) { + output.add(precedenceKeys[lastKey]!); + } else { + output.add(keyValue.join('\n')); + } + } + for (var key in precedenceKeys.keys){ + if (! foundKeys.contains(key)){ + output.add(key + ':'); + output.add(precedenceKeys[key]!); + } + } + File(target).writeAsStringSync(output.join('\n') + '\n'); + } + /// Merges or copies files from two source directories into a target directory. + /// [pathSource] contains files with lower priorities. + /// [pathPreferences] contains files with higher priorities. + /// [pathTarget] is the target directory. + /// Only files with the same node will be merged. + /// Merging: the two source files contains keys. If a key is part of both + /// files the key content of of the higher priority file is taken. + void merge(String pathSource, String pathPreference, String pathTarget) { + final dirSource = Directory(pathSource); + final dirPreference = Directory(pathPreference); + final dirTarget = Directory(pathTarget); + if (!dirSource.existsSync()) { + logger.error('source not a directory: $pathSource'); + } else if (!dirPreference.existsSync()) { + logger.error('source not a directory: $pathPreference'); + } else if (!dirTarget.existsSync()) { + logger.error('source not a directory: $pathTarget'); + } else { + /// Copy/merge the files from dirSource: + for (var file in dirSource.listSync()) { + final node = basename(file.path); + if (! node.endsWith('.yaml')){ + continue; + } + final fnPreference = join(pathPreference, node); + final fnTarget = join(pathTarget, node); + final filePreference = File(fnPreference); + if (!(file is File)) { + logger.error('not a file: ${file.path} ignoring...'); + } else if (filePreference.existsSync()) { + mergeOneFile(file, filePreference, fnTarget); + } else { + logger.log('copying ${file.path}', LEVEL_DETAIL); + file.copy(fnTarget); + } + } + + /// Copy the "single" files from preferences: + for (var file in dirPreference.listSync()) { + final node = basename(file.path); + if (! node.endsWith('.yaml')){ + continue; + } + final fnSource = join(pathSource, node); + final fnTarget = join(pathTarget, node); + File fileSource = File(fnSource); + if (file is File && !fileSource.existsSync()) { + logger.log('copying ${file.path}', LEVEL_DETAIL); + file.copy(fnTarget); + } + } + } + } +} +void main(List args){ + final logger = MemoryLogger(LEVEL_DETAIL); + if (args.length != 3){ + print('''Usage: yaml_merger DIR_SOURCE DIR_PREFERENCE DIR_OUTPUT + Merges the yaml files from DIR_SOURCE (with lower priority) and DIR_PREFERENCE + (with higher priority) into files in DIR_OUTPUT. +Version: 0.1.0 +Example: +yaml_merger data/sql data/sql/precedence /tmp/sql +'''); + print('+++ wrong count of arguments: ${args.length} instead of 3'); + } else { + final merger = Merger(logger); + merger.merge(args[0], args[1], args[2]); + } +} \ No newline at end of file diff --git a/lib/base/i18n.dart b/lib/base/i18n.dart new file mode 100644 index 0000000..0763460 --- /dev/null +++ b/lib/base/i18n.dart @@ -0,0 +1,55 @@ +class I18N { + static I18N? instance; + Map> modules = {}; + String locale = 'de'; + factory I18N() { + return instance!; + } + I18N.internal(String directory) { + instance = this; + } + String module(String name) { + return name; + } + + /// Translates the [key] into the local language using the namespace [module]. + /// Returns the translation or (if not found) the key. + String tr(String key, [String module = '!global']) { + String? rc; + final mapModule = modules[module]; + if (mapModule != null) { + rc = mapModule[key]; + if (rc == null) { + if (module == '!global') { + rc = key; + } else { + rc = tr(key, '!global'); + } + } + } + return rc!; + } + + /// Translates the [key] into the local language using the namespace [module]. + /// [key] contains placeholders "{0}", "{1}"... which will be replaced by + /// the matching entry in the array [args]. + /// Returns the translation or (if not found) the key. + String trArgs(String key, List args, [String module = '!global']) { + String rc = tr(key, module); + for (var ix = 0; ix < args.length; ix++) { + rc = rc.replaceAll('{$ix}', args[ix].toString()); + } + return rc; + } + + /// Translates the [keySingular] or [keyPlural] into the local language + /// using the namespace [module]. + /// If [count] is 1 the translation of [keySingular] is returned. + /// Otherwise the translation of [keyPlural] is returned. + /// Returns the translation or (if not found) the key. + String trPlural(String keySingular, String keyPlural, int count, + [String module = '!global']) { + String rc = tr(count == 1 ? keySingular : keyPlural, module); + return rc; + } +} diff --git a/lib/meta/module_meta_data.dart b/lib/meta/module_meta_data.dart index 46b9f63..7d31612 100644 --- a/lib/meta/module_meta_data.dart +++ b/lib/meta/module_meta_data.dart @@ -1,10 +1,49 @@ -import 'package:path/path.dart'; import '../base/defines.dart'; +/// Describes a button of a page. +class ButtonMetaData extends WidgetMetaData { + ButtonMetaData(String name) : super(name, WidgetType.button) { + //@ToDo + } +} +/// Describes a field related to a database column. +class DbFieldMetaData extends WidgetMetaData { + PropertyMetaData? reference; + DbFieldMetaData(String name, String reference) + : super(name, WidgetType.dbField) { + //@ToDo + } +} + +enum DisplayType { + line, + multiLine, + combobox, +} + +/// Describes a field of the page. +class FieldMetaData extends WidgetMetaData { + DataType dataType; + DisplayType displayType; + final String options; + FieldMetaData(String name, this.options, + {this.dataType = DataType.string, this.displayType = DisplayType.line}) + : super(name, WidgetType.field); +} + class MetaException extends FormatException {} /// Stores the meta data of a module. class ModuleMetaData { + static final metaColumns = [ + 'created', + 'createdBy', + 'changed', + 'changedBy', + 'deleted', + 'deletedBy' + ]; + /// The module name, e.g. users final String moduleName; @@ -35,9 +74,7 @@ class ModuleMetaData { item.module = this; properties[item.name] = item; if (item.columnName.isEmpty) { - final prefix = shortModifiedLabel && - ['created', 'createdBy', 'changed', 'changedBy'] - .contains(item.name) + final prefix = shortModifiedLabel && metaColumns.contains(item.name) ? '' : columnPrefix + '_'; item.columnName = prefix + item.name.toLowerCase(); @@ -45,70 +82,7 @@ class ModuleMetaData { } } - /// Returns a DDL statement creating the database table. - String createDbTable() { - final buffer = StringBuffer(); - buffer.write('-- DO NOT CHANGE. This file is created by the meta_tool\n'); - buffer.write('CREATE TABLE $tableName (\n'); - String? primary; - for (var item in list) { - if (item.options.contains('primary')) { - primary = item.columnName; - } - buffer.write(' ${item.columnName}'); - buffer.write(' ' + mySqlType(item.dataType, item.size)); - buffer.write(dbOptions(item) + ',\n'); - } - buffer.write(' PRIMARY KEY ($primary)\n'); - buffer.write(');\n'); - return buffer.toString(); - } - - /// Returns a Dart class definition of the module. - String createModuleData() { - final buffer = StringBuffer(); - buffer.write('// DO NOT CHANGE. This file is created by the meta_tool\n'); - buffer.write("import '../../base/defines.dart';\n"); - buffer.write("import '../../base/helper.dart';\n"); - buffer.write('class ${moduleNameSingular}Data{\n'); - for (var item in list) { - buffer.write(' ' + dartType(item.dataType) + '? ${item.name};\n'); - } - buffer.write(' ${moduleNameSingular}Data({'); - String separator = ''; - for (var item in list) { - buffer.write('$separator this.${item.name}'); - separator = ','; - } - buffer.write('});\n'); - buffer.write(' ${moduleNameSingular}Data.fromYaml(YamlMap data) {\n'); - for (var item in list) { - final name = item.columnName; - final type = item.dataType.toString(); - //id = data.containsKey('id') ? int.tryParse(data['id']) : null; - buffer.write( - " ${item.name} = data.containsKey('$name') ? valueOf($type, data['$name']) : null;\n"); - } - buffer.write(' }\n'); - buffer.write(' static DataType? dataTypeOf(String name) {\n'); - buffer.write(' DataType? rc;\n'); - buffer.write(' switch(name){\n'); - for (var item in list) { - buffer.write(" case '${item.name}':\n"); - buffer.write(' rc = ${item.dataType};\n'); - buffer.write(' break;\n'); - } - buffer.write(''' default: - break; - } - return rc; - } -'''); - buffer.write('}\n'); - - return buffer.toString(); - } - + /// Returns a given [dataType] as string. String dartType(DataType dataType) { String rc; switch (dataType) { @@ -200,13 +174,36 @@ class ModuleMetaData { } return rc; } + + /// Returns the properties that are not in [metaColumns]. + /// : if the name of the property is [included] the property is always part of + /// the result. + Iterable standardColumns([String included = '']) { + final rc = list.where( + (item) => item.name == included || !metaColumns.contains(item.name)); + return rc; + } } -/// Stores the meta data of a module property. +class PageMetaData { + final String name; + final PageType pageType; + List? fields; + PageMetaData( + this.name, + this.pageType, + this.fields, + ); +} + +enum PageType { create, custom, delete, edit, list } + +/// Stores the meta data of a module property stored as column in a database. class PropertyMetaData { /// Name of the property (Dart name). final String name; ModuleMetaData module = ModuleMetaData('', []); + final String label; /// A colon delimited list of options, e.g. ':notnull:unique:'. final String options; @@ -221,29 +218,17 @@ class PropertyMetaData { /// The foreign key if dataType is DataType.reference, e.g. 'users.user_id' String? reference; - PropertyMetaData(this.name, this.dataType, this.options, this.widgetType, + PropertyMetaData( + this.name, this.dataType, this.options, this.widgetType, this.label, {this.columnName = '', this.size = 0, this.reference}); } -String filenameToClass(String filename) { - final parts = basenameWithoutExtension(filename).split('_'); - String rc = ''; - for (var part in parts) { - if (part.isNotEmpty) { - rc += part[0].toUpperCase() + part.substring(1); - } - } - return rc; +/// Describes a widget used in the page. +/// Base class of all other widgets. +class WidgetMetaData { + final WidgetType widgetType; + final String name; + WidgetMetaData(this.name, this.widgetType); } -String classToFilename(String className) { - String upperCase = className.toUpperCase(); - String rc = ''; - for (var ix = 0; ix < className.length; ix++) { - if (className[ix] == upperCase[ix] && ix > 0) { - rc += '_'; - } - rc = className[ix]; - } - return rc.toLowerCase(); -} +enum WidgetType { button, field, dbField } diff --git a/lib/meta/modules.dart b/lib/meta/modules.dart index e8e8425..bedf989 100644 --- a/lib/meta/modules.dart +++ b/lib/meta/modules.dart @@ -3,15 +3,14 @@ import 'dart:io'; import 'module_meta_data.dart'; import 'users_meta.dart'; /// Returns the meta data of the module given by [name]. -ModuleMetaData moduleByName(String name) { - ModuleMetaData rc; +/// Returns null if not found. +ModuleMetaData? moduleByName(String name) { + ModuleMetaData? rc; switch (name) { case 'Users': rc = UserMeta(); break; default: - stdout.write('+++ unknown module: $name. Using "UserMeta".'); - rc = UserMeta(); break; } return rc; @@ -22,3 +21,4 @@ List moduleNames(){ 'Users', ]; } + diff --git a/lib/meta/roles_meta.dart b/lib/meta/roles_meta.dart new file mode 100644 index 0000000..ef454ba --- /dev/null +++ b/lib/meta/roles_meta.dart @@ -0,0 +1,34 @@ +import '../base/defines.dart'; +import 'module_meta_data.dart'; +import '../base/i18n.dart'; + +final i18N = I18N(); +final M = i18N.module("Roles"); + +class RoleMeta extends ModuleMetaData { + static RoleMeta instance = RoleMeta.internal(); + RoleMeta.internal() + : super( + 'Roles', + [ + PropertyMetaData('id', DataType.reference, ':primary:', 'combo', + i18N.tr('Id', M)), + PropertyMetaData( + 'name', DataType.string, ':notnull:', '', i18N.tr('Name', M), + size: 64), + PropertyMetaData('created', DataType.datetime, ':hidden:', '', + i18N.tr('Created', M)), + PropertyMetaData('createdBy', DataType.string, ':hidden:', '', + i18N.tr('Created by', M), + size: 32), + PropertyMetaData('changed', DataType.datetime, ':hidden:', '', + i18N.tr('Changed', M)), + PropertyMetaData('changedBy', DataType.string, ':hidden:', '', + i18N.tr('Changed by', M), + size: 32), + ], + ); + factory RoleMeta() { + return instance; + } +} diff --git a/lib/meta/users_meta.dart b/lib/meta/users_meta.dart index 8a6ec10..2d15ff9 100644 --- a/lib/meta/users_meta.dart +++ b/lib/meta/users_meta.dart @@ -1,5 +1,8 @@ import '../base/defines.dart'; import 'module_meta_data.dart'; +import '../base/i18n.dart'; +final i18N = I18N(); +final M = i18N.module("Users"); class UserMeta extends ModuleMetaData { static UserMeta instance = UserMeta.internal(); @@ -7,22 +10,30 @@ class UserMeta extends ModuleMetaData { : super( 'Users', [ - PropertyMetaData('id', DataType.reference, ':primary:', 'combo'), + PropertyMetaData('id', DataType.reference, ':primary:', 'combo', + i18N.tr('Id', M)), PropertyMetaData('name', DataType.string, ':notnull:', '', + i18N.tr('Name', M), size: 64), - PropertyMetaData( - 'displayName', DataType.string, ':unique:notnull:', '', + PropertyMetaData('displayName', DataType.string, ':unique:notnull:', + '', i18N.tr('Display name', M), size: 32), PropertyMetaData('email', DataType.string, ':unique:notnull:', '', + i18N.tr('EMail', M), size: 255), - PropertyMetaData('role', DataType.reference, ':notnull:', ''), - PropertyMetaData('created', DataType.datetime, '', ''), - PropertyMetaData('createdBy', DataType.string, '', '', size: 32), - PropertyMetaData('changed', DataType.datetime, '', ''), - PropertyMetaData('changedBy', DataType.string, '', '', size: 32), + PropertyMetaData('role', DataType.reference, ':notnull:', '', + i18N.tr('Role', M), reference: 'roles.role_id'), + PropertyMetaData('created', DataType.datetime, ':hidden:', '', + i18N.tr('Created', M)), + PropertyMetaData('createdBy', DataType.string, ':hidden:', '', + i18N.tr('Created by', M), + size: 32), + PropertyMetaData('changed', DataType.datetime, ':hidden:', '', + i18N.tr('Changed', M)), + PropertyMetaData('changedBy', DataType.string, ':hidden:', '', + i18N.tr('Changed by', M), + size: 32), ], - tableName: 'loginusers', - shortModifiedLabel: true, ); factory UserMeta() { return instance; diff --git a/lib/page/r_data.dart b/lib/page/r_data.dart deleted file mode 100644 index f35600e..0000000 --- a/lib/page/r_data.dart +++ /dev/null @@ -1,13 +0,0 @@ -// DO NOT CHANGE. This file is created by the meta_tool -class User{ - int? id; - String? name; - String? displayName; - String? email; - int? role; - DateTime? created; - String? createdBy; - DateTime? changed; - String? changedBy; - User({ this.id, this.name, this.displayName, this.email, this.role, this.created, this.createdBy, this.changed, this.changedBy}); -} diff --git a/lib/page/users/user_list.dart b/lib/page/users/user_list.dart index 316e529..1675056 100644 --- a/lib/page/users/user_list.dart +++ b/lib/page/users/user_list.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../base/defines.dart'; import 'user_state.dart'; -import 'user_data.dart'; import 'package:equatable/equatable.dart'; class User extends StatefulWidget { @@ -15,7 +14,7 @@ class _UserState extends State { @override void initState() { - context.read().add(UserEvent(EventSource.undef, EventCardinality.undef)); + //context.read().add(UserEvent(EventSource.undef, EventCardinality.undef)); super.initState(); } @@ -31,9 +30,9 @@ class _UserState extends State { builder: (context, UserState state) { return Column( children: [ - ResultDisplay( - text: _getDisplayText(state.calculationModel), - ), + //ResultDisplay( + // text: _getDisplayText(state.calculationModel), + //), Row( children: [ _getButton(text: '7', onTap: () => numberPressed(7)), @@ -86,8 +85,8 @@ class _UserState extends State { ], ), Spacer(), - UserHistoryContainer( - calculations: state.history.reversed.toList()) + //UserHistoryContainer( + // calculations: state.history.reversed.toList()) ], ); }, @@ -99,6 +98,7 @@ class _UserState extends State { required void Function() onTap, Color backgroundColor = Colors.white, Color textColor = Colors.black}) { + /* return CalculatorButton( label: text, onTap: onTap, @@ -107,24 +107,26 @@ class _UserState extends State { backgroundColor: backgroundColor, labelColor: textColor, ); + */ + return SizedBox(width: 10); } numberPressed(int number) { - context.read().add(NumberPressed(number: number)); + //context.read().add(NumberPressed(number: number)); } operatorPressed(String operator) { - context.read().add(OperatorPressed(operator: operator)); + //context.read().add(OperatorPressed(operator: operator)); } calculateResult() { - context.read().add(CalculateResult()); + //context.read().add(CalculateResult()); } clear() { - context.read().add(ClearUser()); + //context.read().add(ClearUser()); } - + /* String _getDisplayText(UserModel model) { if (model.result != null) { return '${model.result}'; @@ -144,4 +146,5 @@ class _UserState extends State { return "${model.result ?? 0}"; } + */ } diff --git a/rest_server/CR b/rest_server/CR new file mode 100755 index 0000000..8420bde --- /dev/null +++ b/rest_server/CR @@ -0,0 +1,6 @@ +#! /bin/bash +APP=rest_server +TRG=tools/$APP +pub get +dart compile exe bin/$APP.dart -o $TRG +ls -ld $TRG diff --git a/rest_server/data/sql/users.sql.yaml b/rest_server/data/sql/users.sql.yaml index e87e0b8..d8848ab 100644 --- a/rest_server/data/sql/users.sql.yaml +++ b/rest_server/data/sql/users.sql.yaml @@ -9,6 +9,10 @@ byId: type: record parameters: [ ":id" ] sql: "select * from loginusers where user_id=:id;" +delete: + type: delete + parameters: [ ":id" ] + sql: "delete * from loginusers where user_id=:id;" update: type: update parameters: [":id", ":name", ":displayname", ":email", ":changedby"] diff --git a/rest_server/lib/rest_server.dart b/rest_server/lib/rest_server.dart index 44b95ad..cacd227 100644 --- a/rest_server/lib/rest_server.dart +++ b/rest_server/lib/rest_server.dart @@ -176,8 +176,8 @@ class RestServer { service: address: 0.0.0.0 port: 58021 - dataDirectory: /var/cache/rest_server/data - sqlDirectory: /etc/rest_server/sql.d + dataDirectory: /var/cache/exhibition/data + sqlDirectory: /usr/share/exhibition/rest_server/data/sql threads: 2 watchDogPause: 60 # logFile: /var/log/local/exhibition.log @@ -226,7 +226,7 @@ clientSessionTimeout: 900 starter: executable, user: appName, group: appName, - description: 'A REST server serving the POLLECTOR project', + description: 'A REST server serving the Exhibition project', ); } } @@ -243,7 +243,7 @@ clientSessionTimeout: 900 static String version() => _version; } -/// Datenklasse für eine Isolate-Instanz. +/// Implements an isolate instance. class ServiceWorker { /// for unittests: static int maxRequests = 0; @@ -490,14 +490,13 @@ class ServiceWorker { logger.log('thread $threadId stopped'); } - /// Prüft, ob ein Isolate auf eine Watchdog-Anfrage antwortet. - /// Hintergrund: Wenn eine DB-Verbindung abbricht, tritt beim nächsten - /// DB-Zugriff eine SocketException auf, die nicht abgefangen werden kann. - /// Der Thread (Isolate) ist dann tot. - /// Daher wird vom Observer-Thread regelmäßig eine Anfrage verschickt. - /// Kommt keine Antwort, ist kein Empfangsthread mehr erreichbar, das Programm - /// wird mittels exit() abgebrochen, damit es von SystemD erneut gestartet - /// wird. + /// Checks whether an isolate instance is reponding to a request. + /// Background: If one DB connection breaks, a SocketException occurs + /// the next time the DB is accessed, which cannot be intercepted. + // The thread (Isolate) is then dead. + // Therefore, the observer thread regularly sends a request. + // f there is no response, the receiving thread can no longer be reached, + // the program is aborted with exit () so that it can be restarted by SystemD. Future observe() async { final duration = Duration( seconds: @@ -518,7 +517,7 @@ class ServiceWorker { } } - /// Ermittelt die Anfragenklasse ("what"), die Teil der URI ist. + /// Determines the request class ("what") that is part of the URI. void prepareResource({bool withId = true}) { if (!(currentRequest?.requestedUri.path.contains('watchdog') ?? false)) { logger.log(currentRequest!.requestedUri.path, LEVEL_FINE); @@ -612,11 +611,11 @@ class ServiceWorker { return buffer; } - /// Fordert per POST eine Aktion an. - /// [what] legt die Aktion fest. - /// [body] ist der Text, der mit der Anfrage mitgeliefert wird. - /// [headers] sind HTTP-Headerkomponenten. - /// Liefert die Antwort der Anfrage. + /// Requests an action via POST. + /// [what] defines the action. + /// [body] is the text that is supplied with the request. + /// [headers] are HTTP header components. + /// Returns the answer to the request. Future runRequest(String what, {String? body, Map? headers}) async { final port = configuration.asInt('port', section: 'service') ?? 58011; @@ -694,7 +693,7 @@ class ServiceWorker { return rc; } - /// Speichert [content] im Verzeichnis [directory] unter dem Namen [node]. + /// Stores [content] in the directory [directory] under the name [node]. Future storeFile(String directory, String node, String content) async { if (directory.isEmpty) { logger.error('storeFile: missing data directory'); diff --git a/rest_server/tools/project.inc b/rest_server/tools/project.inc new file mode 100644 index 0000000..3504b64 --- /dev/null +++ b/rest_server/tools/project.inc @@ -0,0 +1 @@ +PROJECT=exhibition diff --git a/test/i18n_text_parser_test.dart b/test/i18n_text_parser_test.dart new file mode 100644 index 0000000..3e854af --- /dev/null +++ b/test/i18n_text_parser_test.dart @@ -0,0 +1,85 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart'; + +import "../bin/i18n_text_parser.dart"; + +void main() { + final logger = MemoryLogger(LEVEL_FINE); + FileSync.initialize(logger); + final fileSync = FileSync(); + final baseDir = init(logger); + final targetDir = join(baseDir, nodeTarget); + test('parse', () { + final parser = I18nTextParser(logger); + parser.scanDirectory(baseDir); + parser.writePot(targetDir); + expect(parser.modules, isNotEmpty); + expect(parser.modules.containsKey('Example'), isTrue); + expect(parser.modules.containsKey('!global'), isTrue); + expect(parser.modules.containsKey('Sample'), isTrue); + expect(parser.modules['Example']!.containsKey('introduction'), isTrue); + expect(parser.modules['Sample']!.containsKey('info'), isTrue); + var fn = join(targetDir, 'Example.pot'); + var content = fileSync.fileAsString(fn); + expect(content, '''# Texts of module Example, parsed by i18n_text_parser + +#: /tmp/unittest/i18n/lib/base/simple.dart:5 +msgid "description" +msgstr "" + +#: /tmp/unittest/i18n/lib/base/simple.dart:5 +msgid "introduction" +msgstr "" +'''); + fn = join(targetDir, 'Sample.pot'); + content = fileSync.fileAsString(fn); + expect(content, '''# Texts of module Sample, parsed by i18n_text_parser + +#: /tmp/unittest/i18n/lib/base/args.dart:5 +msgid "info" +msgstr "" +'''); + fn = join(targetDir, '!global.pot'); + content = fileSync.fileAsString(fn); + expect(content, '''# Texts of module !global, parsed by i18n_text_parser + +#: /tmp/unittest/i18n/lib/base/simple.dart:10 +#: /tmp/unittest/i18n/lib/base/args.dart:9 +msgid "Status line" +msgstr "" +'''); + }); +} + +const nodeTarget = 'output'; + +String init(MemoryLogger logger) { + final fileSync = FileSync(); + final baseDir = fileSync.tempDirectory('i18n', subDirs: 'unittest'); + final subDir = fileSync.tempDirectory('base', subDirs: 'unittest/i18n/lib'); + fileSync.toFile(join(subDir, 'simple.dart'), r'''import 'dart:io'; +final i18N = I18N(); +final M = i18N.module('Example'); +String header(){ + return i18N.tr('introduction', M) + " " + i18N.tr( + "description", + M); +} +String footer(){ + return I18N().tr("Status line"); +} +'''); + fileSync.toFile(join(subDir, 'args.dart'), r'''import 'dart:io'; +final i18N = I18N(); +final M = i18N.module("Sample"); +String header(String user, String role){ + return i18N.trArgs('Name: {0} Role: {1}', [user, role], M) + " " + i18N.tr( + r'info', M); +} +String footer(){ + return I18N().tr('Status line'); +} +'''); + return baseDir; +} diff --git a/test/yaml_merger_test.dart b/test/yaml_merger_test.dart new file mode 100644 index 0000000..f214344 --- /dev/null +++ b/test/yaml_merger_test.dart @@ -0,0 +1,117 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart'; + +import "../bin/yaml_merger.dart"; + +const nodePrecedence = 'precedence'; +const nodeTarget = 'output'; +void main() { + final logger = MemoryLogger(LEVEL_FINE); + FileSync.initialize(logger); + final fileSync = FileSync(); + final baseDir = init(logger); + final targetDir = join(baseDir, nodeTarget); + test('merge', () { + final merger = Merger(logger); + merger.merge( + baseDir, join(baseDir, nodePrecedence), join(baseDir, nodeTarget)); + var content = fileSync.fileAsString(join(targetDir, 'users.sql.yaml')); + expect(content, r'''--- +# SQL statements of the module "Users": +module: Users +list: + type: list + parameters: [":name", ":role"] + sql: "select * from loginusers where user_name like :filterName; + and (:role is null or user_role=:role);" +byId: + type: record + parameters: [ ":id" ] + sql: "select * from loginusers where user_id=:id;" +insert: + type: insert + parameters: [":name", ":displayname", ":email", ":createdby"] + sql: "INSERT INTO loginusers(user_name, user_displayname, user_email, user_changedby) + VALUES(:name, :displayname, :email, NOW(), :createdby);" +byName: + type: record + parameters: [":name"] + sql: "select * from loginusers where user_name=:name;" +'''); + content = fileSync.fileAsString(join(targetDir, 'roles.sql.yaml')); + expect(content, r'''--- +# SQL statements of the module "Roles": +module: Roles +list: + type: list + parameters: [":name", ":role"] + sql: "select * from loginusers where user_name like :filterName; + and (:role is null or user_role=:role);" +'''); + content = fileSync.fileAsString(join(targetDir, 'misc.sql.yaml')); + expect(content, r'''--- +# SQL statements of the module "Misc": +module: Misc +tables: + type: list + parameters: [] + sql: "show tables;" +'''); + }); +} + +String init(MemoryLogger logger) { + final fileSync = FileSync(); + final baseDir = fileSync.tempDirectory('yaml_merger', subDirs: 'unittest'); + final precedenceDir = join(baseDir, nodePrecedence); + fileSync.ensureDirectory(precedenceDir); + fileSync.ensureDirectory(join(baseDir, nodeTarget)); + fileSync.toFile(join(baseDir, 'users.sql.yaml'), r'''--- +# SQL statements of the module "Users": +module: Users +list: + type: list + parameters: [] + sql: "select * from loginusers;" +byId: + type: record + parameters: [ ":id" ] + sql: "select * from loginusers where user_id=:id;" +insert: + type: insert + parameters: [":name", ":displayname", ":email", ":createdby"] + sql: "INSERT INTO loginusers(user_name, user_displayname, user_email, user_changedby) + VALUES(:name, :displayname, :email, NOW(), :createdby);" +'''); + fileSync.toFile(join(baseDir, 'roles.sql.yaml'), r'''--- +# SQL statements of the module "Roles": +module: Roles +list: + type: list + parameters: [] + sql: select * from loginusers; +'''); + fileSync.toFile(join(precedenceDir, 'users.sql.yaml'), r'''--- +# Overriding SQL statements of the module "Users": +module: Users +list: + type: list + parameters: [":name", ":role"] + sql: "select * from loginusers where user_name like :filterName; + and (:role is null or user_role=:role);" +byName: + type: record + parameters: [":name"] + sql: "select * from loginusers where user_name=:name;" +'''); + fileSync.toFile(join(precedenceDir, 'misc.sql.yaml'), r'''--- +# SQL statements of the module "Misc": +module: Misc +tables: + type: list + parameters: [] + sql: "show tables;" +'''); + return baseDir; +} diff --git a/tools/CompileMerge b/tools/CompileMerge new file mode 100755 index 0000000..e3e62dd --- /dev/null +++ b/tools/CompileMerge @@ -0,0 +1,5 @@ +#! /bin/bash +APP=yaml_merger +TRG=tools/$APP +dart compile exe bin/$APP.dart -o $TRG +ls -ld $TRG diff --git a/tools/InitProject b/tools/InitProject new file mode 100755 index 0000000..10bd3e6 --- /dev/null +++ b/tools/InitProject @@ -0,0 +1,92 @@ +#! /bin/bash +APP=$1 +APP2=$(echo ${APP:0:1} | tr a-z A-Z)${APP:1} +function Usage(){ + echo "Usage: InitProject PROJECT" + echo " Prepares the project PROJECT for using the framework exhibition" + echo "PROJECT: the name of the already existing project (created with 'flutter create')" + echo "Example:" + echo "InitProject myapp" + echo "+++ $*" +} +function MkDirs(){ + for dir in tools rest_server/data; do + mkdir -p $dir + done +} +function EnsureDir(){ + local dir=$1 + if [ ! -d $dir ]; then + echo "creating $dir" + mkdir -p $dir + fi +} +function CopyFiles(){ + local projDir=$(pwd) + cd ../exhibition + local srcDir=$(pwd) + for file in lib/base/*.dart lib/meta/module_meta_data.dart lib/meta/modules.dart \ + rest_server/pubspec.yaml \ + tools/CompileMerge tools/PackRestServer \ + ; do + cd $projDir + local dir=$(dirname $file) + EnsureDir $dir + echo "creating $file" + cp -a ../exhibition/$file $file + cd $srcDir + done + cd $projDir +} +function CopyAndReplace(){ + local projDir=$(pwd) + cd ../exhibition + local srcDir=$(pwd) + for file in lib/setting/*.dart \ + pubspec.yaml \ + lib/persistence/*.dart \ + lib/page/*.dart \ + rest_server/lib/*.dart rest_server/bin/*.dart rest_server/tools/project.inc \ + rest_server/CR \ + lib/main.dart \ + lib/meta/*_meta.dart \ + bin/*.dart \ + ; do + cd $projDir + local dir=$(dirname $file) + EnsureDir $dir + local file2=${file/exhibition/$APP} + echo "copy and replace $file -> $(basename $file2)" + sed <../exhibition/$file >$file2 -e "s/exhibition/$APP/g" -e "s/Exhibition/$APP2/g" + chmod --reference=../exhibition/$file $file2 + cd $srcDir + done + cd $projDir +} +function SymbolicLinks(){ + for links in ReCreateMetaTool:. Meta:. ; do + local src=../exhibition/${links%:*} + local trg=${links#*:} + local trg2=$trg + test $trg = . && trg2=$(basename $src) + echo "trg: $trg2" + if [ ! -L $trg2 ]; then + echo "linking $src -> $trg2" + ln -s $src $trg2 + fi + done +} +function PrepareTools(){ + test -e tools/yaml_merger || tools/CompileMerger +} +if [ -z "$APP" ]; then + Usage "missing PROJECT" +elif [ ! -d ../$APP ]; then + Usage "wrong current directory: Please go into the base folder of the project." +else + PrepareTools + MkDirs + CopyFiles + CopyAndReplace + SymbolicLinks +fi diff --git a/tools/PackRestServer b/tools/PackRestServer new file mode 100755 index 0000000..4fbc077 --- /dev/null +++ b/tools/PackRestServer @@ -0,0 +1,43 @@ +#! /bin/bash +BASE_DIR=/tmp/rest_server +SCRIPT_INSTALL=/tmp/InstallRestServer +TAR_NODE=rest_server.tgz +TAR=/tmp/$TAR_NODE +source rest_server/tools/project.inc + +function DoIt(){ + test -d $BASE_DIR && rm -Rf $BASE_DIR + mkdir -p $BASE_DIR/data/sql + cd rest_server + ./CR + test -d data/sql/precedence || mkdir -p data/sql/precedence + cd .. + cp -av rest_server/tools/rest_server $BASE_DIR + tools/CompileMerge + tools/yaml_merge data/sql data/sql/precedence $BASE_DIR/data/sql + cat <$BASE_DIR/INSTALL.TXT +# ------------ +# Installation: +./InstallRestServer +# or manually: +DIR=\$(pwd) +mkdir -p /usr/share/$PROJECT && cd /usr/share/$PROJECT +tar xzf \DIR/$TAR_NODE +# ./rest_server install +./rest_server install /usr/share/$PROJECT/rest_server $PROJECT +# see /usr/share/$PROJECT and /etc/$PROJECT +EOS + tar -C $BASE_DIR -czf $TAR . + cat <$SCRIPT_INSTALL +#! /bin/bash +DIR=\$(pwd) +mkdir -p /usr/share/$PROJECT && cd /usr/share/$PROJECT +tar xzf \$DIR/rest_server.tgz +./rest_server install /usr/share/$PROJECT/rest_server $PROJECT +EOS + chmod uog+x $SCRIPT_INSTALL + echo "================" + echo "= put $TAR and $SCRIPT_INSTALL to the backend server and..." + cat $BASE_DIR/INSTALL.TXT +} +DoIt