Making AppleScript Records Dynamic using ASObjc

###Making AppleScript Records Dynamic using ASObjc
NOTE: This post started out as a response to this script posted by @ComplexPoint (Rob):
Listing record keys in “Execute an AppleScript” actions

Since it evolved into a complete solution, and a reusable method, it has been moved to a new topic.



[quote="JMichaelTX, post:2, topic:4080"]
Can you help us with the next step:  Getting the value from the record when the key is in a variable:
[/quote]

[quote="ComplexPoint, post:3, topic:4080"]
The next step is probably to switch to JavaScript, in which querying a dictionary is rather simpler:
[/quote]

JavaScript does offer a number of advantages in using "records" (JS objects) and arrays.

But if one needs to stay in AppleScript, then here's a solution that is not all that complex, if you understand ASObjC (which I barely do).  

But thanks to Shane Stanley's great book:
[Everyday AppleScriptObjC, Third Edition](http://www.macosxautomation.com/applescript/apps/everyday_book.html)

I have been able to cobble together a decent solution, building on @ComplexPoint's original script.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

####Background

Using native AppleScript, you cannot dynamically address records, using a variable for the key.  For example, this does NOT work:
```applescript
set keyName to "City"
set somevalue to keyName of myRec  ## FAILS 

--- Instead your must:
set somevalue to City of myRec   ## Works
```

The below script solves this issue.

###Script to Make AppleScript Records Dynamic
Ver 1.1.2 (updated 2016-06-13 13:14 CT)
<img src="/uploads/default/original/2X/9/9fa034801953dfe7d1745afcb7087c7530a06e23.gif" width="70" height="17">
I just got feedback from Shane on a better method for dealing with conversion of types between ASObjC and AppleScript.  See comments at bottom of script.

```applescript
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"

set myRec to {SomeNumber:3.14, |name|:"nemo", address:"Tokyo", language:"Latin", vehicle:"tandem", SomeList:{"item1", "item2", "item3"}}

set myNumber to my getRecValue("SomeNumber", myRec)
--> 3.14

set myList to my getRecValue("SomeList", myRec)
--> {"item1", "item2", "item3"}

--- GET ALL KEYS IN RECORD ---

set keyList to my getRecKeyList(myRec)

--- GET VALUES FOR ALL KEYS IN RECORD ---

set AppleScript's text item delimiters to ", "
repeat with oKey in keyList
  set keyStr to text of oKey
  set valueOfKey to my getRecValue(keyStr, myRec)
  log (keyStr & ":   Class: " & (class of valueOfKey) & "   Value: " & valueOfKey)
end repeat

--~~~~~~~~~~~~~~~~~~~~ END OF MAIN SCRIPT ~~~~~~~~~~~~~~~~~~~~~


--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on getRecKeyList(pRecord)
  ------------------------------------------------------------
  (*
  PARAMETER:  pRecord -- AppleScript Record
  RETURNS:    List of text items of Keys in Record
  ------------------------------------------------------------
  *)
  return ¬
    (current application's NSDictionary's ¬
      dictionaryWithDictionary:pRecord)'s allKeys() as list
end getRecKeyList


--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
on getRecValue(pKeyStr, pRecord)
  ------------------------------------------------------------
  (*
  VER: 1.1.2    2016-06-13
  
  PARAMETERS:
    • pKeyStr   : Record Key (string)
    • pRecord   : Record to be searched
  
  RETURNS:  Value for Specified Key (in class of that value)
          • This can be text, number, list, record, whatever
  ------------------------------------------------------------
  *)
  
  local theDict, theResult, tempArray
  
  set theDict to current application's NSDictionary's dictionaryWithDictionary:pRecord
  set theResult to theDict's objectForKey:pKeyStr
  
  --- Convert ASObjC Class of theResult to AppleScript Class ---
  --   This covers text, numbers, lists, records, whatever,
  --   including missing value
  --   (per Shane Stanley 2016-06-13)
  
  set tempArray to current application's NSArray's arrayWithArray:{theResult}
  return item 1 of (tempArray as list)
  
end getRecValue

(*

NOTE FROM SHANE STANLEY (2016-06-13) (paraphrased)

Instead of checking for class, use this, which also handles missing value:
set theResult to theDict’s objectForKey:pKeyStr
set someArray to current application’s NSArray’s arrayWithArray:{theResult}
return item 1 of (someArray as list)

What this does is makes a single-item array containing theResult.
When that’s converted to an AppleScript list, the item within it
will also be converted to an AppleScript item if it can.
So this covers all the bridgeable AS classes
– text, numbers, lists, records, whatever [including missing value]

You then extract the item from the list.

*)
```

###Results:

```applescript
(*language:   Class: text   Value: Latin*)
(*SomeNumber:   Class: real   Value: 3.14*)
(*vehicle:   Class: text   Value: tandem*)
(*SomeList:   Class: list   Value: item1, item2, item3*)
(*name:   Class: text   Value: nemo*)
(*address:   Class: text   Value: Tokyo*)
```

Rob, thanks for getting this topic started.
1 Like

The JavaScript for Automation ObjC object provides .unwrap() and .deepUnwrap() methods for converting ObjC value to JavaScript values. (A .js suffix at the end of an expression is also interpreted as syntactic sugar for a call to ObjC.unwrap)

Given Shane Stanley's very useful Array conversion trick, it looks as if a first working draft of an unwrap() function for AppleScript might be:

-- unwrap :: NSObject -> AS value
on unwrap(objCValue)
    if objCValue is missing value then
        return missing value
    else
        set ca to current application
        return item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap

Which we could use, updating the original code with an example in which the record contains array, (sub)record and number values as well as strings, to write something like this:

use framework "Foundation"

on run {}
    set rec to {|name|:missing value, address:["Tokyo", "Ginza", 888], |language|:{home:"Dutch", work:"English"}, vehicle:"tandem", profession:"nudnik"}
    
    set lstKeys to keysOfRecord(rec)
    
    return {keys:lstKeys, values:map(mClosure(justValue, {rec:rec}), lstKeys), pairs:map(mClosure(keyValPair, {rec:rec}), lstKeys)}
end run

-- unwrap :: NSObject -> AS value
on unwrap(objCValue)
    set ca to current application
    if objCValue is missing value then
        return missing value
    else
        return item 1 of ((ca's NSArray's arrayWithObject:objCValue) as list)
    end if
end unwrap

-- keysOfRecord :: Record -> [String]
on keysOfRecord(rec)
    set ca to current application
    return (ca's NSDictionary's dictionaryWithDictionary:rec)'s allKeys() as list
end keysOfRecord

-- maybeRecordValue :: Record -> String -> AS Value | missing value
on maybeRecordValue(rec, strKey)
    set ca to current application
    
    return unwrap(((ca's NSDictionary's dictionaryWithDictionary:rec)'s objectForKey:strKey))
end maybeRecordValue

on justValue(strKey)
    maybeRecordValue(my closure's rec, strKey)
end justValue

on keyValPair(strKey)
    {strKey, maybeRecordValue(my closure's rec, strKey)}
end keyValPair


-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    set mf to mReturn(f)
    set lng to length of xs
    set lst to {}
    repeat with i from 1 to lng
        set end of lst to mf's lambda(item i of xs, i, xs)
    end repeat
    return lst
end map

-- Handler -> Record -> Script
on mClosure(f, recBindings)
    script
        property closure : recBindings
        property lambda : f
    end script
end mClosure

-- Lift 2nd class function into 1st class wrapper 
-- handler function --> first class script object
on mReturn(f)
    if class of f is script then return f
    script
        property lambda : f
    end script
end mReturn
1 Like

I just made a minor update to the script above. There is no change in functionality, just a modest improvement in efficiency based on latest feedback from Shane Stanley:

From: Shane Stanley
Date: Mon, Jun 13, 2016 at 1:20 AM

If you're lazy, you can also use this:

set theResult to theDict's objectForKey:pKeyStr
set someArray to current application's NSArray's arrayWithArray:{theResult}
return item 1 of (someArray as list)

The difference is that this version will cope with theResult being missing value.

But if you know the class, it's more efficient to just convert directly; this is really a back-stop for when you don't.

3 posts were split to a new topic: Shane Stanley’s great book: Everyday AppleScriptObjC, Third Edition

Updated Applescript records (adding or updating arbitrary (runtime-generated) key-value pairs) through NSMutableDictionary

use framework "Foundation"

on run
    set rec to {alpha:2, beta:4, gamma:8}
    
    set rec to updatedRecord(rec, "river" & "Delta", 8 ^ 3 as integer)
    
    return rec
end run

-- updatedRecord :: Record -> String -> a -> Record
on updatedRecord(rec, strKey, varValue)
    set ca to current application
    set nsDct to (ca's NSMutableDictionary's dictionaryWithDictionary:rec)
    nsDct's setValue:varValue forKey:strKey
    item 1 of ((ca's NSArray's arrayWithObject:nsDct) as list)
end updatedRecord

Result:

{gamma:8, riverDelta:512, alpha:2, beta:4}
1 Like