From 7ed14cef1ec44efb3a61edad597c74a415b23bc0 Mon Sep 17 00:00:00 2001 From: Patrick Stewart Date: Sun, 15 Dec 2024 16:26:30 -0700 Subject: [PATCH] add: adding macroable package --- packages/macroable/.gitignore | 7 ++ packages/macroable/CHANGELOG.md | 3 + packages/macroable/LICENSE.md | 10 +++ packages/macroable/README.md | 78 +++++++++++++++++++ packages/macroable/analysis_options.yaml | 30 +++++++ .../macroable/example/macroable_example.dart | 42 ++++++++++ packages/macroable/lib/macroable.dart | 3 + packages/macroable/lib/src/macroable.dart | 59 ++++++++++++++ packages/macroable/pubspec.yaml | 14 ++++ packages/macroable/test/macroable_test.dart | 69 ++++++++++++++++ 10 files changed, 315 insertions(+) create mode 100644 packages/macroable/.gitignore create mode 100644 packages/macroable/CHANGELOG.md create mode 100644 packages/macroable/LICENSE.md create mode 100644 packages/macroable/README.md create mode 100644 packages/macroable/analysis_options.yaml create mode 100644 packages/macroable/example/macroable_example.dart create mode 100644 packages/macroable/lib/macroable.dart create mode 100644 packages/macroable/lib/src/macroable.dart create mode 100644 packages/macroable/pubspec.yaml create mode 100644 packages/macroable/test/macroable_test.dart diff --git a/packages/macroable/.gitignore b/packages/macroable/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/packages/macroable/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/packages/macroable/CHANGELOG.md b/packages/macroable/CHANGELOG.md new file mode 100644 index 0000000..effe43c --- /dev/null +++ b/packages/macroable/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/packages/macroable/LICENSE.md b/packages/macroable/LICENSE.md new file mode 100644 index 0000000..0fd0d03 --- /dev/null +++ b/packages/macroable/LICENSE.md @@ -0,0 +1,10 @@ +The MIT License (MIT) + +The Laravel Framework is Copyright (c) Taylor Otwell +The Fabric Framework is Copyright (c) Vieo, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/macroable/README.md b/packages/macroable/README.md new file mode 100644 index 0000000..10aae59 --- /dev/null +++ b/packages/macroable/README.md @@ -0,0 +1,78 @@ +# Platform Macroable + +A Dart implementation of Laravel's Macroable trait, allowing you to add methods to classes at runtime. + +## Features + +- Add custom methods to classes at runtime +- Mix in methods from other classes +- Check for the existence of macros +- Flush all macros for a given class + +## Getting started + +Add this package to your `pubspec.yaml`: + +```yaml +dependencies: + platform_macroable: ^1.0.0 +``` + +Then run `dart pub get` or `flutter pub get` to install the package. + +## Usage + +Here's a simple example of how to use the `Macroable` mixin: + +```dart +import 'package:platform_macroable/macroable.dart'; + +class MyClass with Macroable { + String regularMethod() => 'This is a regular method'; +} + +void main() { + // Register a macro + Macroable.macro(MyClass, 'customMethod', () => 'This is a custom method'); + + final instance = MyClass(); + + // Call the regular method + print(instance.regularMethod()); + + // Call the macro method + print((instance as dynamic).customMethod()); + + // Check if a macro exists + print(Macroable.hasMacro(MyClass, 'customMethod')); // true + print(Macroable.hasMacro(MyClass, 'nonExistentMethod')); // false + + // Add methods from a mixin + class MyMixin { + String mixinMethod() => 'This is a mixin method'; + } + + Macroable.mixin(MyClass, MyMixin()); + + // Call the mixin method + print((instance as dynamic).mixinMethod()); + + // Flush all macros + Macroable.flushMacros(MyClass); + + // This will now throw a NoSuchMethodError + try { + (instance as dynamic).customMethod(); + } catch (e) { + print('Caught exception: $e'); + } +} +``` + +## Additional Information + +For more detailed examples, please refer to the `example/macroable_example.dart` file in the package. + +If you encounter any issues or have feature requests, please file them on the [issue tracker](https://github.com/yourusername/platform_macroable/issues). + +Contributions are welcome! Please read our [contributing guidelines](https://github.com/yourusername/platform_macroable/blob/main/CONTRIBUTING.md) before submitting a pull request. diff --git a/packages/macroable/analysis_options.yaml b/packages/macroable/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/packages/macroable/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/packages/macroable/example/macroable_example.dart b/packages/macroable/example/macroable_example.dart new file mode 100644 index 0000000..4f8fee4 --- /dev/null +++ b/packages/macroable/example/macroable_example.dart @@ -0,0 +1,42 @@ +import 'package:platform_macroable/macroable.dart'; + +class MyClass with Macroable { + String regularMethod() => 'This is a regular method'; +} + +class MyMixin { + String mixinMethod() => 'This is a mixin method'; +} + +void main() { + // Register a macro + Macroable.macro(MyClass, 'customMethod', () => 'This is a custom method'); + + final instance = MyClass(); + + // Call the regular method + print(instance.regularMethod()); + + // Call the macro method + print((instance as dynamic).customMethod()); + + // Check if a macro exists + print(Macroable.hasMacro(MyClass, 'customMethod')); // true + print(Macroable.hasMacro(MyClass, 'nonExistentMethod')); // false + + // Add methods from a mixin + Macroable.mixin(MyClass, MyMixin()); + + // Call the mixin method + print((instance as dynamic).mixinMethod()); + + // Flush all macros + Macroable.flushMacros(MyClass); + + // This will now throw a NoSuchMethodError + try { + (instance as dynamic).customMethod(); + } catch (e) { + print('Caught exception: $e'); + } +} diff --git a/packages/macroable/lib/macroable.dart b/packages/macroable/lib/macroable.dart new file mode 100644 index 0000000..75a46a5 --- /dev/null +++ b/packages/macroable/lib/macroable.dart @@ -0,0 +1,3 @@ +library macroable; + +export 'src/macroable.dart'; diff --git a/packages/macroable/lib/src/macroable.dart b/packages/macroable/lib/src/macroable.dart new file mode 100644 index 0000000..582d13d --- /dev/null +++ b/packages/macroable/lib/src/macroable.dart @@ -0,0 +1,59 @@ +import 'dart:mirrors'; + +mixin Macroable { + static final Map> _macros = {}; + + static void macro(Type type, String name, Function macro) { + _macros.putIfAbsent(type, () => {}); + _macros[type]![Symbol(name)] = macro; + } + + static bool hasMacro(Type type, String name) { + return _macros[type]?.containsKey(Symbol(name)) ?? false; + } + + static void mixin(Type type, Object mixin, {bool replace = true}) { + final methods = reflect(mixin) + .type + .declarations + .values + .whereType() + .where((m) => m.isRegularMethod && !m.isPrivate); + + for (final method in methods) { + final name = MirrorSystem.getName(method.simpleName); + if (replace || !hasMacro(type, name)) { + macro(type, name, (List args, + [Map namedArgs = const {}]) { + return reflect(mixin) + .invoke(method.simpleName, args, namedArgs) + .reflectee; + }); + } + } + } + + static void flushMacros(Type type) { + _macros.remove(type); + } + + dynamic noSuchMethod(Invocation invocation) { + final macro = _macros[runtimeType]?[invocation.memberName]; + + if (macro != null) { + try { + return Function.apply( + macro, [invocation.positionalArguments], invocation.namedArguments); + } catch (e) { + try { + return Function.apply( + macro, invocation.positionalArguments, invocation.namedArguments); + } catch (e) { + return (macro as dynamic)(); + } + } + } + + return super.noSuchMethod(invocation); + } +} diff --git a/packages/macroable/pubspec.yaml b/packages/macroable/pubspec.yaml new file mode 100644 index 0000000..451155e --- /dev/null +++ b/packages/macroable/pubspec.yaml @@ -0,0 +1,14 @@ +name: platform_macroable +description: A Dart implementation of Laravel's Macroable trait. +version: 1.0.0 +homepage: https://github.com/yourusername/macroable + +environment: + sdk: '>=2.12.0 <3.0.0' + +dependencies: + # Add any dependencies here + +dev_dependencies: + test: ^1.16.0 + lints: ^2.0.0 diff --git a/packages/macroable/test/macroable_test.dart b/packages/macroable/test/macroable_test.dart new file mode 100644 index 0000000..7cfd85a --- /dev/null +++ b/packages/macroable/test/macroable_test.dart @@ -0,0 +1,69 @@ +import 'package:platform_macroable/macroable.dart'; +import 'package:test/test.dart'; + +class TestClass with Macroable { + String regularMethod() => 'regular method'; +} + +class TestMixin { + String mixinMethod() => 'mixin method'; +} + +void main() { + group('Macroable', () { + late TestClass instance; + + setUp(() { + instance = TestClass(); + }); + + tearDown(() { + Macroable.flushMacros(TestClass); + }); + + test('regular methods work', () { + expect(instance.regularMethod(), equals('regular method')); + }); + + test('can add and call macro methods', () { + Macroable.macro(TestClass, 'macroMethod', () => 'macro method'); + expect((instance as dynamic).macroMethod(), equals('macro method')); + }); + + test('can check if macro exists', () { + Macroable.macro(TestClass, 'existingMacro', () => 'exists'); + expect(Macroable.hasMacro(TestClass, 'existingMacro'), isTrue); + expect(Macroable.hasMacro(TestClass, 'nonExistingMacro'), isFalse); + }); + + test('can mix in methods from other classes', () { + Macroable.mixin(TestClass, TestMixin()); + expect((instance as dynamic).mixinMethod(), equals('mixin method')); + }); + + test('can flush macros', () { + Macroable.macro(TestClass, 'flushMe', () => 'flush me'); + expect(Macroable.hasMacro(TestClass, 'flushMe'), isTrue); + Macroable.flushMacros(TestClass); + expect(Macroable.hasMacro(TestClass, 'flushMe'), isFalse); + }); + + test('throws NoSuchMethodError for non-existent methods', () { + expect(() => (instance as dynamic).nonExistentMethod(), + throwsNoSuchMethodError); + }); + + test('can add macros with parameters', () { + Macroable.macro( + TestClass, 'paramMacro', (String param) => 'Hello, $param!'); + expect( + (instance as dynamic).paramMacro('World'), equals('Hello, World!')); + }); + + test('can override existing macros', () { + Macroable.macro(TestClass, 'overrideMacro', () => 'original'); + Macroable.macro(TestClass, 'overrideMacro', () => 'overridden'); + expect((instance as dynamic).overrideMacro(), equals('overridden')); + }); + }); +}