From f61703f6cfd060c1c9e2c2c64032ddb5b4fafd8d Mon Sep 17 00:00:00 2001 From: Hamatoma Date: Thu, 1 Oct 2020 02:09:28 +0200 Subject: [PATCH] Improvements of the model --- .gitignore | 1 + Coverage.sh | 4 + dartdoc_options.yaml | 4 + lib/flutter_bones.dart | 31 ++-- lib/src/helper/settings.dart | 43 ++++- lib/src/helper/string_helper.dart | 46 ++++++ lib/src/helper/validators.dart | 22 +-- lib/src/model/button_model.dart | 48 ++++++ lib/src/model/checkbox_model.dart | 20 +++ lib/src/model/combobox_model.dart | 32 ++++ lib/src/model/empty_line_model.dart | 27 ++++ lib/src/model/field_model.dart | 91 ++++------- lib/src/model/model_base.dart | 136 ++++++++++++---- lib/src/model/model_types.dart | 2 +- lib/src/model/module/user_model.dart | 34 ++-- lib/src/model/module_model.dart | 39 ++++- lib/src/model/page_model.dart | 47 ++++-- lib/src/model/section_model.dart | 114 +++++++++++-- lib/src/model/text_field_model.dart | 31 ++++ lib/src/model/text_model.dart | 29 ++++ lib/src/model/widget_model.dart | 80 ++-------- lib/src/widget/raised_button_bone.dart | 60 +++++++ .../{simpleform.dart => simple_form.dart} | 2 +- lib/src/widget/view.dart | 151 ++++++++++++++++++ test/helpers/settings_test.dart | 23 ++- test/helpers/string_helper_test.dart | 43 +++++ test/helpers/validators_test.dart | 7 +- test/model/model_test.dart | 54 +++++++ 28 files changed, 985 insertions(+), 236 deletions(-) create mode 100755 Coverage.sh create mode 100644 dartdoc_options.yaml create mode 100644 lib/src/helper/string_helper.dart create mode 100644 lib/src/model/button_model.dart create mode 100644 lib/src/model/checkbox_model.dart create mode 100644 lib/src/model/combobox_model.dart create mode 100644 lib/src/model/empty_line_model.dart create mode 100644 lib/src/model/text_field_model.dart create mode 100644 lib/src/model/text_model.dart create mode 100644 lib/src/widget/raised_button_bone.dart rename lib/src/widget/{simpleform.dart => simple_form.dart} (99%) create mode 100644 lib/src/widget/view.dart create mode 100644 test/helpers/string_helper_test.dart create mode 100644 test/model/model_test.dart diff --git a/.gitignore b/.gitignore index 95b551c..77fcaac 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +coverage/ pubspec.lock # wk: @ToDo: nicht sicher diff --git a/Coverage.sh b/Coverage.sh new file mode 100755 index 0000000..fa9044d --- /dev/null +++ b/Coverage.sh @@ -0,0 +1,4 @@ +#! /bin/sh +flutter test --coverage +rm -Rf coverage/html/* +genhtml --show-details --output-directory=coverage/html coverage/lcov.info diff --git a/dartdoc_options.yaml b/dartdoc_options.yaml new file mode 100644 index 0000000..afb6933 --- /dev/null +++ b/dartdoc_options.yaml @@ -0,0 +1,4 @@ +dartdoc: + linkToSource: + root: '.' + uriTemplate: 'https://github.com/dart-lang/dartdoc/blob/v0.34.0/%f%#L%l%' diff --git a/lib/flutter_bones.dart b/lib/flutter_bones.dart index 82ea4cd..8d645ba 100644 --- a/lib/flutter_bones.dart +++ b/lib/flutter_bones.dart @@ -1,14 +1,27 @@ -export 'src/widget/simpleform.dart'; +/// Helper definitions for flutter applications. +/// +/// Allows a model driven design of flutter apps: +/// The design of the screens is done by a yaml file (or a compatible map). export 'src/helper/settings.dart'; +export 'src/helper/string_helper.dart'; export 'src/helper/validators.dart'; -export 'src/page/page_data.dart'; -export 'src/page/login_page.dart'; -export 'src/page/role_page.dart'; -export 'src/page/user_page.dart'; -export 'src/model/model_types.dart'; -export 'src/model/model_base.dart'; -export 'src/model/widget_model.dart'; +export 'src/model/button_model.dart'; +export 'src/model/checkbox_model.dart'; +export 'src/model/combobox_model.dart'; +export 'src/model/empty_line_model.dart'; export 'src/model/field_model.dart'; +export 'src/model/model_base.dart'; +export 'src/model/model_types.dart'; export 'src/model/module_model.dart'; +export 'src/model/page_model.dart'; export 'src/model/section_model.dart'; -export 'src/model/page_model.dart'; \ No newline at end of file +export 'src/model/text_field_model.dart'; +export 'src/model/text_model.dart'; +export 'src/model/widget_model.dart'; +export 'src/page/login_page.dart'; +export 'src/page/page_data.dart'; +export 'src/page/role_page.dart'; +export 'src/page/user_page.dart'; +export 'src/widget/raised_button_bone.dart'; +export 'src/widget/simple_form.dart'; +export 'src/widget/view.dart'; diff --git a/lib/src/helper/settings.dart b/lib/src/helper/settings.dart index 59b75ca..82523e0 100644 --- a/lib/src/helper/settings.dart +++ b/lib/src/helper/settings.dart @@ -1,26 +1,57 @@ +import 'package:dart_bones/dart_bones.dart'; import 'package:flutter/material.dart'; class Settings { static Locale locale = Locale('US', 'en'); + static final mapWidgetData = { + 'form.card.padding': '16.0', + 'form.gap.field_button.height': '16.0', + }; + static Settings _instance; + + final BaseConfiguration widgetConfiguration; + + final BaseLogger logger; + + factory Settings(BaseLogger logger, {BaseConfiguration widgetConfiguration}) { + if (_instance == null) { + _instance = Settings.internal( + widgetConfiguration == null + ? BaseConfiguration(mapWidgetData, logger) + : widgetConfiguration, + logger); + } + return _instance; + } + + Settings.internal(this.widgetConfiguration, this.logger) { + BaseConfiguration widgetConfiguration = + BaseConfiguration(mapWidgetData, logger); + } + /// Sets the locale code. /// [locale] the info about localisation /// Finding [locale] of an app: @see https://github.com/flutter/website/blob/master/examples/internationalization/minimal/lib/main.dart static setLocale(locale) => Settings.locale = locale; - static setLocaleByNames({String country='US', String language='en'}) => Settings.locale = Locale(country, language); + + static setLocaleByNames({String country = 'US', String language = 'en'}) => + Settings.locale = Locale(country, language); + /// Translates a [text] with a given translation [map]. /// Structure of the [map]: { : { : translation } } /// return: [text] if no translation has been found, otherwise: the translation from the [map] - static String translate(String text, Map> map, {Map placeholders}){ + static String translate(String text, Map> map, + {Map placeholders}) { var rc = text; - if (map.containsKey(text) && map[text].containsKey(locale.languageCode)){ - rc = map[text][locale.languageCode]; + if (map.containsKey(text) && map[text].containsKey(locale.languageCode)) { + rc = map[text][locale.languageCode]; } - if (placeholders != null){ + if (placeholders != null) { placeholders.keys.forEach((key) { rc = rc.replaceAll('%{$key}', placeholders[key]); }); } return rc; } -} \ No newline at end of file +} diff --git a/lib/src/helper/string_helper.dart b/lib/src/helper/string_helper.dart new file mode 100644 index 0000000..35ce393 --- /dev/null +++ b/lib/src/helper/string_helper.dart @@ -0,0 +1,46 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class StringHelper { + static final regExpTrue = RegExp(r'^(true|yes)$', caseSensitive: false); + static final regExpFalse = RegExp(r'^(false|no)$', caseSensitive: false); + + /// Converts a string to a given [dataType]. + static dynamic fromString(String value, DataType dataType) { + dynamic rc; + if (dataType == DataType.string) { + rc = value; + } else if (value != null && value.isNotEmpty) { + switch (dataType) { + case DataType.bool: + if (regExpTrue.firstMatch(value) != null) { + rc = true; + } else if (regExpFalse.firstMatch(value) != null) { + rc = false; + } else { + rc = null; + } + break; + case DataType.string: + rc = value; + break; + case DataType.currency: + case DataType.float: + rc = StringUtils.asFloat(value); + break; + case DataType.date: + case DataType.dateTime: + rc = StringUtils.stringToDateTime(value); + break; + case DataType.int: + case DataType.reference: + rc = StringUtils.asInt(value); + break; + default: + rc = ''; + break; + } + } + return rc; + } +} diff --git a/lib/src/helper/validators.dart b/lib/src/helper/validators.dart index fee37c3..7bb567b 100644 --- a/lib/src/helper/validators.dart +++ b/lib/src/helper/validators.dart @@ -3,14 +3,15 @@ import 'package:flutter_bones/flutter_bones.dart'; import 'package:meta/meta.dart'; /// Tests whether [input] is an valid email address. -/// returns null if not empty, otherwise an error message +/// Returns null if it is a valid email address, otherwise an error message. String checkEmail(String input) => Validation.isEmail(input) ? null : _vt('Not an email address: %{0} Example: joe@example.com', {'0': input}); -/// returns null if not empty, otherwise an error message +/// Tests whether [input] is an integer. +/// Returns null for an integer otherwise an error message. String checkInt(String input) => - Validation.isInt(input) ? null : _vt('Not a number: %{0}', {'0': input}); + Validation.isInt(input) ? null : _vt('Not an integer: %{0}', {'0': input}); /// Validates an [input] with many [validators]. String checkMany(String input, List validators) { @@ -25,21 +26,20 @@ String checkMany(String input, List validators) { return rc; } -/// returns null if not empty, otherwise an error message +/// Tests whether [input] is a natural number (a non negative integer). +/// Returns null if it is a nat, otherwise an error message. String checkNat(String input) => Validation.isNat(input) ? null : _vt('Not a not negative number: %{0}', {'0': input}); -// Tests whether [input] is a natural number (>= 0). /// Tests whether [input] is not empty. -/// returns null if not empty, otherwise an error message +/// Returns null if it is not empty, otherwise an error message. String checkNotEmpty(String input) { return input.isEmpty ? _vt('Please fill in') : null; } -// Tests whether [input] is an integer (>= 0). /// Tests whether [input] is an valid phone number. -/// returns null if not empty, otherwise an error message +/// Returns null if it is a phone number, otherwise an error message. String checkPhoneNumber(String input) => Validation.isPhoneNumber(input) ? null : _vt('Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"', @@ -55,15 +55,15 @@ class ValidatorTranslations { 'Please fill in': { 'de': 'Bitte ausfüllen', }, - 'Not a number: %{0}': { - 'de': 'Keine Zahl: %{0}', + 'Not an integer: %{0}': { + 'de': 'Keine ganze Zahl: %{0}', }, 'Not an email address: %{0} Example: joe@example.com': { 'de': 'Keine gültige EMailadresse: %{0} Beispiel: joe@example.com', }, 'Not a phone number: %{0} Examples: "089-123452 "+49-89-12345"': { 'de': - 'Keine gültige Telefonnummer: %{0} Beispiele: "089-123452 "+49-89-12345"', + 'Keine gültige Telefonnummer: %{0} Beispiele: "089-123452 "+49-89-12345"', }, }; } diff --git a/lib/src/model/button_model.dart b/lib/src/model/button_model.dart new file mode 100644 index 0000000..a8eea1a --- /dev/null +++ b/lib/src/model/button_model.dart @@ -0,0 +1,48 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +enum ButtonModelType { + cancel, + custom, + search, + store, +} + +typedef ButtonOnPressed = void Function(String name); + +class ButtonModel extends WidgetModel { + static final regExprOptions = RegExp(r'^(undef)$'); + String text; + String name; + final Map map; + List options; + ButtonModelType buttonModelType; + + ButtonModel(SectionModel section, PageModel page, this.map, BaseLogger logger) + : super(section, page, WidgetModelType.button, logger); + + VoidCallback onPressed; + VoidCallback onLongPressed; + ValueChanged onHighlightChanged; + + /// Returns the name including the names of the parent + @override + String fullName() => '${section.name}.$name'; + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous(map, 'buttonType name options text widgetType'.split(' ')); + name = parseString('name', map, required: true); + text = parseString('text', map); + buttonModelType = + parseEnum('buttonType', map, ButtonModelType.values); + buttonModelType ??= ButtonModelType.custom; + options = parseOptions('options', map); + onPressed = () => logger.error('${fullName()}: missing onPressed'); + checkOptionsByRegExpr(options, regExprOptions); + } + + @override + String widgetName() => name; +} diff --git a/lib/src/model/checkbox_model.dart b/lib/src/model/checkbox_model.dart new file mode 100644 index 0000000..1d88809 --- /dev/null +++ b/lib/src/model/checkbox_model.dart @@ -0,0 +1,20 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class CheckboxModel extends FieldModel { + static final regExprOptions = RegExp(r'^(readonly|disabled|required|undef)$'); + + final Map map; + + CheckboxModel( + SectionModel section, PageModel page, this.map, BaseLogger logger) + : super(section, page, map, WidgetModelType.checkbox, logger); + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous(map, 'name label options toolTip widgetType'.split(' ')); + super.parse(); + options = parseOptions('options', map); + checkOptionsByRegExpr(options, regExprOptions); + } +} diff --git a/lib/src/model/combobox_model.dart b/lib/src/model/combobox_model.dart new file mode 100644 index 0000000..b393d5f --- /dev/null +++ b/lib/src/model/combobox_model.dart @@ -0,0 +1,32 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class ComboboxModel extends FieldModel { + static final regExprOptions = RegExp(r'^(readonly|disabled|required|undef)$'); + List texts; + List values; + FormFieldValidator validator; + FormFieldSetter onSaved; + + final Map map; + + ComboboxModel( + SectionModel section, PageModel page, this.map, BaseLogger logger) + : super(section, page, map, WidgetModelType.combobox, logger); + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous( + map, + 'name label dataType options texts toolTip widgetType values' + .split(' ')); + super.parse(); + texts = parseStringList('texts', map); + values = dataType == DataType.string + ? parseStringList('values', map) + : parseValueList('values', map, dataType); + options = parseOptions('options', map); + checkOptionsByRegExpr(options, regExprOptions); + } +} diff --git a/lib/src/model/empty_line_model.dart b/lib/src/model/empty_line_model.dart new file mode 100644 index 0000000..abee90f --- /dev/null +++ b/lib/src/model/empty_line_model.dart @@ -0,0 +1,27 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class EmptyLineModel extends WidgetModel { + static final regExprOptions = RegExp(r'^(unknown)$'); + List options; + bool isRichText; + final Map map; + + EmptyLineModel( + SectionModel section, PageModel page, this.map, BaseLogger logger) + : super(section, page, WidgetModelType.emptyLine, logger); + + /// Returns the name including the names of the parent + @override + String fullName() => '${section.name}.${widgetName()}'; + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous(map, 'options widgetType'.split(' ')); + options = parseOptions('options', map); + checkOptionsByRegExpr(options, regExprOptions); + } + + @override + String widgetName() => 'emptyLine$id'; +} diff --git a/lib/src/model/field_model.dart b/lib/src/model/field_model.dart index adb0092..0fed9f8 100644 --- a/lib/src/model/field_model.dart +++ b/lib/src/model/field_model.dart @@ -1,85 +1,52 @@ -import 'package:flutter/material.dart'; import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; -import 'package:meta/meta.dart'; -class FieldModel extends ModelBase { - static final regExprOptions = RegExp(r'^(undef|readonly|disabled|password|required}'); - final SectionModel parent; +class FieldModel extends WidgetModel { + static final regExprOptions = + RegExp(r'^(undef|readonly|disabled|password|required)$'); String name; String label; String toolTip; - FieldModelType fieldModelType; DataType dataType; - int maxSize; - int rows; - List options; - FormFieldValidator validator; FormFieldSetter onSaved; + List options; final Map map; + var _value; - FieldModel(this.parent, this.map, BaseLogger logger) : super(logger); + FieldModel(SectionModel section, PageModel page, this.map, + WidgetModelType fieldModelType, BaseLogger logger) + : super(section, page, fieldModelType, logger); + + get value => _value; + + /// Stores the value of the field. + /// Conversion is done if [value] is a string and [this.dataType] is not. + set value(value) { + if (dataType != DataType.string && value.runtimeType == String) { + _value = StringHelper.fromString(value, dataType); + } else { + _value = value; + } + } /// Returns the name including the names of the parent @override - String fullName() => '${parent.fullName()}.$name'; + String fullName() => '${page.name}.$name'; /// Parses the map and stores the data in the instance. void parse() { - checkSuperfluous(map, 'name label toolTip fieldModelType dataType maxSize rows options'.split(' ')); + checkSuperfluous( + map, + 'fieldType dataType label maxSize name options rows toolTip ' + .split(' ')); name = parseString('name', map, required: true); label = parseString('label', map, required: false); toolTip = parseString('toolTip', map, required: false); - fieldModelType = parseEnum( - 'fieldModelType', map, FieldModelType.values); - dataType = parseEnum( - 'dataType', map, DataType.values); - maxSize = parseInt('maxSize', map, required: false); - rows = parseInt('rows', map, required: false); - options = parseOptions('options', map); + dataType = parseEnum('dataType', map, DataType.values); } - /// Tests the validity of the entries in [optionList] - /// Errors will be logged. - bool checkOptions() { - bool rc = false; - options.forEach((element) { - if (regExprOptions.firstMatch(element) == null) { - logger.error('unbekannte Feldoption $element in ${fullName()}'); - rc = true; - } - }); - return rc; - } - /// Returns the widget representing the field. - Widget widget(Key formKey){ - Widget rc; - switch(fieldModelType){ - case FieldModelType.text: - rc = TextFormField(key: formKey, - validator: validator, - decoration: InputDecoration(labelText: label), - onSaved: onSaved, - maxLength: maxSize, - maxLines: rows, - readOnly: options.contains('readonly'), - obscureText: options.contains('password'), - ); - break; - case FieldModelType.checkbox: - rc = Checkbox(key: formKey, - decoration: InputDecoration(labelText: label), - onSaved: onSaved, - maxLength: maxSize, - maxLines: rows, - readOnly: options.contains('readonly'), - obscureText: options.contains('password'), - ); - break; - } - return rc; - } + @override + String widgetName() => name; } - -enum FieldModelType { checkbox, combobox, image, text, } diff --git a/lib/src/model/model_base.dart b/lib/src/model/model_base.dart index ff06252..b379ef2 100644 --- a/lib/src/model/model_base.dart +++ b/lib/src/model/model_base.dart @@ -1,22 +1,37 @@ -import 'package:flutter/material.dart'; import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; import 'package:meta/meta.dart'; /// Base class of all models. abstract class ModelBase { BaseLogger logger; + ModelBase(this.logger); + /// Tests the validity of the entries in [options] comparing with an [regExpr]. + /// Errors will be logged. + bool checkOptionsByRegExpr(List options, RegExp regExpr) { + bool rc = false; + options.forEach((element) { + if (regExpr.firstMatch(element) == null) { + logger.error('unbekannte Feldoption $element in ${fullName()}'); + rc = true; + } + }); + return rc; + } + /// Tests a [map] for superfluous [keys]. /// Keys of the [map] that are not listed in [keys] will be logged as errors. /// Keys that do not have the type String are logged as errors. void checkSuperfluous(Map map, List keys) { map.keys.forEach((element) { if (element.runtimeType != String) { - logger.error('wrong key type ${element.runtimeType} in ${fullName()}'); - } else if (!keys.contains(map[element])) { - logger.error('wrong key ${element.runtimeType} in ${fullName()}'); + logger.error( + 'wrong attribute data type ${element.runtimeType} in ${fullName()}'); + } else if (!keys.contains(element)) { + logger.error('unknown attribute "${element}" in ${fullName()}'); } }); } @@ -27,14 +42,15 @@ abstract class ModelBase { /// Fetches an entry from a map addressed by a [key]. /// An error is logged if [required] is true and the map does not contain the key. T parseEnum(String key, Map map, List values, - {required: bool}) { + {bool required = false}) { T rc; if (!map.containsKey(key)) { if (required) { logger.error('missing $key in ${fullName()}'); } } else { - rc = StringUtils.stringToEnum(map[key], values); + final text = map[key] as String; + rc = StringUtils.stringToEnum(text, values); } return rc; } @@ -42,26 +58,26 @@ abstract class ModelBase { /// Fetches an entry from a map addressed by a [key]. /// An error is logged if [required] is true and the map does not contain the key. int parseInt(String key, Map map, {required: bool}) { - int rc; - if (!map.containsKey(key)) { - if (required) { - logger.error('missing $key in ${fullName()}'); - } else { - if (map[key].runtimeType == int) { - rc = map[key]; - } else if (map[key].runtimeType == String) { - if (Validation.isInt(map[key])) { - rc = StringUtils.asInt(map[key]); - } else { - logger - .error('not an integer: ${map[key]} map[$key] in {fullName()}'); - } - } else { - logger.error('not an integer: ${map[key]} map[$key] in {fullName()}'); - } - } - } - return rc; + int rc; + if (!map.containsKey(key)) { + if (required) { + logger.error('missing $key in ${fullName()}'); + } else { + if (map[key].runtimeType == int) { + rc = map[key]; + } else if (map[key].runtimeType == String) { + if (Validation.isInt(map[key])) { + rc = StringUtils.asInt(map[key]); + } else { + logger + .error('not an integer: ${map[key]} map[$key] in {fullName()}'); + } + } else { + logger.error('not an integer: ${map[key]} map[$key] in {fullName()}'); + } + } + } + return rc; } /// Fetches an entry from a map addressed by a [key]. @@ -76,15 +92,79 @@ abstract class ModelBase { /// Fetches an entry from a map addressed by a [key]. /// An error is logged if [required] is true and the map does not contain the key. - String parseString(String key, Map map, {required: bool}) { + String parseString(String key, Map map, + {bool required = false}) { String rc; if (!map.containsKey(key)) { if (required) { logger.error('missing $key in ${fullName()}'); + } + } else { + rc = map[key] as String; + } + return rc; + } + + /// Fetches an entry from a map addressed by a [key]. + /// This entry is splitted by the delimiter given at index 0. + /// Example: ";a;b" returns ['a', 'b']. + /// An error is logged if [required] is true and the map does not contain the key. + List parseStringList(String key, Map map, + {bool required = false}) { + var rc = []; + if (!map.containsKey(key)) { + if (required) { + logger.error('missing $key in ${fullName()}'); + } + } else { + final value = map[key] as String; + if (value.length < 2 && required) { + logger.error( + 'the string list "$value" do not start with an delimiter, e.g. ";adam;bob"'); } else { - rc = map[key] as String; + rc = value.substring(1).split(value[0]); } } return rc; } + + /// Fetches an entry from a map addressed by a [key]. + /// This entry is splitted by the delimiter given at index 0. + /// Example: ";a;b" returns ['a', 'b']. + /// An error is logged if [required] is true and the map does not contain the key. + List parseValueList(String key, Map map, + DataType dataType, + {bool required = false}) { + if (dataType == null) { + dataType = DataType.string; + } + final strings = parseStringList(key, map, required: required); + final rc = strings.map((item) { + var rc2; + switch (dataType) { + case DataType.int: + rc2 = StringUtils.asInt(item); + break; + default: + logger.error('unknown dataType in parseValueList()'); + rc2 = item; + } + return rc2; + }); + return rc; + } + + /// Tests whether an [object] is a list type. + /// Works for JSArray, List + static bool isList(Object object) { + final name = object.runtimeType.toString(); + return name.contains('List') || name.contains('Array'); + } + + /// Tests whether an [object] is a list type. + /// Works for _InternalLinkedHashMap, _JsonMap, Map + static bool isMap(Object object) { + final name = object.runtimeType.toString(); + return name.contains('Map'); + } } diff --git a/lib/src/model/model_types.dart b/lib/src/model/model_types.dart index d441b53..16667a9 100644 --- a/lib/src/model/model_types.dart +++ b/lib/src/model/model_types.dart @@ -1,4 +1,4 @@ -import 'package:flutter_bones/flutter_bones.dart'; +// on change: adapt StringHelper.fromString() enum DataType { bool, currency, date, dateTime, float, int, reference, string, } diff --git a/lib/src/model/module/user_model.dart b/lib/src/model/module/user_model.dart index 6416fcd..942bb3a 100644 --- a/lib/src/model/module/user_model.dart +++ b/lib/src/model/module/user_model.dart @@ -1,34 +1,42 @@ -import 'package:flutter_bones/flutter_bones.dart'; import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; -class UserModel extends ModuleModel{ - +class UserModel extends ModuleModel { static final model = { "module": "user", "pages": [ { "name": "create", - "pageModeType": "create", + "pageType": "create", "sections": [ { - "sectionModeType": "simpleForm", - "fields": [ + "sectionType": "simpleForm", + "children": [ + { + "widgetType": "text", + "text": "*_Erfassung eines neuen Benutzers:_*", + "options": "rich", + }, + { + "widgetType": "emptyLine", + }, { + "widgetType": "textField", "name": "user", - "fieldModelType": "text", "label": "Benutzer", "options": "required;unique", }, { + "widgetType": "textField", "name": "displayname", "label": "Anzeigename", - "fieldModelType": "text", + "fieldType": "text", "options": "required", }, { + "widgetType": "combobox", "name": "role", "label": "Rolle", - "fieldModelType": "combobox", "dataType": "reference", "options": "required;undef", }, @@ -38,6 +46,10 @@ class UserModel extends ModuleModel{ }, ], }; - UserModel(BaseLogger logger): super(model, logger); -} \ No newline at end of file + UserModel(BaseLogger logger) : super(model, logger); + + /// Returns the name including the names of the parent + @override + String fullName() => name; +} diff --git a/lib/src/model/module_model.dart b/lib/src/model/module_model.dart index dfacd9f..8b65cb1 100644 --- a/lib/src/model/module_model.dart +++ b/lib/src/model/module_model.dart @@ -1,24 +1,49 @@ -import 'package:flutter/material.dart'; +import 'package:dart_bones/dart_bones.dart'; import 'package:flutter_bones/flutter_bones.dart'; import 'package:meta/meta.dart'; -import 'package:dart_bones/dart_bones.dart'; class ModuleModel extends ModelBase { + static final regExprOptions = RegExp(r'^(unknown)$'); final Map map; String name; List options; - List pages; + @protected + final pages = []; + @protected + final pageMap = {}; + + ModuleModel(this.map, BaseLogger logger) : super(logger); + + /// Appends a [page] to the instance. + void addPage(PageModel page) { + pages.add(page); + pageMap[page.name] = page; + } - ModuleModel(this.map, BaseLogger logger): super(logger); /// Returns the name including the names of the parent @override String fullName() => name; + /// Returns a child page given by [name], null otherwise. + PageModel pageByName(String name) => + pageMap.containsKey(name) ? pageMap[name] : null; + /// Parses the map and stores the data in the instance. - void parse(){ - checkSuperfluous(map, 'module pages options'.split(' ')); + void parse() { + checkSuperfluous(map, 'module options pages'.split(' ')); name = parseString('module', map, required: true); - pages = PageModel.parseList(this, map['pages'], logger); + if (!map.containsKey('pages')) { + logger.error('Module $name: missing pages'); + } else { + final item = map['pages']; + if (!ModelBase.isList(item)) { + logger.error( + 'curious item in page list of ${fullName()}: ${StringUtils.limitString("$item", 80)}'); + } else { + PageModel.parseList(this, item, logger); + } + } options = parseOptions('options', map); + checkOptionsByRegExpr(options, regExprOptions); } } diff --git a/lib/src/model/page_model.dart b/lib/src/model/page_model.dart index 1281b73..35d1996 100644 --- a/lib/src/model/page_model.dart +++ b/lib/src/model/page_model.dart @@ -1,35 +1,56 @@ import 'package:dart_bones/dart_bones.dart'; import 'package:flutter_bones/flutter_bones.dart'; -import 'package:meta/meta.dart'; /// Represents one screen of the application. class PageModel extends ModelBase { - static final regExprOptions = RegExp(r'^(unknown)'); + static final regExprOptions = RegExp(r'^(unknown)$'); final ModuleModel module; - final Mapmap; + final Map map; String name; PageModelType pageModelType; - final Listsections = []; + final List sections = []; List options; - PageModel(this.module, this.map, BaseLogger logger): super(logger); + PageModel(this.module, this.map, BaseLogger logger) : super(logger); /// Returns the name including the names of the parent @override - String fullName() => '${module.fullName()}.$name'; + String fullName() => '${module.name}.$name'; /// Parses the map and stores the data in the instance. - void parse(){ - checkSuperfluous(map, 'name pageModelType sections options'.split(' ')); + void parse() { + checkSuperfluous(map, 'name options pageType sections'.split(' ')); name = parseString('name', map, required: true); - pageModelType = parseEnum('fieldTypeInfo', map, PageModelType.values); + pageModelType = + parseEnum('pageType', map, PageModelType.values); options = parseOptions('options', map); + if (!map.containsKey('sections')) { + logger.error('missing sections in ${fullName()}'); + } else { + final item = map['sections']; + if (!ModelBase.isList(item)) { + logger.error('"sections" is not an array in ${fullName()}: ' + '${StringUtils.limitString(item.toString(), 80)}'); + } else { + SectionModel.parseSections(this, null, item, logger); + } + } + checkOptionsByRegExpr(options, regExprOptions); } + /// Returns a list of Pages constructed by the Json like [map]. - static List parseList( - ModuleModel parent, List> map, BaseLogger logger) { - final rc = map.map((item) => PageModel(parent, item, logger)); - return rc; + static void parseList( + ModuleModel module, List> map, BaseLogger logger) { + for (var item in map) { + if (!ModelBase.isMap(item)) { + module.logger.error( + 'curious item in section list of ${module.fullName()}: ${StringUtils.limitString("$item", 80)}'); + } else { + final page = PageModel(module, item, logger); + page.parse(); + module.addPage(page); + } + } } } diff --git a/lib/src/model/section_model.dart b/lib/src/model/section_model.dart index 70ec50b..bc8aaa4 100644 --- a/lib/src/model/section_model.dart +++ b/lib/src/model/section_model.dart @@ -1,38 +1,120 @@ -import 'package:flutter/material.dart'; import 'package:dart_bones/dart_bones.dart'; import 'package:flutter_bones/flutter_bones.dart'; -import 'package:meta/meta.dart'; /// A part of a page represented by one widget. -class SectionModel extends ModelBase { - static final regExprOptions = RegExp(r'^(unknown)'); +class SectionModel extends WidgetModel { + static final regExprOptions = RegExp(r'^(unknown)$'); SectionModelType sectionModelType; - final PageModel page; String name; - List fields; + final children = []; + final buttons = []; List options; + final int no; final Map map; - SectionModel(this.page, this.map, BaseLogger logger) : super(logger); + + SectionModel(this.no, PageModel page, SectionModel section, this.map, + BaseLogger logger) + : super(section, page, WidgetModelType.section, logger); /// Returns the name including the names of the parent @override - String fullName() => '${page.fullName()}.$name'; + String fullName() => + section == null ? '${page.name}.$name' : '${section.name}.$name'; /// Parses the map and stores the data in the instance. void parse() { - checkSuperfluous(map, 'name sectionModelType fields options'.split(' ')); - name = parseString('name', map, required: true); + checkSuperfluous( + map, 'children fields name options sectionType widgetType'.split(' ')); sectionModelType = parseEnum( - 'sectionModelType', map, PageModelType.values); + 'sectionType', map, SectionModelType.values); + name = parseString('name', map) ?? + '${StringUtils.enumToString(sectionModelType)}$no'; options = parseOptions('options', map); + checkOptionsByRegExpr(options, regExprOptions); + if (!map.containsKey('children')) { + logger.error('missing children in ${fullName()}'); + } else { + final childrenList = map['children']; + if (!ModelBase.isList(childrenList)) { + logger.error('"children" is not a list in ${fullName()}: ' + '${StringUtils.limitString(childrenList.toString(), 80)}'); + } else { + int no = 0; + for (var child in childrenList) { + no++; + if (!ModelBase.isMap(childrenList)) { + logger + .error('child $no of "children" is not a map in ${fullName()}: ' + '${StringUtils.limitString(child.toString(), 80)}'); + } else { + if (!child.containsKey('widgetType')) { + logger.error( + 'child $no of "children" does not have "widgetType" in ${fullName()}: ' + '${StringUtils.limitString(child.toString(), 80)}'); + } else { + final widgetType = StringUtils.stringToEnum( + child['widgetType'].toString(), WidgetModelType.values); + WidgetModel widget; + switch (widgetType) { + case WidgetModelType.checkbox: + widget = CheckboxModel(this, page, child, logger); + break; + case WidgetModelType.combobox: + widget = ComboboxModel(this, page, child, logger); + break; + case WidgetModelType.textField: + widget = TextFieldModel(this, page, child, logger); + break; + case WidgetModelType.button: + widget = ButtonModel(this, page, child, logger); + break; + case WidgetModelType.text: + widget = TextModel(this, page, child, logger); + break; + case WidgetModelType.emptyLine: + widget = EmptyLineModel(this, page, child, logger); + break; + default: + //@ToDo: nested section + logger.error( + 'Section: unknown "widgetType" ${child['widgetType']} in ${fullName()}'); + break; + } + if (widget != null) { + children.add(widget); + } + } + } + } + //parseSections(page, this, childrenList, logger); + } + } } - /// Returns a list of Pages constructed by the Json like [map]. - static List parseList( - PageModel parent, List> map, BaseLogger logger) { - final rc = map.map((item) => SectionModel(parent, item, logger)); - return rc; + /// Returns a list of SectionModel constructed by the Json like [map]. + static void parseSections(PageModel page, SectionModel section, + List> map, BaseLogger logger) { + final rc = []; + int no = 0; + for (var item in map) { + no++; + if (!ModelBase.isMap(item)) { + page.logger.error( + 'curious item in section list of ${page.fullName()}: ${StringUtils.limitString("$item", 80)}'); + } else { + final current = SectionModel(no, page, null, item, logger); + current.parse(); + if (section != null) { + section.children.add(current); + } else { + page.sections.add(current); + } + } + } } + + @override + String widgetName() => name; } enum SectionModelType { diff --git a/lib/src/model/text_field_model.dart b/lib/src/model/text_field_model.dart new file mode 100644 index 0000000..7b39972 --- /dev/null +++ b/lib/src/model/text_field_model.dart @@ -0,0 +1,31 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class TextFieldModel extends FieldModel { + static final regExprOptions = + RegExp(r'^(readonly|disabled|password|required|unique)$'); + int maxSize; + int rows; + + FormFieldValidator validator; + + FormFieldSetter onSaved; + + final Map map; + + TextFieldModel( + SectionModel section, PageModel page, this.map, BaseLogger logger) + : super(section, page, map, WidgetModelType.textField, logger); + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous( + map, 'dataType label maxSize name options rows toolTip'.split(' ')); + super.parse(); + maxSize = parseInt('maxSize', map, required: false); + rows = parseInt('rows', map, required: false); + options = parseOptions('options', map); + checkOptionsByRegExpr(options, regExprOptions); + } +} diff --git a/lib/src/model/text_model.dart b/lib/src/model/text_model.dart new file mode 100644 index 0000000..fbb4333 --- /dev/null +++ b/lib/src/model/text_model.dart @@ -0,0 +1,29 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class TextModel extends WidgetModel { + static final regExprOptions = RegExp(r'^(richtext)$'); + List options; + String text; + bool isRichText; + final Map map; + + TextModel(SectionModel section, PageModel page, this.map, BaseLogger logger) + : super(section, page, WidgetModelType.text, logger); + + /// Returns the name including the names of the parent + @override + String fullName() => '${section.name}.text$id'; + + /// Parses the map and stores the data in the instance. + void parse() { + checkSuperfluous(map, 'options text widgetType'.split(' ')); + text = parseString('text', map, required: true); + options = parseOptions('options', map); + checkOptionsByRegExpr(options, regExprOptions); + isRichText = options.contains('richtext'); + } + + @override + String widgetName() => 'text$id'; +} diff --git a/lib/src/model/widget_model.dart b/lib/src/model/widget_model.dart index fe688cd..c802b0c 100644 --- a/lib/src/model/widget_model.dart +++ b/lib/src/model/widget_model.dart @@ -1,75 +1,29 @@ -import 'package:flutter/material.dart'; import 'package:dart_bones/dart_bones.dart'; import 'package:flutter_bones/flutter_bones.dart'; -import 'package:meta/meta.dart'; /// A base class for items inside a page: SectionModel FieldModel TextModel... abstract class WidgetModel extends ModelBase { + static int lastId = 0; final SectionModel section; final PageModel page; - final Map map; + final WidgetModelType widgetModelType; + int id; - WidgetModel(this.section, this.page, BaseLogger logger) : super(logger); - - /// Returns the name including the names of the parent - @override - String fullName() => '${parent.fullName()}.$name'; - - /// Parses the map and stores the data in the instance. - void parse() { - checkSuperfluous(map, 'name label toolTip fieldModelType dataType maxSize rows options'.split(' ')); - name = parseString('name', map, required: true); - label = parseString('label', map, required: false); - toolTip = parseString('toolTip', map, required: false); - fieldModelType = parseEnum( - 'fieldModelType', map, WidgetModelType.values); - dataType = parseEnum( - 'dataType', map, DataType.values); - maxSize = parseInt('maxSize', map, required: false); - rows = parseInt('rows', map, required: false); - options = parseOptions('options', map); + WidgetModel(this.section, this.page, this.widgetModelType, BaseLogger logger) + : super(logger) { + this.id = ++lastId; } - /// Tests the validity of the entries in [optionList] - /// Errors will be logged. - bool checkOptions() { - bool rc = false; - options.forEach((element) { - if (regExprOptions.firstMatch(element) == null) { - logger.error('unbekannte Feldoption $element in ${fullName()}'); - rc = true; - } - }); - return rc; - } - /// Returns the widget representing the field. - Widget widget(Key formKey){ - Widget rc; - switch(fieldModelType){ - case WidgetModelType.text: - rc = TextFormField(key: formKey, - validator: validator, - decoration: InputDecoration(labelText: label), - onSaved: onSaved, - maxLength: maxSize, - maxLines: rows, - readOnly: options.contains('readonly'), - obscureText: options.contains('password'), - ); - break; - case WidgetModelType.checkbox: - rc = Checkbox(key: formKey, - decoration: InputDecoration(labelText: label), - onSaved: onSaved, - maxLength: maxSize, - maxLines: rows, - readOnly: options.contains('readonly'), - obscureText: options.contains('password'), - ); - break; - } - return rc; - } + String widgetName(); } -enum WidgetModelType { checkbox, combobox, image, text, } +enum WidgetModelType { + button, + checkbox, + combobox, + emptyLine, + image, + section, + text, + textField, +} diff --git a/lib/src/widget/raised_button_bone.dart b/lib/src/widget/raised_button_bone.dart new file mode 100644 index 0000000..1506fb4 --- /dev/null +++ b/lib/src/widget/raised_button_bone.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; + +class RaisedButtonBone extends RaisedButton { + final ButtonModel model; + + RaisedButtonBone(this.model, + {ValueChanged onHighlightChanged, + ButtonTextTheme textTheme, + Color textColor, + Color disabledTextColor, + Color color, + Color disabledColor, + Color focusColor, + Color hoverColor, + Color highlightColor, + Color splashColor, + Brightness colorBrightness, + double elevation, + double focusElevation, + double hoverElevation, + double highlightElevation, + double disabledElevation, + EdgeInsetsGeometry padding, + VisualDensity visualDensity, + ShapeBorder shape, + Clip clipBehavior: Clip.none, + FocusNode focusNode, + bool autofocus: false, + MaterialTapTargetSize materialTapTargetSize, + Duration animationDuration, + Widget child}) + : super( + onPressed: model.onPressed, + onLongPress: model.onLongPressed, + onHighlightChanged: model.onHighlightChanged, + textTheme: textTheme, + textColor: textColor, + disabledTextColor: disabledTextColor, + color: color, + disabledColor: disabledColor, + focusColor: focusColor, + hoverColor: hoverColor, + highlightColor: highlightColor, + splashColor: splashColor, + colorBrightness: colorBrightness, + elevation: elevation, + focusElevation: focusElevation, + hoverElevation: hoverElevation, + highlightElevation: highlightElevation, + disabledElevation: disabledElevation, + padding: padding, + visualDensity: visualDensity, + shape: shape, + focusNode: focusNode, + autofocus: autofocus, + materialTapTargetSize: materialTapTargetSize, + animationDuration: animationDuration, + child: child); +} diff --git a/lib/src/widget/simpleform.dart b/lib/src/widget/simple_form.dart similarity index 99% rename from lib/src/widget/simpleform.dart rename to lib/src/widget/simple_form.dart index 3bc5269..0b6a712 100644 --- a/lib/src/widget/simpleform.dart +++ b/lib/src/widget/simple_form.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; class SimpleForm { static Form simpleForm({@required Key key, @required List fields, diff --git a/lib/src/widget/view.dart b/lib/src/widget/view.dart new file mode 100644 index 0000000..e022755 --- /dev/null +++ b/lib/src/widget/view.dart @@ -0,0 +1,151 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:flutter_bones/src/model/widget_model.dart'; + +class View { + static View _instance; + Settings settings; + BaseConfiguration widgetConfiguration; + + final BaseLogger logger; + + factory View(BaseLogger logger) { + if (_instance == null) { + _instance = View.internal(logger); + _instance.settings = Settings(logger); + _instance.widgetConfiguration = _instance.settings.widgetConfiguration; + } + return _instance; + } + + View.internal(this.logger); + + /// Creates a button from the [model]. + Widget button(ButtonModel model) { + final rc = RaisedButtonBone( + model, + child: Text(model.text), + ); + return rc; + } + + /// Creates a list of buttons from a list of [models]. + List buttonList(List models) { + final rc = []; + for (var item in models) { + rc.add(button(item)); + } + return rc; + } + + /// Creates a checkbox from the [model]. + Widget checkbox(WidgetModel model) { + var rc; + return rc; + } + + /// Creates an empty line from the [model]. + Widget emptyLine(EmptyLineModel model) { + var rc; + return rc; + } + + /// Creates image from the [model]. + Widget image(WidgetModel model) { + var rc; + return rc; + } + + /// Creates a section from the [model]. + Widget section(WidgetModel model) { + var rc; + return rc; + } + + /// Returns a form with the properties given by the [model] + /// [formKey] identifies the form. Used for form validation and saving. + Form simpleForm({SectionModel model, Key formKey}) { + assert(formKey != null); + final padding = + widgetConfiguration.asFloat('form.card.padding', defaultValue: 16.0); + final children = widgetsOfSection(model.children); + final buttons = buttonList(model.buttons); + final rc = Form( + key: formKey, + child: Card( + margin: EdgeInsets.symmetric(vertical: padding, horizontal: padding), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0), + child: ListView(children: [ + ...children, + SizedBox( + height: widgetConfiguration.asFloat( + 'form.gap.field_button.height', + defaultValue: 16.0)), + ...buttons + ])), + )); + return rc; + } + + /// Creates a text from the [model]. + Widget text(TextModel model) { + final rc = Text(model.text); + return rc; + } + + /// Creates a form text field from the [model]. + Widget textField(TextFieldModel model) { + var rc = toolTip( + TextFormField( + validator: model.validator, + decoration: InputDecoration(labelText: model.label), + onSaved: (input) => model.value(input), + ), + model); + return rc; + } + + /// If a tool tip is defined in [model] the [widget] is completed with that. + /// Otherwise [widget] is returned. + Widget toolTip(Widget widget, FieldModel model) { + Widget rc; + if (model.toolTip == null) { + rc = widget; + } else { + // @ToDo: ToolTip + rc = widget; + } + return rc; + } + + List widgetsOfSection(List children) { + final rc = []; + for (var child in children) { + switch (child.widgetModelType) { + case WidgetModelType.textField: + rc.add(textField(child)); + break; + case WidgetModelType.emptyLine: + rc.add(emptyLine(child)); + break; + case WidgetModelType.text: + rc.add(text(child)); + break; + case WidgetModelType.checkbox: + rc.add(checkbox(child)); + break; + case WidgetModelType.combobox: + rc.add(text(child)); + break; + case WidgetModelType.image: + rc.add(image(child)); + break; + case WidgetModelType.section: + rc.add(section(child)); + break; + } + } + } +} diff --git a/test/helpers/settings_test.dart b/test/helpers/settings_test.dart index cebeaa2..27c7f6a 100644 --- a/test/helpers/settings_test.dart +++ b/test/helpers/settings_test.dart @@ -1,17 +1,34 @@ +import 'package:dart_bones/dart_bones.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bones/flutter_bones.dart'; import 'package:test/test.dart'; void main() { - final map = { 'help' : { 'de' : 'Hilfe'}, - 'wrong name %{0}' : { 'de' : 'falscher name %{0}' } + final map = { + 'help': {'de': 'Hilfe'}, + 'wrong name %{0}': {'de': 'falscher name %{0}'} }; + final logger = MemoryLogger(LEVEL_FINE); setUpAll(() => Settings.setLocaleByNames(language: 'en')); group('Settings', () { + test('basic', () { + Settings.setLocale(Locale('de', 'de')); + final settings = Settings(logger); + expect(Settings.translate('abc', map), equals('abc')); + expect(Settings.translate('help', map), equals('help')); + expect( + Settings.translate('wrong name %{0}', map, + placeholders: {'0': 'Joe'}), + equals('wrong name Joe')); + }); test('translate-en', () { Settings.setLocaleByNames(language: 'en'); expect(Settings.translate('abc', map), equals('abc')); expect(Settings.translate('help', map), equals('help')); - expect(Settings.translate('wrong name %{0}', map, placeholders: {'0' : 'Joe'}), equals('wrong name Joe')); + expect( + Settings.translate('wrong name %{0}', map, + placeholders: {'0': 'Joe'}), + equals('wrong name Joe')); }); test('translate-de', () { Settings.setLocaleByNames(language: 'de'); diff --git a/test/helpers/string_helper_test.dart b/test/helpers/string_helper_test.dart new file mode 100644 index 0000000..8de5891 --- /dev/null +++ b/test/helpers/string_helper_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_bones/flutter_bones.dart'; +import 'package:test/test.dart'; + +void main() { + group('fromString', () { + test('fromString-string', () { + expect(StringHelper.fromString('abc', DataType.string), equals('abc')); + expect(StringHelper.fromString('', DataType.string), equals('')); + expect(StringHelper.fromString(null, DataType.string), isNull); + }); + test('fromString-num', () { + expect(StringHelper.fromString('123', DataType.int), equals(123)); + expect(StringHelper.fromString('12.3', DataType.float), equals(12.3)); + expect( + StringHelper.fromString('12.35', DataType.currency), equals(12.35)); + expect(StringHelper.fromString('1234567', DataType.reference), + equals(1234567)); + expect(StringHelper.fromString('', DataType.int), isNull); + expect(StringHelper.fromString(null, DataType.int), isNull); + }); + test('fromString-bool', () { + expect(StringHelper.fromString('True', DataType.bool), isTrue); + expect(StringHelper.fromString('yEs', DataType.bool), isTrue); + expect(StringHelper.fromString('nO', DataType.bool), isFalse); + expect(StringHelper.fromString('FALSE', DataType.bool), isFalse); + expect(StringHelper.fromString('wrong', DataType.bool), isNull); + expect(StringHelper.fromString('', DataType.bool), isNull); + expect(StringHelper.fromString(null, DataType.bool), isNull); + }); + test('fromString-datetime', () { + expect(StringHelper.fromString('2020.1.2', DataType.date), + equals(DateTime(2020, 1, 2))); + expect(StringHelper.fromString('2020.1.2-3:44:52', DataType.dateTime), + equals(DateTime(2020, 1, 2, 3, 44, 52))); + expect(StringHelper.fromString('', DataType.date), isNull); + expect(StringHelper.fromString(null, DataType.dateTime), isNull); + }); + test('fromString-error', () { + expect(StringHelper.fromString('2020.1.2', null), + startsWith('{ + "module": "demo1", + "pages": [ + { + "name": "create", + "pageType": "create", + "sections": [ + { + "sectionType": "simpleForm", + "children": [ + { + "widgetType": "textField", + "name": "user", + "label": "User", + "options": "required;unique", + }, + { + "widgetType": "button", + "name": "buttonStore", + "label": "Save", + }, + ] + } + ] + }, + ], + }; + + Demo1(BaseLogger logger) : super(model, logger); + + /// Returns the name including the names of the parent + @override + String fullName() => name; +} -- 2.39.5