How do I pass an arbitrary AppleScript entry to Cocoa in a scripting application?

I have a Cocoa application with an AppleScript dictionary described in an XML .sdef file. All AppleScript classes, commands, etc. Defined in sdef are operational.

Except for my submit form command. The submit form command is my only command trying to pass a parameter, which is an arbitrary hash table of information from AppleScript to Cocoa. I assume this should be done by passing an AppleScript record that 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 that I came across. Please note that the dictionary / dictionary keys cannot be known / defined in advance. They are arbitrary.

Here is the definition of this command in my sdef xml file:

 <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 that responds to this command in 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 responds correctly to all other AppleScript commands that I defined. The tab object also responds to the submit form command if I do not submit the optional parameter with values. Therefore, I know that I have set up the basics correctly. The only problem is the arbitrary parameter record β†’ NSDictionary .

When I execute AppleScript above in the AppleScript Editor.app , I get this error on the Cocoa side:

 +[NSDictionary scriptingRecordWithDescriptor:]: unrecognized selector sent to class 0x7fff707c6048 

and this one from the side of 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 

Can someone tell me what I am missing? For reference, the entire application is open source on GitHub:

http://github.com/itod/fluidium

+4
source share
4 answers

To the right - NSdictionaries and AppleScript entries look as if they will mix, but are not actually used (NSDictionaries use object keys β€” say, strings) where AppleScript entries use four-letter character codes (thanks to their legacy AppleEvent / Classic Mac OS) .

See this thread on the Apple AppleScript mailing list

So what you really need to do, in your case, is to unzip the AppleScript record you recorded and translate it into your NSDictionary. You can write the code yourself, but it gets complicated and dives deep into the AE manager.

However, this work was really done for you in some code under the index for appscript / appscript-objc (appscript is a library for Python and Ruby and Objective-C, which allows you to communicate with AppleScriptable applications without having to use AppleScript. -objc can be used where you would use Cocoa Scripting, but with less restrictions this technology is less.)

Code is available at sourceforge . A few weeks ago, I sent a patch to the author so you can create a JUST base framework for appscript-objc that you need in this case: all you have to do is pack and unzip the Applescript / AppleEvent entries.

For other googlers, there is another way to do this so as not to use appscript: ToxicAppleEvents . There is a method that translates dictionaries into Apple Event Records.

+2
source

Cocoa will easily convert NSDictionary objects to AppleScript (AS) records and vice versa for you, you only need to tell how to do it.

First of all, you need to define the record-type in the script definition file ( .sdef ), for example

 <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 is the name that will have a value in the AS record. If the name is equal to the NSDictionary key, then the <cocoa> tag is not required ( success , method , body in the above example), if not, you can use the <cocoa> tag to tell Cocoa the correct key to read this value (in the above example code is the name in the AS record, but in NSDictionary there will be replyCode , I just did it for demo purposes here).

It is very important that you tell Cocoa that the AS type must have this field, otherwise Cocoa does not know how to convert this value to an AS value. All values ​​are optional by defualt, but if present, they should be of the expected type. Here's a short table of how the most common Foundation types correspond to AS (incomplete) types:

  AS Type | Foundation Type -------------+----------------- boolean | NSNumber date | NSDate file | NSURL integer | NSNumber number | NSNumber real | NSNumber text | NSString 

See Apple Table 1-1 , β€œAn Introduction to Cocoa Scripting Guide”

Of course, the value itself may be another nested entry, just define a record-type for it, use the name record-type in the property specification, and in NSDictionary value should be a suitable dictionary.

Ok, try the full sample. Let us 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 this command in Obj-C, which is 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 an incorrect error handling. Instead, we could return an error. Well, there are even special error handling methods for AS that we could use here (for example, setting some properties that we inherited from NSScriptCommand ), but this is just a sample after all.

Finally, we need the AS code to verify it:

 tell application "MyCoolApp" set httpResp to http get "http://badserver.invalid" end tell 

Result:

 {success:false} 

As expected, now the 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} 

As expected.

But wait, you wanted everything to be the other way around, right? Okay, try this. 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 also implement this 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 

And we check it:

 tell application "MyCoolApp" set httpResp to http get "http://stackoverflow.com" print http response httpResp end tell 

And her application is registered in the console:

 Dictionary is { body = "<!DOCTYPE html>..."; method = GET; replyCode = 200; success = 1; } 

So this, of course, works both ways.

Well, now you can complain that this is not arbitrary, because you need to determine what keys (can) exist and what type they will have, if they exist. You're right. However, as a rule, the data is not arbitrary, I mean that after the code must be able to understand, and therefore it must at least follow certain rules and patterns.

If you really have no idea what data to expect, for example. as a dump tool that simply converts two well-defined data formats without any understanding of the data itself, why do you pass it as a record at all? Why don't you just convert this entry to a simple syntactic string value (like a list of properties, JSON, XML, CSV), and then pass it to Cocoa as a string and finally convert back to objects? This is a simple but very powerful approach. Cocoa's list of Parsing or JSON properties is done with perhaps four lines of code. Well, perhaps this is not the fastest approach, but anyone who mentions AppleScript and high performance in one sentence has already made a fundamental mistake to start with; AppleScript, of course, can be many, but "fast" is none of the properties you can expect.

+3
source

If you know the fields in the dictionary that you wrap and the types of keys that you want to display on / from AppleScript are predictable, it seems like the best solution to use the definition entry as indicated in another answer, which also helps link to Apple's documentation, which I at least personally missed out the scripting guide.

If the above requirements do not meet your needs for any reason, an alternative solution is to implement +scriptingRecordWithDescriptor: as categories for NSDictionary. I found this solution in the Fluidium project in question. Here's the insert from 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 

I can confirm that using + scriptingRecordWithDecriptor: with a special command of the equivalent kind worked for me.

+2
source

09/11/2016, Mac OS 10.11.6 The problem is this: how to convert an AppleScript entry in NSDictionary to cocoa?

An AppleScript entry uses AppleScript properties as keys, numbers, or strings as values.

An NSDictionary uses the corresponding cocoa keys as keys (in the form of NSString objects) and NSNumber or NSString values ​​for the four main types in an AppleScript entry: string, integer, double, and boolean.

Suggested solution for scripts + (id) RecordWithDescriptor: (NSAppleEventDescriptor *) inDesc does not work in my case.

The main change in my implementation is that each class in the AppleScript environment defines its own properties and AppleScript codes. The key object to define is NSScriptClassDescription, which contains the relationship between AppleScript codes and cocoa keys. An additional complication is that the NSAppleEventDescriptor used as a parameter in the method is an incoming AppleScript entry (or a list of entries in my case). This NSAppleEventDescriptor can take many forms.

One entry in an AppleScript entry is special: {class: "script class name"}. Codes check its presence.

The only replacement you have to make in the code is to enter the name of your AppleScript application for "The name of your apple script suite." This method is implemented as a category in 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 
0
source

Source: https://habr.com/ru/post/1299204/


All Articles