|
9 | 9 | > - **Breaking changes may occur at any time** |
10 | 10 | > - **APIs are experimental and unstable** |
11 | 11 | > - **Use for development and testing only** |
| 12 | +
|
| 13 | +## Design Notes |
| 14 | + |
| 15 | +### FieldMask — Deferred Ergonomics Decisions |
| 16 | + |
| 17 | +The current `FieldMask` implementation validates paths against a per-message |
| 18 | +schema inside `FieldMask.build`, throws on mismatch, and stores the |
| 19 | +wire-format paths privately so `toString()` produces the server-facing |
| 20 | +comma-separated string. Construction entry point today is a **per-message |
| 21 | +factory function** generated alongside each message — e.g. |
| 22 | +`alertFieldMask('displayName', 'condition.op')` — which supplies the schema |
| 23 | +and message name and delegates to `FieldMask.build`. This section records |
| 24 | +two refinements we've discussed but deferred; they do not change the |
| 25 | +validation semantics, only the call-site shape. |
| 26 | + |
| 27 | +#### Q1 — Call-site ergonomics |
| 28 | + |
| 29 | +**Current (Option C, per-message factory):** |
| 30 | + |
| 31 | +```ts |
| 32 | +import {alertFieldMask} from '@databricks/sdk-alerts/v1'; |
| 33 | +const mask = alertFieldMask('displayName', 'condition.op'); |
| 34 | +``` |
| 35 | + |
| 36 | +- Pros: best discoverability (auto-complete on import), single-argument call, |
| 37 | + error messages name the target message, and users never think about |
| 38 | + schemas or message names. |
| 39 | +- Cons: one generated factory per message (~60+ across the SDK). Each is a |
| 40 | + thin one-liner, but they multiply with every new message. |
| 41 | + |
| 42 | +**Option A — raw `FieldMask.build` at the call site:** |
| 43 | + |
| 44 | +```ts |
| 45 | +import {FieldMask} from '@databricks/sdk-core/wkt'; |
| 46 | +import {Alert, alertFieldMaskSchema} from '@databricks/sdk-alerts/v1'; |
| 47 | + |
| 48 | +const mask = FieldMask.build<Alert>( |
| 49 | + ['displayName', 'condition.op'], |
| 50 | + alertFieldMaskSchema, |
| 51 | +); |
| 52 | +``` |
| 53 | + |
| 54 | +- Pros: zero helper functions anywhere. Only `FieldMask.build` exists. |
| 55 | +- Cons: two-argument low-level call at every usage site. Users must |
| 56 | + import the schema explicitly and know which schema pairs with which |
| 57 | + type. |
| 58 | + |
| 59 | +**Option B — one generic helper in `sdk-core`:** |
| 60 | + |
| 61 | +```ts |
| 62 | +import {fieldMask} from '@databricks/sdk-core/wkt'; |
| 63 | +import {Alert} from '@databricks/sdk-alerts/v1'; |
| 64 | + |
| 65 | +const mask = fieldMask(Alert, 'displayName', 'condition.op'); |
| 66 | +``` |
| 67 | + |
| 68 | +- Pros: a single `fieldMask(...)` replaces the ~60+ per-message factories. |
| 69 | + `Alert` supplies both the schema and the message name as properties on |
| 70 | + itself, so the call site stays one argument plus paths. |
| 71 | +- Cons: requires `Alert` the import to be **both** a TypeScript type and a |
| 72 | + runtime descriptor value on the same name. See Q2 below. |
| 73 | + |
| 74 | +#### Q2 — Interface + const declaration merging on the message name |
| 75 | + |
| 76 | +A TypeScript pattern that lets one exported identifier occupy both the |
| 77 | +type-space and the value-space: |
| 78 | + |
| 79 | +```ts |
| 80 | +// Type — describes instance shape. Zero runtime cost. |
| 81 | +export interface Alert { |
| 82 | + displayName?: string; |
| 83 | + condition?: Condition; |
| 84 | +} |
| 85 | + |
| 86 | +// Runtime value — carries the schema under the same name. |
| 87 | +export const Alert: MessageDescriptor<Alert> = { |
| 88 | + fieldMaskSchema: { |
| 89 | + displayName: {wire: 'display_name'}, |
| 90 | + condition: {wire: 'condition', children: () => Condition.fieldMaskSchema}, |
| 91 | + }, |
| 92 | +}; |
| 93 | +``` |
| 94 | + |
| 95 | +Usage after this change: |
| 96 | + |
| 97 | +```ts |
| 98 | +const a: Alert = {displayName: 'foo'}; // Alert as a type (literal shape) |
| 99 | +const s = Alert.fieldMaskSchema; // Alert as a value (runtime) |
| 100 | +const mask = fieldMask(Alert, 'displayName'); // Option B unblocked |
| 101 | +``` |
| 102 | + |
| 103 | +TypeScript supports this via |
| 104 | +[declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html). |
| 105 | +`interface X` occupies type-space, `const X` occupies value-space; they do |
| 106 | +not collide. The same pattern shows up in `lib.dom.d.ts`, `Promise`, and |
| 107 | +various DI libraries. |
| 108 | + |
| 109 | +- Pros: single import, natural `Alert.fieldMaskSchema` access, enables |
| 110 | + Option B without asking users to remember a separate `AlertSchema` name. |
| 111 | +- Cons: the pattern is less common in everyday TS and can surprise readers |
| 112 | + who expect classes for anything with static-like members. The alternative |
| 113 | + is to use a distinct name — e.g. `AlertSchema` or `AlertDescriptor` — for |
| 114 | + the runtime value, at the cost of one extra identifier to learn per |
| 115 | + message. |
| 116 | + |
| 117 | +#### Why we haven't landed Q1/Q2 yet |
| 118 | + |
| 119 | +Both are call-site ergonomics refactors; neither changes the validation |
| 120 | +semantics or the per-message schema generation. We want the current |
| 121 | +`FieldMask.build` path (validate, translate, store wire paths privately, |
| 122 | +`toString()` joins) to settle before adjusting the call-site shape, so |
| 123 | +the two axes of change don't churn simultaneously. When we revisit, the |
| 124 | +likely landing is **Option B + Option Q2** together — one generic |
| 125 | +`fieldMask()` plus the interface+const merge, which most closely matches |
| 126 | +what similar TypeScript validation libraries (Zod, Valibot, TypeBox, etc.) |
| 127 | +expose while staying consistent with the SDK's existing interface-first |
| 128 | +convention for message types. |
0 commit comments