diff --git a/docs/.api-usability/audit-2026-06-24.md b/docs/.api-usability/audit-2026-06-24.md new file mode 100644 index 00000000..152a980f --- /dev/null +++ b/docs/.api-usability/audit-2026-06-24.md @@ -0,0 +1,123 @@ +# API Usability Audit — 2026-06-24 + +Scope: public surface only — `FastSense` (`addLine`/`addThreshold`/`addTag`/…/`render`/`updateData`), +`DashboardEngine` (`addWidget`/`Theme`/`render`/`save`/`load`), Tag constructors +(`SensorTag`/`StateTag`/`MonitorTag`/`CompositeTag`/`DerivedTag`), `TagRegistry`, +`toStruct`/`fromStruct`. Internals out of scope. + +Runtime: headless `matlab -batch` (R2025b) — the live matlab MCP was not connected this session. +Friction was reproduced by running real code through the public API, not by reading internals. + +## Score + +| Metric | Value | +|---|---| +| Entry points with helpful "valid options" validation | DashboardEngine ctor ✅, attachPlantLog ✅, `addWidget` type ✅; Tag ctors ✅ (all six fixed) | +| Confusing / dead-end errors (no recovery path) | **0** open (was 1) | +| Methods that silently discard unknown options | **0** (was 4: addThreshold/Band/Marker/Shaded — now warn) | +| Discoverability: `functionSignatures.json` for tab-completion | **FastSense** (ctor + add* + setScale) + **Dashboard** (ctor + addWidget type enum); Tag pending | +| Lines-to-first-plot (example_basic) | ~4 (`FastSense` → `addLine` → `addThreshold` → `render`) — good | + +## Findings (severity-ordered) + +### 🔴 F-1 — `addWidget` unknown type was a dead-end — ✅ RESOLVED (this run) +- **Where:** `DashboardEngine.addWidget` → `error('DashboardEngine:unknownType', ...)`. +- **What a user hit:** any wrong guess (`'linechart'`, `'line'`, `'plot'`, `'numbers'`, `'bar'`) + returned `Unknown widget type: ` with **no list of valid types and no pointer** to how to + find them — on the #1 dashboard-building entry point. It was also inconsistent with the *same + method's* unknown-name-value error (line 164: `... Valid options: %s`). +- **Fix (additive, message-only — ID `DashboardEngine:unknownType` unchanged):** + now `Unknown widget type 'linechart'. Valid types: barchart, chipbar, divider, fastsense, gauge, + group, heatmap, histogram, iconcard, image, multistatus, number, rawaxes, scatter, sparkline, + status, table, text, timeline. Call DashboardEngine.widgetTypes() for descriptions.` + List is derived from `DashboardWidgetRegistry.types()` (single source of truth — custom-registered + types appear automatically). +- **Proof:** 18/18 passed across `TestDashboardEngineWidgetTypes` + `TestDashboardWidgetRegistry` + (incl. `testUnknownTypeStillErrors` which asserts the ID); valid `number` add and the `kpi` + deprecation alias both still work. + +### 🟠 F-2 — Tag constructors' `unknownOption` didn't list valid keys — ✅ RESOLVED (2nd run) +- **Where:** `SensorTag`, `StateTag`, `MonitorTag`, `CompositeTag`, `DerivedTag`, and `Tag` base all + threw `Unknown option ''.` (DerivedTag: `Unknown NV key ''.`) with **no list of valid + name-value keys** — contradicting the project's own convention (CLAUDE.md: *"unknown keys throw + immediately with helpful message listing valid options"*) and inconsistent with `DashboardEngine`'s + `Valid options: %s`. A user who mistyped `'Unit'` for `'Units'` got no hint of the correct spelling. +- **Fix (additive, message-only — all six `*:unknownOption` IDs unchanged):** each unknown-option + error now appends `Valid options: %s`, built via `strjoin` from the key arrays already local to each + `splitArgs_` (so the list cannot drift from what the constructor accepts). Examples: + - `SensorTag('s', 'Unit', 'bar')` → `Unknown option 'Unit'. Valid options: Name, Units, Description, + Labels, Metadata, Criticality, SourceRef, ID, Source, MatFile, KeyName, RawSource, X, Y.` + - `MonitorTag(...,'NotARealKey',5)` → lists `... AlarmOffConditionFn, MinDuration, EventStore, + OnEventStart, OnEventEnd, Persist, DataStore, EventLog.` + DerivedTag's wording was normalized from `Unknown NV key` to `Unknown option` for family consistency. +- **Proof:** 156/156 passed across `TestTag`, `TestSensorTag`, `TestStateTag`, `TestMonitorTag`, + `TestCompositeTag`, `TestDerivedTag` (incl. every `unknownOption` ID assertion); before/after + reproduced for all five concrete subclasses. The unreachable defensive `otherwise` branches at + `MonitorTag:199` / `DerivedTag:218` were intentionally left untouched (they never see a user key — + `monArgs`/`ownArgs` only hold already-recognized keys). + +### 🟠 F-5 — `FastSense` add* methods silently discarded unknown options — ✅ RESOLVED (3rd run) +- **Where:** `FastSense.addThreshold`, `addBand`, `addMarker`, `addShaded` — each has a *closed* + option set but called `parseOpts(defaults, args, obj.Verbose)` and **discarded** the unmatched + output (`[parsed, ~]`). A misspelled option (`addThreshold(5, 'Colour', 'r')`, `addMarker(.., + 'Symbol','x')`) was silently dropped: no error, no warning (unless `Verbose`), and the intended + styling silently did nothing. (Found during this run — not in the original F-1..F-4 list.) +- **Why it matters:** silent failure is worse than a loud one, and it directly violated the project's + own *house convention* — the `FastSense` constructor's comment: *"closed option set, so reject + unknown/misspelled keys immediately rather than silently discarding them."* `addLine` and the + constructor already reject unknown keys; these four were the inconsistent holdouts. +- **Fix (additive, non-breaking):** new private helper `warnUnknownOpts_` warns (does **not** error) + for each unrecognized option, naming the method and listing valid options. Wired into the four + sites; scripts still run identically (the option was already being ignored — now the mistake is + visible). Error would have been breaking (silent→throw); a **warning** keeps existing scripts + working while surfacing the bug. `addFill` was left alone — it legitimately forwards its unmatched + options to `addShaded`. +- **Proof:** all four typos now warn (e.g. `addMarker: unknown option 'Symbol' ignored. Valid + options: Marker, MarkerSize, Color, Label`); **0 false positives** on valid + case-insensitive + calls; 48/48 pass across `TestAddThreshold`, `TestAddBand`, `TestAddMarker`, `TestAddShaded`, + `TestThresholdLabels`, `TestRender`, plus 5/5 flat add-* tests. +- **Future (breaking, report-only):** a major version could promote the warning to a hard error to + fully match the constructor's reject-immediately convention. + +### 🟢 F-3 — No `functionSignatures.json` (tab-completion discoverability) — ✅ RESOLVED (4th run, FastSense scope) +- **Where:** library roots had none — MATLAB editor tab-completion couldn't suggest any public-API + name-value option. +- **Fix (additive, new file — zero runtime change, ignored by Octave):** added + `libs/FastSense/functionSignatures.json` covering the core plotting surface — the `FastSense` + constructor plus `addLine`, `addThreshold`, `addBand`, `addMarker`, `addShaded`, `setScale` — with + enumerated `choices` for the discrete options (`Direction` upper/lower, `XScale`/`YScale` + linear/log, `DownsampleMethod` minmax/lttb, `Marker`, `LineStyle`). The editor now offers these as + you type. +- **Proof:** `validateFunctionSignaturesJSON` returns **0 messages** (schema-valid, all method/type + names resolve against the real class metadata with FastSense on the path); runtime smoke + (construct + add* + setScale) clean — no error, no warning; no `.m` changed so existing + tests/examples are inherently unaffected. +- **Remaining (additive follow-ups):** `libs/Dashboard/functionSignatures.json` for + `DashboardEngine.addWidget` (the `type` arg as a `choices` enum — the single highest-value + completion, directly preventing the run-1 wrong-type trap at typing time) and + `libs/SensorThreshold/functionSignatures.json` for the Tag constructors. + +### 🟢 F-4 — Example bootstrap boilerplate *(minor / informational)* +- Every example repeats `projectRoot = fileparts(fileparts(fileparts(mfilename('fullpath'))));` + + `run(fullfile(projectRoot,'install.m'))`. This is example-script bootstrap, not public-API + friction, so it is informational only — no API change warranted. + +### 🟢 F-3b — `DashboardEngine.addWidget` had no type tab-completion — ✅ RESOLVED (5th run) +- **Fix (additive, new file — zero runtime change):** added `libs/Dashboard/functionSignatures.json` + covering the `DashboardEngine` constructor (Name + Theme/LiveInterval/InfoFile/ProgressMode/ + ShowTimePanel) and `addWidget`, with the `type` argument modeled as a `choices` enum of all 19 + built-in widget types — so the editor now lists the valid types *as you type*, complementing the + run-1 error-message fix at the point of entry. Common `addWidget` options (Position, Title, Tag, + Label) are offered too. +- **Proof:** `validateFunctionSignaturesJSON` returns **0 messages** (after switching the optional + positional `Name` from the deprecated `"optional"` kind to `"ordered"`, which R2025b now requires); + runtime smoke confirms `addWidget('number', ...)` still returns a `NumberWidget` and an unknown + type still errors `DashboardEngine:unknownType` — the metadata file is runtime-inert. + +## Next-worst additive finding for `/api-polish` +**F-3c** — add `libs/SensorThreshold/functionSignatures.json` for the Tag constructors +(`SensorTag`, `StateTag`, `MonitorTag`, `CompositeTag`, `DerivedTag`), completing the +tab-completion coverage for the third core public surface. The valid name-value keys are already +enumerated per class (from the run-2 work) — `Name`/`Units`/`Description`/`Labels`/`Metadata`/ +`Criticality`/`SourceRef` universals plus each class's extras (e.g. SensorTag `ID`/`Source`/`X`/`Y`, +MonitorTag `MinDuration`/`Persist`/…). Purely additive (new file). diff --git a/libs/Dashboard/DashboardEngine.m b/libs/Dashboard/DashboardEngine.m index a13e5992..f3e7370d 100644 --- a/libs/Dashboard/DashboardEngine.m +++ b/libs/Dashboard/DashboardEngine.m @@ -395,7 +395,8 @@ function switchPage(obj, pageIdx) w = ctor(varargin{:}); else error('DashboardEngine:unknownType', ... - 'Unknown widget type: %s', type); + 'Unknown widget type ''%s''. Valid types: %s. Call DashboardEngine.widgetTypes() for descriptions.', ... + type, strjoin(DashboardWidgetRegistry.types(), ', ')); end % Preserve timeline no-store warning diff --git a/libs/Dashboard/functionSignatures.json b/libs/Dashboard/functionSignatures.json new file mode 100644 index 00000000..46640379 --- /dev/null +++ b/libs/Dashboard/functionSignatures.json @@ -0,0 +1,25 @@ +{ + "_schemaVersion": "1.0.0", + + "DashboardEngine.DashboardEngine": { + "inputs": [ + {"name": "Name", "kind": "ordered", "type": ["char"]}, + {"name": "Theme", "kind": "namevalue", "type": ["choices={'light','dark'}"]}, + {"name": "LiveInterval", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "InfoFile", "kind": "namevalue", "type": ["char"]}, + {"name": "ProgressMode", "kind": "namevalue", "type": ["choices={'auto','on','off'}"]}, + {"name": "ShowTimePanel", "kind": "namevalue", "type": ["logical", "scalar"]} + ] + }, + + "DashboardEngine.addWidget": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["DashboardEngine"]}, + {"name": "type", "kind": "required", "type": ["choices={'barchart','chipbar','divider','fastsense','gauge','group','heatmap','histogram','iconcard','image','multistatus','number','rawaxes','scatter','sparkline','status','table','text','timeline'}"]}, + {"name": "Position", "kind": "namevalue", "type": ["numeric", "size=1,4"]}, + {"name": "Title", "kind": "namevalue", "type": ["char"]}, + {"name": "Tag", "kind": "namevalue"}, + {"name": "Label", "kind": "namevalue", "type": ["char"]} + ] + } +} diff --git a/libs/FastSense/FastSense.m b/libs/FastSense/FastSense.m index fc98c7fc..d84ab098 100644 --- a/libs/FastSense/FastSense.m +++ b/libs/FastSense/FastSense.m @@ -655,7 +655,8 @@ function addThreshold(obj, varargin) defaults.Color = obj.Theme.ThresholdColor; defaults.LineStyle = obj.Theme.ThresholdStyle; defaults.Label = ''; - [parsed, ~] = parseOpts(defaults, nvPairs, obj.Verbose); + [parsed, unmatched] = parseOpts(defaults, nvPairs); + warnUnknownOpts_('addThreshold', unmatched, fieldnames(defaults)); t.Value = []; t.X = []; @@ -722,7 +723,8 @@ function addBand(obj, yLow, yHigh, varargin) defaults.FaceAlpha = obj.Theme.BandAlpha; defaults.EdgeColor = 'none'; defaults.Label = ''; - [parsed, ~] = parseOpts(defaults, varargin, obj.Verbose); + [parsed, unmatched] = parseOpts(defaults, varargin); + warnUnknownOpts_('addBand', unmatched, fieldnames(defaults)); b.YLow = yLow; b.YHigh = yHigh; @@ -784,7 +786,8 @@ function addMarker(obj, x, y, varargin) defaults.MarkerSize = 6; defaults.Color = obj.Theme.ThresholdColor; defaults.Label = ''; - [parsed, ~] = parseOpts(defaults, varargin, obj.Verbose); + [parsed, unmatched] = parseOpts(defaults, varargin); + warnUnknownOpts_('addMarker', unmatched, fieldnames(defaults)); m.X = x; m.Y = y; @@ -894,7 +897,8 @@ function addShaded(obj, x, y1, y2, varargin) defaults.FaceAlpha = 0.15; defaults.EdgeColor = 'none'; defaults.DisplayName = ''; - [parsed, ~] = parseOpts(defaults, varargin, obj.Verbose); + [parsed, unmatched] = parseOpts(defaults, varargin); + warnUnknownOpts_('addShaded', unmatched, fieldnames(defaults)); s.X = x; s.Y1 = y1; diff --git a/libs/FastSense/functionSignatures.json b/libs/FastSense/functionSignatures.json new file mode 100644 index 00000000..ca0e7133 --- /dev/null +++ b/libs/FastSense/functionSignatures.json @@ -0,0 +1,97 @@ +{ + "_schemaVersion": "1.0.0", + + "FastSense.FastSense": { + "inputs": [ + {"name": "Parent", "kind": "namevalue", "type": ["matlab.graphics.axis.Axes"]}, + {"name": "LinkGroup", "kind": "namevalue"}, + {"name": "Theme", "kind": "namevalue", "type": ["choices={'light','dark'}"]}, + {"name": "Verbose", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "XScale", "kind": "namevalue", "type": ["choices={'linear','log'}"]}, + {"name": "YScale", "kind": "namevalue", "type": ["choices={'linear','log'}"]}, + {"name": "DefaultDownsampleMethod","kind": "namevalue", "type": ["choices={'minmax','lttb'}"]}, + {"name": "LiveInterval", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "HoverCrosshair", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "MinPointsForDownsample", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "DownsampleFactor", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "PyramidReduction", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "StorageMode", "kind": "namevalue", "type": ["char"]}, + {"name": "MemoryLimit", "kind": "namevalue", "type": ["numeric", "scalar"]} + ] + }, + + "FastSense.addLine": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "x", "kind": "required", "type": ["numeric"]}, + {"name": "y", "kind": "required", "type": ["numeric"]}, + {"name": "DownsampleMethod", "kind": "namevalue", "type": ["choices={'minmax','lttb'}"]}, + {"name": "AssumeSorted", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "HasNaN", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "XType", "kind": "namevalue", "type": ["choices={'numeric','datenum'}"]}, + {"name": "Metadata", "kind": "namevalue", "type": ["struct"]}, + {"name": "Color", "kind": "namevalue"}, + {"name": "LineStyle", "kind": "namevalue", "type": ["choices={'-','--',':','-.','none'}"]}, + {"name": "LineWidth", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "DisplayName", "kind": "namevalue", "type": ["char"]}, + {"name": "Marker", "kind": "namevalue"} + ] + }, + + "FastSense.addThreshold": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "value", "kind": "required", "type": ["numeric"]}, + {"name": "Direction", "kind": "namevalue", "type": ["choices={'upper','lower'}"]}, + {"name": "ShowViolations", "kind": "namevalue", "type": ["logical", "scalar"]}, + {"name": "Color", "kind": "namevalue"}, + {"name": "LineStyle", "kind": "namevalue", "type": ["choices={'-','--',':','-.'}"]}, + {"name": "Label", "kind": "namevalue", "type": ["char"]} + ] + }, + + "FastSense.addBand": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "yLow", "kind": "required", "type": ["numeric", "scalar"]}, + {"name": "yHigh", "kind": "required", "type": ["numeric", "scalar"]}, + {"name": "FaceColor", "kind": "namevalue"}, + {"name": "FaceAlpha", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "EdgeColor", "kind": "namevalue"}, + {"name": "Label", "kind": "namevalue", "type": ["char"]} + ] + }, + + "FastSense.addMarker": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "x", "kind": "required", "type": ["numeric"]}, + {"name": "y", "kind": "required", "type": ["numeric"]}, + {"name": "Marker", "kind": "namevalue", "type": ["choices={'o','+','*','.','x','s','d','^','v'}"]}, + {"name": "MarkerSize", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "Color", "kind": "namevalue"}, + {"name": "Label", "kind": "namevalue", "type": ["char"]} + ] + }, + + "FastSense.addShaded": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "x", "kind": "required", "type": ["numeric"]}, + {"name": "y1", "kind": "required", "type": ["numeric"]}, + {"name": "y2", "kind": "required", "type": ["numeric"]}, + {"name": "FaceColor", "kind": "namevalue"}, + {"name": "FaceAlpha", "kind": "namevalue", "type": ["numeric", "scalar"]}, + {"name": "EdgeColor", "kind": "namevalue"}, + {"name": "DisplayName", "kind": "namevalue", "type": ["char"]} + ] + }, + + "FastSense.setScale": { + "inputs": [ + {"name": "obj", "kind": "required", "type": ["FastSense"]}, + {"name": "XScale", "kind": "namevalue", "type": ["choices={'linear','log'}"]}, + {"name": "YScale", "kind": "namevalue", "type": ["choices={'linear','log'}"]} + ] + } +} diff --git a/libs/FastSense/private/warnUnknownOpts_.m b/libs/FastSense/private/warnUnknownOpts_.m new file mode 100644 index 00000000..3e98334a --- /dev/null +++ b/libs/FastSense/private/warnUnknownOpts_.m @@ -0,0 +1,33 @@ +function warnUnknownOpts_(method, unmatched, validFields) +%WARNUNKNOWNOPTS_ Warn (non-fatally) for unrecognized name-value options. +% warnUnknownOpts_(METHOD, UNMATCHED, VALIDFIELDS) emits a +% FastSense:unknownOption warning for each field of the UNMATCHED struct +% returned by parseOpts, naming the calling METHOD and listing the +% VALIDFIELDS the method accepts. +% +% Closed-option-set FastSense methods (addThreshold, addBand, addMarker, +% addShaded) previously discarded misspelled options silently — e.g. +% addThreshold(5, 'Colour', 'r') did nothing and said nothing. This +% surfaces the mistake, mirroring the FastSense constructor's +% reject-unknown-keys house convention, but as a WARNING rather than an +% error so existing scripts keep running unchanged (backward-compatible). +% +% Inputs: +% method — char, the calling method name (for the message context) +% unmatched — struct of unrecognized name-value pairs (parseOpts output) +% validFields — cellstr of the option names the method accepts +% +% This is a private helper for FastSense. +% +% See also parseOpts, FastSense. + + if nargin < 2 || isempty(unmatched) + return; + end + keys = fieldnames(unmatched); + for i = 1:numel(keys) + warning('FastSense:unknownOption', ... + '%s: unknown option ''%s'' ignored. Valid options: %s', ... + method, keys{i}, strjoin(validFields, ', ')); + end +end diff --git a/libs/SensorThreshold/CompositeTag.m b/libs/SensorThreshold/CompositeTag.m index ffbb3d26..79907f48 100644 --- a/libs/SensorThreshold/CompositeTag.m +++ b/libs/SensorThreshold/CompositeTag.m @@ -704,7 +704,8 @@ function validateMode_(mode) cmpArgs{end+1} = v; %#ok else error('CompositeTag:unknownOption', ... - 'Unknown option ''%s''.', k); + 'Unknown option ''%s''. Valid options: %s.', ... + k, strjoin([tagKeys, cmpKeys], ', ')); end i = i + 2; end diff --git a/libs/SensorThreshold/DerivedTag.m b/libs/SensorThreshold/DerivedTag.m index 63128c06..ac0b270d 100644 --- a/libs/SensorThreshold/DerivedTag.m +++ b/libs/SensorThreshold/DerivedTag.m @@ -592,7 +592,8 @@ function notifyListeners_(obj) ownArgs{end+1} = v; %#ok else error('DerivedTag:unknownOption', ... - 'Unknown NV key ''%s''.', k); + 'Unknown option ''%s''. Valid options: %s.', ... + k, strjoin([tagKeys, ownKeys], ', ')); end i = i + 2; end diff --git a/libs/SensorThreshold/MonitorTag.m b/libs/SensorThreshold/MonitorTag.m index 65f057b0..5bbe8c43 100644 --- a/libs/SensorThreshold/MonitorTag.m +++ b/libs/SensorThreshold/MonitorTag.m @@ -1223,7 +1223,8 @@ function fireEventsOnRisingEdges_(obj, px, bin) monArgs{end+1} = v; %#ok else error('MonitorTag:unknownOption', ... - 'Unknown option ''%s''.', k); + 'Unknown option ''%s''. Valid options: %s.', ... + k, strjoin([tagKeys, monKeys], ', ')); end end end diff --git a/libs/SensorThreshold/SensorTag.m b/libs/SensorThreshold/SensorTag.m index 4f4f4ff8..0761ca97 100644 --- a/libs/SensorThreshold/SensorTag.m +++ b/libs/SensorThreshold/SensorTag.m @@ -436,7 +436,8 @@ function notifyListeners_(obj) inlineY = v; else error('SensorTag:unknownOption', ... - 'Unknown option ''%s''.', k); + 'Unknown option ''%s''. Valid options: %s.', ... + k, strjoin([tagKeys, sensorKeys, {'X', 'Y'}], ', ')); end end end diff --git a/libs/SensorThreshold/StateTag.m b/libs/SensorThreshold/StateTag.m index 59553c14..6868f637 100644 --- a/libs/SensorThreshold/StateTag.m +++ b/libs/SensorThreshold/StateTag.m @@ -288,7 +288,8 @@ function notifyListeners_(obj) hasRs = true; else error('StateTag:unknownOption', ... - 'Unknown option ''%s''.', char(k)); + 'Unknown option ''%s''. Valid options: %s.', ... + char(k), strjoin([tagKeys, {'X', 'Y', 'RawSource'}], ', ')); end i = i + 2; end diff --git a/libs/SensorThreshold/Tag.m b/libs/SensorThreshold/Tag.m index 4cd28912..da2fa847 100644 --- a/libs/SensorThreshold/Tag.m +++ b/libs/SensorThreshold/Tag.m @@ -87,6 +87,8 @@ obj.Key = key; obj.Name = key; % default Name = Key + validKeys = {'Name', 'Units', 'Description', 'Labels', ... + 'Metadata', 'Criticality', 'SourceRef'}; for i = 1:2:numel(varargin) switch varargin{i} case 'Name', obj.Name = varargin{i+1}; @@ -98,7 +100,8 @@ case 'SourceRef', obj.SourceRef = varargin{i+1}; otherwise error('Tag:unknownOption', ... - 'Unknown option ''%s''.', varargin{i}); + 'Unknown option ''%s''. Valid options: %s.', ... + varargin{i}, strjoin(validKeys, ', ')); end end end