From 1fdba4aba546101c0d25665e41c774839832e1c1 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 28 Jun 2026 21:46:29 +0200 Subject: [PATCH] =?UTF-8?q?feat(draw):=20template=20dispatch=20=E2=80=94?= =?UTF-8?q?=20lower=20a=20drawing-card=20doc=20to=20SVG=20(T-318)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveDrawingSvg lowers a DrawingCardDoc to an SVG string: primitive docs use inline svg or read svgPath (via an injected reader); template docs use a registered DrawingRegistry handler, so the d2/icon/compare/image children plug in. Honest DrawErr on no source / unknown template / unreadable path / empty output. Flutter-free, covered by dart test (7 cases). Not yet wired. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/src/draw/draw_dispatch.dart | 70 +++++++++++++++++++++++++++++++ test/draw/draw_dispatch_test.dart | 51 ++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 lib/src/draw/draw_dispatch.dart create mode 100644 test/draw/draw_dispatch_test.dart diff --git a/lib/src/draw/draw_dispatch.dart b/lib/src/draw/draw_dispatch.dart new file mode 100644 index 00000000..be57bb2f --- /dev/null +++ b/lib/src/draw/draw_dispatch.dart @@ -0,0 +1,70 @@ +/// Drawing-card template dispatch (T-318 / D-103). +/// +/// Lowers a [DrawingCardDoc] to an SVG string — the substrate the renderer +/// paints (D-103). In primitive mode the SVG is the doc's inline `svg` or the +/// contents of its `svgPath`; in template mode a registered handler (`d2`, +/// `icon`, `compare`, `image` — the child tickets) produces the SVG from the +/// doc's fields. The handlers and the file reader are injected, so this stays +/// headless- and `dart test`-friendly (no ambient filesystem, mirroring how +/// image.show injects its path resolver). +/// +/// Honest result: every failure (no source, unknown template, unreadable path, +/// empty handler output) returns a [DrawErr] with a message rather than +/// throwing — the command layer turns it into an IpcError userError. +/// +/// Flutter-free: pure Dart, runs under `dart test`. +library; + +import 'draw_doc.dart'; + +/// Produces an SVG string for a template-mode doc, or `null` on failure. +typedef DrawingTemplateHandler = Future Function(DrawingCardDoc doc); + +/// Reads a file's contents, or `null` if unreadable. Injected for testability. +typedef DrawingFileReader = Future Function(String path); + +/// Registry of template handlers, keyed by `template` name. +class DrawingRegistry { + final Map _handlers = {}; + + /// Register (or replace) the handler for [template]. + void register(String template, DrawingTemplateHandler handler) => _handlers[template] = handler; + + DrawingTemplateHandler? handlerFor(String template) => _handlers[template]; + + bool get isEmpty => _handlers.isEmpty; +} + +/// Outcome of lowering a doc to SVG. +sealed class DrawResult { + const DrawResult(); +} + +class DrawOk extends DrawResult { + const DrawOk(this.svg); + final String svg; +} + +class DrawErr extends DrawResult { + const DrawErr(this.message); + final String message; +} + +/// Lower [doc] to an SVG string. Primitive docs use their inline `svg` or read +/// `svgPath` via [readFile]; template docs use the matching handler in +/// [registry]. +Future resolveDrawingSvg(DrawingCardDoc doc, DrawingRegistry registry, {required DrawingFileReader readFile}) async { + if (doc.isPrimitive) { + if (doc.svg != null) return DrawOk(doc.svg!); + if (doc.svgPath != null) { + final contents = await readFile(doc.svgPath!); + return contents == null ? DrawErr('cannot read ${doc.svgPath}') : DrawOk(contents); + } + return const DrawErr('drawing card has no svg, svgPath, or template'); + } + + final handler = registry.handlerFor(doc.template!); + if (handler == null) return DrawErr('unknown drawing template: ${doc.template}'); + final svg = await handler(doc); + return svg == null ? DrawErr('template ${doc.template} produced no SVG') : DrawOk(svg); +} diff --git a/test/draw/draw_dispatch_test.dart b/test/draw/draw_dispatch_test.dart new file mode 100644 index 00000000..1e1c161e --- /dev/null +++ b/test/draw/draw_dispatch_test.dart @@ -0,0 +1,51 @@ +import 'package:clide/src/draw/draw_dispatch.dart'; +import 'package:clide/src/draw/draw_doc.dart'; +import 'package:test/test.dart'; + +void main() { + DrawingFileReader reader(Map files) => + (p) async => files[p]; + Future resolve(DrawingCardDoc doc, DrawingRegistry reg, {Map files = const {}}) => + resolveDrawingSvg(doc, reg, readFile: reader(files)); + + group('resolveDrawingSvg', () { + test('primitive: inline svg passes through', () async { + final r = await resolve(parseDrawingCardDoc({'svg': ''})!, DrawingRegistry()); + expect((r as DrawOk).svg, ''); + }); + + test('primitive: svgPath is read via the injected reader', () async { + final r = await resolve(parseDrawingCardDoc({'svgPath': 'd.svg'})!, DrawingRegistry(), files: {'d.svg': ''}); + expect((r as DrawOk).svg, ''); + }); + + test('primitive: an unreadable svgPath is an honest error', () async { + final r = await resolve(parseDrawingCardDoc({'svgPath': 'missing.svg'})!, DrawingRegistry()); + expect(r, isA()); + expect((r as DrawErr).message, contains('missing.svg')); + }); + + test('primitive: no source at all is an error', () async { + final r = await resolve(parseDrawingCardDoc(const {})!, DrawingRegistry()); + expect(r, isA()); + }); + + test('template: an unknown template is an error', () async { + final r = await resolve(parseDrawingCardDoc({'template': 'd2', 'source': 'a -> b'})!, DrawingRegistry()); + expect(r, isA()); + expect((r as DrawErr).message, contains('d2')); + }); + + test('template: a registered handler lowers the doc to SVG', () async { + final reg = DrawingRegistry()..register('d2', (doc) async => ''); + final r = await resolve(parseDrawingCardDoc({'template': 'd2', 'source': 'a -> b'})!, reg); + expect((r as DrawOk).svg, ''); + }); + + test('template: a handler that returns null is an error', () async { + final reg = DrawingRegistry()..register('d2', (_) async => null); + final r = await resolve(parseDrawingCardDoc({'template': 'd2'})!, reg); + expect(r, isA()); + }); + }); +}