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.