diff --git a/ALIGNMENT.md b/ALIGNMENT.md index eddbe63..506a2e2 100644 --- a/ALIGNMENT.md +++ b/ALIGNMENT.md @@ -54,7 +54,7 @@ Goal: align bDS2 with old bDS behavior. Use the Allium specs as the contract onl - Spec: now specifies WebP quality 80 and AI JPEG quality 85. - Action: tend `specs/media_processing.allium` to specify WebP quality 80 and AI JPEG quality 85. -## P2: Media Import Event Shape +## P2: Media Import Event Shape (done) - Old bDS: imports by source path plus optional metadata in project context. - bDS2 now: imports with attrs including `source_path` and `project_id`. diff --git a/specs/media.allium b/specs/media.allium index 4389732..d1500a1 100644 --- a/specs/media.allium +++ b/specs/media.allium @@ -9,7 +9,7 @@ surface MediaControlSurface { facing _: MediaOperator provides: - ImportMediaRequested(project, source_file) + ImportMediaRequested(source_path, project, metadata) UpdateMediaRequested(media, changes) DeleteMediaRequested(media) UpsertMediaTranslationRequested(media, language, title, alt, caption) @@ -121,22 +121,29 @@ invariant DateBasedMediaLayout { } rule ImportMedia { - when: ImportMediaRequested(project, source_file) - let uuid_name = generate_uuid() + extension(source_file) + when: ImportMediaRequested(source_path, project, metadata) + -- metadata is optional import context: title, alt, caption, author, + -- language, and tags may be supplied by the caller. + let uuid_name = generate_uuid() + extension(source_path) let dest = format("media/{yyyy}/{mm}/{uuid_name}", yyyy: now.year, mm: now.month_padded) ensures: Media.created( project: project, filename: uuid_name, - original_name: source_file.name, - mime_type: detect_mime(source_file), - size: source_file.size, - width: detect_width(source_file), - height: detect_height(source_file), + original_name: basename(source_path), + mime_type: detect_mime(source_path), + size: file_size(source_path), + width: detect_width(source_path), + height: detect_height(source_path), + title: metadata.title, + alt: metadata.alt, + caption: metadata.caption, + author: metadata.author, + language: metadata.language, file_path: dest, - tags: {} + tags: metadata.tags ) - ensures: FileCopied(source_file, dest) + ensures: FileCopied(source_path, dest) ensures: SidecarWritten(media) ensures: ThumbnailsGenerated(media) ensures: SearchIndexUpdated(media) diff --git a/specs/media_processing.allium b/specs/media_processing.allium index b8c754a..076b6c6 100644 --- a/specs/media_processing.allium +++ b/specs/media_processing.allium @@ -14,7 +14,7 @@ surface MediaProcessingControlSurface { facing _: MediaProcessingOperator provides: - ImportMediaRequested(source_path, project) + ImportMediaRequested(source_path, project, metadata) TagMediaRequested(media, tags) DeleteMediaRequested(media) ValidateMediaRequested(project) @@ -239,11 +239,13 @@ invariant MediaTranslationFileLayout { -- ============================================================================ rule ImportMedia { - when: ImportMediaRequested(source_path, project) + when: ImportMediaRequested(source_path, project, metadata) + -- metadata is optional import context: title, alt, caption, author, + -- language, and tags may be supplied by the caller. -- 1. Validate file type (must be supported image) -- 2. Generate UUID v4 filename -- 3. Copy to media/{YYYY}/{MM}/{uuid}.{ext} - -- 4. Write sidecar {binary_path}.meta + -- 4. Apply optional metadata and write sidecar {binary_path}.meta -- 5. Generate four thumbnail sizes -- 6. Index for search (FTS5) ensures: media/Media.created( @@ -253,6 +255,12 @@ rule ImportMedia { size: file_size(source_path), width: extract_width_from_header(source_path), height: extract_height_from_header(source_path), + title: metadata.title, + alt: metadata.alt, + caption: metadata.caption, + author: metadata.author, + language: metadata.language, + tags: metadata.tags, file_path: format("media/{yyyy}/{mm}/{uuid}.{ext}"), sidecar_path: format("media/{yyyy}/{mm}/{uuid}.{ext}.meta"), checksum: sha256(source_path) diff --git a/test/bds/alignment_test.exs b/test/bds/alignment_test.exs new file mode 100644 index 0000000..fc73a4e --- /dev/null +++ b/test/bds/alignment_test.exs @@ -0,0 +1,31 @@ +defmodule BDS.AlignmentTest do + use ExUnit.Case, async: true + + @media_import_specs [ + "specs/media.allium", + "specs/media_processing.allium" + ] + + test "media import specs use one source-path-first event shape" do + signatures = + @media_import_specs + |> Enum.flat_map(fn path -> + path + |> File.read!() + |> import_media_signatures(path) + end) + + assert signatures == [ + {"specs/media.allium", "source_path, project, metadata"}, + {"specs/media.allium", "source_path, project, metadata"}, + {"specs/media_processing.allium", "source_path, project, metadata"}, + {"specs/media_processing.allium", "source_path, project, metadata"} + ] + end + + defp import_media_signatures(source, path) do + ~r/ImportMediaRequested\(([^)]+)\)/ + |> Regex.scan(source, capture: :all_but_first) + |> Enum.map(fn [signature] -> {path, signature} end) + end +end