如何在可编写脚本的应用程序中将任意AppleScript记录传递给Cocoa?

Tod*_*orf 5 macos cocoa applescript cocoa-scripting

我有一个Cocoa应用程序,其中包含.sdef XML文件中描述的AppleScript字典.sdef中定义的所有AppleScript类,命令等都是工作属性.

除了我的"提交表单"命令."提交表单"命令是我唯一的命令,它尝试将一个参数传递给AppleScript,该参数是从AppleScript到Cocoa的任意哈希表.我认为这应该通过传递AppleScript来完成,AppleScript record将自动转换为NSDictionaryCocoa端.

tell application "Fluidium"
    tell selected tab of browser window 1
        submit form with name "foo" with values {bar:"baz"}
    end tell
end tell
Run Code Online (Sandbox Code Playgroud)

"with values"参数是record- > NSDictionary参数我遇到了麻烦.请注意,记录/字典的键不能事先知道/定义.他们是任意的.

以下是我的sdef XML中此命令的定义:

<command name="submit form" code="FuSSSbmt" description="...">
    <direct-parameter type="specifier" optional="yes" description="..."/>
    <parameter type="text" name="with name" code="Name" optional="yes" description="...">
        <cocoa key="name"/>
    </parameter>
    <parameter type="record" name="with values" code="Vals" optional="yes" description="...">
        <cocoa key="values"/>
    </parameter>
</command>
Run Code Online (Sandbox Code Playgroud)

我有一个"tab"对象,它响应sdef中的这个命令:

<class name="tab" code="fTab" description="A browser tab.">
    ...
    <responds-to command="submit form">
        <cocoa method="handleSubmitFormCommand:"/>
    </responds-to>
Run Code Online (Sandbox Code Playgroud)

和可可:

- (id)handleSubmitFormCommand:(NSScriptCommand *)cmd {
    ...
}
Run Code Online (Sandbox Code Playgroud)

"tab"对象正确响应我定义的所有其他AppleScript命令.如果我不发送可选的"with values"参数,"tab"对象也会响应"submit form"命令.所以我知道我已经正确设置了基础知识.唯一的问题似乎是任意record- > NSDictionaryparam.

当我执行上面的AppleScript时AppleScript Editor.app,我在Cocoa端出现了这个错误:

+[NSDictionary scriptingRecordWithDescriptor:]: unrecognized selector sent to class 0x7fff707c6048
Run Code Online (Sandbox Code Playgroud)

这个在AppleScript方面:

error "Fluidium got an error: selected tab of browser window 1 doesn’t understand the submit form message." number -1708 from selected tab of browser window 1
Run Code Online (Sandbox Code Playgroud)

谁能告诉我我错过了什么?作为参考,整个应用程序是GitHub上的开源:

http://github.com/itod/fluidium

Mec*_*cki 6

Cocoa会将NSDictionary对象无缝转换为AppleScript(AS)记录,反之亦然,您只需要告诉它如何做到这一点.

首先,您需要record-type在脚本定义(.sdef)文件中定义一个,例如

<record-type  name="http response" code="HTRE">
    <property name="success" code="HTSU" type="boolean"
        description="Was the HTTP call successful?"
    />

    <property name="method" code="HTME" type="text"
        description="Request method (GET|POST|...)."
    />

    <property name="code" code="HTRC" type="integer"
        description="HTTP response code (200|404|...)."
    >
        <cocoa key="replyCode"/>
    </property>

    <property name="body" code="HTBO" type="text"
        description="The body of the HTTP response."
    />
</record-type>
Run Code Online (Sandbox Code Playgroud)

name是该值在AS记录中的名称.如果名称等于NSDictionary关键,没有<cocoa>标签要求(success,method,body在上面的例子),如果没有,你可以使用一个<cocoa>标签来告诉可可正确的密钥读取该值(在上面的例子中,code在作为名称记录,但在NSDictionary密钥将是replyCode相反;我只是为了演示目的这里做了这个).

告诉Cocoa这个字段应该具有哪种AS类型非常重要,否则Cocoa不知道如何将该值转换为AS值.默认情况下,所有值都是可选的,但如果它们存在,则它们必须具有预期的类型.这是一个关于最常见的Foundation类型如何与AS类型匹配的小表(不完整):

 AS Type     | Foundation Type
-------------+-----------------
 boolean     | NSNumber
 date        | NSDate
 file        | NSURL
 integer     | NSNumber
 number      | NSNumber
 real        | NSNumber
 text        | NSString
Run Code Online (Sandbox Code Playgroud)

请参阅表1-1苹果"的介绍可可脚本指南 "

当然,一个值本身可以是另一个嵌套记录,只需record-type为它定义一个,record-typeproperty规范中使用该名称,NSDictionary然后该值必须是匹配的字典.

好吧,让我们试试一个完整的样本.让我们在.sdef文件中定义一个简单的HTTP get命令:

<command name="http get" code="httpGET_">
    <cocoa class="HTTPFetcher"/>
    <direct-parameter type="text"
        description="URL to fetch."
    />
    <result type="http response"/>
</command>
Run Code Online (Sandbox Code Playgroud)

现在我们需要在Obj-C中实现该命令,这很简单:

#import <Foundation/Foundation.h>

// The code below assumes you are using ARC (Automatic Reference Counting).
// It will leak memory if you don't!

// We just subclass NSScriptCommand
@interface HTTPFetcher : NSScriptCommand
@end


@implementation HTTPFetcher

static NSString
    *const SuccessKey   = @"success",
    *const MethodKey    = @"method",
    *const ReplyCodeKey = @"replyCode",
    *const BodyKey      = @"body"
;

// This is the only method we must override
- (id)performDefaultImplementation {
    // We expect a string parameter
    id directParameter = [self directParameter];
    if (![directParameter isKindOfClass:[NSString class]]) return nil;

    // Valid URL?
    NSString * urlString = directParameter;
    NSURL * url = [NSURL URLWithString:urlString];
    if (!url) return @{ SuccessKey : @(false) };

    // We must run synchronously, even if that blocks main thread
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    if (!sem) return nil;

    // Setup the simplest HTTP get request possible.
    NSURLRequest * req = [NSURLRequest requestWithURL:url];
    if (!req) return nil;

    // This is where the final script result is stored.
    __block NSDictionary * result = nil;

    // Setup a data task
    NSURLSession * ses = [NSURLSession sharedSession];
    NSURLSessionDataTask * tsk = [ses dataTaskWithRequest:req
        completionHandler:^(
            NSData *_Nullable data,
            NSURLResponse *_Nullable response,
            NSError *_Nullable error
        ) {
            if (error) {
                result = @{ SuccessKey : @(false) };

            } else {
                NSHTTPURLResponse * urlResp = (
                    [response isKindOfClass:[NSHTTPURLResponse class]] ?
                    (NSHTTPURLResponse *)response : nil
                );

                // Of course that is bad code! Instead of always assuming UTF8
                // encoding, we should look at the HTTP headers and see if
                // there is a charset enconding given. If we downloaded a
                // webpage it may also be found as a meta tag in the header
                // section of the HTML. If that all fails, we should at
                // least try to guess the correct encoding.
                NSString * body = (
                    data ?
                    [[NSString alloc]
                        initWithData:data encoding:NSUTF8StringEncoding
                    ]
                    : nil
                );

                NSMutableDictionary * mresult = [
                    @{ SuccessKey: @(true),
                        MethodKey: req.HTTPMethod
                    } mutableCopy
                ];
                if (urlResp) {
                    mresult[ReplyCodeKey] = @(urlResp.statusCode);
                }
                if (body) {
                    mresult[BodyKey] = body;
                }
                result = mresult;
            }

            // Unblock the main thread
            dispatch_semaphore_signal(sem);
        }
    ];
    if (!tsk) return nil;

    // Start the task and wait until it has finished
    [tsk resume];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

    return result;
}
Run Code Online (Sandbox Code Playgroud)

当然,nil在内部故障的情况下返回是错误的错误处理.我们可能会返回错误.好吧,甚至还有我们可以在这里使用的AS的特殊错误处理方法(例如设置我们继承的某些属性NSScriptCommand),但它毕竟只是一个样本.

最后我们需要一些AS代码来测试它:

tell application "MyCoolApp"
    set httpResp to http get "http://badserver.invalid"
end tell
Run Code Online (Sandbox Code Playgroud)

结果:

{success:false}
Run Code Online (Sandbox Code Playgroud)

正如所料,现在成功:

tell application "MyCoolApp"
    set httpResp to http get "http://stackoverflow.com"
end tell
Run Code Online (Sandbox Code Playgroud)

结果:

{success:true, body:"<!DOCTYPE html>...",  method:"GET", code:200}
Run Code Online (Sandbox Code Playgroud)

也如预期的那样.

但是等等,你反过来想要它,对吧?好的,我们也试试吧.我们只是重用我们的类型并制作另一个命令:

<command name="print http response" code="httpPRRE">
    <cocoa class="HTTPResponsePrinter"/>
    <direct-parameter type="http response"
        description="HTTP response to print"
    />
</command>
Run Code Online (Sandbox Code Playgroud)

我们也实现了这个命令:

#import <Foundation/Foundation.h>

@interface HTTPResponsePrinter : NSScriptCommand
@end


@implementation HTTPResponsePrinter

- (id)performDefaultImplementation {
    // We expect a dictionary parameter
    id directParameter = [self directParameter];
    if (![directParameter isKindOfClass:[NSDictionary class]]) return nil;

    NSDictionary * dict = directParameter;
    NSLog(@"Dictionary is %@", dict);
    return nil;
}

@end
Run Code Online (Sandbox Code Playgroud)

我们测试它:

tell application "MyCoolApp"
    set httpResp to http get "http://stackoverflow.com"
    print http response httpResp
end tell
Run Code Online (Sandbox Code Playgroud)

而她就是我们的应用程序登录到控制台:

Dictionary is {
    body = "<!DOCTYPE html>...";
    method = GET;
    replyCode = 200;
    success = 1;
}
Run Code Online (Sandbox Code Playgroud)

所以,当然,它有两种方式.

好了,现在你可以抱怨,这是不是真的武断,毕竟你需要定义键(可能)存在,如果存在的话,他们将有什么样的类型.你是对的.但是,通常数据并不是任意的,我的意思是,在所有代码必须能够理解它之后,它必须至少遵循某种规则和模式.

如果您真的不知道期望什么数据,例如像转储工具只是在两种定义良好的数据格式之间转换而不了解数据本身,为什么要将它作为记录传递?为什么不直接将该记录转换为易于解析的字符串值(例如Property List,JSON,XML,CSV),然后将Cocoa作为字符串传递并最终将其转换回对象?这是一种简单而又非常强大的方法.在Cocoa中解析属性列表或JSON可能使用四行代码完成.好吧,它可能不是最快的方法,但是只要一句话提到AppleScript和高性能,就已经犯了一个根本性的错误; AppleScript当然可能会很多,但"快速"并不是您所期望的属性.


Rya*_*cox 3

正确 - NSDictionaries 和 AppleScript 记录看起来像是会混合在一起,但实际上它们不会混合(NSDictionaries 使用对象键 - 比如说字符串),而 AppleScript 记录使用四个字母字符代码(感谢它们的 AppleEvent/Classic Mac OS 传统)。

请参阅Apple 的 AppleScript Implementer 邮件列表中的此主题

因此,就您的情况而言,您实际需要做的是解压您拥有的 AppleScript 记录并将其翻译到您的 NSDictionary 中。您可以自己编写代码,但它很复杂并且深入到 AE 管理器中。

然而,这项工作实际上已经在appscript/appscript-objc的一些底层代码中为您完成了(appscript 是一个用于 Python、Ruby 和 Objective-C 的库,可让您与 AppleScriptable 应用程序进行通信,而无需实际使用 AppleScript。appscript-objc可以在使用 Cocoa 脚本的地方使用,但该技术的糟糕限制较少。)

该代码可在 sourceforge 上找到。几周前我向作者提交了一个补丁,这样您就可以构建 appscript-objc 的底层基础,在这种情况下,这就是您所需要的:您所需要做的就是打包和解包 Applescript/AppleEvent 记录。

对于其他谷歌用户,还有另一种方法可以做到这一点,那就是不使用 appscript: ToxicAppleEvents。那里有一个方法可以将字典翻译成苹果事件记录。