Skip to main content
zenshop

Using dart_eval for Magic Actions

Making zenshop scriptable with the power of Dart

I've been using dart_eval to enable Magic Actions for zenshop, I wanted to share how it was made possible.

Firstly, what is dart_eval?

It's a way to compile and execute Dart code within your Dart application at runtime.

It features a powerful Bridge between your app and your scripts - it can even modify your applictation code.

What is a Magic Action?

A Magic Action is a way for zenshop customers to add new functionality for their specific needs. I intend to expand the functionality of Magic Actions as a long-term goal for zenshop, including client-side enhancements (as described here), as well as cloud functionality - potentially using Firecracker.

What we're going to build

The goals for this first release of Magic Actions.

  • Select a bunch of conversations, customers, orders.
  • Define a bunch of scripts at runtime in the app.
  • Execute any desired script with the selected data.

zenshop - selecting conversations

zenshop magic actions

zenshop - selecting a Magic Action

zenshop running a magic action

Building a very simple script

So first, you need to grab dart_eval if you want to get started yourself.

I looked at the provided example and then quickly realised the amount of boilerplate code required for this meant that the tests were a better place to understand how to put something together.

Luckily the test cases are really good in this package.

Here's how we start, I'm going to use a queue to process 'scripts' one-by-one, in the app.

The ZenshopApi has a static method, that contains our script, the selected id for a conversation is passed into the script.

for (var id in selectedIds) {
    appQueue.add(
        () => Future(
            () => ZenshopApi.executeResolveConversationById(id, true),
        ),
    );
}

Building a queue is easy enough, especially with queue and get.

Here's what our script looks like:

class ZenshopApi {

  static executeResolveConversationById(String id, bool resolved) {
    final compiler = Compiler();
    compiler.defineBridgeClasses([$ZenshopApi.$declaration]);

    final program = compiler.compile({
      'resolve_conversation': {
        'main.dart': '''
        import 'package:zenshop_lib/zenshop_lib.dart';

        bool main() {
          final api = ZenshopApi();
          return api.resolveConversationById("$id", $resolved);
        }
      '''
      }
    });

    final runtime = Runtime.ofProgram(program);

    runtime.registerBridgeFunc(
      'package:zenshop_lib/zenshop_lib.dart',
      'ZenshopApi.',
      $ZenshopApi.$construct,
      isBridge: true,
    );

    runtime.setup();
    runtime.executeLib('package:resolve_conversation/main.dart', 'main');
  }

  bool resolveConversationById(String id, bool resolved) {
    final GraphQLClient client = Get.find();
    client.mutate$ResolveConversation(
      Options$Mutation$ResolveConversation(
        variables: Variables$Mutation$ResolveConversation(
          input: Input$ResolveConversationMutationInput(
            id: id,
            resolved: resolved,
          ),
        ),
      ),
    );

    return true;
  }
}

So, clearly quite a bit of code here.

I expect in the near future, Magic Actions will have a defined API that customers can use.

Here's what I'm aiming for, I'll expose the script engine so users can upload their own Dart, in something like this:

import 'package:zenshop_lib/zenshop_lib.dart';

bool runMagicAction(String conversationId) async {
    final api = ZenshopApi();
    await api.resolveConversationById(conversationId, true);
    return true;
}

The only trouble with dart_eval is the sheer amount of boilerplate needed to do even simple things.

class $ZenshopApi extends ZenshopApi with $Bridge {
  $ZenshopApi : super;

  static $ZenshopApi $construct(Runtime runtime, $Value? target, List<$Value?> args) {
    return $ZenshopApi(args[0]!.$value);
  }

  static const $type = BridgeTypeRef(BridgeTypeSpec('package:zenshop_lib/zenshop_lib.dart', 'ZenshopApi'));

  static const $declaration = BridgeClassDef(BridgeClassType($type),
      constructors: {
        '': BridgeConstructorDef(BridgeFunctionDef(returns: BridgeTypeAnnotation($type), params: [], namedParams: []))
      },
      methods: {
        'resolveConversationById': BridgeMethodDef(
          BridgeFunctionDef(
            returns: BridgeTypeAnnotation(
              BridgeTypeRef.type(
                RuntimeTypes.boolType,
              ),
            ),
            params: [
              BridgeParameter(
                'id',
                BridgeTypeAnnotation(
                  BridgeTypeRef.type(RuntimeTypes.stringType),
                ),
                false,
              ),
              BridgeParameter(
                'resolved',
                BridgeTypeAnnotation(
                  BridgeTypeRef.type(RuntimeTypes.boolType),
                ),
                false,
              ),
            ],
          ),
        )
      },
      getters: {},
      setters: {},
      fields: {},
      bridge: true);

  
  $Value? $bridgeGet(String identifier) {
    switch (identifier) {
      case 'resolveConversationById':
        return $Function((Runtime rt, $Value? target, List<$Value?> args) {
          return $bool(
            super.resolveConversationById(
              args[0]!.$value,
              args[1]!.$value,
            ),
          );
        });
    }
    throw UnimplementedError();
  }

  
  void $bridgeSet(String identifier, $Value value) {
    switch (identifier) {
      default:
        throw UnimplementedError();
    }
  }

  
  bool resolveConversationById(String id, bool resolved) {
    return $_invoke('resolveConversationById', [$String(id), $bool(resolved)]);
  }
}

Summary

Dart is a powerful, dynamic language that can do some really interesting things.

I look forward to exposing the Magic Actions API to customers in the next update.