From 9e9ab5fbb305207bd2525844c59dc33b2166f3f2 Mon Sep 17 00:00:00 2001 From: Jeff Peiffer Date: Sat, 5 Oct 2024 16:44:58 -0400 Subject: [PATCH] Yay! Updates! --- CHANGELOG.md | 7 +- README.md | 8 ++ lib/expressions.dart | 2 +- lib/src/expressions/evaluator.dart | 4 +- lib/src/expressions/expressions.dart | 2 +- .../functions/number_functions.dart | 9 ++ lib/src/expressions/parser.dart | 2 +- lib/src/expressions/standard_members.dart | 59 ++++++++- pubspec.yaml | 12 +- test/expressions/async_evaluator_test.dart | 43 +++---- test/templates/built_in_objects_test.dart | 112 ++++++++++++++++++ 11 files changed, 217 insertions(+), 43 deletions(-) create mode 100644 lib/src/expressions/functions/number_functions.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index e233102..4b6b0c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ -## [3.2.0+10] - October 1, 2024 - -* Automated dependency updates +## [3.3.0] - October 5th, 2024 +* Adds `path` to `Map`, `List`, and `String` objects to find the json path value. +* Adds `NumberFormat` as an option +* Adds `format` to all `num` types to apply the `NumberFormat` pattern. ## [3.2.0+9] - June 11, 2024 diff --git a/README.md b/README.md index f1c6747..311acb4 100644 --- a/README.md +++ b/README.md @@ -362,6 +362,7 @@ In addition to the items supported by the [Iterable](#iterable) class, a [List]( | Function | Example | |----------|---------| | [asMap](https://api.flutter.dev/flutter/dart-core/List/asMap.html) | `${list.asMap()[2]}` | +| path(String jsonPath) | Applys the JSON path to return the first matching value. | [reversed](https://api.flutter.dev/flutter/dart-core/List/reversed.html) | `${list.reversed.first}` | | toJson([int padding]) | `${list.toJson(2)}` | | [sort](https://api.flutter.dev/flutter/dart-core/List/sort.html) | `${list.sort().first}` | @@ -435,6 +436,7 @@ The following [Map](https://api.flutter.dev/flutter/dart-core/Map-class.html) me | [isNotEmpty](https://api.flutter.dev/flutter/dart-core/Map/isNotEmpty.html) | `${map.isNotEmpty ? map.values.first : 'null'}` | | [length](https://api.flutter.dev/flutter/dart-core/Map/length.html) | `${map.length}` | | [remove](https://api.flutter.dev/flutter/dart-core/Map/remove.html) | `${map.remove('key')}` | +| path(String jsonPath) | Applys the JSON path to return the first matching value. | toJson([int padding]) | `${map.toJson(2)}` | | [values](https://api.flutter.dev/flutter/dart-core/Map/values.html) | `${map.values.first}` | @@ -521,11 +523,17 @@ The following [String](https://api.flutter.dev/flutter/dart-core/String-class.ht | [length](https://api.flutter.dev/flutter/dart-core/String/length.html) | `${str.length}` | | [padLeft](https://api.flutter.dev/flutter/dart-core/String/padLeft.html) | `${str.padLeft(2, ' ')}` | | [padRight](https://api.flutter.dev/flutter/dart-core/String/padRight.html) | `${str.padRight(2, ' ')}` | +| path(String jsonPath) | `${str.path($.firstName)}` | Attempts to decode the string using JSON or YAML and then applys the JSON path to return the first matching value. | [replaceAll](https://api.flutter.dev/flutter/dart-core/String/replaceAll.html) | `${str.replaceAll('other', 'foo')}` | | [replaceFirst](https://api.flutter.dev/flutter/dart-core/String/replaceFirst.html) | `${str.replaceFirst('other', 'foo')}` | | [split](https://api.flutter.dev/flutter/dart-core/String/split.html) | `${str.split(',').join('\n')}` | | [startsWith](https://api.flutter.dev/flutter/dart-core/String/startsWith.html) | `${str.startsWith('other')}` | | [substring](https://api.flutter.dev/flutter/dart-core/String/substring.html) | `${str.substring(begin, end)}` | +| toBool | `${str.toBool()}` | Converts the string to a `bool`. The result will be `true` if and only if the lower case version of the string equals the value of `"true"`. +| toDouble | `${str.toDouble()}` | Attempts to convert the string into a `double`. Should the parsing fail, the result will be `null`. +| toInt | `${str.toInt()}` | Attempts to convert the string into an `int`. Should the parsing fail, the result will be `null`. +| [toLowerCase](https://api.flutter.dev/flutter/dart-core/String/toLowerCase.html) | `${str.toLowerCase()}` | +| [toLowerCase](https://api.flutter.dev/flutter/dart-core/String/toLowerCase.html) | `${str.toLowerCase()}` | | [toLowerCase](https://api.flutter.dev/flutter/dart-core/String/toLowerCase.html) | `${str.toLowerCase()}` | | [toUpperCase](https://api.flutter.dev/flutter/dart-core/String/toUpperCase.html) | `${str.toUpperCase()}` | | [trim](https://api.flutter.dev/flutter/dart-core/String/trim.html) | `${str.trim()}` | diff --git a/lib/expressions.dart b/lib/expressions.dart index b21a1b2..3c17cfb 100644 --- a/lib/expressions.dart +++ b/lib/expressions.dart @@ -1,4 +1,4 @@ -library; +library expressions; export 'src/expressions/evaluator.dart'; export 'src/expressions/expressions.dart'; diff --git a/lib/src/expressions/evaluator.dart b/lib/src/expressions/evaluator.dart index 11d108f..90a9687 100644 --- a/lib/src/expressions/evaluator.dart +++ b/lib/src/expressions/evaluator.dart @@ -1,4 +1,4 @@ -library; +library expressions.evaluator; import 'package:json_class/json_class.dart'; import 'package:logging/logging.dart'; @@ -20,6 +20,7 @@ export 'functions/duration_functions.dart'; export 'functions/encrypt_functions.dart'; export 'functions/future_functions.dart'; export 'functions/json_path_functions.dart'; +export 'functions/number_functions.dart'; export 'functions/random_functions.dart'; /// Handles evaluation of expressions @@ -76,6 +77,7 @@ class ExpressionEvaluator { ...EncryptFunctions.functions, ...FutureFunctions.functions, ...JsonPathFunctions.functions, + ...NumberFunctions.functions, ...RandomFunctions.functions, }; diff --git a/lib/src/expressions/expressions.dart b/lib/src/expressions/expressions.dart index 23f498d..86cee99 100644 --- a/lib/src/expressions/expressions.dart +++ b/lib/src/expressions/expressions.dart @@ -1,4 +1,4 @@ -library; +library expressions.core; import 'package:meta/meta.dart'; import 'package:petitparser/petitparser.dart'; diff --git a/lib/src/expressions/functions/number_functions.dart b/lib/src/expressions/functions/number_functions.dart new file mode 100644 index 0000000..f9a6ba1 --- /dev/null +++ b/lib/src/expressions/functions/number_functions.dart @@ -0,0 +1,9 @@ +import 'package:intl/intl.dart'; + +/// Class containing functions related to Number processing. +class NumberFunctions { + /// The functions related to Number processing. + static final functions = { + 'NumberFormat': (pattern) => NumberFormat(pattern), + }; +} diff --git a/lib/src/expressions/parser.dart b/lib/src/expressions/parser.dart index fd6b977..b3c65bb 100644 --- a/lib/src/expressions/parser.dart +++ b/lib/src/expressions/parser.dart @@ -1,4 +1,4 @@ -library; +library expressions.parser; import 'package:meta/meta.dart'; import 'package:petitparser/petitparser.dart'; diff --git a/lib/src/expressions/standard_members.dart b/lib/src/expressions/standard_members.dart index 4575166..74c28a4 100644 --- a/lib/src/expressions/standard_members.dart +++ b/lib/src/expressions/standard_members.dart @@ -34,6 +34,8 @@ dynamic lookupStandardMembers(dynamic target, String name) { result = _processMap(target, name); } else if (target is MapEntry) { result = _processMapEntry(target, name); + } else if (target is NumberFormat) { + result = _processNumberFormat(target, name); } else if (target is double || target is int || target is num) { result = _processNum(target, name); } else if (target is String) { @@ -150,6 +152,10 @@ dynamic _processDateTime(DateTime target, String name) { result = target.compareTo; break; + case 'format': + result = (pattern) => DateFormat(pattern).format(target); + break; + case 'isAfter': result = target.isAfter; break; @@ -370,6 +376,10 @@ dynamic _processList(List target, String name) { result = target.reversed; break; + case 'path': + result = (path) => JsonPath(path).readValues(target).first; + break; + case 'sort': result = () { target.sort((a, b) { @@ -482,6 +492,10 @@ dynamic _processMap(Map target, String name) { result = target.length; break; + case 'path': + result = (path) => JsonPath(path).readValues(target).first; + break; + case 'remove': result = target.remove; break; @@ -555,6 +569,10 @@ dynamic _processNum(num target, String name) { result = target.floorToDouble; break; + case 'format': + result = (format) => NumberFormat(format).format(target); + break; + case 'isFinite': result = target.isFinite; break; @@ -619,6 +637,21 @@ dynamic _processNum(num target, String name) { return result; } +dynamic _processNumberFormat(NumberFormat target, String name) { + dynamic result; + switch (name) { + case 'format': + result = target.format; + break; + + case 'parse': + result = target.parse; + break; + } + + return result; +} + dynamic _processRsa(Rsa target, String name) { dynamic result; @@ -672,7 +705,10 @@ dynamic _processString(String target, String name) { break; case 'decode': - result = () => yaon.parse(target); + result = () => yaon.parse( + target, + normalize: true, + ); break; case 'endsWith': @@ -707,6 +743,15 @@ dynamic _processString(String target, String name) { result = target.padRight; break; + case 'path': + result = (path) => JsonPath(path) + .readValues(yaon.parse( + target, + normalize: true, + )) + .first; + break; + case 'replaceAll': result = target.replaceAll; break; @@ -727,10 +772,22 @@ dynamic _processString(String target, String name) { result = target.substring; break; + case 'toBool': + result = target.toLowerCase() == 'true'; + break; + case 'toLowerCase': result = target.toLowerCase; break; + case 'toDouble': + result = () => double.tryParse(target); + break; + + case 'toInt': + result = () => double.tryParse(target)?.toInt(); + break; + case 'toUpperCase': result = target.toUpperCase; break; diff --git a/pubspec.yaml b/pubspec.yaml index aaff4c3..da7d29d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: 'template_expressions' description: 'A Dart library to process string based templates using expressions.' homepage: 'https://github.com/peiffer-innovations/template_expressions' -version: '3.2.0+10' +version: '3.3.0' environment: sdk: '>=3.0.0 <4.0.0' @@ -12,19 +12,19 @@ dependencies: encrypt: '^5.0.3' fake_async: '^1.3.0' intl: '^0.19.0' - json_class: '^3.0.0+16' + json_class: '^3.0.0+14' json_path: '>=0.6.3 <1.0.0' logging: '^1.2.0' meta: '^1.12.0' petitparser: '^6.0.1' pointycastle: '^3.9.1' - quiver: '^3.2.2' - rxdart: '^0.28.0' + quiver: '^3.2.1' + rxdart: '^0.27.7' yaon: '^1.1.4+10' dev_dependencies: - flutter_lints: '^5.0.0' - test: '^1.25.8' + flutter_lints: '^4.0.0' + test: '^1.25.7' permittedLicenses: - 'Apache-2.0' diff --git a/test/expressions/async_evaluator_test.dart b/test/expressions/async_evaluator_test.dart index ced9ae9..721adcd 100644 --- a/test/expressions/async_evaluator_test.dart +++ b/test/expressions/async_evaluator_test.dart @@ -121,23 +121,19 @@ void main() { stream.listen((v) => current = v, onDone: () => done = true); controllerX.add(40); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, null); controllerY.add(60); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, false); controllerY.add(20); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, true); controllerX.add(10); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, false); await async.flushMicrotasksUntil(controllerX.close()); @@ -280,39 +276,30 @@ void main() { dynamic current; var done = false; - stream.listen( - (v) => current = v, - onDone: () => done = true, - ); + stream.listen((v) => current = v, onDone: () => done = true); controllerX.add('y'); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, null); controllerY.add(1); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, 1); controllerX.add('z'); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, 1); controllerZ.add(2); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, 2); controllerY.add(3); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, 2); controllerZ.add(4); - // async.flushMicrotasks(); - await async.unblock(); + async.flushMicrotasks(); expect(current, 4); await async.flushMicrotasksUntil(controllerX.close()); @@ -413,9 +400,8 @@ void main() { } FutureOr fakeAsync( - FutureOr Function(fake_async.FakeAsync async) callback, { - DateTime? initialTime, -}) { + FutureOr Function(fake_async.FakeAsync async) callback, + {DateTime? initialTime}) { final async = fake_async.FakeAsync(initialTime: initialTime); final f = async.run(callback); if (f is Future) { @@ -428,8 +414,7 @@ extension FakeAsyncX on fake_async.FakeAsync { Future flushMicrotasksUntil(Future f) async { var isDone = false; f = f.whenComplete( - () => isDone = true, - ); // check if all work in body has been done + () => isDone = true); // check if all work in body has been done while (!isDone) { // flush the microtasks in real async zone await unblock(); diff --git a/test/templates/built_in_objects_test.dart b/test/templates/built_in_objects_test.dart index 093d4c7..ac77dc2 100644 --- a/test/templates/built_in_objects_test.dart +++ b/test/templates/built_in_objects_test.dart @@ -178,6 +178,14 @@ void main() { template.process(context: context), '2022-02-07', ); + + template = Template( + value: r'${DateTime([2022, 02, 07]).format("yyyy-MM-dd")}', + ); + expect( + template.process(context: context), + '2022-02-07', + ); }); test('now', () { @@ -349,6 +357,60 @@ void main() { 'John', ); }); + + test('list', () { + expect( + Template(value: r'${x.path("$[1]")}').process( + context: { + 'x': [ + 'foo', + 'bar', + ], + }, + ), + 'bar', + ); + }); + test('map', () { + expect( + Template(value: r'${x.path("$.bar")}').process(context: { + 'x': { + 'foo': 'foo', + 'bar': 'bar', + }, + }), + 'bar', + ); + }); + test('json string', () { + expect( + Template(value: r'${x.path("$.bar")}').process( + context: { + 'x': ''' + { + 'foo': 'foo', + 'bar': 'bar', + } + ''' + }, + ), + 'bar', + ); + }); + + test('yaml string', () { + expect( + Template(value: r'${x.path("$.bar")}').process( + context: { + 'x': ''' +foo: foo +bar: bar +''' + }, + ), + 'bar', + ); + }); }); group('List', () { @@ -382,6 +444,56 @@ void main() { }); }); + group('NumberFormat', () { + test('format', () { + expect( + Template(value: r'${NumberFormat("#,###.00").format(x)}') + .process(context: { + 'x': 1234, + }), + '1,234.00', + ); + expect( + Template(value: r'${NumberFormat("#,###.00").format(x)}') + .process(context: { + 'x': 1234.0, + }), + '1,234.00', + ); + expect( + Template(value: r'${x.format("#,###.00")}').process(context: { + 'x': 1234.0, + }), + '1,234.00', + ); + expect( + Template(value: r'${x.format("#,###.00")}').process(context: { + 'x': 1234, + }), + '1,234.00', + ); + }); + + test('parse', () { + expect( + Template(value: r'${x.toInt()}').evaluate(context: {'x': '1234.45'}), + 1234, + ); + expect( + Template(value: r'${x.toDouble()}').evaluate(context: {'x': '1234.45'}), + 1234.45, + ); + expect( + Template(value: r'${x.toInt()}').evaluate(context: {'x': 'foo'}), + null, + ); + expect( + Template(value: r'${x.toDouble()}').evaluate(context: {'x': 'foo'}), + null, + ); + }); + }); + group('random', () { test('int', () { final template = Template(