Как передать произвольную запись AppleScript в какао в приложении с поддержкой скриптов?
У меня есть приложение Какао со словарем AppleScript, описанным в XML-файле.sdef. Все классы AppleScript, команды и т. Д., Определенные в sdef, являются рабочим свойством.
За исключением моей команды "отправить форму". Команда "отправить форму" - моя единственная команда, пытающаяся передать параметр, представляющий собой произвольный хеш-таблицу информации, от AppleScript до какао. Я предполагаю, что это должно быть сделано путем передачи AppleScript record
который будет автоматически преобразован в NSDictionary
на стороне какао.
tell application "Fluidium"
tell selected tab of browser window 1
submit form with name "foo" with values {bar:"baz"}
end tell
end tell
Параметр "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>
И у меня есть объект "tab", который отвечает на эту команду в sdef:
<class name="tab" code="fTab" description="A browser tab.">
...
<responds-to command="submit form">
<cocoa method="handleSubmitFormCommand:"/>
</responds-to>
и какао:
- (id)handleSubmitFormCommand:(NSScriptCommand *)cmd {
...
}
Объект "tab" правильно реагирует на все другие команды AppleScript, которые я определил. Объект "tab" также отвечает на команду "submit form", если я не отправляю необязательный параметр "with values". Так что я знаю, что правильно настроил основы. Единственная проблема, кажется, произвольная record
->NSDictionary
пары.
Когда я выполняю AppleScript выше в AppleScript Editor.app
, Я получаю эту ошибку на стороне какао:
+[NSDictionary scriptingRecordWithDescriptor:]: unrecognized selector sent to class 0x7fff707c6048
и этот на стороне 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
Кто-нибудь может сказать мне, что мне не хватает? Для справки все приложение с открытым исходным кодом на GitHub:
4 ответа
Справа - NSDictionaries и записи AppleScript кажутся смешанными, но на самом деле это не так (NSDictionaries используют объектные ключи - скажем, строки), где записи AppleScript используют четырехбуквенные коды символов (благодаря их наследию AppleEvent/Classic Mac OS).
Посмотрите эту ветку в списке рассылки AppleScript Developer.
Итак, в действительности вам нужно распаковать имеющуюся у вас запись AppleScript и перевести ее в свой NSDictionary. Вы можете написать код самостоятельно, но он сложен и глубоко погружается в менеджер AE.
Однако эта работа фактически была проделана для вас в некотором нижележащем коде для appscript / appscript-objc (appscript - это библиотека для Python, Ruby и Objective-C, которая позволяет вам общаться с приложениями AppleScriptable без необходимости использовать AppleScript. Appscript-objc. может использоваться там, где вы будете использовать сценарии Какао, но имеет меньше отвратительных ограничений этой технологии.)
Код доступен на sourceforge. Я представил патч несколько недель назад автору, чтобы вы могли создать JUST основополагающую основу для appscript-objc, и все, что вам нужно в этом случае: все, что вам нужно сделать, это упаковать и распаковать записи Applescript/AppleEvent.
Для других googlers есть еще один способ сделать это, не используя appscript: ToxicAppleEvents. Там есть метод, который переводит словари в Apple Event Records.
Какао будет легко конвертировать 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>
name
имя, которое будет иметь это значение в записи AS. Если имя равно NSDictionary
ключ, нет <cocoa>
тег обязателен (success
, method
, body
в приведенном выше примере), если нет, вы можете использовать <cocoa>
тег, чтобы сказать Какао правильный ключ для чтения этого значения (в примере выше, code
это имя в записи AS, но в NSDictionary
ключ будет replyCode
вместо; Я просто сделал это для демонстрационных целей здесь).
Очень важно, чтобы вы указали Какао, какой тип AS должен иметь это поле, иначе Какао не знает, как преобразовать это значение в значение AS. Все значения являются необязательными по умолчанию, но если они присутствуют, они должны иметь ожидаемый тип. Вот небольшая таблица того, как наиболее распространенные типы Foundation соответствуют типам AS (неполные):
AS Type | Foundation Type
-------------+-----------------
boolean | NSNumber
date | NSDate
file | NSURL
integer | NSNumber
number | NSNumber
real | NSNumber
text | NSString
См. Таблицу 1-1 Apple "Руководство по написанию сценариев для какао".
Конечно, само значение может быть другой вложенной записью, просто определите record-type
для этого используйте record-type
имя в property
спецификация и в NSDictionary
значение должно быть соответствующим словарем.
Что ж, давайте попробуем полный образец. Давайте определим простую команду HTTP get в нашем .sdef
файл:
<command name="http get" code="httpGET_">
<cocoa class="HTTPFetcher"/>
<direct-parameter type="text"
description="URL to fetch."
/>
<result type="http response"/>
</command>
Теперь нам нужно реализовать эту команду в 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;
}
Конечно, возвращаясь nil
в случае внутренних сбоев плохая обработка ошибок. Мы могли бы вернуть ошибку вместо. Ну, есть даже специальные методы обработки ошибок для AS, которые мы могли бы использовать здесь (например, установка определенных свойств, которые мы унаследовали от NSScriptCommand
), но это всего лишь образец.
Наконец, нам нужен AS-код для его проверки:
tell application "MyCoolApp"
set httpResp to http get "http://badserver.invalid"
end tell
Результат:
{success:false}
Как и ожидалось, теперь тот, который преуспевает:
tell application "MyCoolApp"
set httpResp to http get "http://stackru.com"
end tell
Результат:
{success:true, body:"<!DOCTYPE html>...", method:"GET", code:200}
Также, как и ожидалось.
Но подождите, вы хотели это наоборот, верно? Хорошо, давайте попробуем это тоже. Мы просто повторно используем наш тип и создаем другую команду:
<command name="print http response" code="httpPRRE">
<cocoa class="HTTPResponsePrinter"/>
<direct-parameter type="http response"
description="HTTP response to print"
/>
</command>
И мы реализуем эту команду также:
#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
И мы проверяем это:
tell application "MyCoolApp"
set httpResp to http get "http://stackru.com"
print http response httpResp
end tell
И это то, что наше приложение регистрирует на консоли:
Dictionary is {
body = "<!DOCTYPE html>...";
method = GET;
replyCode = 200;
success = 1;
}
Так что, конечно, это работает в обе стороны.
Что ж, теперь вы можете жаловаться, что это не совсем произвольно, ведь вам нужно определить, какие ключи (могут) существовать и какого типа они будут иметь, если они существуют. Вы правы. Однако, как правило, данные не настолько произвольны, я имею в виду, в конце концов, код должен уметь их понимать и поэтому должен, по крайней мере, следовать определенным правилам и шаблонам.
Если вы действительно не представляете, какие данные ожидать, например, как инструмент дампа, который просто конвертирует между двумя четко определенными форматами данных без какого-либо понимания самих данных, почему вы вообще передаете их как запись? Почему бы вам просто не преобразовать эту запись в легко разбираемое строковое значение (например, список свойств, JSON, XML, CSV), затем передать ее в виде строки Какао и, наконец, преобразовать ее обратно в объекты? Это очень простой, но очень мощный подход. Синтаксический анализ списка свойств или JSON в Какао выполняется, возможно, с четырьмя строками кода. Хорошо, возможно, это не самый быстрый подход, но тот, кто упоминает AppleScript и высокую производительность в одном предложении, уже совершил фундаментальную ошибку; AppleScript, конечно, может быть много, но "быстрый" - это не то свойство, которое вы можете ожидать.
Если вы знаете, что поля в словаре, который вы упаковываете, и типы ключей, которые вы хотите отобразить в / из AppleScript, предсказуемы, кажется, что лучшее решение - это использовать определение записи, как отмечено в другом ответе, который также содержит полезные ссылки на документацию Apple. что я по крайней мере лично пропустил из руководства по написанию сценариев.
Если вышеуказанные требования по какой-либо причине не соответствуют вашим потребностям, альтернативным решением является внедрение +scriptingRecordWithDescriptor:
как категория для NSDictionary. Я нашел это решение в проекте Fluidium, о котором идет речь. Вот паста из NSDictionary + FUScripting.m:
@implementation NSDictionary (FUScripting)
+ (id)scriptingRecordWithDescriptor:(NSAppleEventDescriptor *)inDesc {
//NSLog(@"inDesc: %@", inDesc);
NSMutableDictionary *d = [NSMutableDictionary dictionary];
NSAppleEventDescriptor *withValuesParam = [inDesc descriptorForKeyword:'usrf']; // 'usrf' keyASUserRecordFields
//NSLog(@"withValuesParam: %@", withValuesParam);
NSString *name = nil;
NSString *value = nil;
// this is 1-indexed!
NSInteger i = 1;
NSInteger count = [withValuesParam numberOfItems];
for ( ; i <= count; i++) {
NSAppleEventDescriptor *desc = [withValuesParam descriptorAtIndex:i];
//NSLog(@"descriptorAtIndex: %@", desc);
NSString *s = [desc stringValue];
if (name) {
value = s;
[d setObject:value forKey:name];
name = nil;
value = nil;
} else {
name = s;
}
}
return [d copy];
}
@end
Я могу подтвердить, что использование +scriptingRecordWithDecriptor: с пользовательской командой эквивалентного вида работало для меня.
11.9.2016, Mac OS 10.11.6 Проблема: как преобразовать запись AppleScript в NSDictionary в мире какао?
В записи AppleScript свойства AppleScript используются в качестве ключей, а числа или строки - в качестве значений.
NSDictionary использует соответствующие ключи какао в качестве ключей (в форме объектов NSString) и значений NSNumber или NSString для четырех самых основных типов в записи AppleScript: string, integer, double и boolean.
Предлагаемое решение для + (id)scriptingRecordWithDescriptor:(NSAppleEventDescriptor *)inDesc не работает в моем случае.
Основное изменение в моей реализации состоит в том, что каждый класс в среде AppleScript определяет свои собственные свойства и коды AppleScript. Ключевым объектом для определения является NSScriptClassDescription, который содержит взаимосвязь между кодами AppleScript и ключами Какао. Дополнительным осложнением является то, что NSAppleEventDescriptor, используемый в качестве параметра в методе, представляет входящую запись AppleScript (или список записей в моем случае). Этот NSAppleEventDescriptor может иметь разные формы.
Одна запись в записи AppleScript является специальной: {class:"имя класса скрипта"}. Коды тестируют на наличие.
Единственная замена, которую вы должны сделать в коде, - это ввести название пакета AppleScript вашего приложения для "Имя вашего набора сценариев Apple" . Метод реализован в виде категории на NSDictionary
#import "NSDictionary+AppleScript.h"
@implementation NSDictionary (AppleScript)
// returns a Dictionary from a apple script record
+ (NSArray <NSDictionary *> * )scriptingRecordWithDescriptor:(NSAppleEventDescriptor *)anEventDescriptor {
NSScriptSuiteRegistry * theRegistry = [NSScriptSuiteRegistry sharedScriptSuiteRegistry] ;
DescType theScriptClassDescriptor = [anEventDescriptor descriptorType] ;
DescType printDescriptorType = NSSwapInt(theScriptClassDescriptor) ;
NSString * theEventDescriptorType = [[NSString alloc] initWithBytes:&printDescriptorType length:sizeof(DescType) encoding:NSUTF8StringEncoding] ;
//NSLog(@"Event descriptor type: %@", theEventDescriptorType) ; // "list" if a list, "reco" if a simple record , class identifier if a class
// Forming a list of AppleEventDescriptors
NSInteger i ;
NSAppleEventDescriptor * aDescriptor ;
NSMutableArray <NSAppleEventDescriptor*> * listOfEventDescriptors = [NSMutableArray array] ;
if ([theEventDescriptorType isEqualToString:@"list"]) {
NSInteger numberOfEvents = [anEventDescriptor numberOfItems] ;
for (i = 1 ; i <= numberOfEvents ; i++) {
aDescriptor = [anEventDescriptor descriptorAtIndex:i] ;
if (aDescriptor) [listOfEventDescriptors addObject:aDescriptor] ;
}
}
else [listOfEventDescriptors addObject:anEventDescriptor] ;
// transforming every NSAppleEventDescriptor into an NSDictionary - key: cocoa key - object: NSString - the parameter value as string
NSMutableArray <NSDictionary *> * theResult = [NSMutableArray arrayWithCapacity:listOfEventDescriptors.count] ;
for (aDescriptor in listOfEventDescriptors) {
theScriptClassDescriptor = [aDescriptor descriptorType] ;
DescType printDescriptorType = NSSwapInt(theScriptClassDescriptor) ;
NSString * theEventDescriptorType = [[NSString alloc] initWithBytes:&printDescriptorType length:sizeof(DescType) encoding:NSUTF8StringEncoding] ;
//NSLog(@"Event descriptor type: %@", theEventDescriptorType) ;
NSMutableDictionary * aRecord = [NSMutableDictionary dictionary] ;
NSInteger numberOfAppleEventItems = [aDescriptor numberOfItems] ;
//NSLog(@"Number of items: %li", numberOfAppleEventItems) ;
NSScriptClassDescription * (^determineClassDescription)() = ^NSScriptClassDescription *() {
NSScriptClassDescription * theResult ;
NSDictionary * theClassDescriptions = [theRegistry classDescriptionsInSuite:@"Arcadiate Suite"] ;
NSArray * allClassDescriptions = theClassDescriptions.allValues ;
NSInteger numOfClasses = allClassDescriptions.count ;
if (numOfClasses == 0) return theResult ;
NSMutableData * thePropertiesCounter = [NSMutableData dataWithLength:(numOfClasses * sizeof(NSInteger))] ;
NSInteger *propertiesCounter = [thePropertiesCounter mutableBytes] ;
AEKeyword aKeyWord ;
NSInteger classCounter = 0 ;
NSScriptClassDescription * aClassDescription ;
NSInteger i ;
NSString * aCocoaKey ;
for (aClassDescription in allClassDescriptions) {
for (i = 1 ; i <= numberOfAppleEventItems ; i++) {
aKeyWord = [aDescriptor keywordForDescriptorAtIndex:i] ;
aCocoaKey = [aClassDescription keyWithAppleEventCode:aKeyWord] ;
if (aCocoaKey.length > 0) propertiesCounter[classCounter] ++ ;
}
classCounter ++ ;
}
NSInteger maxClassIndex = NSNotFound ;
for (i = 0 ; i < numOfClasses ; i++) {
if (propertiesCounter[i] > 0) {
if (maxClassIndex != NSNotFound) {
if (propertiesCounter[i] > propertiesCounter[maxClassIndex]) maxClassIndex = i ;
}
else maxClassIndex = i ;
}
}
//NSLog(@"Max class index: %li", maxClassIndex) ;
//if (maxClassIndex != NSNotFound) NSLog(@"Number of matching properties: %li", propertiesCounter[maxClassIndex]) ;
if (maxClassIndex != NSNotFound) theResult = allClassDescriptions[maxClassIndex] ;
return theResult ;
} ;
NSScriptClassDescription * theRelevantScriptClass ;
if ([theEventDescriptorType isEqualToString:@"reco"]) theRelevantScriptClass = determineClassDescription() ;
else theRelevantScriptClass = [theRegistry classDescriptionWithAppleEventCode:theScriptClassDescriptor] ;
if (theRelevantScriptClass) {
//NSLog(@"Targeted Script Class: %@", theRelevantScriptClass) ;
NSString * aCocoaKey, *stringValue ;
NSInteger integerValue ;
BOOL booleanValue ;
id aValue ;
stringValue = [theRelevantScriptClass implementationClassName] ;
if (stringValue.length > 0) aRecord[@"className"] = aValue ;
AEKeyword aKeyWord ;
NSAppleEventDescriptor * parameterDescriptor ;
NSString * printableParameterDescriptorType ;
DescType parameterDescriptorType ;
for (i = 1 ; i <= numberOfAppleEventItems ; i++) {
aValue = nil ;
aKeyWord = [aDescriptor keywordForDescriptorAtIndex:i] ;
aCocoaKey = [theRelevantScriptClass keyWithAppleEventCode:aKeyWord] ;
parameterDescriptor = [aDescriptor paramDescriptorForKeyword:aKeyWord] ;
parameterDescriptorType = [parameterDescriptor descriptorType] ;
printDescriptorType = NSSwapInt(parameterDescriptorType) ;
printableParameterDescriptorType = [[NSString alloc] initWithBytes:&printDescriptorType length:sizeof(DescType) encoding:NSUTF8StringEncoding] ;
//NSLog(@"Parameter type: %@", printableParameterDescriptorType) ;
if ([printableParameterDescriptorType isEqualToString:@"doub"]) {
stringValue = [parameterDescriptor stringValue] ;
if (stringValue.length > 0) {
aValue = @([stringValue doubleValue]) ;
}
}
else if ([printableParameterDescriptorType isEqualToString:@"long"]) {
integerValue = [parameterDescriptor int32Value] ;
aValue = @(integerValue) ;
}
else if ([printableParameterDescriptorType isEqualToString:@"utxt"]) {
stringValue = [parameterDescriptor stringValue] ;
if (stringValue.length > 0) {
aValue = stringValue ;
}
}
else if ( ([printableParameterDescriptorType isEqualToString:@"true"]) || ([printableParameterDescriptorType isEqualToString:@"fals"]) ) {
booleanValue = [parameterDescriptor booleanValue] ;
aValue = @(booleanValue) ;
}
else {
stringValue = [parameterDescriptor stringValue] ;
if (stringValue.length > 0) {
aValue = stringValue ;
}
}
if ((aCocoaKey.length != 0) && (aValue)) aRecord[aCocoaKey] = aValue ;
}
}
[theResult addObject:aRecord] ;
}
return theResult ;
}
@end