AppleScript framework: architecture proposal

  • For awhile now I've been wanting to write a little Cocoa app to automate
    some iTunes tasks for me (I know, there are a ton of these things
    already out there; stick with me, this gets more interesting). Doing so
    clearly involves sending Apple Events to iTunes, and after some research
    into the topic, I've decided I don't want to mess with raw Apple Events.
    I can crank out AppleScript routines to do the actual controlling in
    just a few minutes; surely there's a way to leverage them from within a
    Cocoa app.

    I found some sample code from Apple called "EmbeddedAppleScripts" that
    calls pre-compiled AppleScript routines using Carbon. Unfortunately, the
    interface it exposes, while easier to use than generating raw Apple
    Events, isn't much prettier. It seems to me that an Objective-C wrapper
    around this would make it much more useful to Cocoa developers. One
    could build a small library of AppleScript routines for interapplication
    communication, and access them through a friendly API.

    I've come up with the following API proposal for such a framework.

    * class ASEmbeddedAppleScript

    - (id)initWithCompiledScriptNamed:(NSString *)scriptName;

    Loads the pre-compiled script. Default would grab it from the main
    bundle; init(...) variants would look for script in other bundles, via a
    file path, using a URL, whatever.

    - (void)registerSubroutineNamed:(NSString *)subroutineName
        withParameters:(ASParameterDescriptionList *)parameters
        returning:(ASReturnTypeDescription *)returnDescription;

    The sample code provided by Apple requires writing little glue functions
    for every unique AppleScript routine signature. E.g., parameterless
    routines that return no value can be handled by a single glue function,
    but a routine that takes a string and a routine that takes an alias
    require two additional glue functions. I don't want to write glue code;
    I want Objective-C to do it for me.

    The registerSubroutineNamed: method would add its parameters to an
    internal table and in effect "create" a glue method for the associated
    AppleScript routine. This would be done through a fairly involved
    forwardInvocation: method in the ASEmbeddedAppleScript class. Once all
    of the AppleScript handlers are registered, the pre-compiled AppleScript
    can be called as if it was implemented as an Objective-C class.

    - (id)executeSubroutineNamed:(NSString *)name
      otherParameters:(???)foo, ...;

    There should also be a means of executing a subroutine directly, without
    having to register it and let forwardInvocation: take care of the actual
    method call. Direct execution would let a class provide prototypes for
    the AppleScript handlers, facilitating compile-time parameter checking.

    * class ASParameterDescriptionList

    - (void)addParameterNamed:(NSString *)name
        ofType:(NSString *)type;

    Adds a parameter to the list. The type is something like "string," or
    "alias," or "list," and describes the type of the parameter as it
    appears to the AppleScript. Internally, the framework knows how to
    convert from the AppleScript type to an associated Cocoa type. The
    built-in types will be provided by the framework ("string" to NSString,
    "list" to NSArray, etc.), and some mechanism should be provided to let
    users add their own conversions (more on this later). The usage examples
    later on should make this clearer.

    * class ASReturnTypeDescription

    - (id)initWithType:(NSString *)type;

    Analogous to the ASParameterDescriptionList, but since only one object
    can be returned, there's no need to keep a list.

    * Examples

    Let's say we have this script:

    on sayHi(prompt, button_names)
        display dialog prompt with buttons button_names
        return the result
    end sayHi

    Setting up an embedded script might look something like this:

    /*-------------------------*/

    ASEmbeddedAppleScript myScript = [[ASEmbeddedAppleScript alloc]
        initWithCompiledScriptNamed:@"my_compiled_script"];

    ASParameterDescriptionList sayHiParams =
      [[ASParameterDescriptionList alloc] init];
    [sayHiParams addParameterNamed:@"" // name doesn't matter; first parameter
        ofType:@"string"];
    [sayHiParams addParameterNamed:@"buttonNames"
        ofType:@"list"];

    ASReturnTypeDescription sayHiReturn = [[ASReturnTypeDescription alloc]
        initWithType:@"string"];

    [myScript registerSubroutineNamed:@"sayHiWithPromp"
        withParameters:sayHiParams
        returning:sayHiReturn];

    /*-------------------------*/

    Then, executing the sayHi subroutine would go like this:

    NSArray *buttonNames = [NSArray arrayWithObjects:@"OK", @"Cancel", nil];
    NSString *buttonClicked;
    buttonClicked = [myScript sayHiWithPrompt:@"Hello, World!"
        buttonNames:buttonNames];

    Easy, right?

    Any comments on this API? Am I missing a much more elegant way of doing
    this? Is there already something like this out there? One thing I'm
    particularly worried about is extensibility. There needs to be an easy
    way to add custom AppleScript->Cocoa object conversions, and I'm not
    sure what the best way to register these would be. There's also the
    issue of error handling: Apple's code returns an OSStatus, which the
    caller must then check to verify the subroutine executed properly.
    What's the "Cocoa way" of handling this? Returning "nil" on an error?

    Feedback is welcome. This little project may never actually see the
    light of day, but if I actually get it coded I'd like to provide it to
    the community. And if I don't, perhaps someone else will pick up the
    torch.

    --
    eric
    _______________________________________________
    cocoa-dev mailing list | <cocoa-dev...>
    Help/Unsubscribe/Archives: http://www.lists.apple.com/mailman/listinfo/cocoa-dev
    Do not post admin requests to the list. They will be ignored.
  • I didn't go as far as you propose in making it pretty to call, but I
    did do a lot of that work already.  I quite like the idea of using
    some forwardInvocation tricks to make the scripts masquerade as
    standard objects.  See what you think.

    <http://homepage.mac.com/kenferry/software.html#KFAppleScript>

    My computer's out of comission right now, but I definitely want to
    play around with your ideas when I can.

    -Ken
    _______________________________________________
    cocoa-dev mailing list | <cocoa-dev...>
    Help/Unsubscribe/Archives: http://www.lists.apple.com/mailman/listinfo/cocoa-dev
    Do not post admin requests to the list. They will be ignored.