Value objects for hive-gimp, malli first.
Everything downstream is defined in terms of these, and the suite is
synthesized from them rather than hand-written: deftrifecta-from-schema
needs an :in schema, so a function that takes a VALUE gets generated
coverage for free while a function that takes an id or a bag of positional
arguments gets none. That is the reason the promote layer here is written
over Descriptor and GimpCommand rather than over a command name.
Two vocabularies meet in this namespace and must not be confused:
WIRE snake_case strings, the GIMP plugin's own contract. :wire,
GimpCommand, RawResponse. JSON keys stay strings.
HIVE kebab-case keywords, what a Clojure caller and an MCP client see.
:name, Invocation.
hive-gimp.command is the only place the two are allowed to touch.
Value objects for hive-gimp, malli first.
Everything downstream is defined in terms of these, and the suite is
synthesized from them rather than hand-written: `deftrifecta-from-schema`
needs an `:in` schema, so a function that takes a VALUE gets generated
coverage for free while a function that takes an id or a bag of positional
arguments gets none. That is the reason the promote layer here is written
over `Descriptor` and `GimpCommand` rather than over a command name.
Two vocabularies meet in this namespace and must not be confused:
WIRE snake_case strings, the GIMP plugin's own contract. `:wire`,
`GimpCommand`, `RawResponse`. JSON keys stay strings.
HIVE kebab-case keywords, what a Clojure caller and an MCP client see.
`:name`, `Invocation`.
`hive-gimp.command` is the only place the two are allowed to touch.The contract as loaded: descriptors indexed by command.
The contract as loaded: descriptors indexed by command.
A GIMP-side command type. snake_case, because the plugin dispatches on the
literal string.
A GIMP-side command `type`. snake_case, because the plugin dispatches on the literal string.
One row of the derived command contract. The whole tool surface is a
sequence of these, which is what makes adding a GIMP command a data change
rather than a new defn (OCP).
One row of the derived command contract. The whole tool surface is a sequence of these, which is what makes adding a GIMP command a data change rather than a new `defn` (OCP).
Where the GIMP plugin socket lives, and how long we are willing to wait.
:host is deliberately not free-form in practice (the plugin binds
localhost) but is kept a plain string so an ssh-forwarded or container
endpoint stays expressible without a code change.
Where the GIMP plugin socket lives, and how long we are willing to wait. `:host` is deliberately not free-form in practice (the plugin binds localhost) but is kept a plain string so an ssh-forwarded or container endpoint stays expressible without a code change.
The two things GIMP's Python-Fu bridge can do with a string.
:eval is reached ONLY by the literal marker python-fu-eval; every other
marker falls through to exec. The reference project's own protocol document
advertises a pyGObject-eval marker that does not exist in the plugin's
dispatch, so a caller following that document gets exec semantics and a
["None"] result instead of a value. hive-gimp.exec emits the marker
that actually works.
The two things GIMP's Python-Fu bridge can do with a string. `:eval` is reached ONLY by the literal marker `python-fu-eval`; every other marker falls through to exec. The reference project's own protocol document advertises a `pyGObject-eval` marker that does not exist in the plugin's dispatch, so a caller following that document gets exec semantics and a `["None"]` result instead of a value. `hive-gimp.exec` emits the marker that actually works.
(explain ?schema value)Human-readable explanation of why value fails ?schema, or nil when it
conforms. Used in error paths, never in hot paths.
Human-readable explanation of why `value` fails `?schema`, or nil when it conforms. Used in error paths, never in hot paths.
One framed request, exactly as the plugin will parse it. String keys, wire spellings, defaults already applied. This is the boundary value: everything above it is Clojure, everything below it is JSON on a socket.
One framed request, exactly as the plugin will parse it. String keys, wire spellings, defaults already applied. This is the boundary value: everything above it is Clojure, everything below it is JSON on a socket.
What a caller asks for, before defaults and before translation to the wire.
Argument keys are kebab-case keywords matching ParamSpec :name.
What a caller asks for, before defaults and before translation to the wire. Argument keys are kebab-case keywords matching `ParamSpec :name`.
The interpreted answer. A closed two-variant shape rather than a bare map, so a caller that forgets to branch fails at the schema instead of silently treating an error payload as a result.
The interpreted answer. A closed two-variant shape rather than a bare map, so a caller that forgets to branch fails at the schema instead of silently treating an error payload as a result.
A parameter as a Clojure caller spells it: kebab-case.
A parameter as a Clojure caller spells it: kebab-case.
One parameter of one command.
:required? and :default are mutually exclusive by construction: the
generator emits :required? true exactly when the Python signature had no
default. :nilable? is separate and means the plugin accepts an explicit
JSON null, which is NOT the same as the parameter being absent. That
distinction is load-bearing: the reference client sends "layer_name": null
and the plugin reads it back with .get, so dropping the key would change
behaviour on a plugin that ever starts distinguishing the two.
One parameter of one command. `:required?` and `:default` are mutually exclusive by construction: the generator emits `:required? true` exactly when the Python signature had no default. `:nilable?` is separate and means the plugin accepts an explicit JSON null, which is NOT the same as the parameter being absent. That distinction is load-bearing: the reference client sends `"layer_name": null` and the plugin reads it back with `.get`, so dropping the key would change behaviour on a plugin that ever starts distinguishing the two.
The scalar shapes the GIMP plugin accepts over JSON. :any is the honest
answer for an unannotated parameter, not a licence to send anything: the
plugin still validates, and an :any simply means this contract cannot say
more than JSON already does.
The scalar shapes the GIMP plugin accepts over JSON. `:any` is the honest answer for an unannotated parameter, not a licence to send anything: the plugin still validates, and an `:any` simply means this contract cannot say more than JSON already does.
The plugin's answer, straight off the wire, before interpretation.
Open on purpose: :results carries whatever the command returns, and a
closed schema here would reject a plugin newer than this contract. The one
thing we do insist on is that status is present and is one of the two
words the plugin actually emits, because an answer without a status is not a
response, it is a framing bug wearing one.
The plugin's answer, straight off the wire, before interpretation. Open on purpose: `:results` carries whatever the command returns, and a closed schema here would reject a plugin newer than this contract. The one thing we do insist on is that `status` is present and is one of the two words the plugin actually emits, because an answer without a status is not a response, it is a framing bug wearing one.
The MCP-facing name of a command. Always gimp_ prefixed so a host that
flattens every addon's tools into one namespace cannot collide.
The MCP-facing name of a command. Always `gimp_` prefixed so a host that flattens every addon's tools into one namespace cannot collide.
A parameter as the plugin spells it: snake_case.
A parameter as the plugin spells it: snake_case.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |