I have a Cocoa application with an AppleScript dictionary described in a .sdef XML file. All of the AppleScript classes, commands, etc. defined in the sdef are working property.
Except for my "submit form" command. The "submit form" command is my only command attempting to pass a parameter that is an arbitrary hashtable of information from AppleScript to Cocoa. I assume this should be done by passing an AppleScript record
which will be automatically converted to an NSDictionary
on the Cocoa side.
tell application "Fluidium"
tell selected tab of browser window 1
submit form with name "foo" with values {bar:"baz"}
end tell
end tell
The "with values" parameter is the record
-> NSDictionary
parameter i am having trouble with. Note that the keys of the record/dictionary cannot be known/defined in advance. They are arbitrary.
Here is the definition of this command in my 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>
And I have a "tab" object which responds to this command in the sdef:
<class name="tab" code="fTab" description="A browser tab.">
...
<responds-to command="submit form">
<cocoa method="handleSubmitFormCommand:"/>
</responds-to>
and Cocoa:
- (id)handleSubmitFormCommand:(NSScriptCommand *)cmd {
...
}
The "tab" object correctly responds to all the other AppleScript commands I have defined. The "tab" object also responds to the "submit form" command if I don't send the optional "with values" param. So I know I have the basics setup correctly. The only problem seems to be the arbitrary record
->NSDictionary
param.
When I execute the AppleScript above in AppleScript Editor.app
, I get this error on the Cocoa side:
+[NSDictionary scriptingRecordWithDescriptor:]: unrecognized selector sent to class 0x7fff707c6048
and this one on the AppleScript side:
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
Can anyone tell me what I'm missing? For reference the entire application is open source on GitHub:
http://github.com/itod/fluidium
Cocoa will seamlessly convert NSDictionary
objects to AppleScript (AS) records and the other way round for you, you only need to tell it how to do that.
First of all you need to define a record-type
in your scripting definition (.sdef
) file, e.g.
<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>
The name
is the name this value will have in the AS record. If the name equals the NSDictionary
key, no <cocoa>
tag is required (success
, method
, body
in the example above), if not, you can use a <cocoa>
tag to tell Cocoa the correct key for reading this value (in the example above, code
is the name in the AS record, but in the NSDictionary
the key will be replyCode
instead; I just made this for demonstration purposes here).
It is very important that you tell Cocoa what AS type this field shall have, otherwise Cocoa doesn't know how to transform that value to an AS value. All values are optional by default but if they are present, they must have the expected type. Here's a small table of how the most common Foundation types match to AS types (incomplete):
AS Type | Foundation Type
-------------+-----------------
boolean | NSNumber
date | NSDate
file | NSURL
integer | NSNumber
number | NSNumber
real | NSNumber
text | NSString
See Table 1-1 of Apple's "Introduction to Cocoa Scripting Guide"
Of course, a value can itself be another nested record, just define a record-type
for it, use the record-type
name in the property
specification and in the NSDictionary
the value must then be a matching dictionary.
Well, let's try a full sample. Let's define a simple HTTP get command in our .sdef
file:
<command name="http get" code="httpGET_">
<cocoa class="HTTPFetcher"/>
<direct-parameter type="text"
description="URL to fetch."
/>
<result type="http response"/>
</command>
Now we need to implement that command in Obj-C which is dead simple:
#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;
}
Of course, returning nil
in case of internal failures is bad error handling. We could return an error instead. Well, there are even special error handling methods for AS we could use here (e.g. setting certain properties we inherited from NSScriptCommand
), but it's just a sample after all.
Finally we need some AS code to test it:
tell application "MyCoolApp"
set httpResp to http get "http://badserver.invalid"
end tell
Result:
{success:false}
As expected, now one that succeeds:
tell application "MyCoolApp"
set httpResp to http get "http://stackoverflow.com"
end tell
Result:
{success:true, body:"<!DOCTYPE html>...", method:"GET", code:200}
Also as expected.
But wait, you wanted it the other way round, right? Okay, let's try that as well. We just reuse our type and make another command:
<command name="print http response" code="httpPRRE">
<cocoa class="HTTPResponsePrinter"/>
<direct-parameter type="http response"
description="HTTP response to print"
/>
</command>
And we implement that command as well:
#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
And we test it:
tell application "MyCoolApp"
set httpResp to http get "http://stackoverflow.com"
print http response httpResp
end tell
And her is what our app logs to console:
Dictionary is {
body = "<!DOCTYPE html>...";
method = GET;
replyCode = 200;
success = 1;
}
So, of course, it works both ways.
Well, you may complain now that this is not really arbitrary, after all you need to define which keys (may) exist and what type they will have if they exist. You are right. However, usually data is not that arbitrary, I mean, after all code must be able to understand it and therefor it must at least follow certain kind of rules and patterns.
If you really have no idea what data to expect, e.g. like a dump tool that just converts between two well defined data formats without any understand of the data itself, why do you pass it as a record at all? Why don't you just convert that record to an easily parse-able string value (e.g. Property List, JSON, XML, CSV), then pass it Cocoa as a string and finally convert it back to objects? This is a dead simple, yet very powerful approach. Parsing Property List or JSON in Cocoa is done with maybe four lines of code. Okay, it's maybe not the fastest approach but whoever mentions AppleScript and high performance in a single sentence already made a fundamental mistake to begin with; AppleScript certainly may be a lot but "fast" is none of the properties you can expect.
Right -- NSDictionaries and AppleScript records seem like they would mix, but they don't actually (NSDictionaries use object keys -- say strings) where AppleScript records use four letter character codes (thanks to their AppleEvent/Classic Mac OS heritage).
See this thread on Apple's AppleScript Implementer's mailing list
So, what you actually need to do, in your case, is to unpack the AppleScript record you have and translate it into your NSDictionary. You could write the code all by yourself, but it's complicated and dives deep into the AE manager.
However, this work has actually been done for you in some underlaying code for appscript/appscript-objc (appscript is an library for Python and Ruby and Objective-C that lets you communicate with AppleScriptable applications without actually having to use AppleScript. appscript-objc could be used where you would use Cocoa Scripting, but has less of the sucky limitations of that technology.)
The code is available on sourceforge. I submitted a patch a few weeks ago to the author so you could build JUST the underlaying foundation for appscript-objc, which is all you need in this case: all you need to do is pack and unpack Applescript/AppleEvent records.
For other googlers, there's another way to do this, that's not using appscript: ToxicAppleEvents. There's a method in there that translates dictionaries into Apple Event Records.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With