Skip to content

Expose the raw AppleEvent descriptor through a .wildCard return type - #8

Merged
fredsimard merged 5 commits into
mainfrom
feature/wildcard-return-type
Aug 30, 2026
Merged

fredsimard merged 5 commits into
mainfrom
feature/wildcard-return-type

Conversation

@fredsimard

Copy link
Copy Markdown
Owner

Closes #5.

What this adds

A .wildCard return type, for the properties that answer with a different type depending on state. The value of a cell in Numbers is documented as "number, date, text, boolean, or missing value" — declare .int there and an empty cell is indistinguishable from a cell holding zero. NSAppleScript already hands back an NSAppleEventDescriptor carrying the real type; the bridge discarded it and coerced to whatever the caller declared.

.wildCard switches on the descriptor's own type instead and returns an AppleScriptValue:

switch try AppleScriptBridge.executeAppleScript(cellValue) as? AppleScriptBridge.AppleScriptValue {
    case .double(let number): …
    case .string(let text):   …
    case .missingValue:       …   // the cell is empty
    default:                  break
}

Numbers, text, booleans, dates, files, missing value, application-defined constants, and lists and records, the last two decoded recursively. Anything else arrives as .unknown carrying the descriptor untouched, so nothing is lost.

Decisions worth reviewing

  • .wildCard, not .descriptor. @olofhellman's suggestion in Expose the raw AppleEvent descriptor, so callers can switch on the actual result type #5, after the AppleEvents typeWildCard tradition. The escape hatch inside the value enum stays .unknown, so the two names don't collide.
  • missing value gets its own case, rather than arriving as .constant('msng'). It is the common answer, and making every caller compare four-character codes for it would be the wrong default. Other constants do carry their raw FourCharCode — what 'autp' means is defined by the application's dictionary, not by AppleScript, so there is no general mapping to hand the caller.
  • No osascript parity. executeAppleScriptViaCommandLine throws the new AppleScriptError.unsupportedReturnType before launching the process, so a .wildCard script never runs and never touches the target application. Returning the printed text as a .string would fake the very type information the feature exists to preserve.
  • Record keys are as written, or as compiled. A key AppleScript claims as terminology is compiled to a four-character code before the script runs, so {name:"Roger", age:42} decodes as ["pnam": .string("Roger"), "age": .int(42)]. Documented everywhere it can surprise.
  • Integers are read at their declared width. int32Value answers 0 for anything wider, which would have turned a typeSInt64 into a silent zero. A value too large for an Int falls back to .unknown rather than reporting a wrong number.

Testing

46 tests, all passing locally. 17 are new, in two layers — both permission-free:

  • Descriptors built directly, for what AppleScript source cannot produce: a 64-bit integer, an unsigned value too large for an Int, an empty list, and a record carrying both a coded key and user fields, including the collision where a user field is named after a code.
  • Self-contained scripts through NSAppleScript, which drive no application and so need no Automation permission. These are the only check against what AppleScript actually emits — that {name:"Roger", age:42} really does arrive with name compiled to 'pnam', and that 3000000000 really does arrive as a real.

Every code snippet in the README and in Documentation/API.md was compiled against the package before being written down, and Examples/AppleScripts.swift builds as a consumer package the way CI builds it.

Version

Cut as 1.2.0: new public API, all of it opt-in, with the default path unchanged. AppleScriptReturnType and AppleScriptError each gained a case, so code switching over either exhaustively without a default stops compiling — the release notes open with that caveat.

Two things I could not verify

  • SwiftLint did not run locally (not installed on this machine). I checked the configured rules by hand — no force unwraps, no trailing whitespace, switch columns aligned — but CI is the real check.
  • The four NSAppleScript execution tests are unproven on a GitHub runner. They pass locally; if the runner disagrees, they are easy to drop without losing the descriptor-level coverage.

Follow-up

#7 tracks @olofhellman's second suggestion, an .osType return type for scripts whose result is known to be a constant. Left out deliberately: it carries its own unsettled question — raw codes versus dictionary terms — that deserves its own discussion.

🤖 Generated with Claude Code

fredsimard and others added 5 commits August 29, 2026 21:35
Some application properties answer with a different type depending on state: the value of a
cell in Numbers is a number, a date, text, a boolean, or missing value when the cell is empty.
Every return type coerced that distinction away before Swift saw it, since the caller had to
declare one type up front and NSAppleScript's descriptor was discarded once it had.

The new .wildCard return type switches on the descriptor's own type instead, and returns an
AppleScriptValue carrying what it found: numbers, text, booleans, dates, files, missing value,
application-defined constants, and lists and records, the last two decoded recursively. A type
the enum does not decode arrives as .unknown, carrying the descriptor untouched, so nothing is
lost and the caller can drill in. Integers are read at the width their descriptor declares,
since int32Value answers zero for anything wider.

Record keys need unpacking: an Apple event record is keyed by four-character code, so the keys
a caller writes are packed into a keyASUserRecordFields list of alternating names and values.
Keys AppleScript owns as terminology are compiled to codes before the script runs and come
through as those, which is why {name:"Roger"} decodes as ["pnam": .string("Roger")].

Constants carry their raw code, as there is no general mapping from a code to a meaning: what
'autp' means is defined by the target application's dictionary. String's appleScriptFourCharCode
conversions make one comparable without hand-assembling the bytes.

osascript prints its result as text, so the descriptor is gone before the command-line method
could read it. That method throws the new .unsupportedReturnType error before launching the
process, so a script declaring .wildCard never runs and never touches the target application.

Closes #5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two layers, both permission-free. The first builds descriptors directly and covers what cannot
be produced from AppleScript source: a 64-bit integer, whose int32Value is asserted to fail so
the reason for reading the bytes is recorded; an unsigned value too large for an Int, which has
to fall back to the descriptor rather than report a wrong number; an empty list, whose index
range would trap if built naively; and a record carrying both a coded key and user fields,
including the collision where a user field is named after a code.

The second runs self-contained scripts through NSAppleScript. They drive no application, so
they need no Automation permission, and they are the only way to check the decoding against
what AppleScript actually emits — that {name:"Roger", age:42} really does arrive with name
compiled to 'pnam', and that 3000000000 really does arrive as a real rather than an integer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The README gets the Numbers cell example the request came from, a table of what each
AppleScriptValue case carries, and the three things that surprise: constants arrive as raw
codes, record keys arrive as written only when AppleScript did not claim them as terminology,
and the command-line method refuses the type outright. API.md gets a section of its own,
mapping each case to the descriptor types it decodes.

The example file gets a Finder script, so it stays runnable on any Mac without extra software:
a disk's format is an edfm enumeration, and its constants are compared at the call site against
the codes Finder's dictionary documents, which is what working with .constant looks like.

Every snippet in both documents was compiled against the package before being written down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A minor bump rather than a patch: the release adds public API. Consumers switching
exhaustively over AppleScriptError or AppleScriptReturnType have a new case to handle, which
the release notes call out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The caveat was the last bullet of the release's notes and named only AppleScriptError. It now
opens the release, where an upgrader reads it before anything else, and names both enums that
gained a case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fredsimard fredsimard self-assigned this Aug 30, 2026
@fredsimard
fredsimard merged commit 01f4f6a into main Aug 30, 2026
3 checks passed
@fredsimard
fredsimard deleted the feature/wildcard-return-type branch August 30, 2026 02:42
@olofhellman

Copy link
Copy Markdown

Looks great! You even included UserRecordFields coercion!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose the raw AppleEvent descriptor, so callers can switch on the actual result type

2 participants